use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ProvenanceChain {
pub input_hash: String,
pub config_hash: String,
pub plan_hash: String,
pub output_hash: String,
pub combined_hash: String,
pub algorithm_id: String,
pub algorithm_version: String,
pub backend_id: String,
pub kernel_version: String,
pub wasm_build_hash: String,
}
pub struct ProvenanceChainArgs {
pub input_hash: String,
pub config_hash: String,
pub plan_hash: String,
pub output_hash: String,
pub combined_hash: String,
pub algorithm_id: String,
pub algorithm_version: String,
pub backend_id: String,
pub kernel_version: String,
pub wasm_build_hash: String,
}
impl ProvenanceChain {
pub fn builder() -> ProvenanceChainBuilder {
ProvenanceChainBuilder::default()
}
pub fn new(args: ProvenanceChainArgs) -> Self {
ProvenanceChain {
input_hash: args.input_hash,
config_hash: args.config_hash,
plan_hash: args.plan_hash,
output_hash: args.output_hash,
combined_hash: args.combined_hash,
algorithm_id: args.algorithm_id,
algorithm_version: args.algorithm_version,
backend_id: args.backend_id,
kernel_version: args.kernel_version,
wasm_build_hash: args.wasm_build_hash,
}
}
pub fn validate(&self) -> Result<(), String> {
if self.input_hash.is_empty() {
return Err("input_hash is empty".to_string());
}
if self.config_hash.is_empty() {
return Err("config_hash is empty".to_string());
}
if self.plan_hash.is_empty() {
return Err("plan_hash is empty".to_string());
}
if self.output_hash.is_empty() {
return Err("output_hash is empty".to_string());
}
if self.combined_hash.is_empty() {
return Err("combined_hash is empty".to_string());
}
if self.algorithm_id.is_empty() {
return Err("algorithm_id is empty".to_string());
}
if self.algorithm_version.is_empty() {
return Err("algorithm_version is empty".to_string());
}
if self.backend_id.is_empty() {
return Err("backend_id is empty".to_string());
}
if self.kernel_version.is_empty() {
return Err("kernel_version is empty".to_string());
}
if self.wasm_build_hash.is_empty() {
return Err("wasm_build_hash is empty".to_string());
}
Ok(())
}
pub fn validate_hash_format(&self) -> Result<(), String> {
let hashes = vec![
("input_hash", &self.input_hash),
("config_hash", &self.config_hash),
("plan_hash", &self.plan_hash),
("output_hash", &self.output_hash),
("combined_hash", &self.combined_hash),
("wasm_build_hash", &self.wasm_build_hash),
];
for (name, hash) in hashes {
if hash.len() != 64 {
return Err(format!(
"{} has incorrect length: {} (expected 64)",
name,
hash.len()
));
}
if !hash.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(format!("{} contains non-hex characters", name));
}
}
Ok(())
}
}
#[derive(Default)]
pub struct ProvenanceChainBuilder {
input_hash: Option<String>,
config_hash: Option<String>,
plan_hash: Option<String>,
output_hash: Option<String>,
combined_hash: Option<String>,
algorithm_id: Option<String>,
algorithm_version: Option<String>,
backend_id: Option<String>,
kernel_version: Option<String>,
wasm_build_hash: Option<String>,
}
impl ProvenanceChainBuilder {
pub fn input_hash(mut self, hash: String) -> Self {
self.input_hash = Some(hash);
self
}
pub fn config_hash(mut self, hash: String) -> Self {
self.config_hash = Some(hash);
self
}
pub fn plan_hash(mut self, hash: String) -> Self {
self.plan_hash = Some(hash);
self
}
pub fn output_hash(mut self, hash: String) -> Self {
self.output_hash = Some(hash);
self
}
pub fn combined_hash(mut self, hash: String) -> Self {
self.combined_hash = Some(hash);
self
}
pub fn algorithm_id(mut self, id: String) -> Self {
self.algorithm_id = Some(id);
self
}
pub fn algorithm_version(mut self, version: String) -> Self {
self.algorithm_version = Some(version);
self
}
pub fn backend_id(mut self, id: String) -> Self {
self.backend_id = Some(id);
self
}
pub fn kernel_version(mut self, version: String) -> Self {
self.kernel_version = Some(version);
self
}
pub fn wasm_build_hash(mut self, hash: String) -> Self {
self.wasm_build_hash = Some(hash);
self
}
pub fn build(self) -> Result<ProvenanceChain, String> {
Ok(ProvenanceChain {
input_hash: self.input_hash.ok_or("input_hash is required")?,
config_hash: self.config_hash.ok_or("config_hash is required")?,
plan_hash: self.plan_hash.ok_or("plan_hash is required")?,
output_hash: self.output_hash.ok_or("output_hash is required")?,
combined_hash: self.combined_hash.ok_or("combined_hash is required")?,
algorithm_id: self.algorithm_id.ok_or("algorithm_id is required")?,
algorithm_version: self
.algorithm_version
.ok_or("algorithm_version is required")?,
backend_id: self.backend_id.ok_or("backend_id is required")?,
kernel_version: self.kernel_version.ok_or("kernel_version is required")?,
wasm_build_hash: self.wasm_build_hash.ok_or("wasm_build_hash is required")?,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_provenance() -> ProvenanceChain {
ProvenanceChain::new(ProvenanceChainArgs {
input_hash: "a".repeat(64),
config_hash: "b".repeat(64),
plan_hash: "c".repeat(64),
output_hash: "d".repeat(64),
combined_hash: "e".repeat(64),
algorithm_id: "dfg".to_string(),
algorithm_version: "26.4.10".to_string(),
backend_id: "wasm".to_string(),
kernel_version: "26.4.10".to_string(),
wasm_build_hash: "f".repeat(64),
})
}
#[test]
fn test_provenance_validation() {
let prov = create_test_provenance();
assert!(prov.validate().is_ok());
}
#[test]
fn test_provenance_hash_format() {
let prov = create_test_provenance();
assert!(prov.validate_hash_format().is_ok());
}
#[test]
fn test_provenance_empty_hash() {
let mut prov = create_test_provenance();
prov.input_hash = String::new();
assert!(prov.validate().is_err());
}
#[test]
fn test_provenance_invalid_hash_length() {
let mut prov = create_test_provenance();
prov.input_hash = "a".repeat(32); assert!(prov.validate_hash_format().is_err());
}
}