diff --git a/.gitattributes b/.gitattributes index 355b64dce1..f97695f901 100644 --- a/.gitattributes +++ b/.gitattributes @@ -115,6 +115,7 @@ *.jpg filter=lfs diff=lfs merge=lfs -text *.jpeg filter=lfs diff=lfs merge=lfs -text *.bmp filter=lfs diff=lfs merge=lfs -text +*.BMP filter=lfs diff=lfs merge=lfs -text *.gif filter=lfs diff=lfs merge=lfs -text *.png filter=lfs diff=lfs merge=lfs -text *.tif filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index b2179fbce5..45769c2163 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -22,7 +22,7 @@ jobs: sdk-preview: true runtime: -x64 codecov: false - - os: macos-latest + - os: macos-13 # macos-latest runs on arm64 runners framework: net6.0 sdk: 6.0.x sdk-preview: true @@ -38,7 +38,7 @@ jobs: framework: net5.0 runtime: -x64 codecov: false - - os: macos-latest + - os: macos-13 # macos-latest runs on arm64 runners framework: net5.0 runtime: -x64 codecov: false @@ -50,7 +50,7 @@ jobs: framework: netcoreapp3.1 runtime: -x64 codecov: false - - os: macos-latest + - os: macos-13 # macos-latest runs on arm64 runners framework: netcoreapp3.1 runtime: -x64 codecov: false @@ -74,6 +74,16 @@ jobs: runs-on: ${{matrix.options.os}} steps: + - name: Install Ubuntu prerequisites + if: ${{ contains(matrix.options.os, 'ubuntu') }} + run: | + # libssl 1.1 (required by old .NET runtimes) + wget http://archive.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.0g-2ubuntu4_amd64.deb + sudo dpkg -i libssl1.1_1.1.0g-2ubuntu4_amd64.deb + + # libgdiplus + sudo apt-get -y install libgdiplus libgif-dev libglib2.0-dev libcairo2-dev libtiff-dev libexif-dev + - name: Git Config shell: bash run: | @@ -91,7 +101,7 @@ jobs: run: git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id - name: Git Setup LFS Cache - uses: actions/cache@v2 + uses: actions/cache@v4 id: lfs-cache with: path: .git/lfs @@ -104,7 +114,7 @@ jobs: uses: NuGet/setup-nuget@v1 - name: NuGet Setup Cache - uses: actions/cache@v2 + uses: actions/cache@v4 id: nuget-cache with: path: ~/.nuget @@ -151,7 +161,7 @@ jobs: XUNIT_PATH: .\tests\ImageSharp.Tests # Required for xunit - name: Export Failed Output - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 if: failure() with: name: actual_output_${{ runner.os }}_${{ matrix.options.framework }}${{ matrix.options.runtime }}.zip @@ -181,7 +191,7 @@ jobs: uses: NuGet/setup-nuget@v1 - name: NuGet Setup Cache - uses: actions/cache@v2 + uses: actions/cache@v4 id: nuget-cache with: path: ~/.nuget @@ -192,9 +202,15 @@ jobs: shell: pwsh run: ./ci-pack.ps1 - - name: MyGet Publish + - name: Feedz Publish shell: pwsh run: | - dotnet nuget push .\artifacts\*.nupkg -k ${{secrets.MYGET_TOKEN}} -s https://www.myget.org/F/sixlabors/api/v2/package - dotnet nuget push .\artifacts\*.snupkg -k ${{secrets.MYGET_TOKEN}} -s https://www.myget.org/F/sixlabors/api/v3/index.json - # TODO: If github.ref starts with 'refs/tags' then it was tag push and we can optionally push out package to nuget.org + dotnet nuget push .\artifacts\*.nupkg -k ${{secrets.FEEDZ_TOKEN}} -s https://f.feedz.io/sixlabors/sixlabors/nuget/index.json --skip-duplicate + dotnet nuget push .\artifacts\*.snupkg -k ${{secrets.FEEDZ_TOKEN}} -s https://f.feedz.io/sixlabors/sixlabors/symbols --skip-duplicate + - name: NuGet Publish + if: ${{ startsWith(github.ref, 'refs/tags/') }} + shell: pwsh + run: | + dotnet nuget push .\artifacts\*.nupkg -k ${{secrets.NUGET_TOKEN}} -s https://api.nuget.org/v3/index.json --skip-duplicate + dotnet nuget push .\artifacts\*.snupkg -k ${{secrets.NUGET_TOKEN}} -s https://api.nuget.org/v3/index.json --skip-duplicate + diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 2b14f2a4b7..45b856a15c 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -34,7 +34,7 @@ jobs: run: git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id - name: Git Setup LFS Cache - uses: actions/cache@v2 + uses: actions/cache@v4 id: lfs-cache with: path: .git/lfs @@ -47,7 +47,7 @@ jobs: uses: NuGet/setup-nuget@v1 - name: NuGet Setup Cache - uses: actions/cache@v2 + uses: actions/cache@v4 id: nuget-cache with: path: ~/.nuget diff --git a/Directory.Build.props b/Directory.Build.props index 26b3cc5afc..524cf27890 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -23,7 +23,7 @@ - preview + 10.0 + + + https://api.nuget.org/v3/index.json; + https://f.feedz.io/sixlabors/sixlabors/nuget/index.json; + https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-eng/nuget/v3/index.json; + + diff --git a/src/Directory.Build.props b/src/Directory.Build.props index faa29865f2..904d404f50 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -18,7 +18,7 @@ - true + diff --git a/src/ImageSharp/Advanced/ParallelRowIterator.cs b/src/ImageSharp/Advanced/ParallelRowIterator.cs index e787b7cfc5..06fbe731d1 100644 --- a/src/ImageSharp/Advanced/ParallelRowIterator.cs +++ b/src/ImageSharp/Advanced/ParallelRowIterator.cs @@ -52,7 +52,7 @@ public static void IterateRows( int width = rectangle.Width; int height = rectangle.Height; - int maxSteps = DivideCeil(width * height, parallelSettings.MinimumPixelsProcessedPerTask); + int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask); int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps); // Avoid TPL overhead in this trivial case: @@ -117,7 +117,7 @@ public static void IterateRows( int width = rectangle.Width; int height = rectangle.Height; - int maxSteps = DivideCeil(width * height, parallelSettings.MinimumPixelsProcessedPerTask); + int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask); int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps); MemoryAllocator allocator = parallelSettings.MemoryAllocator; @@ -181,7 +181,7 @@ public static void IterateRowIntervals( int width = rectangle.Width; int height = rectangle.Height; - int maxSteps = DivideCeil(width * height, parallelSettings.MinimumPixelsProcessedPerTask); + int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask); int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps); // Avoid TPL overhead in this trivial case: @@ -243,7 +243,7 @@ public static void IterateRowIntervals( int width = rectangle.Width; int height = rectangle.Height; - int maxSteps = DivideCeil(width * height, parallelSettings.MinimumPixelsProcessedPerTask); + int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask); int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps); MemoryAllocator allocator = parallelSettings.MemoryAllocator; @@ -270,7 +270,7 @@ public static void IterateRowIntervals( } [MethodImpl(InliningOptions.ShortMethod)] - private static int DivideCeil(int dividend, int divisor) => 1 + ((dividend - 1) / divisor); + private static int DivideCeil(long dividend, int divisor) => (int)Math.Min(1 + ((dividend - 1) / divisor), int.MaxValue); private static void ValidateRectangle(Rectangle rectangle) { diff --git a/src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs b/src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs index f4b0543b84..bb97ff79eb 100644 --- a/src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs +++ b/src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs @@ -161,6 +161,11 @@ public override int Read(byte[] buffer, int offset, int count) bytesToRead = Math.Min(count - totalBytesRead, this.currentDataRemaining); this.currentDataRemaining -= bytesToRead; bytesRead = this.innerStream.Read(buffer, offset, bytesToRead); + if (bytesRead == 0) + { + return totalBytesRead; + } + totalBytesRead += bytesRead; } @@ -168,22 +173,13 @@ public override int Read(byte[] buffer, int offset, int count) } /// - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotSupportedException(); - } + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); /// - public override void SetLength(long value) - { - throw new NotSupportedException(); - } + public override void SetLength(long value) => throw new NotSupportedException(); /// - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotSupportedException(); - } + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); /// protected override void Dispose(bool disposing) @@ -245,22 +241,17 @@ private bool InitializeInflateStream(bool isCriticalChunk) // CINFO is not defined in this specification for CM not equal to 8. throw new ImageFormatException($"Invalid window size for ZLIB header: cinfo={cinfo}"); } - else - { - return false; - } + + return false; } } + else if (isCriticalChunk) + { + throw new ImageFormatException($"Bad method for ZLIB header: cmf={cmf}"); + } else { - if (isCriticalChunk) - { - throw new ImageFormatException($"Bad method for ZLIB header: cmf={cmf}"); - } - else - { - return false; - } + return false; } // The preset dictionary. @@ -269,7 +260,11 @@ private bool InitializeInflateStream(bool isCriticalChunk) { // We don't need this for inflate so simply skip by the next four bytes. // https://tools.ietf.org/html/rfc1950#page-6 - this.innerStream.Read(ChecksumBuffer, 0, 4); + if (this.innerStream.Read(ChecksumBuffer, 0, 4) != 4) + { + return false; + } + this.currentDataRemaining -= 4; } diff --git a/src/ImageSharp/Formats/Gif/GifDecoder.cs b/src/ImageSharp/Formats/Gif/GifDecoder.cs index 6d6cfc0792..d0682e5413 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoder.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoder.cs @@ -23,6 +23,11 @@ public sealed class GifDecoder : IImageDecoder, IGifDecoderOptions, IImageInfoDe /// public FrameDecodingMode DecodingMode { get; set; } = FrameDecodingMode.All; + /// + /// Gets or sets the maximum number of gif frames. + /// + public uint MaxFrames { get; set; } = uint.MaxValue; + /// public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index d17e89cd45..886f667e4c 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -3,6 +3,7 @@ using System; using System.Buffers; +using System.Collections.Generic; using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -22,19 +23,24 @@ namespace SixLabors.ImageSharp.Formats.Gif internal sealed class GifDecoderCore : IImageDecoderInternals { /// - /// The temp buffer used to reduce allocations. + /// The temp buffer. /// - private readonly byte[] buffer = new byte[16]; + private byte[] buffer = new byte[16]; /// - /// The currently loaded stream. + /// The global color table. /// - private BufferedReadStream stream; + private IMemoryOwner globalColorTable; /// - /// The global color table. + /// The current local color table. /// - private IMemoryOwner globalColorTable; + private IMemoryOwner currentLocalColorTable; + + /// + /// Gets the size in bytes of the current local color table. + /// + private int currentLocalColorTableSize; /// /// The area to restore. @@ -56,6 +62,26 @@ internal sealed class GifDecoderCore : IImageDecoderInternals /// private GifImageDescriptor imageDescriptor; + /// + /// The global configuration. + /// + private readonly Configuration configuration; + + /// + /// Used for allocating memory during processing operations. + /// + private readonly MemoryAllocator memoryAllocator; + + /// + /// The maximum number of frames to decode. Inclusive. + /// + private readonly uint maxFrames; + + /// + /// Whether to skip metadata during decode. + /// + private readonly bool skipMetadata; + /// /// The abstract metadata. /// @@ -73,23 +99,14 @@ internal sealed class GifDecoderCore : IImageDecoderInternals /// The decoder options. public GifDecoderCore(Configuration configuration, IGifDecoderOptions options) { - this.IgnoreMetadata = options.IgnoreMetadata; - this.DecodingMode = options.DecodingMode; - this.Configuration = configuration ?? Configuration.Default; + this.skipMetadata = options.IgnoreMetadata; + this.configuration = configuration ?? Configuration.Default; + this.maxFrames = options.DecodingMode == FrameDecodingMode.All ? options.MaxFrames : 1; + this.memoryAllocator = this.configuration.MemoryAllocator; } /// - public Configuration Configuration { get; } - - /// - /// Gets or sets a value indicating whether the metadata should be ignored when the image is being decoded. - /// - public bool IgnoreMetadata { get; internal set; } - - /// - /// Gets the decoding mode for multi-frame images. - /// - public FrameDecodingMode DecodingMode { get; } + public Configuration Configuration => this.configuration; /// /// Gets the dimensions of the image. @@ -102,6 +119,7 @@ public GifDecoderCore(Configuration configuration, IGifDecoderOptions options) public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { + uint frameCount = 0; Image image = null; ImageFrame previousFrame = null; try @@ -114,28 +132,32 @@ public Image Decode(BufferedReadStream stream, CancellationToken { if (nextFlag == GifConstants.ImageLabel) { - if (previousFrame != null && this.DecodingMode == FrameDecodingMode.First) + if (previousFrame != null && ++frameCount == this.maxFrames) { break; } - this.ReadFrame(ref image, ref previousFrame); + this.ReadFrame(stream, ref image, ref previousFrame); + + // Reset per-frame state. + this.imageDescriptor = default; + this.graphicsControlExtension = default; } else if (nextFlag == GifConstants.ExtensionIntroducer) { switch (stream.ReadByte()) { case GifConstants.GraphicControlLabel: - this.ReadGraphicalControlExtension(); + this.ReadGraphicalControlExtension(stream); break; case GifConstants.CommentLabel: - this.ReadComments(); + this.ReadComments(stream); break; case GifConstants.ApplicationExtensionLabel: - this.ReadApplicationExtension(); + this.ReadApplicationExtension(stream); break; case GifConstants.PlainTextLabel: - this.SkipBlock(); // Not supported by any known decoder. + SkipBlock(stream); // Not supported by any known decoder. break; } } @@ -154,6 +176,12 @@ public Image Decode(BufferedReadStream stream, CancellationToken finally { this.globalColorTable?.Dispose(); + this.currentLocalColorTable?.Dispose(); + } + + if (image is null) + { + GifThrowHelper.ThrowInvalidImageContentException("No data"); } return image; @@ -162,6 +190,9 @@ public Image Decode(BufferedReadStream stream, CancellationToken /// public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { + uint frameCount = 0; + ImageFrameMetadata? previousFrame = null; + List framesMetadata = new(); try { this.ReadLogicalScreenDescriptorAndGlobalColorTable(stream); @@ -172,23 +203,32 @@ public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancella { if (nextFlag == GifConstants.ImageLabel) { - this.ReadImageDescriptor(); + if (previousFrame != null && ++frameCount == this.maxFrames) + { + break; + } + + this.ReadFrameMetadata(stream, framesMetadata, ref previousFrame); + + // Reset per-frame state. + this.imageDescriptor = default; + this.graphicsControlExtension = default; } else if (nextFlag == GifConstants.ExtensionIntroducer) { switch (stream.ReadByte()) { case GifConstants.GraphicControlLabel: - this.SkipBlock(); // Skip graphic control extension block + this.ReadGraphicalControlExtension(stream); break; case GifConstants.CommentLabel: - this.ReadComments(); + this.ReadComments(stream); break; case GifConstants.ApplicationExtensionLabel: - this.ReadApplicationExtension(); + this.ReadApplicationExtension(stream); break; case GifConstants.PlainTextLabel: - this.SkipBlock(); // Not supported by any known decoder. + SkipBlock(stream); // Not supported by any known decoder. break; } } @@ -207,6 +247,12 @@ public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancella finally { this.globalColorTable?.Dispose(); + this.currentLocalColorTable?.Dispose(); + } + + if (this.logicalScreenDescriptor.Width == 0 && this.logicalScreenDescriptor.Height == 0) + { + GifThrowHelper.ThrowNoHeader(); } return new ImageInfo( @@ -219,9 +265,10 @@ public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancella /// /// Reads the graphic control extension. /// - private void ReadGraphicalControlExtension() + /// The containing image data. + private void ReadGraphicalControlExtension(BufferedReadStream stream) { - int bytesRead = this.stream.Read(this.buffer, 0, 6); + int bytesRead = stream.Read(this.buffer, 0, 6); if (bytesRead != 6) { GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the graphic control extension"); @@ -233,9 +280,10 @@ private void ReadGraphicalControlExtension() /// /// Reads the image descriptor. /// - private void ReadImageDescriptor() + /// The containing image data. + private void ReadImageDescriptor(BufferedReadStream stream) { - int bytesRead = this.stream.Read(this.buffer, 0, 9); + int bytesRead = stream.Read(this.buffer, 0, 9); if (bytesRead != 9) { GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the image descriptor"); @@ -251,9 +299,10 @@ private void ReadImageDescriptor() /// /// Reads the logical screen descriptor. /// - private void ReadLogicalScreenDescriptor() + /// The containing image data. + private void ReadLogicalScreenDescriptor(BufferedReadStream stream) { - int bytesRead = this.stream.Read(this.buffer, 0, 7); + int bytesRead = stream.Read(this.buffer, 0, 7); if (bytesRead != 7) { GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the logical screen descriptor"); @@ -266,103 +315,115 @@ private void ReadLogicalScreenDescriptor() /// Reads the application extension block parsing any animation or XMP information /// if present. /// - private void ReadApplicationExtension() + /// The containing image data. + private void ReadApplicationExtension(BufferedReadStream stream) { - int appLength = this.stream.ReadByte(); + int appLength = stream.ReadByte(); // 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) { - this.stream.Read(this.buffer, 0, GifConstants.ApplicationBlockSize); + stream.Read(this.buffer, 0, GifConstants.ApplicationBlockSize); bool isXmp = this.buffer.AsSpan().StartsWith(GifConstants.XmpApplicationIdentificationBytes); - - if (isXmp && !this.IgnoreMetadata) + if (isXmp && !this.skipMetadata) { - var extension = GifXmpApplicationExtension.Read(this.stream, this.MemoryAllocator); + GifXmpApplicationExtension extension = GifXmpApplicationExtension.Read(stream, this.memoryAllocator); if (extension.Data.Length > 0) { - this.metadata.XmpProfile = new XmpProfile(extension.Data); + this.metadata!.XmpProfile = new XmpProfile(extension.Data); + } + else + { + // Reset the stream position and continue. + stream.Position = position; + SkipBlock(stream, appLength); } return; } - else - { - int subBlockSize = this.stream.ReadByte(); - // TODO: There's also a NETSCAPE buffer extension. - // http://www.vurdalakov.net/misc/gif/netscape-buffering-application-extension - if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize) - { - this.stream.Read(this.buffer, 0, GifConstants.NetscapeLoopingSubBlockSize); - this.gifMetadata.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.AsSpan(1)).RepeatCount; - this.stream.Skip(1); // Skip the terminator. - return; - } + int subBlockSize = stream.ReadByte(); - // Could be something else not supported yet. - // Skip the subblock and terminator. - this.SkipBlock(subBlockSize); + // 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, 0, GifConstants.NetscapeLoopingSubBlockSize); + this.gifMetadata!.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.AsSpan(1)).RepeatCount; + stream.Skip(1); // Skip the terminator. + return; } + // Could be something else not supported yet. + // Skip the subblock and terminator. + SkipBlock(stream, subBlockSize); + return; } - this.SkipBlock(appLength); // Not supported by any known decoder. + SkipBlock(stream, appLength); // Not supported by any known decoder. } /// /// Skips over a block or reads its terminator. - /// The length of the block to skip. /// - private void SkipBlock(int blockSize = 0) + /// The containing image data. + /// The length of the block to skip. + private static void SkipBlock(BufferedReadStream stream, int blockSize = 0) { if (blockSize > 0) { - this.stream.Skip(blockSize); + stream.Skip(blockSize); } int flag; - while ((flag = this.stream.ReadByte()) > 0) + while ((flag = stream.ReadByte()) > 0) { - this.stream.Skip(flag); + stream.Skip(flag); } } /// /// Reads the gif comments. /// - private void ReadComments() + /// The containing image data. + private void ReadComments(BufferedReadStream stream) { int length; - var stringBuilder = new StringBuilder(); - while ((length = this.stream.ReadByte()) != 0) + StringBuilder stringBuilder = new(); + while ((length = stream.ReadByte()) != 0) { if (length > GifConstants.MaxCommentSubBlockLength) { GifThrowHelper.ThrowInvalidImageContentException($"Gif comment length '{length}' exceeds max '{GifConstants.MaxCommentSubBlockLength}' of a comment data block"); } - if (this.IgnoreMetadata) + if (length == -1) + { + GifThrowHelper.ThrowInvalidImageContentException("Unexpected end of stream while reading gif comment"); + } + + if (this.skipMetadata) { - this.stream.Seek(length, SeekOrigin.Current); + stream.Seek(length, SeekOrigin.Current); continue; } - using IMemoryOwner commentsBuffer = this.MemoryAllocator.Allocate(length); + using IMemoryOwner commentsBuffer = this.memoryAllocator.Allocate(length); Span commentsSpan = commentsBuffer.GetSpan(); - this.stream.Read(commentsSpan); + stream.Read(commentsSpan); string commentPart = GifConstants.Encoding.GetString(commentsSpan); stringBuilder.Append(commentPart); } if (stringBuilder.Length > 0) { - this.gifMetadata.Comments.Add(stringBuilder.ToString()); + this.gifMetadata!.Comments.Add(stringBuilder.ToString()); } } @@ -370,93 +431,77 @@ private void ReadComments() /// Reads an individual gif frame. /// /// The pixel format. + /// The containing image data. /// The image to decode the information to. /// The previous frame. - private void ReadFrame(ref Image image, ref ImageFrame previousFrame) + private void ReadFrame(BufferedReadStream stream, ref Image? image, ref ImageFrame? previousFrame) where TPixel : unmanaged, IPixel { - this.ReadImageDescriptor(); - - IMemoryOwner localColorTable = null; - Buffer2D indices = null; - try - { - // Determine the color table for this frame. If there is a local one, use it otherwise use the global color table. - if (this.imageDescriptor.LocalColorTableFlag) - { - int length = this.imageDescriptor.LocalColorTableSize * 3; - localColorTable = this.Configuration.MemoryAllocator.Allocate(length, AllocationOptions.Clean); - this.stream.Read(localColorTable.GetSpan()); - } + this.ReadImageDescriptor(stream); - indices = this.Configuration.MemoryAllocator.Allocate2D(this.imageDescriptor.Width, this.imageDescriptor.Height, AllocationOptions.Clean); - this.ReadFrameIndices(indices); + // Determine the color table for this frame. If there is a local one, use it otherwise use the global color table. + bool hasLocalColorTable = this.imageDescriptor.LocalColorTableFlag; - Span rawColorTable = default; - if (localColorTable != null) - { - rawColorTable = localColorTable.GetSpan(); - } - else if (this.globalColorTable != null) - { - rawColorTable = this.globalColorTable.GetSpan(); - } - - ReadOnlySpan colorTable = MemoryMarshal.Cast(rawColorTable); - this.ReadFrameColors(ref image, ref previousFrame, indices, colorTable, this.imageDescriptor); + if (hasLocalColorTable) + { + // Read and store the local color table. We allocate the maximum possible size and slice to match. + int length = this.currentLocalColorTableSize = this.imageDescriptor.LocalColorTableSize * 3; + this.currentLocalColorTable ??= this.configuration.MemoryAllocator.Allocate(768, AllocationOptions.Clean); + stream.Read(this.currentLocalColorTable.GetSpan().Slice(0, length)); + } - // Skip any remaining blocks - this.SkipBlock(); + Span rawColorTable = default; + if (hasLocalColorTable) + { + rawColorTable = this.currentLocalColorTable!.GetSpan().Slice(0, this.currentLocalColorTableSize); } - finally + else if (this.globalColorTable != null) { - localColorTable?.Dispose(); - indices?.Dispose(); + rawColorTable = this.globalColorTable.GetSpan(); } - } - /// - /// Reads the frame indices marking the color to use for each pixel. - /// - /// The 2D pixel buffer to write to. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void ReadFrameIndices(Buffer2D indices) - { - int minCodeSize = this.stream.ReadByte(); - using var lzwDecoder = new LzwDecoder(this.Configuration.MemoryAllocator, this.stream); - lzwDecoder.DecodePixels(minCodeSize, indices); + ReadOnlySpan colorTable = MemoryMarshal.Cast(rawColorTable); + this.ReadFrameColors(stream, ref image, ref previousFrame, colorTable, this.imageDescriptor); + + // Skip any remaining blocks + SkipBlock(stream); } /// /// Reads the frames colors, mapping indices to colors. /// /// The pixel format. + /// The containing image data. /// The image to decode the information to. /// The previous frame. - /// The indexed pixels. /// The color table containing the available colors. /// The - private void ReadFrameColors(ref Image image, ref ImageFrame previousFrame, Buffer2D indices, ReadOnlySpan colorTable, in GifImageDescriptor descriptor) + private void ReadFrameColors( + BufferedReadStream stream, + ref Image? image, + ref ImageFrame? previousFrame, + ReadOnlySpan colorTable, + in GifImageDescriptor descriptor) where TPixel : unmanaged, IPixel { int imageWidth = this.logicalScreenDescriptor.Width; int imageHeight = this.logicalScreenDescriptor.Height; bool transFlag = this.graphicsControlExtension.TransparencyFlag; - ImageFrame prevFrame = null; - ImageFrame currentFrame = null; + ImageFrame? prevFrame = null; + ImageFrame? currentFrame = null; ImageFrame imageFrame; if (previousFrame is null) { if (!transFlag) { - image = new Image(this.Configuration, imageWidth, imageHeight, Color.Black.ToPixel(), this.metadata); + image = new Image(this.configuration, imageWidth, imageHeight, Color.Black.ToPixel(), this.metadata); } else { // This initializes the image to become fully transparent because the alpha channel is zero. - image = new Image(this.Configuration, imageWidth, imageHeight, this.metadata); + image = new Image(this.configuration, imageWidth, imageHeight, this.metadata); } this.SetFrameMetadata(image.Frames.RootFrame.Metadata); @@ -470,7 +515,10 @@ private void ReadFrameColors(ref Image image, ref ImageFrame(ref Image image, ref ImageFrame indicesRowOwner = this.memoryAllocator.Allocate(descriptor.Width); + Span indicesRow = indicesRowOwner.Memory.Span; + ref byte indicesRowRef = ref MemoryMarshal.GetReference(indicesRow); + + int minCodeSize = stream.ReadByte(); + if (LzwDecoder.IsValidMinCodeSize(minCodeSize)) { - ref byte indicesRowRef = ref MemoryMarshal.GetReference(indices.DangerousGetRowSpan(y - descriptorTop)); + using LzwDecoder lzwDecoder = new(this.configuration.MemoryAllocator, stream, minCodeSize); - // Check if this image is interlaced. - int writeY; // the target y offset to write to - if (descriptor.InterlaceFlag) + for (int y = descriptorTop; y < descriptorBottom && y < imageHeight; y++) { - // If so then we read lines at predetermined offsets. - // When an entire image height worth of offset lines has been read we consider this a pass. - // With each pass the number of offset lines changes and the starting line changes. - if (interlaceY >= descriptor.Height) + // Check if this image is interlaced. + int writeY; // the target y offset to write to + if (descriptor.InterlaceFlag) { - interlacePass++; - switch (interlacePass) + // If so then we read lines at predetermined offsets. + // When an entire image height worth of offset lines has been read we consider this a pass. + // With each pass the number of offset lines changes and the starting line changes. + if (interlaceY >= descriptor.Height) { - case 1: - interlaceY = 4; - break; - case 2: - interlaceY = 2; - interlaceIncrement = 4; - break; - case 3: - interlaceY = 1; - interlaceIncrement = 2; - break; + interlacePass++; + switch (interlacePass) + { + case 1: + interlaceY = 4; + break; + case 2: + interlaceY = 2; + interlaceIncrement = 4; + break; + case 3: + interlaceY = 1; + interlaceIncrement = 2; + break; + } } - } - writeY = interlaceY + descriptor.Top; - interlaceY += interlaceIncrement; - } - else - { - writeY = y; - } + writeY = Math.Min(interlaceY + descriptor.Top, image.Height); + interlaceY += interlaceIncrement; + } + else + { + writeY = y; + } - ref TPixel rowRef = ref MemoryMarshal.GetReference(imageFrame.PixelBuffer.DangerousGetRowSpan(writeY)); + lzwDecoder.DecodePixelRow(indicesRow); + ref TPixel rowRef = ref MemoryMarshal.GetReference(imageFrame.PixelBuffer.DangerousGetRowSpan(writeY)); - if (!transFlag) - { - // #403 The left + width value can be larger than the image width - for (int x = descriptorLeft; x < descriptorRight && x < imageWidth; x++) + if (!transFlag) { - int index = Numerics.Clamp(Unsafe.Add(ref indicesRowRef, x - descriptorLeft), 0, colorTableMaxIdx); - ref TPixel pixel = ref Unsafe.Add(ref rowRef, x); - Rgb24 rgb = colorTable[index]; - pixel.FromRgb24(rgb); + // #403 The left + width value can be larger than the image width + for (int x = descriptorLeft; x < descriptorRight && x < imageWidth; x++) + { + int index = Numerics.Clamp(Unsafe.Add(ref indicesRowRef, x - descriptorLeft), 0, colorTableMaxIdx); + ref TPixel pixel = ref Unsafe.Add(ref rowRef, x); + Rgb24 rgb = colorTable[index]; + pixel.FromRgb24(rgb); + } } - } - else - { - for (int x = descriptorLeft; x < descriptorRight && x < imageWidth; x++) + else { - int index = Numerics.Clamp(Unsafe.Add(ref indicesRowRef, x - descriptorLeft), 0, colorTableMaxIdx); - if (transIndex != index) + for (int x = descriptorLeft; x < descriptorRight && x < imageWidth; x++) { + int index = Unsafe.Add(ref indicesRowRef, x - descriptorLeft); + + // Treat any out of bounds values as transparent. + if (index > colorTableMaxIdx || index == transIndex) + { + continue; + } + ref TPixel pixel = ref Unsafe.Add(ref rowRef, x); Rgb24 rgb = colorTable[index]; pixel.FromRgb24(rgb); @@ -574,6 +637,43 @@ private void ReadFrameColors(ref Image image, ref ImageFrame + /// Reads the frames metadata. + /// + /// The containing image data. + /// The collection of frame metadata. + /// The previous frame metadata. + private void ReadFrameMetadata(BufferedReadStream stream, List frameMetadata, ref ImageFrameMetadata? previousFrame) + { + this.ReadImageDescriptor(stream); + + // Skip the color table for this frame if local. + if (this.imageDescriptor.LocalColorTableFlag) + { + // Read and store the local color table. We allocate the maximum possible size and slice to match. + int length = this.currentLocalColorTableSize = this.imageDescriptor.LocalColorTableSize * 3; + this.currentLocalColorTable ??= this.configuration.MemoryAllocator.Allocate(768, AllocationOptions.Clean); + stream.Read(this.currentLocalColorTable.GetSpan().Slice(0, length)); + } + + // Skip the frame indices. Pixels length + mincode size. + // The gif format does not tell us the length of the compressed data beforehand. + int minCodeSize = stream.ReadByte(); + if (LzwDecoder.IsValidMinCodeSize(minCodeSize)) + { + using LzwDecoder lzwDecoder = new(this.configuration.MemoryAllocator, stream, minCodeSize); + lzwDecoder.SkipIndices(this.imageDescriptor.Width * this.imageDescriptor.Height); + } + + ImageFrameMetadata currentFrame = new(); + frameMetadata.Add(currentFrame); + this.SetFrameMetadata(currentFrame); + previousFrame = currentFrame; + + // Skip any remaining blocks + SkipBlock(stream); + } + /// /// Restores the current frame area to the background. /// @@ -587,7 +687,7 @@ private void RestoreToBackground(ImageFrame frame) return; } - var interest = Rectangle.Intersect(frame.Bounds(), this.restoreArea.Value); + Rectangle interest = Rectangle.Intersect(frame.Bounds(), this.restoreArea.Value); Buffer2DRegion pixelRegion = frame.PixelBuffer.GetRegion(interest); pixelRegion.Clear(); @@ -595,7 +695,7 @@ private void RestoreToBackground(ImageFrame frame) } /// - /// Sets the frames metadata. + /// Sets the metadata for the image frame. /// /// The metadata. [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -628,13 +728,11 @@ private void SetFrameMetadata(ImageFrameMetadata meta) /// The stream containing image data. private void ReadLogicalScreenDescriptorAndGlobalColorTable(BufferedReadStream stream) { - this.stream = stream; - // Skip the identifier - this.stream.Skip(6); - this.ReadLogicalScreenDescriptor(); + stream.Skip(6); + this.ReadLogicalScreenDescriptor(stream); - var meta = new ImageMetadata(); + ImageMetadata meta = new(); // The Pixel Aspect Ratio is defined to be the quotient of the pixel's // width over its height. The value range in this field allows @@ -671,16 +769,26 @@ private void ReadLogicalScreenDescriptorAndGlobalColorTable(BufferedReadStream s if (this.logicalScreenDescriptor.GlobalColorTableFlag) { int globalColorTableLength = this.logicalScreenDescriptor.GlobalColorTableSize * 3; - this.gifMetadata.GlobalColorTableLength = globalColorTableLength; - if (globalColorTableLength > 0) { - this.globalColorTable = this.MemoryAllocator.Allocate(globalColorTableLength, AllocationOptions.Clean); + this.globalColorTable = this.memoryAllocator.Allocate(globalColorTableLength, AllocationOptions.Clean); - // Read the global color table data from the stream - stream.Read(this.globalColorTable.GetSpan()); + // Read the global color table data from the stream and preserve it in the gif metadata + Span globalColorTableSpan = this.globalColorTable.GetSpan(); + stream.Read(globalColorTableSpan); + + //Color[] colorTable = new Color[this.logicalScreenDescriptor.GlobalColorTableSize]; + //ReadOnlySpan rgbTable = MemoryMarshal.Cast(globalColorTableSpan); + //for (int i = 0; i < colorTable.Length; i++) + //{ + // colorTable[i] = new Color(rgbTable[i]); + //} + + //this.gifMetadata.GlobalColorTable = colorTable; } } + + //this.gifMetadata.BackgroundColorIndex = this.logicalScreenDescriptor.BackgroundColorIndex; } } } diff --git a/src/ImageSharp/Formats/Gif/GifThrowHelper.cs b/src/ImageSharp/Formats/Gif/GifThrowHelper.cs index b85bb139ad..ce6d14b44a 100644 --- a/src/ImageSharp/Formats/Gif/GifThrowHelper.cs +++ b/src/ImageSharp/Formats/Gif/GifThrowHelper.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.Formats.Gif @@ -24,5 +25,11 @@ public static void ThrowInvalidImageContentException(string errorMessage) /// if no inner exception is specified. [MethodImpl(InliningOptions.ColdPath)] public static void ThrowInvalidImageContentException(string errorMessage, Exception innerException) => throw new InvalidImageContentException(errorMessage, innerException); + + [MethodImpl(InliningOptions.ColdPath)] + public static void ThrowNoHeader() => throw new InvalidImageContentException("Gif image does not contain a Logical Screen Descriptor."); + + [MethodImpl(InliningOptions.ColdPath)] + public static void ThrowNoData() => throw new InvalidImageContentException("Unable to read Gif image data"); } } diff --git a/src/ImageSharp/Formats/Gif/IGifDecoderOptions.cs b/src/ImageSharp/Formats/Gif/IGifDecoderOptions.cs index 56bb6d6519..98145e0554 100644 --- a/src/ImageSharp/Formats/Gif/IGifDecoderOptions.cs +++ b/src/ImageSharp/Formats/Gif/IGifDecoderOptions.cs @@ -19,5 +19,10 @@ internal interface IGifDecoderOptions /// Gets the decoding mode for multi-frame images. /// FrameDecodingMode DecodingMode { get; } + + /// + /// Gets or sets the maximum number of gif frames. + /// + uint MaxFrames { get; set; } } } diff --git a/src/ImageSharp/Formats/Gif/LzwDecoder.cs b/src/ImageSharp/Formats/Gif/LzwDecoder.cs index 2a07200016..6ad88f95cf 100644 --- a/src/ImageSharp/Formats/Gif/LzwDecoder.cs +++ b/src/ImageSharp/Formats/Gif/LzwDecoder.cs @@ -20,6 +20,11 @@ internal sealed class LzwDecoder : IDisposable /// private const int MaxStackSize = 4096; + /// + /// The maximum bits for a lzw code. + /// + private const int MaximumLzwBits = 12; + /// /// The null code. /// @@ -33,17 +38,36 @@ internal sealed class LzwDecoder : IDisposable /// /// The prefix buffer. /// - private readonly IMemoryOwner prefix; + private readonly IMemoryOwner prefixOwner; /// /// The suffix buffer. /// - private readonly IMemoryOwner suffix; + private readonly IMemoryOwner suffixOwner; + + /// + /// The scratch buffer for reading data blocks. + /// + private readonly IMemoryOwner bufferOwner; /// /// The pixel stack buffer. /// - private readonly IMemoryOwner pixelStack; + private readonly IMemoryOwner pixelStackOwner; + private readonly int minCodeSize; + private readonly int clearCode; + private readonly int endCode; + private int code; + private int codeSize; + private int codeMask; + private int availableCode; + private int oldCode = NullCode; + private int bits; + private int top; + private int count; + private int bufferIndex; + private int data; + private int first; /// /// Initializes a new instance of the class @@ -51,90 +75,236 @@ internal sealed class LzwDecoder : IDisposable /// /// The to use for buffer allocations. /// The stream to read from. + /// The minimum code size. /// is null. - public LzwDecoder(MemoryAllocator memoryAllocator, BufferedReadStream stream) + public LzwDecoder(MemoryAllocator memoryAllocator, BufferedReadStream stream, int minCodeSize) { this.stream = stream ?? throw new ArgumentNullException(nameof(stream)); + Guard.IsTrue(IsValidMinCodeSize(minCodeSize), nameof(minCodeSize), "Invalid minimum code size."); + + this.prefixOwner = memoryAllocator.Allocate(MaxStackSize, AllocationOptions.Clean); + this.suffixOwner = memoryAllocator.Allocate(MaxStackSize, AllocationOptions.Clean); + this.pixelStackOwner = memoryAllocator.Allocate(MaxStackSize + 1, AllocationOptions.Clean); + this.bufferOwner = memoryAllocator.Allocate(byte.MaxValue, AllocationOptions.None); + this.minCodeSize = minCodeSize; - this.prefix = memoryAllocator.Allocate(MaxStackSize, AllocationOptions.Clean); - this.suffix = memoryAllocator.Allocate(MaxStackSize, AllocationOptions.Clean); - this.pixelStack = memoryAllocator.Allocate(MaxStackSize + 1, AllocationOptions.Clean); + // Calculate the clear code. The value of the clear code is 2 ^ minCodeSize + this.clearCode = 1 << minCodeSize; + this.codeSize = minCodeSize + 1; + this.codeMask = (1 << this.codeSize) - 1; + this.endCode = this.clearCode + 1; + this.availableCode = this.clearCode + 2; + + // Fill the suffix buffer with the initial values represented by the number of colors. + Span suffix = this.suffixOwner.GetSpan().Slice(0, this.clearCode); + int i; + for (i = 0; i < suffix.Length; i++) + { + suffix[i] = i; + } + + this.code = i; } /// - /// Decodes and decompresses all pixel indices from the stream. + /// Gets a value indicating whether the minimum code size is valid. /// - /// Minimum code size of the data. - /// The pixel array to decode to. - public void DecodePixels(int minCodeSize, Buffer2D pixels) + /// The minimum code size. + /// + /// if the minimum code size is valid; otherwise, . + /// + public static bool IsValidMinCodeSize(int minCodeSize) { - // Calculate the clear code. The value of the clear code is 2 ^ minCodeSize - int clearCode = 1 << minCodeSize; - // It is possible to specify a larger LZW minimum code size than the palette length in bits // which may leave a gap in the codes where no colors are assigned. // http://www.matthewflickinger.com/lab/whatsinagif/lzw_image_data.asp#lzw_compression - if (minCodeSize < 2 || clearCode > MaxStackSize) + if (minCodeSize < 2 || minCodeSize > MaximumLzwBits || 1 << minCodeSize > MaxStackSize) { // Don't attempt to decode the frame indices. // Theoretically we could determine a min code size from the length of the provided // color palette but we won't bother since the image is most likely corrupted. - GifThrowHelper.ThrowInvalidImageContentException("Gif Image does not contain a valid LZW minimum code."); + return false; } - // The resulting index table length. - int width = pixels.Width; - int height = pixels.Height; - int length = width * height; + return true; + } - int codeSize = minCodeSize + 1; + /// + /// Decodes and decompresses all pixel indices for a single row from the stream, assigning the pixel values to the buffer. + /// + /// The pixel indices array to decode to. + public void DecodePixelRow(Span indices) + { + indices.Clear(); + + // Get span values from the owners. + Span prefix = this.prefixOwner.GetSpan(); + Span suffix = this.suffixOwner.GetSpan(); + Span pixelStack = this.pixelStackOwner.GetSpan(); + Span buffer = this.bufferOwner.GetSpan(); + + // Cache frequently accessed instance fields into locals. + // This helps avoid repeated field loads inside the tight loop. + BufferedReadStream stream = this.stream; + int top = this.top; + int bits = this.bits; + int codeSize = this.codeSize; + int codeMask = this.codeMask; + int minCodeSize = this.minCodeSize; + int availableCode = this.availableCode; + int oldCode = this.oldCode; + int first = this.first; + int data = this.data; + int count = this.count; + int bufferIndex = this.bufferIndex; + int code = this.code; + int clearCode = this.clearCode; + int endCode = this.endCode; + + int i = 0; + while (i < indices.Length) + { + if (top == 0) + { + if (bits < codeSize) + { + // Load bytes until there are enough bits for a code. + if (count == 0) + { + // Read a new data block. + count = ReadBlock(stream, buffer); + if (count == 0) + { + break; + } - // Calculate the end code - int endCode = clearCode + 1; + bufferIndex = 0; + } - // Calculate the available code. - int availableCode = clearCode + 2; + data += buffer[bufferIndex] << bits; + bits += 8; + bufferIndex++; + count--; + continue; + } - // Jillzhangs Code see: http://giflib.codeplex.com/ - // Adapted from John Cristy's ImageMagick. - int code; - int oldCode = NullCode; - int codeMask = (1 << codeSize) - 1; - int bits = 0; + // Get the next code + code = data & codeMask; + data >>= codeSize; + bits -= codeSize; - int top = 0; - int count = 0; - int bi = 0; - int xyz = 0; + // Interpret the code + if (code > availableCode || code == endCode) + { + break; + } - int data = 0; - int first = 0; + if (code == clearCode) + { + // Reset the decoder + codeSize = minCodeSize + 1; + codeMask = (1 << codeSize) - 1; + availableCode = clearCode + 2; + oldCode = NullCode; + continue; + } - ref int prefixRef = ref MemoryMarshal.GetReference(this.prefix.GetSpan()); - ref int suffixRef = ref MemoryMarshal.GetReference(this.suffix.GetSpan()); - ref int pixelStackRef = ref MemoryMarshal.GetReference(this.pixelStack.GetSpan()); + if (oldCode == NullCode) + { + pixelStack[top++] = suffix[code]; + oldCode = code; + first = code; + continue; + } - for (code = 0; code < clearCode; code++) - { - Unsafe.Add(ref suffixRef, code) = (byte)code; - } + int inCode = code; + if (code == availableCode) + { + pixelStack[top++] = first; + code = oldCode; + } + + while (code > clearCode && top < MaxStackSize) + { + pixelStack[top++] = suffix[code]; + code = prefix[code]; + } - Span buffer = stackalloc byte[byte.MaxValue]; + int suffixCode = suffix[code]; + first = suffixCode; + pixelStack[top++] = suffixCode; - int y = 0; - int x = 0; - int rowMax = width; - ref byte pixelsRowRef = ref MemoryMarshal.GetReference(pixels.DangerousGetRowSpan(y)); - while (xyz < length) - { - // Reset row reference. - if (xyz == rowMax) - { - x = 0; - pixelsRowRef = ref MemoryMarshal.GetReference(pixels.DangerousGetRowSpan(++y)); - rowMax = (y * width) + width; + // Fix for GIFs that have "deferred clear code" as per: + // https://bugzilla.mozilla.org/show_bug.cgi?id=55918 + if (availableCode < MaxStackSize) + { + prefix[availableCode] = oldCode; + suffix[availableCode] = first; + availableCode++; + if (availableCode == codeMask + 1 && availableCode < MaxStackSize) + { + codeSize++; + codeMask = (1 << codeSize) - 1; + } + } + + oldCode = inCode; } + // Pop a pixel off the pixel stack. + top--; + + // Clear missing pixels. + indices[i++] = (byte)pixelStack[top]; + } + + // Write back the local values to the instance fields. + this.top = top; + this.bits = bits; + this.codeSize = codeSize; + this.codeMask = codeMask; + this.availableCode = availableCode; + this.oldCode = oldCode; + this.first = first; + this.data = data; + this.count = count; + this.bufferIndex = bufferIndex; + this.code = code; + } + + /// + /// Decodes and decompresses all pixel indices from the stream allowing skipping of the data. + /// + /// The resulting index table length. + public void SkipIndices(int length) + { + // Get span values from the owners. + Span prefix = this.prefixOwner.GetSpan(); + Span suffix = this.suffixOwner.GetSpan(); + Span pixelStack = this.pixelStackOwner.GetSpan(); + Span buffer = this.bufferOwner.GetSpan(); + + // Cache frequently accessed instance fields into locals. + // This helps avoid repeated field loads inside the tight loop. + BufferedReadStream stream = this.stream; + int top = this.top; + int bits = this.bits; + int codeSize = this.codeSize; + int codeMask = this.codeMask; + int minCodeSize = this.minCodeSize; + int availableCode = this.availableCode; + int oldCode = this.oldCode; + int first = this.first; + int data = this.data; + int count = this.count; + int bufferIndex = this.bufferIndex; + int code = this.code; + int clearCode = this.clearCode; + int endCode = this.endCode; + + int i = 0; + while (i < length) + { if (top == 0) { if (bits < codeSize) @@ -143,19 +313,18 @@ public void DecodePixels(int minCodeSize, Buffer2D pixels) if (count == 0) { // Read a new data block. - count = this.ReadBlock(buffer); + count = ReadBlock(stream, buffer); if (count == 0) { break; } - bi = 0; + bufferIndex = 0; } - data += buffer[bi] << bits; - + data += buffer[bufferIndex] << bits; bits += 8; - bi++; + bufferIndex++; count--; continue; } @@ -183,7 +352,7 @@ public void DecodePixels(int minCodeSize, Buffer2D pixels) if (oldCode == NullCode) { - Unsafe.Add(ref pixelStackRef, top++) = Unsafe.Add(ref suffixRef, code); + pixelStack[top++] = suffix[code]; oldCode = code; first = code; continue; @@ -192,27 +361,26 @@ public void DecodePixels(int minCodeSize, Buffer2D pixels) int inCode = code; if (code == availableCode) { - Unsafe.Add(ref pixelStackRef, top++) = (byte)first; - + pixelStack[top++] = first; code = oldCode; } - while (code > clearCode) + while (code > clearCode && top < MaxStackSize) { - Unsafe.Add(ref pixelStackRef, top++) = Unsafe.Add(ref suffixRef, code); - code = Unsafe.Add(ref prefixRef, code); + pixelStack[top++] = suffix[code]; + code = prefix[code]; } - int suffixCode = Unsafe.Add(ref suffixRef, code); + int suffixCode = suffix[code]; first = suffixCode; - Unsafe.Add(ref pixelStackRef, top++) = suffixCode; + pixelStack[top++] = suffixCode; - // Fix for Gifs that have "deferred clear code" as per here : + // Fix for GIFs that have "deferred clear code" as per: // https://bugzilla.mozilla.org/show_bug.cgi?id=55918 if (availableCode < MaxStackSize) { - Unsafe.Add(ref prefixRef, availableCode) = oldCode; - Unsafe.Add(ref suffixRef, availableCode) = first; + prefix[availableCode] = oldCode; + suffix[availableCode] = first; availableCode++; if (availableCode == codeMask + 1 && availableCode < MaxStackSize) { @@ -227,31 +395,44 @@ public void DecodePixels(int minCodeSize, Buffer2D pixels) // Pop a pixel off the pixel stack. top--; - // Clear missing pixels - xyz++; - Unsafe.Add(ref pixelsRowRef, x++) = (byte)Unsafe.Add(ref pixelStackRef, top); + // Skip missing pixels. + i++; } + + // Write back the local values to the instance fields. + this.top = top; + this.bits = bits; + this.codeSize = codeSize; + this.codeMask = codeMask; + this.availableCode = availableCode; + this.oldCode = oldCode; + this.first = first; + this.data = data; + this.count = count; + this.bufferIndex = bufferIndex; + this.code = code; } /// /// Reads the next data block from the stream. A data block begins with a byte, /// which defines the size of the block, followed by the block itself. /// + /// The stream to read from. /// The buffer to store the block in. /// /// The . /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private int ReadBlock(Span buffer) + private static int ReadBlock(BufferedReadStream stream, Span buffer) { - int bufferSize = this.stream.ReadByte(); + int bufferSize = stream.ReadByte(); if (bufferSize < 1) { return 0; } - int count = this.stream.Read(buffer, 0, bufferSize); + int count = stream.Read(buffer, 0, bufferSize); return count != bufferSize ? 0 : bufferSize; } @@ -259,9 +440,10 @@ private int ReadBlock(Span buffer) /// public void Dispose() { - this.prefix.Dispose(); - this.suffix.Dispose(); - this.pixelStack.Dispose(); + this.prefixOwner.Dispose(); + this.suffixOwner.Dispose(); + this.pixelStackOwner.Dispose(); + this.bufferOwner.Dispose(); } } } diff --git a/src/ImageSharp/Formats/ImageDecoderUtilities.cs b/src/ImageSharp/Formats/ImageDecoderUtilities.cs index 71ecda8938..a2bbe34058 100644 --- a/src/ImageSharp/Formats/ImageDecoderUtilities.cs +++ b/src/ImageSharp/Formats/ImageDecoderUtilities.cs @@ -46,7 +46,8 @@ public static Image Decode( CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - using var bufferedReadStream = new BufferedReadStream(configuration, stream); + // Test may pass a BufferedReadStream in order to monitor EOF hits, if so, use the existing instance. + BufferedReadStream bufferedReadStream = stream as BufferedReadStream ?? new BufferedReadStream(configuration, stream); try { @@ -56,6 +57,13 @@ public static Image Decode( { throw largeImageExceptionFactory(ex, decoder.Dimensions); } + finally + { + if (bufferedReadStream != stream) + { + bufferedReadStream.Dispose(); + } + } } private static InvalidImageContentException DefaultLargeImageExceptionFactory( diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanBuffer.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanBuffer.cs index 3664cb4eb3..c6fe34e22e 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanBuffer.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanBuffer.cs @@ -22,6 +22,9 @@ internal struct HuffmanScanBuffer // Whether there is no more good data to pull from the stream for the current mcu. private bool badData; + // How many times have we hit the eof. + private int eofHitCount; + public HuffmanScanBuffer(BufferedReadStream stream) { this.stream = stream; @@ -31,6 +34,7 @@ public HuffmanScanBuffer(BufferedReadStream stream) this.MarkerPosition = 0; this.badData = false; this.NoData = false; + this.eofHitCount = 0; } /// @@ -211,13 +215,23 @@ public bool FindNextMarker() private int ReadStream() { int value = this.badData ? 0 : this.stream.ReadByte(); - if (value == -1) + + // We've encountered the end of the file stream which means there's no EOI marker or the marker has been read + // during decoding of the SOS marker. + // When reading individual bits 'badData' simply means we have hit a marker, When data is '0' and the stream is exhausted + // we know we have hit the EOI and completed decoding the scan buffer. + if (value == -1 || (this.badData && this.data == 0 && this.stream.Position >= this.stream.Length)) { - // We've encountered the end of the file stream which means there's no EOI marker + // We've hit the end of the file stream more times than allowed which means there's no EOI marker // in the image or the SOS marker has the wrong dimensions set. - this.badData = true; - this.NoData = true; - value = 0; + if (this.eofHitCount > JpegConstants.Huffman.FetchLoop) + { + this.badData = true; + this.NoData = true; + value = 0; + } + + this.eofHitCount++; } return value; diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs index 532892e060..220bc16798 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs @@ -116,7 +116,8 @@ public override void InjectFrameData(JpegFrame frame, IRawJpegData jpegData) this.pixelBuffer = allocator.Allocate2D( frame.PixelWidth, frame.PixelHeight, - this.configuration.PreferContiguousImageBuffers); + this.configuration.PreferContiguousImageBuffers, + AllocationOptions.Clean); this.paddedProxyPixelRow = allocator.Allocate(frame.PixelWidth + 3); // component processors from spectral to Rgba32 @@ -215,4 +216,4 @@ public void Dispose() this.pixelBuffer?.Dispose(); } } -} +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs index eab5e6a082..fc35f82b7a 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs @@ -147,7 +147,7 @@ public static int EstimateQuality(ref Block8x8F table, ReadOnlySpan target quality = (int)Math.Round(5000.0 / sumPercent); } - return quality; + return Numerics.Clamp(quality, MinQualityFactor, MaxQualityFactor); } /// diff --git a/src/ImageSharp/Formats/Pbm/BinaryDecoder.cs b/src/ImageSharp/Formats/Pbm/BinaryDecoder.cs index 33af30434c..25563d2d95 100644 --- a/src/ImageSharp/Formats/Pbm/BinaryDecoder.cs +++ b/src/ImageSharp/Formats/Pbm/BinaryDecoder.cs @@ -72,7 +72,11 @@ private static void ProcessGrayscale(Configuration configuration, Buffer for (int y = 0; y < height; y++) { - stream.Read(rowSpan); + if (stream.Read(rowSpan) < rowSpan.Length) + { + return; + } + Span pixelSpan = pixels.DangerousGetRowSpan(y); PixelOperations.Instance.FromL8Bytes( configuration, @@ -94,7 +98,11 @@ private static void ProcessWideGrayscale(Configuration configuration, Bu for (int y = 0; y < height; y++) { - stream.Read(rowSpan); + if (stream.Read(rowSpan) < rowSpan.Length) + { + return; + } + Span pixelSpan = pixels.DangerousGetRowSpan(y); PixelOperations.Instance.FromL16Bytes( configuration, @@ -116,7 +124,11 @@ private static void ProcessRgb(Configuration configuration, Buffer2D pixelSpan = pixels.DangerousGetRowSpan(y); PixelOperations.Instance.FromRgb24Bytes( configuration, @@ -138,7 +150,11 @@ private static void ProcessWideRgb(Configuration configuration, Buffer2D for (int y = 0; y < height; y++) { - stream.Read(rowSpan); + if (stream.Read(rowSpan) < rowSpan.Length) + { + return; + } + Span pixelSpan = pixels.DangerousGetRowSpan(y); PixelOperations.Instance.FromRgb48Bytes( configuration, @@ -153,7 +169,6 @@ private static void ProcessBlackAndWhite(Configuration configuration, Bu { int width = pixels.Width; int height = pixels.Height; - int startBit = 0; MemoryAllocator allocator = configuration.MemoryAllocator; using IMemoryOwner row = allocator.Allocate(width); Span rowSpan = row.GetSpan(); @@ -163,23 +178,17 @@ private static void ProcessBlackAndWhite(Configuration configuration, Bu for (int x = 0; x < width;) { int raw = stream.ReadByte(); - int bit = startBit; - startBit = 0; - for (; bit < 8; bit++) + if (raw < 0) + { + return; + } + + int stopBit = Math.Min(8, width - x); + for (int bit = 0; bit < stopBit; bit++) { bool bitValue = (raw & (0x80 >> bit)) != 0; rowSpan[x] = bitValue ? black : white; x++; - if (x == width) - { - startBit = (bit + 1) & 7; // Round off to below 8. - if (startBit != 0) - { - stream.Seek(-1, System.IO.SeekOrigin.Current); - } - - break; - } } } diff --git a/src/ImageSharp/Formats/Pbm/BufferedReadStreamExtensions.cs b/src/ImageSharp/Formats/Pbm/BufferedReadStreamExtensions.cs index 581d3e592b..94468f90aa 100644 --- a/src/ImageSharp/Formats/Pbm/BufferedReadStreamExtensions.cs +++ b/src/ImageSharp/Formats/Pbm/BufferedReadStreamExtensions.cs @@ -12,14 +12,20 @@ namespace SixLabors.ImageSharp.Formats.Pbm internal static class BufferedReadStreamExtensions { /// - /// Skip over any whitespace or any comments. + /// Skip over any whitespace or any comments and signal if EOF has been reached. /// - public static void SkipWhitespaceAndComments(this BufferedReadStream stream) + /// The buffered read stream. + /// if EOF has been reached while reading the stream; see langword="true"/> otherwise. + public static bool SkipWhitespaceAndComments(this BufferedReadStream stream) { bool isWhitespace; do { int val = stream.ReadByte(); + if (val < 0) + { + return false; + } // Comments start with '#' and end at the next new-line. if (val == 0x23) @@ -28,8 +34,12 @@ public static void SkipWhitespaceAndComments(this BufferedReadStream stream) do { innerValue = stream.ReadByte(); + if (innerValue < 0) + { + return false; + } } - while (innerValue != 0x0a); + while (innerValue is not 0x0a); // Continue searching for whitespace. val = innerValue; @@ -39,18 +49,31 @@ public static void SkipWhitespaceAndComments(this BufferedReadStream stream) } while (isWhitespace); stream.Seek(-1, SeekOrigin.Current); + return true; } /// - /// Read a decimal text value. + /// Read a decimal text value and signal if EOF has been reached. /// - /// The integer value of the decimal. - public static int ReadDecimal(this BufferedReadStream stream) + /// The buffered read stream. + /// The read value. + /// if EOF has been reached while reading the stream; otherwise. + /// + /// A 'false' return value doesn't mean that the parsing has been failed, since it's possible to reach EOF while reading the last decimal in the file. + /// It's up to the call site to handle such a situation. + /// + public static bool ReadDecimal(this BufferedReadStream stream, out int value) { - int value = 0; + value = 0; while (true) { - int current = stream.ReadByte() - 0x30; + int current = stream.ReadByte(); + if (current < 0) + { + return false; + } + + current -= 0x30; if ((uint)current > 9) { break; @@ -59,7 +82,7 @@ public static int ReadDecimal(this BufferedReadStream stream) value = (value * 10) + current; } - return value; + return true; } } } diff --git a/src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs b/src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs index 749fc0292b..ccd5041239 100644 --- a/src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs +++ b/src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs @@ -90,6 +90,7 @@ public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancella /// Processes the ppm header. /// /// The input stream. + /// An EOF marker has been read before the image has been decoded. private void ProcessHeader(BufferedReadStream stream) { Span buffer = stackalloc byte[2]; @@ -139,14 +140,22 @@ private void ProcessHeader(BufferedReadStream stream) throw new InvalidImageContentException("Unknown of not implemented image type encountered."); } - stream.SkipWhitespaceAndComments(); - int width = stream.ReadDecimal(); - stream.SkipWhitespaceAndComments(); - int height = stream.ReadDecimal(); - stream.SkipWhitespaceAndComments(); + if (!stream.SkipWhitespaceAndComments() || + !stream.ReadDecimal(out int width) || + !stream.SkipWhitespaceAndComments() || + !stream.ReadDecimal(out int height) || + !stream.SkipWhitespaceAndComments()) + { + ThrowPrematureEof(); + } + if (this.ColorType != PbmColorType.BlackAndWhite) { - this.maxPixelValue = stream.ReadDecimal(); + if (!stream.ReadDecimal(out this.maxPixelValue)) + { + ThrowPrematureEof(); + } + if (this.maxPixelValue > 255) { this.ComponentType = PbmComponentType.Short; @@ -169,6 +178,8 @@ private void ProcessHeader(BufferedReadStream stream) meta.Encoding = this.Encoding; meta.ColorType = this.ColorType; meta.ComponentType = this.ComponentType; + + static void ThrowPrematureEof() => throw new InvalidImageContentException("Reached EOF while reading the header."); } private void ProcessPixels(BufferedReadStream stream, Buffer2D pixels) diff --git a/src/ImageSharp/Formats/Pbm/PlainDecoder.cs b/src/ImageSharp/Formats/Pbm/PlainDecoder.cs index aeb527dd20..f5e0378cea 100644 --- a/src/ImageSharp/Formats/Pbm/PlainDecoder.cs +++ b/src/ImageSharp/Formats/Pbm/PlainDecoder.cs @@ -66,13 +66,18 @@ private static void ProcessGrayscale(Configuration configuration, Buffer using IMemoryOwner row = allocator.Allocate(width); Span rowSpan = row.GetSpan(); + bool eofReached = false; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { - byte value = (byte)stream.ReadDecimal(); - stream.SkipWhitespaceAndComments(); - rowSpan[x] = new L8(value); + stream.ReadDecimal(out int value); + rowSpan[x] = new L8((byte)value); + eofReached = !stream.SkipWhitespaceAndComments(); + if (eofReached) + { + break; + } } Span pixelSpan = pixels.DangerousGetRowSpan(y); @@ -80,6 +85,11 @@ private static void ProcessGrayscale(Configuration configuration, Buffer configuration, rowSpan, pixelSpan); + + if (eofReached) + { + return; + } } } @@ -92,13 +102,18 @@ private static void ProcessWideGrayscale(Configuration configuration, Bu using IMemoryOwner row = allocator.Allocate(width); Span rowSpan = row.GetSpan(); + bool eofReached = false; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { - ushort value = (ushort)stream.ReadDecimal(); - stream.SkipWhitespaceAndComments(); - rowSpan[x] = new L16(value); + stream.ReadDecimal(out int value); + rowSpan[x] = new L16((ushort)value); + eofReached = !stream.SkipWhitespaceAndComments(); + if (eofReached) + { + break; + } } Span pixelSpan = pixels.DangerousGetRowSpan(y); @@ -106,6 +121,11 @@ private static void ProcessWideGrayscale(Configuration configuration, Bu configuration, rowSpan, pixelSpan); + + if (eofReached) + { + return; + } } } @@ -118,17 +138,29 @@ private static void ProcessRgb(Configuration configuration, Buffer2D row = allocator.Allocate(width); Span rowSpan = row.GetSpan(); + bool eofReached = false; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { - byte red = (byte)stream.ReadDecimal(); - stream.SkipWhitespaceAndComments(); - byte green = (byte)stream.ReadDecimal(); - stream.SkipWhitespaceAndComments(); - byte blue = (byte)stream.ReadDecimal(); - stream.SkipWhitespaceAndComments(); - rowSpan[x] = new Rgb24(red, green, blue); + if (!stream.ReadDecimal(out int red) || + !stream.SkipWhitespaceAndComments() || + !stream.ReadDecimal(out int green) || + !stream.SkipWhitespaceAndComments()) + { + // Reached EOF before reading a full RGB value + eofReached = true; + break; + } + + stream.ReadDecimal(out int blue); + + rowSpan[x] = new Rgb24((byte)red, (byte)green, (byte)blue); + eofReached = !stream.SkipWhitespaceAndComments(); + if (eofReached) + { + break; + } } Span pixelSpan = pixels.DangerousGetRowSpan(y); @@ -136,6 +168,11 @@ private static void ProcessRgb(Configuration configuration, Buffer2D(Configuration configuration, Buffer2D using IMemoryOwner row = allocator.Allocate(width); Span rowSpan = row.GetSpan(); + bool eofReached = false; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { - ushort red = (ushort)stream.ReadDecimal(); - stream.SkipWhitespaceAndComments(); - ushort green = (ushort)stream.ReadDecimal(); - stream.SkipWhitespaceAndComments(); - ushort blue = (ushort)stream.ReadDecimal(); - stream.SkipWhitespaceAndComments(); - rowSpan[x] = new Rgb48(red, green, blue); + if (!stream.ReadDecimal(out int red) || + !stream.SkipWhitespaceAndComments() || + !stream.ReadDecimal(out int green) || + !stream.SkipWhitespaceAndComments()) + { + // Reached EOF before reading a full RGB value + eofReached = true; + break; + } + + stream.ReadDecimal(out int blue); + + rowSpan[x] = new Rgb48((ushort)red, (ushort)green, (ushort)blue); + eofReached = !stream.SkipWhitespaceAndComments(); + if (eofReached) + { + break; + } } Span pixelSpan = pixels.DangerousGetRowSpan(y); @@ -166,6 +215,11 @@ private static void ProcessWideRgb(Configuration configuration, Buffer2D configuration, rowSpan, pixelSpan); + + if (eofReached) + { + return; + } } } @@ -178,13 +232,19 @@ private static void ProcessBlackAndWhite(Configuration configuration, Bu using IMemoryOwner row = allocator.Allocate(width); Span rowSpan = row.GetSpan(); + bool eofReached = false; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { - int value = stream.ReadDecimal(); - stream.SkipWhitespaceAndComments(); + stream.ReadDecimal(out int value); + rowSpan[x] = value == 0 ? White : Black; + eofReached = !stream.SkipWhitespaceAndComments(); + if (eofReached) + { + break; + } } Span pixelSpan = pixels.DangerousGetRowSpan(y); @@ -192,6 +252,11 @@ private static void ProcessBlackAndWhite(Configuration configuration, Bu configuration, rowSpan, pixelSpan); + + if (eofReached) + { + return; + } } } } diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 12770bc521..d6c3256e06 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -464,7 +464,7 @@ private void ReadGammaChunk(PngMetadata pngMetadata, ReadOnlySpan data) private void InitializeImage(ImageMetadata metadata, out Image image) where TPixel : unmanaged, IPixel { - image = Image.CreateUninitialized( + image = new Image( this.Configuration, this.header.Width, this.header.Height, @@ -1504,6 +1504,9 @@ private void SkipChunkDataAndCrc(in PngChunk chunk) private IMemoryOwner ReadChunkData(int length) { // We rent the buffer here to return it afterwards in Decode() + // We don't want to throw a degenerated memory exception here as we want to allow partial decoding + // so limit the length. + length = (int)Math.Min(length, this.currentStream.Length - this.currentStream.Position); IMemoryOwner buffer = this.Configuration.MemoryAllocator.Allocate(length, AllocationOptions.Clean); this.currentStream.Read(buffer.GetSpan(), 0, length); diff --git a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs index 26bc566d65..afb89836c0 100644 --- a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs +++ b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs @@ -250,6 +250,7 @@ public static void ProcessPaletteScanline( ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); ReadOnlySpan palettePixels = MemoryMarshal.Cast(palette); ref Rgb24 palettePixelsRef = ref MemoryMarshal.GetReference(palettePixels); + int maxIndex = palettePixels.Length - 1; if (paletteAlpha?.Length > 0) { @@ -260,7 +261,7 @@ public static void ProcessPaletteScanline( for (int x = 0; x < header.Width; x++) { - int index = Unsafe.Add(ref scanlineSpanRef, x); + int index = Numerics.Clamp(Unsafe.Add(ref scanlineSpanRef, x), 0, maxIndex); rgba.Rgb = Unsafe.Add(ref palettePixelsRef, index); rgba.A = paletteAlpha.Length > index ? Unsafe.Add(ref paletteAlphaRef, index) : byte.MaxValue; @@ -272,8 +273,8 @@ public static void ProcessPaletteScanline( { for (int x = 0; x < header.Width; x++) { - int index = Unsafe.Add(ref scanlineSpanRef, x); - Rgb24 rgb = Unsafe.Add(ref palettePixelsRef, index); + uint index = Unsafe.Add(ref scanlineSpanRef, x); + Rgb24 rgb = Unsafe.Add(ref palettePixelsRef, (int)Math.Min(index, maxIndex)); pixel.FromRgb24(rgb); Unsafe.Add(ref rowSpanRef, x) = pixel; diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index d101ccd94a..523ead871e 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -97,7 +97,7 @@ public Image Decode(BufferedReadStream stream, CancellationToken throw new UnknownImageFormatException("Width or height cannot be 0"); } - var image = Image.CreateUninitialized(this.Configuration, this.fileHeader.Width, this.fileHeader.Height, this.metadata); + Image image = new(this.Configuration, this.fileHeader.Width, this.fileHeader.Height, this.metadata); Buffer2D pixels = image.GetRootFramePixelBuffer(); if (this.fileHeader.ColorMapType == 1) diff --git a/src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs b/src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs index cce1d278cc..1d7592679c 100644 --- a/src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs +++ b/src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs @@ -43,7 +43,7 @@ public DirectoryReader(Stream stream, MemoryAllocator allocator) public IEnumerable Read() { this.ByteOrder = ReadByteOrder(this.stream); - var headerReader = new HeaderReader(this.stream, this.ByteOrder); + HeaderReader headerReader = new(this.stream, this.ByteOrder); headerReader.ReadFileHeader(); this.nextIfdOffset = headerReader.FirstIfdOffset; @@ -55,7 +55,12 @@ public IEnumerable Read() private static ByteOrder ReadByteOrder(Stream stream) { Span headerBytes = stackalloc byte[2]; - stream.Read(headerBytes); + + if (stream.Read(headerBytes) != 2) + { + throw TiffThrowHelper.ThrowInvalidHeader(); + } + if (headerBytes[0] == TiffConstants.ByteOrderLittleEndian && headerBytes[1] == TiffConstants.ByteOrderLittleEndian) { return ByteOrder.LittleEndian; @@ -74,7 +79,7 @@ private IEnumerable ReadIfds(bool isBigTiff) var readers = new List(); while (this.nextIfdOffset != 0 && this.nextIfdOffset < (ulong)this.stream.Length) { - var reader = new EntryReader(this.stream, this.ByteOrder, this.allocator); + EntryReader reader = new(this.stream, this.ByteOrder, this.allocator); reader.ReadTags(isBigTiff, this.nextIfdOffset); if (reader.BigValues.Count > 0) @@ -88,6 +93,11 @@ private IEnumerable ReadIfds(bool isBigTiff) } } + if (this.nextIfdOffset >= reader.NextIfdOffset && reader.NextIfdOffset != 0) + { + TiffThrowHelper.ThrowImageFormatException("TIFF image contains circular directory offsets"); + } + this.nextIfdOffset = reader.NextIfdOffset; readers.Add(reader); diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index 695359e5ea..37cb9a4fc4 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -329,60 +329,68 @@ public void Encode(Image image, Stream stream) bool alphaCompressionSucceeded = false; using var alphaEncoder = new AlphaEncoder(); Span alphaData = Span.Empty; - if (hasAlpha) + IMemoryOwner encodedAlphaData = null; + try { - // TODO: This can potentially run in an separate task. - IMemoryOwner encodedAlphaData = alphaEncoder.EncodeAlpha(image, this.configuration, this.memoryAllocator, this.alphaCompression, out alphaDataSize); - alphaData = encodedAlphaData.GetSpan(); - if (alphaDataSize < pixelCount) + if (hasAlpha) { - // Only use compressed data, if the compressed data is actually smaller then the uncompressed data. - alphaCompressionSucceeded = true; + // TODO: This can potentially run in an separate task. + encodedAlphaData = alphaEncoder.EncodeAlpha(image, this.configuration, this.memoryAllocator, this.alphaCompression, out alphaDataSize); + alphaData = encodedAlphaData.GetSpan(); + if (alphaDataSize < pixelCount) + { + // Only use compressed data, if the compressed data is actually smaller then the uncompressed data. + alphaCompressionSucceeded = true; + } } - } - // Stats-collection loop. - this.StatLoop(width, height, yStride, uvStride); - it.Init(); - it.InitFilter(); - var info = new Vp8ModeScore(); - var residual = new Vp8Residual(); - do - { - bool dontUseSkip = !this.Proba.UseSkipProba; - info.Clear(); - it.Import(y, u, v, yStride, uvStride, width, height, false); - - // Warning! order is important: first call VP8Decimate() and - // *then* decide how to code the skip decision if there's one. - if (!this.Decimate(it, ref info, this.rdOptLevel) || dontUseSkip) - { - this.CodeResiduals(it, info, residual); - } - else + // Stats-collection loop. + this.StatLoop(width, height, yStride, uvStride); + it.Init(); + it.InitFilter(); + var info = new Vp8ModeScore(); + var residual = new Vp8Residual(); + do { - it.ResetAfterSkip(); + bool dontUseSkip = !this.Proba.UseSkipProba; + info.Clear(); + it.Import(y, u, v, yStride, uvStride, width, height, false); + + // Warning! order is important: first call VP8Decimate() and + // *then* decide how to code the skip decision if there's one. + if (!this.Decimate(it, ref info, this.rdOptLevel) || dontUseSkip) + { + this.CodeResiduals(it, info, residual); + } + else + { + it.ResetAfterSkip(); + } + + it.SaveBoundary(); } + while (it.Next()); - it.SaveBoundary(); + // Store filter stats. + this.AdjustFilterStrength(); + + // Write bytes from the bitwriter buffer to the stream. + ImageMetadata metadata = image.Metadata; + metadata.SyncProfiles(); + this.bitWriter.WriteEncodedImageToStream( + stream, + metadata.ExifProfile, + metadata.XmpProfile, + (uint)width, + (uint)height, + hasAlpha, + alphaData.Slice(0, alphaDataSize), + this.alphaCompression && alphaCompressionSucceeded); + } + finally + { + encodedAlphaData?.Dispose(); } - while (it.Next()); - - // Store filter stats. - this.AdjustFilterStrength(); - - // Write bytes from the bitwriter buffer to the stream. - ImageMetadata metadata = image.Metadata; - metadata.SyncProfiles(); - this.bitWriter.WriteEncodedImageToStream( - stream, - metadata.ExifProfile, - metadata.XmpProfile, - (uint)width, - (uint)height, - hasAlpha, - alphaData, - this.alphaCompression && alphaCompressionSucceeded); } /// diff --git a/src/ImageSharp/IO/BufferedReadStream.cs b/src/ImageSharp/IO/BufferedReadStream.cs index 2823b8ed6f..28103359e6 100644 --- a/src/ImageSharp/IO/BufferedReadStream.cs +++ b/src/ImageSharp/IO/BufferedReadStream.cs @@ -65,6 +65,11 @@ public BufferedReadStream(Configuration configuration, Stream stream) this.readBufferIndex = this.BufferSize; } + /// + /// Gets the number indicating the EOF hits occured while reading from this instance. + /// + public int EofHitCount { get; private set; } + /// /// Gets the size, in bytes, of the underlying buffer. /// @@ -138,6 +143,7 @@ public override int ReadByte() { if (this.readerPosition >= this.Length) { + this.EofHitCount++; return -1; } @@ -303,7 +309,7 @@ private int ReadToBufferViaCopyFast(Span buffer) this.readerPosition += n; this.readBufferIndex += n; - + this.CheckEof(n); return n; } @@ -361,6 +367,7 @@ private int ReadToBufferDirectSlow(Span buffer) this.Position += n; + this.CheckEof(n); return n; } @@ -427,5 +434,14 @@ private unsafe void CopyBytes(byte[] buffer, int offset, int count) Buffer.BlockCopy(this.readBuffer, this.readBufferIndex, buffer, offset, count); } } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CheckEof(int read) + { + if (read == 0) + { + this.EofHitCount++; + } + } } } diff --git a/src/ImageSharp/Memory/Allocators/Internals/UnmanagedMemoryHandle.cs b/src/ImageSharp/Memory/Allocators/Internals/UnmanagedMemoryHandle.cs index 59d4d5bda4..df8dfbe0a0 100644 --- a/src/ImageSharp/Memory/Allocators/Internals/UnmanagedMemoryHandle.cs +++ b/src/ImageSharp/Memory/Allocators/Internals/UnmanagedMemoryHandle.cs @@ -14,7 +14,7 @@ namespace SixLabors.ImageSharp.Memory.Internals internal struct UnmanagedMemoryHandle : IEquatable { // Number of allocation re-attempts when detecting OutOfMemoryException. - private const int MaxAllocationAttempts = 1000; + private const int MaxAllocationAttempts = 10; // Track allocations for testing purposes: private static int totalOutstandingHandles; diff --git a/src/ImageSharp/Memory/Allocators/MemoryAllocator.cs b/src/ImageSharp/Memory/Allocators/MemoryAllocator.cs index 4df78d9d93..c67af18c3d 100644 --- a/src/ImageSharp/Memory/Allocators/MemoryAllocator.cs +++ b/src/ImageSharp/Memory/Allocators/MemoryAllocator.cs @@ -3,6 +3,8 @@ using System; using System.Buffers; +using System.Collections.Generic; +using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.Memory { @@ -11,6 +13,8 @@ namespace SixLabors.ImageSharp.Memory /// public abstract class MemoryAllocator { + private const int OneGigabyte = 1 << 30; + /// /// Gets the default platform-specific global instance that /// serves as the default value for . @@ -21,6 +25,10 @@ public abstract class MemoryAllocator /// public static MemoryAllocator Default { get; } = Create(); + internal long MemoryGroupAllocationLimitBytes { get; private set; } = Environment.Is64BitProcess ? 4L * OneGigabyte : OneGigabyte; + + internal int SingleBufferAllocationLimitBytes { get; private set; } = OneGigabyte; + /// /// Gets the length of the largest contiguous buffer that can be handled by this allocator instance in bytes. /// @@ -31,16 +39,24 @@ public abstract class MemoryAllocator /// Creates a default instance of a optimized for the executing platform. /// /// The . - public static MemoryAllocator Create() => - new UniformUnmanagedMemoryPoolMemoryAllocator(null); + public static MemoryAllocator Create() => Create(default); /// /// Creates the default using the provided options. /// /// The . /// The . - public static MemoryAllocator Create(MemoryAllocatorOptions options) => - new UniformUnmanagedMemoryPoolMemoryAllocator(options.MaximumPoolSizeMegabytes); + public static MemoryAllocator Create(MemoryAllocatorOptions options) + { + UniformUnmanagedMemoryPoolMemoryAllocator allocator = new(options.MaximumPoolSizeMegabytes); + if (options.AllocationLimitMegabytes.HasValue) + { + allocator.MemoryGroupAllocationLimitBytes = options.AllocationLimitMegabytes.Value * 1024L * 1024L; + allocator.SingleBufferAllocationLimitBytes = (int)Math.Min(allocator.SingleBufferAllocationLimitBytes, allocator.MemoryGroupAllocationLimitBytes); + } + + return allocator; + } /// /// Allocates an , holding a of length . @@ -65,16 +81,35 @@ public virtual void ReleaseRetainedResources() /// /// Allocates a . /// + /// The type of element to allocate. /// The total length of the buffer. /// The expected alignment (eg. to make sure image rows fit into single buffers). /// The . /// A new . /// Thrown when 'blockAlignment' converted to bytes is greater than the buffer capacity of the allocator. - internal virtual MemoryGroup AllocateGroup( + internal MemoryGroup AllocateGroup( long totalLength, int bufferAlignment, AllocationOptions options = AllocationOptions.None) where T : struct - => MemoryGroup.Allocate(this, totalLength, bufferAlignment, options); + { + if (totalLength < 0) + { + throw new InvalidMemoryOperationException($"Attempted to allocate a buffer of negative length={totalLength}."); + } + + ulong totalLengthInBytes = (ulong)totalLength * (ulong)Unsafe.SizeOf(); + if (totalLengthInBytes > (ulong)this.MemoryGroupAllocationLimitBytes) + { + throw new InvalidMemoryOperationException($"Attempted to allocate a buffer of length={totalLengthInBytes} that exceeded the limit {this.MemoryGroupAllocationLimitBytes}."); + } + + // Cast to long is safe because we already checked that the total length is within the limit. + return this.AllocateGroupCore(totalLength, (long)totalLengthInBytes, bufferAlignment, options); + } + + internal virtual MemoryGroup AllocateGroupCore(long totalLengthInElements, long totalLengthInBytes, int bufferAlignment, AllocationOptions options) + where T : struct + => MemoryGroup.Allocate(this, totalLengthInElements, bufferAlignment, options); } } diff --git a/src/ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs b/src/ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs index 22a0410755..70dc515fe4 100644 --- a/src/ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs +++ b/src/ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. namespace SixLabors.ImageSharp.Memory @@ -9,6 +9,7 @@ namespace SixLabors.ImageSharp.Memory public struct MemoryAllocatorOptions { private int? maximumPoolSizeMegabytes; + private int? allocationLimitMegabytes; /// /// Gets or sets a value defining the maximum size of the 's internal memory pool @@ -27,5 +28,23 @@ public int? MaximumPoolSizeMegabytes this.maximumPoolSizeMegabytes = value; } } + + /// + /// Gets or sets a value defining the maximum (discontiguous) buffer size that can be allocated by the allocator in Megabytes. + /// means platform default: 1GB on 32-bit processes, 4GB on 64-bit processes. + /// + public int? AllocationLimitMegabytes + { + readonly get => this.allocationLimitMegabytes; + set + { + if (value.HasValue) + { + Guard.MustBeGreaterThan(value.Value, 0, nameof(this.AllocationLimitMegabytes)); + } + + this.allocationLimitMegabytes = value; + } + } } } diff --git a/src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs b/src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs index a53ecbc66e..e2b132139b 100644 --- a/src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs +++ b/src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs @@ -1,7 +1,8 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. using System.Buffers; +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Memory.Internals; namespace SixLabors.ImageSharp.Memory @@ -17,7 +18,16 @@ public sealed class SimpleGcMemoryAllocator : MemoryAllocator /// public override IMemoryOwner Allocate(int length, AllocationOptions options = AllocationOptions.None) { - Guard.MustBeGreaterThanOrEqualTo(length, 0, nameof(length)); + if (length < 0) + { + throw new InvalidMemoryOperationException($"Attempted to allocate a buffer of negative length={length}."); + } + + ulong lengthInBytes = (ulong)length * (ulong)Unsafe.SizeOf(); + if (lengthInBytes > (ulong)this.SingleBufferAllocationLimitBytes) + { + throw new InvalidMemoryOperationException($"Attempted to allocate a buffer of length={lengthInBytes} that exceeded the limit {this.SingleBufferAllocationLimitBytes}."); + } return new BasicArrayBuffer(new T[length]); } diff --git a/src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs b/src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs index 0da4ff9f8c..a8056db537 100644 --- a/src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs +++ b/src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs @@ -87,10 +87,18 @@ public override IMemoryOwner Allocate( int length, AllocationOptions options = AllocationOptions.None) { - Guard.MustBeGreaterThanOrEqualTo(length, 0, nameof(length)); - int lengthInBytes = length * Unsafe.SizeOf(); + if (length < 0) + { + throw new InvalidMemoryOperationException($"Attempted to allocate a buffer of negative length={length}."); + } + + ulong lengthInBytes = (ulong)length * (ulong)Unsafe.SizeOf(); + if (lengthInBytes > (ulong)this.SingleBufferAllocationLimitBytes) + { + throw new InvalidMemoryOperationException($"Attempted to allocate a buffer of length={lengthInBytes} that exceeded the limit {this.SingleBufferAllocationLimitBytes}."); + } - if (lengthInBytes <= this.sharedArrayPoolThresholdInBytes) + if (lengthInBytes <= (ulong)this.sharedArrayPoolThresholdInBytes) { var buffer = new SharedArrayPoolBuffer(length); if (options.Has(AllocationOptions.Clean)) @@ -101,7 +109,7 @@ public override IMemoryOwner Allocate( return buffer; } - if (lengthInBytes <= this.poolBufferSizeInBytes) + if (lengthInBytes <= (ulong)this.poolBufferSizeInBytes) { UnmanagedMemoryHandle mem = this.pool.Rent(); if (mem.IsValid) @@ -115,15 +123,15 @@ public override IMemoryOwner Allocate( } /// - internal override MemoryGroup AllocateGroup( - long totalLength, + internal override MemoryGroup AllocateGroupCore( + long totalLengthInElements, + long totalLengthInBytes, int bufferAlignment, AllocationOptions options = AllocationOptions.None) { - long totalLengthInBytes = totalLength * Unsafe.SizeOf(); if (totalLengthInBytes <= this.sharedArrayPoolThresholdInBytes) { - var buffer = new SharedArrayPoolBuffer((int)totalLength); + var buffer = new SharedArrayPoolBuffer((int)totalLengthInElements); return MemoryGroup.CreateContiguous(buffer, options.Has(AllocationOptions.Clean)); } @@ -133,18 +141,18 @@ internal override MemoryGroup AllocateGroup( UnmanagedMemoryHandle mem = this.pool.Rent(); if (mem.IsValid) { - UnmanagedBuffer buffer = this.pool.CreateGuardedBuffer(mem, (int)totalLength, options.Has(AllocationOptions.Clean)); + UnmanagedBuffer buffer = this.pool.CreateGuardedBuffer(mem, (int)totalLengthInElements, options.Has(AllocationOptions.Clean)); return MemoryGroup.CreateContiguous(buffer, options.Has(AllocationOptions.Clean)); } } // Attempt to rent the whole group from the pool, allocate a group of unmanaged buffers if the attempt fails: - if (MemoryGroup.TryAllocate(this.pool, totalLength, bufferAlignment, options, out MemoryGroup poolGroup)) + if (MemoryGroup.TryAllocate(this.pool, totalLengthInElements, bufferAlignment, options, out MemoryGroup? poolGroup)) { return poolGroup; } - return MemoryGroup.Allocate(this.nonPoolAllocator, totalLength, bufferAlignment, options); + return MemoryGroup.Allocate(this.nonPoolAllocator, totalLengthInElements, bufferAlignment, options); } public override void ReleaseRetainedResources() => this.pool.Release(); diff --git a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs index 9844f4a3bc..d77c2c4dc0 100644 --- a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs +++ b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs @@ -20,7 +20,7 @@ namespace SixLabors.ImageSharp.Memory /// The element type. internal abstract partial class MemoryGroup : IMemoryGroup, IDisposable where T : struct - { + { private static readonly int ElementSize = Unsafe.SizeOf(); private MemoryGroupSpanCache memoryGroupSpanCache; @@ -43,7 +43,7 @@ private MemoryGroup(int bufferLength, long totalLength) /// public bool IsValid { get; private set; } = true; - public MemoryGroupView View { get; private set; } + public MemoryGroupView View { get; private set; } = null!; /// public abstract Memory this[int index] { get; } @@ -85,12 +85,14 @@ public static MemoryGroup Allocate( { int bufferCapacityInBytes = allocator.GetBufferCapacityInBytes(); Guard.NotNull(allocator, nameof(allocator)); - Guard.MustBeGreaterThanOrEqualTo(totalLengthInElements, 0, nameof(totalLengthInElements)); - Guard.MustBeGreaterThanOrEqualTo(bufferAlignmentInElements, 0, nameof(bufferAlignmentInElements)); - int blockCapacityInElements = bufferCapacityInBytes / ElementSize; + if (totalLengthInElements < 0) + { + throw new InvalidMemoryOperationException($"Attempted to allocate a buffer of negative length={totalLengthInElements}."); + } - if (bufferAlignmentInElements > blockCapacityInElements) + int blockCapacityInElements = bufferCapacityInBytes / ElementSize; + if (bufferAlignmentInElements < 0 || bufferAlignmentInElements > blockCapacityInElements) { throw new InvalidMemoryOperationException( $"The buffer capacity of the provided MemoryAllocator is insufficient for the requested buffer alignment: {bufferAlignmentInElements}."); diff --git a/src/ImageSharp/Memory/InvalidMemoryOperationException.cs b/src/ImageSharp/Memory/InvalidMemoryOperationException.cs index 92b1d8d359..e9730fb964 100644 --- a/src/ImageSharp/Memory/InvalidMemoryOperationException.cs +++ b/src/ImageSharp/Memory/InvalidMemoryOperationException.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Diagnostics.CodeAnalysis; namespace SixLabors.ImageSharp.Memory { diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs index 43ec45a34f..acc4c201b7 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs @@ -4,7 +4,7 @@ using System; using System.IO; using Microsoft.DotNet.RemoteExecutor; - +using Microsoft.DotNet.XUnitExtensions; using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; @@ -619,5 +619,18 @@ public void BmpDecoder_CanDecode_Os2BitmapArray(TestImageProvider(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + // On V2 this is throwing InvalidOperationException, + // because of the validation logic in BmpInfoHeader.VerifyDimensions(). + Assert.Throws(() => + { + using Image image = provider.GetImage(BmpDecoder); + }); + } } } diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs index 7a5241c5a8..124a2688a3 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs @@ -183,6 +183,17 @@ public void Issue1530_BadDescriptorDimensions(TestImageProvider image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact); } + // https://github.com/SixLabors/ImageSharp/issues/2758 + [Theory] + [WithFile(TestImages.Gif.Issues.Issue2758, PixelTypes.Rgba32)] + public void Issue2758_BadDescriptorDimensions(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + image.DebugSaveMultiFrame(provider); + image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact); + } + // https://github.com/SixLabors/ImageSharp/issues/405 [Theory] [WithFile(TestImages.Gif.Issues.BadAppExtLength, PixelTypes.Rgba32)] @@ -279,15 +290,9 @@ public void Issue2012EmptyXmp(TestImageProvider provider) public void Issue2012BadMinCode(TestImageProvider provider) where TPixel : unmanaged, IPixel { - Exception ex = Record.Exception( - () => - { - using Image image = provider.GetImage(); - image.DebugSave(provider); - }); - - Assert.NotNull(ex); - Assert.Contains("Gif Image does not contain a valid LZW minimum code.", ex.Message); + using Image image = provider.GetImage(); + image.DebugSave(provider); + image.CompareToReferenceOutput(provider); } // https://bugzilla.mozilla.org/show_bug.cgi?id=55918 @@ -301,5 +306,41 @@ public void IssueDeferredClearCode(TestImageProvider provider) image.DebugSave(provider); image.CompareFirstFrameToReferenceOutput(ImageComparer.Exact, provider); } + + // https://github.com/SixLabors/ImageSharp/issues/2743 + [Theory] + [WithFile(TestImages.Gif.Issues.BadMaxLzwBits, PixelTypes.Rgba32)] + public void IssueTooLargeLzwBits(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + image.DebugSaveMultiFrame(provider); + image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact); + } + + // https://github.com/SixLabors/ImageSharp/issues/2859 + [Theory] + [WithFile(TestImages.Gif.Issues.Issue2859_A, PixelTypes.Rgba32)] + [WithFile(TestImages.Gif.Issues.Issue2859_B, PixelTypes.Rgba32)] + public void Issue2859_LZWPixelStackOverflow(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + image.DebugSaveMultiFrame(provider); + image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact); + } + + // https://github.com/SixLabors/ImageSharp/issues/2953 + [Theory] + [WithFile(TestImages.Gif.Issues.Issue2953, PixelTypes.Rgba32)] + public void Issue2953(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + // We should throw a InvalidImageContentException when trying to identify or load an invalid GIF file. + var testFile = TestFile.Create(provider.SourceFileOrDescription); + + Assert.Throws(() => Image.Identify(testFile.FullPath)); + Assert.Throws(() => Image.Load(testFile.FullPath)); + } } } diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs index efabed5b29..8bd9a96ae4 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs @@ -190,5 +190,20 @@ public void Decode_VerifyRepeatCount(string imagePath, uint repeatCount) } } } + + [Theory] + [InlineData(TestImages.Gif.Issues.BadMaxLzwBits, 8)] + [InlineData(TestImages.Gif.Issues.Issue2012BadMinCode, 1)] + public void Identify_Frames_Bad_Lzw(string imagePath, int framesCount) + { + TestFile testFile = TestFile.Create(imagePath); + using MemoryStream stream = new(testFile.Bytes, false); + + IImageInfo imageInfo = Image.Identify(stream); + + Assert.NotNull(imageInfo); + GifMetadata gifMetadata = imageInfo.Metadata.GetGifMetadata(); + Assert.NotNull(gifMetadata); + } } } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs index d9915f17d6..d662a23bcc 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs @@ -10,6 +10,7 @@ using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; using Xunit; @@ -364,6 +365,31 @@ public void EncodedStringTags_Read() } } + [Theory(Skip = "2.1 JPEG decoder detects this image as invalid.")] + [WithFile(TestImages.Jpeg.Issues.Issue2758, PixelTypes.L8)] + public void Issue2758_DecodeWorks(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + + Assert.Equal(59787, image.Width); + Assert.Equal(511, image.Height); + + JpegMetadata meta = image.Metadata.GetJpegMetadata(); + + // Quality determination should be between 1-100. + Assert.Equal(15, meta.LuminanceQuality); + Assert.Equal(1, meta.ChrominanceQuality); + + // We want to test the encoder to ensure the determined values can be encoded but not by encoding + // the full size image as it would be too slow. + // We will crop the image to a smaller size and then encode it. + image.Mutate(x => x.Crop(new(0, 0, 100, 100))); + + using MemoryStream ms = new(); + image.Save(ms, new JpegEncoder()); + } + private static void VerifyEncodedStrings(ExifProfile exif) { Assert.NotNull(exif); diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs index d12c840a1b..284e4fa254 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs @@ -229,6 +229,19 @@ public void Issue2133_DeduceColorSpace(TestImageProvider provide } } + // https://github.com/SixLabors/ImageSharp/issues/2638 + [Theory] + [WithFile(TestImages.Jpeg.Issues.Issue2638, PixelTypes.Rgba32)] + public void Issue2638_DecodeWorks(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage(JpegDecoder)) + { + image.DebugSave(provider); + image.CompareToOriginal(provider); + } + } + // DEBUG ONLY! // The PDF.js output should be saved by "tests\ImageSharp.Tests\Formats\Jpg\pdfjs\jpeg-converter.htm" // into "\tests\Images\ActualOutput\JpegDecoderTests\" @@ -261,5 +274,22 @@ public void ValidateProgressivePdfJsOutput( this.Output.WriteLine($"Difference for PORT: {portReport.DifferencePercentageString}"); } } + + [Theory] + [WithFile(TestImages.Jpeg.Issues.HangBadScan, PixelTypes.L8)] + public void DecodeHang(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + if (TestEnvironment.IsWindows && + TestEnvironment.RunsOnCI) + { + // Windows CI runs consistently fail with OOM. + return; + } + + using Image image = provider.GetImage(JpegDecoder); + Assert.Equal(65503, image.Width); + Assert.Equal(65503, image.Height); + } } } diff --git a/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs index eb3bc8c9a5..8708320e09 100644 --- a/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs @@ -2,8 +2,10 @@ // Licensed under the Apache License, Version 2.0. using System.IO; +using System.Text; using SixLabors.ImageSharp.Formats.Pbm; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities; using Xunit; using static SixLabors.ImageSharp.Tests.TestImages.Pbm; @@ -97,5 +99,25 @@ public void DecodeReferenceImage(TestImageProvider provider, str bool isGrayscale = extension is "pgm" or "pbm"; image.CompareToReferenceOutput(provider, grayscale: isGrayscale); } + + + [Fact] + public void PlainText_PrematureEof() + { + byte[] bytes = Encoding.ASCII.GetBytes($"P1\n100 100\n1 0 1 0 1 0"); + using EofHitCounter eofHitCounter = EofHitCounter.RunDecoder(bytes); + + Assert.True(eofHitCounter.EofHitCount <= 2); + Assert.Equal(new Size(100, 100), eofHitCounter.Image.Size()); + } + + [Fact] + public void Binary_PrematureEof() + { + using EofHitCounter eofHitCounter = EofHitCounter.RunDecoder(RgbBinaryPrematureEof); + + Assert.True(eofHitCounter.EofHitCount <= 2); + Assert.Equal(new Size(29, 30), eofHitCounter.Image.Size()); + } } } diff --git a/tests/ImageSharp.Tests/Formats/Pbm/PbmMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Pbm/PbmMetadataTests.cs index 7915d224a9..8b5381c7ea 100644 --- a/tests/ImageSharp.Tests/Formats/Pbm/PbmMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Pbm/PbmMetadataTests.cs @@ -1,7 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System; using System.IO; +using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Pbm; using Xunit; @@ -82,5 +84,12 @@ public void Identify_DetectsCorrectComponentType(string imagePath, PbmComponentT Assert.NotNull(bitmapMetadata); Assert.Equal(expectedComponentType, bitmapMetadata.ComponentType); } + + [Fact] + public void Identify_EofInHeader_ThrowsInvalidImageContentException() + { + byte[] bytes = Convert.FromBase64String("UDEjWAAACQAAAAA="); + Assert.Throws(() => Image.Identify(bytes)); + } } } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs index a4fcf63baf..c361b1deb4 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs @@ -526,5 +526,13 @@ static void RunTest(string providerDump, string nonContiguousBuffersStr) "Disco") .Dispose(); } + + [Theory] + [InlineData(TestImages.Png.Bad.Issue2714BadPalette)] + public void Decode_BadPalette(string file) + { + string path = Path.GetFullPath(Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, file)); + using Image image = Image.Load(path); + } } } diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index 2a8cbcbf78..a4243c94b6 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -668,6 +668,18 @@ public void TiffDecoder_ThrowsException_WithTooManyDirectories(TestImage } }); + [Theory] + [WithFile(JpegCompressedGray0000539558, PixelTypes.Rgba32)] + public void TiffDecoder_ThrowsException_WithCircular_IFD_Offsets(TestImageProvider provider) + where TPixel : unmanaged, IPixel + => Assert.Throws( + () => + { + using (provider.GetImage(TiffDecoder)) + { + } + }); + [Theory] [WithFileCollection(nameof(MultiframeTestImages), PixelTypes.Rgba32)] public void DecodeMultiframe(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs b/tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs index f46c9519ca..7585998a64 100644 --- a/tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs +++ b/tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using System.Numerics; +using System.Runtime.CompilerServices; using System.Threading; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; @@ -410,6 +411,41 @@ void RowAction(RowInterval rows, Span memory) Assert.Contains(width <= 0 ? "Width" : "Height", ex.Message); } + [Fact] + public void CanIterateWithoutIntOverflow() + { + ParallelExecutionSettings parallelSettings = ParallelExecutionSettings.FromConfiguration(Configuration.Default); + const int max = 100_000; + + Rectangle rect = new(0, 0, max, max); + int intervalMaxY = 0; + void RowAction(RowInterval rows, Span memory) => intervalMaxY = Math.Max(rows.Max, intervalMaxY); + + TestRowOperation operation = new(0); + TestRowIntervalOperation intervalOperation = new(RowAction); + + ParallelRowIterator.IterateRows(Configuration.Default, rect, in operation); + Assert.Equal(max - 1, operation.MaxY.Value); + + ParallelRowIterator.IterateRowIntervals, Rgba32>(rect, in parallelSettings, in intervalOperation); + Assert.Equal(max, intervalMaxY); + } + + private readonly struct TestRowOperation : IRowOperation + { + public TestRowOperation(int _) => this.MaxY = new StrongBox(); + + public StrongBox MaxY { get; } + + public void Invoke(int y) + { + lock (this.MaxY) + { + this.MaxY.Value = Math.Max(y, this.MaxY.Value); + } + } + } + private readonly struct TestRowIntervalOperation : IRowIntervalOperation { private readonly Action action; diff --git a/tests/ImageSharp.Tests/Image/ImageTests.cs b/tests/ImageSharp.Tests/Image/ImageTests.cs index 0a9e2817a5..1bfd307cbd 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.cs @@ -40,6 +40,10 @@ public void Width_Height() } } + [Fact] + public void Width_Height_SizeNotRepresentable_ThrowsInvalidImageOperationException() + => Assert.Throws(() => new Image(int.MaxValue, int.MaxValue)); + [Fact] public void Configuration_Width_Height() { diff --git a/tests/ImageSharp.Tests/Memory/Allocators/SimpleGcMemoryAllocatorTests.cs b/tests/ImageSharp.Tests/Memory/Allocators/SimpleGcMemoryAllocatorTests.cs index 3ab45be82d..c4ff74cdd2 100644 --- a/tests/ImageSharp.Tests/Memory/Allocators/SimpleGcMemoryAllocatorTests.cs +++ b/tests/ImageSharp.Tests/Memory/Allocators/SimpleGcMemoryAllocatorTests.cs @@ -21,13 +21,17 @@ public BufferTests() protected SimpleGcMemoryAllocator MemoryAllocator { get; } = new SimpleGcMemoryAllocator(); - [Theory] - [InlineData(-1)] - public void Allocate_IncorrectAmount_ThrowsCorrect_ArgumentOutOfRangeException(int length) + public static TheoryData InvalidLengths { get; set; } = new() { - ArgumentOutOfRangeException ex = Assert.Throws(() => this.MemoryAllocator.Allocate(length)); - Assert.Equal("length", ex.ParamName); - } + { -1 }, + { (1 << 30) + 1 } + }; + + [Theory] + [MemberData(nameof(InvalidLengths))] + public void Allocate_IncorrectAmount_ThrowsCorrect_InvalidMemoryOperationException(int length) + => Assert.Throws( + () => this.MemoryAllocator.Allocate(length)); [Fact] public unsafe void Allocate_MemoryIsPinnableMultipleTimes() diff --git a/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs b/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs index 0520c3c1fe..ff2d13b9de 100644 --- a/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs +++ b/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs @@ -111,6 +111,24 @@ public void AllocateGroup_MultipleTimes_ExceedPoolLimit() } } + [Fact] + public void AllocateGroup_SizeInBytesOverLongMaxValue_ThrowsInvalidMemoryOperationException() + { + var allocator = new UniformUnmanagedMemoryPoolMemoryAllocator(null); + Assert.Throws(() => allocator.AllocateGroup(int.MaxValue * (long)int.MaxValue, int.MaxValue)); + } + + public static TheoryData InvalidLengths { get; set; } = new() + { + { -1 }, + { (1 << 30) + 1 } + }; + + [Theory] + [MemberData(nameof(InvalidLengths))] + public void Allocate_IncorrectAmount_ThrowsCorrect_InvalidMemoryOperationException(int length) + => Assert.Throws(() => new UniformUnmanagedMemoryPoolMemoryAllocator(null).Allocate(length)); + [Fact] public unsafe void Allocate_MemoryIsPinnableMultipleTimes() { @@ -380,6 +398,30 @@ private static void AllocateSingleAndForget(UniformUnmanagedMemoryPoolMemoryAllo } } + [Fact] + public void Allocate_OverLimit_ThrowsInvalidMemoryOperationException() + { + MemoryAllocator allocator = MemoryAllocator.Create(new MemoryAllocatorOptions() + { + AllocationLimitMegabytes = 4 + }); + const int oneMb = 1 << 20; + allocator.Allocate(4 * oneMb).Dispose(); // Should work + Assert.Throws(() => allocator.Allocate(5 * oneMb)); + } + + [Fact] + public void AllocateGroup_OverLimit_ThrowsInvalidMemoryOperationException() + { + MemoryAllocator allocator = MemoryAllocator.Create(new MemoryAllocatorOptions() + { + AllocationLimitMegabytes = 4 + }); + const int oneMb = 1 << 20; + allocator.AllocateGroup(4 * oneMb, 1024).Dispose(); // Should work + Assert.Throws(() => allocator.AllocateGroup(5 * oneMb, 1024)); + } + #if NETCOREAPP3_1_OR_GREATER [Fact] public void Issue2001_NegativeMemoryReportedByGc() @@ -394,5 +436,21 @@ static void RunTest() } } #endif + + [ConditionalFact(typeof(Environment), nameof(Environment.Is64BitProcess))] + public void MemoryAllocator_Create_SetHighLimit() + { + RemoteExecutor.Invoke(RunTest).Dispose(); + static void RunTest() + { + const long threeGB = 3L * (1 << 30); + MemoryAllocator allocator = MemoryAllocator.Create(new MemoryAllocatorOptions() + { + AllocationLimitMegabytes = (int)(threeGB / 1024) + }); + using MemoryGroup memoryGroup = allocator.AllocateGroup(threeGB, 1024); + Assert.Equal(threeGB, memoryGroup.TotalLength); + } + } } } diff --git a/tests/ImageSharp.Tests/Memory/Buffer2DTests.cs b/tests/ImageSharp.Tests/Memory/Buffer2DTests.cs index 486fbe4640..751f35890b 100644 --- a/tests/ImageSharp.Tests/Memory/Buffer2DTests.cs +++ b/tests/ImageSharp.Tests/Memory/Buffer2DTests.cs @@ -7,6 +7,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; using Xunit; @@ -342,5 +343,27 @@ public void PublicMemoryGroup_IsMemoryGroupView() Assert.False(mgBefore.IsValid); Assert.NotSame(mgBefore, buffer1.MemoryGroup); } + + public static TheoryData InvalidLengths { get; set; } = new() + { + { new(-1, -1) }, + { new(32768, 32769) }, + { new(32769, 32768) } + }; + + [Theory] + [MemberData(nameof(InvalidLengths))] + public void Allocate_IncorrectAmount_ThrowsCorrect_InvalidMemoryOperationException(Size size) + => Assert.Throws(() => this.MemoryAllocator.Allocate2D(size.Width, size.Height)); + + [Theory] + [MemberData(nameof(InvalidLengths))] + public void Allocate_IncorrectAmount_ThrowsCorrect_InvalidMemoryOperationException_Size(Size size) + => Assert.Throws(() => this.MemoryAllocator.Allocate2D(new Size(size))); + + [Theory] + [MemberData(nameof(InvalidLengths))] + public void Allocate_IncorrectAmount_ThrowsCorrect_InvalidMemoryOperationException_OverAligned(Size size) + => Assert.Throws(() => this.MemoryAllocator.Allocate2DOveraligned(size.Width, size.Height, 1)); } } diff --git a/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.Allocate.cs b/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.Allocate.cs index adfafcb890..299a6c5134 100644 --- a/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.Allocate.cs +++ b/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.Allocate.cs @@ -1,8 +1,7 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. using System; -using System.Buffers; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; @@ -10,7 +9,6 @@ using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory.Internals; using Xunit; -using Xunit.Abstractions; namespace SixLabors.ImageSharp.Tests.Memory.DiscontiguousBuffers { @@ -225,12 +223,18 @@ public void MemoryAllocatorIsUtilizedCorrectly(AllocationOptions allocationOptio [StructLayout(LayoutKind.Sequential, Size = 5)] internal struct S5 { - public override string ToString() => "S5"; + public override readonly string ToString() => nameof(S5); } [StructLayout(LayoutKind.Sequential, Size = 4)] internal struct S4 { - public override string ToString() => "S4"; + public override readonly string ToString() => nameof(S4); + } + + [StructLayout(LayoutKind.Explicit, Size = 512)] + internal struct S512 + { + public override readonly string ToString() => nameof(S512); } } diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 13f965a592..9210be394e 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -156,6 +156,8 @@ public static class Bad // Invalid color type. public const string ColorTypeOne = "Png/xc1n0g08.png"; public const string ColorTypeNine = "Png/xc9n2c08.png"; + + public const string Issue2714BadPalette = "Png/issues/Issue_2714.png"; } } @@ -266,6 +268,9 @@ public static class Issues public const string ExifNullArrayTag = "Jpg/issues/issue-2056-exif-null-array.jpg"; public const string ValidExifArgumentNullExceptionOnEncode = "Jpg/issues/Issue2087-exif-null-reference-on-encode.jpg"; public const string Issue2133DeduceColorSpace = "Jpg/issues/Issue2133.jpg"; + public const string HangBadScan = "Jpg/issues/Hang_C438A851.jpg"; + public const string Issue2758 = "Jpg/issues/issue-2758.jpg"; + public const string Issue2638 = "Jpg/issues/Issue2638.jpg"; public static class Fuzz { @@ -386,6 +391,8 @@ public static class Bmp public const string Rgba321010102 = "Bmp/rgba32-1010102.bmp"; public const string RgbaAlphaBitfields = "Bmp/rgba32abf.bmp"; + public const string Issue2696 = "Bmp/issue-2696.bmp"; + public static readonly string[] BitFields = { Rgb32bfdef, @@ -450,6 +457,7 @@ public static class Issues public const string BadAppExtLength = "Gif/issues/issue405_badappextlength252.gif"; public const string BadAppExtLength_2 = "Gif/issues/issue405_badappextlength252-2.gif"; public const string BadDescriptorWidth = "Gif/issues/issue403_baddescriptorwidth.gif"; + public const string BadMaxLzwBits = "Gif/issues/issue_2743.gif"; public const string DeferredClearCode = "Gif/issues/bugzilla-55918.gif"; public const string Issue1505 = "Gif/issues/issue1505_argumentoutofrange.png"; public const string Issue1530 = "Gif/issues/issue1530.gif"; @@ -457,6 +465,10 @@ public static class Issues public const string Issue1962NoColorTable = "Gif/issues/issue1962_tiniest_gif_1st.gif"; public const string Issue2012EmptyXmp = "Gif/issues/issue2012_Stronghold-Crusader-Extreme-Cover.gif"; public const string Issue2012BadMinCode = "Gif/issues/issue2012_drona1.gif"; + public const string Issue2758 = "Gif/issues/issue_2758.gif"; + public const string Issue2859_A = "Gif/issues/issue_2859_A.gif"; + public const string Issue2859_B = "Gif/issues/issue_2859_B.gif"; + public const string Issue2953 = "Gif/issues/issue_2953.gif"; } public static readonly string[] All = { Rings, Giphy, Cheers, Trans, Kumin, Leo, Ratio4x1, Ratio1x4 }; @@ -915,6 +927,7 @@ public static class Tiff public const string Issues1716Rgb161616BitLittleEndian = "Tiff/Issues/Issue1716.tiff"; public const string Issues1891 = "Tiff/Issues/Issue1891.tiff"; public const string Issues2123 = "Tiff/Issues/Issue2123.tiff"; + public const string JpegCompressedGray0000539558 = "Tiff/Issues/JpegCompressedGray-0000539558.tiff"; public const string SmallRgbDeflate = "Tiff/rgb_small_deflate.tiff"; public const string SmallRgbLzw = "Tiff/rgb_small_lzw.tiff"; @@ -976,6 +989,7 @@ public static class Pbm public const string GrayscalePlainNormalized = "Pbm/grayscale_plain_normalized.pgm"; public const string GrayscalePlainMagick = "Pbm/grayscale_plain_magick.pgm"; public const string RgbBinary = "Pbm/00000_00000.ppm"; + public const string RgbBinaryPrematureEof = "Pbm/00000_00000_premature_eof.ppm"; public const string RgbPlain = "Pbm/rgb_plain.ppm"; public const string RgbPlainNormalized = "Pbm/rgb_plain_normalized.ppm"; public const string RgbPlainMagick = "Pbm/rgb_plain_magick.ppm"; diff --git a/tests/ImageSharp.Tests/TestUtilities/EofHitCounter.cs b/tests/ImageSharp.Tests/TestUtilities/EofHitCounter.cs new file mode 100644 index 0000000000..b627221f5b --- /dev/null +++ b/tests/ImageSharp.Tests/TestUtilities/EofHitCounter.cs @@ -0,0 +1,39 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; +using SixLabors.ImageSharp.IO; + +namespace SixLabors.ImageSharp.Tests.TestUtilities +{ + internal class EofHitCounter : IDisposable + { + private readonly BufferedReadStream stream; + + public EofHitCounter(BufferedReadStream stream, Image image) + { + this.stream = stream; + this.Image = image; + } + + public int EofHitCount => this.stream.EofHitCount; + + public Image Image { get; private set; } + + public static EofHitCounter RunDecoder(string testImage) => RunDecoder(TestFile.Create(testImage).Bytes); + + public static EofHitCounter RunDecoder(byte[] imageData) + { + BufferedReadStream stream = new(Configuration.Default, new MemoryStream(imageData)); + Image image = Image.Load(stream); + return new EofHitCounter(stream, image); + } + + public void Dispose() + { + this.stream.Dispose(); + this.Image.Dispose(); + } + } +} diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2012BadMinCode_Rgba32_issue2012_drona1.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2012BadMinCode_Rgba32_issue2012_drona1.png new file mode 100644 index 0000000000..b07e806620 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2012BadMinCode_Rgba32_issue2012_drona1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:588d055a93c7b4fdb62e8b77f3ae08753a9e8990151cb0523f5e761996189b70 +size 142244 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2758_BadDescriptorDimensions_Rgba32_issue_2758.gif/00.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2758_BadDescriptorDimensions_Rgba32_issue_2758.gif/00.png new file mode 100644 index 0000000000..f63cc98ada --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2758_BadDescriptorDimensions_Rgba32_issue_2758.gif/00.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4f39b23217f1d095eeb8eed5ccea36be813c307a60ef4b1942e9f74028451c38 +size 81944 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2758_BadDescriptorDimensions_Rgba32_issue_2758.gif/01.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2758_BadDescriptorDimensions_Rgba32_issue_2758.gif/01.png new file mode 100644 index 0000000000..f63cc98ada --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2758_BadDescriptorDimensions_Rgba32_issue_2758.gif/01.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4f39b23217f1d095eeb8eed5ccea36be813c307a60ef4b1942e9f74028451c38 +size 81944 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2859_LZWPixelStackOverflow_Rgba32_issue_2859_A.gif/00.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2859_LZWPixelStackOverflow_Rgba32_issue_2859_A.gif/00.png new file mode 100644 index 0000000000..d8c8df1263 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2859_LZWPixelStackOverflow_Rgba32_issue_2859_A.gif/00.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:daa78347749c6ff49891e2e379a373599cd35c98b453af9bf8eac52f615f935c +size 12237 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2859_LZWPixelStackOverflow_Rgba32_issue_2859_B.gif/00.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2859_LZWPixelStackOverflow_Rgba32_issue_2859_B.gif/00.png new file mode 100644 index 0000000000..36c3683187 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2859_LZWPixelStackOverflow_Rgba32_issue_2859_B.gif/00.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:731299281f942f277ce6803e0adda3b5dd0395eb79cae26cabc9d56905fae0fd +size 1833 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2859_LZWPixelStackOverflow_Rgba32_issue_2859_B.gif/01.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2859_LZWPixelStackOverflow_Rgba32_issue_2859_B.gif/01.png new file mode 100644 index 0000000000..c03e5817f0 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2859_LZWPixelStackOverflow_Rgba32_issue_2859_B.gif/01.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:50ccac7739142578d99a76b6d39ba377099d4a7ac30cbb0a5aee44ef1e7c9c8c +size 1271 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/IssueTooLargeLzwBits_Rgba32_issue_2743.gif/00.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/IssueTooLargeLzwBits_Rgba32_issue_2743.gif/00.png new file mode 100644 index 0000000000..923fbc1225 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/IssueTooLargeLzwBits_Rgba32_issue_2743.gif/00.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:800d1ec2d7c7c99d449db1f49ef202cf18214016eae65ebc4216d6f4b1f4d328 +size 537 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/IssueTooLargeLzwBits_Rgba32_issue_2743.gif/01.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/IssueTooLargeLzwBits_Rgba32_issue_2743.gif/01.png new file mode 100644 index 0000000000..6c2134d8b8 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/IssueTooLargeLzwBits_Rgba32_issue_2743.gif/01.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:94dcd97831b16165f3331e429d72d7ef546e04038cab754c7918f9cf535ff30a +size 542 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/IssueTooLargeLzwBits_Rgba32_issue_2743.gif/02.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/IssueTooLargeLzwBits_Rgba32_issue_2743.gif/02.png new file mode 100644 index 0000000000..6f50397ea4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/IssueTooLargeLzwBits_Rgba32_issue_2743.gif/02.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec1a589a8fae1b17a82b70a9583ea2ee012a476b1fa8fdba27fee2b7ce0403b2 +size 540 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/IssueTooLargeLzwBits_Rgba32_issue_2743.gif/03.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/IssueTooLargeLzwBits_Rgba32_issue_2743.gif/03.png new file mode 100644 index 0000000000..82061ba0aa --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/IssueTooLargeLzwBits_Rgba32_issue_2743.gif/03.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c8751f4fafd5c56066dbb8d64a3890fc420a3bd66881a55e309ba274b6d14e4 +size 542 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/IssueTooLargeLzwBits_Rgba32_issue_2743.gif/04.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/IssueTooLargeLzwBits_Rgba32_issue_2743.gif/04.png new file mode 100644 index 0000000000..8902eb824a --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/IssueTooLargeLzwBits_Rgba32_issue_2743.gif/04.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b78516c9874cb15de4c4b98ed307e8105d962fc6bfa7aa3490b2c7e13b455a2d +size 544 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/IssueTooLargeLzwBits_Rgba32_issue_2743.gif/05.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/IssueTooLargeLzwBits_Rgba32_issue_2743.gif/05.png new file mode 100644 index 0000000000..82061ba0aa --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/IssueTooLargeLzwBits_Rgba32_issue_2743.gif/05.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c8751f4fafd5c56066dbb8d64a3890fc420a3bd66881a55e309ba274b6d14e4 +size 542 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/IssueTooLargeLzwBits_Rgba32_issue_2743.gif/06.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/IssueTooLargeLzwBits_Rgba32_issue_2743.gif/06.png new file mode 100644 index 0000000000..6f50397ea4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/IssueTooLargeLzwBits_Rgba32_issue_2743.gif/06.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec1a589a8fae1b17a82b70a9583ea2ee012a476b1fa8fdba27fee2b7ce0403b2 +size 540 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/IssueTooLargeLzwBits_Rgba32_issue_2743.gif/07.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/IssueTooLargeLzwBits_Rgba32_issue_2743.gif/07.png new file mode 100644 index 0000000000..efba40c99d --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/IssueTooLargeLzwBits_Rgba32_issue_2743.gif/07.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5016a323018f09e292165ad5392d82dcbad5e79c2b6b93aff3322dffff80b309 +size 126 diff --git a/tests/Images/Input/Bmp/issue-2696.bmp b/tests/Images/Input/Bmp/issue-2696.bmp new file mode 100644 index 0000000000..6770dd9469 --- /dev/null +++ b/tests/Images/Input/Bmp/issue-2696.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bc42cda9bac8fc73351ad03bf55214069bb8d31ea5bdd806321a8cc8b56c282e +size 126 diff --git a/tests/Images/Input/Gif/issues/issue_2743.gif b/tests/Images/Input/Gif/issues/issue_2743.gif new file mode 100644 index 0000000000..4ce61340d9 --- /dev/null +++ b/tests/Images/Input/Gif/issues/issue_2743.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4be51cb9c258a6518d791ad2810fa0d71449805a5d5a8f95dcc7da2dc558ed73 +size 166413 diff --git a/tests/Images/Input/Gif/issues/issue_2758.gif b/tests/Images/Input/Gif/issues/issue_2758.gif new file mode 100644 index 0000000000..17db9fa132 --- /dev/null +++ b/tests/Images/Input/Gif/issues/issue_2758.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13e9374181c7536d1d2ecb514753a5290c0ec06234ca079c6c8c8a832586b668 +size 199 diff --git a/tests/Images/Input/Gif/issues/issue_2859_A.gif b/tests/Images/Input/Gif/issues/issue_2859_A.gif new file mode 100644 index 0000000000..f19a047525 --- /dev/null +++ b/tests/Images/Input/Gif/issues/issue_2859_A.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:50a1a4afc62a3a36ff83596f1eb55d91cdd184c64e0d1339bbea17205c23eee1 +size 3406142 diff --git a/tests/Images/Input/Gif/issues/issue_2859_B.gif b/tests/Images/Input/Gif/issues/issue_2859_B.gif new file mode 100644 index 0000000000..109b0f8797 --- /dev/null +++ b/tests/Images/Input/Gif/issues/issue_2859_B.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:db9b2992be772a4f0ac495e994a17c7c50fb6de9794cfb9afc4a3dc26ffdfae0 +size 4543 diff --git a/tests/Images/Input/Gif/issues/issue_2953.gif b/tests/Images/Input/Gif/issues/issue_2953.gif new file mode 100644 index 0000000000..98c06e5c58 --- /dev/null +++ b/tests/Images/Input/Gif/issues/issue_2953.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4fa4a002b264a41677cc10f115f3572111852993a53ee8cea5f4ec0bf8dec195 +size 40 diff --git a/tests/Images/Input/Jpg/issues/Hang_C438A851.jpg b/tests/Images/Input/Jpg/issues/Hang_C438A851.jpg new file mode 100644 index 0000000000..97ab9ad0fb --- /dev/null +++ b/tests/Images/Input/Jpg/issues/Hang_C438A851.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:580760756f2e7e3ed0752a4ec53d6b6786a4f005606f3a50878f732b3b2a1bcb +size 413 diff --git a/tests/Images/Input/Jpg/issues/Issue2638.jpg b/tests/Images/Input/Jpg/issues/Issue2638.jpg new file mode 100644 index 0000000000..f42d67b0e8 --- /dev/null +++ b/tests/Images/Input/Jpg/issues/Issue2638.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:208d5b0b727bbef120a7e090e020a48f99c9e264c2d3939ba749f8620853c1fe +size 70876 diff --git a/tests/Images/Input/Jpg/issues/issue-2758.jpg b/tests/Images/Input/Jpg/issues/issue-2758.jpg new file mode 100644 index 0000000000..48ee1159ec --- /dev/null +++ b/tests/Images/Input/Jpg/issues/issue-2758.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f32a238b57b7073f7442f8ae7efd6ba3ae4cda30d57e6666fb8a1eaa27108558 +size 1412 diff --git a/tests/Images/Input/Pbm/00000_00000_premature_eof.ppm b/tests/Images/Input/Pbm/00000_00000_premature_eof.ppm new file mode 100644 index 0000000000..445cd0059a --- /dev/null +++ b/tests/Images/Input/Pbm/00000_00000_premature_eof.ppm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39cf6ca5b2f9d428c0c33e0fc7ab5e92c31e0c8a7d9e0276b9285f51a8ff547c +size 69 diff --git a/tests/Images/Input/Png/issues/Issue_2714.png b/tests/Images/Input/Png/issues/Issue_2714.png new file mode 100644 index 0000000000..9bb231dd9f --- /dev/null +++ b/tests/Images/Input/Png/issues/Issue_2714.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9a4b6efc3090dbd70ae9efe97ea817464845263536beea4e80fd7c884dee6c5a +size 128 diff --git a/tests/Images/Input/Tiff/Issues/JpegCompressedGray-0000539558.tiff b/tests/Images/Input/Tiff/Issues/JpegCompressedGray-0000539558.tiff new file mode 100644 index 0000000000..934bf3c9a3 --- /dev/null +++ b/tests/Images/Input/Tiff/Issues/JpegCompressedGray-0000539558.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f1ca630b5e46c7b5f21100fa8c0fbf27b79ca9da8cd95897667b64aedccf6e5 +size 539558