use std::path::{Path, PathBuf};
use antigen_macros::presents;
use super::manifest::{DepEntry, read_manifest_deps};
use super::schema::{ContentHashRecord, DepAttestation, MaintainerSnapshot, SandboxKind};
use super::witness::{
ContentHashState, DepAttestedState, DepPinnedState, MaintainerState, SandboxState,
};
#[must_use]
pub fn supply_chain_root(workspace_root: &Path) -> PathBuf {
workspace_root.join(".attest").join("supply-chain")
}
#[must_use]
#[presents(PathTraversalViaUnvalidatedComponent)]
pub fn dep_attest_path(workspace_root: &Path, crate_name: &str, version: &str) -> PathBuf {
let dir = supply_chain_root(workspace_root).join("dep-attest");
if !is_valid_crate_name(crate_name) || !is_valid_version(version) {
return dir;
}
dir.join(format!("{crate_name}@{version}.json"))
}
#[must_use]
#[presents(PathTraversalViaUnvalidatedComponent)]
pub fn content_hash_path(workspace_root: &Path, crate_name: &str, version: &str) -> PathBuf {
let dir = supply_chain_root(workspace_root).join("content-hash");
if !is_valid_crate_name(crate_name) || !is_valid_version(version) {
return dir;
}
dir.join(format!("{crate_name}@{version}.json"))
}
#[must_use]
#[presents(PathTraversalViaUnvalidatedComponent)]
pub fn maintainer_path(workspace_root: &Path, crate_name: &str) -> PathBuf {
let dir = supply_chain_root(workspace_root).join("maintainer");
if !is_valid_crate_name(crate_name) {
return dir;
}
dir.join(format!("{crate_name}.json"))
}
#[must_use]
pub fn is_valid_crate_name(name: &str) -> bool {
!name.is_empty()
&& name
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '-')
}
#[must_use]
pub fn is_valid_version(version: &str) -> bool {
!version.is_empty()
&& version
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '.' | '-' | '+'))
}
#[must_use]
pub fn evaluate_dep_pinned(workspace_root: &Path, crate_name: Option<&str>) -> DepPinnedState {
let manifest = workspace_root.join("Cargo.toml");
let entries = read_manifest_deps(&manifest);
evaluate_dep_pinned_against(&entries, crate_name)
}
#[must_use]
pub fn evaluate_dep_pinned_against(
entries: &[DepEntry],
crate_name: Option<&str>,
) -> DepPinnedState {
let mut unpinned: Vec<String> = Vec::new();
let mut saw_named = false;
for entry in entries {
if let Some(named) = crate_name {
if entry.name != named {
continue;
}
saw_named = true;
}
if !entry.is_exact_pinned() {
unpinned.push(entry.name.clone());
}
}
if let Some(named) = crate_name {
if !saw_named {
return DepPinnedState::NotInManifest {
crate_name: named.to_string(),
};
}
}
if unpinned.is_empty() {
DepPinnedState::AllPinned
} else {
DepPinnedState::Unpinned {
unpinned_deps: unpinned,
}
}
}
#[must_use]
pub fn evaluate_dep_attested(
workspace_root: &Path,
crate_name: &str,
version: &str,
exact_version: bool,
) -> DepAttestedState {
let path = dep_attest_path(workspace_root, crate_name, version);
let Ok(content) = std::fs::read_to_string(&path) else {
if exact_version {
return DepAttestedState::SidecarMissing;
}
return find_any_attest_for_crate(workspace_root, crate_name)
.unwrap_or(DepAttestedState::SidecarMissing);
};
let att = match serde_json::from_str::<DepAttestation>(&content) {
Err(e) => {
return DepAttestedState::SidecarMalformed {
error: e.to_string(),
};
},
Ok(a) => a,
};
if !att.has_reviewable_artifact() {
return DepAttestedState::AttestedWithoutReviewableArtifact;
}
if exact_version && att.version != version {
return DepAttestedState::AttestationStale {
attested_version: att.version,
requested_version: version.to_string(),
};
}
DepAttestedState::Attested {
review_scope: att.review_scope,
}
}
fn find_any_attest_for_crate(workspace_root: &Path, crate_name: &str) -> Option<DepAttestedState> {
let dir = supply_chain_root(workspace_root).join("dep-attest");
let entries = std::fs::read_dir(&dir).ok()?;
for entry in entries.flatten() {
let path = entry.path();
let stem = path.file_stem()?.to_string_lossy().to_string();
if let Some(at) = stem.find('@') {
let candidate = &stem[..at];
if candidate == crate_name {
let content = std::fs::read_to_string(&path).ok()?;
let att: DepAttestation = serde_json::from_str(&content).ok()?;
if !att.has_reviewable_artifact() {
return Some(DepAttestedState::AttestedWithoutReviewableArtifact);
}
return Some(DepAttestedState::Attested {
review_scope: att.review_scope,
});
}
}
}
None
}
#[must_use]
pub fn evaluate_content_hash_matches(
workspace_root: &Path,
crate_name: &str,
version: &str,
) -> ContentHashState {
let recorded = match load_content_hash_record(workspace_root, crate_name, version) {
Ok(Some(r)) => r,
Ok(None) => return ContentHashState::NoAttestation,
Err(e) => return ContentHashState::SidecarMalformed { error: e },
};
let lockfile = workspace_root.join("Cargo.lock");
let Some(current) = current_hash_from_lockfile(&lockfile, crate_name, version) else {
return ContentHashState::CrateNotInLockfile {
crate_name: crate_name.to_string(),
};
};
if current == recorded.content_hash {
ContentHashState::Matches
} else {
ContentHashState::Mismatch {
recorded: recorded.content_hash,
current,
}
}
}
#[must_use]
pub fn compare_live_cksum(served: Option<&str>, expected: &str) -> super::witness::LiveCksumState {
use super::witness::LiveCksumState;
match served {
None => LiveCksumState::Unverifiable {
reason: "crates.io registry unreachable (offline, network error, or version \
absent from the index) — the live hash could not be obtained"
.to_string(),
},
Some(h) if h == expected => LiveCksumState::Verified {
hash: h.to_string(),
},
Some(h) => LiveCksumState::Mismatch {
expected: expected.to_string(),
served: h.to_string(),
},
}
}
pub fn load_content_hash_record(
workspace_root: &Path,
crate_name: &str,
version: &str,
) -> Result<Option<ContentHashRecord>, String> {
let path = content_hash_path(workspace_root, crate_name, version);
let Ok(content) = std::fs::read_to_string(&path) else {
return Ok(None);
};
serde_json::from_str(&content)
.map(Some)
.map_err(|e| e.to_string())
}
pub fn save_content_hash_record(
workspace_root: &Path,
record: &ContentHashRecord,
) -> std::io::Result<PathBuf> {
let path = content_hash_path(workspace_root, &record.crate_name, &record.version);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let json = serde_json::to_string_pretty(record).map_err(std::io::Error::other)?;
std::fs::write(&path, &json)?;
Ok(path)
}
#[must_use]
pub fn current_hash_from_lockfile(
lockfile_path: &Path,
crate_name: &str,
version: &str,
) -> Option<String> {
let content = std::fs::read_to_string(lockfile_path).ok()?;
let mut in_package = false;
let mut name_match = false;
let mut version_match = false;
let mut checksum: Option<String> = None;
for line in content.lines() {
let line = line.trim();
if line == "[[package]]" {
if in_package && name_match && version_match && checksum.is_some() {
return checksum;
}
in_package = true;
name_match = false;
version_match = false;
checksum = None;
continue;
}
if !in_package {
continue;
}
if let Some(rest) = line.strip_prefix("name = ") {
if let Some(v) = strip_quotes_simple(rest) {
name_match = v == crate_name;
}
} else if let Some(rest) = line.strip_prefix("version = ") {
if let Some(v) = strip_quotes_simple(rest) {
version_match = v == version;
}
} else if let Some(rest) = line.strip_prefix("checksum = ") {
if let Some(v) = strip_quotes_simple(rest) {
checksum = Some(v.to_string());
}
}
}
if in_package && name_match && version_match && checksum.is_some() {
return checksum;
}
None
}
#[must_use]
pub fn resolved_version_from_lockfile(lockfile_path: &Path, crate_name: &str) -> Option<String> {
let content = std::fs::read_to_string(lockfile_path).ok()?;
let mut in_package = false;
let mut name_match = false;
let mut version: Option<String> = None;
for line in content.lines() {
let line = line.trim();
if line == "[[package]]" {
if in_package && name_match {
if let Some(v) = version.take() {
return Some(v);
}
}
in_package = true;
name_match = false;
version = None;
continue;
}
if !in_package {
continue;
}
if let Some(rest) = line.strip_prefix("name = ") {
if let Some(v) = strip_quotes_simple(rest) {
name_match = v == crate_name;
}
} else if let Some(rest) = line.strip_prefix("version = ") {
if let Some(v) = strip_quotes_simple(rest) {
version = Some(v.to_string());
}
}
}
if in_package && name_match {
return version;
}
None
}
fn strip_quotes_simple(s: &str) -> Option<&str> {
let s = s.trim();
s.strip_prefix('"').and_then(|s| s.strip_suffix('"'))
}
#[must_use]
pub fn evaluate_maintainer_unchanged(
workspace_root: &Path,
crate_name: &str,
since_version: &str,
) -> MaintainerState {
if !is_valid_crate_name(crate_name) {
return MaintainerState::SnapshotMissing;
}
let Some(snap) = load_maintainer_snapshot(workspace_root, crate_name) else {
return MaintainerState::SnapshotMissing;
};
if snap.since_version != since_version {
return MaintainerState::CratesIoQueryUnavailable;
}
MaintainerState::Unchanged
}
#[must_use]
pub fn load_maintainer_snapshot(
workspace_root: &Path,
crate_name: &str,
) -> Option<MaintainerSnapshot> {
let path = maintainer_path(workspace_root, crate_name);
let content = std::fs::read_to_string(&path).ok()?;
serde_json::from_str(&content).ok()
}
#[must_use]
pub const fn evaluate_sandbox_clean(_crate_name: &str, sandbox_kind: SandboxKind) -> SandboxState {
SandboxState::ToolingNotYetAvailable { sandbox_kind }
}
#[cfg(test)]
mod tests {
use antigen_macros::defended_by;
use tempfile::TempDir;
use super::*;
use crate::supply_chain::schema::ReviewScope;
fn write_manifest(dir: &Path, content: &str) {
std::fs::write(dir.join("Cargo.toml"), content).unwrap();
}
#[test]
fn dep_pinned_all_pinned() {
let tmp = TempDir::new().unwrap();
write_manifest(
tmp.path(),
r#"
[dependencies]
serde = "=1.0.197"
"#,
);
assert_eq!(
evaluate_dep_pinned(tmp.path(), None),
DepPinnedState::AllPinned
);
}
#[test]
fn dep_pinned_flags_caret() {
let tmp = TempDir::new().unwrap();
write_manifest(
tmp.path(),
r#"
[dependencies]
serde = "1.0"
clap = "=4.0"
"#,
);
match evaluate_dep_pinned(tmp.path(), None) {
DepPinnedState::Unpinned { unpinned_deps } => {
assert_eq!(unpinned_deps, vec!["serde".to_string()]);
},
other => panic!("expected Unpinned, got {other:?}"),
}
}
#[test]
fn dep_pinned_named_not_in_manifest() {
let tmp = TempDir::new().unwrap();
write_manifest(
tmp.path(),
r#"
[dependencies]
serde = "=1.0.197"
"#,
);
match evaluate_dep_pinned(tmp.path(), Some("missing")) {
DepPinnedState::NotInManifest { crate_name } => assert_eq!(crate_name, "missing"),
other => panic!("expected NotInManifest, got {other:?}"),
}
}
#[test]
fn dep_attested_sidecar_missing() {
let tmp = TempDir::new().unwrap();
let state = evaluate_dep_attested(tmp.path(), "serde", "1.0.197", true);
assert_eq!(state, DepAttestedState::SidecarMissing);
}
#[test]
fn dep_attested_empty_artifact_flags_rubber_stamp() {
let tmp = TempDir::new().unwrap();
let att = DepAttestation {
crate_name: "serde".to_string(),
version: "1.0.197".to_string(),
exact_version: true,
reviewable_artifact: PathBuf::new(),
review_scope: ReviewScope::MetadataOnly,
signed_by: "alice".to_string(),
date: "2026-05-22".to_string(),
rationale: None,
};
let path = dep_attest_path(tmp.path(), "serde", "1.0.197");
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(&path, serde_json::to_string(&att).unwrap()).unwrap();
assert_eq!(
evaluate_dep_attested(tmp.path(), "serde", "1.0.197", true),
DepAttestedState::AttestedWithoutReviewableArtifact
);
}
#[test]
fn content_hash_no_attestation_state() {
let tmp = TempDir::new().unwrap();
assert_eq!(
evaluate_content_hash_matches(tmp.path(), "serde", "1.0.197"),
ContentHashState::NoAttestation
);
}
#[test]
fn content_hash_match_path() {
let tmp = TempDir::new().unwrap();
let record = ContentHashRecord {
crate_name: "serde".to_string(),
version: "1.0.197".to_string(),
content_hash: "abc123".to_string(),
hash_source: "cargo-lock-checksum".to_string(),
signed_by: "alice".to_string(),
date: "2026-05-22".to_string(),
};
save_content_hash_record(tmp.path(), &record).unwrap();
std::fs::write(
tmp.path().join("Cargo.lock"),
r#"
[[package]]
name = "serde"
version = "1.0.197"
checksum = "abc123"
"#,
)
.unwrap();
assert_eq!(
evaluate_content_hash_matches(tmp.path(), "serde", "1.0.197"),
ContentHashState::Matches
);
}
#[test]
fn content_hash_mismatch_path() {
let tmp = TempDir::new().unwrap();
let record = ContentHashRecord {
crate_name: "serde".to_string(),
version: "1.0.197".to_string(),
content_hash: "recorded-hash".to_string(),
hash_source: "cargo-lock-checksum".to_string(),
signed_by: "alice".to_string(),
date: "2026-05-22".to_string(),
};
save_content_hash_record(tmp.path(), &record).unwrap();
std::fs::write(
tmp.path().join("Cargo.lock"),
r#"
[[package]]
name = "serde"
version = "1.0.197"
checksum = "swapped-hash"
"#,
)
.unwrap();
match evaluate_content_hash_matches(tmp.path(), "serde", "1.0.197") {
ContentHashState::Mismatch { recorded, current } => {
assert_eq!(recorded, "recorded-hash");
assert_eq!(current, "swapped-hash");
},
other => panic!("expected Mismatch, got {other:?}"),
}
}
#[test]
fn content_hash_crate_not_in_lockfile() {
let tmp = TempDir::new().unwrap();
let record = ContentHashRecord {
crate_name: "serde".to_string(),
version: "1.0.197".to_string(),
content_hash: "abc".to_string(),
hash_source: "cargo-lock-checksum".to_string(),
signed_by: "alice".to_string(),
date: "2026-05-22".to_string(),
};
save_content_hash_record(tmp.path(), &record).unwrap();
match evaluate_content_hash_matches(tmp.path(), "serde", "1.0.197") {
ContentHashState::CrateNotInLockfile { crate_name } => assert_eq!(crate_name, "serde"),
other => panic!("expected CrateNotInLockfile, got {other:?}"),
}
}
#[test]
fn maintainer_missing_snapshot() {
let tmp = TempDir::new().unwrap();
assert_eq!(
evaluate_maintainer_unchanged(tmp.path(), "serde", "1.0.197"),
MaintainerState::SnapshotMissing
);
}
#[test]
fn sandbox_v02_returns_tooling_unavailable() {
let state = evaluate_sandbox_clean("serde", SandboxKind::Build);
assert_eq!(
state,
SandboxState::ToolingNotYetAvailable {
sandbox_kind: SandboxKind::Build
}
);
}
#[test]
#[defended_by(PathTraversalViaUnvalidatedComponent)]
fn path_builders_reject_traversal_crate_name() {
let root = Path::new("/ws");
let sc_root = supply_chain_root(root);
for evil in [
"../../../etc/passwd",
"..",
"foo/bar",
"foo\\bar",
"a/../../b",
"", ] {
for built in [
dep_attest_path(root, evil, "1.0.0"),
content_hash_path(root, evil, "1.0.0"),
maintainer_path(root, evil),
] {
assert!(
built.starts_with(&sc_root),
"path-builder must stay within supply_chain_root for evil \
crate_name {evil:?}; got {built:?}"
);
assert!(
!built.to_string_lossy().contains(".."),
"built path must not carry a `..` traversal component for \
{evil:?}; got {built:?}"
);
}
}
}
#[test]
fn path_builders_reject_traversal_version() {
let root = Path::new("/ws");
let sc_root = supply_chain_root(root);
for evil_version in ["../../../etc", "1.0.0/../../.."] {
for built in [
dep_attest_path(root, "serde", evil_version),
content_hash_path(root, "serde", evil_version),
] {
assert!(built.starts_with(&sc_root));
assert!(!built.to_string_lossy().contains(".."));
}
}
}
#[test]
fn path_builders_compose_valid_names() {
let root = Path::new("/ws");
let p = dep_attest_path(root, "serde_json", "1.0.197");
assert!(p.ends_with("serde_json@1.0.197.json"));
let m = maintainer_path(root, "tokio-util");
assert!(m.ends_with("tokio-util.json"));
}
#[test]
fn resolved_version_reads_the_locked_version() {
let tmp = TempDir::new().unwrap();
let lock = tmp.path().join("Cargo.lock");
std::fs::write(
&lock,
r#"# This file is automatically @generated by Cargo.
version = 3
[[package]]
name = "serde"
version = "1.0.197"
checksum = "abc123"
[[package]]
name = "tokio"
version = "1.36.0"
"#,
)
.unwrap();
assert_eq!(
resolved_version_from_lockfile(&lock, "serde").as_deref(),
Some("1.0.197")
);
assert_eq!(
resolved_version_from_lockfile(&lock, "tokio").as_deref(),
Some("1.36.0")
);
}
#[test]
fn resolved_version_absent_crate_is_none() {
let tmp = TempDir::new().unwrap();
let lock = tmp.path().join("Cargo.lock");
std::fs::write(
&lock,
"[[package]]\nname = \"serde\"\nversion = \"1.0.197\"\n",
)
.unwrap();
assert_eq!(resolved_version_from_lockfile(&lock, "not-a-dep"), None);
}
#[test]
fn resolved_version_missing_lockfile_is_none() {
let tmp = TempDir::new().unwrap();
assert_eq!(
resolved_version_from_lockfile(&tmp.path().join("Cargo.lock"), "serde"),
None
);
}
#[test]
fn live_cksum_matching_served_hash_is_verified() {
use crate::supply_chain::witness::LiveCksumState;
let out = compare_live_cksum(Some("abc123"), "abc123");
assert_eq!(
out,
LiveCksumState::Verified {
hash: "abc123".into()
}
);
assert!(out.is_verified() && !out.is_unverifiable());
}
#[test]
fn live_cksum_differing_served_hash_is_mismatch() {
use crate::supply_chain::witness::LiveCksumState;
let out = compare_live_cksum(Some("served-deadbeef"), "expected-abc123");
assert_eq!(
out,
LiveCksumState::Mismatch {
expected: "expected-abc123".into(),
served: "served-deadbeef".into(),
}
);
assert!(!out.is_verified() && !out.is_unverifiable());
}
#[test]
fn live_cksum_offline_is_unverifiable_not_pass_or_fail() {
use crate::supply_chain::witness::LiveCksumState;
let out = compare_live_cksum(None, "expected-abc123");
assert!(
out.is_unverifiable(),
"offline must be Unverifiable (⊥): {out:?}"
);
assert!(
!out.is_verified(),
"offline must NOT read as verified (false-green): {out:?}"
);
assert!(
matches!(out, LiveCksumState::Unverifiable { .. }),
"offline is the third value, never Mismatch (false-alarm): {out:?}"
);
}
#[test]
fn live_cksum_three_outcomes_are_pairwise_distinct() {
let verified = compare_live_cksum(Some("h"), "h");
let mismatch = compare_live_cksum(Some("h"), "other");
let unverifiable = compare_live_cksum(None, "h");
assert_ne!(verified, mismatch);
assert_ne!(verified, unverifiable);
assert_ne!(mismatch, unverifiable);
}
}