use std::{
path::{Path, PathBuf},
process::Command,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Wasm4pmError {
NotFound,
CommandFailed(String),
InvalidOutput(String),
EnvError(String),
}
impl std::fmt::Display for Wasm4pmError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotFound => write!(f, "wpm binary not found"),
Self::CommandFailed(msg) => write!(f, "wpm command failed: {msg}"),
Self::InvalidOutput(msg) => write!(f, "invalid wpm output: {msg}"),
Self::EnvError(msg) => write!(f, "wpm env error: {msg}"),
}
}
}
impl std::error::Error for Wasm4pmError {}
pub type Wasm4pmResult<T> = Result<T, Wasm4pmError>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Wpm4Command {
Audit,
Lean,
SpcStatus,
ReceiptDoctor,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WpmVerdict {
Pass,
Warn,
Fail,
Partial,
NotAvailable,
}
#[derive(Debug, Clone)]
pub struct Wasm4pmShell {
binary_path: PathBuf,
}
impl Wasm4pmShell {
pub fn discover() -> Wasm4pmResult<Self> {
if let Ok(val) = std::env::var("WPM_BIN") {
let p = PathBuf::from(&val);
if p.exists() {
return Ok(Self { binary_path: p });
}
return Err(Wasm4pmError::EnvError(format!("WPM_BIN={val} does not exist")));
}
if let Ok(val) = std::env::var("WPM_PATH") {
let p = PathBuf::from(&val);
if p.exists() {
return Ok(Self { binary_path: p });
}
}
if let Ok(val) = std::env::var("WPM_SEARCH_PATHS") {
for dir in val.split(':') {
let p = PathBuf::from(dir).join("wpm");
if p.exists() {
return Ok(Self { binary_path: p });
}
}
}
for rel in &["./wpm", "./bin/wpm", "../wpm"] {
let p = PathBuf::from(rel);
if p.exists() {
return Ok(Self { binary_path: p });
}
}
if let Some(path_val) = std::env::var_os("PATH") {
for dir in std::env::split_paths(&path_val) {
let p = dir.join("wpm");
if p.exists() {
return Ok(Self { binary_path: p });
}
}
}
Err(Wasm4pmError::NotFound)
}
pub fn from_path(path: impl Into<PathBuf>) -> Wasm4pmResult<Self> {
let p = path.into();
if p.exists() {
Ok(Self { binary_path: p })
} else {
Err(Wasm4pmError::NotFound)
}
}
pub fn binary_path(&self) -> &Path {
&self.binary_path
}
pub fn run_command(&self, cmd: Wpm4Command, args: &[&str]) -> Wasm4pmResult<String> {
let cmd_str = match cmd {
Wpm4Command::Audit => "audit",
Wpm4Command::Lean => "lean",
Wpm4Command::SpcStatus => "spc_status",
Wpm4Command::ReceiptDoctor => "receipt_doctor",
};
let output = Command::new(&self.binary_path)
.arg(cmd_str)
.args(args)
.output()
.map_err(|e| Wasm4pmError::CommandFailed(e.to_string()))?;
let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
if output.status.success() {
Ok(stdout)
} else {
Err(Wasm4pmError::CommandFailed(stdout))
}
}
pub fn audit(&self, evidence_data: &str) -> Wasm4pmResult<String> {
use std::io::Write;
let tmp_path = std::env::temp_dir().join(format!(
"star_toml_wpm_audit_{}.json",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.subsec_nanos()
));
let mut f =
std::fs::File::create(&tmp_path).map_err(|e| Wasm4pmError::EnvError(e.to_string()))?;
f.write_all(evidence_data.as_bytes()).map_err(|e| Wasm4pmError::EnvError(e.to_string()))?;
drop(f);
let path = tmp_path.to_string_lossy().into_owned();
let result = self.run_command(Wpm4Command::Audit, &[&path]);
let _ = std::fs::remove_file(&tmp_path);
result
}
pub fn lean(&self) -> Wasm4pmResult<String> {
self.run_command(Wpm4Command::Lean, &[])
}
pub fn spc_status(&self) -> Wasm4pmResult<String> {
self.run_command(Wpm4Command::SpcStatus, &[])
}
pub fn receipt_doctor(&self) -> Wasm4pmResult<String> {
self.run_command(Wpm4Command::ReceiptDoctor, &[])
}
pub fn infer_verdict(output: &str, exit_success: bool) -> WpmVerdict {
if !exit_success {
return WpmVerdict::Fail;
}
let lower = output.to_lowercase();
if lower.contains("fail") || lower.contains("error") {
WpmVerdict::Fail
} else if lower.contains("warn") || lower.contains("drift") {
WpmVerdict::Warn
} else if lower.contains("pass") || lower.contains("ok") || output.trim().is_empty() {
WpmVerdict::Pass
} else {
WpmVerdict::Warn
}
}
pub fn audit_with_verdict(&self, evidence_data: &str) -> WpmVerdict {
match self.audit(evidence_data) {
Ok(output) => Self::infer_verdict(&output, true),
Err(Wasm4pmError::NotFound) => WpmVerdict::NotAvailable,
Err(_) => WpmVerdict::Fail,
}
}
pub fn lean_with_verdict(&self) -> WpmVerdict {
match self.lean() {
Ok(output) => Self::infer_verdict(&output, true),
Err(Wasm4pmError::NotFound) => WpmVerdict::NotAvailable,
Err(_) => WpmVerdict::Fail,
}
}
pub fn spc_status_with_verdict(&self) -> WpmVerdict {
match self.spc_status() {
Ok(output) => Self::infer_verdict(&output, true),
Err(Wasm4pmError::NotFound) => WpmVerdict::NotAvailable,
Err(_) => WpmVerdict::Fail,
}
}
pub fn receipt_doctor_with_verdict(&self) -> WpmVerdict {
match self.receipt_doctor() {
Ok(output) => Self::infer_verdict(&output, true),
Err(Wasm4pmError::NotFound) => WpmVerdict::NotAvailable,
Err(_) => WpmVerdict::Fail,
}
}
}