From 79062fc80928a5e01d0425145243ac01c6fcdeae Mon Sep 17 00:00:00 2001 From: Adamstuhltrager <156963919+Adamstuhltrager@users.noreply.github.com> Date: Fri, 11 Sep 2026 08:13:39 -0400 Subject: [PATCH] Throw InvalidDataException when Skia cannot decode an image SKCodec.Create returns null for data Skia does not recognise, and SKBitmap.Decode returns null when the codec fails to produce pixels, for example on a truncated progressive JPEG. Both were dereferenced without a check, so callers saw a NullReferenceException with no indication that the input was at fault. Route codec creation and bitmap decoding through helpers that throw an InvalidDataException with a descriptive message instead, and throw FileNotFoundException up front for a missing file. Add tests covering unrecognised data, a truncated progressive JPEG fixture and a missing file for every public overload. Co-Authored-By: Claude Fable 5.1 --- .../BlurHashEncoder.cs | 37 ++++++-- .../BlurHashEncoderInvalidInputTests.cs | 89 ++++++++++++++++++ .../BlurHashSharp.SkiaSharp.Tests.csproj | 4 + .../TestData/truncated-progressive.jpg | Bin 0 -> 459 bytes 4 files changed, 124 insertions(+), 6 deletions(-) create mode 100644 tests/BlurHashSharp.SkiaSharp.Tests/BlurHashEncoderInvalidInputTests.cs create mode 100644 tests/BlurHashSharp.SkiaSharp.Tests/TestData/truncated-progressive.jpg diff --git a/src/BlurHashSharp.SkiaSharp/BlurHashEncoder.cs b/src/BlurHashSharp.SkiaSharp/BlurHashEncoder.cs index e762fb6..acd79de 100644 --- a/src/BlurHashSharp.SkiaSharp/BlurHashEncoder.cs +++ b/src/BlurHashSharp.SkiaSharp/BlurHashEncoder.cs @@ -15,9 +15,10 @@ public static class BlurHashEncoder /// The number y components. /// The IO stream of an encoded image. /// BlurHash representation of the image. + /// The image is in an unsupported format or could not be decoded. public static string Encode(int xComponent, int yComponent, Stream stream) { - using (SKCodec codec = SKCodec.Create(stream)) + using (SKCodec codec = CreateCodec(stream)) { return Encode(xComponent, yComponent, codec); } @@ -30,9 +31,11 @@ public static string Encode(int xComponent, int yComponent, Stream stream) /// The number y components. /// The path to an encoded image on the file system. /// BlurHash representation of the image. + /// The file does not exist. + /// The image is in an unsupported format or could not be decoded. public static string Encode(int xComponent, int yComponent, string filename) { - using (SKCodec codec = SKCodec.Create(filename)) + using (SKCodec codec = CreateCodec(filename)) { return Encode(xComponent, yComponent, codec); } @@ -49,7 +52,7 @@ internal static string Encode(int xComponent, int yComponent, SKCodec codec) ColorSpace = SKColorSpace.CreateSrgb() }; - using (SKBitmap bitmap = SKBitmap.Decode(codec, newInfo)) + using (SKBitmap bitmap = DecodeBitmap(codec, newInfo)) { return EncodeInternal(xComponent, yComponent, bitmap); } @@ -64,9 +67,10 @@ internal static string Encode(int xComponent, int yComponent, SKCodec codec) /// The maximum width to resize the image to. /// The maximum height to resize the image to. /// BlurHash representation of the image. + /// The image is in an unsupported format or could not be decoded. public static string Encode(int xComponent, int yComponent, Stream stream, int maxWidth, int maxHeight) { - using (SKCodec codec = SKCodec.Create(stream)) + using (SKCodec codec = CreateCodec(stream)) { return Encode(xComponent, yComponent, codec, maxWidth, maxHeight); } @@ -81,9 +85,11 @@ public static string Encode(int xComponent, int yComponent, Stream stream, int m /// The maximum width to resize the image to. /// The maximum height to resize the image to. /// BlurHash representation of the image. + /// The file does not exist. + /// The image is in an unsupported format or could not be decoded. public static string Encode(int xComponent, int yComponent, string filename, int maxWidth, int maxHeight) { - using (SKCodec codec = SKCodec.Create(filename)) + using (SKCodec codec = CreateCodec(filename)) { return Encode(xComponent, yComponent, codec, maxWidth, maxHeight); } @@ -111,7 +117,7 @@ internal static string Encode(int xComponent, int yComponent, SKCodec codec, int ColorSpace = SKColorSpace.CreateSrgb() }; - using (SKBitmap bitmap = SKBitmap.Decode(codec, newInfo)) + using (SKBitmap bitmap = DecodeBitmap(codec, newInfo)) { if (scaleFactor == 0f) { @@ -129,6 +135,25 @@ internal static string Encode(int xComponent, int yComponent, SKCodec codec, int } } + private static SKCodec CreateCodec(Stream stream) + => SKCodec.Create(stream) + ?? throw new InvalidDataException("The stream does not contain an image in a format supported by SkiaSharp."); + + private static SKCodec CreateCodec(string filename) + { + if (!File.Exists(filename)) + { + throw new FileNotFoundException("The image file could not be found.", filename); + } + + return SKCodec.Create(filename) + ?? throw new InvalidDataException($"The file '{filename}' does not contain an image in a format supported by SkiaSharp."); + } + + private static SKBitmap DecodeBitmap(SKCodec codec, SKImageInfo info) + => SKBitmap.Decode(codec, info) + ?? throw new InvalidDataException("The image could not be decoded; the data is corrupt or truncated."); + internal static string EncodeInternal(int xComponent, int yComponent, SKBitmap bitmap) => CoreBlurHashEncoder.Encode(xComponent, yComponent, bitmap.Width, bitmap.Height, bitmap.GetPixelSpan(), bitmap.RowBytes, PixelFormat.RGB888x); } diff --git a/tests/BlurHashSharp.SkiaSharp.Tests/BlurHashEncoderInvalidInputTests.cs b/tests/BlurHashSharp.SkiaSharp.Tests/BlurHashEncoderInvalidInputTests.cs new file mode 100644 index 0000000..b4ecbaf --- /dev/null +++ b/tests/BlurHashSharp.SkiaSharp.Tests/BlurHashEncoderInvalidInputTests.cs @@ -0,0 +1,89 @@ +using System; +using System.IO; +using Xunit; + +namespace BlurHashSharp.SkiaSharp.Tests +{ + public class BlurHashEncoderInvalidInputTests + { + // A 64x96 progressive JPEG cut off half way through its first scan: Skia reads the header + // and creates a codec, but decoding the pixels fails with InvalidInput. + private static readonly string TruncatedImagePath = Path.Join(AppContext.BaseDirectory, "TestData", "truncated-progressive.jpg"); + + private static byte[] UnrecognizedData() + { + var data = new byte[2048]; + for (int i = 0; i < data.Length; i++) + { + data[i] = (byte)i; + } + + return data; + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Encode_UnrecognizedStream_ThrowsInvalidDataException(bool resize) + { + using var stream = new MemoryStream(UnrecognizedData()); + + Assert.Throws(() => resize + ? BlurHashEncoder.Encode(4, 3, stream, 128, 128) + : BlurHashEncoder.Encode(4, 3, stream)); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Encode_UnrecognizedFile_ThrowsInvalidDataException(bool resize) + { + var path = Path.Join(Path.GetTempPath(), Path.GetRandomFileName() + ".jpg"); + File.WriteAllBytes(path, UnrecognizedData()); + try + { + Assert.Throws(() => resize + ? BlurHashEncoder.Encode(4, 3, path, 128, 128) + : BlurHashEncoder.Encode(4, 3, path)); + } + finally + { + File.Delete(path); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Encode_TruncatedStream_ThrowsInvalidDataException(bool resize) + { + using var stream = File.OpenRead(TruncatedImagePath); + + Assert.Throws(() => resize + ? BlurHashEncoder.Encode(4, 3, stream, 32, 32) + : BlurHashEncoder.Encode(4, 3, stream)); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Encode_TruncatedFile_ThrowsInvalidDataException(bool resize) + { + Assert.Throws(() => resize + ? BlurHashEncoder.Encode(4, 3, TruncatedImagePath, 32, 32) + : BlurHashEncoder.Encode(4, 3, TruncatedImagePath)); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Encode_MissingFile_ThrowsFileNotFoundException(bool resize) + { + var path = Path.Join(Path.GetTempPath(), Path.GetRandomFileName() + ".png"); + + Assert.Throws(() => resize + ? BlurHashEncoder.Encode(4, 3, path, 128, 128) + : BlurHashEncoder.Encode(4, 3, path)); + } + } +} diff --git a/tests/BlurHashSharp.SkiaSharp.Tests/BlurHashSharp.SkiaSharp.Tests.csproj b/tests/BlurHashSharp.SkiaSharp.Tests/BlurHashSharp.SkiaSharp.Tests.csproj index 169b878..3d18c02 100644 --- a/tests/BlurHashSharp.SkiaSharp.Tests/BlurHashSharp.SkiaSharp.Tests.csproj +++ b/tests/BlurHashSharp.SkiaSharp.Tests/BlurHashSharp.SkiaSharp.Tests.csproj @@ -18,6 +18,10 @@ + + + + diff --git a/tests/BlurHashSharp.SkiaSharp.Tests/TestData/truncated-progressive.jpg b/tests/BlurHashSharp.SkiaSharp.Tests/TestData/truncated-progressive.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0402dd315752c5b70722d5808900e3350c67928e GIT binary patch literal 459 zcmex=r~OVP|6de~W>K znUR4>fLQ?O$DeVpY~!_T7Nt!wpS!+1(AcK$mEb9nN|!e)@{Qx(Eh?*&y`nrtBj4Cd z#P4*eu<40+3uWdySKbO(ls0?C+