Skip to content

Commit bcab5fd

Browse files
authored
Handle eof and missing ihdr in png decoder (#1718)
* Handle EOF and missing IHDR in PNG decoder Replace ReadExact with ReadAtLeast to detect EOF and break out of the chunk loop cleanly for truncated/streaming inputs. Stop processing if a chunk length is too large (break instead of throwing). Add an explicit check that IHDR was seen and throw if missing. Rewind idat.Position before calling Reconstruct so the IDAT stream is read from the start. * Update Directory.Build.props * .
1 parent 11a29fd commit bcab5fd

4 files changed

Lines changed: 106 additions & 3 deletions

File tree

src/Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<Project>
33
<PropertyGroup>
44
<NoWarn>CA1822;CS1591;CS0649;xUnit1026;xUnit1013;CS1573;VerifyTestsProjectDir;VerifySetParameters;PolyFillTargetsForNuget;xUnit1051;NU1608;NU1109</NoWarn>
5-
<Version>31.16.0</Version>
5+
<Version>31.16.1</Version>
66
<ImplicitUsings>enable</ImplicitUsings>
77
<LangVersion>preview</LangVersion>
88
<AssemblyVersion>1.0.0</AssemblyVersion>

src/Verify.Tests/Compare/Png/PngDecoderTests.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,52 @@ public void Large_256x256()
144144
Assert.Equal(rgba, image.Rgba);
145145
}
146146

147+
[Fact]
148+
public void Small_GrayAlpha()
149+
{
150+
const int width = 4;
151+
const int height = 4;
152+
var ga = new byte[width * height * 2];
153+
for (var i = 0; i < width * height; i++)
154+
{
155+
ga[i * 2] = (byte)(i * 16);
156+
ga[i * 2 + 1] = (byte)(255 - i * 8);
157+
}
158+
159+
var png = PngTestHelper.EncodeGrayAlpha(width, height, ga);
160+
var image = PngDecoder.Decode(new MemoryStream(png));
161+
Assert.Equal(width, image.Width);
162+
Assert.Equal(height, image.Height);
163+
for (var i = 0; i < width * height; i++)
164+
{
165+
var g = ga[i * 2];
166+
Assert.Equal(g, image.Rgba[i * 4]);
167+
Assert.Equal(g, image.Rgba[i * 4 + 1]);
168+
Assert.Equal(g, image.Rgba[i * 4 + 2]);
169+
Assert.Equal(ga[i * 2 + 1], image.Rgba[i * 4 + 3]);
170+
}
171+
}
172+
173+
[Fact]
174+
public void Multiple_Idat_Chunks()
175+
{
176+
const int width = 32;
177+
const int height = 32;
178+
var rgba = new byte[width * height * 4];
179+
new Random(7).NextBytes(rgba);
180+
var png = PngTestHelper.EncodeRgbaMultipleIdat(width, height, rgba, chunkSize: 64);
181+
var image = PngDecoder.Decode(new MemoryStream(png));
182+
Assert.Equal(rgba, image.Rgba);
183+
}
184+
185+
[Fact]
186+
public void Rejects_Missing_Idat()
187+
{
188+
var png = PngTestHelper.EncodeWithoutIdat(1, 1);
189+
var exception = Assert.Throws<Exception>(() => PngDecoder.Decode(new MemoryStream(png)));
190+
Assert.Contains("IDAT", exception.Message);
191+
}
192+
147193
[Fact]
148194
public void Rejects_Bad_Signature()
149195
{

src/Verify.Tests/Compare/Png/PngTestHelper.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ public static byte[] EncodeGray(int width, int height, byte[] gray)
2929
return BuildPng(ihdr, null, null, raw);
3030
}
3131

32+
public static byte[] EncodeGrayAlpha(int width, int height, byte[] grayAlpha)
33+
{
34+
var raw = AddFilterBytes(grayAlpha, width * 2, height);
35+
var ihdr = BuildIhdr(width, height, colorType: 4);
36+
return BuildPng(ihdr, null, null, raw);
37+
}
38+
3239
public static byte[] EncodePaletted(int width, int height, byte[] indices, byte[] palette, byte[]? trns = null)
3340
{
3441
var raw = AddFilterBytes(indices, width, height);
@@ -61,6 +68,34 @@ static byte[] BuildIhdr(int width, int height, byte colorType)
6168
return data;
6269
}
6370

71+
public static byte[] EncodeWithoutIdat(int width, int height)
72+
{
73+
var ihdr = BuildIhdr(width, height, colorType: 6);
74+
using var stream = new MemoryStream();
75+
stream.Write(signature, 0, signature.Length);
76+
WriteChunk(stream, "IHDR", ihdr);
77+
WriteChunk(stream, "IEND", []);
78+
return stream.ToArray();
79+
}
80+
81+
public static byte[] EncodeRgbaMultipleIdat(int width, int height, byte[] rgba, int chunkSize)
82+
{
83+
var raw = AddFilterBytes(rgba, width * 4, height);
84+
var compressed = ZlibCompress(raw);
85+
var ihdr = BuildIhdr(width, height, colorType: 6);
86+
using var stream = new MemoryStream();
87+
stream.Write(signature, 0, signature.Length);
88+
WriteChunk(stream, "IHDR", ihdr);
89+
for (var offset = 0; offset < compressed.Length; offset += chunkSize)
90+
{
91+
var length = Math.Min(chunkSize, compressed.Length - offset);
92+
WriteChunk(stream, "IDAT", compressed.AsSpan(offset, length).ToArray());
93+
}
94+
95+
WriteChunk(stream, "IEND", []);
96+
return stream.ToArray();
97+
}
98+
6499
static byte[] BuildPng(byte[] ihdr, byte[]? plte, byte[]? trns, byte[] raw)
65100
{
66101
var compressed = ZlibCompress(raw);

src/Verify/Compare/Png/PngDecoder.cs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,17 @@ public static PngImage Decode(Stream stream)
3131

3232
while (true)
3333
{
34-
ReadExact(stream, header);
34+
if (stream.ReadAtLeast(header, header.Length, throwOnEndOfStream: false) < header.Length)
35+
{
36+
break;
37+
}
38+
3539
var length = ReadUInt32BigEndian(header);
3640
var type = ((uint)header[4] << 24) | ((uint)header[5] << 16) | ((uint)header[6] << 8) | header[7];
3741

3842
if (length > int.MaxValue)
3943
{
40-
throw new("PNG chunk too large.");
44+
break;
4145
}
4246

4347
var intLength = (int)length;
@@ -106,6 +110,11 @@ public static PngImage Decode(Stream stream)
106110
throw new("PNG missing IHDR.");
107111
}
108112

113+
if (idat.Length == 0)
114+
{
115+
throw new("PNG missing IDAT.");
116+
}
117+
109118
ReadExact(stream, crc);
110119
idat.Position = 0;
111120
return Reconstruct(idat, width, height, colorType, palette, transparency);
@@ -118,6 +127,19 @@ public static PngImage Decode(Stream stream)
118127

119128
ReadExact(stream, crc);
120129
}
130+
131+
if (!seenIhdr)
132+
{
133+
throw new("PNG missing IHDR.");
134+
}
135+
136+
if (idat.Length == 0)
137+
{
138+
throw new("PNG missing IDAT.");
139+
}
140+
141+
idat.Position = 0;
142+
return Reconstruct(idat, width, height, colorType, palette, transparency);
121143
}
122144

123145
static PngImage Reconstruct(Stream idat, int width, int height, byte colorType, byte[]? palette, byte[]? trns)

0 commit comments

Comments
 (0)