#![forbid(unsafe_code)]
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use sha2::Digest;
use crate::store::{Store, StoreConfig};
fn parse_manifest(text: &str) -> BTreeMap<String, String> {
let mut m = BTreeMap::new();
for line in text.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((k, v)) = line.split_once('=') {
m.insert(k.trim().to_string(), v.trim().to_string());
}
}
m
}
fn fixture_files(m: &BTreeMap<String, String>) -> Vec<(String, usize, String)> {
let mut out = Vec::new();
for (k, v) in m {
if let Some(rest) = k.strip_prefix("file ") {
let _ = rest;
}
if k == "file" {
let parts: Vec<&str> = v.split_whitespace().collect();
if parts.len() == 3 {
out.push((
parts[0].to_string(),
parts[1].parse::<usize>().expect("size"),
parts[2].to_string(),
));
}
}
}
out.sort();
out
}
fn store_dir_hash(store_dir: &Path) -> String {
let mut rels: Vec<PathBuf> = Vec::new();
let mut stack = vec![store_dir.to_path_buf()];
while let Some(dir) = stack.pop() {
for entry in std::fs::read_dir(&dir).expect("read store dir") {
let path = entry.expect("entry").path();
if path.is_dir() {
stack.push(path);
} else if path.file_name().and_then(|n| n.to_str()) != Some("lock") {
rels.push(path);
}
}
}
rels.sort();
let mut lines = String::new();
for p in rels {
let bytes = std::fs::read(&p).expect("read file");
let h = hex_str(&sha2::Sha256::digest(&bytes));
let rel = format!(
"./{}",
p.strip_prefix(store_dir)
.expect("under store dir")
.to_string_lossy()
.replace('\\', "/")
);
lines.push_str(&format!("{h} {rel}\n"));
}
hex_str(&sha2::Sha256::digest(lines.as_bytes()))
}
fn hex_str(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect()
}
fn court_era(era: &str) {
let base = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("testdata")
.join("golden")
.join(era);
let manifest_text = std::fs::read_to_string(base.join("manifest.txt"))
.unwrap_or_else(|e| panic!("{era}: missing manifest.txt: {e}"));
let m = parse_manifest(&manifest_text);
let store_dir = base.join("store");
assert_eq!(
m.get("era").map(|s| s.as_str()),
Some(era),
"{era}: era mismatch"
);
let creator: &str = m.get("creator_version").map(|s| s.as_str()).unwrap_or("?");
let expected_creator = match era {
"v0.3.0" => "0.3.0",
"v0.5.2" => "0.5.2",
"v0.6.3" => "0.6.3",
_ => panic!("{era}: unknown era"),
};
assert_eq!(creator, expected_creator, "{era}: creator_version mismatch");
assert!(
m.contains_key("creator_revision"),
"{era}: missing creator_revision"
);
let config = StoreConfig::default();
let store = Store::open(&store_dir, &config)
.unwrap_or_else(|e| panic!("{era}: Store::open failed: {e}"));
let root = store.current_root();
let fmaj: u16 = m
.get("format_major")
.expect("format_major")
.parse()
.expect("major");
let fmin: u16 = m
.get("format_minor")
.expect("format_minor")
.parse()
.expect("minor");
assert_eq!(root.format_major, fmaj, "{era}: format major");
assert_eq!(root.format_minor, fmin, "{era}: format minor");
let bits = store.feature_bits();
match crate::format::features::check_with_version(bits, true, fmaj, fmin) {
crate::format::features::Compatibility::Ok => {}
other => panic!("{era}: compatibility decision must be Ok, got {other:?}"),
}
let report = crate::fsck::fsck(&store_dir, &crate::fsck::FsckOptions::default())
.unwrap_or_else(|e| panic!("{era}: fsck failed: {e}"));
assert!(
report.is_clean(),
"{era}: fsck must be clean: {:?}",
report.issues
);
for (name, size, hash) in fixture_files(&m) {
let entry = store
.dir_lookup(1, name.as_bytes())
.unwrap_or_else(|e| panic!("{era}: lookup {name}: {e}"))
.unwrap_or_else(|| panic!("{era}: file {name} missing from root"));
let inode = store
.get_inode(entry.ino)
.expect("inode")
.expect("inode exists");
assert_eq!(inode.size as usize, size, "{era}: {name} size");
let bytes = store
.read_file(entry.ino, 0, size as u64)
.unwrap_or_else(|e| panic!("{era}: read {name}: {e}"));
assert_eq!(bytes.len(), size, "{era}: {name} read length");
let got = hex_str(blake3::hash(&bytes).as_bytes());
assert_eq!(
got, hash,
"{era}: {name} materialized bytes do not match the manifest hash"
);
}
let snapshots = store.list_snapshots().expect("list_snapshots");
assert!(
snapshots.is_empty(),
"{era}: fixtures must carry no snapshots"
);
drop(store);
let sealed = m.get("store_dir_hash").expect("store_dir_hash").to_string();
let got = store_dir_hash(&store_dir);
assert_eq!(
got, sealed,
"{era}: store-dir hash changed — the fixture was regenerated or \
corrupted; fixtures are immutable historical bytes"
);
}
#[test]
fn golden_store_compatibility_court() {
let golden = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("testdata")
.join("golden");
let mut eras: Vec<String> = std::fs::read_dir(&golden)
.expect("golden dir")
.filter_map(|e| {
let e = e.expect("entry");
if e.path().join("manifest.txt").is_file() {
Some(e.file_name().to_string_lossy().to_string())
} else {
None
}
})
.collect();
eras.sort();
assert!(
!eras.is_empty(),
"no golden fixtures found under testdata/golden — run tools/make-golden-fixtures.sh"
);
for era in eras {
court_era(&era);
}
}