use crate::{FormatError, Result};
const HEADER_LEN: usize = 78;
const NAME_LEN: usize = 32;
const TYPE_CREATOR_OFFSET: usize = 0x3C;
const NUM_RECORDS_OFFSET: usize = 0x4C;
const RECORD_ENTRY_LEN: usize = 8;
#[derive(Debug, Clone, Copy)]
pub struct RecordInfo {
pub offset: u32,
pub attributes: u8,
pub unique_id: u32,
}
#[derive(Debug, Clone)]
pub struct PalmDb {
pub name: Vec<u8>,
pub type_creator: [u8; 8],
pub records: Vec<RecordInfo>,
total_len: usize,
}
impl PalmDb {
pub fn parse(data: &[u8]) -> Result<Self> {
if data.len() < HEADER_LEN {
return Err(FormatError::Truncated(HEADER_LEN));
}
let mut name = data[0..NAME_LEN].to_vec();
if let Some(nul) = name.iter().position(|&b| b == 0) {
name.truncate(nul);
}
let mut type_creator = [0u8; 8];
type_creator.copy_from_slice(&data[TYPE_CREATOR_OFFSET..TYPE_CREATOR_OFFSET + 8]);
let num_records =
u16::from_be_bytes([data[NUM_RECORDS_OFFSET], data[NUM_RECORDS_OFFSET + 1]]) as usize;
let table_end = HEADER_LEN + num_records * RECORD_ENTRY_LEN;
if data.len() < table_end {
return Err(FormatError::Truncated(table_end));
}
let mut records = Vec::with_capacity(num_records);
for i in 0..num_records {
let base = HEADER_LEN + i * RECORD_ENTRY_LEN;
let offset = u32::from_be_bytes(data[base..base + 4].try_into().unwrap());
let attributes = data[base + 4];
let unique_id = (data[base + 5] as u32) << 16
| (data[base + 6] as u32) << 8
| data[base + 7] as u32;
records.push(RecordInfo {
offset,
attributes,
unique_id,
});
}
Ok(PalmDb {
name,
type_creator,
records,
total_len: data.len(),
})
}
pub fn record_range(&self, index: usize) -> Option<(usize, usize)> {
let start = self.records.get(index)?.offset as usize;
let end = self
.records
.get(index + 1)
.map(|r| r.offset as usize)
.unwrap_or(self.total_len);
Some((start, end))
}
pub fn record<'a>(&self, data: &'a [u8], index: usize) -> Option<&'a [u8]> {
let (start, end) = self.record_range(index)?;
data.get(start..end)
}
pub fn num_records(&self) -> usize {
self.records.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn synth() -> Vec<u8> {
let mut d = vec![0u8; HEADER_LEN + 2 * RECORD_ENTRY_LEN + 8];
d[..4].copy_from_slice(b"Test");
d[TYPE_CREATOR_OFFSET..TYPE_CREATOR_OFFSET + 8].copy_from_slice(b"BOOKMOBI");
d[NUM_RECORDS_OFFSET..NUM_RECORDS_OFFSET + 2].copy_from_slice(&2u16.to_be_bytes());
let rec0 = (HEADER_LEN + 2 * RECORD_ENTRY_LEN) as u32;
let rec1 = rec0 + 4;
d[HEADER_LEN..HEADER_LEN + 4].copy_from_slice(&rec0.to_be_bytes());
d[HEADER_LEN + 8..HEADER_LEN + 12].copy_from_slice(&rec1.to_be_bytes());
d
}
#[test]
fn parses_header_and_records() {
let img = synth();
let db = PalmDb::parse(&img).unwrap();
assert_eq!(db.name, b"Test");
assert_eq!(&db.type_creator, b"BOOKMOBI");
assert_eq!(db.num_records(), 2);
assert_eq!(db.record(&img, 0).unwrap().len(), 4);
}
}