mdict-rs 0.1.4

Library-first Rust parser for MDict .mdx and .mdd dictionaries
Documentation
use crate::cursor::Cursor;
use crate::error::{Error, Result};
use crate::source::FileSource;
use crate::types::Header;

#[derive(Debug, Clone)]
pub struct RecordBlockInfo {
    pub comp_offset: u64,
    pub comp_size: u64,
    pub decomp_offset: u64,
    pub decomp_size: u64,
}

#[derive(Debug, Clone)]
pub struct RecordIndex {
    pub num_entries: u64,
    pub total_decompressed_len: u64,
    pub blocks: Vec<RecordBlockInfo>,
}

impl RecordIndex {
    pub fn find_block(&self, record_offset: u64) -> Option<usize> {
        let mut left = 0usize;
        let mut right = self.blocks.len();
        while left < right {
            let mid = (left + right) / 2;
            let block = &self.blocks[mid];
            let end = block.decomp_offset + block.decomp_size;
            if record_offset < block.decomp_offset {
                right = mid;
            } else if record_offset >= end {
                left = mid + 1;
            } else {
                return Some(mid);
            }
        }
        None
    }
}

pub fn parse_record_index(
    source: &FileSource,
    header: &Header,
    record_section_offset: u64,
) -> Result<RecordIndex> {
    if !header.is_v2() {
        return Err(Error::Unsupported("MDict version 1.x"));
    }

    let header_bytes = source.read_exact_at(record_section_offset, 32, "record section header")?;
    let mut cursor = Cursor::new(&header_bytes);
    let num_blocks = cursor.read_u64_be("record num_blocks")?;
    let num_entries = cursor.read_u64_be("record num_entries")?;
    let index_len = cursor.read_u64_be("record index length")?;
    let blocks_len = cursor.read_u64_be("record blocks length")?;

    let index_bytes = source.read_exact_at(
        record_section_offset + 32,
        index_len as usize,
        "record block index",
    )?;
    let mut cursor = Cursor::new(&index_bytes);
    let mut blocks = Vec::with_capacity(num_blocks as usize);
    let mut comp_offset = record_section_offset + 32 + index_len;
    let mut decomp_offset = 0u64;
    let mut total_comp = 0u64;

    for _ in 0..num_blocks {
        let comp_size = cursor.read_u64_be("record block compressed size")?;
        let decomp_size = cursor.read_u64_be("record block decompressed size")?;
        blocks.push(RecordBlockInfo {
            comp_offset,
            comp_size,
            decomp_offset,
            decomp_size,
        });
        comp_offset = comp_offset
            .checked_add(comp_size)
            .ok_or(Error::InvalidFormat("record block offset overflow"))?;
        decomp_offset = decomp_offset
            .checked_add(decomp_size)
            .ok_or(Error::InvalidFormat("record block offset overflow"))?;
        total_comp = total_comp
            .checked_add(comp_size)
            .ok_or(Error::InvalidFormat("record block length overflow"))?;
    }

    if total_comp != blocks_len {
        return Err(Error::InvalidData(format!(
            "record blocks length mismatch: header={blocks_len}, summed={total_comp}"
        )));
    }

    Ok(RecordIndex {
        num_entries,
        total_decompressed_len: decomp_offset,
        blocks,
    })
}