Browse Source

Merge remote-tracking branch 'origin/main' into bp/arithmeticcoding

# Conflicts:
#	src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
pull/2073/head
Brian Popow 4 years ago
parent
commit
af70b1dc7a
  1. 1
      src/Directory.Build.props
  2. 21
      src/ImageSharp/Diagnostics/MemoryDiagnostics.cs
  3. 9
      src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
  4. 18
      src/ImageSharp/Formats/Gif/GifDecoderCore.cs
  5. 5
      src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs
  6. 101
      src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
  7. 3
      src/ImageSharp/Formats/Jpeg/JpegThrowHelper.cs
  8. 12
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  9. 6
      src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs
  10. 64
      src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
  11. 258
      src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
  12. 7
      src/ImageSharp/Formats/Webp/WebpThrowHelper.cs
  13. 9
      src/ImageSharp/IO/BufferedReadStream.cs
  14. 9
      src/ImageSharp/Metadata/Profiles/Exif/ExifEncodedStringHelpers.cs
  15. 1
      tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs
  16. 1
      tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs
  17. 1
      tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs
  18. 15
      tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs
  19. 1
      tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
  20. 33
      tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.Metadata.cs
  21. 2
      tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs
  22. 1
      tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs
  23. 1
      tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
  24. 1
      tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs
  25. 1
      tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
  26. 1
      tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs
  27. 13
      tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs
  28. 1
      tests/ImageSharp.Tests/Image/ImageTests.LoadPixelData.cs
  29. 2
      tests/ImageSharp.Tests/Image/LargeImageIntegrationTests.cs
  30. 77
      tests/ImageSharp.Tests/MemoryAllocatorValidator.cs
  31. 3
      tests/ImageSharp.Tests/TestImages.cs
  32. 2
      tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparingUtils.cs
  33. 8
      tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs
  34. 33
      tests/ImageSharp.Tests/ValidateDisposedMemoryAllocationsAttribute.cs
  35. 3
      tests/Images/Input/Jpg/issues/Issue2087-exif-null-reference-on-encode.jpg
  36. 3
      tests/Images/Input/Jpg/issues/fuzz/Issue2085-NullReferenceException.jpg
  37. 4
      tests/Images/Input/Webp/exif_lossy.webp
  38. 3
      tests/Images/Input/Webp/exif_lossy_not_enough_data.webp

1
src/Directory.Build.props

@ -27,6 +27,7 @@
<InternalsVisibleTo Include="ImageSharp.Benchmarks" Key="$(SixLaborsPublicKey)" /> <InternalsVisibleTo Include="ImageSharp.Benchmarks" Key="$(SixLaborsPublicKey)" />
<InternalsVisibleTo Include="ImageSharp.Tests.ProfilingSandbox" Key="$(SixLaborsPublicKey)" /> <InternalsVisibleTo Include="ImageSharp.Tests.ProfilingSandbox" Key="$(SixLaborsPublicKey)" />
<InternalsVisibleTo Include="SixLabors.ImageSharp.Tests" Key="$(SixLaborsPublicKey)" /> <InternalsVisibleTo Include="SixLabors.ImageSharp.Tests" Key="$(SixLaborsPublicKey)" />
<InternalsVisibleTo Include="SixLabors.ImageSharp.Drawing.Tests" Key="$(SixLaborsPublicKey)" />
</ItemGroup> </ItemGroup>
</Project> </Project>

21
src/ImageSharp/Diagnostics/MemoryDiagnostics.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System;
using System.Threading; using System.Threading;
namespace SixLabors.ImageSharp.Diagnostics namespace SixLabors.ImageSharp.Diagnostics
@ -47,6 +48,16 @@ namespace SixLabors.ImageSharp.Diagnostics
} }
} }
/// <summary>
/// Fires when ImageSharp allocates memory from a MemoryAllocator
/// </summary>
internal static event Action MemoryAllocated;
/// <summary>
/// Fires when ImageSharp releases memory allocated from a MemoryAllocator
/// </summary>
internal static event Action MemoryReleased;
/// <summary> /// <summary>
/// Gets a value indicating the total number of memory resource objects leaked to the finalizer. /// Gets a value indicating the total number of memory resource objects leaked to the finalizer.
/// </summary> /// </summary>
@ -54,11 +65,17 @@ namespace SixLabors.ImageSharp.Diagnostics
internal static bool UndisposedAllocationSubscribed => Volatile.Read(ref undisposedAllocationSubscriptionCounter) > 0; internal static bool UndisposedAllocationSubscribed => Volatile.Read(ref undisposedAllocationSubscriptionCounter) > 0;
internal static void IncrementTotalUndisposedAllocationCount() => internal static void IncrementTotalUndisposedAllocationCount()
{
Interlocked.Increment(ref totalUndisposedAllocationCount); Interlocked.Increment(ref totalUndisposedAllocationCount);
MemoryAllocated?.Invoke();
}
internal static void DecrementTotalUndisposedAllocationCount() => internal static void DecrementTotalUndisposedAllocationCount()
{
Interlocked.Decrement(ref totalUndisposedAllocationCount); Interlocked.Decrement(ref totalUndisposedAllocationCount);
MemoryReleased?.Invoke();
}
internal static void RaiseUndisposedMemoryResource(string allocationStackTrace) internal static void RaiseUndisposedMemoryResource(string allocationStackTrace)
{ {

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

@ -122,11 +122,12 @@ namespace SixLabors.ImageSharp.Formats.Bmp
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken) public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
Image<TPixel> image = null;
try try
{ {
int bytesPerColorMapEntry = this.ReadImageHeaders(stream, out bool inverted, out byte[] palette); int bytesPerColorMapEntry = this.ReadImageHeaders(stream, out bool inverted, out byte[] palette);
var image = new Image<TPixel>(this.Configuration, this.infoHeader.Width, this.infoHeader.Height, this.metadata); image = new Image<TPixel>(this.Configuration, this.infoHeader.Width, this.infoHeader.Height, this.metadata);
Buffer2D<TPixel> pixels = image.GetRootFramePixelBuffer(); Buffer2D<TPixel> pixels = image.GetRootFramePixelBuffer();
@ -193,8 +194,14 @@ namespace SixLabors.ImageSharp.Formats.Bmp
} }
catch (IndexOutOfRangeException e) catch (IndexOutOfRangeException e)
{ {
image?.Dispose();
throw new ImageFormatException("Bitmap does not have a valid format.", e); throw new ImageFormatException("Bitmap does not have a valid format.", e);
} }
catch
{
image?.Dispose();
throw;
}
} }
/// <inheritdoc /> /// <inheritdoc />

18
src/ImageSharp/Formats/Gif/GifDecoderCore.cs

@ -221,7 +221,11 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary> /// </summary>
private void ReadGraphicalControlExtension() private void ReadGraphicalControlExtension()
{ {
this.stream.Read(this.buffer, 0, 6); int bytesRead = this.stream.Read(this.buffer, 0, 6);
if (bytesRead != 6)
{
GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the graphic control extension");
}
this.graphicsControlExtension = GifGraphicControlExtension.Parse(this.buffer); this.graphicsControlExtension = GifGraphicControlExtension.Parse(this.buffer);
} }
@ -231,7 +235,11 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary> /// </summary>
private void ReadImageDescriptor() private void ReadImageDescriptor()
{ {
this.stream.Read(this.buffer, 0, 9); int bytesRead = this.stream.Read(this.buffer, 0, 9);
if (bytesRead != 9)
{
GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the image descriptor");
}
this.imageDescriptor = GifImageDescriptor.Parse(this.buffer); this.imageDescriptor = GifImageDescriptor.Parse(this.buffer);
if (this.imageDescriptor.Height == 0 || this.imageDescriptor.Width == 0) if (this.imageDescriptor.Height == 0 || this.imageDescriptor.Width == 0)
@ -245,7 +253,11 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary> /// </summary>
private void ReadLogicalScreenDescriptor() private void ReadLogicalScreenDescriptor()
{ {
this.stream.Read(this.buffer, 0, 7); int bytesRead = this.stream.Read(this.buffer, 0, 7);
if (bytesRead != 7)
{
GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the logical screen descriptor");
}
this.logicalScreenDescriptor = GifLogicalScreenDescriptor.Parse(this.buffer); this.logicalScreenDescriptor = GifLogicalScreenDescriptor.Parse(this.buffer);
} }

5
src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs

@ -95,7 +95,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
} }
} }
return this.pixelBuffer; var buffer = this.pixelBuffer;
this.pixelBuffer = null;
return buffer;
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -210,6 +212,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
this.rgbBuffer?.Dispose(); this.rgbBuffer?.Dispose();
this.paddedProxyPixelRow?.Dispose(); this.paddedProxyPixelRow?.Dispose();
this.pixelBuffer?.Dispose();
} }
} }
} }

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

