Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,9 @@ public ArithmeticScanDecoder(BufferedReadStream stream, SpectralConverter conver
this.ct = -16; // Force reading 2 initial bytes to fill C.
}

/// <inheritdoc/>
public SegmentIntegrityHandling SegmentIntegrityHandling { get; set; }

/// <inheritdoc/>
public int ResetInterval
{
Expand Down Expand Up @@ -1100,9 +1103,10 @@ private bool HandleRestart()
{
if (this.restartInterval > 0 && (--this.todo) == 0)
{
if (this.scanBuffer.Marker == JpegConstants.Markers.XFF)
// Use the same marker recovery and integrity policy as Huffman-coded scans.
if (this.scanBuffer.Marker == JpegConstants.Markers.XFF || this.scanBuffer.HasReservedMarker())
{
if (!this.scanBuffer.FindNextMarker())
if (!this.scanBuffer.FindNextMarker(this.SegmentIntegrityHandling))
{
return false;
}
Expand All @@ -1119,8 +1123,7 @@ private bool HandleRestart()
component.AcStatistics?.Reset();
}

this.Reset();

// Reset clears the detected marker. Classify it first so a real segment marker can be read again.
if (this.scanBuffer.HasRestartMarker())
{
this.Reset();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ public HuffmanScanDecoder(
this.acHuffmanTables = new HuffmanTable[maxTables];
}

/// <inheritdoc/>
public SegmentIntegrityHandling SegmentIntegrityHandling { get; set; }

/// <summary>
/// Sets reset interval determined by RST markers.
/// </summary>
Expand Down Expand Up @@ -745,9 +748,10 @@ private bool HandleRestart()
{
if (this.restartInterval > 0 && (--this.todo) == 0)
{
if (this.scanBuffer.Marker == JpegConstants.Markers.XFF)
// A reserved marker cannot complete a restart interval. The shared reader applies the recovery policy.
if (this.scanBuffer.Marker == JpegConstants.Markers.XFF || this.scanBuffer.HasReservedMarker())
{
if (!this.scanBuffer.FindNextMarker())
if (!this.scanBuffer.FindNextMarker(this.SegmentIntegrityHandling))
{
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder;
/// </summary>
internal interface IJpegScanDecoder
{
/// <summary>
/// Sets the recovery policy for errors in compressed scan data.
/// </summary>
public SegmentIntegrityHandling SegmentIntegrityHandling { set; }

/// <summary>
/// Sets the reset interval.
/// </summary>
Expand Down
62 changes: 43 additions & 19 deletions src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBitReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,13 @@ public void Reset()
[MethodImpl(InliningOptions.ShortMethod)]
public readonly bool HasBadMarker() => this.Marker != JpegConstants.Markers.XFF && !this.HasRestartMarker();

/// <summary>
/// Determines whether the detected marker is TEM or a reserved code before SOF0.
/// </summary>
/// <returns>True for TEM and reserved marker codes.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public readonly bool HasReservedMarker() => this.Marker < JpegConstants.Markers.SOF0;

[MethodImpl(InliningOptions.AlwaysInline)]
public void FillBuffer()
{
Expand Down Expand Up @@ -177,39 +184,56 @@ private ulong GetBytes()
return temp;
}

/// <summary>
/// Finds the next marker at a restart boundary and applies the selected image-data recovery policy.
/// </summary>
/// <param name="integrityHandling">The recovery policy for compressed image data.</param>
/// <returns>True if a marker was found. False if the stream ended first.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public bool FindNextMarker()
public bool FindNextMarker(SegmentIntegrityHandling integrityHandling)
{
while (true)
do
{
int b = this.stream.ReadByte();
if (b == -1)
// The bit reader can already hold a marker from its entropy-data lookahead.
// Libjpeg treats codes before SOF0 as damaged image data and scans forward to another marker.
// Only IgnoreImageData permits that recovery. Real segment markers stay with the segment parser.
if (this.HasReservedMarker() && integrityHandling != SegmentIntegrityHandling.IgnoreImageData)
{
return false;
JpegThrowHelper.ThrowInvalidImageContentException("Unexpected marker before a JPEG restart marker.");
}

// Found a marker.
if (b == JpegConstants.Markers.XFF)
while (true)
{
while (b == JpegConstants.Markers.XFF)
int b = this.stream.ReadByte();
if (b == -1)
{
// Loop here to discard any padding FF bytes on terminating marker.
b = this.stream.ReadByte();
if (b == -1)
{
return false;
}
return false;
}

// Found a valid marker. Exit loop
if (b != 0)
if (b == JpegConstants.Markers.XFF)
{
this.Marker = (byte)b;
this.MarkerPosition = this.stream.Position - 2;
return true;
// Multiple FF bytes are marker padding. FF 00 is a stuffed data byte, not a marker.
while (b == JpegConstants.Markers.XFF)
{
b = this.stream.ReadByte();
if (b == -1)
{
return false;
}
}

if (b != 0)
{
this.Marker = (byte)b;
this.MarkerPosition = this.stream.Position - 2;
break;
}
}
}
}
while (this.HasReservedMarker());

return true;
}

[MethodImpl(InliningOptions.AlwaysInline)]
Expand Down
8 changes: 8 additions & 0 deletions src/ImageSharp/Formats/Jpeg/JpegConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,14 @@ internal static class Markers
/// </remarks>
/// </summary>
public const byte RST7 = 0xD7;

/// <summary>
/// Temporary private marker for arithmetic coding.
/// </summary>
/// <remarks>
/// This marker stands alone. It has no length field or payload.
/// </remarks>
public const byte TEM = 0x01;
}

/// <summary>
Expand Down
14 changes: 11 additions & 3 deletions src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,9 @@ public static JpegFileMarker FindNextFileMarker(BufferedReadStream stream)
}
}

// Found a valid marker. Exit loop
if (b is not 0 and (< JpegConstants.Markers.RST0 or > JpegConstants.Markers.RST7))
// FF 00 is a stuffed data byte. RSTn and TEM have no length field (ITU T.81, Table B.1).
// Skip these standalone markers so the next marker's bytes are not read as a length.
if (b is not 0 and not JpegConstants.Markers.TEM and (< JpegConstants.Markers.RST0 or > JpegConstants.Markers.RST7))
{
return new JpegFileMarker((byte)(uint)b, stream.Position - 2);
}
Expand Down Expand Up @@ -324,6 +325,9 @@ internal void ParseStream(BufferedReadStream stream, SpectralConverter spectralC

this.scanDecoder ??= new HuffmanScanDecoder(stream, spectralConverter, cancellationToken);

// Apply the image-data policy to the active decoder, including one supplied by TIFF.
this.scanDecoder.SegmentIntegrityHandling = this.Options.SegmentIntegrityHandling;

this.Metadata ??= new ImageMetadata();

Span<byte> markerBuffer = stackalloc byte[2];
Expand Down Expand Up @@ -386,7 +390,11 @@ internal void ParseStream(BufferedReadStream stream, SpectralConverter spectralC
case JpegConstants.Markers.SOF10:
case JpegConstants.Markers.SOF13:
case JpegConstants.Markers.SOF14:
this.scanDecoder = new ArithmeticScanDecoder(stream, spectralConverter, cancellationToken);
this.scanDecoder = new ArithmeticScanDecoder(stream, spectralConverter, cancellationToken)
{
SegmentIntegrityHandling = this.Options.SegmentIntegrityHandling
};

if (this.resetInterval.HasValue)
{
this.scanDecoder.ResetInterval = this.resetInterval.Value;
Expand Down
94 changes: 94 additions & 0 deletions tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs;
using Xunit.Abstractions;
using static SixLabors.ImageSharp.Tests.TestImages.Jpeg;

// ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Tests.Formats.Jpg;
Expand Down Expand Up @@ -300,6 +301,99 @@ public void Issue2136_DecodeWorks<TPixel>(TestImageProvider<TPixel> provider)
image.CompareToOriginal(provider);
}

/// <summary>
/// Checks restart recovery against clean stored files whose pixels also match in libjpeg.
/// </summary>
/// <param name="originalFile">The clean JPEG input.</param>
/// <param name="malformedFile">The input with extra markers before restart markers.</param>
[Theory]
[InlineData(MarkerDirectory + "baseline.jpg", MarkerDirectory + "baseline-reserved-12.jpg")]
[InlineData(MarkerDirectory + "baseline.jpg", MarkerDirectory + "baseline-tem.jpg")]
[InlineData(MarkerDirectory + "baseline.jpg", MarkerDirectory + "baseline-reserved-bf.jpg")]
[InlineData(MarkerDirectory + "baseline.jpg", MarkerDirectory + "baseline-reserved-chain.jpg")]
[InlineData(MarkerDirectory + "noninterleaved.jpg", MarkerDirectory + "noninterleaved-reserved-12.jpg")]
[InlineData(MarkerDirectory + "progressive.jpg", MarkerDirectory + "progressive-reserved-12.jpg")]
[InlineData(MarkerDirectory + "arithmetic.jpg", MarkerDirectory + "arithmetic-reserved-12.jpg")]
[InlineData(MarkerDirectory + "arithmetic-progressive.jpg", MarkerDirectory + "arithmetic-progressive-reserved-12.jpg")]
public void Decode_ReservedMarkerBeforeRestartMarkers_RecoversWithIgnoreImageData(string originalFile, string malformedFile)
{
// These stored files have complete restart intervals. Libjpeg warns about the extra markers,
// then finds the restart marker and produces the same pixels as the clean file.
// The arithmetic inputs declare their conditioning tables to isolate restart recovery.
DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreImageData };
using Image<Rgba32> expected = Image.Load<Rgba32>(TestFile.Create(originalFile).Bytes);
using Image<Rgba32> actual = Image.Load<Rgba32>(options, TestFile.Create(malformedFile).Bytes);

ImageComparer.Exact.VerifySimilarity(expected, actual);
}

/// <summary>
/// Rejects image-data errors when the selected policy permits only ancillary recovery or no recovery.
/// </summary>
/// <param name="fileName">The input with reserved markers before restart markers.</param>
[Theory]
[InlineData(MarkerDirectory + "baseline-reserved-12.jpg")]
[InlineData(MarkerDirectory + "noninterleaved-reserved-12.jpg")]
[InlineData(MarkerDirectory + "progressive-reserved-12.jpg")]
[InlineData(MarkerDirectory + "arithmetic-reserved-12.jpg")]
[InlineData(MarkerDirectory + "arithmetic-progressive-reserved-12.jpg")]
public void Decode_ReservedMarkerBeforeRestartMarkers_RespectsIntegrity(string fileName)
{
byte[] data = TestFile.Create(fileName).Bytes;
DecoderOptions strict = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict };
DecoderOptions ancillary = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreAncillary };

Assert.Throws<InvalidImageContentException>(() =>
{
using Image<Rgba32> image = Image.Load<Rgba32>(strict, data);
});

Assert.Throws<InvalidImageContentException>(() =>
{
using Image<Rgba32> image = Image.Load<Rgba32>(ancillary, data);
});
}

/// <summary>
/// Accepts a standalone TEM marker between header segments under every integrity policy.
/// </summary>
[Fact]
public void Decode_TemMarkerBetweenHeaderSegments_IsSkipped()
{
byte[] data = TestFile.Create(MarkerDirectory + "header-tem.jpg").Bytes;
using Image<Rgba32> expected = Image.Load<Rgba32>(TestFile.Create(MarkerDirectory + "baseline.jpg").Bytes);

// TEM has no length field. The following bytes belong to the next marker, not to TEM.
foreach (SegmentIntegrityHandling integrity in Enum.GetValues<SegmentIntegrityHandling>())
{
DecoderOptions options = new() { SegmentIntegrityHandling = integrity };
Assert.Equal(new Size(32, 24), Image.Identify(options, data).Size);

using Image<Rgba32> actual = Image.Load<Rgba32>(options, data);
ImageComparer.Exact.VerifySimilarity(expected, actual);
}
}

/// <summary>
/// Keeps required header lengths valid after a standalone TEM marker.
/// </summary>
[Fact]
public void Decode_InvalidQuantizationSegmentAfterTem_RemainsFatal()
{
// TEM precedes DQT in this stored file. DQT declares more bytes than the file contains.
// No image data has been read, so there is no safe point for recovery.
byte[] data = TestFile.Create(MarkerDirectory + "header-tem-invalid-dqt-length.jpg").Bytes;

foreach (SegmentIntegrityHandling integrity in Enum.GetValues<SegmentIntegrityHandling>())
{
DecoderOptions options = new() { SegmentIntegrityHandling = integrity };
Assert.Throws<InvalidImageContentException>(() =>
{
using Image<Rgba32> image = Image.Load<Rgba32>(options, data);
});
}
}

// https://github.com/SixLabors/ImageSharp/issues/2315
// https://github.com/SixLabors/ImageSharp/issues/2334
[Theory]
Expand Down
5 changes: 5 additions & 0 deletions tests/ImageSharp.Tests/TestImages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,11 @@ public static class Bad

public static class Jpeg
{
/// <summary>
/// Stored JPEG marker and restart-recovery inputs.
/// </summary>
public const string MarkerDirectory = "Jpg/markers/";

public static class ICC
{
public const string SRgb = "Jpg/icc-profiles/Momiji-sRGB-yes.jpg";
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions tests/Images/Input/Jpg/markers/arithmetic-progressive.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions tests/Images/Input/Jpg/markers/arithmetic-reserved-12.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions tests/Images/Input/Jpg/markers/arithmetic.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions tests/Images/Input/Jpg/markers/baseline-reserved-12.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions tests/Images/Input/Jpg/markers/baseline-reserved-bf.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions tests/Images/Input/Jpg/markers/baseline-reserved-chain.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions tests/Images/Input/Jpg/markers/baseline-tem.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions tests/Images/Input/Jpg/markers/baseline.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions tests/Images/Input/Jpg/markers/header-tem.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions tests/Images/Input/Jpg/markers/noninterleaved-reserved-12.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions tests/Images/Input/Jpg/markers/noninterleaved.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions tests/Images/Input/Jpg/markers/progressive-reserved-12.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions tests/Images/Input/Jpg/markers/progressive.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading