218 lines
7.5 KiB
TypeScript
218 lines
7.5 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
decodeEmbedding,
|
|
extractCollectionFace,
|
|
inspectCollection,
|
|
inspectDirectory,
|
|
inspectWoff2Directory,
|
|
inspectWoff2Header,
|
|
} from "../../src/core/sfnt";
|
|
|
|
describe("SFNT directory validation", () => {
|
|
it("inventories bounded TrueType tables", () => {
|
|
const report = inspectDirectory(sfnt(["head", "cmap", "OS/2"]));
|
|
expect(report.container).toBe("truetype");
|
|
expect(report.tables.map((table) => table.tag)).toEqual([
|
|
"head",
|
|
"cmap",
|
|
"OS/2",
|
|
]);
|
|
expect(report.tables[2]?.description).toContain("embedding");
|
|
});
|
|
|
|
it("rejects unsupported and out-of-bounds containers before library parsing", () => {
|
|
expect(() => inspectDirectory(signature("wOF2"))).toThrow(/WOFF2/u);
|
|
const invalid = sfnt(["head"]),
|
|
view = new DataView(invalid);
|
|
view.setUint32(12 + 8, invalid.byteLength - 2, false);
|
|
view.setUint32(12 + 12, 12, false);
|
|
expect(() => inspectDirectory(invalid)).toThrow(/file boundary/u);
|
|
});
|
|
|
|
it("validates WOFF2 container sizes before decompression", () => {
|
|
const buffer = new ArrayBuffer(52),
|
|
bytes = new Uint8Array(buffer),
|
|
view = new DataView(buffer);
|
|
bytes.set(new TextEncoder().encode("wOF2"), 0);
|
|
bytes.set(new TextEncoder().encode("\u0000\u0001\u0000\u0000"), 4);
|
|
view.setUint32(8, buffer.byteLength, false);
|
|
view.setUint16(12, 1, false);
|
|
view.setUint32(16, 32, false);
|
|
view.setUint32(20, 4, false);
|
|
expect(inspectWoff2Header(buffer)).toEqual({
|
|
declaredSize: 52,
|
|
expandedSize: 32,
|
|
count: 1,
|
|
});
|
|
view.setUint32(16, 70 * 1024 * 1024, false);
|
|
expect(() => inspectWoff2Header(buffer)).toThrow(/64 MiB/u);
|
|
});
|
|
|
|
it("rejects reserved WOFF2 transform versions before decoding", () => {
|
|
expect(() => inspectWoff2Directory(woff2Table(5, 1))).toThrow(
|
|
/reserved transform version/u,
|
|
);
|
|
expect(() => inspectWoff2Directory(woff2Table(3, 2))).toThrow(
|
|
/invalid transform version/u,
|
|
);
|
|
expect(inspectWoff2Directory(woff2Table(5, 0)).tables[0]).toMatchObject({
|
|
tag: "name",
|
|
storedLength: 4,
|
|
});
|
|
expect(inspectWoff2Directory(woff2Table(3, 1)).tables[0]).toMatchObject({
|
|
tag: "hmtx",
|
|
storedLength: 4,
|
|
});
|
|
});
|
|
|
|
it("validates TTC face offsets and reconstructs a standalone selected face", () => {
|
|
const source = ttc([
|
|
sfnt(["head", "cmap"]),
|
|
sfnt(["head", "name", "CFF "]),
|
|
]),
|
|
collection = inspectCollection(source);
|
|
expect(collection).toMatchObject({
|
|
version: "1.0",
|
|
hasDigitalSignature: false,
|
|
});
|
|
expect(collection.faces.map((face) => face.directory.flavor)).toEqual([
|
|
"TrueType outlines",
|
|
"TrueType outlines",
|
|
]);
|
|
const extracted = extractCollectionFace(source, 1),
|
|
report = inspectDirectory(extracted);
|
|
expect(report.tables.map((table) => table.tag)).toEqual([
|
|
"head",
|
|
"name",
|
|
"CFF ",
|
|
]);
|
|
expect(
|
|
report.tables.every((table) => table.offset < extracted.byteLength),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("rejects malformed TTC versions, counts, duplicate and unaligned offsets", () => {
|
|
const invalidVersion = ttc([sfnt(["head"])]),
|
|
invalidCount = ttc([sfnt(["head"])]),
|
|
duplicate = ttc([sfnt(["head"]), sfnt(["head"])]),
|
|
unaligned = ttc([sfnt(["head"])]);
|
|
new DataView(invalidVersion).setUint32(4, 0x0003_0000, false);
|
|
expect(() => inspectCollection(invalidVersion)).toThrow(/version/u);
|
|
new DataView(invalidCount).setUint32(8, 65, false);
|
|
expect(() => inspectCollection(invalidCount)).toThrow(/64-face/u);
|
|
const duplicateView = new DataView(duplicate);
|
|
duplicateView.setUint32(16, duplicateView.getUint32(12, false), false);
|
|
expect(() => inspectCollection(duplicate)).toThrow(/duplicate/u);
|
|
new DataView(unaligned).setUint32(12, 17, false);
|
|
expect(() => inspectCollection(unaligned)).toThrow(/aligned/u);
|
|
});
|
|
|
|
it("validates the optional TTC 2.0 DSIG boundary", () => {
|
|
const source = ttc([sfnt(["head"])], 0x0002_0000),
|
|
headerEnd = 12 + 4;
|
|
expect(inspectCollection(source).version).toBe("2.0");
|
|
const view = new DataView(source);
|
|
view.setUint32(headerEnd, 0x4453_4947, false);
|
|
expect(() => inspectCollection(source)).toThrow(/incomplete/u);
|
|
});
|
|
});
|
|
|
|
describe("embedding flags", () => {
|
|
it("decodes and enforces subsetting restrictions", () => {
|
|
expect(decodeEmbedding(0).subsetAllowed).toBe(true);
|
|
expect(decodeEmbedding(0x0002).level).toBe("restricted");
|
|
expect(decodeEmbedding(0x0100)).toMatchObject({
|
|
noSubsetting: true,
|
|
subsetAllowed: false,
|
|
});
|
|
expect(decodeEmbedding(0x0208)).toMatchObject({
|
|
level: "editable",
|
|
bitmapOnly: true,
|
|
subsetAllowed: false,
|
|
});
|
|
});
|
|
});
|
|
|
|
function signature(value: string) {
|
|
const bytes = new Uint8Array(4);
|
|
[...value].forEach(
|
|
(character, index) => (bytes[index] = character.charCodeAt(0)),
|
|
);
|
|
return bytes.buffer;
|
|
}
|
|
|
|
function woff2Table(tagIndex: number, transformVersion: number) {
|
|
const glyfOrLoca = tagIndex === 10 || tagIndex === 11,
|
|
declaresTransform = glyfOrLoca
|
|
? transformVersion === 0
|
|
: transformVersion !== 0,
|
|
directoryBytes = declaresTransform ? 3 : 2,
|
|
buffer = new ArrayBuffer(48 + directoryBytes + 1),
|
|
bytes = new Uint8Array(buffer),
|
|
view = new DataView(buffer);
|
|
bytes.set(new TextEncoder().encode("wOF2"), 0);
|
|
view.setUint32(4, 0x0001_0000, false);
|
|
view.setUint32(8, buffer.byteLength, false);
|
|
view.setUint16(12, 1, false);
|
|
view.setUint32(16, 32, false);
|
|
view.setUint32(20, 1, false);
|
|
bytes[48] = (transformVersion << 6) | tagIndex;
|
|
bytes[49] = 4;
|
|
if (declaresTransform) bytes[50] = 4;
|
|
return buffer;
|
|
}
|
|
|
|
function sfnt(tags: string[]) {
|
|
const directoryBytes = 12 + tags.length * 16,
|
|
tableBytes = 12,
|
|
buffer = new ArrayBuffer(directoryBytes + tags.length * tableBytes),
|
|
view = new DataView(buffer);
|
|
view.setUint32(0, 0x0001_0000, false);
|
|
view.setUint16(4, tags.length, false);
|
|
tags.forEach((tag, index) => {
|
|
const base = 12 + index * 16;
|
|
[...tag].forEach((character, offset) =>
|
|
view.setUint8(base + offset, character.charCodeAt(0)),
|
|
);
|
|
view.setUint32(base + 4, index + 1, false);
|
|
view.setUint32(base + 8, directoryBytes + index * tableBytes, false);
|
|
view.setUint32(base + 12, tableBytes, false);
|
|
});
|
|
return buffer;
|
|
}
|
|
|
|
function ttc(fonts: ArrayBuffer[], version = 0x0001_0000) {
|
|
const extra = version === 0x0002_0000 ? 12 : 0,
|
|
headerSize = 12 + fonts.length * 4 + extra,
|
|
offsets: number[] = [];
|
|
let size = align4(headerSize);
|
|
for (const font of fonts) {
|
|
offsets.push(size);
|
|
size += align4(font.byteLength);
|
|
}
|
|
const buffer = new ArrayBuffer(size),
|
|
output = new Uint8Array(buffer),
|
|
view = new DataView(buffer);
|
|
output.set(new TextEncoder().encode("ttcf"), 0);
|
|
view.setUint32(4, version, false);
|
|
view.setUint32(8, fonts.length, false);
|
|
fonts.forEach((font, index) => {
|
|
const faceOffset = offsets[index]!,
|
|
source = new Uint8Array(font),
|
|
sourceView = new DataView(font);
|
|
view.setUint32(12 + index * 4, faceOffset, false);
|
|
output.set(source, faceOffset);
|
|
const tableCount = sourceView.getUint16(4, false);
|
|
for (let tableIndex = 0; tableIndex < tableCount; tableIndex += 1) {
|
|
const recordOffset = faceOffset + 12 + tableIndex * 16,
|
|
sourceOffset = sourceView.getUint32(12 + tableIndex * 16 + 8, false);
|
|
view.setUint32(recordOffset + 8, faceOffset + sourceOffset, false);
|
|
}
|
|
});
|
|
return buffer;
|
|
}
|
|
|
|
function align4(value: number) {
|
|
return (value + 3) & ~3;
|
|
}
|