Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions src/lib/media/__tests__/findQuickTimeIlstStringValue.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { describe, it, expect } from 'vitest';
import findQuickTimeIlstStringValue from '../findQuickTimeIlstStringValue';
import readMp4BoxHeader from '../readMp4BoxHeader';

const box = (type: string, payload: Buffer): Buffer => {
const header = Buffer.alloc(8);
header.writeUInt32BE(8 + payload.length, 0);
header.write(type, 4, 'ascii');
return Buffer.concat([header, payload]);
};

const dataBox = (value: string): Buffer => {
const valueBytes = Buffer.from(value, 'utf8');
const payload = Buffer.alloc(8 + valueBytes.length);
payload.writeUInt32BE(1, 0); // type indicator: UTF-8 text
payload.writeUInt32BE(0, 4); // locale
valueBytes.copy(payload, 8);
return box('data', payload);
};

const ilstBox = (
entriesByIndex: { keyIndex: number; value: string }[]
): Buffer => {
const entries = entriesByIndex.map(({ keyIndex, value }) => {
const data = dataBox(value);
const header = Buffer.alloc(8);
header.writeUInt32BE(8 + data.length, 0);
header.writeUInt32BE(keyIndex, 4); // "type" is the 1-based key index
return Buffer.concat([header, data]);
});
return box('ilst', Buffer.concat(entries));
};

describe('findQuickTimeIlstStringValue', () => {
it('resolves the string value for a matching key index', () => {
const buffer = ilstBox([
{ keyIndex: 1, value: 'Apple' },
{ keyIndex: 2, value: '2026-06-17T12:30:03-0400' },
]);
const box = readMp4BoxHeader(buffer, 0)!;

expect(findQuickTimeIlstStringValue(buffer, box, 2)).toBe(
'2026-06-17T12:30:03-0400'
);
});

it('returns undefined when no entry matches the key index', () => {
const buffer = ilstBox([{ keyIndex: 1, value: 'Apple' }]);
const box = readMp4BoxHeader(buffer, 0)!;

expect(findQuickTimeIlstStringValue(buffer, box, 2)).toBeUndefined();
});
});
47 changes: 47 additions & 0 deletions src/lib/media/__tests__/findQuickTimeKeyIndex.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { describe, it, expect } from 'vitest';
import findQuickTimeKeyIndex from '../findQuickTimeKeyIndex';
import readMp4BoxHeader from '../readMp4BoxHeader';

const keysBox = (keyNames: string[]): Buffer => {
const entries = keyNames.map((name) => {
const nameBytes = Buffer.from(name, 'utf8');
const entry = Buffer.alloc(8 + nameBytes.length);
entry.writeUInt32BE(8 + nameBytes.length, 0);
entry.write('mdta', 4, 'ascii');
nameBytes.copy(entry, 8);
return entry;
});

const header = Buffer.alloc(8);
header.writeUInt32BE(0, 0); // version + flags
header.writeUInt32BE(keyNames.length, 4); // entry count
const payload = Buffer.concat([header, ...entries]);

const boxHeader = Buffer.alloc(8);
boxHeader.writeUInt32BE(8 + payload.length, 0);
boxHeader.write('keys', 4, 'ascii');
return Buffer.concat([boxHeader, payload]);
};

describe('findQuickTimeKeyIndex', () => {
it('returns the 1-based index of a matching key', () => {
const buffer = keysBox([
'com.apple.quicktime.make',
'com.apple.quicktime.creationdate',
]);
const box = readMp4BoxHeader(buffer, 0)!;

expect(
findQuickTimeKeyIndex(buffer, box, 'com.apple.quicktime.creationdate')
).toBe(2);
});

it('returns undefined when the key is absent', () => {
const buffer = keysBox(['com.apple.quicktime.make']);
const box = readMp4BoxHeader(buffer, 0)!;

expect(
findQuickTimeKeyIndex(buffer, box, 'com.apple.quicktime.creationdate')
).toBeUndefined();
});
});
47 changes: 47 additions & 0 deletions src/lib/media/__tests__/listMp4ChildBoxes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { describe, it, expect } from 'vitest';
import listMp4ChildBoxes from '../listMp4ChildBoxes';

