use std::borrow::Cow;
use chardetng::{EncodingDetector, Iso2022JpDetection, Utf8Detection};
use encoding_rs::{Encoding, WINDOWS_1252};
const UTF8_BOM: [u8; 3] = [0xEF, 0xBB, 0xBF];
const UTF16_LE_BOM: [u8; 2] = [0xFF, 0xFE];
const UTF16_BE_BOM: [u8; 2] = [0xFE, 0xFF];
pub fn decode_paradox_bytes(bytes: &[u8]) -> Cow<'_, str> {
if let Some(rest) = bytes.strip_prefix(UTF8_BOM.as_slice()) {
return Cow::Owned(String::from_utf8_lossy(rest).into_owned());
}
if bytes.starts_with(UTF16_LE_BOM.as_slice()) {
let (decoded, _, _) = encoding_rs::UTF_16LE.decode(&bytes[2..]);
return Cow::Owned(decoded.into_owned());
}
if bytes.starts_with(UTF16_BE_BOM.as_slice()) {
let (decoded, _, _) = encoding_rs::UTF_16BE.decode(&bytes[2..]);
return Cow::Owned(decoded.into_owned());
}
if let Ok(text) = std::str::from_utf8(bytes) {
return Cow::Borrowed(text);
}
if let Some(decoded) = decode_via_chardet(bytes) {
return Cow::Owned(decoded);
}
let (decoded, _, _) = WINDOWS_1252.decode(bytes);
Cow::Owned(decoded.into_owned())
}
fn decode_via_chardet(bytes: &[u8]) -> Option<String> {
let mut detector = EncodingDetector::new(Iso2022JpDetection::Deny);
detector.feed(bytes, true);
let encoding = detector.guess(None, Utf8Detection::Deny);
if !is_legacy_double_byte(encoding) {
return None;
}
let (decoded, _, had_errors) = encoding.decode(bytes);
if had_errors {
return None;
}
Some(decoded.into_owned())
}
fn is_legacy_double_byte(encoding: &'static Encoding) -> bool {
encoding == encoding_rs::GB18030
|| encoding == encoding_rs::GBK
|| encoding == encoding_rs::BIG5
|| encoding == encoding_rs::EUC_JP
|| encoding == encoding_rs::SHIFT_JIS
|| encoding == encoding_rs::EUC_KR
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pure_ascii_borrows() {
let bytes = b"name = \"Catherine\"";
let out = decode_paradox_bytes(bytes);
assert!(matches!(out, Cow::Borrowed(_)));
assert_eq!(out, "name = \"Catherine\"");
}
#[test]
fn utf8_with_combining_chars_borrows() {
let bytes = b"dynasty = \"de Bragan\xc3\xa7a\"";
let out = decode_paradox_bytes(bytes);
assert!(matches!(out, Cow::Borrowed(_)));
assert!(out.contains("Bragança"));
}
#[test]
fn windows_1252_high_byte_falls_back_losslessly() {
let bytes = b"dynasty = \"de Bragan\xe7a\"";
let out = decode_paradox_bytes(bytes);
assert_eq!(out, "dynasty = \"de Bragança\"");
}
#[test]
fn utf8_and_windows_1252_converge_for_same_logical_text() {
let utf8 = b"\xc3\xa7\xc3\xa9\xc3\xb1";
let win1252 = b"\xe7\xe9\xf1";
assert_eq!(decode_paradox_bytes(utf8), decode_paradox_bytes(win1252));
}
#[test]
fn utf8_bom_is_stripped() {
let mut bytes = vec![0xEF, 0xBB, 0xBF];
bytes.extend_from_slice(b"name = yes");
assert_eq!(decode_paradox_bytes(&bytes), "name = yes");
}
#[test]
fn utf16_le_bom_decodes() {
let bytes = [0xFF, 0xFE, b'a', 0x00, b'b', 0x00];
assert_eq!(decode_paradox_bytes(&bytes), "ab");
}
#[test]
fn utf16_be_bom_decodes() {
let bytes = [0xFE, 0xFF, 0x00, b'a', 0x00, b'b'];
assert_eq!(decode_paradox_bytes(&bytes), "ab");
}
#[test]
fn gbk_bytes_decode_to_chinese_characters() {
let bytes = b"\xbf\xad\xc9\xaa\xc1\xd5";
let decoded = decode_paradox_bytes(bytes);
assert_eq!(decoded, "凯瑟琳");
assert_eq!(decode_paradox_bytes(bytes), decoded);
}
#[test]
fn windows_1252_only_path_still_falls_back() {
let bytes = b"\xe7";
assert_eq!(decode_paradox_bytes(bytes), "ç");
}
}