Skip to content

feat: port EOT container parsing + fix libeot-parity bugs (v1.5.0) - #5

Merged
ChristopherVR merged 3 commits into
mainfrom
feat/port-eot-and-fix-cvt
Jul 17, 2026
Merged

feat: port EOT container parsing + fix libeot-parity bugs (v1.5.0)#5
ChristopherVR merged 3 commits into
mainfrom
feat/port-eot-and-fix-cvt

Conversation

@ChristopherVR

Copy link
Copy Markdown
Owner

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

  • CVT table halved (critical). unpackCVT read the leading U16 as a byte count; the MTX spec (§5.2, "USHORT numEntries — Number of cvt entries") and libeot's unpackCVT both 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.
  • 4 MiB output cap rejected legal fonts. out_len is a 24-bit header field, so the real ceiling is 2^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.
  • BitIO edge cases. size is now clamped to the buffer length (an over-large size previously read past the end and silently yielded zero bits), and bitCount is no longer mutated before the end-of-data throw (a caught error left the reader returning a stale bit).
  • Dropped validity checks restored. Missing maxp/head/hmtx and a head table under 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.

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.ttf in one call.
  • parseEotMetadata(bytes) — full little-endian header parse: 0x504C magic, UTF-16LE name strings, v2 root string, v3 EUDC trailer, and the version-retry loop for files whose declared version disagrees with their layout (badVersion flag rather than failure).
  • canLegallyEdit(metadata)fsType embedding-permission check.
  • EotError / EotErrorCode — machine-discriminable errors mirroring libeot's EOTError enum.

The demo now uses parseEotMetadata; the README leads with eotToTtf.

Verification

  • bun run typecheck, bun run build (ESM + CJS + .d.ts), demo bundle — all pass
  • bun run test209 passing (up from 190; +19 for the new EOT parser and regression guards)
  • Runtime smoke test through the built dist/: eotToTtf round-trips font data and surfaces EotError with codes

Bumps to 1.5.0 (new public API). Merging to main will publish to npm via the existing workflow.

🤖 Generated with Claude Code

ChristopherVR and others added 3 commits July 18, 2026 06:40
…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>
@ChristopherVR
ChristopherVR merged commit 2afbab1 into main Jul 17, 2026
1 check passed
@ChristopherVR
ChristopherVR deleted the feat/port-eot-and-fix-cvt branch July 17, 2026 20:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant