Browse Source

Normalize metadata extents and preserve decoder recovery policies

pull/3187/head
James Jackson-South 3 weeks ago
parent
commit
18532d7a74
  1. 72
      src/ImageSharp/Common/Extensions/BufferedReadStreamExtensions.cs
  2. 51
      src/ImageSharp/Common/Extensions/StreamExtensions.cs
  3. 5
      src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
  4. 2
      src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs
  5. 4
      src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs
  6. 18
      src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
  7. 8
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  8. 2
      src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs
  9. 2
      src/ImageSharp/Formats/Webp/BitReader/BitReaderBase.cs
  10. 4
      src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs
  11. 5
      src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
  12. 133
      src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs
  13. 18
      src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
  14. 60
      tests/ImageSharp.Tests/Common/BufferedReadStreamExtensionsTests.cs
  15. 111
      tests/ImageSharp.Tests/Common/StreamExtensionsTests.cs
  16. 29
      tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs
  17. 176
      tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs
  18. 4
      tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs

72
src/ImageSharp/Common/Extensions/BufferedReadStreamExtensions.cs

@ -0,0 +1,72 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.IO;
namespace SixLabors.ImageSharp;
/// <summary>
/// Extension methods for the <see cref="BufferedReadStream"/> type.
/// </summary>
internal static class BufferedReadStreamExtensions
{
/// <summary>
/// Determines whether the complete read range is contained in the stream.
/// </summary>
/// <param name="stream">The stream containing the data.</param>
/// <param name="offset">The absolute start of the range.</param>
/// <param name="length">The number of bytes in the range.</param>
/// <returns>Whether the range is contained in the stream.</returns>
public static bool IsReadRangeValid(this BufferedReadStream stream, long offset, ulong length)
{
// Compare the offset first so subtraction cannot underflow, and avoid
// adding an untrusted length to the offset where it could wrap around.
ulong streamLength = (ulong)stream.Length;
return (ulong)offset <= streamLength && length <= streamLength - (ulong)offset;
}
/// <summary>
/// Gets a buffer length when the complete read fits in both the stream and an integer-sized buffer.
/// </summary>
/// <param name="stream">The stream containing the data.</param>
/// <param name="length">The declared length in bytes.</param>
/// <param name="bufferLength">The validated length, or zero when the range is invalid.</param>
/// <returns>Whether the complete read is valid.</returns>
public static bool TryGetReadLength(this BufferedReadStream stream, ulong length, out int bufferLength)
{
if (length > int.MaxValue || !stream.IsReadRangeValid(stream.Position, length))
{
bufferLength = 0;
return false;
}
bufferLength = (int)length;
return true;
}
/// <summary>
/// Reads data from the stream into a slice of the provided buffer.
/// </summary>
/// <param name="stream">The stream.</param>
/// <param name="buffer">The buffer.</param>
/// <param name="offset">The offset within the buffer where bytes are read into.</param>
/// <param name="count">The number of bytes, if available, to read.</param>
/// <returns>The actual number of bytes read.</returns>
public static int Read(this BufferedReadStream stream, Span<byte> buffer, int offset, int count)
=> stream.Read(buffer.Slice(offset, count));
/// <summary>
/// Advances the stream by the specified number of bytes. Nonpositive counts are ignored.
/// </summary>
/// <param name="stream">The stream.</param>
/// <param name="count">The number of bytes to skip.</param>
public static void Skip(this BufferedReadStream stream, int count)
{
if (count > 0)
{
// BufferedReadStream is always seekable; its position setter preserves
// buffered data when the destination is inside the current buffer.
stream.Position += count;
}
}
}

51
src/ImageSharp/Common/Extensions/StreamExtensions.cs

