use anyhow::{anyhow, Context, Result};
use sha2::{Digest, Sha256};
use std::path::{Path, PathBuf};
use crate::cli::handlers::work_contract::ContractBinding;
pub fn resolve_binding(project_path: &Path, token: &str) -> Result<ContractBinding> {
let (contract, equation) = ContractBinding::parse_token(token).ok_or_else(|| {
anyhow!(
"invalid --implements token '{}': expected <contract>/<equation>",
token
)
})?;
let file = locate_contract_file(project_path, &contract)
.with_context(|| format!("contract '{}' not found in search path", contract))?;
let bytes =
std::fs::read(&file).with_context(|| format!("failed to read {}", file.display()))?;
let sha = hex_digest(&bytes);
Ok(ContractBinding {
contract,
equation,
file,
sha,
bound_at: chrono::Utc::now(),
})
}
fn locate_contract_file(project_path: &Path, contract: &str) -> Result<PathBuf> {
let candidates = candidate_paths(project_path, contract);
for candidate in &candidates {
if candidate.exists() {
return Ok(candidate.clone());
}
}
Err(anyhow!(
"no contract YAML found. Searched:\n - {}",
candidates
.iter()
.map(|p| p.display().to_string())
.collect::<Vec<_>>()
.join("\n - ")
))
}
fn candidate_paths(project_path: &Path, contract: &str) -> Vec<PathBuf> {
let filename = format!("{}.yaml", contract);
let mut out = vec![project_path.join("contracts").join(&filename)];
if let Some(parent) = project_path.parent() {
out.push(
parent
.join("provable-contracts")
.join("contracts")
.join(&filename),
);
}
if let Ok(env_path) = std::env::var("PMAT_CONTRACTS_PATH") {
for dir in env_path.split(':').filter(|s| !s.is_empty()) {
out.push(PathBuf::from(dir).join(&filename));
}
}
out
}
fn hex_digest(bytes: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(bytes);
let digest = hasher.finalize();
let mut s = String::with_capacity(digest.len() * 2);
for b in digest {
use std::fmt::Write;
let _ = write!(&mut s, "{:02x}", b);
}
s
}
pub fn resolve_all(project_path: &Path, tokens: &[String]) -> Result<Vec<ContractBinding>> {
if tokens.is_empty() {
return Ok(Vec::new());
}
let mut bindings = Vec::with_capacity(tokens.len());
let mut errors = Vec::new();
for token in tokens {
match resolve_binding(project_path, token) {
Ok(b) => bindings.push(b),
Err(e) => errors.push(format!(" --implements {}: {}", token, e)),
}
}
if !errors.is_empty() {
anyhow::bail!(
"failed to resolve {} binding(s):\n{}",
errors.len(),
errors.join("\n")
);
}
let mut seen = std::collections::HashSet::new();
for b in &bindings {
let k = b.key();
if !seen.insert(k.clone()) {
anyhow::bail!("duplicate --implements target '{}'", k);
}
}
Ok(bindings)
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
fn write_yaml(dir: &Path, contract: &str, body: &str) -> PathBuf {
let contracts_dir = dir.join("contracts");
std::fs::create_dir_all(&contracts_dir).unwrap();
let path = contracts_dir.join(format!("{}.yaml", contract));
std::fs::write(&path, body).unwrap();
path
}
#[test]
fn resolve_finds_contract_in_project_root() {
let temp = tempdir().unwrap();
write_yaml(temp.path(), "rope-kernel-v1", "equations:\n rope: {}\n");
let binding = resolve_binding(temp.path(), "rope-kernel-v1/rope").unwrap();
assert_eq!(binding.contract, "rope-kernel-v1");
assert_eq!(binding.equation, "rope");
assert_eq!(binding.sha.len(), 64); }
#[test]
fn resolve_computes_stable_sha() {
let temp = tempdir().unwrap();
write_yaml(temp.path(), "k", "body: 1\n");
let a = resolve_binding(temp.path(), "k/eq").unwrap();
let b = resolve_binding(temp.path(), "k/eq").unwrap();
assert_eq!(a.sha, b.sha);
}
#[test]
fn resolve_sha_changes_when_bytes_change() {
let temp = tempdir().unwrap();
write_yaml(temp.path(), "k", "body: 1\n");
let a = resolve_binding(temp.path(), "k/eq").unwrap();
write_yaml(temp.path(), "k", "body: 2\n");
let b = resolve_binding(temp.path(), "k/eq").unwrap();
assert_ne!(a.sha, b.sha, "SHA must reflect file content changes");
}
#[test]
fn resolve_errors_on_missing_file() {
let temp = tempdir().unwrap();
let err = resolve_binding(temp.path(), "nope/eq").unwrap_err();
let msg = format!("{:#}", err);
assert!(
msg.contains("nope.yaml"),
"error should list the missing file, got: {}",
msg
);
}
#[test]
fn resolve_errors_on_malformed_token() {
let temp = tempdir().unwrap();
let err = resolve_binding(temp.path(), "no-slash-token").unwrap_err();
assert!(format!("{:#}", err).contains("invalid --implements token"));
}
#[test]
fn resolve_all_empty_is_ok() {
let temp = tempdir().unwrap();
let out = resolve_all(temp.path(), &[]).unwrap();
assert!(out.is_empty());
}
#[test]
fn resolve_all_detects_duplicates() {
let temp = tempdir().unwrap();
write_yaml(temp.path(), "k", "equations: {}\n");
let err = resolve_all(temp.path(), &["k/eq".to_string(), "k/eq".to_string()]).unwrap_err();
assert!(format!("{:#}", err).contains("duplicate"));
}
#[test]
fn resolve_all_aggregates_errors() {
let temp = tempdir().unwrap();
let err = resolve_all(temp.path(), &["a/eq".to_string(), "b/eq".to_string()]).unwrap_err();
let msg = format!("{:#}", err);
assert!(msg.contains("a/eq"));
assert!(msg.contains("b/eq"));
}
#[test]
fn resolve_honors_pmat_contracts_path() {
let temp = tempdir().unwrap();
let ext_dir = tempdir().unwrap();
let yaml_path = ext_dir.path().join("external.yaml");
std::fs::write(&yaml_path, "equations: {}\n").unwrap();
let prev = std::env::var("PMAT_CONTRACTS_PATH").ok();
unsafe {
std::env::set_var("PMAT_CONTRACTS_PATH", ext_dir.path());
}
let result = resolve_binding(temp.path(), "external/eq");
unsafe {
match prev {
Some(v) => std::env::set_var("PMAT_CONTRACTS_PATH", v),
None => std::env::remove_var("PMAT_CONTRACTS_PATH"),
}
}
let binding = result.unwrap();
assert_eq!(binding.file, yaml_path);
}
}