@@ -642,3 +642,111 @@ describe("extractPngMetadataFromBuffer cICP ordering", () => {
642642 expect ( extractPngMetadataFromBuffer ( onlyCicp ) ) . toBeNull ( ) ;
643643 } ) ;
644644} ) ;
645+
646+ describe ( "PNG chunk walk — integrity of the fallback itself" , ( ) => {
647+ const IHDR_4K = [ 0 , 0 , 0x0f , 0 , 0 , 0 , 0x08 , 0x70 , 16 , 2 , 0 , 0 , 0 ] ;
648+ const CICP_PQ = [ 9 , 16 , 0 , 1 ] ;
649+
650+ // Regression: the walk used to continue past cICP to IEND, which made
651+ // whole-file integrity a precondition for returning anything. A damaged
652+ // trailing chunk in an otherwise-good HDR PNG nulled the whole result, and
653+ // extractMediaMetadata then re-throws the ffprobe error it had swallowed
654+ // rather than using the fallback it just computed.
655+ it ( "returns metadata even when a chunk AFTER cICP is corrupt" , ( ) => {
656+ const bad = pngChunk ( "tEXt" , [ 65 , 66 ] ) ;
657+ bad [ bad . length - 1 ] ^= 0xff ; // break the CRC
658+ const png = buildPngWithChunks ( [
659+ pngChunk ( "IHDR" , IHDR_4K ) ,
660+ pngChunk ( "cICP" , CICP_PQ ) ,
661+ pngChunk ( "IDAT" , [ 0x78 , 0x9c , 0x63 , 0x00 , 0x00 , 0x00 , 0x02 , 0x00 , 0x01 ] ) ,
662+ bad ,
663+ pngChunk ( "IEND" , [ ] ) ,
664+ ] ) ;
665+ expect ( extractPngMetadataFromBuffer ( png ) ) . toEqual ( {
666+ width : 3840 ,
667+ height : 2160 ,
668+ colorSpace : { colorPrimaries : "bt2020" , colorTransfer : "smpte2084" , colorSpace : "gbr" } ,
669+ } ) ;
670+ } ) ;
671+
672+ it ( "survives outright truncation after cICP" , ( ) => {
673+ const png = buildPngWithChunks ( [ pngChunk ( "IHDR" , IHDR_4K ) , pngChunk ( "cICP" , CICP_PQ ) ] ) ;
674+ const truncated = Buffer . concat ( [ png , Buffer . from ( [ 0 , 0 , 0x7f , 0xff , 73 , 68 , 65 , 84 ] ) ] ) ;
675+ expect ( extractPngMetadataFromBuffer ( truncated ) ?. width ) . toBe ( 3840 ) ;
676+ } ) ;
677+
678+ // Regression: IHDR had no first-chunk anchor, so a later one overwrote the
679+ // real dimensions and the producer laid out a 1-pixel image.
680+ it ( "ignores a second IHDR" , ( ) => {
681+ const png = buildPngWithChunks ( [
682+ pngChunk ( "IHDR" , IHDR_4K ) ,
683+ pngChunk ( "cICP" , CICP_PQ ) ,
684+ pngChunk ( "IDAT" , [ 0x78 , 0x9c , 0x63 , 0x00 , 0x00 , 0x00 , 0x02 , 0x00 , 0x01 ] ) ,
685+ pngChunk ( "IHDR" , [ 0 , 0 , 0 , 1 , 0 , 0 , 0 , 1 , 16 , 2 , 0 , 0 , 0 ] ) ,
686+ pngChunk ( "IEND" , [ ] ) ,
687+ ] ) ;
688+ const meta = extractPngMetadataFromBuffer ( png ) ;
689+ expect ( meta ?. width ) . toBe ( 3840 ) ;
690+ expect ( meta ?. height ) . toBe ( 2160 ) ;
691+ } ) ;
692+
693+ // A truncated 8-byte IHDR used to be accepted, reading height out of the
694+ // CRC bytes; the spec length is 13.
695+ it ( "rejects a short IHDR rather than reading garbage dimensions" , ( ) => {
696+ const png = buildPngWithChunks ( [
697+ pngChunk ( "IHDR" , [ 0 , 0 , 0 , 7 , 0 , 0 , 0 , 9 ] ) ,
698+ pngChunk ( "cICP" , CICP_PQ ) ,
699+ pngChunk ( "IEND" , [ ] ) ,
700+ ] ) ;
701+ expect ( extractPngMetadataFromBuffer ( png ) ) . toBeNull ( ) ;
702+ } ) ;
703+
704+ it ( "still rejects a PNG whose IHDR or cICP itself is corrupt" , ( ) => {
705+ const badIhdr = pngChunk ( "IHDR" , IHDR_4K ) ;
706+ badIhdr [ badIhdr . length - 1 ] ^= 0xff ;
707+ expect (
708+ extractPngMetadataFromBuffer ( buildPngWithChunks ( [ badIhdr , pngChunk ( "IEND" , [ ] ) ] ) ) ,
709+ ) . toBeNull ( ) ;
710+ } ) ;
711+ } ) ;
712+
713+ describe ( "crc32 works on every runtime the package declares" , ( ) => {
714+ afterEach ( ( ) => {
715+ vi . resetModules ( ) ;
716+ vi . doUnmock ( "node:zlib" ) ;
717+ } ) ;
718+
719+ /** Load ffprobe.ts as it would evaluate on Node 22.0/22.1. */
720+ async function loadWithoutNativeCrc32 ( ) {
721+ const actual = await vi . importActual < typeof import ( "node:zlib" ) > ( "node:zlib" ) ;
722+ vi . resetModules ( ) ;
723+ // zlib.crc32 landed in 22.2.0, but engine and cli both declare
724+ // `"node": ">=22"` behind a major-only gate. A NAMED import of a missing
725+ // export throws at module evaluation, so ffprobe.ts would fail to load
726+ // entirely on those runtimes — before any PNG is touched.
727+ vi . doMock ( "node:zlib" , ( ) => ( { ...actual , crc32 : undefined } ) ) ;
728+ return import ( "./ffprobe.js" ) ;
729+ }
730+
731+ it ( "parses an HDR PNG identically with the native crc32 unavailable" , async ( ) => {
732+ const png = buildPngWithChunks ( [
733+ pngChunk ( "IHDR" , [ 0 , 0 , 0x0f , 0 , 0 , 0 , 0x08 , 0x70 , 16 , 2 , 0 , 0 , 0 ] ) ,
734+ pngChunk ( "cICP" , [ 9 , 16 , 0 , 1 ] ) ,
735+ pngChunk ( "IEND" , [ ] ) ,
736+ ] ) ;
737+ const withNative = extractPngMetadataFromBuffer ( png ) ;
738+ expect ( withNative ?. colorSpace ?. colorTransfer ) . toBe ( "smpte2084" ) ;
739+
740+ const fresh = await loadWithoutNativeCrc32 ( ) ;
741+ expect ( fresh . extractPngMetadataFromBuffer ( png ) ) . toEqual ( withNative ) ;
742+ } ) ;
743+
744+ it ( "rejects a corrupt chunk on the fallback path too" , async ( ) => {
745+ const bad = pngChunk ( "IHDR" , [ 0 , 0 , 0x0f , 0 , 0 , 0 , 0x08 , 0x70 , 16 , 2 , 0 , 0 , 0 ] ) ;
746+ bad [ bad . length - 1 ] ^= 0xff ;
747+ const png = buildPngWithChunks ( [ bad , pngChunk ( "IEND" , [ ] ) ] ) ;
748+
749+ const fresh = await loadWithoutNativeCrc32 ( ) ;
750+ expect ( fresh . extractPngMetadataFromBuffer ( png ) ) . toBeNull ( ) ;
751+ } ) ;
752+ } ) ;
0 commit comments