Skip to content

Commit 6cc6c66

Browse files
Allow setting ZipArchiveEntry general-purpose flag bits (#98278)
* 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 <[email protected]>
1 parent 315c4c4 commit 6cc6c66

File tree

5 files changed

+164
-7
lines changed

5 files changed

+164
-7
lines changed

src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.cs

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public partial class ZipArchiveEntry
4444
private List<ZipGenericExtraField>? _cdUnknownExtraFields;
4545
private List<ZipGenericExtraField>? _lhUnknownExtraFields;
4646
private byte[] _fileComment;
47-
private readonly CompressionLevel? _compressionLevel;
47+
private readonly CompressionLevel _compressionLevel;
4848

4949
// Initializes a ZipArchiveEntry instance for an existing archive entry.
5050
internal ZipArchiveEntry(ZipArchive archive, ZipCentralDirectoryFileHeader cd)
@@ -86,7 +86,7 @@ internal ZipArchiveEntry(ZipArchive archive, ZipCentralDirectoryFileHeader cd)
8686

8787
_fileComment = cd.FileComment;
8888

89-
_compressionLevel = null;
89+
_compressionLevel = MapCompressionLevel(_generalPurposeBitFlag, CompressionMethod);
9090
}
9191

9292
// 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
9898
{
9999
CompressionMethod = CompressionMethodValues.Stored;
100100
}
101+
_generalPurposeBitFlag = MapDeflateCompressionOption(_generalPurposeBitFlag, _compressionLevel, CompressionMethod);
101102
}
102103

103104
// Initializes a ZipArchiveEntry instance for a new archive entry.
@@ -111,8 +112,9 @@ internal ZipArchiveEntry(ZipArchive archive, string entryName)
111112
_versionMadeByPlatform = CurrentZipPlatform;
112113
_versionMadeBySpecification = ZipVersionNeededValues.Default;
113114
_versionToExtract = ZipVersionNeededValues.Default; // this must happen before following two assignment
114-
_generalPurposeBitFlag = 0;
115+
_compressionLevel = CompressionLevel.Optimal;
115116
CompressionMethod = CompressionMethodValues.Deflate;
117+
_generalPurposeBitFlag = MapDeflateCompressionOption(0, _compressionLevel, CompressionMethod);
116118
_lastModified = DateTimeOffset.Now;
117119

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

139141
_fileComment = Array.Empty<byte>();
140142

141-
_compressionLevel = null;
142-
143143
if (_storedEntryNameBytes.Length > ushort.MaxValue)
144144
throw new ArgumentException(SR.EntryNamesTooLong);
145145

@@ -632,7 +632,7 @@ private CheckSumAndSizeWriteStream GetDataCompressor(Stream backingStream, bool
632632
case CompressionMethodValues.Deflate:
633633
case CompressionMethodValues.Deflate64:
634634
default:
635-
compressorStream = new DeflateStream(backingStream, _compressionLevel ?? CompressionLevel.Optimal, leaveBackingStreamOpen);
635+
compressorStream = new DeflateStream(backingStream, _compressionLevel, leaveBackingStreamOpen);
636636
break;
637637

638638
}
@@ -799,6 +799,46 @@ private bool IsOpenable(bool needToUncompress, bool needToLoadIntoMemory, out st
799799

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

802+
private static CompressionLevel MapCompressionLevel(BitFlagValues generalPurposeBitFlag, CompressionMethodValues compressionMethod)
803+
{
804+
// Information about the Deflate compression option is stored in bits 1 and 2 of the general purpose bit flags.
805+
// If the compression method is not Deflate, the Deflate compression option is invalid - default to NoCompression.
806+
if (compressionMethod == CompressionMethodValues.Deflate || compressionMethod == CompressionMethodValues.Deflate64)
807+
{
808+
return ((int)generalPurposeBitFlag & 0x6) switch
809+
{
810+
0 => CompressionLevel.Optimal,
811+
2 => CompressionLevel.SmallestSize,
812+
4 => CompressionLevel.Fastest,
813+
6 => CompressionLevel.Fastest,
814+
_ => CompressionLevel.Optimal
815+
};
816+
}
817+
else
818+
{
819+
return CompressionLevel.NoCompression;
820+
}
821+
}
822+
823+
private static BitFlagValues MapDeflateCompressionOption(BitFlagValues generalPurposeBitFlag, CompressionLevel compressionLevel, CompressionMethodValues compressionMethod)
824+
{
825+
ushort deflateCompressionOptions = (ushort)(
826+
// The Deflate compression level is only valid if the compression method is actually Deflate (or Deflate64). If it's not, the
827+
// value of the two bits is undefined and they should be zeroed out.
828+
compressionMethod == CompressionMethodValues.Deflate || compressionMethod == CompressionMethodValues.Deflate64
829+
? compressionLevel switch
830+
{
831+
CompressionLevel.Optimal => 0,
832+
CompressionLevel.SmallestSize => 2,
833+
CompressionLevel.Fastest => 6,
834+
CompressionLevel.NoCompression => 6,
835+
_ => 0
836+
}
837+
: 0);
838+
839+
return (BitFlagValues)(((int)generalPurposeBitFlag & ~0x6) | deflateCompressionOptions);
840+
}
841+
802842
// return value is true if we allocated an extra field for 64 bit headers, un/compressed size
803843
private bool WriteLocalFileHeader(bool isEmptyFile)
804844
{

src/libraries/System.IO.Compression/tests/ZipArchive/zip_CreateTests.cs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,78 @@ public static void CreateUncompressedArchive()
142142
}
143143
}
144144

