mdict-rs 0.1.4

Library-first Rust parser for MDict .mdx and .mdd dictionaries
Documentation
use crate::compression::decode_block;
use crate::crypto::{decrypt_keyword_header_block, decrypt_keyword_index_block};
use crate::cursor::Cursor;
use crate::encoding::TextEncoding;
use crate::error::{Error, Result};
use crate::limits::MAX_KEY_INDEX_BYTES;
use crate::source::FileSource;
use crate::types::{Header, OpenOptions};

#[derive(Debug, Clone)]
pub struct KeyBlockInfo {
    pub entry_count: u64,
    pub entry_start_index: u64,
    pub first_key: String,
    pub last_key: String,
    pub normalized_first: String,
    pub normalized_last: String,
    pub comp_offset: u64,
    pub comp_size: u64,
    pub decomp_size: u64,
}

#[derive(Debug, Clone)]
pub struct KeyIndex {
    pub num_entries: u64,
    pub blocks: Vec<KeyBlockInfo>,
    pub key_blocks_offset: u64,
    pub record_section_offset: u64,
}

pub fn parse_key_index(
    source: &FileSource,
    header: &Header,
    key_encoding: TextEncoding,
    keyword_section_offset: u64,
    options: &OpenOptions,
) -> Result<KeyIndex> {
    if !header.is_v2() {
        return Err(Error::Unsupported("MDict version 1.x"));
    }

    let mut raw_header =
        source.read_exact_at(keyword_section_offset, 44, "keyword section header")?;
    let checksum = u32::from_be_bytes([
        raw_header[40],
        raw_header[41],
        raw_header[42],
        raw_header[43],
    ]);

    if header.encryption_mode().has_keyword_header() {
        let passcode = options.passcode.as_ref().ok_or(Error::MissingPasscode)?;
        decrypt_keyword_header_block(&mut raw_header[..40], passcode)?;
    }
    crate::checksum::verify_adler32("keyword section header", &raw_header[..40], checksum)?;

    let mut cursor = Cursor::new(&raw_header[..40]);
    let num_blocks = cursor.read_u64_be("keyword num_blocks")?;
    let num_entries = cursor.read_u64_be("keyword num_entries")?;
    let key_index_decomp_len = cursor.read_u64_be("keyword index decompressed length")?;
    let key_index_comp_len = cursor.read_u64_be("keyword index compressed length")?;
    let key_blocks_len = cursor.read_u64_be("keyword blocks length")?;

    if key_index_decomp_len as usize > MAX_KEY_INDEX_BYTES {
        return Err(Error::LimitExceeded {
            limit: "key_index_decompressed_bytes",
            value: key_index_decomp_len,
            max: MAX_KEY_INDEX_BYTES as u64,
        });
    }

    let key_index_offset = keyword_section_offset + 44;
    let mut key_index_bytes = source.read_exact_at(
        key_index_offset,
        key_index_comp_len as usize,
        "keyword index block",
    )?;
    if header.encryption_mode().has_keyword_index() {
        if key_index_bytes.len() < 8 {
            return Err(Error::truncated(
                "keyword index block",
                8,
                key_index_bytes.len(),
            ));
        }
        let block_checksum = u32::from_be_bytes([
            key_index_bytes[4],
            key_index_bytes[5],
            key_index_bytes[6],
            key_index_bytes[7],
        ]);
        decrypt_keyword_index_block(block_checksum, &mut key_index_bytes[8..]);
    }
    let decoded = decode_block(
        "keyword index block",
        &key_index_bytes,
        key_index_decomp_len as usize,
    )?;

    let blocks = parse_keyword_index_entries(
        &decoded,
        num_blocks as usize,
        key_encoding,
        header.key_case_sensitive,
        header.strip_key,
        key_index_offset + key_index_comp_len,
    )?;
    let total_entries = blocks
        .last()
        .map(|block| {
            block
                .entry_start_index
                .checked_add(block.entry_count)
                .ok_or(Error::InvalidFormat("keyword entry count overflow"))
        })
        .transpose()?
        .unwrap_or(0);
    if total_entries != num_entries {
        return Err(Error::InvalidData(format!(
            "keyword entry count mismatch: header={num_entries}, summed={total_entries}"
        )));
    }

    let total_comp = blocks
        .iter()
        .try_fold(0u64, |acc, block| acc.checked_add(block.comp_size))
        .ok_or(Error::InvalidFormat("keyword blocks length overflow"))?;
    if total_comp != key_blocks_len {
        return Err(Error::InvalidData(format!(
            "keyword blocks length mismatch: header={key_blocks_len}, summed={total_comp}"
        )));
    }

    Ok(KeyIndex {
        num_entries,
        record_section_offset: key_index_offset + key_index_comp_len + key_blocks_len,
        key_blocks_offset: key_index_offset + key_index_comp_len,
        blocks,
    })
}

fn parse_keyword_index_entries(
    bytes: &[u8],
    num_blocks: usize,
    encoding: TextEncoding,
    case_sensitive: bool,
    strip_key: bool,
    key_blocks_offset: u64,
) -> Result<Vec<KeyBlockInfo>> {
    let mut cursor = Cursor::new(bytes);
    let mut blocks = Vec::with_capacity(num_blocks);
    let mut comp_offset = key_blocks_offset;
    let mut entry_start_index = 0u64;

    for _ in 0..num_blocks {
        let entry_count = cursor.read_u64_be("keyword block entry count")?;
        let first_units = cursor.read_u16_be("keyword block first key size")? as usize;
        let first_bytes = cursor.read_bytes(
            first_units * encoding.unit_size(),
            "keyword block first key",
        )?;
        let first_key = encoding.decode(first_bytes, "keyword block first key")?;
        let terminator =
            cursor.read_bytes(encoding.unit_size(), "keyword block first key terminator")?;
        if terminator.iter().any(|byte| *byte != 0) {
            return Err(Error::InvalidFormat(
                "invalid keyword block first-key terminator",
            ));
        }

        let last_units = cursor.read_u16_be("keyword block last key size")? as usize;
        let last_bytes =
            cursor.read_bytes(last_units * encoding.unit_size(), "keyword block last key")?;
        let last_key = encoding.decode(last_bytes, "keyword block last key")?;
        let terminator =
            cursor.read_bytes(encoding.unit_size(), "keyword block last key terminator")?;
        if terminator.iter().any(|byte| *byte != 0) {
            return Err(Error::InvalidFormat(
                "invalid keyword block last-key terminator",
            ));
        }

        let comp_size = cursor.read_u64_be("keyword block compressed size")?;
        let decomp_size = cursor.read_u64_be("keyword block decompressed size")?;

        blocks.push(KeyBlockInfo {
            entry_count,
            entry_start_index,
            first_key: first_key.clone(),
            last_key: last_key.clone(),
            normalized_first: encoding.normalize_key(&first_key, case_sensitive, strip_key),
            normalized_last: encoding.normalize_key(&last_key, case_sensitive, strip_key),
            comp_offset,
            comp_size,
            decomp_size,
        });

        comp_offset = comp_offset
            .checked_add(comp_size)
            .ok_or(Error::InvalidFormat("keyword block offset overflow"))?;
        entry_start_index = entry_start_index
            .checked_add(entry_count)
            .ok_or(Error::InvalidFormat("keyword entry index overflow"))?;
    }

    if !cursor.is_empty() {
        return Err(Error::InvalidData(format!(
            "keyword index has {} trailing bytes",
            cursor.remaining()
        )));
    }

    Ok(blocks)
}