Browse Source

Merge pull request #3119 from SixLabors/js/proper-segment-integrity-handling

Enhance decoders with segment integrity handling
pull/3123/head
James Jackson-South 3 weeks ago
committed by GitHub
parent
commit
1bc4207d74
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 33
      src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
  2. 2
      src/ImageSharp/Formats/DecoderOptions.cs
  3. 163
      src/ImageSharp/Formats/Gif/GifDecoderCore.cs
  4. 17
      src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs
  5. 68
      src/ImageSharp/Formats/ImageDecoderCore.cs
  6. 134
      src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
  7. 10
      src/ImageSharp/Formats/Png/PngChunk.cs
  8. 101
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  9. 22
      src/ImageSharp/Formats/SegmentIntegrityHandling.cs
  10. 14
      src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
  11. 45
      src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
  12. 102
      src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs
  13. 22
      src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
  14. 2
      src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccChromaticityTagDataEntry.cs
  15. 41
      tests/ImageSharp.Tests/Formats/Bmp/BmpMetadataTests.cs
  16. 50
      tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs
  17. 67
      tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
  18. 6
      tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
  19. 38
      tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
  20. 4
      tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs
  21. 28
      tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs
  22. 1
      tests/ImageSharp.Tests/TestImages.cs
  23. 32
      tests/ImageSharp.Tests/TestUtilities/CorruptedMetadataImageFactory.cs
  24. 3
      tests/Images/Input/Jpg/issues/issue3118-multiple-sof.jpg

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

@ -1431,12 +1431,8 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
this.infoHeader = BmpInfoHeader.ParseV5(buffer);
if (this.infoHeader.ProfileData != 0 && this.infoHeader.ProfileSize != 0)
{
// Read color profile.
long streamPosition = stream.Position;
byte[] iccProfileData = new byte[this.infoHeader.ProfileSize];
stream.Position = infoHeaderStart + this.infoHeader.ProfileData;
stream.Read(iccProfileData);
this.metadata.IccProfile = new IccProfile(iccProfileData);
this.ExecuteAncillarySegmentAction(() => this.ReadIccProfile(stream, this.metadata, infoHeaderStart));
stream.Position = streamPosition;
}
}
@ -1470,6 +1466,33 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
this.Dimensions = new Size(this.infoHeader.Width, this.infoHeader.Height);
}
/// <summary>
/// Reads the embedded ICC profile from the BMP V5 info header.
/// </summary>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="imageMetadata">The image metadata.</param>
/// <param name="infoHeaderStart">The stream position where the info header begins.</param>
private void ReadIccProfile(BufferedReadStream stream, ImageMetadata imageMetadata, long infoHeaderStart)
{
byte[] iccProfileData = new byte[this.infoHeader.ProfileSize];
stream.Position = infoHeaderStart + this.infoHeader.ProfileData;
if (stream.Read(iccProfileData) != iccProfileData.Length)
{
BmpThrowHelper.ThrowInvalidImageContentException("Not enough data to read BMP ICC profile.");
}
IccProfile profile = new(iccProfileData);
if (profile.CheckIsValid())
{
imageMetadata.IccProfile = profile;
}
else
{
throw new InvalidIccProfileException("Invalid BMP ICC profile.");
}
}
/// <summary>
/// Reads the <see cref="BmpFileHeader"/> from the stream.
/// </summary>

2
src/ImageSharp/Formats/DecoderOptions.cs

@ -60,7 +60,7 @@ public sealed class DecoderOptions
/// <summary>
/// Gets the segment error handling strategy to use during decoding.
/// </summary>
public SegmentIntegrityHandling SegmentIntegrityHandling { get; init; } = SegmentIntegrityHandling.IgnoreNonCritical;
public SegmentIntegrityHandling SegmentIntegrityHandling { get; init; } = SegmentIntegrityHandling.IgnoreAncillary;
/// <summary>
/// Gets a value that controls how ICC profiles are handled during decode.

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

