use encoding_rs::Encoding;
use crate::text_encoding::{decode_text, normalize_newlines};
const META_SNIFF_BYTES: usize = 1024;
pub(crate) fn decode_html(bytes: &[u8]) -> String {
if starts_with_bom(bytes) {
return decode_text(bytes).0;
}
if crate::text_encoding::sniffs_utf16(bytes) {
return decode_text(bytes).0;
}
if let Ok(text) = std::str::from_utf8(bytes) {
return normalize_newlines(text.to_string());
}
if let Some(encoding) = declared_encoding(bytes) {
let (text, _, _) = encoding.decode(bytes);
return normalize_newlines(text.into_owned());
}
decode_text(bytes).0
}
fn starts_with_bom(bytes: &[u8]) -> bool {
bytes.starts_with(&[0xEF, 0xBB, 0xBF])
|| bytes.starts_with(&[0xFF, 0xFE])
|| bytes.starts_with(&[0xFE, 0xFF])
}
fn declared_encoding(bytes: &[u8]) -> Option<&'static Encoding> {
let head = &bytes[..bytes.len().min(META_SNIFF_BYTES)];
let text = String::from_utf8_lossy(head).to_ascii_lowercase();
let mut rest = text.as_str();
while let Some(at) = rest.find("<meta") {
rest = &rest[at + 5..];
let tag = match rest.find('>') {
Some(end) => &rest[..end],
None => rest, };
if let Some(label) = charset_label(tag) {
if let Some(encoding) = Encoding::for_label(label.as_bytes()) {
return Some(encoding);
}
}
}
None
}
fn charset_label(tag: &str) -> Option<String> {
let at = tag.find("charset")?;
let after = tag[at + 7..].trim_start();
let after = after.strip_prefix('=')?.trim_start();
let value = match after.strip_prefix(['"', '\'']) {
Some(inner) => inner.split(['"', '\'']).next().unwrap_or(inner),
None => after
.split([' ', '\t', '\n', '\r', ';', '/', '>', '"', '\''])
.next()
.unwrap_or(after),
};
let value = value.trim();
(!value.is_empty()).then(|| value.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_utf8_is_untouched_even_when_mislabelled() {
let src = b"<meta charset=\"windows-1251\"><p>caf\xc3\xa9</p>";
assert!(decode_html(src).contains("café"));
}
#[test]
fn declared_encoding_is_honoured_for_non_utf8_bytes() {
let src = b"<meta charset=\"windows-1251\"><p>\xef\xf0\xe8</p>";
let out = decode_html(src);
assert!(out.contains("при"), "got {out:?}");
assert!(!out.contains('\u{FFFD}'), "no replacement chars: {out:?}");
}
#[test]
fn http_equiv_content_type_spelling_works() {
let src = b"<meta http-equiv=\"Content-Type\" content=\"text/html; charset=iso-8859-6\"><p>\xc7\xe4</p>";
let out = decode_html(src);
assert!(!out.contains('\u{FFFD}'), "got {out:?}");
}
#[test]
fn unquoted_charset_value_works() {
let src = b"<meta charset=windows-1251><p>\xef</p>";
assert!(!decode_html(src).contains('\u{FFFD}'));
}
#[test]
fn charset_outside_a_meta_tag_is_ignored() {
let src = b"<a href=\"/x?charset=windows-1251\">l</a><p>\x93q\x94</p>";
let out = decode_html(src);
assert!(
out.contains('\u{201C}') && out.contains('\u{201D}'),
"got {out:?}"
);
}
#[test]
fn unknown_label_falls_through_to_detection() {
let src = b"<meta charset=\"not-a-real-encoding\"><p>\x93q\x94</p>";
let out = decode_html(src);
assert!(!out.is_empty());
assert!(out.contains('\u{201C}'), "cp1252 fallback: {out:?}");
}
#[test]
fn declaration_past_the_sniff_window_is_not_read() {
let mut src = b"<!--".to_vec();
src.extend(std::iter::repeat_n(b' ', META_SNIFF_BYTES));
src.extend_from_slice(b"--><meta charset=\"windows-1251\"><p>\xef</p>");
assert!(!decode_html(&src).is_empty());
}
#[test]
fn never_panics_on_arbitrary_bytes() {
for src in [
&b""[..],
&b"<meta charset="[..],
&b"<meta charset=\""[..],
&b"<meta"[..],
&[0xFF, 0xFE][..],
&[0x00, 0x00, 0x00][..],
] {
let _ = decode_html(src);
}
}
}