1use std::path::{Path, PathBuf};
9use tracing::{debug, trace};
10
11use crate::Result;
12
13#[derive(Debug, Clone)]
28pub struct OciCache {
29 root: PathBuf,
30}
31
32impl Default for OciCache {
33 fn default() -> Self {
34 let cache_dir = dirs::cache_dir()
35 .unwrap_or_else(|| PathBuf::from(".cache"))
36 .join("cuenv")
37 .join("oci");
38 Self::new(cache_dir)
39 }
40}
41
42impl OciCache {
43 #[must_use]
45 pub fn new(root: PathBuf) -> Self {
46 Self { root }
47 }
48
49 #[must_use]
51 pub fn root(&self) -> &Path {
52 &self.root
53 }
54
55 #[must_use]
57 pub fn blob_path(&self, digest: &str) -> PathBuf {
58 let (algo, hash) = parse_digest(digest);
59 self.root.join("blobs").join(algo).join(hash)
60 }
61
62 #[must_use]
67 pub fn binary_dir(&self, digest: &str) -> PathBuf {
68 let (algo, hash) = parse_digest(digest);
69 self.root.join("bin").join(algo).join(hash)
70 }
71
72 #[must_use]
74 pub fn binary_path(&self, digest: &str, binary_name: &str) -> PathBuf {
75 self.binary_dir(digest).join(binary_name)
76 }
77
78 #[must_use]
80 pub fn has_blob(&self, digest: &str) -> bool {
81 self.blob_path(digest).exists()
82 }
83
84 #[must_use]
86 pub fn has_binary(&self, digest: &str, binary_name: &str) -> bool {
87 self.binary_path(digest, binary_name).exists()
88 }
89
90 #[must_use]
92 pub fn get_binary(&self, digest: &str, binary_name: &str) -> Option<PathBuf> {
93 let path = self.binary_path(digest, binary_name);
94 if path.exists() {
95 trace!(digest, ?path, "Cache hit for binary");
96 Some(path)
97 } else {
98 trace!(digest, "Cache miss for binary");
99 None
100 }
101 }
102
103 pub fn store_blob(&self, digest: &str, source: &Path) -> Result<PathBuf> {
107 let dest = self.blob_path(digest);
108 self.store_file(source, &dest)?;
109 debug!(digest, ?dest, "Stored blob in cache");
110 Ok(dest)
111 }
112
113 pub fn store_binary(&self, digest: &str, binary_name: &str, source: &Path) -> Result<PathBuf> {
117 let dest = self.binary_path(digest, binary_name);
118 self.store_file(source, &dest)?;
119 debug!(digest, binary_name, ?dest, "Stored binary in cache");
120 Ok(dest)
121 }
122
123 fn store_file(&self, source: &Path, dest: &Path) -> Result<()> {
125 if let Some(parent) = dest.parent() {
126 std::fs::create_dir_all(parent)?;
127 }
128 std::fs::copy(source, dest)?;
129 Ok(())
130 }
131
132 pub fn ensure_dirs(&self) -> Result<()> {
134 std::fs::create_dir_all(self.root.join("blobs").join("sha256"))?;
135 std::fs::create_dir_all(self.root.join("bin").join("sha256"))?;
136 Ok(())
137 }
138}
139
140fn parse_digest(digest: &str) -> (&str, &str) {
146 if let Some((algo, hash)) = digest.split_once(':') {
147 (algo, hash)
148 } else {
149 ("sha256", digest)
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156 use tempfile::TempDir;
157
158 #[test]
159 fn test_cache_paths() {
160 let cache = OciCache::new(PathBuf::from("/tmp/cache"));
161
162 assert_eq!(
163 cache.blob_path("sha256:abc123"),
164 PathBuf::from("/tmp/cache/blobs/sha256/abc123")
165 );
166 assert_eq!(
167 cache.binary_dir("sha256:def456"),
168 PathBuf::from("/tmp/cache/bin/sha256/def456")
169 );
170 assert_eq!(
171 cache.binary_path("sha256:def456", "jq"),
172 PathBuf::from("/tmp/cache/bin/sha256/def456/jq")
173 );
174 }
175
176 #[test]
177 fn test_cache_store_and_get() -> Result<()> {
178 let temp = TempDir::new()?;
179 let cache = OciCache::new(temp.path().to_path_buf());
180 cache.ensure_dirs()?;
181
182 let test_file = temp.path().join("test_binary");
184 std::fs::write(&test_file, b"test content")?;
185
186 let digest = "sha256:abc123";
188 let binary_name = "jq";
189 let cached_path = cache.store_binary(digest, binary_name, &test_file)?;
190
191 assert!(cache.has_binary(digest, binary_name));
193 assert_eq!(cache.get_binary(digest, binary_name), Some(cached_path));
194
195 let content = std::fs::read(cache.binary_path(digest, binary_name))?;
197 assert_eq!(content, b"test content");
198
199 Ok(())
200 }
201
202 #[test]
203 fn test_parse_digest() {
204 assert_eq!(parse_digest("sha256:abc123"), ("sha256", "abc123"));
205 assert_eq!(parse_digest("sha512:def456"), ("sha512", "def456"));
206 assert_eq!(parse_digest("abc123"), ("sha256", "abc123"));
207 }
208
209 #[test]
210 fn test_cache_default() {
211 let cache = OciCache::default();
212 let root = cache.root();
214 assert!(root.to_string_lossy().contains("oci"));
215 }
216
217 #[test]
218 fn test_cache_root() {
219 let cache = OciCache::new(PathBuf::from("/custom/cache"));
220 assert_eq!(cache.root(), Path::new("/custom/cache"));
221 }
222
223 #[test]
224 fn test_cache_clone() {
225 let cache = OciCache::new(PathBuf::from("/tmp/test"));
226 let cloned = cache.clone();
227 assert_eq!(cache.root(), cloned.root());
228 }
229
230 #[test]
231 fn test_cache_debug() {
232 let cache = OciCache::new(PathBuf::from("/tmp/test"));
233 let debug = format!("{cache:?}");
234 assert!(debug.contains("OciCache"));
235 assert!(debug.contains("/tmp/test"));
236 }
237
238 #[test]
239 fn test_has_blob_missing() {
240 let temp = TempDir::new().unwrap();
241 let cache = OciCache::new(temp.path().to_path_buf());
242 assert!(!cache.has_blob("sha256:nonexistent"));
243 }
244
245 #[test]
246 fn test_has_binary_missing() {
247 let temp = TempDir::new().unwrap();
248 let cache = OciCache::new(temp.path().to_path_buf());
249 assert!(!cache.has_binary("sha256:nonexistent", "missing"));
250 }
251
252 #[test]
253 fn test_get_binary_missing() {
254 let temp = TempDir::new().unwrap();
255 let cache = OciCache::new(temp.path().to_path_buf());
256 assert!(cache.get_binary("sha256:nonexistent", "missing").is_none());
257 }
258
259 #[test]
260 fn test_store_blob() -> Result<()> {
261 let temp = TempDir::new()?;
262 let cache = OciCache::new(temp.path().to_path_buf());
263
264 let source = temp.path().join("source_blob");
266 std::fs::write(&source, b"blob data")?;
267
268 let digest = "sha256:blobhash123";
269 let stored = cache.store_blob(digest, &source)?;
270
271 assert!(cache.has_blob(digest));
272 assert_eq!(stored, cache.blob_path(digest));
273
274 let content = std::fs::read(&stored)?;
275 assert_eq!(content, b"blob data");
276
277 Ok(())
278 }
279
280 #[test]
281 fn test_ensure_dirs() -> Result<()> {
282 let temp = TempDir::new()?;
283 let cache = OciCache::new(temp.path().to_path_buf());
284 cache.ensure_dirs()?;
285
286 assert!(temp.path().join("blobs").join("sha256").exists());
287 assert!(temp.path().join("bin").join("sha256").exists());
288
289 Ok(())
290 }
291
292 #[test]
293 fn test_blob_path_without_prefix() {
294 let cache = OciCache::new(PathBuf::from("/tmp/cache"));
295 assert_eq!(
297 cache.blob_path("abc123"),
298 PathBuf::from("/tmp/cache/blobs/sha256/abc123")
299 );
300 }
301
302 #[test]
303 fn test_binary_dir_without_prefix() {
304 let cache = OciCache::new(PathBuf::from("/tmp/cache"));
305 assert_eq!(
306 cache.binary_dir("xyz789"),
307 PathBuf::from("/tmp/cache/bin/sha256/xyz789")
308 );
309 }
310
311 #[test]
312 fn test_parse_digest_sha512() {
313 let (algo, hash) = parse_digest("sha512:longhashvalue");
314 assert_eq!(algo, "sha512");
315 assert_eq!(hash, "longhashvalue");
316 }
317}