Skip to content

Commit f944918

Browse files
authored
Merge pull request #40 from SimonCropp/PngNormalizer
Normalize pngs
2 parents 1bd09d2 + 674f0a5 commit f944918

22 files changed

Lines changed: 448 additions & 13 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#L144-L149' 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#L160-L165' 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#L144-L149' 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#L160-L165' title='Snippet source file'>snippet source</a> | <a href='#snippet-ConvertAsync' title='Start of snippet'>anchor</a></sup>
6060
<!-- endSnippet -->
6161

6262

src/DeterministicIoPackaging/DeterministicIoPackaging.csproj

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44
</PropertyGroup>
55
<ItemGroup>
66
<PackageReference Include="Polyfill" PrivateAssets="all" />
7+
<PackageReference Include="System.IO.Hashing" />
78
<PackageReference Include="ProjectDefaults" PrivateAssets="all" />
89
<PackageReference Include="Microsoft.Sbom.Targets" PrivateAssets="all" Condition="'$(CI)' == 'true'" />
910
</ItemGroup>
1011
<ItemGroup Condition="'$(TargetFramework)' == 'net472' OR '$(TargetFramework)' == 'net48'">
12+
<PackageReference Include="System.Threading.Tasks.Extensions" />
1113
<PackageReference Include="System.Buffers" />
1214
<PackageReference Include="System.IO.Compression" />
1315
<PackageReference Include="System.Memory" />
1416
</ItemGroup>
15-
</Project>
17+
</Project>

src/DeterministicIoPackaging/DeterministicPackage.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ static void DuplicateEntry(Entry sourceEntry, Archive targetArchive)
5454
return;
5555
}
5656

57+
if (sourceEntry.FullName.EndsWith(".png", StringComparison.OrdinalIgnoreCase))
58+
{
59+
PngNormalizer.Normalize(sourceStream, targetStream);
60+
return;
61+
}
62+
5763
sourceStream.CopyTo(targetStream);
5864
}
5965

@@ -80,6 +86,12 @@ static async Task DuplicateEntryAsync(Entry sourceEntry, Archive targetArchive,
8086
return;
8187
}
8288

89+
if (sourceEntry.FullName.EndsWith(".png", StringComparison.OrdinalIgnoreCase))
90+
{
91+
await PngNormalizer.NormalizeAsync(sourceStream, targetStream, cancel);
92+
return;
93+
}
94+
8395
await sourceStream.CopyToAsync(targetStream, cancel);
8496
}
8597

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
using System.Buffers.Binary;
2+
using System.IO.Hashing;
3+
4+
namespace DeterministicIoPackaging;
5+
6+
static class PngNormalizer
7+
{
8+
static readonly byte[] pngSignature = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
9+
static readonly byte[] idatType = "IDAT"u8.ToArray();
10+
11+
public static void Normalize(Stream source, Stream target)
12+
{
13+
var header = new byte[8];
14+
source.ReadExactly(header, 0, 8);
15+
target.Write(pngSignature);
16+
17+
using var idatData = new MemoryStream();
18+
var flushedIdat = false;
19+
20+
while (true)
21+
{
22+
source.ReadExactly(header, 0, 8);
23+
var chunkLength = BinaryPrimitives.ReadInt32BigEndian(header.AsSpan(0, 4));
24+
25+
var body = new byte[chunkLength + 4];
26+
source.ReadExactly(body, 0, body.Length);
27+
28+
if (ProcessChunk(header, body, chunkLength, target, idatData, ref flushedIdat))
29+
{
30+
break;
31+
}
32+
}
33+
}
34+
35+
public static async Task NormalizeAsync(Stream source, Stream target, Cancel cancel)
36+
{
37+
var header = new byte[8];
38+
await source.ReadExactlyAsync(header, 0, 8, cancel);
39+
target.Write(pngSignature);
40+
41+
using var idatData = new MemoryStream();
42+
var flushedIdat = false;
43+
44+
while (true)
45+
{
46+
await source.ReadExactlyAsync(header, 0, 8, cancel);
47+
var chunkLength = BinaryPrimitives.ReadInt32BigEndian(header.AsSpan(0, 4));
48+
49+
var body = new byte[chunkLength + 4];
50+
await source.ReadExactlyAsync(body, 0, body.Length, cancel);
51+
52+
if (ProcessChunk(header, body, chunkLength, target, idatData, ref flushedIdat))
53+
{
54+
break;
55+
}
56+
}
57+
}
58+
59+
static bool ProcessChunk(byte[] header, byte[] body, int chunkLength, Stream target, MemoryStream idatData, ref bool flushedIdat)
60+
{
61+
var isIdat = header[4] == 'I' && header[5] == 'D' &&
62+
header[6] == 'A' && header[7] == 'T';
63+
var isIend = header[4] == 'I' && header[5] == 'E' &&
64+
header[6] == 'N' && header[7] == 'D';
65+
66+
if (isIdat)
67+
{
68+
idatData.Write(body, 0, chunkLength);
69+
}
70+
else
71+
{
72+
if (!flushedIdat && idatData.Length > 0)
73+
{
74+
flushedIdat = true;
75+
WriteNormalizedIdat(target, idatData.GetBuffer(), (int) idatData.Length);
76+
}
77+
78+
target.Write(header);
79+
target.Write(body);
80+
}
81+
82+
return isIend;
83+
}
84+
85+
static void WriteNormalizedIdat(Stream target, byte[] zlibBytes, int zlibLength)
86+
{
87+
using var decompressedStream = new MemoryStream();
88+
using (var zlibInput = new MemoryStream(zlibBytes, 0, zlibLength))
89+
using (var zlibStream = new ZLibStream(zlibInput, CompressionMode.Decompress))
90+
{
91+
zlibStream.CopyTo(decompressedStream);
92+
}
93+
94+
using var compressOutput = new MemoryStream();
95+
using (var zlibStream = new ZLibStream(compressOutput, CompressionLevel.Optimal, leaveOpen: true))
96+
{
97+
zlibStream.Write(decompressedStream.GetBuffer(), 0, (int) decompressedStream.Length);
98+
}
99+
100+
var length = (int) compressOutput.Length;
101+
var header = new byte[4];
102+
BinaryPrimitives.WriteInt32BigEndian(header, length);
103+
target.Write(header);
104+
target.Write(idatType);
105+
target.Write(compressOutput.GetBuffer(), 0, length);
106+
107+
var crc = new Crc32();
108+
crc.Append(idatType);
109+
crc.Append(compressOutput.GetBuffer().AsSpan(0, length));
110+
var crcBytes = new byte[4];
111+
BinaryPrimitives.WriteUInt32BigEndian(crcBytes, crc.GetCurrentHashAsUInt32());
112+
target.Write(crcBytes);
113+
}
114+
}

