#[cfg(not(feature = "std"))]
use alloc::{format, string::String, vec::Vec};
use core::ops::Range;
pub(crate) fn form_byte_range(offset: u32, size_be: [u8; 4]) -> Range<u64> {
let begin = offset as u64;
let size = u32::from_be_bytes(size_be) as u64;
begin..begin.saturating_add(8).saturating_add(size)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum DirmComponentKind {
Page,
Shared,
Thumbnail,
}
#[derive(Debug, Clone)]
pub(crate) struct DirmComponent {
pub kind: DirmComponentKind,
pub id: String,
#[cfg_attr(not(feature = "async"), allow(dead_code))]
pub size: u32,
}
#[derive(Debug, Clone)]
pub(crate) struct DirmPayload {
pub flags: u8,
pub nfiles: u16,
pub offsets: Vec<u32>,
pub metadata: Vec<u8>,
}
pub(crate) const BUNDLED_FLAG: u8 = 0x80;
impl DirmPayload {
pub fn is_bundled(&self) -> bool {
self.flags & BUNDLED_FLAG != 0
}
#[cfg(feature = "std")]
pub fn peek_bundled(data: &[u8]) -> bool {
data.first().is_some_and(|&flags| flags & BUNDLED_FLAG != 0)
}
pub fn decode(data: &[u8]) -> Result<Self, &'static str> {
if data.len() < 3 {
return Err("DIRM chunk too short");
}
let flags = data[0];
let nfiles = u16::from_be_bytes([data[1], data[2]]);
let bundled = flags & BUNDLED_FLAG != 0;
let mut pos = 3usize;
let mut offsets = Vec::new();
if bundled {
let n = nfiles as usize;
let end = pos
.checked_add(n * 4)
.ok_or("DIRM offset table size overflow")?;
if end > data.len() {
return Err("DIRM offset table truncated");
}
offsets.reserve(n);
for i in 0..n {
let b = pos + i * 4;
offsets.push(u32::from_be_bytes([
data[b],
data[b + 1],
data[b + 2],
data[b + 3],
]));
}
pos = end;
}
Ok(Self {
flags,
nfiles,
offsets,
metadata: data[pos..].to_vec(),
})
}
#[cfg(feature = "std")]
pub fn encode(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(3 + self.offsets.len() * 4 + self.metadata.len());
out.push(self.flags);
out.extend_from_slice(&self.nfiles.to_be_bytes());
if self.is_bundled() {
for &off in &self.offsets {
out.extend_from_slice(&off.to_be_bytes());
}
}
out.extend_from_slice(&self.metadata);
out
}
pub fn components(&self) -> Vec<DirmComponent> {
let n = self.nfiles as usize;
let meta = crate::bzz::bzz_decode(&self.metadata).unwrap_or_default();
let flags_start = n * 3;
if flags_start + n > meta.len() {
return (0..n)
.map(|i| DirmComponent {
kind: DirmComponentKind::Page,
id: format!("p{i:04}"),
size: 0,
})
.collect();
}
let mut out = Vec::with_capacity(n);
let mut pos = flags_start + n;
for (i, &flag) in meta[flags_start..flags_start + n].iter().enumerate() {
let kind = match flag & 0x3f {
1 => DirmComponentKind::Page,
2 => DirmComponentKind::Thumbnail,
_ => DirmComponentKind::Shared,
};
let id = read_nt_string(&meta, &mut pos).unwrap_or_default();
if flag & 0x80 != 0 {
let _ = read_nt_string(&meta, &mut pos);
}
if flag & 0x40 != 0 {
let _ = read_nt_string(&meta, &mut pos);
}
let size = u32::from_be_bytes([0, meta[i * 3], meta[i * 3 + 1], meta[i * 3 + 2]]);
out.push(DirmComponent { kind, id, size });
}
out
}
#[cfg(feature = "std")]
pub fn build_bundled(count: usize, flags: &[u8], ids: &[String], sizes: &[u32]) -> Self {
Self {
flags: BUNDLED_FLAG | 1,
nfiles: count as u16,
offsets: vec![0; count],
metadata: build_metadata(count, flags, ids, sizes),
}
}
#[cfg(feature = "std")]
pub fn build_indirect(count: usize, flags: &[u8], ids: &[String]) -> Self {
Self {
flags: 0x01,
nfiles: count as u16,
offsets: Vec::new(),
metadata: build_metadata(count, flags, ids, &[]),
}
}
}
#[cfg(feature = "std")]
fn build_metadata(count: usize, flags: &[u8], ids: &[String], sizes: &[u32]) -> Vec<u8> {
let mut meta = Vec::new();
for i in 0..count {
let size = sizes.get(i).copied().unwrap_or(0).min(0xff_ffff);
meta.extend_from_slice(&size.to_be_bytes()[1..]);
}
for &f in flags {
meta.push(f);
}
for id in ids {
meta.extend_from_slice(id.as_bytes());
meta.push(0);
}
for id in ids {
meta.extend_from_slice(id.as_bytes());
meta.push(0);
}
meta.extend(core::iter::repeat_n(0u8, count));
crate::bzz_encode::bzz_encode(&meta)
}
fn read_nt_string(data: &[u8], pos: &mut usize) -> Option<String> {
let start = *pos;
let rest = data.get(start..)?;
let nul = rest.iter().position(|&b| b == 0)?;
*pos = start + nul + 1;
core::str::from_utf8(&rest[..nul]).ok().map(String::from)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn form_byte_range_spans_header_plus_payload() {
let r = form_byte_range(0x10, [0x00, 0x00, 0x12, 0x34]);
assert_eq!(r, 0x10..(0x10 + 8 + 0x1234));
}
#[test]
#[cfg(feature = "std")]
fn dirm_size_table_matches_form_boundaries() {
let mut checked = 0usize;
for dir in ["tests/corpus", "tests/fixtures"] {
let Ok(rd) = std::fs::read_dir(dir) else {
continue;
};
for entry in rd.flatten() {
let path = entry.path();
if path.extension().is_none_or(|e| e != "djvu") {
continue;
}
let data = std::fs::read(&path).unwrap();
if data.len() < 24 || &data[12..16] != b"DJVM" || &data[16..20] != b"DIRM" {
continue;
}
let dlen = u32::from_be_bytes(data[20..24].try_into().unwrap()) as usize;
let Ok(payload) = DirmPayload::decode(&data[24..24 + dlen]) else {
continue;
};
if !payload.is_bundled() {
continue;
}
for (c, &off) in payload.components().iter().zip(&payload.offsets) {
if c.size == 0 {
continue; }
let off = off as usize;
let form = u32::from_be_bytes(data[off + 4..off + 8].try_into().unwrap());
assert_eq!(
c.size as u64,
form as u64 + 8,
"{}: component {} size table disagrees with FORM header",
path.display(),
c.id
);
checked += 1;
}
}
}
assert!(
checked > 0,
"no bundled fixtures with populated size tables"
);
}
#[test]
fn form_byte_range_saturates_on_overflow() {
let r = form_byte_range(u32::MAX, [0xff, 0xff, 0xff, 0xff]);
assert_eq!(r.start, u32::MAX as u64);
assert_eq!(r.end, (u32::MAX as u64) + 8 + (u32::MAX as u64));
}
#[test]
fn decode_encode_roundtrip_bundled() {
let chunk = [
0x81, 0x00, 0x02, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x12, 0x34, 0xde, 0xad, 0xbe, 0xef, ];
let p = DirmPayload::decode(&chunk).expect("decode");
assert!(p.is_bundled());
assert_eq!(p.nfiles, 2);
assert_eq!(p.offsets, vec![0x10, 0x1234]);
assert_eq!(p.metadata, vec![0xde, 0xad, 0xbe, 0xef]);
assert_eq!(p.encode(), chunk, "encode∘decode must be identity");
}
#[test]
fn decode_encode_roundtrip_indirect() {
let chunk = [0x01, 0x00, 0x03, 0x01, 0x02, 0x03];
let p = DirmPayload::decode(&chunk).expect("decode");
assert!(!p.is_bundled());
assert_eq!(p.nfiles, 3);
assert!(p.offsets.is_empty());
assert_eq!(p.metadata, vec![0x01, 0x02, 0x03]);
assert_eq!(p.encode(), chunk, "encode∘decode must be identity");
}
#[test]
fn decode_rejects_truncated_offset_table() {
let chunk = [0x80, 0x00, 0x04, 0x00, 0x00, 0x00, 0x10];
assert!(DirmPayload::decode(&chunk).is_err());
}
#[test]
fn decode_rejects_too_short() {
assert!(DirmPayload::decode(&[0x80, 0x00]).is_err());
}
#[test]
fn build_bundled_components_roundtrip() {
let ids = vec!["page1".to_string(), "dict".to_string()];
let flags = vec![1u8, 0u8]; let p = DirmPayload::build_bundled(2, &flags, &ids, &[]);
assert!(p.is_bundled());
assert_eq!(p.nfiles, 2);
assert_eq!(p.offsets, vec![0, 0]);
let comps = p.components();
assert_eq!(comps.len(), 2);
assert_eq!(comps[0].kind, DirmComponentKind::Page);
assert_eq!(comps[0].id, "page1");
assert_eq!(comps[1].kind, DirmComponentKind::Shared);
assert_eq!(comps[1].id, "dict");
let bytes = p.encode();
let p2 = DirmPayload::decode(&bytes).expect("decode built");
assert_eq!(p2.encode(), bytes);
}
#[test]
fn build_indirect_has_no_offset_table() {
let ids = vec!["a.djvu".to_string(), "b.djvu".to_string()];
let p = DirmPayload::build_indirect(2, &[1, 1], &ids);
assert!(!p.is_bundled());
assert!(p.offsets.is_empty());
let bytes = p.encode();
assert_eq!(bytes[0], 0x01);
let comps = p.components();
assert_eq!(
comps.iter().map(|c| c.id.as_str()).collect::<Vec<_>>(),
["a.djvu", "b.djvu"]
);
}
#[test]
fn components_reads_name_and_title_when_flags_set() {
let ids = vec!["pg1".to_string(), "pg2".to_string()];
let flags = vec![0x81u8, 0x41u8]; let p = DirmPayload::build_indirect(2, &flags, &ids);
let comps = p.components();
assert_eq!(comps.len(), 2);
assert_eq!(comps[0].id, "pg1");
}
#[test]
fn components_short_metadata_yields_synthetic_pages() {
let p = DirmPayload {
flags: 0x80,
nfiles: 2,
offsets: vec![0, 0],
metadata: Vec::new(),
};
let comps = p.components();
assert_eq!(comps.len(), 2);
assert!(comps.iter().all(|c| c.kind == DirmComponentKind::Page));
assert_eq!(comps[0].id, "p0000");
assert_eq!(comps[1].id, "p0001");
}
}