@ -237,11 +237,16 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.Metadata = new ImageMetadata(); this.Metadata = new ImageMetadata();
this.QuantizationTables = new Block8x8F[4]; this.QuantizationTables = new Block8x8F[4];
this.scanDecoder = scanDecoder; this.scanDecoder = scanDecoder;
if (tableBytes.Length < 4)
{
JpegThrowHelper.ThrowInvalidImageContentException("Not enough data to read marker");
}
using var ms = new MemoryStream(tableBytes); using var ms = new MemoryStream(tableBytes);
using var stream = new BufferedReadStream(this.Configuration, ms); using var stream = new BufferedReadStream(this.Configuration, ms);
// Check for the Start Of Image marker. // Check for the Start Of Image marker.
stream.Read(this.markerBuffer, 0, 2); int bytesRead = stream.Read(this.markerBuffer, 0, 2);
var fileMarker = new JpegFileMarker(this.markerBuffer[1], 0); var fileMarker = new JpegFileMarker(this.markerBuffer[1], 0);
if (fileMarker.Marker != JpegConstants.Markers.SOI) if (fileMarker.Marker != JpegConstants.Markers.SOI)
{ {
@ -249,16 +254,23 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
} }
// Read next marker. // Read next marker.
stream.Read(this.markerBuffer, 0, 2); bytesRead = stream.Read(this.markerBuffer, 0, 2);
byte marker = this.markerBuffer[1]; fileMarker = new JpegFileMarker(this.markerBuffer[1], (int)stream.Position - 2);
fileMarker = new JpegFileMarker(marker, (int)stream.Position - 2);
while (fileMarker.Marker != JpegConstants.Markers.EOI || (fileMarker.Marker == JpegConstants.Markers.EOI && fileMarker.Invalid)) while (fileMarker.Marker != JpegConstants.Markers.EOI || (fileMarker.Marker == JpegConstants.Markers.EOI && fileMarker.Invalid))
{ {
if (!fileMarker.Invalid) if (!fileMarker.Invalid)
{ {
// Get the marker length. // Get the marker length.
int remaining = this.ReadUint16(stream) - 2; int markerContentByteSize = this.ReadUint16(stream) - 2;
// Check whether stream actually has enought bytes to read
// markerContentByteSize is always positive so we cast
// to uint to avoid sign extension
if (stream.RemainingBytes < (uint)markerContentByteSize)
{
JpegThrowHelper.ThrowNotEnoughBytesForMarker(fileMarker.Marker);
}
switch (fileMarker.Marker) switch (fileMarker.Marker)
{ {
@ -268,13 +280,13 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
case JpegConstants.Markers.RST7: case JpegConstants.Markers.RST7:
break; break;
case JpegConstants.Markers.DHT: case JpegConstants.Markers.DHT:
this.ProcessDefineHuffmanTablesMarker(stream, remaining); this.ProcessDefineHuffmanTablesMarker(stream, markerContentByteSize);
break; break;
case JpegConstants.Markers.DQT: case JpegConstants.Markers.DQT:
this.ProcessDefineQuantizationTablesMarker(stream, remaining); this.ProcessDefineQuantizationTablesMarker(stream, markerContentByteSize);
break; break;
case JpegConstants.Markers.DRI: case JpegConstants.Markers.DRI:
this.ProcessDefineRestartIntervalMarker(stream, remaining); this.ProcessDefineRestartIntervalMarker(stream, markerContentByteSize);
break; break;
case JpegConstants.Markers.EOI: case JpegConstants.Markers.EOI:
return; return;
@ -282,7 +294,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
} }
// Read next marker. // Read next marker.
stream.Read(this.markerBuffer, 0, 2); bytesRead = stream.Read(this.markerBuffer, 0, 2);
if (bytesRead != 2)
{
JpegThrowHelper.ThrowInvalidImageContentException("Not enough data to read marker");
}
fileMarker = new JpegFileMarker(this.markerBuffer[1], 0); fileMarker = new JpegFileMarker(this.markerBuffer[1], 0);
} }
} }
@ -324,14 +341,23 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
if (!fileMarker.Invalid) if (!fileMarker.Invalid)
{ {
// Get the marker length. // Get the marker length.
int remaining = this.ReadUint16(stream) - 2; int markerContentByteSize = this.ReadUint16(stream) - 2;
// Check whether stream actually has enough bytes to read
// markerContentByteSize is always positive so we cast
// to uint to avoid sign extension.
if (stream.RemainingBytes < (uint)markerContentByteSize)
{
JpegThrowHelper.ThrowNotEnoughBytesForMarker(fileMarker.Marker);
}
switch (fileMarker.Marker) switch (fileMarker.Marker)
{ {
case JpegConstants.Markers.SOF0: case JpegConstants.Markers.SOF0:
case JpegConstants.Markers.SOF1: case JpegConstants.Markers.SOF1:
case JpegConstants.Markers.SOF2: case JpegConstants.Markers.SOF2:
this.ProcessStartOfFrameMarker(stream, remaining, fileMarker, ComponentType.Huffman, metadataOnly);
this.ProcessStartOfFrameMarker(stream, markerContentByteSize, fileMarker, ComponentType.Huffman, metadataOnly);
break; break;
case JpegConstants.Markers.SOF9: case JpegConstants.Markers.SOF9:
@ -344,7 +370,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.scanDecoder.ResetInterval = this.resetInterval.Value; this.scanDecoder.ResetInterval = this.resetInterval.Value;
} }
this.ProcessStartOfFrameMarker(stream, remaining, fileMarker, ComponentType.Arithmetic, metadataOnly); this.ProcessStartOfFrameMarker(stream, markerContentByteSize, fileMarker, ComponentType.Arithmetic, metadataOnly);
break; break;
case JpegConstants.Markers.SOF5: case JpegConstants.Markers.SOF5:
@ -368,7 +394,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
case JpegConstants.Markers.SOS: case JpegConstants.Markers.SOS:
if (!metadataOnly) if (!metadataOnly)
{ {
this.ProcessStartOfScanMarker(stream, remaining); this.ProcessStartOfScanMarker(stream, markerContentByteSize);
break; break;
} }
else else
@ -382,41 +408,41 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
if (metadataOnly) if (metadataOnly)
{ {
stream.Skip(remaining); stream.Skip(markerContentByteSize);
} }
else else
{ {
this.ProcessDefineHuffmanTablesMarker(stream, remaining); this.ProcessDefineHuffmanTablesMarker(stream, markerContentByteSize);
} }
break; break;
case JpegConstants.Markers.DQT: case JpegConstants.Markers.DQT:
this.ProcessDefineQuantizationTablesMarker(stream, remaining); this.ProcessDefineQuantizationTablesMarker(stream, markerContentByteSize);
break; break;
case JpegConstants.Markers.DRI: case JpegConstants.Markers.DRI:
if (metadataOnly) if (metadataOnly)
{ {
stream.Skip(remaining); stream.Skip(markerContentByteSize);
} }
else else
{ {
this.ProcessDefineRestartIntervalMarker(stream, remaining); this.ProcessDefineRestartIntervalMarker(stream, markerContentByteSize);
} }
break; break;
case JpegConstants.Markers.APP0: case JpegConstants.Markers.APP0:
this.ProcessApplicationHeaderMarker(stream, remaining); this.ProcessApplicationHeaderMarker(stream, markerContentByteSize);
break; break;
case JpegConstants.Markers.APP1: case JpegConstants.Markers.APP1:
this.ProcessApp1Marker(stream, remaining); this.ProcessApp1Marker(stream, markerContentByteSize);
break; break;
case JpegConstants.Markers.APP2: case JpegConstants.Markers.APP2:
this.ProcessApp2Marker(stream, remaining); this.ProcessApp2Marker(stream, markerContentByteSize);
break; break;
case JpegConstants.Markers.APP3: case JpegConstants.Markers.APP3:
@ -429,30 +455,30 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
case JpegConstants.Markers.APP10: case JpegConstants.Markers.APP10:
case JpegConstants.Markers.APP11: case JpegConstants.Markers.APP11:
case JpegConstants.Markers.APP12: case JpegConstants.Markers.APP12:
stream.Skip(remaining); stream.Skip(markerContentByteSize);
break; break;
case JpegConstants.Markers.APP13: case JpegConstants.Markers.APP13:
this.ProcessApp13Marker(stream, remaining); this.ProcessApp13Marker(stream, markerContentByteSize);
break; break;
case JpegConstants.Markers.APP14: case JpegConstants.Markers.APP14:
this.ProcessApp14Marker(stream, remaining); this.ProcessApp14Marker(stream, markerContentByteSize);
break; break;
case JpegConstants.Markers.APP15: case JpegConstants.Markers.APP15:
case JpegConstants.Markers.COM: case JpegConstants.Markers.COM:
stream.Skip(remaining); stream.Skip(markerContentByteSize);
break; break;
case JpegConstants.Markers.DAC: case JpegConstants.Markers.DAC:
if (metadataOnly) if (metadataOnly)
{ {
stream.Skip(remaining); stream.Skip(markerContentByteSize);
} }
else else
{ {
this.ProcessArithmeticTable(stream, remaining); this.ProcessArithmeticTable(stream, markerContentByteSize);
} }
break; break;
@ -748,7 +774,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
if (ProfileResolver.IsProfile(this.temp, ProfileResolver.XmpMarker.Slice(0, ExifMarkerLength))) if (ProfileResolver.IsProfile(this.temp, ProfileResolver.XmpMarker.Slice(0, ExifMarkerLength)))
{ {
int remainingXmpMarkerBytes = XmpMarkerLength - ExifMarkerLength; const int remainingXmpMarkerBytes = XmpMarkerLength - ExifMarkerLength;
if (remaining < remainingXmpMarkerBytes || this.IgnoreMetadata)
{
// Skip the application header length.
stream.Skip(remaining);
return;
}
stream.Read(this.temp, ExifMarkerLength, remainingXmpMarkerBytes); stream.Read(this.temp, ExifMarkerLength, remainingXmpMarkerBytes);
remaining -= remainingXmpMarkerBytes; remaining -= remainingXmpMarkerBytes;
if (ProfileResolver.IsProfile(this.temp, ProfileResolver.XmpMarker)) if (ProfileResolver.IsProfile(this.temp, ProfileResolver.XmpMarker))
@ -1347,7 +1380,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
int selectorsBytes = selectorsCount * 2; int selectorsBytes = selectorsCount * 2;
if (remaining != 4 + selectorsBytes) if (remaining != 4 + selectorsBytes)
{ {
JpegThrowHelper.ThrowBadMarker("SOS", remaining); JpegThrowHelper.ThrowBadMarker(nameof(JpegConstants.Markers.SOS), remaining);
} }
// selectorsCount*2 bytes: component index + huffman tables indices // selectorsCount*2 bytes: component index + huffman tables indices
@ -1399,8 +1432,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
component.AcTableId = acTableIndex; component.AcTableId = acTableIndex;
} }
// 3 bytes: Progressive scan decoding data // 3 bytes: Progressive scan decoding data.
stream.Read(this.temp, 0, 3); int bytesRead = stream.Read(this.temp, 0, 3);
if (bytesRead != 3)
{
JpegThrowHelper.ThrowInvalidImageContentException("Not enough data to read progressive scan decoding data");
}
int spectralStart = this.temp[0]; int spectralStart = this.temp[0];
this.scanDecoder.SpectralStart = spectralStart; this.scanDecoder.SpectralStart = spectralStart;
@ -1431,7 +1468,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
int bytesRead = stream.Read(this.markerBuffer, 0, 2); int bytesRead = stream.Read(this.markerBuffer, 0, 2);
if (bytesRead != 2) if (bytesRead != 2)
{ {
JpegThrowHelper.ThrowInvalidImageContentException("stream does not contain enough data, could not read ushort."); JpegThrowHelper.ThrowInvalidImageContentException("jpeg stream does not contain enough data, could not read ushort.");
} }
return BinaryPrimitives.ReadUInt16BigEndian(this.markerBuffer); return BinaryPrimitives.ReadUInt16BigEndian(this.markerBuffer);

3
src/ImageSharp/Formats/Jpeg/JpegThrowHelper.cs

@ -25,6 +25,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
[MethodImpl(InliningOptions.ColdPath)] [MethodImpl(InliningOptions.ColdPath)]
public static void ThrowBadMarker(string marker, int length) => throw new InvalidImageContentException($"Marker {marker} has bad length {length}."); public static void ThrowBadMarker(string marker, int length) => throw new InvalidImageContentException($"Marker {marker} has bad length {length}.");
[MethodImpl(InliningOptions.ColdPath)]
public static void ThrowNotEnoughBytesForMarker(byte marker) => throw new InvalidImageContentException($"Input stream does not have enough bytes to parse declared contents of the {marker:X2} marker.");
[MethodImpl(InliningOptions.ColdPath)] [MethodImpl(InliningOptions.ColdPath)]
public static void ThrowBadQuantizationTableIndex(int index) => throw new InvalidImageContentException($"Bad Quantization Table index {index}."); public static void ThrowBadQuantizationTableIndex(int index) => throw new InvalidImageContentException($"Bad Quantization Table index {index}.");

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

@ -227,10 +227,16 @@ namespace SixLabors.ImageSharp.Formats.Png
return image; return image;
} }
catch
{
image?.Dispose();
throw;
}
finally finally
{ {
this.scanline?.Dispose(); this.scanline?.Dispose();
this.previousScanline?.Dispose(); this.previousScanline?.Dispose();
this.nextChunk?.Data?.Dispose();
} }
} }
@ -472,6 +478,8 @@ namespace SixLabors.ImageSharp.Formats.Png
this.bytesPerSample = this.header.BitDepth / 8; this.bytesPerSample = this.header.BitDepth / 8;
} }
this.previousScanline?.Dispose();
this.scanline?.Dispose();
this.previousScanline = this.memoryAllocator.Allocate<byte>(this.bytesPerScanline, AllocationOptions.Clean); this.previousScanline = this.memoryAllocator.Allocate<byte>(this.bytesPerScanline, AllocationOptions.Clean);
this.scanline = this.Configuration.MemoryAllocator.Allocate<byte>(this.bytesPerScanline, AllocationOptions.Clean); this.scanline = this.Configuration.MemoryAllocator.Allocate<byte>(this.bytesPerScanline, AllocationOptions.Clean);
} }
@ -1359,6 +1367,7 @@ namespace SixLabors.ImageSharp.Formats.Png
{ {
if (chunk.Type == PngChunkType.Data) if (chunk.Type == PngChunkType.Data)
{ {
chunk.Data?.Dispose();
return chunk.Length; return chunk.Length;
} }
@ -1453,6 +1462,9 @@ namespace SixLabors.ImageSharp.Formats.Png
if (validCrc != inputCrc) if (validCrc != inputCrc)
{ {
string chunkTypeName = Encoding.ASCII.GetString(chunkType); string chunkTypeName = Encoding.ASCII.GetString(chunkType);
// ensure when throwing we dispose the data back to the memory allocator
chunk.Data?.Dispose();
PngThrowHelper.ThrowInvalidChunkCrc(chunkTypeName); PngThrowHelper.ThrowInvalidChunkCrc(chunkTypeName);
} }
} }

