1use std::borrow::Cow;
8
9pub fn decode_entities<'a>(input: &'a str) -> Cow<'a, str> {
25 if !input.as_bytes().contains(&b'&') {
27 return Cow::Borrowed(input);
28 }
29
30 decode_entities_slow(input)
31}
32
33fn decode_entities_slow(input: &str) -> Cow<'_, str> {
35 let mut result = String::with_capacity(input.len());
36 let mut cursor = 0usize;
37
38 while let Some(rel_amp) = input[cursor..].find('&') {
39 let amp = cursor + rel_amp;
40 result.push_str(&input[cursor..amp]);
42
43 if let Some(rel_semi) = input[amp + 1..].find(';') {
45 let semi = amp + 1 + rel_semi;
46 let entity_body = &input[amp + 1..semi];
47 if try_decode_entity_into(entity_body, &mut result) {
48 cursor = semi + 1;
49 continue;
50 }
51 }
52
53 result.push('&');
55 cursor = amp + 1;
56 }
57
58 if cursor < input.len() {
60 result.push_str(&input[cursor..]);
61 }
62
63 Cow::Owned(result)
64}
65
66fn try_decode_entity_into(body: &str, result: &mut String) -> bool {
71 if body.is_empty() {
72 return false;
73 }
74
75 if let Some(digits) = body.strip_prefix('#') {
76 if digits.starts_with('x') || digits.starts_with('X') {
78 if let Some(c) = fhp_core::entity::decode_numeric(&digits[1..], true) {
80 result.push(c);
81 return true;
82 }
83 } else {
84 if let Some(c) = fhp_core::entity::decode_numeric(digits, false) {
86 result.push(c);
87 return true;
88 }
89 }
90 } else {
91 if let Some(s) = fhp_core::entity::decode_named(body) {
93 result.push_str(s);
94 return true;
95 }
96 }
97
98 false
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 #[test]
106 fn no_entities_borrowed() {
107 let result = decode_entities("hello world");
108 assert!(matches!(result, Cow::Borrowed(_)));
109 assert_eq!(result, "hello world");
110 }
111
112 #[test]
113 fn named_entities() {
114 assert_eq!(decode_entities("&"), "&");
115 assert_eq!(decode_entities("<"), "<");
116 assert_eq!(decode_entities(">"), ">");
117 assert_eq!(decode_entities("""), "\"");
118 assert_eq!(decode_entities("'"), "'");
119 }
120
121 #[test]
122 fn numeric_decimal() {
123 assert_eq!(decode_entities("<"), "<");
124 assert_eq!(decode_entities(">"), ">");
125 assert_eq!(decode_entities("&"), "&");
126 }
127
128 #[test]
129 fn numeric_hex() {
130 assert_eq!(decode_entities("<"), "<");
131 assert_eq!(decode_entities(">"), ">");
132 assert_eq!(decode_entities("<"), "<");
133 }
134
135 #[test]
136 fn mixed_entities() {
137 assert_eq!(decode_entities("a & b < c > d"), "a & b < c > d");
138 }
139
140 #[test]
141 fn unknown_entity_passthrough() {
142 assert_eq!(decode_entities("&unknown;"), "&unknown;");
143 }
144
145 #[test]
146 fn ampersand_without_semicolon() {
147 assert_eq!(decode_entities("a & b"), "a & b");
148 }
149
150 #[test]
151 fn empty_input() {
152 let result = decode_entities("");
153 assert!(matches!(result, Cow::Borrowed(_)));
154 assert_eq!(result, "");
155 }
156
157 #[test]
158 fn entity_at_end() {
159 assert_eq!(decode_entities("hello&"), "hello&");
160 }
161
162 #[test]
163 fn consecutive_entities() {
164 assert_eq!(decode_entities("<>&"), "<>&");
165 }
166}