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

[release/6.0-staging] Allow setting ZipArchiveEntry general-purpose flag bits #102096

Closed
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public partial class ZipArchiveEntry
private List<ZipGenericExtraField>? _cdUnknownExtraFields;
private List<ZipGenericExtraField>? _lhUnknownExtraFields;
private readonly byte[]? _fileComment;
private readonly CompressionLevel? _compressionLevel;
private readonly CompressionLevel _compressionLevel;

// Initializes, attaches it to archive
internal ZipArchiveEntry(ZipArchive archive, ZipCentralDirectoryFileHeader cd)
Expand Down Expand Up @@ -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
Expand All @@ -91,6 +91,7 @@ internal ZipArchiveEntry(ZipArchive archive, string entryName, CompressionLevel
{
CompressionMethod = CompressionMethodValues.Stored;
}
_generalPurposeBitFlag = MapDeflateCompressionOption(_generalPurposeBitFlag, _compressionLevel, CompressionMethod);
}

// Initializes new entry
Expand All @@ -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
Expand All @@ -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);

Expand Down Expand Up @@ -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;

}
Expand Down Expand Up @@ -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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
59 changes: 59 additions & 0 deletions src/libraries/System.IO.Packaging/tests/ReflectionTests.cs
Original file line number Diff line number Diff line change
@@ -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("_entryNameAndCommentEncoding", BindingFlags.Instance | BindingFlags.NonPublic);
carlossanlop marked this conversation as resolved.
Show resolved Hide resolved
System.Text.Encoding archiveEncoding = (archiveEncodingFieldInfo.GetValue(archive) as System.Text.Encoding) ?? System.Text.Encoding.UTF8;

foreach (ZipArchiveEntry entry in archive.Entries)
carlossanlop marked this conversation as resolved.
Show resolved Hide resolved
{
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);
carlossanlop marked this conversation as resolved.
Show resolved Hide resolved

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");
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
<ItemGroup>
<Compile Include="Tests.cs" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' != '.NETFramework'">
<Compile Include="ReflectionTests.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.IO.Packaging.TestData" Version="$(SystemIOPackagingTestDataVersion)" />
<ProjectReference Include="..\src\System.IO.Packaging.csproj" />
Expand Down
45 changes: 43 additions & 2 deletions src/libraries/System.IO.Packaging/tests/Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = @"<Hello>Test</Hello>";
internal const string s_DocumentXml = @"<Hello>Test</Hello>";
private const string s_ResourceXml = @"<Resource>Test</Resource>";

private FileInfo GetTempFileInfoFromExistingFile(string existingFileName, [CallerMemberName] string memberName = null, [CallerLineNumber] int lineNumber = 0)
Expand Down 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));
carlossanlop marked this conversation as resolved.
Show resolved Hide resolved

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
Loading