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)
}