use std::borrow::Cow;
use encoding_rs::{BIG5, GBK};
use crate::error::{Error, Result};
use crate::types::{ContainerKind, Header};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextEncoding {
Utf8,
Utf16Le,
Gbk,
Big5,
}
impl TextEncoding {
pub fn for_container(kind: ContainerKind, header: &Header) -> Result<Self> {
match kind {
ContainerKind::Mdd => Ok(Self::Utf16Le),
ContainerKind::Mdx => {
let label = header
.encoding_label
.as_deref()
.unwrap_or("UTF-8")
.trim()
.to_ascii_uppercase();
match label.as_str() {
"UTF-8" | "UTF8" => Ok(Self::Utf8),
"UTF-16" | "UTF-16LE" | "UTF16" => Ok(Self::Utf16Le),
"GBK" | "GB2312" | "GB18030" => Ok(Self::Gbk),
"BIG5" => Ok(Self::Big5),
_ => Err(Error::Unsupported("text encoding")),
}
}
}
}
pub fn unit_size(self) -> usize {
match self {
Self::Utf16Le => 2,
Self::Utf8 | Self::Gbk | Self::Big5 => 1,
}
}
pub fn decode(self, bytes: &[u8], context: &'static str) -> Result<String> {
match self {
Self::Utf8 => String::from_utf8(bytes.to_vec()).map_err(|_| Error::Decode {
context,
encoding: "utf-8",
}),
Self::Utf16Le => {
if bytes.len() % 2 != 0 {
return Err(Error::Decode {
context,
encoding: "utf-16le",
});
}
let units = bytes
.chunks_exact(2)
.map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
.collect::<Vec<_>>();
String::from_utf16(&units).map_err(|_| Error::Decode {
context,
encoding: "utf-16le",
})
}
Self::Gbk => decode_encoding_rs(GBK, bytes, context, "gbk"),
Self::Big5 => decode_encoding_rs(BIG5, bytes, context, "big5"),
}
}
pub fn split_terminated<'a>(
self,
bytes: &'a [u8],
offset: usize,
context: &'static str,
) -> Result<(&'a [u8], usize)> {
match self {
Self::Utf16Le => {
let mut index = offset;
while index + 1 < bytes.len() {
if bytes[index] == 0 && bytes[index + 1] == 0 {
return Ok((&bytes[offset..index], index + 2));
}
index += 2;
}
Err(Error::InvalidData(format!(
"missing UTF-16 terminator for {context}"
)))
}
Self::Utf8 | Self::Gbk | Self::Big5 => {
let tail = bytes
.get(offset..)
.ok_or(Error::InvalidFormat("terminated string offset overflow"))?;
let rel = tail.iter().position(|byte| *byte == 0).ok_or_else(|| {
Error::InvalidData(format!("missing terminator for {context}"))
})?;
let end = offset + rel;
Ok((&bytes[offset..end], end + 1))
}
}
}
pub fn normalize_key(self, key: &str, case_sensitive: bool, strip_key: bool) -> String {
let trimmed = if strip_key {
key.trim_matches(char::is_whitespace)
} else {
key
};
let without_nul = trimmed.trim_matches('\0');
if case_sensitive {
without_nul.to_owned()
} else {
without_nul.to_lowercase()
}
}
}
fn decode_encoding_rs(
encoding: &'static encoding_rs::Encoding,
bytes: &[u8],
context: &'static str,
name: &'static str,
) -> Result<String> {
let decoded = encoding
.decode_without_bom_handling_and_without_replacement(bytes)
.ok_or(Error::Decode {
context,
encoding: name,
})?;
Ok(match decoded {
Cow::Borrowed(text) => text.to_owned(),
Cow::Owned(text) => text,
})
}