use std::collections::BTreeMap;
use std::fs::File;
use std::io::Read;
use std::path::Path;
use crate::error::{WhisperError, WhisperResult};
use crate::format::export::TensorData;
#[cfg(test)]
mod tests;
#[derive(Debug, Clone)]
pub struct CheckResult {
pub name: String,
pub passed: bool,
pub message: String,
}
impl CheckResult {
#[must_use]
pub fn pass(name: impl Into<String>, message: impl Into<String>) -> Self {
Self {
name: name.into(),
passed: true,
message: message.into(),
}
}
#[must_use]
pub fn fail(name: impl Into<String>, message: impl Into<String>) -> Self {
Self {
name: name.into(),
passed: false,
message: message.into(),
}
}
}
#[derive(Debug, Clone)]
pub struct VerificationReport {
pub checks: Vec<CheckResult>,
pub passed: bool,
pub total_checks: usize,
pub passed_checks: usize,
}
impl VerificationReport {
#[must_use]
pub fn new() -> Self {
Self {
checks: Vec::new(),
passed: true,
total_checks: 0,
passed_checks: 0,
}
}
pub fn add(&mut self, result: CheckResult) {
self.total_checks += 1;
if result.passed {
self.passed_checks += 1;
} else {
self.passed = false;
}
self.checks.push(result);
}
#[must_use]
pub fn pass_rate(&self) -> f64 {
if self.total_checks == 0 {
100.0
} else {
(self.passed_checks as f64 / self.total_checks as f64) * 100.0
}
}
}
impl Default for VerificationReport {
fn default() -> Self {
Self::new()
}
}
pub struct Verifier {
pub(crate) min_pass_rate: f64,
}
impl Default for Verifier {
fn default() -> Self {
Self::new()
}
}
impl Verifier {
#[must_use]
pub fn new() -> Self {
Self {
min_pass_rate: 88.0,
} }
#[must_use]
pub fn with_min_pass_rate(mut self, rate: f64) -> Self {
self.min_pass_rate = rate;
self
}
pub fn verify_apr<P: AsRef<Path>>(&self, path: P) -> WhisperResult<VerificationReport> {
let path = path.as_ref();
let mut report = VerificationReport::new();
if !path.exists() {
report.add(CheckResult::fail(
"A1_file_exists",
format!("File not found: {}", path.display()),
));
return Ok(report);
}
report.add(CheckResult::pass("A1_file_exists", "File exists"));
let mut file = match File::open(path) {
Ok(f) => {
report.add(CheckResult::pass("A2_readable", "File is readable"));
f
}
Err(e) => {
report.add(CheckResult::fail(
"A2_readable",
format!("Cannot read file: {}", e),
));
return Ok(report);
}
};
let mut magic = [0u8; 4];
if file.read_exact(&mut magic).is_ok() && &magic == b"APR\0" {
report.add(CheckResult::pass("A3_magic", "APR magic bytes valid"));
} else {
report.add(CheckResult::fail(
"A3_magic",
"Invalid APR magic bytes (expected APR\\0)",
));
}
let metadata = std::fs::metadata(path).map_err(WhisperError::Io)?;
let size = metadata.len();
if size >= 64 {
report.add(CheckResult::pass(
"A4_size",
format!("File size: {} bytes", size),
));
} else {
report.add(CheckResult::fail(
"A4_size",
format!("File too small: {} bytes (min 64)", size),
));
}
let mut buffer = vec![0u8; 1024.min(size as usize)];
file = File::open(path).map_err(WhisperError::Io)?;
let _ = file.read(&mut buffer);
let content = String::from_utf8_lossy(&buffer);
let secret_patterns = [
"PRIVATE KEY",
"sk-",
"api_key",
"password",
"secret",
"token",
];
let mut found_secrets = false;
for pattern in &secret_patterns {
if content.to_lowercase().contains(&pattern.to_lowercase()) {
report.add(CheckResult::fail(
"C6_no_secrets",
format!("Potential secret found: {}", pattern),
));
found_secrets = true;
break;
}
}
if !found_secrets {
report.add(CheckResult::pass(
"C6_no_secrets",
"No obvious secrets found",
));
}
Ok(report)
}
pub fn verify_safetensors<P: AsRef<Path>>(&self, path: P) -> WhisperResult<VerificationReport> {
let path = path.as_ref();
let mut report = VerificationReport::new();
if !path.exists() {
report.add(CheckResult::fail(
"A1_file_exists",
format!("File not found: {}", path.display()),
));
return Ok(report);
}
report.add(CheckResult::pass("A1_file_exists", "File exists"));
let data = std::fs::read(path).map_err(WhisperError::Io)?;
if data.len() < 8 {
report.add(CheckResult::fail(
"A4_header_size",
"File too small for SafeTensors header",
));
return Ok(report);
}
report.add(CheckResult::pass(
"A4_header_size",
"Header size field present",
));
let header_bytes: [u8; 8] = data[0..8]
.try_into()
.map_err(|_| WhisperError::Format("Header bytes too short".to_string()))?;
let header_len = u64::from_le_bytes(header_bytes) as usize;
if header_len > 100_000_000 {
report.add(CheckResult::fail(
"C11_header_limit",
format!("Header too large: {} bytes (max 100MB)", header_len),
));
} else {
report.add(CheckResult::pass(
"C11_header_limit",
format!("Header size: {} bytes", header_len),
));
}
if data.len() >= 8 + header_len {
let header_bytes = &data[8..8 + header_len];
match std::str::from_utf8(header_bytes) {
Ok(header_str) => {
let trimmed = header_str.trim();
if trimmed.starts_with('{') && trimmed.ends_with('}') {
report.add(CheckResult::pass("A5_json_valid", "Header is valid JSON"));
} else {
report.add(CheckResult::fail(
"A5_json_valid",
"Header JSON doesn't start with '{' or end with '}'",
));
}
}
Err(e) => {
report.add(CheckResult::fail(
"A5_json_valid",
format!("Header is not valid UTF-8: {}", e),
));
}
}
} else {
report.add(CheckResult::fail(
"A5_json_valid",
"File truncated before header end",
));
}
Ok(report)
}
pub fn verify_tensors(
&self,
tensors: &BTreeMap<String, TensorData>,
) -> WhisperResult<VerificationReport> {
let mut report = VerificationReport::new();
for (name, tensor) in tensors {
let has_nan = tensor.data.iter().any(|v| v.is_nan());
if has_nan {
report.add(CheckResult::fail(
format!("A10_no_nan_{}", name),
format!("Tensor '{}' contains NaN values", name),
));
} else {
report.add(CheckResult::pass(
format!("A10_no_nan_{}", name),
format!("Tensor '{}' has no NaN", name),
));
}
let has_inf = tensor.data.iter().any(|v| v.is_infinite());
if has_inf {
report.add(CheckResult::fail(
format!("A11_no_inf_{}", name),
format!("Tensor '{}' contains Inf values", name),
));
} else {
report.add(CheckResult::pass(
format!("A11_no_inf_{}", name),
format!("Tensor '{}' has no Inf", name),
));
}
let expected = tensor.expected_elements();
if tensor.data.len() == expected {
report.add(CheckResult::pass(
format!("A7_shape_{}", name),
format!("Tensor '{}' shape {:?} matches data", name, tensor.shape),
));
} else {
report.add(CheckResult::fail(
format!("A7_shape_{}", name),
format!(
"Tensor '{}' shape {:?} expects {} elements, got {}",
name,
tensor.shape,
expected,
tensor.data.len()
),
));
}
}
Ok(report)
}
#[must_use]
pub fn meets_threshold(&self, report: &VerificationReport) -> bool {
report.pass_rate() >= self.min_pass_rate
}
}
pub fn verify_apr<P: AsRef<Path>>(path: P) -> WhisperResult<VerificationReport> {
Verifier::new().verify_apr(path)
}
pub fn verify_safetensors<P: AsRef<Path>>(path: P) -> WhisperResult<VerificationReport> {
Verifier::new().verify_safetensors(path)
}