mdict-rs 0.1.4

Library-first Rust parser for MDict .mdx and .mdd dictionaries
Documentation
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,
    })
}