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,