@ -146,10 +146,10 @@ internal sealed class GifDecoderCore : ImageDecoderCore
this.ReadGraphicalControlExtension(stream);
break;
case GifConstants.CommentLabel:
this.ReadComments(stream);
this.ExecuteAncillarySegmentAction(() => this.ReadComments(stream));
break;
case GifConstants.ApplicationExtensionLabel:
this.ReadApplicationExtension(stream);
this.ExecuteAncillarySegmentAction(() => this.ReadApplicationExtension(stream));
break;
case GifConstants.PlainTextLabel:
SkipBlock(stream); // Not supported by any known decoder.
@ -226,10 +226,10 @@ internal sealed class GifDecoderCore : ImageDecoderCore
this.ReadGraphicalControlExtension(stream);
break;
case GifConstants.CommentLabel:
this.ReadComments(stream);
this.ExecuteAncillarySegmentAction(() => this.ReadComments(stream));
break;
case GifConstants.ApplicationExtensionLabel:
this.ReadApplicationExtension(stream);
this.ExecuteAncillarySegmentAction(() => this.ReadApplicationExtension(stream));
break;
case GifConstants.PlainTextLabel:
SkipBlock(stream); // Not supported by any known decoder.
@ -266,6 +266,13 @@ internal sealed class GifDecoderCore : ImageDecoderCore
GifThrowHelper.ThrowNoHeader();
}
// Ignoring a malformed ancillary extension must not let identify succeed for a file
// that never contained any readable image frame data.
if (previousFrame is null)
{
GifThrowHelper.ThrowNoData();
}
return new ImageInfo(
new Size(this.logicalScreenDescriptor.Width, this.logicalScreenDescriptor.Height),
this.metadata,
@ -331,51 +338,128 @@ internal sealed class GifDecoderCore : ImageDecoderCore
private void ReadApplicationExtension(BufferedReadStream stream)
{
int appLength = stream.ReadByte();
if (appLength == -1)
{
GifThrowHelper.ThrowInvalidImageContentException("Unexpected end of stream while reading gif application extension");
}
if (appLength != GifConstants.ApplicationBlockSize)
{
this.ThrowOrIgnoreNonStrictSegmentError($"Gif application extension length '{appLength}' is invalid");
SkipBlock(stream, appLength);
return;
}
// If the length is 11 then it's a valid extension and most likely
// a NETSCAPE, XMP or ANIMEXTS extension. We want the loop count from this.
long position = stream.Position;
if (appLength == GifConstants.ApplicationBlockSize)
int bytesRead = stream.Read(this.buffer.Span, 0, GifConstants.ApplicationBlockSize);
if (bytesRead != GifConstants.ApplicationBlockSize)
{
stream.Read(this.buffer.Span, 0, GifConstants.ApplicationBlockSize);
bool isXmp = this.buffer.Span.StartsWith(GifConstants.XmpApplicationIdentificationBytes);
if (isXmp && !this.skipMetadata)
{
GifXmpApplicationExtension extension = GifXmpApplicationExtension.Read(stream, this.memoryAllocator);
if (extension.Data.Length > 0)
{
this.metadata!.XmpProfile = new XmpProfile(extension.Data);
}
else
{
// Reset the stream position and continue.
stream.Position = position;
SkipBlock(stream, appLength);
}
GifThrowHelper.ThrowInvalidImageContentException("Unexpected end of stream while reading gif application extension");
}
return;
}
bool isXmp = this.buffer.Span.StartsWith(GifConstants.XmpApplicationIdentificationBytes);
if (isXmp)
{
this.ReadXmpApplicationExtension(stream, position, appLength);
return;
}
int subBlockSize = stream.ReadByte();
int subBlockSize = stream.ReadByte();
if (subBlockSize == -1)
{
GifThrowHelper.ThrowInvalidImageContentException("Unexpected end of stream while reading gif application extension");
}
// TODO: There's also a NETSCAPE buffer extension.
// http://www.vurdalakov.net/misc/gif/netscape-buffering-application-extension
if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize)
{
stream.Read(this.buffer.Span, 0, GifConstants.NetscapeLoopingSubBlockSize);
this.gifMetadata!.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.Span[1..]).RepeatCount;
stream.Skip(1); // Skip the terminator.
return;
}
// TODO: There's also a NETSCAPE buffer extension.
// http://www.vurdalakov.net/misc/gif/netscape-buffering-application-extension
if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize)
{
this.ReadNetscapeApplicationExtension(stream);
return;
}
// Could be something else not supported yet.
// Skip the subblock and terminator.
SkipBlock(stream, subBlockSize);
}
/// <summary>
/// Reads the GIF XMP application extension.
/// </summary>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="applicationPosition">The stream position where the application identifier begins.</param>
/// <param name="appLength">The application block length.</param>
private void ReadXmpApplicationExtension(BufferedReadStream stream, long applicationPosition, int appLength)
{
if (this.skipMetadata)
{
stream.Position = applicationPosition;
SkipBlock(stream, appLength);
return;
}
// Could be something else not supported yet.
// Skip the subblock and terminator.
SkipBlock(stream, subBlockSize);
bool completed = false;
this.ExecuteAncillarySegmentAction(
() =>
{
this.ReadXmpApplicationExtensionData(stream, applicationPosition, appLength);
completed = true;
});
if (!completed)
{
stream.Position = applicationPosition;
SkipBlock(stream, appLength);
}
}
/// <summary>
/// Reads the GIF XMP application extension data.
/// </summary>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="applicationPosition">The stream position where the application identifier begins.</param>
/// <param name="appLength">The application block length.</param>
private void ReadXmpApplicationExtensionData(BufferedReadStream stream, long applicationPosition, int appLength)
{
GifXmpApplicationExtension extension = GifXmpApplicationExtension.Read(stream, this.memoryAllocator);
if (extension.Data.Length > 0)
{
this.metadata!.XmpProfile = new XmpProfile(extension.Data);
return;
}
SkipBlock(stream, appLength); // Not supported by any known decoder.
stream.Position = applicationPosition;
SkipBlock(stream, appLength);
}
/// <summary>
/// Reads the GIF NETSCAPE looping application extension.
/// </summary>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
private void ReadNetscapeApplicationExtension(BufferedReadStream stream) =>
this.ExecuteAncillarySegmentAction(() => this.ReadNetscapeApplicationExtensionData(stream));
/// <summary>
/// Reads the GIF NETSCAPE looping application extension data.
/// </summary>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
private void ReadNetscapeApplicationExtensionData(BufferedReadStream stream)
{
int bytesRead = stream.Read(this.buffer.Span, 0, GifConstants.NetscapeLoopingSubBlockSize);
if (bytesRead != GifConstants.NetscapeLoopingSubBlockSize)
{
throw new InvalidImageContentException("Unexpected end of stream while reading gif application extension");
}
this.gifMetadata!.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.Span[1..]).RepeatCount;
int terminator = stream.ReadByte();
if (terminator == -1)
{
throw new InvalidImageContentException("Unexpected end of stream while reading gif application extension");
}
}
/// <summary>
@ -428,7 +512,12 @@ internal sealed class GifDecoderCore : ImageDecoderCore
using IMemoryOwner<byte> commentsBuffer = this.memoryAllocator.Allocate<byte>(length);
Span<byte> commentsSpan = commentsBuffer.GetSpan();
stream.Read(commentsSpan);
int bytesRead = stream.Read(commentsSpan);
if (bytesRead != length)
{
GifThrowHelper.ThrowInvalidImageContentException("Unexpected end of stream while reading gif comment");
}
string commentPart = GifConstants.Encoding.GetString(commentsSpan);
stringBuilder.Append(commentPart);
}

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

