use heddle_format::compression::CompressionConfig;
use super::*;
use crate::object::{Attribution, Blob, ContentHash, Principal, State, StateId, Tree, TreeEntry};
fn fixture() -> (State, Vec<(PackObjectId, ObjectType, Vec<u8>)>) {
let blob = Blob::new(b"selected source".to_vec());
let tree = Tree::from_entries(vec![
TreeEntry::file("file.rs", blob.hash(), false).expect("file"),
TreeEntry::symlink("link", blob.hash()).expect("symlink"),
TreeEntry::spoollink(
"private-child",
crate::object::SpoolId::parse("owner/private-child").expect("spool ID"),
StateId::from_bytes([4; 32]),
)
.expect("spoollink"),
]);
let mut state = State::new_snapshot(
tree.hash(),
vec![StateId::from_bytes([3; 32])],
Attribution::human(Principal::new("owner", "owner@example.test")),
);
state.provenance = Some(ContentHash::compute(b"private provenance"));
let entries = vec![
(
PackObjectId::StateId(state.id()),
ObjectType::State,
state.encode_current_msgpack().expect("State"),
),
(
PackObjectId::Hash(tree.hash()),
ObjectType::Tree,
tree.encode_canonical().expect("tree"),
),
(
PackObjectId::Hash(blob.hash()),
ObjectType::Blob,
blob.into_content(),
),
];
(state, entries)
}
fn reader(entries: Vec<(PackObjectId, ObjectType, Vec<u8>)>) -> PackReader<'static> {
let mut builder = PackBuilder::for_repack(CompressionConfig::default(), 0);
for (id, kind, data) in entries {
builder.add_id(id, kind, data);
}
let (pack, index, _) = builder.build().expect("pack");
PackReader::from_bytes(pack, index, &std::env::temp_dir()).expect("reader")
}
#[test]
fn source_metadata_point_read_does_not_sort_physical_offsets() {
let (state, entries) = fixture();
let mut builder = PackBuilder::for_repack(CompressionConfig::default(), 0);
for (id, kind, bytes) in entries {
builder.add_id(id, kind, bytes);
}
let (pack, index, _) = builder.build().expect("pack");
let directory = tempfile::tempdir().expect("metadata fixture");
let pack_path = directory.path().join("source.pack");
let index_path = directory.path().join("source.idx");
let scratch = directory.path().join("tmp");
std::fs::write(&pack_path, pack).expect("pack bytes");
std::fs::write(&index_path, index).expect("index bytes");
let reader = PackReader::open(&pack_path, &index_path, &scratch).expect("reader");
let (kind, bytes) = reader
.get_object(&PackObjectId::StateId(state.id()))
.expect("point read")
.expect("selected State");
assert_eq!(kind, ObjectType::State);
assert_eq!(
bytes,
state.encode_current_msgpack().expect("canonical State")
);
assert!(
!scratch.exists(),
"a metadata point lookup needs no disk sort"
);
reader
.validate_source_closure(&state, 65536)
.expect("full closure");
assert!(
scratch.exists(),
"full closure traversal builds the disk index"
);
}
#[test]
fn selected_source_closure_is_complete_without_private_history_or_child_spools() {
let (state, entries) = fixture();
let reader = reader(entries);
reader
.validate_source_closure(&state, 65536)
.expect("complete selected source");
assert_eq!(reader.object_count(), 3);
}
#[test]
fn visible_source_pack_proves_hidden_leaves_without_reading_their_content() {
use crate::object::{
AudienceTier, EntryRedactions, EntryVisibilityEntry, PartialTree, PartialTreeLeaf,
VisibilityTier, encode_redacted_projection, visible,
};
let open = Blob::new(b"visible bytes".to_vec());
let secret = Blob::new(b"hidden bytes".to_vec());
let hidden_tree = ContentHash::compute(b"hidden subtree deliberately absent");
let tree = Tree::from_entries_salted_v4(
vec![
TreeEntry::directory("private-dir", hidden_tree).expect("directory"),
TreeEntry::file("secret-name.txt", secret.hash(), false).expect("secret"),
TreeEntry::file("visible.txt", open.hash(), false).expect("visible"),
],
vec![[1; 32], [2; 32], [3; 32]],
)
.expect("salted root");
let state = State::new_snapshot(
tree.hash(),
vec![],
Attribution::human(Principal::new("owner", "owner@example.test")),
);
let mut redactions = EntryRedactions::default();
for name in ["private-dir", "secret-name.txt"] {
redactions.extend_overrides(
&[EntryVisibilityEntry {
tree_id: tree.hash(),
leaf_hash: tree.v4_leaf_hash_for(name).expect("leaf"),
tier: VisibilityTier::Private {
scope_label: "security".into(),
},
}],
|tier| visible(tier, &AudienceTier::Internal),
);
}
let source = SelectedSource {
entries: vec![
(
PackObjectId::Hash(tree.hash()),
ObjectType::Tree,
tree.encode_canonical().expect("tree"),
),
(
PackObjectId::Hash(open.hash()),
ObjectType::Blob,
open.content().to_vec(),
),
],
blob_reads: std::cell::Cell::new(0),
};
let temp = tempfile::tempdir().expect("temporary pack");
let path = temp.path().join("visible.pack");
let index = temp.path().join("visible.idx");
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create_new(true)
.open(&path)
.expect("pack file");
let builder = StreamingPackBuilder::new(
file,
index.clone(),
Default::default(),
temp.path().join("buckets"),
)
.expect("builder");
build_visible_source_pack(builder, &source, &state, &[], &redactions, 65536)
.expect("visible closure requires no hidden bytes");
assert_eq!(source.blob_reads.get(), 1);
let packed = PackReader::open(&path, &index, &std::env::temp_dir()).expect("pack reader");
let verified = packed
.validate_visible_source_closure(&state, 65536)
.expect("verified disclosure");
assert_eq!(packed.object_count(), 3);
assert_eq!(verified.partial_tree_count(), 1);
let mut partial_trees = Vec::new();
verified
.visit_partial_trees(|partial| {
partial_trees.push(partial.clone());
Ok(())
})
.expect("partial proofs");
assert_eq!(partial_trees[0].declared_root(), state.tree);
assert_eq!(partial_trees[0].redacted_count(), 2);
assert!(
packed
.get_hashed_object(&secret.hash())
.expect("lookup hidden blob")
.is_none()
);
assert!(
packed
.get_hashed_object(&hidden_tree)
.expect("lookup hidden tree")
.is_none()
);
assert!(
packed.validate_source_closure(&state, 65536).is_err(),
"partial disclosure must not become complete source availability"
);
let mut records = Vec::new();
packed
.visit_objects(|id, kind, bytes| {
if kind == ObjectType::Tree {
assert!(
!bytes
.windows(b"secret-name.txt".len())
.any(|part| part == b"secret-name.txt")
);
}
records.push((id, kind, bytes.to_vec()));
Ok(())
})
.expect("inspect disclosed records");
let mut extra = records.clone();
extra.push((
PackObjectId::Hash(secret.hash()),
ObjectType::Blob,
secret.into_content(),
));
assert!(
reader(extra)
.validate_visible_source_closure(&state, 65536)
.is_err(),
"hidden bytes cannot be smuggled alongside a valid partial proof"
);
let partial = &partial_trees[0];
let mut forged = partial.leaves().to_vec();
let hidden = forged
.iter_mut()
.find(|leaf| matches!(leaf, PartialTreeLeaf::Redacted { .. }))
.expect("redacted leaf");
*hidden = PartialTreeLeaf::Redacted {
leaf_hash: ContentHash::compute(b"forged commitment"),
};
let forged = PartialTree::new(state.tree, forged);
let forged = encode_redacted_projection(&forged).expect("encode malformed proof fixture");
let root = records
.iter_mut()
.find(|(id, _, _)| *id == PackObjectId::Hash(state.tree))
.expect("root record");
root.2 = forged;
assert!(
reader(records)
.validate_visible_source_closure(&state, 65536)
.is_err(),
"a changed opaque commitment no longer proves the selected root"
);
let changed_tree = Tree::from_entries_salted_v4(
vec![TreeEntry::file("visible.txt", open.hash(), false).expect("changed entry")],
vec![[9; 32]],
)
.expect("fresh salted tree");
let changed = State::new_snapshot(
changed_tree.hash(),
vec![state.id()],
state.attribution.clone(),
);
let changed_source = SelectedSource {
entries: vec![
(
PackObjectId::Hash(changed_tree.hash()),
ObjectType::Tree,
changed_tree.encode_canonical().expect("changed tree"),
),
(
PackObjectId::Hash(open.hash()),
ObjectType::Blob,
open.content().to_vec(),
),
],
blob_reads: std::cell::Cell::new(0),
};
let changed_path = temp.path().join("changed.pack");
let changed_index = temp.path().join("changed.idx");
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create_new(true)
.open(&changed_path)
.expect("changed pack");
let builder = StreamingPackBuilder::new(
file,
changed_index.clone(),
Default::default(),
temp.path().join("changed-buckets"),
)
.expect("changed builder");
let (_, _, complete) =
build_visible_source_pack(builder, &changed_source, &changed, &[], &redactions, 65536)
.expect("fresh visible closure");
assert!(
complete,
"ancestor overrides do not taint a different salted leaf"
);
PackReader::open(&changed_path, &changed_index, &std::env::temp_dir())
.expect("changed reader")
.validate_source_closure(&changed, 65536)
.expect("actually complete source");
}
#[test]
fn publication_rejects_missing_source_and_unselected_objects() {
let (state, entries) = fixture();
let mut repeated = entries.clone();
repeated.push(entries[2].clone());
assert!(
reader(repeated)
.validate_source_closure(&state, 65536)
.is_err(),
"a selected source disclosure requires one record per identity"
);
let mut missing = entries.clone();
missing.pop();
assert!(
reader(missing)
.validate_source_closure(&state, 65536)
.is_err(),
"missing content must not produce an availability receipt"
);
let mut extra = entries.clone();
let secret = Blob::new(b"private transcript".to_vec());
extra.push((
PackObjectId::Hash(secret.hash()),
ObjectType::Blob,
secret.into_content(),
));
assert!(
reader(extra)
.validate_source_closure(&state, 65536)
.is_err(),
"unselected content cannot persist inside a source pack"
);
let mut false_hash = entries.clone();
false_hash[2].2 = b"changed content".to_vec();
assert!(
reader(false_hash)
.validate_source_closure(&state, 65536)
.is_err(),
"pack checksum alone does not verify logical object identity"
);
assert!(
reader(entries).validate_source_closure(&state, 1).is_err(),
"decoded byte budget"
);
}
#[test]
fn publication_rejects_unindexed_bytes_even_when_selected_objects_are_complete() {
let (state, entries) = fixture();
let mut builder = PackBuilder::for_repack(CompressionConfig::default(), 0);
for (id, kind, data) in entries {
builder.add_id(id, kind, data);
}
let (mut pack, index, _) = builder.build().expect("pack");
let secret = Blob::new(b"private transcript".to_vec());
pack.truncate(pack.len() - PACK_CHECKSUM_LEN);
encode_tagged_entry_parts(
&mut pack,
PackObjectId::Hash(secret.hash()),
ObjectType::Blob,
secret.size(),
None,
secret.content(),
)
.expect("unindexed record");
append_container_checksum(&mut pack);
let reader = PackReader::from_bytes(pack, index, &std::env::temp_dir())
.expect("well-formed container with trailing record");
assert!(
reader.validate_source_closure(&state, 65536).is_err(),
"an unindexed record must not cross the selected disclosure boundary"
);
}
struct SelectedSource {
entries: Vec<(PackObjectId, ObjectType, Vec<u8>)>,
blob_reads: std::cell::Cell<usize>,
}
impl crate::object::ObjectSource for SelectedSource {
fn get_tree(&self, hash: &ContentHash) -> crate::store::Result<Option<Tree>> {
let bytes = self
.entries
.iter()
.find(|(id, kind, _)| *id == PackObjectId::Hash(*hash) && *kind == ObjectType::Tree);
Ok(bytes
.map(|(_, _, bytes)| Tree::decode_canonical(bytes))
.transpose()?)
}
fn get_state(&self, _: &StateId) -> crate::store::Result<Option<State>> {
panic!("source publication must never read historical States");
}
fn get_blob(&self, hash: &ContentHash) -> crate::store::Result<Option<Blob>> {
self.blob_reads.set(self.blob_reads.get() + 1);
let bytes = self
.entries
.iter()
.find(|(id, kind, _)| *id == PackObjectId::Hash(*hash) && *kind == ObjectType::Blob);
Ok(bytes.map(|(_, _, bytes)| Blob::new(bytes.clone())))
}
fn decoded_blob_len(&self, hash: &ContentHash) -> crate::store::Result<Option<u64>> {
Ok(self
.entries
.iter()
.find(|(id, kind, _)| *id == PackObjectId::Hash(*hash) && *kind == ObjectType::Blob)
.map(|(_, _, bytes)| bytes.len() as u64))
}
}
fn export_selected(
source: &SelectedSource,
state: &State,
max_bytes: u64,
) -> crate::store::Result<PackReader<'static>> {
let dir = tempfile::tempdir().expect("source spool");
let index_path = dir.path().join("index");
let builder = StreamingPackBuilder::new(
std::io::Cursor::new(Vec::new()),
index_path.clone(),
CompressionConfig::default(),
dir.path().join("buckets"),
)?;
let (pack, stats) = build_source_pack(builder, source, state, max_bytes)?;
assert_eq!(stats.object_count, 3);
PackReader::from_bytes(
pack.into_inner(),
std::fs::read(index_path)?,
&std::env::temp_dir(),
)
}
#[test]
fn source_export_reads_only_selected_content_and_emits_a_complete_pack() {
let (state, entries) = fixture();
let source = SelectedSource {
entries,
blob_reads: std::cell::Cell::new(0),
};
let reader = export_selected(&source, &state, 65536).expect("source-only export");
assert_eq!(
source.blob_reads.get(),
1,
"a file and symlink sharing content need only one read"
);
reader
.validate_source_closure(&state, 65536)
.expect("exact closure");
assert_eq!(reader.object_count(), 3);
}
#[test]
fn source_export_checks_address_and_budget_before_publication() {
let (state, entries) = fixture();
let mut source = SelectedSource {
entries,
blob_reads: std::cell::Cell::new(0),
};
assert!(
export_selected(&source, &state, 1).is_err(),
"decoded byte budget"
);
assert_eq!(source.blob_reads.get(), 0);
source.entries[2].2 = b"wrong bytes under the original address".to_vec();
assert!(
export_selected(&source, &state, 65536).is_err(),
"producer must reject a corrupt source address"
);
}
#[test]
fn buffered_source_readers_keep_validation_scratch_under_the_supplied_root() {
let (state, entries) = fixture();
let mut builder = PackBuilder::for_repack(Default::default(), 0);
for (id, kind, bytes) in entries {
builder.add_id(id, kind, bytes);
}
let (pack, index, _) = builder.build().expect("pack");
let directory = tempfile::tempdir().expect("store");
let scratch = directory.path().join("tmp");
let borrowed = PackReader::from_slice(&pack, &index, &scratch).expect("borrowed reader");
let owned = PackReader::from_bytes(pack.clone(), &index, &scratch).expect("owned reader");
for reader in [&borrowed, &owned] {
let closure = reader
.validate_visible_source_closure(&state, 65536)
.expect("closure");
assert_eq!(
std::fs::read_dir(&scratch).expect("scratch files").count(),
1,
"proof scratch lives in the caller's root"
);
drop(closure);
assert_eq!(
std::fs::read_dir(&scratch).expect("clean scratch").count(),
0
);
}
}