#![deny(missing_docs,
missing_debug_implementations, missing_copy_implementations,
trivial_casts,
// trivial_numeric_casts, // <- something in the `named!` macro is tripping this,
// so disabling it for now
unsafe_code,
unstable_features,
unused_import_braces, unused_qualifications)]
#![cfg_attr(feature = "dev", allow(unstable_features))]
#![cfg_attr(feature = "dev", feature(plugin))]
#![cfg_attr(feature = "dev", plugin(clippy))]
#[macro_use]
extern crate nom;
#[macro_use]
extern crate error_chain;
#[macro_use]
extern crate bitflags;
#[cfg(test)]
#[macro_use]
extern crate nom_test_helpers;
use std::str;
use std::fmt;
use nom::{rest, IResult, be_u16, be_u32, be_u8};
pub use errors::*;
#[allow(unused_doc_comment)]
mod errors {
error_chain!{}
}
#[allow(missing_docs)]
#[derive(Debug, Clone, PartialEq)]
pub struct PalmDB<'a> {
pub name: &'a str,
attributes: Attributes,
pub version: u16,
pub creation_date: u32,
pub modified_date: u32,
pub last_backup_date: u32,
pub modification_number: u32,
pub app_info_id: u32,
pub sort_info_id: u32,
pub type_: &'a str,
pub creator: &'a str,
pub unique_id_seed: u32,
pub next_record_list_id: u32,
num_records: u16,
record_info_list: &'a [u8],
records: &'a [u8],
}
impl<'a> PalmDB<'a> {
pub fn parse(input: &'a [u8]) -> Result<PalmDB<'a>> {
match parse_db(input) {
IResult::Done(_, o) => {
Ok(o)
},
_ => {
Err(ErrorKind::Msg("Could not parse PalmDB file".into()).into())
}
}
}
pub fn record_info(&self, number: usize) -> Result<RecordInfo> {
if number >= self.num_records as usize {
return Err(
ErrorKind::Msg(format!("Record number {} is out-of-bounds", number)).into(),
);
}
let start = number * 8;
let end = start + 8;
let parsed = record_info_parser(&self.record_info_list[start..end]);
Ok(match parsed {
IResult::Done(_, o) => {
let (offset, attrs, a1, a2, a3) = o;
RecordInfo {
offset,
attrs,
val: (a1 as u32) << 16 | (a2 as u32) << 8 | (a3 as u32),
}
}
_ => {
return Err(
ErrorKind::Msg("Could not parse record info".into()).into(),
)
}
})
}
fn offset_to_idx(&self, offset: usize) -> Result<usize> {
let base = self.record_info(0)?;
Ok(offset - base.offset as usize)
}
fn is_last(&self, number: usize) -> bool {
number == (self.num_records - 1) as usize
}
pub fn get(&self, number: usize) -> Result<&'a [u8]> {
let record_info = self.record_info(number)?;
let loc = record_info.offset;
let start = self.offset_to_idx(loc as usize)?;
let end = if self.is_last(number) {
self.records.len()
} else {
let next_record = self.record_info(number + 1)?;
let next_offset = next_record.offset;
self.offset_to_idx(next_offset as usize)?
};
Ok(&self.records[start..end])
}
pub fn is_read_only(&self) -> bool {
self.attributes.contains(READ_ONLY)
}
pub fn app_info_area_is_dirty(&self) -> bool {
self.attributes.contains(DIRTY_APP_INFO_AREA)
}
pub fn do_backup(&self) -> bool {
self.attributes.contains(BACKUP)
}
pub fn okay_to_install_over(&self) -> bool {
self.attributes.contains(INSTALL_OVER)
}
pub fn do_force_reset(&self) -> bool {
self.attributes.contains(FORCE_RESET)
}
pub fn no_allow_copy(&self) -> bool {
self.attributes.contains(NO_ALLOW_COPY)
}
pub fn len(&self) -> usize {
self.num_records as usize
}
pub fn is_empty(&self) -> bool {
self.num_records == 0
}
}
impl<'a> fmt::Display for PalmDB<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"PalmDB {{
name: {},
attributes: {:?},
version: {},
creation_date: {},
modified_date: {},
last_backup_date: {},
modification_number: {},
app_info_id: {},
sort_info_id: {},
type_: {},
creator: {},
unique_id_seed: {},
next_record_list_id: {},
num_records: {},
record_info_list: <{} bytes>,
records: <{} bytes>,
}}",
self.name,
self.attributes,
self.version,
self.creation_date,
self.modified_date,
self.last_backup_date,
self.modification_number,
self.app_info_id,
self.sort_info_id,
self.type_,
self.creator,
self.unique_id_seed,
self.next_record_list_id,
self.num_records,
self.record_info_list.len(),
self.records.len()
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RecordInfo {
pub offset: u32,
attrs: RecordAttributes,
val: u32,
}
impl RecordInfo {
pub fn is_secret(&self) -> bool {
self.attrs.contains(SECRET_RECORD_BIT)
}
pub fn is_in_use(&self) -> bool {
self.attrs.contains(BUSY_BIT)
}
pub fn is_dirty(&self) -> bool {
self.attrs.contains(DIRTY_RECORD_BIT)
}
pub fn delete_on_next_hotsync(&self) -> bool {
self.attrs.contains(DELETE_ON_NEXT_HOTSYNC)
}
}
bitflags! {
struct Attributes: u32 {
const READ_ONLY = 0b0000_0010;
const DIRTY_APP_INFO_AREA = 0b0000_0100;
const BACKUP = 0b0000_1000;
const INSTALL_OVER = 0b0001_0000;
const FORCE_RESET = 0b0010_0000;
const NO_ALLOW_COPY = 0b0100_0000;
}
}
bitflags! {
struct RecordAttributes: u32 {
const SECRET_RECORD_BIT = 0b0001_0000;
const BUSY_BIT = 0b0010_0000;
const DIRTY_RECORD_BIT = 0b0100_0000;
const DELETE_ON_NEXT_HOTSYNC = 0b1000_0000;
}
}
named!(record_info_parser< (u32, RecordAttributes, u8, u8, u8) >,
do_parse!(
offset: be_u32 >>
attrs: map_opt!(be_u8, |a| RecordAttributes::from_bits(a as u32)) >>
a1: be_u8 >>
a2: be_u8 >>
a3: be_u8 >>
((offset, attrs, a1, a2, a3))
)
);
named!(parse_db< PalmDB >,
do_parse!(
name: map!(map_res!(take!(31), str::from_utf8), |s| s.trim_matches('\0')) >>
tag!(b"\0") >>
attributes: map_opt!(be_u16, |a| Attributes::from_bits(a as u32)) >>
version: be_u16 >>
creation_date: be_u32 >>
modified_date: be_u32 >>
last_backup_date: be_u32 >>
modification_number: be_u32 >>
app_info_id: be_u32 >>
sort_info_id: be_u32 >>
type_: map_res!(take!(4), str::from_utf8) >>
creator: map_res!(take!(4), str::from_utf8) >>
unique_id_seed: be_u32 >>
next_record_list_id: be_u32 >>
num_records: be_u16 >>
record_info_list: take!(8 * num_records) >>
opt!(tag!("\u{0}\u{0}")) >>
records: rest >>
(PalmDB {
name: name,
attributes: attributes,
version: version,
creation_date,
modified_date,
last_backup_date,
modification_number,
app_info_id,
sort_info_id,
type_,
creator,
unique_id_seed,
next_record_list_id,
num_records,
record_info_list,
records,
})
)
);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_record_info_parser() {
let data = vec![0, 0, 4, 210, 0b0011_0000, 0, 1, 0];
let record = record_info_parser(&data[..]);
let flags = SECRET_RECORD_BIT | BUSY_BIT;
let should_be = ( 1234, flags, 0, 1, 0);
assert_finished_and_eq!(record, should_be);
}
#[test]
fn test_parse_db() {
let data = vec![
'F' as u8, 'o' as u8, 'o' as u8, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0b0000_0010,
0, 1,
0, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0,
'B' as u8, 'O' as u8, 'O' as u8, 'K' as u8,
'M' as u8, 'O' as u8, 'B' as u8, 'I' as u8,
0, 0, 0, 0,
0, 0, 0, 0,
0, 1,
0, 0, 0, 86, 0b0001_0000, 0, 0, 0,
'b' as u8, 'a' as u8, 'r' as u8,
];
let db = parse_db(&data[..]);
assert_finished!(db);
match db {
IResult::Done(_, o) => {
assert_eq!(o.name, "Foo");
assert!(o.is_read_only());
assert_eq!(o.version, 1);
assert_eq!(o.len(), 1);
let record_info = o.record_info(0).expect("Couldn't get record info");
assert!(record_info.is_secret());
let record = o.get(0).expect("Couldn't get record");
assert_eq!(record.len(), 3);
assert_eq!(String::from_utf8_lossy(record), "bar".to_string());
},
_ => panic!("How did we get here?"),
};
}
}