@ -30,7 +30,11 @@ internal readonly struct GifXmpApplicationExtension : IGifExtension
/// <returns>The XMP metadata</returns>
public static GifXmpApplicationExtension Read(Stream stream, MemoryAllocator allocator)
{
byte[] xmpBytes = ReadXmpData(stream, allocator);
byte[] xmpBytes = ReadXmpData(stream, allocator, out bool terminated);
if (!terminated)
{
throw new InvalidImageContentException("Unexpected end of stream while reading gif XMP data");
}
// Exclude the "magic trailer", see XMP Specification Part 3, 1.1.2 GIF
int xmpLength = xmpBytes.Length - 256; // 257 - unread 0x0
@ -71,7 +75,7 @@ internal readonly struct GifXmpApplicationExtension : IGifExtension
return this.ContentLength;
}
private static byte[] ReadXmpData(Stream stream, MemoryAllocator allocator)
private static byte[] ReadXmpData(Stream stream, MemoryAllocator allocator, out bool terminated)
{
using ChunkedMemoryStream bytes = new(allocator);
@ -83,8 +87,15 @@ internal readonly struct GifXmpApplicationExtension : IGifExtension
while (true)
{
int b = stream.ReadByte();
if (b <= 0)
if (b == 0)
{
terminated = true;
return bytes.ToArray();
}
if (b < 0)
{
terminated = false;
return bytes.ToArray();
}

68
src/ImageSharp/Formats/ImageDecoderCore.cs

@ -33,6 +33,74 @@ internal abstract class ImageDecoderCore
/// </summary>
public Size Dimensions { get; protected internal set; }
/// <summary>
/// Executes a known ancillary segment parsing action using the configured integrity policy.
/// </summary>
/// <param name="action">The action.</param>
protected void ExecuteAncillarySegmentAction(Action action)
{
if (this.Options.SegmentIntegrityHandling is SegmentIntegrityHandling.Strict)
{
action();
return;
}
try
{
action();
}
catch (Exception ex) when (ex
is ImageFormatException
or InvalidIccProfileException
or InvalidImageContentException
or InvalidOperationException
or NotSupportedException)
{
// Intentionally ignored in non-strict segment integrity modes.
}
}
/// <summary>
/// Executes a known image data segment parsing action using the configured integrity policy.
/// </summary>
/// <param name="action">The action.</param>
protected void ExecuteImageDataSegmentAction(Action action)
{
if (this.Options.SegmentIntegrityHandling is not SegmentIntegrityHandling.IgnoreImageData)
{
action();
return;
}
try
{
action();
}
catch (Exception ex) when (ex
is ImageFormatException
or InvalidIccProfileException
or InvalidImageContentException
or InvalidOperationException
or NotSupportedException)
{
// Intentionally ignored when image data integrity handling is set to IgnoreImageData.
}
}
/// <summary>
/// Throws unless the decoder is running in a non-strict segment integrity mode.
/// Use this only from within <see cref="ExecuteAncillarySegmentAction"/> when local control flow
/// must continue after the error.
/// </summary>
/// <param name="message">The exception message.</param>
protected void ThrowOrIgnoreNonStrictSegmentError(string message)
{
if (this.Options.SegmentIntegrityHandling is SegmentIntegrityHandling.Strict)
{
throw new InvalidImageContentException(message);
}
}
/// <summary>
/// Reads the raw image information from the specified stream.
/// </summary>

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

@ -208,11 +208,7 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
JpegThrowHelper.ThrowInvalidImageContentException("Missing SOS marker.");
}
this.InitExifProfile();
this.InitIccProfile();
this.InitIptcProfile();
this.InitXmpProfile();
this.InitDerivedMetadataProperties();
this.InitializeMetadataProfiles();
_ = this.Options.TryGetIccProfileForColorConversion(this.Metadata.IccProfile, out IccProfile profile);
@ -232,11 +228,7 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
JpegThrowHelper.ThrowInvalidImageContentException("Missing SOS marker.");
}
this.InitExifProfile();
this.InitIccProfile();
this.InitIptcProfile();
this.InitXmpProfile();
this.InitDerivedMetadataProperties();
this.InitializeMetadataProfiles();
Size pixelSize = this.Frame.PixelSize;
return new ImageInfo(new Size(pixelSize.Width, pixelSize.Height), this.Metadata);
@ -385,7 +377,11 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
case JpegConstants.Markers.SOF1:
case JpegConstants.Markers.SOF2:
this.ProcessStartOfFrameMarker(stream, markerContentByteSize, fileMarker, ComponentType.Huffman, metadataOnly);
if (!this.ProcessStartOfFrameMarker(stream, markerContentByteSize, fileMarker, ComponentType.Huffman, metadataOnly))
{
return;
}
break;
case JpegConstants.Markers.SOF9:
@ -398,7 +394,11 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
this.scanDecoder.ResetInterval = this.resetInterval.Value;
}
this.ProcessStartOfFrameMarker(stream, markerContentByteSize, fileMarker, ComponentType.Arithmetic, metadataOnly);
if (!this.ProcessStartOfFrameMarker(stream, markerContentByteSize, fileMarker, ComponentType.Arithmetic, metadataOnly))
{
return;
}
break;
case JpegConstants.Markers.SOF5:
@ -429,7 +429,9 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
}
// It's highly unlikely that APPn related data will be found after the SOS marker
// We should have gathered everything we need by now.
// So we can stop parsing here and return the metadata we have parsed so far, instead
// of trying to parse any APPn markers after the SOS marker and risking running out of
// memory or other exceptions.
return;
case JpegConstants.Markers.DHT:
@ -701,6 +703,10 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
{
this.Metadata.IccProfile = profile;
}
else
{
throw new InvalidIccProfileException("Invalid ICC profile.");
}
}
}
@ -771,6 +777,18 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
return 0;
}
/// <summary>
/// Initializes decoded metadata profiles using the configured ancillary segment handling policy.
/// </summary>
private void InitializeMetadataProfiles()
{
this.ExecuteAncillarySegmentAction(this.InitExifProfile);
this.ExecuteAncillarySegmentAction(this.InitIccProfile);
this.ExecuteAncillarySegmentAction(this.InitIptcProfile);
this.ExecuteAncillarySegmentAction(this.InitXmpProfile);
this.ExecuteAncillarySegmentAction(this.InitDerivedMetadataProperties);
}
/// <summary>
/// Extends the profile with additional data.
/// </summary>
@ -794,7 +812,16 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
// We can only decode JFif identifiers.
// Some images contain multiple JFIF markers (Issue 1932) so we check to see
// if it's already been read.
if (remaining < JFifMarker.Length || (!this.jFif.Equals(default)))
if (remaining < JFifMarker.Length)
{
this.ThrowOrIgnoreNonStrictSegmentError("Bad App0 Marker length.");
// Skip the application header length
stream.Skip(remaining);
return;
}
if (!this.jFif.Equals(default))
{
// Skip the application header length
stream.Skip(remaining);
@ -804,7 +831,10 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
Span<byte> temp = stackalloc byte[2 * 16 * 4];
stream.Read(temp, 0, JFifMarker.Length);
_ = JFifMarker.TryParse(temp, out this.jFif);
if (!JFifMarker.TryParse(temp, out this.jFif))
{
this.ThrowOrIgnoreNonStrictSegmentError("Invalid App0 marker.");
}
remaining -= JFifMarker.Length;
@ -813,7 +843,9 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
{
if (stream.Position + remaining >= stream.Length)
{
JpegThrowHelper.ThrowInvalidImageContentException("Bad App0 Marker length.");
this.ThrowOrIgnoreNonStrictSegmentError("Bad App0 Marker length.");
stream.Skip(remaining);
return;
}
stream.Skip(remaining);
@ -829,7 +861,16 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
{
const int exifMarkerLength = 6;
const int xmpMarkerLength = 29;
if (remaining < exifMarkerLength || this.skipMetadata)
if (remaining < exifMarkerLength)
{
this.ThrowOrIgnoreNonStrictSegmentError("Bad App1 Marker length.");
// Skip the application header length.
stream.Skip(remaining);
return;
}
if (this.skipMetadata)
{
// Skip the application header length.
stream.Skip(remaining);
@ -838,7 +879,9 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
if (stream.Position + remaining >= stream.Length)
{
JpegThrowHelper.ThrowInvalidImageContentException("Bad App1 Marker length.");
this.ThrowOrIgnoreNonStrictSegmentError("Bad App1 Marker length.");
stream.Skip(remaining);
return;
}
Span<byte> temp = stackalloc byte[2 * 16 * 4];
@ -869,8 +912,10 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
if (ProfileResolver.IsProfile(temp, ProfileResolver.XmpMarker[..exifMarkerLength]))
{
const int remainingXmpMarkerBytes = xmpMarkerLength - exifMarkerLength;
if (remaining < remainingXmpMarkerBytes || this.skipMetadata)
if (remaining < remainingXmpMarkerBytes)
{
this.ThrowOrIgnoreNonStrictSegmentError("Bad App1 Marker length.");
// Skip the application header length.
stream.Skip(remaining);
return;
@ -896,6 +941,10 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
remaining = 0;
}
else
{
this.ThrowOrIgnoreNonStrictSegmentError("Invalid App1 marker.");
}
}
// Skip over any remaining bytes of this header.
@ -911,7 +960,15 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
{
// Length is 14 though we only need to check 12.
const int icclength = 14;
if (remaining < icclength || this.skipMetadata)
if (remaining < icclength)
{
this.ThrowOrIgnoreNonStrictSegmentError("Bad App2 Marker length.");
stream.Skip(remaining);
return;
}
if (this.skipMetadata)
{
stream.Skip(remaining);
return;
@ -952,7 +1009,15 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
/// <param name="remaining">The remaining bytes in the segment block.</param>
private void ProcessApp13Marker(BufferedReadStream stream, int remaining)
{
if (remaining < ProfileResolver.AdobePhotoshopApp13Marker.Length || this.skipMetadata)
if (remaining < ProfileResolver.AdobePhotoshopApp13Marker.Length)
{
this.ThrowOrIgnoreNonStrictSegmentError("Bad App13 Marker length.");
stream.Skip(remaining);
return;
}
if (this.skipMetadata)
{
stream.Skip(remaining);
return;
@ -970,6 +1035,7 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
{
if (!ProfileResolver.IsProfile(blockDataSpan[..4], ProfileResolver.AdobeImageResourceBlockMarker))
{
this.ThrowOrIgnoreNonStrictSegmentError("Invalid App13 marker.");
return;
}
@ -986,6 +1052,9 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
this.iptcData = blockDataSpan.Slice(dataStartIdx, resourceDataSize).ToArray();
break;
}
this.ThrowOrIgnoreNonStrictSegmentError("Invalid App13 marker.");
return;
}
else
{
@ -995,6 +1064,7 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
if (blockDataSpan.Length < dataStartIdx + resourceDataSize)
{
// Not enough data or the resource data size is wrong.
this.ThrowOrIgnoreNonStrictSegmentError("Invalid App13 marker.");
break;
}
@ -1089,6 +1159,8 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
const int markerLength = AdobeMarker.Length;
if (remaining < markerLength)
{
this.ThrowOrIgnoreNonStrictSegmentError("Bad App14 Marker length.");
// Skip the application header length
stream.Skip(remaining);
return;
@ -1103,6 +1175,10 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
{
this.hasAdobeMarker = true;
}
else
{
this.ThrowOrIgnoreNonStrictSegmentError("Invalid App14 marker.");
}
if (remaining > 0)
{
@ -1212,13 +1288,19 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
/// <param name="frameMarker">The current frame marker.</param>
/// <param name="decodingComponentType">The jpeg decoding component type.</param>
/// <param name="metadataOnly">Whether to parse metadata only.</param>
private void ProcessStartOfFrameMarker(BufferedReadStream stream, int remaining, in JpegFileMarker frameMarker, ComponentType decodingComponentType, bool metadataOnly)
private bool ProcessStartOfFrameMarker(BufferedReadStream stream, int remaining, in JpegFileMarker frameMarker, ComponentType decodingComponentType, bool metadataOnly)
{
if (this.Frame != null)
{
if (metadataOnly)
// If we have found the SOS marker, we can stop parsing as we have all
// the information we need to decode the image.
// It's possible that there are APPn related markers after the SOS marker,
// but it's highly unlikely and we would be better off stopping parsing
// and decoding the image instead of trying to parse those APPn markers
// and risking running out of memory or other exceptions.
if (this.hasSOSMarker)
{
return;
return false;
}
JpegThrowHelper.ThrowInvalidImageContentException("Multiple SOF markers. Only single frame jpegs supported.");
@ -1351,6 +1433,8 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
this.Frame.Init(maxH, maxV);
this.scanDecoder.InjectFrameData(this.Frame, this);
}
return true;
}
/// <summary>
@ -1550,7 +1634,7 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
arithmeticScanDecoder.InitDecodingTables(this.arithmeticDecodingTables);
}
this.InitIccProfile();
this.ExecuteAncillarySegmentAction(this.InitIccProfile);
_ = this.Options.TryGetIccProfileForColorConversion(this.Metadata.IccProfile, out IccProfile profile);
this.scanDecoder.ParseEntropyCodedData(selectorsCount, profile);
}

10
src/ImageSharp/Formats/Png/PngChunk.cs

@ -43,11 +43,11 @@ internal readonly struct PngChunk
/// </summary>
/// <param name="handling">The segment handling behavior.</param>
public bool IsCritical(SegmentIntegrityHandling handling)
=> handling switch
=> this.Type switch
{
SegmentIntegrityHandling.IgnoreNone => true,
SegmentIntegrityHandling.IgnoreNonCritical => this.Type is PngChunkType.Header or PngChunkType.Palette or PngChunkType.Data or PngChunkType.FrameData,
SegmentIntegrityHandling.IgnoreData => this.Type is PngChunkType.Header or PngChunkType.Palette,
_ => false,
PngChunkType.Header => true,
PngChunkType.Palette => true,
PngChunkType.Data or PngChunkType.FrameData => handling < SegmentIntegrityHandling.IgnoreImageData,
_ => handling < SegmentIntegrityHandling.IgnoreAncillary,
};
}

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

@ -800,18 +800,46 @@ internal sealed class PngDecoderCore : ImageDecoderCore
PngMetadata pngMetadata,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
using IMemoryOwner<TPixel>? blendMemory = frameControl.BlendMode == FrameBlendMode.Over
? this.memoryAllocator.Allocate<TPixel>(imageFrame.Width, AllocationOptions.Clean)
: null;
this.ExecuteImageDataSegmentAction(() => this.DecodePixelDataCore(
frameControl,
compressedStream,
imageFrame,
pngMetadata,
blendMemory,
cancellationToken));
this.hasImageData = true;
}
/// <summary>
/// Decodes the raw pixel data row by row.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="frameControl">The frame control.</param>
/// <param name="compressedStream">The compressed pixel data stream.</param>
/// <param name="imageFrame">The image frame to decode to.</param>
/// <param name="pngMetadata">The png metadata.</param>
/// <param name="blendMemory">The optional row blending buffer.</param>
/// <param name="cancellationToken">The cancellation token.</param>
private void DecodePixelDataCore<TPixel>(
FrameControl frameControl,
DeflateStream compressedStream,
ImageFrame<TPixel> imageFrame,
PngMetadata pngMetadata,
IMemoryOwner<TPixel>? blendMemory,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
int currentRow = (int)frameControl.YOffset;
int currentRowBytesRead = 0;
int height = (int)frameControl.YMax;
IMemoryOwner<TPixel>? blendMemory = null;
Span<TPixel> blendRowBuffer = [];
if (frameControl.BlendMode == FrameBlendMode.Over)
{
blendMemory = this.memoryAllocator.Allocate<TPixel>(imageFrame.Width, AllocationOptions.Clean);
blendRowBuffer = blendMemory.Memory.Span;
}
Span<TPixel> blendRowBuffer = blendMemory is null ? [] : blendMemory.Memory.Span;
while (currentRow < height)
{
@ -855,11 +883,6 @@ internal sealed class PngDecoderCore : ImageDecoderCore
break;
default:
if (this.segmentIntegrityHandling is SegmentIntegrityHandling.IgnoreData or SegmentIntegrityHandling.IgnoreAll)
{
goto EXIT;
}
PngThrowHelper.ThrowUnknownFilter();
break;
}
@ -870,8 +893,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore
}
EXIT:
this.hasImageData = true;
blendMemory?.Dispose();
return;
}
/// <summary>
@ -890,6 +912,41 @@ internal sealed class PngDecoderCore : ImageDecoderCore
PngMetadata pngMetadata,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
using IMemoryOwner<TPixel>? blendMemory = frameControl.BlendMode == FrameBlendMode.Over
? this.memoryAllocator.Allocate<TPixel>(imageFrame.Width, AllocationOptions.Clean)
: null;
FrameControl frameControlCopy = frameControl;
this.ExecuteImageDataSegmentAction(() => this.DecodeInterlacedPixelDataCore(
frameControlCopy,
compressedStream,
imageFrame,
pngMetadata,
blendMemory,
cancellationToken));
this.hasImageData = true;
}
/// <summary>
/// Decodes the raw interlaced pixel data row by row.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="frameControl">The frame control.</param>
/// <param name="compressedStream">The compressed pixel data stream.</param>
/// <param name="imageFrame">The current image frame.</param>
/// <param name="pngMetadata">The png metadata.</param>
/// <param name="blendMemory">The optional row blending buffer.</param>
/// <param name="cancellationToken">The cancellation token.</param>
private void DecodeInterlacedPixelDataCore<TPixel>(
FrameControl frameControl,
DeflateStream compressedStream,
ImageFrame<TPixel> imageFrame,
PngMetadata pngMetadata,
IMemoryOwner<TPixel>? blendMemory,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
int currentRow = Adam7.FirstRow[0] + (int)frameControl.YOffset;
int currentRowBytesRead = 0;
@ -899,13 +956,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore
Buffer2D<TPixel> imageBuffer = imageFrame.PixelBuffer;
IMemoryOwner<TPixel>? blendMemory = null;
Span<TPixel> blendRowBuffer = [];
if (frameControl.BlendMode == FrameBlendMode.Over)
{
blendMemory = this.memoryAllocator.Allocate<TPixel>(imageFrame.Width, AllocationOptions.Clean);
blendRowBuffer = blendMemory.Memory.Span;
}
Span<TPixel> blendRowBuffer = blendMemory is null ? [] : blendMemory.Memory.Span;
while (true)
{
@ -962,11 +1013,6 @@ internal sealed class PngDecoderCore : ImageDecoderCore
break;
default:
if (this.segmentIntegrityHandling is SegmentIntegrityHandling.IgnoreData or SegmentIntegrityHandling.IgnoreAll)
{
goto EXIT;
}
PngThrowHelper.ThrowUnknownFilter();
break;
}
@ -1002,8 +1048,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore
}
EXIT:
this.hasImageData = true;
blendMemory?.Dispose();
return;
}
/// <summary>

22
src/ImageSharp/Formats/SegmentIntegrityHandling.cs

@ -1,30 +1,26 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats;
/// <summary>
/// Specifies how to handle validation of errors in different segments of encoded image files.
/// Specifies how to handle validation of recoverable errors in ancillary and image data segments.
/// Structural errors that prevent safe decoding remain fatal regardless of the selected mode.
/// </summary>
public enum SegmentIntegrityHandling
{
/// <summary>
/// Do not ignore any errors.
/// Do not ignore any recoverable ancillary or image data segment errors.
/// </summary>
IgnoreNone,
Strict = 0,
/// <summary>
/// Ignore errors in non-critical segments of the encoded image.
/// Ignore recoverable errors in ancillary segments, such as optional metadata.
/// </summary>
IgnoreNonCritical,
IgnoreAncillary = 1,
/// <summary>
/// Ignore errors in data segments (e.g., image data, metadata).
/// Ignore recoverable errors in image data segments in addition to ancillary segments.
/// </summary>
IgnoreData,
/// <summary>
/// Ignore errors in all segments.
/// </summary>
IgnoreAll
IgnoreImageData = 2,
}

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

@ -289,7 +289,19 @@ internal class TiffDecoderCore : ImageDecoderCore
// We resolve the ICC profile early so that we can use it for color conversion if needed.
if (tags.TryGetValue(ExifTag.IccProfile, out IExifValue<byte[]> iccProfileBytes))
{
imageFrameMetaData.IccProfile = new IccProfile(iccProfileBytes.Value);
this.ExecuteAncillarySegmentAction(
() =>
{
IccProfile profile = new(iccProfileBytes.Value);
if (profile.CheckIsValid())
{
imageFrameMetaData.IccProfile = profile;
}
else
{
throw new InvalidIccProfileException("Invalid TIFF ICC profile.");
}
});
}
}

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

@ -64,9 +64,9 @@ internal class WebpAnimationDecoder : IDisposable
private readonly BackgroundColorHandling backgroundColorHandling;
/// <summary>
/// How to handle validation of errors in different segments of encoded image files.
/// Executes a known ancillary segment parsing action using the configured integrity policy.
/// </summary>
private readonly SegmentIntegrityHandling segmentIntegrityHandling;
private readonly Action<Action> executeAncillarySegmentAction;
/// <summary>
/// Initializes a new instance of the <see cref="WebpAnimationDecoder"/> class.
@ -76,21 +76,21 @@ internal class WebpAnimationDecoder : IDisposable
/// <param name="maxFrames">The maximum number of frames to decode. Inclusive.</param>
/// <param name="skipMetadata">Whether to skip metadata.</param>
/// <param name="backgroundColorHandling">The flag to decide how to handle the background color in the Animation Chunk.</param>
/// <param name="segmentIntegrityHandling">How to handle validation of errors in different segments of encoded image files.</param>
/// <param name="executeAncillarySegmentAction">Executes a known ancillary segment parsing action using the configured integrity policy.</param>
public WebpAnimationDecoder(
MemoryAllocator memoryAllocator,
Configuration configuration,
uint maxFrames,
bool skipMetadata,
BackgroundColorHandling backgroundColorHandling,
SegmentIntegrityHandling segmentIntegrityHandling)
Action<Action> executeAncillarySegmentAction)
{
this.memoryAllocator = memoryAllocator;
this.configuration = configuration;
this.maxFrames = maxFrames;
this.skipMetadata = skipMetadata;
this.backgroundColorHandling = backgroundColorHandling;
this.segmentIntegrityHandling = segmentIntegrityHandling;
this.executeAncillarySegmentAction = executeAncillarySegmentAction;
}
/// <summary>
@ -118,7 +118,6 @@ internal class WebpAnimationDecoder : IDisposable
: features.AnimationBackgroundColor!.Value;
bool ignoreMetadata = this.skipMetadata;
SegmentIntegrityHandling segmentIntegrityHandling = this.segmentIntegrityHandling;
Span<byte> buffer = stackalloc byte[4];
uint frameCount = 0;
int remainingBytes = (int)completeDataSize;
@ -139,13 +138,7 @@ internal class WebpAnimationDecoder : IDisposable
case WebpChunkType.Iccp:
case WebpChunkType.Xmp:
case WebpChunkType.Exif:
WebpChunkParsingUtils.ParseOptionalChunks(
stream,
chunkType,
this.metadata,
ignoreMetadata,
segmentIntegrityHandling,
buffer);
this.ReadOptionalChunk(stream, chunkType, this.metadata, ignoreMetadata);
break;
default:
@ -196,7 +189,6 @@ internal class WebpAnimationDecoder : IDisposable
TPixel backgroundPixel = backgroundColor.ToPixel<TPixel>();
bool ignoreMetadata = this.skipMetadata;
SegmentIntegrityHandling segmentIntegrityHandling = this.segmentIntegrityHandling;
Span<byte> buffer = stackalloc byte[4];
uint frameCount = 0;
int remainingBytes = (int)completeDataSize;
@ -223,7 +215,7 @@ internal class WebpAnimationDecoder : IDisposable
case WebpChunkType.Iccp:
case WebpChunkType.Xmp:
case WebpChunkType.Exif:
WebpChunkParsingUtils.ParseOptionalChunks(stream, chunkType, image!.Metadata, ignoreMetadata, segmentIntegrityHandling, buffer);
this.ReadOptionalChunk(stream, chunkType, image!.Metadata, ignoreMetadata);
break;
default:
@ -380,6 +372,29 @@ internal class WebpAnimationDecoder : IDisposable
frameMetadata.DisposalMode = frameData.DisposalMethod;
}
private void ReadOptionalChunk(
BufferedReadStream stream,
WebpChunkType chunkType,
ImageMetadata imageMetadata,
bool ignoreMetadata)
{
switch (chunkType)
{
case WebpChunkType.Iccp:
// 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;
case WebpChunkType.Exif:
this.executeAncillarySegmentAction(() => WebpChunkParsingUtils.ReadExifProfile(stream, imageMetadata, ignoreMetadata));
break;
case WebpChunkType.Xmp:
this.executeAncillarySegmentAction(() => WebpChunkParsingUtils.ReadXmpProfile(stream, imageMetadata, ignoreMetadata));
break;
}
}
/// <summary>
/// Reads the ALPH chunk data.
/// </summary>

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

