use std::borrow::Cow;
pub fn encode_safe(input: &str) -> Cow<'_, str> {
let bytes = input.as_bytes();
let first = bytes
.iter()
.position(|b| matches!(b, b'&' | b'<' | b'>' | b'"' | b'\'' | b'/'));
let Some(pos) = first else {
return Cow::Borrowed(input);
};
let mut out = String::with_capacity(input.len() + 6);
out.push_str(&input[..pos]);
let mut start = pos;
for (i, &b) in bytes[pos..].iter().enumerate() {
let replacement = match b {
b'&' => "&",
b'<' => "<",
b'>' => ">",
b'"' => """,
b'\'' => "'",
b'/' => "/",
_ => continue,
};
out.push_str(&input[start..pos + i]);
out.push_str(replacement);
start = pos + i + 1;
}
out.push_str(&input[start..]);
Cow::Owned(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_escaping_returns_borrowed() {
let input = "Hello World 123";
let result = encode_safe(input);
assert!(matches!(result, Cow::Borrowed(_)));
assert_eq!(result, "Hello World 123");
}
#[test]
fn empty_string_returns_borrowed() {
let result = encode_safe("");
assert!(matches!(result, Cow::Borrowed(_)));
assert_eq!(result, "");
}
#[test]
fn escapes_ampersand() {
assert_eq!(encode_safe("a&b"), "a&b");
}
#[test]
fn escapes_less_than() {
assert_eq!(encode_safe("<script>"), "<script>");
}
#[test]
fn escapes_greater_than() {
assert_eq!(encode_safe("a>b"), "a>b");
}
#[test]
fn escapes_double_quote() {
assert_eq!(encode_safe(r#"a"b"#), "a"b");
}
#[test]
fn escapes_single_quote() {
assert_eq!(encode_safe("a'b"), "a'b");
}
#[test]
fn escapes_forward_slash() {
assert_eq!(encode_safe("a/b"), "a/b");
}
#[test]
fn escapes_all_special_chars() {
assert_eq!(encode_safe(r#"&<>"'/"#), "&<>"'/");
}
#[test]
fn preserves_unicode() {
assert_eq!(encode_safe("こんにちは"), "こんにちは");
assert!(matches!(encode_safe("こんにちは"), Cow::Borrowed(_)));
}
#[test]
fn mixed_unicode_and_special() {
assert_eq!(encode_safe("日本語&テスト"), "日本語&テスト");
}
#[test]
fn escapes_at_start() {
assert_eq!(encode_safe("&start"), "&start");
}
#[test]
fn escapes_at_end() {
assert_eq!(encode_safe("end&"), "end&");
}
#[test]
fn multiple_consecutive_escapes() {
assert_eq!(encode_safe("&&"), "&&");
}
#[test]
fn realistic_nonce() {
let nonce = "YWJjZGVmZ2hpamtsbW5vcA==";
let result = encode_safe(nonce);
assert!(matches!(result, Cow::Borrowed(_)));
}
#[test]
fn realistic_attribute_value() {
assert_eq!(
encode_safe("John O'Brien & Associates"),
"John O'Brien & Associates"
);
}
#[test]
fn realistic_xss_attempt() {
assert_eq!(
encode_safe("<script>alert('xss')</script>"),
"<script>alert('xss')</script>"
);
}
#[test]
fn escapes_all_known_special_chars() {
let test_cases = [
("", ""),
("hello", "hello"),
("&", "&"),
("<", "<"),
(">", ">"),
("\"", """),
("'", "'"),
("/", "/"),
("a&b<c>d\"e'f/g", "a&b<c>d"e'f/g"),
];
for (input, expected) in test_cases {
assert_eq!(encode_safe(input), expected, "Failed for input: {input:?}");
}
}
}