Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow setting ZipArchiveEntry general-purpose flag bits #98278

Merged
merged 7 commits into from
May 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public partial class ZipArchiveEntry
private List<ZipGenericExtraField>? _cdUnknownExtraFields;
private List<ZipGenericExtraField>? _lhUnknownExtraFields;
private byte[] _fileComment;
private readonly CompressionLevel? _compressionLevel;
private readonly CompressionLevel _compressionLevel;

// Initializes a ZipArchiveEntry instance for an existing archive entry.
internal ZipArchiveEntry(ZipArchive archive, ZipCentralDirectoryFileHeader cd)
Expand Down Expand Up @@ -86,7 +86,7 @@ internal ZipArchiveEntry(ZipArchive archive, ZipCentralDirectoryFileHeader cd)

_fileComment = cd.FileComment;

_compressionLevel = null;
_compressionLevel = MapCompressionLevel(_generalPurposeBitFlag, CompressionMethod);
}

// Initializes a ZipArchiveEntry instance for a new archive entry with a specified compression level.
Expand All @@ -98,6 +98,7 @@ internal ZipArchiveEntry(ZipArchive archive, string entryName, CompressionLevel
{
CompressionMethod = CompressionMethodValues.Stored;
}
_generalPurposeBitFlag = MapDeflateCompressionOption(_generalPurposeBitFlag, _compressionLevel, CompressionMethod);
}

// Initializes a ZipArchiveEntry instance for a new archive entry.
Expand All @@ -111,8 +112,9 @@ internal ZipArchiveEntry(ZipArchive archive, string entryName)
_versionMadeByPlatform = CurrentZipPlatform;
_versionMadeBySpecification = ZipVersionNeededValues.Default;
_versionToExtract = ZipVersionNeededValues.Default; // this must happen before following two assignment
_generalPurposeBitFlag = 0;
_compressionLevel = CompressionLevel.Optimal;
edwardneal marked this conversation as resolved.
Show resolved Hide resolved
CompressionMethod = CompressionMethodValues.Deflate;
_generalPurposeBitFlag = MapDeflateCompressionOption(0, _compressionLevel, CompressionMethod);
_lastModified = DateTimeOffset.Now;

_compressedSize = 0; // we don't know these yet
Expand All @@ -138,8 +140,6 @@ internal ZipArchiveEntry(ZipArchive archive, string entryName)

_fileComment = Array.Empty<byte>();

_compressionLevel = null;

if (_storedEntryNameBytes.Length > ushort.MaxValue)
throw new ArgumentException(SR.EntryNamesTooLong);

Expand Down Expand Up @@ -632,7 +632,7 @@ private CheckSumAndSizeWriteStream GetDataCompressor(Stream backingStream, bool
case CompressionMethodValues.Deflate:
case CompressionMethodValues.Deflate64:
default:
compressorStream = new DeflateStream(backingStream, _compressionLevel ?? CompressionLevel.Optimal, leaveBackingStreamOpen);
compressorStream = new DeflateStream(backingStream, _compressionLevel, leaveBackingStreamOpen);
break;

}
Expand Down Expand Up @@ -799,6 +799,46 @@ private bool IsOpenable(bool needToUncompress, bool needToLoadIntoMemory, out st

private bool SizesTooLarge() => _compressedSize > uint.MaxValue || _uncompressedSize > uint.MaxValue;

private static CompressionLevel MapCompressionLevel(BitFlagValues generalPurposeBitFlag, CompressionMethodValues compressionMethod)
{
// Information about the Deflate compression option is stored in bits 1 and 2 of the general purpose bit flags.
// If the compression method is not Deflate, the Deflate compression option is invalid - default to NoCompression.
if (compressionMethod == CompressionMethodValues.Deflate || compressionMethod == CompressionMethodValues.Deflate64)
{
return ((int)generalPurposeBitFlag & 0x6) switch
{
0 => CompressionLevel.Optimal,
2 => CompressionLevel.SmallestSize,
4 => CompressionLevel.Fastest,
6 => CompressionLevel.Fastest,
_ => CompressionLevel.Optimal
};
}
else
{
return CompressionLevel.NoCompression;
}
}

private static BitFlagValues MapDeflateCompressionOption(BitFlagValues generalPurposeBitFlag, CompressionLevel compressionLevel, CompressionMethodValues compressionMethod)
{
ushort deflateCompressionOptions = (ushort)(
// The Deflate compression level is only valid if the compression method is actually Deflate (or Deflate64). If it's not, the
// value of the two bits is undefined and they should be zeroed out.
compressionMethod == CompressionMethodValues.Deflate || compressionMethod == CompressionMethodValues.Deflate64
? compressionLevel switch
{
CompressionLevel.Optimal => 0,
CompressionLevel.SmallestSize => 2,
CompressionLevel.Fastest => 6,
CompressionLevel.NoCompression => 6,
_ => 0
}
: 0);

return (BitFlagValues)(((int)generalPurposeBitFlag & ~0x6) | deflateCompressionOptions);
}

// return value is true if we allocated an extra field for 64 bit headers, un/compressed size
private bool WriteLocalFileHeader(bool isEmptyFile)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,78 @@ public static void CreateUncompressedArchive()
}
}