@ -343,69 +343,18 @@ internal static class WebpChunkParsingUtils
throw new ImageFormatException("Invalid Webp data, could not read chunk type.");
}
/// <summary>
/// Parses optional metadata chunks. There SHOULD be at most one chunk of each type ('EXIF' and 'XMP ').
/// If there are more such chunks, readers MAY ignore all except the first one.
/// Also, a file may possibly contain both 'EXIF' and 'XMP ' chunks.
/// </summary>
/// <param name="stream">The stream to read the data from.</param>
/// <param name="chunkType">The chunk type to parse.</param>
/// <param name="metadata">The image metadata to write to.</param>
/// <param name="ignoreMetadata">If true, metadata will be ignored.</param>
/// <param name="segmentIntegrityHandling">Indicates how to handle segment integrity issues.</param>
/// <param name="buffer">Buffer to store the data read from the stream.</param>
public static void ParseOptionalChunks(
BufferedReadStream stream,
WebpChunkType chunkType,
ImageMetadata metadata,
bool ignoreMetadata,
SegmentIntegrityHandling segmentIntegrityHandling,
Span<byte> buffer)
{
long streamLength = stream.Length;
while (stream.Position < streamLength)
{
switch (chunkType)
{
case WebpChunkType.Iccp:
ReadIccProfile(stream, metadata, ignoreMetadata, segmentIntegrityHandling, buffer);
break;
case WebpChunkType.Exif:
ReadExifProfile(stream, metadata, ignoreMetadata, segmentIntegrityHandling, buffer);
break;
case WebpChunkType.Xmp:
ReadXmpProfile(stream, metadata, ignoreMetadata, segmentIntegrityHandling, buffer);
break;
default:
// Ignore unknown chunks.
// These must always fall after the image data so we are safe to always skip them.
uint chunkLength = ReadChunkSize(stream, buffer, false);
stream.Skip((int)chunkLength);
break;
}
}
}
/// <summary>
/// Reads the ICCP chunk from the stream.
/// </summary>
/// <param name="stream">The stream to decode from.</param>
/// <param name="metadata">The image metadata.</param>
/// <param name="ignoreMetadata">If true, metadata will be ignored.</param>
/// <param name="segmentIntegrityHandling">Indicates how to handle segment integrity issues.</param>
/// <param name="buffer">Temporary buffer.</param>
public static void ReadIccProfile(
BufferedReadStream stream,
ImageMetadata metadata,
bool ignoreMetadata,
SegmentIntegrityHandling segmentIntegrityHandling,
Span<byte> buffer)
bool ignoreMetadata)
{
// While ICC profiles are optional, an invalid ICC profile cannot be ignored as it must precede the image data
// and since we canot determine its size to allow skipping without reading the chunk size, we have to throw if it's invalid.
// Hence we do not consider segment integrity handling here.
Span<byte> buffer = stackalloc byte[4];
uint iccpChunkSize = ReadChunkSize(stream, buffer);
if (ignoreMetadata || metadata.IccProfile != null)
{
@ -413,22 +362,20 @@ internal static class WebpChunkParsingUtils
}
else
{
bool ignoreNone = segmentIntegrityHandling == SegmentIntegrityHandling.IgnoreNone;
byte[] iccpData = new byte[iccpChunkSize];
int bytesRead = stream.Read(iccpData, 0, (int)iccpChunkSize);
// We have the size but the profile is invalid if we cannot read enough data.
// Use the segment integrity handling to determine if we throw.
if (bytesRead != iccpChunkSize && ignoreNone)
if (bytesRead != iccpChunkSize)
{
WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the iccp chunk");
}
IccProfile profile = new(iccpData);
if (profile.CheckIsValid())
if (!profile.CheckIsValid())
{
metadata.IccProfile = profile;
throw new InvalidIccProfileException("Invalid ICC profile.");
}
metadata.IccProfile = profile;
}
}
@ -438,17 +385,13 @@ internal static class WebpChunkParsingUtils
/// <param name="stream">The stream to decode from.</param>
/// <param name="metadata">The image metadata.</param>
/// <param name="ignoreMetadata">If true, metadata will be ignored.</param>
/// <param name="segmentIntegrityHandling">Indicates how to handle segment integrity issues.</param>
/// <param name="buffer">Temporary buffer.</param>
public static void ReadExifProfile(
BufferedReadStream stream,
ImageMetadata metadata,
bool ignoreMetadata,
SegmentIntegrityHandling segmentIntegrityHandling,
Span<byte> buffer)
bool ignoreMetadata)
{
bool ignoreNone = segmentIntegrityHandling == SegmentIntegrityHandling.IgnoreNone;
uint exifChunkSize = ReadChunkSize(stream, buffer, ignoreNone);
Span<byte> buffer = stackalloc byte[4];
uint exifChunkSize = ReadChunkSize(stream, buffer);
if (ignoreMetadata || metadata.ExifProfile != null)
{
stream.Skip((int)exifChunkSize);
@ -459,12 +402,7 @@ internal static class WebpChunkParsingUtils
int bytesRead = stream.Read(exifData, 0, (int)exifChunkSize);
if (bytesRead != exifChunkSize)
{
if (ignoreNone)
{
WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the EXIF profile");
}
return;
WebpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for the EXIF profile");
}
ExifProfile exifProfile = new(exifData);
@ -490,18 +428,13 @@ internal static class WebpChunkParsingUtils
/// <param name="stream">The stream to decode from.</param>
/// <param name="metadata">The image metadata.</param>
/// <param name="ignoreMetadata">If true, metadata will be ignored.</param>
/// <param name="segmentIntegrityHandling">Indicates how to handle segment integrity issues.</param>
/// <param name="buffer">Temporary buffer.</param>
public static void ReadXmpProfile(
BufferedReadStream stream,
ImageMetadata metadata,
bool ignoreMetadata,
SegmentIntegrityHandling segmentIntegrityHandling,
Span<byte> buffer)
bool ignoreMetadata)
{
bool ignoreNone = segmentIntegrityHandling == SegmentIntegrityHandling.IgnoreNone;
uint xmpChunkSize = ReadChunkSize(stream, buffer, ignoreNone);
Span<byte> buffer = stackalloc byte[4];
uint xmpChunkSize = ReadChunkSize(stream, buffer);
if (ignoreMetadata || metadata.XmpProfile != null)
{
stream.Skip((int)xmpChunkSize);
@ -512,12 +445,7 @@ internal static class WebpChunkParsingUtils
int bytesRead = stream.Read(xmpData, 0, (int)xmpChunkSize);
if (bytesRead != xmpChunkSize)
{
if (ignoreNone)
{
WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the XMP profile");
}
return;
WebpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for the XMP profile");
}
metadata.XmpProfile = new XmpProfile(xmpData);

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

@ -52,8 +52,6 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable
/// </summary>
private readonly BackgroundColorHandling backgroundColorHandling;
private readonly SegmentIntegrityHandling segmentIntegrityHandling;
/// <summary>
/// Initializes a new instance of the <see cref="WebpDecoderCore"/> class.
/// </summary>
@ -62,7 +60,6 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable
: base(options.GeneralOptions)
{
this.backgroundColorHandling = options.BackgroundColorHandling;
this.segmentIntegrityHandling = options.GeneralOptions.SegmentIntegrityHandling;
this.configuration = options.GeneralOptions.Configuration;
this.skipMetadata = options.GeneralOptions.SkipMetadata;
this.maxFrames = options.GeneralOptions.MaxFrames;
@ -90,7 +87,7 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable
this.maxFrames,
this.skipMetadata,
this.backgroundColorHandling,
this.segmentIntegrityHandling);
this.ExecuteAncillarySegmentAction);
return animationDecoder.Decode<TPixel>(stream, this.webImageInfo.Features, this.webImageInfo.Width, this.webImageInfo.Height, fileSize);
}
@ -149,7 +146,7 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable
this.maxFrames,
this.skipMetadata,
this.backgroundColorHandling,
this.segmentIntegrityHandling);
this.ExecuteAncillarySegmentAction);
return animationDecoder.Identify(
stream,
@ -278,19 +275,21 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable
Span<byte> buffer)
{
bool ignoreMetadata = this.skipMetadata;
SegmentIntegrityHandling integrityHandling = this.segmentIntegrityHandling;
switch (chunkType)
{
case WebpChunkType.Iccp:
WebpChunkParsingUtils.ReadIccProfile(stream, metadata, ignoreMetadata, integrityHandling, buffer);
// 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;
case WebpChunkType.Exif:
WebpChunkParsingUtils.ReadExifProfile(stream, metadata, ignoreMetadata, integrityHandling, buffer);
this.ExecuteAncillarySegmentAction(() => WebpChunkParsingUtils.ReadExifProfile(stream, metadata, ignoreMetadata));
break;
case WebpChunkType.Xmp:
WebpChunkParsingUtils.ReadXmpProfile(stream, metadata, ignoreMetadata, integrityHandling, buffer);
this.ExecuteAncillarySegmentAction(() => WebpChunkParsingUtils.ReadXmpProfile(stream, metadata, ignoreMetadata));
break;
case WebpChunkType.AnimationParameter:
@ -320,7 +319,6 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable
private void ParseOptionalChunks(BufferedReadStream stream, ImageMetadata metadata, WebpFeatures features, Span<byte> buffer)
{
bool ignoreMetadata = this.skipMetadata;
SegmentIntegrityHandling integrityHandling = this.segmentIntegrityHandling;
if (ignoreMetadata || (!features.ExifProfile && !features.XmpMetaData))
{
@ -334,11 +332,11 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable
WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer);
if (chunkType == WebpChunkType.Exif && metadata.ExifProfile == null)
{
WebpChunkParsingUtils.ReadExifProfile(stream, metadata, ignoreMetadata, integrityHandling, buffer);
this.ExecuteAncillarySegmentAction(() => WebpChunkParsingUtils.ReadExifProfile(stream, metadata, ignoreMetadata));
}
else if (chunkType == WebpChunkType.Xmp && metadata.XmpProfile == null)
{
WebpChunkParsingUtils.ReadXmpProfile(stream, metadata, ignoreMetadata, integrityHandling, buffer);
this.ExecuteAncillarySegmentAction(() => WebpChunkParsingUtils.ReadXmpProfile(stream, metadata, ignoreMetadata));
}
else
{

2
src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccChromaticityTagDataEntry.cs

@ -140,7 +140,7 @@ internal sealed class IccChromaticityTagDataEntry : IccTagDataEntry, IEquatable<
[0.155, 0.070]
];
default:
throw new ArgumentException("Unrecognized colorant encoding");
throw new InvalidIccProfileException("Unrecognized colorant encoding");
}
}

41
tests/ImageSharp.Tests/Formats/Bmp/BmpMetadataTests.cs

@ -1,8 +1,11 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.TestUtilities;
using static SixLabors.ImageSharp.Tests.TestImages.Bmp;
// ReSharper disable InconsistentNaming
@ -46,7 +49,7 @@ public class BmpMetadataTests
}
[Theory]
[WithFile(IccProfile, PixelTypes.Rgba32)]
[WithFile(TestImages.Bmp.IccProfile, PixelTypes.Rgba32)]
public void Decoder_CanReadColorProfile<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
@ -56,4 +59,40 @@ public class BmpMetadataTests
Assert.NotNull(metaData.IccProfile);
Assert.Equal(16, metaData.IccProfile.Entries.Length);
}
[Fact]
public void Identify_MalformedIccProfile_IgnoresNonCriticalErrorsByDefault()
{
ImageInfo info = Image.Identify(CreateBmpWithMalformedIccProfile());
Assert.Equal(1, info.Width);
Assert.Equal(1, info.Height);
}
[Fact]
public void Decode_MalformedIccProfile_IgnoresNonCriticalErrorsByDefault()
{
using Image<Rgba32> image = Image.Load<Rgba32>(CreateBmpWithMalformedIccProfile());
Assert.Equal(1, image.Width);
Assert.Equal(1, image.Height);
}
[Fact]
public void Identify_MalformedIccProfile_ThrowsWithStrict()
{
DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict };
Assert.Throws<InvalidIccProfileException>(() => Image.Identify(options, CreateBmpWithMalformedIccProfile()));
}
[Fact]
public void Decode_MalformedIccProfile_ThrowsWithStrict()
{
DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict };
Assert.Throws<InvalidIccProfileException>(() =>
{
using Image<Rgba32> image = Image.Load<Rgba32>(options, CreateBmpWithMalformedIccProfile());
});
}
private static byte[] CreateBmpWithMalformedIccProfile()
=> CorruptedMetadataImageFactory.CreateImageWithMalformedIccProfile(new BmpEncoder());
}

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