const box = (type: string, payload: Buffer = Buffer.alloc(0)): Buffer => {
const header = Buffer.alloc(8);
header.writeUInt32BE(8 + payload.length, 0);
header.write(type, 4, 'ascii');
return Buffer.concat([header, payload]);
};

describe('listMp4ChildBoxes', () => {
it('walks sibling boxes in order', () => {
const buffer = Buffer.concat([
box('ftyp', Buffer.from('qt ')),
box('free'),
box('moov', Buffer.from('x')),
]);

const boxes = listMp4ChildBoxes(buffer, 0, buffer.length);
expect(boxes.map((b) => b.type)).toEqual(['ftyp', 'free', 'moov']);
});

it('scopes to the given [start, end) range', () => {
const inner = Buffer.concat([box('keys'), box('ilst')]);
const buffer = Buffer.concat([box('meta', inner)]);
const meta = listMp4ChildBoxes(buffer, 0, buffer.length)[0];

const children = listMp4ChildBoxes(
buffer,
meta.contentStart,
meta.contentEnd
);
expect(children.map((b) => b.type)).toEqual(['keys', 'ilst']);
});

it('returns an empty array for an empty range', () => {
expect(listMp4ChildBoxes(Buffer.alloc(0), 0, 0)).toEqual([]);
});

it('stops at the first malformed header instead of throwing', () => {
const buffer = Buffer.concat([box('free'), Buffer.from('ab')]);

expect(
listMp4ChildBoxes(buffer, 0, buffer.length).map((b) => b.type)
).toEqual(['free']);
});
});
61 changes: 61 additions & 0 deletions src/lib/media/__tests__/readMp4BoxHeader.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { describe, it, expect } from 'vitest';
import readMp4BoxHeader from '../readMp4BoxHeader';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use @/* path alias for imports pointing to src/.

As per coding guidelines, imports resolving under src/ should use the @/* alias instead of relative paths.

♻️ Proposed fix
-import readMp4BoxHeader from '../readMp4BoxHeader';
+import readMp4BoxHeader from '`@/lib/media/readMp4BoxHeader`';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import readMp4BoxHeader from '../readMp4BoxHeader';
import readMp4BoxHeader from '`@/lib/media/readMp4BoxHeader`';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/media/__tests__/readMp4BoxHeader.test.ts` at line 2, Update the
import in readMp4BoxHeader tests to use the configured `@/`* alias for the
src-resolved readMp4BoxHeader module instead of a relative path.

Source: Coding guidelines


const box = (type: string, payload: Buffer): Buffer => {
const header = Buffer.alloc(8);
header.writeUInt32BE(8 + payload.length, 0);
header.write(type, 4, 'ascii');
return Buffer.concat([header, payload]);
};

describe('readMp4BoxHeader', () => {
it('reads a standard 32-bit box header', () => {
const buffer = box('ftyp', Buffer.from('payload'));

expect(readMp4BoxHeader(buffer, 0)).toEqual({
type: 'ftyp',
start: 0,
contentStart: 8,
contentEnd: 15,
});
});

it('reads a 64-bit extended-size box (size === 1)', () => {
const payload = Buffer.from('payload');
const buffer = Buffer.alloc(16 + payload.length);
buffer.writeUInt32BE(1, 0);
buffer.write('mdat', 4, 'ascii');
buffer.writeUInt32BE(0, 8);
buffer.writeUInt32BE(16 + payload.length, 12);
payload.copy(buffer, 16);

expect(readMp4BoxHeader(buffer, 0)).toEqual({
type: 'mdat',
start: 0,
contentStart: 16,
contentEnd: 16 + payload.length,
});
});

it('treats size === 0 as extending to end of buffer', () => {
const header = Buffer.alloc(8);
header.writeUInt32BE(0, 0);
header.write('mdat', 4, 'ascii');
const buffer = Buffer.concat([header, Buffer.from('rest-of-file')]);

const result = readMp4BoxHeader(buffer, 0);
expect(result?.contentEnd).toBe(buffer.length);
});

it('returns undefined when fewer than 8 bytes remain', () => {
expect(readMp4BoxHeader(Buffer.from('ab'), 0)).toBeUndefined();
});

it('returns undefined when the declared size overruns the buffer', () => {
const header = Buffer.alloc(8);
header.writeUInt32BE(999, 0);
header.write('ftyp', 4, 'ascii');

expect(readMp4BoxHeader(header, 0)).toBeUndefined();
});
});
94 changes: 94 additions & 0 deletions src/lib/media/__tests__/readQuickTimeCreationDate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { describe, it, expect } from 'vitest';
import readQuickTimeCreationDate from '../readQuickTimeCreationDate';

const box = (type: string, payload: Buffer): Buffer => {
const header = Buffer.alloc(8);
header.writeUInt32BE(8 + payload.length, 0);
header.write(type, 4, 'ascii');
return Buffer.concat([header, payload]);
};

const keysBox = (keyNames: string[]): Buffer => {
const entries = keyNames.map((name) => {
const nameBytes = Buffer.from(name, 'utf8');
const entry = Buffer.alloc(8 + nameBytes.length);
entry.writeUInt32BE(8 + nameBytes.length, 0);
entry.write('mdta', 4, 'ascii');
nameBytes.copy(entry, 8);
return entry;
});

const header = Buffer.alloc(8);
header.writeUInt32BE(0, 0); // version + flags
header.writeUInt32BE(keyNames.length, 4); // entry count
return box('keys', Buffer.concat([header, ...entries]));
};

const dataBox = (value: string): Buffer => {
const valueBytes = Buffer.from(value, 'utf8');
const payload = Buffer.alloc(8 + valueBytes.length);
payload.writeUInt32BE(1, 0); // type indicator: UTF-8 text
payload.writeUInt32BE(0, 4); // locale
valueBytes.copy(payload, 8);
return box('data', payload);
};

const ilstBox = (
entriesByIndex: { keyIndex: number; value: string }[]
): Buffer => {
const entries = entriesByIndex.map(({ keyIndex, value }) => {
const data = dataBox(value);
const header = Buffer.alloc(8);
header.writeUInt32BE(8 + data.length, 0);
header.writeUInt32BE(keyIndex, 4); // "type" is the 1-based key index
return Buffer.concat([header, data]);
});
return box('ilst', Buffer.concat(entries));
};

const buildQuickTimeBuffer = (
keyNames: string[],
entriesByIndex: { keyIndex: number; value: string }[]
): Buffer => {
const hdlr = box('hdlr', Buffer.alloc(4));
const meta = box(
'meta',
Buffer.concat([hdlr, keysBox(keyNames), ilstBox(entriesByIndex)])
);
const mvhd = box('mvhd', Buffer.alloc(4));
const moov = box('moov', Buffer.concat([mvhd, meta]));
const ftyp = box('ftyp', Buffer.from('qt '));
return Buffer.concat([ftyp, moov]);
};

describe('readQuickTimeCreationDate', () => {
it('extracts com.apple.quicktime.creationdate from moov/meta/keys+ilst', () => {
const buffer = buildQuickTimeBuffer(
['com.apple.quicktime.make', 'com.apple.quicktime.creationdate'],
[
{ keyIndex: 1, value: 'Apple' },
{ keyIndex: 2, value: '2026-06-17T12:30:03-0400' },
]
);

expect(readQuickTimeCreationDate(buffer)).toBe('2026-06-17T12:30:03-0400');
});

it('returns undefined when there is no moov box', () => {
expect(readQuickTimeCreationDate(Buffer.from('not-mp4'))).toBeUndefined();
});

it('returns undefined when moov has no meta box', () => {
const moov = box('moov', box('mvhd', Buffer.alloc(4)));
expect(readQuickTimeCreationDate(moov)).toBeUndefined();
});

it('returns undefined when the creationdate key is absent', () => {
const buffer = buildQuickTimeBuffer(
['com.apple.quicktime.make'],
[{ keyIndex: 1, value: 'Apple' }]
);

expect(readQuickTimeCreationDate(buffer)).toBeUndefined();
});
});
35 changes: 35 additions & 0 deletions src/lib/media/findQuickTimeIlstStringValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import listMp4ChildBoxes from './listMp4ChildBoxes';
import type { Mp4Box } from './readMp4BoxHeader';

/**
* Finds the string value for `keyIndex` inside a QuickTime `ilst` box. Each
* ilst entry's 4-byte "type" is actually the 1-based key index from the
* `keys` box, not an ASCII tag; the value itself lives in a nested `data`
* box (4-byte type indicator, 4-byte locale, then the value bytes).
*/
const findQuickTimeIlstStringValue = (
buffer: Buffer,
ilstBox: Mp4Box,
keyIndex: number
): string | undefined => {
const entries = listMp4ChildBoxes(
buffer,
ilstBox.contentStart,
ilstBox.contentEnd
);
const entry = entries.find(
(box) => buffer.readUInt32BE(box.start + 4) === keyIndex
);
if (!entry) return undefined;

const dataBox = listMp4ChildBoxes(
buffer,
entry.contentStart,
entry.contentEnd
).find((box) => box.type === 'data');
if (!dataBox) return undefined;

return buffer.toString('utf8', dataBox.contentStart + 8, dataBox.contentEnd);
};