// This test checks to ensure that setting the compression level of an archive entry sets the general-purpose
// bit flags correctly. It verifies that these have been set by reading from the MemoryStream manually, and by
// reopening the generated file to confirm that the compression levels match.
[Theory]
// Special-case NoCompression: in this case, the CompressionMethod becomes Stored and the bits are unset.
[InlineData(CompressionLevel.NoCompression, 0)]
[InlineData(CompressionLevel.Optimal, 0)]
[InlineData(CompressionLevel.SmallestSize, 2)]
[InlineData(CompressionLevel.Fastest, 6)]
public static void CreateArchiveEntriesWithBitFlags(CompressionLevel compressionLevel, ushort expectedGeneralBitFlags)
{
var testfilename = "testfile";
var testFileContent = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.";
var utf8WithoutBom = new Text.UTF8Encoding(encoderShouldEmitUTF8Identifier: false);

byte[] zipFileContent;

using (var testStream = new MemoryStream())
{

using (var zip = new ZipArchive(testStream, ZipArchiveMode.Create))
{
ZipArchiveEntry newEntry = zip.CreateEntry(testfilename, compressionLevel);
using (var writer = new StreamWriter(newEntry.Open(), utf8WithoutBom))
{
writer.Write(testFileContent);
writer.Flush();
}

ZipArchiveEntry secondNewEntry = zip.CreateEntry(testFileContent + "_post", CompressionLevel.NoCompression);
}

zipFileContent = testStream.ToArray();
}

// expected bit flags are at position 6 in the file header
var generalBitFlags = System.Buffers.Binary.BinaryPrimitives.ReadUInt16LittleEndian(zipFileContent.AsSpan(6));

Assert.Equal(expectedGeneralBitFlags, generalBitFlags);

using (var reReadStream = new MemoryStream(zipFileContent))
{
using (var reReadZip = new ZipArchive(reReadStream, ZipArchiveMode.Read))
{
var firstArchive = reReadZip.Entries[0];
var secondArchive = reReadZip.Entries[1];
var compressionLevelFieldInfo = typeof(ZipArchiveEntry).GetField("_compressionLevel", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
var generalBitFlagsFieldInfo = typeof(ZipArchiveEntry).GetField("_generalPurposeBitFlag", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);

var reReadCompressionLevel = (CompressionLevel)compressionLevelFieldInfo.GetValue(firstArchive);
var reReadGeneralBitFlags = (ushort)generalBitFlagsFieldInfo.GetValue(firstArchive);

Assert.Equal(compressionLevel, reReadCompressionLevel);
Assert.Equal(expectedGeneralBitFlags, reReadGeneralBitFlags);

reReadCompressionLevel = (CompressionLevel)compressionLevelFieldInfo.GetValue(secondArchive);
Assert.Equal(CompressionLevel.NoCompression, reReadCompressionLevel);

using (var strm = firstArchive.Open())
{
var readBuffer = new byte[firstArchive.Length];

strm.Read(readBuffer);

var readText = Text.Encoding.UTF8.GetString(readBuffer);

Assert.Equal(readText, testFileContent);
}
}
}
}

[Fact]
public static void CreateNormal_VerifyDataDescriptor()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,11 @@ internal static void GetZipCompressionMethodFromOpcCompressionOption(
break;
case CompressionOption.Maximum:
{
#if NET
compressionLevel = CompressionLevel.SmallestSize;
#else
compressionLevel = CompressionLevel.Optimal;
#endif
}
break;
case CompressionOption.Fast:
Expand Down
2 changes: 1 addition & 1 deletion src/libraries/System.IO.Packaging/tests/ReflectionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public void Verify_GeneralPurposeBitFlag_NotSetTo_Unicode()
FieldInfo fieldInfo = typeof(ZipArchiveEntry).GetField("_generalPurposeBitFlag", BindingFlags.Instance | BindingFlags.NonPublic);
object fieldObject = fieldInfo.GetValue(entry);
ushort shortField = (ushort)fieldObject;
Assert.Equal(0, shortField); // If it was UTF8, we would set the general purpose bit flag to 0x800 (UnicodeFileNameAndComment)
Assert.Equal(0, shortField & 0x800); // If it was UTF8, we would set the general purpose bit flag to 0x800 (UnicodeFileNameAndComment)
edwardneal marked this conversation as resolved.
Show resolved Hide resolved
CheckCharacters(entry.Name);
CheckCharacters(entry.Comment); // Unavailable in .NET Framework
}
Expand Down
41 changes: 41 additions & 0 deletions src/libraries/System.IO.Packaging/tests/Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3988,6 +3988,47 @@ public void CreatePackUriWithFragment()

}