@ -408,4 +408,54 @@ public class GifDecoderTests
image.DebugSaveMultiFrame(provider);
image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact);
}
[Fact]
public void Identify_MalformedApplicationExtension_IgnoresNonCriticalErrorsByDefault()
{
ImageInfo info = Image.Identify(CreateGifWithMalformedApplicationExtension());
Assert.Equal(1, info.Width);
Assert.Equal(1, info.Height);
}
[Fact]
public void Decode_MalformedApplicationExtension_IgnoresNonCriticalErrorsByDefault()
{
using Image<Rgba32> image = Image.Load<Rgba32>(CreateGifWithMalformedApplicationExtension());
Assert.Equal(1, image.Width);
Assert.Equal(1, image.Height);
}
[Fact]
public void Identify_MalformedApplicationExtension_ThrowsWithStrict()
{
DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict };
Assert.Throws<InvalidImageContentException>(() => Image.Identify(options, CreateGifWithMalformedApplicationExtension()));
}
[Fact]
public void Decode_MalformedApplicationExtension_ThrowsWithStrict()
{
DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict };
Assert.Throws<InvalidImageContentException>(() =>
{
using Image<Rgba32> image = Image.Load<Rgba32>(options, CreateGifWithMalformedApplicationExtension());
});
}
private static byte[] CreateGifWithMalformedApplicationExtension()
{
// The application extension declares a 5-byte application block (0x05), but GIF application
// extensions require the fixed identifier/authentication block to be 11 bytes long.
return
[
(byte)'G', (byte)'I', (byte)'F', (byte)'8', (byte)'9', (byte)'a',
0x01, 0x00, 0x01, 0x00, 0x80, 0x00, 0x00,
0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF,
0x21, 0xFF, 0x05, (byte)'B', (byte)'a', (byte)'d', (byte)'A', (byte)'p', 0x00,
0x21, 0xF9, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00,
0x2C, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00,
0x02, 0x02, 0x44, 0x01, 0x00,
0x3B
];
}
}

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

