# disarm > A compiled-Rust toolkit that canonicalizes and neutralizes adversarial Unicode — > homoglyph spoofing, bidi / Trojan-Source, zalgo and invisible characters — before > it reaches classifiers, indexes, logs and identifiers. MIT licensed, with no GPL > anywhere in the dependency chain. Bindings for Python, Rust, Node, Ruby, Java and > Kotlin, plus a C ABI, all at the same version. Disambiguation: this is `disarm` the Unicode-security library at disarm.dev, on PyPI, crates.io, npm, RubyGems and Maven Central as `dev.disarm`. It is unrelated to the DISARM Foundation's influence-operations framework at disarm.foundation, and to the DiSARM malaria decision-support project. ## Tools Each runs the library compiled to WebAssembly, in the browser. No text is uploaded. - [Remove invisible characters](https://disarm.dev/tools/remove-invisible-characters/): Strips zero-width (U+200B, U+200C, U+200D, U+2060, U+FEFF), bidi controls (U+00AD, U+061C, U+200E, U+200F, U+202A–U+202E, U+2066–U+2069), tag characters (U+E0000–U+E007F), variation selectors, private-use codepoints, noncharacters and non-whitespace controls. Reports every removal by codepoint, class and count, because cleaned text looks identical to the input. Includes verified examples in seven languages. - [Detect Trojan Source](https://disarm.dev/tools/detect-trojan-source/): Finds bidirectional controls in source code — overrides and embeddings (U+202A–U+202E), isolates (U+2066–U+2069) and marks (U+200E, U+200F, U+061C, U+00AD) — reporting line and column for each, and showing rendered order against logical order. Separately flags a direction conflict caused by real right-to-left letters, which contains no control character to strip. That is CVE-2021-42572, and the two cases need different checks. - [Check confusable characters](https://disarm.dev/tools/check-confusable-characters/): Folds TR39 confusables and homoglyphs toward Latin under both of disarm's digit policies at once. They differ deliberately: `numeric` maps a non-Latin digit to the ASCII digit, which is what running text means, while `tr39` maps it to a Latin letter, which is what an identifier skeleton needs. `g००gle` folds to `g00gle` under numeric and `google` under tr39. Reports the mapping character by character and flags where the policies disagree. - [Detect zalgo text](https://disarm.dev/tools/detect-zalgo-text/): Shows combining-mark depth per base character and reports `is_zalgo` at every threshold. The point is where the count stops working: Vietnamese `ế` decomposes to U+0065 U+0302 U+0301, a base plus two marks, and a two-mark zalgo is the same shape, so no threshold separates them. A threshold of 1 rejects ordinary Vietnamese and Hebrew; 3 is disarm's detection default and 2 its capping default, because 2 is what Vietnamese needs. - [Detect script spoofing](https://disarm.dev/tools/detect-script-spoofing/): Analyses a hostname label by label for mixed-script and whole-script confusability. The point is that `suspicious` alone cannot decide: it is true both for an all-Cyrillic spelling of a Latin brand and for an ordinary Russian domain. `whole_script_confusable`, reported per label, is the discriminator — the spoof folds onto a Latin name and the real domain does not. Combined with the script of the TLD it expresses the usable policy: a label folding onto Latin under a Latin TLD is imitating something. - [Normalize Unicode whitespace](https://disarm.dev/tools/normalize-whitespace/): Folds every blank-rendering character to a single ASCII space, collapses runs and trims both ends, via `collapse_whitespace`. The distinction the page exists to make: a character can render as a gap without carrying the Unicode White_Space property. Braille Pattern Blank (U+2800), the Hangul fillers (U+115F, U+1160, U+3164) and the information separators (U+001C-U+001F) all render blank and are all missed by `str.isspace`, `\s`, `Character.isWhitespace` and `char::is_whitespace`. disarm folds by what renders blank, not by property. It deliberately folds rather than deletes, so `a\rb` becomes `a b` and never `ab`, and it deliberately leaves zero-width and control characters alone — those are `strip_zero_width_chars` and `strip_control_chars`. - [Sanitize a filename](https://disarm.dev/tools/sanitize-filename/): Converts an arbitrary string into a filename legal on Windows, macOS and Linux, via `sanitize_filename`. The cross-platform point: macOS stores filenames decomposed (NFD) and Windows and Linux store what they are given (usually NFC), so the same visible name is a different byte string depending on the machine it came from — `Tiếng Việt.doc` is an 18-byte name composed and 22 decomposed, and a Korean name can more than double. Measured here: NFC, NFD, NFKC and NFKD all sanitize to the same output, because transliteration to ASCII is the first pipeline step and byte-counting truncation therefore never sees the difference. Two orderings in the documented pipeline are load-bearing: transliteration before truncation (above), and reserved-name detection *after* transliteration — `áux.txt` is not a Windows device name until the accent is folded, and disarm returns `_aux.txt`. Platforms: `posix` forbids only `/` and NUL with no reserved names; `windows` adds `< > : " | ? *`, control characters and the device names; `universal` is the union and the default. - [Truncate text without breaking emoji](https://disarm.dev/tools/truncate-without-breaking-emoji/): Text has four lengths and they disagree, via `grapheme_len`, `grapheme_split`, `grapheme_truncate`, `terminal_width` and `grapheme_width`. The family emoji U+1F468 ZWJ U+1F469 ZWJ U+1F467 ZWJ U+1F466 is 25 bytes, 7 codepoints, 11 UTF-16 code units, 1 grapheme cluster and 2 terminal columns; all five numbers are correct and they answer different questions. Cutting by the wrong one corrupts: a codepoint slice at 1 keeps the father and discards the joiners and the rest of the family, a skin-tone modifier is dropped so a different person is depicted, and a flag loses its partner regional indicator. A byte slice can end mid-sequence and stop being valid UTF-8. The codepoint case is the dangerous one because the output is valid text that says something the author did not write. A cut is clean exactly when it lands on a boundary reported by `grapheme_split`. Note that cluster count is not column count: a CJK character is one cluster and two columns. - [Why don't these two strings match?](https://disarm.dev/tools/why-dont-these-strings-match/): Takes two strings and answers the question people actually arrive with. Shows a codepoint-aligned diff marking which codepoints belong to only one of them, then reports every disarm function that makes the two equal, narrowest first. The candidate list is not reasoned about: each is applied to both strings and the results compared, so the answer is what the library does rather than what it is documented to do. Typical answers: `normalize(NFC)` for an accent stored two ways, `strip_zero_width_chars` for a zero-width character inside a word, `collapse_whitespace` for a no-break space, `normalize_confusables` for letters from another script, `normalize(NFKC)` for fullwidth forms, `fold_case` for case. Prefer the narrowest when repairing a value; prefer a preset such as `search_key` when deriving a comparison key, where breadth is the point. - [Which cleanup do I need?](https://disarm.dev/tools/compare-presets/): Runs all eight presets against one string side by side — `strip_format`, `strip_obfuscation`, `canonicalize`, `canonicalize_strict`, `search_key`, `sort_key`, `catalog_key`, `ml_normalize` — because composition and ordering are disarm's actual argument and neither shows when a page demonstrates a single call. It also reports, per preset, which shell and markup metacharacters appear in the output that were not in the input. That is the canonicalize-before-validate rule made checkable: compatibility normalization maps fullwidth forms onto ASCII, so `<script>` becomes `