diff --git a/src/libraries/Microsoft.Windows.Compatibility/src/Microsoft.Windows.Compatibility.csproj b/src/libraries/Microsoft.Windows.Compatibility/src/Microsoft.Windows.Compatibility.csproj
index a86115b820874c..8730ae4041f350 100644
--- a/src/libraries/Microsoft.Windows.Compatibility/src/Microsoft.Windows.Compatibility.csproj
+++ b/src/libraries/Microsoft.Windows.Compatibility/src/Microsoft.Windows.Compatibility.csproj
@@ -5,8 +5,8 @@
false
true
- false
- 8
+ true
+ 9
$(NoWarn);NU5128
This Windows Compatibility Pack provides access to APIs that were previously available only for .NET Framework. It can be used from both .NET Core as well as .NET Standard.
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 0481ee770dfba1..5b9aecacae6abe 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
@@ -41,7 +41,7 @@ public partial class ZipArchiveEntry
private List? _cdUnknownExtraFields;
private List? _lhUnknownExtraFields;
private readonly byte[]? _fileComment;
- private readonly CompressionLevel? _compressionLevel;
+ private readonly CompressionLevel _compressionLevel;
// Initializes, attaches it to archive
internal ZipArchiveEntry(ZipArchive archive, ZipCentralDirectoryFileHeader cd)
@@ -79,7 +79,7 @@ internal ZipArchiveEntry(ZipArchive archive, ZipCentralDirectoryFileHeader cd)
_cdUnknownExtraFields = cd.ExtraFields;
_fileComment = cd.FileComment;
- _compressionLevel = null;
+ _compressionLevel = MapCompressionLevel(_generalPurposeBitFlag, CompressionMethod);
}
// Initializes new entry
@@ -91,6 +91,7 @@ internal ZipArchiveEntry(ZipArchive archive, string entryName, CompressionLevel
{
CompressionMethod = CompressionMethodValues.Stored;
}
+ _generalPurposeBitFlag = MapDeflateCompressionOption(_generalPurposeBitFlag, _compressionLevel, CompressionMethod);
}
// Initializes new entry
@@ -104,8 +105,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
@@ -127,8 +129,6 @@ internal ZipArchiveEntry(ZipArchive archive, string entryName)
_lhUnknownExtraFields = null;
_fileComment = null;
- _compressionLevel = null;
-
if (_storedEntryNameBytes.Length > ushort.MaxValue)
throw new ArgumentException(SR.EntryNamesTooLong);
@@ -617,7 +617,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;
}
@@ -784,6 +784,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 b5075cd8ba5944..ae924f3a950eec 100644
--- a/src/libraries/System.IO.Compression/tests/ZipArchive/zip_CreateTests.cs
+++ b/src/libraries/System.IO.Compression/tests/ZipArchive/zip_CreateTests.cs
@@ -144,6 +144,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.csproj b/src/libraries/System.IO.Packaging/src/System.IO.Packaging.csproj
index 3444e2515ce837..37669f160fae15 100644
--- a/src/libraries/System.IO.Packaging/src/System.IO.Packaging.csproj
+++ b/src/libraries/System.IO.Packaging/src/System.IO.Packaging.csproj
@@ -7,8 +7,8 @@
$(NoWarn);CA1847
true
- false
- 0
+ true
+ 1
Provides classes that support storage of multiple data objects in a single container.
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 8f4fd15ddfc5a4..92a975856eca52 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
@@ -404,7 +404,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
new file mode 100644
index 00000000000000..27db7a764f3c7f
--- /dev/null
+++ b/src/libraries/System.IO.Packaging/tests/ReflectionTests.cs
@@ -0,0 +1,59 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.IO.Compression;
+using System.Reflection;
+using Xunit;
+
+namespace System.IO.Packaging.Tests;
+
+public class ReflectionTests
+{
+ [Fact]
+ public void Verify_GeneralPurposeBitFlag_NotSetTo_Unicode()
+ {
+ using MemoryStream ms = new();
+
+ using (ZipPackage package = (ZipPackage)Package.Open(ms, FileMode.Create, FileAccess.Write))
+ {
+ Uri uri = PackUriHelper.CreatePartUri(new Uri("document.xml", UriKind.Relative));
+ ZipPackagePart part = (ZipPackagePart)package.CreatePart(uri, Tests.Mime_MediaTypeNames_Text_Xml, CompressionOption.NotCompressed);
+ using (Stream partStream = part.GetStream())
+ {
+ using StreamWriter sw = new(partStream);
+ sw.Write(Tests.s_DocumentXml);
+ }
+ package.CreateRelationship(part.Uri, TargetMode.Internal, "http://packageRelType", "rId1234");
+ }
+
+ ms.Position = 0;
+ using (ZipArchive archive = new ZipArchive(ms, ZipArchiveMode.Read, leaveOpen: false))
+ {
+ FieldInfo archiveEncodingFieldInfo = typeof(ZipArchive).GetField("_entryNameEncoding", BindingFlags.Instance | BindingFlags.NonPublic);
+ System.Text.Encoding archiveEncoding = (archiveEncodingFieldInfo.GetValue(archive) as System.Text.Encoding) ?? System.Text.Encoding.UTF8;
+
+ foreach (ZipArchiveEntry entry in archive.Entries)
+ {
+ FieldInfo fieldInfo = typeof(ZipArchiveEntry).GetField("_generalPurposeBitFlag", BindingFlags.Instance | BindingFlags.NonPublic);
+ object fieldObject = fieldInfo.GetValue(entry);
+ ushort shortField = (ushort)fieldObject;
+ Assert.Equal(0, shortField & 0x800); // If it was UTF8, we would set the general purpose bit flag to 0x800 (UnicodeFileNameAndComment)
+ CheckCharacters(entry.Name);
+ fieldInfo = typeof(ZipArchiveEntry).GetField("_fileComment", BindingFlags.Instance | BindingFlags.NonPublic);
+ byte[] commentBytes = (byte[]?)fieldInfo.GetValue(entry) ?? Array.Empty();
+
+ CheckCharacters(archiveEncoding.GetString(commentBytes));
+ }
+ }
+
+ void CheckCharacters(string value)
+ {
+ for (int i = 0; i < value.Length; i++)
+ {
+ char c = value[i];
+ Assert.True(c >= 32 && c <= 126, $"ZipArchiveEntry name character {c} requires UTF8");
+ }
+ }
+ }
+
+}
diff --git a/src/libraries/System.IO.Packaging/tests/System.IO.Packaging.Tests.csproj b/src/libraries/System.IO.Packaging/tests/System.IO.Packaging.Tests.csproj
index f9a26bb66dc20b..9bc5f7e92faffa 100644
--- a/src/libraries/System.IO.Packaging/tests/System.IO.Packaging.Tests.csproj
+++ b/src/libraries/System.IO.Packaging/tests/System.IO.Packaging.Tests.csproj
@@ -5,6 +5,9 @@
+
+
+
diff --git a/src/libraries/System.IO.Packaging/tests/Tests.cs b/src/libraries/System.IO.Packaging/tests/Tests.cs
index 8dd92b410ba202..38884769c8f11a 100644
--- a/src/libraries/System.IO.Packaging/tests/Tests.cs
+++ b/src/libraries/System.IO.Packaging/tests/Tests.cs
@@ -12,9 +12,9 @@ namespace System.IO.Packaging.Tests
{
public class Tests : FileCleanupTestBase
{
- private const string Mime_MediaTypeNames_Text_Xml = "text/xml";
+ internal const string Mime_MediaTypeNames_Text_Xml = "text/xml";
private const string Mime_MediaTypeNames_Image_Jpeg = "image/jpeg"; // System.Net.Mime.MediaTypeNames.Image.Jpeg
- private const string s_DocumentXml = @"Test";
+ internal const string s_DocumentXml = @"Test";
private const string s_ResourceXml = @"Test";
private FileInfo GetTempFileInfoFromExistingFile(string existingFileName, [CallerMemberName] string memberName = null, [CallerLineNumber] int lineNumber = 0)
@@ -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)) & 0x6;
+
+ 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";
}