diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.cs index 14988fcab72817..d54cb5a9547131 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.cs @@ -44,7 +44,7 @@ public partial class ZipArchiveEntry private List? _cdUnknownExtraFields; private List? _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) @@ -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. @@ -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. @@ -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 @@ -138,8 +140,6 @@ internal ZipArchiveEntry(ZipArchive archive, string entryName) _fileComment = Array.Empty(); - _compressionLevel = null; - if (_storedEntryNameBytes.Length > ushort.MaxValue) throw new ArgumentException(SR.EntryNamesTooLong); @@ -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; } @@ -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) { diff --git a/src/libraries/System.IO.Compression/tests/ZipArchive/zip_CreateTests.cs b/src/libraries/System.IO.Compression/tests/ZipArchive/zip_CreateTests.cs index fe1daa839bfe75..dae65db374f40d 100644 --- a/src/libraries/System.IO.Compression/tests/ZipArchive/zip_CreateTests.cs +++ b/src/libraries/System.IO.Compression/tests/ZipArchive/zip_CreateTests.cs @@ -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() { diff --git a/src/libraries/System.IO.Packaging/src/System/IO/Packaging/ZipPackage.cs b/src/libraries/System.IO.Packaging/src/System/IO/Packaging/ZipPackage.cs index c30915c5eacf9b..2d8f4273ad10a8 100644 --- a/src/libraries/System.IO.Packaging/src/System/IO/Packaging/ZipPackage.cs +++ b/src/libraries/System.IO.Packaging/src/System/IO/Packaging/ZipPackage.cs @@ -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: diff --git a/src/libraries/System.IO.Packaging/tests/ReflectionTests.cs b/src/libraries/System.IO.Packaging/tests/ReflectionTests.cs index 78f080aa6ce782..91e31b356e855d 100644 --- a/src/libraries/System.IO.Packaging/tests/ReflectionTests.cs +++ b/src/libraries/System.IO.Packaging/tests/ReflectionTests.cs @@ -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 } diff --git a/src/libraries/System.IO.Packaging/tests/Tests.cs b/src/libraries/System.IO.Packaging/tests/Tests.cs index 4d547db5e1c199..2d814bdde5051a 100644 --- a/src/libraries/System.IO.Packaging/tests/Tests.cs +++ b/src/libraries/System.IO.Packaging/tests/Tests.cs @@ -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"; }