/** * Client-side validation for {@link setFormFont} arguments. Mirrors the * Java FormFontValidator and .NET FormFontValidator added for PDF-2184. * * The engine validates again on receipt — this client check is the * fail-fast layer that surfaces obvious mistakes before any network I/O. */ const MAX_NAME_LENGTH = 63; const MAX_FONT_BYTES = 50 * 1024 * 1024; // 50 MB const ALLOWED_NAME = /^[A-Za-z0-9_\-.+]+$/; /** * Validate a PDF font name. Allowed characters: letters, digits, '_', '-', * '.', '+'. The '+' is permitted to accept subset prefixes like * "AAAAAA+Poppins-Regular". Names must be 1-63 characters. * * @throws Error when the name fails validation. */ export function validateFormFontName(fontName: string): void { if (fontName == null || typeof fontName !== "string" || fontName.length === 0) { throw new Error("fontName must be a non-empty string"); } if (fontName.length > MAX_NAME_LENGTH) { throw new Error( `fontName must be at most ${MAX_NAME_LENGTH} characters (got ${fontName.length})` ); } if (!ALLOWED_NAME.test(fontName)) { throw new Error( "fontName may only contain letters, digits, and the characters '_', '-', '.', '+'" ); } } /** * Validate raw font bytes look like a TrueType / OpenType / TTC sfnt * container. Catches the obvious cases: empty payload, oversize payload, * ASCII text passed instead of bytes, and all-zero payloads. * * @throws Error when the data fails validation. */ export function validateFormFontData(fontData: Buffer): void { if (!fontData || fontData.length === 0) { throw new Error("fontData must not be empty"); } if (fontData.length > MAX_FONT_BYTES) { throw new Error( `fontData exceeds the ${MAX_FONT_BYTES} byte limit (got ${fontData.length})` ); } if (fontData.length < 4) { throw new Error("fontData is too small to be a valid font file"); } // Reject obviously-non-font payloads: all-zero buffer or a pure ASCII text // payload. We don't try to fully parse the sfnt header here — the engine // does that — but we do catch the common mistakes. let nonZero = false; let asciiOnly = true; for (let i = 0; i < Math.min(fontData.length, 4096); i++) { const b = fontData[i] ?? 0; if (b !== 0) nonZero = true; if (b > 0x7e || (b < 0x20 && b !== 0x09 && b !== 0x0a && b !== 0x0d)) { asciiOnly = false; } } if (!nonZero) { throw new Error("fontData is all zeros — not a valid font file"); } if (asciiOnly) { throw new Error("fontData looks like ASCII text — not a valid font file"); } // sfnt magic — TTF (0x00010000), OTF ('OTTO'), TTC ('ttcf'), or 'true'. const m0 = fontData[0], m1 = fontData[1], m2 = fontData[2], m3 = fontData[3]; const isTtfV1 = m0 === 0x00 && m1 === 0x01 && m2 === 0x00 && m3 === 0x00; const isOtto = m0 === 0x4f && m1 === 0x54 && m2 === 0x54 && m3 === 0x4f; const isTtcf = m0 === 0x74 && m1 === 0x74 && m2 === 0x63 && m3 === 0x66; const isTrue = m0 === 0x74 && m1 === 0x72 && m2 === 0x75 && m3 === 0x65; if (!(isTtfV1 || isOtto || isTtcf || isTrue)) { throw new Error( "fontData does not have a recognised TTF/OTF/TTC sfnt magic" ); } }