diff --git a/src/modules/Elsa.IO.Compression/Activities/CreateZipArchive.cs b/src/modules/Elsa.IO.Compression/Activities/CreateZipArchive.cs index c7eee4d13..1e373731a 100644 --- a/src/modules/Elsa.IO.Compression/Activities/CreateZipArchive.cs +++ b/src/modules/Elsa.IO.Compression/Activities/CreateZipArchive.cs @@ -80,7 +80,7 @@ public class CreateZipArchive : CodeActivity try { - using var zipArchive = new ZipArchive(zipStream, ZipArchiveMode.Create, leaveOpen: true); + using var zipArchive = new ZipArchive(zipStream, ZipArchiveMode.Update, leaveOpen: true); var entryIndex = 0; var compressionLevel = CompressionLevel.Get(context); @@ -122,19 +122,73 @@ public class CreateZipArchive : CodeActivity CompressionLevel compressionLevel) { var binaryContent = await resolver.ResolveAsync(entryContent, context.CancellationToken); - - var entryName = binaryContent.Name?.GetNameAndExtension() + + var entryName = binaryContent.Name?.GetNameAndExtension() ?? string.Format(DefaultEntryNameFormat, entryIndex + 1); - + + // Get a unique name following Windows convention + entryName = GetUniqueEntryName(zipArchive, entryName); + var archiveEntry = zipArchive.CreateEntry(entryName, compressionLevel); await using var entryStream = archiveEntry.Open(); await binaryContent.Stream.CopyToAsync(entryStream, context.CancellationToken); await entryStream.FlushAsync(context.CancellationToken); - + if (entryContent is not Stream) { await binaryContent.Stream.DisposeAsync(); } } + + private static string GetUniqueEntryName(ZipArchive zipArchive, string originalName) + { + // If no duplicate exists, use the original name + if (!zipArchive.Entries.Any(entry => entry.Name.Equals(originalName, StringComparison.OrdinalIgnoreCase))) + { + return originalName; + } + + // Split the name into filename and extension + string filenameWithoutExtension = Path.GetFileNameWithoutExtension(originalName); + string extension = Path.GetExtension(originalName); + + // Find the highest index used for this filename pattern + int highestIndex = 0; + + // Check for the original name and any name with pattern "name(n).ext" + foreach (var entry in zipArchive.Entries) + { + if (entry.Name.Equals(originalName, StringComparison.OrdinalIgnoreCase)) + continue; // Skip the exact match as we already know it exists + + string entryNameWithoutExt = Path.GetFileNameWithoutExtension(entry.Name); + string entryExt = Path.GetExtension(entry.Name); + + if (!entryExt.Equals(extension, StringComparison.OrdinalIgnoreCase)) + continue; // Different extension + + if (entryNameWithoutExt.StartsWith(filenameWithoutExtension, StringComparison.OrdinalIgnoreCase) && + entryNameWithoutExt.Length > filenameWithoutExtension.Length && + entryNameWithoutExt[filenameWithoutExtension.Length] == '(') + { + // Extract the number between parentheses + var closingParenIndex = entryNameWithoutExt.LastIndexOf(')'); + if (closingParenIndex > filenameWithoutExtension.Length + 1) + { + var indexStr = entryNameWithoutExt.Substring( + filenameWithoutExtension.Length + 1, + closingParenIndex - filenameWithoutExtension.Length - 1); + + if (int.TryParse(indexStr, out int index)) + { + highestIndex = Math.Max(highestIndex, index); + } + } + } + } + + // Create a new name with the next available index + return $"{filenameWithoutExtension}({highestIndex + 1}){extension}"; + } } \ No newline at end of file diff --git a/src/modules/Elsa.IO/Extensions/ContentTypeExtensions.cs b/src/modules/Elsa.IO/Extensions/ContentTypeExtensions.cs index 45e82a262..8c4957b7d 100644 --- a/src/modules/Elsa.IO/Extensions/ContentTypeExtensions.cs +++ b/src/modules/Elsa.IO/Extensions/ContentTypeExtensions.cs @@ -96,27 +96,43 @@ public static class ContentTypeExtensions if (s.Length % 4 != 0) return false; - // Check valid Base64 characters - for (var i = 0; i < s.Length; i++) + // Check padding position and count + var paddingIndex = s.IndexOf('='); + if (paddingIndex > 0) + { + // Padding must be at the end + if (paddingIndex < s.Length - 2) + return false; + + // All characters after first '=' must also be '=' + if (s.Substring(paddingIndex).Any(c => c != '=')) + return false; + } + + // Check for valid Base64 characters + for (int i = 0; i < (paddingIndex > 0 ? paddingIndex : s.Length); i++) { var c = s[i]; - - var isValid = + var isValid = c is >= 'A' and <= 'Z' || c is >= 'a' and <= 'z' || c is >= '0' and <= '9' || - c == '+' || c == '/' || c == '='; + c == '+' || c == '/'; if (!isValid) return false; } - // Try actual decoding and roundtrip + // Additional check for short strings that are just lowercase+numbers + // This catches "content2" and similar false positives + if (s.Length <= 10 && s.All(c => char.IsLower(c) || char.IsDigit(c))) + return false; + + // Try actual decoding try { - var data = Convert.FromBase64String(s); - var reEncoded = Convert.ToBase64String(data); - return s == reEncoded; + _ = Convert.FromBase64String(s); + return true; } catch {