@ -1,8 +1,6 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Buffers;
namespace SixLabors.ImageSharp; namespace SixLabors.ImageSharp;
/// <summary> /// <summary>
@ -19,53 +17,4 @@ internal static class StreamExtensions
/// <param name="count">The number of bytes to write to the stream.</param> /// <param name="count">The number of bytes to write to the stream.</param>
public static void Write(this Stream stream, Span<byte> buffer, int offset, int count) public static void Write(this Stream stream, Span<byte> buffer, int offset, int count)
=> stream.Write(buffer.Slice(offset, count)); => stream.Write(buffer.Slice(offset, count));
/// <summary>
/// Reads data from a stream into the provided buffer.
/// </summary>
/// <param name="stream">The stream.</param>
/// <param name="buffer">The buffer.</param>
/// <param name="offset">The offset within the buffer where the bytes are read into.</param>
/// <param name="count">The number of bytes, if available, to read.</param>
/// <returns>The actual number of bytes read.</returns>
public static int Read(this Stream stream, Span<byte> buffer, int offset, int count)
=> stream.Read(buffer.Slice(offset, count));
/// <summary>
/// Skips the number of bytes in the given stream.
/// </summary>
/// <param name="stream">The stream.</param>
/// <param name="count">A byte offset relative to the origin parameter.</param>
public static void Skip(this Stream stream, int count)
{
if (count < 1)
{
return;
}
if (stream.CanSeek)
{
stream.Seek(count, SeekOrigin.Current);
return;
}
byte[] buffer = ArrayPool<byte>.Shared.Rent(count);
try
{
while (count > 0)
{
int bytesRead = stream.Read(buffer, 0, count);
if (bytesRead == 0)
{
break;
}
count -= bytesRead;
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
} }

5
src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs

@ -1429,7 +1429,7 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
// > 108 bytes // > 108 bytes
infoHeaderType = BmpInfoHeaderType.WinVersion5; infoHeaderType = BmpInfoHeaderType.WinVersion5;
this.infoHeader = BmpInfoHeader.ParseV5(buffer); this.infoHeader = BmpInfoHeader.ParseV5(buffer);
if (this.infoHeader.ProfileData != 0 && this.infoHeader.ProfileSize != 0) if (!this.Options.SkipMetadata && this.infoHeader.ProfileData != 0 && this.infoHeader.ProfileSize != 0)
{ {
long streamPosition = stream.Position; long streamPosition = stream.Position;
this.ExecuteAncillarySegmentAction(() => this.ReadIccProfile(stream, this.metadata, infoHeaderStart)); this.ExecuteAncillarySegmentAction(() => this.ReadIccProfile(stream, this.metadata, infoHeaderStart));
@ -1477,8 +1477,7 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
long profileStart = infoHeaderStart + this.infoHeader.ProfileData; long profileStart = infoHeaderStart + this.infoHeader.ProfileData;
if (this.infoHeader.ProfileData < 0 || if (this.infoHeader.ProfileData < 0 ||
this.infoHeader.ProfileSize <= 0 || this.infoHeader.ProfileSize <= 0 ||
profileStart > stream.Length || !stream.IsReadRangeValid(profileStart, (uint)this.infoHeader.ProfileSize))
this.infoHeader.ProfileSize > stream.Length - profileStart)
{ {
BmpThrowHelper.ThrowInvalidImageContentException("Not enough data to read BMP ICC profile."); BmpThrowHelper.ThrowInvalidImageContentException("Not enough data to read BMP ICC profile.");
} }

2
src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs

@ -59,7 +59,7 @@ internal abstract class ExrBaseDecompressor : ExrBaseCompression
int totalRead = 0; int totalRead = 0;
while (totalRead < uncompressedBytes) while (totalRead < uncompressedBytes)
{ {
int bytesRead = dataStream.Read(uncompressed, totalRead, (int)uncompressedBytes - totalRead); int bytesRead = dataStream.Read(uncompressed.Slice(totalRead, (int)uncompressedBytes - totalRead));
if (bytesRead <= 0) if (bytesRead <= 0)
{ {
break; break;

4
src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs

@ -28,7 +28,7 @@ internal readonly struct GifXmpApplicationExtension : IGifExtension
/// <param name="stream">The stream to read from.</param> /// <param name="stream">The stream to read from.</param>
/// <param name="allocator">The memory allocator.</param> /// <param name="allocator">The memory allocator.</param>
/// <returns>The XMP metadata</returns> /// <returns>The XMP metadata</returns>
public static GifXmpApplicationExtension Read(Stream stream, MemoryAllocator allocator) public static GifXmpApplicationExtension Read(BufferedReadStream stream, MemoryAllocator allocator)
{ {
byte[] xmpBytes = ReadXmpData(stream, allocator, out bool terminated); byte[] xmpBytes = ReadXmpData(stream, allocator, out bool terminated);
if (!terminated) if (!terminated)
@ -75,7 +75,7 @@ internal readonly struct GifXmpApplicationExtension : IGifExtension
return this.ContentLength; return this.ContentLength;
} }
private static byte[] ReadXmpData(Stream stream, MemoryAllocator allocator, out bool terminated) private static byte[] ReadXmpData(BufferedReadStream stream, MemoryAllocator allocator, out bool terminated)
{ {
using ChunkedMemoryStream bytes = new(allocator); using ChunkedMemoryStream bytes = new(allocator);

18
src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs

@ -274,10 +274,9 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
// Get the marker length. // Get the marker length.
int markerContentByteSize = ReadUint16(stream, markerBuffer) - 2; int markerContentByteSize = ReadUint16(stream, markerBuffer) - 2;
// Check whether the stream actually has enough bytes to read // Validate the entire segment before parsing it. Casting directly
// markerContentByteSize is always positive so we cast // to ulong also rejects lengths smaller than the two-byte length field.
// to uint to avoid sign extension if (!stream.IsReadRangeValid(stream.Position, (ulong)markerContentByteSize))
if (stream.RemainingBytes < (uint)markerContentByteSize)
{ {
JpegThrowHelper.ThrowNotEnoughBytesForMarker(fileMarker.Marker); JpegThrowHelper.ThrowNotEnoughBytesForMarker(fileMarker.Marker);
} }
@ -351,10 +350,9 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
// Get the marker length. // Get the marker length.
int markerContentByteSize = ReadUint16(stream, markerBuffer) - 2; int markerContentByteSize = ReadUint16(stream, markerBuffer) - 2;
// Check whether stream actually has enough bytes to read // Validate the entire segment before parsing it. Casting directly
// markerContentByteSize is always positive so we cast // to ulong also rejects lengths smaller than the two-byte length field.
// to uint to avoid sign extension. if (!stream.IsReadRangeValid(stream.Position, (ulong)markerContentByteSize))
if (stream.RemainingBytes < (uint)markerContentByteSize)
{ {
if (metadataOnly && this.Metadata != null && this.Frame != null) if (metadataOnly && this.Metadata != null && this.Frame != null)
{ {
@ -841,7 +839,7 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
// TODO: thumbnail // TODO: thumbnail
if (remaining > 0) if (remaining > 0)
{ {
if (stream.Position + remaining >= stream.Length) if (!stream.IsReadRangeValid(stream.Position, (ulong)remaining + 1))
{ {
this.ThrowOrIgnoreNonStrictSegmentError("Bad App0 Marker length."); this.ThrowOrIgnoreNonStrictSegmentError("Bad App0 Marker length.");
stream.Skip(remaining); stream.Skip(remaining);
@ -877,7 +875,7 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
return; return;
} }
if (stream.Position + remaining >= stream.Length) if (!stream.IsReadRangeValid(stream.Position, (ulong)remaining + 1))
{ {
this.ThrowOrIgnoreNonStrictSegmentError("Bad App1 Marker length."); this.ThrowOrIgnoreNonStrictSegmentError("Bad App1 Marker length.");
stream.Skip(remaining); stream.Skip(remaining);

8
src/ImageSharp/Formats/Png/PngDecoderCore.cs

@ -884,7 +884,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore
while (currentRowBytesRead < bytesPerFrameScanline) while (currentRowBytesRead < bytesPerFrameScanline)
{ {
int bytesRead = compressedStream.Read(scanSpan, currentRowBytesRead, bytesPerFrameScanline - currentRowBytesRead); int bytesRead = compressedStream.Read(scanSpan.Slice(currentRowBytesRead, bytesPerFrameScanline - currentRowBytesRead));
if (bytesRead <= 0) if (bytesRead <= 0)
{ {
goto EXIT; goto EXIT;
@ -1016,7 +1016,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
while (currentRowBytesRead < bytesPerInterlaceScanline) while (currentRowBytesRead < bytesPerInterlaceScanline)
{ {
int bytesRead = compressedStream.Read(this.scanline.GetSpan(), currentRowBytesRead, bytesPerInterlaceScanline - currentRowBytesRead); int bytesRead = compressedStream.Read(this.scanline.GetSpan().Slice(currentRowBytesRead, bytesPerInterlaceScanline - currentRowBytesRead));
if (bytesRead <= 0) if (bytesRead <= 0)
{ {
goto EXIT; goto EXIT;
@ -1984,7 +1984,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore
return false; return false;
} }
int bytesRead = inflateStream.CompressedStream.Read(destUncompressedData, 0, destUncompressedData.Length); int bytesRead = inflateStream.CompressedStream.Read(destUncompressedData);
while (bytesRead != 0) while (bytesRead != 0)
{ {
if (memoryStreamOutput.Length > maxLength) if (memoryStreamOutput.Length > maxLength)
@ -1994,7 +1994,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore
} }
memoryStreamOutput.Write(destUncompressedData[..bytesRead]); memoryStreamOutput.Write(destUncompressedData[..bytesRead]);
bytesRead = inflateStream.CompressedStream.Read(destUncompressedData, 0, destUncompressedData.Length); bytesRead = inflateStream.CompressedStream.Read(destUncompressedData);
} }
uncompressedBytesArray = memoryStreamOutput.ToArray(); uncompressedBytesArray = memoryStreamOutput.ToArray();

2
src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs

@ -69,7 +69,7 @@ internal sealed class DeflateTiffCompression : TiffBaseDecompressor
int totalRead = 0; int totalRead = 0;
while (totalRead < buffer.Length) while (totalRead < buffer.Length)
{ {
int bytesRead = dataStream.Read(buffer, totalRead, buffer.Length - totalRead); int bytesRead = dataStream.Read(buffer[totalRead..]);
if (bytesRead <= 0) if (bytesRead <= 0)
{ {
break; break;

2
src/ImageSharp/Formats/Webp/BitReader/BitReaderBase.cs

@ -34,7 +34,7 @@ internal abstract class BitReaderBase : IDisposable
{ {
IMemoryOwner<byte> data = memoryAllocator.Allocate<byte>(bytesToRead, AllocationOptions.Clean); IMemoryOwner<byte> data = memoryAllocator.Allocate<byte>(bytesToRead, AllocationOptions.Clean);
Span<byte> dataSpan = data.Memory.Span; Span<byte> dataSpan = data.Memory.Span;
input.Read(dataSpan[..bytesToRead], 0, bytesToRead); input.Read(dataSpan[..bytesToRead]);
return data; return data;
} }

4
src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs

@ -1,6 +1,8 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.IO;
namespace SixLabors.ImageSharp.Formats.Webp.Chunks; namespace SixLabors.ImageSharp.Formats.Webp.Chunks;
internal readonly struct WebpFrameData internal readonly struct WebpFrameData
@ -120,7 +122,7 @@ internal readonly struct WebpFrameData
/// </summary> /// </summary>
/// <param name="stream">The stream to read from.</param> /// <param name="stream">The stream to read from.</param>
/// <returns>Animation frame data.</returns> /// <returns>Animation frame data.</returns>
public static WebpFrameData Parse(Stream stream) public static WebpFrameData Parse(BufferedReadStream stream)
{ {
Span<byte> buffer = stackalloc byte[4]; Span<byte> buffer = stackalloc byte[4];

5
src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs

@ -381,10 +381,7 @@ internal class WebpAnimationDecoder : IDisposable
switch (chunkType) switch (chunkType)
{ {
case WebpChunkType.Iccp: case WebpChunkType.Iccp:
WebpChunkParsingUtils.ReadIccProfile(stream, imageMetadata, ignoreMetadata, this.executeAncillarySegmentAction);
// While ICC profiles are optional, an invalid ICC profile cannot be ignored because it must
// precede the frame data, and we cannot safely skip it without successfully reading its size.
WebpChunkParsingUtils.ReadIccProfile(stream, imageMetadata, ignoreMetadata);
break; break;
case WebpChunkType.Exif: case WebpChunkType.Exif:
this.executeAncillarySegmentAction(() => WebpChunkParsingUtils.ReadExifProfile(stream, imageMetadata, ignoreMetadata)); this.executeAncillarySegmentAction(() => WebpChunkParsingUtils.ReadExifProfile(stream, imageMetadata, ignoreMetadata));

133
src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs

@ -262,7 +262,7 @@ internal static class WebpChunkParsingUtils
/// <exception cref="ImageFormatException"> /// <exception cref="ImageFormatException">
/// Thrown if the input stream is not valid. /// Thrown if the input stream is not valid.
/// </exception> /// </exception>
public static uint ReadUInt24LittleEndian(Stream stream, Span<byte> buffer) public static uint ReadUInt24LittleEndian(BufferedReadStream stream, Span<byte> buffer)
{ {
if (stream.Read(buffer, 0, 3) == 3) if (stream.Read(buffer, 0, 3) == 3)
{ {
@ -306,12 +306,33 @@ internal static class WebpChunkParsingUtils
/// <param name="required">If true, the chunk size is required to be read, otherwise it can be skipped.</param> /// <param name="required">If true, the chunk size is required to be read, otherwise it can be skipped.</param>
/// <returns>The chunk size in bytes.</returns> /// <returns>The chunk size in bytes.</returns>
/// <exception cref="ImageFormatException">Thrown if the input stream is not valid.</exception> /// <exception cref="ImageFormatException">Thrown if the input stream is not valid.</exception>
public static uint ReadChunkSize(Stream stream, Span<byte> buffer, bool required = true) public static uint ReadChunkSize(BufferedReadStream stream, Span<byte> buffer, bool required = true)
{
ulong chunkSize = ReadPaddedChunkSize(stream, buffer, required);
// 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)
{
WebpThrowHelper.ThrowInvalidImageContentException("WebP chunk size exceeds the supported maximum.");
}
return (uint)chunkSize;
}
/// <summary>
/// Reads a chunk's complete padded extent without wrapping a uint-sized payload length.
/// </summary>
/// <param name="stream">The input stream.</param>
/// <param name="buffer">The four-byte size buffer.</param>
/// <param name="required">Whether an incomplete size field is an error.</param>
/// <returns>The padded extent, or remaining bytes when an optional size field is incomplete.</returns>
private static ulong ReadPaddedChunkSize(BufferedReadStream stream, Span<byte> buffer, bool required)
{ {
if (stream.Read(buffer) is 4) if (stream.Read(buffer) is 4)
{ {
uint chunkSize = BinaryPrimitives.ReadUInt32LittleEndian(buffer); uint chunkSize = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
return chunkSize % 2 is 0 ? chunkSize : chunkSize + 1; return (ulong)chunkSize + (chunkSize & 1);
} }
if (required) if (required)
@ -320,7 +341,7 @@ internal static class WebpChunkParsingUtils
} }
// Return the size of the remaining data in the stream. // Return the size of the remaining data in the stream.
return (uint)(stream.Length - stream.Position); return (ulong)stream.RemainingBytes;
} }
/// <summary> /// <summary>
@ -349,34 +370,36 @@ internal static class WebpChunkParsingUtils
/// <param name="stream">The stream to decode from.</param> /// <param name="stream">The stream to decode from.</param>
/// <param name="metadata">The image metadata.</param> /// <param name="metadata">The image metadata.</param>
/// <param name="ignoreMetadata">If true, metadata will be ignored.</param> /// <param name="ignoreMetadata">If true, metadata will be ignored.</param>
/// <param name="executeAncillarySegmentAction">Executes profile parsing under the decoder's integrity policy.</param>
public static void ReadIccProfile( public static void ReadIccProfile(
BufferedReadStream stream, BufferedReadStream stream,
ImageMetadata metadata, ImageMetadata metadata,
bool ignoreMetadata) bool ignoreMetadata,
Action<Action> executeAncillarySegmentAction)
{ {
Span<byte> buffer = stackalloc byte[4]; ulong chunkSize = ReadPaddedChunkSize(stream, stackalloc byte[4], true);
int iccpChunkSize = ValidateMetadataChunkSize(stream, ReadChunkSize(stream, buffer), "ICCP");
if (ignoreMetadata || metadata.IccProfile != null) // 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))
{ {
stream.Skip(iccpChunkSize); WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the ICCP chunk.");
} }
else
executeAncillarySegmentAction(() =>
{ {
byte[] iccpData = new byte[iccpChunkSize]; byte[]? iccpData = ReadMetadataChunk(stream, chunkSize, ignoreMetadata || metadata.IccProfile != null);
int bytesRead = stream.Read(iccpData, 0, iccpChunkSize); if (iccpData is not null)
if (bytesRead != iccpChunkSize)
{ {
WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the iccp chunk"); IccProfile profile = new(iccpData);
} if (!profile.CheckIsValid())
{
throw new InvalidIccProfileException("Invalid ICC profile.");
}
IccProfile profile = new(iccpData); metadata.IccProfile = profile;
if (!profile.CheckIsValid())
{
throw new InvalidIccProfileException("Invalid ICC profile.");
} }
});
metadata.IccProfile = profile;
}
} }
/// <summary> /// <summary>
@ -390,21 +413,10 @@ internal static class WebpChunkParsingUtils
ImageMetadata metadata, ImageMetadata metadata,
bool ignoreMetadata) bool ignoreMetadata)
{ {
Span<byte> buffer = stackalloc byte[4]; ulong chunkSize = ReadPaddedChunkSize(stream, stackalloc byte[4], !ignoreMetadata);
int exifChunkSize = ValidateMetadataChunkSize(stream, ReadChunkSize(stream, buffer), "EXIF"); byte[]? exifData = ReadMetadataChunk(stream, chunkSize, ignoreMetadata || metadata.ExifProfile != null);
if (ignoreMetadata || metadata.ExifProfile != null) if (exifData is not null)
{
stream.Skip(exifChunkSize);
}
else
{ {
byte[] exifData = new byte[exifChunkSize];
int bytesRead = stream.Read(exifData, 0, exifChunkSize);
if (bytesRead != exifChunkSize)
{
WebpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for the EXIF profile");
}
ExifProfile exifProfile = new(exifData); ExifProfile exifProfile = new(exifData);
// Set the resolution from the metadata. // Set the resolution from the metadata.
@ -433,33 +445,46 @@ internal static class WebpChunkParsingUtils
ImageMetadata metadata, ImageMetadata metadata,
bool ignoreMetadata) bool ignoreMetadata)
{ {
Span<byte> buffer = stackalloc byte[4]; ulong chunkSize = ReadPaddedChunkSize(stream, stackalloc byte[4], !ignoreMetadata);
int xmpChunkSize = ValidateMetadataChunkSize(stream, ReadChunkSize(stream, buffer), "XMP"); byte[]? xmpData = ReadMetadataChunk(stream, chunkSize, ignoreMetadata || metadata.XmpProfile != null);
if (ignoreMetadata || metadata.XmpProfile != null) if (xmpData is not null)
{ {
stream.Skip(xmpChunkSize);
}
else
{
byte[] xmpData = new byte[xmpChunkSize];
int bytesRead = stream.Read(xmpData, 0, xmpChunkSize);
if (bytesRead != xmpChunkSize)
{
WebpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for the XMP profile");
}
metadata.XmpProfile = new XmpProfile(xmpData); metadata.XmpProfile = new XmpProfile(xmpData);
} }
} }
private static int ValidateMetadataChunkSize(BufferedReadStream stream, uint chunkSize, string chunkName) /// <summary>
/// Reads a metadata payload, leaving the stream at the next chunk or EOF on a recoverable error.
/// Callers execute metadata parsing under the decoder's ancillary integrity policy.
/// </summary>
/// <param name="stream">The input stream positioned at the chunk payload.</param>
/// <param name="paddedLength">The declared extent including its padding byte.</param>
/// <param name="ignoreMetadata">Whether to skip the payload without parsing it.</param>
/// <returns>The payload, or null when metadata is skipped.</returns>
private static byte[]? ReadMetadataChunk(BufferedReadStream stream, ulong paddedLength, bool ignoreMetadata)
{ {
if (chunkSize > int.MaxValue || chunkSize > stream.Length - stream.Position) long chunkEnd = stream.Position + (long)Math.Min(paddedLength, (ulong)stream.RemainingBytes);
if (ignoreMetadata)
{
stream.Position = chunkEnd;
return null;
}
if (!stream.TryGetReadLength(paddedLength, out int bufferLength))
{
// Ignoring an ancillary error must not make the next parser interpret
// this payload as another chunk header. A truncated chunk consumes EOF.
stream.Position = chunkEnd;
WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the metadata chunk.");
}
byte[] data = new byte[bufferLength];
if (stream.Read(data) != bufferLength)
{ {
WebpThrowHelper.ThrowInvalidImageContentException($"Not enough data to read the {chunkName} chunk"); WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the metadata chunk.");
} }
return (int)chunkSize; return data;
} }
private static double GetExifResolutionValue(ExifProfile exifProfile, ExifTag<Rational> tag) private static double GetExifResolutionValue(ExifProfile exifProfile, ExifTag<Rational> tag)

18
src/ImageSharp/Formats/Webp/WebpDecoderCore.cs

@ -92,6 +92,13 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable
return animationDecoder.Decode<TPixel>(stream, this.webImageInfo.Features, this.webImageInfo.Width, this.webImageInfo.Height, fileSize); return animationDecoder.Decode<TPixel>(stream, this.webImageInfo.Features, this.webImageInfo.Width, this.webImageInfo.Height, fileSize);
} }
// A VP8X header alone describes a canvas, not decodable image data.
// Ignoring a truncated optional chunk must not bypass this requirement.
if (this.webImageInfo.Vp8BitReader is null && this.webImageInfo.Vp8LBitReader is null)
{
WebpThrowHelper.ThrowInvalidImageContentException("Missing WebP image data.");
}
image = new Image<TPixel>(this.configuration, (int)this.webImageInfo.Width, (int)this.webImageInfo.Height, metadata); image = new Image<TPixel>(this.configuration, (int)this.webImageInfo.Width, (int)this.webImageInfo.Height, metadata);
Buffer2D<TPixel> pixels = image.GetRootFramePixelBuffer(); Buffer2D<TPixel> pixels = image.GetRootFramePixelBuffer();
if (this.webImageInfo.IsLossless) if (this.webImageInfo.IsLossless)
@ -278,10 +285,7 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable
switch (chunkType) switch (chunkType)
{ {
case WebpChunkType.Iccp: case WebpChunkType.Iccp:
WebpChunkParsingUtils.ReadIccProfile(stream, metadata, ignoreMetadata, this.ExecuteAncillarySegmentAction);
// While ICC profiles are optional, an invalid ICC profile cannot be ignored because it must
// precede the image data, and we cannot safely skip it without successfully reading its size.
WebpChunkParsingUtils.ReadIccProfile(stream, metadata, ignoreMetadata);
break; break;
case WebpChunkType.Exif: case WebpChunkType.Exif:
@ -330,17 +334,17 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable
{ {
// Read chunk header. // Read chunk header.
WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer); WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer);
if (chunkType == WebpChunkType.Exif && metadata.ExifProfile == null) if (chunkType == WebpChunkType.Exif)
{ {
this.ExecuteAncillarySegmentAction(() => WebpChunkParsingUtils.ReadExifProfile(stream, metadata, ignoreMetadata)); this.ExecuteAncillarySegmentAction(() => WebpChunkParsingUtils.ReadExifProfile(stream, metadata, ignoreMetadata));
} }
else if (chunkType == WebpChunkType.Xmp && metadata.XmpProfile == null) else if (chunkType == WebpChunkType.Xmp)
{ {
this.ExecuteAncillarySegmentAction(() => WebpChunkParsingUtils.ReadXmpProfile(stream, metadata, ignoreMetadata)); this.ExecuteAncillarySegmentAction(() => WebpChunkParsingUtils.ReadXmpProfile(stream, metadata, ignoreMetadata));
} }
else else
{ {
// Skip duplicate XMP or EXIF chunk. // Skip unknown chunks.
uint chunkLength = WebpChunkParsingUtils.ReadChunkSize(stream, buffer, false); uint chunkLength = WebpChunkParsingUtils.ReadChunkSize(stream, buffer, false);
stream.Skip((int)chunkLength); stream.Skip((int)chunkLength);
} }

60
tests/ImageSharp.Tests/Common/BufferedReadStreamExtensionsTests.cs

@ -0,0 +1,60 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.IO;
namespace SixLabors.ImageSharp.Tests.Common;
public class BufferedReadStreamExtensionsTests
{
[Theory]
[InlineData(0L, 8UL, true)]
[InlineData(8L, 0UL, true)]
[InlineData(7L, 2UL, false)]
[InlineData(9L, 0UL, false)]
[InlineData(-1L, 1UL, false)]
[InlineData(long.MaxValue, ulong.MaxValue, false)]
[InlineData(0L, ulong.MaxValue, false)]
public void IsReadRangeValid_ChecksCompleteExtent(long offset, ulong length, bool expected)
{
using MemoryStream input = new(new byte[8]);
using BufferedReadStream stream = new(Configuration.Default, input);
Assert.Equal(expected, stream.IsReadRangeValid(offset, length));
Assert.Equal(0, stream.Position);
}
[Theory]
[InlineData(0UL, true, 0)]
[InlineData(6UL, true, 6)]
[InlineData(7UL, false, 0)]
[InlineData(1073741824UL, false, 0)]
[InlineData(4294967294UL, false, 0)]
[InlineData(4294967296UL, false, 0)]
[InlineData(ulong.MaxValue, false, 0)]
public void TryGetReadLength_ReturnsResultWithoutMovingStream(ulong length, bool expected, int expectedLength)
{
using MemoryStream input = new(new byte[8]);
using BufferedReadStream stream = new(Configuration.Default, input);
stream.Position = 2;
Assert.Equal(expected, stream.TryGetReadLength(length, out int bufferLength));
Assert.Equal(expectedLength, bufferLength);
Assert.Equal(2, stream.Position);
}
[Theory]
[InlineData(0)]
[InlineData(-1)]
public void Skip_CountZeroOrLower_PositionNotChanged(int count)
{
using MemoryStream input = new(new byte[8]);
using BufferedReadStream stream = new(Configuration.Default, input);
stream.Position = 4;
stream.Skip(count);
Assert.Equal(4, stream.Position);
Assert.Equal(0, stream.ReadByte());
}
}

111
tests/ImageSharp.Tests/Common/StreamExtensionsTests.cs

@ -1,111 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Tests.Common;
public class StreamExtensionsTests
{
[Theory]
[InlineData(0)]
[InlineData(-1)]
public void Skip_CountZeroOrLower_PositionNotChanged(int count)
{
using (MemoryStream memStream = new(5))
{
memStream.Position = 4;
memStream.Skip(count);
Assert.Equal(4, memStream.Position);
}
}
[Fact]
public void Skip_SeekableStream_SeekIsCalled()
{
using (SeekableStream seekableStream = new(4))
{
seekableStream.Skip(4);
Assert.Equal(4, seekableStream.Offset);
Assert.Equal(SeekOrigin.Current, seekableStream.Loc);
}
}
[Fact]
public void Skip_NonSeekableStream_BytesAreRead()
{
using (NonSeekableStream nonSeekableStream = new())
{
nonSeekableStream.Skip(5);
Assert.Equal(3, nonSeekableStream.Counts.Count);
Assert.Equal(5, nonSeekableStream.Counts[0]);
Assert.Equal(3, nonSeekableStream.Counts[1]);
Assert.Equal(1, nonSeekableStream.Counts[2]);
}
}
[Fact]
public void Skip_EofStream_NoExceptionIsThrown()
{
using (EofStream eofStream = new(7))
{
eofStream.Skip(7);
Assert.Equal(0, eofStream.Position);
}
}
private class SeekableStream : MemoryStream
{
public long Offset;
public SeekOrigin Loc;
public SeekableStream(int capacity)
: base(capacity)
{
}
public override long Seek(long offset, SeekOrigin loc)
{
this.Offset = offset;
this.Loc = loc;
return base.Seek(offset, loc);
}
}
private class NonSeekableStream : MemoryStream
{
public override bool CanSeek => false;
public List<int> Counts = new();
public NonSeekableStream()
: base(4)
{
}
public override int Read(byte[] buffer, int offset, int count)
{
this.Counts.Add(count);
return Math.Min(2, count);
}
}
private class EofStream : MemoryStream
{
public override bool CanSeek => false;
public EofStream(int capacity)
: base(capacity)
{
}
public override int Read(byte[] buffer, int offset, int count)
{
return 0;
}
}
}

29
tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs

@ -34,8 +34,14 @@ public class BmpDecoderTests
{ RLE8, 2835, 2835, PixelResolutionUnit.PixelsPerMeter } { RLE8, 2835, 2835, PixelResolutionUnit.PixelsPerMeter }
}; };
[Fact] [Theory]
public void Decode_WithProfileLargerThanRemainingData_ThrowsInStrictMode() [InlineData(SegmentIntegrityHandling.Strict, false)]
[InlineData(SegmentIntegrityHandling.IgnoreAncillary, false)]
[InlineData(SegmentIntegrityHandling.IgnoreImageData, false)]
[InlineData(SegmentIntegrityHandling.Strict, true)]
[InlineData(SegmentIntegrityHandling.IgnoreAncillary, true)]
[InlineData(SegmentIntegrityHandling.IgnoreImageData, true)]
public void Decode_WithProfileLargerThanRemainingData_RespectsOptions(SegmentIntegrityHandling integrityHandling, bool skipMetadata)
{ {
byte[] payload = Convert.FromHexString( byte[] payload = Convert.FromHexString(
"424D8E000000000000008A0000007C0000000100000001000000010018000000" + "424D8E000000000000008A0000007C0000000100000001000000010018000000" +
@ -43,9 +49,24 @@ public class BmpDecoderTests
"0000000000000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000000000000000000000" +
"000000000000000000000000000000000000000000000000000000000000C800" + "000000000000000000000000000000000000000000000000000000000000C800" +
"00000000004000000000000000"); "00000000004000000000000000");
DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict }; DecoderOptions options = new() { SegmentIntegrityHandling = integrityHandling, SkipMetadata = skipMetadata };
Assert.Throws<InvalidImageContentException>(() => Image.Load(options, payload)); if (integrityHandling is SegmentIntegrityHandling.Strict && !skipMetadata)
{
Assert.Throws<InvalidImageContentException>(() => Image.Load(options, payload));
Assert.Throws<InvalidImageContentException>(() => Image.Identify(options, payload));
}
else
{
using Image<Rgba32> image = Image.Load<Rgba32>(options, payload);
Assert.Equal(new Size(1, 1), image.Size);
Assert.Equal(new Rgba32(0, 0, 0), image[0, 0]);
Assert.Null(image.Metadata.IccProfile);
ImageInfo info = Image.Identify(options, payload);
Assert.Equal(image.Size, info.Size);
Assert.Null(info.Metadata.IccProfile);
}
} }
[Theory] [Theory]

176
tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs

@ -1,9 +1,12 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Buffers.Binary;
using System.Text;
using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.TestUtilities; using SixLabors.ImageSharp.Tests.TestUtilities;
@ -13,6 +16,35 @@ namespace SixLabors.ImageSharp.Tests.Formats.Webp;
[Trait("Format", "Webp")] [Trait("Format", "Webp")]
public class WebpMetaDataTests public class WebpMetaDataTests
{ {
public static IEnumerable<object[]> IccMetadataOptions()
{
foreach (SegmentIntegrityHandling integrity in new[] { SegmentIntegrityHandling.Strict, SegmentIntegrityHandling.IgnoreAncillary, SegmentIntegrityHandling.IgnoreImageData })
{
foreach (bool skipMetadata in new[] { false, true })
{
foreach (bool animated in new[] { false, true })
{
yield return new object[] { integrity, skipMetadata, animated };
}
}
}
}
public static IEnumerable<object[]> TruncatedMetadataOptions()
{
foreach (string chunkType in new[] { "EXIF", "XMP " })
{
foreach (uint length in new[] { 0x40000000U, 0xFFFFFFFEU, uint.MaxValue })
{
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 };
}
}
}
}
[Theory] [Theory]
[WithFile(TestImages.Webp.Lossy.BikeWithExif, PixelTypes.Rgba32, false)] [WithFile(TestImages.Webp.Lossy.BikeWithExif, PixelTypes.Rgba32, false)]
[WithFile(TestImages.Webp.Lossy.BikeWithExif, PixelTypes.Rgba32, true)] [WithFile(TestImages.Webp.Lossy.BikeWithExif, PixelTypes.Rgba32, true)]
@ -229,23 +261,151 @@ public class WebpMetaDataTests
}); });
} }
[Fact] [Theory]
public void Decode_WithOversizedIccChunk_ThrowsInvalidImageContentException() [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( byte[] payload = Convert.FromHexString(
"524946462200000057454250565038580A0000002000000000000000000049434350FEFFFFFF01020304"); "524946462200000057454250565038580A0000002000000000000000000049434350FEFFFFFF01020304");
Encoding.ASCII.GetBytes(chunkType, payload.AsSpan(30, 4));
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(34), length);
DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict };
Assert.Throws<InvalidImageContentException>(() => Image.Load(payload)); Assert.Throws<InvalidImageContentException>(() => Image.Load(options, payload));
Assert.Throws<InvalidImageContentException>(() => Image.Identify(payload)); Assert.Throws<InvalidImageContentException>(() => Image.Identify(options, payload));
} }
[Fact] [Theory]
public void Decode_WithIccChunkLargerThanRemainingData_ThrowsInvalidImageContentException() [InlineData("ICCP")]
[InlineData("EXIF")]
[InlineData("XMP ")]
public void Decode_WithMetadataChunkLargerThanRemainingData_ThrowsInStrictMode(string chunkType)
{ {
byte[] payload = Convert.FromHexString( byte[] payload = Convert.FromHexString(
"524946460000000057454250565038580A00000020000000010000010000494343500000004000000000"); "524946460000000057454250565038580A00000020000000010000010000494343500000004000000000");
Encoding.ASCII.GetBytes(chunkType, payload.AsSpan(30, 4));
DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict };
Assert.Throws<InvalidImageContentException>(() => Image.Load(options, payload));
Assert.Throws<InvalidImageContentException>(() => Image.Identify(options, payload));
}
[Theory]
[MemberData(nameof(TruncatedMetadataOptions))]
public void Decode_TruncatedTrailingMetadata_RespectsOptions(string chunkType, uint length, SegmentIntegrityHandling integrity, bool skipMetadata)
{
byte[] payload = CreateWebpWithMetadata(chunkType, length, false, false);
DecoderOptions options = new() { SegmentIntegrityHandling = integrity, SkipMetadata = skipMetadata };
if (integrity is SegmentIntegrityHandling.Strict && !skipMetadata)
{
Assert.Throws<InvalidImageContentException>(() => Image.Load(options, payload));
Assert.Throws<InvalidImageContentException>(() => Image.Identify(options, payload));
}
else
{
using Image<Rgba32> image = Image.Load<Rgba32>(options, payload);
Assert.Equal(new Size(2, 2), image.Size);
for (int y = 0; y < image.Height; y++)
{
for (int x = 0; x < image.Width; x++)
{
Assert.Equal(new Rgba32(17, 34, 51), image[x, y]);
}
}
Assert.Null(image.Metadata.ExifProfile);
Assert.Null(image.Metadata.XmpProfile);
ImageInfo info = Image.Identify(options, payload);
Assert.Equal(image.Size, info.Size);
Assert.Null(info.Metadata.ExifProfile);
Assert.Null(info.Metadata.XmpProfile);
}
}
[Theory]
[MemberData(nameof(IccMetadataOptions))]
public void Decode_InvalidIccPayload_RespectsOptionsAndReadsFollowingImage(SegmentIntegrityHandling integrity, bool skipMetadata, bool animated)
{
byte[] payload = CreateWebpWithMetadata("ICCP", 4, true, animated);
DecoderOptions options = new() { SegmentIntegrityHandling = integrity, SkipMetadata = skipMetadata };
if (integrity is SegmentIntegrityHandling.Strict && !skipMetadata)
{
Assert.Throws<InvalidIccProfileException>(() => Image.Load(options, payload));
Assert.Throws<InvalidIccProfileException>(() => Image.Identify(options, payload));
}
else
{
using Image<Rgba32> image = Image.Load<Rgba32>(options, payload);
Assert.Equal(new Size(2, 2), image.Size);
Assert.Equal(animated ? 2 : 1, image.Frames.Count);
Assert.Equal(new Rgba32(17, 34, 51), image[0, 0]);
Assert.Null(image.Metadata.IccProfile);
ImageInfo info = Image.Identify(options, payload);
Assert.Equal(image.Size, info.Size);
Assert.Null(info.Metadata.IccProfile);
}
}
[Theory]
[MemberData(nameof(IccMetadataOptions))]
public void Decode_TruncatedIccFraming_RemainsFatal(SegmentIntegrityHandling integrity, bool skipMetadata, bool animated)
{
byte[] payload = CreateWebpWithMetadata("ICCP", 0x40000000, true, animated);
DecoderOptions options = new() { SegmentIntegrityHandling = integrity, SkipMetadata = skipMetadata };
Assert.Throws<InvalidImageContentException>(() => Image.Load(options, payload));
Assert.Throws<InvalidImageContentException>(() => Image.Identify(options, payload));
}
/// <summary>
/// Places a metadata declaration around a complete lossless image to test recovery independently of pixel truncation.
/// </summary>
private static byte[] CreateWebpWithMetadata(string chunkType, uint length, bool beforeImage, 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<Rgba32> source = new(2, 2, new Rgba32(17, 34, 51));
if (animated)
{
header[20] |= 0x02;
using Image<Rgba32> secondFrame = new(2, 2, new Rgba32(51, 34, 17));
source.Frames.AddFrame(secondFrame.Frames.RootFrame);
}
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)
{
combined.Write(header.AsSpan(30));
}
// 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)
{
combined.Write(header.AsSpan(30));
}
Assert.Throws<InvalidImageContentException>(() => Image.Load(payload)); byte[] payload = combined.ToArray();
Assert.Throws<InvalidImageContentException>(() => Image.Identify(payload)); BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(4), (uint)payload.Length - 8);
return payload;
} }
} }

4
tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs

@ -197,7 +197,7 @@ public class ChunkedMemoryStreamTests
readonlyStream.Position = 0; readonlyStream.Position = 0;
bytArrRet = new byte[(int)readonlyStream.Length]; bytArrRet = new byte[(int)readonlyStream.Length];
readonlyStream.Read(bytArrRet, 0, (int)readonlyStream.Length); readonlyStream.Read(bytArrRet);
for (int i = 0; i < bytArr.Length; i++) for (int i = 0; i < bytArr.Length; i++)
{ {
Assert.Equal(bytArr[i], bytArrRet[i]); Assert.Equal(bytArr[i], bytArrRet[i]);
@ -216,7 +216,7 @@ public class ChunkedMemoryStreamTests
ms2.WriteTo(ms3); ms2.WriteTo(ms3);
ms3.Position = 0; ms3.Position = 0;
bytArrRet = new byte[(int)ms3.Length]; bytArrRet = new byte[(int)ms3.Length];
ms3.Read(bytArrRet, 0, (int)ms3.Length); ms3.Read(bytArrRet);
for (int i = 0; i < bytArr.Length; i++) for (int i = 0; i < bytArr.Length; i++)
{ {
Assert.Equal(bytArr[i], bytArrRet[i]); Assert.Equal(bytArr[i], bytArrRet[i]);

Loading…
Cancel
Save