From 35cdaad7ef02a9a775029fbecb87cc2be827826e Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Thu, 10 Jul 2025 10:07:01 +0200 Subject: [PATCH 1/5] Implementing better base64 evaluation + appropriate zip entry naming --- .../Activities/CreateZipArchive.cs | 64 +++++++++++++++++-- .../Extensions/ContentTypeExtensions.cs | 34 +++++++--- 2 files changed, 84 insertions(+), 14 deletions(-) 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 { From 792155c794b5db236c916f01618ca816140632be Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Thu, 10 Jul 2025 10:24:32 +0200 Subject: [PATCH 2/5] Improving performance of existing entry check --- .../Activities/CreateZipArchive.cs | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/src/modules/Elsa.IO.Compression/Activities/CreateZipArchive.cs b/src/modules/Elsa.IO.Compression/Activities/CreateZipArchive.cs index 1e373731a..2b028549f 100644 --- a/src/modules/Elsa.IO.Compression/Activities/CreateZipArchive.cs +++ b/src/modules/Elsa.IO.Compression/Activities/CreateZipArchive.cs @@ -143,43 +143,39 @@ public class CreateZipArchive : CodeActivity 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; + var filenameWithoutExtension = Path.GetFileNameWithoutExtension(originalName); + var extension = Path.GetExtension(originalName); + + var originalExists = false; + var 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); + { + originalExists = true; + } - if (!entryExt.Equals(extension, StringComparison.OrdinalIgnoreCase)) - continue; // Different extension + var entryNameWithoutExtension = Path.GetFileNameWithoutExtension(entry.Name); + var entryExtension = Path.GetExtension(entry.Name); + + // Only process entries with the same extension + if (!entryExtension.Equals(extension, StringComparison.OrdinalIgnoreCase)) + continue; - if (entryNameWithoutExt.StartsWith(filenameWithoutExtension, StringComparison.OrdinalIgnoreCase) && - entryNameWithoutExt.Length > filenameWithoutExtension.Length && - entryNameWithoutExt[filenameWithoutExtension.Length] == '(') + // Check if this entry follows our naming pattern + if (entryNameWithoutExtension.StartsWith(filenameWithoutExtension, StringComparison.OrdinalIgnoreCase) && + entryNameWithoutExtension.Length > filenameWithoutExtension.Length && + entryNameWithoutExtension[filenameWithoutExtension.Length] == '(') { // Extract the number between parentheses - var closingParenIndex = entryNameWithoutExt.LastIndexOf(')'); + var closingParenIndex = entryNameWithoutExtension.LastIndexOf(')'); if (closingParenIndex > filenameWithoutExtension.Length + 1) { - var indexStr = entryNameWithoutExt.Substring( - filenameWithoutExtension.Length + 1, + var indexStr = entryNameWithoutExtension.Substring( + filenameWithoutExtension.Length + 1, closingParenIndex - filenameWithoutExtension.Length - 1); - + if (int.TryParse(indexStr, out int index)) { highestIndex = Math.Max(highestIndex, index); @@ -187,8 +183,12 @@ public class CreateZipArchive : CodeActivity } } } - - // Create a new name with the next available index + + if (!originalExists) + { + return originalName; + } + return $"{filenameWithoutExtension}({highestIndex + 1}){extension}"; } } \ No newline at end of file From f6e1ed093a117da0d06da00a14674e0768ce1183 Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Thu, 10 Jul 2025 10:44:35 +0200 Subject: [PATCH 3/5] small QoL improvements: more edge cases and constraints for base64 --- .../Activities/CreateZipArchive.cs | 3 +-- .../Elsa.IO/Extensions/ContentTypeExtensions.cs | 15 ++++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/modules/Elsa.IO.Compression/Activities/CreateZipArchive.cs b/src/modules/Elsa.IO.Compression/Activities/CreateZipArchive.cs index 2b028549f..4c2b81ad6 100644 --- a/src/modules/Elsa.IO.Compression/Activities/CreateZipArchive.cs +++ b/src/modules/Elsa.IO.Compression/Activities/CreateZipArchive.cs @@ -125,8 +125,7 @@ public class CreateZipArchive : CodeActivity 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); diff --git a/src/modules/Elsa.IO/Extensions/ContentTypeExtensions.cs b/src/modules/Elsa.IO/Extensions/ContentTypeExtensions.cs index 8c4957b7d..9eac5a42a 100644 --- a/src/modules/Elsa.IO/Extensions/ContentTypeExtensions.cs +++ b/src/modules/Elsa.IO/Extensions/ContentTypeExtensions.cs @@ -98,19 +98,20 @@ public static class ContentTypeExtensions // Check padding position and count var paddingIndex = s.IndexOf('='); - if (paddingIndex > 0) + + switch (paddingIndex) { + // Padding cannot be at index 0 + case <= 0: // Padding must be at the end - if (paddingIndex < s.Length - 2) - return false; - + case > 0 when paddingIndex < s.Length - 2: // All characters after first '=' must also be '=' - if (s.Substring(paddingIndex).Any(c => c != '=')) + case > 0 when s[paddingIndex..].Any(c => c != '='): return false; } // Check for valid Base64 characters - for (int i = 0; i < (paddingIndex > 0 ? paddingIndex : s.Length); i++) + for (var i = 0; i < paddingIndex; i++) { var c = s[i]; var isValid = @@ -124,7 +125,7 @@ public static class ContentTypeExtensions } // Additional check for short strings that are just lowercase+numbers - // This catches "content2" and similar false positives + // This catches "whatever" and similar false positives if (s.Length <= 10 && s.All(c => char.IsLower(c) || char.IsDigit(c))) return false; From 9346595d80d6349422ebae9e129ac0204d70b941 Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Thu, 10 Jul 2025 10:54:05 +0200 Subject: [PATCH 4/5] improving entry naming logic for better performance and readability --- .../Activities/CreateZipArchive.cs | 55 ++++++++++++------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/src/modules/Elsa.IO.Compression/Activities/CreateZipArchive.cs b/src/modules/Elsa.IO.Compression/Activities/CreateZipArchive.cs index 4c2b81ad6..e322d7594 100644 --- a/src/modules/Elsa.IO.Compression/Activities/CreateZipArchive.cs +++ b/src/modules/Elsa.IO.Compression/Activities/CreateZipArchive.cs @@ -126,6 +126,7 @@ public class CreateZipArchive : CodeActivity 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); @@ -150,11 +151,13 @@ public class CreateZipArchive : CodeActivity foreach (var entry in zipArchive.Entries) { - if (entry.Name.Equals(originalName, StringComparison.OrdinalIgnoreCase)) + if (!entry.Name.Equals(originalName, StringComparison.OrdinalIgnoreCase)) { - originalExists = true; + continue; } + originalExists = true; + var entryNameWithoutExtension = Path.GetFileNameWithoutExtension(entry.Name); var entryExtension = Path.GetExtension(entry.Name); @@ -163,24 +166,7 @@ public class CreateZipArchive : CodeActivity continue; // Check if this entry follows our naming pattern - if (entryNameWithoutExtension.StartsWith(filenameWithoutExtension, StringComparison.OrdinalIgnoreCase) && - entryNameWithoutExtension.Length > filenameWithoutExtension.Length && - entryNameWithoutExtension[filenameWithoutExtension.Length] == '(') - { - // Extract the number between parentheses - var closingParenIndex = entryNameWithoutExtension.LastIndexOf(')'); - if (closingParenIndex > filenameWithoutExtension.Length + 1) - { - var indexStr = entryNameWithoutExtension.Substring( - filenameWithoutExtension.Length + 1, - closingParenIndex - filenameWithoutExtension.Length - 1); - - if (int.TryParse(indexStr, out int index)) - { - highestIndex = Math.Max(highestIndex, index); - } - } - } + highestIndex = HighestEntryNameIndex(entryNameWithoutExtension, filenameWithoutExtension, highestIndex); } if (!originalExists) @@ -190,4 +176,33 @@ public class CreateZipArchive : CodeActivity return $"{filenameWithoutExtension}({highestIndex + 1}){extension}"; } + + private static int HighestEntryNameIndex(string entryNameWithoutExtension, string filenameWithoutExtension, + int highestIndex) + { + if (!entryNameWithoutExtension.StartsWith(filenameWithoutExtension, StringComparison.OrdinalIgnoreCase) || + entryNameWithoutExtension.Length <= filenameWithoutExtension.Length || + entryNameWithoutExtension[filenameWithoutExtension.Length] != '(') + { + return highestIndex; + } + + // Extract the number between parentheses + var closingParenIndex = entryNameWithoutExtension.LastIndexOf(')'); + if (closingParenIndex <= filenameWithoutExtension.Length + 1) + { + return highestIndex; + } + + var indexStr = entryNameWithoutExtension.Substring( + filenameWithoutExtension.Length + 1, + closingParenIndex - filenameWithoutExtension.Length - 1); + + if (int.TryParse(indexStr, out var index)) + { + highestIndex = Math.Max(highestIndex, index); + } + + return highestIndex; + } } \ No newline at end of file From fe08794f096ff1b121b7d7947ae0eed1cb923a2a Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Thu, 10 Jul 2025 11:00:22 +0200 Subject: [PATCH 5/5] fix on padding index validation for base64 --- src/modules/Elsa.IO/Extensions/ContentTypeExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/Elsa.IO/Extensions/ContentTypeExtensions.cs b/src/modules/Elsa.IO/Extensions/ContentTypeExtensions.cs index 9eac5a42a..45bf158ef 100644 --- a/src/modules/Elsa.IO/Extensions/ContentTypeExtensions.cs +++ b/src/modules/Elsa.IO/Extensions/ContentTypeExtensions.cs @@ -102,7 +102,7 @@ public static class ContentTypeExtensions switch (paddingIndex) { // Padding cannot be at index 0 - case <= 0: + case 0: // Padding must be at the end case > 0 when paddingIndex < s.Length - 2: // All characters after first '=' must also be '='