webui_handler/
html_encode.rs1use std::borrow::Cow;
10
11pub fn encode_safe(input: &str) -> Cow<'_, str> {
23 let bytes = input.as_bytes();
24
25 let first = bytes
27 .iter()
28 .position(|b| matches!(b, b'&' | b'<' | b'>' | b'"' | b'\'' | b'/'));
29
30 let Some(pos) = first else {
31 return Cow::Borrowed(input);
32 };
33
34 let mut out = String::with_capacity(input.len() + 6);
36 out.push_str(&input[..pos]);
37
38 let mut start = pos;
39 for (i, &b) in bytes[pos..].iter().enumerate() {
40 let replacement = match b {
41 b'&' => "&",
42 b'<' => "<",
43 b'>' => ">",
44 b'"' => """,
45 b'\'' => "'",
46 b'/' => "/",
47 _ => continue,
48 };
49 out.push_str(&input[start..pos + i]);
51 out.push_str(replacement);
52 start = pos + i + 1;
53 }
54 out.push_str(&input[start..]);
56
57 Cow::Owned(out)
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63
64 #[test]
65 fn no_escaping_returns_borrowed() {
66 let input = "Hello World 123";
67 let result = encode_safe(input);
68 assert!(matches!(result, Cow::Borrowed(_)));
69 assert_eq!(result, "Hello World 123");
70 }
71
72 #[test]
73 fn empty_string_returns_borrowed() {
74 let result = encode_safe("");
75 assert!(matches!(result, Cow::Borrowed(_)));
76 assert_eq!(result, "");
77 }
78
79 #[test]
80 fn escapes_ampersand() {
81 assert_eq!(encode_safe("a&b"), "a&b");
82 }
83
84 #[test]
85 fn escapes_less_than() {
86 assert_eq!(encode_safe("<script>"), "<script>");
87 }
88
89 #[test]
90 fn escapes_greater_than() {
91 assert_eq!(encode_safe("a>b"), "a>b");
92 }
93
94 #[test]
95 fn escapes_double_quote() {
96 assert_eq!(encode_safe(r#"a"b"#), "a"b");
97 }
98
99 #[test]
100 fn escapes_single_quote() {
101 assert_eq!(encode_safe("a'b"), "a'b");
102 }
103
104 #[test]
105 fn escapes_forward_slash() {
106 assert_eq!(encode_safe("a/b"), "a/b");
107 }
108
109 #[test]
110 fn escapes_all_special_chars() {
111 assert_eq!(encode_safe(r#"&<>"'/"#), "&<>"'/");
112 }
113
114 #[test]
115 fn preserves_unicode() {
116 assert_eq!(encode_safe("こんにちは"), "こんにちは");
117 assert!(matches!(encode_safe("こんにちは"), Cow::Borrowed(_)));
118 }
119
120 #[test]
121 fn mixed_unicode_and_special() {
122 assert_eq!(encode_safe("日本語&テスト"), "日本語&テスト");
123 }
124
125 #[test]
126 fn escapes_at_start() {
127 assert_eq!(encode_safe("&start"), "&start");
128 }
129
130 #[test]
131 fn escapes_at_end() {
132 assert_eq!(encode_safe("end&"), "end&");
133 }
134
135 #[test]
136 fn multiple_consecutive_escapes() {
137 assert_eq!(encode_safe("&&"), "&&");
138 }
139
140 #[test]
141 fn realistic_nonce() {
142 let nonce = "YWJjZGVmZ2hpamtsbW5vcA==";
144 let result = encode_safe(nonce);
145 assert!(matches!(result, Cow::Borrowed(_)));
146 }
147
148 #[test]
149 fn realistic_attribute_value() {
150 assert_eq!(
151 encode_safe("John O'Brien & Associates"),
152 "John O'Brien & Associates"
153 );
154 }
155
156 #[test]
157 fn realistic_xss_attempt() {
158 assert_eq!(
159 encode_safe("<script>alert('xss')</script>"),
160 "<script>alert('xss')</script>"
161 );
162 }
163
164 #[test]
165 fn escapes_all_known_special_chars() {
166 let test_cases = [
168 ("", ""),
169 ("hello", "hello"),
170 ("&", "&"),
171 ("<", "<"),
172 (">", ">"),
173 ("\"", """),
174 ("'", "'"),
175 ("/", "/"),
176 ("a&b<c>d\"e'f/g", "a&b<c>d"e'f/g"),
177 ];
178 for (input, expected) in test_cases {
179 assert_eq!(encode_safe(input), expected, "Failed for input: {input:?}");
180 }
181 }
182}