diff --git a/src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs b/src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs index e7b135dc96..2e58999ed2 100644 --- a/src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs +++ b/src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs @@ -121,13 +121,14 @@ public long WriteHeaderTo(Stream stream) /// Reads the animation frame header. /// /// The stream to read from. + /// The end of the RIFF payload containing this frame. /// Animation frame data. - public static WebpFrameData Parse(BufferedReadStream stream) + public static WebpFrameData Parse(BufferedReadStream stream, long containerEnd) { Span buffer = stackalloc byte[4]; return new WebpFrameData( - dataSize: WebpChunkParsingUtils.ReadChunkSize(stream, buffer), + dataSize: WebpChunkParsingUtils.ReadChunkSize(stream, buffer, containerEnd, 16, false), x: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) * 2, y: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) * 2, width: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) + 1, diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index 323ee30b90..032e8be446 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -100,16 +100,18 @@ public WebpAnimationDecoder( /// The webp features. /// The width of the image. /// The height of the image. - /// The size of the image data in bytes. + /// The metadata already read before the ANIM chunk. + /// The end of the RIFF payload. public ImageInfo Identify( BufferedReadStream stream, WebpFeatures features, uint width, uint height, - uint completeDataSize) + ImageMetadata metadata, + long containerEnd) { List framesMetadata = []; - this.metadata = new ImageMetadata(); + this.metadata = metadata; this.webpMetadata = this.metadata.GetWebpMetadata(); this.webpMetadata.RepeatCount = features.AnimationLoopCount; @@ -120,34 +122,32 @@ public ImageInfo Identify( bool ignoreMetadata = this.skipMetadata; Span buffer = stackalloc byte[4]; uint frameCount = 0; - int remainingBytes = (int)completeDataSize; - while (remainingBytes > 0) + while (Math.Min(containerEnd, stream.Length) - stream.Position >= WebpConstants.ChunkHeaderSize) { WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer); - remainingBytes -= 4; switch (chunkType) { case WebpChunkType.FrameData: ImageFrameMetadata frameMetadata = new(); - uint dataSize = ReadFrameInfo(stream, ref frameMetadata); + ReadFrameInfo(stream, frameMetadata, containerEnd); framesMetadata.Add(frameMetadata); - remainingBytes -= (int)dataSize; + frameCount++; break; case WebpChunkType.Iccp: case WebpChunkType.Xmp: case WebpChunkType.Exif: - this.ReadOptionalChunk(stream, chunkType, this.metadata, ignoreMetadata); + this.ReadOptionalChunk(stream, chunkType, this.metadata, ignoreMetadata, containerEnd); break; default: - // Specification explicitly states to ignore unknown chunks. - // We do not support writing these chunks at present. + // Unknown chunks still consume their payload and padding before the next header. + WebpChunkParsingUtils.SkipChunk(stream, buffer, containerEnd); break; } - if (stream.Position == stream.Length || ++frameCount == this.maxFrames) + if (frameCount == this.maxFrames) { break; } @@ -164,20 +164,22 @@ public ImageInfo Identify( /// The webp features. /// The width of the image. /// The height of the image. - /// The size of the image data in bytes. + /// The metadata already read before the ANIM chunk. + /// The end of the RIFF payload. public Image Decode( BufferedReadStream stream, WebpFeatures features, uint width, uint height, - uint completeDataSize) + ImageMetadata metadata, + long containerEnd) where TPixel : unmanaged, IPixel { Image? image = null; ImageFrame? previousFrame = null; WebpFrameData? prevFrameData = null; - this.metadata = new ImageMetadata(); + this.metadata = metadata; this.webpMetadata = this.metadata.GetWebpMetadata(); this.webpMetadata.RepeatCount = features.AnimationLoopCount; @@ -191,66 +193,75 @@ public Image Decode( bool ignoreMetadata = this.skipMetadata; Span buffer = stackalloc byte[4]; uint frameCount = 0; - int remainingBytes = (int)completeDataSize; - - while (remainingBytes > 0) + try { - WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer); - remainingBytes -= 4; - switch (chunkType) + while (Math.Min(containerEnd, stream.Length) - stream.Position >= WebpConstants.ChunkHeaderSize) { - case WebpChunkType.FrameData: - - uint dataSize = this.ReadFrame( - stream, - ref image, - ref previousFrame, - ref prevFrameData, - width, - height, - backgroundPixel); - - remainingBytes -= (int)dataSize; - break; - case WebpChunkType.Iccp: - case WebpChunkType.Xmp: - case WebpChunkType.Exif: - this.ReadOptionalChunk(stream, chunkType, image!.Metadata, ignoreMetadata); - break; - default: + WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer); + switch (chunkType) + { + case WebpChunkType.FrameData: + + this.ReadFrame( + stream, + ref image, + ref previousFrame, + ref prevFrameData, + width, + height, + backgroundPixel, + containerEnd); + + frameCount++; + break; + case WebpChunkType.Iccp: + case WebpChunkType.Xmp: + case WebpChunkType.Exif: + this.ReadOptionalChunk(stream, chunkType, this.metadata, ignoreMetadata, containerEnd); + break; + default: + + // Unknown chunks still consume their payload and padding before the next header. + WebpChunkParsingUtils.SkipChunk(stream, buffer, containerEnd); + break; + } - // Specification explicitly states to ignore unknown chunks. - // We do not support writing these chunks at present. + if (frameCount == this.maxFrames) + { break; + } } - if (stream.Position == stream.Length || ++frameCount == this.maxFrames) + if (image is null) { - break; + WebpThrowHelper.ThrowInvalidImageContentException("Missing WebP animation frame data."); } - } - return image!; + return image; + } + catch + { + image?.Dispose(); + throw; + } } /// /// Reads frame information from the specified stream and updates the provided frame metadata. /// /// The stream from which to read the frame information. Must support reading and seeking. - /// A reference to the structure that will be updated with the parsed frame metadata. - /// The number of bytes read from the stream while parsing the frame information. - private static uint ReadFrameInfo(BufferedReadStream stream, ref ImageFrameMetadata frameMetadata) + /// The metadata updated from the frame header. + /// The end of the RIFF payload. + private static void ReadFrameInfo(BufferedReadStream stream, ImageFrameMetadata frameMetadata, long containerEnd) { - WebpFrameData frameData = WebpFrameData.Parse(stream); + WebpFrameData frameData = WebpFrameData.Parse(stream, containerEnd); SetFrameMetadata(frameMetadata, frameData); // Size of the frame header chunk. const int chunkHeaderSize = 16; uint remaining = frameData.DataSize - chunkHeaderSize; - stream.Skip((int)remaining); - - return remaining; + stream.Position += remaining + (frameData.DataSize & 1); } /// @@ -264,19 +275,27 @@ private static uint ReadFrameInfo(BufferedReadStream stream, ref ImageFrameMetad /// The width of the image. /// The height of the image. /// The default background color of the canvas in. - /// The number of bytes read from the stream while parsing the frame information. - private uint ReadFrame( + /// The end of the RIFF payload. + private void ReadFrame( BufferedReadStream stream, ref Image? image, ref ImageFrame? previousFrame, ref WebpFrameData? prevFrameData, uint width, uint height, - TPixel backgroundColor) + TPixel backgroundColor, + long containerEnd) where TPixel : unmanaged, IPixel { - WebpFrameData frameData = WebpFrameData.Parse(stream); - long streamStartPosition = stream.Position; + WebpFrameData frameData = WebpFrameData.Parse(stream, containerEnd); + + // Nested chunks must fit inside this ANMF payload. Bytes in the next frame cannot complete this frame. + long frameEnd = stream.Position + frameData.DataSize - 16; + if (frameEnd - stream.Position < WebpConstants.ChunkHeaderSize || !new Rectangle(0, 0, (int)width, (int)height).Contains(frameData.Bounds)) + { + WebpThrowHelper.ThrowInvalidImageContentException("Invalid WebP animation frame bounds or missing image chunk."); + } + Span buffer = stackalloc byte[4]; WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer); @@ -284,8 +303,13 @@ private uint ReadFrame( byte alphaChunkHeader = 0; if (chunkType is WebpChunkType.Alpha) { - alphaChunkHeader = this.ReadAlphaData(stream); + alphaChunkHeader = this.ReadAlphaData(stream, frameEnd); hasAlpha = true; + if (frameEnd - stream.Position < WebpConstants.ChunkHeaderSize) + { + WebpThrowHelper.ThrowInvalidImageContentException("Missing WebP frame image chunk after alpha data."); + } + chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer); } @@ -294,7 +318,7 @@ private uint ReadFrame( switch (chunkType) { case WebpChunkType.Vp8: - webpInfo = WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, stream, buffer, features); + webpInfo = WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, stream, buffer, features, frameEnd, false); features.Alpha = hasAlpha; features.AlphaChunkHeader = alphaChunkHeader; break; @@ -304,13 +328,19 @@ private uint ReadFrame( WebpThrowHelper.ThrowNotSupportedException("Alpha channel is not supported for lossless webp images."); } - webpInfo = WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, buffer, features); + webpInfo = WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, buffer, features, frameEnd, false); break; default: WebpThrowHelper.ThrowImageFormatException("Read unexpected chunk type, should be VP8 or VP8L"); break; } + using WebpImageInfo frameInfo = webpInfo!; + if (frameInfo.Width != frameData.Width || frameInfo.Height != frameData.Height) + { + WebpThrowHelper.ThrowInvalidImageContentException("WebP frame dimensions do not match its image chunk."); + } + ImageFrame currentFrame; if (previousFrame is null) { @@ -347,7 +377,6 @@ private uint ReadFrame( using Buffer2D pixelData = this.DecodeImageFrameData(frameData, webpInfo); DrawDecodedImageFrameOnCanvas(pixelData, currentFrame, interest, blend); - webpInfo?.Dispose(); previousFrame = currentFrame; prevFrameData = frameData; @@ -356,7 +385,7 @@ private uint ReadFrame( this.restoreArea = interest; } - return (uint)(stream.Position - streamStartPosition); + stream.Position = frameEnd + (frameData.DataSize & 1); } /// @@ -372,11 +401,20 @@ private static void SetFrameMetadata(ImageFrameMetadata meta, WebpFrameData fram frameMetadata.DisposalMode = frameData.DisposalMethod; } + /// + /// Reads metadata within RIFF and applies the ancillary recovery policy to complete chunks. + /// + /// The stream positioned at the chunk size. + /// The metadata chunk type. + /// The destination metadata. + /// Whether to skip metadata contents. + /// The end of the RIFF payload. private void ReadOptionalChunk( BufferedReadStream stream, WebpChunkType chunkType, ImageMetadata imageMetadata, - bool ignoreMetadata) + bool ignoreMetadata, + long containerEnd) { switch (chunkType) { @@ -386,13 +424,13 @@ private void ReadOptionalChunk( // precede the frame data, and we cannot safely skip it without successfully reading its size. // ReadIccProfile therefore validates the complete chunk extent before invoking the ancillary // handler. Only errors in the contents of a complete chunk follow that recovery policy. - WebpChunkParsingUtils.ReadIccProfile(stream, imageMetadata, ignoreMetadata, this.executeAncillarySegmentAction); + WebpChunkParsingUtils.ReadIccProfile(stream, imageMetadata, ignoreMetadata, this.executeAncillarySegmentAction, containerEnd); break; case WebpChunkType.Exif: - this.executeAncillarySegmentAction(() => WebpChunkParsingUtils.ReadExifProfile(stream, imageMetadata, ignoreMetadata)); + this.executeAncillarySegmentAction(() => WebpChunkParsingUtils.ReadExifProfile(stream, imageMetadata, ignoreMetadata, containerEnd)); break; case WebpChunkType.Xmp: - this.executeAncillarySegmentAction(() => WebpChunkParsingUtils.ReadXmpProfile(stream, imageMetadata, ignoreMetadata)); + this.executeAncillarySegmentAction(() => WebpChunkParsingUtils.ReadXmpProfile(stream, imageMetadata, ignoreMetadata, containerEnd)); break; } } @@ -401,17 +439,24 @@ private void ReadOptionalChunk( /// Reads the ALPH chunk data. /// /// The stream to read from. - private byte ReadAlphaData(BufferedReadStream stream) + /// The end of the ANMF payload. + private byte ReadAlphaData(BufferedReadStream stream, long containerEnd) { this.alphaData?.Dispose(); - uint alphaChunkSize = WebpChunkParsingUtils.ReadChunkSize(stream, stackalloc byte[4]); + uint alphaChunkSize = WebpChunkParsingUtils.ReadChunkSize(stream, stackalloc byte[4], containerEnd, 1, false); + long chunkEnd = stream.Position + alphaChunkSize + (alphaChunkSize & 1); int alphaDataSize = (int)(alphaChunkSize - 1); this.alphaData = this.memoryAllocator.Allocate(alphaDataSize); byte alphaChunkHeader = (byte)stream.ReadByte(); Span alphaData = this.alphaData.GetSpan(); - _ = stream.Read(alphaData, 0, alphaDataSize); + if (stream.Read(alphaData, 0, alphaDataSize) != alphaDataSize) + { + WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read WebP frame alpha data."); + } + + stream.Position = chunkEnd; return alphaChunkHeader; } diff --git a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs index 958f4df80b..5af53068bf 100644 --- a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs +++ b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs @@ -23,17 +23,20 @@ internal static class WebpChunkParsingUtils /// The buffered read stream. /// The scratch buffer to use while reading. /// The webp features to parse. + /// The end of the containing RIFF or ANMF payload. + /// Whether image-data recovery permits an incomplete compressed payload at EOF. /// Information about this webp image. - public static WebpImageInfo ReadVp8Header(MemoryAllocator memoryAllocator, BufferedReadStream stream, Span buffer, WebpFeatures features) + public static WebpImageInfo ReadVp8Header( + MemoryAllocator memoryAllocator, + BufferedReadStream stream, + Span buffer, + WebpFeatures features, + long containerEnd, + bool allowTruncatedPayload) { - // VP8 data size (not including this 4 bytes). - int bytesRead = stream.Read(buffer, 0, 4); - if (bytesRead != 4) - { - WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the VP8 header"); - } - - uint dataSize = BinaryPrimitives.ReadUInt32LittleEndian(buffer); + uint dataSize = ReadChunkSize(stream, buffer, containerEnd, WebpConstants.Vp8FrameHeaderSize, allowTruncatedPayload); + long chunkEnd = Math.Min(stream.Position + dataSize + (dataSize & 1), stream.Length); + int bytesRead; // Remaining counts the available image data payload. uint remaining = dataSize; @@ -120,6 +123,8 @@ public static WebpImageInfo ReadVp8Header(MemoryAllocator memoryAllocator, Buffe Vp8BitReader bitReader = new(stream, remaining, memoryAllocator, partitionLength) { Remaining = remaining }; + stream.Position = chunkEnd; + return new WebpImageInfo { DataSize = dataSize, @@ -145,50 +150,67 @@ public static WebpImageInfo ReadVp8Header(MemoryAllocator memoryAllocator, Buffe /// The buffered read stream. /// The scratch buffer to use while reading. /// The webp features to parse. - public static WebpImageInfo ReadVp8LHeader(MemoryAllocator memoryAllocator, BufferedReadStream stream, Span buffer, WebpFeatures features) + /// The end of the containing RIFF or ANMF payload. + /// Whether image-data recovery permits an incomplete compressed payload at EOF. + public static WebpImageInfo ReadVp8LHeader( + MemoryAllocator memoryAllocator, + BufferedReadStream stream, + Span buffer, + WebpFeatures features, + long containerEnd, + bool allowTruncatedPayload) { - // VP8 data size. - uint imageDataSize = ReadChunkSize(stream, buffer); - + // The five-byte VP8L header must fit inside its own chunk, not in following chunks or trailing data. + uint imageDataSize = ReadChunkSize(stream, buffer, containerEnd, 5, allowTruncatedPayload); + long chunkEnd = Math.Min(stream.Position + imageDataSize + (imageDataSize & 1), stream.Length); Vp8LBitReader bitReader = new(stream, imageDataSize, memoryAllocator); - - // One byte signature, should be 0x2f. - uint signature = bitReader.ReadValue(8); - if (signature != WebpConstants.Vp8LHeaderMagicByte) + try { - WebpThrowHelper.ThrowImageFormatException("Invalid VP8L signature"); - } + // One byte signature, should be 0x2f. + uint signature = bitReader.ReadValue(8); + if (signature != WebpConstants.Vp8LHeaderMagicByte) + { + WebpThrowHelper.ThrowImageFormatException("Invalid VP8L signature"); + } - // The first 28 bits of the bitstream specify the width and height of the image. - uint width = bitReader.ReadValue(WebpConstants.Vp8LImageSizeBits) + 1; - uint height = bitReader.ReadValue(WebpConstants.Vp8LImageSizeBits) + 1; - if (width == 0 || height == 0) - { - WebpThrowHelper.ThrowImageFormatException("invalid width or height read"); - } + // The first 28 bits of the bitstream specify the width and height of the image. + uint width = bitReader.ReadValue(WebpConstants.Vp8LImageSizeBits) + 1; + uint height = bitReader.ReadValue(WebpConstants.Vp8LImageSizeBits) + 1; + if (width == 0 || height == 0) + { + WebpThrowHelper.ThrowImageFormatException("invalid width or height read"); + } - // The alphaIsUsed flag should be set to 0 when all alpha values are 255 in the picture, and 1 otherwise. - // Alpha may have already been set by the VP8X chunk. - features.Alpha |= bitReader.ReadBit(); + // The alphaIsUsed flag should be set to 0 when all alpha values are 255 in the picture, and 1 otherwise. + // Alpha may have already been set by the VP8X chunk. + features.Alpha |= bitReader.ReadBit(); - // The next 3 bits are the version. The version number is a 3 bit code that must be set to 0. - // Any other value should be treated as an error. - uint version = bitReader.ReadValue(WebpConstants.Vp8LVersionBits); - if (version != 0) - { - WebpThrowHelper.ThrowNotSupportedException($"Unexpected version number {version} found in VP8L header"); - } + // The next 3 bits are the version. The version number is a 3 bit code that must be set to 0. + // Any other value should be treated as an error. + uint version = bitReader.ReadValue(WebpConstants.Vp8LVersionBits); + if (version != 0) + { + WebpThrowHelper.ThrowNotSupportedException($"Unexpected version number {version} found in VP8L header"); + } - return new WebpImageInfo + stream.Position = chunkEnd; + + return new WebpImageInfo + { + DataSize = imageDataSize, + Width = width, + Height = height, + BitsPerPixel = features.Alpha ? WebpBitsPerPixel.Bit32 : WebpBitsPerPixel.Bit24, + IsLossless = true, + Features = features, + Vp8LBitReader = bitReader + }; + } + catch { - DataSize = imageDataSize, - Width = width, - Height = height, - BitsPerPixel = features.Alpha ? WebpBitsPerPixel.Bit32 : WebpBitsPerPixel.Bit24, - IsLossless = true, - Features = features, - Vp8LBitReader = bitReader - }; + bitReader.Dispose(); + throw; + } } /// @@ -203,37 +225,37 @@ public static WebpImageInfo ReadVp8LHeader(MemoryAllocator memoryAllocator, Buff /// The buffered read stream. /// The scratch buffer to use while reading. /// The webp features to parse. + /// The end of the containing RIFF or ANMF payload. /// Information about this webp image. - public static WebpImageInfo ReadVp8XHeader(BufferedReadStream stream, Span buffer, WebpFeatures features) + public static WebpImageInfo ReadVp8XHeader(BufferedReadStream stream, Span buffer, WebpFeatures features, long containerEnd) { - uint fileSize = ReadChunkSize(stream, buffer); + uint size = ReadChunkSize(stream, buffer, containerEnd, 10, false); + if (size != 10) + { + WebpThrowHelper.ThrowInvalidImageContentException("The VP8X payload must contain exactly 10 bytes."); + } // The first byte contains information about the image features used. + // Its two most significant bits and its least significant bit are reserved. Writers set them to zero, + // but the container specification requires readers to ignore them, so they are not validated. byte imageFeatures = (byte)stream.ReadByte(); - // The first two bit of it are reserved and should be 0. - if (imageFeatures >> 6 != 0) - { - WebpThrowHelper.ThrowImageFormatException("first two bits of the VP8X header are expected to be zero"); - } - - // If bit 3 is set, a ICC Profile Chunk should be present. + // If bit 5 is set, an ICC Profile Chunk should be present. features.IccProfile = (imageFeatures & (1 << 5)) != 0; // If bit 4 is set, any of the frames of the image contain transparency information ("alpha" chunk). features.Alpha = (imageFeatures & (1 << 4)) != 0; - // If bit 5 is set, a EXIF metadata should be present. + // If bit 3 is set, EXIF metadata should be present. features.ExifProfile = (imageFeatures & (1 << 3)) != 0; - // If bit 6 is set, XMP metadata should be present. + // If bit 2 is set, XMP metadata should be present. features.XmpMetaData = (imageFeatures & (1 << 2)) != 0; - // If bit 7 is set, animation should be present. + // If bit 1 is set, animation should be present. features.Animation = (imageFeatures & (1 << 1)) != 0; - // 3 reserved bytes should follow which are supposed to be zero. - // No other decoder actually checks this though. + // Readers ignore these three reserved bytes, as they ignore reserved feature bits. stream.Read(buffer, 0, 3); // 3 bytes for the width. @@ -259,8 +281,8 @@ public static WebpImageInfo ReadVp8XHeader(BufferedReadStream stream, Span /// The stream to read from. /// The buffer to store the read data into. /// A unsigned 24 bit integer. - /// - /// Thrown if the input stream is not valid. + /// + /// Thrown if the stream ends before the integer. /// public static uint ReadUInt24LittleEndian(BufferedReadStream stream, Span buffer) { @@ -270,7 +292,7 @@ public static uint ReadUInt24LittleEndian(BufferedReadStream stream, Span return BinaryPrimitives.ReadUInt32LittleEndian(buffer); } - throw new ImageFormatException("Invalid Webp data, could not read unsigned 24 bit integer."); + throw new InvalidImageContentException("Invalid Webp data, could not read unsigned 24 bit integer."); } /// @@ -298,46 +320,66 @@ public static unsafe void WriteUInt24LittleEndian(Stream stream, uint data) } /// - /// Reads the chunk size. If Chunk Size is odd, a single padding byte will be added to the payload, - /// so the chunk size will be increased by 1 in those cases. + /// Reads a required chunk size and validates its complete extent before any payload allocation or read. /// - /// The stream to read the data from. - /// Buffer to store the data read from the stream. - /// If true, the chunk size is required to be read, otherwise it can be skipped. - /// The chunk size in bytes. - /// Thrown if the input stream is not valid. - public static uint ReadChunkSize(BufferedReadStream stream, Span buffer, bool required = true) + /// The stream positioned at the size field. + /// The four-byte size buffer. + /// The end of the containing RIFF or ANMF payload. + /// The minimum payload size required by the chunk header. + /// Whether to retain the bit reader's EOF recovery for compressed image data. + /// The payload size, excluding its RIFF padding byte. + public static uint ReadChunkSize(BufferedReadStream stream, Span buffer, long containerEnd, uint minimumSize, bool allowTruncatedPayload) { - ulong chunkSize = ReadPaddedChunkSize(stream, buffer, required); + ulong size = ReadChunkPayloadSize(stream, buffer, true); + + // Widen before adding padding. The parent end also excludes bytes outside RIFF and adjacent ANMF frames. + ulong paddedSize = size + (size & 1); + if (size < minimumSize || paddedSize > (ulong)(containerEnd - stream.Position) || stream.RemainingBytes < minimumSize) + { + WebpThrowHelper.ThrowInvalidImageContentException("WebP chunk extends beyond its container or has an incomplete header."); + } - // Structural chunk sizes must remain representable by their uint-sized consumers. - // Metadata readers retain the wider extent so their recovery can skip it safely. - if (chunkSize > uint.MaxValue) + // Only compressed image data has EOF recovery. Required headers and parent boundaries remain fatal. + // Bit readers retain their existing zero-filled tail when IgnoreImageData permits that recovery. + if (!allowTruncatedPayload && paddedSize > (ulong)stream.RemainingBytes) { - WebpThrowHelper.ThrowInvalidImageContentException("WebP chunk size exceeds the supported maximum."); + WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the WebP chunk payload."); } - return (uint)chunkSize; + return (uint)size; } /// - /// Reads a chunk's complete padded extent without wrapping a uint-sized payload length. + /// Skips an unknown chunk without reading beyond its containing RIFF payload. + /// + /// The stream positioned at the size field. + /// The four-byte size buffer. + /// The end of the containing RIFF payload. + public static void SkipChunk(BufferedReadStream stream, Span buffer, long containerEnd) + { + ulong size = ReadChunkPayloadSize(stream, buffer, true); + stream.Position += (long)Math.Min(size + (size & 1), (ulong)(Math.Min(containerEnd, stream.Length) - stream.Position)); + } + + /// + /// Reads the payload size stored in a chunk header. RIFF follows an odd-sized payload with a padding byte + /// that belongs to the chunk but not to its payload, so the returned size excludes it. /// /// The input stream. /// The four-byte size buffer. /// Whether an incomplete size field is an error. - /// The padded extent, or remaining bytes when an optional size field is incomplete. - private static ulong ReadPaddedChunkSize(BufferedReadStream stream, Span buffer, bool required) + /// The payload size, or remaining bytes when an optional size field is incomplete. + private static ulong ReadChunkPayloadSize(BufferedReadStream stream, Span buffer, bool required) { if (stream.Read(buffer) is 4) { - uint chunkSize = BinaryPrimitives.ReadUInt32LittleEndian(buffer); - return (ulong)chunkSize + (chunkSize & 1); + return BinaryPrimitives.ReadUInt32LittleEndian(buffer); } if (required) { - throw new ImageFormatException("Invalid Webp data, could not read chunk size."); + // Only a truncated stream leaves fewer than four bytes for the size field. + throw new InvalidImageContentException("Invalid Webp data, could not read chunk size."); } // Return the size of the remaining data in the stream. @@ -349,8 +391,8 @@ private static ulong ReadPaddedChunkSize(BufferedReadStream stream, Span b /// /// The stream to read the data from. /// Buffer to store the data read from the stream. - /// - /// Thrown if the input stream is not valid. + /// + /// Thrown if the stream ends inside the chunk type. /// public static WebpChunkType ReadChunkType(BufferedReadStream stream, Span buffer) { @@ -360,8 +402,8 @@ public static WebpChunkType ReadChunkType(BufferedReadStream stream, Span } // While we ignore unknown chunks we still need a to be a ble to read a chunk type - // known or otherwise from the stream. - throw new ImageFormatException("Invalid Webp data, could not read chunk type."); + // known or otherwise from the stream. Only a truncated stream leaves fewer than four bytes. + throw new InvalidImageContentException("Invalid Webp data, could not read chunk type."); } /// @@ -371,24 +413,26 @@ public static WebpChunkType ReadChunkType(BufferedReadStream stream, Span /// The image metadata. /// If true, metadata will be ignored. /// Executes profile parsing under the decoder's integrity policy. + /// The end of the RIFF payload. public static void ReadIccProfile( BufferedReadStream stream, ImageMetadata metadata, bool ignoreMetadata, - Action executeAncillarySegmentAction) + Action executeAncillarySegmentAction, + long containerEnd) { - ulong chunkSize = ReadPaddedChunkSize(stream, stackalloc byte[4], true); + ulong payloadSize = ReadChunkPayloadSize(stream, stackalloc byte[4], true); - // ICCP precedes image/frame data. Its framing must be readable even when - // metadata is skipped; otherwise there is no safe location to resume decoding. - if (!stream.IsReadRangeValid(stream.Position, chunkSize)) + // ICCP precedes image/frame data. Its framing, including the padding byte of an odd-sized payload, + // must be readable even when metadata is skipped; otherwise there is no safe location to resume decoding. + if (payloadSize + (payloadSize & 1) > (ulong)(Math.Min(containerEnd, stream.Length) - stream.Position)) { WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the ICCP chunk."); } executeAncillarySegmentAction(() => { - byte[]? iccpData = ReadMetadataChunk(stream, chunkSize, ignoreMetadata || metadata.IccProfile != null); + byte[]? iccpData = ReadMetadataChunk(stream, payloadSize, ignoreMetadata || metadata.IccProfile != null, containerEnd); if (iccpData is not null) { IccProfile profile = new(iccpData); @@ -408,13 +452,15 @@ public static void ReadIccProfile( /// The stream to decode from. /// The image metadata. /// If true, metadata will be ignored. + /// The end of the RIFF payload. public static void ReadExifProfile( BufferedReadStream stream, ImageMetadata metadata, - bool ignoreMetadata) + bool ignoreMetadata, + long containerEnd) { - ulong chunkSize = ReadPaddedChunkSize(stream, stackalloc byte[4], !ignoreMetadata); - byte[]? exifData = ReadMetadataChunk(stream, chunkSize, ignoreMetadata || metadata.ExifProfile != null); + ulong payloadSize = ReadChunkPayloadSize(stream, stackalloc byte[4], !ignoreMetadata); + byte[]? exifData = ReadMetadataChunk(stream, payloadSize, ignoreMetadata || metadata.ExifProfile != null, containerEnd); if (exifData is not null) { ExifProfile exifProfile = new(exifData); @@ -440,13 +486,15 @@ public static void ReadExifProfile( /// The stream to decode from. /// The image metadata. /// If true, metadata will be ignored. + /// The end of the RIFF payload. public static void ReadXmpProfile( BufferedReadStream stream, ImageMetadata metadata, - bool ignoreMetadata) + bool ignoreMetadata, + long containerEnd) { - ulong chunkSize = ReadPaddedChunkSize(stream, stackalloc byte[4], !ignoreMetadata); - byte[]? xmpData = ReadMetadataChunk(stream, chunkSize, ignoreMetadata || metadata.XmpProfile != null); + ulong payloadSize = ReadChunkPayloadSize(stream, stackalloc byte[4], !ignoreMetadata); + byte[]? xmpData = ReadMetadataChunk(stream, payloadSize, ignoreMetadata || metadata.XmpProfile != null, containerEnd); if (xmpData is not null) { metadata.XmpProfile = new XmpProfile(xmpData); @@ -458,19 +506,23 @@ public static void ReadXmpProfile( /// Callers execute metadata parsing under the decoder's ancillary integrity policy. /// /// The input stream positioned at the chunk payload. - /// The declared extent including its padding byte. + /// The declared payload length, excluding its padding byte. /// Whether to skip the payload without parsing it. + /// The end of the RIFF payload. /// The payload, or null when metadata is skipped. - private static byte[]? ReadMetadataChunk(BufferedReadStream stream, ulong paddedLength, bool ignoreMetadata) + private static byte[]? ReadMetadataChunk(BufferedReadStream stream, ulong payloadLength, bool ignoreMetadata, long containerEnd) { - long chunkEnd = stream.Position + (long)Math.Min(paddedLength, (ulong)stream.RemainingBytes); + // An odd-sized payload is followed by a padding byte that is part of the chunk but not of the payload. + // The next chunk starts after it, clamped to the end of a truncated stream. + ulong paddedLength = payloadLength + (payloadLength & 1); + long chunkEnd = stream.Position + (long)Math.Min(paddedLength, (ulong)(Math.Min(containerEnd, stream.Length) - stream.Position)); if (ignoreMetadata) { stream.Position = chunkEnd; return null; } - if (!stream.TryGetReadLength(paddedLength, out int bufferLength)) + if (paddedLength > (ulong)(Math.Min(containerEnd, stream.Length) - stream.Position) || !stream.TryGetReadLength(payloadLength, out _)) { // Ignoring an ancillary error must not make the next parser interpret // this payload as another chunk header. A truncated chunk consumes EOF. @@ -478,12 +530,15 @@ public static void ReadXmpProfile( WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the metadata chunk."); } + int bufferLength = (int)payloadLength; byte[] data = new byte[bufferLength]; if (stream.Read(data) != bufferLength) { WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the metadata chunk."); } + // Skip the padding byte, so it is neither returned as payload nor read as the next chunk header. + stream.Position = chunkEnd; return data; } diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs index c2a4e44653..0c0d59c7a5 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs @@ -52,6 +52,11 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable /// private readonly BackgroundColorHandling backgroundColorHandling; + /// + /// The stream position where the RIFF payload ends. Chunk parsing stops here, so bytes after the payload are ignored. + /// + private long riffEnd; + /// /// Initializes a new instance of the class. /// @@ -75,7 +80,7 @@ protected override Image Decode(BufferedReadStream stream, Cance ImageMetadata metadata = new(); Span buffer = stackalloc byte[4]; - uint fileSize = ReadImageHeader(stream, buffer); + this.ReadImageHeader(stream, buffer); using (this.webImageInfo = this.ReadVp8Info(stream, metadata)) { @@ -89,7 +94,13 @@ protected override Image Decode(BufferedReadStream stream, Cance this.backgroundColorHandling, this.ExecuteAncillarySegmentAction); - return animationDecoder.Decode(stream, this.webImageInfo.Features, this.webImageInfo.Width, this.webImageInfo.Height, fileSize); + return animationDecoder.Decode( + stream, + this.webImageInfo.Features, + this.webImageInfo.Width, + this.webImageInfo.Height, + metadata, + this.riffEnd); } // A VP8X header alone describes a canvas, not decodable image data. @@ -140,7 +151,7 @@ protected override Image Decode(BufferedReadStream stream, Cance /// protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { - uint fileSize = ReadImageHeader(stream, stackalloc byte[4]); + this.ReadImageHeader(stream, stackalloc byte[4]); ImageMetadata metadata = new(); using (this.webImageInfo = this.ReadVp8Info(stream, metadata, true)) @@ -160,7 +171,8 @@ protected override ImageInfo Identify(BufferedReadStream stream, CancellationTok this.webImageInfo.Features, this.webImageInfo.Width, this.webImageInfo.Height, - fileSize); + metadata, + this.riffEnd); } return new ImageInfo( @@ -170,25 +182,39 @@ protected override ImageInfo Identify(BufferedReadStream stream, CancellationTok } /// - /// Reads and skips over the image header. + /// Reads and skips over the image header, and records where the RIFF payload ends. /// /// The stream to decode from. /// Temporary buffer. - /// The file size in bytes. - private static uint ReadImageHeader(BufferedReadStream stream, Span buffer) + private void ReadImageHeader(BufferedReadStream stream, Span buffer) { + long riffStart = stream.Position; + // Skip FourCC header, we already know its a RIFF file at this point. stream.Skip(4); // Read file size. // The size of the file in bytes starting at offset 8. // The file size in the header is the total size of the chunks that follow plus 4 bytes for the ‘WEBP’ FourCC. - uint fileSize = WebpChunkParsingUtils.ReadChunkSize(stream, buffer); + if (stream.Read(buffer) != 4) + { + WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the RIFF size."); + } + + uint fileSize = BinaryPrimitives.ReadUInt32LittleEndian(buffer); + + // Bytes after the RIFF payload are not part of the image, so chunks are only read up to its end, + // as libwebp does. Keep the declared end separate from EOF: IgnoreImageData can recover a compressed + // payload cut short at EOF, but it must never borrow bytes from outside its declared parent. + this.riffEnd = riffStart + WebpConstants.ChunkHeaderSize + fileSize; // Skip 'WEBP' from the header. stream.Skip(4); - return fileSize; + if (fileSize < 12 || Math.Min(this.riffEnd, stream.Length) - stream.Position < WebpConstants.ChunkHeaderSize) + { + WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the first WebP chunk."); + } } /// @@ -207,59 +233,89 @@ private WebpImageInfo ReadVp8Info(BufferedReadStream stream, ImageMetadata metad WebpImageInfo? info = null; WebpFeatures features = new(); - switch (chunkType) + bool allowTruncatedPayload = this.Options.SegmentIntegrityHandling == SegmentIntegrityHandling.IgnoreImageData; + + // Retain local ownership until return. Metadata parsing can fail after a copied bitstream has been allocated. + try { - case WebpChunkType.Vp8: - info = WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, stream, buffer, features); - webpMetadata.FileFormat = WebpFileFormatType.Lossy; - webpMetadata.ColorType = WebpColorType.Yuv; - return info; - case WebpChunkType.Vp8L: - info = WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, buffer, features); - webpMetadata.FileFormat = WebpFileFormatType.Lossless; - webpMetadata.ColorType = info.Features?.Alpha == true ? WebpColorType.Rgba : WebpColorType.Rgb; - return info; - case WebpChunkType.Vp8X: - info = WebpChunkParsingUtils.ReadVp8XHeader(stream, buffer, features); - while (stream.Position < stream.Length) - { - chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer); - if (chunkType == WebpChunkType.Vp8) - { - info = WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, stream, buffer, features); - webpMetadata.FileFormat = WebpFileFormatType.Lossy; - webpMetadata.ColorType = info.Features?.Alpha == true ? WebpColorType.Rgba : WebpColorType.Rgb; - } - else if (chunkType == WebpChunkType.Vp8L) - { - info = WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, buffer, features); - webpMetadata.FileFormat = WebpFileFormatType.Lossless; - webpMetadata.ColorType = info.Features?.Alpha == true ? WebpColorType.Rgba : WebpColorType.Rgb; - } - else if (WebpChunkParsingUtils.IsOptionalVp8XChunk(chunkType)) + switch (chunkType) + { + case WebpChunkType.Vp8: + info = WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, stream, buffer, features, this.riffEnd, allowTruncatedPayload); + webpMetadata.FileFormat = WebpFileFormatType.Lossy; + webpMetadata.ColorType = WebpColorType.Yuv; + return info; + case WebpChunkType.Vp8L: + info = WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, buffer, features, this.riffEnd, allowTruncatedPayload); + webpMetadata.FileFormat = WebpFileFormatType.Lossless; + webpMetadata.ColorType = info.Features?.Alpha == true ? WebpColorType.Rgba : WebpColorType.Rgb; + return info; + case WebpChunkType.Vp8X: + info = WebpChunkParsingUtils.ReadVp8XHeader(stream, buffer, features, this.riffEnd); + + // Fewer bytes than a chunk header before the end of the RIFF payload cannot start another chunk. + while (Math.Min(this.riffEnd, stream.Length) - stream.Position >= WebpConstants.ChunkHeaderSize) { - // ANIM chunks appear before EXIF and XMP chunks. - // Return after parsing an ANIM chunk - The animated decoder will handle the rest. - bool isAnimationChunk = this.ParseOptionalExtendedChunks(stream, metadata, chunkType, features, ignoreAlpha, buffer); - if (isAnimationChunk) + chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer); + if (chunkType == WebpChunkType.Vp8) { - return info; + WebpImageInfo imageInfo = WebpChunkParsingUtils.ReadVp8Header( + this.memoryAllocator, + stream, + buffer, + features, + this.riffEnd, + allowTruncatedPayload); + + info.Dispose(); + info = imageInfo; + webpMetadata.FileFormat = WebpFileFormatType.Lossy; + webpMetadata.ColorType = info.Features?.Alpha == true ? WebpColorType.Rgba : WebpColorType.Rgb; + } + else if (chunkType == WebpChunkType.Vp8L) + { + WebpImageInfo imageInfo = WebpChunkParsingUtils.ReadVp8LHeader( + this.memoryAllocator, + stream, + buffer, + features, + this.riffEnd, + allowTruncatedPayload); + + info.Dispose(); + info = imageInfo; + webpMetadata.FileFormat = WebpFileFormatType.Lossless; + webpMetadata.ColorType = info.Features?.Alpha == true ? WebpColorType.Rgba : WebpColorType.Rgb; + } + else if (WebpChunkParsingUtils.IsOptionalVp8XChunk(chunkType)) + { + // ANIM chunks appear before EXIF and XMP chunks. + // Return after parsing an ANIM chunk - The animated decoder will handle the rest. + bool isAnimationChunk = this.ParseOptionalExtendedChunks(stream, metadata, chunkType, features, ignoreAlpha, buffer); + if (isAnimationChunk) + { + return info; + } + } + else + { + // Unknown chunks do not change the pixels. Widen their padded size before skipping, + // and stop at RIFF when the declaration exceeds the available container bytes. + WebpChunkParsingUtils.SkipChunk(stream, buffer, this.riffEnd); } } - else - { - // Ignore unknown chunks. - // These must always fall after the image data so we are safe to always skip them. - uint chunkSize = WebpChunkParsingUtils.ReadChunkSize(stream, buffer, false); - stream.Skip((int)chunkSize); - } - } - return info; - default: - WebpThrowHelper.ThrowImageFormatException("Unrecognized VP8 header"); - return - new WebpImageInfo(); // this return will never be reached, because throw helper will throw an exception. + return info; + default: + WebpThrowHelper.ThrowImageFormatException("Unrecognized VP8 header"); + return + new WebpImageInfo(); // this return will never be reached, because throw helper will throw an exception. + } + } + catch + { + info?.Dispose(); + throw; } } @@ -290,19 +346,19 @@ private bool ParseOptionalExtendedChunks( // precede the image data, and we cannot safely skip it without successfully reading its size. // ReadIccProfile therefore validates the complete chunk extent before invoking the ancillary // handler. Only errors in the contents of a complete chunk follow that recovery policy. - WebpChunkParsingUtils.ReadIccProfile(stream, metadata, ignoreMetadata, this.ExecuteAncillarySegmentAction); + WebpChunkParsingUtils.ReadIccProfile(stream, metadata, ignoreMetadata, this.ExecuteAncillarySegmentAction, this.riffEnd); break; case WebpChunkType.Exif: - this.ExecuteAncillarySegmentAction(() => WebpChunkParsingUtils.ReadExifProfile(stream, metadata, ignoreMetadata)); + this.ExecuteAncillarySegmentAction(() => WebpChunkParsingUtils.ReadExifProfile(stream, metadata, ignoreMetadata, this.riffEnd)); break; case WebpChunkType.Xmp: - this.ExecuteAncillarySegmentAction(() => WebpChunkParsingUtils.ReadXmpProfile(stream, metadata, ignoreMetadata)); + this.ExecuteAncillarySegmentAction(() => WebpChunkParsingUtils.ReadXmpProfile(stream, metadata, ignoreMetadata, this.riffEnd)); break; case WebpChunkType.AnimationParameter: - ReadAnimationParameters(stream, features, buffer); + ReadAnimationParameters(stream, features, buffer, this.riffEnd); return true; case WebpChunkType.Alpha: @@ -334,24 +390,24 @@ private void ParseOptionalChunks(BufferedReadStream stream, ImageMetadata metada return; } - long streamLength = stream.Length; - while (stream.Position < streamLength) + // Fewer bytes than a chunk header before the end of the RIFF payload cannot start another chunk. + while (Math.Min(this.riffEnd, stream.Length) - stream.Position >= WebpConstants.ChunkHeaderSize) { // Read chunk header. WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer); if (chunkType == WebpChunkType.Exif) { - this.ExecuteAncillarySegmentAction(() => WebpChunkParsingUtils.ReadExifProfile(stream, metadata, ignoreMetadata)); + this.ExecuteAncillarySegmentAction(() => WebpChunkParsingUtils.ReadExifProfile(stream, metadata, ignoreMetadata, this.riffEnd)); } else if (chunkType == WebpChunkType.Xmp) { - this.ExecuteAncillarySegmentAction(() => WebpChunkParsingUtils.ReadXmpProfile(stream, metadata, ignoreMetadata)); + this.ExecuteAncillarySegmentAction(() => WebpChunkParsingUtils.ReadXmpProfile(stream, metadata, ignoreMetadata, this.riffEnd)); } else { // Skip unknown chunks. - uint chunkLength = WebpChunkParsingUtils.ReadChunkSize(stream, buffer, false); - stream.Skip((int)chunkLength); + // A chunk extending past the RIFF payload ends the parsing instead of wrapping to a negative skip. + WebpChunkParsingUtils.SkipChunk(stream, buffer, this.riffEnd); } } } @@ -362,10 +418,12 @@ private void ParseOptionalChunks(BufferedReadStream stream, ImageMetadata metada /// The stream to decode from. /// The webp features. /// Temporary buffer. - private static void ReadAnimationParameters(BufferedReadStream stream, WebpFeatures features, Span buffer) + /// The end of the RIFF payload. + private static void ReadAnimationParameters(BufferedReadStream stream, WebpFeatures features, Span buffer, long containerEnd) { features.Animation = true; - uint animationChunkSize = WebpChunkParsingUtils.ReadChunkSize(stream, buffer); + uint animationChunkSize = WebpChunkParsingUtils.ReadChunkSize(stream, buffer, containerEnd, 6, false); + long chunkEnd = stream.Position + animationChunkSize + (animationChunkSize & 1); byte blue = (byte)stream.ReadByte(); byte green = (byte)stream.ReadByte(); byte red = (byte)stream.ReadByte(); @@ -378,6 +436,7 @@ private static void ReadAnimationParameters(BufferedReadStream stream, WebpFeatu } features.AnimationLoopCount = BinaryPrimitives.ReadUInt16LittleEndian(buffer); + stream.Position = chunkEnd; } /// @@ -389,15 +448,17 @@ private static void ReadAnimationParameters(BufferedReadStream stream, WebpFeatu /// Temporary buffer. private void ReadAlphaData(BufferedReadStream stream, WebpFeatures features, bool ignoreAlpha, Span buffer) { - uint alphaChunkSize = WebpChunkParsingUtils.ReadChunkSize(stream, buffer); + uint alphaChunkSize = WebpChunkParsingUtils.ReadChunkSize(stream, buffer, this.riffEnd, 1, false); + long chunkEnd = stream.Position + alphaChunkSize + (alphaChunkSize & 1); if (ignoreAlpha) { - stream.Skip((int)alphaChunkSize); + stream.Position = chunkEnd; return; } features.AlphaChunkHeader = (byte)stream.ReadByte(); int alphaDataSize = (int)(alphaChunkSize - 1); + this.alphaData?.Dispose(); this.alphaData = this.memoryAllocator.Allocate(alphaDataSize); Span alphaData = this.alphaData.GetSpan(); int bytesRead = stream.Read(alphaData, 0, alphaDataSize); @@ -405,6 +466,8 @@ private void ReadAlphaData(BufferedReadStream stream, WebpFeatures features, boo { WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the alpha data from the stream"); } + + stream.Position = chunkEnd; } /// diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs index a3e3b81cf0..c80019bdef 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs @@ -406,8 +406,15 @@ public void Decode_AnimatedLossy_AlphaBlending_Works(TestImageProvider(TestImageProvider provider) where TPixel : unmanaged, IPixel { - // Just make sure no exception is thrown. The reference decoder fails to load the image. - using Image image = provider.GetImage(WebpDecoder.Instance); + // These stored files end before their declared VP8L payload ends. Local libwebp reports NOT_ENOUGH_DATA. + // Recovery belongs to IgnoreImageData; ignoring ancillary errors must not permit missing image bytes. + DecoderOptions strict = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict }; + DecoderOptions ancillary = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreAncillary }; + Assert.Throws(() => provider.GetImage(WebpDecoder.Instance, strict)); + Assert.Throws(() => provider.GetImage(WebpDecoder.Instance, ancillary)); + + DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreImageData }; + using Image image = provider.GetImage(WebpDecoder.Instance, options); image.DebugSave(provider); } @@ -501,10 +508,11 @@ public void WebpDecoder_CanDecode_Issue2866(TestImageProvider pr [WithFile(Lossless.LossLessCorruptImage3, PixelTypes.Rgba32)] public void WebpDecoder_ThrowImageFormatException_OnInvalidImages(TestImageProvider provider) where TPixel : unmanaged, IPixel => + // Ignore the missing tail so this assertion still tests the invalid compressed color transform. Assert.Throws( () => { - using (provider.GetImage(WebpDecoder.Instance)) + using (provider.GetImage(WebpDecoder.Instance, new DecoderOptions { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreImageData })) { } }); @@ -621,4 +629,81 @@ public void Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile(Tes image.CompareToReferenceOutput(provider); Assert.Null(image.Metadata.IccProfile); } + + /// + /// Checks that ignored container fields and data do not change pixels or metadata. + /// + [Theory] + [InlineData(ContainerDirectory + "odd-metadata.webp", ContainerDirectory + "reserved-fields.webp")] + [InlineData(Lossy.Small02, ContainerDirectory + "lossy-trailing.webp")] + [InlineData(Lossless.NoTransform1, ContainerDirectory + "lossless-trailing.webp")] + [InlineData(Lossy.AlphaCompressedHorizontalFilter, ContainerDirectory + "alpha-trailing.webp")] + [InlineData(ContainerDirectory + "odd-metadata.webp", ContainerDirectory + "metadata-trailing.webp")] + [InlineData(ContainerDirectory + "odd-metadata.webp", ContainerDirectory + "unknown-signed.webp")] + [InlineData(ContainerDirectory + "odd-metadata.webp", ContainerDirectory + "unknown-unsigned.webp")] + [InlineData(ContainerDirectory + "odd-metadata.webp", ContainerDirectory + "unknown-maximum.webp")] + [InlineData(ContainerDirectory + "animation.webp", ContainerDirectory + "animation-trailing.webp")] + [InlineData(ContainerDirectory + "animation.webp", ContainerDirectory + "animation-unknown.webp")] + public void Decode_ContainerVariations_PreservePixelsAndMetadata(string originalFile, string modifiedFile) + { + // Local libwebp confirms identical still pixels and two frames for each animation variant. + // The stored files alter reserved fields, append bytes outside RIFF, or add an unknown chunk. + byte[] original = TestFile.Create(originalFile).Bytes; + byte[] modified = TestFile.Create(modifiedFile).Bytes; + DecoderOptions options = new() { MaxFrames = 2 }; + using Image expected = Image.Load(options, original); + using Image actual = Image.Load(options, modified); + + ImageComparer.Exact.VerifySimilarity(expected, actual); + Assert.Equal(expected.Frames.Count, actual.Frames.Count); + Assert.Equal(expected.Metadata.ExifProfile?.ToByteArray(), actual.Metadata.ExifProfile?.ToByteArray()); + Assert.Equal(expected.Metadata.XmpProfile?.ToByteArray(), actual.Metadata.XmpProfile?.ToByteArray()); + Assert.Equal(expected.Metadata.IccProfile?.ToByteArray(), actual.Metadata.IccProfile?.ToByteArray()); + + ImageInfo expectedInfo = Image.Identify(options, original); + ImageInfo actualInfo = Image.Identify(options, modified); + Assert.Equal(expectedInfo.Size, actualInfo.Size); + Assert.Equal(expectedInfo.FrameMetadataCollection.Count, actualInfo.FrameMetadataCollection.Count); + } + + /// + /// Rejects headers and image payloads whose declared extent exceeds RIFF. + /// + [Theory] + [InlineData(ContainerDirectory + "vp8-crosses-riff.webp")] + [InlineData(ContainerDirectory + "vp8l-crosses-riff.webp")] + [InlineData(ContainerDirectory + "vp8x-crosses-riff.webp")] + [InlineData(ContainerDirectory + "alpha-crosses-riff.webp")] + [InlineData(ContainerDirectory + "anim-crosses-riff.webp")] + [InlineData(ContainerDirectory + "frame-crosses-riff.webp")] + [InlineData(ContainerDirectory + "vp8l-short-header.webp")] + [InlineData(ContainerDirectory + "vp8x-short-header.webp")] + public void Decode_ChunkOutsideRiff_RejectsInvalidFraming(string fileName) + { + byte[] data = TestFile.Create(fileName).Bytes; + + // These files retain bytes after RIFF. Those bytes must not complete a header or image payload. + // Framing errors cannot be recovered by ignoring image or ancillary segment contents. + foreach (SegmentIntegrityHandling integrity in Enum.GetValues()) + { + DecoderOptions options = new() { SegmentIntegrityHandling = integrity }; + Assert.Throws(() => Image.Load(options, data)); + Assert.Throws(() => Image.Identify(options, data)); + } + } + + /// + /// Rejects an image payload that borrows bytes from the next animation frame. + /// + [Fact] + public void Decode_FrameImageOutsideAnmf_RejectsInvalidFraming() + { + // The VP8L payload extends into the next ANMF chunk. Local libwebp rejects this file. + byte[] data = TestFile.Create(ContainerDirectory + "frame-crosses-anmf.webp").Bytes; + foreach (SegmentIntegrityHandling integrity in Enum.GetValues()) + { + DecoderOptions options = new() { SegmentIntegrityHandling = integrity }; + Assert.Throws(() => Image.Load(options, data)); + } + } } diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs index 324070e4a5..1cb9890626 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs @@ -1,13 +1,12 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Buffers.Binary; -using System.Text; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestDataIcc; using SixLabors.ImageSharp.Tests.TestUtilities; // ReSharper disable InconsistentNaming @@ -38,8 +37,9 @@ public static IEnumerable TruncatedMetadataOptions() { foreach (SegmentIntegrityHandling integrity in new[] { SegmentIntegrityHandling.Strict, SegmentIntegrityHandling.IgnoreAncillary, SegmentIntegrityHandling.IgnoreImageData }) { - yield return new object[] { chunkType, length, integrity, false }; - yield return new object[] { chunkType, length, integrity, true }; + string fileName = $"{TestImages.Webp.ContainerDirectory}{chunkType.Trim().ToLowerInvariant()}-trailing-{length:x8}.webp"; + yield return new object[] { fileName, integrity, false }; + yield return new object[] { fileName, integrity, true }; } } } @@ -262,33 +262,19 @@ public void Decode_InvalidExifChunk_ThrowsWithStrict() } [Theory] - [InlineData("ICCP", 0xFFFFFFFEU)] - [InlineData("EXIF", 0xFFFFFFFEU)] - [InlineData("XMP ", 0xFFFFFFFEU)] - [InlineData("ICCP", uint.MaxValue)] - [InlineData("EXIF", uint.MaxValue)] - [InlineData("XMP ", uint.MaxValue)] + [InlineData("iccp", 0x40000000U)] + [InlineData("exif", 0x40000000U)] + [InlineData("xmp", 0x40000000U)] + [InlineData("iccp", 0xFFFFFFFEU)] + [InlineData("exif", 0xFFFFFFFEU)] + [InlineData("xmp", 0xFFFFFFFEU)] + [InlineData("iccp", uint.MaxValue)] + [InlineData("exif", uint.MaxValue)] + [InlineData("xmp", uint.MaxValue)] public void Decode_WithOversizedMetadataChunk_ThrowsInvalidImageContentException(string chunkType, uint length) { - byte[] payload = Convert.FromHexString( - "524946462200000057454250565038580A0000002000000000000000000049434350FEFFFFFF01020304"); - Encoding.ASCII.GetBytes(chunkType, payload.AsSpan(30, 4)); - BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(34), length); - DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict }; - - Assert.Throws(() => Image.Load(options, payload)); - Assert.Throws(() => Image.Identify(options, payload)); - } - - [Theory] - [InlineData("ICCP")] - [InlineData("EXIF")] - [InlineData("XMP ")] - public void Decode_WithMetadataChunkLargerThanRemainingData_ThrowsInStrictMode(string chunkType) - { - byte[] payload = Convert.FromHexString( - "524946460000000057454250565038580A00000020000000010000010000494343500000004000000000"); - Encoding.ASCII.GetBytes(chunkType, payload.AsSpan(30, 4)); + // The stored RIFF size covers the file. The metadata declaration itself exceeds that boundary. + byte[] payload = TestFile.Create($"{TestImages.Webp.ContainerDirectory}{chunkType}-oversized-{length:x8}.webp").Bytes; DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict }; Assert.Throws(() => Image.Load(options, payload)); @@ -297,9 +283,9 @@ public void Decode_WithMetadataChunkLargerThanRemainingData_ThrowsInStrictMode(s [Theory] [MemberData(nameof(TruncatedMetadataOptions))] - public void Decode_TruncatedTrailingMetadata_RespectsOptions(string chunkType, uint length, SegmentIntegrityHandling integrity, bool skipMetadata) + public void Decode_TruncatedTrailingMetadata_RespectsOptions(string fileName, SegmentIntegrityHandling integrity, bool skipMetadata) { - byte[] payload = CreateWebpWithMetadata(chunkType, length, false, false); + byte[] payload = TestFile.Create(fileName).Bytes; DecoderOptions options = new() { SegmentIntegrityHandling = integrity, SkipMetadata = skipMetadata }; if (integrity is SegmentIntegrityHandling.Strict && !skipMetadata) @@ -333,7 +319,7 @@ public void Decode_TruncatedTrailingMetadata_RespectsOptions(string chunkType, u [MemberData(nameof(IccMetadataOptions))] public void Decode_InvalidIccPayload_RespectsOptionsAndReadsFollowingImage(SegmentIntegrityHandling integrity, bool skipMetadata, bool animated) { - byte[] payload = CreateWebpWithMetadata("ICCP", 4, true, animated); + byte[] payload = TestFile.Create(TestImages.Webp.ContainerDirectory + (animated ? "invalid-icc-animated.webp" : "invalid-icc.webp")).Bytes; DecoderOptions options = new() { SegmentIntegrityHandling = integrity, SkipMetadata = skipMetadata }; if (integrity is SegmentIntegrityHandling.Strict && !skipMetadata) @@ -359,53 +345,74 @@ public void Decode_InvalidIccPayload_RespectsOptionsAndReadsFollowingImage(Segme [MemberData(nameof(IccMetadataOptions))] public void Decode_TruncatedIccFraming_RemainsFatal(SegmentIntegrityHandling integrity, bool skipMetadata, bool animated) { - byte[] payload = CreateWebpWithMetadata("ICCP", 0x40000000, true, animated); + byte[] payload = TestFile.Create(TestImages.Webp.ContainerDirectory + (animated ? "truncated-icc-animated.webp" : "truncated-icc.webp")).Bytes; DecoderOptions options = new() { SegmentIntegrityHandling = integrity, SkipMetadata = skipMetadata }; Assert.Throws(() => Image.Load(options, payload)); Assert.Throws(() => Image.Identify(options, payload)); } - /// - /// Places a metadata declaration around a complete lossless image to test recovery independently of pixel truncation. - /// - private static byte[] CreateWebpWithMetadata(string chunkType, uint length, bool beforeImage, bool animated) + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public void Decode_OddLengthMetadataChunks_ReadsPayloadsWithoutPaddingByte(bool skipMetadata, bool animated) { - byte[] header = Convert.FromHexString( - "524946460000000057454250565038580A00000020000000010000010000494343500000004000000000"); - header[20] = chunkType switch { "ICCP" => 0x20, "EXIF" => 0x08, _ => 0x04 }; - Encoding.ASCII.GetBytes(chunkType, header.AsSpan(30, 4)); - BinaryPrimitives.WriteUInt32LittleEndian(header.AsSpan(34), length); - - using Image source = new(2, 2, new Rgba32(17, 34, 51)); - if (animated) + // Local libwebp extracts these exact odd-length payloads and decodes the same solid 2x2 image. + byte[] payload = TestFile.Create(TestImages.Webp.ContainerDirectory + (animated ? "odd-metadata-animated.webp" : "odd-metadata.webp")).Bytes; + byte[] icc = TestFile.Create(TestImages.Webp.ContainerDirectory + "odd.icc").Bytes; + byte[] xmp = TestFile.Create(TestImages.Webp.ContainerDirectory + "odd.xmp").Bytes; + + DecoderOptions options = new() { SkipMetadata = skipMetadata }; + using Image image = Image.Load(options, payload); + + // ICCP precedes ANIM. Entering the frame decoder must retain that profile, not replace its metadata owner. + Assert.Equal(animated ? 2 : 1, image.Frames.Count); + Assert.Equal(new Rgba32(17, 34, 51), image[0, 0]); + if (skipMetadata) { - header[20] |= 0x02; - using Image secondFrame = new(2, 2, new Rgba32(51, 34, 17)); - source.Frames.AddFrame(secondFrame.Frames.RootFrame); + Assert.Null(image.Metadata.IccProfile); + Assert.Null(image.Metadata.ExifProfile); + Assert.Null(image.Metadata.XmpProfile); } - - using MemoryStream encoded = new(); - source.Save(encoded, new WebpEncoder { FileFormat = WebpFileFormatType.Lossless }); - byte[] imageData = encoded.ToArray(); - using MemoryStream combined = new(); - combined.Write(header.AsSpan(0, 30)); - if (beforeImage) + else { - combined.Write(header.AsSpan(30)); + Assert.Equal(icc, image.Metadata.IccProfile.ToByteArray()); + Assert.Equal("ImageSharp", image.Metadata.ExifProfile.GetValue(ExifTag.Software).Value); + Assert.Equal(xmp, image.Metadata.XmpProfile.ToByteArray()); } + } - // Replace the encoder's extended header when present, keeping its complete - // image or animation chunks and the deliberately chosen metadata declaration. - int imageChunkOffset = imageData.AsSpan(12, 4).SequenceEqual("VP8X"u8) ? 30 : 12; - combined.Write(imageData.AsSpan(imageChunkOffset)); - if (!beforeImage) + /// + /// Applies ancillary recovery only within RIFF and keeps incomplete ICC framing fatal. + /// + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Decode_MetadataOutsideRiff_RespectsIntegrity(bool skipMetadata) + { + // Complete metadata bytes exist in the physical file, but the RIFF declaration excludes part of them. + byte[] exif = TestFile.Create(TestImages.Webp.ContainerDirectory + "exif-crosses-riff.webp").Bytes; + byte[] icc = TestFile.Create(TestImages.Webp.ContainerDirectory + "icc-crosses-riff.webp").Bytes; + foreach (SegmentIntegrityHandling integrity in Enum.GetValues()) { - combined.Write(header.AsSpan(30)); - } + DecoderOptions options = new() { SegmentIntegrityHandling = integrity, SkipMetadata = skipMetadata }; + Assert.Throws(() => Image.Load(options, icc)); + Assert.Throws(() => Image.Identify(options, icc)); - byte[] payload = combined.ToArray(); - BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(4), (uint)payload.Length - 8); - return payload; + if (integrity == SegmentIntegrityHandling.Strict && !skipMetadata) + { + Assert.Throws(() => Image.Load(options, exif)); + Assert.Throws(() => Image.Identify(options, exif)); + } + else + { + using Image image = Image.Load(options, exif); + Assert.Equal(new Rgba32(17, 34, 51), image[0, 0]); + Assert.Null(image.Metadata.ExifProfile); + Assert.Null(Image.Identify(options, exif).Metadata.ExifProfile); + } + } } } diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index b30226ee87..13e31e7079 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -787,6 +787,11 @@ public static class Tga public static class Webp { + /// + /// Stored WebP container boundary and metadata inputs. + /// + public const string ContainerDirectory = "Webp/container/"; + // Reference image as png public const string Peak = "Webp/peak.png"; diff --git a/tests/Images/Input/Webp/container/alpha-crosses-riff.webp b/tests/Images/Input/Webp/container/alpha-crosses-riff.webp new file mode 100644 index 0000000000..9272b608f6 --- /dev/null +++ b/tests/Images/Input/Webp/container/alpha-crosses-riff.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:09bc5da7375e05d45229dc26fd26b87b12d37cc4ae64dddbff59991cd0b81edf +size 15592 diff --git a/tests/Images/Input/Webp/container/alpha-trailing.webp b/tests/Images/Input/Webp/container/alpha-trailing.webp new file mode 100644 index 0000000000..82704363c5 --- /dev/null +++ b/tests/Images/Input/Webp/container/alpha-trailing.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0163048b134c9807bb94eddea19466527bff7531ed5e8bc0c70072e365907a62 +size 15633 diff --git a/tests/Images/Input/Webp/container/anim-crosses-riff.webp b/tests/Images/Input/Webp/container/anim-crosses-riff.webp new file mode 100644 index 0000000000..3b5aef03a6 --- /dev/null +++ b/tests/Images/Input/Webp/container/anim-crosses-riff.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:586e82227dbd2765c5d55591f808ee2a845d13c0b011b4581e042bc6c97e9892 +size 144 diff --git a/tests/Images/Input/Webp/container/animation-trailing.webp b/tests/Images/Input/Webp/container/animation-trailing.webp new file mode 100644 index 0000000000..2d154295e9 --- /dev/null +++ b/tests/Images/Input/Webp/container/animation-trailing.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63896b9106a92a7b6975ed1d9a9cd3238e6e18d9a7dc922aa3012f673d8036dc +size 194 diff --git a/tests/Images/Input/Webp/container/animation-unknown.webp b/tests/Images/Input/Webp/container/animation-unknown.webp new file mode 100644 index 0000000000..6038a0891b --- /dev/null +++ b/tests/Images/Input/Webp/container/animation-unknown.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8b67e6b478395a9b7c97c8f4449edb6fe6d45bfa5e61fa6c5fa0bfff4902794a +size 160 diff --git a/tests/Images/Input/Webp/container/animation.webp b/tests/Images/Input/Webp/container/animation.webp new file mode 100644 index 0000000000..564c17aa8b --- /dev/null +++ b/tests/Images/Input/Webp/container/animation.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:699a7d5cd43e32a3206f6d62ad86bf45ecdca89d784f10e69a4a7d6b3365cbe1 +size 144 diff --git a/tests/Images/Input/Webp/container/exif-crosses-riff.webp b/tests/Images/Input/Webp/container/exif-crosses-riff.webp new file mode 100644 index 0000000000..3575b55b34 --- /dev/null +++ b/tests/Images/Input/Webp/container/exif-crosses-riff.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ad367b08cc2f4fb42b8f603c17cf632394b093a89ee2e82ff24e452be159a9d +size 102 diff --git a/tests/Images/Input/Webp/container/exif-oversized-40000000.webp b/tests/Images/Input/Webp/container/exif-oversized-40000000.webp new file mode 100644 index 0000000000..59a15d548d --- /dev/null +++ b/tests/Images/Input/Webp/container/exif-oversized-40000000.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:282d1c24ff4f70354fdb34e95e233943b20a15203fd71e5630b5b20ddd92c838 +size 42 diff --git a/tests/Images/Input/Webp/container/exif-oversized-fffffffe.webp b/tests/Images/Input/Webp/container/exif-oversized-fffffffe.webp new file mode 100644 index 0000000000..2fb2e70cdb --- /dev/null +++ b/tests/Images/Input/Webp/container/exif-oversized-fffffffe.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:60f808da4332a002fcde76bb58119698068a733f11b6cdf28ba8e4b19315f42b +size 42 diff --git a/tests/Images/Input/Webp/container/exif-oversized-ffffffff.webp b/tests/Images/Input/Webp/container/exif-oversized-ffffffff.webp new file mode 100644 index 0000000000..6fb8cfa975 --- /dev/null +++ b/tests/Images/Input/Webp/container/exif-oversized-ffffffff.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:012a7d6ed23747d6de1e64235f5e49ba2bfec59fcbd05667292c0447c837713c +size 42 diff --git a/tests/Images/Input/Webp/container/exif-trailing-40000000.webp b/tests/Images/Input/Webp/container/exif-trailing-40000000.webp new file mode 100644 index 0000000000..87c6e48964 --- /dev/null +++ b/tests/Images/Input/Webp/container/exif-trailing-40000000.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:08f9002c3aa324c2dfda829815621611fbae1d2b38e747a76f5d8398d4268b46 +size 68 diff --git a/tests/Images/Input/Webp/container/exif-trailing-fffffffe.webp b/tests/Images/Input/Webp/container/exif-trailing-fffffffe.webp new file mode 100644 index 0000000000..2a99e0e438 --- /dev/null +++ b/tests/Images/Input/Webp/container/exif-trailing-fffffffe.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:30045f539f8451906877f151df367773173850b989090956ae0f5090921b84bd +size 68 diff --git a/tests/Images/Input/Webp/container/exif-trailing-ffffffff.webp b/tests/Images/Input/Webp/container/exif-trailing-ffffffff.webp new file mode 100644 index 0000000000..c366244e8b --- /dev/null +++ b/tests/Images/Input/Webp/container/exif-trailing-ffffffff.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:924b98ac9d7bd166a71e92674a4f453c05fbf5e7c1836a965771585201c8b3cd +size 68 diff --git a/tests/Images/Input/Webp/container/frame-crosses-anmf.webp b/tests/Images/Input/Webp/container/frame-crosses-anmf.webp new file mode 100644 index 0000000000..67aceba551 --- /dev/null +++ b/tests/Images/Input/Webp/container/frame-crosses-anmf.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b1adde7a5a50f2b553b39edd3dc73b4076665da50fd84a720424385b745d473 +size 144 diff --git a/tests/Images/Input/Webp/container/frame-crosses-riff.webp b/tests/Images/Input/Webp/container/frame-crosses-riff.webp new file mode 100644 index 0000000000..2932cc3833 --- /dev/null +++ b/tests/Images/Input/Webp/container/frame-crosses-riff.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3886b6570081efe99d66194dbc1c8b00013cd6b34dbafa9d8166bf1dd894887 +size 144 diff --git a/tests/Images/Input/Webp/container/icc-crosses-riff.webp b/tests/Images/Input/Webp/container/icc-crosses-riff.webp new file mode 100644 index 0000000000..f3c5f5ca0a --- /dev/null +++ b/tests/Images/Input/Webp/container/icc-crosses-riff.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e4e7d8c31fa8d158a0010491ed460a88ff27ab35ed3e117418265e1f40e1a567 +size 3090 diff --git a/tests/Images/Input/Webp/container/iccp-oversized-40000000.webp b/tests/Images/Input/Webp/container/iccp-oversized-40000000.webp new file mode 100644 index 0000000000..2b67366bcc --- /dev/null +++ b/tests/Images/Input/Webp/container/iccp-oversized-40000000.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:adda83ffd21904ab4fd256e9b215e82ea5c80409c74a9ae340611c528e5f7550 +size 42 diff --git a/tests/Images/Input/Webp/container/iccp-oversized-fffffffe.webp b/tests/Images/Input/Webp/container/iccp-oversized-fffffffe.webp new file mode 100644 index 0000000000..6e3b1354df --- /dev/null +++ b/tests/Images/Input/Webp/container/iccp-oversized-fffffffe.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fd2fb9d91693cfcd5cf21f074f69d4ee07f6cedebfbe5e3ac90c1f7d22f0a3e1 +size 42 diff --git a/tests/Images/Input/Webp/container/iccp-oversized-ffffffff.webp b/tests/Images/Input/Webp/container/iccp-oversized-ffffffff.webp new file mode 100644 index 0000000000..5c08c01669 --- /dev/null +++ b/tests/Images/Input/Webp/container/iccp-oversized-ffffffff.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce86eade9dd0840645d18e627ea5b373ded14ee73252f78ad6c11d64ccfc99ca +size 42 diff --git a/tests/Images/Input/Webp/container/invalid-icc-animated.webp b/tests/Images/Input/Webp/container/invalid-icc-animated.webp new file mode 100644 index 0000000000..1cd8e52b60 --- /dev/null +++ b/tests/Images/Input/Webp/container/invalid-icc-animated.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:20648a2052f5e34278c6e523799454b080ee4a03c5519da532aad751eee49be0 +size 156 diff --git a/tests/Images/Input/Webp/container/invalid-icc.webp b/tests/Images/Input/Webp/container/invalid-icc.webp new file mode 100644 index 0000000000..3b9ad7896c --- /dev/null +++ b/tests/Images/Input/Webp/container/invalid-icc.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:524abc085d8c3785d521e47a6c8fd62090c854de5520e1efa77616876ff4ed89 +size 68 diff --git a/tests/Images/Input/Webp/container/lossless-trailing.webp b/tests/Images/Input/Webp/container/lossless-trailing.webp new file mode 100644 index 0000000000..f8e05cc12b --- /dev/null +++ b/tests/Images/Input/Webp/container/lossless-trailing.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2bf778053e281f77608ab1cecee6869c3c2c64543b224bfa49d8b6419d2e6d73 +size 91 diff --git a/tests/Images/Input/Webp/container/lossy-trailing.webp b/tests/Images/Input/Webp/container/lossy-trailing.webp new file mode 100644 index 0000000000..032642ffed --- /dev/null +++ b/tests/Images/Input/Webp/container/lossy-trailing.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:679e570ad89f17f44ea1244639e5ab8fc3b6a7f22ea6df6bd9e34e511b27308f +size 135 diff --git a/tests/Images/Input/Webp/container/metadata-trailing.webp b/tests/Images/Input/Webp/container/metadata-trailing.webp new file mode 100644 index 0000000000..a557b4212e --- /dev/null +++ b/tests/Images/Input/Webp/container/metadata-trailing.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc6c9f374b14b1bfa8ae34a83da89ec5871fa3b7054d490d54eced12dcd82860 +size 3235 diff --git a/tests/Images/Input/Webp/container/odd-metadata-animated.webp b/tests/Images/Input/Webp/container/odd-metadata-animated.webp new file mode 100644 index 0000000000..de4212eb10 --- /dev/null +++ b/tests/Images/Input/Webp/container/odd-metadata-animated.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:faa7a5b6c049ab763b7728584d9823f25376e4f8fd6969516eb6d6871b5cd2a5 +size 3282 diff --git a/tests/Images/Input/Webp/container/odd-metadata.webp b/tests/Images/Input/Webp/container/odd-metadata.webp new file mode 100644 index 0000000000..d314158390 --- /dev/null +++ b/tests/Images/Input/Webp/container/odd-metadata.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c7f8a3a085642819682619762fa1c403d319e7f1351ad6b62db78159ad108005 +size 3194 diff --git a/tests/Images/Input/Webp/container/odd.icc b/tests/Images/Input/Webp/container/odd.icc new file mode 100644 index 0000000000..0f4ac43c34 --- /dev/null +++ b/tests/Images/Input/Webp/container/odd.icc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a3a43e9054afc30b2593d78488e9b6396b1686dcec91bf814c99bf9157988e80 +size 3025 diff --git a/tests/Images/Input/Webp/container/odd.xmp b/tests/Images/Input/Webp/container/odd.xmp new file mode 100644 index 0000000000..aec23f77bb --- /dev/null +++ b/tests/Images/Input/Webp/container/odd.xmp @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/Images/Input/Webp/container/reserved-fields.webp b/tests/Images/Input/Webp/container/reserved-fields.webp new file mode 100644 index 0000000000..54440852fd --- /dev/null +++ b/tests/Images/Input/Webp/container/reserved-fields.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:720e9393fee739b956104a4e35eebcaf92cf05d4f75436355b3c8cd4789dfb34 +size 3194 diff --git a/tests/Images/Input/Webp/container/solid.webp b/tests/Images/Input/Webp/container/solid.webp new file mode 100644 index 0000000000..f07d7dcc8d --- /dev/null +++ b/tests/Images/Input/Webp/container/solid.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9c1164f1c98ca6114b3bfab14befdf53bfa3f32201769bf929b5c9b684e7ee1f +size 38 diff --git a/tests/Images/Input/Webp/container/truncated-icc-animated.webp b/tests/Images/Input/Webp/container/truncated-icc-animated.webp new file mode 100644 index 0000000000..2e21969d1f --- /dev/null +++ b/tests/Images/Input/Webp/container/truncated-icc-animated.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05ca4851ba80841c52c739cc4617a32dc7653d9bf852afea1ea33f7c416579dc +size 156 diff --git a/tests/Images/Input/Webp/container/truncated-icc.webp b/tests/Images/Input/Webp/container/truncated-icc.webp new file mode 100644 index 0000000000..7eb05a2191 --- /dev/null +++ b/tests/Images/Input/Webp/container/truncated-icc.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6f3b86b8917f0d15baf921aca9ba59e10a3d7e18715d25a5558c20315f4e8300 +size 68 diff --git a/tests/Images/Input/Webp/container/unknown-maximum.webp b/tests/Images/Input/Webp/container/unknown-maximum.webp new file mode 100644 index 0000000000..e1ec0b9e2b --- /dev/null +++ b/tests/Images/Input/Webp/container/unknown-maximum.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e60e811eedea648027b3d00ed8dd90af067f321cbef563f34ba5b3ca7511d897 +size 3205 diff --git a/tests/Images/Input/Webp/container/unknown-signed.webp b/tests/Images/Input/Webp/container/unknown-signed.webp new file mode 100644 index 0000000000..71d6e315af --- /dev/null +++ b/tests/Images/Input/Webp/container/unknown-signed.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de36e99d8323af50aefea60e3b27d67643acedc0b3de869dade661b36c96fb1d +size 3205 diff --git a/tests/Images/Input/Webp/container/unknown-unsigned.webp b/tests/Images/Input/Webp/container/unknown-unsigned.webp new file mode 100644 index 0000000000..1a4855b163 --- /dev/null +++ b/tests/Images/Input/Webp/container/unknown-unsigned.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e6e1941a7316d15d7ca0c6e4b61882a4b787fcd0ec8386743dd61d077944760 +size 3205 diff --git a/tests/Images/Input/Webp/container/vp8-crosses-riff.webp b/tests/Images/Input/Webp/container/vp8-crosses-riff.webp new file mode 100644 index 0000000000..753c262004 --- /dev/null +++ b/tests/Images/Input/Webp/container/vp8-crosses-riff.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5d7f5da05f1ae366c320cef29c6d0d83179e3c9f065ff897492fb58d9a3f8ea7 +size 94 diff --git a/tests/Images/Input/Webp/container/vp8l-crosses-riff.webp b/tests/Images/Input/Webp/container/vp8l-crosses-riff.webp new file mode 100644 index 0000000000..56d477fd49 --- /dev/null +++ b/tests/Images/Input/Webp/container/vp8l-crosses-riff.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3fa289ca9a94d7e50c03ff223105a8eab4b02e00c6eb732153a2200d9b5cbf15 +size 50 diff --git a/tests/Images/Input/Webp/container/vp8l-short-header.webp b/tests/Images/Input/Webp/container/vp8l-short-header.webp new file mode 100644 index 0000000000..ef3ad10bba --- /dev/null +++ b/tests/Images/Input/Webp/container/vp8l-short-header.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aec2a7c26017b39d6f18423b7e15ebdfb96036d5bc2e27df866c1883c7656a99 +size 50 diff --git a/tests/Images/Input/Webp/container/vp8x-crosses-riff.webp b/tests/Images/Input/Webp/container/vp8x-crosses-riff.webp new file mode 100644 index 0000000000..d007296019 --- /dev/null +++ b/tests/Images/Input/Webp/container/vp8x-crosses-riff.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ce585f18873be613f1d9973632d75cbdc770036fcf14bff783201c646fdb65e +size 15592 diff --git a/tests/Images/Input/Webp/container/vp8x-short-header.webp b/tests/Images/Input/Webp/container/vp8x-short-header.webp new file mode 100644 index 0000000000..a964445a7c --- /dev/null +++ b/tests/Images/Input/Webp/container/vp8x-short-header.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3aa4a880d820b65530cc1fc20f5cf6774ac09aeeaf16ce260f48e0bfeec0273d +size 15592 diff --git a/tests/Images/Input/Webp/container/xmp-oversized-40000000.webp b/tests/Images/Input/Webp/container/xmp-oversized-40000000.webp new file mode 100644 index 0000000000..97c45c7ffb --- /dev/null +++ b/tests/Images/Input/Webp/container/xmp-oversized-40000000.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:596b69b4326bac8ec8db5a29b5265de94d239bf3fbe0b0721427e1d546c19167 +size 42 diff --git a/tests/Images/Input/Webp/container/xmp-oversized-fffffffe.webp b/tests/Images/Input/Webp/container/xmp-oversized-fffffffe.webp new file mode 100644 index 0000000000..fc00d2479e --- /dev/null +++ b/tests/Images/Input/Webp/container/xmp-oversized-fffffffe.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2a9a98c6e135e9469c4317735437905b7f4aee3c7b00af819245c54f04b08940 +size 42 diff --git a/tests/Images/Input/Webp/container/xmp-oversized-ffffffff.webp b/tests/Images/Input/Webp/container/xmp-oversized-ffffffff.webp new file mode 100644 index 0000000000..a69249a378 --- /dev/null +++ b/tests/Images/Input/Webp/container/xmp-oversized-ffffffff.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ac4277b72303c6980f073fc73d02702ec12e6c4edf2a23c1a9639e63a9c70c5b +size 42 diff --git a/tests/Images/Input/Webp/container/xmp-trailing-40000000.webp b/tests/Images/Input/Webp/container/xmp-trailing-40000000.webp new file mode 100644 index 0000000000..bc665d229a --- /dev/null +++ b/tests/Images/Input/Webp/container/xmp-trailing-40000000.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:54e2b14a780d8aaa98d17a0126f307443bb1a05f5f333f0e1f91b7332df2fe09 +size 68 diff --git a/tests/Images/Input/Webp/container/xmp-trailing-fffffffe.webp b/tests/Images/Input/Webp/container/xmp-trailing-fffffffe.webp new file mode 100644 index 0000000000..3d28a9fd29 --- /dev/null +++ b/tests/Images/Input/Webp/container/xmp-trailing-fffffffe.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5c013fdff7494048bee6c312f2e537a68dc68d859aa6830e73ffdec8f2879fbf +size 68 diff --git a/tests/Images/Input/Webp/container/xmp-trailing-ffffffff.webp b/tests/Images/Input/Webp/container/xmp-trailing-ffffffff.webp new file mode 100644 index 0000000000..f6fed3fee0 --- /dev/null +++ b/tests/Images/Input/Webp/container/xmp-trailing-ffffffff.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44d29ce6553faed51f62e9fa4eb1136f386021deedca5427f96ef5b35911f99d +size 68