@ -457,4 +457,71 @@ public partial class JpegDecoderTests
byte[] data = [0xFF, 0xD8, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30];
using Image<Rgba32> image = Image.Load<Rgba32>(data);
});
// https://github.com/SixLabors/ImageSharp/issues/3118
[Theory]
[WithFile(TestImages.Jpeg.Issues.Issue3118, PixelTypes.Rgb24)]
public void Issue3118_Multiple_SOF_WithSOS_DoesNotThrow<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(JpegDecoder.Instance);
image.DebugSave(provider);
}
[Fact]
public void Identify_MalformedApp13Segment_IgnoresNonCriticalErrorsByDefault()
{
ImageInfo info = Image.Identify(CreateJpegWithMalformedApp13Segment());
Assert.True(info.Width > 0);
Assert.True(info.Height > 0);
}
[Fact]
public void Decode_MalformedApp13Segment_IgnoresNonCriticalErrorsByDefault()
{
using Image<Rgba32> image = Image.Load<Rgba32>(CreateJpegWithMalformedApp13Segment());
Assert.True(image.Width > 0);
Assert.True(image.Height > 0);
}
[Fact]
public void Identify_MalformedApp13Segment_ThrowsWithStrict()
{
DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict };
Assert.Throws<InvalidImageContentException>(() => Image.Identify(options, CreateJpegWithMalformedApp13Segment()));
}
[Fact]
public void Decode_MalformedApp13Segment_ThrowsWithStrict()
{
DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict };
Assert.Throws<InvalidImageContentException>(() =>
{
using Image<Rgba32> image = Image.Load<Rgba32>(options, CreateJpegWithMalformedApp13Segment());
});
}
private static byte[] CreateJpegWithMalformedApp13Segment()
{
byte[] source = TestFile.Create(TestImages.Jpeg.Baseline.Calliphora).Bytes;
// This APP13 segment starts with the valid "Photoshop 3.0\0" identifier, but the remaining
// payload does not begin with the required "8BIM" image resource block signature.
byte[] malformedApp13 =
[
0xFF, 0xED,
0x00, 0x1D,
(byte)'P', (byte)'h', (byte)'o', (byte)'t', (byte)'o', (byte)'s', (byte)'h', (byte)'o', (byte)'p', (byte)' ', (byte)'3', (byte)'.', (byte)'0', 0x00,
(byte)'B', (byte)'a', (byte)'d', (byte)'R', (byte)'e', (byte)'s', (byte)'o', (byte)'u', (byte)'r', (byte)'c', (byte)'e', (byte)'!', (byte)'!'
];
byte[] payload = new byte[source.Length + malformedApp13.Length];
payload[0] = source[0];
payload[1] = source[1];
Buffer.BlockCopy(malformedApp13, 0, payload, 2, malformedApp13.Length);
Buffer.BlockCopy(source, 2, payload, 2 + malformedApp13.Length, source.Length - 2);
return payload;
}
}

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

