use std::borrow::Cow;
pub fn decode_entities<'a>(input: &'a str) -> Cow<'a, str> {
if !input.as_bytes().contains(&b'&') {
return Cow::Borrowed(input);
}
decode_entities_slow(input)
}
fn decode_entities_slow(input: &str) -> Cow<'_, str> {
let mut result = String::with_capacity(input.len());
let mut cursor = 0usize;
while let Some(rel_amp) = input[cursor..].find('&') {
let amp = cursor + rel_amp;
result.push_str(&input[cursor..amp]);
if let Some(rel_semi) = input[amp + 1..].find(';') {
let semi = amp + 1 + rel_semi;
let entity_body = &input[amp + 1..semi];
if try_decode_entity_into(entity_body, &mut result) {
cursor = semi + 1;
continue;
}
}
result.push('&');
cursor = amp + 1;
}
if cursor < input.len() {
result.push_str(&input[cursor..]);
}
Cow::Owned(result)
}
fn try_decode_entity_into(body: &str, result: &mut String) -> bool {
if body.is_empty() {
return false;
}
if let Some(digits) = body.strip_prefix('#') {
if digits.starts_with('x') || digits.starts_with('X') {
if let Some(c) = fhp_core::entity::decode_numeric(&digits[1..], true) {
result.push(c);
return true;
}
} else {
if let Some(c) = fhp_core::entity::decode_numeric(digits, false) {
result.push(c);
return true;
}
}
} else {
if let Some(s) = fhp_core::entity::decode_named(body) {
result.push_str(s);
return true;
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_entities_borrowed() {
let result = decode_entities("hello world");
assert!(matches!(result, Cow::Borrowed(_)));
assert_eq!(result, "hello world");
}
#[test]
fn named_entities() {
assert_eq!(decode_entities("&"), "&");
assert_eq!(decode_entities("<"), "<");
assert_eq!(decode_entities(">"), ">");
assert_eq!(decode_entities("""), "\"");
assert_eq!(decode_entities("'"), "'");
}
#[test]
fn numeric_decimal() {
assert_eq!(decode_entities("<"), "<");
assert_eq!(decode_entities(">"), ">");
assert_eq!(decode_entities("&"), "&");
}
#[test]
fn numeric_hex() {
assert_eq!(decode_entities("<"), "<");
assert_eq!(decode_entities(">"), ">");
assert_eq!(decode_entities("<"), "<");
}
#[test]
fn mixed_entities() {
assert_eq!(decode_entities("a & b < c > d"), "a & b < c > d");
}
#[test]
fn unknown_entity_passthrough() {
assert_eq!(decode_entities("&unknown;"), "&unknown;");
}
#[test]
fn ampersand_without_semicolon() {
assert_eq!(decode_entities("a & b"), "a & b");
}
#[test]
fn empty_input() {
let result = decode_entities("");
assert!(matches!(result, Cow::Borrowed(_)));
assert_eq!(result, "");
}
#[test]
fn entity_at_end() {
assert_eq!(decode_entities("hello&"), "hello&");
}
#[test]
fn consecutive_entities() {
assert_eq!(decode_entities("<>&"), "<>&");
}
}