use crate::bytesio::{align_up, get_u16, get_u32, get_u64, put_u16, put_u32, put_u64};
use crate::error::{Error, Result};
use crate::hash::xxh3_64;
pub const HEADER_LEN: usize = 128;
pub const OWNER_LEN: usize = 88;
pub const DIR_ENTRY_LEN: usize = 32;
pub const ALIGN: usize = 64;
const FAST_KINDS: usize = 32;
const OWNER_AT: usize = 32;
const SUM_AT: usize = HEADER_LEN - 8;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Format {
pub magic: u32,
pub version: u16,
}
#[derive(Debug)]
pub struct Writer {
format: Format,
flags: u16,
owner: [u8; OWNER_LEN],
sections: Vec<(u16, u16, Vec<u8>)>,
}
impl Writer {
#[must_use]
pub fn new(format: Format) -> Self {
Self { format, flags: 0, owner: [0; OWNER_LEN], sections: Vec::new() }
}
pub fn flags(&mut self, flags: u16) -> &mut Self {
self.flags = flags;
self
}
pub fn owner(&mut self, bytes: &[u8]) -> &mut Self {
let n = bytes.len().min(OWNER_LEN);
self.owner[..n].copy_from_slice(&bytes[..n]);
self
}
pub fn section(&mut self, kind: u16, bytes: Vec<u8>) -> &mut Self {
self.section_flagged(kind, 0, bytes)
}
pub fn section_flagged(&mut self, kind: u16, flags: u16, bytes: Vec<u8>) -> &mut Self {
if !bytes.is_empty() {
self.sections.push((kind, flags, bytes));
}
self
}
#[must_use]
pub fn finish(&self) -> Vec<u8> {
let dir_end = HEADER_LEN + self.sections.len() * DIR_ENTRY_LEN;
let mut offset = align_up(dir_end, ALIGN);
let mut dir = Vec::with_capacity(self.sections.len() * DIR_ENTRY_LEN);
for (kind, flags, bytes) in &self.sections {
put_u16(&mut dir, *kind);
put_u16(&mut dir, *flags);
put_u32(&mut dir, 0);
put_u64(&mut dir, offset as u64);
put_u64(&mut dir, bytes.len() as u64);
put_u64(&mut dir, xxh3_64(bytes));
offset = align_up(offset + bytes.len(), ALIGN);
}
let total = offset.max(dir_end);
let mut out = Vec::with_capacity(total);
put_u32(&mut out, self.format.magic);
put_u16(&mut out, self.format.version);
put_u16(&mut out, self.flags);
put_u32(&mut out, HEADER_LEN as u32);
put_u32(&mut out, self.sections.len() as u32);
put_u64(&mut out, total as u64);
put_u64(&mut out, xxh3_64(&dir));
out.extend_from_slice(&self.owner);
let sum = xxh3_64(&out[..SUM_AT]);
put_u64(&mut out, sum);
out.extend_from_slice(&dir);
for (_, _, bytes) in &self.sections {
out.resize(align_up(out.len(), ALIGN), 0);
out.extend_from_slice(bytes);
}
out.resize(total, 0);
out
}
}
pub fn peek(bytes: &[u8], format: Format) -> Result<(u16, [u8; OWNER_LEN])> {
check_header(bytes, format)?;
let mut owner = [0; OWNER_LEN];
owner.copy_from_slice(&bytes[OWNER_AT..OWNER_AT + OWNER_LEN]);
Ok((get_u16(bytes, 6), owner))
}
fn check_header(bytes: &[u8], format: Format) -> Result<()> {
if bytes.len() < HEADER_LEN {
return Err(Error::format("file shorter than header"));
}
if get_u32(bytes, 0) != format.magic {
return Err(Error::format("bad magic"));
}
let version = get_u16(bytes, 4);
if version != format.version {
return Err(Error::Version { found: version, supported: format.version });
}
if xxh3_64(&bytes[..SUM_AT]) != get_u64(bytes, SUM_AT) {
return Err(Error::format("header checksum mismatch"));
}
if get_u32(bytes, 8) as usize != HEADER_LEN {
return Err(Error::format("unexpected header length"));
}
Ok(())
}
#[derive(Clone, Debug)]
pub struct Dir {
pub flags: u16,
owner: [u8; OWNER_LEN],
fast: [(usize, usize, u16); FAST_KINDS],
other: Vec<(u16, usize, usize, u16)>,
}
impl Dir {
pub fn parse(bytes: &[u8], format: Format) -> Result<Self> {
check_header(bytes, format)?;
if get_u64(bytes, 16) != bytes.len() as u64 {
return Err(Error::format("total length does not match file size"));
}
let count = get_u32(bytes, 12) as usize;
let dir_end = count
.checked_mul(DIR_ENTRY_LEN)
.and_then(|l| l.checked_add(HEADER_LEN))
.filter(|&e| e <= bytes.len())
.ok_or_else(|| Error::format("directory exceeds file"))?;
if xxh3_64(&bytes[HEADER_LEN..dir_end]) != get_u64(bytes, 24) {
return Err(Error::format("directory checksum mismatch"));
}
let mut dir = Self {
flags: get_u16(bytes, 6),
owner: [0; OWNER_LEN],
fast: [(0, 0, 0); FAST_KINDS],
other: Vec::new(),
};
dir.owner.copy_from_slice(&bytes[OWNER_AT..OWNER_AT + OWNER_LEN]);
let mut spans: Vec<(usize, usize)> = Vec::with_capacity(count.min(64));
for i in 0..count {
let e = HEADER_LEN + i * DIR_ENTRY_LEN;
let (kind, sflags) = (get_u16(bytes, e), get_u16(bytes, e + 2));
let (offset, length) = (get_u64(bytes, e + 8), get_u64(bytes, e + 16));
let offset = usize::try_from(offset).map_err(|_| Error::format("section offset"))?;
let length = usize::try_from(length).map_err(|_| Error::format("section length"))?;
let end =
offset.checked_add(length).ok_or_else(|| Error::format("section overflow"))?;
if offset < dir_end || end > bytes.len() {
return Err(Error::format("section out of bounds"));
}
if !offset.is_multiple_of(ALIGN) {
return Err(Error::format("section misaligned"));
}
if xxh3_64(&bytes[offset..end]) != get_u64(bytes, e + 24) {
return Err(Error::format("section checksum mismatch"));
}
if dir.range(kind).is_some() {
return Err(Error::format("duplicate section"));
}
match dir.fast.get_mut(usize::from(kind)) {
Some(slot) => *slot = (offset, length, sflags),
None => dir.other.push((kind, offset, length, sflags)),
}
spans.push((offset, end));
}
spans.sort_unstable();
let mut at = dir_end;
for (start, end) in spans.into_iter().chain([(bytes.len(), bytes.len())]) {
if start < at || bytes[at..start].iter().any(|&b| b != 0) {
return Err(Error::format("sections overlap or padding is not zero"));
}
at = end;
}
Ok(dir)
}
#[must_use]
pub fn owner(&self) -> &[u8; OWNER_LEN] {
&self.owner
}
#[must_use]
pub fn span(&self, kind: u16) -> std::ops::Range<usize> {
self.range(kind).map_or(0..0, |(o, l)| o..o + l)
}
fn entry(&self, kind: u16) -> Option<(usize, usize, u16)> {
match self.fast.get(usize::from(kind)) {
Some(&(o, l, f)) => (l > 0).then_some((o, l, f)),
None => self.other.iter().find(|e| e.0 == kind).map(|&(_, o, l, f)| (o, l, f)),
}
}
fn range(&self, kind: u16) -> Option<(usize, usize)> {
self.entry(kind).map(|(o, l, _)| (o, l))
}
#[must_use]
pub fn section_flags(&self, kind: u16) -> u16 {
self.entry(kind).map_or(0, |e| e.2)
}
#[must_use]
pub fn has(&self, kind: u16) -> bool {
self.entry(kind).is_some()
}
#[must_use]
pub fn section<'a>(&self, bytes: &'a [u8], kind: u16) -> &'a [u8] {
self.range(kind).and_then(|(o, l)| bytes.get(o..o + l)).unwrap_or(&[])
}
}
#[cfg(test)]
mod tests {
use super::*;
const F: Format = Format { magic: 0x5453_4554, version: 3 };
#[test]
fn round_trips_sections_and_owner_fields() {
let mut w = Writer::new(F);
w.flags(7).owner(&[1, 2, 3]).section(2, vec![9; 100]).section(40, b"far".to_vec());
w.section(5, Vec::new());
let bytes = w.finish();
assert!(bytes.len().is_multiple_of(ALIGN));
let d = Dir::parse(&bytes, F).unwrap();
assert_eq!((d.flags, &d.owner()[..4]), (7, &[1, 2, 3, 0][..]));
assert_eq!(d.section(&bytes, 2), &[9; 100][..]);
assert_eq!(d.section(&bytes, 40), b"far");
assert!(d.section(&bytes, 5).is_empty() && d.section(&bytes, 3).is_empty());
assert!(d.has(2) && !d.has(5) && d.section_flags(2) == 0);
let mut w = Writer::new(F);
w.section_flagged(1, 0b10, vec![1]).section_flagged(50, 3, vec![2]);
let bytes = w.finish();
let d = Dir::parse(&bytes, F).unwrap();
assert_eq!((d.section_flags(1), d.section_flags(50), d.section_flags(9)), (0b10, 3, 0));
let empty = Writer::new(F).finish();
assert_eq!(empty.len(), HEADER_LEN);
assert!(Dir::parse(&empty, F).is_ok());
}
#[test]
fn every_byte_is_checked() {
let mut w = Writer::new(F);
w.owner(&[5; 10]).section(1, vec![1; 70]).section(2, vec![2; 3]);
let bytes = w.finish();
for i in 0..bytes.len() {
let mut bad = bytes.clone();
bad[i] ^= 0x40;
assert!(Dir::parse(&bad, F).is_err(), "byte {i}");
}
assert!(Dir::parse(&bytes, Format { version: 4, ..F }).is_err());
assert!(Dir::parse(&bytes[..bytes.len() - 1], F).is_err());
}
}