Changelog: 0.9.x¶
Archived verbatim from CHANGELOG.md. Newer releases are in the changelog; the archive index lists every older series.
[0.9.1] — 2026-06-13¶
Added¶
strip_log_injection(text, *, replacement='\ufffd', keep_tab=False)(#307). A stateless, character-level encoder that makes untrusted text safe to write as a log line: it replaces CR/LF/NEL/LS/PS (record forging), NUL/C0/C1 controls (parser corruption), and ESC/DEL (terminal hijack via ANSI escapes) withreplacement(default U+FFFD).\tis neutralized by default (keep_tab=False) to block TSV/logfmt column injection. Idempotent; ASCII-clean fast-path returns the original object; never emits a raw CR/LF/ESC. It owns the log-record and operator-terminal sinks but makes no HTML-log-viewer-safety claim (that is stored XSS — encode at the viewer withescape_html) and is not a log4shell defense (see Threat Model).escape_html(text)andpercent_encode(text, *, component)output encoders (#311). Standalone terminal encoders applied at the output sink — deliberately notTextPipeline/PROFILESsteps (a pipeline is context-free; baking encoding in invites double-encoding and wrong-context escaping).escape_htmlescapes the five HTML metacharacters for element/quoted-attribute context (ASCII fast-path returns the original object; not idempotent by design).percent_encodedoes RFC 3986 percent-encoding for a requiredComponent(PATH/SEGMENT/QUERY/FORM; UTF-8 byte-based, ASCII output,FORMuses space→+). Both are mechanism-named and carry the #306 scope-boundary discipline: they are the narrow, context-pinned exception to "disarm is not an output sanitizer," not a general XSS/injection defense (see Threat Model).
Changed (breaking)¶
- Renamed
is_safe_hostname()→is_suspicious_hostname()and inverted its boolean. The old name asserted a safety it cannot guarantee —safe=Trueonly meant "no mixed-script label and no bundled-table confusable found," yet whole-script spoofs and out-of-table confusables still returnedsafe=True(the false-assurance pattern #306/#308/#309 removed elsewhere, but as a literalsafeboolean a caller branches on). The function now returns(suspicious, analysis)wheresuspicious=Truemeans a problem was detected; the result structSafeHostnameDetails→HostnameAnalysis, fieldsafe→suspicious(inverted). The granularscripts/mixed_script/has_confusables/canonicalfields are unchanged. No alias — invert call sites:safe, d = is_safe_hostname(h)→suspicious, a = is_suspicious_hostname(h). (#313) - Renamed policy profile
web_input_sanitize→normalize_web_input. Follows thesanitize_user_input → normalize_user_inputrename: "sanitize" wrongly implied output/injection safety, and was especially misleading here because this profile is lighter thannormalize_user_input()(NFKC + confusables only; no bidi/zero-width/control/zalgo stripping). Useget_pipeline("normalize_web_input"). No alias is kept. - Renamed
sanitize_user_input()→normalize_user_input(). The old name implied output sanitization (injection safety); this preset performs input Unicode normalization only and is not an XSS/SQL defense (see Threat Model). ThePRESETSregistry key changes to match ("normalize_user_input"). No alias is kept — update call sites directly.
Documentation¶
- Stated the XSS/injection scope boundary explicitly (#306): README, the docs site, and THREAT_MODEL now say plainly that disarm normalizes input and is not an output sanitizer — it performs no HTML/JS/SQL/shell escaping and never replaces context-aware output encoding at the sink (NFKC can even surface ASCII metacharacters from fullwidth lookalikes). This boundary is the conceptual basis for the renames and the new output encoders in this release.
Security¶
- Supply-chain hardening (#260): added
cargo deny(license allow-list, banned/wildcard crates, crates.io-only sources viadeny.toml) to the required Rust checks passed gate, alongside the existingcargo audit. Releases now attach a CycloneDX SBOM (*.cdx.json) of the Rust dependency graph, and PyPI distributions carry PEP 740 build-provenance attestations via OIDC Trusted Publishing. Verification is documented in SECURITY.md. - Bumped
pyo30.24 → 0.29, resolving two upstream advisories:GHSA-36hh-v3qg-5jq4(HIGH — out-of-bounds read innth/nth_backforPyList/PyTupleiterators) andGHSA-chgr-c6px-7xpp(MEDIUM — missingSyncbound onPyCFunction::new_closureclosures). Includes the binding-layer API migration the bump requires (GILwith_gil/allow_threads→attach/detach,PyObject→Py<PyAny>,downcast_exact→cast_exact); no functional change to any transform. (#315)
Internal¶
- Docs: build the MkDocs site in CI and deploy to Cloudflare Pages (served at the unchanged
docs.disarm.dev), replacing the Read the Docs trigger.mkdocs build --strictruns in GitHub Actions (Python-only — mkdocstrings parses source statically); push tomaindeploys production, PRs get preview deploys. Legacy/en/latest/*URLs 301 to root viadocs/_redirects. Removed.readthedocs.yamlandRTD_TOKEN. (#314) - CI: replaced the custom
conversations-resolved.ymlworkflow with GitHub's native Require conversation resolution before merging branch-protection setting. The bespoke "Conversations resolved" status check (#55) was flaky — stale check runs lingered after threads were resolved and blocked otherwise-green PRs. Behavior is unchanged (unresolved review threads still block merge), now enforced by the built-in gate instead of a workflow + required status check.
[0.9.0] — 2026-06-11¶
The first release under the disarm name — the continuation of translit-rs
(last released as 0.8.1). See #264 for the rename rationale. The 0.0.0 entries
on PyPI / crates.io / npm are name-reservation placeholders, not releases; 0.9.0
is the first functional disarm release.
Changed¶
- Renamed the project from
translittodisarm(#264). This unifies the distribution and import names under a singledisarm: - PyPI distribution
translit-rs→disarm;import translit→import disarm. - Native module
translit._translit→disarm._disarm; cratetranslit→disarm. - Console script
translit→disarm. - Breaking: the public base exception
TranslitError→DisarmError(the subclassesInvalidArgumentError/ResourceLimitError/UnsupportedErrorkeep their names).DisarmErrorremains aValueErrorsubclass, soexcept ValueErrorkeeps working. - Breaking: the context-dictionary environment variable
TRANSLIT_DICT_DIR→DISARM_DICT_DIR. - Canonical URLs moved to
https://disarm.dev/https://docs.disarm.dev; the repository moved tohttps://github.com/raeq/disarm.
Fixed¶
uv.locknow declaresrequires-python = ">=3.10", matchingpyproject.toml(it had drifted to>=3.9after the 3.10 floor landed in #277).