@ -405,7 +405,7 @@ public partial class PngDecoderTests
TestFile testFile = TestFile.Create(imagePath);
using MemoryStream stream = new(testFile.Bytes, false);
ImageInfo imageInfo = Image.Identify(new DecoderOptions { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreData }, stream);
ImageInfo imageInfo = Image.Identify(new DecoderOptions { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreImageData }, stream);
Assert.NotNull(imageInfo);
Assert.Equal(expectedPixelSize, imageInfo.PixelType.BitsPerPixel);
@ -594,7 +594,7 @@ public partial class PngDecoderTests
public void Decode_InvalidDataChunkCrc_IgnoreCrcErrors<TPixel>(TestImageProvider<TPixel> provider, bool compare)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(PngDecoder.Instance, new DecoderOptions() { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreData });
using Image<TPixel> image = provider.GetImage(PngDecoder.Instance, new DecoderOptions() { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreImageData });
image.DebugSave(provider);
if (compare)
@ -775,7 +775,7 @@ public partial class PngDecoderTests
public void Binary_PrematureEof()
{
PngDecoder decoder = PngDecoder.Instance;
PngDecoderOptions options = new() { GeneralOptions = new DecoderOptions { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreData } };
PngDecoderOptions options = new() { GeneralOptions = new DecoderOptions { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreImageData } };
using EofHitCounter eofHitCounter = EofHitCounter.RunDecoder(TestImages.Png.Bad.FlagOfGermany0000016446, decoder, options);
// TODO: Try to reduce this to 1.

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

@ -6,7 +6,9 @@ using System.Runtime.Intrinsics.X86;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Tiff;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.TestUtilities;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using static SixLabors.ImageSharp.Tests.TestImages.Tiff;
@ -868,4 +870,40 @@ public class TiffDecoderTests : TiffDecoderBaseTester
[WithFile(Issue2983, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_Issue2983<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider);
[Fact]
public void Identify_MalformedIccProfile_IgnoresNonCriticalErrorsByDefault()
{
ImageInfo info = Image.Identify(CreateTiffWithMalformedIccProfile());
Assert.Equal(1, info.Width);
Assert.Equal(1, info.Height);
}
[Fact]
public void Decode_MalformedIccProfile_IgnoresNonCriticalErrorsByDefault()
{
using Image<Rgba32> image = Image.Load<Rgba32>(CreateTiffWithMalformedIccProfile());
Assert.Equal(1, image.Width);
Assert.Equal(1, image.Height);
}
[Fact]
public void Identify_MalformedIccProfile_ThrowsWithStrict()
{
DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict };
Assert.Throws<InvalidIccProfileException>(() => Image.Identify(options, CreateTiffWithMalformedIccProfile()));
}
[Fact]
public void Decode_MalformedIccProfile_ThrowsWithStrict()
{
DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict };
Assert.Throws<InvalidIccProfileException>(() =>
{
using Image<Rgba32> image = Image.Load<Rgba32>(options, CreateTiffWithMalformedIccProfile());
});
}
private static byte[] CreateTiffWithMalformedIccProfile()
=> CorruptedMetadataImageFactory.CreateImageWithMalformedIccProfile(new TiffEncoder());
}

