use regex::Regex;
use std::sync::LazyLock;
static CHARSET_RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r#"(?i)(?:^|;)\s*charset\s*=\s*["']?([\w-]+)"#).expect("charset regex is valid")
});
#[must_use]
pub fn charset_from_content_type(content_type: &str) -> Option<&str> {
CHARSET_RE
.captures(content_type)
.and_then(|caps| caps.get(1))
.map(|m| m.as_str())
}
#[must_use]
pub fn decode_body(bytes: &[u8], content_type: &str) -> String {
let encoding = charset_from_content_type(content_type)
.and_then(|label| encoding_rs::Encoding::for_label_no_replacement(label.as_bytes()))
.unwrap_or(encoding_rs::UTF_8);
let (text, _, _) = encoding.decode(bytes);
text.into_owned()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bare_charset_is_extracted() {
assert_eq!(
charset_from_content_type("text/html; charset=utf-8"),
Some("utf-8")
);
}
#[test]
fn double_quoted_charset_is_extracted() {
assert_eq!(
charset_from_content_type(r#"text/html; charset="ISO-8859-1""#),
Some("ISO-8859-1")
);
}
#[test]
fn single_quoted_charset_is_extracted() {
assert_eq!(
charset_from_content_type("text/html; charset='windows-1252'"),
Some("windows-1252")
);
}
#[test]
fn charset_is_case_insensitive() {
assert_eq!(
charset_from_content_type("text/html; CHARSET=UTF-8"),
Some("UTF-8")
);
}
#[test]
fn missing_charset_returns_none() {
assert_eq!(charset_from_content_type("text/html"), None);
assert_eq!(charset_from_content_type(""), None);
}
#[test]
fn whitespace_around_equals_is_tolerated() {
assert_eq!(
charset_from_content_type("text/html; charset = ISO-8859-1"),
Some("ISO-8859-1")
);
assert_eq!(
charset_from_content_type("text/html;charset=utf-8"),
Some("utf-8")
);
}
#[test]
fn other_parameter_names_do_not_match() {
assert_eq!(
charset_from_content_type("text/html; x-charset=utf-16be"),
None
);
assert_eq!(
charset_from_content_type(r#"multipart/mixed; boundary="==charset=utf-16be==""#),
None
);
}
#[test]
fn replacement_encoding_labels_fall_back_to_utf8() {
let body = b"<html>hello world</html>";
assert_eq!(
decode_body(body, "text/html; charset=hz-gb-2312"),
"<html>hello world</html>"
);
assert_eq!(
decode_body(body, "text/html; charset=iso-2022-kr"),
"<html>hello world</html>"
);
}
#[test]
fn latin1_body_is_decoded() {
let bytes = b"caf\xe9";
assert_eq!(
decode_body(bytes, "text/html; charset=ISO-8859-1"),
"caf\u{e9}"
);
}
#[test]
fn quoted_latin1_charset_is_honoured() {
let bytes = b"caf\xe9";
assert_eq!(
decode_body(bytes, r#"text/html; charset="ISO-8859-1""#),
"caf\u{e9}"
);
}
#[test]
fn shift_jis_body_is_decoded() {
let bytes = b"\x93\xfa\x96\x7b";
assert_eq!(
decode_body(bytes, "text/html; charset=Shift_JIS"),
"\u{65e5}\u{672c}"
);
}
#[test]
fn unknown_charset_falls_back_to_utf8() {
assert_eq!(
decode_body("héllo".as_bytes(), "text/html; charset=bogus-enc"),
"héllo"
);
}
#[test]
fn missing_content_type_falls_back_to_utf8() {
assert_eq!(decode_body("plain".as_bytes(), ""), "plain");
}
#[test]
fn invalid_utf8_without_charset_is_lossy() {
let bytes = b"caf\xe9";
assert_eq!(decode_body(bytes, "text/html"), "caf\u{fffd}");
}
#[test]
fn utf8_bom_overrides_header_charset() {
let mut bytes = vec![0xef, 0xbb, 0xbf];
bytes.extend_from_slice("café".as_bytes());
assert_eq!(decode_body(&bytes, "text/html; charset=ISO-8859-1"), "café");
}
}