|
| 1 | +using System.Text; |
| 2 | + |
| 3 | +namespace DeterministicIoPackaging; |
| 4 | + |
| 5 | +/// <summary> |
| 6 | +/// Rewrites a ZIP archive so every entry uses method 0 (Stored). |
| 7 | +/// This ensures byte-identical output across all .NET runtimes, |
| 8 | +/// since the built-in ZipArchive on net48 ignores CompressionLevel.NoCompression. |
| 9 | +/// </summary> |
| 10 | +static class ZipStorer |
| 11 | +{ |
| 12 | + public static void RewriteAsStored(MemoryStream source, Stream target) |
| 13 | + { |
| 14 | + source.Position = 0; |
| 15 | + using var archive = new ZipArchive(source, ZipArchiveMode.Read, leaveOpen: true); |
| 16 | + |
| 17 | + using var writer = new BinaryWriter(target, Encoding.UTF8, leaveOpen: true); |
| 18 | + var entries = new List<(string name, byte[] data, uint crc, long headerOffset)>(); |
| 19 | + |
| 20 | + foreach (var entry in archive.Entries.OrderBy(_ => _.FullName, StringComparer.Ordinal)) |
| 21 | + { |
| 22 | + var headerOffset = target.Position; |
| 23 | + |
| 24 | + byte[] data; |
| 25 | + using (var entryStream = entry.Open()) |
| 26 | + using (var ms = new MemoryStream()) |
| 27 | + { |
| 28 | + entryStream.CopyTo(ms); |
| 29 | + data = ms.ToArray(); |
| 30 | + } |
| 31 | + |
| 32 | + var crc = Crc32(data); |
| 33 | + var nameBytes = Encoding.UTF8.GetBytes(entry.FullName); |
| 34 | + |
| 35 | + // Local file header |
| 36 | + writer.Write(0x04034b50u); // signature |
| 37 | + writer.Write((ushort)20); // version needed |
| 38 | + writer.Write((ushort)0); // general purpose flags |
| 39 | + writer.Write((ushort)0); // compression method: Stored |
| 40 | + writer.Write(DosTime); // last mod time |
| 41 | + writer.Write(DosDate); // last mod date |
| 42 | + writer.Write(crc); // crc-32 |
| 43 | + writer.Write((uint)data.Length); // compressed size |
| 44 | + writer.Write((uint)data.Length); // uncompressed size |
| 45 | + writer.Write((ushort)nameBytes.Length); // file name length |
| 46 | + writer.Write((ushort)0); // extra field length |
| 47 | + writer.Write(nameBytes); |
| 48 | + writer.Write(data); |
| 49 | + |
| 50 | + entries.Add((entry.FullName, data, crc, headerOffset)); |
| 51 | + } |
| 52 | + |
| 53 | + var centralStart = target.Position; |
| 54 | + |
| 55 | + foreach (var (name, data, crc, headerOffset) in entries) |
| 56 | + { |
| 57 | + var nameBytes = Encoding.UTF8.GetBytes(name); |
| 58 | + |
| 59 | + // Central directory header |
| 60 | + writer.Write(0x02014b50u); // signature |
| 61 | + writer.Write((ushort)20); // version made by |
| 62 | + writer.Write((ushort)20); // version needed |
| 63 | + writer.Write((ushort)0); // general purpose flags |
| 64 | + writer.Write((ushort)0); // compression method: Stored |
| 65 | + writer.Write(DosTime); // last mod time |
| 66 | + writer.Write(DosDate); // last mod date |
| 67 | + writer.Write(crc); // crc-32 |
| 68 | + writer.Write((uint)data.Length); // compressed size |
| 69 | + writer.Write((uint)data.Length); // uncompressed size |
| 70 | + writer.Write((ushort)nameBytes.Length); // file name length |
| 71 | + writer.Write((ushort)0); // extra field length |
| 72 | + writer.Write((ushort)0); // file comment length |
| 73 | + writer.Write((ushort)0); // disk number start |
| 74 | + writer.Write((ushort)0); // internal file attributes |
| 75 | + writer.Write(0u); // external file attributes |
| 76 | + writer.Write((uint)headerOffset); // relative offset of local header |
| 77 | + writer.Write(nameBytes); |
| 78 | + } |
| 79 | + |
| 80 | + var centralEnd = target.Position; |
| 81 | + var centralSize = centralEnd - centralStart; |
| 82 | + |
| 83 | + // End of central directory record |
| 84 | + writer.Write(0x06054b50u); // signature |
| 85 | + writer.Write((ushort)0); // disk number |
| 86 | + writer.Write((ushort)0); // disk with central directory |
| 87 | + writer.Write((ushort)entries.Count); // entries on this disk |
| 88 | + writer.Write((ushort)entries.Count); // total entries |
| 89 | + writer.Write((uint)centralSize); // central directory size |
| 90 | + writer.Write((uint)centralStart); // central directory offset |
| 91 | + writer.Write((ushort)0); // comment length |
| 92 | + } |
| 93 | + |
| 94 | + // DOS date/time for 2020-01-01 00:00:00 |
| 95 | + const ushort DosTime = 0; |
| 96 | + const ushort DosDate = (40 << 9) | (1 << 5) | 1; // year=2020-1980=40, month=1, day=1 |
| 97 | + |
| 98 | + static uint Crc32(byte[] data) |
| 99 | + { |
| 100 | + var crc = 0xFFFFFFFFu; |
| 101 | + foreach (var b in data) |
| 102 | + { |
| 103 | + crc ^= b; |
| 104 | + for (var i = 0; i < 8; i++) |
| 105 | + { |
| 106 | + crc = (crc >> 1) ^ (0xEDB88320u & ~((crc & 1) - 1)); |
| 107 | + } |
| 108 | + } |
| 109 | + |
| 110 | + return ~crc; |
| 111 | + } |
| 112 | +} |
0 commit comments