#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
#[cfg(feature = "checksum")]
use byteorder::{ByteOrder, LittleEndian};
use crate::error::FormatError;
#[derive(Debug, Clone)]
pub struct FractalHeapHeader {
pub heap_id_length: u16,
pub io_filter_encoded_length: u16,
pub max_managed_object_size: u32,
pub table_width: u16,
pub starting_block_size: u64,
pub max_direct_block_size: u64,
pub max_heap_size: u16,
pub starting_row_of_indirect_blocks: u16,
pub root_block_address: u64,
pub current_rows_in_root_indirect_block: u16,
pub managed_objects_count: u64,
}
fn read_offset(data: &[u8], pos: usize, size: u8) -> Result<u64, FormatError> {
let s = size as usize;
if pos + s > data.len() {
return Err(FormatError::UnexpectedEof {
expected: pos + s,
available: data.len(),
});
}
Ok(match size {
2 => u16::from_le_bytes([data[pos], data[pos + 1]]) as u64,
4 => u32::from_le_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as u64,
8 => u64::from_le_bytes([
data[pos], data[pos + 1], data[pos + 2], data[pos + 3],
data[pos + 4], data[pos + 5], data[pos + 6], data[pos + 7],
]),
_ => return Err(FormatError::InvalidOffsetSize(size)),
})
}
fn ensure_len(data: &[u8], pos: usize, needed: usize) -> Result<(), FormatError> {
match pos.checked_add(needed) {
Some(end) if end <= data.len() => Ok(()),
_ => Err(FormatError::UnexpectedEof {
expected: pos.saturating_add(needed),
available: data.len(),
}),
}
}
fn is_undefined(val: u64, offset_size: u8) -> bool {
match offset_size {
2 => val == 0xFFFF,
4 => val == 0xFFFF_FFFF,
8 => val == 0xFFFF_FFFF_FFFF_FFFF,
_ => false,
}
}
impl FractalHeapHeader {
pub fn parse(
file_data: &[u8],
offset: usize,
offset_size: u8,
length_size: u8,
) -> Result<FractalHeapHeader, FormatError> {
ensure_len(file_data, offset, 5)?;
if &file_data[offset..offset + 4] != b"FRHP" {
return Err(FormatError::InvalidFractalHeapSignature);
}
let version = file_data[offset + 4];
if version != 0 {
return Err(FormatError::InvalidFractalHeapVersion(version));
}
let os = offset_size as usize;
let ls = length_size as usize;
let mut pos = offset + 5;
ensure_len(file_data, pos, 2)?;
let heap_id_length = u16::from_le_bytes([file_data[pos], file_data[pos + 1]]);
pos += 2;
ensure_len(file_data, pos, 2)?;
let io_filter_encoded_length = u16::from_le_bytes([file_data[pos], file_data[pos + 1]]);
pos += 2;
ensure_len(file_data, pos, 1)?;
let _flags = file_data[pos];
pos += 1;
ensure_len(file_data, pos, 4)?;
let max_managed_object_size = u32::from_le_bytes([
file_data[pos], file_data[pos + 1], file_data[pos + 2], file_data[pos + 3],
]);
pos += 4;
let skip_size = 5 * ls + 2 * os;
ensure_len(file_data, pos, skip_size)?;
pos += skip_size;
let managed_objects_count = read_offset(file_data, pos, length_size)?;
pos += ls;
pos += ls;
pos += ls;
pos += ls;
pos += ls;
ensure_len(file_data, pos, 2)?;
let table_width = u16::from_le_bytes([file_data[pos], file_data[pos + 1]]);
pos += 2;
let starting_block_size = read_offset(file_data, pos, length_size)?;
pos += ls;
let max_direct_block_size = read_offset(file_data, pos, length_size)?;
pos += ls;
ensure_len(file_data, pos, 2)?;
let max_heap_size = u16::from_le_bytes([file_data[pos], file_data[pos + 1]]);
pos += 2;
ensure_len(file_data, pos, 2)?;
let starting_row_of_indirect_blocks =
u16::from_le_bytes([file_data[pos], file_data[pos + 1]]);
pos += 2;
let root_block_address = read_offset(file_data, pos, offset_size)?;
pos += os;
ensure_len(file_data, pos, 2)?;
let current_rows_in_root_indirect_block =
u16::from_le_bytes([file_data[pos], file_data[pos + 1]]);
#[allow(unused_variables, unused_mut, unused_assignments)]
let mut pos = pos + 2;
if io_filter_encoded_length > 0 {
#[allow(unused_assignments)]
{
pos += ls + 4;
}
}
#[cfg(feature = "checksum")]
{
ensure_len(file_data, pos, 4)?;
let stored = LittleEndian::read_u32(&file_data[pos..pos + 4]);
let computed = crate::checksum::jenkins_lookup3(&file_data[offset..pos]);
if computed != stored {
return Err(FormatError::ChecksumMismatch {
expected: stored,
computed,
});
}
}
Ok(FractalHeapHeader {
heap_id_length,
io_filter_encoded_length,
max_managed_object_size,
table_width,
starting_block_size,
max_direct_block_size,
max_heap_size,
starting_row_of_indirect_blocks,
root_block_address,
current_rows_in_root_indirect_block,
managed_objects_count,
})
}
pub fn decode_managed_id(&self, id_bytes: &[u8]) -> Result<(u64, u64), FormatError> {
if id_bytes.is_empty() {
return Err(FormatError::UnexpectedEof {
expected: 1,
available: 0,
});
}
let id_type = (id_bytes[0] >> 6) & 0x03;
if id_type != 0 {
return Err(FormatError::InvalidHeapIdType(id_type));
}
let payload = &id_bytes[1..];
let mut combined: u64 = 0;
for (i, &b) in payload.iter().enumerate() {
if i >= 8 {
break;
}
combined |= (b as u64) << (i * 8);
}
let offset_bits = self.max_heap_size as u32;
let offset_mask = if offset_bits >= 64 {
u64::MAX
} else {
(1u64 << offset_bits) - 1
};
let heap_offset = combined & offset_mask;
let total_payload_bits = (payload.len() as u32) * 8;
let length_bits = total_payload_bits.saturating_sub(offset_bits);
let length_val = if length_bits == 0 {
0
} else {
let length_mask = if length_bits >= 64 {
u64::MAX
} else {
(1u64 << length_bits) - 1
};
(combined >> offset_bits) & length_mask
};
Ok((heap_offset, length_val))
}
pub fn read_managed_object(
&self,
file_data: &[u8],
id_bytes: &[u8],
offset_size: u8,
) -> Result<Vec<u8>, FormatError> {
let (heap_offset, obj_len) = self.decode_managed_id(id_bytes)?;
if is_undefined(self.root_block_address, offset_size) {
return Err(FormatError::UnexpectedEof {
expected: 1,
available: 0,
});
}
if self.current_rows_in_root_indirect_block == 0 {
self.read_from_direct_block(
file_data,
self.root_block_address as usize,
self.starting_block_size,
0, heap_offset,
obj_len as usize,
offset_size,
)
} else {
self.read_from_indirect_block(
file_data,
self.root_block_address as usize,
self.current_rows_in_root_indirect_block,
0, heap_offset,
obj_len as usize,
offset_size,
64, )
}
}
#[allow(clippy::too_many_arguments)]
fn read_from_direct_block(
&self,
file_data: &[u8],
block_addr: usize,
_block_size: u64,
block_heap_offset: u64,
target_offset: u64,
length: usize,
_offset_size: u8,
) -> Result<Vec<u8>, FormatError> {
let local_offset = (target_offset - block_heap_offset) as usize;
let pos = block_addr + local_offset;
ensure_len(file_data, pos, length)?;
Ok(file_data[pos..pos + length].to_vec())
}
#[allow(clippy::too_many_arguments)]
fn read_from_indirect_block(
&self,
file_data: &[u8],
iblock_addr: usize,
nrows: u16,
iblock_heap_offset: u64,
target_offset: u64,
length: usize,
offset_size: u8,
depth_remaining: u16,
) -> Result<Vec<u8>, FormatError> {
if depth_remaining == 0 {
return Err(FormatError::ChunkedReadError(
"fractal heap: maximum recursion depth exceeded".into(),
));
}
ensure_len(file_data, iblock_addr, 4)?;
if &file_data[iblock_addr..iblock_addr + 4] != b"FHIB" {
return Err(FormatError::InvalidFractalHeapSignature);
}
let block_offset_bytes = (self.max_heap_size as usize).div_ceil(8);
let iblock_header = 5 + offset_size as usize + block_offset_bytes;
let mut pos = iblock_addr + iblock_header;
let tw = self.table_width as u64;
let nrows_usize = nrows as usize;
let mut current_heap_offset = iblock_heap_offset;
let start_indirect = self.starting_row_of_indirect_blocks as usize;
let max_direct_rows = nrows_usize.min(start_indirect);
for row in 0..max_direct_rows {
let block_size = self.block_size_for_row(row);
for _col in 0..tw {
let child_addr = read_offset(file_data, pos, offset_size)?;
pos += offset_size as usize;
if self.io_filter_encoded_length > 0 {
pos += 4; }
if !is_undefined(child_addr, offset_size) {
let block_end = current_heap_offset + block_size;
if target_offset >= current_heap_offset && target_offset < block_end {
return self.read_from_direct_block(
file_data,
child_addr as usize,
block_size,
current_heap_offset,
target_offset,
length,
offset_size,
);
}
}
current_heap_offset += block_size;
}
}
for row in start_indirect..nrows_usize {
let block_size = self.block_size_for_row(row);
let child_nrows = row - start_indirect + 1;
for _col in 0..tw {
let child_addr = read_offset(file_data, pos, offset_size)?;
pos += offset_size as usize;
if !is_undefined(child_addr, offset_size) {
let total_child_space = self.indirect_block_heap_size(child_nrows);
let block_end = current_heap_offset + total_child_space;
if target_offset >= current_heap_offset && target_offset < block_end {
return self.read_from_indirect_block(
file_data,
child_addr as usize,
child_nrows as u16,
current_heap_offset,
target_offset,
length,
offset_size,
depth_remaining - 1,
);
}
current_heap_offset += total_child_space;
} else {
let total_child_space = self.indirect_block_heap_size(child_nrows);
current_heap_offset += total_child_space;
}
}
let _ = block_size;
}
Err(FormatError::UnexpectedEof {
expected: target_offset as usize + length,
available: file_data.len(),
})
}
fn block_size_for_row(&self, row: usize) -> u64 {
let sbs = self.starting_block_size;
if row <= 1 {
sbs
} else {
sbs * (1u64 << (row - 1))
}
}
fn indirect_block_heap_size(&self, nrows: usize) -> u64 {
let tw = self.table_width as u64;
let mut total = 0u64;
for row in 0..nrows {
total += self.block_size_for_row(row) * tw;
}
total
}
}
#[cfg(test)]
mod tests {
use super::*;
fn build_simple_heap(offset_size: u8, length_size: u8) -> (Vec<u8>, usize) {
let os = offset_size as usize;
let ls = length_size as usize;
let max_heap_size: u16 = 16; let block_offset_bytes = (max_heap_size as usize).div_ceil(8);
let dblock_offset = 256usize;
let block_size: u64 = 128;
let mut buf = vec![0u8; 1024];
let mut pos = 0;
buf[pos..pos + 4].copy_from_slice(b"FRHP");
pos += 4;
buf[pos] = 0; pos += 1;
buf[pos..pos + 2].copy_from_slice(&7u16.to_le_bytes());
pos += 2;
buf[pos..pos + 2].copy_from_slice(&0u16.to_le_bytes());
pos += 2;
buf[pos] = 0;
pos += 1;
buf[pos..pos + 4].copy_from_slice(&64u32.to_le_bytes());
pos += 4;
pos += ls;
for i in 0..os { buf[pos + i] = 0xFF; }
pos += os;
pos += ls;
for i in 0..os { buf[pos + i] = 0xFF; }
pos += os;
pos += ls;
pos += ls;
pos += ls;
buf[pos] = 1;
pos += ls;
pos += ls;
pos += ls;
pos += ls;
pos += ls;
buf[pos..pos + 2].copy_from_slice(&4u16.to_le_bytes());
pos += 2;
match length_size {
4 => buf[pos..pos + 4].copy_from_slice(&(block_size as u32).to_le_bytes()),
8 => buf[pos..pos + 8].copy_from_slice(&block_size.to_le_bytes()),
_ => {}
}
pos += ls;
match length_size {
4 => buf[pos..pos + 4].copy_from_slice(&1024u32.to_le_bytes()),
8 => buf[pos..pos + 8].copy_from_slice(&1024u64.to_le_bytes()),
_ => {}
}
pos += ls;
buf[pos..pos + 2].copy_from_slice(&max_heap_size.to_le_bytes());
pos += 2;
buf[pos..pos + 2].copy_from_slice(&2u16.to_le_bytes());
pos += 2;
match offset_size {
4 => buf[pos..pos + 4].copy_from_slice(&(dblock_offset as u32).to_le_bytes()),
8 => buf[pos..pos + 8].copy_from_slice(&(dblock_offset as u64).to_le_bytes()),
_ => {}
}
pos += os;
buf[pos..pos + 2].copy_from_slice(&0u16.to_le_bytes());
pos += 2;
let checksum = crate::checksum::jenkins_lookup3(&buf[0..pos]);
buf[pos..pos + 4].copy_from_slice(&checksum.to_le_bytes());
pos += 4;
let header_end = pos;
pos = dblock_offset;
buf[pos..pos + 4].copy_from_slice(b"FHDB");
pos += 4;
buf[pos] = 0; pos += 1;
pos += os;
pos += block_offset_bytes;
let data_start = pos;
let test_data = b"Hello, World!";
buf[data_start..data_start + test_data.len()].copy_from_slice(test_data);
(buf, header_end)
}
#[test]
fn parse_header() {
let (file_data, _) = build_simple_heap(8, 8);
let hdr = FractalHeapHeader::parse(&file_data, 0, 8, 8).unwrap();
assert_eq!(hdr.heap_id_length, 7);
assert_eq!(hdr.io_filter_encoded_length, 0);
assert_eq!(hdr.max_managed_object_size, 64);
assert_eq!(hdr.table_width, 4);
assert_eq!(hdr.starting_block_size, 128);
assert_eq!(hdr.max_heap_size, 16);
assert_eq!(hdr.current_rows_in_root_indirect_block, 0);
assert_eq!(hdr.managed_objects_count, 1);
}
#[test]
fn decode_managed_id() {
let (file_data, _) = build_simple_heap(8, 8);
let hdr = FractalHeapHeader::parse(&file_data, 0, 8, 8).unwrap();
let offset: u64 = 0;
let length: u64 = 13;
let payload = offset | (length << hdr.max_heap_size);
let mut id = vec![0u8; 7];
id[0] = 0x00; for i in 0..6 {
id[1 + i] = ((payload >> (i * 8)) & 0xFF) as u8;
}
let (off, len) = hdr.decode_managed_id(&id).unwrap();
assert_eq!(off, 0);
assert_eq!(len, 13);
}
#[test]
fn read_managed_object_from_direct_block() {
let (file_data, _) = build_simple_heap(8, 8);
let hdr = FractalHeapHeader::parse(&file_data, 0, 8, 8).unwrap();
let dblock_header_size = 5 + 8 + ((hdr.max_heap_size as usize + 7) / 8); let offset: u64 = dblock_header_size as u64;
let length: u64 = 13;
let payload = offset | (length << hdr.max_heap_size);
let mut id = vec![0u8; 7];
id[0] = 0x00;
for i in 0..6 {
id[1 + i] = ((payload >> (i * 8)) & 0xFF) as u8;
}
let obj = hdr.read_managed_object(&file_data, &id, 8).unwrap();
assert_eq!(&obj, b"Hello, World!");
}
#[test]
fn invalid_signature() {
let mut data = vec![0u8; 128];
data[0..4].copy_from_slice(b"XXXX");
let err = FractalHeapHeader::parse(&data, 0, 8, 8).unwrap_err();
assert_eq!(err, FormatError::InvalidFractalHeapSignature);
}
#[test]
fn invalid_version() {
let mut data = vec![0u8; 128];
data[0..4].copy_from_slice(b"FRHP");
data[4] = 1; let err = FractalHeapHeader::parse(&data, 0, 8, 8).unwrap_err();
assert_eq!(err, FormatError::InvalidFractalHeapVersion(1));
}
#[test]
fn invalid_heap_id_type() {
let (file_data, _) = build_simple_heap(8, 8);
let hdr = FractalHeapHeader::parse(&file_data, 0, 8, 8).unwrap();
let id = vec![0x40u8, 0, 0, 0, 0, 0, 0]; let err = hdr.decode_managed_id(&id).unwrap_err();
assert_eq!(err, FormatError::InvalidHeapIdType(1));
}
}