Skip to content

Commit 5773021

Browse files
authored
Raw zip writer (#44)
1 parent f4bb6d3 commit 5773021

22 files changed

Lines changed: 146 additions & 10 deletions

readme.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ Example file formats that leverage System.IO.Packaging
4444
using var sourceStream = File.OpenRead(packagePath);
4545
await DeterministicPackage.ConvertAsync(sourceStream, targetStream);
4646
```
47-
<sup><a href='/src/Tests/Tests.cs#L160-L165' title='Snippet source file'>snippet source</a> | <a href='#snippet-ConvertAsync' title='Start of snippet'>anchor</a></sup>
47+
<sup><a href='/src/Tests/Tests.cs#L174-L179' title='Snippet source file'>snippet source</a> | <a href='#snippet-ConvertAsync' title='Start of snippet'>anchor</a></sup>
4848
<!-- endSnippet -->
4949

5050

@@ -56,7 +56,7 @@ await DeterministicPackage.ConvertAsync(sourceStream, targetStream);
5656
using var sourceStream = File.OpenRead(packagePath);
5757
await DeterministicPackage.ConvertAsync(sourceStream, targetStream);
5858
```
59-
<sup><a href='/src/Tests/Tests.cs#L160-L165' title='Snippet source file'>snippet source</a> | <a href='#snippet-ConvertAsync' title='Start of snippet'>anchor</a></sup>
59+
<sup><a href='/src/Tests/Tests.cs#L174-L179' title='Snippet source file'>snippet source</a> | <a href='#snippet-ConvertAsync' title='Start of snippet'>anchor</a></sup>
6060
<!-- endSnippet -->
6161

6262

src/DeterministicIoPackaging/DeterministicPackage_Convert.cs

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,22 +20,32 @@ public static async Task<MemoryStream> ConvertAsync(Stream source)
2020

2121
public static void Convert(Stream source, Stream target)
2222
{
23-
using var sourceArchive = ReadArchive(source);
24-
using var targetArchive = CreateArchive(target);
25-
foreach (var sourceEntry in sourceArchive.OrderedEntries())
23+
var intermediate = new MemoryStream();
24+
using (var sourceArchive = ReadArchive(source))
25+
using (var targetArchive = CreateArchive(intermediate))
2626
{
27-
DuplicateEntry(sourceEntry, targetArchive);
27+
foreach (var sourceEntry in sourceArchive.OrderedEntries())
28+
{
29+
DuplicateEntry(sourceEntry, targetArchive);
30+
}
2831
}
32+
33+
ZipStorer.RewriteAsStored(intermediate, target);
2934
}
3035

3136
public static async Task ConvertAsync(Stream source, Stream target, Cancel token = default)
3237
{
33-
using var sourceArchive = ReadArchive(source);
34-
using var targetArchive = CreateArchive(target);
35-
foreach (var sourceEntry in OrderedEntries(sourceArchive))
38+
var intermediate = new MemoryStream();
39+
using (var sourceArchive = ReadArchive(source))
40+
using (var targetArchive = CreateArchive(intermediate))
3641
{
37-
await DuplicateEntryAsync(sourceEntry, targetArchive, token);
42+
foreach (var sourceEntry in OrderedEntries(sourceArchive))
43+
{
44+
await DuplicateEntryAsync(sourceEntry, targetArchive, token);
45+
}
3846
}
47+
48+
ZipStorer.RewriteAsStored(intermediate, target);
3949
}
4050

4151
private static IOrderedEnumerable<Entry> OrderedEntries(this Archive archive) =>
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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 Bytes
Binary file not shown.
-65 Bytes
Binary file not shown.
0 Bytes
Binary file not shown.
-90 Bytes
Binary file not shown.
Binary file not shown.
-60 Bytes
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)