Skip to content

Commit 68a88d1

Browse files
Force write of local file header when "version needed to extract" changes (#112032)
aapt2 creates ZIP entries with a "version needed to extract" of 0.0, but sometimes writes these entries with deflate compression (which requires a "version needed to extract" of 2.0.) When a ZipArchive is opened in Update mode and a new item is added, the central directory is written with the correct value for this field. However, the local file header for existing entries isn't rewritten (because it hasn't changed.) This leads to a mismatch between the local file header values and the central directory header values, which causes Android app builds to fail. When an existing entry's "version needed to extract" is changed, we force its local file header to be rewritten. There's a test to cover this. I've also added an entry to the packaged_resources.zip file from the issue and confirmed that the field matches between the local file header and the CD header for all compressed entries in the resultant file. * Add failing test * Force local file headers to be rewritten if the version to extract is changed * Code review Adding/correcting comments in test, and slightly reducing the diff in ZipArchiveEntry. --------- Co-authored-by: Carlos Sánchez López <[email protected]>
1 parent 9b77dd4 commit 68a88d1

File tree

2 files changed

+149
-7
lines changed

2 files changed

+149
-7
lines changed

Diff for: src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.cs

+4-2
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,15 @@ internal ZipArchiveEntry(ZipArchive archive, ZipCentralDirectoryFileHeader cd)
5353
_archive = archive;
5454

5555
_originallyInArchive = true;
56+
Changes = ZipArchive.ChangeState.Unchanged;
5657

5758
_diskNumberStart = cd.DiskNumberStart;
5859
_versionMadeByPlatform = (ZipVersionMadeByPlatform)cd.VersionMadeByCompatibility;
5960
_versionMadeBySpecification = (ZipVersionNeededValues)cd.VersionMadeBySpecification;
6061
_versionToExtract = (ZipVersionNeededValues)cd.VersionNeededToExtract;
6162
_generalPurposeBitFlag = (BitFlagValues)cd.GeneralPurposeBitFlag;
6263
_isEncrypted = (_generalPurposeBitFlag & BitFlagValues.IsEncrypted) != 0;
64+
// Setting CompressionMethod can change the _versionToExtract variable, which can change the value of Changes
6365
CompressionMethod = (CompressionMethodValues)cd.CompressionMethod;
6466
_lastModified = new DateTimeOffset(ZipHelper.DosTimeToDateTime(cd.LastModified));
6567
_compressedSize = cd.CompressedSize;
@@ -88,8 +90,6 @@ internal ZipArchiveEntry(ZipArchive archive, ZipCentralDirectoryFileHeader cd)
8890
_fileComment = cd.FileComment;
8991

9092
_compressionLevel = MapCompressionLevel(_generalPurposeBitFlag, CompressionMethod);
91-
92-
Changes = ZipArchive.ChangeState.Unchanged;
9393
}
9494

9595
// Initializes a ZipArchiveEntry instance for a new archive entry with a specified compression level.
@@ -1243,10 +1243,12 @@ private void VersionToExtractAtLeast(ZipVersionNeededValues value)
12431243
if (_versionToExtract < value)
12441244
{
12451245
_versionToExtract = value;
1246+
Changes |= ZipArchive.ChangeState.FixedLengthMetadata;
12461247
}
12471248
if (_versionMadeBySpecification < value)
12481249
{
12491250
_versionMadeBySpecification = value;
1251+
Changes |= ZipArchive.ChangeState.FixedLengthMetadata;
12501252
}
12511253
}
12521254

Diff for: src/libraries/System.IO.Compression/tests/ZipArchive/zip_InvalidParametersAndStrangeFiles.cs

+145-5
Original file line numberDiff line numberDiff line change
@@ -907,6 +907,146 @@ public static async Task ZipArchive_InvalidHuffmanData()
907907
}
908908
}
909909

