diff --git a/src/SharpEmu.Libs/VideoOut/PngSplashLoader.cs b/src/SharpEmu.Libs/VideoOut/PngSplashLoader.cs index 13fc7c5..0a893e3 100644 --- a/src/SharpEmu.Libs/VideoOut/PngSplashLoader.cs +++ b/src/SharpEmu.Libs/VideoOut/PngSplashLoader.cs @@ -8,6 +8,9 @@ namespace SharpEmu.Libs.VideoOut; internal static class PngSplashLoader { + private const uint CrcPolynomial = 0xEDB88320; + private static readonly uint[] CrcTable = BuildCrcTable(); + private static ReadOnlySpan PngSignature => [ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, @@ -90,6 +93,13 @@ internal static class PngSplashLoader var chunkType = png.Slice(offset + 4, 4); var chunkData = png.Slice(offset + 8, (int)chunkLength); + var expectedCrc = BinaryPrimitives.ReadUInt32BigEndian( + png.Slice(offset + 8 + (int)chunkLength, 4)); + if (CalculateCrc(chunkType, chunkData) != expectedCrc) + { + return false; + } + if (chunkType.SequenceEqual("IHDR"u8)) { if (chunkData.Length != 13) @@ -186,6 +196,41 @@ internal static class PngSplashLoader return true; } + private static uint CalculateCrc(ReadOnlySpan chunkType, ReadOnlySpan chunkData) + { + var crc = UpdateCrc(uint.MaxValue, chunkType); + return ~UpdateCrc(crc, chunkData); + } + + private static uint UpdateCrc(uint crc, ReadOnlySpan bytes) + { + foreach (var value in bytes) + { + crc = CrcTable[(byte)(crc ^ value)] ^ (crc >> 8); + } + + return crc; + } + + private static uint[] BuildCrcTable() + { + var table = new uint[256]; + for (var index = 0; index < table.Length; index++) + { + var value = (uint)index; + for (var bit = 0; bit < 8; bit++) + { + value = (value & 1) != 0 + ? CrcPolynomial ^ (value >> 1) + : value >> 1; + } + + table[index] = value; + } + + return table; + } + private static bool TryUnfilter( byte filter, ReadOnlySpan source, diff --git a/tests/SharpEmu.Libs.Tests/VideoOut/PngSplashLoaderTests.cs b/tests/SharpEmu.Libs.Tests/VideoOut/PngSplashLoaderTests.cs new file mode 100644 index 0000000..7793dfd --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/VideoOut/PngSplashLoaderTests.cs @@ -0,0 +1,52 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.Libs.VideoOut; +using Xunit; + +namespace SharpEmu.Libs.Tests.VideoOut; + +public sealed class PngSplashLoaderTests +{ + private const int IhdrCrcOffset = 29; + private const int IdatCrcOffset = 53; + private const int IendCrcOffset = 65; + private const string ValidRgbPng = + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGMQMgkDAAD4AJ3MaiF4AAAAAElFTkSuQmCC"; + + [Theory] + [InlineData(IhdrCrcOffset)] + [InlineData(IdatCrcOffset)] + [InlineData(IendCrcOffset)] + public void TryLoadRejectsInvalidChunkCrc(int crcOffset) + { + var app0Root = Path.Combine(Path.GetTempPath(), $"sharpemu-png-{Guid.NewGuid():N}"); + var sceSysPath = Path.Combine(app0Root, "sce_sys"); + var pngPath = Path.Combine(sceSysPath, "pic0.png"); + var previousApp0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR"); + + Directory.CreateDirectory(sceSysPath); + try + { + Environment.SetEnvironmentVariable("SHARPEMU_APP0_DIR", app0Root); + + var png = Convert.FromBase64String(ValidRgbPng); + File.WriteAllBytes(pngPath, png); + + Assert.True(PngSplashLoader.TryLoad(out var pixels, out var width, out var height)); + Assert.Equal(1U, width); + Assert.Equal(1U, height); + Assert.Equal([0x56, 0x34, 0x12, 0xFF], pixels); + + png[crcOffset] ^= 0x01; + File.WriteAllBytes(pngPath, png); + + Assert.False(PngSplashLoader.TryLoad(out _, out _, out _)); + } + finally + { + Environment.SetEnvironmentVariable("SHARPEMU_APP0_DIR", previousApp0Root); + Directory.Delete(app0Root, recursive: true); + } + } +}