zero_width_strip/lib.rs
1//! # zero-width-strip
2//!
3//! Strip zero-width and bidi-control Unicode characters from text.
4//!
5//! Zero-width characters (U+200B–U+200F, U+2060, U+FEFF, etc.) and bidi
6//! overrides (U+202A–U+202E) are invisible in most renderers but are
7//! preserved by most tokenizers, which makes them a clean payload
8//! channel for prompt-injection attacks ("invisible instructions"
9//! hidden inside otherwise plain text).
10//!
11//! This crate strips them.
12//!
13//! ## Example
14//!
15//! ```
16//! use zero_width_strip::{strip, has_invisible};
17//! let dirty = "hello\u{200B}\u{202E}world";
18//! assert!(has_invisible(dirty));
19//! assert_eq!(strip(dirty), "helloworld");
20//! ```
21
22#![deny(missing_docs)]
23
24/// True when the input contains any zero-width or bidi-override char.
25pub fn has_invisible(s: &str) -> bool {
26 s.chars().any(is_invisible)
27}
28
29/// Return a copy of `s` with every zero-width / bidi-override char
30/// removed.
31pub fn strip(s: &str) -> String {
32 s.chars().filter(|c| !is_invisible(*c)).collect()
33}
34
35/// Strip into a caller-provided buffer (avoids an allocation).
36pub fn strip_into(s: &str, out: &mut String) {
37 out.reserve(s.len());
38 for c in s.chars() {
39 if !is_invisible(c) {
40 out.push(c);
41 }
42 }
43}
44
45/// Per-char test. The list covers:
46/// - U+200B…U+200F (zero-width space, ZWNJ, ZWJ, LRM, RLM)
47/// - U+202A…U+202E (LRE, RLE, PDF, LRO, RLO — bidi controls)
48/// - U+2060…U+2064 (word joiner + invisible math operators)
49/// - U+2066…U+2069 (LRI, RLI, FSI, PDI — bidi isolates)
50/// - U+FEFF (BOM / zero-width no-break space)
51/// - U+180E (Mongolian vowel separator)
52fn is_invisible(c: char) -> bool {
53 matches!(c as u32,
54 0x200B..=0x200F
55 | 0x202A..=0x202E
56 | 0x2060..=0x2064
57 | 0x2066..=0x2069
58 | 0x180E
59 | 0xFEFF
60 )
61}