Skip to content

Commit d0889a8

Browse files
committed
fix(files): read HEIF compatible brands, not just the major brand
A standards-valid HEIF may carry a generic major brand such as isom and declare heic, heix or mif1 only among the compatible brands that follow the minor_version at offset 12. Reading bytes 8-11 alone classified those as non-HEIF, skipping the fallback decode and leaving a small undecodable file to reach the model as raw bytes.
1 parent 6188438 commit d0889a8

2 files changed

Lines changed: 37 additions & 5 deletions

File tree

apps/sim/lib/uploads/server/heic.test.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,17 @@
44
import { describe, expect, it } from 'vitest'
55
import { isHeifContainer, transcodeHeicToJpeg } from '@/lib/uploads/server/heic'
66

7-
/** An ISO-BMFF header: 4-byte box size, the `ftyp` marker, then the major brand. */
8-
function ftypHeader(brand: string): Buffer {
9-
const header = Buffer.alloc(16)
10-
header.writeUInt32BE(16, 0)
7+
/**
8+
* An ISO-BMFF `ftyp` box: 4-byte size, the `ftyp` marker, the major brand, a
9+
* 4-byte minor version, then any compatible brands.
10+
*/
11+
function ftypHeader(brand: string, compatible: string[] = []): Buffer {
12+
const size = 16 + compatible.length * 4
13+
const header = Buffer.alloc(size)
14+
header.writeUInt32BE(size, 0)
1115
header.write('ftyp', 4, 'ascii')
1216
header.write(brand, 8, 'ascii')
17+
compatible.forEach((entry, index) => header.write(entry, 16 + index * 4, 'ascii'))
1318
return header
1419
}
1520

@@ -48,6 +53,22 @@ describe('isHeifContainer', () => {
4853
expect(isHeifContainer(ftypHeader('qt '))).toBe(false)
4954
})
5055

56+
it('detects a HEIF brand declared only among the compatible brands', () => {
57+
// Standards-valid: a generic major brand with the HEIF brand listed after it.
58+
expect(isHeifContainer(ftypHeader('isom', ['iso2', 'heic', 'mif1']))).toBe(true)
59+
expect(isHeifContainer(ftypHeader('mp42', ['heix']))).toBe(true)
60+
})
61+
62+
it('rejects a box whose compatible brands are all non-HEIF', () => {
63+
expect(isHeifContainer(ftypHeader('isom', ['iso2', 'mp41', 'mp42']))).toBe(false)
64+
})
65+
66+
it('does not read compatible brands past the declared box size', () => {
67+
const truncated = ftypHeader('isom', ['heic'])
68+
truncated.writeUInt32BE(16, 0)
69+
expect(isHeifContainer(truncated)).toBe(false)
70+
})
71+
5172
it('rejects buffers too short to carry a brand', () => {
5273
expect(isHeifContainer(Buffer.alloc(0))).toBe(false)
5374
expect(isHeifContainer(ftypHeader('heic').subarray(0, 11))).toBe(false)

apps/sim/lib/uploads/server/heic.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,18 @@ const HEIF_BRANDS = new Set([
3434
export function isHeifContainer(buffer: Buffer): boolean {
3535
if (buffer.length < 12) return false
3636
if (buffer.toString('ascii', 4, 8) !== 'ftyp') return false
37-
return HEIF_BRANDS.has(buffer.toString('ascii', 8, 12))
37+
if (HEIF_BRANDS.has(buffer.toString('ascii', 8, 12))) return true
38+
39+
// A standards-valid HEIF may carry a generic major brand such as `isom` and name
40+
// the HEIF brand only among the compatible brands, which follow the 4-byte
41+
// minor_version at offset 12 and run to the end of the box. A declared size of 0
42+
// or 1 (the ISO-BMFF size escapes, which `ftyp` does not use) leaves `end` below
43+
// the loop's start, so those simply do not scan.
44+
const end = Math.min(buffer.readUInt32BE(0), buffer.length)
45+
for (let offset = 16; offset + 4 <= end; offset += 4) {
46+
if (HEIF_BRANDS.has(buffer.toString('ascii', offset, offset + 4))) return true
47+
}
48+
return false
3849
}
3950

4051
/**

0 commit comments

Comments
 (0)