use std::error::Error;
use std::fs;
use std::path::Path;
use crate::branch::refstore::BranchRefStore;
use crate::branch::snapshot::SnapshotRegistry;
use crate::branch::{BranchRefRecord, BranchShardRef};
use crate::tree::Hash;
use super::metadata::{read_refs_dir, read_snapshot_registry};
fn copy_dir(from: &Path, to: &Path) -> Result<(), Box<dyn Error>> {
fs::create_dir_all(to)?;
for entry in fs::read_dir(from)? {
let entry = entry?;
fs::copy(entry.path(), to.join(entry.file_name()))?;
}
Ok(())
}
fn production_refs_outcome(dir: &Path) -> Result<String, String> {
BranchRefStore::open(dir)
.map(|store| format!("{:?}", store.list().collect::<Vec<_>>()))
.map_err(|_error| "reject".to_owned())
}
fn vacuum_refs_outcome(dir: &Path) -> Result<String, String> {
read_refs_dir(dir)
.map(|scan| format!("{:?}", scan.records.iter().collect::<Vec<_>>()))
.map_err(|_error| "reject".to_owned())
}
fn assert_refs_equivalent(fixture: &Path, case: &str) -> Result<(), Box<dyn Error>> {
let (_production, _vacuum) = refs_outcomes(fixture, case)?;
Ok(())
}
fn assert_refs_both_reject(fixture: &Path, case: &str) -> Result<(), Box<dyn Error>> {
let (production, vacuum) = refs_outcomes(fixture, case)?;
assert!(production.is_err(), "case {case}: production must reject");
assert!(vacuum.is_err(), "case {case}: the vacuum must reject");
Ok(())
}
type Outcome = Result<String, String>;
fn refs_outcomes(fixture: &Path, case: &str) -> Result<(Outcome, Outcome), Box<dyn Error>> {
let production_copy = tempfile::tempdir()?;
let vacuum_copy = tempfile::tempdir()?;
copy_dir(fixture, production_copy.path())?;
copy_dir(fixture, vacuum_copy.path())?;
let production = production_refs_outcome(production_copy.path());
let vacuum = vacuum_refs_outcome(vacuum_copy.path());
assert_eq!(
production, vacuum,
"case {case}: the two readers must accept/reject identically"
);
Ok((production, vacuum))
}
fn valid_refs_fixture(dir: &Path) -> Result<(), Box<dyn Error>> {
let mut store = BranchRefStore::open(dir)?;
store.create(BranchRefRecord {
name: "fixture".to_owned(),
created: 7,
kind: crate::branch::BranchKind::Work,
namespace_lineage: None,
seq: 3,
timestamp: 11,
shards: vec![BranchShardRef {
shard_id: 1,
fork_anchor: Hash::from_bytes([0x11; 32]),
head: Hash::from_bytes([0x22; 32]),
}],
parents: vec![Hash::from_bytes([0x33; 32])],
})?;
Ok(())
}
fn sole_ref_file(dir: &Path) -> Result<std::path::PathBuf, Box<dyn Error>> {
fs::read_dir(dir)?
.filter_map(Result::ok)
.map(|entry| entry.path())
.find(|path| path.extension().and_then(|ext| ext.to_str()) == Some("ref"))
.ok_or_else(|| "fixture holds one .ref file".into())
}
#[test]
fn hbr1_corpus_accepts_and_rejects_identically() -> Result<(), Box<dyn Error>> {
let valid = tempfile::tempdir()?;
valid_refs_fixture(valid.path())?;
assert_refs_equivalent(valid.path(), "valid")?;
let truncated = tempfile::tempdir()?;
valid_refs_fixture(truncated.path())?;
let ref_file = sole_ref_file(truncated.path())?;
let bytes = fs::read(&ref_file)?;
fs::write(&ref_file, &bytes[..bytes.len() - 5])?;
assert_refs_equivalent(truncated.path(), "truncated")?;
let trailing = tempfile::tempdir()?;
valid_refs_fixture(trailing.path())?;
let ref_file = sole_ref_file(trailing.path())?;
let mut bytes = fs::read(&ref_file)?;
bytes.extend_from_slice(b"xyz");
fs::write(&ref_file, bytes)?;
assert_refs_equivalent(trailing.path(), "trailing")?;
let magic = tempfile::tempdir()?;
valid_refs_fixture(magic.path())?;
let ref_file = sole_ref_file(magic.path())?;
let mut bytes = fs::read(&ref_file)?;
bytes[0] = b'X';
fs::write(&ref_file, bytes)?;
assert_refs_equivalent(magic.path(), "magic")?;
let misfiled = tempfile::tempdir()?;
valid_refs_fixture(misfiled.path())?;
let ref_file = sole_ref_file(misfiled.path())?;
fs::rename(
&ref_file,
misfiled.path().join("00000000000000000000000000000000.ref"),
)?;
assert_refs_equivalent(misfiled.path(), "misfiled")?;
let foreign = tempfile::tempdir()?;
valid_refs_fixture(foreign.path())?;
fs::write(foreign.path().join("README.txt"), b"not a record")?;
assert_refs_equivalent(foreign.path(), "foreign")?;
let debris = tempfile::tempdir()?;
valid_refs_fixture(debris.path())?;
fs::write(debris.path().join(".branch-crash.tmp"), b"leftover")?;
assert_refs_equivalent(debris.path(), "debris")?;
let corrupt_length = tempfile::tempdir()?;
valid_refs_fixture(corrupt_length.path())?;
let ref_file = sole_ref_file(corrupt_length.path())?;
let mut bytes = fs::read(&ref_file)?;
bytes[10] = 0xFF; fs::write(&ref_file, bytes)?;
assert_refs_both_reject(corrupt_length.path(), "corrupt-length-prefix")?;
let mutated = tempfile::tempdir()?;
valid_refs_fixture(mutated.path())?;
let ref_file = sole_ref_file(mutated.path())?;
let mut bytes = fs::read(&ref_file)?;
let middle = bytes.len() / 2;
bytes[middle] ^= 0xFF;
fs::write(&ref_file, bytes)?;
assert_refs_equivalent(mutated.path(), "payload-mutation-equivalent")?;
let huge_count = tempfile::tempdir()?;
let mut bytes = Vec::new();
bytes.extend_from_slice(b"HBR1");
crate::branch::persist::push_bytes(&mut bytes, b"x");
crate::branch::persist::push_u64(&mut bytes, 1); crate::branch::persist::push_u64(&mut bytes, 1); crate::branch::persist::push_u64(&mut bytes, 1); crate::branch::persist::push_u64(&mut bytes, u64::MAX); fs::write(
huge_count
.path()
.join("00000000000000000000000000000000.ref"),
bytes,
)?;
assert_refs_both_reject(huge_count.path(), "huge-shard-count")?;
let non_utf8 = tempfile::tempdir()?;
let mut bytes = Vec::new();
bytes.extend_from_slice(b"HBR1");
crate::branch::persist::push_bytes(&mut bytes, &[0xFF, 0xFE, 0xFD]);
crate::branch::persist::push_u64(&mut bytes, 1); crate::branch::persist::push_u64(&mut bytes, 1); crate::branch::persist::push_u64(&mut bytes, 1); crate::branch::persist::push_u64(&mut bytes, 0); crate::branch::persist::push_u64(&mut bytes, 0); fs::write(
non_utf8.path().join("00000000000000000000000000000000.ref"),
bytes,
)?;
assert_refs_equivalent(non_utf8.path(), "non-utf8-name")?;
Ok(())
}
fn production_registry_outcome(path: &Path) -> Result<String, String> {
SnapshotRegistry::open(path)
.map(|registry| format!("{:?}", registry.list_snapshots()))
.map_err(|_error| "reject".to_owned())
}
fn vacuum_registry_outcome(path: &Path) -> Result<String, String> {
read_snapshot_registry(path)
.map(|entries| {
format!(
"{:?}",
entries
.iter()
.map(|entry| (entry.name.clone(), entry.root_hash, entry.timestamp))
.collect::<Vec<_>>()
)
})
.map_err(|_error| "reject".to_owned())
}
fn assert_registry_equivalent(path: &Path, case: &str) {
assert_eq!(
production_registry_outcome(path),
vacuum_registry_outcome(path),
"case {case}: the two readers must accept/reject identically"
);
}
fn assert_registry_both_reject(path: &Path, case: &str) {
let production = production_registry_outcome(path);
let vacuum = vacuum_registry_outcome(path);
assert_eq!(
production, vacuum,
"case {case}: the two readers must accept/reject identically"
);
assert!(production.is_err(), "case {case}: production must reject");
assert!(vacuum.is_err(), "case {case}: the vacuum must reject");
}
#[test]
fn hsr1_corpus_accepts_and_rejects_identically() -> Result<(), Box<dyn Error>> {
let temp = tempfile::tempdir()?;
let path = temp.path().join("snapshots.hsr");
let mut registry = SnapshotRegistry::open(&path)?;
registry.name_at("one", Hash::from_bytes([0x44; 32]), 5)?;
registry.name_at("two", Hash::from_bytes([0x55; 32]), 6)?;
drop(registry);
let valid_bytes = fs::read(&path)?;
assert_registry_equivalent(&path, "valid");
fs::write(&path, &valid_bytes[..valid_bytes.len() - 3])?;
assert_registry_equivalent(&path, "truncated");
let mut trailing = valid_bytes.clone();
trailing.extend_from_slice(b"zz");
fs::write(&path, trailing)?;
assert_registry_equivalent(&path, "trailing");
let mut magic = valid_bytes.clone();
magic[0] = b'X';
fs::write(&path, magic)?;
assert_registry_equivalent(&path, "magic");
let mut corrupt_count = valid_bytes.clone();
corrupt_count[10] = 0xFF; fs::write(&path, corrupt_count)?;
assert_registry_both_reject(&path, "corrupt-count-prefix");
let mut mutated = valid_bytes;
let middle = mutated.len() / 2;
mutated[middle] ^= 0xFF;
fs::write(&path, mutated)?;
assert_registry_equivalent(&path, "payload-mutation-equivalent");
let mut short_hash = Vec::new();
short_hash.extend_from_slice(b"HSR1");
crate::branch::persist::push_u64(&mut short_hash, 1); crate::branch::persist::push_bytes(&mut short_hash, b"stub");
short_hash.extend_from_slice(&[0xAA; 20]); crate::branch::persist::push_u64(&mut short_hash, 5); fs::write(&path, short_hash)?;
assert_registry_equivalent(&path, "wrong-length-hash");
Ok(())
}