1use std::io::Read;
4use std::path::{Path, PathBuf};
5
6use oci_client::Reference;
7use serde::{Deserialize, Serialize};
8use sha2::{Digest as Sha2Digest, Sha256};
9
10use crate::{
11 config::ImageConfig,
12 digest::Digest,
13 erofs::ErofsReader,
14 error::{ImageError, ImageResult},
15};
16
17const LAYERS_DIR: &str = "layers";
23
24const FSMETA_DIR: &str = "fsmeta";
26
27const VMDK_DIR: &str = "vmdk";
29
30const FLAT_DIR: &str = "flat";
32const FLAT_REFS_DIR: &str = "refs";
33const FLAT_BLOBS_DIR: &str = "blobs";
34const FLAT_LOCKS_DIR: &str = "locks";
35
36const MANIFESTS_DIR: &str = "manifests";
38
39const TMP_DIR: &str = "tmp";
41
42const EROFS_ALIGNMENT_BYTES: u64 = 4096;
44
45#[derive(Clone)]
65pub struct GlobalCache {
66 layers_dir: PathBuf,
68
69 fsmeta_dir: PathBuf,
71
72 vmdk_dir: PathBuf,
74
75 flat_refs_dir: PathBuf,
77
78 flat_blobs_dir: PathBuf,
80
81 flat_locks_dir: PathBuf,
83
84 manifests_dir: PathBuf,
86
87 tmp_dir: PathBuf,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct CachedImageMetadata {
94 pub manifest_digest: String,
96 pub config_digest: String,
98 pub raw_manifest_json: String,
100 pub raw_config_json: String,
102 pub config: ImageConfig,
104 pub layers: Vec<CachedLayerMetadata>,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct CachedLayerMetadata {
111 pub digest: String,
113 pub media_type: Option<String>,
115 pub size_bytes: Option<u64>,
117 pub diff_id: String,
119}
120
121#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
123pub struct FlatRootfsRef {
124 pub schema: u32,
126 pub manifest_digest: String,
128 pub derivation_digest: String,
130 pub artifact_digest: String,
132 pub materializer_abi: u32,
134 pub uuid: String,
136 pub virtual_size_bytes: u64,
138 pub inode_count: u64,
140 pub content_bytes: u64,
142}
143
144impl GlobalCache {
149 pub fn new(cache_dir: &Path) -> ImageResult<Self> {
153 let layers_dir = cache_dir.join(LAYERS_DIR);
154 let fsmeta_dir = cache_dir.join(FSMETA_DIR);
155 let vmdk_dir = cache_dir.join(VMDK_DIR);
156 let flat_dir = cache_dir.join(FLAT_DIR);
157 let flat_refs_dir = flat_dir.join(FLAT_REFS_DIR);
158 let flat_blobs_dir = flat_dir.join(FLAT_BLOBS_DIR);
159 let flat_locks_dir = flat_dir.join(FLAT_LOCKS_DIR);
160 let manifests_dir = cache_dir.join(MANIFESTS_DIR);
161 let tmp_dir = cache_dir.join(TMP_DIR);
162
163 for dir in [
164 &layers_dir,
165 &fsmeta_dir,
166 &vmdk_dir,
167 &flat_refs_dir,
168 &flat_blobs_dir,
169 &flat_locks_dir,
170 &manifests_dir,
171 &tmp_dir,
172 ] {
173 std::fs::create_dir_all(dir).map_err(|e| ImageError::Cache {
174 path: dir.clone(),
175 source: e,
176 })?;
177 }
178
179 Ok(Self {
180 layers_dir,
181 fsmeta_dir,
182 vmdk_dir,
183 flat_refs_dir,
184 flat_blobs_dir,
185 flat_locks_dir,
186 manifests_dir,
187 tmp_dir,
188 })
189 }
190
191 pub async fn new_async(cache_dir: &Path) -> ImageResult<Self> {
193 let layers_dir = cache_dir.join(LAYERS_DIR);
194 let fsmeta_dir = cache_dir.join(FSMETA_DIR);
195 let vmdk_dir = cache_dir.join(VMDK_DIR);
196 let flat_dir = cache_dir.join(FLAT_DIR);
197 let flat_refs_dir = flat_dir.join(FLAT_REFS_DIR);
198 let flat_blobs_dir = flat_dir.join(FLAT_BLOBS_DIR);
199 let flat_locks_dir = flat_dir.join(FLAT_LOCKS_DIR);
200 let manifests_dir = cache_dir.join(MANIFESTS_DIR);
201 let tmp_dir = cache_dir.join(TMP_DIR);
202
203 for dir in [
204 &layers_dir,
205 &fsmeta_dir,
206 &vmdk_dir,
207 &flat_refs_dir,
208 &flat_blobs_dir,
209 &flat_locks_dir,
210 &manifests_dir,
211 &tmp_dir,
212 ] {
213 tokio::fs::create_dir_all(dir)
214 .await
215 .map_err(|e| ImageError::Cache {
216 path: dir.clone(),
217 source: e,
218 })?;
219 }
220
221 Ok(Self {
222 layers_dir,
223 fsmeta_dir,
224 vmdk_dir,
225 flat_refs_dir,
226 flat_blobs_dir,
227 flat_locks_dir,
228 manifests_dir,
229 tmp_dir,
230 })
231 }
232
233 pub fn layers_dir(&self) -> &Path {
237 &self.layers_dir
238 }
239
240 pub fn layer_erofs_path(&self, diff_id: &Digest) -> PathBuf {
242 self.layers_dir
243 .join(format!("{}.erofs", diff_id.to_path_safe()))
244 }
245
246 pub fn layer_erofs_lock_path(&self, diff_id: &Digest) -> PathBuf {
248 self.layers_dir
249 .join(format!("{}.erofs.lock", diff_id.to_path_safe()))
250 }
251
252 pub fn is_layer_materialized(&self, diff_id: &Digest) -> bool {
254 is_valid_erofs_artifact(&self.layer_erofs_path(diff_id))
255 }
256
257 pub fn all_layers_materialized(&self, diff_ids: &[Digest]) -> bool {
259 diff_ids.iter().all(|d| self.is_layer_materialized(d))
260 }
261
262 pub fn fsmeta_dir(&self) -> &Path {
266 &self.fsmeta_dir
267 }
268
269 pub fn fsmeta_erofs_path(&self, manifest_digest: &Digest) -> PathBuf {
271 self.fsmeta_dir
272 .join(format!("{}.erofs", manifest_digest.to_path_safe()))
273 }
274
275 pub fn fsmeta_erofs_lock_path(&self, manifest_digest: &Digest) -> PathBuf {
277 self.fsmeta_dir
278 .join(format!("{}.erofs.lock", manifest_digest.to_path_safe()))
279 }
280
281 pub fn is_fsmeta_materialized(&self, manifest_digest: &Digest) -> bool {
283 is_valid_erofs_artifact(&self.fsmeta_erofs_path(manifest_digest))
284 }
285
286 pub fn vmdk_dir(&self) -> &Path {
290 &self.vmdk_dir
291 }
292
293 pub fn vmdk_path(&self, manifest_digest: &Digest) -> PathBuf {
295 self.vmdk_dir
296 .join(format!("{}.vmdk", manifest_digest.to_path_safe()))
297 }
298
299 pub fn vmdk_lock_path(&self, manifest_digest: &Digest) -> PathBuf {
301 self.vmdk_dir
302 .join(format!("{}.vmdk.lock", manifest_digest.to_path_safe()))
303 }
304
305 pub fn is_vmdk_materialized(&self, manifest_digest: &Digest) -> bool {
307 self.vmdk_path(manifest_digest).exists()
308 }
309
310 pub fn flat_ref_path(&self, manifest_digest: &Digest) -> PathBuf {
314 self.flat_refs_dir
315 .join(format!("{}.json", manifest_digest.to_path_safe()))
316 }
317
318 pub fn flat_blob_path(&self, artifact_digest: &Digest) -> PathBuf {
320 self.flat_blobs_dir
321 .join(format!("{}.raw", artifact_digest.to_path_safe()))
322 }
323
324 pub fn flat_lock_path(&self, derivation_digest: &Digest) -> PathBuf {
326 self.flat_locks_dir
327 .join(format!("{}.lock", derivation_digest.to_path_safe()))
328 }
329
330 pub fn flat_work_dir(&self, derivation_digest: &Digest) -> PathBuf {
332 self.tmp_dir
333 .join(format!("{}.flat.work", derivation_digest.to_path_safe()))
334 }
335
336 pub fn publish_flat_blob(
338 &self,
339 candidate: &Path,
340 artifact_digest: &Digest,
341 expected_size: u64,
342 ) -> ImageResult<PathBuf> {
343 let destination = self.flat_blob_path(artifact_digest);
344 if let Ok(metadata) = std::fs::metadata(&destination) {
345 if metadata.len() != expected_size {
346 return Err(ImageError::Cache {
347 path: destination,
348 source: std::io::Error::new(
349 std::io::ErrorKind::InvalidData,
350 "content-addressed flat blob has an unexpected size",
351 ),
352 });
353 }
354 if flat_blob_matches_digest(&destination, artifact_digest)? {
355 let _ = std::fs::remove_file(candidate);
356 return Ok(destination);
357 }
358
359 std::fs::remove_file(&destination).map_err(|source| ImageError::Cache {
364 path: destination.clone(),
365 source,
366 })?;
367 }
368 std::fs::rename(candidate, &destination).map_err(|source| ImageError::Cache {
369 path: destination.clone(),
370 source,
371 })?;
372 sync_directory(&self.flat_blobs_dir)?;
373 Ok(destination)
374 }
375
376 pub fn read_flat_ref(&self, manifest_digest: &Digest) -> ImageResult<Option<FlatRootfsRef>> {
378 let path = self.flat_ref_path(manifest_digest);
379 let data = match std::fs::read_to_string(&path) {
380 Ok(data) => data,
381 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
382 Err(source) => return Err(ImageError::Cache { path, source }),
383 };
384 let reference = match serde_json::from_str::<FlatRootfsRef>(&data) {
385 Ok(reference)
386 if reference.schema == 1
387 && reference.manifest_digest == manifest_digest.to_string() =>
388 {
389 reference
390 }
391 Ok(_) => return Ok(None),
392 Err(error) => {
393 tracing::warn!(path = %path.display(), %error, "corrupt flat rootfs ref, ignoring");
394 return Ok(None);
395 }
396 };
397 let artifact_digest = match reference.artifact_digest.parse::<Digest>() {
398 Ok(digest) => digest,
399 Err(_) => return Ok(None),
400 };
401 let blob_path = self.flat_blob_path(&artifact_digest);
402 match std::fs::metadata(&blob_path) {
403 Ok(metadata) if metadata.len() == reference.virtual_size_bytes => Ok(Some(reference)),
404 Ok(_) | Err(_) => Ok(None),
405 }
406 }
407
408 pub fn write_flat_ref(
410 &self,
411 manifest_digest: &Digest,
412 reference: &FlatRootfsRef,
413 ) -> ImageResult<()> {
414 let path = self.flat_ref_path(manifest_digest);
415 let temp_path = path.with_extension("json.part");
416 let payload = serde_json::to_vec_pretty(reference).map_err(|error| {
417 ImageError::ConfigParse(format!("failed to serialize flat rootfs ref: {error}"))
418 })?;
419 let mut temp = std::fs::File::create(&temp_path).map_err(|source| ImageError::Cache {
420 path: temp_path.clone(),
421 source,
422 })?;
423 use std::io::Write;
424 temp.write_all(&payload)
425 .map_err(|source| ImageError::Cache {
426 path: temp_path.clone(),
427 source,
428 })?;
429 temp.sync_all().map_err(|source| ImageError::Cache {
430 path: temp_path.clone(),
431 source,
432 })?;
433 std::fs::rename(&temp_path, &path).map_err(|source| ImageError::Cache {
434 path: path.clone(),
435 source,
436 })?;
437 sync_directory(&self.flat_refs_dir)?;
438 Ok(())
439 }
440
441 pub fn tmp_dir(&self) -> &Path {
445 &self.tmp_dir
446 }
447
448 pub fn part_path(&self, blob_digest: &Digest) -> PathBuf {
450 self.tmp_dir
451 .join(format!("{}.part", blob_digest.to_path_safe()))
452 }
453
454 pub fn download_lock_path(&self, blob_digest: &Digest) -> PathBuf {
456 self.tmp_dir
457 .join(format!("{}.download.lock", blob_digest.to_path_safe()))
458 }
459
460 pub fn work_dir(&self, key: &Digest) -> PathBuf {
462 self.tmp_dir.join(format!("{}.work", key.to_path_safe()))
463 }
464
465 pub fn manifests_dir(&self) -> &Path {
469 &self.manifests_dir
470 }
471
472 pub fn image_lock_path(&self, reference: &Reference) -> PathBuf {
474 self.manifests_dir
475 .join(format!("{}.lock", image_cache_key(reference)))
476 }
477
478 pub fn read_image_metadata(
480 &self,
481 reference: &Reference,
482 ) -> ImageResult<Option<CachedImageMetadata>> {
483 let path = self.image_metadata_path(reference);
484
485 let data = match std::fs::read_to_string(&path) {
486 Ok(data) => data,
487 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
488 Err(e) => return Err(ImageError::Cache { path, source: e }),
489 };
490
491 parse_cached_image_metadata(&path, &data)
492 }
493
494 pub async fn read_image_metadata_async(
496 &self,
497 reference: &Reference,
498 ) -> ImageResult<Option<CachedImageMetadata>> {
499 let path = self.image_metadata_path(reference);
500
501 let data = match tokio::fs::read_to_string(&path).await {
502 Ok(data) => data,
503 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
504 Err(e) => return Err(ImageError::Cache { path, source: e }),
505 };
506
507 parse_cached_image_metadata(&path, &data)
508 }
509
510 #[cfg_attr(not(test), allow(dead_code))]
512 pub(crate) fn write_image_metadata(
513 &self,
514 reference: &Reference,
515 metadata: &CachedImageMetadata,
516 ) -> ImageResult<()> {
517 let path = self.image_metadata_path(reference);
518 let temp_path = path.with_extension("json.part");
519 let payload = serde_json::to_vec(metadata).map_err(|e| {
520 ImageError::ConfigParse(format!("failed to serialize cached image metadata: {e}"))
521 })?;
522
523 std::fs::write(&temp_path, payload).map_err(|e| ImageError::Cache {
524 path: temp_path.clone(),
525 source: e,
526 })?;
527 std::fs::rename(&temp_path, &path).map_err(|e| ImageError::Cache { path, source: e })?;
528
529 Ok(())
530 }
531
532 pub async fn write_image_metadata_async(
534 &self,
535 reference: &Reference,
536 metadata: &CachedImageMetadata,
537 ) -> ImageResult<()> {
538 let path = self.image_metadata_path(reference);
539 let temp_path = path.with_extension("json.part");
540 let payload = serde_json::to_vec(metadata).map_err(|e| {
541 ImageError::ConfigParse(format!("failed to serialize cached image metadata: {e}"))
542 })?;
543
544 tokio::fs::write(&temp_path, payload)
545 .await
546 .map_err(|e| ImageError::Cache {
547 path: temp_path.clone(),
548 source: e,
549 })?;
550 tokio::fs::rename(&temp_path, &path)
551 .await
552 .map_err(|e| ImageError::Cache { path, source: e })?;
553
554 Ok(())
555 }
556
557 pub fn delete_image_metadata(&self, reference: &Reference) -> ImageResult<()> {
559 let path = self.image_metadata_path(reference);
560 match std::fs::remove_file(&path) {
561 Ok(()) => Ok(()),
562 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
563 Err(e) => Err(ImageError::Cache { path, source: e }),
564 }
565 }
566
567 pub async fn delete_image_metadata_async(&self, reference: &Reference) -> ImageResult<()> {
569 let path = self.image_metadata_path(reference);
570 match tokio::fs::remove_file(&path).await {
571 Ok(()) => Ok(()),
572 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
573 Err(e) => Err(ImageError::Cache { path, source: e }),
574 }
575 }
576
577 pub fn image_metadata_path(&self, reference: &Reference) -> PathBuf {
579 self.manifests_dir
580 .join(format!("{}.json", image_cache_key(reference)))
581 }
582
583 pub fn tar_path(&self, digest: &Digest) -> PathBuf {
587 self.layers_dir
588 .join(format!("{}.tar.gz", digest.to_path_safe()))
589 }
590}
591
592fn image_cache_key(reference: &Reference) -> String {
597 let mut hasher = Sha256::new();
598 hasher.update(reference.to_string().as_bytes());
599 hex::encode(hasher.finalize())
600}
601
602#[cfg(unix)]
603fn sync_directory(path: &Path) -> ImageResult<()> {
604 let directory = std::fs::File::open(path).map_err(|source| ImageError::Cache {
605 path: path.to_path_buf(),
606 source,
607 })?;
608 directory.sync_all().map_err(|source| ImageError::Cache {
609 path: path.to_path_buf(),
610 source,
611 })
612}
613
614#[cfg(not(unix))]
615fn sync_directory(_path: &Path) -> ImageResult<()> {
616 Ok(())
617}
618
619pub(crate) fn parse_cached_image_metadata(
620 path: &Path,
621 data: &str,
622) -> ImageResult<Option<CachedImageMetadata>> {
623 match serde_json::from_str::<CachedImageMetadata>(data) {
624 Ok(metadata) => Ok(Some(metadata)),
625 Err(e) => {
626 tracing::warn!(
627 path = %path.display(),
628 error = %e,
629 "corrupt image metadata cache, ignoring"
630 );
631 Ok(None)
632 }
633 }
634}
635
636pub(crate) fn is_valid_erofs_artifact(path: &Path) -> bool {
637 let Ok(meta) = std::fs::metadata(path) else {
638 return false;
639 };
640 let len = meta.len();
641 if !meta.is_file() || len == 0 || len % EROFS_ALIGNMENT_BYTES != 0 {
642 return false;
643 }
644
645 let Ok(file) = std::fs::File::open(path) else {
649 return false;
650 };
651 let Ok(mut reader) = ErofsReader::new(file) else {
652 return false;
653 };
654 reader.root_directory_metadata().is_ok()
655}
656
657pub(crate) async fn is_valid_erofs_artifact_async(path: &Path) -> bool {
658 let path = path.to_path_buf();
659 tokio::task::spawn_blocking(move || is_valid_erofs_artifact(&path))
660 .await
661 .unwrap_or(false)
662}
663
664fn flat_blob_matches_digest(path: &Path, expected: &Digest) -> ImageResult<bool> {
665 let mut file = std::fs::File::open(path).map_err(|source| ImageError::Cache {
666 path: path.to_path_buf(),
667 source,
668 })?;
669 let mut hasher = Sha256::new();
670 let mut buffer = [0u8; 1024 * 1024];
671 loop {
672 let read = file.read(&mut buffer).map_err(|source| ImageError::Cache {
673 path: path.to_path_buf(),
674 source,
675 })?;
676 if read == 0 {
677 break;
678 }
679 hasher.update(&buffer[..read]);
680 }
681 Ok(format!("sha256:{}", hex::encode(hasher.finalize())) == expected.to_string())
682}
683
684#[cfg(test)]
689mod tests {
690 use super::*;
691
692 fn digest(byte: char) -> Digest {
693 format!("sha256:{}", byte.to_string().repeat(64))
694 .parse()
695 .unwrap()
696 }
697
698 #[test]
699 fn flat_cache_separates_manifest_refs_from_content_blobs() {
700 let directory = tempfile::tempdir().unwrap();
701 let cache = GlobalCache::new(directory.path()).unwrap();
702 let manifest = digest('a');
703 let derivation = digest('b');
704 let artifact = digest('c');
705
706 assert!(cache.flat_ref_path(&manifest).ends_with(
707 "flat/refs/sha256_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.json"
708 ));
709 assert!(cache.flat_blob_path(&artifact).ends_with(
710 "flat/blobs/sha256_cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc.raw"
711 ));
712 assert!(cache.flat_lock_path(&derivation).ends_with(
713 "flat/locks/sha256_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.lock"
714 ));
715
716 let blob_path = cache.flat_blob_path(&artifact);
717 let blob = std::fs::File::create(&blob_path).unwrap();
718 blob.set_len(4096).unwrap();
719 let reference = FlatRootfsRef {
720 schema: 1,
721 manifest_digest: manifest.to_string(),
722 derivation_digest: derivation.to_string(),
723 artifact_digest: artifact.to_string(),
724 materializer_abi: 1,
725 uuid: "00".repeat(16),
726 virtual_size_bytes: 4096,
727 inode_count: 2,
728 content_bytes: 7,
729 };
730 cache.write_flat_ref(&manifest, &reference).unwrap();
731
732 assert_eq!(cache.read_flat_ref(&manifest).unwrap(), Some(reference));
733 }
734
735 #[test]
736 fn aligned_garbage_is_not_a_valid_erofs_cache_artifact() {
737 let directory = tempfile::tempdir().unwrap();
738 let path = directory.path().join("corrupt.erofs");
739 let file = std::fs::File::create(&path).unwrap();
740 file.set_len(EROFS_ALIGNMENT_BYTES).unwrap();
741
742 assert!(!is_valid_erofs_artifact(&path));
743 }
744
745 #[test]
746 fn flat_ref_must_name_the_manifest_that_indexes_it() {
747 let directory = tempfile::tempdir().unwrap();
748 let cache = GlobalCache::new(directory.path()).unwrap();
749 let indexed_manifest = digest('a');
750 let wrong_manifest = digest('b');
751 let artifact = digest('c');
752 std::fs::write(cache.flat_blob_path(&artifact), [0u8; 8]).unwrap();
753 let reference = FlatRootfsRef {
754 schema: 1,
755 manifest_digest: wrong_manifest.to_string(),
756 derivation_digest: digest('d').to_string(),
757 artifact_digest: artifact.to_string(),
758 materializer_abi: 1,
759 uuid: "00".repeat(16),
760 virtual_size_bytes: 8,
761 inode_count: 2,
762 content_bytes: 0,
763 };
764 cache.write_flat_ref(&indexed_manifest, &reference).unwrap();
765
766 assert_eq!(cache.read_flat_ref(&indexed_manifest).unwrap(), None);
767 }
768
769 #[test]
770 fn publishing_replaces_same_size_blob_with_wrong_content() {
771 let directory = tempfile::tempdir().unwrap();
772 let cache = GlobalCache::new(directory.path()).unwrap();
773 let candidate = directory.path().join("candidate.raw");
774 let expected_bytes = b"good";
775 std::fs::write(&candidate, expected_bytes).unwrap();
776 let expected: Digest = format!("sha256:{}", hex::encode(Sha256::digest(expected_bytes)))
777 .parse()
778 .unwrap();
779 let destination = cache.flat_blob_path(&expected);
780 std::fs::write(&destination, b"evil").unwrap();
781
782 cache
783 .publish_flat_blob(&candidate, &expected, expected_bytes.len() as u64)
784 .unwrap();
785
786 assert_eq!(std::fs::read(destination).unwrap(), expected_bytes);
787 }
788}