rtl-flip-detect 0.1.0

Detect right-to-left override (U+202E) and other bidi-control characters that flip rendering of strings. Used in filename-spoof and prompt-injection attacks. Zero deps.
Documentation
//! # rtl-flip-detect
//!
//! Detect bidi-control characters that flip rendered direction.
//!
//! The classic attack: a filename `evil\u{202E}cod.exe` renders as
//! `evilexe.doc` because U+202E (RIGHT-TO-LEFT OVERRIDE) flips
//! everything after it. Same trick works inside any text the model
//! displays back, or a tool argument.
//!
//! This crate finds and strips those.
//!
//! Controls flagged:
//! - U+202A LRE, U+202B RLE, U+202D LRO, U+202E RLO
//! - U+202C PDF (pop directional formatting — could close an attacker's open)
//! - U+2066 LRI, U+2067 RLI, U+2068 FSI, U+2069 PDI
//!
//! ## Example
//!
//! ```
//! use rtl_flip_detect::{has_rtl_flip, strip_rtl_flips};
//! let evil = "evil\u{202E}cod.exe";
//! assert!(has_rtl_flip(evil));
//! assert_eq!(strip_rtl_flips(evil), "evilcod.exe");
//! ```

#![deny(missing_docs)]

/// True when the input contains any bidi-control char that could flip
/// direction.
pub fn has_rtl_flip(s: &str) -> bool {
    s.chars().any(is_bidi_control)
}

/// Return the byte positions of every bidi-control char in `s`.
pub fn find_rtl_flips(s: &str) -> Vec<(usize, char)> {
    s.char_indices()
        .filter(|(_, c)| is_bidi_control(*c))
        .collect()
}

/// Strip every bidi-control char from `s`.
pub fn strip_rtl_flips(s: &str) -> String {
    s.chars().filter(|c| !is_bidi_control(*c)).collect()
}

fn is_bidi_control(c: char) -> bool {
    matches!(c as u32,
        0x202A..=0x202E
        | 0x2066..=0x2069
    )
}