Skip to content

Commit e69e622

Browse files
Merge pull request #2589 from svenclaesson/better_handle_corrupt_png
Relaxed handle of corrupt png files
2 parents 60e0298 + 091cb1e commit e69e622

File tree

11 files changed

+191
-47
lines changed

11 files changed

+191
-47
lines changed

src/ImageSharp/Formats/Png/PngChunk.cs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,13 @@ public PngChunk(int length, PngChunkType type, IMemoryOwner<byte> data = null)
4141
/// <summary>
4242
/// Gets a value indicating whether the given chunk is critical to decoding
4343
/// </summary>
44-
public bool IsCritical =>
45-
this.Type is PngChunkType.Header or
46-
PngChunkType.Palette or
47-
PngChunkType.Data or
48-
PngChunkType.FrameData;
44+
/// <param name="handling">The chunk CRC handling behavior.</param>
45+
public bool IsCritical(PngCrcChunkHandling handling)
46+
=> handling switch
47+
{
48+
PngCrcChunkHandling.IgnoreNone => true,
49+
PngCrcChunkHandling.IgnoreNonCritical => this.Type is PngChunkType.Header or PngChunkType.Palette or PngChunkType.Data or PngChunkType.FrameData,
50+
PngCrcChunkHandling.IgnoreData => this.Type is PngChunkType.Header or PngChunkType.Palette,
51+
_ => false,
52+
};
4953
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
namespace SixLabors.ImageSharp.Formats.Png;
5+
6+
/// <summary>
7+
/// Specifies how to handle validation of any CRC (Cyclic Redundancy Check) data within the encoded PNG.
8+
/// </summary>
9+
public enum PngCrcChunkHandling
10+
{
11+
/// <summary>
12+
/// Do not ignore any CRC chunk errors.
13+
/// </summary>
14+
IgnoreNone,
15+
16+
/// <summary>
17+
/// Ignore CRC errors in non critical chunks.
18+
/// </summary>
19+
IgnoreNonCritical,
20+
21+
/// <summary>
22+
/// Ignore CRC errors in data chunks.
23+
/// </summary>
24+
IgnoreData,
25+
26+
/// <summary>
27+
/// Ignore CRC errors in all chunks.
28+
/// </summary>
29+
IgnoreAll
30+
}

