// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System.Buffers.Binary; using System.IO.Compression; using System.Numerics; using System.Text; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Exr.Constants; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.Memory; namespace SixLabors.ImageSharp.Tests.Formats.Exr; [Trait("Format", "Exr")] [ValidateDisposedMemoryAllocations] public class ExrZipDecoderTests { /// /// Incomplete and oversized blocks are rejected unless image-data recovery is enabled. /// /// The inflated payload length. /// The image-data integrity policy. [Theory] [InlineData(0, SegmentIntegrityHandling.Strict)] [InlineData(8, SegmentIntegrityHandling.Strict)] [InlineData(1025, SegmentIntegrityHandling.Strict)] [InlineData(0, SegmentIntegrityHandling.IgnoreAncillary)] [InlineData(8, SegmentIntegrityHandling.IgnoreAncillary)] [InlineData(1025, SegmentIntegrityHandling.IgnoreAncillary)] public void Decode_InvalidInflatedBlock_Throws(int length, SegmentIntegrityHandling integrity) { byte[] data = BuildExr(ZlibCompress(new byte[length]), ExrPixelType.Float, 2, 0); DecoderOptions options = new() { SegmentIntegrityHandling = integrity }; Assert.Throws(() => Image.Load(options, data)); } /// /// Recovering an invalid image-data block must not expose partially decoded or pooled bytes. /// /// The stored sample type. /// The inflated payload length. [Theory] [InlineData(ExrPixelType.Half, 0)] [InlineData(ExrPixelType.Half, 8)] [InlineData(ExrPixelType.Half, 1025)] [InlineData(ExrPixelType.Float, 0)] [InlineData(ExrPixelType.Float, 8)] [InlineData(ExrPixelType.Float, 1025)] [InlineData(ExrPixelType.UnsignedInt, 0)] [InlineData(ExrPixelType.UnsignedInt, 8)] [InlineData(ExrPixelType.UnsignedInt, 1025)] public void Decode_InvalidInflatedBlock_IgnoreImageData_ClearsPixels(ExrPixelType pixelType, int length) { byte[] data = BuildExr(ZlibCompress(new byte[length]), pixelType, 2, 0); Configuration configuration = Configuration.Default.Clone(); configuration.MemoryAllocator = new TestMemoryAllocator(0x3F); DecoderOptions options = new() { Configuration = configuration, SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreImageData }; using Image image = Image.Load(options, data); Assert.Equal(new Size(256, 1), image.Size); for (int x = 0; x < image.Width; x++) { Assert.Equal(new Vector4(0, 0, 0, 1), image[x, 0].ToVector4()); } } /// /// Missing or truncated zlib headers obey the image-data integrity policy. /// /// The number of available zlib header bytes. /// The image-data integrity policy. [Theory] [InlineData(0, SegmentIntegrityHandling.Strict)] [InlineData(1, SegmentIntegrityHandling.Strict)] [InlineData(0, SegmentIntegrityHandling.IgnoreAncillary)] [InlineData(1, SegmentIntegrityHandling.IgnoreAncillary)] [InlineData(0, SegmentIntegrityHandling.IgnoreImageData)] [InlineData(1, SegmentIntegrityHandling.IgnoreImageData)] public void Decode_IncompleteZlibHeader_RespectsIntegrityHandling(int length, SegmentIntegrityHandling integrity) { byte[] header = [0x78, 0x9C]; byte[] data = BuildExr(header[..length], ExrPixelType.Float, 2, 0); Configuration configuration = Configuration.Default.Clone(); configuration.MemoryAllocator = new TestMemoryAllocator(0x3F); DecoderOptions options = new() { Configuration = configuration, SegmentIntegrityHandling = integrity }; if (integrity == SegmentIntegrityHandling.IgnoreImageData) { using Image image = Image.Load(options, data); Assert.Equal(new Size(256, 1), image.Size); for (int x = 0; x < image.Width; x++) { Assert.Equal(new Vector4(0, 0, 0, 1), image[x, 0].ToVector4()); } } else { Assert.Throws(() => Image.Load(options, data)); } } /// /// Missing color channels must not inherit the allocator's previous contents. /// /// The stored sample type. /// The ZIP compression code. /// The data window's first row coordinate. [Theory] [InlineData(ExrPixelType.Half, 2, 0)] [InlineData(ExrPixelType.Float, 2, 0)] [InlineData(ExrPixelType.UnsignedInt, 2, 0)] [InlineData(ExrPixelType.Half, 3, 0)] [InlineData(ExrPixelType.Float, 3, 0)] [InlineData(ExrPixelType.UnsignedInt, 3, 0)] [InlineData(ExrPixelType.Half, 3, -10)] [InlineData(ExrPixelType.Float, 3, -10)] [InlineData(ExrPixelType.UnsignedInt, 3, -10)] [InlineData(ExrPixelType.Half, 3, 10)] [InlineData(ExrPixelType.Float, 3, 10)] [InlineData(ExrPixelType.UnsignedInt, 3, 10)] public void Decode_SingleRedChannel_InitializesMissingColorChannels(ExrPixelType pixelType, byte compression, int yMin) { byte[] predicted = new byte[256 * (pixelType == ExrPixelType.Half ? 2 : 4)]; // A zero first byte followed by 128-valued differences reconstructs an all-zero sample plane. predicted.AsSpan(1).Fill(128); byte[] data = BuildExr(ZlibCompress(predicted), pixelType, compression, yMin); Configuration configuration = Configuration.Default.Clone(); configuration.MemoryAllocator = new TestMemoryAllocator(0x3F); DecoderOptions options = new() { Configuration = configuration }; using Image image = Image.Load(options, data); Assert.Equal(new Size(256, 1), image.Size); for (int x = 0; x < image.Width; x++) { Assert.Equal(new Vector4(0, 0, 0, 1), image[x, 0].ToVector4()); } } /// /// Compresses the predictor bytes for a scanline block. /// /// The predictor bytes. /// The zlib stream. private static byte[] ZlibCompress(byte[] data) { if (data.Length == 0) { // An empty write produces no output on some runtimes. Use a complete zlib stream // containing an empty final DEFLATE block and Adler-32 checksum instead. return [0x78, 0x9C, 0x03, 0x00, 0x00, 0x00, 0x00, 0x01]; } using MemoryStream output = new(); using (ZLibStream zlib = new(output, CompressionLevel.Optimal, leaveOpen: true)) { zlib.Write(data); } return output.ToArray(); } /// /// Builds a single-row EXR containing only the red channel. /// /// The compressed scanline bytes. /// The stored sample type. /// The ZIP compression code. /// The data window's first row coordinate. /// The encoded image. private static byte[] BuildExr(byte[] compressed, ExrPixelType pixelType, byte compression, int yMin) { const int width = 256; const int height = 1; using MemoryStream output = new(); using BinaryWriter writer = new(output); writer.Write(new byte[] { 0x76, 0x2F, 0x31, 0x01 }); writer.Write((byte)2); writer.Write(new byte[] { 0, 0, 0 }); using (MemoryStream channelStream = new()) using (BinaryWriter channelWriter = new(channelStream)) { WriteString(channelWriter, "R"); channelWriter.Write((int)pixelType); channelWriter.Write((byte)0); channelWriter.Write(new byte[] { 0, 0, 0 }); channelWriter.Write(1); channelWriter.Write(1); channelWriter.Write((byte)0); WriteAttribute(writer, "channels", "chlist", channelStream.ToArray()); } WriteAttribute(writer, "compression", "compression", [compression]); using (MemoryStream boxStream = new()) using (BinaryWriter boxWriter = new(boxStream)) { boxWriter.Write(0); boxWriter.Write(yMin); boxWriter.Write(width - 1); boxWriter.Write(yMin + height - 1); byte[] box = boxStream.ToArray(); WriteAttribute(writer, "dataWindow", "box2i", box); WriteAttribute(writer, "displayWindow", "box2i", box); } WriteAttribute(writer, "lineOrder", "lineOrder", [0]); byte[] one = new byte[4]; BinaryPrimitives.WriteSingleLittleEndian(one, 1F); WriteAttribute(writer, "pixelAspectRatio", "float", one); WriteAttribute(writer, "screenWindowCenter", "v2f", new byte[8]); WriteAttribute(writer, "screenWindowWidth", "float", one); writer.Write((byte)0); long chunkStart = output.Position + sizeof(ulong); writer.Write((ulong)chunkStart); writer.Write(yMin); writer.Write((uint)compressed.Length); writer.Write(compressed); return output.ToArray(); } private static void WriteString(BinaryWriter writer, string value) { writer.Write(Encoding.ASCII.GetBytes(value)); writer.Write((byte)0); } private static void WriteAttribute(BinaryWriter writer, string name, string type, byte[] value) { WriteString(writer, name); WriteString(writer, type); writer.Write(value.Length); writer.Write(value); } }