use crate::commands::shared_cache::{
blob_path_for, classify_pull_permission_outcome, resolve_registry_root, PullPermissionOutcome,
};
use serde_json::Value;
use std::fs;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct SharedCacheLintArgs {
pub observation_file: String,
pub json: bool,
}
#[derive(Debug, Clone, serde::Serialize)]
struct GateReport {
gate: &'static str,
falsify_id: &'static str,
outcome: String,
passed: bool,
}
pub fn run(args: SharedCacheLintArgs) -> Result<(), String> {
let path = Path::new(&args.observation_file);
if !path.exists() {
return Err(format!(
"FALSIFY-CRUX-A-21: observation file not found: {}",
args.observation_file
));
}
let raw = fs::read_to_string(path)
.map_err(|e| format!("FALSIFY-CRUX-A-21: failed to read observation: {e}"))?;
if raw.trim().is_empty() {
return Err("FALSIFY-CRUX-A-21: observation file is empty".to_string());
}
let obs: Value = serde_json::from_str(&raw)
.map_err(|e| format!("FALSIFY-CRUX-A-21: observation is not valid JSON: {e}"))?;
let mut reports: Vec<GateReport> = Vec::new();
let mut failures: Vec<String> = Vec::new();
if let Some(v) = obs.get("dedup") {
let (r, err) = run_dedup_gate(v);
reports.push(r);
if let Some(e) = err {
failures.push(e);
}
}
if let Some(v) = obs.get("permission") {
let (r, err) = run_permission_gate(v);
reports.push(r);
if let Some(e) = err {
failures.push(e);
}
}
if reports.is_empty() {
return Err("FALSIFY-CRUX-A-21: observation has none of dedup/permission".into());
}
if args.json {
let payload = serde_json::json!({
"contract": "CRUX-A-21",
"gates": reports,
});
println!("{}", serde_json::to_string_pretty(&payload).unwrap());
} else {
for r in &reports {
let tag = if r.passed { "PASS" } else { "FAIL" };
println!("[{tag}] {} ({}): {}", r.gate, r.falsify_id, r.outcome);
}
}
if !failures.is_empty() {
return Err(failures.join("\n"));
}
Ok(())
}
fn parse_str(v: Option<&Value>) -> Option<String> {
v.and_then(|x| x.as_str()).map(|s| s.to_string())
}
fn run_dedup_gate(v: &Value) -> (GateReport, Option<String>) {
let apr_models_env = parse_str(v.get("apr_models_env"));
let home = parse_str(v.get("home")).unwrap_or_default();
let expected_root = parse_str(v.get("expected_root"));
let hex_a = parse_str(v.get("sha256_hex_a"));
let hex_b = parse_str(v.get("sha256_hex_b"));
let expected_same_path = v
.get("expected_same_path")
.and_then(|x| x.as_bool())
.unwrap_or(true);
let root = match resolve_registry_root(apr_models_env.as_deref(), Path::new(&home)) {
Ok(r) => r,
Err(e) => {
let desc = format!("resolve_registry_root error: {e:?}");
return (
GateReport {
gate: "dedup",
falsify_id: "FALSIFY-CRUX-A-21-001",
outcome: desc.clone(),
passed: false,
},
Some(format!("FALSIFY-CRUX-A-21-001 dedup gate failed: {desc}")),
);
}
};
let root_ok = expected_root
.as_ref()
.map(|want| PathBuf::from(want) == root)
.unwrap_or(true);
let (path_a, path_b, parse_ok) = match (hex_a.as_deref(), hex_b.as_deref()) {
(Some(a), Some(b)) => {
let pa = blob_path_for(&root, a);
let pb = blob_path_for(&root, b);
match (pa, pb) {
(Ok(pa), Ok(pb)) => (Some(pa), Some(pb), true),
_ => (None, None, false),
}
}
(None, None) => (None, None, true),
_ => (None, None, false),
};
let same_path_ok = match (&path_a, &path_b) {
(Some(a), Some(b)) => (a == b) == expected_same_path,
_ => true, };
let passed = root_ok && parse_ok && same_path_ok;
let path_a_str = path_a
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "(none)".to_string());
let path_b_str = path_b
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "(none)".to_string());
let desc = format!(
"resolved={} expected_root={} same_path_ok={same_path_ok} parse_ok={parse_ok} path_a={path_a_str} path_b={path_b_str}",
root.display(),
expected_root.as_deref().unwrap_or("(unspecified)")
);
let err = if passed {
None
} else {
Some(format!("FALSIFY-CRUX-A-21-001 dedup gate failed: {desc}"))
};
(
GateReport {
gate: "dedup",
falsify_id: "FALSIFY-CRUX-A-21-001",
outcome: desc,
passed,
},
err,
)
}
fn parse_kind(s: &str) -> Option<ErrorKind> {
match s.to_ascii_lowercase().as_str() {
"permission_denied" | "permissiondenied" | "eacces" => Some(ErrorKind::PermissionDenied),
"not_found" | "notfound" | "enoent" => Some(ErrorKind::NotFound),
"unexpected_eof" | "unexpectedeof" => Some(ErrorKind::UnexpectedEof),
"interrupted" => Some(ErrorKind::Interrupted),
"timed_out" | "timedout" => Some(ErrorKind::TimedOut),
"already_exists" | "alreadyexists" => Some(ErrorKind::AlreadyExists),
_ => None,
}
}
fn outcome_tag(o: &PullPermissionOutcome) -> &'static str {
match o {
PullPermissionOutcome::Ok => "ok",
PullPermissionOutcome::PermissionDenied { .. } => "permission_denied",
PullPermissionOutcome::NotFound { .. } => "not_found",
PullPermissionOutcome::Other { .. } => "other",
}
}
fn outcome_exit_code(o: &PullPermissionOutcome) -> i32 {
match o {
PullPermissionOutcome::Ok => 0,
PullPermissionOutcome::PermissionDenied { exit_code, .. } => *exit_code,
PullPermissionOutcome::NotFound { exit_code } => *exit_code,
PullPermissionOutcome::Other { exit_code, .. } => *exit_code,
}
}
fn run_permission_gate(v: &Value) -> (GateReport, Option<String>) {
let kind_str = parse_str(v.get("kind"));
let expected_outcome = parse_str(v.get("expected_outcome")).map(|s| s.to_ascii_lowercase());
let expected_exit_code = v.get("expected_exit_code").and_then(|x| x.as_i64());
let expected_hint_substr = parse_str(v.get("expected_hint_substring"));
let kind = match kind_str.as_deref().and_then(parse_kind) {
Some(k) => k,
None => {
let desc = format!(
"unknown or missing ErrorKind: {:?}",
kind_str.as_deref().unwrap_or("(unspecified)")
);
return (
GateReport {
gate: "permission",
falsify_id: "FALSIFY-CRUX-A-21-002",
outcome: desc.clone(),
passed: false,
},
Some(format!(
"FALSIFY-CRUX-A-21-002 permission gate failed: {desc}"
)),
);
}
};
let result = classify_pull_permission_outcome(kind);
let got_tag = outcome_tag(&result);
let got_exit = outcome_exit_code(&result);
let outcome_ok = expected_outcome
.as_deref()
.map(|want| want == got_tag)
.unwrap_or(true);
let exit_ok = expected_exit_code
.map(|want| want as i32 == got_exit)
.unwrap_or(true);
let hint_ok = match (&result, &expected_hint_substr) {
(PullPermissionOutcome::PermissionDenied { hint, .. }, Some(sub)) => hint
.to_ascii_lowercase()
.contains(&sub.to_ascii_lowercase()),
_ => true,
};
let never_ok_on_eacces = !matches!(
(kind, &result),
(ErrorKind::PermissionDenied, PullPermissionOutcome::Ok)
);
let passed = outcome_ok && exit_ok && hint_ok && never_ok_on_eacces;
let desc = format!(
"kind={} classified={got_tag} exit={got_exit} outcome_ok={outcome_ok} exit_ok={exit_ok} hint_ok={hint_ok}",
kind_str.as_deref().unwrap_or("(unspecified)")
);
let err = if passed {
None
} else {
Some(format!(
"FALSIFY-CRUX-A-21-002 permission gate failed: {desc}"
))
};
(
GateReport {
gate: "permission",
falsify_id: "FALSIFY-CRUX-A-21-002",
outcome: desc,
passed,
},
err,
)
}