Merge pull request #567 from alectimison-maker/fix/meta-charset-sniffing

fix(net): reject unrelated meta charset hints
This commit is contained in:
SGavrl
2026-08-07 16:45:51 +00:00
committed by GitHub

View File

@@ -203,6 +203,71 @@ fn charset_from_content_type(header: &str) -> Option<String> {
None None
} }
/// Return an exact attribute value from a lowercased `<meta ...>` tag.
///
/// This deliberately tokenizes attribute names instead of searching for a
/// substring: `data-charset` and a description containing `charset=...` are
/// not character encoding declarations.
fn meta_attribute<'a>(tag: &'a str, target: &str) -> Option<&'a str> {
let mut rest = tag.strip_prefix("<meta")?;
if rest
.chars()
.next()
.is_some_and(|c| !c.is_ascii_whitespace() && !matches!(c, '>' | '/'))
{
return None;
}
while !rest.is_empty() {
rest = rest.trim_start_matches(|c: char| c.is_ascii_whitespace() || c == '/');
if rest.is_empty() || rest.starts_with('>') {
break;
}
let name_end = rest
.find(|c: char| c.is_ascii_whitespace() || matches!(c, '=' | '>' | '/'))
.unwrap_or(rest.len());
if name_end == 0 {
rest = &rest[rest.chars().next()?.len_utf8()..];
continue;
}
let name = &rest[..name_end];
rest = rest[name_end..].trim_start();
let mut value = "";
if let Some(after_equals) = rest.strip_prefix('=') {
let after_equals = after_equals.trim_start();
if let Some(quote) = after_equals
.chars()
.next()
.filter(|c| matches!(c, '"' | '\''))
{
let quoted = &after_equals[quote.len_utf8()..];
if let Some(end) = quoted.find(quote) {
value = &quoted[..end];
rest = &quoted[end + quote.len_utf8()..];
} else {
value = quoted;
rest = "";
}
} else {
let end = after_equals
.find(|c: char| c.is_ascii_whitespace() || matches!(c, '>' | '/'))
.unwrap_or(after_equals.len());
value = &after_equals[..end];
rest = &after_equals[end..];
}
}
if name.eq_ignore_ascii_case(target) {
return Some(value);
}
}
None
}
/// Scan the first 1024 bytes for a `<meta charset="...">` or /// Scan the first 1024 bytes for a `<meta charset="...">` or
/// `<meta http-equiv="Content-Type" content="...; charset=...">` declaration. /// `<meta http-equiv="Content-Type" content="...; charset=...">` declaration.
/// We only look at ASCII bytes; valid meta-charset declarations are always /// We only look at ASCII bytes; valid meta-charset declarations are always
@@ -223,21 +288,21 @@ fn sniff_meta_charset(bytes: &[u8]) -> Option<&'static Encoding> {
let end = s[abs..].find('>').map(|e| abs + e).unwrap_or(s.len()); let end = s[abs..].find('>').map(|e| abs + e).unwrap_or(s.len());
let tag = &s[abs..end]; let tag = &s[abs..end];
if let Some(charset_pos) = tag.find("charset") { if let Some(enc) = meta_attribute(tag, "charset")
let after = &tag[charset_pos + "charset".len()..]; .filter(|value| !value.is_empty())
let after = after.trim_start(); .and_then(|value| Encoding::for_label(value.as_bytes()))
if let Some(eq_rest) = after.strip_prefix('=') { {
let value = eq_rest return Some(enc);
.trim_start() }
.trim_start_matches(|c: char| c == '"' || c == '\'')
.split(|c: char| c == '"' || c == '\'' || c == ';' || c.is_whitespace() || c == '/') let is_legacy_declaration = meta_attribute(tag, "http-equiv")
.next() .is_some_and(|value| value.eq_ignore_ascii_case("content-type"));
.unwrap_or(""); if is_legacy_declaration {
if !value.is_empty() { if let Some(enc) = meta_attribute(tag, "content")
if let Some(enc) = Encoding::for_label(value.as_bytes()) { .and_then(charset_from_content_type)
return Some(enc); .and_then(|value| Encoding::for_label(value.as_bytes()))
} {
} return Some(enc);
} }
} }
@@ -283,6 +348,19 @@ mod tests {
assert_eq!(enc.name(), "EUC-KR"); assert_eq!(enc.name(), "EUC-KR");
} }
#[test]
fn unrelated_meta_attributes_do_not_declare_a_charset() {
for bytes in [
&b"<meta name=\"description\" content=\"charset=gbk\">"[..],
&b"<meta data-charset=\"gbk\">"[..],
&b"<metadata charset=\"gbk\">"[..],
] {
let (enc, source) = detect_encoding(bytes, None);
assert_eq!(enc.name(), "UTF-8");
assert_eq!(source, "default-utf8");
}
}
#[test] #[test]
fn no_charset_anywhere_falls_back_to_utf8() { fn no_charset_anywhere_falls_back_to_utf8() {
let bytes = b"<html><body>hello</body></html>"; let bytes = b"<html><body>hello</body></html>";