From cbf7099279d3a168030c210f5f27d31b189f890f Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Thu, 16 Apr 2026 17:20:53 +1000 Subject: [PATCH 1/3] 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. --- src/Verify/Compare/Png/PngDecoder.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Verify/Compare/Png/PngDecoder.cs b/src/Verify/Compare/Png/PngDecoder.cs index 1385ece9b..ebecedddd 100644 --- a/src/Verify/Compare/Png/PngDecoder.cs +++ b/src/Verify/Compare/Png/PngDecoder.cs @@ -31,13 +31,17 @@ public static PngImage Decode(Stream stream) while (true) { - ReadExact(stream, header); + if (stream.ReadAtLeast(header, header.Length, throwOnEndOfStream: false) < header.Length) + { + break; + } + var length = ReadUInt32BigEndian(header); var type = ((uint)header[4] << 24) | ((uint)header[5] << 16) | ((uint)header[6] << 8) | header[7]; if (length > int.MaxValue) { - throw new("PNG chunk too large."); + break; } var intLength = (int)length; @@ -118,6 +122,14 @@ public static PngImage Decode(Stream stream) ReadExact(stream, crc); } + + if (!seenIhdr) + { + throw new("PNG missing IHDR."); + } + + idat.Position = 0; + return Reconstruct(idat, width, height, colorType, palette, transparency); } static PngImage Reconstruct(Stream idat, int width, int height, byte colorType, byte[]? palette, byte[]? trns) From 261ab3200c5c3faa4ee251cf3ebdca387738bdca Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Thu, 16 Apr 2026 17:21:36 +1000 Subject: [PATCH 2/3] Update Directory.Build.props --- src/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index c49e7686c..0a08cdf12 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,7 +2,7 @@ CA1822;CS1591;CS0649;xUnit1026;xUnit1013;CS1573;VerifyTestsProjectDir;VerifySetParameters;PolyFillTargetsForNuget;xUnit1051;NU1608;NU1109 - 31.16.0 + 31.16.1 enable preview 1.0.0 From 9fb7539a33fbff5f646eca0d75e8a9a237f203d9 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Thu, 16 Apr 2026 17:29:42 +1000 Subject: [PATCH 3/3] . --- .../Compare/Png/PngDecoderTests.cs | 46 +++++++++++++++++++ src/Verify.Tests/Compare/Png/PngTestHelper.cs | 35 ++++++++++++++ src/Verify/Compare/Png/PngDecoder.cs | 10 ++++ 3 files changed, 91 insertions(+) diff --git a/src/Verify.Tests/Compare/Png/PngDecoderTests.cs b/src/Verify.Tests/Compare/Png/PngDecoderTests.cs index 56fff801e..675b5297c 100644 --- a/src/Verify.Tests/Compare/Png/PngDecoderTests.cs +++ b/src/Verify.Tests/Compare/Png/PngDecoderTests.cs @@ -144,6 +144,52 @@ public void Large_256x256() Assert.Equal(rgba, image.Rgba); } + [Fact] + public void Small_GrayAlpha() + { + const int width = 4; + const int height = 4; + var ga = new byte[width * height * 2]; + for (var i = 0; i < width * height; i++) + { + ga[i * 2] = (byte)(i * 16); + ga[i * 2 + 1] = (byte)(255 - i * 8); + } + + var png = PngTestHelper.EncodeGrayAlpha(width, height, ga); + var image = PngDecoder.Decode(new MemoryStream(png)); + Assert.Equal(width, image.Width); + Assert.Equal(height, image.Height); + for (var i = 0; i < width * height; i++) + { + var g = ga[i * 2]; + Assert.Equal(g, image.Rgba[i * 4]); + Assert.Equal(g, image.Rgba[i * 4 + 1]); + Assert.Equal(g, image.Rgba[i * 4 + 2]); + Assert.Equal(ga[i * 2 + 1], image.Rgba[i * 4 + 3]); + } + } + + [Fact] + public void Multiple_Idat_Chunks() + { + const int width = 32; + const int height = 32; + var rgba = new byte[width * height * 4]; + new Random(7).NextBytes(rgba); + var png = PngTestHelper.EncodeRgbaMultipleIdat(width, height, rgba, chunkSize: 64); + var image = PngDecoder.Decode(new MemoryStream(png)); + Assert.Equal(rgba, image.Rgba); + } + + [Fact] + public void Rejects_Missing_Idat() + { + var png = PngTestHelper.EncodeWithoutIdat(1, 1); + var exception = Assert.Throws(() => PngDecoder.Decode(new MemoryStream(png))); + Assert.Contains("IDAT", exception.Message); + } + [Fact] public void Rejects_Bad_Signature() { diff --git a/src/Verify.Tests/Compare/Png/PngTestHelper.cs b/src/Verify.Tests/Compare/Png/PngTestHelper.cs index 61252f8bd..4adab1cc8 100644 --- a/src/Verify.Tests/Compare/Png/PngTestHelper.cs +++ b/src/Verify.Tests/Compare/Png/PngTestHelper.cs @@ -29,6 +29,13 @@ public static byte[] EncodeGray(int width, int height, byte[] gray) return BuildPng(ihdr, null, null, raw); } + public static byte[] EncodeGrayAlpha(int width, int height, byte[] grayAlpha) + { + var raw = AddFilterBytes(grayAlpha, width * 2, height); + var ihdr = BuildIhdr(width, height, colorType: 4); + return BuildPng(ihdr, null, null, raw); + } + public static byte[] EncodePaletted(int width, int height, byte[] indices, byte[] palette, byte[]? trns = null) { var raw = AddFilterBytes(indices, width, height); @@ -61,6 +68,34 @@ static byte[] BuildIhdr(int width, int height, byte colorType) return data; } + public static byte[] EncodeWithoutIdat(int width, int height) + { + var ihdr = BuildIhdr(width, height, colorType: 6); + using var stream = new MemoryStream(); + stream.Write(signature, 0, signature.Length); + WriteChunk(stream, "IHDR", ihdr); + WriteChunk(stream, "IEND", []); + return stream.ToArray(); + } + + public static byte[] EncodeRgbaMultipleIdat(int width, int height, byte[] rgba, int chunkSize) + { + var raw = AddFilterBytes(rgba, width * 4, height); + var compressed = ZlibCompress(raw); + var ihdr = BuildIhdr(width, height, colorType: 6); + using var stream = new MemoryStream(); + stream.Write(signature, 0, signature.Length); + WriteChunk(stream, "IHDR", ihdr); + for (var offset = 0; offset < compressed.Length; offset += chunkSize) + { + var length = Math.Min(chunkSize, compressed.Length - offset); + WriteChunk(stream, "IDAT", compressed.AsSpan(offset, length).ToArray()); + } + + WriteChunk(stream, "IEND", []); + return stream.ToArray(); + } + static byte[] BuildPng(byte[] ihdr, byte[]? plte, byte[]? trns, byte[] raw) { var compressed = ZlibCompress(raw); diff --git a/src/Verify/Compare/Png/PngDecoder.cs b/src/Verify/Compare/Png/PngDecoder.cs index ebecedddd..780896814 100644 --- a/src/Verify/Compare/Png/PngDecoder.cs +++ b/src/Verify/Compare/Png/PngDecoder.cs @@ -110,6 +110,11 @@ public static PngImage Decode(Stream stream) throw new("PNG missing IHDR."); } + if (idat.Length == 0) + { + throw new("PNG missing IDAT."); + } + ReadExact(stream, crc); idat.Position = 0; return Reconstruct(idat, width, height, colorType, palette, transparency); @@ -128,6 +133,11 @@ public static PngImage Decode(Stream stream) throw new("PNG missing IHDR."); } + if (idat.Length == 0) + { + throw new("PNG missing IDAT."); + } + idat.Position = 0; return Reconstruct(idat, width, height, colorType, palette, transparency); }