feat: 建立可选字体包协议与安全注册器

This commit is contained in:
SkyJourney
2026-07-31 20:36:22 +08:00
parent e5da699ea3
commit c76454be9d
14 changed files with 1083 additions and 4 deletions
@@ -0,0 +1,75 @@
import { createHash } from "node:crypto";
import { mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
export function hash(content: Uint8Array | string) {
return createHash("sha256").update(content).digest("hex");
}
export async function writeFontPack(
root: string,
options: {
id?: string;
version?: string;
minimumAppVersion?: string;
maximumAppVersionExclusive?: string;
target?: string;
web?: Uint8Array;
docx?: Uint8Array;
corruptWebHash?: boolean;
} = {}
) {
const id = options.id ?? "official-cjk";
const version = options.version ?? "1.0.0";
const directory = join(root, id, version);
const web = options.web ?? new Uint8Array([1, 2, 3]);
const docx = options.docx ?? new Uint8Array([4, 5, 6, 7]);
const license = "SIL Open Font License 1.1";
await mkdir(join(directory, "fonts"), { recursive: true });
await writeFile(join(directory, "fonts", "serif.woff2"), web);
await writeFile(join(directory, "fonts", "serif.ttf"), docx);
await writeFile(join(directory, "OFL.txt"), license, "utf8");
const maximum = options.maximumAppVersionExclusive
? { maximumAppVersionExclusive: options.maximumAppVersionExclusive }
: {};
await writeFile(
join(directory, "font-pack.json"),
JSON.stringify({
manifestVersion: 1,
id,
version,
name: "原生中文字体包",
description: "测试字体包",
license: "OFL-1.1",
licenseFile: "OFL.txt",
licenseSha256: hash(license),
licenseBytes: Buffer.byteLength(license),
compatibility: {
minimumAppVersion: options.minimumAppVersion ?? "0.6.0",
...maximum
},
faces: [
{
id: "serif-regular",
targets: [options.target ?? "Mdpdf Fandol Song", "FandolSong"],
weight: 400,
style: "normal",
web: {
path: "fonts/serif.woff2",
format: "woff2",
bytes: web.byteLength,
sha256: options.corruptWebHash ? "0".repeat(64) : hash(web)
},
docx: {
path: "fonts/serif.ttf",
format: "truetype",
bytes: docx.byteLength,
sha256: hash(docx)
}
}
]
}),
"utf8"
);
return directory;
}
@@ -0,0 +1,92 @@
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
discoverFontPacks,
readInstalledFontPackAsset
} from "../src/index.js";
import { writeFontPack } from "./fixtures.js";
const temporaryDirectories: string[] = [];
async function temporaryDirectory() {
const directory = await mkdtemp(join(tmpdir(), "mdpdf-font-pack-"));
temporaryDirectories.push(directory);
return directory;
}
afterEach(async () => {
await Promise.all(
temporaryDirectories.splice(0).map((directory) =>
rm(directory, { recursive: true, force: true })
)
);
});
describe("字体包注册器", () => {
it("发现并验证兼容字体包,生成稳定指纹", async () => {
const root = await temporaryDirectory();
await writeFontPack(root);
const first = await discoverFontPacks({ roots: [root], appVersion: "0.6.0" });
const second = await discoverFontPacks({ roots: [root], appVersion: "0.6.0" });
expect(first.packs).toHaveLength(1);
expect(first.diagnostics).toEqual([]);
expect(first.fingerprint).toBe(second.fingerprint);
expect(await readInstalledFontPackAsset(first.packs[0]!.faces[0]!.docx)).toEqual(
new Uint8Array([4, 5, 6, 7])
);
});
it("同一根目录选择最高兼容版本并记录被覆盖版本", async () => {
const root = await temporaryDirectory();
await writeFontPack(root, { version: "1.0.0" });
await writeFontPack(root, { version: "1.2.0" });
const result = await discoverFontPacks({ roots: [root], appVersion: "0.6.0" });
expect(result.packs[0]?.version).toBe("1.2.0");
expect(result.diagnostics.map((item) => item.code)).toContain("PACK_SHADOWED");
});
it("多根目录优先使用靠前根目录中的包", async () => {
const firstRoot = await temporaryDirectory();
const secondRoot = await temporaryDirectory();
await writeFontPack(firstRoot, { version: "1.0.0" });
await writeFontPack(secondRoot, { version: "2.0.0" });
const result = await discoverFontPacks({
roots: [firstRoot, secondRoot],
appVersion: "0.6.0"
});
expect(result.packs[0]?.version).toBe("1.0.0");
});
it("隔离不兼容和哈希错误的字体包", async () => {
const root = await temporaryDirectory();
await writeFontPack(root, {
id: "future-pack",
minimumAppVersion: "0.7.0"
});
await writeFontPack(root, { id: "broken-pack", corruptWebHash: true });
const result = await discoverFontPacks({ roots: [root], appVersion: "0.6.0" });
expect(result.packs).toEqual([]);
expect(result.diagnostics.map((item) => item.code).sort()).toEqual([
"MANIFEST_INVALID",
"PACK_INCOMPATIBLE"
]);
});
it("读取时重新验证资源,拒绝注册后被替换的文件", async () => {
const root = await temporaryDirectory();
await writeFontPack(root);
const result = await discoverFontPacks({ roots: [root], appVersion: "0.6.0" });
const asset = result.packs[0]!.faces[0]!.web;
await writeFile(asset.absolutePath, new Uint8Array([9, 9, 9]));
await expect(readInstalledFontPackAsset(asset)).rejects.toThrow("SHA-256");
});
it("根目录缺失时返回信息诊断而不阻断", async () => {
const root = join(await temporaryDirectory(), "missing");
const result = await discoverFontPacks({ roots: [root], appVersion: "0.6.0" });
expect(result.packs).toEqual([]);
expect(result.diagnostics[0]?.code).toBe("ROOT_NOT_FOUND");
});
});
@@ -0,0 +1,63 @@
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
discoverFontPacks,
resolveFontPackFaces
} from "../src/index.js";
import { writeFontPack } from "./fixtures.js";
const temporaryDirectories: string[] = [];
afterEach(async () => {
await Promise.all(
temporaryDirectories.splice(0).map((directory) =>
rm(directory, { recursive: true, force: true })
)
);
});
describe("字体包候选解析", () => {
it("按家族别名、字重和样式精确匹配", async () => {
const root = await mkdtemp(join(tmpdir(), "mdpdf-font-resolve-"));
temporaryDirectories.push(root);
await writeFontPack(root);
const registry = await discoverFontPacks({ roots: [root], appVersion: "0.6.0" });
const result = resolveFontPackFaces(
[
{
family: "Theme Song",
aliases: ["FandolSong"],
weight: 400,
style: "normal"
},
{
family: "Theme Song",
aliases: ["FandolSong"],
weight: 700,
style: "normal"
}
],
registry.packs,
{ reportMissing: true }
);
expect(result.resolved[0]?.matchedFamily).toBe("FandolSong");
expect(result.unmatched).toHaveLength(1);
expect(result.diagnostics[0]?.code).toBe("FONT_FACE_NOT_PROVIDED");
});
it("允许调用方显式指定字体包优先级", async () => {
const root = await mkdtemp(join(tmpdir(), "mdpdf-font-priority-"));
temporaryDirectories.push(root);
await writeFontPack(root, { id: "alpha-pack" });
await writeFontPack(root, { id: "preferred-pack" });
const registry = await discoverFontPacks({ roots: [root], appVersion: "0.6.0" });
const result = resolveFontPackFaces(
[{ family: "FandolSong", weight: 400, style: "normal" }],
registry.packs,
{ preferredPackIds: ["preferred-pack"] }
);
expect(result.resolved[0]?.pack.id).toBe("preferred-pack");
});
});
@@ -0,0 +1,45 @@
import { describe, expect, it } from "vitest";
import {
compareSemanticVersions,
fontPackManifestSchema,
isAppVersionCompatible,
semanticVersionSchema
} from "../src/index.js";
describe("字体包协议", () => {
it("按 SemVer 规则比较版本并处理预发布版本", () => {
expect(compareSemanticVersions("1.0.0-beta.2", "1.0.0-beta.10")).toBeLessThan(0);
expect(compareSemanticVersions("1.0.0-rc.1", "1.0.0")).toBeLessThan(0);
expect(semanticVersionSchema.safeParse("1.0.0-01").success).toBe(false);
});
it("使用包含下限、不包含上限的应用兼容区间", () => {
const compatibility = {
minimumAppVersion: "0.6.0",
maximumAppVersionExclusive: "0.7.0"
};
expect(isAppVersionCompatible("0.6.0", compatibility)).toBe(true);
expect(isAppVersionCompatible("0.6.9", compatibility)).toBe(true);
expect(isAppVersionCompatible("0.7.0", compatibility)).toBe(false);
});
it("拒绝越界资源路径和倒置的兼容区间", () => {
const parsed = fontPackManifestSchema.safeParse({
manifestVersion: 1,
id: "official-cjk",
version: "1.0.0",
name: "测试",
description: "",
license: "OFL-1.1",
licenseFile: "../OFL.txt",
licenseSha256: "a".repeat(64),
licenseBytes: 10,
compatibility: {
minimumAppVersion: "0.7.0",
maximumAppVersionExclusive: "0.6.0"
},
faces: []
});
expect(parsed.success).toBe(false);
});
});