Merge pull request #567 from alectimison-maker/fix/meta-charset-sniffing
fix(net): reject unrelated meta charset hints
This commit is contained in:
@@ -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 = "ed[..end];
|
||||||
|
rest = "ed[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>";
|
||||||
|
|||||||
Reference in New Issue
Block a user