mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-12 07:09:59 +00:00
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:
co-authored by
Claude Opus 5
parent
2af3f4d0ed
commit
96a6e8bd95
@@ -709,3 +709,44 @@ describe("PNG chunk walk — integrity of the fallback itself", () => {
|
|||||||
).toBeNull();
|
).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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// fallow-ignore-file code-duplication complexity
|
// fallow-ignore-file code-duplication complexity
|
||||||
import { spawn } from "child_process";
|
import { spawn } from "child_process";
|
||||||
import { readFileSync } from "fs";
|
import { readFileSync } from "fs";
|
||||||
import { crc32 } from "node:zlib";
|
import * as zlib from "node:zlib";
|
||||||
import { basename, extname } from "path";
|
import { basename, extname } from "path";
|
||||||
import { redactTelemetryString } from "@hyperframes/core";
|
import { redactTelemetryString } from "@hyperframes/core";
|
||||||
import { FFPROBE_PATH_ENV, getFfprobeBinary } from "./ffmpegBinaries.js";
|
import { FFPROBE_PATH_ENV, getFfprobeBinary } from "./ffmpegBinaries.js";
|
||||||
@@ -170,11 +170,34 @@ interface StillImageMetadata {
|
|||||||
|
|
||||||
// node:zlib's crc32 is native and takes a running seed, so the chunk type and
|
// node:zlib's crc32 is native and takes a running seed, so the chunk type and
|
||||||
// the chunk data can be CRC'd in sequence without concatenating them into a
|
// the chunk data can be CRC'd in sequence without concatenating them into a
|
||||||
// throwaway buffer. The hand-rolled bit-at-a-time loop this replaces cost
|
// throwaway buffer: ~210 ms -> ~1.3 ms on a 12 MiB PNG.
|
||||||
// ~210 ms on a 12 MiB PNG; this is ~1.3 ms. Available on this repo's
|
//
|
||||||
// "node": ">=22".
|
// It landed in Node 22.2.0, and this repo declares `"node": ">=22"` with a
|
||||||
|
// major-only runtime gate, so 22.0 and 22.1 are still supported. A NAMED
|
||||||
|
// import of a missing export throws at module evaluation — i.e. `ffprobe.ts`
|
||||||
|
// would fail to load at all on those, long before any PNG is parsed — so the
|
||||||
|
// namespace import plus this capability check is deliberate. Raising the
|
||||||
|
// floor to 22.2.0 instead would be a user-facing support change, which does
|
||||||
|
// not belong in a PNG bug fix.
|
||||||
|
const nativeCrc32 = typeof zlib.crc32 === "function" ? zlib.crc32 : undefined;
|
||||||
|
|
||||||
|
/** Bit-at-a-time fallback for Node 22.0/22.1. Correct, just slower. */
|
||||||
|
function crc32Fallback(data: Buffer, seed: number): number {
|
||||||
|
let crc = seed ^ 0xffffffff;
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
crc ^= data[i] ?? 0;
|
||||||
|
for (let bit = 0; bit < 8; bit++) {
|
||||||
|
const mask = -(crc & 1);
|
||||||
|
crc = (crc >>> 1) ^ (0xedb88320 & mask);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (crc ^ 0xffffffff) >>> 0;
|
||||||
|
}
|
||||||
|
|
||||||
function chunkCrc32(chunkType: string, chunkData: Buffer): number {
|
function chunkCrc32(chunkType: string, chunkData: Buffer): number {
|
||||||
return crc32(chunkData, crc32(Buffer.from(chunkType, "ascii")));
|
const typeBytes = Buffer.from(chunkType, "ascii");
|
||||||
|
if (nativeCrc32) return nativeCrc32(chunkData, nativeCrc32(typeBytes));
|
||||||
|
return crc32Fallback(chunkData, crc32Fallback(typeBytes, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extractPngMetadataFromBuffer(buf: Buffer): StillImageMetadata | null {
|
export function extractPngMetadataFromBuffer(buf: Buffer): StillImageMetadata | null {
|
||||||
|
|||||||
Reference in New Issue
Block a user