Merge pull request #6783 from elsa-workflows/bug/zip-activity-base64-correction
Implementing better base64 evaluation + appropriate zip entry naming
This commit is contained in:
commit
fd5579e551
|
|
@ -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,87 @@ 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)
|
||||
{
|
||||
var filenameWithoutExtension = Path.GetFileNameWithoutExtension(originalName);
|
||||
var extension = Path.GetExtension(originalName);
|
||||
|
||||
var originalExists = false;
|
||||
var highestIndex = 0;
|
||||
|
||||
foreach (var entry in zipArchive.Entries)
|
||||
{
|
||||
if (!entry.Name.Equals(originalName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
originalExists = true;
|
||||
|
||||
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;
|
||||
|
||||
// Check if this entry follows our naming pattern
|
||||
highestIndex = HighestEntryNameIndex(entryNameWithoutExtension, filenameWithoutExtension, highestIndex);
|
||||
}
|
||||
|
||||
if (!originalExists)
|
||||
{
|
||||
return originalName;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -96,27 +96,44 @@ 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('=');
|
||||
|
||||
switch (paddingIndex)
|
||||
{
|
||||
// Padding cannot be at index 0
|
||||
case 0:
|
||||
// Padding must be at the end
|
||||
case > 0 when paddingIndex < s.Length - 2:
|
||||
// All characters after first '=' must also be '='
|
||||
case > 0 when s[paddingIndex..].Any(c => c != '='):
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for valid Base64 characters
|
||||
for (var i = 0; i < paddingIndex; 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 "whatever" 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