use std::borrow::Cow;
const WINDOWS_1252: [u32; 32] = [
8364, 129, 8218, 402, 8222, 8230, 8224, 8225, 710, 8240, 352, 8249, 338, 141, 381, 143, 144,
8216, 8217, 8220, 8221, 8226, 8211, 8212, 732, 8482, 353, 8250, 339, 157, 382, 376,
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) enum ReferenceContext {
Text,
AttributeValue,
}
pub(crate) fn decode_character_references(html: &str, context: ReferenceContext) -> Cow<'_, str> {
let Some(first) = html.find('&') else {
return Cow::Borrowed(html);
};
let is_attribute_value = context == ReferenceContext::AttributeValue;
let mut result = String::with_capacity(html.len());
result.push_str(&html[..first]);
let mut i = first;
let mut decoded_any = false;
while let Some(ch) = html[i..].chars().next() {
if ch != '&' {
result.push(ch);
i += ch.len_utf8();
continue;
}
let rest = &html[i + 1..];
if let Some((consumed, decoded)) = decode_numeric_entity(rest, is_attribute_value) {
result.push(decoded);
decoded_any = true;
i += 1 + consumed; continue;
}
if let Some((consumed, decoded)) = decode_named_entity(rest, is_attribute_value) {
result.push_str(decoded);
decoded_any = true;
i += 1 + consumed; continue;
}
result.push('&');
i += 1;
}
if decoded_any {
Cow::Owned(result)
} else {
Cow::Borrowed(html)
}
}
fn decode_numeric_entity(rest: &str, is_attribute_value: bool) -> Option<(usize, char)> {
if !rest.starts_with('#') {
return None;
}
let after_hash = &rest[1..];
let is_hex = after_hash.starts_with('x') || after_hash.starts_with('X');
let digits_start_offset = if is_hex { 1 } else { 0 };
let digits_start = &after_hash[digits_start_offset..];
let is_digit = |b: u8| {
if is_hex {
b.is_ascii_hexdigit()
} else {
b.is_ascii_digit()
}
};
let digit_count = digits_start
.bytes()
.position(|b| !is_digit(b))
.unwrap_or(digits_start.len());
if digit_count == 0 {
return None;
}
let digits = &digits_start[..digit_count];
let code = if is_hex {
u32::from_str_radix(digits, 16)
} else {
digits.parse::<u32>()
}
.unwrap_or(u32::MAX);
let has_semicolon = digits_start.as_bytes().get(digit_count) == Some(&b';');
let total_consumed = 1 + digits_start_offset + digit_count + if has_semicolon { 1 } else { 0 };
let validated = validate_code(code, is_attribute_value);
let decoded = char::from_u32(validated)?;
Some((total_consumed, decoded))
}
#[inline]
fn decode_named_entity(rest: &str, is_attribute_value: bool) -> Option<(usize, &'static str)> {
let reference = tsv_html::match_named_character_reference(rest)?;
let name = reference.name();
let len = name.len();
if is_attribute_value
&& !name.ends_with(';')
&& let Some(next) = rest[len..].chars().next()
&& (next == '=' || next.is_ascii_alphanumeric())
{
return None;
}
Some((len, reference.characters()))
}
fn validate_code(code: u32, is_attribute_value: bool) -> u32 {
const NUL: u32 = 0;
const MAX_CODE_POINT: u32 = 0x10FFFF;
const SURROGATES: std::ops::RangeInclusive<u32> = 0xD800..=0xDFFF;
if code == 10 && !is_attribute_value {
return 32;
}
if code < 128 {
return code;
}
if code <= 159 {
return WINDOWS_1252[(code - 128) as usize];
}
if SURROGATES.contains(&code) || code > MAX_CODE_POINT {
return NUL;
}
code
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_decode_overview() {
assert_eq!(
decode_character_references("<tag>", ReferenceContext::Text),
"<tag>"
);
assert_eq!(
decode_character_references("& "", ReferenceContext::Text),
"& \""
);
assert_eq!(
decode_character_references("A", ReferenceContext::Text),
"A"
);
assert_eq!(
decode_character_references("A", ReferenceContext::Text),
"A"
);
assert_eq!(
decode_character_references("A", ReferenceContext::Text),
"A"
);
assert_eq!(
decode_character_references("&=", ReferenceContext::AttributeValue),
"&="
);
assert!(matches!(
decode_character_references("a & b", ReferenceContext::Text),
Cow::Borrowed(_)
));
}
#[test]
fn test_longest_entity_name() {
assert_eq!(
decode_character_references(
"∳",
ReferenceContext::Text
),
"\u{2233}"
);
}
#[test]
fn test_long_name_run() {
let text = format!("&{}", "a".repeat(10_000));
assert_eq!(
decode_character_references(&text, ReferenceContext::Text),
text
);
let text = format!("&{}", "a".repeat(10_000));
assert_eq!(
decode_character_references(&text, ReferenceContext::Text),
format!("&{}", "a".repeat(10_000))
);
}
#[test]
fn test_basic_named_entities() {
assert_eq!(
decode_character_references("<", ReferenceContext::Text),
"<"
);
assert_eq!(
decode_character_references(">", ReferenceContext::Text),
">"
);
assert_eq!(
decode_character_references("&", ReferenceContext::Text),
"&"
);
assert_eq!(
decode_character_references(""", ReferenceContext::Text),
"\""
);
assert_eq!(
decode_character_references(" ", ReferenceContext::Text),
"\u{00A0}"
);
assert_eq!(
decode_character_references("©", ReferenceContext::Text),
"©"
);
}
#[test]
fn test_numeric_decimal() {
assert_eq!(
decode_character_references("A", ReferenceContext::Text),
"A"
);
assert_eq!(
decode_character_references("€", ReferenceContext::Text),
"€"
);
assert_eq!(
decode_character_references("💩", ReferenceContext::Text),
"💩"
);
}
#[test]
fn test_numeric_hex() {
assert_eq!(
decode_character_references("A", ReferenceContext::Text),
"A"
);
assert_eq!(
decode_character_references("A", ReferenceContext::Text),
"A"
);
assert_eq!(
decode_character_references("€", ReferenceContext::Text),
"€"
);
assert_eq!(
decode_character_references("💩", ReferenceContext::Text),
"💩"
);
}
#[test]
fn test_mixed_content() {
assert_eq!(
decode_character_references("<tag>", ReferenceContext::Text),
"<tag>"
);
assert_eq!(
decode_character_references("& " ©", ReferenceContext::Text),
"& \" ©"
);
}
#[test]
fn test_invalid_entities() {
assert_eq!(
decode_character_references("&unknown;", ReferenceContext::Text),
"&unknown;"
);
}
#[test]
fn test_validate_code() {
assert_eq!(validate_code(10, false), 32);
assert_eq!(validate_code(10, true), 10);
assert_eq!(validate_code(65, false), 65);
assert_eq!(validate_code(128, false), 8364);
assert_eq!(validate_code(0x00C6, false), 0x00C6);
assert_eq!(validate_code(0xD800, false), 0);
assert_eq!(validate_code(0xDFFF, false), 0);
assert_eq!(validate_code(0x1F4A9, false), 0x1F4A9); }
#[test]
fn test_legacy_entities_no_semicolon() {
assert_eq!(
decode_character_references("&", ReferenceContext::Text),
"&"
);
assert_eq!(
decode_character_references("©", ReferenceContext::Text),
"©"
);
assert_eq!(
decode_character_references("<", ReferenceContext::Text),
"<"
);
assert_eq!(
decode_character_references(">", ReferenceContext::Text),
">"
);
}
#[test]
fn test_attribute_context_rules() {
assert_eq!(
decode_character_references("&=value", ReferenceContext::AttributeValue),
"&=value"
);
assert_eq!(
decode_character_references("&ersand", ReferenceContext::AttributeValue),
"&ersand"
);
assert_eq!(
decode_character_references("&&more", ReferenceContext::AttributeValue),
"&&more"
);
assert_eq!(
decode_character_references("& ", ReferenceContext::AttributeValue),
"& "
);
assert_eq!(
decode_character_references("&=test", ReferenceContext::AttributeValue),
"&=test"
);
assert_eq!(
decode_character_references("&_x", ReferenceContext::AttributeValue),
"&_x"
);
assert_eq!(
decode_character_references("©\u{4e2d}", ReferenceContext::AttributeValue),
"©\u{4e2d}"
);
assert_eq!(
decode_character_references("©\u{663}", ReferenceContext::AttributeValue),
"©\u{663}"
);
assert_eq!(
decode_character_references("©\u{ff11}", ReferenceContext::AttributeValue),
"©\u{ff11}"
);
assert_eq!(
decode_character_references("&_x", ReferenceContext::Text),
"&_x"
);
assert_eq!(
decode_character_references("©\u{4e2d}", ReferenceContext::Text),
"©\u{4e2d}"
);
}
#[test]
fn test_incomplete_entities() {
assert_eq!(
decode_character_references("&am", ReferenceContext::Text),
"&am"
);
assert_eq!(
decode_character_references("&l", ReferenceContext::Text),
"&l"
);
assert_eq!(
decode_character_references("&", ReferenceContext::Text),
"&"
);
}
#[test]
fn test_numeric_edge_cases() {
assert_eq!(
decode_character_references("�", ReferenceContext::Text),
"\0"
);
assert_eq!(
decode_character_references("�", ReferenceContext::Text),
"\0"
);
assert_eq!(
decode_character_references("�", ReferenceContext::Text),
"\0"
);
assert_eq!(
decode_character_references(" ", ReferenceContext::Text),
" "
);
assert_eq!(
decode_character_references("€", ReferenceContext::Text),
"€"
);
}
#[test]
fn test_uppercase_hex_entity() {
assert_eq!(
decode_character_references("A", ReferenceContext::Text),
"A"
);
assert_eq!(
decode_character_references("💩", ReferenceContext::Text),
"💩"
);
assert_eq!(
decode_character_references("A", ReferenceContext::Text),
"A"
);
assert_eq!(
decode_character_references("💩", ReferenceContext::Text),
"💩"
);
}
#[test]
fn test_numeric_no_semicolon() {
assert_eq!(
decode_character_references("A", ReferenceContext::Text),
"A"
);
assert_eq!(
decode_character_references("A", ReferenceContext::Text),
"A"
);
assert_eq!(
decode_character_references("A", ReferenceContext::Text),
"A"
);
}
#[test]
fn test_degenerate_references() {
assert_eq!(
decode_character_references("&;", ReferenceContext::Text),
"&;"
);
assert_eq!(
decode_character_references("&#;", ReferenceContext::Text),
"&#;"
);
assert_eq!(
decode_character_references("&#x;", ReferenceContext::Text),
"&#x;"
);
assert_eq!(
decode_character_references("&#X;", ReferenceContext::Text),
"&#X;"
);
}
#[test]
fn test_multibyte_scan_sweep() {
const ALPHABET: [char; 14] = [
'&',
'#',
'x',
'X',
';',
'1',
'a',
'A',
'_',
'\u{4e2d}',
'\u{663}',
'\u{1f4a9}',
'\u{feff}',
'\u{301}',
];
let base = ALPHABET.len() as u64;
let mut input = String::new();
for len in 1..=4 {
for n in 0..base.pow(len) {
input.clear();
let mut rest = n;
for _ in 0..len {
input.push(ALPHABET[(rest % base) as usize]);
rest /= base;
}
for context in [ReferenceContext::Text, ReferenceContext::AttributeValue] {
let decoded = decode_character_references(&input, context);
assert!(
decoded.chars().count() <= input.chars().count(),
"{input:?} grew to {decoded:?} (context: {context:?})"
);
if !input.contains('&') {
assert_eq!(
decoded, input,
"rewrote {input:?} with no reference in it (context: {context:?})"
);
}
}
}
}
}
#[test]
fn test_multibyte_name_run() {
assert_eq!(
decode_character_references("a&b\u{4e2d}pos;", ReferenceContext::AttributeValue),
"a&b\u{4e2d}pos;"
);
assert_eq!(
decode_character_references("a&b\u{4e2d}pos", ReferenceContext::AttributeValue),
"a&b\u{4e2d}pos"
);
assert_eq!(
decode_character_references("& a&b\u{4e2d}pos;", ReferenceContext::AttributeValue),
"& a&b\u{4e2d}pos;"
);
assert_eq!(
decode_character_references("\u{663}1;", ReferenceContext::Text),
"\u{4}\u{663}1;"
);
assert_eq!(
decode_character_references("\u{1f4a9}1;", ReferenceContext::Text),
"\u{4}\u{1f4a9}1;"
);
assert_eq!(
decode_character_references("&#x\u{663}1;", ReferenceContext::Text),
"&#x\u{663}1;"
);
}
#[test]
fn test_multi_codepoint_entities() {
assert_eq!(
decode_character_references("≂̸", ReferenceContext::Text),
"\u{2242}\u{338}"
);
assert_eq!(
decode_character_references("≪⃒", ReferenceContext::Text),
"\u{226a}\u{20d2}"
);
assert_eq!(
decode_character_references("∩︀", ReferenceContext::Text),
"\u{2229}\u{fe00}"
);
assert_eq!(
decode_character_references("fj", ReferenceContext::Text),
"fj"
);
assert_eq!(
decode_character_references("  ", ReferenceContext::Text),
"\u{205f}\u{200a}"
);
assert_eq!(
decode_character_references("=⃥", ReferenceContext::AttributeValue),
"=\u{20e5}"
);
assert_eq!(
decode_character_references("≂", ReferenceContext::Text),
"\u{2242}"
);
assert_eq!(
decode_character_references("∩", ReferenceContext::Text),
"\u{2229}"
);
assert_eq!(
decode_character_references("&NotEqualTilde", ReferenceContext::Text),
"&NotEqualTilde"
);
}
#[test]
fn test_longest_match_fallthrough() {
assert_eq!(
decode_character_references("©RIGHT", ReferenceContext::Text),
"©RIGHT"
);
assert_eq!(
decode_character_references("¬it;", ReferenceContext::Text),
"¬it;"
);
}
#[test]
fn test_numeric_overflow_and_plane_boundaries() {
assert_eq!(
decode_character_references("�", ReferenceContext::Text),
"\0"
);
assert_eq!(
decode_character_references("�", ReferenceContext::Text),
"\0"
);
assert_eq!(
decode_character_references("�", ReferenceContext::Text),
"\0"
);
assert_eq!(
decode_character_references("�", ReferenceContext::Text),
"\0"
);
assert_eq!(
decode_character_references("�", ReferenceContext::Text),
"\0"
);
assert_eq!(
decode_character_references("", ReferenceContext::Text),
"\u{2FFFF}"
);
assert_eq!(
decode_character_references("𰀀", ReferenceContext::Text),
"\u{30000}"
);
assert_eq!(
decode_character_references("󰀀", ReferenceContext::Text),
"\u{F0000}"
);
assert_eq!(
decode_character_references("", ReferenceContext::Text),
"\u{10FFFF}"
);
assert_eq!(
decode_character_references("�", ReferenceContext::Text),
"\0"
);
}
}