pub fn escape(input: &str) -> String {
if !input.bytes().any(|byte| matches!(byte, b'&' | b'<' | b'>' | b'"' | b'\'')) {
return input.to_string();
}
let mut out = String::with_capacity(input.len() + 16);
for ch in input.chars() {
match ch {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
other => out.push(other),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn escapes_every_character_that_can_change_markup() {
assert_eq!(escape("a & b"), "a & b");
assert_eq!(escape("<b>"), "<b>");
assert_eq!(escape("say \"hi\""), "say "hi"");
assert_eq!(escape("it's"), "it's");
}
#[test]
fn a_script_tag_in_data_cannot_break_out() {
let escaped = escape("<script>alert('xss')</script>");
assert!(!escaped.contains('<'));
assert!(!escaped.contains('>'));
assert_eq!(escaped, "<script>alert('xss')</script>");
}
#[test]
fn clean_text_passes_through_unchanged() {
assert_eq!(escape("Ada Lovelace — 1843"), "Ada Lovelace — 1843");
}
}