use crate::core::store::verify::{verify_store, EntryStatus, EntryVerdict};
use std::path::{Path, PathBuf};
pub(crate) fn cmd_store_verify(store_dir: &Path, repair: bool, json: bool) -> Result<(), String> {
let verdicts = verify_store(store_dir)?;
let repaired = if repair {
repair_mismatches(store_dir, &verdicts)?
} else {
Vec::new()
};
if json {
print_json(&verdicts, &repaired);
} else {
print_text(&verdicts, &repaired);
}
let remaining = verdicts
.iter()
.filter(|v| v.is_failure())
.filter(|v| !repaired.contains(&v.hash))
.count();
if remaining > 0 {
return Err(format!(
"{remaining} store entries failed verification: the bytes on disk are not the \
bytes the entry recorded, or the entry is too malformed to check"
));
}
Ok(())
}
fn repair_mismatches(store_dir: &Path, verdicts: &[EntryVerdict]) -> Result<Vec<String>, String> {
let mut removed = Vec::new();
for verdict in verdicts {
if !matches!(verdict.status, EntryStatus::Mismatch { .. }) {
continue;
}
let path = entry_path(store_dir, &verdict.hash);
std::fs::remove_dir_all(&path)
.map_err(|e| format!("repair: cannot remove {}: {e}", path.display()))?;
removed.push(verdict.hash.clone());
}
Ok(removed)
}
fn entry_path(store_dir: &Path, hash: &str) -> PathBuf {
store_dir.join(hash.strip_prefix("blake3:").unwrap_or(hash))
}
fn label(status: &EntryStatus) -> &'static str {
match status {
EntryStatus::Ok => "ok",
EntryStatus::Mismatch { .. } => "MISMATCH",
EntryStatus::Unsealed => "unsealed",
EntryStatus::Malformed(_) => "MALFORMED",
}
}
fn detail(status: &EntryStatus) -> String {
match status {
EntryStatus::Ok => String::new(),
EntryStatus::Mismatch { expected, actual } => {
format!("recorded {expected}, holds {actual}")
}
EntryStatus::Unsealed => {
"written before schema 1.1; no output digest to compare against".to_string()
}
EntryStatus::Malformed(e) => e.clone(),
}
}
fn print_text(verdicts: &[EntryVerdict], repaired: &[String]) {
for v in verdicts {
let detail = detail(&v.status);
if detail.is_empty() {
println!("{:<10} {}", label(&v.status), v.hash);
} else {
println!("{:<10} {} — {detail}", label(&v.status), v.hash);
}
}
let ok = verdicts
.iter()
.filter(|v| v.status == EntryStatus::Ok)
.count();
let unsealed = verdicts
.iter()
.filter(|v| v.status == EntryStatus::Unsealed)
.count();
let failed = verdicts.iter().filter(|v| v.is_failure()).count();
println!("Verified: {ok} | Unsealed: {unsealed} | Failed: {failed}");
if !repaired.is_empty() {
println!("Repaired (removed): {}", repaired.len());
}
}
fn print_json(verdicts: &[EntryVerdict], repaired: &[String]) {
let results: Vec<_> = verdicts
.iter()
.map(|v| {
serde_json::json!({
"hash": v.hash,
"status": label(&v.status),
"valid": v.status == EntryStatus::Ok,
"detail": detail(&v.status),
})
})
.collect();
let report = serde_json::json!({
"verified": verdicts.iter().filter(|v| v.status == EntryStatus::Ok).count(),
"unsealed": verdicts.iter().filter(|v| v.status == EntryStatus::Unsealed).count(),
"failed": verdicts.iter().filter(|v| v.is_failure()).count(),
"repaired": repaired,
"results": results,
});
println!(
"{}",
serde_json::to_string_pretty(&report).unwrap_or_else(|_| "{}".to_string())
);
}
pub(crate) fn cmd_cache_verify_report(store_dir: &Path, json: bool) -> Result<(), String> {
let verdicts = verify_store(store_dir)?;
let verified = verdicts
.iter()
.filter(|v| v.status == EntryStatus::Ok)
.count();
let failed = verdicts.iter().filter(|v| v.is_failure()).count();
if json {
let results: Vec<_> = verdicts.iter().map(cache_verdict_json).collect();
let report = serde_json::json!({
"verified": verified, "failed": failed,
"results": results,
});
println!(
"{}",
serde_json::to_string_pretty(&report).unwrap_or_else(|_| "{}".to_string())
);
} else {
println!("Verified: {verified} | Failed: {failed}");
}
if failed > 0 {
Err(format!("{failed} store entries failed verification"))
} else {
Ok(())
}
}
fn cache_verdict_json(verdict: &EntryVerdict) -> serde_json::Value {
let name = verdict
.hash
.strip_prefix("blake3:")
.unwrap_or(&verdict.hash);
let (expected, actual) = match &verdict.status {
EntryStatus::Ok => (String::new(), String::new()),
EntryStatus::Mismatch { expected, actual } => (expected.clone(), actual.clone()),
EntryStatus::Unsealed => (String::new(), String::new()),
EntryStatus::Malformed(e) => (String::new(), e.clone()),
};
serde_json::json!({
"hash": name,
"valid": verdict.status == EntryStatus::Ok,
"expected": expected,
"actual": actual,
})
}