Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,670 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { parseMp4 } from "./mp4-atom.js";
// ---------------------------------------------------------------------------
// Minimal MP4/M4A buffer builders
// ---------------------------------------------------------------------------
function uint32BE(n: number): number[] {
return [(n >> 24) & 0xff, (n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff];
}
function boxBytes(type: string, data: number[]): number[] {
const size = 8 + data.length;
// Encode type as Latin-1: each char maps directly to its char code (handles © = 0xA9)
const typeBytes = [
type.charCodeAt(0) & 0xff,
type.charCodeAt(1) & 0xff,
type.charCodeAt(2) & 0xff,
type.charCodeAt(3) & 0xff,
];
return [...uint32BE(size), ...typeBytes, ...data];
}
function textDataBox(text: string): number[] {
const textBytes = Array.from(new TextEncoder().encode(text));
// data box: type(4) "data", flags+version(4) = 0x00 00 00 01 (UTF-8), locale(4) = 0
return boxBytes("data", [
0x00,
0x00,
0x00,
0x01, // flags: text/UTF-8
0x00,
0x00,
0x00,
0x00, // locale
...textBytes,
]);
}
function itemBox(itemType: string, textContent: string): number[] {
return boxBytes(itemType, textDataBox(textContent));
}
function buildMinimalMp4WithIlst(ilstItems: number[]): Uint8Array {
// ilst box
const ilst = boxBytes("ilst", ilstItems);
// meta box (needs 4-byte version+flags prefix before children)
const metaData = [0x00, 0x00, 0x00, 0x00, ...ilst]; // version+flags + ilst
const meta = boxBytes("meta", metaData);
// udta box
const udta = boxBytes("udta", meta);
// moov box
const moov = boxBytes("moov", udta);
return new Uint8Array(moov);
}
describe("mp4-atom parser", () => {
it("parses ©nam (title)", { timeout: 5000 }, () => {
const buf = buildMinimalMp4WithIlst(itemBox("\u00a9nam", "Test Title"));
const track = parseMp4(buf);
expect(track.title).toBe("Test Title");
expect(track.format).toBe("mp4");
});
it("parses ©ART (artist)", { timeout: 5000 }, () => {
const buf = buildMinimalMp4WithIlst(
itemBox("\u00a9ART", "Test Artist")
);
const track = parseMp4(buf);
expect(track.artist).toBe("Test Artist");
});
it("parses ©alb (album)", { timeout: 5000 }, () => {
const buf = buildMinimalMp4WithIlst(itemBox("\u00a9alb", "Test Album"));
const track = parseMp4(buf);
expect(track.album).toBe("Test Album");
});
it("parses aART (album artist)", { timeout: 5000 }, () => {
const buf = buildMinimalMp4WithIlst(itemBox("aART", "Various Artists"));
const track = parseMp4(buf);
expect(track.albumArtist).toBe("Various Artists");
});
it("parses ©day (year)", { timeout: 5000 }, () => {
const buf = buildMinimalMp4WithIlst(itemBox("\u00a9day", "2023-05-01"));
const track = parseMp4(buf);
expect(track.year).toBe(2023);
});
it("parses ©cmt (comment)", { timeout: 5000 }, () => {
const buf = buildMinimalMp4WithIlst(itemBox("\u00a9cmt", "A comment"));
const track = parseMp4(buf);
expect(track.comment).toBe("A comment");
});
it("parses ©gen (genre)", { timeout: 5000 }, () => {
const buf = buildMinimalMp4WithIlst(itemBox("\u00a9gen", "Electronic"));
const track = parseMp4(buf);
expect(track.genre).toBe("Electronic");
});
it("parses trkn (track number)", { timeout: 5000 }, () => {
// trkn data: 0x00 0x00 (padding) 0x00 0x03 (track 3) 0x00 0x0c (total 12)
const trknData = boxBytes("data", [
0x00,
0x00,
0x00,
0x00, // flags: implicit integer
0x00,
0x00,
0x00,
0x00, // locale
0x00,
0x00, // padding
0x00,
0x03, // track number: 3
0x00,
0x0c, // total: 12
]);
const trknBox = boxBytes("trkn", trknData);
const buf = buildMinimalMp4WithIlst(trknBox);
const track = parseMp4(buf);
expect(track.trackNumber).toBe(3);
expect(track.totalTracks).toBe(12);
});
it("parses disk (disc number)", { timeout: 5000 }, () => {
const diskData = boxBytes("data", [
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x02, // disc 2
0x00,
0x03, // of 3
]);
const diskBox = boxBytes("disk", diskData);
const buf = buildMinimalMp4WithIlst(diskBox);
const track = parseMp4(buf);
expect(track.discNumber).toBe(2);
expect(track.totalDiscs).toBe(3);
});
it("returns empty object when no moov box", { timeout: 5000 }, () => {
const buf = new Uint8Array([
0x00, 0x00, 0x00, 0x10, 0x66, 0x72, 0x65, 0x65,
]); // "free" box
expect(parseMp4(buf)).toEqual({});
});
it("returns empty object for short buffer", { timeout: 5000 }, () => {
expect(parseMp4(new Uint8Array([0x00, 0x01, 0x02]))).toEqual({});
});
it("respects skipCovers option", { timeout: 5000 }, () => {
// Build covr item with data box
const covrData = boxBytes("data", [
0x00,
0x00,
0x00,
0x0d, // flags: 0x0d = JPEG
0x00,
0x00,
0x00,
0x00, // locale
0xff,
0xd8,
0xff, // minimal JPEG bytes
]);
const covrBox = boxBytes("covr", covrData);
const buf = buildMinimalMp4WithIlst(covrBox);
const withCover = parseMp4(buf, { skipCovers: false });
const noCover = parseMp4(buf, { skipCovers: true });
// Cover parsing may succeed or not depending on minimal bytes — just check skip works
if (withCover.cover) {
expect(noCover.cover).toBeUndefined();
}
});
it("parses gnre (integer genre)", { timeout: 5000 }, () => {
// gnre item box with an integer data box: index=18 (1-based) = "Rock"
const gnreData = boxBytes("data", [
0x00,
0x00,
0x00,
0x00, // flags: implicit integer
0x00,
0x00,
0x00,
0x00, // locale
0x00,
0x12, // index: 18 → "Rock"
]);
const gnreBox = boxBytes("gnre", gnreData);
const buf = buildMinimalMp4WithIlst(gnreBox);
const track = parseMp4(buf);
expect(track.genre).toBe("Rock");
});
it("ignores gnre with index 0 (invalid)", { timeout: 5000 }, () => {
// idx=0 is not valid (gnre is 1-based), should not set genre
const gnreData = boxBytes("data", [
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00, // index: 0
]);
const gnreBox = boxBytes("gnre", gnreData);
const buf = buildMinimalMp4WithIlst(gnreBox);
const track = parseMp4(buf);
expect(track.genre).toBeUndefined();
});
it("parses ©lyr (lyrics)", { timeout: 5000 }, () => {
const buf = buildMinimalMp4WithIlst(
itemBox("\u00a9lyr", "Amazing lyrics")
);
const track = parseMp4(buf);
expect(track.lyrics).toBe("Amazing lyrics");
});
it("parses covr (PNG cover art)", { timeout: 5000 }, () => {
// Build a covr data box with PNG flag (0x0e) and some bytes
const imgBytes = [0x89, 0x50, 0x4e, 0x47]; // PNG magic bytes
const covrData = boxBytes("data", [
0x00,
0x00,
0x00,
0x0e, // flags: 0x0e = PNG
0x00,
0x00,
0x00,
0x00, // locale
...imgBytes,
]);
const covrBox = boxBytes("covr", covrData);
const buf = buildMinimalMp4WithIlst(covrBox);
const track = parseMp4(buf, { skipCovers: false });
expect(track.cover).toBeDefined();
expect(track.cover?.mimeType).toBe("image/png");
});
it("skips data box with dataSize <= 8 in readItmData", {
timeout: 5000,
}, () => {
// A ©nam item with a data box that has only 8 bytes (the header) — too small for content
// This triggers `if (child.dataSize > 8)` FALSE branch → returns null → readItmText returns ""
const tinyDataBox = boxBytes("data", [
0x00,
0x00,
0x00,
0x01, // flags
0x00,
0x00,
0x00,
0x00, // locale (exactly 8 bytes total data content)
]);
// Note: tinyDataBox has 8 bytes of data → dataSize = 8, NOT > 8 → returns null
const itemWithTinyData = boxBytes("\u00a9nam", tinyDataBox);
const buf = buildMinimalMp4WithIlst(itemWithTinyData);
const track = parseMp4(buf);
expect(track.title).toBeUndefined();
});
it("handles iterBoxes with zero-size box (breaks)", {
timeout: 5000,
}, () => {
// A moov box containing a zero-size box (size=0 means extends to EOF, treated as terminal)
// iterBoxes breaks when size === 0
const zeroSizeBox = [0x00, 0x00, 0x00, 0x00, 0x74, 0x65, 0x73, 0x74]; // size=0, type="test"
const moovData = [...zeroSizeBox];
const moovSize = 8 + moovData.length;
const buf = new Uint8Array([
...uint32BE(moovSize),
0x6d,
0x6f,
0x6f,
0x76, // moov
...moovData,
]);
const track = parseMp4(buf);
// No ilst found → returns { format: "mp4" }
expect(track.format).toBe("mp4");
});
it("handles iterBoxes with corrupted small-size box (size < 8, breaks)", {
timeout: 5000,
}, () => {
// A moov containing a box with size=4 (too small to be valid)
const corruptBox = [0x00, 0x00, 0x00, 0x04, 0x74, 0x65, 0x73, 0x74]; // size=4
const moovData = [...corruptBox];
const moovSize = 8 + moovData.length;
const buf = new Uint8Array([
...uint32BE(moovSize),
0x6d,
0x6f,
0x6f,
0x76,
...moovData,
]);
const track = parseMp4(buf);
expect(track.format).toBe("mp4");
});
it("skips 64-bit extended-size box in iterBoxes", { timeout: 5000 }, () => {
// A moov with a box that has size=1 (64-bit extended size indicator)
// iterBoxes skips it and advances by 16
const extendedBox = [
0x00,
0x00,
0x00,
0x01, // size=1 (64-bit indicator)
0x74,
0x65,
0x73,
0x74, // type="test"
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x10, // 64-bit size=16
]; // 16 bytes total, skipped
const moovData = [...extendedBox];
const moovSize = 8 + moovData.length;
const buf = new Uint8Array([
...uint32BE(moovSize),
0x6d,
0x6f,
0x6f,
0x76,
...moovData,
]);
const track = parseMp4(buf);
expect(track.format).toBe("mp4");
});
it("returns format mp4 with no ilst", { timeout: 5000 }, () => {
// A minimal moov with no meta/ilst — parseMp4 should return { format: "mp4" }
const moovData: number[] = [];
const moovSize = 8 + moovData.length;
const uint32BE2 = (n: number) => [
(n >> 24) & 0xff,
(n >> 16) & 0xff,
(n >> 8) & 0xff,
n & 0xff,
];
const buf = new Uint8Array([
...uint32BE2(moovSize),
0x6d,
0x6f,
0x6f,
0x76,
...moovData,
]);
const track = parseMp4(buf);
expect(track.format).toBe("mp4");
});
it("returns empty string from readItmText when data box missing", {
timeout: 5000,
}, () => {
// A ©nam item box with NO data child box — readItmText should return ""
// and therefore title should remain undefined
const emptyItemBox = boxBytes("\u00a9nam", []); // no data child
const buf = buildMinimalMp4WithIlst(emptyItemBox);
const track = parseMp4(buf);
expect(track.title).toBeUndefined();
expect(track.format).toBe("mp4");
});
it("handles trkn item with data box too small for readTrackOrDisc", {
timeout: 5000,
}, () => {
// readTrackOrDisc needs data.length >= 6; < 6 returns {num:0, total:0}
// (num=0 → not set, total=0 → not set)
const tinyTrackData = boxBytes("data", [
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00, // only 2 bytes after header (< 6 needed for num+total)
]);
const trknBox = boxBytes("trkn", tinyTrackData);
const buf = buildMinimalMp4WithIlst(trknBox);
const track = parseMp4(buf);
// num=0 means trackNumber not set
expect(track.trackNumber).toBeUndefined();
});
it("handles covr with data box dataSize <= 8 (continues, returns null)", {
timeout: 5000,
}, () => {
// parseCovr: if child.dataSize <= 8, continue — no cover returned
const tinyCovrData = boxBytes(
"data",
[
0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00,
0x00,
// only 8 bytes → dataSize = 8, NOT > 8 → continue
]
);
const covrBox = boxBytes("covr", tinyCovrData);
const buf = buildMinimalMp4WithIlst(covrBox);
const track = parseMp4(buf, { skipCovers: false });
expect(track.cover).toBeUndefined();
});
it("returns null from parseCovr when no data child box found", {
timeout: 5000,
}, () => {
// covr item with no data box — parseCovr returns null → cover undefined
const covrBoxNoData = boxBytes("covr", []); // no data child
const buf = buildMinimalMp4WithIlst(covrBoxNoData);
const track = parseMp4(buf, { skipCovers: false });
expect(track.cover).toBeUndefined();
});
it("parseCovr skips non-data child box type before reaching data child", {
timeout: 5000,
}, () => {
// covr item with a non-"data" typed child first (triggers `if (child.type !== "data") continue`)
// then a valid data child
const nonDataChild = boxBytes("free", [0x00, 0x00]); // type "free" not "data"
const imgBytes = [0xff, 0xd8, 0xff]; // minimal JPEG
const dataChild = boxBytes("data", [
0x00,
0x00,
0x00,
0x0d, // flags: JPEG
0x00,
0x00,
0x00,
0x00, // locale
...imgBytes,
]);
const covrBox = boxBytes("covr", [...nonDataChild, ...dataChild]);
const buf = buildMinimalMp4WithIlst(covrBox);
const track = parseMp4(buf, { skipCovers: false });
// After skipping non-data child, data child is found → cover set
expect(track.cover).toBeDefined();
});
it("readItmData skips non-data children (child.type !== data else path)", {
timeout: 5000,
}, () => {
// A ©nam item with a non-"data" child — readItmData returns null → title undefined
const nonDataChild = boxBytes("name", [0x41, 0x42]); // type "name" not "data"
const itemBox = boxBytes("\u00a9nam", nonDataChild);
const buf = buildMinimalMp4WithIlst(itemBox);
const track = parseMp4(buf);
expect(track.title).toBeUndefined();
});
it("finds ilst via moov/meta fallback path (no udta)", {
timeout: 5000,
}, () => {
// Build moov/meta/ilst directly (skip udta) — exercises the fallback path
const ilst = boxBytes("ilst", itemBox("\u00a9nam", "FallbackTitle"));
const metaData = [0x00, 0x00, 0x00, 0x00, ...ilst]; // version+flags + ilst
const meta = boxBytes("meta", metaData);
const moov = boxBytes("moov", meta); // no udta — meta is directly inside moov
const buf = new Uint8Array(moov);
const track = parseMp4(buf);
expect(track.title).toBe("FallbackTitle");
expect(track.format).toBe("mp4");
});
it("handles udta with no meta inside (metaInUdta is null)", {
timeout: 5000,
}, () => {
// moov/udta with no meta inside — metaInUdta=null → else path of if(metaInUdta)
const udta = boxBytes("udta", []); // udta with no meta child
const moov = boxBytes("moov", udta);
const buf = new Uint8Array(moov);
const track = parseMp4(buf);
// No meta anywhere → format: "mp4" (no ilst found)
expect(track.format).toBe("mp4");
});
it("handles ©nam item that resolves to empty string (val falsy — else path)", {
timeout: 5000,
}, () => {
// A data box with only null bytes — readItmText returns "" → val is falsy
const emptyDataBox = boxBytes("data", [
0x00,
0x00,
0x00,
0x01, // flags: text
0x00,
0x00,
0x00,
0x00, // locale
0x00, // null byte content → trims to ""
]);
const emptyItem = boxBytes("\u00a9nam", emptyDataBox);
const buf = buildMinimalMp4WithIlst(emptyItem);
const track = parseMp4(buf);
expect(track.title).toBeUndefined();
});
it("handles ©ART, ©alb, aART items resolving to empty (val falsy)", {
timeout: 5000,
}, () => {
const emptyDataBox = boxBytes(
"data",
[0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00]
);
const items = [
...boxBytes("\u00a9ART", emptyDataBox),
...boxBytes("\u00a9alb", emptyDataBox),
...boxBytes("aART", emptyDataBox),
];
const buf = buildMinimalMp4WithIlst(items);
const track = parseMp4(buf);
expect(track.artist).toBeUndefined();
expect(track.album).toBeUndefined();
expect(track.albumArtist).toBeUndefined();
});
it("handles ©day with non-numeric year (NaN → year not set)", {
timeout: 5000,
}, () => {
const buf = buildMinimalMp4WithIlst(
itemBox("\u00a9day", "unknwn-01-01")
);
const track = parseMp4(buf);
expect(track.year).toBeUndefined();
});
it("handles ©day item with empty text (val falsy — else path)", {
timeout: 5000,
}, () => {
const emptyDataBox = boxBytes(
"data",
[0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00]
);
const buf = buildMinimalMp4WithIlst(
boxBytes("\u00a9day", emptyDataBox)
);
const track = parseMp4(buf);
expect(track.year).toBeUndefined();
});
it("handles disk with num=0 and total=0 (else paths of discNumber/totalDiscs)", {
timeout: 5000,
}, () => {
// disk data with 0x00 for both num and total
const diskData = boxBytes("data", [
0x00,
0x00,
0x00,
0x00, // flags
0x00,
0x00,
0x00,
0x00, // locale
0x00,
0x00, // padding
0x00,
0x00, // num = 0
0x00,
0x00, // total = 0
]);
const diskBox = boxBytes("disk", diskData);
const buf = buildMinimalMp4WithIlst(diskBox);
const track = parseMp4(buf);
// num=0 → discNumber not set; total=0 → totalDiscs not set
expect(track.discNumber).toBeUndefined();
expect(track.totalDiscs).toBeUndefined();
});
it("handles gnre with index out of range (idx > GENRES.length — else path)", {
timeout: 5000,
}, () => {
// idx = 999 > GENRES.length (20) → if(genreName) is FALSE
const gnreData = boxBytes("data", [
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x03,
0xe7, // index: 999
]);
const gnreBox = boxBytes("gnre", gnreData);
const buf = buildMinimalMp4WithIlst(gnreBox);
const track = parseMp4(buf);
expect(track.genre).toBeUndefined();
});
it("handles gnre where readItmData returns null (data && data.length >= 2 is false)", {
timeout: 5000,
}, () => {
// gnre item with no data child → readItmData returns null → `if (data && ...)` FALSE
const gnreBoxNoData = boxBytes("gnre", []); // no data child
const buf = buildMinimalMp4WithIlst(gnreBoxNoData);
const track = parseMp4(buf);
expect(track.genre).toBeUndefined();
});
it("handles ©gen, ©cmt, ©lyr items resolving to empty (val falsy)", {
timeout: 5000,
}, () => {
const emptyDataBox = boxBytes(
"data",
[0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00]
);
const items = [
...boxBytes("\u00a9gen", emptyDataBox),
...boxBytes("\u00a9cmt", emptyDataBox),
...boxBytes("\u00a9lyr", emptyDataBox),
];
const buf = buildMinimalMp4WithIlst(items);
const track = parseMp4(buf);
expect(track.genre).toBeUndefined();
expect(track.comment).toBeUndefined();
expect(track.lyrics).toBeUndefined();
});
it("handles covr with oversized image (exceeds maxCoverSize)", {
timeout: 5000,
}, () => {
// maxCoverSize = 4, but img has 4 bytes — exactly at limit, not over
// To exceed: make img 5 bytes with maxCoverSize 4
const imgBytes = [0xff, 0xd8, 0xff, 0xe0, 0x00]; // 5 bytes
const covrData = boxBytes("data", [
0x00,
0x00,
0x00,
0x0d, // JPEG flag
0x00,
0x00,
0x00,
0x00,
...imgBytes,
]);
const covrBox = boxBytes("covr", covrData);
const buf = buildMinimalMp4WithIlst(covrBox);
const track = parseMp4(buf, { skipCovers: false, maxCoverSize: 4 });
expect(track.cover).toBeUndefined();
});
});