use chardetng::{EncodingDetector, Iso2022JpDetection, Utf8Detection};
use simdutf8::basic::from_utf8;
pub fn is_utf8(data: &[u8]) -> bool {
from_utf8(data).is_ok()
}
pub fn has_utf8_bom(data: &[u8]) -> bool {
data.len() >= 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF
}
pub fn skip_bom(data: &[u8]) -> &[u8] {
if has_utf8_bom(data) { &data[3..] } else { data }
}
pub(crate) fn is_utf8_ignoring_truncated_tail(data: &[u8]) -> bool {
match simdutf8::compat::from_utf8(data) {
Ok(_) => true,
Err(e) => e.error_len().is_none(),
}
}
pub fn detect_encoding(data: &[u8]) -> EncodingInfo {
detect_encoding_impl(data, is_utf8).1
}
fn detect_encoding_impl(
data: &[u8],
utf8_validator: fn(&[u8]) -> bool,
) -> (&'static encoding_rs::Encoding, EncodingInfo) {
if data.starts_with(&[0xFF, 0xFE]) {
return (
encoding_rs::UTF_16LE,
EncodingInfo::with_name("UTF-16LE", false, true),
);
}
if data.starts_with(&[0xFE, 0xFF]) {
return (
encoding_rs::UTF_16BE,
EncodingInfo::with_name("UTF-16BE", false, true),
);
}
let has_bom = has_utf8_bom(data);
let data_without_bom = skip_bom(data);
let valid_utf8 = utf8_validator(data_without_bom);
if valid_utf8 {
return (
encoding_rs::UTF_8,
EncodingInfo::with_name("UTF-8", true, has_bom),
);
}
let mut detector = EncodingDetector::new(Iso2022JpDetection::Deny);
detector.feed(data, true);
let encoding = detector.guess(None, Utf8Detection::Allow);
(
encoding,
EncodingInfo::with_name(encoding.name(), false, has_bom),
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EncodingInfo {
pub name: &'static str,
pub is_utf8: bool,
pub has_bom: bool,
}
impl EncodingInfo {
pub const fn new(is_utf8: bool, has_bom: bool) -> Self {
Self {
name: if is_utf8 { "UTF-8" } else { "unknown" },
is_utf8,
has_bom,
}
}
pub const fn with_name(name: &'static str, is_utf8: bool, has_bom: bool) -> Self {
Self {
name,
is_utf8,
has_bom,
}
}
}
pub fn detect_and_transcode(data: &[u8]) -> (std::borrow::Cow<'_, [u8]>, EncodingInfo) {
let (encoding, encoding_info) = detect_encoding_impl(data, is_utf8_ignoring_truncated_tail);
if encoding == encoding_rs::UTF_8 {
return (std::borrow::Cow::Borrowed(data), encoding_info);
}
let (decoded, _, _) = encoding.decode(data);
let transcoded = std::borrow::Cow::Owned(decoded.into_owned().into_bytes());
(transcoded, encoding_info)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_utf8() {
assert!(is_utf8(b"Hello, World!"));
assert!(is_utf8("こんにちは".as_bytes()));
assert!(is_utf8(b""));
}
#[test]
fn test_invalid_utf8() {
assert!(!is_utf8(&[0xFF, 0xFE]));
assert!(!is_utf8(&[0x80, 0x81, 0x82]));
}
#[test]
fn test_utf8_bom() {
let with_bom = [0xEF, 0xBB, 0xBF, b'a', b'b', b'c'];
let without_bom = b"abc";
assert!(has_utf8_bom(&with_bom));
assert!(!has_utf8_bom(without_bom));
assert_eq!(skip_bom(&with_bom), b"abc");
assert_eq!(skip_bom(without_bom), b"abc");
}
#[test]
fn test_detect_encoding() {
let info = detect_encoding(b"Hello");
assert_eq!(info.name, "UTF-8");
assert!(info.is_utf8);
assert!(!info.has_bom);
let with_bom = [0xEF, 0xBB, 0xBF, b'H', b'i'];
let info = detect_encoding(&with_bom);
assert_eq!(info.name, "UTF-8");
assert!(info.is_utf8);
assert!(info.has_bom);
let utf16le = [0xFF, 0xFE, b'H', 0x00];
let info = detect_encoding(&utf16le);
assert_eq!(info.name, "UTF-16LE");
assert!(!info.is_utf8);
assert!(info.has_bom);
}
#[test]
fn test_detect_encoding_is_strict_about_incomplete_tail() {
assert!(!detect_encoding(b"caf\xC3").is_utf8);
assert!(!is_utf8(b"caf\xC3"));
}
#[test]
fn test_sampled_check_tolerates_only_a_truncated_tail() {
assert!(is_utf8_ignoring_truncated_tail(b"caf\xC3"));
assert!(!is_utf8_ignoring_truncated_tail(b"ca\xC3\xC3fe"));
assert!(!is_utf8_ignoring_truncated_tail(&[0x80, 0x81]));
assert!(is_utf8_ignoring_truncated_tail("café".as_bytes()));
}
#[test]
fn test_detect_and_transcode_utf8() {
let data = b"Hello, World!";
let (result, encoding) = detect_and_transcode(data);
assert_eq!(&result[..], data);
assert_eq!(encoding, EncodingInfo::with_name("UTF-8", true, false));
}
#[test]
fn test_detect_and_transcode_utf16_le() {
let data: &[u8] = &[0xFF, 0xFE, b'H', 0x00, b'i', 0x00];
let (result, encoding) = detect_and_transcode(data);
assert!(is_utf8(&result));
assert_eq!(encoding, EncodingInfo::with_name("UTF-16LE", false, true));
}
#[test]
fn test_detect_and_transcode_windows1251() {
let data: &[u8] = &[0xCF, 0xF0, 0xE8, 0xE2, 0xE5, 0xF2];
let (result, encoding) = detect_and_transcode(data);
assert!(is_utf8(&result));
assert_eq!(encoding.name, "windows-1251");
assert!(!encoding.is_utf8);
assert!(!encoding.has_bom);
}
}