use std::borrow::Cow;
fn is_dangerous_control(c: char) -> bool {
let code = c as u32;
code < 0x20 || code == 0x7f || (0x80..=0x9F).contains(&code)
}
pub(crate) fn is_bidi_or_invisible(c: char) -> bool {
matches!(
c,
'\u{00AD}' | '\u{061C}' | '\u{180E}' | '\u{202A}'..='\u{202E}' | '\u{2060}'..='\u{2064}' | '\u{2066}'..='\u{2069}' | '\u{200B}'..='\u{200F}' | '\u{FEFF}' )
}
pub(crate) fn strip_bidi_and_invisible(s: &str) -> Cow<'_, str> {
if s.chars().any(is_bidi_or_invisible) {
Cow::Owned(s.chars().filter(|c| !is_bidi_or_invisible(*c)).collect())
} else {
Cow::Borrowed(s)
}
}
#[must_use]
pub fn sanitize_for_terminal(input: &str) -> Cow<'_, str> {
let stripped = strip_bidi_and_invisible(input);
if !stripped.chars().any(is_dangerous_control) {
return stripped;
}
let cleaned: String = stripped
.chars()
.map(|c| if is_dangerous_control(c) { '?' } else { c })
.collect();
Cow::Owned(cleaned)
}
#[must_use]
pub fn safe_url(url: &str) -> Option<&str> {
if url.starts_with("https://") && !url.chars().any(is_dangerous_control) {
Some(url)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sanitize_borrows_clean_input() {
match sanitize_for_terminal("clean ascii") {
Cow::Borrowed(s) => assert_eq!(s, "clean ascii"),
Cow::Owned(_) => panic!("clean input should not allocate"),
}
}
#[test]
fn sanitize_replaces_all_control_chars() {
let dirty = "a\x1bb\x07c\x00d\x7fe\nf";
let cleaned = sanitize_for_terminal(dirty);
assert_eq!(cleaned.as_ref(), "a?b?c?d?e?f");
}
#[test]
fn sanitize_replaces_c1_control_range() {
let dirty = "a\u{009b}[31mb\u{009d}OSC\u{009c}c";
let cleaned = sanitize_for_terminal(dirty);
assert_eq!(cleaned.as_ref(), "a?[31mb?OSC?c");
}
#[test]
fn sanitize_strips_bidi_and_invisible_chars() {
let dirty = "user\u{202E}nimda\u{200B}x";
assert_eq!(sanitize_for_terminal(dirty).as_ref(), "usernimdax");
}
#[test]
fn sanitize_strips_bidi_and_replaces_controls_together() {
let dirty = "a\u{202E}\x1bb";
assert_eq!(sanitize_for_terminal(dirty).as_ref(), "a?b");
}
#[test]
fn safe_url_rejects_c1_control_chars() {
assert_eq!(safe_url("https://a.com/\u{009b}[0m"), None);
}
#[test]
fn safe_url_accepts_clean_https() {
assert_eq!(
safe_url("https://example.com/x"),
Some("https://example.com/x")
);
}
#[test]
fn safe_url_rejects_non_https_and_control_chars() {
assert_eq!(safe_url("http://example.com"), None);
assert_eq!(safe_url("javascript:alert(1)"), None);
assert_eq!(safe_url("ftp://example.com"), None);
assert_eq!(safe_url("https://a.com/\x1b[0m"), None);
assert_eq!(safe_url(""), None);
}
}