6
src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs

@ -65,7 +65,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors
jpegDecoder.ParseStream(stream, spectralConverterGray, CancellationToken.None); jpegDecoder.ParseStream(stream, spectralConverterGray, CancellationToken.None);
// TODO: Should we pass through the CancellationToken from the tiff decoder? // TODO: Should we pass through the CancellationToken from the tiff decoder?
CopyImageBytesToBuffer(buffer, spectralConverterGray.GetPixelBuffer(CancellationToken.None)); using var decompressedBuffer = spectralConverterGray.GetPixelBuffer(CancellationToken.None);
CopyImageBytesToBuffer(buffer, decompressedBuffer);
break; break;
} }
@ -81,7 +82,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors
jpegDecoder.ParseStream(stream, spectralConverter, CancellationToken.None); jpegDecoder.ParseStream(stream, spectralConverter, CancellationToken.None);
// TODO: Should we pass through the CancellationToken from the tiff decoder? // TODO: Should we pass through the CancellationToken from the tiff decoder?
CopyImageBytesToBuffer(buffer, spectralConverter.GetPixelBuffer(CancellationToken.None)); using var decompressedBuffer = spectralConverter.GetPixelBuffer(CancellationToken.None);
CopyImageBytesToBuffer(buffer, decompressedBuffer);
break; break;
} }