4
tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs

@ -11,6 +11,7 @@ using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.TestDataIcc;
using SixLabors.ImageSharp.Tests.TestUtilities;
using static SixLabors.ImageSharp.Tests.TestImages.Tiff;
@ -335,8 +336,7 @@ public class TiffMetadataTests
iptcProfile.SetValue(IptcTag.Name, "Test name");
rootFrameInput.Metadata.IptcProfile = iptcProfile;
IccProfileHeader iccProfileHeader = new() { Class = IccProfileClass.ColorSpace };
IccProfile iccProfile = new();
IccProfile iccProfile = new(IccTestDataProfiles.ProfileRandomArray);
rootFrameInput.Metadata.IccProfile = iccProfile;
TiffFrameMetadata frameMetaInput = rootFrameInput.Metadata.GetTiffMetadata();

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

@ -200,4 +200,32 @@ public class WebpMetaDataTests
});
Assert.Null(ex);
}
[Fact]
public void Identify_InvalidExifChunk_IgnoresNonCriticalErrorsByDefault()
{
using MemoryStream stream = new(TestFile.Create(TestImages.Webp.Lossy.WithExifNotEnoughData).Bytes, false);
ImageInfo info = Image.Identify(stream);
Assert.True(info.Width > 0);
Assert.True(info.Height > 0);
}
[Fact]
public void Identify_InvalidExifChunk_ThrowsWithStrict()
{
DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict };
using MemoryStream stream = new(TestFile.Create(TestImages.Webp.Lossy.WithExifNotEnoughData).Bytes, false);
Assert.Throws<InvalidImageContentException>(() => Image.Identify(options, stream));
}
[Fact]
public void Decode_InvalidExifChunk_ThrowsWithStrict()
{
DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict };
using MemoryStream stream = new(TestFile.Create(TestImages.Webp.Lossy.WithExifNotEnoughData).Bytes, false);
Assert.Throws<InvalidImageContentException>(() =>
{
using Image<Rgba32> image = Image.Load<Rgba32>(options, stream);
});
}
}

1
tests/ImageSharp.Tests/TestImages.cs

@ -361,6 +361,7 @@ public static class TestImages
public const string Issue2758 = "Jpg/issues/issue-2758.jpg";
public const string Issue2857 = "Jpg/issues/issue-2857-subsub-ifds.jpg";
public const string Issue2948 = "Jpg/issues/issue-2948-sos.jpg";
public const string Issue3118 = "Jpg/issues/issue3118-multiple-sof.jpg";
public static class Fuzz
{

32
tests/ImageSharp.Tests/TestUtilities/CorruptedMetadataImageFactory.cs

@ -0,0 +1,32 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.TestDataIcc;
namespace SixLabors.ImageSharp.Tests.TestUtilities;
/// <summary>
/// Creates encoded images with malformed metadata payloads while keeping the surrounding container valid.
/// </summary>
internal static class CorruptedMetadataImageFactory
{
/// <summary>
/// Creates an encoded single-pixel image with a malformed ICC profile.
/// </summary>
/// <param name="encoder">The encoder used to produce the image bytes.</param>
/// <returns>The encoded image bytes with a malformed ICC profile payload.</returns>
public static byte[] CreateImageWithMalformedIccProfile(IImageEncoder encoder)
{
using Image<Rgba32> image = new(1, 1);
image[0, 0] = new Rgba32(255, 0, 0, 255);
image.Metadata.IccProfile = new IccProfile(IccTestDataProfiles.HeaderInvalidSizeSmallArray);
using MemoryStream stream = new();
image.Save(stream, encoder);
return stream.ToArray();
}
}

3
tests/Images/Input/Jpg/issues/issue3118-multiple-sof.jpg

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