use std::collections::{BTreeMap, HashMap};
use crate::{FormatError, Result};
pub const MAGIC: &[u8; 4] = b"TPZ0";
pub const HEADER_RECORD_MARKER: u8 = 0x63; pub const END_OF_HEADERS_MARKER: u8 = 0x64;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RecordEntry {
pub offset: u64,
pub decompressed_len: u64,
pub compressed_len: u64,
}
impl RecordEntry {
pub fn is_compressed(&self) -> bool {
self.compressed_len > 0
}
}
#[derive(Debug, Default)]
pub struct TopazContainer {
pub header_records: HashMap<Vec<u8>, Vec<RecordEntry>>,
pub book_payload_offset: u64,
}
#[derive(Debug, Clone, Copy)]
pub struct PayloadRecord<'a> {
pub tag: &'a [u8],
pub index: u64,
pub encrypted: bool,
pub compressed: bool,
pub raw: &'a [u8],
}
#[derive(Debug, Default, Clone)]
pub struct Metadata {
pub entries: BTreeMap<Vec<u8>, Vec<u8>>,
}
impl Metadata {
pub fn get(&self, key: &[u8]) -> Option<&[u8]> {
self.entries.get(key).map(Vec::as_slice)
}
pub fn keys(&self) -> Vec<&[u8]> {
match self.entries.get(b"keys".as_slice()) {
Some(v) if !v.is_empty() => v.split(|&b| b == b',').collect(),
_ => Vec::new(),
}
}
pub fn pid_meta(&self) -> (Vec<u8>, Vec<u8>) {
let md1 = self
.entries
.get(b"keys".as_slice())
.cloned()
.unwrap_or_default();
let mut md2 = Vec::new();
for key in self.keys() {
if let Some(value) = self.entries.get(key) {
md2.extend_from_slice(value);
}
}
(md1, md2)
}
}
struct Reader<'a> {
data: &'a [u8],
pos: usize,
}
impl<'a> Reader<'a> {
fn new(data: &'a [u8]) -> Self {
Self { data, pos: 0 }
}
fn seek(&mut self, pos: usize) {
self.pos = pos;
}
fn read_u8(&mut self) -> Result<u8> {
let byte = *self
.data
.get(self.pos)
.ok_or(FormatError::Truncated(self.pos))?;
self.pos += 1;
Ok(byte)
}
fn read_encoded(&mut self) -> Result<i64> {
let rest = self
.data
.get(self.pos..)
.ok_or(FormatError::Truncated(self.pos))?;
let (value, consumed) = read_encoded_number(rest)?;
self.pos += consumed;
Ok(value)
}
fn read_lp_string(&mut self) -> Result<&'a [u8]> {
let len = self.read_encoded()?;
let len = usize::try_from(len)
.map_err(|_| FormatError::Invalid(format!("negative string length {len}")))?;
self.read_bytes(len)
}
fn read_bytes(&mut self, len: usize) -> Result<&'a [u8]> {
let end = self
.pos
.checked_add(len)
.ok_or(FormatError::Truncated(self.pos))?;
let bytes = self
.data
.get(self.pos..end)
.ok_or(FormatError::Truncated(self.pos))?;
self.pos = end;
Ok(bytes)
}
}
pub fn read_encoded_number(data: &[u8]) -> Result<(i64, usize)> {
let mut pos = 0usize;
let mut byte = *data.get(pos).ok_or(FormatError::Truncated(pos))?;
pos += 1;
let negative = byte == 0xFF;
if negative {
byte = *data.get(pos).ok_or(FormatError::Truncated(pos))?;
pos += 1;
}
let magnitude: i64 = if byte < 0x80 {
i64::from(byte)
} else {
let mut acc = i64::from(byte & 0x7F);
while byte >= 0x80 {
byte = *data.get(pos).ok_or(FormatError::Truncated(pos))?;
pos += 1;
acc = (acc << 7) + i64::from(byte & 0x7F);
}
acc
};
Ok((if negative { -magnitude } else { magnitude }, pos))
}
impl TopazContainer {
pub fn parse(data: &[u8]) -> Result<Self> {
let magic = data.get(0..4).ok_or(FormatError::Truncated(0))?;
if magic != MAGIC {
return Err(FormatError::BadMagic(
String::from_utf8_lossy(magic).into_owned(),
));
}
let mut reader = Reader::new(data);
reader.seek(4);
let nb_records = reader.read_encoded()?;
let nb_records = usize::try_from(nb_records).map_err(|_| {
FormatError::Invalid(format!("negative header record count {nb_records}"))
})?;
let mut header_records: HashMap<Vec<u8>, Vec<RecordEntry>> = HashMap::new();
for _ in 0..nb_records {
let marker = reader.read_u8()?;
if marker != HEADER_RECORD_MARKER {
return Err(FormatError::Invalid(format!(
"expected header record marker 0x{HEADER_RECORD_MARKER:02x}, found 0x{marker:02x}"
)));
}
let name = reader.read_lp_string()?.to_vec();
let nb_values = reader.read_encoded()?;
let nb_values = usize::try_from(nb_values).map_err(|_| {
FormatError::Invalid(format!("negative header value count {nb_values}"))
})?;
let mut entries = Vec::with_capacity(nb_values);
for _ in 0..nb_values {
let offset = read_u64(reader.read_encoded()?, "record offset")?;
let decompressed_len = read_u64(reader.read_encoded()?, "decompressed length")?;
let compressed_len = read_u64(reader.read_encoded()?, "compressed length")?;
entries.push(RecordEntry {
offset,
decompressed_len,
compressed_len,
});
}
header_records.insert(name, entries);
}
let end = reader.read_u8()?;
if end != END_OF_HEADERS_MARKER {
return Err(FormatError::Invalid(format!(
"expected end-of-headers marker 0x{END_OF_HEADERS_MARKER:02x}, found 0x{end:02x}"
)));
}
Ok(Self {
header_records,
book_payload_offset: reader.pos as u64,
})
}
fn entry(&self, name: &[u8], index: usize) -> Result<&RecordEntry> {
self.header_records
.get(name)
.and_then(|entries| entries.get(index))
.ok_or_else(|| {
FormatError::Invalid(format!(
"record {}[{index}] not found",
String::from_utf8_lossy(name)
))
})
}
pub fn payload_record<'a>(
&self,
data: &'a [u8],
name: &[u8],
index: usize,
) -> Result<PayloadRecord<'a>> {
let entry = *self.entry(name, index)?;
let start = self
.book_payload_offset
.checked_add(entry.offset)
.and_then(|abs| usize::try_from(abs).ok())
.ok_or_else(|| FormatError::Invalid("payload record offset overflow".to_owned()))?;
let mut reader = Reader::new(data);
reader.seek(start);
let tag = reader.read_lp_string()?;
if tag != name {
return Err(FormatError::Invalid(format!(
"record tag {:?} does not match requested name {:?}",
String::from_utf8_lossy(tag),
String::from_utf8_lossy(name)
)));
}
let stored_index = reader.read_encoded()?;
let (encrypted, real_index) = if stored_index < 0 {
(true, -stored_index - 1)
} else {
(false, stored_index)
};
if real_index != index as i64 {
return Err(FormatError::Invalid(format!(
"record index {real_index} does not match requested index {index}"
)));
}
let compressed = entry.is_compressed();
let stored_len = if compressed {
entry.compressed_len
} else {
entry.decompressed_len
};
let stored_len = usize::try_from(stored_len)
.map_err(|_| FormatError::Invalid(format!("record length {stored_len} too large")))?;
let raw = reader.read_bytes(stored_len)?;
Ok(PayloadRecord {
tag,
index: real_index as u64,
encrypted,
compressed,
raw,
})
}
pub fn parse_metadata(&self, data: &[u8]) -> Result<Metadata> {
let entry = self.entry(b"metadata", 0)?;
let start = self
.book_payload_offset
.checked_add(entry.offset)
.and_then(|abs| usize::try_from(abs).ok())
.ok_or_else(|| FormatError::Invalid("metadata offset overflow".to_owned()))?;
let mut reader = Reader::new(data);
reader.seek(start);
let tag = reader.read_lp_string()?;
if tag != b"metadata" {
return Err(FormatError::Invalid(format!(
"metadata tag mismatch: {:?}",
String::from_utf8_lossy(tag)
)));
}
let _flags = reader.read_u8()?;
let count = reader.read_u8()?;
let mut entries = BTreeMap::new();
for _ in 0..count {
let key = reader.read_lp_string()?.to_vec();
let value = reader.read_lp_string()?.to_vec();
entries.insert(key, value);
}
Ok(Metadata { entries })
}
}
fn read_u64(value: i64, what: &str) -> Result<u64> {
u64::try_from(value).map_err(|_| FormatError::Invalid(format!("negative {what} {value}")))
}
#[cfg(test)]
mod tests {
use super::*;
fn enc(n: i64) -> Vec<u8> {
let mut out = Vec::new();
if n < 0 {
out.push(0xFF);
}
let magnitude = n.unsigned_abs();
if magnitude < 0x80 {
out.push(magnitude as u8);
return out;
}
let mut groups = Vec::new();
let mut v = magnitude;
while v > 0 {
groups.push((v & 0x7F) as u8);
v >>= 7;
}
groups.reverse();
if n > 0 && groups[0] == 0x7F {
groups.insert(0, 0x00);
}
let last = groups.len() - 1;
for (i, g) in groups.iter().enumerate() {
out.push(if i < last { g | 0x80 } else { *g });
}
out
}
fn lp(s: &[u8]) -> Vec<u8> {
let mut out = enc(s.len() as i64);
out.extend_from_slice(s);
out
}
#[test]
fn encoded_number_single_byte() {
assert_eq!(read_encoded_number(&[0x05]).unwrap(), (5, 1));
assert_eq!(read_encoded_number(&[0x00]).unwrap(), (0, 1));
assert_eq!(read_encoded_number(&[0x7F]).unwrap(), (127, 1));
}
#[test]
fn encoded_number_multi_byte() {
assert_eq!(read_encoded_number(&[0x82, 0x2C]).unwrap(), (300, 2));
assert_eq!(
read_encoded_number(&[0x81, 0x80, 0x00]).unwrap(),
(16384, 3)
);
}
#[test]
fn encoded_number_negative() {
assert_eq!(read_encoded_number(&[0xFF, 0x05]).unwrap(), (-5, 2));
assert_eq!(read_encoded_number(&[0xFF, 0x82, 0x2C]).unwrap(), (-300, 3));
}
#[test]
fn encoded_number_roundtrips() {
for n in [
0i64, 1, 5, 127, 128, 300, 16383, 16384, 1_000_000, -1, -300, -70000,
] {
let bytes = enc(n);
assert_eq!(
read_encoded_number(&bytes).unwrap(),
(n, bytes.len()),
"n={n}"
);
}
}
#[test]
fn encoded_number_truncated() {
assert!(matches!(
read_encoded_number(&[]),
Err(FormatError::Truncated(_))
));
assert!(matches!(
read_encoded_number(&[0x82]),
Err(FormatError::Truncated(_))
));
assert!(matches!(
read_encoded_number(&[0xFF]),
Err(FormatError::Truncated(_))
));
}
fn build_container() -> Vec<u8> {
let mut metadata = Vec::new();
metadata.extend_from_slice(&lp(b"metadata"));
metadata.push(0x00); metadata.push(0x03); metadata.extend_from_slice(&lp(b"keys"));
metadata.extend_from_slice(&lp(b"key1,key2"));
metadata.extend_from_slice(&lp(b"key1"));
metadata.extend_from_slice(&lp(b"AAA"));
metadata.extend_from_slice(&lp(b"key2"));
metadata.extend_from_slice(&lp(b"BBB"));
let comp_data = b"COMPRESSEDDATA";
let mut page0 = Vec::new();
page0.extend_from_slice(&lp(b"page"));
page0.extend_from_slice(&enc(0)); page0.extend_from_slice(comp_data);
let enc_data = b"ENCRYPTEDDATA";
let mut page1 = Vec::new();
page1.extend_from_slice(&lp(b"page"));
page1.extend_from_slice(&enc(-2)); page1.extend_from_slice(enc_data);
let meta_off = 0u64;
let page0_off = metadata.len() as u64;
let page1_off = (metadata.len() + page0.len()) as u64;
let mut header = Vec::new();
header.extend_from_slice(&enc(2));
header.push(HEADER_RECORD_MARKER);
header.extend_from_slice(&lp(b"metadata"));
header.extend_from_slice(&enc(1)); header.extend_from_slice(&enc(meta_off as i64));
header.extend_from_slice(&enc(metadata.len() as i64)); header.extend_from_slice(&enc(0));
header.push(HEADER_RECORD_MARKER);
header.extend_from_slice(&lp(b"page"));
header.extend_from_slice(&enc(2)); header.extend_from_slice(&enc(page0_off as i64));
header.extend_from_slice(&enc(999)); header.extend_from_slice(&enc(comp_data.len() as i64)); header.extend_from_slice(&enc(page1_off as i64));
header.extend_from_slice(&enc(enc_data.len() as i64)); header.extend_from_slice(&enc(0));
let mut file = Vec::new();
file.extend_from_slice(MAGIC);
file.extend_from_slice(&header);
file.push(END_OF_HEADERS_MARKER);
file.extend_from_slice(&metadata);
file.extend_from_slice(&page0);
file.extend_from_slice(&page1);
file
}
#[test]
fn parses_header_and_payload_offset() {
let data = build_container();
let container = TopazContainer::parse(&data).unwrap();
assert_eq!(container.header_records.len(), 2);
assert_eq!(container.header_records[b"metadata".as_slice()].len(), 1);
assert_eq!(container.header_records[b"page".as_slice()].len(), 2);
let meta_entry = container.header_records[b"metadata".as_slice()][0];
assert_eq!(meta_entry.offset, 0);
assert!(!meta_entry.is_compressed());
assert_eq!(data[container.book_payload_offset as usize], 8);
}
#[test]
fn compressed_and_encrypted_records() {
let data = build_container();
let container = TopazContainer::parse(&data).unwrap();
let r0 = container.payload_record(&data, b"page", 0).unwrap();
assert_eq!(r0.tag, b"page");
assert_eq!(r0.index, 0);
assert!(!r0.encrypted);
assert!(r0.compressed);
assert_eq!(r0.raw, b"COMPRESSEDDATA");
let r1 = container.payload_record(&data, b"page", 1).unwrap();
assert_eq!(r1.tag, b"page");
assert_eq!(r1.index, 1);
assert!(r1.encrypted);
assert!(!r1.compressed);
assert_eq!(r1.raw, b"ENCRYPTEDDATA");
}
#[test]
fn parses_metadata_and_pid_info() {
let data = build_container();
let container = TopazContainer::parse(&data).unwrap();
let meta = container.parse_metadata(&data).unwrap();
assert_eq!(meta.get(b"key1"), Some(b"AAA".as_slice()));
assert_eq!(meta.keys(), vec![b"key1".as_slice(), b"key2".as_slice()]);
let (md1, md2) = meta.pid_meta();
assert_eq!(md1, b"key1,key2");
assert_eq!(md2, b"AAABBB");
}
#[test]
fn rejects_bad_magic() {
let mut data = build_container();
data[0..4].copy_from_slice(b"XXXX");
assert!(matches!(
TopazContainer::parse(&data),
Err(FormatError::BadMagic(_))
));
}
#[test]
fn rejects_bad_header_marker() {
let mut data = build_container();
data[5] = 0x00;
assert!(matches!(
TopazContainer::parse(&data),
Err(FormatError::Invalid(_))
));
}
#[test]
fn missing_record_is_typed_error() {
let data = build_container();
let container = TopazContainer::parse(&data).unwrap();
assert!(matches!(
container.payload_record(&data, b"glyphs", 0),
Err(FormatError::Invalid(_))
));
}
}