#[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,
}
#[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}"),
})
.collect();
}
let mut out = Vec::with_capacity(n);
let mut pos = flags_start + n;
for &flag in &meta[flags_start..flags_start + n] {
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);
}
out.push(DirmComponent { kind, id });
}
out
}
#[cfg(feature = "std")]
pub fn build_bundled(count: usize, flags: &[u8], ids: &[String]) -> Self {
Self {
flags: 0x80,
nfiles: count as u16,
offsets: vec![0; count],
metadata: build_metadata(count, flags, ids),
}
}
#[cfg(feature = "std")]
pub fn build_indirect(count: usize, flags: &[u8], ids: &[String]) -> Self {
Self {
flags: 0x00,
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]) -> Vec<u8> {
let mut meta = Vec::new();
for _ in 0..count {
meta.extend_from_slice(&[0, 0, 0]);
}
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]
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], 0x00);
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");
}
}