64
src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs

@ -157,40 +157,52 @@ namespace SixLabors.ImageSharp.Formats.Tiff
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken) public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
this.inputStream = stream;
var reader = new DirectoryReader(stream, this.Configuration.MemoryAllocator);
IEnumerable<ExifProfile> directories = reader.Read();
this.byteOrder = reader.ByteOrder;
this.isBigTiff = reader.IsBigTiff;
var frames = new List<ImageFrame<TPixel>>(); var frames = new List<ImageFrame<TPixel>>();
foreach (ExifProfile ifd in directories) try
{ {
cancellationToken.ThrowIfCancellationRequested(); this.inputStream = stream;
ImageFrame<TPixel> frame = this.DecodeFrame<TPixel>(ifd, cancellationToken); var reader = new DirectoryReader(stream, this.Configuration.MemoryAllocator);
frames.Add(frame);
IEnumerable<ExifProfile> directories = reader.Read();
this.byteOrder = reader.ByteOrder;
this.isBigTiff = reader.IsBigTiff;
if (this.decodingMode is FrameDecodingMode.First) foreach (ExifProfile ifd in directories)
{ {
break; cancellationToken.ThrowIfCancellationRequested();
ImageFrame<TPixel> frame = this.DecodeFrame<TPixel>(ifd, cancellationToken);
frames.Add(frame);
if (this.decodingMode is FrameDecodingMode.First)
{
break;
}
} }
}
ImageMetadata metadata = TiffDecoderMetadataCreator.Create(frames, this.ignoreMetadata, reader.ByteOrder, reader.IsBigTiff); ImageMetadata metadata = TiffDecoderMetadataCreator.Create(frames, this.ignoreMetadata, reader.ByteOrder, reader.IsBigTiff);
// TODO: Tiff frames can have different sizes. // TODO: Tiff frames can have different sizes.
ImageFrame<TPixel> root = frames[0]; ImageFrame<TPixel> root = frames[0];
this.Dimensions = root.Size(); this.Dimensions = root.Size();
foreach (ImageFrame<TPixel> frame in frames) foreach (ImageFrame<TPixel> frame in frames)
{
if (frame.Size() != root.Size())
{ {
TiffThrowHelper.ThrowNotSupported("Images with different sizes are not supported"); if (frame.Size() != root.Size())
{
TiffThrowHelper.ThrowNotSupported("Images with different sizes are not supported");
}
} }
return new Image<TPixel>(this.Configuration, metadata, frames);
} }
catch
{
foreach (ImageFrame<TPixel> f in frames)
{
f.Dispose();
}
return new Image<TPixel>(this.Configuration, metadata, frames); throw;
}
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -240,8 +252,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff
var stripOffsetsArray = (Array)tags.GetValueInternal(ExifTag.StripOffsets).GetValue(); var stripOffsetsArray = (Array)tags.GetValueInternal(ExifTag.StripOffsets).GetValue();
var stripByteCountsArray = (Array)tags.GetValueInternal(ExifTag.StripByteCounts).GetValue(); var stripByteCountsArray = (Array)tags.GetValueInternal(ExifTag.StripByteCounts).GetValue();
IMemoryOwner<ulong> stripOffsetsMemory = this.ConvertNumbers(stripOffsetsArray, out Span<ulong> stripOffsets); using IMemoryOwner<ulong> stripOffsetsMemory = this.ConvertNumbers(stripOffsetsArray, out Span<ulong> stripOffsets);
IMemoryOwner<ulong> stripByteCountsMemory = this.ConvertNumbers(stripByteCountsArray, out Span<ulong> stripByteCounts); using IMemoryOwner<ulong> stripByteCountsMemory = this.ConvertNumbers(stripByteCountsArray, out Span<ulong> stripByteCounts);
if (this.PlanarConfiguration == TiffPlanarConfiguration.Planar) if (this.PlanarConfiguration == TiffPlanarConfiguration.Planar)
{ {
@ -262,8 +274,6 @@ namespace SixLabors.ImageSharp.Formats.Tiff
cancellationToken); cancellationToken);
} }
stripOffsetsMemory?.Dispose();
stripByteCountsMemory?.Dispose();
return frame; return frame;
} }

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

