Skip to content

Commit

Permalink
Allow setting ZipArchiveEntry general-purpose flag bits (#98278)
Browse files Browse the repository at this point in the history
* Use CompressionLevel to set general-purpose bit flags

Also changed mapping of ZipPackage's compression options, such that CompressionOption.Maximum now sets the compression level to SmallestSize, and SuperFast now sets the compression level to NoCompression.
Both of these changes restore compatibility with the .NET Framework.

* Made function verbs consistent

* Added test to verify read file contents

* Corrected failing Packaging test

This test was intended to ensure that bit 11 isn't set. It was actually performing a blind comparison of the entire bit field. Other tests in System.IO.Packaging function properly.

* Changes following code review

* Updated the conditional compilation directives for the .NET Framework/Core package CompressionLevel mappings.
* Specifying a CompressionMethod other than Deflate or Deflate64 will now set the compression level to NoCompression, and will write zeros to the relevant general purpose bits.
* The CompressionLevel always defaulted to Optimal for new archives, but this is now explicitly set (rather than relying upon setting it to null and null-coalescing it to Optimal.) This removes a condition to test for.
* Updated the test data for the CreateArchiveEntriesWithBitFlags test. If the compression level is set to NoCompression, we should expect the CompressionMethod to become Stored (which unsets the general purpose bits, returning an expected result of zero.)

* Code review changes

* Updated mapping between general purpose bit fields and CompressionLevel.
* Updated mapping from CompressionOption to CompressionLevel.
* Added test to verify round-trip of CompressionOption and its setting of the general purpose bit fields.

---------

Co-authored-by: Carlos Sánchez López <1175054+carlossanlop@users.noreply.github.com>
  • Loading branch information
edwardneal and carlossanlop committed May 9, 2024
1 parent 315c4c4 commit 6cc6c66
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 7 deletions.
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;
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 string GetOpcNameFromZipItemName(string zipItemName)
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)
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

0 comments on commit 6cc6c66

Please sign in to comment.