1use crate::trust::{
12 DEFAULT_TRUSTED_MODEL, TrustRootBinding, TrustedModelFile, TrustedModelManifest,
13 trust_root_binding,
14};
15use sha2::{Digest, Sha256};
16use std::io::Read as _;
17use std::path::Path;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum LocalFileStatus {
31 Ready,
33 Missing,
35 Partial,
37 Invalid,
39 CannotCheck,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum LocalFileIntegrity {
46 TrustedDigest,
48 Unverified,
50 NotAvailable,
52 Mismatch,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum ModelProvenance {
59 AppManaged,
61 UserSupplied,
63}
64
65impl LocalFileStatus {
66 pub fn user_label(&self) -> &'static str {
68 match self {
69 LocalFileStatus::Ready => "Ready",
70 LocalFileStatus::Missing => "Needed",
71 LocalFileStatus::Partial => "Needs to finish",
72 LocalFileStatus::Invalid => "Needs to be replaced",
73 LocalFileStatus::CannotCheck => "Could not check",
74 }
75 }
76
77 pub fn needs_work(&self) -> bool {
79 !matches!(self, LocalFileStatus::Ready)
80 }
81}
82
83#[derive(Debug, Clone)]
87pub struct FileReadiness {
88 logical_name: &'static str,
90 relative_path: &'static str,
92 status: LocalFileStatus,
94 integrity: LocalFileIntegrity,
96}
97
98impl FileReadiness {
99 pub fn logical_name(&self) -> &'static str {
100 self.logical_name
101 }
102
103 pub fn relative_path(&self) -> &'static str {
104 self.relative_path
105 }
106
107 pub fn status(&self) -> LocalFileStatus {
108 self.status
109 }
110
111 pub fn integrity(&self) -> LocalFileIntegrity {
112 self.integrity
113 }
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120pub enum ModelReadiness {
121 Ready,
123 NeedsDownload,
125 NeedsRepair,
127 CannotCheck,
129}
130
131#[derive(Debug, Clone)]
135pub struct ModelReadinessReport {
136 provenance: ModelProvenance,
137 overall: ModelReadiness,
138 files: Vec<FileReadiness>,
139 trust_root: Option<TrustRootBinding>,
140}
141
142impl ModelReadinessReport {
143 pub fn provenance(&self) -> ModelProvenance {
144 self.provenance
145 }
146
147 pub fn overall(&self) -> ModelReadiness {
148 self.overall
149 }
150
151 pub fn files(&self) -> &[FileReadiness] {
152 &self.files
153 }
154
155 pub fn files_needing_work(&self) -> Vec<&FileReadiness> {
157 self.files
158 .iter()
159 .filter(|f| f.status.needs_work())
160 .collect()
161 }
162
163 pub fn ready_count(&self) -> usize {
165 self.files
166 .iter()
167 .filter(|f| f.status == LocalFileStatus::Ready)
168 .count()
169 }
170
171 pub fn total_count(&self) -> usize {
172 self.files.len()
173 }
174
175 pub(crate) fn matches_trust_root(&self, binding: TrustRootBinding) -> bool {
176 self.trust_root == Some(binding)
177 }
178
179 #[cfg(test)]
180 pub(crate) fn set_file_state_for_test(
181 &mut self,
182 relative_path: &str,
183 status: LocalFileStatus,
184 integrity: LocalFileIntegrity,
185 ) {
186 let file = self
187 .files
188 .iter_mut()
189 .find(|file| file.relative_path == relative_path)
190 .expect("test file must exist");
191 file.status = status;
192 file.integrity = integrity;
193 self.overall = derive_overall_readiness(&self.files);
194 }
195
196 #[cfg(test)]
197 pub(crate) fn remove_file_for_test(&mut self, relative_path: &str) {
198 self.files
199 .retain(|file| file.relative_path != relative_path);
200 self.overall = derive_overall_readiness(&self.files);
201 }
202}
203
204pub fn check_model_readiness(model_dir: &Path) -> ModelReadinessReport {
214 check_model_readiness_inner(
215 model_dir,
216 &DEFAULT_TRUSTED_MODEL,
217 ModelProvenance::UserSupplied,
218 )
219}
220
221pub fn check_user_supplied_model_readiness(model_dir: &Path) -> ModelReadinessReport {
223 check_model_readiness(model_dir)
224}
225
226pub fn check_app_managed_model_readiness(model_dir: &Path) -> ModelReadinessReport {
234 check_model_readiness_inner(
235 model_dir,
236 &DEFAULT_TRUSTED_MODEL,
237 ModelProvenance::AppManaged,
238 )
239}
240
241#[cfg(test)]
244pub(crate) fn check_app_managed_model_readiness_against(
245 model_dir: &Path,
246 manifest: &'static TrustedModelManifest,
247) -> ModelReadinessReport {
248 check_model_readiness_inner(model_dir, manifest, ModelProvenance::AppManaged)
249}
250
251fn check_model_readiness_inner(
252 model_dir: &Path,
253 manifest: &'static TrustedModelManifest,
254 provenance: ModelProvenance,
255) -> ModelReadinessReport {
256 let mut files = Vec::new();
257
258 for trusted_file in manifest.files {
259 let full_path = model_dir.join(trusted_file.relative_path);
260 let part_path = model_dir.join(format!("{}.part", trusted_file.relative_path));
261 let (status, integrity) =
262 check_single_file(&full_path, &part_path, trusted_file, provenance);
263 files.push(FileReadiness {
264 logical_name: trusted_file.logical_name,
265 relative_path: trusted_file.relative_path,
266 status,
267 integrity,
268 });
269 }
270
271 let overall = derive_overall_readiness(&files);
272 ModelReadinessReport {
273 provenance,
274 overall,
275 files,
276 trust_root: (provenance == ModelProvenance::AppManaged)
277 .then(|| trust_root_binding(manifest)),
278 }
279}
280
281fn check_single_file(
282 path: &Path,
283 part_path: &Path,
284 trusted_file: &TrustedModelFile,
285 provenance: ModelProvenance,
286) -> (LocalFileStatus, LocalFileIntegrity) {
287 match std::fs::metadata(path) {
289 Ok(meta) => {
290 if !meta.is_file() {
291 return (LocalFileStatus::Invalid, LocalFileIntegrity::Mismatch);
292 }
293 if meta.len() == 0 {
294 return (LocalFileStatus::Invalid, LocalFileIntegrity::Mismatch);
295 }
296 if provenance == ModelProvenance::UserSupplied {
297 return (LocalFileStatus::Ready, LocalFileIntegrity::Unverified);
298 }
299 if meta.len() != trusted_file.exact_size_bytes {
300 return (LocalFileStatus::Invalid, LocalFileIntegrity::Mismatch);
301 }
302 match sha256_file(path) {
303 Ok(digest) if digest == trusted_file.sha256 => {
304 (LocalFileStatus::Ready, LocalFileIntegrity::TrustedDigest)
305 }
306 Ok(_) => (LocalFileStatus::Invalid, LocalFileIntegrity::Mismatch),
307 Err(_) => (
308 LocalFileStatus::CannotCheck,
309 LocalFileIntegrity::NotAvailable,
310 ),
311 }
312 }
313 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
314 if part_path.exists() {
316 (LocalFileStatus::Partial, LocalFileIntegrity::NotAvailable)
317 } else {
318 (LocalFileStatus::Missing, LocalFileIntegrity::NotAvailable)
319 }
320 }
321 Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => (
322 LocalFileStatus::CannotCheck,
323 LocalFileIntegrity::NotAvailable,
324 ),
325 Err(_) => (
326 LocalFileStatus::CannotCheck,
327 LocalFileIntegrity::NotAvailable,
328 ),
329 }
330}
331
332fn sha256_file(path: &Path) -> std::io::Result<String> {
333 let mut file = std::fs::File::open(path)?;
334 let mut hasher = Sha256::new();
335 let mut buffer = [0_u8; 64 * 1024];
336 loop {
337 let count = file.read(&mut buffer)?;
338 if count == 0 {
339 break;
340 }
341 hasher.update(&buffer[..count]);
342 }
343 let digest = hasher.finalize();
344 let mut encoded = String::with_capacity(64);
345 const HEX: &[u8; 16] = b"0123456789abcdef";
346 for byte in digest {
347 encoded.push(HEX[(byte >> 4) as usize] as char);
348 encoded.push(HEX[(byte & 0x0f) as usize] as char);
349 }
350 Ok(encoded)
351}
352
353fn derive_overall_readiness(files: &[FileReadiness]) -> ModelReadiness {
354 if files
355 .iter()
356 .any(|f| f.status == LocalFileStatus::CannotCheck)
357 {
358 return ModelReadiness::CannotCheck;
359 }
360 if files.iter().all(|f| f.status == LocalFileStatus::Ready) {
361 return ModelReadiness::Ready;
362 }
363 if files.iter().any(|f| f.status == LocalFileStatus::Missing) {
364 return ModelReadiness::NeedsDownload;
365 }
366 ModelReadiness::NeedsRepair
368}