use crate::trust::{
DEFAULT_TRUSTED_MODEL, TrustRootBinding, TrustedModelFile, TrustedModelManifest,
trust_root_binding,
};
use sha2::{Digest, Sha256};
use std::io::Read as _;
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LocalFileStatus {
Ready,
Missing,
Partial,
Invalid,
CannotCheck,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LocalFileIntegrity {
TrustedDigest,
Unverified,
NotAvailable,
Mismatch,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModelProvenance {
AppManaged,
UserSupplied,
}
impl LocalFileStatus {
pub fn user_label(&self) -> &'static str {
match self {
LocalFileStatus::Ready => "Ready",
LocalFileStatus::Missing => "Needed",
LocalFileStatus::Partial => "Needs to finish",
LocalFileStatus::Invalid => "Needs to be replaced",
LocalFileStatus::CannotCheck => "Could not check",
}
}
pub fn needs_work(&self) -> bool {
!matches!(self, LocalFileStatus::Ready)
}
}
#[derive(Debug, Clone)]
pub struct FileReadiness {
logical_name: &'static str,
relative_path: &'static str,
status: LocalFileStatus,
integrity: LocalFileIntegrity,
}
impl FileReadiness {
pub fn logical_name(&self) -> &'static str {
self.logical_name
}
pub fn relative_path(&self) -> &'static str {
self.relative_path
}
pub fn status(&self) -> LocalFileStatus {
self.status
}
pub fn integrity(&self) -> LocalFileIntegrity {
self.integrity
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModelReadiness {
Ready,
NeedsDownload,
NeedsRepair,
CannotCheck,
}
#[derive(Debug, Clone)]
pub struct ModelReadinessReport {
provenance: ModelProvenance,
overall: ModelReadiness,
files: Vec<FileReadiness>,
trust_root: Option<TrustRootBinding>,
}
impl ModelReadinessReport {
pub fn provenance(&self) -> ModelProvenance {
self.provenance
}
pub fn overall(&self) -> ModelReadiness {
self.overall
}
pub fn files(&self) -> &[FileReadiness] {
&self.files
}
pub fn files_needing_work(&self) -> Vec<&FileReadiness> {
self.files
.iter()
.filter(|f| f.status.needs_work())
.collect()
}
pub fn ready_count(&self) -> usize {
self.files
.iter()
.filter(|f| f.status == LocalFileStatus::Ready)
.count()
}
pub fn total_count(&self) -> usize {
self.files.len()
}
pub(crate) fn matches_trust_root(&self, binding: TrustRootBinding) -> bool {
self.trust_root == Some(binding)
}
#[cfg(test)]
pub(crate) fn set_file_state_for_test(
&mut self,
relative_path: &str,
status: LocalFileStatus,
integrity: LocalFileIntegrity,
) {
let file = self
.files
.iter_mut()
.find(|file| file.relative_path == relative_path)
.expect("test file must exist");
file.status = status;
file.integrity = integrity;
self.overall = derive_overall_readiness(&self.files);
}
#[cfg(test)]
pub(crate) fn remove_file_for_test(&mut self, relative_path: &str) {
self.files
.retain(|file| file.relative_path != relative_path);
self.overall = derive_overall_readiness(&self.files);
}
}
pub fn check_model_readiness(model_dir: &Path) -> ModelReadinessReport {
check_model_readiness_inner(
model_dir,
&DEFAULT_TRUSTED_MODEL,
ModelProvenance::UserSupplied,
)
}
pub fn check_user_supplied_model_readiness(model_dir: &Path) -> ModelReadinessReport {
check_model_readiness(model_dir)
}
pub fn check_app_managed_model_readiness(model_dir: &Path) -> ModelReadinessReport {
check_model_readiness_inner(
model_dir,
&DEFAULT_TRUSTED_MODEL,
ModelProvenance::AppManaged,
)
}
#[cfg(test)]
pub(crate) fn check_app_managed_model_readiness_against(
model_dir: &Path,
manifest: &'static TrustedModelManifest,
) -> ModelReadinessReport {
check_model_readiness_inner(model_dir, manifest, ModelProvenance::AppManaged)
}
fn check_model_readiness_inner(
model_dir: &Path,
manifest: &'static TrustedModelManifest,
provenance: ModelProvenance,
) -> ModelReadinessReport {
let mut files = Vec::new();
for trusted_file in manifest.files {
let full_path = model_dir.join(trusted_file.relative_path);
let part_path = model_dir.join(format!("{}.part", trusted_file.relative_path));
let (status, integrity) =
check_single_file(&full_path, &part_path, trusted_file, provenance);
files.push(FileReadiness {
logical_name: trusted_file.logical_name,
relative_path: trusted_file.relative_path,
status,
integrity,
});
}
let overall = derive_overall_readiness(&files);
ModelReadinessReport {
provenance,
overall,
files,
trust_root: (provenance == ModelProvenance::AppManaged)
.then(|| trust_root_binding(manifest)),
}
}
fn check_single_file(
path: &Path,
part_path: &Path,
trusted_file: &TrustedModelFile,
provenance: ModelProvenance,
) -> (LocalFileStatus, LocalFileIntegrity) {
match std::fs::metadata(path) {
Ok(meta) => {
if !meta.is_file() {
return (LocalFileStatus::Invalid, LocalFileIntegrity::Mismatch);
}
if meta.len() == 0 {
return (LocalFileStatus::Invalid, LocalFileIntegrity::Mismatch);
}
if provenance == ModelProvenance::UserSupplied {
return (LocalFileStatus::Ready, LocalFileIntegrity::Unverified);
}
if meta.len() != trusted_file.exact_size_bytes {
return (LocalFileStatus::Invalid, LocalFileIntegrity::Mismatch);
}
match sha256_file(path) {
Ok(digest) if digest == trusted_file.sha256 => {
(LocalFileStatus::Ready, LocalFileIntegrity::TrustedDigest)
}
Ok(_) => (LocalFileStatus::Invalid, LocalFileIntegrity::Mismatch),
Err(_) => (
LocalFileStatus::CannotCheck,
LocalFileIntegrity::NotAvailable,
),
}
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
if part_path.exists() {
(LocalFileStatus::Partial, LocalFileIntegrity::NotAvailable)
} else {
(LocalFileStatus::Missing, LocalFileIntegrity::NotAvailable)
}
}
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => (
LocalFileStatus::CannotCheck,
LocalFileIntegrity::NotAvailable,
),
Err(_) => (
LocalFileStatus::CannotCheck,
LocalFileIntegrity::NotAvailable,
),
}
}
fn sha256_file(path: &Path) -> std::io::Result<String> {
let mut file = std::fs::File::open(path)?;
let mut hasher = Sha256::new();
let mut buffer = [0_u8; 64 * 1024];
loop {
let count = file.read(&mut buffer)?;
if count == 0 {
break;
}
hasher.update(&buffer[..count]);
}
let digest = hasher.finalize();
let mut encoded = String::with_capacity(64);
const HEX: &[u8; 16] = b"0123456789abcdef";
for byte in digest {
encoded.push(HEX[(byte >> 4) as usize] as char);
encoded.push(HEX[(byte & 0x0f) as usize] as char);
}
Ok(encoded)
}
fn derive_overall_readiness(files: &[FileReadiness]) -> ModelReadiness {
if files
.iter()
.any(|f| f.status == LocalFileStatus::CannotCheck)
{
return ModelReadiness::CannotCheck;
}
if files.iter().all(|f| f.status == LocalFileStatus::Ready) {
return ModelReadiness::Ready;
}
if files.iter().any(|f| f.status == LocalFileStatus::Missing) {
return ModelReadiness::NeedsDownload;
}
ModelReadiness::NeedsRepair
}