From 326a421bce6626448d222c53efa86843fdbe9508 Mon Sep 17 00:00:00 2001 From: wfurt Date: Fri, 8 May 2026 20:24:57 -0700 Subject: [PATCH 1/3] add zip test for blob with negative size --- .../zip_InvalidParametersAndStrangeFiles.cs | 182 ++++++++++++++++++ 1 file changed, 182 insertions(+) diff --git a/src/libraries/System.IO.Compression/tests/ZipArchive/zip_InvalidParametersAndStrangeFiles.cs b/src/libraries/System.IO.Compression/tests/ZipArchive/zip_InvalidParametersAndStrangeFiles.cs index bc8cd23f27a130..c8760adac656ff 100644 --- a/src/libraries/System.IO.Compression/tests/ZipArchive/zip_InvalidParametersAndStrangeFiles.cs +++ b/src/libraries/System.IO.Compression/tests/ZipArchive/zip_InvalidParametersAndStrangeFiles.cs @@ -2399,5 +2399,187 @@ public static async Task OpenWithFileAccess_DisposedArchive_Throws(bool async) Assert.Throws(() => entry.Open(FileAccess.Read)); await Assert.ThrowsAsync(() => entry.OpenAsync(FileAccess.Read)); } + + [Fact] + public static void Zip64ExtraField_NegativeUncompressedSize_DoesNotCauseHarm() + { + // Validation for IO-021: Zip64 extra field with negative UncompressedSize (-1). + // The minimal 16-byte Zip64 extra field bypasses the negative-value check in + // Zip64ExtraField.TryGetZip64BlockFromGenericExtraField (the check only runs after + // all four fields would have been read). This test verifies that the bypassed + // negative value does NOT lead to memory corruption, buffer over-read, infinite + // loop, or other harmful behavior — downstream code handles it safely. + byte[] zipArchive = CreateZipWithNegativeZip64UncompressedSize(); + + using ZipArchive archive = new ZipArchive(new MemoryStream(zipArchive), ZipArchiveMode.Read); + Assert.Equal(1, archive.Entries.Count); + ZipArchiveEntry entry = archive.Entries[0]; + + // The negative value propagates as-is to the entry's Length property. + // This is observable but not by itself harmful. + Assert.Equal(-1L, entry.Length); + Assert.Equal(0L, entry.CompressedLength); + + // Attempting to actually read the data must fail safely — either by throwing, + // or by returning zero bytes (matching the actual stored data). It must NOT + // crash, hang, allocate based on the negative size, or read past the end of + // the underlying stream. Defense-in-depth in CrcValidatingReadStream rejects + // a negative expected length on Open(), so an ArgumentOutOfRangeException or + // InvalidDataException is the expected outcome here. + try + { + using Stream s = entry.Open(); + byte[] buffer = new byte[1024]; + int totalRead = 0; + int read; + while ((read = s.Read(buffer, 0, buffer.Length)) > 0) + { + totalRead += read; + // Guard against the negative size being misinterpreted as a huge unsigned + // length that would let the read loop run forever. + Assert.True(totalRead <= 1024 * 1024, "Read returned more data than the archive contains."); + } + Assert.Equal(0, totalRead); + } + catch (Exception ex) when (ex is InvalidDataException or ArgumentOutOfRangeException) + { + // Acceptable: downstream code rejects the malformed entry on Open/Read. + } + + // Re-open and confirm enumerating Entries again is still safe. + using ZipArchive archive2 = new ZipArchive(new MemoryStream(zipArchive), ZipArchiveMode.Read); + foreach (ZipArchiveEntry e in archive2.Entries) + { + Assert.Equal("test.txt", e.FullName); + } + } + + private static byte[] CreateZipWithNegativeZip64UncompressedSize() + { + // Create a minimal ZIP with Zip64 extra field containing negative UncompressedSize (-1) + // Structure: + // - Local file header with Zip64 extra field + // - Empty file data + // - Central directory header with Zip64 extra field + // - End of central directory record + + using (MemoryStream ms = new MemoryStream()) + { + static void WriteUInt16(Stream stream, ushort value) + { + Span buffer = stackalloc byte[2]; + BinaryPrimitives.WriteUInt16LittleEndian(buffer, value); + stream.Write(buffer); + } + + static void WriteUInt32(Stream stream, uint value) + { + Span buffer = stackalloc byte[4]; + BinaryPrimitives.WriteUInt32LittleEndian(buffer, value); + stream.Write(buffer); + } + + static void WriteInt64(Stream stream, long value) + { + Span buffer = stackalloc byte[8]; + BinaryPrimitives.WriteInt64LittleEndian(buffer, value); + stream.Write(buffer); + } + + // Local File Header + const uint localFileHeaderSig = 0x04034b50; + const ushort versionNeeded = 45; // ZIP64 requires version 4.5+ + const ushort generalPurposeBitFlag = 0; + const ushort compressionMethod = 0; // No compression + const ushort lastModFileTime = 0; + const ushort lastModFileDate = 0; + const uint crc32 = 0; + const uint compressedSize = 0xFFFFFFFF; // Indicates Zip64 extra field + const uint uncompressedSize = 0xFFFFFFFF; // Indicates Zip64 extra field + const string fileName = "test.txt"; + byte[] fileNameBytes = Encoding.UTF8.GetBytes(fileName); + + // Zip64 extra field: + // Tag (2 bytes) = 1, Size (2 bytes) = 16 (for 2 x 8-byte fields), UncompressedSize (8 bytes), CompressedSize (8 bytes) + const ushort zip64Tag = 1; + const ushort zip64Size = 16; // 8 bytes for uncompressed + 8 bytes for compressed + long negativeUncompressedSize = -1; // 0xFFFFFFFFFFFFFFFF in two's complement + long negativeCompressedSize = 0; + + // Write local file header + WriteUInt32(ms, localFileHeaderSig); + WriteUInt16(ms, versionNeeded); + WriteUInt16(ms, generalPurposeBitFlag); + WriteUInt16(ms, compressionMethod); + WriteUInt16(ms, lastModFileTime); + WriteUInt16(ms, lastModFileDate); + WriteUInt32(ms, crc32); + WriteUInt32(ms, compressedSize); + WriteUInt32(ms, uncompressedSize); + WriteUInt16(ms, (ushort)fileNameBytes.Length); + + // Extra field length = 4 (tag + size) + 16 (zip64 data) + WriteUInt16(ms, (ushort)(4 + zip64Size)); + + // Write filename + ms.Write(fileNameBytes, 0, fileNameBytes.Length); + + // Write Zip64 extra field + WriteUInt16(ms, zip64Tag); + WriteUInt16(ms, zip64Size); + WriteInt64(ms, negativeUncompressedSize); // Negative value! + WriteInt64(ms, negativeCompressedSize); + + // No file data + long dataOffset = ms.Position; + + // Central Directory File Header + const uint centralDirSig = 0x02014b50; + const ushort versionMadeBy = 45; + + WriteUInt32(ms, centralDirSig); + WriteUInt16(ms, versionMadeBy); + WriteUInt16(ms, versionNeeded); + WriteUInt16(ms, generalPurposeBitFlag); + WriteUInt16(ms, compressionMethod); + WriteUInt16(ms, lastModFileTime); + WriteUInt16(ms, lastModFileDate); + WriteUInt32(ms, crc32); + WriteUInt32(ms, compressedSize); + WriteUInt32(ms, uncompressedSize); + WriteUInt16(ms, (ushort)fileNameBytes.Length); + WriteUInt16(ms, (ushort)(4 + zip64Size)); // Extra field length + WriteUInt16(ms, 0); // File comment length + WriteUInt16(ms, 0); // Disk number start + WriteUInt16(ms, 0); // Internal file attributes + WriteUInt32(ms, 0); // External file attributes + WriteUInt32(ms, 0); // Relative offset of local header + + // Write filename + ms.Write(fileNameBytes, 0, fileNameBytes.Length); + + // Write Zip64 extra field + WriteUInt16(ms, zip64Tag); + WriteUInt16(ms, zip64Size); + WriteInt64(ms, negativeUncompressedSize); + WriteInt64(ms, negativeCompressedSize); + + long centralDirSize = ms.Position - dataOffset; + long centralDirOffset = dataOffset; + + // End of Central Directory Record + const uint endCentralDirSig = 0x06054b50; + WriteUInt32(ms, endCentralDirSig); + WriteUInt16(ms, 0); // Disk number + WriteUInt16(ms, 0); // Disk with central directory + WriteUInt16(ms, 1); // Number of entries on this disk + WriteUInt16(ms, 1); // Total number of entries + WriteUInt32(ms, (uint)centralDirSize); + WriteUInt32(ms, (uint)centralDirOffset); + WriteUInt16(ms, 0); // Comment length + + return ms.ToArray(); + } + } } } From 7c5122417ad36ed2bf639958a11ab2e5b37bb04d Mon Sep 17 00:00:00 2001 From: wfurt Date: Wed, 20 May 2026 17:45:22 -0700 Subject: [PATCH 2/3] Address review feedback - Reject negative compressed/uncompressed sizes in IsOpenableInitialVerifications so a malformed Zip64 entry surfaces as InvalidDataException instead of ArgumentOutOfRangeException. - Tighten test catch to InvalidDataException only. - Simplify test comments to describe externally observable behavior. - Rename zip64 size locals and centralDirectoryOffset for clarity. --- .../System/IO/Compression/ZipArchiveEntry.cs | 5 +++ .../zip_InvalidParametersAndStrangeFiles.cs | 41 ++++++++----------- 2 files changed, 23 insertions(+), 23 deletions(-) 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 e84666144c9379..9d36462affcde9 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 @@ -973,6 +973,11 @@ private bool IsOpenableInitialVerifications(bool needToUncompress, out string? m message = SR.LocalFileHeaderCorrupt; return false; } + if (_compressedSize < 0 || _uncompressedSize < 0) + { + message = SR.LocalFileHeaderCorrupt; + return false; + } _archive.ArchiveStream.Seek(_offsetOfLocalHeader, SeekOrigin.Begin); return true; diff --git a/src/libraries/System.IO.Compression/tests/ZipArchive/zip_InvalidParametersAndStrangeFiles.cs b/src/libraries/System.IO.Compression/tests/ZipArchive/zip_InvalidParametersAndStrangeFiles.cs index dbccc959b91816..5c485b2c3f4a13 100644 --- a/src/libraries/System.IO.Compression/tests/ZipArchive/zip_InvalidParametersAndStrangeFiles.cs +++ b/src/libraries/System.IO.Compression/tests/ZipArchive/zip_InvalidParametersAndStrangeFiles.cs @@ -2438,12 +2438,10 @@ public static async Task OpenWithFileAccess_DisposedArchive_Throws(bool async) [Fact] public static void Zip64ExtraField_NegativeUncompressedSize_DoesNotCauseHarm() { - // Validation for IO-021: Zip64 extra field with negative UncompressedSize (-1). - // The minimal 16-byte Zip64 extra field bypasses the negative-value check in - // Zip64ExtraField.TryGetZip64BlockFromGenericExtraField (the check only runs after - // all four fields would have been read). This test verifies that the bypassed - // negative value does NOT lead to memory corruption, buffer over-read, infinite - // loop, or other harmful behavior — downstream code handles it safely. + // Validation for IO-021: a ZIP64 extra field encodes the uncompressed size as + // 0xFFFF_FFFF_FFFF_FFFF, which is observed as -1L. This malformed metadata must + // not lead to memory corruption, buffer over-read, infinite loop, or other + // harmful behavior. The archive should continue to handle the entry safely. byte[] zipArchive = CreateZipWithNegativeZip64UncompressedSize(); using ZipArchive archive = new ZipArchive(new MemoryStream(zipArchive), ZipArchiveMode.Read); @@ -2455,12 +2453,10 @@ public static void Zip64ExtraField_NegativeUncompressedSize_DoesNotCauseHarm() Assert.Equal(-1L, entry.Length); Assert.Equal(0L, entry.CompressedLength); - // Attempting to actually read the data must fail safely — either by throwing, - // or by returning zero bytes (matching the actual stored data). It must NOT - // crash, hang, allocate based on the negative size, or read past the end of - // the underlying stream. Defense-in-depth in CrcValidatingReadStream rejects - // a negative expected length on Open(), so an ArgumentOutOfRangeException or - // InvalidDataException is the expected outcome here. + // Attempting to actually read the data must fail safely — either by throwing + // InvalidDataException, or by returning zero bytes (matching the actual stored + // data). It must NOT crash, hang, allocate based on the negative size, or read + // past the end of the underlying stream. try { using Stream s = entry.Open(); @@ -2476,7 +2472,7 @@ public static void Zip64ExtraField_NegativeUncompressedSize_DoesNotCauseHarm() } Assert.Equal(0, totalRead); } - catch (Exception ex) when (ex is InvalidDataException or ArgumentOutOfRangeException) + catch (InvalidDataException) { // Acceptable: downstream code rejects the malformed entry on Open/Read. } @@ -2538,8 +2534,8 @@ static void WriteInt64(Stream stream, long value) // Tag (2 bytes) = 1, Size (2 bytes) = 16 (for 2 x 8-byte fields), UncompressedSize (8 bytes), CompressedSize (8 bytes) const ushort zip64Tag = 1; const ushort zip64Size = 16; // 8 bytes for uncompressed + 8 bytes for compressed - long negativeUncompressedSize = -1; // 0xFFFFFFFFFFFFFFFF in two's complement - long negativeCompressedSize = 0; + long zip64UncompressedSize = -1; // 0xFFFFFFFFFFFFFFFF in two's complement + long zip64CompressedSize = 0; // Write local file header WriteUInt32(ms, localFileHeaderSig); @@ -2562,11 +2558,11 @@ static void WriteInt64(Stream stream, long value) // Write Zip64 extra field WriteUInt16(ms, zip64Tag); WriteUInt16(ms, zip64Size); - WriteInt64(ms, negativeUncompressedSize); // Negative value! - WriteInt64(ms, negativeCompressedSize); + WriteInt64(ms, zip64UncompressedSize); // Negative value! + WriteInt64(ms, zip64CompressedSize); // No file data - long dataOffset = ms.Position; + long centralDirectoryOffset = ms.Position; // Central Directory File Header const uint centralDirSig = 0x02014b50; @@ -2596,11 +2592,10 @@ static void WriteInt64(Stream stream, long value) // Write Zip64 extra field WriteUInt16(ms, zip64Tag); WriteUInt16(ms, zip64Size); - WriteInt64(ms, negativeUncompressedSize); - WriteInt64(ms, negativeCompressedSize); + WriteInt64(ms, zip64UncompressedSize); + WriteInt64(ms, zip64CompressedSize); - long centralDirSize = ms.Position - dataOffset; - long centralDirOffset = dataOffset; + long centralDirSize = ms.Position - centralDirectoryOffset; // End of Central Directory Record const uint endCentralDirSig = 0x06054b50; @@ -2610,7 +2605,7 @@ static void WriteInt64(Stream stream, long value) WriteUInt16(ms, 1); // Number of entries on this disk WriteUInt16(ms, 1); // Total number of entries WriteUInt32(ms, (uint)centralDirSize); - WriteUInt32(ms, (uint)centralDirOffset); + WriteUInt32(ms, (uint)centralDirectoryOffset); WriteUInt16(ms, 0); // Comment length return ms.ToArray(); From ef20b423c447600b57551020c0305ded090fae7d Mon Sep 17 00:00:00 2001 From: alinpahontu2912 Date: Mon, 8 Jun 2026 13:43:30 +0300 Subject: [PATCH 3/3] add early checks for zip64 malformed sizes --- .../System/IO/Compression/ZipArchiveEntry.cs | 5 - .../src/System/IO/Compression/ZipBlocks.cs | 31 +- .../zip_InvalidParametersAndStrangeFiles.cs | 275 +++++++----------- 3 files changed, 130 insertions(+), 181 deletions(-) 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 9d36462affcde9..e84666144c9379 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 @@ -973,11 +973,6 @@ private bool IsOpenableInitialVerifications(bool needToUncompress, out string? m message = SR.LocalFileHeaderCorrupt; return false; } - if (_compressedSize < 0 || _uncompressedSize < 0) - { - message = SR.LocalFileHeaderCorrupt; - return false; - } _archive.ArchiveStream.Seek(_offsetOfLocalHeader, SeekOrigin.Begin); return true; diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipBlocks.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipBlocks.cs index ee5bd67e21f65c..9e6c5ba913efae 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipBlocks.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipBlocks.cs @@ -257,9 +257,18 @@ private static bool TryGetZip64BlockFromGenericExtraField(ZipGenericExtraField e // 2. When the size indicates that all the information is available ("slightly invalid files"). bool readAllFields = extraField.Size >= MaximumExtraFieldLength; + // The original values are unsigned 64-bit, so a negative signed value means the + // value does not fit in Int64 and cannot be represented by the rest of the API + // (which uses long). Validate each field as it is read so that short extra fields + // (which exit early below) cannot bypass the check. + if (readUncompressedSize) { zip64Block._uncompressedSize = BinaryPrimitives.ReadInt64LittleEndian(data); + if (zip64Block._uncompressedSize < 0) + { + throw new InvalidDataException(SR.FieldTooBigUncompressedSize); + } data = data.Slice(FieldLengths.UncompressedSize); } else if (readAllFields) @@ -275,6 +284,10 @@ private static bool TryGetZip64BlockFromGenericExtraField(ZipGenericExtraField e if (readCompressedSize) { zip64Block._compressedSize = BinaryPrimitives.ReadInt64LittleEndian(data); + if (zip64Block._compressedSize < 0) + { + throw new InvalidDataException(SR.FieldTooBigCompressedSize); + } data = data.Slice(FieldLengths.CompressedSize); } else if (readAllFields) @@ -290,6 +303,10 @@ private static bool TryGetZip64BlockFromGenericExtraField(ZipGenericExtraField e if (readLocalHeaderOffset) { zip64Block._localHeaderOffset = BinaryPrimitives.ReadInt64LittleEndian(data); + if (zip64Block._localHeaderOffset < 0) + { + throw new InvalidDataException(SR.FieldTooBigLocalHeaderOffset); + } data = data.Slice(FieldLengths.LocalHeaderOffset); } else if (readAllFields) @@ -307,20 +324,6 @@ private static bool TryGetZip64BlockFromGenericExtraField(ZipGenericExtraField e zip64Block._startDiskNumber = BinaryPrimitives.ReadUInt32LittleEndian(data); } - // original values are unsigned, so implies value is too big to fit in signed integer - if (zip64Block._uncompressedSize < 0) - { - throw new InvalidDataException(SR.FieldTooBigUncompressedSize); - } - if (zip64Block._compressedSize < 0) - { - throw new InvalidDataException(SR.FieldTooBigCompressedSize); - } - if (zip64Block._localHeaderOffset < 0) - { - throw new InvalidDataException(SR.FieldTooBigLocalHeaderOffset); - } - return true; } diff --git a/src/libraries/System.IO.Compression/tests/ZipArchive/zip_InvalidParametersAndStrangeFiles.cs b/src/libraries/System.IO.Compression/tests/ZipArchive/zip_InvalidParametersAndStrangeFiles.cs index 5c485b2c3f4a13..f21623e2e6b903 100644 --- a/src/libraries/System.IO.Compression/tests/ZipArchive/zip_InvalidParametersAndStrangeFiles.cs +++ b/src/libraries/System.IO.Compression/tests/ZipArchive/zip_InvalidParametersAndStrangeFiles.cs @@ -2435,181 +2435,132 @@ public static async Task OpenWithFileAccess_DisposedArchive_Throws(bool async) await Assert.ThrowsAsync(() => entry.OpenAsync(FileAccess.Read)); } - [Fact] - public static void Zip64ExtraField_NegativeUncompressedSize_DoesNotCauseHarm() + [Theory] + [MemberData(nameof(Get_Booleans_Data))] + public static async Task Zip64ExtraField_NegativeUncompressedSize_Throws(bool async) { - // Validation for IO-021: a ZIP64 extra field encodes the uncompressed size as - // 0xFFFF_FFFF_FFFF_FFFF, which is observed as -1L. This malformed metadata must - // not lead to memory corruption, buffer over-read, infinite loop, or other - // harmful behavior. The archive should continue to handle the entry safely. + // A ZIP64 extra field that encodes the uncompressed size as 0xFFFF_FFFF_FFFF_FFFF + // (observed as -1L) cannot be represented by the long-based public surface and is + // malformed. The central directory parser must reject it eagerly so that callers + // never see a negative Length or CompressedLength on a ZipArchiveEntry. byte[] zipArchive = CreateZipWithNegativeZip64UncompressedSize(); - using ZipArchive archive = new ZipArchive(new MemoryStream(zipArchive), ZipArchiveMode.Read); - Assert.Equal(1, archive.Entries.Count); - ZipArchiveEntry entry = archive.Entries[0]; - - // The negative value propagates as-is to the entry's Length property. - // This is observable but not by itself harmful. - Assert.Equal(-1L, entry.Length); - Assert.Equal(0L, entry.CompressedLength); - - // Attempting to actually read the data must fail safely — either by throwing - // InvalidDataException, or by returning zero bytes (matching the actual stored - // data). It must NOT crash, hang, allocate based on the negative size, or read - // past the end of the underlying stream. - try - { - using Stream s = entry.Open(); - byte[] buffer = new byte[1024]; - int totalRead = 0; - int read; - while ((read = s.Read(buffer, 0, buffer.Length)) > 0) - { - totalRead += read; - // Guard against the negative size being misinterpreted as a huge unsigned - // length that would let the read loop run forever. - Assert.True(totalRead <= 1024 * 1024, "Read returned more data than the archive contains."); - } - Assert.Equal(0, totalRead); - } - catch (InvalidDataException) - { - // Acceptable: downstream code rejects the malformed entry on Open/Read. - } - - // Re-open and confirm enumerating Entries again is still safe. - using ZipArchive archive2 = new ZipArchive(new MemoryStream(zipArchive), ZipArchiveMode.Read); - foreach (ZipArchiveEntry e in archive2.Entries) + await Assert.ThrowsAsync(async () => { - Assert.Equal("test.txt", e.FullName); - } + ZipArchive archive = await CreateZipArchive(async, new MemoryStream(zipArchive), ZipArchiveMode.Read); + await DisposeZipArchive(async, archive); + }); } private static byte[] CreateZipWithNegativeZip64UncompressedSize() { - // Create a minimal ZIP with Zip64 extra field containing negative UncompressedSize (-1) - // Structure: - // - Local file header with Zip64 extra field - // - Empty file data - // - Central directory header with Zip64 extra field - // - End of central directory record + // Crafts a minimal ZIP whose central directory entry uses the ZIP64 sentinel + // (0xFFFFFFFF) for both compressed and uncompressed size, while the matching + // ZIP64 extra field stores -1L (0xFFFFFFFFFFFFFFFF) as the 64-bit uncompressed + // size. The local file header uses the same shape so the file is internally + // consistent up to the malformed size value. + // + // Layout: + // Local file header + ZIP64 extra (uncompressed=-1, compressed=0) + // (no file data) + // Central directory header + ZIP64 extra (uncompressed=-1, compressed=0) + // End of central directory record + using MemoryStream ms = new MemoryStream(); - using (MemoryStream ms = new MemoryStream()) + static void WriteUInt16(Stream stream, ushort value) { - static void WriteUInt16(Stream stream, ushort value) - { - Span buffer = stackalloc byte[2]; - BinaryPrimitives.WriteUInt16LittleEndian(buffer, value); - stream.Write(buffer); - } - - static void WriteUInt32(Stream stream, uint value) - { - Span buffer = stackalloc byte[4]; - BinaryPrimitives.WriteUInt32LittleEndian(buffer, value); - stream.Write(buffer); - } + Span buffer = stackalloc byte[2]; + BinaryPrimitives.WriteUInt16LittleEndian(buffer, value); + stream.Write(buffer); + } - static void WriteInt64(Stream stream, long value) - { - Span buffer = stackalloc byte[8]; - BinaryPrimitives.WriteInt64LittleEndian(buffer, value); - stream.Write(buffer); - } + static void WriteUInt32(Stream stream, uint value) + { + Span buffer = stackalloc byte[4]; + BinaryPrimitives.WriteUInt32LittleEndian(buffer, value); + stream.Write(buffer); + } - // Local File Header - const uint localFileHeaderSig = 0x04034b50; - const ushort versionNeeded = 45; // ZIP64 requires version 4.5+ - const ushort generalPurposeBitFlag = 0; - const ushort compressionMethod = 0; // No compression - const ushort lastModFileTime = 0; - const ushort lastModFileDate = 0; - const uint crc32 = 0; - const uint compressedSize = 0xFFFFFFFF; // Indicates Zip64 extra field - const uint uncompressedSize = 0xFFFFFFFF; // Indicates Zip64 extra field - const string fileName = "test.txt"; - byte[] fileNameBytes = Encoding.UTF8.GetBytes(fileName); - - // Zip64 extra field: - // Tag (2 bytes) = 1, Size (2 bytes) = 16 (for 2 x 8-byte fields), UncompressedSize (8 bytes), CompressedSize (8 bytes) - const ushort zip64Tag = 1; - const ushort zip64Size = 16; // 8 bytes for uncompressed + 8 bytes for compressed - long zip64UncompressedSize = -1; // 0xFFFFFFFFFFFFFFFF in two's complement - long zip64CompressedSize = 0; - - // Write local file header - WriteUInt32(ms, localFileHeaderSig); - WriteUInt16(ms, versionNeeded); - WriteUInt16(ms, generalPurposeBitFlag); - WriteUInt16(ms, compressionMethod); - WriteUInt16(ms, lastModFileTime); - WriteUInt16(ms, lastModFileDate); - WriteUInt32(ms, crc32); - WriteUInt32(ms, compressedSize); - WriteUInt32(ms, uncompressedSize); - WriteUInt16(ms, (ushort)fileNameBytes.Length); - - // Extra field length = 4 (tag + size) + 16 (zip64 data) - WriteUInt16(ms, (ushort)(4 + zip64Size)); - - // Write filename - ms.Write(fileNameBytes, 0, fileNameBytes.Length); - - // Write Zip64 extra field - WriteUInt16(ms, zip64Tag); - WriteUInt16(ms, zip64Size); - WriteInt64(ms, zip64UncompressedSize); // Negative value! - WriteInt64(ms, zip64CompressedSize); - - // No file data - long centralDirectoryOffset = ms.Position; - - // Central Directory File Header - const uint centralDirSig = 0x02014b50; - const ushort versionMadeBy = 45; - - WriteUInt32(ms, centralDirSig); - WriteUInt16(ms, versionMadeBy); - WriteUInt16(ms, versionNeeded); - WriteUInt16(ms, generalPurposeBitFlag); - WriteUInt16(ms, compressionMethod); - WriteUInt16(ms, lastModFileTime); - WriteUInt16(ms, lastModFileDate); - WriteUInt32(ms, crc32); - WriteUInt32(ms, compressedSize); - WriteUInt32(ms, uncompressedSize); - WriteUInt16(ms, (ushort)fileNameBytes.Length); - WriteUInt16(ms, (ushort)(4 + zip64Size)); // Extra field length - WriteUInt16(ms, 0); // File comment length - WriteUInt16(ms, 0); // Disk number start - WriteUInt16(ms, 0); // Internal file attributes - WriteUInt32(ms, 0); // External file attributes - WriteUInt32(ms, 0); // Relative offset of local header - - // Write filename - ms.Write(fileNameBytes, 0, fileNameBytes.Length); - - // Write Zip64 extra field - WriteUInt16(ms, zip64Tag); - WriteUInt16(ms, zip64Size); - WriteInt64(ms, zip64UncompressedSize); - WriteInt64(ms, zip64CompressedSize); - - long centralDirSize = ms.Position - centralDirectoryOffset; - - // End of Central Directory Record - const uint endCentralDirSig = 0x06054b50; - WriteUInt32(ms, endCentralDirSig); - WriteUInt16(ms, 0); // Disk number - WriteUInt16(ms, 0); // Disk with central directory - WriteUInt16(ms, 1); // Number of entries on this disk - WriteUInt16(ms, 1); // Total number of entries - WriteUInt32(ms, (uint)centralDirSize); - WriteUInt32(ms, (uint)centralDirectoryOffset); - WriteUInt16(ms, 0); // Comment length - - return ms.ToArray(); + static void WriteInt64(Stream stream, long value) + { + Span buffer = stackalloc byte[8]; + BinaryPrimitives.WriteInt64LittleEndian(buffer, value); + stream.Write(buffer); } + + const uint LocalFileHeaderSig = 0x04034b50; + const uint CentralDirSig = 0x02014b50; + const uint EndCentralDirSig = 0x06054b50; + const ushort VersionNeeded = 45; + const ushort VersionMadeBy = 45; + const ushort GeneralPurposeBitFlag = 0; + const ushort CompressionMethod = 0; + const ushort LastModFileTime = 0; + const ushort LastModFileDate = 0; + const uint Crc32 = 0; + const uint SentinelSize = 0xFFFFFFFF; + const ushort Zip64Tag = 1; + const ushort Zip64ExtraDataSize = 16; + const ushort Zip64ExtraTotalSize = 4 + Zip64ExtraDataSize; + const long Zip64UncompressedSize = -1; + const long Zip64CompressedSize = 0; + + byte[] fileNameBytes = Encoding.UTF8.GetBytes("test.txt"); + + WriteUInt32(ms, LocalFileHeaderSig); + WriteUInt16(ms, VersionNeeded); + WriteUInt16(ms, GeneralPurposeBitFlag); + WriteUInt16(ms, CompressionMethod); + WriteUInt16(ms, LastModFileTime); + WriteUInt16(ms, LastModFileDate); + WriteUInt32(ms, Crc32); + WriteUInt32(ms, SentinelSize); + WriteUInt32(ms, SentinelSize); + WriteUInt16(ms, (ushort)fileNameBytes.Length); + WriteUInt16(ms, Zip64ExtraTotalSize); + ms.Write(fileNameBytes, 0, fileNameBytes.Length); + WriteUInt16(ms, Zip64Tag); + WriteUInt16(ms, Zip64ExtraDataSize); + WriteInt64(ms, Zip64UncompressedSize); + WriteInt64(ms, Zip64CompressedSize); + + long centralDirectoryOffset = ms.Position; + + WriteUInt32(ms, CentralDirSig); + WriteUInt16(ms, VersionMadeBy); + WriteUInt16(ms, VersionNeeded); + WriteUInt16(ms, GeneralPurposeBitFlag); + WriteUInt16(ms, CompressionMethod); + WriteUInt16(ms, LastModFileTime); + WriteUInt16(ms, LastModFileDate); + WriteUInt32(ms, Crc32); + WriteUInt32(ms, SentinelSize); + WriteUInt32(ms, SentinelSize); + WriteUInt16(ms, (ushort)fileNameBytes.Length); + WriteUInt16(ms, Zip64ExtraTotalSize); + WriteUInt16(ms, 0); + WriteUInt16(ms, 0); + WriteUInt16(ms, 0); + WriteUInt32(ms, 0); + WriteUInt32(ms, 0); + ms.Write(fileNameBytes, 0, fileNameBytes.Length); + WriteUInt16(ms, Zip64Tag); + WriteUInt16(ms, Zip64ExtraDataSize); + WriteInt64(ms, Zip64UncompressedSize); + WriteInt64(ms, Zip64CompressedSize); + + long centralDirSize = ms.Position - centralDirectoryOffset; + + WriteUInt32(ms, EndCentralDirSig); + WriteUInt16(ms, 0); + WriteUInt16(ms, 0); + WriteUInt16(ms, 1); + WriteUInt16(ms, 1); + WriteUInt32(ms, (uint)centralDirSize); + WriteUInt32(ms, (uint)centralDirectoryOffset); + WriteUInt16(ms, 0); + + return ms.ToArray(); } } }