@ -82,38 +82,47 @@ namespace SixLabors.ImageSharp.Formats.Webp
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken) public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
this.Metadata = new ImageMetadata(); Image<TPixel> image = null;
this.currentStream = stream; try
{
this.Metadata = new ImageMetadata();
this.currentStream = stream;
uint fileSize = this.ReadImageHeader(); uint fileSize = this.ReadImageHeader();
using (this.webImageInfo = this.ReadVp8Info()) using (this.webImageInfo = this.ReadVp8Info())
{
if (this.webImageInfo.Features is { Animation: true })
{ {
WebpThrowHelper.ThrowNotSupportedException("Animations are not supported"); if (this.webImageInfo.Features is { Animation: true })
} {
WebpThrowHelper.ThrowNotSupportedException("Animations are not supported");
}
var image = new Image<TPixel>(this.Configuration, (int)this.webImageInfo.Width, (int)this.webImageInfo.Height, this.Metadata); image = new Image<TPixel>(this.Configuration, (int)this.webImageInfo.Width, (int)this.webImageInfo.Height, this.Metadata);
Buffer2D<TPixel> pixels = image.GetRootFramePixelBuffer(); Buffer2D<TPixel> pixels = image.GetRootFramePixelBuffer();
if (this.webImageInfo.IsLossless) if (this.webImageInfo.IsLossless)
{ {
var losslessDecoder = new WebpLosslessDecoder(this.webImageInfo.Vp8LBitReader, this.memoryAllocator, this.Configuration); var losslessDecoder = new WebpLosslessDecoder(this.webImageInfo.Vp8LBitReader, this.memoryAllocator, this.Configuration);
losslessDecoder.Decode(pixels, image.Width, image.Height); losslessDecoder.Decode(pixels, image.Width, image.Height);
} }
else else
{ {
var lossyDecoder = new WebpLossyDecoder(this.webImageInfo.Vp8BitReader, this.memoryAllocator, this.Configuration); var lossyDecoder = new WebpLossyDecoder(this.webImageInfo.Vp8BitReader, this.memoryAllocator, this.Configuration);
lossyDecoder.Decode(pixels, image.Width, image.Height, this.webImageInfo); lossyDecoder.Decode(pixels, image.Width, image.Height, this.webImageInfo);
} }
// There can be optional chunks after the image data, like EXIF and XMP. // There can be optional chunks after the image data, like EXIF and XMP.
if (this.webImageInfo.Features != null) if (this.webImageInfo.Features != null)
{ {
this.ParseOptionalChunks(this.webImageInfo.Features); this.ParseOptionalChunks(this.webImageInfo.Features);
} }
return image; return image;
}
}
catch
{
image?.Dispose();
throw;
} }
} }
@ -190,7 +199,11 @@ namespace SixLabors.ImageSharp.Formats.Webp
uint fileSize = this.ReadChunkSize(); uint fileSize = this.ReadChunkSize();
// The first byte contains information about the image features used. // The first byte contains information about the image features used.
byte imageFeatures = (byte)this.currentStream.ReadByte(); int imageFeatures = this.currentStream.ReadByte();
if (imageFeatures == -1)
{
WebpThrowHelper.ThrowInvalidImageContentException("VP8X header doe not contain enough data");
}
// The first two bit of it are reserved and should be 0. // The first two bit of it are reserved and should be 0.
if (imageFeatures >> 6 != 0) if (imageFeatures >> 6 != 0)
@ -214,19 +227,34 @@ namespace SixLabors.ImageSharp.Formats.Webp
features.Animation = (imageFeatures & (1 << 1)) != 0; features.Animation = (imageFeatures & (1 << 1)) != 0;
// 3 reserved bytes should follow which are supposed to be zero. // 3 reserved bytes should follow which are supposed to be zero.
this.currentStream.Read(this.buffer, 0, 3); int bytesRead = this.currentStream.Read(this.buffer, 0, 3);
if (bytesRead != 3)
{
WebpThrowHelper.ThrowInvalidImageContentException("VP8X header does not contain enough data");
}
if (this.buffer[0] != 0 || this.buffer[1] != 0 || this.buffer[2] != 0) if (this.buffer[0] != 0 || this.buffer[1] != 0 || this.buffer[2] != 0)
{ {
WebpThrowHelper.ThrowImageFormatException("reserved bytes should be zero"); WebpThrowHelper.ThrowImageFormatException("reserved bytes should be zero");
} }
// 3 bytes for the width. // 3 bytes for the width.
this.currentStream.Read(this.buffer, 0, 3); bytesRead = this.currentStream.Read(this.buffer, 0, 3);
if (bytesRead != 3)
{
WebpThrowHelper.ThrowInvalidImageContentException("VP8 header does not contain enough data to read the width");
}
this.buffer[3] = 0; this.buffer[3] = 0;
uint width = (uint)BinaryPrimitives.ReadInt32LittleEndian(this.buffer) + 1; uint width = (uint)BinaryPrimitives.ReadInt32LittleEndian(this.buffer) + 1;
// 3 bytes for the height. // 3 bytes for the height.
this.currentStream.Read(this.buffer, 0, 3); bytesRead = this.currentStream.Read(this.buffer, 0, 3);
if (bytesRead != 3)
{
WebpThrowHelper.ThrowInvalidImageContentException("VP8 header does not contain enough data to read the height");
}
this.buffer[3] = 0; this.buffer[3] = 0;
uint height = (uint)BinaryPrimitives.ReadInt32LittleEndian(this.buffer) + 1; uint height = (uint)BinaryPrimitives.ReadInt32LittleEndian(this.buffer) + 1;
@ -272,7 +300,12 @@ namespace SixLabors.ImageSharp.Formats.Webp
this.webpMetadata.FileFormat = WebpFileFormatType.Lossy; this.webpMetadata.FileFormat = WebpFileFormatType.Lossy;
// VP8 data size (not including this 4 bytes). // VP8 data size (not including this 4 bytes).
this.currentStream.Read(this.buffer, 0, 4); int bytesRead = this.currentStream.Read(this.buffer, 0, 4);
if (bytesRead != 4)
{
WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the VP8 data size");
}
uint dataSize = BinaryPrimitives.ReadUInt32LittleEndian(this.buffer); uint dataSize = BinaryPrimitives.ReadUInt32LittleEndian(this.buffer);
// remaining counts the available image data payload. // remaining counts the available image data payload.
@ -284,7 +317,12 @@ namespace SixLabors.ImageSharp.Formats.Webp
// - A 3-bit version number. // - A 3-bit version number.
// - A 1-bit show_frame flag. // - A 1-bit show_frame flag.
// - A 19-bit field containing the size of the first data partition in bytes. // - A 19-bit field containing the size of the first data partition in bytes.
this.currentStream.Read(this.buffer, 0, 3); bytesRead = this.currentStream.Read(this.buffer, 0, 3);
if (bytesRead != 3)
{
WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the VP8 frame tag");
}
uint frameTag = (uint)(this.buffer[0] | (this.buffer[1] << 8) | (this.buffer[2] << 16)); uint frameTag = (uint)(this.buffer[0] | (this.buffer[1] << 8) | (this.buffer[2] << 16));
remaining -= 3; remaining -= 3;
bool isNoKeyFrame = (frameTag & 0x1) == 1; bool isNoKeyFrame = (frameTag & 0x1) == 1;
@ -312,13 +350,23 @@ namespace SixLabors.ImageSharp.Formats.Webp
} }
// Check for VP8 magic bytes. // Check for VP8 magic bytes.
this.currentStream.Read(this.buffer, 0, 3); bytesRead = this.currentStream.Read(this.buffer, 0, 3);
if (bytesRead != 3)
{
WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the VP8 magic bytes");
}
if (!this.buffer.AsSpan(0, 3).SequenceEqual(WebpConstants.Vp8HeaderMagicBytes)) if (!this.buffer.AsSpan(0, 3).SequenceEqual(WebpConstants.Vp8HeaderMagicBytes))
{ {
WebpThrowHelper.ThrowImageFormatException("VP8 magic bytes not found"); WebpThrowHelper.ThrowImageFormatException("VP8 magic bytes not found");
} }
this.currentStream.Read(this.buffer, 0, 4); bytesRead = this.currentStream.Read(this.buffer, 0, 4);
if (bytesRead != 4)
{
WebpThrowHelper.ThrowInvalidImageContentException("VP8 header does not contain enough data to read the image width and height");
}
uint tmp = (uint)BinaryPrimitives.ReadInt16LittleEndian(this.buffer); uint tmp = (uint)BinaryPrimitives.ReadInt16LittleEndian(this.buffer);
uint width = tmp & 0x3fff; uint width = tmp & 0x3fff;
sbyte xScale = (sbyte)(tmp >> 6); sbyte xScale = (sbyte)(tmp >> 6);
@ -429,54 +477,15 @@ namespace SixLabors.ImageSharp.Formats.Webp
switch (chunkType) switch (chunkType)
{ {
case WebpChunkType.Iccp: case WebpChunkType.Iccp:
uint iccpChunkSize = this.ReadChunkSize(); this.ReadIccProfile();
if (this.IgnoreMetadata)
{
this.currentStream.Skip((int)iccpChunkSize);
}
else
{
byte[] iccpData = new byte[iccpChunkSize];
this.currentStream.Read(iccpData, 0, (int)iccpChunkSize);
var profile = new IccProfile(iccpData);
if (profile.CheckIsValid())
{
this.Metadata.IccProfile = profile;
}
}
break; break;
case WebpChunkType.Exif: case WebpChunkType.Exif:
uint exifChunkSize = this.ReadChunkSize(); this.ReadExifProfile();
if (this.IgnoreMetadata)
{
this.currentStream.Skip((int)exifChunkSize);
}
else
{
byte[] exifData = new byte[exifChunkSize];
this.currentStream.Read(exifData, 0, (int)exifChunkSize);
var profile = new ExifProfile(exifData);
this.Metadata.ExifProfile = profile;
}
break; break;
case WebpChunkType.Xmp: case WebpChunkType.Xmp:
uint xmpChunkSize = this.ReadChunkSize(); this.ReadXmpProfile();
if (this.IgnoreMetadata)
{
this.currentStream.Skip((int)xmpChunkSize);
}
else
{
byte[] xmpData = new byte[xmpChunkSize];
this.currentStream.Read(xmpData, 0, (int)xmpChunkSize);
var profile = new XmpProfile(xmpData);
this.Metadata.XmpProfile = profile;
}
break; break;
case WebpChunkType.Animation: case WebpChunkType.Animation:
@ -488,7 +497,12 @@ namespace SixLabors.ImageSharp.Formats.Webp
features.AlphaChunkHeader = (byte)this.currentStream.ReadByte(); features.AlphaChunkHeader = (byte)this.currentStream.ReadByte();
int alphaDataSize = (int)(alphaChunkSize - 1); int alphaDataSize = (int)(alphaChunkSize - 1);
features.AlphaData = this.memoryAllocator.Allocate<byte>(alphaDataSize); features.AlphaData = this.memoryAllocator.Allocate<byte>(alphaDataSize);
this.currentStream.Read(features.AlphaData.Memory.Span, 0, alphaDataSize); int bytesRead = this.currentStream.Read(features.AlphaData.Memory.Span, 0, alphaDataSize);
if (bytesRead != alphaDataSize)
{
WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the alpha chunk");
}
break; break;
default: default:
WebpThrowHelper.ThrowImageFormatException("Unexpected chunk followed VP8X header"); WebpThrowHelper.ThrowImageFormatException("Unexpected chunk followed VP8X header");
@ -514,22 +528,100 @@ namespace SixLabors.ImageSharp.Formats.Webp
{ {
// Read chunk header. // Read chunk header.
WebpChunkType chunkType = this.ReadChunkType(); WebpChunkType chunkType = this.ReadChunkType();
uint chunkLength = this.ReadChunkSize();
if (chunkType == WebpChunkType.Exif && this.Metadata.ExifProfile == null) if (chunkType == WebpChunkType.Exif && this.Metadata.ExifProfile == null)
{ {
byte[] exifData = new byte[chunkLength]; this.ReadExifProfile();
this.currentStream.Read(exifData, 0, (int)chunkLength); }
this.Metadata.ExifProfile = new ExifProfile(exifData); else if (chunkType == WebpChunkType.Xmp && this.Metadata.XmpProfile == null)
{
this.ReadXmpProfile();
} }
else else
{ {
// Skip XMP chunk data or any duplicate EXIF chunk. // Skip duplicate XMP or EXIF chunk.
uint chunkLength = this.ReadChunkSize();
this.currentStream.Skip((int)chunkLength); this.currentStream.Skip((int)chunkLength);
} }
} }
} }
/// <summary>
/// Reads the EXIF profile from the stream.
/// </summary>
private void ReadExifProfile()
{
uint exifChunkSize = this.ReadChunkSize();
if (this.IgnoreMetadata)
{
this.currentStream.Skip((int)exifChunkSize);
}
else
{
byte[] exifData = new byte[exifChunkSize];
int bytesRead = this.currentStream.Read(exifData, 0, (int)exifChunkSize);
if (bytesRead != exifChunkSize)
{
// Ignore invalid chunk.
return;
}
var profile = new ExifProfile(exifData);
this.Metadata.ExifProfile = profile;
}
}
/// <summary>
/// Reads the XMP profile the stream.
/// </summary>
private void ReadXmpProfile()
{
uint xmpChunkSize = this.ReadChunkSize();
if (this.IgnoreMetadata)
{
this.currentStream.Skip((int)xmpChunkSize);
}
else
{
byte[] xmpData = new byte[xmpChunkSize];
int bytesRead = this.currentStream.Read(xmpData, 0, (int)xmpChunkSize);
if (bytesRead != xmpChunkSize)
{
// Ignore invalid chunk.
return;
}
var profile = new XmpProfile(xmpData);
this.Metadata.XmpProfile = profile;
}
}
/// <summary>
/// Reads the ICCP chunk from the stream.
/// </summary>
private void ReadIccProfile()
{
uint iccpChunkSize = this.ReadChunkSize();
if (this.IgnoreMetadata)
{
this.currentStream.Skip((int)iccpChunkSize);
}
else
{
byte[] iccpData = new byte[iccpChunkSize];
int bytesRead = this.currentStream.Read(iccpData, 0, (int)iccpChunkSize);
if (bytesRead != iccpChunkSize)
{
WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the iccp chunk");
}
var profile = new IccProfile(iccpData);
if (profile.CheckIsValid())
{
this.Metadata.IccProfile = profile;
}
}
}
/// <summary> /// <summary>
/// Identifies the chunk type from the chunk. /// Identifies the chunk type from the chunk.
/// </summary> /// </summary>

