fix(engine): keep the PNG CRC working on Node 22.0/22.1

zlib.crc32 landed in Node 22.2.0, but engine and cli both declare
`"node": ">=22"` and the runtime gate is major-only, so 22.0 and 22.1
are supported. A NAMED import of a missing export throws at module
EVALUATION — ffprobe.ts would have failed to load at all on those
runtimes, before any PNG was touched, taking every probe with it.

Namespace import plus a capability check, with the previous
bit-at-a-time implementation retained as the fallback. Modern runtimes
keep the 210ms -> 1.3ms win; older ones keep working.

Raising the floor to >=22.2.0 was the alternative, but that is a
user-facing support change and does not belong in a PNG bug fix.

Tests: the same HDR PNG parses identically with the native export
absent, and a corrupt chunk still rejects on the fallback path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vance Ingalls
2026-07-31 15:54:46 -07:00
co-authored by Claude Opus 5
parent 2af3f4d0ed
commit 96a6e8bd95
2 changed files with 69 additions and 5 deletions
+41
View File
@@ -709,3 +709,44 @@ describe("PNG chunk walk — integrity of the fallback itself", () => {
).toBeNull();
});
});
describe("crc32 works on every runtime the package declares", () => {
afterEach(() => {
vi.resetModules();
vi.doUnmock("node:zlib");
});
/** Load ffprobe.ts as it would evaluate on Node 22.0/22.1. */
async function loadWithoutNativeCrc32() {
const actual = await vi.importActual<typeof import("node:zlib")>("node:zlib");
vi.resetModules();
// zlib.crc32 landed in 22.2.0, but engine and cli both declare
// `"node": ">=22"` behind a major-only gate. A NAMED import of a missing
// export throws at module evaluation, so ffprobe.ts would fail to load
// entirely on those runtimes — before any PNG is touched.
vi.doMock("node:zlib", () => ({ ...actual, crc32: undefined }));
return import("./ffprobe.js");
}
it("parses an HDR PNG identically with the native crc32 unavailable", async () => {
const png = buildPngWithChunks([
pngChunk("IHDR", [0, 0, 0x0f, 0, 0, 0, 0x08, 0x70, 16, 2, 0, 0, 0]),
pngChunk("cICP", [9, 16, 0, 1]),
pngChunk("IEND", []),
]);
const withNative = extractPngMetadataFromBuffer(png);
expect(withNative?.colorSpace?.colorTransfer).toBe("smpte2084");
const fresh = await loadWithoutNativeCrc32();
expect(fresh.extractPngMetadataFromBuffer(png)).toEqual(withNative);
});
it("rejects a corrupt chunk on the fallback path too", async () => {
const bad = pngChunk("IHDR", [0, 0, 0x0f, 0, 0, 0, 0x08, 0x70, 16, 2, 0, 0, 0]);
bad[bad.length - 1] ^= 0xff;
const png = buildPngWithChunks([bad, pngChunk("IEND", [])]);
const fresh = await loadWithoutNativeCrc32();
expect(fresh.extractPngMetadataFromBuffer(png)).toBeNull();
});
});