diff --git a/packages/desktop/packages/shared/src/utils/__tests__/binary-detection.test.ts b/packages/desktop/packages/shared/src/utils/__tests__/binary-detection.test.ts index 6187db9dd06..cdb28a20285 100644 --- a/packages/desktop/packages/shared/src/utils/__tests__/binary-detection.test.ts +++ b/packages/desktop/packages/shared/src/utils/__tests__/binary-detection.test.ts @@ -29,6 +29,17 @@ const MINIMAL_PNG = Buffer.from( 'base64' ); +// RIFF containers share the "RIFF" prefix; bytes 8-11 carry the actual form tag. +const riffBuffer = (form: string) => + Buffer.concat([ + Buffer.from([0x52, 0x49, 0x46, 0x46]), // "RIFF" + Buffer.from([0x24, 0x00, 0x00, 0x00]), // chunk size (non-zero, like a real file) + Buffer.from(form, 'ascii'), // form tag at bytes 8-11 + Buffer.alloc(16), + ]); +const MINIMAL_WEBP = riffBuffer('WEBP'); +const MINIMAL_WAV = riffBuffer('WAVE'); + // Large PNG-like binary: PNG magic + binary junk with null bytes (>= 256 base64 chars, >= 128 decoded bytes) const LARGE_PNG_BINARY = (() => { const buf = Buffer.alloc(300); @@ -382,6 +393,14 @@ describe('detectExtensionFromMagic', () => { test('returns empty for tiny buffer', () => { expect(detectExtensionFromMagic(Buffer.from([0x89]))).toBe(''); }); + + test('detects WebP from the RIFF form tag, not as WAV', () => { + expect(detectExtensionFromMagic(MINIMAL_WEBP)).toBe('.webp'); + }); + + test('still detects WAV from the RIFF/WAVE form tag', () => { + expect(detectExtensionFromMagic(MINIMAL_WAV)).toBe('.wav'); + }); }); // ============================================================ diff --git a/packages/desktop/packages/shared/src/utils/binary-detection.ts b/packages/desktop/packages/shared/src/utils/binary-detection.ts index 426efb951e1..fb79ef68207 100644 --- a/packages/desktop/packages/shared/src/utils/binary-detection.ts +++ b/packages/desktop/packages/shared/src/utils/binary-detection.ts @@ -32,7 +32,6 @@ const MAGIC_SIGNATURES: Array<{ bytes: number[]; ext: string }> = [ { bytes: [0x42, 0x4D], ext: '.bmp' }, // BM { bytes: [0x49, 0x44, 0x33], ext: '.mp3' }, // ID3 (MP3 with ID3 tag) { bytes: [0xFF, 0xFB], ext: '.mp3' }, // MP3 frame sync - { bytes: [0x52, 0x49, 0x46, 0x46], ext: '.wav' }, // RIFF (WAV container) { bytes: [0x4F, 0x67, 0x67, 0x53], ext: '.ogg' }, // OggS { bytes: [0x66, 0x4C, 0x61, 0x43], ext: '.flac' }, // fLaC ]; @@ -118,6 +117,23 @@ export function looksLikeBinary(buffer: Buffer): boolean { export function detectExtensionFromMagic(buffer: Buffer): string { if (buffer.length < 8) return ''; + // RIFF is a shared container (WAV, WebP, AVI); the four-character form tag at + // bytes 8-11 is what actually distinguishes them, so the "RIFF" prefix alone + // is ambiguous and must not be assumed to be WAV. + if ( + buffer.length >= 12 && + buffer[0] === 0x52 && + buffer[1] === 0x49 && + buffer[2] === 0x46 && + buffer[3] === 0x46 + ) { + const form = buffer.toString('ascii', 8, 12); + if (form === 'WEBP') return '.webp'; + if (form === 'AVI ') return '.avi'; + if (form === 'WAVE') return '.wav'; + return ''; + } + for (const sig of MAGIC_SIGNATURES) { if (sig.bytes.every((byte, i) => buffer[i] === byte)) { return sig.ext;