From fb9a3be0bc110b74ba5c4b61c52836fa8e7ede2a Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Thu, 14 May 2026 03:51:50 +0000 Subject: [PATCH] fix(mime-bytes): detect HEIC/HEIF instead of misidentifying as MP4 Sort fileTypes by magicBytes length descending so that longer (more specific) magic byte patterns are checked before shorter prefixes. This fixes HEIC and HEIF images being misidentified as video/mp4 because both formats share the ISO Base Media File Format 'ftyp' container prefix. Closes #967 --- .../__tests__/file-type-detector.test.ts | 29 +++++++++++++++++++ uploads/mime-bytes/src/file-type-detector.ts | 10 +++++-- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/uploads/mime-bytes/__tests__/file-type-detector.test.ts b/uploads/mime-bytes/__tests__/file-type-detector.test.ts index f083614f5c..9fdef5ce7c 100644 --- a/uploads/mime-bytes/__tests__/file-type-detector.test.ts +++ b/uploads/mime-bytes/__tests__/file-type-detector.test.ts @@ -65,6 +65,7 @@ describe('FileTypeDetector', () => { }); it('should detect MP4 files with offset', async () => { + // ftyp + "isom" brand = generic MP4 const mp4Buffer = Buffer.from([ 0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6F, 0x6D, 0x00, 0x00, 0x02, 0x00 @@ -77,6 +78,34 @@ describe('FileTypeDetector', () => { expect(result?.extensions).toContain('mp4'); }); + it('should detect HEIC files (not misidentify as MP4)', async () => { + // ftyp + "heic" brand = HEIC image + const heicBuffer = Buffer.from([ + 0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70, + 0x68, 0x65, 0x69, 0x63, 0x00, 0x00, 0x00, 0x00 + ]); + + const result = await detector.detectFromBuffer(heicBuffer); + expect(result).toBeDefined(); + expect(result?.name).toBe('heic'); + expect(result?.mimeType).toBe('image/heic'); + expect(result?.extensions).toContain('heic'); + }); + + it('should detect HEIF files (not misidentify as MP4)', async () => { + // ftyp + "mif1" brand = HEIF image + const heifBuffer = Buffer.from([ + 0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70, + 0x6D, 0x69, 0x66, 0x31, 0x00, 0x00, 0x00, 0x00 + ]); + + const result = await detector.detectFromBuffer(heifBuffer); + expect(result).toBeDefined(); + expect(result?.name).toBe('heif'); + expect(result?.mimeType).toBe('image/heif'); + expect(result?.extensions).toContain('heif'); + }); + it('should detect UTF-8 BOM', async () => { const utf8Buffer = Buffer.from([ 0xEF, 0xBB, 0xBF, 0x48, 0x65, 0x6C, 0x6C, 0x6F diff --git a/uploads/mime-bytes/src/file-type-detector.ts b/uploads/mime-bytes/src/file-type-detector.ts index e92387a234..e8b8a8e1cb 100644 --- a/uploads/mime-bytes/src/file-type-detector.ts +++ b/uploads/mime-bytes/src/file-type-detector.ts @@ -29,8 +29,14 @@ export class FileTypeDetector { private extensionCache: Map; constructor(options: FileTypeDetectorOptions = {}) { - // Create a copy of FILE_TYPES to avoid modifying the global registry - this.fileTypes = [...FILE_TYPES]; + // Create a copy of FILE_TYPES sorted so that longer magic byte sequences + // are checked before shorter ones. This ensures that specific formats + // (e.g. HEIC with 8-byte signature "ftypheic") are matched before generic + // container formats (e.g. MP4 with 4-byte signature "ftyp") that share + // the same prefix. + this.fileTypes = [...FILE_TYPES].sort( + (a, b) => b.magicBytes.length - a.magicBytes.length + ); this.options = { peekBytes: options.peekBytes || 32, checkMultipleOffsets: options.checkMultipleOffsets !== false,