1use crate::format::checksum::checksum_metadata;
20use crate::format::chunk_index::btree_v2::{
21 collect_btree_v2_records, Bt2Header, Bt2Tree, BT2_TYPE_GRP_CORDER, BT2_TYPE_GRP_NAME,
22};
23use crate::format::creation_order::CreationOrder;
24use crate::format::fractal_heap::{
25 collect_managed_blocks, read_heap_object, FractalHeapHeader, HeapId, HeapParams,
26};
27use crate::format::fractal_heap_write::{build_heap, HeapBlock};
28use crate::format::messages::link::LinkMessage;
29use crate::format::messages::link_info::LinkInfoMessage;
30use crate::format::{BlockReader, FormatContext, FormatError, FormatResult, UNDEF_ADDR};
31
32const FHEAP_ID_LEN: usize = 7;
35
36const NAME_RECORD_LEN: usize = 4 + FHEAP_ID_LEN;
39
40const CORDER_RECORD_LEN: usize = 8 + FHEAP_ID_LEN;
43
44const NAME_BT2_NODE_SIZE: u32 = 512;
47
48pub fn name_hash(name: &str) -> u32 {
50 checksum_metadata(name.as_bytes())
51}
52
53pub fn read_dense_links<R: BlockReader>(
66 linfo: &LinkInfoMessage,
67 ctx: &FormatContext,
68 reader: &mut R,
69) -> FormatResult<Vec<LinkMessage>> {
70 if linfo.fractal_heap_address == UNDEF_ADDR {
71 return Ok(Vec::new());
72 }
73 if linfo.name_btree_address == UNDEF_ADDR {
74 return Err(FormatError::InvalidData(
75 "dense link storage without a name index B-tree".into(),
76 ));
77 }
78
79 let heap_buf = reader.read_block(linfo.fractal_heap_address, 512)?;
82 let heap = FractalHeapHeader::decode(&heap_buf, ctx)?;
83 let blocks = collect_managed_blocks(&heap, ctx, reader)?;
84
85 let bt2_buf = reader.read_block(linfo.name_btree_address, 256)?;
86 let bt2 = Bt2Header::decode(&bt2_buf, ctx)?;
87 if bt2.record_type != BT2_TYPE_GRP_NAME {
88 return Err(FormatError::InvalidData(format!(
89 "link name index has B-tree record type {}, expected {}",
90 bt2.record_type, BT2_TYPE_GRP_NAME
91 )));
92 }
93 if (bt2.record_size as usize) < NAME_RECORD_LEN {
94 return Err(FormatError::InvalidData(format!(
95 "link name index record is {} bytes, expected at least {}",
96 bt2.record_size, NAME_RECORD_LEN
97 )));
98 }
99
100 let records = collect_btree_v2_records(&bt2, ctx, reader)?;
101 let rec_size = bt2.record_size as usize;
102 let mut links = Vec::with_capacity(records.len() / rec_size);
103 for rec in records.chunks_exact(rec_size) {
104 let id = HeapId::parse(&rec[4..4 + FHEAP_ID_LEN], &heap, ctx)?;
105 let bytes = read_heap_object(&id, &heap, ctx, &blocks, reader)?;
106 links.push(LinkMessage::decode(&bytes, ctx)?.0);
107 }
108 Ok(links)
109}
110
111#[derive(Debug, Clone, PartialEq)]
114pub struct DenseLinkStorage {
115 pub linfo: LinkInfoMessage,
117 pub blocks: Vec<HeapBlock>,
119}
120
121pub fn build_dense_links(
136 links: &[LinkMessage],
137 ctx: &FormatContext,
138 order: CreationOrder,
139 alloc: &mut dyn FnMut(u64) -> u64,
140) -> FormatResult<DenseLinkStorage> {
141 let objects: Vec<Vec<u8>> = links.iter().map(|l| l.encode(ctx)).collect();
142 let heap = build_heap(&HeapParams::group_links(), ctx, &objects, alloc)?;
143
144 let mut by_name: Vec<usize> = (0..links.len()).collect();
148 by_name.sort_by(|&a, &b| {
149 name_hash(&links[a].name)
150 .cmp(&name_hash(&links[b].name))
151 .then_with(|| links[a].name.cmp(&links[b].name))
152 });
153
154 let mut records = Vec::with_capacity(by_name.len() * NAME_RECORD_LEN);
155 for &i in &by_name {
156 records.extend_from_slice(&name_hash(&links[i].name).to_le_bytes());
157 records.extend_from_slice(&heap.ids[i]);
158 }
159
160 let mut blocks = heap.blocks;
161 let bt2_addr = build_index(
162 BT2_TYPE_GRP_NAME,
163 NAME_RECORD_LEN as u16,
164 &records,
165 ctx,
166 alloc,
167 &mut blocks,
168 );
169
170 let corders: Option<Vec<i64>> = order
174 .is_tracked()
175 .then(|| links.iter().map(|l| l.creation_order).collect())
176 .flatten();
177 if order.is_tracked() && corders.is_none() {
178 return Err(FormatError::InvalidData(
179 "a group tracking link creation order has a link with no creation order".into(),
180 ));
181 }
182
183 let corder_bt2_addr = order.is_indexed().then(|| {
184 let corders = corders.as_ref().expect("indexed implies tracked");
185 let mut by_corder: Vec<usize> = (0..links.len()).collect();
186 by_corder.sort_by_key(|&i| corders[i]);
187 let mut records = Vec::with_capacity(by_corder.len() * CORDER_RECORD_LEN);
188 for &i in &by_corder {
189 records.extend_from_slice(&corders[i].to_le_bytes());
190 records.extend_from_slice(&heap.ids[i]);
191 }
192 build_index(
193 BT2_TYPE_GRP_CORDER,
194 CORDER_RECORD_LEN as u16,
195 &records,
196 ctx,
197 alloc,
198 &mut blocks,
199 )
200 });
201
202 Ok(DenseLinkStorage {
203 linfo: LinkInfoMessage {
204 max_creation_order: corders.map(|c| c.len() as u64),
207 fractal_heap_address: heap.header_addr,
208 name_btree_address: bt2_addr,
209 creation_order_btree_address: corder_bt2_addr,
210 },
211 blocks,
212 })
213}
214
215fn build_index(
218 record_type: u8,
219 record_size: u16,
220 records: &[u8],
221 ctx: &FormatContext,
222 alloc: &mut dyn FnMut(u64) -> u64,
223 blocks: &mut Vec<HeapBlock>,
224) -> u64 {
225 let tree = Bt2Tree::build(
226 record_type,
227 record_size,
228 NAME_BT2_NODE_SIZE,
229 ctx.sizeof_addr,
230 records,
231 );
232 let bt2_addr = alloc(tree.header(UNDEF_ADDR).encoded_size(ctx) as u64);
233 let node_addrs: Vec<u64> = tree
234 .nodes
235 .iter()
236 .map(|_| alloc(tree.node_size as u64))
237 .collect();
238 for (image, &addr) in tree.encode(ctx, &node_addrs).into_iter().zip(&node_addrs) {
239 blocks.push(HeapBlock {
240 addr,
241 len: tree.node_size as u64,
242 image,
243 });
244 }
245 let root_addr = node_addrs.last().copied().unwrap_or(UNDEF_ADDR);
246 let image = tree.header(root_addr).encode(ctx);
247 blocks.push(HeapBlock {
248 addr: bt2_addr,
249 len: image.len() as u64,
250 image,
251 });
252 bt2_addr
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258
259 fn ctx() -> FormatContext {
260 FormatContext {
261 sizeof_addr: 8,
262 sizeof_size: 8,
263 }
264 }
265
266 struct MemFile {
269 bytes: Vec<u8>,
270 }
271
272 impl MemFile {
273 fn new() -> Self {
274 Self { bytes: vec![0; 16] }
276 }
277 fn alloc(&mut self, len: u64) -> u64 {
278 let addr = self.bytes.len() as u64;
279 self.bytes.resize(self.bytes.len() + len as usize, 0);
280 addr
281 }
282 }
283
284 impl BlockReader for MemFile {
285 fn read_block(&mut self, offset: u64, len: usize) -> FormatResult<Vec<u8>> {
286 let start = offset as usize;
287 if start > self.bytes.len() {
288 return Err(FormatError::BufferTooShort {
289 needed: start,
290 available: self.bytes.len(),
291 });
292 }
293 let end = (start + len).min(self.bytes.len());
294 Ok(self.bytes[start..end].to_vec())
295 }
296 }
297
298 fn round_trip(links: &[LinkMessage]) -> (MemFile, DenseLinkStorage, Vec<LinkMessage>) {
301 round_trip_ordered(links, CreationOrder::Untracked)
302 }
303
304 fn round_trip_ordered(
306 links: &[LinkMessage],
307 order: CreationOrder,
308 ) -> (MemFile, DenseLinkStorage, Vec<LinkMessage>) {
309 let mut file = MemFile::new();
310 let dense = build_dense_links(links, &ctx(), order, &mut |len| file.alloc(len)).unwrap();
311 for block in &dense.blocks {
312 assert_eq!(block.len as usize, block.image.len(), "block len vs image");
313 let at = block.addr as usize;
314 file.bytes[at..at + block.image.len()].copy_from_slice(&block.image);
315 }
316 let read = read_dense_links(&dense.linfo, &ctx(), &mut file).unwrap();
317 (file, dense, read)
318 }
319
320 #[test]
321 fn compact_linfo_reads_no_dense_links() {
322 let mut file = MemFile::new();
323 assert!(
324 read_dense_links(&LinkInfoMessage::compact(), &ctx(), &mut file)
325 .unwrap()
326 .is_empty()
327 );
328 }
329
330 #[test]
331 fn a_dozen_links_round_trip_through_dense_storage() {
332 let links: Vec<LinkMessage> = (0..12)
333 .map(|i| LinkMessage::hard(&format!("d{i:02}"), 0x400 + i as u64 * 8))
334 .collect();
335 let (_file, _dense, read) = round_trip(&links);
336
337 assert_eq!(read.len(), links.len());
338 for want in &links {
341 let got = read
342 .iter()
343 .find(|l| l.name == want.name)
344 .unwrap_or_else(|| panic!("'{}' missing from dense storage", want.name));
345 assert_eq!(got, want);
346 }
347 }
348
349 #[test]
350 fn a_soft_link_round_trips_beside_hard_ones() {
351 let links = vec![
352 LinkMessage::hard("orig", 0x800),
353 LinkMessage::soft("alias", "/orig"),
354 ];
355 let (_file, _dense, read) = round_trip(&links);
356 assert_eq!(read.len(), 2);
357 for want in &links {
358 assert_eq!(read.iter().find(|l| l.name == want.name).unwrap(), want);
359 }
360 }
361
362 #[test]
363 fn a_group_with_no_links_yields_an_empty_index() {
364 let (_file, _dense, read) = round_trip(&[]);
365 assert!(read.is_empty());
366 }
367
368 #[test]
371 fn the_heap_uses_the_group_parameters() {
372 let links: Vec<LinkMessage> = (0..12)
373 .map(|i| LinkMessage::hard(&format!("d{i:02}"), 0x400 + i as u64 * 8))
374 .collect();
375 let (mut file, dense, _read) = round_trip(&links);
376 let heap_buf = file
377 .read_block(dense.linfo.fractal_heap_address, 512)
378 .unwrap();
379 let heap = FractalHeapHeader::decode(&heap_buf, &ctx()).unwrap();
380 assert_eq!(heap.id_len, 7);
381 assert_eq!(heap.heap_off_size, 4);
382 assert_eq!(heap.heap_len_size, 2);
383 assert_eq!(heap.start_block_size, 512);
384 assert_eq!(heap.man_nobjs, 12);
385 }
386
387 #[test]
388 fn name_records_are_ordered_by_hash() {
389 let links: Vec<LinkMessage> = (0..128)
392 .map(|i| LinkMessage::hard(&format!("d{i:03}"), 0x400 + i as u64 * 8))
393 .collect();
394 let (mut file, dense, read) = round_trip(&links);
395 assert_eq!(read.len(), links.len());
396
397 let bt2_buf = file
398 .read_block(dense.linfo.name_btree_address, 256)
399 .unwrap();
400 let bt2 = Bt2Header::decode(&bt2_buf, &ctx()).unwrap();
401 assert_eq!(bt2.record_type, BT2_TYPE_GRP_NAME);
402 assert_eq!(bt2.record_size as usize, NAME_RECORD_LEN);
403 assert!(bt2.depth > 0, "expected a multi-level index, got one leaf");
404
405 let records = collect_btree_v2_records(&bt2, &ctx(), &mut file).unwrap();
406 let hashes: Vec<u32> = records
407 .as_chunks::<NAME_RECORD_LEN>()
408 .0
409 .iter()
410 .map(|r| u32::from_le_bytes(r[0..4].try_into().unwrap()))
411 .collect();
412 assert_eq!(hashes.len(), links.len());
413 assert!(
414 hashes.windows(2).all(|w| w[0] <= w[1]),
415 "name index is not hash-ordered: {hashes:?}"
416 );
417 }
418
419 #[test]
424 fn a_tracked_group_gets_a_creation_order_index() {
425 let links: Vec<LinkMessage> = (0..12u32)
428 .map(|i| {
429 LinkMessage::hard(&format!("d{:02}", 11 - i), 0x400 + i as u64 * 8)
430 .with_creation_order(i as i64)
431 })
432 .collect();
433 let (mut file, dense, read) = round_trip_ordered(&links, CreationOrder::Indexed);
434 assert_eq!(read.len(), links.len());
435 assert_eq!(dense.linfo.max_creation_order, Some(12));
436
437 let addr = dense
438 .linfo
439 .creation_order_btree_address
440 .expect("tracked links must carry a creation-order index");
441 let bt2 = Bt2Header::decode(&file.read_block(addr, 256).unwrap(), &ctx()).unwrap();
442 assert_eq!(bt2.record_type, BT2_TYPE_GRP_CORDER);
443 assert_eq!(bt2.record_size as usize, CORDER_RECORD_LEN);
444
445 let records = collect_btree_v2_records(&bt2, &ctx(), &mut file).unwrap();
446 let corders: Vec<i64> = records
447 .as_chunks::<CORDER_RECORD_LEN>()
448 .0
449 .iter()
450 .map(|r| i64::from_le_bytes(r[0..8].try_into().unwrap()))
451 .collect();
452 assert_eq!(corders, (0..12i64).collect::<Vec<_>>());
453 }
454
455 #[test]
457 fn an_untracked_group_gets_no_creation_order_index() {
458 let links: Vec<LinkMessage> = (0..12)
459 .map(|i| LinkMessage::hard(&format!("d{i:02}"), 0x400 + i as u64 * 8))
460 .collect();
461 let (_file, dense, _read) = round_trip(&links);
462 assert_eq!(dense.linfo.creation_order_btree_address, None);
463 assert_eq!(dense.linfo.max_creation_order, None);
464 }
465
466 #[test]
470 fn a_tracked_but_unindexed_group_records_the_maximum_and_no_index() {
471 let links: Vec<LinkMessage> = (0..12u32)
472 .map(|i| {
473 LinkMessage::hard(&format!("d{i:02}"), 0x400 + i as u64 * 8)
474 .with_creation_order(i as i64)
475 })
476 .collect();
477 let (_file, dense, read) = round_trip_ordered(&links, CreationOrder::Tracked);
478 assert_eq!(read.len(), links.len());
479 assert_eq!(dense.linfo.max_creation_order, Some(12));
480 assert_eq!(dense.linfo.creation_order_btree_address, None);
481 }
482
483 #[test]
487 fn tracking_links_that_carry_no_creation_order_is_refused() {
488 let links: Vec<LinkMessage> = (0..12)
489 .map(|i| LinkMessage::hard(&format!("d{i:02}"), 0x400 + i as u64 * 8))
490 .collect();
491 let mut file = MemFile::new();
492 let err = build_dense_links(&links, &ctx(), CreationOrder::Tracked, &mut |len| {
493 file.alloc(len)
494 })
495 .unwrap_err();
496 assert!(matches!(err, FormatError::InvalidData(_)), "{err:?}");
497 }
498
499 #[test]
500 fn dense_linfo_without_name_index_is_an_error() {
501 let linfo = LinkInfoMessage {
502 max_creation_order: None,
503 fractal_heap_address: 512,
504 name_btree_address: UNDEF_ADDR,
505 creation_order_btree_address: None,
506 };
507 let mut file = MemFile::new();
508 let err = read_dense_links(&linfo, &ctx(), &mut file).unwrap_err();
509 assert!(matches!(err, FormatError::InvalidData(_)));
510 }
511}