910+
[Fact]
911+
public static void ZipArchive_InvalidVersionToExtract()
912+
{
913+
using (MemoryStream updatedStream = new MemoryStream())
914+
{
915+
int originalLocalVersionToExtract = s_inconsistentVersionToExtract[4];
916+
int originalCentralDirectoryVersionToExtract = s_inconsistentVersionToExtract[57];
917+
918+
// The existing archive will have a "version to extract" of 0.0, but will contain entries
919+
// with deflate compression (which has a minimum version to extract of 2.0.)
920+
Assert.Equal(0x00, originalLocalVersionToExtract);
921+
Assert.Equal(0x00, originalCentralDirectoryVersionToExtract);
922+
923+
// Write the example data to the stream. We expect to be able to read it (and the entry contents) successfully.
924+
updatedStream.Write(s_inconsistentVersionToExtract);
925+
updatedStream.Seek(0, SeekOrigin.Begin);
926+
927+
using (ZipArchive originalArchive = new ZipArchive(updatedStream, ZipArchiveMode.Read, true))
928+
{
929+
Assert.Equal(1, originalArchive.Entries.Count);
930+
931+
ZipArchiveEntry firstEntry = originalArchive.Entries[0];
932+
933+
Assert.Equal("first.bin", firstEntry.Name);
934+
Assert.Equal(10, firstEntry.Length);
935+
936+
using (Stream entryStream = firstEntry.Open())
937+
{
938+
Assert.Equal(10, firstEntry.Length);
939+
940+
byte[] uncompressedBytes = new byte[firstEntry.Length];
941+
int bytesRead = entryStream.Read(uncompressedBytes);
942+
943+
Assert.Equal(10, bytesRead);
944+
945+
Assert.Equal(new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09 }, uncompressedBytes);
946+
}
947+
}
948+
949+
updatedStream.Seek(0, SeekOrigin.Begin);
950+
951+
// Create a new entry, forcing the central directory headers to be rewritten. The local file header
952+
// for first.bin would normally be skipped (because it hasn't changed) but it needs to be rewritten
953+
// because the central directory headers will be rewritten with a valid value and the local file header
954+
// needs to match.
955+
using (ZipArchive updatedArchive = new ZipArchive(updatedStream, ZipArchiveMode.Update))
956+
{
957+
ZipArchiveEntry newEntry = updatedArchive.CreateEntry("second.bin", CompressionLevel.NoCompression);
958+
}
959+
960+
byte[] updatedContents = updatedStream.ToArray();
961+
int updatedLocalVersionToExtract = updatedContents[4];
962+
int updatedCentralDirectoryVersionToExtract = updatedContents[97];
963+
964+
Assert.Equal(20, updatedCentralDirectoryVersionToExtract);
965+
Assert.Equal(20, updatedLocalVersionToExtract);
966+
}
967+
}
968+
969+
private static readonly byte[] s_inconsistentVersionToExtract =
970+
{
971+
// ===== Local file header signature 0x04034b50
972+
0x50, 0x4b, 0x03, 0x04,
973+
// version to extract 0.0 (invalid - this should be at least 2.0 to make use of deflate compression)
974+
0x00, 0x00,
975+
// general purpose flags
976+
0x02, 0x00, // 0000_0002 'for maximum-compression deflating'
977+
// Deflate
978+
0x08, 0x00,
979+
// Last mod file time
980+
0x3b, 0x33,
981+
// Last mod date
982+
0x3f, 0x5a,
983+
// CRC32
984+
0x46, 0xd7, 0x6c, 0x45,
985+
// compressed size
986+
0x0c, 0x00, 0x00, 0x00,
987+
// uncompressed size
988+
0x0a, 0x00, 0x00, 0x00,
989+
// file name length
990+
0x09, 0x00,
991+
// extra field length
992+
0x00, 0x00,
993+
// filename
994+
0x66, 0x69, 0x72, 0x73, 0x74, 0x2e, 0x62, 0x69, 0x6e,
995+
// -------------
996+
// Data!
997+
0x63, 0x60, 0x64, 0x62, 0x66, 0x61, 0x65, 0x63, 0xe7, 0xe0, 0x04, 0x00,
998+
// -------- Central directory signature 0x02014b50
999+
0x50, 0x4b, 0x01, 0x02,
1000+
// version made by 2.0
1001+
0x14, 0x00,
1002+
// version to extract 0.0 (invalid - this should be at least 2.0 to make use of deflate compression)
1003+
0x00, 0x00,
1004+
// general purpose flags
1005+
0x02, 0x00,
1006+
// Deflate
1007+
0x08, 0x00,
1008+
// Last mod file time
1009+
0x3b, 0x33,
1010+
// Last mod date
1011+
0x3f, 0x5a,
1012+
// CRC32
1013+
0x46, 0xd7, 0x6c, 0x45,
1014+
// compressed size
1015+
0x0c, 0x00, 0x00, 0x00,
1016+
// uncompressed size
1017+
0x0a, 0x00, 0x00, 0x00,
1018+
// file name length
1019+
0x09, 0x00,
1020+
// extra field length
1021+
0x00, 0x00,
1022+
// file comment length
1023+
0x00, 0x00,
1024+
// disk number start
1025+
0x00, 0x00,
1026+
// internal file attributes
1027+
0x00, 0x00,
1028+
// external file attributes
1029+
0x00, 0x00, 0x00, 0x00,
1030+
// relative offset of local header
1031+
0x00, 0x00, 0x00, 0x00,
1032+
// file name
1033+
0x66, 0x69, 0x72, 0x73, 0x74, 0x2e, 0x62, 0x69, 0x6e,
1034+
// == 'end of CD' signature 0x06054b50
1035+
0x50, 0x4b, 0x05, 0x06,
1036+
// disk number, disk number with CD
1037+
0x00, 0x00,
1038+
0x00, 0x00,
1039+
// total number of entries in CD on this disk, and overall
1040+
0x01, 0x00,
1041+
0x01, 0x00,
1042+
// size of CD
1043+
0x37, 0x00, 0x00, 0x00,
1044+
// offset of start of CD wrt start disk
1045+
0x33, 0x00, 0x00, 0x00,
1046+
// comment length
1047+
0x00, 0x00
1048+
};
1049+
9101050
private static readonly byte[] s_slightlyIncorrectZip64 =
9111051
{
9121052
// ===== Local file header signature 0x04034b50
@@ -925,7 +1065,7 @@ public static async Task ZipArchive_InvalidHuffmanData()
9251065
0x0c, 0x7e, 0x7f, 0xd8,
9261066
// compressed size
9271067
0xff, 0xff, 0xff, 0xff,
928-
// UNcompressed size
1068+
// uncompressed size
9291069
0xff, 0xff, 0xff, 0xff,
9301070
// file name length
9311071
0x08, 0x00,
@@ -976,7 +1116,7 @@ public static async Task ZipArchive_InvalidHuffmanData()
9761116
0x0c, 0x7e, 0x7f, 0xd8,
9771117
// 4 byte compressed size, index 120 (-1 indicates refer to Zip64 extra field)
9781118
0xff, 0xff, 0xff, 0xff,
979-
// 4 byte UNcompressed size, index 124 (-1 indicates refer to Zip64 extra field)
1119+
// 4 byte uncompressed size, index 124 (-1 indicates refer to Zip64 extra field)
9801120
0xff, 0xff, 0xff, 0xff,
9811121
// file name length
9821122
0x08, 0x00,
@@ -1066,7 +1206,7 @@ public static async Task ZipArchive_InvalidHuffmanData()
10661206
0x0c, 0x7e, 0x7f, 0xd8,
10671207
// compressed size
10681208
0xff, 0xff, 0xff, 0xff,
1069-
// UNcompressed size
1209+
// uncompressed size
10701210
0xff, 0xff, 0xff, 0xff,
10711211
// file name length
10721212

@@ -1079,7 +1219,7 @@ public static async Task ZipArchive_InvalidHuffmanData()
10791219
0x01, 0x00,
10801220
// size of extra field block
10811221
0x20, 0x00,
1082-
// 8 byte Zip64 UNcompressed size, index 42
1222+
// 8 byte Zip64 uncompressed size, index 42
10831223
0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
10841224
// 8 byte Zip64 compressed size, index 50
10851225
0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
@@ -1122,7 +1262,7 @@ public static async Task ZipArchive_InvalidHuffmanData()
11221262
0x0c, 0x7e, 0x7f, 0xd8,
11231263
// 4 byte compressed size, index 120 (-1 indicates refer to Zip64 extra field)
11241264
0xff, 0xff, 0xff, 0xff,
1125-
// 4 byte UNcompressed size, index 124 (-1 indicates refer to Zip64 extra field)
1265+
// 4 byte uncompressed size, index 124 (-1 indicates refer to Zip64 extra field)
11261266
0xff, 0xff, 0xff, 0xff,
11271267
// file name length
11281268
0x08, 0x00,

0 commit comments

Comments
 (0)