mdict-rs 0.1.4

Library-first Rust parser for MDict .mdx and .mdd dictionaries
Documentation
use crate::checksum::verify_adler32;
use crate::error::{Error, Result};
use crate::limits::{MAX_COMPRESSED_BLOCK_BYTES, MAX_DECOMPRESSED_BLOCK_BYTES};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompressionType {
    None,
    Lzo,
    Zlib,
}

pub fn decode_block(
    context: &'static str,
    block: &[u8],
    expected_decompressed_len: usize,
) -> Result<Vec<u8>> {
    if block.len() < 8 {
        return Err(Error::truncated(context, 8, block.len()));
    }
    if block.len() > MAX_COMPRESSED_BLOCK_BYTES {
        return Err(Error::LimitExceeded {
            limit: "compressed_block_bytes",
            value: block.len() as u64,
            max: MAX_COMPRESSED_BLOCK_BYTES as u64,
        });
    }
    if expected_decompressed_len > MAX_DECOMPRESSED_BLOCK_BYTES {
        return Err(Error::LimitExceeded {
            limit: "decompressed_block_bytes",
            value: expected_decompressed_len as u64,
            max: MAX_DECOMPRESSED_BLOCK_BYTES as u64,
        });
    }

    let comp_type = parse_comp_type(&block[..4])?;
    let checksum = u32::from_be_bytes([block[4], block[5], block[6], block[7]]);
    let payload = &block[8..];

    let decoded = match comp_type {
        CompressionType::None => payload.to_vec(),
        CompressionType::Zlib => miniz_oxide::inflate::decompress_to_vec_zlib(payload)
            .map_err(|_| Error::InvalidData(format!("invalid zlib block in {context}")))?,
        CompressionType::Lzo => decode_lzo_block(context, payload, expected_decompressed_len)?,
    };

    if decoded.len() != expected_decompressed_len {
        return Err(Error::InvalidData(format!(
            "{context} decompressed to {} bytes, expected {expected_decompressed_len}",
            decoded.len()
        )));
    }

    verify_adler32(context, &decoded, checksum)?;
    Ok(decoded)
}

#[cfg(feature = "lzo")]
fn decode_lzo_block(
    context: &'static str,
    payload: &[u8],
    expected_decompressed_len: usize,
) -> Result<Vec<u8>> {
    let mut out = vec![0; expected_decompressed_len];
    let actual = lzokay::decompress::decompress(payload, &mut out)
        .map_err(|_| Error::InvalidData(format!("invalid lzo block in {context}")))?;
    if actual != expected_decompressed_len {
        return Err(Error::InvalidData(format!(
            "{context} decompressed to {actual} bytes, expected {expected_decompressed_len}"
        )));
    }
    Ok(out)
}

#[cfg(not(feature = "lzo"))]
fn decode_lzo_block(
    _context: &'static str,
    _payload: &[u8],
    _expected_decompressed_len: usize,
) -> Result<Vec<u8>> {
    Err(Error::Unsupported(
        "LZO compressed blocks (enable the `lzo` feature)",
    ))
}

pub fn parse_comp_type(bytes: &[u8]) -> Result<CompressionType> {
    match bytes {
        [0, 0, 0, 0] => Ok(CompressionType::None),
        [1, 0, 0, 0] => Ok(CompressionType::Lzo),
        [2, 0, 0, 0] => Ok(CompressionType::Zlib),
        _ => Err(Error::InvalidData(format!(
            "unknown compression tag: {:02x?}",
            bytes
        ))),
    }
}

#[cfg(test)]
pub fn encode_zlib_block(data: &[u8]) -> Vec<u8> {
    let mut out = Vec::with_capacity(8 + data.len());
    out.extend_from_slice(&[2, 0, 0, 0]);
    out.extend_from_slice(&crate::checksum::adler32(data).to_be_bytes());
    out.extend_from_slice(&miniz_oxide::deflate::compress_to_vec_zlib(data, 6));
    out
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn round_trips_zlib_block() {
        let block = encode_zlib_block(b"hello mdict");
        let decoded = decode_block("test block", &block, 11).unwrap();
        assert_eq!(decoded, b"hello mdict");
    }
}