- Every zstd frame is decoded; anything but tar padding after the archive is refused.
- pack.toml must name the index entry's locale; the files the index promises (spelling, hyphenation) must be in the archive.
- Allow-list: pack.toml, spelling.aff, spelling.dic, hyphenation.pat at the root, LICENCES/<[A-Za-z0-9._-]> without a leading or trailing dot; Windows device names (con, prn, aux, nul, com1-9, lpt1-9, any extension) refused in names and locales.
- Pack names lose control and bidi/format (Cf) characters before they are stored or listed.
- An unreadable language-packs.json is kept as .bad and rebuilt from the pack folders (each pack.toml); no pack drops out.
- A bad hand-made entry is Error::Index(IndexError::Invalid), not BadUrl; index parse errors are Index errors too.
- Dead file:// handling removed.
- language_pack_index_overridden() exposes the developer override; README says it bypasses the pin and works in release builds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
Additive: new src/language_packs.rs (+ tests); store.rs only widens a few helpers to pub(crate).
Developer override CRAFT_LANGUAGE_PACK_INDEX for local end-to-end testing (no index pin; archives
still verified).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2