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