use super::commands::VerifyArgs;
use super::helpers::*;
use crate::core::verify::{verify_hermetic, verify_resource, Verdict, VerifyOutcome};
use std::path::Path;
pub(crate) fn cmd_verify(args: &VerifyArgs, verbose: bool) -> Result<(), String> {
let VerifyArgs {
file,
resource: resource_filter,
tag: tag_filter,
state_dir,
json,
keep_scratch,
check_declared_inputs,
} = args;
let (json, keep_scratch, hermetic) = (*json, *keep_scratch, *check_declared_inputs);
let (resource_filter, tag_filter) = (resource_filter.as_deref(), tag_filter.as_deref());
let config = parse_and_validate(file)?;
let scratch_base = std::env::temp_dir().join(format!("forjar-verify-{}", std::process::id()));
let mut outcomes: Vec<VerifyOutcome> = Vec::new();
for (id, resource) in &config.resources {
if resource_filter.is_some_and(|f| id != f) {
continue;
}
if let Some(tag) = tag_filter {
if !resource.tags.iter().any(|t| t == tag) {
continue;
}
}
let recorded = recorded_output_hash(state_dir.as_path(), id);
let scratch = scratch_base.join(id);
if verbose {
eprintln!("verify {id}: scratch {}", scratch.display());
}
outcomes.push(if hermetic {
verify_hermetic(id, resource, recorded.as_deref(), &scratch)
} else {
verify_resource(id, resource, recorded.as_deref(), &scratch)
});
}
if !keep_scratch {
let _ = std::fs::remove_dir_all(&scratch_base);
} else if !outcomes.is_empty() {
eprintln!("scratch retained at {}", scratch_base.display());
}
if json {
print_json(&outcomes);
} else {
print_human(&outcomes);
}
let failed = outcomes.iter().filter(|o| o.verdict.is_failure()).count();
if failed > 0 {
return Err(format!("{failed} resource(s) did not reproduce"));
}
Ok(())
}
fn recorded_output_hash(state_dir: &Path, id: &str) -> Option<String> {
let entries = std::fs::read_dir(state_dir).ok()?;
for entry in entries.filter_map(Result::ok) {
let lock = entry.path().join("state.lock.yaml");
if !lock.is_file() {
continue;
}
let Ok(text) = std::fs::read_to_string(&lock) else {
continue;
};
let Ok(doc) = serde_yaml_ng::from_str::<serde_yaml_ng::Value>(&text) else {
continue;
};
if let Some(h) = doc
.get("resources")
.and_then(|r| r.get(id))
.and_then(|r| r.get("details"))
.and_then(|d| d.get("output_hash"))
.and_then(|v| v.as_str())
{
return Some(h.to_string());
}
}
None
}
fn print_human(outcomes: &[VerifyOutcome]) {
for o in outcomes {
match &o.verdict {
Verdict::Reproduced => println!(" reproduced {}", o.resource_id),
Verdict::Diverged { recorded, .. } => {
println!(" DIVERGED {} (recorded {})", o.resource_id, recorded);
}
Verdict::CommandFailed { status } => {
println!(" FAILED {} ({status})", o.resource_id);
}
Verdict::UndeclaredInput { hermetic } => {
println!(
" UNDECLARED {} — reproduces from the full tree but not from \n\
\x20 its declared inputs alone: {hermetic}",
o.resource_id
);
}
Verdict::Skipped(r) => println!(" skipped {} ({})", o.resource_id, r.as_str()),
}
}
let repro = outcomes
.iter()
.filter(|o| o.verdict == Verdict::Reproduced)
.count();
let failed = outcomes.iter().filter(|o| o.verdict.is_failure()).count();
println!(
"\nVerify: {repro} reproduced, {failed} not reproduced, {} skipped.",
outcomes.len() - repro - failed
);
}
fn print_json(outcomes: &[VerifyOutcome]) {
let items: Vec<String> = outcomes
.iter()
.map(|o| {
let extra = match &o.verdict {
Verdict::Diverged {
recorded,
regenerated,
} => format!(
r#","recorded":"{recorded}","regenerated":{}"#,
regenerated
.as_ref()
.map_or("null".to_string(), |r| format!("\"{r}\""))
),
Verdict::CommandFailed { status } => {
format!(r#","error":"{}""#, status.replace('"', "'"))
}
Verdict::UndeclaredInput { hermetic } => {
format!(r#","hermetic":"{}""#, hermetic.replace('"', "'"))
}
Verdict::Skipped(r) => format!(r#","reason":"{}""#, r.as_str()),
Verdict::Reproduced => String::new(),
};
format!(
r#"{{"resource":"{}","verdict":"{}"{extra}}}"#,
o.resource_id,
o.verdict.as_str()
)
})
.collect();
println!(
r#"{{"reproduced":{},"not_reproduced":{},"results":[{}]}}"#,
outcomes
.iter()
.filter(|o| o.verdict == Verdict::Reproduced)
.count(),
outcomes.iter().filter(|o| o.verdict.is_failure()).count(),
items.join(",")
);
}