diff --git a/src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs b/src/ImageSharp/Compression/Zlib/ZlibInflateReader.cs similarity index 95% rename from src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs rename to src/ImageSharp/Compression/Zlib/ZlibInflateReader.cs index 11f34dac8..de69717da 100644 --- a/src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs +++ b/src/ImageSharp/Compression/Zlib/ZlibInflateReader.cs @@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Compression.Zlib; /// over the remaining DEFLATE payload. The /// Adler-32 trailer is not validated. /// -internal sealed class ZlibInflateStream : IDisposable +internal sealed class ZlibInflateReader : IDisposable { /// /// Used to read the Adler-32 and Crc-32 checksums. @@ -23,10 +23,10 @@ internal sealed class ZlibInflateStream : IDisposable private readonly ChunkedReadStream segmentStream; - public ZlibInflateStream(BufferedReadStream innerStream) + public ZlibInflateReader(BufferedReadStream innerStream) => this.segmentStream = new ChunkedReadStream(innerStream); - public ZlibInflateStream(BufferedReadStream innerStream, Func getData) + public ZlibInflateReader(BufferedReadStream innerStream, Func getData) => this.segmentStream = new ChunkedReadStream(innerStream, getData); /// diff --git a/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs b/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs index 1b2d95ba5..f598955f4 100644 --- a/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs +++ b/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs @@ -45,7 +45,7 @@ internal abstract class ExrBaseDecompressor : ExrBaseCompression protected static int UndoZipCompression(BufferedReadStream stream, uint compressedBytes, Span uncompressed, uint uncompressedBytes) { long pos = stream.Position; - using ZlibInflateStream inflateStream = new( + using ZlibInflateReader inflateStream = new( stream, () => { diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index f3e2bbdbe..28f9a989b 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -321,7 +321,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore case PngChunkType.End: goto EOF; case PngChunkType.ProprietaryApple: - this.isCgbi = true; + this.ReadCgbiChunk(); break; } } @@ -525,7 +525,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore goto EOF; case PngChunkType.ProprietaryApple: - this.isCgbi = true; + this.ReadCgbiChunk(); break; default: @@ -788,7 +788,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore return; } - using ZlibInflateStream inflateStream = new(this.currentStream, getData); + using ZlibInflateReader inflateStream = new(this.currentStream, getData); if (!inflateStream.AllocateNewBytes(chunkLength, !this.hasImageData)) { return; @@ -1458,6 +1458,21 @@ internal sealed class PngDecoderCore : ImageDecoderCore } } + /// + /// Marks the image as CgBI and validates a header that has already been read. + /// + private void ReadCgbiChunk() + { + this.isCgbi = true; + + // Although Apple's pngcrush normally writes CgBI before IHDR, accepting the + // reverse order must apply the same compatibility validation. + if (!Equals(this.header, default(PngHeader))) + { + ThrowIfInvalidCgbiContent(this.header); + } + } + private static void ThrowIfInvalidCgbiContent(in PngHeader header) { if (header.BitDepth != 8 || (header.ColorType is not PngColorType.Rgb and not PngColorType.RgbWithAlpha)) @@ -1952,7 +1967,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore using MemoryStream memoryStreamOutput = new(compressedData.Length); using UnmanagedMemoryStream memoryStreamInput = new(compressedDataBase, compressedData.Length); using BufferedReadStream bufferedStream = new(this.configuration, memoryStreamInput); - using ZlibInflateStream inflateStream = new(bufferedStream); + using ZlibInflateReader inflateStream = new(bufferedStream); Span destUncompressedData = destBuffer.GetSpan(); if (!inflateStream.AllocateNewBytes(compressedData.Length, false)) diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs index 4e176f28d..d3b65c537 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs @@ -54,7 +54,7 @@ internal sealed class DeflateTiffCompression : TiffBaseDecompressor protected override void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span buffer, CancellationToken cancellationToken) { long pos = stream.Position; - using (ZlibInflateStream deframeStream = new( + using (ZlibInflateReader deframeStream = new( stream, () => { diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs index 2fbbe695e..88ed512d1 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers.Binary; using System.Runtime.Intrinsics.X86; using Microsoft.DotNet.RemoteExecutor; using SixLabors.ImageSharp.Formats; @@ -789,6 +790,60 @@ public partial class PngDecoderTests Assert.Contains("CgBI is only supported for 8-bit truecolor images", ex.Message); } + [Theory] + [InlineData(TestImages.Png.Cgbi.BitDepth16)] + [InlineData(TestImages.Png.Cgbi.Palette)] + public void Identify_CgBI_AfterHeader_IncompatibleHeader_ThrowsInvalidImageContentException(string imagePath) + { + TestFile testFile = TestFile.Create(imagePath); + byte[] reordered = MoveFirstPngChunkAfterSecond(testFile.Bytes); + using MemoryStream stream = new(reordered, false); + + InvalidImageContentException ex = Assert.Throws(() => Image.Identify(stream)); + Assert.Contains("CgBI is only supported for 8-bit truecolor images", ex.Message); + } + + [Theory] + [InlineData(TestImages.Png.Cgbi.BitDepth16)] + [InlineData(TestImages.Png.Cgbi.Palette)] + public void Decode_CgBI_AfterHeader_IncompatibleHeader_ThrowsInvalidImageContentException(string imagePath) + { + TestFile testFile = TestFile.Create(imagePath); + byte[] reordered = MoveFirstPngChunkAfterSecond(testFile.Bytes); + using MemoryStream stream = new(reordered, false); + + InvalidImageContentException ex = Assert.Throws( + () => { using Image image = PngDecoder.Instance.Decode(DecoderOptions.Default, stream); }); + Assert.Contains("CgBI is only supported for 8-bit truecolor images", ex.Message); + } + + /// + /// Moves the first PNG chunk after the second while preserving each chunk's data and CRC. + /// + /// A PNG whose first two chunks should be exchanged. + /// A copy of the PNG with its first two chunks exchanged. + private static byte[] MoveFirstPngChunkAfterSecond(byte[] source) + { + const int signatureLength = 8; + const int chunkOverheadLength = 12; + + int firstChunkLength = BinaryPrimitives.ReadInt32BigEndian(source.AsSpan(signatureLength, 4)) + chunkOverheadLength; + int secondChunkOffset = signatureLength + firstChunkLength; + int secondChunkLength = BinaryPrimitives.ReadInt32BigEndian(source.AsSpan(secondChunkOffset, 4)) + chunkOverheadLength; + byte[] reordered = new byte[source.Length]; + + source.AsSpan(0, signatureLength).CopyTo(reordered); + source.AsSpan(secondChunkOffset, secondChunkLength).CopyTo(reordered.AsSpan(signatureLength)); + source.AsSpan(signatureLength, firstChunkLength).CopyTo(reordered.AsSpan(signatureLength + secondChunkLength)); + + // Chunk CRCs cover only each chunk's type and data, so moving complete chunks + // leaves both checksums valid and isolates ordering as the tested behavior. + source.AsSpan(secondChunkOffset + secondChunkLength) + .CopyTo(reordered.AsSpan(signatureLength + secondChunkLength + firstChunkLength)); + + return reordered; + } + [Theory] [WithFile(TestImages.Png.Splash, PixelTypes.Rgba32)] [WithFile(TestImages.Png.Bike, PixelTypes.Rgba32)]