7
src/ImageSharp/Formats/Webp/WebpThrowHelper.cs

@ -8,6 +8,13 @@ namespace SixLabors.ImageSharp.Formats.Webp
{ {
internal static class WebpThrowHelper internal static class WebpThrowHelper
{ {
/// <summary>
/// Cold path optimization for throwing <see cref="InvalidImageContentException"/>'s.
/// </summary>
/// <param name="errorMessage">The error message for the exception.</param>
[MethodImpl(InliningOptions.ColdPath)]
public static void ThrowInvalidImageContentException(string errorMessage) => throw new InvalidImageContentException(errorMessage);
/// <summary> /// <summary>
/// Cold path optimization for throwing <see cref="ImageFormatException"/>-s /// Cold path optimization for throwing <see cref="ImageFormatException"/>-s
/// </summary> /// </summary>

9
src/ImageSharp/IO/BufferedReadStream.cs

@ -114,6 +114,15 @@ namespace SixLabors.ImageSharp.IO
/// <inheritdoc/> /// <inheritdoc/>
public override bool CanWrite { get; } = false; public override bool CanWrite { get; } = false;
/// <summary>
/// Gets remaining byte count available to read.
/// </summary>
public long RemainingBytes
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this.Length - this.Position;
}
/// <summary> /// <summary>
/// Gets the underlying stream. /// Gets the underlying stream.
/// </summary> /// </summary>

9
src/ImageSharp/Metadata/Profiles/Exif/ExifEncodedStringHelpers.cs

@ -80,7 +80,15 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
} }
public static unsafe int Write(Encoding encoding, string value, Span<byte> destination) public static unsafe int Write(Encoding encoding, string value, Span<byte> destination)
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER || NET
=> encoding.GetBytes(value.AsSpan(), destination);
#else
{ {
if (value.Length == 0)
{
return 0;
}
fixed (char* c = value) fixed (char* c = value)
{ {
fixed (byte* b = destination) fixed (byte* b = destination)
@ -89,6 +97,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
} }
} }
} }
#endif
private static bool TryDetect(ReadOnlySpan<byte> buffer, out CharacterCode code) private static bool TryDetect(ReadOnlySpan<byte> buffer, out CharacterCode code)
{ {

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

@ -20,6 +20,7 @@ using static SixLabors.ImageSharp.Tests.TestImages.Bmp;
namespace SixLabors.ImageSharp.Tests.Formats.Bmp namespace SixLabors.ImageSharp.Tests.Formats.Bmp
{ {
[Trait("Format", "Bmp")] [Trait("Format", "Bmp")]
[ValidateDisposedMemoryAllocations]
public class BmpDecoderTests public class BmpDecoderTests
{ {
public const PixelTypes CommonNonDefaultPixelTypes = PixelTypes.Rgba32 | PixelTypes.Bgra32 | PixelTypes.RgbaVector; public const PixelTypes CommonNonDefaultPixelTypes = PixelTypes.Rgba32 | PixelTypes.Bgra32 | PixelTypes.RgbaVector;

1
tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs

@ -18,6 +18,7 @@ using Xunit;
namespace SixLabors.ImageSharp.Tests.Formats.Gif namespace SixLabors.ImageSharp.Tests.Formats.Gif
{ {
[Trait("Format", "Gif")] [Trait("Format", "Gif")]
[ValidateDisposedMemoryAllocations]
public class GifDecoderTests public class GifDecoderTests
{ {
private const PixelTypes TestPixelTypes = PixelTypes.Rgba32 | PixelTypes.RgbaVector | PixelTypes.Argb32; private const PixelTypes TestPixelTypes = PixelTypes.Rgba32 | PixelTypes.RgbaVector | PixelTypes.Argb32;

1
tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs

@ -101,6 +101,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
TestImages.Jpeg.Issues.Fuzz.IndexOutOfRangeException1693A, TestImages.Jpeg.Issues.Fuzz.IndexOutOfRangeException1693A,
TestImages.Jpeg.Issues.Fuzz.IndexOutOfRangeException1693B, TestImages.Jpeg.Issues.Fuzz.IndexOutOfRangeException1693B,
TestImages.Jpeg.Issues.Fuzz.IndexOutOfRangeException824C, TestImages.Jpeg.Issues.Fuzz.IndexOutOfRangeException824C,
TestImages.Jpeg.Issues.Fuzz.NullReferenceException2085,
}; };
private static readonly Dictionary<string, float> CustomToleranceValues = new() private static readonly Dictionary<string, float> CustomToleranceValues = new()

15
tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs

@ -179,11 +179,16 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
var testFile = TestFile.Create(imagePath); var testFile = TestFile.Create(imagePath);
using (var stream = new MemoryStream(testFile.Bytes, false)) using (var stream = new MemoryStream(testFile.Bytes, false))
{ {
IImageInfo imageInfo = useIdentify if (useIdentify)
? ((IImageInfoDetector)decoder).Identify(Configuration.Default, stream, default) {
: decoder.Decode<Rgba32>(Configuration.Default, stream, default); IImageInfo imageInfo = ((IImageInfoDetector)decoder).Identify(Configuration.Default, stream, default);
test(imageInfo);
test(imageInfo); }
else
{
using var img = decoder.Decode<Rgba32>(Configuration.Default, stream, default);
test(img);
}
} }
} }

1
tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs

@ -22,6 +22,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
{ {
// TODO: Scatter test cases into multiple test classes // TODO: Scatter test cases into multiple test classes
[Trait("Format", "Jpg")] [Trait("Format", "Jpg")]
[ValidateDisposedMemoryAllocations]
public partial class JpegDecoderTests public partial class JpegDecoderTests
{ {
private static MagickReferenceDecoder ReferenceDecoder => new(); private static MagickReferenceDecoder ReferenceDecoder => new();

33
tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.Metadata.cs

@ -0,0 +1,33 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.IO;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.PixelFormats;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Formats.Jpg
{
[Trait("Format", "Jpg")]
public partial class JpegEncoderTests
{
[Theory]
[WithFile(TestImages.Jpeg.Issues.ValidExifArgumentNullExceptionOnEncode, PixelTypes.Rgba32)]
public void Encode_WithValidExifProfile_DoesNotThrowException<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
Exception ex = Record.Exception(() =>
{
var encoder = new JpegEncoder();
var stream = new MemoryStream();
using Image<TPixel> image = provider.GetImage(JpegDecoder);
image.Save(stream, encoder);
});
Assert.Null(ex);
}
}
}

2
tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs

@ -20,7 +20,7 @@ using Xunit;
namespace SixLabors.ImageSharp.Tests.Formats.Jpg namespace SixLabors.ImageSharp.Tests.Formats.Jpg
{ {
[Trait("Format", "Jpg")] [Trait("Format", "Jpg")]
public class JpegEncoderTests public partial class JpegEncoderTests
{ {
private static JpegEncoder JpegEncoder => new(); private static JpegEncoder JpegEncoder => new();

1
tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs

@ -11,6 +11,7 @@ using static SixLabors.ImageSharp.Tests.TestImages.Pbm;
namespace SixLabors.ImageSharp.Tests.Formats.Pbm namespace SixLabors.ImageSharp.Tests.Formats.Pbm
{ {
[Trait("Format", "Pbm")] [Trait("Format", "Pbm")]
[ValidateDisposedMemoryAllocations]
public class PbmDecoderTests public class PbmDecoderTests
{ {
[Theory] [Theory]

1
tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs

@ -19,6 +19,7 @@ using Xunit;
namespace SixLabors.ImageSharp.Tests.Formats.Png namespace SixLabors.ImageSharp.Tests.Formats.Png
{ {
[Trait("Format", "Png")] [Trait("Format", "Png")]
[ValidateDisposedMemoryAllocations]
public partial class PngDecoderTests public partial class PngDecoderTests
{ {
private const PixelTypes TestPixelTypes = PixelTypes.Rgba32 | PixelTypes.RgbaVector | PixelTypes.Argb32; private const PixelTypes TestPixelTypes = PixelTypes.Rgba32 | PixelTypes.RgbaVector | PixelTypes.Argb32;

1
tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs

@ -16,6 +16,7 @@ using static SixLabors.ImageSharp.Tests.TestImages.Tga;
namespace SixLabors.ImageSharp.Tests.Formats.Tga namespace SixLabors.ImageSharp.Tests.Formats.Tga
{ {
[Trait("Format", "Tga")] [Trait("Format", "Tga")]
[ValidateDisposedMemoryAllocations]
public class TgaDecoderTests public class TgaDecoderTests
{ {
private static TgaDecoder TgaDecoder => new(); private static TgaDecoder TgaDecoder => new();

1
tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs

@ -14,6 +14,7 @@ using static SixLabors.ImageSharp.Tests.TestImages.Tiff;
namespace SixLabors.ImageSharp.Tests.Formats.Tiff namespace SixLabors.ImageSharp.Tests.Formats.Tiff
{ {
[Trait("Format", "Tiff")] [Trait("Format", "Tiff")]
[ValidateDisposedMemoryAllocations]
public class TiffDecoderTests : TiffDecoderBaseTester public class TiffDecoderTests : TiffDecoderBaseTester
{ {
public static readonly string[] MultiframeTestImages = Multiframes; public static readonly string[] MultiframeTestImages = Multiframes;

1
tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs

@ -13,6 +13,7 @@ using static SixLabors.ImageSharp.Tests.TestImages.Webp;
namespace SixLabors.ImageSharp.Tests.Formats.Webp namespace SixLabors.ImageSharp.Tests.Formats.Webp
{ {
[Trait("Format", "Webp")] [Trait("Format", "Webp")]
[ValidateDisposedMemoryAllocations]
public class WebpDecoderTests public class WebpDecoderTests
{ {
private static WebpDecoder WebpDecoder => new(); private static WebpDecoder WebpDecoder => new();

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

@ -1,6 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Formats.Webp;
@ -150,5 +151,17 @@ namespace SixLabors.ImageSharp.Tests.Formats.Webp
Assert.NotNull(actualExif); Assert.NotNull(actualExif);
Assert.Equal(expectedExif.Values.Count, actualExif.Values.Count); Assert.Equal(expectedExif.Values.Count, actualExif.Values.Count);
} }
[Theory]
[WithFile(TestImages.Webp.Lossy.WithExifNotEnoughData, PixelTypes.Rgba32)]
public void WebpDecoder_IgnoresInvalidExifChunk<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
Exception ex = Record.Exception(() =>
{
using Image<TPixel> image = provider.GetImage();
});
Assert.Null(ex);
}
} }
} }

1
tests/ImageSharp.Tests/Image/ImageTests.LoadPixelData.cs

@ -14,6 +14,7 @@ namespace SixLabors.ImageSharp.Tests
[Theory] [Theory]
[InlineData(false)] [InlineData(false)]
[InlineData(true)] [InlineData(true)]
[ValidateDisposedMemoryAllocations]
public void FromPixels(bool useSpan) public void FromPixels(bool useSpan)
{ {
Rgba32[] data = { Color.Black, Color.White, Color.White, Color.Black, }; Rgba32[] data = { Color.Black, Color.White, Color.White, Color.Black, };

2
tests/ImageSharp.Tests/Image/LargeImageIntegrationTests.cs

@ -55,6 +55,8 @@ namespace SixLabors.ImageSharp.Tests
static void RunTest(string formatInner) static void RunTest(string formatInner)
{ {
using IDisposable mem = MemoryAllocatorValidator.MonitorAllocations();
Configuration configuration = Configuration.Default.Clone(); Configuration configuration = Configuration.Default.Clone();
configuration.PreferContiguousImageBuffers = true; configuration.PreferContiguousImageBuffers = true;
IImageEncoder encoder = configuration.ImageFormatsManager.FindEncoder( IImageEncoder encoder = configuration.ImageFormatsManager.FindEncoder(

77
tests/ImageSharp.Tests/MemoryAllocatorValidator.cs

@ -0,0 +1,77 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Threading;
using SixLabors.ImageSharp.Diagnostics;
using Xunit;
namespace SixLabors.ImageSharp.Tests
{
public static class MemoryAllocatorValidator
{
private static readonly AsyncLocal<TestMemoryDiagnostics> LocalInstance = new();
public static bool MonitoringAllocations => LocalInstance.Value != null;
static MemoryAllocatorValidator()
{
MemoryDiagnostics.MemoryAllocated += MemoryDiagnostics_MemoryAllocated;
MemoryDiagnostics.MemoryReleased += MemoryDiagnostics_MemoryReleased;
}
private static void MemoryDiagnostics_MemoryReleased()
{
TestMemoryDiagnostics backing = LocalInstance.Value;
if (backing != null)
{
backing.TotalRemainingAllocated--;
}
}
private static void MemoryDiagnostics_MemoryAllocated()
{
TestMemoryDiagnostics backing = LocalInstance.Value;
if (backing != null)
{
backing.TotalAllocated++;
backing.TotalRemainingAllocated++;
}
}
public static TestMemoryDiagnostics MonitorAllocations()
{
var diag = new TestMemoryDiagnostics();
LocalInstance.Value = diag;
return diag;
}
public static void StopMonitoringAllocations() => LocalInstance.Value = null;
public static void ValidateAllocations(int expectedAllocationCount = 0)
=> LocalInstance.Value?.Validate(expectedAllocationCount);
public class TestMemoryDiagnostics : IDisposable
{
public int TotalAllocated { get; set; }
public int TotalRemainingAllocated { get; set; }
public void Validate(int expectedAllocationCount)
{
var count = this.TotalRemainingAllocated;
var pass = expectedAllocationCount == count;
Assert.True(pass, $"Expected a {expectedAllocationCount} undisposed buffers but found {count}");
}
public void Dispose()
{
this.Validate(0);
if (LocalInstance.Value == this)
{
StopMonitoringAllocations();
}
}
}
}
}

3
tests/ImageSharp.Tests/TestImages.cs

@ -271,6 +271,7 @@ namespace SixLabors.ImageSharp.Tests
public const string InvalidIptcTag = "Jpg/issues/Issue1942InvalidIptcTag.jpg"; public const string InvalidIptcTag = "Jpg/issues/Issue1942InvalidIptcTag.jpg";
public const string Issue2057App1Parsing = "Jpg/issues/Issue2057-App1Parsing.jpg"; public const string Issue2057App1Parsing = "Jpg/issues/Issue2057-App1Parsing.jpg";
public const string ExifNullArrayTag = "Jpg/issues/issue-2056-exif-null-array.jpg"; public const string ExifNullArrayTag = "Jpg/issues/issue-2056-exif-null-array.jpg";
public const string ValidExifArgumentNullExceptionOnEncode = "Jpg/issues/Issue2087-exif-null-reference-on-encode.jpg";
public static class Fuzz public static class Fuzz
{ {
@ -299,6 +300,7 @@ namespace SixLabors.ImageSharp.Tests
public const string AccessViolationException922 = "Jpg/issues/fuzz/Issue922-AccessViolationException.jpg"; public const string AccessViolationException922 = "Jpg/issues/fuzz/Issue922-AccessViolationException.jpg";
public const string IndexOutOfRangeException1693A = "Jpg/issues/fuzz/Issue1693-IndexOutOfRangeException-A.jpg"; public const string IndexOutOfRangeException1693A = "Jpg/issues/fuzz/Issue1693-IndexOutOfRangeException-A.jpg";
public const string IndexOutOfRangeException1693B = "Jpg/issues/fuzz/Issue1693-IndexOutOfRangeException-B.jpg"; public const string IndexOutOfRangeException1693B = "Jpg/issues/fuzz/Issue1693-IndexOutOfRangeException-B.jpg";
public const string NullReferenceException2085 = "Jpg/issues/fuzz/Issue2085-NullReferenceException.jpg";
} }
} }
@ -642,6 +644,7 @@ namespace SixLabors.ImageSharp.Tests
{ {
public const string Earth = "Webp/earth_lossy.webp"; public const string Earth = "Webp/earth_lossy.webp";
public const string WithExif = "Webp/exif_lossy.webp"; public const string WithExif = "Webp/exif_lossy.webp";
public const string WithExifNotEnoughData = "Webp/exif_lossy_not_enough_data.webp";
public const string WithIccp = "Webp/lossy_with_iccp.webp"; public const string WithIccp = "Webp/lossy_with_iccp.webp";
public const string WithXmp = "Webp/xmp_lossy.webp"; public const string WithXmp = "Webp/xmp_lossy.webp";
public const string BikeSmall = "Webp/bike_lossless_small.webp"; public const string BikeSmall = "Webp/bike_lossless_small.webp";

2
tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparingUtils.cs

@ -26,7 +26,7 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison
} }
var testFile = TestFile.Create(path); var testFile = TestFile.Create(path);
Image<Rgba32> magickImage = DecodeWithMagick<Rgba32>(new FileInfo(testFile.FullPath)); using Image<Rgba32> magickImage = DecodeWithMagick<Rgba32>(new FileInfo(testFile.FullPath));
if (useExactComparer) if (useExactComparer)
{ {
ImageComparer.Exact.VerifySimilarity(magickImage, image); ImageComparer.Exact.VerifySimilarity(magickImage, image);

8
tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs

@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using SixLabors.ImageSharp.Diagnostics;
using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -158,8 +159,13 @@ namespace SixLabors.ImageSharp.Tests
return this.LoadImage(decoder); return this.LoadImage(decoder);
} }
var key = new Key(this.PixelType, this.FilePath, decoder); // do not cache so we can track allocation correctly when validating memory
if (MemoryAllocatorValidator.MonitoringAllocations)
{
return this.LoadImage(decoder);
}
var key = new Key(this.PixelType, this.FilePath, decoder);
Image<TPixel> cachedImage = Cache.GetOrAdd(key, _ => this.LoadImage(decoder)); Image<TPixel> cachedImage = Cache.GetOrAdd(key, _ => this.LoadImage(decoder));
return cachedImage.Clone(this.Configuration); return cachedImage.Clone(this.Configuration);

33
tests/ImageSharp.Tests/ValidateDisposedMemoryAllocationsAttribute.cs

@ -0,0 +1,33 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Diagnostics;
using System.Reflection;
using Xunit.Sdk;
namespace SixLabors.ImageSharp.Tests
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class ValidateDisposedMemoryAllocationsAttribute : BeforeAfterTestAttribute
{
private readonly int expected = 0;
public ValidateDisposedMemoryAllocationsAttribute()
: this(0)
{
}
public ValidateDisposedMemoryAllocationsAttribute(int expected)
=> this.expected = expected;
public override void Before(MethodInfo methodUnderTest)
=> MemoryAllocatorValidator.MonitorAllocations();
public override void After(MethodInfo methodUnderTest)
{
MemoryAllocatorValidator.ValidateAllocations(this.expected);
MemoryAllocatorValidator.StopMonitoringAllocations();
}
}
}

3
tests/Images/Input/Jpg/issues/Issue2087-exif-null-reference-on-encode.jpg

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4d41a41180a3371d0c4a724b40a4c86f6f975dab6be9da96964a484818770394
size 30715

3
tests/Images/Input/Jpg/issues/fuzz/Issue2085-NullReferenceException.jpg

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d478beff34179fda26238a44434607c276f55438ee96824c5af8c0188d358d8d
size 234

4
tests/Images/Input/Webp/exif_lossy.webp

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:fdf4e9b20af4168f4177d33f7f502906343bbaaae2af9b90e1531bd4452b317b oid sha256:5c53967bfefcfece8cd4411740c1c394e75864ca61a7a9751df3b28e727c0205
size 40765 size 68646

3
tests/Images/Input/Webp/exif_lossy_not_enough_data.webp

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fdf4e9b20af4168f4177d33f7f502906343bbaaae2af9b90e1531bd4452b317b
size 40765
Loading…
Cancel
Save