@@ -2,7 +2,12 @@ import pm from "picomatch";
22import { describe , expect , it } from "vitest" ;
33
44import type { LocalPattern } from "./images.js" ;
5- import { matchLocalPattern , matchRemotePattern as mRP } from "./images.js" ;
5+ import {
6+ detectImageContentType ,
7+ matchLocalPattern ,
8+ matchRemotePattern as mRP ,
9+ parseCdnCgiImageRequest ,
10+ } from "./images.js" ;
611
712/**
813 * See https://github.com/vercel/next.js/blob/64702a9/test/unit/image-optimizer/match-remote-pattern.test.ts
@@ -426,3 +431,150 @@ describe("matchLocalPattern", () => {
426431 expect ( mLP ( p , "/path/to/file.txt?q=1&a=two" ) ) . toBe ( true ) ;
427432 } ) ;
428433} ) ;
434+
435+ describe ( "parseCdnCgiImageRequest" , ( ) => {
436+ it ( "should parse a valid local image request" , ( ) => {
437+ const result = parseCdnCgiImageRequest (
438+ "/cdn-cgi/image/width=640,quality=75,format=auto/_next/static/media/photo.png"
439+ ) ;
440+ expect ( result ) . toEqual ( { ok : true , url : "/_next/static/media/photo.png" , static : false } ) ;
441+ } ) ;
442+
443+ it ( "should parse a valid remote image request" , ( ) => {
444+ const result = parseCdnCgiImageRequest (
445+ "/cdn-cgi/image/width=1080,quality=75,format=auto/https://example.com/photo.jpg"
446+ ) ;
447+ expect ( result ) . toEqual ( { ok : true , url : "https://example.com/photo.jpg" , static : false } ) ;
448+ } ) ;
449+
450+ it ( "should reject when pathname does not match /cdn-cgi/image/ format" , ( ) => {
451+ const result = parseCdnCgiImageRequest ( "/cdn-cgi/image/" ) ;
452+ expect ( result ) . toEqual ( { ok : false , message : "Invalid /cdn-cgi/image/ URL format" } ) ;
453+ } ) ;
454+
455+ it ( "should reject when options segment has no trailing image URL" , ( ) => {
456+ const result = parseCdnCgiImageRequest ( "/cdn-cgi/image/width=640" ) ;
457+ expect ( result ) . toEqual ( { ok : false , message : "Invalid /cdn-cgi/image/ URL format" } ) ;
458+ } ) ;
459+
460+ it ( "should reject protocol-relative URLs" , ( ) => {
461+ const result = parseCdnCgiImageRequest (
462+ "/cdn-cgi/image/width=640,quality=75,format=auto//evil.com/photo.jpg"
463+ ) ;
464+ expect ( result ) . toEqual ( {
465+ ok : false ,
466+ message : '"url" parameter cannot be a protocol-relative URL (//)' ,
467+ } ) ;
468+ } ) ;
469+
470+ it ( "should add leading slash to relative image URLs" , ( ) => {
471+ const result = parseCdnCgiImageRequest (
472+ "/cdn-cgi/image/width=640,quality=75,format=auto/path/to/image.png"
473+ ) ;
474+ expect ( result ) . toMatchObject ( { ok : true , url : "/path/to/image.png" } ) ;
475+ } ) ;
476+ } ) ;
477+
478+ describe ( "detectImageContentType" , ( ) => {
479+ it ( "should detect JPEG" , ( ) => {
480+ const buffer = new Uint8Array ( 32 ) ;
481+ buffer [ 0 ] = 0xff ;
482+ buffer [ 1 ] = 0xd8 ;
483+ buffer [ 2 ] = 0xff ;
484+ expect ( detectImageContentType ( buffer ) ) . toBe ( "image/jpeg" ) ;
485+ } ) ;
486+
487+ it ( "should detect PNG" , ( ) => {
488+ const buffer = new Uint8Array ( 32 ) ;
489+ const pngHeader = [ 0x89 , 0x50 , 0x4e , 0x47 , 0x0d , 0x0a , 0x1a , 0x0a ] ;
490+ pngHeader . forEach ( ( b , i ) => ( buffer [ i ] = b ) ) ;
491+ expect ( detectImageContentType ( buffer ) ) . toBe ( "image/png" ) ;
492+ } ) ;
493+
494+ it ( "should detect GIF" , ( ) => {
495+ const buffer = new Uint8Array ( 32 ) ;
496+ const gifHeader = [ 0x47 , 0x49 , 0x46 , 0x38 ] ;
497+ gifHeader . forEach ( ( b , i ) => ( buffer [ i ] = b ) ) ;
498+ expect ( detectImageContentType ( buffer ) ) . toBe ( "image/gif" ) ;
499+ } ) ;
500+
501+ it ( "should detect WebP" , ( ) => {
502+ const buffer = new Uint8Array ( 32 ) ;
503+ // RIFF....WEBP
504+ const webpHeader = [ 0x52 , 0x49 , 0x46 , 0x46 , 0x00 , 0x00 , 0x00 , 0x00 , 0x57 , 0x45 , 0x42 , 0x50 ] ;
505+ webpHeader . forEach ( ( b , i ) => ( buffer [ i ] = b ) ) ;
506+ expect ( detectImageContentType ( buffer ) ) . toBe ( "image/webp" ) ;
507+ } ) ;
508+
509+ it ( "should detect SVG (<?xml prefix)" , ( ) => {
510+ const buffer = new Uint8Array ( 32 ) ;
511+ const svgHeader = [ 0x3c , 0x3f , 0x78 , 0x6d , 0x6c ] ;
512+ svgHeader . forEach ( ( b , i ) => ( buffer [ i ] = b ) ) ;
513+ expect ( detectImageContentType ( buffer ) ) . toBe ( "image/svg+xml" ) ;
514+ } ) ;
515+
516+ it ( "should detect SVG (<svg prefix)" , ( ) => {
517+ const buffer = new Uint8Array ( 32 ) ;
518+ const svgHeader = [ 0x3c , 0x73 , 0x76 , 0x67 ] ;
519+ svgHeader . forEach ( ( b , i ) => ( buffer [ i ] = b ) ) ;
520+ expect ( detectImageContentType ( buffer ) ) . toBe ( "image/svg+xml" ) ;
521+ } ) ;
522+
523+ it ( "should detect AVIF" , ( ) => {
524+ const buffer = new Uint8Array ( 32 ) ;
525+ const avifHeader = [ 0x00 , 0x00 , 0x00 , 0x00 , 0x66 , 0x74 , 0x79 , 0x70 , 0x61 , 0x76 , 0x69 , 0x66 ] ;
526+ // Bytes at positions 0-3 are wildcards (any non-zero matches), fill with typical values
527+ buffer [ 0 ] = 0x00 ;
528+ buffer [ 1 ] = 0x00 ;
529+ buffer [ 2 ] = 0x00 ;
530+ buffer [ 3 ] = 0x1c ; // non-zero (file size prefix); the detection uses !b to skip zero bytes
531+ avifHeader . forEach ( ( b , i ) => {
532+ if ( b !== 0 ) buffer [ i ] = b ;
533+ } ) ;
534+ expect ( detectImageContentType ( buffer ) ) . toBe ( "image/avif" ) ;
535+ } ) ;
536+
537+ it ( "should detect ICO" , ( ) => {
538+ const buffer = new Uint8Array ( 32 ) ;
539+ const icoHeader = [ 0x00 , 0x00 , 0x01 , 0x00 ] ;
540+ icoHeader . forEach ( ( b , i ) => ( buffer [ i ] = b ) ) ;
541+ expect ( detectImageContentType ( buffer ) ) . toBe ( "image/x-icon" ) ;
542+ } ) ;
543+
544+ it ( "should detect TIFF" , ( ) => {
545+ const buffer = new Uint8Array ( 32 ) ;
546+ const tiffHeader = [ 0x49 , 0x49 , 0x2a , 0x00 ] ;
547+ tiffHeader . forEach ( ( b , i ) => ( buffer [ i ] = b ) ) ;
548+ expect ( detectImageContentType ( buffer ) ) . toBe ( "image/tiff" ) ;
549+ } ) ;
550+
551+ it ( "should detect BMP" , ( ) => {
552+ const buffer = new Uint8Array ( 32 ) ;
553+ buffer [ 0 ] = 0x42 ;
554+ buffer [ 1 ] = 0x4d ;
555+ expect ( detectImageContentType ( buffer ) ) . toBe ( "image/bmp" ) ;
556+ } ) ;
557+
558+ it ( "should detect JXL (short signature)" , ( ) => {
559+ const buffer = new Uint8Array ( 32 ) ;
560+ buffer [ 0 ] = 0xff ;
561+ buffer [ 1 ] = 0x0a ;
562+ expect ( detectImageContentType ( buffer ) ) . toBe ( "image/jxl" ) ;
563+ } ) ;
564+
565+ it ( "should detect JXL (long signature)" , ( ) => {
566+ const buffer = new Uint8Array ( 32 ) ;
567+ const jxlHeader = [ 0x00 , 0x00 , 0x00 , 0x0c , 0x4a , 0x58 , 0x4c , 0x20 , 0x0d , 0x0a , 0x87 , 0x0a ] ;
568+ jxlHeader . forEach ( ( b , i ) => ( buffer [ i ] = b ) ) ;
569+ expect ( detectImageContentType ( buffer ) ) . toBe ( "image/jxl" ) ;
570+ } ) ;
571+
572+ it ( "should return null for unknown content" , ( ) => {
573+ const buffer = new Uint8Array ( 32 ) ;
574+ buffer . fill ( 0x00 ) ;
575+ buffer [ 0 ] = 0x01 ;
576+ buffer [ 1 ] = 0x02 ;
577+ buffer [ 2 ] = 0x03 ;
578+ expect ( detectImageContentType ( buffer ) ) . toBeNull ( ) ;
579+ } ) ;
580+ } ) ;
0 commit comments