671 lines
22 KiB
TypeScript
671 lines
22 KiB
TypeScript
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();
|
|
});
|
|
});
|