1use std::collections::HashMap;
35
36use limnifs_core::{
37 compute_merkle_root, dir_node_hash, hash_empty_section, hash_section,
38 parse_feature_flags_section, parse_history, parse_manifest_header, parse_metadata_blob,
39 parse_metadata_reference, parse_slab_index, ContentHandle, CoreError, DirectoryNode, Inode,
40 ManifestCursor, SectionHashes, FEATURE_FLAGS_SECTION_VERSION, HISTORY_SECTION_VERSION,
41 METADATA_REFERENCE_SECTION_VERSION, SLAB_INDEX_SECTION_VERSION,
42};
43use limnifs_format::{ManifestRoot, SlabId};
44
45#[derive(Debug)]
47pub enum FlattenError {
48 Core(CoreError),
50 ExternalMetadata { layer: usize },
54 Empty,
56}
57
58impl std::fmt::Display for FlattenError {
59 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60 match self {
61 Self::Core(e) => write!(f, "flatten: parse error: {e}"),
62 Self::ExternalMetadata { layer } => {
63 write!(
64 f,
65 "flatten: layer {layer} has external metadata (v1 requires inlined)"
66 )
67 }
68 Self::Empty => write!(f, "flatten: no input layers"),
69 }
70 }
71}
72
73impl std::error::Error for FlattenError {}
74
75impl From<CoreError> for FlattenError {
76 fn from(e: CoreError) -> Self {
77 Self::Core(e)
78 }
79}
80
81#[derive(Clone, Debug)]
83pub struct FlattenArtifact {
84 pub bytes: Vec<u8>,
86 pub merkle_root: ManifestRoot,
88 pub inode_count: usize,
90 pub dir_node_count: usize,
92 pub slab_count: usize,
94 pub layer_count: usize,
96}
97
98pub fn flatten(layers: &[&[u8]]) -> Result<FlattenArtifact, FlattenError> {
112 if layers.is_empty() {
113 return Err(FlattenError::Empty);
114 }
115 let layer_count = layers.len();
116
117 let mut merged_inodes: HashMap<u64, Inode> = HashMap::new();
118 let mut merged_dir_nodes: HashMap<[u8; 32], DirectoryNode> = HashMap::new();
119 let mut merged_slab_entries: Vec<(SlabId, Vec<String>)> = Vec::new();
120 let mut seen_slab_ids: HashMap<[u8; 40], usize> = HashMap::new();
121
122 for (i, layer_bytes) in layers.iter().enumerate() {
123 let parsed = parse_layer(layer_bytes)?;
124 if !parsed.metadata_reference.is_inlined() {
125 return Err(FlattenError::ExternalMetadata { layer: i });
126 }
127 let Some(blob_bytes) = parsed.metadata_reference.inline_metadata.as_deref() else {
128 return Err(FlattenError::ExternalMetadata { layer: i });
129 };
130 let mut blob_cursor = ManifestCursor::new(blob_bytes);
131 let blob = parse_metadata_blob(&mut blob_cursor)?;
132
133 for inode in blob.inodes {
135 merged_inodes.insert(inode.number, inode);
136 }
137 for dir_node in blob.dir_nodes {
139 let hash = dir_node_hash(&dir_node.entries);
140 merged_dir_nodes.insert(hash, dir_node);
141 }
142 for slab in parsed.slab_index {
144 let key = slab.slab_id.to_bytes();
145 if let Some(&idx) = seen_slab_ids.get(&key) {
146 let existing = merged_slab_entries
148 .get_mut(idx)
149 .expect("seen_slab_ids points into merged_slab_entries");
150 for loc in slab.locators {
151 let uri = loc.uri;
152 if !existing.1.contains(&uri) {
153 existing.1.push(uri);
154 }
155 }
156 } else {
157 seen_slab_ids.insert(key, merged_slab_entries.len());
158 let locators: Vec<String> = slab.locators.into_iter().map(|l| l.uri).collect();
159 merged_slab_entries.push((slab.slab_id, locators));
160 }
161 }
162 }
163
164 let mut inodes: Vec<Inode> = merged_inodes.into_values().collect();
167 inodes.sort_by_key(|i| i.number);
168 let mut dir_nodes: Vec<DirectoryNode> = merged_dir_nodes.into_values().collect();
169 dir_nodes.sort_by_key(|a| dir_node_hash(&a.entries));
170 merged_slab_entries.sort_by_key(|a| a.0.to_bytes());
171
172 let inode_count = inodes.len();
173 let dir_node_count = dir_nodes.len();
174 let slab_count = merged_slab_entries.len();
175
176 let bytes = encode_manifest(
177 &inodes,
178 &dir_nodes,
179 &merged_slab_entries,
180 u64::try_from(layer_count).expect("layer_count fits u64"),
181 );
182
183 let merkle_root = compute_merkle_root_from_sections(&bytes);
184
185 Ok(FlattenArtifact {
186 bytes,
187 merkle_root,
188 inode_count,
189 dir_node_count,
190 slab_count,
191 layer_count,
192 })
193}
194
195struct ParsedLayer {
197 metadata_reference: limnifs_core::MetadataReference,
198 slab_index: Vec<limnifs_core::SlabIndexEntry>,
199}
200
201fn parse_layer(bytes: &[u8]) -> Result<ParsedLayer, CoreError> {
202 let mut cursor = ManifestCursor::new(bytes);
203 let _ = parse_manifest_header(&mut cursor)?;
204 let _ = parse_feature_flags_section(&mut cursor)?;
205 let metadata_reference = parse_metadata_reference(&mut cursor)?;
206 let slab_index_v = parse_slab_index(&mut cursor)?;
207 let _ = parse_history(&mut cursor)?;
209 Ok(ParsedLayer {
210 metadata_reference,
211 slab_index: slab_index_v.entries,
212 })
213}
214
215fn encode_manifest(
216 inodes: &[Inode],
217 dir_nodes: &[DirectoryNode],
218 slab_entries: &[(SlabId, Vec<String>)],
219 layer_count: u64,
220) -> Vec<u8> {
221 let metadata_blob = encode_metadata_blob(inodes, dir_nodes);
222 let metadata_hash = hash_section(&metadata_blob);
223
224 let mut manifest = Vec::new();
225
226 let header_start = manifest.len();
227 manifest.extend_from_slice(&limnifs_core::ManifestHeader::current().to_bytes());
228 let header_end = manifest.len();
229
230 let flags_start = manifest.len();
231 manifest.push(FEATURE_FLAGS_SECTION_VERSION);
232 manifest.extend_from_slice(&0u32.to_le_bytes());
233 let flags_end = manifest.len();
234
235 let meta_ref_start = manifest.len();
236 manifest.push(METADATA_REFERENCE_SECTION_VERSION);
237 manifest.extend_from_slice(&metadata_hash);
238 manifest.extend_from_slice(&0u32.to_le_bytes());
239 let inline_len = u32::try_from(metadata_blob.len()).expect("metadata fits u32");
240 manifest.extend_from_slice(&inline_len.to_le_bytes());
241 manifest.extend_from_slice(&metadata_blob);
242 let meta_ref_end = manifest.len();
243
244 let slab_index_start = manifest.len();
245 manifest.push(SLAB_INDEX_SECTION_VERSION);
246 manifest.extend_from_slice(&u32::try_from(slab_entries.len()).unwrap().to_le_bytes());
247 for (slab_id, locators) in slab_entries {
248 manifest.extend_from_slice(&slab_id.to_bytes());
249 manifest.extend_from_slice(&u32::try_from(locators.len()).unwrap().to_le_bytes());
250 for loc in locators {
251 let loc_bytes = loc.as_bytes();
252 let loc_len = u32::try_from(loc_bytes.len()).expect("locator fits u32");
253 manifest.extend_from_slice(&loc_len.to_le_bytes());
254 manifest.extend_from_slice(loc_bytes);
255 }
256 }
257 let slab_index_end = manifest.len();
258
259 let history_start = manifest.len();
260 manifest.push(HISTORY_SECTION_VERSION);
261 manifest.extend_from_slice(&1u32.to_le_bytes());
262 manifest.push(0x03);
264 manifest.extend_from_slice(&0u64.to_le_bytes()); manifest.extend_from_slice(&0u32.to_le_bytes()); let layer_count_bytes = layer_count.to_le_bytes();
268 manifest.extend_from_slice(
269 &u32::try_from(layer_count_bytes.len())
270 .unwrap()
271 .to_le_bytes(),
272 );
273 manifest.extend_from_slice(&layer_count_bytes);
274 let history_end = manifest.len();
275
276 let hashes = SectionHashes {
277 metadata: metadata_hash,
278 format_header: hash_section(&manifest[header_start..header_end]),
279 feature_flags: hash_section(&manifest[flags_start..flags_end]),
280 metadata_reference: hash_section(&manifest[meta_ref_start..meta_ref_end]),
281 slab_index: hash_section(&manifest[slab_index_start..slab_index_end]),
282 crypto_params: hash_empty_section(),
283 ec_params: hash_empty_section(),
284 dms_policy: hash_empty_section(),
285 delta_linkage: hash_empty_section(),
286 history: hash_section(&manifest[history_start..history_end]),
287 };
288 let _ = hashes;
289
290 manifest
291}
292
293fn compute_merkle_root_from_sections(manifest: &[u8]) -> ManifestRoot {
294 let mut cursor = ManifestCursor::new(manifest);
298 let header_start = 0;
299 parse_manifest_header(&mut cursor).expect("we just encoded this");
300 let header_end = cursor.position();
301 let flags_start = header_end;
302 parse_feature_flags_section(&mut cursor).expect("we just encoded this");
303 let flags_end = cursor.position();
304 let meta_ref_start = flags_end;
305 let metadata_reference = parse_metadata_reference(&mut cursor).expect("we just encoded this");
306 let meta_ref_end = cursor.position();
307 let slab_index_start = meta_ref_end;
308 parse_slab_index(&mut cursor).expect("we just encoded this");
309 let slab_index_end = cursor.position();
310 parse_history(&mut cursor).expect("we just encoded this");
311 let history_end = cursor.position();
312 let _ = history_end;
313
314 let hashes = SectionHashes {
315 metadata: metadata_reference.metadata_hash,
316 format_header: hash_section(&manifest[header_start..header_end]),
317 feature_flags: hash_section(&manifest[flags_start..flags_end]),
318 metadata_reference: hash_section(&manifest[meta_ref_start..meta_ref_end]),
319 slab_index: hash_section(&manifest[slab_index_start..slab_index_end]),
320 crypto_params: hash_empty_section(),
321 ec_params: hash_empty_section(),
322 dms_policy: hash_empty_section(),
323 delta_linkage: hash_empty_section(),
324 history: hash_section(&manifest[slab_index_end..cursor.position()]),
325 };
326 compute_merkle_root(&hashes)
327}
328
329fn encode_metadata_blob(inodes: &[Inode], dir_nodes: &[DirectoryNode]) -> Vec<u8> {
331 let mut out = Vec::new();
332 out.extend_from_slice(&u32::try_from(inodes.len()).unwrap().to_le_bytes());
333 for inode in inodes {
334 encode_inode(&mut out, inode);
335 }
336 out.extend_from_slice(&u32::try_from(dir_nodes.len()).unwrap().to_le_bytes());
337 for dir_node in dir_nodes {
338 encode_dir_node(&mut out, dir_node);
339 }
340 out
341}
342
343fn encode_inode(out: &mut Vec<u8>, inode: &Inode) {
344 out.extend_from_slice(&inode.number.to_le_bytes());
345 out.extend_from_slice(&inode.mode.to_le_bytes());
346 out.extend_from_slice(&0u32.to_le_bytes());
348 out.extend_from_slice(&0u32.to_le_bytes());
349 out.extend_from_slice(&inode.mtime_ns.to_le_bytes());
350 out.extend_from_slice(&inode.mtime_ns.to_le_bytes());
351 out.extend_from_slice(&1u32.to_le_bytes());
352 match &inode.content_handle {
353 ContentHandle::InlineData(data) => {
354 out.push(0x04);
355 let len = u32::try_from(data.len()).expect("data fits u32");
356 out.extend_from_slice(&len.to_le_bytes());
357 out.extend_from_slice(data);
358 }
359 ContentHandle::SharedInline(_) => {
360 out.push(0x04);
363 out.extend_from_slice(&0u32.to_le_bytes());
364 }
365 ContentHandle::SliceMap(slices) => {
366 out.push(0x00);
367 let slice_count = u32::try_from(slices.len()).expect("slice count fits u32");
368 out.extend_from_slice(&slice_count.to_le_bytes());
369 for slice in slices {
370 out.extend_from_slice(&slice.file_byte_start.to_le_bytes());
371 out.extend_from_slice(&slice.file_byte_end.to_le_bytes());
372 out.extend_from_slice(slice.drop_id.as_bytes());
373 out.extend_from_slice(&0u32.to_le_bytes());
374 let drop_byte_len = u32::try_from(slice.file_byte_end - slice.file_byte_start)
375 .expect("slice range fits u32");
376 out.extend_from_slice(&drop_byte_len.to_le_bytes());
377 }
378 }
379 ContentHandle::Directory(hash) => {
380 out.push(0x00);
381 out.extend_from_slice(hash);
382 }
383 ContentHandle::Symlink(_) | ContentHandle::Device(_) | ContentHandle::Pipe(_) => {
384 out.push(0x00);
386 out.extend_from_slice(&0u32.to_le_bytes());
387 }
388 }
389}
390
391fn encode_dir_node(out: &mut Vec<u8>, dir_node: &DirectoryNode) {
392 out.push(1u8); let count = u32::try_from(dir_node.entries.len()).expect("entry count fits u32");
394 out.extend_from_slice(&count.to_le_bytes());
395 for entry in &dir_node.entries {
396 let name_bytes = entry.name.as_bytes();
397 let name_len = u32::try_from(name_bytes.len()).expect("name fits u32");
398 out.extend_from_slice(&name_len.to_le_bytes());
399 out.extend_from_slice(name_bytes);
400 out.extend_from_slice(&entry.inode_number.to_le_bytes());
401 out.push(entry.entry_type);
402 }
403}
404
405#[cfg(test)]
406mod tests {
407 use super::*;
408 use crate::write_directory;
409 use std::path::Path;
410
411 fn make_tree(dir: &Path, files: &[(&str, &[u8])]) {
412 std::fs::create_dir_all(dir).expect("mkdir");
413 for (name, content) in files {
414 std::fs::write(dir.join(name), content).expect("write");
415 }
416 }
417
418 #[test]
419 fn rejects_empty_input() {
420 let err = flatten(&[]).unwrap_err();
421 assert!(matches!(err, FlattenError::Empty));
422 }
423
424 #[test]
425 fn single_layer_round_trips() {
426 let temp = std::env::temp_dir().join(format!(
429 "limnifs-flatten-single-{}-{}",
430 std::process::id(),
431 "a"
432 ));
433 make_tree(&temp, &[("a.txt", b"aaa"), ("b.txt", b"bbb")]);
434 let artifact = write_directory(&temp).expect("write");
435 std::fs::remove_dir_all(&temp).ok();
436
437 let flat = flatten(&[&artifact.bytes]).expect("flatten");
438 assert_eq!(flat.layer_count, 1);
439 assert_eq!(flat.inode_count, artifact.inode_count);
440 let _ = flat.merkle_root;
443 }
444
445 #[test]
446 fn merge_two_layers_latest_wins() {
447 let temp1 =
451 std::env::temp_dir().join(format!("limnifs-flatten-2a-{}-{}", std::process::id(), "x"));
452 let temp2 =
453 std::env::temp_dir().join(format!("limnifs-flatten-2b-{}-{}", std::process::id(), "x"));
454 make_tree(&temp1, &[("a.txt", b"old")]);
455 make_tree(&temp2, &[("a.txt", b"new")]);
456 let a1 = write_directory(&temp1).expect("write1");
457 let a2 = write_directory(&temp2).expect("write2");
458 std::fs::remove_dir_all(&temp1).ok();
459 std::fs::remove_dir_all(&temp2).ok();
460
461 let flat = flatten(&[&a1.bytes, &a2.bytes]).expect("flatten");
462 let mut cursor = ManifestCursor::new(&flat.bytes);
464 parse_manifest_header(&mut cursor).expect("header");
465 parse_feature_flags_section(&mut cursor).expect("flags");
466 let meta_ref = parse_metadata_reference(&mut cursor).expect("meta ref");
467 assert!(meta_ref.is_inlined());
468 parse_slab_index(&mut cursor).expect("slab index");
469 parse_history(&mut cursor).expect("history");
470 assert_eq!(flat.layer_count, 2);
471 assert_eq!(flat.inode_count, a2.inode_count);
475 }
476
477 #[test]
478 fn merge_three_layers_preserves_history_op() {
479 let temp1 =
480 std::env::temp_dir().join(format!("limnifs-flatten-3a-{}-{}", std::process::id(), "y"));
481 let temp2 =
482 std::env::temp_dir().join(format!("limnifs-flatten-3b-{}-{}", std::process::id(), "y"));
483 let temp3 =
484 std::env::temp_dir().join(format!("limnifs-flatten-3c-{}-{}", std::process::id(), "y"));
485 make_tree(&temp1, &[("a", b"1")]);
486 make_tree(&temp2, &[("a", b"2")]);
487 make_tree(&temp3, &[("a", b"3")]);
488 let layers: Vec<_> = [&temp1, &temp2, &temp3]
489 .iter()
490 .map(|p| write_directory(p).expect("write").bytes)
491 .collect();
492 for t in [&temp1, &temp2, &temp3] {
493 std::fs::remove_dir_all(t).ok();
494 }
495 let layer_refs: Vec<&[u8]> = layers.iter().map(Vec::as_slice).collect();
496
497 let flat = flatten(&layer_refs).expect("flatten");
498 assert_eq!(flat.layer_count, 3);
499
500 let mut cursor = ManifestCursor::new(&flat.bytes);
502 parse_manifest_header(&mut cursor).unwrap();
503 parse_feature_flags_section(&mut cursor).unwrap();
504 parse_metadata_reference(&mut cursor).unwrap();
505 parse_slab_index(&mut cursor).unwrap();
506 let history = parse_history(&mut cursor).unwrap();
507 assert_eq!(history.entries.len(), 1);
508 let entry = &history.entries[0];
509 assert_eq!(entry.op, limnifs_core::HistoryOp::Flatten);
510 let stored = u64::from_le_bytes(
511 entry
512 .params
513 .get(0..8)
514 .and_then(|s| s.try_into().ok())
515 .unwrap_or([0u8; 8]),
516 );
517 assert_eq!(stored, 3);
518 }
519
520 #[test]
521 fn merge_with_disjoint_files_combines_inodes() {
522 let temp1 =
535 std::env::temp_dir().join(format!("limnifs-flatten-disjoint-1-{}", std::process::id()));
536 let temp2 =
537 std::env::temp_dir().join(format!("limnifs-flatten-disjoint-2-{}", std::process::id()));
538 make_tree(&temp1, &[("a.txt", b"aaa")]);
539 make_tree(&temp2, &[("b.txt", b"bbb")]);
540 let a1 = write_directory(&temp1).expect("write1");
541 let a2 = write_directory(&temp2).expect("write2");
542 std::fs::remove_dir_all(&temp1).ok();
543 std::fs::remove_dir_all(&temp2).ok();
544
545 let flat = flatten(&[&a1.bytes, &a2.bytes]).expect("flatten");
546 assert_eq!(flat.layer_count, 2);
547 assert_eq!(flat.inode_count, 2);
550
551 let mut cursor = ManifestCursor::new(&flat.bytes);
552 parse_manifest_header(&mut cursor).unwrap();
553 parse_feature_flags_section(&mut cursor).unwrap();
554 let meta_ref = parse_metadata_reference(&mut cursor).unwrap();
555 let blob_bytes = meta_ref.inline_metadata.as_deref().expect("inlined");
556 let mut blob_cursor = ManifestCursor::new(blob_bytes);
557 let blob = parse_metadata_blob(&mut blob_cursor).expect("blob");
558 let has_b = blob
561 .inodes
562 .iter()
563 .any(|i| matches!(&i.content_handle, ContentHandle::InlineData(d) if d == b"bbb"));
564 assert!(has_b, "latest layer's content must win on inode conflict");
565 }
566
567 #[test]
568 fn flatten_is_deterministic() {
569 let temp1 =
570 std::env::temp_dir().join(format!("limnifs-flatten-det-1-{}", std::process::id()));
571 let temp2 =
572 std::env::temp_dir().join(format!("limnifs-flatten-det-2-{}", std::process::id()));
573 make_tree(&temp1, &[("x", b"1")]);
574 make_tree(&temp2, &[("y", b"2")]);
575 let a1 = write_directory(&temp1).expect("w1");
576 let a2 = write_directory(&temp2).expect("w2");
577 std::fs::remove_dir_all(&temp1).ok();
578 std::fs::remove_dir_all(&temp2).ok();
579
580 let f1 = flatten(&[&a1.bytes, &a2.bytes]).expect("flatten");
581 let f2 = flatten(&[&a1.bytes, &a2.bytes]).expect("flatten");
582 assert_eq!(f1.bytes, f2.bytes, "flatten must be deterministic");
583 assert_eq!(f1.merkle_root, f2.merkle_root);
584 }
585
586 #[test]
587 fn flatten_preserves_drop_ids() {
588 let large = vec![0x42u8; 8192]; let temp1 =
593 std::env::temp_dir().join(format!("limnifs-flatten-drops-1-{}", std::process::id()));
594 let temp2 =
595 std::env::temp_dir().join(format!("limnifs-flatten-drops-2-{}", std::process::id()));
596 make_tree(&temp1, &[("a.bin", &large)]);
597 make_tree(&temp2, &[("b.bin", &large)]);
598 let a1 = write_directory(&temp1).expect("w1");
599 let a2 = write_directory(&temp2).expect("w2");
600 std::fs::remove_dir_all(&temp1).ok();
601 std::fs::remove_dir_all(&temp2).ok();
602
603 let input_drops: std::collections::HashSet<[u8; 32]> =
604 extract_drop_ids(&[&a1.bytes, &a2.bytes]);
605 let flat = flatten(&[&a1.bytes, &a2.bytes]).expect("flatten");
606 let flat_drops = extract_drop_ids(&[&flat.bytes]);
607
608 assert_eq!(input_drops, flat_drops, "flatten must preserve all DropIds");
609 }
610
611 fn extract_drop_ids(layers: &[&[u8]]) -> std::collections::HashSet<[u8; 32]> {
612 let mut out = std::collections::HashSet::new();
613 for layer in layers {
614 let mut cursor = ManifestCursor::new(layer);
615 parse_manifest_header(&mut cursor).unwrap();
616 parse_feature_flags_section(&mut cursor).unwrap();
617 let meta_ref = parse_metadata_reference(&mut cursor).unwrap();
618 let Some(blob_bytes) = meta_ref.inline_metadata.as_deref() else {
619 continue;
620 };
621 let mut blob_cursor = ManifestCursor::new(blob_bytes);
622 let blob = parse_metadata_blob(&mut blob_cursor).unwrap();
623 for inode in &blob.inodes {
624 if let ContentHandle::SliceMap(slices) = &inode.content_handle {
625 for slice in slices {
626 out.insert(*slice.drop_id.as_bytes());
627 }
628 }
629 }
630 }
631 out
632 }
633}