src/ImageSharp/Formats/Png/PngDecoder.cs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Png;
88
/// <summary>
99
/// Decoder for generating an image out of a png encoded stream.
1010
/// </summary>
11-
public sealed class PngDecoder : ImageDecoder
11+
public sealed class PngDecoder : SpecializedImageDecoder<PngDecoderOptions>
1212
{
1313
private PngDecoder()
1414
{
@@ -25,31 +25,31 @@ protected override ImageInfo Identify(DecoderOptions options, Stream stream, Can
2525
Guard.NotNull(options, nameof(options));
2626
Guard.NotNull(stream, nameof(stream));
2727

28-
return new PngDecoderCore(options).Identify(options.Configuration, stream, cancellationToken);
28+
return new PngDecoderCore(new PngDecoderOptions() { GeneralOptions = options }).Identify(options.Configuration, stream, cancellationToken);
2929
}
3030

3131
/// <inheritdoc/>
32-
protected override Image<TPixel> Decode<TPixel>(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
32+
protected override Image<TPixel> Decode<TPixel>(PngDecoderOptions options, Stream stream, CancellationToken cancellationToken)
3333
{
3434
Guard.NotNull(options, nameof(options));
3535
Guard.NotNull(stream, nameof(stream));
3636

3737
PngDecoderCore decoder = new(options);
38-
Image<TPixel> image = decoder.Decode<TPixel>(options.Configuration, stream, cancellationToken);
38+
Image<TPixel> image = decoder.Decode<TPixel>(options.GeneralOptions.Configuration, stream, cancellationToken);
3939

40-
ScaleToTargetSize(options, image);
40+
ScaleToTargetSize(options.GeneralOptions, image);
4141

4242
return image;
4343
}
4444

4545
/// <inheritdoc/>
46-
protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
46+
protected override Image Decode(PngDecoderOptions options, Stream stream, CancellationToken cancellationToken)
4747
{
4848
Guard.NotNull(options, nameof(options));
4949
Guard.NotNull(stream, nameof(stream));
5050

5151
PngDecoderCore decoder = new(options, true);
52-
ImageInfo info = decoder.Identify(options.Configuration, stream, cancellationToken);
52+
ImageInfo info = decoder.Identify(options.GeneralOptions.Configuration, stream, cancellationToken);
5353
stream.Position = 0;
5454

5555
PngMetadata meta = info.Metadata.GetPngMetadata();
@@ -99,4 +99,7 @@ protected override Image Decode(DecoderOptions options, Stream stream, Cancellat
9999
return this.Decode<Rgba32>(options, stream, cancellationToken);
100100
}
101101
}
102+
103+
/// <inheritdoc/>
104+
protected override PngDecoderOptions CreateDefaultSpecializedOptions(DecoderOptions options) => new PngDecoderOptions() { GeneralOptions = options };
102105
}

src/ImageSharp/Formats/Png/PngDecoderCore.cs

Lines changed: 77 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -114,27 +114,34 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
114114
/// </summary>
115115
private PngChunk? nextChunk;
116116

117+
/// <summary>
118+
/// How to handle CRC errors.
119+
/// </summary>
120+
private readonly PngCrcChunkHandling pngCrcChunkHandling;
121+
117122
/// <summary>
118123
/// Initializes a new instance of the <see cref="PngDecoderCore"/> class.
119124
/// </summary>
120125
/// <param name="options">The decoder options.</param>
121-
public PngDecoderCore(DecoderOptions options)
126+
public PngDecoderCore(PngDecoderOptions options)
122127
{
123-
this.Options = options;
124-
this.configuration = options.Configuration;
125-
this.maxFrames = options.MaxFrames;
126-
this.skipMetadata = options.SkipMetadata;
128+
this.Options = options.GeneralOptions;
129+
this.configuration = options.GeneralOptions.Configuration;
130+
this.maxFrames = options.GeneralOptions.MaxFrames;
131+
this.skipMetadata = options.GeneralOptions.SkipMetadata;
127132
this.memoryAllocator = this.configuration.MemoryAllocator;
133+
this.pngCrcChunkHandling = options.PngCrcChunkHandling;
128134
}
129135

130-
internal PngDecoderCore(DecoderOptions options, bool colorMetadataOnly)
136+
internal PngDecoderCore(PngDecoderOptions options, bool colorMetadataOnly)
131137
{
132-
this.Options = options;
138+
this.Options = options.GeneralOptions;
133139
this.colorMetadataOnly = colorMetadataOnly;
134-
this.maxFrames = options.MaxFrames;
140+
this.maxFrames = options.GeneralOptions.MaxFrames;
135141
this.skipMetadata = true;
136-
this.configuration = options.Configuration;
142+
this.configuration = options.GeneralOptions.Configuration;
137143
this.memoryAllocator = this.configuration.MemoryAllocator;
144+
this.pngCrcChunkHandling = options.PngCrcChunkHandling;
138145
}
139146

140147
/// <inheritdoc/>
@@ -576,11 +583,23 @@ private static void ReadGammaChunk(PngMetadata pngMetadata, ReadOnlySpan<byte> d
576583
private void InitializeImage<TPixel>(ImageMetadata metadata, FrameControl frameControl, out Image<TPixel> image)
577584
where TPixel : unmanaged, IPixel<TPixel>
578585
{
579-
image = Image.CreateUninitialized<TPixel>(
580-
this.configuration,
581-
this.header.Width,
582-
this.header.Height,
583-
metadata);
586+
// When ignoring data CRCs, we can't use the image constructor that leaves the buffer uncleared.
587+
if (this.pngCrcChunkHandling is PngCrcChunkHandling.IgnoreData or PngCrcChunkHandling.IgnoreAll)
588+
{
589+
image = new Image<TPixel>(
590+
this.configuration,
591+
this.header.Width,
592+
this.header.Height,
593+
metadata);
594+
}
595+
else
596+
{
597+
image = Image.CreateUninitialized<TPixel>(
598+
this.configuration,
599+
this.header.Width,
600+
this.header.Height,
601+
metadata);
602+
}
584603

585604
PngFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetPngMetadata();
586605
frameMetadata.FromChunk(in frameControl);
@@ -803,6 +822,11 @@ private void DecodePixelData<TPixel>(
803822
break;
804823

805824
default:
825+
if (this.pngCrcChunkHandling is PngCrcChunkHandling.IgnoreData or PngCrcChunkHandling.IgnoreAll)
826+
{
827+
goto EXIT;
828+
}
829+
806830
PngThrowHelper.ThrowUnknownFilter();
807831
break;
808832
}
@@ -812,6 +836,7 @@ private void DecodePixelData<TPixel>(
812836
currentRow++;
813837
}
814838

839+
EXIT:
815840
blendMemory?.Dispose();
816841
}
817842

@@ -903,6 +928,11 @@ private void DecodeInterlacedPixelData<TPixel>(
903928
break;
904929

905930
default:
931+
if (this.pngCrcChunkHandling is PngCrcChunkHandling.IgnoreData or PngCrcChunkHandling.IgnoreAll)
932+
{
933+
goto EXIT;
934+
}
935+
906936
PngThrowHelper.ThrowUnknownFilter();
907937
break;
908938
}
@@ -937,6 +967,7 @@ private void DecodeInterlacedPixelData<TPixel>(
937967
}
938968
}
939969

970+
EXIT:
940971
blendMemory?.Dispose();
941972
}
942973

@@ -1364,7 +1395,7 @@ private void ReadCompressedTextChunk(ImageMetadata baseMetadata, PngMetadata met
13641395

13651396
ReadOnlySpan<byte> compressedData = data[(zeroIndex + 2)..];
13661397

1367-
if (this.TryUncompressTextData(compressedData, PngConstants.Encoding, out string? uncompressed)
1398+
if (this.TryDecompressTextData(compressedData, PngConstants.Encoding, out string? uncompressed)
13681399
&& !TryReadTextChunkMetadata(baseMetadata, name, uncompressed))
13691400
{
13701401
metadata.TextData.Add(new PngTextData(name, uncompressed, string.Empty, string.Empty));
@@ -1508,19 +1539,19 @@ private void ReadColorProfileChunk(ImageMetadata metadata, ReadOnlySpan<byte> da
15081539

15091540
ReadOnlySpan<byte> compressedData = data[(zeroIndex + 2)..];
15101541

1511-
if (this.TryUncompressZlibData(compressedData, out byte[] iccpProfileBytes))
1542+
if (this.TryDecompressZlibData(compressedData, out byte[] iccpProfileBytes))
15121543
{
15131544
metadata.IccProfile = new IccProfile(iccpProfileBytes);
15141545
}
15151546
}
15161547

15171548
/// <summary>
1518-
/// Tries to un-compress zlib compressed data.
1549+
/// Tries to decompress zlib compressed data.
15191550
/// </summary>
15201551
/// <param name="compressedData">The compressed data.</param>
15211552
/// <param name="uncompressedBytesArray">The uncompressed bytes array.</param>
15221553
/// <returns>True, if de-compressing was successful.</returns>
1523-
private unsafe bool TryUncompressZlibData(ReadOnlySpan<byte> compressedData, out byte[] uncompressedBytesArray)
1554+
private unsafe bool TryDecompressZlibData(ReadOnlySpan<byte> compressedData, out byte[] uncompressedBytesArray)
15241555
{
15251556
fixed (byte* compressedDataBase = compressedData)
15261557
{
@@ -1657,7 +1688,7 @@ private void ReadInternationalTextChunk(ImageMetadata metadata, ReadOnlySpan<byt
16571688
{
16581689
ReadOnlySpan<byte> compressedData = data[dataStartIdx..];
16591690

1660-
if (this.TryUncompressTextData(compressedData, PngConstants.TranslatedEncoding, out string? uncompressed))
1691+
if (this.TryDecompressTextData(compressedData, PngConstants.TranslatedEncoding, out string? uncompressed))
16611692
{
16621693
pngMetadata.TextData.Add(new PngTextData(keyword, uncompressed, language, translatedKeyword));
16631694
}
@@ -1680,9 +1711,9 @@ private void ReadInternationalTextChunk(ImageMetadata metadata, ReadOnlySpan<byt
16801711
/// <param name="encoding">The string encoding to use.</param>
16811712
/// <param name="value">The uncompressed value.</param>
16821713
/// <returns>The <see cref="bool"/>.</returns>
1683-
private bool TryUncompressTextData(ReadOnlySpan<byte> compressedData, Encoding encoding, [NotNullWhen(true)] out string? value)
1714+
private bool TryDecompressTextData(ReadOnlySpan<byte> compressedData, Encoding encoding, [NotNullWhen(true)] out string? value)
16841715
{
1685-
if (this.TryUncompressZlibData(compressedData, out byte[] uncompressedData))
1716+
if (this.TryDecompressZlibData(compressedData, out byte[] uncompressedData))
16861717
{
16871718
value = encoding.GetString(uncompressedData);
16881719
return true;
@@ -1705,7 +1736,11 @@ private int ReadNextDataChunk()
17051736

17061737
Span<byte> buffer = stackalloc byte[20];
17071738

1708-
_ = this.currentStream.Read(buffer, 0, 4);
1739+
int length = this.currentStream.Read(buffer, 0, 4);
1740+
if (length == 0)
1741+
{
1742+
return 0;
1743+
}
17091744

17101745
if (this.TryReadChunk(buffer, out PngChunk chunk))
17111746
{
@@ -1734,7 +1769,11 @@ private int ReadNextFrameDataChunk()
17341769

17351770
Span<byte> buffer = stackalloc byte[20];
17361771

1737-
_ = this.currentStream.Read(buffer, 0, 4);
1772+
int length = this.currentStream.Read(buffer, 0, 4);
1773+
if (length == 0)
1774+
{
1775+
return 0;
1776+
}
17381777

17391778
if (this.TryReadChunk(buffer, out PngChunk chunk))
17401779
{
@@ -1771,21 +1810,27 @@ private bool TryReadChunk(Span<byte> buffer, out PngChunk chunk)
17711810
return true;
17721811
}
17731812

1774-
if (!this.TryReadChunkLength(buffer, out int length))
1813+
if (this.currentStream.Position >= this.currentStream.Length - 1)
17751814
{
1815+
// IEND
17761816
chunk = default;
1817+
return false;
1818+
}
17771819

1820+
if (!this.TryReadChunkLength(buffer, out int length))
1821+
{
17781822
// IEND
1823+
chunk = default;
17791824
return false;
17801825
}
17811826

1782-
while (length < 0 || length > (this.currentStream.Length - this.currentStream.Position))
1827+
while (length < 0)
17831828
{
17841829
// Not a valid chunk so try again until we reach a known chunk.
17851830
if (!this.TryReadChunkLength(buffer, out length))
17861831
{
1832+
// IEND
17871833
chunk = default;
1788-
17891834
return false;
17901835
}
17911836
}
@@ -1797,13 +1842,14 @@ private bool TryReadChunk(Span<byte> buffer, out PngChunk chunk)
17971842
if (this.colorMetadataOnly && type != PngChunkType.Header && type != PngChunkType.Transparency && type != PngChunkType.Palette)
17981843
{
17991844
chunk = new PngChunk(length, type);
1800-
18011845
return true;
18021846
}
18031847

1804-
long pos = this.currentStream.Position;
1848+
// A chunk might report a length that exceeds the length of the stream.
1849+
// Take the minimum of the two values to ensure we don't read past the end of the stream.
1850+
long position = this.currentStream.Position;
18051851
chunk = new PngChunk(
1806-
length: length,
1852+
length: (int)Math.Min(length, this.currentStream.Length - position),
18071853
type: type,
18081854
data: this.ReadChunkData(length));
18091855

@@ -1813,7 +1859,7 @@ private bool TryReadChunk(Span<byte> buffer, out PngChunk chunk)
18131859
// was only read to verifying the CRC is correct.
18141860
if (type is PngChunkType.Data or PngChunkType.FrameData)
18151861
{
1816-
this.currentStream.Position = pos;
1862+
this.currentStream.Position = position;
18171863
}
18181864

18191865
return true;
@@ -1827,8 +1873,7 @@ private bool TryReadChunk(Span<byte> buffer, out PngChunk chunk)
18271873
private void ValidateChunk(in PngChunk chunk, Span<byte> buffer)
18281874
{
18291875
uint inputCrc = this.ReadChunkCrc(buffer);
1830-
1831-
if (chunk.IsCritical)
1876+
if (chunk.IsCritical(this.pngCrcChunkHandling))
18321877
{
18331878
Span<byte> chunkType = stackalloc byte[4];
18341879
BinaryPrimitives.WriteUInt32BigEndian(chunkType, (uint)chunk.Type);
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
namespace SixLabors.ImageSharp.Formats.Png;
5+
6+
/// <summary>
7+
/// Configuration options for decoding png images.
8+
/// </summary>
9+
public sealed class PngDecoderOptions : ISpecializedDecoderOptions
10+
{
11+
/// <inheritdoc/>
12+
public DecoderOptions GeneralOptions { get; init; } = new DecoderOptions();
13+
14+
/// <summary>
15+
/// Gets a value indicating how to handle validation of any CRC (Cyclic Redundancy Check) data within the encoded PNG.
16+
/// </summary>
17+
public PngCrcChunkHandling PngCrcChunkHandling { get; init; } = PngCrcChunkHandling.IgnoreNonCritical;
18+
}

tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public void IfAutoLoadWellKnownFormatsIsTrueAllFormatsAreLoaded()
4343
Assert.Equal(1, this.DefaultFormatsManager.ImageDecoders.Select(item => item.Value).OfType<PngDecoder>().Count());
4444
Assert.Equal(1, this.DefaultFormatsManager.ImageDecoders.Select(item => item.Value).OfType<BmpDecoder>().Count());
4545
Assert.Equal(1, this.DefaultFormatsManager.ImageDecoders.Select(item => item.Value).OfType<JpegDecoder>().Count());
46-
Assert.Equal(1, this.DefaultFormatsManager.ImageDecoders.Select(item => item.Value).OfType<BmpDecoder>().Count());
46+
Assert.Equal(1, this.DefaultFormatsManager.ImageDecoders.Select(item => item.Value).OfType<GifDecoder>().Count());
4747
Assert.Equal(1, this.DefaultFormatsManager.ImageDecoders.Select(item => item.Value).OfType<TgaDecoder>().Count());
4848
Assert.Equal(1, this.DefaultFormatsManager.ImageDecoders.Select(item => item.Value).OfType<TiffDecoder>().Count());
4949
Assert.Equal(1, this.DefaultFormatsManager.ImageDecoders.Select(item => item.Value).OfType<WebpDecoder>().Count());

0 commit comments

Comments
 (0)