mirror of
https://github.com/lexmount/moli.git
synced 2026-10-03 08:00:49 +00:00
fix(encoding): match Blink exact charset lookup
This commit is contained in:
@@ -1,9 +1,20 @@
|
||||
//! Shared parsing for MIME values and HTTP response `Content-Type` fields.
|
||||
//! MIME parsing for two distinct browser contexts.
|
||||
//!
|
||||
//! Web-facing MIME operations use the WHATWG parser, while transport response
|
||||
//! metadata uses Chromium's deliberately more tolerant network semantics. The
|
||||
//! two entry points are named separately so callers cannot select the wrong
|
||||
//! behavior through a generic leniency flag.
|
||||
//! Choose the entry point from the source of the value:
|
||||
//!
|
||||
//! - [`parse_mime_type`] applies the WHATWG MIME parsing and serialization
|
||||
//! rules. Use it for standards-facing MIME operations such as Blob/File type
|
||||
//! handling, MIME classification, and interpreting an already selected MIME
|
||||
//! value.
|
||||
//! - [`parse_response_content_type`] matches Chromium's network-layer parsing
|
||||
//! of a raw HTTP response `Content-Type` field. Use it when transport
|
||||
//! metadata must retain Chromium behavior for values such as `charset` and
|
||||
//! multipart `boundary`.
|
||||
//!
|
||||
//! These parsers intentionally have different validity and recovery rules. A
|
||||
//! value accepted by one is not necessarily accepted by the other, so callers
|
||||
//! must select the parser from the value's origin rather than treating the two
|
||||
//! result types as interchangeable.
|
||||
|
||||
mod response;
|
||||
mod whatwg;
|
||||
|
||||
@@ -35,12 +35,14 @@ impl ResponseContentType {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses an HTTP response `Content-Type` like Chromium's
|
||||
/// Parses a raw HTTP response `Content-Type` field like Chromium's
|
||||
/// `net::HttpUtil::ParseContentType` and `net::ParseMimeType` path.
|
||||
///
|
||||
/// This intentionally accepts malformed values seen on the network, including
|
||||
/// an unterminated quoted parameter and an empty subtype. Use
|
||||
/// [`crate::parse_mime_type`] for standards-facing Web API behavior.
|
||||
/// an unterminated quoted parameter and an empty subtype. Use this only for
|
||||
/// response transport metadata, such as extracting `charset` or multipart
|
||||
/// `boundary`; standards-facing MIME operations must use
|
||||
/// [`crate::parse_mime_type`].
|
||||
pub fn parse_response_content_type(input: &str) -> Option<ResponseContentType> {
|
||||
// Chromium treats an exact bare wildcard as meaningless, while retaining
|
||||
// wildcard values that have parameters or even trailing whitespace.
|
||||
|
||||
@@ -44,7 +44,13 @@ impl fmt::Display for MimeType {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a MIME value using WHATWG validation and parameter recovery rules.
|
||||
/// Parses a standards-facing MIME value using WHATWG validation,
|
||||
/// normalization, and parameter recovery rules.
|
||||
///
|
||||
/// Use this for Web API MIME operations and for interpreting an already
|
||||
/// selected MIME value. Raw HTTP response `Content-Type` metadata must instead
|
||||
/// use [`crate::parse_response_content_type`] so its Chromium network behavior
|
||||
/// is preserved.
|
||||
pub fn parse_mime_type(input: &str) -> Option<MimeType> {
|
||||
input.parse().ok().map(|inner| MimeType { inner })
|
||||
}
|
||||
@@ -64,7 +70,12 @@ pub fn mime_charset(input: &str) -> Option<String> {
|
||||
mime_parameter(input, "charset")
|
||||
}
|
||||
|
||||
/// Normalizes a Web API MIME string without parsing its structure.
|
||||
/// Normalizes a Blob/File-style Web API MIME string without parsing its
|
||||
/// structure.
|
||||
///
|
||||
/// This only validates the permitted byte range and lowercases ASCII. It does
|
||||
/// not produce a [`MimeType`] and is not a substitute for
|
||||
/// [`parse_mime_type`].
|
||||
pub fn normalize_web_api_mime_type(raw: &str) -> String {
|
||||
if raw.is_empty() || raw.bytes().any(|byte| !(0x20..=0x7e).contains(&byte)) {
|
||||
return String::new();
|
||||
|
||||
@@ -1,8 +1,21 @@
|
||||
use encoding_rs::Encoding;
|
||||
use moli_content_type::parse_response_content_type;
|
||||
|
||||
/// Looks up an encoding label after the caller has applied its own whitespace
|
||||
/// rules, matching Blink's exact `TextEncoding` registry lookup.
|
||||
pub fn encoding_for_label(label: &str) -> Option<&'static Encoding> {
|
||||
Encoding::for_label(label.trim().as_bytes())
|
||||
let bytes = label.as_bytes();
|
||||
// `encoding_rs` implements the Encoding Standard's preprocessing and
|
||||
// trims ASCII whitespace itself. Blink's response path has already
|
||||
// removed HTTP LWS, so accepting anything still present here (notably VT
|
||||
// and FF) would turn an invalid response charset into a valid label.
|
||||
if bytes.first().is_some_and(|byte| byte.is_ascii_whitespace())
|
||||
|| bytes.last().is_some_and(|byte| byte.is_ascii_whitespace())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
Encoding::for_label(bytes)
|
||||
}
|
||||
|
||||
/// The `charset` parameter of a `Content-Type` value.
|
||||
|
||||
@@ -746,6 +746,40 @@ fn header_charset_matches_chromium_network_tolerances() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_charset_does_not_trim_non_http_ascii_whitespace() {
|
||||
for whitespace in ['\u{000b}', '\u{000c}'] {
|
||||
let headers = vec![(
|
||||
"Content-Type".to_owned(),
|
||||
format!("text/html; charset={whitespace}gbk"),
|
||||
)];
|
||||
|
||||
assert_eq!(
|
||||
charset_from_headers(&headers).as_deref(),
|
||||
Some(format!("{whitespace}gbk").as_str())
|
||||
);
|
||||
assert_eq!(
|
||||
decode_html_document(&gbk_bytes("太平洋"), &headers).1,
|
||||
"windows-1252"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encoding_label_lookup_requires_callers_to_preprocess_whitespace() {
|
||||
assert_eq!(encoding_for_label("gbk"), Some(encoding_rs::GBK));
|
||||
for label in [
|
||||
" gbk",
|
||||
"gbk ",
|
||||
"\tgbk",
|
||||
"gbk\n",
|
||||
"\u{000b}gbk",
|
||||
"gbk\u{000c}",
|
||||
] {
|
||||
assert_eq!(encoding_for_label(label), None, "label={label:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_charset_preserves_chromium_empty_parameter_precedence() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user