feat: port EOT container parsing + fix libeot-parity bugs (v1.5.0) - #5
Merged
Conversation
…y checks) Port-parity fixes found by diffing against libeot (MPL 2.0) and the W3C MTX spec: - ctf-parser: unpackCVT read the leading U16 as a byte count and halved every hinted font's Control Value Table. The MTX spec (§5.2 "USHORT numEntries") and libeot's unpackCVT both treat it as an entry count. Fixed; the existing test was self-referential (built the fixture under the same wrong assumption) and now asserts the correct 3-entry / 6-byte result. - lzcomp: the 4 MiB MAX_OUT_LEN cap rejected legal fonts. out_len is a 24-bit header field, so 2^24-1 is the real bound; raised to match, since the field width already bounds allocation. RLE growth cap raised accordingly. - bitio: clamp `size` to the buffer length so an over-large size can't read past the end and silently yield zero bits; don't mutate bitCount before the end-of-data throw so a caught error leaves the reader consistent. - ctf-parser: restore libeot's structural checks that had been dropped — missing maxp/head/hmtx and a head table < 12 bytes now raise errors instead of decoding a silently-empty or malformed font; corrupt 0xFB/0xFC hop codes raise instead of injecting a literal 251 into the instruction stream. - errors: add a machine-discriminable EotError/EotErrorCode mirroring libeot's EOTError enum, exported from the package entry point. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ports libeot's EOT.c (~310 lines), the one significant module the original port skipped — the library previously began one step after libeot's entry point, forcing callers to guess where the font data starts and whether it is compressed/encrypted. New public API (exported from index.ts): - parseEotMetadata(bytes): full little-endian EOT header parse, including the 0x504C magic check, UTF-16LE name strings, version 2 root string and version 3 EUDC trailer, and the version-retry loop that copes with files whose declared version disagrees with their layout (sets metadata.badVersion instead of failing, mirroring EOT_WARN_BAD_VERSION). - eotToTtf(bytes): parse -> slice font data -> decompressEotFont; the drop-in equivalent of libeot's EOT2ttf_* entry points. - canLegallyEdit(metadata): fsType embedding-permission check. The demo now calls parseEotMetadata instead of its own tail-guessing heuristic (which never validated the magic number), and reports the font name/version. The header bounds check uses `>` where libeot's EOT_ENSURE_SCANNER macro used `>=`; the latter is an off-by-one that rejects the exact-fit case, and libeot's own string/array helpers already use `>`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
README now leads with eotToTtf (whole .eot -> .ttf) and documents parseEotMetadata / canLegallyEdit / EotError. Bump to 1.5.0 for the new public API surface. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This was referenced Jul 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Reviewed the port against upstream libeot (MPL-2.0) and the W3C MTX spec. This fixes several correctness bugs the port carried and adds the one significant module it had skipped.
Correctness fixes
unpackCVTread the leadingU16as a byte count; the MTX spec (§5.2, "USHORT numEntries — Number of cvt entries") and libeot'sunpackCVTboth treat it as an entry count. Every hinted font's Control Value Table was reconstructed at half length. The existing test passed only because its fixture was built under the same wrong assumption — now corrected and turned into a regression guard.out_lenis a 24-bit header field, so the real ceiling is2^24 − 1(~16 MiB). The field width already bounds allocation; the 4 MiB cap only blocked valid large (e.g. CJK) fonts. Raised to the true bound.sizeis now clamped to the buffer length (an over-largesizepreviously read past the end and silently yielded zero bits), andbitCountis no longer mutated before the end-of-data throw (a caught error left the reader returning a stale bit).maxp/head/hmtxand aheadtable under 12 bytes now raise errors instead of decoding a silently-empty or malformed font; corrupt0xFB/0xFChop codes raise instead of injecting a literal251into the instruction stream.New: EOT container parsing
Ports libeot's
EOT.c(~310 lines) — the library previously began after libeot's entry point, so callers had to guess the font-data offset and the compressed/encrypted flags (the demo used an unexported tail-guessing heuristic that never checked the magic number).eotToTtf(bytes)— whole.eot→.ttfin one call.parseEotMetadata(bytes)— full little-endian header parse:0x504Cmagic, UTF-16LE name strings, v2 root string, v3 EUDC trailer, and the version-retry loop for files whose declared version disagrees with their layout (badVersionflag rather than failure).canLegallyEdit(metadata)—fsTypeembedding-permission check.EotError/EotErrorCode— machine-discriminable errors mirroring libeot'sEOTErrorenum.The demo now uses
parseEotMetadata; the README leads witheotToTtf.Verification
bun run typecheck,bun run build(ESM + CJS + .d.ts), demo bundle — all passbun run test— 209 passing (up from 190; +19 for the new EOT parser and regression guards)dist/:eotToTtfround-trips font data and surfacesEotErrorwith codesBumps to 1.5.0 (new public API). Merging to
mainwill publish to npm via the existing workflow.🤖 Generated with Claude Code