use std::fs;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::error::{Result, VaultError};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationProbe {
pub label: String,
pub expected_checksum: String,
pub input_shape: Vec<usize>,
pub dtype: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationResult {
pub probe_label: String,
pub passed: bool,
pub actual_checksum: Option<String>,
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationReport {
pub model_name: String,
pub file_path: String,
pub results: Vec<ValidationResult>,
pub overall_pass: bool,
pub validated_at: String,
}
pub struct ValidationStore {
base_dir: PathBuf,
}
impl ValidationStore {
pub fn new(vault_path: &Path) -> Result<Self> {
let base_dir = vault_path.join("validations");
fs::create_dir_all(&base_dir)?;
Ok(Self { base_dir })
}
fn probe_file(&self, model: &str) -> PathBuf {
self.base_dir.join(format!("{}.probes.json", model))
}
pub fn save_probes(&self, model: &str, probes: &[ValidationProbe]) -> Result<()> {
let json = serde_json::to_string_pretty(probes)?;
fs::write(self.probe_file(model), json)?;
Ok(())
}
pub fn load_probes(&self, model: &str) -> Result<Vec<ValidationProbe>> {
let path = self.probe_file(model);
if !path.exists() {
return Ok(Vec::new());
}
let data = fs::read_to_string(&path)?;
let probes: Vec<ValidationProbe> = serde_json::from_str(&data)?;
Ok(probes)
}
pub fn validate(&self, model: &str, file_path: &Path) -> Result<ValidationReport> {
let probes = self.load_probes(model)?;
let mut results = Vec::new();
if !file_path.exists() {
results.push(ValidationResult {
probe_label: "file-exists".into(),
passed: false,
actual_checksum: None,
message: format!("File not found: {}", file_path.display()),
});
} else {
let meta = fs::metadata(file_path)?;
results.push(ValidationResult {
probe_label: "file-exists".into(),
passed: true,
actual_checksum: None,
message: format!("OK ({} bytes)", meta.len()),
});
if meta.len() == 0 {
results.push(ValidationResult {
probe_label: "non-empty".into(),
passed: false,
actual_checksum: None,
message: "File is empty".into(),
});
} else {
results.push(ValidationResult {
probe_label: "non-empty".into(),
passed: true,
actual_checksum: None,
message: "OK".into(),
});
}
}
for probe in &probes {
if probe.label == "file-integrity" {
let actual = file_sha256(file_path);
let passed = actual.as_deref() == Some(probe.expected_checksum.as_str());
results.push(ValidationResult {
probe_label: probe.label.clone(),
passed,
actual_checksum: actual.clone(),
message: if passed {
"Checksum matches".into()
} else {
format!(
"Checksum mismatch: expected {}, got {}",
probe.expected_checksum,
actual.unwrap_or_else(|| "N/A".into())
)
},
});
} else {
results.push(ValidationResult {
probe_label: probe.label.clone(),
passed: true,
actual_checksum: None,
message: "Probe skipped (requires runtime)".into(),
});
}
}
let overall_pass = results.iter().all(|r| r.passed);
Ok(ValidationReport {
model_name: model.to_string(),
file_path: file_path.display().to_string(),
results,
overall_pass,
validated_at: chrono::Utc::now().to_rfc3339(),
})
}
pub fn create_integrity_probe(&self, model: &str, file_path: &Path) -> Result<()> {
let checksum = file_sha256(file_path).ok_or_else(|| {
VaultError::IoError(std::io::Error::new(
std::io::ErrorKind::NotFound,
"Cannot hash file",
))
})?;
let probe = ValidationProbe {
label: "file-integrity".into(),
expected_checksum: checksum,
input_shape: vec![],
dtype: "bytes".into(),
};
let mut probes = self.load_probes(model)?;
probes.retain(|p| p.label != "file-integrity");
probes.push(probe);
self.save_probes(model, &probes)
}
}
fn file_sha256(path: &Path) -> Option<String> {
use sha2::Digest;
let data = fs::read(path).ok()?;
let hash = sha2::Sha256::digest(&data);
Some(hex::encode(hash))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_missing_file() {
let dir = tempfile::tempdir().unwrap();
let store = ValidationStore::new(dir.path()).unwrap();
let report = store.validate("test", Path::new("/nonexistent")).unwrap();
assert!(!report.overall_pass);
}
#[test]
fn test_validate_existing_file() {
let dir = tempfile::tempdir().unwrap();
let store = ValidationStore::new(dir.path()).unwrap();
let model_file = dir.path().join("model.bin");
fs::write(&model_file, b"hello model").unwrap();
let report = store.validate("test", &model_file).unwrap();
assert!(report.overall_pass);
assert_eq!(report.results.len(), 2); }
#[test]
fn test_integrity_probe() {
let dir = tempfile::tempdir().unwrap();
let store = ValidationStore::new(dir.path()).unwrap();
let model_file = dir.path().join("model.bin");
fs::write(&model_file, b"deterministic content").unwrap();
store
.create_integrity_probe("mymodel", &model_file)
.unwrap();
let report = store.validate("mymodel", &model_file).unwrap();
assert!(report.overall_pass);
fs::write(&model_file, b"corrupted content").unwrap();
let report2 = store.validate("mymodel", &model_file).unwrap();
assert!(!report2.overall_pass);
}
#[test]
fn test_probe_persistence() {
let dir = tempfile::tempdir().unwrap();
let store = ValidationStore::new(dir.path()).unwrap();
let probes = vec![ValidationProbe {
label: "test-probe".into(),
expected_checksum: "abc123".into(),
input_shape: vec![1, 3, 224, 224],
dtype: "f32".into(),
}];
store.save_probes("mymodel", &probes).unwrap();
let loaded = store.load_probes("mymodel").unwrap();
assert_eq!(loaded.len(), 1);
assert_eq!(loaded[0].label, "test-probe");
}
}