use crate::format::checksum::checksum_metadata;
use crate::format::chunk_index::btree_v2::{
collect_btree_v2_records, Bt2Header, Bt2Tree, BT2_TYPE_GRP_CORDER, BT2_TYPE_GRP_NAME,
};
use crate::format::creation_order::CreationOrder;
use crate::format::fractal_heap::{
collect_managed_blocks, read_heap_object, FractalHeapHeader, HeapId, HeapParams,
};
use crate::format::fractal_heap_write::{build_heap, HeapBlock};
use crate::format::messages::link::LinkMessage;
use crate::format::messages::link_info::LinkInfoMessage;
use crate::format::{BlockReader, FormatContext, FormatError, FormatResult, UNDEF_ADDR};
const FHEAP_ID_LEN: usize = 7;
const NAME_RECORD_LEN: usize = 4 + FHEAP_ID_LEN;
const CORDER_RECORD_LEN: usize = 8 + FHEAP_ID_LEN;
const NAME_BT2_NODE_SIZE: u32 = 512;
pub fn name_hash(name: &str) -> u32 {
checksum_metadata(name.as_bytes())
}
pub fn read_dense_links<R: BlockReader>(
linfo: &LinkInfoMessage,
ctx: &FormatContext,
reader: &mut R,
) -> FormatResult<Vec<LinkMessage>> {
if linfo.fractal_heap_address == UNDEF_ADDR {
return Ok(Vec::new());
}
if linfo.name_btree_address == UNDEF_ADDR {
return Err(FormatError::InvalidData(
"dense link storage without a name index B-tree".into(),
));
}
let heap_buf = reader.read_block(linfo.fractal_heap_address, 512)?;
let heap = FractalHeapHeader::decode(&heap_buf, ctx)?;
let blocks = collect_managed_blocks(&heap, ctx, reader)?;
let bt2_buf = reader.read_block(linfo.name_btree_address, 256)?;
let bt2 = Bt2Header::decode(&bt2_buf, ctx)?;
if bt2.record_type != BT2_TYPE_GRP_NAME {
return Err(FormatError::InvalidData(format!(
"link name index has B-tree record type {}, expected {}",
bt2.record_type, BT2_TYPE_GRP_NAME
)));
}
if (bt2.record_size as usize) < NAME_RECORD_LEN {
return Err(FormatError::InvalidData(format!(
"link name index record is {} bytes, expected at least {}",
bt2.record_size, NAME_RECORD_LEN
)));
}
let records = collect_btree_v2_records(&bt2, ctx, reader)?;
let rec_size = bt2.record_size as usize;
let mut links = Vec::with_capacity(records.len() / rec_size);
for rec in records.chunks_exact(rec_size) {
let id = HeapId::parse(&rec[4..4 + FHEAP_ID_LEN], &heap, ctx)?;
let bytes = read_heap_object(&id, &heap, ctx, &blocks, reader)?;
links.push(LinkMessage::decode(&bytes, ctx)?.0);
}
Ok(links)
}
#[derive(Debug, Clone, PartialEq)]
pub struct DenseLinkStorage {
pub linfo: LinkInfoMessage,
pub blocks: Vec<HeapBlock>,
}
pub fn build_dense_links(
links: &[LinkMessage],
ctx: &FormatContext,
order: CreationOrder,
alloc: &mut dyn FnMut(u64) -> u64,
) -> FormatResult<DenseLinkStorage> {
let objects: Vec<Vec<u8>> = links.iter().map(|l| l.encode(ctx)).collect();
let heap = build_heap(&HeapParams::group_links(), ctx, &objects, alloc)?;
let mut by_name: Vec<usize> = (0..links.len()).collect();
by_name.sort_by(|&a, &b| {
name_hash(&links[a].name)
.cmp(&name_hash(&links[b].name))
.then_with(|| links[a].name.cmp(&links[b].name))
});
let mut records = Vec::with_capacity(by_name.len() * NAME_RECORD_LEN);
for &i in &by_name {
records.extend_from_slice(&name_hash(&links[i].name).to_le_bytes());
records.extend_from_slice(&heap.ids[i]);
}
let mut blocks = heap.blocks;
let bt2_addr = build_index(
BT2_TYPE_GRP_NAME,
NAME_RECORD_LEN as u16,
&records,
ctx,
alloc,
&mut blocks,
);
let corders: Option<Vec<i64>> = order
.is_tracked()
.then(|| links.iter().map(|l| l.creation_order).collect())
.flatten();
if order.is_tracked() && corders.is_none() {
return Err(FormatError::InvalidData(
"a group tracking link creation order has a link with no creation order".into(),
));
}
let corder_bt2_addr = order.is_indexed().then(|| {
let corders = corders.as_ref().expect("indexed implies tracked");
let mut by_corder: Vec<usize> = (0..links.len()).collect();
by_corder.sort_by_key(|&i| corders[i]);
let mut records = Vec::with_capacity(by_corder.len() * CORDER_RECORD_LEN);
for &i in &by_corder {
records.extend_from_slice(&corders[i].to_le_bytes());
records.extend_from_slice(&heap.ids[i]);
}
build_index(
BT2_TYPE_GRP_CORDER,
CORDER_RECORD_LEN as u16,
&records,
ctx,
alloc,
&mut blocks,
)
});
Ok(DenseLinkStorage {
linfo: LinkInfoMessage {
max_creation_order: corders.map(|c| c.len() as u64),
fractal_heap_address: heap.header_addr,
name_btree_address: bt2_addr,
creation_order_btree_address: corder_bt2_addr,
},
blocks,
})
}
fn build_index(
record_type: u8,
record_size: u16,
records: &[u8],
ctx: &FormatContext,
alloc: &mut dyn FnMut(u64) -> u64,
blocks: &mut Vec<HeapBlock>,
) -> u64 {
let tree = Bt2Tree::build(
record_type,
record_size,
NAME_BT2_NODE_SIZE,
ctx.sizeof_addr,
records,
);
let bt2_addr = alloc(tree.header(UNDEF_ADDR).encoded_size(ctx) as u64);
let node_addrs: Vec<u64> = tree
.nodes
.iter()
.map(|_| alloc(tree.node_size as u64))
.collect();
for (image, &addr) in tree.encode(ctx, &node_addrs).into_iter().zip(&node_addrs) {
blocks.push(HeapBlock {
addr,
len: tree.node_size as u64,
image,
});
}
let root_addr = node_addrs.last().copied().unwrap_or(UNDEF_ADDR);
let image = tree.header(root_addr).encode(ctx);
blocks.push(HeapBlock {
addr: bt2_addr,
len: image.len() as u64,
image,
});
bt2_addr
}
#[cfg(test)]
mod tests {
use super::*;
fn ctx() -> FormatContext {
FormatContext {
sizeof_addr: 8,
sizeof_size: 8,
}
}
struct MemFile {
bytes: Vec<u8>,
}
impl MemFile {
fn new() -> Self {
Self { bytes: vec![0; 16] }
}
fn alloc(&mut self, len: u64) -> u64 {
let addr = self.bytes.len() as u64;
self.bytes.resize(self.bytes.len() + len as usize, 0);
addr
}
}
impl BlockReader for MemFile {
fn read_block(&mut self, offset: u64, len: usize) -> FormatResult<Vec<u8>> {
let start = offset as usize;
if start > self.bytes.len() {
return Err(FormatError::BufferTooShort {
needed: start,
available: self.bytes.len(),
});
}
let end = (start + len).min(self.bytes.len());
Ok(self.bytes[start..end].to_vec())
}
}
fn round_trip(links: &[LinkMessage]) -> (MemFile, DenseLinkStorage, Vec<LinkMessage>) {
round_trip_ordered(links, CreationOrder::Untracked)
}
fn round_trip_ordered(
links: &[LinkMessage],
order: CreationOrder,
) -> (MemFile, DenseLinkStorage, Vec<LinkMessage>) {
let mut file = MemFile::new();
let dense = build_dense_links(links, &ctx(), order, &mut |len| file.alloc(len)).unwrap();
for block in &dense.blocks {
assert_eq!(block.len as usize, block.image.len(), "block len vs image");
let at = block.addr as usize;
file.bytes[at..at + block.image.len()].copy_from_slice(&block.image);
}
let read = read_dense_links(&dense.linfo, &ctx(), &mut file).unwrap();
(file, dense, read)
}
#[test]
fn compact_linfo_reads_no_dense_links() {
let mut file = MemFile::new();
assert!(
read_dense_links(&LinkInfoMessage::compact(), &ctx(), &mut file)
.unwrap()
.is_empty()
);
}
#[test]
fn a_dozen_links_round_trip_through_dense_storage() {
let links: Vec<LinkMessage> = (0..12)
.map(|i| LinkMessage::hard(&format!("d{i:02}"), 0x400 + i as u64 * 8))
.collect();
let (_file, _dense, read) = round_trip(&links);
assert_eq!(read.len(), links.len());
for want in &links {
let got = read
.iter()
.find(|l| l.name == want.name)
.unwrap_or_else(|| panic!("'{}' missing from dense storage", want.name));
assert_eq!(got, want);
}
}
#[test]
fn a_soft_link_round_trips_beside_hard_ones() {
let links = vec![
LinkMessage::hard("orig", 0x800),
LinkMessage::soft("alias", "/orig"),
];
let (_file, _dense, read) = round_trip(&links);
assert_eq!(read.len(), 2);
for want in &links {
assert_eq!(read.iter().find(|l| l.name == want.name).unwrap(), want);
}
}
#[test]
fn a_group_with_no_links_yields_an_empty_index() {
let (_file, _dense, read) = round_trip(&[]);
assert!(read.is_empty());
}
#[test]
fn the_heap_uses_the_group_parameters() {
let links: Vec<LinkMessage> = (0..12)
.map(|i| LinkMessage::hard(&format!("d{i:02}"), 0x400 + i as u64 * 8))
.collect();
let (mut file, dense, _read) = round_trip(&links);
let heap_buf = file
.read_block(dense.linfo.fractal_heap_address, 512)
.unwrap();
let heap = FractalHeapHeader::decode(&heap_buf, &ctx()).unwrap();
assert_eq!(heap.id_len, 7);
assert_eq!(heap.heap_off_size, 4);
assert_eq!(heap.heap_len_size, 2);
assert_eq!(heap.start_block_size, 512);
assert_eq!(heap.man_nobjs, 12);
}
#[test]
fn name_records_are_ordered_by_hash() {
let links: Vec<LinkMessage> = (0..128)
.map(|i| LinkMessage::hard(&format!("d{i:03}"), 0x400 + i as u64 * 8))
.collect();
let (mut file, dense, read) = round_trip(&links);
assert_eq!(read.len(), links.len());
let bt2_buf = file
.read_block(dense.linfo.name_btree_address, 256)
.unwrap();
let bt2 = Bt2Header::decode(&bt2_buf, &ctx()).unwrap();
assert_eq!(bt2.record_type, BT2_TYPE_GRP_NAME);
assert_eq!(bt2.record_size as usize, NAME_RECORD_LEN);
assert!(bt2.depth > 0, "expected a multi-level index, got one leaf");
let records = collect_btree_v2_records(&bt2, &ctx(), &mut file).unwrap();
let hashes: Vec<u32> = records
.as_chunks::<NAME_RECORD_LEN>()
.0
.iter()
.map(|r| u32::from_le_bytes(r[0..4].try_into().unwrap()))
.collect();
assert_eq!(hashes.len(), links.len());
assert!(
hashes.windows(2).all(|w| w[0] <= w[1]),
"name index is not hash-ordered: {hashes:?}"
);
}
#[test]
fn a_tracked_group_gets_a_creation_order_index() {
let links: Vec<LinkMessage> = (0..12u32)
.map(|i| {
LinkMessage::hard(&format!("d{:02}", 11 - i), 0x400 + i as u64 * 8)
.with_creation_order(i as i64)
})
.collect();
let (mut file, dense, read) = round_trip_ordered(&links, CreationOrder::Indexed);
assert_eq!(read.len(), links.len());
assert_eq!(dense.linfo.max_creation_order, Some(12));
let addr = dense
.linfo
.creation_order_btree_address
.expect("tracked links must carry a creation-order index");
let bt2 = Bt2Header::decode(&file.read_block(addr, 256).unwrap(), &ctx()).unwrap();
assert_eq!(bt2.record_type, BT2_TYPE_GRP_CORDER);
assert_eq!(bt2.record_size as usize, CORDER_RECORD_LEN);
let records = collect_btree_v2_records(&bt2, &ctx(), &mut file).unwrap();
let corders: Vec<i64> = records
.as_chunks::<CORDER_RECORD_LEN>()
.0
.iter()
.map(|r| i64::from_le_bytes(r[0..8].try_into().unwrap()))
.collect();
assert_eq!(corders, (0..12i64).collect::<Vec<_>>());
}
#[test]
fn an_untracked_group_gets_no_creation_order_index() {
let links: Vec<LinkMessage> = (0..12)
.map(|i| LinkMessage::hard(&format!("d{i:02}"), 0x400 + i as u64 * 8))
.collect();
let (_file, dense, _read) = round_trip(&links);
assert_eq!(dense.linfo.creation_order_btree_address, None);
assert_eq!(dense.linfo.max_creation_order, None);
}
#[test]
fn a_tracked_but_unindexed_group_records_the_maximum_and_no_index() {
let links: Vec<LinkMessage> = (0..12u32)
.map(|i| {
LinkMessage::hard(&format!("d{i:02}"), 0x400 + i as u64 * 8)
.with_creation_order(i as i64)
})
.collect();
let (_file, dense, read) = round_trip_ordered(&links, CreationOrder::Tracked);
assert_eq!(read.len(), links.len());
assert_eq!(dense.linfo.max_creation_order, Some(12));
assert_eq!(dense.linfo.creation_order_btree_address, None);
}
#[test]
fn tracking_links_that_carry_no_creation_order_is_refused() {
let links: Vec<LinkMessage> = (0..12)
.map(|i| LinkMessage::hard(&format!("d{i:02}"), 0x400 + i as u64 * 8))
.collect();
let mut file = MemFile::new();
let err = build_dense_links(&links, &ctx(), CreationOrder::Tracked, &mut |len| {
file.alloc(len)
})
.unwrap_err();
assert!(matches!(err, FormatError::InvalidData(_)), "{err:?}");
}
#[test]
fn dense_linfo_without_name_index_is_an_error() {
let linfo = LinkInfoMessage {
max_creation_order: None,
fractal_heap_address: 512,
name_btree_address: UNDEF_ADDR,
creation_order_btree_address: None,
};
let mut file = MemFile::new();
let err = read_dense_links(&linfo, &ctx(), &mut file).unwrap_err();
assert!(matches!(err, FormatError::InvalidData(_)));
}
}