145+
// This test checks to ensure that setting the compression level of an archive entry sets the general-purpose
146+
// bit flags correctly. It verifies that these have been set by reading from the MemoryStream manually, and by
147+
// reopening the generated file to confirm that the compression levels match.
148+
[Theory]
149+
// Special-case NoCompression: in this case, the CompressionMethod becomes Stored and the bits are unset.
150+
[InlineData(CompressionLevel.NoCompression, 0)]
151+
[InlineData(CompressionLevel.Optimal, 0)]
152+
[InlineData(CompressionLevel.SmallestSize, 2)]
153+
[InlineData(CompressionLevel.Fastest, 6)]
154+
public static void CreateArchiveEntriesWithBitFlags(CompressionLevel compressionLevel, ushort expectedGeneralBitFlags)
155+
{
156+
var testfilename = "testfile";
157+
var testFileContent = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.";
158+
var utf8WithoutBom = new Text.UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
159+
160+
byte[] zipFileContent;
161+
162+
using (var testStream = new MemoryStream())
163+
{
164+
165+
using (var zip = new ZipArchive(testStream, ZipArchiveMode.Create))
166+
{
167+
ZipArchiveEntry newEntry = zip.CreateEntry(testfilename, compressionLevel);
168+
using (var writer = new StreamWriter(newEntry.Open(), utf8WithoutBom))
169+
{
170+
writer.Write(testFileContent);
171+
writer.Flush();
172+
}
173+
174+
ZipArchiveEntry secondNewEntry = zip.CreateEntry(testFileContent + "_post", CompressionLevel.NoCompression);
175+
}
176+
177+
zipFileContent = testStream.ToArray();
178+
}
179+
180+
// expected bit flags are at position 6 in the file header
181+
var generalBitFlags = System.Buffers.Binary.BinaryPrimitives.ReadUInt16LittleEndian(zipFileContent.AsSpan(6));
182+
183+
Assert.Equal(expectedGeneralBitFlags, generalBitFlags);
184+
185+
using (var reReadStream = new MemoryStream(zipFileContent))
186+
{
187+
using (var reReadZip = new ZipArchive(reReadStream, ZipArchiveMode.Read))
188+
{
189+
var firstArchive = reReadZip.Entries[0];
190+
var secondArchive = reReadZip.Entries[1];
191+
var compressionLevelFieldInfo = typeof(ZipArchiveEntry).GetField("_compressionLevel", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
192+
var generalBitFlagsFieldInfo = typeof(ZipArchiveEntry).GetField("_generalPurposeBitFlag", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
193+
194+
var reReadCompressionLevel = (CompressionLevel)compressionLevelFieldInfo.GetValue(firstArchive);
195+
var reReadGeneralBitFlags = (ushort)generalBitFlagsFieldInfo.GetValue(firstArchive);
196+
197+
Assert.Equal(compressionLevel, reReadCompressionLevel);
198+
Assert.Equal(expectedGeneralBitFlags, reReadGeneralBitFlags);
199+
200+
reReadCompressionLevel = (CompressionLevel)compressionLevelFieldInfo.GetValue(secondArchive);
201+
Assert.Equal(CompressionLevel.NoCompression, reReadCompressionLevel);
202+
203+
using (var strm = firstArchive.Open())
204+
{
205+
var readBuffer = new byte[firstArchive.Length];
206+
207+
strm.Read(readBuffer);
208+
209+
var readText = Text.Encoding.UTF8.GetString(readBuffer);
210+
211+
Assert.Equal(readText, testFileContent);
212+
}
213+
}
214+
}
215+
}
216+
145217
[Fact]
146218
public static void CreateNormal_VerifyDataDescriptor()
147219
{

src/libraries/System.IO.Packaging/src/System/IO/Packaging/ZipPackage.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,11 @@ internal static void GetZipCompressionMethodFromOpcCompressionOption(
383383
break;
384384
case CompressionOption.Maximum:
385385
{
386+
#if NET
387+
compressionLevel = CompressionLevel.SmallestSize;
388+
#else
386389
compressionLevel = CompressionLevel.Optimal;
390+
#endif
387391
}
388392
break;
389393
case CompressionOption.Fast:

src/libraries/System.IO.Packaging/tests/ReflectionTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public void Verify_GeneralPurposeBitFlag_NotSetTo_Unicode()
3434
FieldInfo fieldInfo = typeof(ZipArchiveEntry).GetField("_generalPurposeBitFlag", BindingFlags.Instance | BindingFlags.NonPublic);
3535
object fieldObject = fieldInfo.GetValue(entry);
3636
ushort shortField = (ushort)fieldObject;
37-
Assert.Equal(0, shortField); // If it was UTF8, we would set the general purpose bit flag to 0x800 (UnicodeFileNameAndComment)
37+
Assert.Equal(0, shortField & 0x800); // If it was UTF8, we would set the general purpose bit flag to 0x800 (UnicodeFileNameAndComment)
3838
CheckCharacters(entry.Name);
3939
CheckCharacters(entry.Comment); // Unavailable in .NET Framework
4040
}

src/libraries/System.IO.Packaging/tests/Tests.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3988,6 +3988,47 @@ public void CreatePackUriWithFragment()
39883988

39893989
}
39903990

3991+
[Theory]
3992+
#if NET
3993+
[InlineData(CompressionOption.NotCompressed, CompressionOption.Normal, 0)]
3994+
[InlineData(CompressionOption.Normal, CompressionOption.Normal, 0)]
3995+
[InlineData(CompressionOption.Maximum, CompressionOption.Normal, 2)]
3996+
[InlineData(CompressionOption.Fast, CompressionOption.Normal, 6)]
3997+
[InlineData(CompressionOption.SuperFast, CompressionOption.Normal, 6)]
3998+
#else
3999+
[InlineData(CompressionOption.NotCompressed, CompressionOption.NotCompressed, 0)]
4000+
[InlineData(CompressionOption.Normal, CompressionOption.Normal, 0)]
4001+
[InlineData(CompressionOption.Maximum, CompressionOption.Maximum, 2)]
4002+
[InlineData(CompressionOption.Fast, CompressionOption.Fast, 4)]
4003+
[InlineData(CompressionOption.SuperFast, CompressionOption.SuperFast, 6)]
4004+
#endif
4005+
public void Roundtrip_Compression_Option(CompressionOption createdCompressionOption, CompressionOption expectedCompressionOption, ushort expectedZipFileBitFlags)
4006+
{
4007+
var documentPath = "untitled.txt";
4008+
Uri partUriDocument = PackUriHelper.CreatePartUri(new Uri(documentPath, UriKind.Relative));
4009+
4010+
using (MemoryStream ms = new MemoryStream())
4011+
{
4012+
Package package = Package.Open(ms, FileMode.Create, FileAccess.ReadWrite);
4013+
PackagePart part = package.CreatePart(partUriDocument, "application/text", createdCompressionOption);
4014+
4015+
package.Flush();
4016+
package.Close();
4017+
(package as IDisposable).Dispose();
4018+
4019+
ms.Seek(0, SeekOrigin.Begin);
4020+
4021+
var zipBytes = ms.ToArray();
4022+
var generalBitFlags = System.Buffers.Binary.BinaryPrimitives.ReadUInt16LittleEndian(zipBytes.AsSpan(6));
4023+
4024+
package = Package.Open(ms, FileMode.Open, FileAccess.Read);
4025+
part = package.GetPart(partUriDocument);
4026+
4027+
Assert.Equal(expectedZipFileBitFlags, generalBitFlags);
4028+
Assert.Equal(expectedCompressionOption, part.CompressionOption);
4029+
}
4030+
}
4031+
39914032
private const string DocumentRelationshipType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument";
39924033
}
39934034

0 commit comments

Comments
 (0)