use std::collections::HashMap;
use limnifs_core::{
compute_merkle_root, dir_node_hash, hash_empty_section, hash_section,
parse_feature_flags_section, parse_history, parse_manifest_header, parse_metadata_blob,
parse_metadata_reference, parse_slab_index, ContentHandle, CoreError, DirectoryNode, Inode,
ManifestCursor, SectionHashes, FEATURE_FLAGS_SECTION_VERSION, HISTORY_SECTION_VERSION,
METADATA_REFERENCE_SECTION_VERSION, SLAB_INDEX_SECTION_VERSION,
};
use limnifs_format::{ManifestRoot, SlabId};
#[derive(Debug)]
pub enum FlattenError {
Core(CoreError),
ExternalMetadata { layer: usize },
Empty,
}
impl std::fmt::Display for FlattenError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Core(e) => write!(f, "flatten: parse error: {e}"),
Self::ExternalMetadata { layer } => {
write!(
f,
"flatten: layer {layer} has external metadata (v1 requires inlined)"
)
}
Self::Empty => write!(f, "flatten: no input layers"),
}
}
}
impl std::error::Error for FlattenError {}
impl From<CoreError> for FlattenError {
fn from(e: CoreError) -> Self {
Self::Core(e)
}
}
#[derive(Clone, Debug)]
pub struct FlattenArtifact {
pub bytes: Vec<u8>,
pub merkle_root: ManifestRoot,
pub inode_count: usize,
pub dir_node_count: usize,
pub slab_count: usize,
pub layer_count: usize,
}
pub fn flatten(layers: &[&[u8]]) -> Result<FlattenArtifact, FlattenError> {
if layers.is_empty() {
return Err(FlattenError::Empty);
}
let layer_count = layers.len();
let mut merged_inodes: HashMap<u64, Inode> = HashMap::new();
let mut merged_dir_nodes: HashMap<[u8; 32], DirectoryNode> = HashMap::new();
let mut merged_slab_entries: Vec<(SlabId, Vec<String>)> = Vec::new();
let mut seen_slab_ids: HashMap<[u8; 40], usize> = HashMap::new();
for (i, layer_bytes) in layers.iter().enumerate() {
let parsed = parse_layer(layer_bytes)?;
if !parsed.metadata_reference.is_inlined() {
return Err(FlattenError::ExternalMetadata { layer: i });
}
let Some(blob_bytes) = parsed.metadata_reference.inline_metadata.as_deref() else {
return Err(FlattenError::ExternalMetadata { layer: i });
};
let mut blob_cursor = ManifestCursor::new(blob_bytes);
let blob = parse_metadata_blob(&mut blob_cursor)?;
for inode in blob.inodes {
merged_inodes.insert(inode.number, inode);
}
for dir_node in blob.dir_nodes {
let hash = dir_node_hash(&dir_node.entries);
merged_dir_nodes.insert(hash, dir_node);
}
for slab in parsed.slab_index {
let key = slab.slab_id.to_bytes();
if let Some(&idx) = seen_slab_ids.get(&key) {
let existing = merged_slab_entries
.get_mut(idx)
.expect("seen_slab_ids points into merged_slab_entries");
for loc in slab.locators {
let uri = loc.uri;
if !existing.1.contains(&uri) {
existing.1.push(uri);
}
}
} else {
seen_slab_ids.insert(key, merged_slab_entries.len());
let locators: Vec<String> = slab.locators.into_iter().map(|l| l.uri).collect();
merged_slab_entries.push((slab.slab_id, locators));
}
}
}
let mut inodes: Vec<Inode> = merged_inodes.into_values().collect();
inodes.sort_by_key(|i| i.number);
let mut dir_nodes: Vec<DirectoryNode> = merged_dir_nodes.into_values().collect();
dir_nodes.sort_by_key(|a| dir_node_hash(&a.entries));
merged_slab_entries.sort_by_key(|a| a.0.to_bytes());
let inode_count = inodes.len();
let dir_node_count = dir_nodes.len();
let slab_count = merged_slab_entries.len();
let bytes = encode_manifest(
&inodes,
&dir_nodes,
&merged_slab_entries,
u64::try_from(layer_count).expect("layer_count fits u64"),
);
let merkle_root = compute_merkle_root_from_sections(&bytes);
Ok(FlattenArtifact {
bytes,
merkle_root,
inode_count,
dir_node_count,
slab_count,
layer_count,
})
}
struct ParsedLayer {
metadata_reference: limnifs_core::MetadataReference,
slab_index: Vec<limnifs_core::SlabIndexEntry>,
}
fn parse_layer(bytes: &[u8]) -> Result<ParsedLayer, CoreError> {
let mut cursor = ManifestCursor::new(bytes);
let _ = parse_manifest_header(&mut cursor)?;
let _ = parse_feature_flags_section(&mut cursor)?;
let metadata_reference = parse_metadata_reference(&mut cursor)?;
let slab_index_v = parse_slab_index(&mut cursor)?;
let _ = parse_history(&mut cursor)?;
Ok(ParsedLayer {
metadata_reference,
slab_index: slab_index_v.entries,
})
}
fn encode_manifest(
inodes: &[Inode],
dir_nodes: &[DirectoryNode],
slab_entries: &[(SlabId, Vec<String>)],
layer_count: u64,
) -> Vec<u8> {
let metadata_blob = encode_metadata_blob(inodes, dir_nodes);
let metadata_hash = hash_section(&metadata_blob);
let mut manifest = Vec::new();
let header_start = manifest.len();
manifest.extend_from_slice(&limnifs_core::ManifestHeader::current().to_bytes());
let header_end = manifest.len();
let flags_start = manifest.len();
manifest.push(FEATURE_FLAGS_SECTION_VERSION);
manifest.extend_from_slice(&0u32.to_le_bytes());
let flags_end = manifest.len();
let meta_ref_start = manifest.len();
manifest.push(METADATA_REFERENCE_SECTION_VERSION);
manifest.extend_from_slice(&metadata_hash);
manifest.extend_from_slice(&0u32.to_le_bytes());
let inline_len = u32::try_from(metadata_blob.len()).expect("metadata fits u32");
manifest.extend_from_slice(&inline_len.to_le_bytes());
manifest.extend_from_slice(&metadata_blob);
let meta_ref_end = manifest.len();
let slab_index_start = manifest.len();
manifest.push(SLAB_INDEX_SECTION_VERSION);
manifest.extend_from_slice(&u32::try_from(slab_entries.len()).unwrap().to_le_bytes());
for (slab_id, locators) in slab_entries {
manifest.extend_from_slice(&slab_id.to_bytes());
manifest.extend_from_slice(&u32::try_from(locators.len()).unwrap().to_le_bytes());
for loc in locators {
let loc_bytes = loc.as_bytes();
let loc_len = u32::try_from(loc_bytes.len()).expect("locator fits u32");
manifest.extend_from_slice(&loc_len.to_le_bytes());
manifest.extend_from_slice(loc_bytes);
}
}
let slab_index_end = manifest.len();
let history_start = manifest.len();
manifest.push(HISTORY_SECTION_VERSION);
manifest.extend_from_slice(&1u32.to_le_bytes());
manifest.push(0x03);
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();
manifest.extend_from_slice(
&u32::try_from(layer_count_bytes.len())
.unwrap()
.to_le_bytes(),
);
manifest.extend_from_slice(&layer_count_bytes);
let history_end = manifest.len();
let hashes = SectionHashes {
metadata: metadata_hash,
format_header: hash_section(&manifest[header_start..header_end]),
feature_flags: hash_section(&manifest[flags_start..flags_end]),
metadata_reference: hash_section(&manifest[meta_ref_start..meta_ref_end]),
slab_index: hash_section(&manifest[slab_index_start..slab_index_end]),
crypto_params: hash_empty_section(),
ec_params: hash_empty_section(),
dms_policy: hash_empty_section(),
delta_linkage: hash_empty_section(),
history: hash_section(&manifest[history_start..history_end]),
};
let _ = hashes;
manifest
}
fn compute_merkle_root_from_sections(manifest: &[u8]) -> ManifestRoot {
let mut cursor = ManifestCursor::new(manifest);
let header_start = 0;
parse_manifest_header(&mut cursor).expect("we just encoded this");
let header_end = cursor.position();
let flags_start = header_end;
parse_feature_flags_section(&mut cursor).expect("we just encoded this");
let flags_end = cursor.position();
let meta_ref_start = flags_end;
let metadata_reference = parse_metadata_reference(&mut cursor).expect("we just encoded this");
let meta_ref_end = cursor.position();
let slab_index_start = meta_ref_end;
parse_slab_index(&mut cursor).expect("we just encoded this");
let slab_index_end = cursor.position();
parse_history(&mut cursor).expect("we just encoded this");
let history_end = cursor.position();
let _ = history_end;
let hashes = SectionHashes {
metadata: metadata_reference.metadata_hash,
format_header: hash_section(&manifest[header_start..header_end]),
feature_flags: hash_section(&manifest[flags_start..flags_end]),
metadata_reference: hash_section(&manifest[meta_ref_start..meta_ref_end]),
slab_index: hash_section(&manifest[slab_index_start..slab_index_end]),
crypto_params: hash_empty_section(),
ec_params: hash_empty_section(),
dms_policy: hash_empty_section(),
delta_linkage: hash_empty_section(),
history: hash_section(&manifest[slab_index_end..cursor.position()]),
};
compute_merkle_root(&hashes)
}
fn encode_metadata_blob(inodes: &[Inode], dir_nodes: &[DirectoryNode]) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&u32::try_from(inodes.len()).unwrap().to_le_bytes());
for inode in inodes {
encode_inode(&mut out, inode);
}
out.extend_from_slice(&u32::try_from(dir_nodes.len()).unwrap().to_le_bytes());
for dir_node in dir_nodes {
encode_dir_node(&mut out, dir_node);
}
out
}
fn encode_inode(out: &mut Vec<u8>, inode: &Inode) {
out.extend_from_slice(&inode.number.to_le_bytes());
out.extend_from_slice(&inode.mode.to_le_bytes());
out.extend_from_slice(&0u32.to_le_bytes());
out.extend_from_slice(&0u32.to_le_bytes());
out.extend_from_slice(&inode.mtime_ns.to_le_bytes());
out.extend_from_slice(&inode.mtime_ns.to_le_bytes());
out.extend_from_slice(&1u32.to_le_bytes());
match &inode.content_handle {
ContentHandle::InlineData(data) => {
out.push(0x04);
let len = u32::try_from(data.len()).expect("data fits u32");
out.extend_from_slice(&len.to_le_bytes());
out.extend_from_slice(data);
}
ContentHandle::SharedInline(_) => {
out.push(0x04);
out.extend_from_slice(&0u32.to_le_bytes());
}
ContentHandle::SliceMap(slices) => {
out.push(0x00);
let slice_count = u32::try_from(slices.len()).expect("slice count fits u32");
out.extend_from_slice(&slice_count.to_le_bytes());
for slice in slices {
out.extend_from_slice(&slice.file_byte_start.to_le_bytes());
out.extend_from_slice(&slice.file_byte_end.to_le_bytes());
out.extend_from_slice(slice.drop_id.as_bytes());
out.extend_from_slice(&0u32.to_le_bytes());
let drop_byte_len = u32::try_from(slice.file_byte_end - slice.file_byte_start)
.expect("slice range fits u32");
out.extend_from_slice(&drop_byte_len.to_le_bytes());
}
}
ContentHandle::Directory(hash) => {
out.push(0x00);
out.extend_from_slice(hash);
}
ContentHandle::Symlink(_) | ContentHandle::Device(_) | ContentHandle::Pipe(_) => {
out.push(0x00);
out.extend_from_slice(&0u32.to_le_bytes());
}
}
}
fn encode_dir_node(out: &mut Vec<u8>, dir_node: &DirectoryNode) {
out.push(1u8); let count = u32::try_from(dir_node.entries.len()).expect("entry count fits u32");
out.extend_from_slice(&count.to_le_bytes());
for entry in &dir_node.entries {
let name_bytes = entry.name.as_bytes();
let name_len = u32::try_from(name_bytes.len()).expect("name fits u32");
out.extend_from_slice(&name_len.to_le_bytes());
out.extend_from_slice(name_bytes);
out.extend_from_slice(&entry.inode_number.to_le_bytes());
out.push(entry.entry_type);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::write_directory;
use std::path::Path;
fn make_tree(dir: &Path, files: &[(&str, &[u8])]) {
std::fs::create_dir_all(dir).expect("mkdir");
for (name, content) in files {
std::fs::write(dir.join(name), content).expect("write");
}
}
#[test]
fn rejects_empty_input() {
let err = flatten(&[]).unwrap_err();
assert!(matches!(err, FlattenError::Empty));
}
#[test]
fn single_layer_round_trips() {
let temp = std::env::temp_dir().join(format!(
"limnifs-flatten-single-{}-{}",
std::process::id(),
"a"
));
make_tree(&temp, &[("a.txt", b"aaa"), ("b.txt", b"bbb")]);
let artifact = write_directory(&temp).expect("write");
std::fs::remove_dir_all(&temp).ok();
let flat = flatten(&[&artifact.bytes]).expect("flatten");
assert_eq!(flat.layer_count, 1);
assert_eq!(flat.inode_count, artifact.inode_count);
let _ = flat.merkle_root;
}
#[test]
fn merge_two_layers_latest_wins() {
let temp1 =
std::env::temp_dir().join(format!("limnifs-flatten-2a-{}-{}", std::process::id(), "x"));
let temp2 =
std::env::temp_dir().join(format!("limnifs-flatten-2b-{}-{}", std::process::id(), "x"));
make_tree(&temp1, &[("a.txt", b"old")]);
make_tree(&temp2, &[("a.txt", b"new")]);
let a1 = write_directory(&temp1).expect("write1");
let a2 = write_directory(&temp2).expect("write2");
std::fs::remove_dir_all(&temp1).ok();
std::fs::remove_dir_all(&temp2).ok();
let flat = flatten(&[&a1.bytes, &a2.bytes]).expect("flatten");
let mut cursor = ManifestCursor::new(&flat.bytes);
parse_manifest_header(&mut cursor).expect("header");
parse_feature_flags_section(&mut cursor).expect("flags");
let meta_ref = parse_metadata_reference(&mut cursor).expect("meta ref");
assert!(meta_ref.is_inlined());
parse_slab_index(&mut cursor).expect("slab index");
parse_history(&mut cursor).expect("history");
assert_eq!(flat.layer_count, 2);
assert_eq!(flat.inode_count, a2.inode_count);
}
#[test]
fn merge_three_layers_preserves_history_op() {
let temp1 =
std::env::temp_dir().join(format!("limnifs-flatten-3a-{}-{}", std::process::id(), "y"));
let temp2 =
std::env::temp_dir().join(format!("limnifs-flatten-3b-{}-{}", std::process::id(), "y"));
let temp3 =
std::env::temp_dir().join(format!("limnifs-flatten-3c-{}-{}", std::process::id(), "y"));
make_tree(&temp1, &[("a", b"1")]);
make_tree(&temp2, &[("a", b"2")]);
make_tree(&temp3, &[("a", b"3")]);
let layers: Vec<_> = [&temp1, &temp2, &temp3]
.iter()
.map(|p| write_directory(p).expect("write").bytes)
.collect();
for t in [&temp1, &temp2, &temp3] {
std::fs::remove_dir_all(t).ok();
}
let layer_refs: Vec<&[u8]> = layers.iter().map(Vec::as_slice).collect();
let flat = flatten(&layer_refs).expect("flatten");
assert_eq!(flat.layer_count, 3);
let mut cursor = ManifestCursor::new(&flat.bytes);
parse_manifest_header(&mut cursor).unwrap();
parse_feature_flags_section(&mut cursor).unwrap();
parse_metadata_reference(&mut cursor).unwrap();
parse_slab_index(&mut cursor).unwrap();
let history = parse_history(&mut cursor).unwrap();
assert_eq!(history.entries.len(), 1);
let entry = &history.entries[0];
assert_eq!(entry.op, limnifs_core::HistoryOp::Flatten);
let stored = u64::from_le_bytes(
entry
.params
.get(0..8)
.and_then(|s| s.try_into().ok())
.unwrap_or([0u8; 8]),
);
assert_eq!(stored, 3);
}
#[test]
fn merge_with_disjoint_files_combines_inodes() {
let temp1 =
std::env::temp_dir().join(format!("limnifs-flatten-disjoint-1-{}", std::process::id()));
let temp2 =
std::env::temp_dir().join(format!("limnifs-flatten-disjoint-2-{}", std::process::id()));
make_tree(&temp1, &[("a.txt", b"aaa")]);
make_tree(&temp2, &[("b.txt", b"bbb")]);
let a1 = write_directory(&temp1).expect("write1");
let a2 = write_directory(&temp2).expect("write2");
std::fs::remove_dir_all(&temp1).ok();
std::fs::remove_dir_all(&temp2).ok();
let flat = flatten(&[&a1.bytes, &a2.bytes]).expect("flatten");
assert_eq!(flat.layer_count, 2);
assert_eq!(flat.inode_count, 2);
let mut cursor = ManifestCursor::new(&flat.bytes);
parse_manifest_header(&mut cursor).unwrap();
parse_feature_flags_section(&mut cursor).unwrap();
let meta_ref = parse_metadata_reference(&mut cursor).unwrap();
let blob_bytes = meta_ref.inline_metadata.as_deref().expect("inlined");
let mut blob_cursor = ManifestCursor::new(blob_bytes);
let blob = parse_metadata_blob(&mut blob_cursor).expect("blob");
let has_b = blob
.inodes
.iter()
.any(|i| matches!(&i.content_handle, ContentHandle::InlineData(d) if d == b"bbb"));
assert!(has_b, "latest layer's content must win on inode conflict");
}
#[test]
fn flatten_is_deterministic() {
let temp1 =
std::env::temp_dir().join(format!("limnifs-flatten-det-1-{}", std::process::id()));
let temp2 =
std::env::temp_dir().join(format!("limnifs-flatten-det-2-{}", std::process::id()));
make_tree(&temp1, &[("x", b"1")]);
make_tree(&temp2, &[("y", b"2")]);
let a1 = write_directory(&temp1).expect("w1");
let a2 = write_directory(&temp2).expect("w2");
std::fs::remove_dir_all(&temp1).ok();
std::fs::remove_dir_all(&temp2).ok();
let f1 = flatten(&[&a1.bytes, &a2.bytes]).expect("flatten");
let f2 = flatten(&[&a1.bytes, &a2.bytes]).expect("flatten");
assert_eq!(f1.bytes, f2.bytes, "flatten must be deterministic");
assert_eq!(f1.merkle_root, f2.merkle_root);
}
#[test]
fn flatten_preserves_drop_ids() {
let large = vec![0x42u8; 8192]; let temp1 =
std::env::temp_dir().join(format!("limnifs-flatten-drops-1-{}", std::process::id()));
let temp2 =
std::env::temp_dir().join(format!("limnifs-flatten-drops-2-{}", std::process::id()));
make_tree(&temp1, &[("a.bin", &large)]);
make_tree(&temp2, &[("b.bin", &large)]);
let a1 = write_directory(&temp1).expect("w1");
let a2 = write_directory(&temp2).expect("w2");
std::fs::remove_dir_all(&temp1).ok();
std::fs::remove_dir_all(&temp2).ok();
let input_drops: std::collections::HashSet<[u8; 32]> =
extract_drop_ids(&[&a1.bytes, &a2.bytes]);
let flat = flatten(&[&a1.bytes, &a2.bytes]).expect("flatten");
let flat_drops = extract_drop_ids(&[&flat.bytes]);
assert_eq!(input_drops, flat_drops, "flatten must preserve all DropIds");
}
fn extract_drop_ids(layers: &[&[u8]]) -> std::collections::HashSet<[u8; 32]> {
let mut out = std::collections::HashSet::new();
for layer in layers {
let mut cursor = ManifestCursor::new(layer);
parse_manifest_header(&mut cursor).unwrap();
parse_feature_flags_section(&mut cursor).unwrap();
let meta_ref = parse_metadata_reference(&mut cursor).unwrap();
let Some(blob_bytes) = meta_ref.inline_metadata.as_deref() else {
continue;
};
let mut blob_cursor = ManifestCursor::new(blob_bytes);
let blob = parse_metadata_blob(&mut blob_cursor).unwrap();
for inode in &blob.inodes {
if let ContentHandle::SliceMap(slices) = &inode.content_handle {
for slice in slices {
out.insert(*slice.drop_id.as_bytes());
}
}
}
}
out
}
}