aperture_cli/engine/
loader.rs1use crate::cache::fingerprint::compute_content_hash;
2use crate::cache::metadata::CacheMetadataManager;
3use crate::cache::models::{CachedSpec, CACHE_FORMAT_VERSION};
4use crate::error::Error;
5use crate::fs::OsFileSystem;
6use std::fs;
7use std::path::Path;
8
9pub fn load_cached_spec<P: AsRef<Path>>(
30 cache_dir: P,
31 spec_name: &str,
32) -> Result<CachedSpec, Error> {
33 let fs = OsFileSystem;
35 let metadata_manager = CacheMetadataManager::new(&fs);
36
37 let spec = match metadata_manager.check_spec_version(&cache_dir, spec_name) {
39 Ok(true) => {
40 load_cached_spec_without_version_check(&cache_dir, spec_name)?
42 }
43 Ok(false) => {
44 load_cached_spec_with_version_check(&cache_dir, spec_name)?
46 }
47 Err(_) => {
48 load_cached_spec_with_version_check(&cache_dir, spec_name)?
50 }
51 };
52
53 check_spec_file_freshness(&cache_dir, spec_name, &metadata_manager)?;
55
56 Ok(spec)
57}
58
59fn check_spec_file_freshness<P: AsRef<Path>>(
68 cache_dir: P,
69 spec_name: &str,
70 metadata_manager: &CacheMetadataManager<'_, OsFileSystem>,
71) -> Result<(), Error> {
72 let Ok(Some((stored_hash, stored_mtime, stored_size))) =
74 metadata_manager.get_stored_fingerprint(&cache_dir, spec_name)
75 else {
76 return Ok(());
77 };
78
79 let Some(config_dir) = cache_dir.as_ref().parent() else {
83 return Ok(()); };
85 let spec_path = config_dir
86 .join(crate::constants::DIR_SPECS)
87 .join(format!("{spec_name}{}", crate::constants::FILE_EXT_YAML));
88
89 let Ok(file_meta) = fs::metadata(&spec_path) else {
91 return Ok(()); };
93 let current_mtime = file_meta
94 .modified()
95 .ok()
96 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
97 .map(|d| d.as_secs());
98 let Some(current_mtime) = current_mtime else {
99 return Ok(()); };
101 let current_size = file_meta.len();
102
103 if stored_mtime != current_mtime || stored_size != current_size {
106 return Err(Error::cache_stale(spec_name));
107 }
108
109 let Ok(content) = fs::read(&spec_path) else {
111 return Ok(()); };
113 let current_hash = compute_content_hash(&content);
114
115 if stored_hash != current_hash {
116 return Err(Error::cache_stale(spec_name));
117 }
118
119 Ok(())
120}
121
122fn load_cached_spec_without_version_check<P: AsRef<Path>>(
124 cache_dir: P,
125 spec_name: &str,
126) -> Result<CachedSpec, Error> {
127 let cache_path = cache_dir
128 .as_ref()
129 .join(format!("{spec_name}{}", crate::constants::FILE_EXT_BIN));
130
131 if !cache_path.exists() {
132 return Err(Error::cached_spec_not_found(spec_name));
133 }
134
135 let cache_data = fs::read(&cache_path)
136 .map_err(|e| Error::io_error(format!("Failed to read cache file: {e}")))?;
137 postcard::from_bytes(&cache_data)
138 .map_err(|e| Error::cached_spec_corrupted(spec_name, e.to_string()))
139}
140
141fn load_cached_spec_with_version_check<P: AsRef<Path>>(
143 cache_dir: P,
144 spec_name: &str,
145) -> Result<CachedSpec, Error> {
146 let cache_path = cache_dir
147 .as_ref()
148 .join(format!("{spec_name}{}", crate::constants::FILE_EXT_BIN));
149
150 if !cache_path.exists() {
151 return Err(Error::cached_spec_not_found(spec_name));
152 }
153
154 let cache_data = fs::read(&cache_path)
155 .map_err(|e| Error::io_error(format!("Failed to read cache file: {e}")))?;
156 let cached_spec: CachedSpec = postcard::from_bytes(&cache_data)
157 .map_err(|e| Error::cached_spec_corrupted(spec_name, e.to_string()))?;
158
159 if cached_spec.cache_format_version != CACHE_FORMAT_VERSION {
161 return Err(Error::cache_version_mismatch(
162 spec_name,
163 cached_spec.cache_format_version,
164 CACHE_FORMAT_VERSION,
165 ));
166 }
167
168 Ok(cached_spec)
169}