use pagesift_net::Decoded;
const SAMPLE: usize = 64 * 1024;
pub fn decode(bytes: &[u8], content_type: Option<&str>) -> (String, Option<String>) {
let declared = content_type.is_some_and(|c| c.to_ascii_lowercase().contains("charset="));
let bom = bom_len(bytes);
if !declared
&& bom == 0
&& !bytes.contains(&0)
&& let Ok(text) = std::str::from_utf8(bytes)
{
return (text.to_owned(), None);
}
if bytes.len() <= SAMPLE {
let decoded = pagesift_net::decode(bytes, content_type, None);
let warning = decoded.warning();
return (decoded.text, warning);
}
let sample: Decoded = pagesift_net::decode(&bytes[..sample_end(bytes)], content_type, None);
if matches!(
sample.encoding_name,
"utf-7" | "utf-32" | "utf-32le" | "utf-32be" | "cp500"
) {
let decoded = pagesift_net::decode(bytes, content_type, None);
let warning = decoded.warning();
return (decoded.text, warning);
}
let warning = sample.warning();
let (text, _) = sample.encoding.decode_without_bom_handling(&bytes[bom..]);
(text.into_owned(), warning)
}
fn sample_end(bytes: &[u8]) -> usize {
let mut end = SAMPLE;
while end > SAMPLE - 4 && bytes[end] & 0xC0 == 0x80 {
end -= 1;
}
end
}
fn bom_len(bytes: &[u8]) -> usize {
if bytes.starts_with(&[0xFF, 0xFE, 0x00, 0x00]) || bytes.starts_with(&[0x00, 0x00, 0xFE, 0xFF])
{
4
} else if bytes.starts_with(&[0xEF, 0xBB, 0xBF]) {
3
} else if bytes.starts_with(&[0xFF, 0xFE]) || bytes.starts_with(&[0xFE, 0xFF]) {
2
} else {
0
}
}
#[cfg(test)]
mod tests {
use super::{SAMPLE, decode};
fn gbk_body(rows: usize) -> String {
"编号,城市,数量,备注\n".to_string()
+ &(0..rows)
.map(|i| format!("{i},北京上海广州深圳成都,{i},订单 {i} 的备注信息很长很长\n"))
.collect::<String>()
}
#[test]
fn header_charset_and_detection_both_work() {
let (sjis, _, _) = encoding_rs::SHIFT_JIS.encode("番号,都市\n1,東京\n");
let (text, warning) = decode(&sjis, Some("text/csv; charset=shift_jis"));
assert_eq!(text, "番号,都市\n1,東京\n");
assert_eq!(warning, None);
let body = gbk_body(40);
let (gbk, _, _) = encoding_rs::GBK.encode(&body);
let (text, _) = decode(&gbk, Some("text/csv"));
assert_eq!(text, body);
let (text, warning) = decode(&gbk, Some("text/csv; charset=utf-8"));
assert_eq!(text, body);
assert!(
warning.as_deref().is_some_and(|w| w.starts_with("decode:")),
"{warning:?}"
);
let (text, warning) = decode(b"a,b\n1,2\n", None);
assert_eq!((text.as_str(), warning), ("a,b\n1,2\n", None));
let (text, _) = decode("\u{feff}编号,城市\n".as_bytes(), None);
assert_eq!(text, "编号,城市\n");
let (text, warning) = decode("编号,城市\n".as_bytes(), Some("text/csv; charset=latin1"));
assert_eq!(text, "编号,城市\n");
assert!(warning.is_some());
}
#[test]
fn large_bodies_are_detected_on_a_sample_and_decoded_whole() {
let body = gbk_body(6000);
let (gbk, _, _) = encoding_rs::GBK.encode(&body);
assert!(gbk.len() > SAMPLE);
let (text, warning) = decode(&gbk, Some("text/csv"));
assert_eq!(text, body);
assert_eq!(warning, None);
let (text, warning) = decode(&gbk, Some("text/csv; charset=utf-8"));
assert_eq!(text, body);
assert!(warning.is_some());
let (text, _) = decode(body.as_bytes(), None);
assert_eq!(text, body);
let mut with_bom = vec![0xFF, 0xFE];
with_bom.extend("a,b\n1,2\n".encode_utf16().flat_map(u16::to_le_bytes));
assert_eq!(decode(&with_bom, None).0, "a,b\n1,2\n");
}
}