Implementing better base64 evaluation + appropriate zip entry naming
This commit is contained in:
parent
940e3b5b13
commit
35cdaad7ef
|
|
@ -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}";
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue