use crate::format::bytes::read_le_uint as read_uint;
use crate::format::checksum::checksum_metadata;
use crate::format::{BlockReader, FormatContext, FormatError, FormatResult, UNDEF_ADDR};
pub const FRHP_SIGNATURE: [u8; 4] = *b"FRHP";
pub const FHIB_SIGNATURE: [u8; 4] = *b"FHIB";
pub const FHDB_SIGNATURE: [u8; 4] = *b"FHDB";
const HDR_FLAG_CHECKSUM_DBLOCKS: u8 = 0x02;
const MAX_BLOCKS: usize = 65_536;
#[derive(Debug, Clone)]
pub struct FractalHeapHeader {
pub id_len: u16,
pub filter_len: u16,
pub checksum_dblocks: bool,
pub max_man_size: u32,
pub huge_next_id: u64,
pub huge_bt2_addr: u64,
pub total_man_free: u64,
pub fs_addr: u64,
pub man_size: u64,
pub man_alloc_size: u64,
pub man_iter_off: u64,
pub man_nobjs: u64,
pub huge_size: u64,
pub huge_nobjs: u64,
pub tiny_size: u64,
pub tiny_nobjs: u64,
pub table_width: u16,
pub start_block_size: u64,
pub max_direct_size: u64,
pub max_heap_size_bits: u16,
pub start_root_rows: u16,
pub table_addr: u64,
pub curr_root_rows: u16,
pub heap_off_size: u8,
pub max_direct_rows: u32,
pub row_block_size: Vec<u64>,
pub row_block_off: Vec<u64>,
pub heap_len_size: u8,
pub huge_id_size: u8,
pub huge_ids_direct: bool,
}
fn log2_of2(n: u64) -> u32 {
if n == 0 || (n & (n - 1)) != 0 {
return 0;
}
n.trailing_zeros()
}
fn size_of_offset_bits(bits: u16) -> u8 {
bits.div_ceil(8) as u8
}
fn limit_enc_size(limit: u64) -> u8 {
let log2 = if limit == 0 {
0
} else {
63 - limit.leading_zeros()
};
(log2 / 8 + 1) as u8
}
fn need(buf: &[u8], pos: usize, n: usize) -> FormatResult<()> {
if buf.len() < pos + n {
Err(FormatError::BufferTooShort {
needed: pos + n,
available: buf.len(),
})
} else {
Ok(())
}
}
impl FractalHeapHeader {
fn base_size(ctx: &FormatContext) -> usize {
let sa = ctx.sizeof_addr as usize;
let ss = ctx.sizeof_size as usize;
4 + 1 + 2 + 2 + 1 + 4 + ss + sa + ss + sa + 8 * ss + 2 + ss + ss + 2 + 2 + sa + 2 + 4
}
pub fn encoded_size(ctx: &FormatContext) -> usize {
Self::base_size(ctx)
}
pub fn new(params: &HeapParams, ctx: &FormatContext) -> Self {
let mut hdr = Self {
id_len: params.id_len,
filter_len: 0,
checksum_dblocks: params.checksum_dblocks,
max_man_size: params.max_man_size,
huge_next_id: 0,
huge_bt2_addr: UNDEF_ADDR,
total_man_free: 0,
fs_addr: UNDEF_ADDR,
man_size: 0,
man_alloc_size: 0,
man_iter_off: 0,
man_nobjs: 0,
huge_size: 0,
huge_nobjs: 0,
tiny_size: 0,
tiny_nobjs: 0,
table_width: params.table_width,
start_block_size: params.start_block_size,
max_direct_size: params.max_direct_size,
max_heap_size_bits: params.max_heap_size_bits,
start_root_rows: params.start_root_rows,
table_addr: UNDEF_ADDR,
curr_root_rows: 0,
heap_off_size: 0,
max_direct_rows: 0,
row_block_size: Vec::new(),
row_block_off: Vec::new(),
heap_len_size: 0,
huge_id_size: 0,
huge_ids_direct: false,
};
hdr.derive(ctx);
hdr
}
fn derive(&mut self, ctx: &FormatContext) {
let sa = ctx.sizeof_addr as usize;
let ss = ctx.sizeof_size as usize;
let start_bits = log2_of2(self.start_block_size);
let first_row_bits = start_bits + log2_of2(self.table_width as u64);
let max_root_rows = (self.max_heap_size_bits as u32)
.saturating_sub(first_row_bits)
.saturating_add(1);
let max_direct_bits = log2_of2(self.max_direct_size);
self.max_direct_rows = max_direct_bits.saturating_sub(start_bits).saturating_add(2);
self.heap_off_size = size_of_offset_bits(self.max_heap_size_bits);
self.row_block_size = Vec::with_capacity(max_root_rows as usize);
self.row_block_off = Vec::with_capacity(max_root_rows as usize);
let mut tmp = self.start_block_size;
let mut off = 0u64;
for row in 0..max_root_rows {
let size = if row == 0 { self.start_block_size } else { tmp };
self.row_block_size.push(size);
self.row_block_off.push(off);
off = off.saturating_add(size.saturating_mul(self.table_width as u64));
if row > 0 {
tmp = tmp.saturating_mul(2);
}
}
let max_dir_blk_off_size = size_of_offset_bits(log2_of2(self.max_direct_size) as u16);
self.heap_len_size = max_dir_blk_off_size.min(limit_enc_size(self.max_man_size as u64));
let direct_huge_id_size = if self.filter_len > 0 {
sa + ss + 4 + ss
} else {
sa + ss
};
self.huge_ids_direct =
self.id_len >= 1 && direct_huge_id_size <= (self.id_len as usize - 1);
self.huge_id_size = if self.huge_ids_direct {
if self.filter_len > 0 {
(sa + ss + ss) as u8
} else {
(sa + ss) as u8
}
} else if self.id_len as usize - 1 < 8 {
(self.id_len - 1) as u8
} else {
8
};
}
pub fn encode(&self, ctx: &FormatContext) -> Vec<u8> {
let sa = ctx.sizeof_addr as usize;
let ss = ctx.sizeof_size as usize;
let mut buf = Vec::with_capacity(Self::base_size(ctx));
buf.extend_from_slice(&FRHP_SIGNATURE);
buf.push(0); buf.extend_from_slice(&self.id_len.to_le_bytes());
buf.extend_from_slice(&self.filter_len.to_le_bytes());
buf.push(if self.checksum_dblocks {
HDR_FLAG_CHECKSUM_DBLOCKS
} else {
0
});
buf.extend_from_slice(&self.max_man_size.to_le_bytes());
buf.extend_from_slice(&self.huge_next_id.to_le_bytes()[..ss]);
buf.extend_from_slice(&self.huge_bt2_addr.to_le_bytes()[..sa]);
buf.extend_from_slice(&self.total_man_free.to_le_bytes()[..ss]);
buf.extend_from_slice(&self.fs_addr.to_le_bytes()[..sa]);
for stat in [
self.man_size,
self.man_alloc_size,
self.man_iter_off,
self.man_nobjs,
self.huge_size,
self.huge_nobjs,
self.tiny_size,
self.tiny_nobjs,
] {
buf.extend_from_slice(&stat.to_le_bytes()[..ss]);
}
buf.extend_from_slice(&self.table_width.to_le_bytes());
buf.extend_from_slice(&self.start_block_size.to_le_bytes()[..ss]);
buf.extend_from_slice(&self.max_direct_size.to_le_bytes()[..ss]);
buf.extend_from_slice(&self.max_heap_size_bits.to_le_bytes());
buf.extend_from_slice(&self.start_root_rows.to_le_bytes());
buf.extend_from_slice(&self.table_addr.to_le_bytes()[..sa]);
buf.extend_from_slice(&self.curr_root_rows.to_le_bytes());
let cksum = checksum_metadata(&buf);
buf.extend_from_slice(&cksum.to_le_bytes());
debug_assert_eq!(buf.len(), Self::base_size(ctx));
buf
}
pub fn decode(buf: &[u8], ctx: &FormatContext) -> FormatResult<Self> {
let sa = ctx.sizeof_addr as usize;
let ss = ctx.sizeof_size as usize;
let base = Self::base_size(ctx);
need(buf, 0, base)?;
if buf[0..4] != FRHP_SIGNATURE {
return Err(FormatError::InvalidSignature);
}
let version = buf[4];
if version != 0 {
return Err(FormatError::InvalidVersion(version));
}
let mut pos = 5;
let id_len = u16::from_le_bytes([buf[pos], buf[pos + 1]]);
pos += 2;
let filter_len = u16::from_le_bytes([buf[pos], buf[pos + 1]]);
pos += 2;
let heap_flags = buf[pos];
pos += 1;
let checksum_dblocks = heap_flags & HDR_FLAG_CHECKSUM_DBLOCKS != 0;
let max_man_size = u32::from_le_bytes([buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]]);
pos += 4;
let huge_next_id = read_uint(&buf[pos..], ss);
pos += ss;
let huge_bt2_addr = read_uint(&buf[pos..], sa);
pos += sa;
let total_man_free = read_uint(&buf[pos..], ss);
pos += ss;
let fs_addr = read_uint(&buf[pos..], sa);
pos += sa;
let mut stats = [0u64; 8];
for stat in &mut stats {
*stat = read_uint(&buf[pos..], ss);
pos += ss;
}
let [man_size, man_alloc_size, man_iter_off, man_nobjs, huge_size, huge_nobjs, tiny_size, tiny_nobjs] =
stats;
let table_width = u16::from_le_bytes([buf[pos], buf[pos + 1]]);
pos += 2;
let start_block_size = read_uint(&buf[pos..], ss);
pos += ss;
let max_direct_size = read_uint(&buf[pos..], ss);
pos += ss;
let max_heap_size_bits = u16::from_le_bytes([buf[pos], buf[pos + 1]]);
pos += 2;
let start_root_rows = u16::from_le_bytes([buf[pos], buf[pos + 1]]);
pos += 2;
let table_addr = read_uint(&buf[pos..], sa);
pos += sa;
let curr_root_rows = u16::from_le_bytes([buf[pos], buf[pos + 1]]);
pos += 2;
debug_assert_eq!(pos, base - 4);
let stored = u32::from_le_bytes([buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]]);
let computed = checksum_metadata(&buf[..pos]);
if stored != computed {
return Err(FormatError::ChecksumMismatch {
expected: stored,
computed,
});
}
if table_width == 0 || start_block_size == 0 {
return Err(FormatError::InvalidData(
"fractal heap doubling-table has zero width or block size".into(),
));
}
let mut hdr = Self {
id_len,
filter_len,
checksum_dblocks,
max_man_size,
huge_next_id,
huge_bt2_addr,
total_man_free,
fs_addr,
man_size,
man_alloc_size,
man_iter_off,
man_nobjs,
huge_size,
huge_nobjs,
tiny_size,
tiny_nobjs,
table_width,
start_block_size,
max_direct_size,
max_heap_size_bits,
start_root_rows,
table_addr,
curr_root_rows,
heap_off_size: 0,
max_direct_rows: 0,
row_block_size: Vec::new(),
row_block_off: Vec::new(),
heap_len_size: 0,
huge_id_size: 0,
huge_ids_direct: false,
};
hdr.derive(ctx);
Ok(hdr)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HeapParams {
pub id_len: u16,
pub table_width: u16,
pub start_block_size: u64,
pub max_direct_size: u64,
pub max_heap_size_bits: u16,
pub start_root_rows: u16,
pub checksum_dblocks: bool,
pub max_man_size: u32,
}
impl HeapParams {
pub fn object_header() -> Self {
Self {
id_len: 8,
table_width: 4,
start_block_size: 1024,
max_direct_size: 64 * 1024,
max_heap_size_bits: 40,
start_root_rows: 1,
checksum_dblocks: true,
max_man_size: 4 * 1024,
}
}
pub fn group_links() -> Self {
Self {
id_len: 7,
table_width: 4,
start_block_size: 512,
max_direct_size: 64 * 1024,
max_heap_size_bits: 32,
start_root_rows: 1,
checksum_dblocks: true,
max_man_size: 4 * 1024,
}
}
}
#[derive(Debug, Clone)]
pub struct ManagedBlock {
pub heap_offset: u64,
pub payload_start: usize,
pub image: Vec<u8>,
}
impl ManagedBlock {
pub fn payload(&self) -> &[u8] {
&self.image[self.payload_start..]
}
fn object_at(&self, offset: u64, len: usize) -> Option<&[u8]> {
let start = offset.checked_sub(self.heap_offset)? as usize;
if start < self.payload_start {
return None;
}
let end = start.checked_add(len)?;
self.image.get(start..end)
}
}
#[derive(Debug, Default)]
struct HeapWalk {
blocks: Vec<ManagedBlock>,
extents: Vec<(u64, u64)>,
}
pub fn collect_managed_blocks<R: BlockReader>(
header: &FractalHeapHeader,
ctx: &FormatContext,
reader: &mut R,
) -> FormatResult<Vec<ManagedBlock>> {
if header.man_nobjs == 0 {
return Ok(Vec::new());
}
Ok(walk_doubling_table(header, ctx, reader)?.blocks)
}
pub fn collect_heap_extents<R: BlockReader>(
heap_addr: u64,
ctx: &FormatContext,
reader: &mut R,
) -> FormatResult<Vec<(u64, u64)>> {
use crate::format::chunk_index::btree_v2::collect_btree_v2_extents;
let buf = reader.read_block(heap_addr, 512)?;
let header = FractalHeapHeader::decode(&buf, ctx)?;
let mut extents = vec![(heap_addr, FractalHeapHeader::encoded_size(ctx) as u64)];
extents.extend(walk_doubling_table(&header, ctx, reader)?.extents);
if header.huge_nobjs > 0 {
if header.huge_ids_direct {
return Err(FormatError::UnsupportedFeature(
"freeing a fractal heap whose huge-object IDs are direct".into(),
));
}
for (addr, len, _id) in huge_object_records(&header, ctx, reader)? {
extents.push((addr, len));
}
extents.extend(collect_btree_v2_extents(header.huge_bt2_addr, ctx, reader)?);
}
Ok(extents)
}
fn walk_doubling_table<R: BlockReader>(
header: &FractalHeapHeader,
ctx: &FormatContext,
reader: &mut R,
) -> FormatResult<HeapWalk> {
if header.table_addr == UNDEF_ADDR {
return Ok(HeapWalk::default());
}
let mut walker = DoublingTableWalker {
header,
ctx,
reader,
walk: HeapWalk::default(),
budget: MAX_BLOCKS,
};
if header.curr_root_rows == 0 {
walker.read_direct_block(header.table_addr, header.start_block_size as usize)?;
} else {
walker.walk_indirect_block(header.table_addr, header.curr_root_rows as u32, 0)?;
}
Ok(walker.walk)
}
struct DoublingTableWalker<'a, R: BlockReader> {
header: &'a FractalHeapHeader,
ctx: &'a FormatContext,
reader: &'a mut R,
walk: HeapWalk,
budget: usize,
}
pub fn collect_managed_objects<R: BlockReader>(
header: &FractalHeapHeader,
ctx: &FormatContext,
reader: &mut R,
) -> FormatResult<Vec<Vec<u8>>> {
Ok(collect_managed_blocks(header, ctx, reader)?
.into_iter()
.map(|b| b.image[b.payload_start..].to_vec())
.collect())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HeapId {
Managed { offset: u64, length: u64 },
HugeIndirect { id: u64 },
HugeDirect { address: u64, length: u64 },
Tiny { data: Vec<u8> },
}
impl HeapId {
pub fn parse(id: &[u8], header: &FractalHeapHeader, ctx: &FormatContext) -> FormatResult<Self> {
let id_len = header.id_len as usize;
if id.len() < id_len || id_len == 0 {
return Err(FormatError::BufferTooShort {
needed: id_len,
available: id.len(),
});
}
let flags = id[0];
let body = &id[1..id_len];
match flags & ID_TYPE_MASK {
ID_TYPE_MAN => {
let off_size = header.heap_off_size as usize;
let len_size = header.heap_len_size as usize;
if body.len() < off_size + len_size {
return Err(FormatError::InvalidData(
"fractal heap managed ID shorter than its offset+length fields".into(),
));
}
Ok(Self::Managed {
offset: read_uint(body, off_size),
length: read_uint(&body[off_size..], len_size),
})
}
ID_TYPE_HUGE => {
let sa = ctx.sizeof_addr as usize;
let ss = ctx.sizeof_size as usize;
if header.huge_ids_direct {
if body.len() < sa + ss {
return Err(FormatError::InvalidData(
"fractal heap direct huge ID shorter than address+length".into(),
));
}
Ok(Self::HugeDirect {
address: read_uint(body, sa),
length: read_uint(&body[sa..], ss),
})
} else {
let n = (header.huge_id_size as usize).min(body.len());
Ok(Self::HugeIndirect {
id: read_uint(body, n),
})
}
}
ID_TYPE_TINY => {
let (len, data) = if header.id_len <= 16 {
((flags & ID_TINY_LEN_MASK) as usize + 1, body)
} else {
if body.is_empty() {
return Err(FormatError::InvalidData(
"fractal heap extended tiny ID has no length byte".into(),
));
}
(
(((flags & ID_TINY_LEN_MASK) as usize) << 8 | body[0] as usize) + 1,
&body[1..],
)
};
if len > data.len() {
return Err(FormatError::InvalidData(
"fractal heap tiny ID length exceeds the ID itself".into(),
));
}
Ok(Self::Tiny {
data: data[..len].to_vec(),
})
}
_ => Err(FormatError::UnsupportedFeature(
"reserved fractal heap object type".into(),
)),
}
}
}
const ID_TYPE_MASK: u8 = 0x30;
const ID_TYPE_MAN: u8 = 0x00;
const ID_TYPE_HUGE: u8 = 0x10;
const ID_TYPE_TINY: u8 = 0x20;
const ID_TINY_LEN_MASK: u8 = 0x0F;
pub fn read_heap_object<R: BlockReader>(
id: &HeapId,
header: &FractalHeapHeader,
ctx: &FormatContext,
blocks: &[ManagedBlock],
reader: &mut R,
) -> FormatResult<Vec<u8>> {
match *id {
HeapId::Managed { offset, length } => {
let length = usize::try_from(length).map_err(|_| {
FormatError::InvalidData("fractal heap object length overflows usize".into())
})?;
blocks
.iter()
.find_map(|b| b.object_at(offset, length))
.map(|s| s.to_vec())
.ok_or_else(|| {
FormatError::InvalidData(format!(
"fractal heap offset {offset} is outside every managed block"
))
})
}
HeapId::Tiny { ref data } => Ok(data.clone()),
HeapId::HugeDirect { address, length } => read_span(reader, address, length),
HeapId::HugeIndirect { id } => {
let (address, length) = lookup_huge_object(id, header, ctx, reader)?;
read_span(reader, address, length)
}
}
}
fn read_span<R: BlockReader>(reader: &mut R, address: u64, length: u64) -> FormatResult<Vec<u8>> {
let length = usize::try_from(length).map_err(|_| {
FormatError::InvalidData("fractal heap huge object length overflows usize".into())
})?;
let buf = reader.read_block(address, length)?;
need(&buf, 0, length)?;
Ok(buf)
}
fn lookup_huge_object<R: BlockReader>(
target_id: u64,
header: &FractalHeapHeader,
ctx: &FormatContext,
reader: &mut R,
) -> FormatResult<(u64, u64)> {
for (addr, len, id) in huge_object_records(header, ctx, reader)? {
if id == target_id {
return Ok((addr, len));
}
}
Err(FormatError::InvalidData(format!(
"huge object ID {target_id} is not in the heap's huge-object B-tree"
)))
}
fn huge_object_records<R: BlockReader>(
header: &FractalHeapHeader,
ctx: &FormatContext,
reader: &mut R,
) -> FormatResult<Vec<(u64, u64, u64)>> {
use crate::format::chunk_index::btree_v2::{collect_btree_v2_records, Bt2Header};
if header.huge_bt2_addr == UNDEF_ADDR {
return Err(FormatError::InvalidData(
"fractal heap has a huge object ID but no huge-object B-tree".into(),
));
}
let hdr_buf = reader.read_block(header.huge_bt2_addr, 256)?;
let bt2 = Bt2Header::decode(&hdr_buf, ctx)?;
let records = collect_btree_v2_records(&bt2, ctx, reader)?;
let sa = ctx.sizeof_addr as usize;
let ss = ctx.sizeof_size as usize;
if header.filter_len > 0 {
return Err(FormatError::UnsupportedFeature(
"filtered fractal heap huge objects".into(),
));
}
let rec_size = bt2.record_size as usize;
if rec_size < sa + ss + ss {
return Err(FormatError::InvalidData(
"huge-object B-tree record is too small for address+length+ID".into(),
));
}
Ok(records
.chunks_exact(rec_size)
.map(|rec| {
(
read_uint(rec, sa),
read_uint(&rec[sa..], ss),
read_uint(&rec[sa + ss..], ss),
)
})
.collect())
}
impl<R: BlockReader> DoublingTableWalker<'_, R> {
fn walk_indirect_block(&mut self, addr: u64, nrows: u32, depth: usize) -> FormatResult<()> {
const MAX_INDIRECT_DEPTH: usize = 256;
if depth > MAX_INDIRECT_DEPTH {
return Err(FormatError::InvalidData(
"fractal heap indirect-block nesting exceeds maximum depth".into(),
));
}
if addr == u64::MAX || nrows == 0 {
return Ok(());
}
if self.budget == 0 {
return Err(FormatError::InvalidData(
"fractal heap block budget exhausted".into(),
));
}
self.budget -= 1;
let header = self.header;
let ctx = self.ctx;
let sa = ctx.sizeof_addr as usize;
let width = header.table_width as usize;
let n_entries = nrows as usize * width;
let dir_rows = nrows.min(header.max_direct_rows) as usize;
let dir_entries = dir_rows * width;
let per_dir_entry = if header.filter_len > 0 {
sa + ctx.sizeof_size as usize + 4
} else {
sa
};
let indir_entries = n_entries - dir_entries;
let block_len = 4
+ 1
+ sa
+ header.heap_off_size as usize
+ dir_entries * per_dir_entry
+ indir_entries * sa
+ 4;
self.walk.extents.push((addr, block_len as u64));
let buf = self.reader.read_block(addr, block_len)?;
need(&buf, 0, block_len)?;
if buf[0..4] != FHIB_SIGNATURE {
return Err(FormatError::InvalidSignature);
}
if buf[4] != 0 {
return Err(FormatError::InvalidVersion(buf[4]));
}
let csum_off = block_len - 4;
let stored = u32::from_le_bytes([
buf[csum_off],
buf[csum_off + 1],
buf[csum_off + 2],
buf[csum_off + 3],
]);
let computed = checksum_metadata(&buf[..csum_off]);
if stored != computed {
return Err(FormatError::ChecksumMismatch {
expected: stored,
computed,
});
}
let mut pos = 4 + 1 + sa + header.heap_off_size as usize;
for entry in 0..n_entries {
let row = entry / width;
let child_addr = read_uint(&buf[pos..], sa);
pos += sa;
if header.filter_len > 0 && row < header.max_direct_rows as usize {
pos += ctx.sizeof_size as usize + 4;
}
if child_addr == u64::MAX || child_addr == 0 {
continue;
}
if row < header.max_direct_rows as usize {
let size = header
.row_block_size
.get(row)
.copied()
.unwrap_or(header.start_block_size) as usize;
self.read_direct_block(child_addr, size)?;
} else {
let block_size = header
.row_block_size
.get(row)
.copied()
.unwrap_or(header.start_block_size);
let child_nrows = indirect_nrows(header, block_size);
self.walk_indirect_block(child_addr, child_nrows, depth + 1)?;
}
}
Ok(())
}
}
pub(crate) fn indirect_nrows(header: &FractalHeapHeader, block_size: u64) -> u32 {
let start_bits = log2_of2(header.start_block_size);
let first_row_bits = start_bits + log2_of2(header.table_width as u64);
let size_log2 = log2_of2(block_size);
size_log2.saturating_sub(first_row_bits).saturating_add(1)
}
impl<R: BlockReader> DoublingTableWalker<'_, R> {
fn read_direct_block(&mut self, addr: u64, size: usize) -> FormatResult<()> {
if addr == UNDEF_ADDR || size == 0 {
return Ok(());
}
if self.budget == 0 {
return Err(FormatError::InvalidData(
"fractal heap block budget exhausted".into(),
));
}
self.budget -= 1;
self.walk.extents.push((addr, size as u64));
let header = self.header;
let ctx = self.ctx;
let sa = ctx.sizeof_addr as usize;
let buf = self.reader.read_block(addr, size)?;
let prefix_min = 4 + 1 + sa + header.heap_off_size as usize;
if buf.len() < prefix_min {
return Ok(());
}
if buf[0..4] != FHDB_SIGNATURE {
return Err(FormatError::InvalidSignature);
}
if buf[4] != 0 {
return Err(FormatError::InvalidVersion(buf[4]));
}
let mut payload_start = prefix_min;
if header.checksum_dblocks {
let chk_off = prefix_min;
if header.filter_len == 0 && buf.len() >= chk_off + 4 {
let stored = u32::from_le_bytes([
buf[chk_off],
buf[chk_off + 1],
buf[chk_off + 2],
buf[chk_off + 3],
]);
let mut image = buf.clone();
image[chk_off..chk_off + 4].fill(0);
let computed = checksum_metadata(&image);
if stored != computed {
return Err(FormatError::ChecksumMismatch {
expected: stored,
computed,
});
}
}
payload_start += 4;
}
if payload_start >= buf.len() {
return Ok(());
}
let heap_offset = read_uint(&buf[4 + 1 + sa..], header.heap_off_size as usize);
self.walk.blocks.push(ManagedBlock {
heap_offset,
payload_start,
image: buf,
});
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn log2_of2_basic() {
assert_eq!(log2_of2(1), 0);
assert_eq!(log2_of2(2), 1);
assert_eq!(log2_of2(512), 9);
assert_eq!(log2_of2(4096), 12);
assert_eq!(log2_of2(3), 0);
assert_eq!(log2_of2(0), 0);
}
#[test]
fn size_of_offset_bits_basic() {
assert_eq!(size_of_offset_bits(0), 0);
assert_eq!(size_of_offset_bits(8), 1);
assert_eq!(size_of_offset_bits(9), 2);
assert_eq!(size_of_offset_bits(16), 2);
assert_eq!(size_of_offset_bits(17), 3);
}
#[test]
fn bad_signature_rejected() {
let ctx = FormatContext {
sizeof_addr: 8,
sizeof_size: 8,
};
let buf = vec![0u8; FractalHeapHeader::base_size(&ctx)];
let err = FractalHeapHeader::decode(&buf, &ctx).unwrap_err();
assert!(matches!(err, FormatError::InvalidSignature));
}
#[test]
fn too_short_rejected() {
let ctx = FormatContext {
sizeof_addr: 8,
sizeof_size: 8,
};
let buf = vec![0u8; 8];
let err = FractalHeapHeader::decode(&buf, &ctx).unwrap_err();
assert!(matches!(err, FormatError::BufferTooShort { .. }));
}
}