1use crate::format::checksum::checksum_metadata;
21use crate::format::chunk_index::btree_v2::{
22 build_index as build_btree_v2_index, collect_btree_v2_records, Bt2Header, BT2_TYPE_ATTR_CORDER,
23 BT2_TYPE_ATTR_NAME,
24};
25use crate::format::creation_order::CreationOrder;
26use crate::format::fractal_heap::{
27 collect_managed_blocks, read_heap_object, FractalHeapHeader, HeapId, HeapParams,
28};
29use crate::format::fractal_heap_write::{build_heap, HeapBlock};
30use crate::format::messages::attr_info::{
31 next_creation_index, AttributeInfoMessage, MAX_CREATION_ORDER_INDEX,
32};
33use crate::format::messages::attribute::AttributeEntry;
34use crate::format::messages::MSG_FLAG_SHARED;
35use crate::format::{BlockReader, FormatContext, FormatError, FormatResult, UNDEF_ADDR};
36
37const FHEAP_ID_LEN: usize = 8;
40
41const NAME_RECORD_LEN: usize = FHEAP_ID_LEN + 1 + 4 + 4;
44
45const CORDER_RECORD_LEN: usize = FHEAP_ID_LEN + 1 + 4;
48
49const NAME_BT2_NODE_SIZE: u32 = 512;
52
53pub fn name_hash(name: &str) -> u32 {
55 checksum_metadata(name.as_bytes())
56}
57
58pub fn read_dense_attributes<R: BlockReader>(
71 ainfo: &AttributeInfoMessage,
72 ctx: &FormatContext,
73 reader: &mut R,
74) -> FormatResult<Vec<AttributeEntry>> {
75 if !ainfo.is_dense() {
76 return Ok(Vec::new());
77 }
78 if ainfo.name_btree_address == UNDEF_ADDR {
79 return Err(FormatError::InvalidData(
80 "dense attribute storage without a name index B-tree".into(),
81 ));
82 }
83
84 let heap_buf = reader.read_block(ainfo.fractal_heap_address, 512)?;
87 let heap = FractalHeapHeader::decode(&heap_buf, ctx)?;
88 let blocks = collect_managed_blocks(&heap, ctx, reader)?;
89
90 let bt2_buf = reader.read_block(ainfo.name_btree_address, 256)?;
91 let bt2 = Bt2Header::decode(&bt2_buf, ctx)?;
92 if bt2.record_type != BT2_TYPE_ATTR_NAME {
93 return Err(FormatError::InvalidData(format!(
94 "attribute name index has B-tree record type {}, expected {}",
95 bt2.record_type, BT2_TYPE_ATTR_NAME
96 )));
97 }
98 if (bt2.record_size as usize) < NAME_RECORD_LEN {
99 return Err(FormatError::InvalidData(format!(
100 "attribute name index record is {} bytes, expected at least {}",
101 bt2.record_size, NAME_RECORD_LEN
102 )));
103 }
104
105 let records = collect_btree_v2_records(&bt2, ctx, reader)?;
106 let rec_size = bt2.record_size as usize;
107 let mut attrs = Vec::with_capacity(records.len() / rec_size);
108 for rec in records.chunks_exact(rec_size) {
109 if rec[FHEAP_ID_LEN] & MSG_FLAG_SHARED != 0 {
113 return Err(FormatError::UnsupportedFeature(
114 "shared (SOHM) dense attribute".into(),
115 ));
116 }
117 let id = HeapId::parse(&rec[..FHEAP_ID_LEN], &heap, ctx)?;
118 let bytes = read_heap_object(&id, &heap, ctx, &blocks, reader)?;
119 let corder = u32::from_le_bytes([
120 rec[FHEAP_ID_LEN + 1],
121 rec[FHEAP_ID_LEN + 2],
122 rec[FHEAP_ID_LEN + 3],
123 rec[FHEAP_ID_LEN + 4],
124 ]);
125 attrs.push(AttributeEntry::parse(&bytes, ctx)?.with_creation_index(decoded_corder(corder)));
126 }
127 Ok(attrs)
128}
129
130fn decoded_corder(corder: u32) -> Option<u16> {
134 u16::try_from(corder)
135 .ok()
136 .filter(|&c| c != MAX_CREATION_ORDER_INDEX)
137}
138
139#[derive(Debug, Clone, PartialEq)]
142pub struct DenseAttributeStorage {
143 pub ainfo: AttributeInfoMessage,
145 pub blocks: Vec<HeapBlock>,
147}
148
149pub fn build_dense_attributes(
165 attrs: &[AttributeEntry],
166 ctx: &FormatContext,
167 order: CreationOrder,
168 alloc: &mut dyn FnMut(u64) -> u64,
169) -> FormatResult<DenseAttributeStorage> {
170 let objects: Vec<Vec<u8>> = attrs.iter().map(|a| a.encode(ctx)).collect();
171 let heap = build_heap(&HeapParams::object_header(), ctx, &objects, alloc)?;
172
173 let mut by_name: Vec<usize> = (0..attrs.len()).collect();
177 by_name.sort_by(|&a, &b| {
178 name_hash(attrs[a].name())
179 .cmp(&name_hash(attrs[b].name()))
180 .then_with(|| attrs[a].name().cmp(attrs[b].name()))
181 });
182
183 let corder = |i: usize| -> u32 {
189 match (order.is_tracked(), attrs[i].creation_index()) {
190 (true, Some(idx)) => u32::from(idx),
191 _ => u32::from(MAX_CREATION_ORDER_INDEX),
192 }
193 };
194
195 let mut records = Vec::with_capacity(by_name.len() * NAME_RECORD_LEN);
196 for &i in &by_name {
197 records.extend_from_slice(&heap.ids[i]);
198 records.push(0);
200 records.extend_from_slice(&corder(i).to_le_bytes());
201 records.extend_from_slice(&name_hash(attrs[i].name()).to_le_bytes());
202 }
203
204 let mut blocks = heap.blocks;
205 let bt2_addr = build_index(
206 BT2_TYPE_ATTR_NAME,
207 NAME_RECORD_LEN as u16,
208 &records,
209 ctx,
210 alloc,
211 &mut blocks,
212 );
213
214 let corder_bt2_addr = order.is_indexed().then(|| {
219 let mut by_corder: Vec<usize> = (0..attrs.len()).collect();
220 by_corder.sort_by_key(|&i| corder(i));
221 let mut records = Vec::with_capacity(attrs.len() * CORDER_RECORD_LEN);
222 for &i in &by_corder {
223 records.extend_from_slice(&heap.ids[i]);
224 records.push(0);
225 records.extend_from_slice(&corder(i).to_le_bytes());
226 }
227 build_index(
228 BT2_TYPE_ATTR_CORDER,
229 CORDER_RECORD_LEN as u16,
230 &records,
231 ctx,
232 alloc,
233 &mut blocks,
234 )
235 });
236
237 Ok(DenseAttributeStorage {
238 ainfo: AttributeInfoMessage {
239 max_creation_index: order.is_tracked().then(|| next_creation_index(attrs)),
244 fractal_heap_address: heap.header_addr,
245 name_btree_address: bt2_addr,
246 creation_order_btree_address: corder_bt2_addr,
247 },
248 blocks,
249 })
250}
251
252fn build_index(
255 record_type: u8,
256 record_size: u16,
257 records: &[u8],
258 ctx: &FormatContext,
259 alloc: &mut dyn FnMut(u64) -> u64,
260 blocks: &mut Vec<HeapBlock>,
261) -> u64 {
262 let (bt2_addr, nodes) = build_btree_v2_index(
263 record_type,
264 record_size,
265 NAME_BT2_NODE_SIZE,
266 records,
267 ctx,
268 alloc,
269 );
270 blocks.extend(nodes.into_iter().map(|(addr, image)| HeapBlock {
271 addr,
272 len: image.len() as u64,
273 image,
274 }));
275 bt2_addr
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281 use crate::format::messages::attribute::AttributeMessage;
282 use crate::format::messages::datatype::DatatypeMessage;
283
284 struct SliceReader<'a>(&'a [u8]);
285
286 impl BlockReader for SliceReader<'_> {
287 fn read_block(&mut self, offset: u64, len: usize) -> FormatResult<Vec<u8>> {
288 let start = offset as usize;
289 if start > self.0.len() {
290 return Err(FormatError::BufferTooShort {
291 needed: start,
292 available: self.0.len(),
293 });
294 }
295 let end = (start + len).min(self.0.len());
296 Ok(self.0[start..end].to_vec())
297 }
298 }
299
300 fn ctx() -> FormatContext {
301 FormatContext {
302 sizeof_addr: 8,
303 sizeof_size: 8,
304 }
305 }
306
307 #[test]
308 fn compact_ainfo_reads_no_dense_attributes() {
309 let ainfo = AttributeInfoMessage::compact();
310 let mut reader = SliceReader(&[]);
311 assert!(read_dense_attributes(&ainfo, &ctx(), &mut reader)
312 .unwrap()
313 .is_empty());
314 }
315
316 struct MemFile {
319 bytes: Vec<u8>,
320 }
321
322 impl MemFile {
323 fn new() -> Self {
324 Self { bytes: vec![0; 16] }
326 }
327 fn alloc(&mut self, len: u64) -> u64 {
328 let addr = self.bytes.len() as u64;
329 self.bytes.resize(self.bytes.len() + len as usize, 0);
330 addr
331 }
332 }
333
334 impl BlockReader for MemFile {
335 fn read_block(&mut self, offset: u64, len: usize) -> FormatResult<Vec<u8>> {
336 let start = offset as usize;
337 if start > self.bytes.len() {
338 return Err(FormatError::BufferTooShort {
339 needed: start,
340 available: self.bytes.len(),
341 });
342 }
343 let end = (start + len).min(self.bytes.len());
344 Ok(self.bytes[start..end].to_vec())
345 }
346 }
347
348 fn round_trip(attrs: &[AttributeEntry]) -> (MemFile, Vec<AttributeEntry>) {
351 let mut file = MemFile::new();
352 let dense = build_dense_attributes(attrs, &ctx(), CreationOrder::Untracked, &mut |len| {
353 file.alloc(len)
354 })
355 .unwrap();
356 for block in &dense.blocks {
357 assert_eq!(block.len as usize, block.image.len(), "block len vs image");
358 let at = block.addr as usize;
359 file.bytes[at..at + block.image.len()].copy_from_slice(&block.image);
360 }
361 let read = read_dense_attributes(&dense.ainfo, &ctx(), &mut file).unwrap();
362 (file, read)
363 }
364
365 fn numeric(name: &str, value: i32) -> AttributeEntry {
366 AttributeMessage::scalar_numeric(
367 name,
368 DatatypeMessage::i32_type(),
369 value.to_le_bytes().to_vec(),
370 )
371 .into()
372 }
373
374 #[test]
375 fn a_dozen_attributes_round_trip_through_dense_storage() {
376 let attrs: Vec<AttributeEntry> = (0..12).map(|i| numeric(&format!("attr{i}"), i)).collect();
377 let (_file, read) = round_trip(&attrs);
378
379 assert_eq!(read.len(), attrs.len());
380 for want in &attrs {
383 let got = read
384 .iter()
385 .find(|a| a.name() == want.name())
386 .unwrap_or_else(|| panic!("'{}' missing from dense storage", want.name()));
387 assert_eq!(got, want);
388 }
389 }
390
391 #[test]
392 fn an_attribute_past_the_managed_size_round_trips_as_a_huge_object() {
393 let data: Vec<u8> = (0..25600i32).flat_map(|v| v.to_le_bytes()).collect();
397 let big = AttributeEntry::from(AttributeMessage::array_numeric(
398 "big",
399 DatatypeMessage::i32_type(),
400 &[25600],
401 data,
402 ));
403 assert!(big.encode(&ctx()).len() > 65535);
404
405 let attrs = vec![numeric("small", 7), big];
406 let (_file, read) = round_trip(&attrs);
407
408 assert_eq!(read.len(), 2);
409 for want in &attrs {
410 let got = read.iter().find(|a| a.name() == want.name()).unwrap();
411 assert_eq!(got, want);
412 }
413 }
414
415 #[test]
416 fn an_object_with_no_attributes_yields_an_empty_index() {
417 let (_file, read) = round_trip(&[]);
418 assert!(read.is_empty());
419 }
420
421 #[test]
425 fn a_tracked_object_gets_a_creation_order_index() {
426 let attrs: Vec<AttributeEntry> = (0..12u16)
429 .map(|i| numeric(&format!("a{:02}", 11 - i), i32::from(i)).with_creation_index(Some(i)))
430 .collect();
431 let mut file = MemFile::new();
432 let dense = build_dense_attributes(&attrs, &ctx(), CreationOrder::Indexed, &mut |len| {
433 file.alloc(len)
434 })
435 .unwrap();
436 for block in &dense.blocks {
437 let at = block.addr as usize;
438 file.bytes[at..at + block.image.len()].copy_from_slice(&block.image);
439 }
440 assert_eq!(dense.ainfo.max_creation_index, Some(12));
441
442 let read = read_dense_attributes(&dense.ainfo, &ctx(), &mut file).unwrap();
444 assert_eq!(read.len(), attrs.len());
445
446 let addr = dense
447 .ainfo
448 .creation_order_btree_address
449 .expect("tracked attributes must carry a creation-order index");
450 let bt2 = Bt2Header::decode(&file.read_block(addr, 256).unwrap(), &ctx()).unwrap();
451 assert_eq!(bt2.record_type, BT2_TYPE_ATTR_CORDER);
452 assert_eq!(bt2.record_size as usize, CORDER_RECORD_LEN);
453
454 let records = collect_btree_v2_records(&bt2, &ctx(), &mut file).unwrap();
455 let corders: Vec<u32> = records
456 .as_chunks::<CORDER_RECORD_LEN>()
457 .0
458 .iter()
459 .map(|r| u32::from_le_bytes(r[FHEAP_ID_LEN + 1..CORDER_RECORD_LEN].try_into().unwrap()))
460 .collect();
461 assert_eq!(corders, (0..12u32).collect::<Vec<_>>());
462
463 let name_bt2 = Bt2Header::decode(
465 &file
466 .read_block(dense.ainfo.name_btree_address, 256)
467 .unwrap(),
468 &ctx(),
469 )
470 .unwrap();
471 let name_records = collect_btree_v2_records(&name_bt2, &ctx(), &mut file).unwrap();
472 let mut seen: Vec<u32> = name_records
473 .as_chunks::<NAME_RECORD_LEN>()
474 .0
475 .iter()
476 .map(|r| u32::from_le_bytes(r[FHEAP_ID_LEN + 1..FHEAP_ID_LEN + 5].try_into().unwrap()))
477 .collect();
478 seen.sort_unstable();
479 assert_eq!(seen, (0..12u32).collect::<Vec<_>>());
480 }
481
482 #[test]
488 fn each_attribute_keeps_the_creation_index_it_carries() {
489 let want = [5u16, 0, 9, 2];
490 let attrs: Vec<AttributeEntry> = want
491 .iter()
492 .enumerate()
493 .map(|(pos, &idx)| {
494 numeric(&format!("n{pos}"), pos as i32).with_creation_index(Some(idx))
495 })
496 .collect();
497 let mut file = MemFile::new();
498 let dense = build_dense_attributes(&attrs, &ctx(), CreationOrder::Indexed, &mut |len| {
499 file.alloc(len)
500 })
501 .unwrap();
502 for block in &dense.blocks {
503 let at = block.addr as usize;
504 file.bytes[at..at + block.image.len()].copy_from_slice(&block.image);
505 }
506
507 assert_eq!(dense.ainfo.max_creation_index, Some(10));
509
510 let read = read_dense_attributes(&dense.ainfo, &ctx(), &mut file).unwrap();
511 for attr in &attrs {
512 let got = read.iter().find(|a| a.name() == attr.name()).unwrap();
513 assert_eq!(
514 got.creation_index(),
515 attr.creation_index(),
516 "{}",
517 attr.name()
518 );
519 }
520
521 let addr = dense.ainfo.creation_order_btree_address.unwrap();
524 let bt2 = Bt2Header::decode(&file.read_block(addr, 256).unwrap(), &ctx()).unwrap();
525 let records = collect_btree_v2_records(&bt2, &ctx(), &mut file).unwrap();
526 let corders: Vec<u32> = records
527 .as_chunks::<CORDER_RECORD_LEN>()
528 .0
529 .iter()
530 .map(|r| u32::from_le_bytes(r[FHEAP_ID_LEN + 1..CORDER_RECORD_LEN].try_into().unwrap()))
531 .collect();
532 assert_eq!(corders, vec![0u32, 2, 5, 9]);
533 }
534
535 #[test]
536 fn name_records_are_ordered_by_hash() {
537 let attrs: Vec<AttributeEntry> = (0..64).map(|i| numeric(&format!("a{i}"), i)).collect();
540 let mut file = MemFile::new();
541 let dense = build_dense_attributes(&attrs, &ctx(), CreationOrder::Untracked, &mut |len| {
542 file.alloc(len)
543 })
544 .unwrap();
545 for block in &dense.blocks {
546 let at = block.addr as usize;
547 file.bytes[at..at + block.image.len()].copy_from_slice(&block.image);
548 }
549
550 let bt2_buf = file
551 .read_block(dense.ainfo.name_btree_address, 256)
552 .unwrap();
553 let bt2 = Bt2Header::decode(&bt2_buf, &ctx()).unwrap();
554 assert!(bt2.depth > 0, "expected a multi-level index, got one leaf");
555 let records = collect_btree_v2_records(&bt2, &ctx(), &mut file).unwrap();
556
557 let hashes: Vec<u32> = records
558 .as_chunks::<NAME_RECORD_LEN>()
559 .0
560 .iter()
561 .map(|r| u32::from_le_bytes(r[13..17].try_into().unwrap()))
562 .collect();
563 assert_eq!(hashes.len(), attrs.len());
564 assert!(
565 hashes.windows(2).all(|w| w[0] <= w[1]),
566 "name index is not hash-ordered: {hashes:?}"
567 );
568 for rec in records.as_chunks::<NAME_RECORD_LEN>().0 {
570 assert_eq!(rec[FHEAP_ID_LEN], 0, "no record is shared");
571 assert_eq!(
572 u32::from_le_bytes(rec[9..13].try_into().unwrap()),
573 u32::from(MAX_CREATION_ORDER_INDEX)
574 );
575 }
576 }
577
578 #[test]
579 fn dense_ainfo_without_name_index_is_an_error() {
580 let ainfo = AttributeInfoMessage {
581 max_creation_index: None,
582 fractal_heap_address: 512,
583 name_btree_address: UNDEF_ADDR,
584 creation_order_btree_address: None,
585 };
586 let mut reader = SliceReader(&[]);
587 let err = read_dense_attributes(&ainfo, &ctx(), &mut reader).unwrap_err();
588 assert!(matches!(err, FormatError::InvalidData(_)));
589 }
590}