#[cfg(feature = "fs")]
use crate::store::FsStore;
use crate::{
object::{ContentHash, PartialTree, Tree, TreeEntry, encode_redacted_projection},
store::{InMemoryStore, ObjectStore, PartialTreeWrite, TreeRead, codec},
};
fn ch(bytes: &[u8]) -> ContentHash {
ContentHash::compute(bytes)
}
fn fixture() -> (Tree, ContentHash, Vec<u8>, ContentHash) {
let entries = vec![
TreeEntry::file("readme.md", ch(b"readme"), false).unwrap(),
TreeEntry::file("secret.md", ch(b"secret"), false).unwrap(),
TreeEntry::directory("src", ch(b"src-tree")).unwrap(),
];
let salts = vec![[0x11; 32], [0x22; 32], [0x33; 32]];
let tree = Tree::from_entries_salted_v4(entries, salts).unwrap();
let root = tree.hash();
let withheld_leaf = Tree::from_entries_salted_v4(
vec![TreeEntry::file("secret.md", ch(b"secret"), false).unwrap()],
vec![[0x22; 32]],
)
.unwrap()
.hash();
let mut redact = std::collections::HashSet::new();
redact.insert(withheld_leaf);
let partial = PartialTree::project(&tree, &redact).unwrap();
assert_eq!(partial.redacted_count(), 1);
let body = encode_redacted_projection(&partial).unwrap();
(tree, root, body, withheld_leaf)
}
#[test]
fn reconstruct_root_of_partial_equals_canonical_hash() {
let (_full, root, body, _withheld) = fixture();
let partial = codec::decode_partial_tree(&body, root).unwrap();
assert_eq!(partial.reconstruct_root(), root);
assert_eq!(partial.declared_root(), root);
let wrong = ch(b"not-the-root");
match codec::decode_partial_tree(&body, wrong) {
Err(crate::store::HeddleError::Corruption { expected, found }) => {
assert_eq!(expected, wrong);
assert_eq!(found, root);
}
other => panic!("expected Corruption binding partial to wrong key, got {other:?}"),
}
}
fn assert_partial_distinct_from_full(store: &impl ObjectStore) {
let (_full, root, body, withheld) = fixture();
match store.put_partial_tree(&root, &body).unwrap() {
PartialTreeWrite::Stored { redacted, visible } => {
assert_eq!(redacted, 1);
assert_eq!(visible, 2);
}
other => panic!("expected Stored, got {other:?}"),
}
assert!(
!store.has_tree(&root).unwrap(),
"partial must not be a full tree"
);
assert!(
store.get_tree(&root).unwrap().is_none(),
"get_tree must not return the partial"
);
assert!(store.has_partial_tree(&root).unwrap());
assert_eq!(
store.get_partial_tree_bytes(&root).unwrap().as_deref(),
Some(body.as_slice())
);
assert_eq!(store.list_partial_trees().unwrap(), vec![root]);
match store.read_tree(&root).unwrap() {
TreeRead::Partial(p) => {
assert_eq!(p.reconstruct_root(), root);
assert_eq!(p.redacted_count(), 1);
assert_eq!(
p.leaves()[..]
.iter()
.find(|l| l.leaf_hash() == withheld)
.map(|l| l.is_redacted()),
Some(true)
);
}
other => panic!("expected Partial, got {other:?}"),
}
assert!(matches!(
store.read_tree(&ch(b"absent")).unwrap(),
TreeRead::Absent
));
}
fn assert_monotone(store: &impl ObjectStore) {
let (full, root, body, _withheld) = fixture();
store.put_tree(&full).unwrap();
assert_eq!(
store.put_partial_tree(&root, &body).unwrap(),
PartialTreeWrite::SupersededByFull
);
assert!(
!store.has_partial_tree(&root).unwrap(),
"partial must not overwrite the full"
);
assert!(matches!(store.read_tree(&root).unwrap(), TreeRead::Full(_)));
}
fn assert_full_supersedes_stale_partial(store: &impl ObjectStore) {
let (full, root, body, _withheld) = fixture();
store.put_partial_tree(&root, &body).unwrap();
assert!(store.has_partial_tree(&root).unwrap());
store.put_tree(&full).unwrap();
match store.read_tree(&root).unwrap() {
TreeRead::Full(t) => assert_eq!(t.hash(), root),
other => panic!("full must supersede the partial at read time, got {other:?}"),
}
store.remove_partial_tree(&root).unwrap();
assert!(!store.has_partial_tree(&root).unwrap());
}
fn assert_full_tree_regression(store: &impl ObjectStore) {
let (full, root, _body, _withheld) = fixture();
store.put_tree(&full).unwrap();
assert!(store.has_tree(&root).unwrap());
assert_eq!(store.get_tree(&root).unwrap().unwrap().hash(), root);
assert!(matches!(store.read_tree(&root).unwrap(), TreeRead::Full(_)));
assert!(!store.has_partial_tree(&root).unwrap());
}
fn assert_put_tree_serialized_routes_hrt1(store: &impl ObjectStore) {
let (_full, root, body, _withheld) = fixture();
let stored = store.put_tree_serialized(&body, root).unwrap();
assert_eq!(stored, root);
assert!(store.has_partial_tree(&root).unwrap());
assert!(!store.has_tree(&root).unwrap());
assert!(matches!(
store.read_tree(&root).unwrap(),
TreeRead::Partial(_)
));
}
#[test]
fn memory_partial_distinct_from_full() {
assert_partial_distinct_from_full(&InMemoryStore::new());
}
#[test]
fn memory_monotone() {
assert_monotone(&InMemoryStore::new());
}
#[test]
fn memory_full_supersedes_stale_partial() {
assert_full_supersedes_stale_partial(&InMemoryStore::new());
}
#[test]
fn memory_full_tree_regression() {
assert_full_tree_regression(&InMemoryStore::new());
}
#[test]
fn memory_put_tree_serialized_routes_hrt1() {
assert_put_tree_serialized_routes_hrt1(&InMemoryStore::new());
}
#[cfg(feature = "fs")]
fn fs_store() -> (tempfile::TempDir, FsStore) {
let temp = tempfile::TempDir::new().unwrap();
let store = FsStore::new(temp.path());
(temp, store)
}
#[test]
#[cfg(feature = "fs")]
fn fs_partial_distinct_from_full() {
let (_t, store) = fs_store();
assert_partial_distinct_from_full(&store);
}
#[test]
#[cfg(feature = "fs")]
fn fs_monotone() {
let (_t, store) = fs_store();
assert_monotone(&store);
}
#[test]
#[cfg(feature = "fs")]
fn fs_full_supersedes_stale_partial() {
let (_t, store) = fs_store();
assert_full_supersedes_stale_partial(&store);
}
#[test]
#[cfg(feature = "fs")]
fn fs_full_tree_regression() {
let (_t, store) = fs_store();
assert_full_tree_regression(&store);
}
#[test]
#[cfg(feature = "fs")]
fn fs_put_tree_serialized_routes_hrt1() {
let (_t, store) = fs_store();
assert_put_tree_serialized_routes_hrt1(&store);
}