export default findQuickTimeIlstStringValue;
29 changes: 29 additions & 0 deletions src/lib/media/findQuickTimeKeyIndex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { Mp4Box } from './readMp4BoxHeader';

/**
* Finds the 1-based index of `targetKeyName` inside a QuickTime `keys` box.
* `keys` is a full box: 4-byte version/flags, 4-byte entry count, then
* entries of [4-byte size][4-byte namespace][key name bytes].
*/
const findQuickTimeKeyIndex = (
buffer: Buffer,
keysBox: Mp4Box,
targetKeyName: string
): number | undefined => {
let offset = keysBox.contentStart + 4;
const count = buffer.readUInt32BE(offset);
offset += 4;

for (let index = 1; index <= count; index += 1) {
const entrySize = buffer.readUInt32BE(offset);
if (entrySize < 8 || offset + entrySize > keysBox.contentEnd)
return undefined;
const keyName = buffer.toString('utf8', offset + 8, offset + entrySize);
if (keyName === targetKeyName) return index;
offset += entrySize;
}

return undefined;
};

export default findQuickTimeKeyIndex;
26 changes: 26 additions & 0 deletions src/lib/media/listMp4ChildBoxes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import readMp4BoxHeader, { type Mp4Box } from './readMp4BoxHeader';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use @/* path alias for imports pointing to src/.

As per coding guidelines, imports resolving under src/ should use the @/* alias instead of relative paths.

♻️ Proposed fix
-import readMp4BoxHeader, { type Mp4Box } from './readMp4BoxHeader';
+import readMp4BoxHeader, { type Mp4Box } from '`@/lib/media/readMp4BoxHeader`';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import readMp4BoxHeader, { type Mp4Box } from './readMp4BoxHeader';
import readMp4BoxHeader, { type Mp4Box } from '`@/lib/media/readMp4BoxHeader`';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/media/listMp4ChildBoxes.ts` at line 1, Update the import of
readMp4BoxHeader and Mp4Box in listMp4ChildBoxes to use the configured `@/`* alias
for the corresponding src module instead of a relative path.

Source: Coding guidelines


/**
* Walks sibling boxes in [start, end) and returns them in order. Stops on
* the first malformed header rather than throwing, so callers can treat a
* truncated/corrupt atom tree as "no metadata" instead of a hard failure.
*/
const listMp4ChildBoxes = (
buffer: Buffer,
start: number,
end: number
): Mp4Box[] => {
const boxes: Mp4Box[] = [];
let offset = start;

while (offset + 8 <= end) {
const box = readMp4BoxHeader(buffer, offset);
if (!box || box.contentEnd <= offset || box.contentEnd > end) break;
boxes.push(box);
offset = box.contentEnd;
}

return boxes;
};

export default listMp4ChildBoxes;
Loading
Loading