From 7b64d631be51478c3e5fc73bf2d99d7f7ed6386e Mon Sep 17 00:00:00 2001 From: Prakash Dalai <86671543+prakashUXtech@users.noreply.github.com> Date: Mon, 19 Oct 2026 00:36:22 +0000 Subject: [PATCH] test(codecs): inflate and PNG write acceptance suites, disabled until IC-3 and IC-5 --- Tests/CoreRasterTests/InflateTests.swift | 114 ++++++++++++++++++++++ Tests/CoreRasterTests/PNGWriteTests.swift | 114 ++++++++++++++++++++++ 2 files changed, 228 insertions(+) create mode 100644 Tests/CoreRasterTests/InflateTests.swift create mode 100644 Tests/CoreRasterTests/PNGWriteTests.swift diff --git a/Tests/CoreRasterTests/InflateTests.swift b/Tests/CoreRasterTests/InflateTests.swift new file mode 100644 index 00000000..5e46ccaf --- /dev/null +++ b/Tests/CoreRasterTests/InflateTests.swift @@ -0,0 +1,114 @@ +// InflateTests.swift: Inflate against DEFLATE streams derived by hand from +// RFC 1951 (stored, fixed and dynamic Huffman blocks; the bit layouts are in +// the comments) and zlib wrappers from RFC 1950; damaged streams and the +// output limit; Deflate then Inflate on seeded buffers. Disabled until IC-3. +@testable import CoreRaster +import Testing +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +@Suite struct InflateTests { + typealias F = PNGFixtures + /// Fixed block (3.2.6): BFINAL 1, BTYPE 01, literal 'a' (0x91, 8 bits), end of block (7 zero bits). + static let fixedA: [UInt8] = [0x4B, 0x04, 0x00] + /// Fixed block: 'a' 'b' 'c', length 6 (code 260) distance 3 (code 2), 'x', length 9 (code 263) distance 1 + /// (code 0, a copy that overlaps its own output), end of block. + static let fixedRepeats: [UInt8] = [0x4B, 0x4C, 0x4A, 0x86, 0xA0, 0x0A, 0x38, 0x00, 0x00] + static let repeatsOut = Array("abcabcabc".utf8) + [UInt8](repeating: 0x78, count: 10) + /// Dynamic block (3.2.7): HLIT 28, HDIST 1, HCLEN 14; code-length code 18 = 0, 1 = 10, 2 = 11; literal/length + /// lengths 'x' 2, end 2, 284 1; two 1-bit distance codes. Data: 'x', 284 + extra 23 (length 250) distance 1, + /// 284 + extra 0 (length 227) distance 1, end: 478 bytes of 'x'. + static let dynamic: [UInt8] = [0xE5, 0xC1, 0x81, 0x00, 0x00, 0x00, 0x00, 0x80, 0x20, 0xED, 0xF1, 0x07, 0xA9, 0x72, 0x01, 0x06] + static let dynamicOut = [UInt8](repeating: 0x78, count: 478) + + func raw(_ bytes: [UInt8], limit: Int = 1 << 20) throws -> [UInt8] { [UInt8](try Inflate.raw(Data(bytes), limit: limit)) } + func zlib(_ bytes: [UInt8], limit: Int = 1 << 20) throws -> [UInt8] { [UInt8](try Inflate.zlib(Data(bytes), limit: limit)) } + + @Test(.disabled("waits on IC-3")) func storedBlocks() throws { + #expect(try raw([0x00, 0x02, 0x00, 0xFD, 0xFF, 0x61, 0x62, 0x01, 0x01, 0x00, 0xFE, 0xFF, 0x63]) == Array("abc".utf8)) + #expect(try zlib([0x78, 0x01, 0x01, 0x03, 0x00, 0xFC, 0xFF, 0x61, 0x62, 0x63, 0x02, 0x4D, 0x01, 0x27]) == Array("abc".utf8)) + #expect(try zlib(F.stored([])).isEmpty) + let long = (0..<70_000).map { UInt8(truncatingIfNeeded: $0 * 7) } + #expect(try zlib(F.stored(long)) == long, "two stored blocks, the first 65,535 bytes") + } + + @Test(.disabled("waits on IC-3")) func fixedBlocks() throws { + #expect(try raw(Self.fixedA) == [0x61]) + #expect(try raw(Self.fixedRepeats) == Self.repeatsOut) + #expect(try zlib(F.zlib(Self.fixedRepeats, of: Self.repeatsOut)) == Self.repeatsOut) + } + + @Test(.disabled("waits on IC-3")) func dynamicBlocks() throws { + #expect(try raw(Self.dynamic) == Self.dynamicOut) + #expect(try zlib(F.zlib(Self.dynamic, of: Self.dynamicOut)) == Self.dynamicOut) + } + + @Test(.disabled("waits on IC-3")) func adlerChecked() throws { + var bytes = F.zlib(Self.dynamic, of: Self.dynamicOut) + bytes[bytes.count - 1] ^= 0x01 + #expect(F.thrown { try zlib(bytes) } == "damaged") + #expect(F.thrown { try zlib(Array(bytes.dropLast(4))) } == "damaged", "the checksum missing") + } + + @Test(.disabled("waits on IC-3")) func damagedStreams() throws { + let a = F.zlib(Self.fixedA, of: [0x61]) + let zlibCases: [(String, [UInt8])] = [ + ("method 9", [0x79, 0x18] + a.dropFirst(2)), ("window over 32 KiB", [0x88, 0x1C] + a.dropFirst(2)), + ("check bits", [0x78, 0x02] + a.dropFirst(2)), ("preset dictionary", [0x78, 0x20] + a.dropFirst(2)), + ("empty", []), + ] + for (name, bytes) in zlibCases { #expect(F.thrown { try zlib(bytes) } == "damaged", "\(name)") } + let rawCases: [(String, [UInt8])] = [ + ("distance 2 after one byte", [0x4B, 0x04, 0x42, 0x00]), + ("over-subscribed code-length code", Array(Self.dynamic.prefix(8)) + [0x10] + Self.dynamic.dropFirst(9)), + ("reserved block type", [0x07]), ("LEN and NLEN disagree", [0x01, 0x03, 0x00, 0xFC, 0xFE, 0x61, 0x62, 0x63]), + ("no final block", [0x00, 0x03, 0x00, 0xFC, 0xFF, 0x61, 0x62, 0x63]), ("cut mid-block", Array(Self.dynamic.prefix(12))), + ] + for (name, bytes) in rawCases { #expect(F.thrown { try raw(bytes) } == "damaged", "\(name)") } + } + + /// The limit is checked before the output grows: a 6-byte stored block then a reserved block type + /// throws `tooLarge` at limit 5 (not `damaged` after the bytes were kept), `damaged` at limit 6. + @Test(.disabled("waits on IC-3")) func limitBeforeGrowth() throws { + let six: [UInt8] = [0x00, 0x06, 0x00, 0xF9, 0xFF] + Array("abcdef".utf8) + [0x07] + #expect(F.thrown { try raw(six, limit: 5) } == "tooLarge") + #expect(F.thrown { try raw(six, limit: 6) } == "damaged") + #expect(F.thrown { try raw(Self.dynamic, limit: 477) } == "tooLarge") + #expect(F.thrown { try raw(Self.dynamic, limit: 0) } == "tooLarge") + #expect(try raw(Self.dynamic, limit: 478) == Self.dynamicOut) + #expect(F.thrown { try zlib(F.stored([UInt8](repeating: 1, count: 1_000)), limit: 999) } == "tooLarge") + } + + @Test(.disabled("waits on IC-3")) func deflateRoundTrip() throws { + var rng = SplitMix64(seed: 0x1C3) + for i in 0..<1_000 { + var bytes: [UInt8] = [] + for _ in 0.. 2 && fixed[2] & 0x06 == 0x02, "levels 1...9 write fixed-Huffman blocks (BTYPE 01)") + #expect(fixed.count < Deflate.zlib(text, level: 0).count) + } + + @Test(.disabled("waits on IC-3")) func fuzzed() { + let seeds = [F.zlib(Self.dynamic, of: Self.dynamicOut), F.zlib(Self.fixedRepeats, of: Self.repeatsOut), F.stored(Array("abcdef".utf8), block: 2)] + let cases = mutations(of: seeds.map { Data($0) }, count: 1_000, seed: 0x1C4) + fuzz(cases) { _ = try Inflate.zlib($0, limit: 1 << 16) } + fuzz(cases.map { $0.dropFirst(2) }) { _ = try Inflate.raw($0, limit: 1 << 16) } + } +} diff --git a/Tests/CoreRasterTests/PNGWriteTests.swift b/Tests/CoreRasterTests/PNGWriteTests.swift new file mode 100644 index 00000000..68110061 --- /dev/null +++ b/Tests/CoreRasterTests/PNGWriteTests.swift @@ -0,0 +1,114 @@ +// PNGWriteTests.swift: PNG.write and PNGCodec.encodePNG checked against the +// W3C PNG specification: rereads equal at 8 bits and within one step at 16, +// IHDR depth, big-endian samples, chunk order and CRCs, IDATs of at most +// 64 KiB, the least-sum filter choice (12.8), and an ORA round trip through +// PNGCodec. Written files are parsed with PNGFixtures. Disabled until IC-5. +@testable import CoreRaster +import CoreColor +import Testing +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +@Suite struct PNGWriteTests { + typealias F = PNGFixtures + + /// Seeded pixels an 8-bit file holds exactly: sRGB bytes with alpha 0 (and colour 0), 128 or 255. + static func bytePixels(_ seed: UInt64, _ width: Int, _ height: Int) throws -> PixelBuffer { + var rng = SplitMix64(seed: seed) + let bytes = (0..<(width * height)).flatMap { _ -> [UInt8] in + let alpha = [UInt8(0), 128, 255][Int.random(in: 0...2, using: &rng)] + return alpha == 0 ? [0, 0, 0, 0] : (0..<3).map { _ in UInt8.random(in: 0...255, using: &rng) } + [alpha] + } + return try PixelBuffer(rgba8: bytes, width: width, height: height) + } + + /// The inflated IDAT stream of a written PNG. + static func scanlines(_ data: Data) throws -> [UInt8] { + let idat = F.chunks(data).filter { $0.type == "IDAT" }.flatMap(\.body) + return [UInt8](try Inflate.zlib(Data(idat), limit: 1 << 24)) + } + + @Test(.disabled("waits on IC-5")) func eightBitRoundTrip() throws { + let source = try Self.bytePixels(0x5E1, 13, 7) + for level in [0, 6, 9] { #expect(try PNG.read(PNG.write(source, level: level)) == source, "level \(level)") } + #expect(try PNGCodec().decodePNG(PNGCodec().encodePNG(source, bitsPerChannel: 8)) == source) + } + + @Test(.disabled("waits on IC-5")) func sixteenBitWithinOneStep() throws { + var rng = SplitMix64(seed: 0x5E2), source = try PixelBuffer(width: 11, height: 6) + for y in 0..<6 { + for x in 0..<11 { + let c = (0..<3).map { _ in Float.random(in: 0...1, using: &rng) } + try source.setPixel(x: x, y: y, RGBA(r: c[0], g: c[1], b: c[2], a: .random(in: 0.25...1, using: &rng)).premultiplied()) + } + } + let back = try PNG.read(PNG.write(source, bitsPerChannel: 16)) + for y in 0..<6 { + for x in 0..<11 { + let p = try source.pixel(x: x, y: y).unpremultiplied(), q = try back.pixel(x: x, y: y).unpremultiplied() + let encoded = { (v: Float) in TransferCurve.encode(Double(v)) } + for (u, v) in [(p.r, q.r), (p.g, q.g), (p.b, q.b)] { #expect(abs(encoded(u) - encoded(v)) <= 1.01 / 65_535) } + #expect(abs(p.a - q.a) <= 0.51 / 65_535) + } + } + } + + /// 7.1: 16-bit samples are written high byte first; encoded 0x1234 is the bytes 0x12 0x34. + @Test(.disabled("waits on IC-5")) func bigEndian16() throws { + let v = Float(TransferCurve.decode(Double(0x1234) / 65_535)) + let lines = try Self.scanlines(PNG.write(PixelBuffer(width: 1, height: 1, fill: RGBA(r: v, g: v, b: v, a: 1)), bitsPerChannel: 16)) + #expect(lines.count == 9 && lines[0] <= 4) + #expect(Array(lines.dropFirst()) == [0x12, 0x34, 0x12, 0x34, 0x12, 0x34, 0xFF, 0xFF]) + } + + @Test(.disabled("waits on IC-5")) func ihdrDepthRecorded() throws { + let source = try Self.bytePixels(0x5E3, 3, 2) + for depth in [8, 16] { + let data = try PNG.write(source, bitsPerChannel: depth), bytes = [UInt8](data) + #expect(bytes.count > 33 && bytes[24] == depth && bytes[25] == 6 && bytes[28] == 0, "IHDR depth, RGBA, not interlaced") + #expect(try PNG.info(data) == PNGInfo(width: 3, height: 2, bitDepth: depth, colorType: 6, interlaced: false)) + } + } + + @Test(.disabled("waits on IC-5")) func chunkCRCsValid() throws { + let data = try PNG.write(Self.bytePixels(0x5E4, 200, 200), level: 0) + let chunks = F.chunks(data) + #expect(Array(data.prefix(8)) == F.signature && chunks.first?.type == "IHDR") + #expect(chunks.last.map { $0.type == "IEND" && $0.body.isEmpty } == true) + #expect(chunks.allSatisfy(\.crcOK) && chunks.map { 12 + $0.body.count }.reduce(8, +) == data.count) + let idats = chunks.filter { $0.type == "IDAT" } + #expect(idats.count >= 3 && idats.allSatisfy { $0.body.count <= 65_536 }, "160,200 stored bytes split by 64 KiB") + } + + /// 12.8: each row takes the filter with the least sum of its bytes read as signed. A 16-pixel horizontal + /// ramp (sRGB 4x + 4, opaque) with equal rows makes None the worst choice on every row. + @Test(.disabled("waits on IC-5")) func adaptiveFilterSmaller() throws { + let row: [UInt8] = (0..<16).flatMap { (x: Int) -> [UInt8] in let v = UInt8(4 * x + 4); return [v, v, v, 255] } + let image = try PixelBuffer(rgba8: row + row + row + row, width: 16, height: 4) + let lines = try Self.scanlines(PNG.write(image)) + #expect(lines.count == 4 * 65) + func cost(_ filtered: [UInt8]) -> Int { filtered.dropFirst().reduce(0) { $0 + abs(Int(Int8(bitPattern: $1))) } } + for y in 0..<4 { + let written = Array(lines[(y * 65)..<(y * 65 + 65)]), prior = y == 0 ? nil : row + let costs = (0...4).map { cost(F.filtered(row, prior: prior, filter: $0, bpp: 4)) } + #expect(written[0] != 0 && written[0] <= 4, "row \(y)") + #expect(written == F.filtered(row, prior: prior, filter: Int(min(written[0], 4)), bpp: 4) && cost(written) == costs.min()) + } + } + + @Test(.disabled("waits on IC-5")) func oraRoundTripThroughPNGCodec() throws { + let layers = try (0..<3).map { PixelLayer(name: "layer \($0)", pixels: try Self.bytePixels(0x5E5 + UInt64($0), 5 + $0, 4), x: $0, y: -$0) } + let image = LayeredImage(width: 9, height: 6, nodes: layers.map { .pixels($0) }, composite: try Self.bytePixels(0x5E9, 9, 6)) + #expect(try OpenRaster.read(OpenRaster.write(image, pixels: PNGCodec()), pixels: PNGCodec()) == image) + } + + @Test(.disabled("waits on IC-5")) func otherDepthsRefused() throws { + let source = try Self.bytePixels(0x5EA, 2, 2) + #expect(F.thrown { try PNG.write(source, bitsPerChannel: 12) } == "unsupported") + #expect(F.thrown { try PNGCodec().encodePNG(source, bitsPerChannel: 12) } == "pixelCodec") + #expect(F.thrown { try PNG.write(PixelBuffer(width: PNG.maxSide + 1, height: 1)) } == "tooLarge") + } +}