[Theory]
#if NET
[InlineData(CompressionOption.NotCompressed, CompressionOption.Normal, 0)]
[InlineData(CompressionOption.Normal, CompressionOption.Normal, 0)]
[InlineData(CompressionOption.Maximum, CompressionOption.Normal, 2)]
[InlineData(CompressionOption.Fast, CompressionOption.Normal, 6)]
[InlineData(CompressionOption.SuperFast, CompressionOption.Normal, 6)]
#else
[InlineData(CompressionOption.NotCompressed, CompressionOption.NotCompressed, 0)]
[InlineData(CompressionOption.Normal, CompressionOption.Normal, 0)]
[InlineData(CompressionOption.Maximum, CompressionOption.Maximum, 2)]
[InlineData(CompressionOption.Fast, CompressionOption.Fast, 4)]
[InlineData(CompressionOption.SuperFast, CompressionOption.SuperFast, 6)]
#endif
public void Roundtrip_Compression_Option(CompressionOption createdCompressionOption, CompressionOption expectedCompressionOption, ushort expectedZipFileBitFlags)
{
var documentPath = "untitled.txt";
Uri partUriDocument = PackUriHelper.CreatePartUri(new Uri(documentPath, UriKind.Relative));

using (MemoryStream ms = new MemoryStream())
{
Package package = Package.Open(ms, FileMode.Create, FileAccess.ReadWrite);
PackagePart part = package.CreatePart(partUriDocument, "application/text", createdCompressionOption);

package.Flush();
package.Close();
(package as IDisposable).Dispose();

ms.Seek(0, SeekOrigin.Begin);

var zipBytes = ms.ToArray();
var generalBitFlags = System.Buffers.Binary.BinaryPrimitives.ReadUInt16LittleEndian(zipBytes.AsSpan(6));

package = Package.Open(ms, FileMode.Open, FileAccess.Read);
part = package.GetPart(partUriDocument);

Assert.Equal(expectedZipFileBitFlags, generalBitFlags);
Assert.Equal(expectedCompressionOption, part.CompressionOption);
}
}

private const string DocumentRelationshipType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument";
}

Expand Down