src/Directory.Build.props

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<Project>
33
<PropertyGroup>
44
<NoWarn>CS1591;CS0649;CA1416;NU1608;NU1109;NU1510</NoWarn>
5-
<Version>0.15.0</Version>
5+
<Version>0.16.0</Version>
66
<LangVersion>preview</LangVersion>
77
<AssemblyVersion>1.0.0</AssemblyVersion>
88
<Description>Modify System.IO.Packaging (https://learn.microsoft.com/en-us/dotnet/api/system.io.packaging) files to ensure they are deterministic. Helpful for testing, build reproducibility, security verification, and ensuring package integrity across different build environments.</Description>
@@ -16,4 +16,4 @@
1616
<Using Include="System.IO.Compression.ZipArchiveEntry" Alias="Entry" />
1717
<Using Include="System.IO.Compression.ZipArchive" Alias="Archive" />
1818
</ItemGroup>
19-
</Project>
19+
</Project>

src/Directory.Packages.props

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@
88
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
99
<PackageVersion Include="NUnit" Version="4.5.0" />
1010
<PackageVersion Include="NUnit3TestAdapter" Version="6.1.0" />
11-
<PackageVersion Include="Polyfill" Version="9.13.0" />
11+
<PackageVersion Include="Polyfill" Version="9.15.0" />
1212
<PackageVersion Include="ProjectDefaults" Version="1.0.172" />
1313
<PackageVersion Include="ProjectFiles" Version="1.0.0" />
1414
<PackageVersion Include="System.IO.Compression" Version="4.3.0" />
15+
<PackageVersion Include="System.IO.Hashing" Version="9.0.4" />
1516
<PackageVersion Include="System.Memory" Version="4.6.3" />
1617
<PackageVersion Include="System.Buffers" Version="4.6.1" />
18+
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
1719
<PackageVersion Include="Verify" Version="31.13.2" />
1820
<PackageVersion Include="Verify.DiffPlex" Version="3.1.2" />
1921
<PackageVersion Include="Verify.NUnit" Version="31.13.2" />
@@ -23,4 +25,4 @@
2325
<PackageVersion Include="EmptyFiles" Version="8.17.2" />
2426
<PackageVersion Include="SimpleInfoName" Version="3.2.0" />
2527
</ItemGroup>
26-
</Project>
28+
</Project>

src/Tests/GlobalUsings.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
11
global using System.Xml.Linq;
2-
global using DeterministicIoPackaging;
2+
global using DeterministicIoPackaging;
3+
global using System.Buffers.Binary;
4+
global using System.IO.Compression;
5+
global using System.IO.Hashing;
6+
global using Polyfills;

0 commit comments

Comments
 (0)