Implementing better base64 evaluation + appropriate zip entry naming

This commit is contained in:
lucas.hipolito 2025-07-10 10:07:01 +02:00
parent 940e3b5b13
commit 35cdaad7ef
2 changed files with 84 additions and 14 deletions

View file

@ -80,7 +80,7 @@ public class CreateZipArchive : CodeActivity<Stream>
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<Stream>
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}";
}
}

View file

@ -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
{