use crate::format::checksum::checksum_metadata;
use crate::format::chunk_index::btree_v2::{
build_index as build_btree_v2_index, collect_btree_v2_records, Bt2Header, BT2_TYPE_ATTR_CORDER,
BT2_TYPE_ATTR_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::attr_info::{
next_creation_index, AttributeInfoMessage, MAX_CREATION_ORDER_INDEX,
};
use crate::format::messages::attribute::AttributeEntry;
use crate::format::messages::MSG_FLAG_SHARED;
use crate::format::{BlockReader, FormatContext, FormatError, FormatResult, UNDEF_ADDR};
const FHEAP_ID_LEN: usize = 8;
const NAME_RECORD_LEN: usize = FHEAP_ID_LEN + 1 + 4 + 4;
const CORDER_RECORD_LEN: usize = FHEAP_ID_LEN + 1 + 4;
const NAME_BT2_NODE_SIZE: u32 = 512;
pub fn name_hash(name: &str) -> u32 {
checksum_metadata(name.as_bytes())
}
pub fn read_dense_attributes<R: BlockReader>(
ainfo: &AttributeInfoMessage,
ctx: &FormatContext,
reader: &mut R,
) -> FormatResult<Vec<AttributeEntry>> {
if !ainfo.is_dense() {
return Ok(Vec::new());
}
if ainfo.name_btree_address == UNDEF_ADDR {
return Err(FormatError::InvalidData(
"dense attribute storage without a name index B-tree".into(),
));
}
let heap_buf = reader.read_block(ainfo.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(ainfo.name_btree_address, 256)?;
let bt2 = Bt2Header::decode(&bt2_buf, ctx)?;
if bt2.record_type != BT2_TYPE_ATTR_NAME {
return Err(FormatError::InvalidData(format!(
"attribute name index has B-tree record type {}, expected {}",
bt2.record_type, BT2_TYPE_ATTR_NAME
)));
}
if (bt2.record_size as usize) < NAME_RECORD_LEN {
return Err(FormatError::InvalidData(format!(
"attribute 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 attrs = Vec::with_capacity(records.len() / rec_size);
for rec in records.chunks_exact(rec_size) {
if rec[FHEAP_ID_LEN] & MSG_FLAG_SHARED != 0 {
return Err(FormatError::UnsupportedFeature(
"shared (SOHM) dense attribute".into(),
));
}
let id = HeapId::parse(&rec[..FHEAP_ID_LEN], &heap, ctx)?;
let bytes = read_heap_object(&id, &heap, ctx, &blocks, reader)?;
let corder = u32::from_le_bytes([
rec[FHEAP_ID_LEN + 1],
rec[FHEAP_ID_LEN + 2],
rec[FHEAP_ID_LEN + 3],
rec[FHEAP_ID_LEN + 4],
]);
attrs.push(AttributeEntry::parse(&bytes, ctx)?.with_creation_index(decoded_corder(corder)));
}
Ok(attrs)
}
fn decoded_corder(corder: u32) -> Option<u16> {
u16::try_from(corder)
.ok()
.filter(|&c| c != MAX_CREATION_ORDER_INDEX)
}
#[derive(Debug, Clone, PartialEq)]
pub struct DenseAttributeStorage {
pub ainfo: AttributeInfoMessage,
pub blocks: Vec<HeapBlock>,
}
pub fn build_dense_attributes(
attrs: &[AttributeEntry],
ctx: &FormatContext,
order: CreationOrder,
alloc: &mut dyn FnMut(u64) -> u64,
) -> FormatResult<DenseAttributeStorage> {
let objects: Vec<Vec<u8>> = attrs.iter().map(|a| a.encode(ctx)).collect();
let heap = build_heap(&HeapParams::object_header(), ctx, &objects, alloc)?;
let mut by_name: Vec<usize> = (0..attrs.len()).collect();
by_name.sort_by(|&a, &b| {
name_hash(attrs[a].name())
.cmp(&name_hash(attrs[b].name()))
.then_with(|| attrs[a].name().cmp(attrs[b].name()))
});
let corder = |i: usize| -> u32 {
match (order.is_tracked(), attrs[i].creation_index()) {
(true, Some(idx)) => u32::from(idx),
_ => u32::from(MAX_CREATION_ORDER_INDEX),
}
};
let mut records = Vec::with_capacity(by_name.len() * NAME_RECORD_LEN);
for &i in &by_name {
records.extend_from_slice(&heap.ids[i]);
records.push(0);
records.extend_from_slice(&corder(i).to_le_bytes());
records.extend_from_slice(&name_hash(attrs[i].name()).to_le_bytes());
}
let mut blocks = heap.blocks;
let bt2_addr = build_index(
BT2_TYPE_ATTR_NAME,
NAME_RECORD_LEN as u16,
&records,
ctx,
alloc,
&mut blocks,
);
let corder_bt2_addr = order.is_indexed().then(|| {
let mut by_corder: Vec<usize> = (0..attrs.len()).collect();
by_corder.sort_by_key(|&i| corder(i));
let mut records = Vec::with_capacity(attrs.len() * CORDER_RECORD_LEN);
for &i in &by_corder {
records.extend_from_slice(&heap.ids[i]);
records.push(0);
records.extend_from_slice(&corder(i).to_le_bytes());
}
build_index(
BT2_TYPE_ATTR_CORDER,
CORDER_RECORD_LEN as u16,
&records,
ctx,
alloc,
&mut blocks,
)
});
Ok(DenseAttributeStorage {
ainfo: AttributeInfoMessage {
max_creation_index: order.is_tracked().then(|| next_creation_index(attrs)),
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 (bt2_addr, nodes) = build_btree_v2_index(
record_type,
record_size,
NAME_BT2_NODE_SIZE,
records,
ctx,
alloc,
);
blocks.extend(nodes.into_iter().map(|(addr, image)| HeapBlock {
addr,
len: image.len() as u64,
image,
}));
bt2_addr
}
#[cfg(test)]
mod tests {
use super::*;
use crate::format::messages::attribute::AttributeMessage;
use crate::format::messages::datatype::DatatypeMessage;
struct SliceReader<'a>(&'a [u8]);
impl BlockReader for SliceReader<'_> {
fn read_block(&mut self, offset: u64, len: usize) -> FormatResult<Vec<u8>> {
let start = offset as usize;
if start > self.0.len() {
return Err(FormatError::BufferTooShort {
needed: start,
available: self.0.len(),
});
}
let end = (start + len).min(self.0.len());
Ok(self.0[start..end].to_vec())
}
}
fn ctx() -> FormatContext {
FormatContext {
sizeof_addr: 8,
sizeof_size: 8,
}
}
#[test]
fn compact_ainfo_reads_no_dense_attributes() {
let ainfo = AttributeInfoMessage::compact();
let mut reader = SliceReader(&[]);
assert!(read_dense_attributes(&ainfo, &ctx(), &mut reader)
.unwrap()
.is_empty());
}
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(attrs: &[AttributeEntry]) -> (MemFile, Vec<AttributeEntry>) {
let mut file = MemFile::new();
let dense = build_dense_attributes(attrs, &ctx(), CreationOrder::Untracked, &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_attributes(&dense.ainfo, &ctx(), &mut file).unwrap();
(file, read)
}
fn numeric(name: &str, value: i32) -> AttributeEntry {
AttributeMessage::scalar_numeric(
name,
DatatypeMessage::i32_type(),
value.to_le_bytes().to_vec(),
)
.into()
}
#[test]
fn a_dozen_attributes_round_trip_through_dense_storage() {
let attrs: Vec<AttributeEntry> = (0..12).map(|i| numeric(&format!("attr{i}"), i)).collect();
let (_file, read) = round_trip(&attrs);
assert_eq!(read.len(), attrs.len());
for want in &attrs {
let got = read
.iter()
.find(|a| a.name() == want.name())
.unwrap_or_else(|| panic!("'{}' missing from dense storage", want.name()));
assert_eq!(got, want);
}
}
#[test]
fn an_attribute_past_the_managed_size_round_trips_as_a_huge_object() {
let data: Vec<u8> = (0..25600i32).flat_map(|v| v.to_le_bytes()).collect();
let big = AttributeEntry::from(AttributeMessage::array_numeric(
"big",
DatatypeMessage::i32_type(),
&[25600],
data,
));
assert!(big.encode(&ctx()).len() > 65535);
let attrs = vec![numeric("small", 7), big];
let (_file, read) = round_trip(&attrs);
assert_eq!(read.len(), 2);
for want in &attrs {
let got = read.iter().find(|a| a.name() == want.name()).unwrap();
assert_eq!(got, want);
}
}
#[test]
fn an_object_with_no_attributes_yields_an_empty_index() {
let (_file, read) = round_trip(&[]);
assert!(read.is_empty());
}
#[test]
fn a_tracked_object_gets_a_creation_order_index() {
let attrs: Vec<AttributeEntry> = (0..12u16)
.map(|i| numeric(&format!("a{:02}", 11 - i), i32::from(i)).with_creation_index(Some(i)))
.collect();
let mut file = MemFile::new();
let dense = build_dense_attributes(&attrs, &ctx(), CreationOrder::Indexed, &mut |len| {
file.alloc(len)
})
.unwrap();
for block in &dense.blocks {
let at = block.addr as usize;
file.bytes[at..at + block.image.len()].copy_from_slice(&block.image);
}
assert_eq!(dense.ainfo.max_creation_index, Some(12));
let read = read_dense_attributes(&dense.ainfo, &ctx(), &mut file).unwrap();
assert_eq!(read.len(), attrs.len());
let addr = dense
.ainfo
.creation_order_btree_address
.expect("tracked attributes 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_ATTR_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<u32> = records
.as_chunks::<CORDER_RECORD_LEN>()
.0
.iter()
.map(|r| u32::from_le_bytes(r[FHEAP_ID_LEN + 1..CORDER_RECORD_LEN].try_into().unwrap()))
.collect();
assert_eq!(corders, (0..12u32).collect::<Vec<_>>());
let name_bt2 = Bt2Header::decode(
&file
.read_block(dense.ainfo.name_btree_address, 256)
.unwrap(),
&ctx(),
)
.unwrap();
let name_records = collect_btree_v2_records(&name_bt2, &ctx(), &mut file).unwrap();
let mut seen: Vec<u32> = name_records
.as_chunks::<NAME_RECORD_LEN>()
.0
.iter()
.map(|r| u32::from_le_bytes(r[FHEAP_ID_LEN + 1..FHEAP_ID_LEN + 5].try_into().unwrap()))
.collect();
seen.sort_unstable();
assert_eq!(seen, (0..12u32).collect::<Vec<_>>());
}
#[test]
fn each_attribute_keeps_the_creation_index_it_carries() {
let want = [5u16, 0, 9, 2];
let attrs: Vec<AttributeEntry> = want
.iter()
.enumerate()
.map(|(pos, &idx)| {
numeric(&format!("n{pos}"), pos as i32).with_creation_index(Some(idx))
})
.collect();
let mut file = MemFile::new();
let dense = build_dense_attributes(&attrs, &ctx(), CreationOrder::Indexed, &mut |len| {
file.alloc(len)
})
.unwrap();
for block in &dense.blocks {
let at = block.addr as usize;
file.bytes[at..at + block.image.len()].copy_from_slice(&block.image);
}
assert_eq!(dense.ainfo.max_creation_index, Some(10));
let read = read_dense_attributes(&dense.ainfo, &ctx(), &mut file).unwrap();
for attr in &attrs {
let got = read.iter().find(|a| a.name() == attr.name()).unwrap();
assert_eq!(
got.creation_index(),
attr.creation_index(),
"{}",
attr.name()
);
}
let addr = dense.ainfo.creation_order_btree_address.unwrap();
let bt2 = Bt2Header::decode(&file.read_block(addr, 256).unwrap(), &ctx()).unwrap();
let records = collect_btree_v2_records(&bt2, &ctx(), &mut file).unwrap();
let corders: Vec<u32> = records
.as_chunks::<CORDER_RECORD_LEN>()
.0
.iter()
.map(|r| u32::from_le_bytes(r[FHEAP_ID_LEN + 1..CORDER_RECORD_LEN].try_into().unwrap()))
.collect();
assert_eq!(corders, vec![0u32, 2, 5, 9]);
}
#[test]
fn name_records_are_ordered_by_hash() {
let attrs: Vec<AttributeEntry> = (0..64).map(|i| numeric(&format!("a{i}"), i)).collect();
let mut file = MemFile::new();
let dense = build_dense_attributes(&attrs, &ctx(), CreationOrder::Untracked, &mut |len| {
file.alloc(len)
})
.unwrap();
for block in &dense.blocks {
let at = block.addr as usize;
file.bytes[at..at + block.image.len()].copy_from_slice(&block.image);
}
let bt2_buf = file
.read_block(dense.ainfo.name_btree_address, 256)
.unwrap();
let bt2 = Bt2Header::decode(&bt2_buf, &ctx()).unwrap();
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[13..17].try_into().unwrap()))
.collect();
assert_eq!(hashes.len(), attrs.len());
assert!(
hashes.windows(2).all(|w| w[0] <= w[1]),
"name index is not hash-ordered: {hashes:?}"
);
for rec in records.as_chunks::<NAME_RECORD_LEN>().0 {
assert_eq!(rec[FHEAP_ID_LEN], 0, "no record is shared");
assert_eq!(
u32::from_le_bytes(rec[9..13].try_into().unwrap()),
u32::from(MAX_CREATION_ORDER_INDEX)
);
}
}
#[test]
fn dense_ainfo_without_name_index_is_an_error() {
let ainfo = AttributeInfoMessage {
max_creation_index: None,
fractal_heap_address: 512,
name_btree_address: UNDEF_ADDR,
creation_order_btree_address: None,
};
let mut reader = SliceReader(&[]);
let err = read_dense_attributes(&ainfo, &ctx(), &mut reader).unwrap_err();
assert!(matches!(err, FormatError::InvalidData(_)));
}
}