1use std::collections::{BTreeMap, HashMap, HashSet};
4use std::fs::{File, OpenOptions};
5use std::io::{self, BufWriter, Read, Write};
6use std::path::{Path, PathBuf};
7use std::sync::{
8 Arc,
9 atomic::{AtomicU64, Ordering},
10};
11
12use serde::{Deserialize, Serialize};
13use sha2::{Digest as Sha2Digest, Sha256};
14
15use crate::{
16 CachedImageMetadata, CachedLayerMetadata, Digest, GlobalCache, ImageConfig, ImageError,
17 ImageResult, Platform, Reference, Registry,
18 erofs::{ErofsEntryKind, ErofsReader},
19 path_bytes::{os_str_bytes, os_string_from_vec, path_bytes},
20 tar::Compression,
21};
22
23use super::tar_ext::TarBuilderExt;
24
25const OCI_CONFIG_MEDIA_TYPE: &str = "application/vnd.oci.image.config.v1+json";
30const OCI_MANIFEST_MEDIA_TYPE: &str = "application/vnd.oci.image.manifest.v1+json";
31const OCI_INDEX_MEDIA_TYPE: &str = "application/vnd.oci.image.index.v1+json";
32const OCI_LAYER_MEDIA_TYPE: &str = "application/vnd.oci.image.layer.v1.tar";
33const OCI_LAYER_GZIP_MEDIA_TYPE: &str = "application/vnd.oci.image.layer.v1.tar+gzip";
34const OCI_LAYER_ZSTD_MEDIA_TYPE: &str = "application/vnd.oci.image.layer.v1.tar+zstd";
35const OCI_REF_NAME_ANNOTATION: &str = "org.opencontainers.image.ref.name";
36const ARCHIVE_METADATA_MAX_BYTES: u64 = 16 * 1024 * 1024;
37const ARCHIVE_LAYER_MAX_BYTES: u64 = 10 * 1024 * 1024 * 1024;
38const ARCHIVE_MAX_ENTRY_COUNT: u64 = 1_000_000;
39const OCI_INDEX_MAX_DEPTH: usize = 32;
40const OCI_INDEX_MAX_COUNT: usize = 1_024;
41static TEMP_FILE_COUNTER: AtomicU64 = AtomicU64::new(0);
42
43#[derive(Debug, Clone, Default)]
49pub struct ImageLoadOptions {
50 pub tags: Vec<String>,
52 pub progress: Option<crate::progress::PullProgressSender>,
54}
55
56#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
58pub enum ImageArchiveFormat {
59 #[default]
61 Docker,
62 Oci,
64}
65
66#[derive(Debug, Clone)]
68pub struct LoadedImage {
69 pub reference: String,
71 pub metadata: CachedImageMetadata,
73}
74
75#[derive(Debug, Clone)]
77pub struct ImageSaveRequest {
78 pub reference: String,
80 pub config: ImageSaveConfig,
82 pub raw_config_json: String,
84 pub layers: Vec<ImageSaveLayer>,
86}
87
88#[derive(Debug, Clone, Default)]
90pub struct ImageSaveConfig {
91 pub architecture: Option<String>,
93 pub os: Option<String>,
95 pub env: Vec<String>,
97 pub entrypoint: Option<Vec<String>>,
99 pub cmd: Option<Vec<String>>,
101 pub working_dir: Option<String>,
103 pub user: Option<String>,
105 pub labels: BTreeMap<String, String>,
107}
108
109#[derive(Debug, Clone)]
111pub struct ImageSaveLayer {
112 pub diff_id: String,
114}
115
116#[derive(Debug)]
117struct PreparedLoadedImage {
118 reference: String,
119 metadata: CachedImageMetadata,
120}
121
122#[derive(Debug)]
123struct PreparedArchiveLoad {
124 images: Vec<PreparedLoadedImage>,
125 staged_layers: Arc<StagedLayerGuard>,
126}
127
128#[derive(Debug)]
129struct OciManifestCandidate {
130 descriptor: oci_spec::image::Descriptor,
131 reference: Option<String>,
132 tree_path: Vec<usize>,
137}
138
139#[derive(Debug)]
140pub(crate) struct StagedLayerGuard {
141 paths: HashMap<String, PathBuf>,
142 cleanup_on_drop: bool,
143}
144
145#[derive(Debug)]
146struct LayerBlobInfo {
147 digest: String,
148 media_type: String,
149 size_bytes: u64,
150 path: PathBuf,
151}
152
153#[derive(Debug, Deserialize)]
154struct DockerManifestEntry {
155 #[serde(rename = "Config")]
156 config: String,
157 #[serde(rename = "RepoTags")]
158 repo_tags: Option<Vec<String>>,
159 #[serde(rename = "Layers")]
160 layers: Vec<String>,
161}
162
163#[derive(Debug, Serialize)]
164struct DockerManifestOut {
165 #[serde(rename = "Config")]
166 config: String,
167 #[serde(rename = "RepoTags")]
168 repo_tags: Vec<String>,
169 #[serde(rename = "Layers")]
170 layers: Vec<String>,
171}
172
173#[derive(Debug)]
174struct GeneratedLayer {
175 diff_id: String,
176 hex: String,
177 path: PathBuf,
178 size: u64,
179}
180
181struct DigestingWriter<W> {
182 inner: W,
183 hasher: Sha256,
184 written: u64,
185}
186
187impl<W> DigestingWriter<W> {
192 fn new(inner: W) -> Self {
193 Self {
194 inner,
195 hasher: Sha256::new(),
196 written: 0,
197 }
198 }
199
200 fn finish(self) -> (W, String, u64) {
201 (
202 self.inner,
203 hex::encode(self.hasher.finalize()),
204 self.written,
205 )
206 }
207}
208
209impl ImageSaveRequest {
210 pub fn from_cached(reference: impl Into<String>, metadata: CachedImageMetadata) -> Self {
215 let (architecture, os) = raw_config_platform(&metadata.raw_config_json);
216
217 let layers = metadata
218 .layers
219 .iter()
220 .map(|layer| ImageSaveLayer {
221 diff_id: layer.diff_id.clone(),
222 })
223 .collect();
224
225 let config = metadata.config;
226 Self {
227 reference: reference.into(),
228 config: ImageSaveConfig {
229 architecture,
230 os,
231 env: config.env,
232 entrypoint: config.entrypoint,
233 cmd: config.cmd,
234 working_dir: config.working_dir,
235 user: config.user,
236 labels: config.labels.into_iter().collect(),
237 },
238 raw_config_json: metadata.raw_config_json,
239 layers,
240 }
241 }
242}
243
244impl StagedLayerGuard {
245 fn new() -> Self {
246 Self {
247 paths: HashMap::new(),
248 cleanup_on_drop: true,
249 }
250 }
251
252 fn track(&mut self, digest: String, path: PathBuf) -> PathBuf {
253 if let Some(existing_path) = self.paths.get(&digest) {
254 let _ = std::fs::remove_file(&path);
255 return existing_path.clone();
256 }
257
258 self.paths.insert(digest, path.clone());
259 path
260 }
261}
262
263impl<W: Write> Write for DigestingWriter<W> {
268 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
269 let written = self.inner.write(buf)?;
270 self.hasher.update(&buf[..written]);
271 self.written += written as u64;
272 Ok(written)
273 }
274
275 fn flush(&mut self) -> io::Result<()> {
276 self.inner.flush()
277 }
278}
279
280impl std::ops::Deref for StagedLayerGuard {
281 type Target = HashMap<String, PathBuf>;
282 fn deref(&self) -> &Self::Target {
283 &self.paths
284 }
285}
286
287impl Drop for StagedLayerGuard {
288 fn drop(&mut self) {
289 if !self.cleanup_on_drop {
290 return;
291 }
292
293 for path in self.paths.values() {
294 let _ = std::fs::remove_file(path);
295 }
296 }
297}
298
299fn raw_config_platform(raw_config_json: &str) -> (Option<String>, Option<String>) {
305 let Ok(value) = serde_json::from_str::<serde_json::Value>(raw_config_json) else {
306 return (None, None);
307 };
308
309 let architecture = value
310 .get("architecture")
311 .and_then(serde_json::Value::as_str)
312 .map(ToOwned::to_owned);
313 let os = value
314 .get("os")
315 .and_then(serde_json::Value::as_str)
316 .map(ToOwned::to_owned);
317
318 (architecture, os)
319}
320
321pub async fn load_archive(
323 cache_dir: &Path,
324 input: &Path,
325 options: ImageLoadOptions,
326) -> ImageResult<Vec<LoadedImage>> {
327 let cache = GlobalCache::new_async(cache_dir).await?;
328 load_archive_into(&cache, input, options).await
329}
330
331pub async fn load_archive_into(
333 cache: &GlobalCache,
334 input: &Path,
335 options: ImageLoadOptions,
336) -> ImageResult<Vec<LoadedImage>> {
337 let operation = cache.operation_or_new();
338 load_archive_with(&operation, input, options, |_| async { Ok(()) }).await
339}
340
341pub async fn load_archive_with<F, Fut>(
345 cache: &GlobalCache,
346 input: &Path,
347 options: ImageLoadOptions,
348 mut publish: F,
349) -> ImageResult<Vec<LoadedImage>>
350where
351 F: FnMut(LoadedImage) -> Fut,
352 Fut: std::future::Future<Output = ImageResult<()>>,
353{
354 let cache_dir = cache
355 .layers_dir()
356 .parent()
357 .expect("cache layers have a parent")
358 .to_path_buf();
359 let input = input.to_path_buf();
360 let progress = options.progress.clone();
361 let worker_dir = cache_dir.clone();
362 let worker_input = input.clone();
363 let worker_options = options.clone();
364 let prepared = tokio::task::spawn_blocking(move || {
365 load_archive_blocking(&worker_dir, &worker_input, worker_options)
366 })
367 .await
368 .map_err(std::io::Error::other)??;
369 let PreparedArchiveLoad {
370 images,
371 mut staged_layers,
372 } = prepared;
373 let mut refreshed = HashMap::new();
374 let mut loaded = Vec::with_capacity(images.len());
375 for mut image in images {
376 let operation = cache.operation_or_new();
377 if let Some(metadata) = refreshed.remove(&image.reference) {
378 image.metadata = metadata;
379 }
380 let reference: Reference = image.reference.parse().map_err(|error| {
381 ImageError::ManifestParse(format!("invalid image reference: {error}"))
382 })?;
383 let mut paths = operation.metadata_paths(&image.metadata)?;
384 paths.push(operation.image_metadata_path(&reference));
385 operation.lease_paths_async(paths).await?;
386 let missing = image.metadata.layers.iter().any(|layer| {
389 !staged_layers.contains_key(&layer.digest)
390 && layer
391 .diff_id
392 .parse()
393 .is_ok_and(|id| !operation.is_layer_materialized(&id))
394 });
395 if missing {
396 let directory = cache_dir.clone();
397 let source = input.clone();
398 let retry_options = options.clone();
399 let prepared = tokio::task::spawn_blocking(move || {
400 prepare_archive(&directory, &source, retry_options, false)
401 })
402 .await
403 .map_err(std::io::Error::other)??;
404 staged_layers = prepared.staged_layers;
405 refreshed = prepared
406 .images
407 .into_iter()
408 .map(|image| (image.reference, image.metadata))
409 .collect();
410 image.metadata = refreshed
411 .remove(&image.reference)
412 .ok_or_else(|| ImageError::ManifestParse("archive changed during import".into()))?;
413 operation
414 .lease_paths_async(operation.metadata_paths(&image.metadata)?)
415 .await?;
416 }
417 let registry = Registry::new(Platform::host_linux(), operation.clone())?;
418 registry
419 .materialize_cached_layers_from_paths(
420 &reference,
421 &image.metadata,
422 false,
423 staged_layers.clone(),
424 progress.clone(),
425 )
426 .await?;
427 operation
428 .write_image_metadata_async(&reference, &image.metadata)
429 .await?;
430 let image = LoadedImage {
431 reference: image.reference,
432 metadata: image.metadata,
433 };
434 publish(image.clone()).await?;
435 loaded.push(image);
436 }
437 Ok(loaded)
438}
439
440pub fn save_docker_archive(
442 cache: &GlobalCache,
443 output: &Path,
444 images: &[ImageSaveRequest],
445) -> ImageResult<()> {
446 save_archive(cache, output, images, ImageArchiveFormat::Docker)
447}
448
449pub fn save_archive(
451 cache: &GlobalCache,
452 output: &Path,
453 images: &[ImageSaveRequest],
454 format: ImageArchiveFormat,
455) -> ImageResult<()> {
456 let mut paths = Vec::new();
457 for image in images {
458 paths.push(
459 cache.image_metadata_path(
460 &image
461 .reference
462 .parse::<Reference>()
463 .map_err(|error| ImageError::ManifestParse(error.to_string()))?,
464 ),
465 );
466 for layer in &image.layers {
467 paths.push(cache.layer_erofs_path(&layer.diff_id.parse()?));
468 }
469 }
470 let _leases = cache.lease_paths(paths)?;
471 match format {
472 ImageArchiveFormat::Docker => save_docker_archive_inner(cache, output, images),
473 ImageArchiveFormat::Oci => save_oci_archive_inner(cache, output, images),
474 }
475}
476
477fn save_docker_archive_inner(
478 cache: &GlobalCache,
479 output: &Path,
480 images: &[ImageSaveRequest],
481) -> ImageResult<()> {
482 if images.is_empty() {
483 return Err(ImageError::ManifestParse(
484 "at least one image reference is required".into(),
485 ));
486 }
487
488 let output_file = File::create(output).map_err(|e| ImageError::Cache {
489 path: output.to_path_buf(),
490 source: e,
491 })?;
492 let mut archive = tar::Builder::new(BufWriter::new(output_file));
493 let mut generated_layers: HashMap<String, GeneratedLayer> = HashMap::new();
494 let mut appended_layers: HashSet<String> = HashSet::new();
495 let mut manifest_entries = Vec::with_capacity(images.len());
496 let mut config_entries = Vec::with_capacity(images.len());
497
498 for image in images {
499 let mut layer_paths = Vec::with_capacity(image.layers.len());
500 let mut regenerated_diff_ids = Vec::with_capacity(image.layers.len());
501
502 for layer in &image.layers {
503 let generated = match generated_layers.get(&layer.diff_id) {
504 Some(generated) => generated,
505 None => {
506 let generated = generate_layer_tar(cache, layer)?;
507 generated_layers.insert(layer.diff_id.clone(), generated);
508 generated_layers.get(&layer.diff_id).unwrap()
509 }
510 };
511
512 regenerated_diff_ids.push(generated.diff_id.clone());
513 layer_paths.push(format!("{}/layer.tar", generated.hex));
514 }
515
516 let config_bytes =
517 docker_config_json(&image.config, &image.raw_config_json, ®enerated_diff_ids)?;
518 let config_hex = sha256_hex(&config_bytes);
519 let config_name = format!("{config_hex}.json");
520
521 config_entries.push((config_name.clone(), config_bytes));
522
523 manifest_entries.push(DockerManifestOut {
524 config: config_name,
525 repo_tags: vec![image.reference.clone()],
526 layers: layer_paths,
527 });
528 }
529
530 let manifest_bytes = serde_json::to_vec_pretty(&manifest_entries)
531 .map_err(|e| ImageError::ConfigParse(format!("serialize docker manifest: {e}")))?;
532 append_bytes(&mut archive, "manifest.json", &manifest_bytes)?;
533
534 for (config_name, config_bytes) in config_entries {
535 append_bytes(&mut archive, &config_name, &config_bytes)?;
536 }
537
538 for image in images {
539 for layer in &image.layers {
540 let generated = generated_layers.get(&layer.diff_id).ok_or_else(|| {
541 ImageError::ManifestParse(format!("missing generated layer {}", layer.diff_id))
542 })?;
543 if appended_layers.insert(generated.hex.clone()) {
544 append_layer_entries(&mut archive, generated)?;
545 }
546 }
547 }
548
549 archive.finish().map_err(ImageError::Io)?;
550
551 for layer in generated_layers.values() {
552 let _ = std::fs::remove_file(&layer.path);
553 }
554
555 Ok(())
556}
557
558fn save_oci_archive_inner(
559 cache: &GlobalCache,
560 output: &Path,
561 images: &[ImageSaveRequest],
562) -> ImageResult<()> {
563 if images.is_empty() {
564 return Err(ImageError::ManifestParse(
565 "at least one image reference is required".into(),
566 ));
567 }
568
569 let output_file = File::create(output).map_err(|e| ImageError::Cache {
570 path: output.to_path_buf(),
571 source: e,
572 })?;
573 let mut archive = tar::Builder::new(BufWriter::new(output_file));
574 let mut generated_layers: HashMap<String, GeneratedLayer> = HashMap::new();
575 let mut appended_metadata_blobs: HashSet<String> = HashSet::new();
576 let mut appended_layer_blobs: HashSet<String> = HashSet::new();
577 let mut layer_blob_order = Vec::new();
578 let mut metadata_blobs = Vec::new();
579 let mut index_manifests = Vec::with_capacity(images.len());
580
581 for image in images {
582 let mut layer_descriptors = Vec::with_capacity(image.layers.len());
583 let mut regenerated_diff_ids = Vec::with_capacity(image.layers.len());
584
585 for layer in &image.layers {
586 let generated = match generated_layers.get(&layer.diff_id) {
587 Some(generated) => generated,
588 None => {
589 let generated = generate_layer_tar(cache, layer)?;
590 generated_layers.insert(layer.diff_id.clone(), generated);
591 generated_layers.get(&layer.diff_id).unwrap()
592 }
593 };
594
595 regenerated_diff_ids.push(generated.diff_id.clone());
596 if appended_layer_blobs.insert(generated.hex.clone()) {
597 layer_blob_order.push(layer.diff_id.clone());
598 }
599 layer_descriptors.push(serde_json::json!({
600 "mediaType": OCI_LAYER_MEDIA_TYPE,
601 "digest": generated.diff_id,
602 "size": generated.size,
603 }));
604 }
605
606 let config_bytes =
607 docker_config_json(&image.config, &image.raw_config_json, ®enerated_diff_ids)?;
608 let config_hex = sha256_hex(&config_bytes);
609 if appended_metadata_blobs.insert(config_hex.clone()) {
610 metadata_blobs.push((config_hex.clone(), config_bytes.clone()));
611 }
612
613 let manifest_bytes = serde_json::to_vec(&serde_json::json!({
614 "schemaVersion": 2,
615 "mediaType": OCI_MANIFEST_MEDIA_TYPE,
616 "config": {
617 "mediaType": OCI_CONFIG_MEDIA_TYPE,
618 "digest": format!("sha256:{config_hex}"),
619 "size": config_bytes.len(),
620 },
621 "layers": layer_descriptors,
622 }))
623 .map_err(|e| ImageError::ManifestParse(format!("serialize OCI manifest: {e}")))?;
624 let manifest_hex = sha256_hex(&manifest_bytes);
625 if appended_metadata_blobs.insert(manifest_hex.clone()) {
626 metadata_blobs.push((manifest_hex.clone(), manifest_bytes.clone()));
627 }
628
629 index_manifests.push(serde_json::json!({
630 "mediaType": OCI_MANIFEST_MEDIA_TYPE,
631 "digest": format!("sha256:{manifest_hex}"),
632 "size": manifest_bytes.len(),
633 "platform": {
634 "architecture": image.config.architecture.as_deref().unwrap_or("amd64"),
635 "os": image.config.os.as_deref().unwrap_or("linux"),
636 },
637 "annotations": {
638 (OCI_REF_NAME_ANNOTATION): image.reference.clone(),
639 },
640 }));
641 }
642
643 let index_bytes = serde_json::to_vec_pretty(&serde_json::json!({
644 "schemaVersion": 2,
645 "mediaType": OCI_INDEX_MEDIA_TYPE,
646 "manifests": index_manifests,
647 }))
648 .map_err(|e| ImageError::ManifestParse(format!("serialize OCI index: {e}")))?;
649
650 append_bytes(
651 &mut archive,
652 "oci-layout",
653 br#"{"imageLayoutVersion":"1.0.0"}"#,
654 )?;
655 append_bytes(&mut archive, "index.json", &index_bytes)?;
656 append_directory(&mut archive, "blobs")?;
657 append_directory(&mut archive, "blobs/sha256")?;
658
659 for (hex, bytes) in metadata_blobs {
660 append_blob_bytes(&mut archive, &hex, &bytes)?;
661 }
662
663 for diff_id in layer_blob_order {
664 let generated = generated_layers.get(&diff_id).ok_or_else(|| {
665 ImageError::ManifestParse(format!("missing generated layer {diff_id}"))
666 })?;
667 append_blob_file(
668 &mut archive,
669 &generated.hex,
670 &generated.path,
671 generated.size,
672 )?;
673 }
674
675 archive.finish().map_err(ImageError::Io)?;
676
677 for layer in generated_layers.values() {
678 let _ = std::fs::remove_file(&layer.path);
679 }
680
681 Ok(())
682}
683
684fn load_archive_blocking(
685 cache_dir: &Path,
686 input: &Path,
687 options: ImageLoadOptions,
688) -> ImageResult<PreparedArchiveLoad> {
689 prepare_archive(cache_dir, input, options, true)
690}
691
692fn prepare_archive(
693 cache_dir: &Path,
694 input: &Path,
695 options: ImageLoadOptions,
696 use_cache: bool,
697) -> ImageResult<PreparedArchiveLoad> {
698 if let Some(manifest_json) = read_archive_entry(input, "manifest.json")? {
701 let manifest: Vec<DockerManifestEntry> = serde_json::from_slice(&manifest_json)
702 .map_err(|e| ImageError::ManifestParse(format!("docker manifest.json: {e}")))?;
703 return prepare_docker_archive(cache_dir, input, options, manifest, use_cache);
704 }
705
706 if read_archive_entry(input, "oci-layout")?.is_some() {
707 return prepare_oci_archive(cache_dir, input, options, use_cache);
708 }
709
710 Err(ImageError::ManifestParse(
711 "archive missing manifest.json or oci-layout".into(),
712 ))
713}
714
715fn prepare_docker_archive(
716 cache_dir: &Path,
717 input: &Path,
718 options: ImageLoadOptions,
719 manifest: Vec<DockerManifestEntry>,
720 use_cache: bool,
721) -> ImageResult<PreparedArchiveLoad> {
722 let cache = GlobalCache::new(cache_dir)?;
723 if manifest.is_empty() {
724 return Err(ImageError::ManifestParse(
725 "docker archive manifest is empty".into(),
726 ));
727 }
728
729 let required_configs = manifest
730 .iter()
731 .map(|image| image.config.clone())
732 .collect::<HashSet<_>>();
733 let required_layers = manifest
734 .iter()
735 .flat_map(|image| image.layers.iter().cloned())
736 .collect::<HashSet<_>>();
737
738 'early_gate: {
743 if !use_cache {
744 break 'early_gate;
745 }
746 let config_blobs = read_archive_entries(input, &required_configs)?;
747 let mut early_images = Vec::new();
748 for (image_index, image) in manifest.iter().enumerate() {
749 let Some(config_bytes) = config_blobs.get(&image.config) else {
750 break 'early_gate;
751 };
752 let (_, diff_ids) = ImageConfig::parse(config_bytes)?;
753 if diff_ids.len() != image.layers.len() {
754 break 'early_gate;
755 }
756 let config_digest = format!("sha256:{}", sha256_hex(config_bytes));
757
758 let mut refs = image
759 .repo_tags
760 .clone()
761 .unwrap_or_default()
762 .into_iter()
763 .filter(|tag| tag != "<none>:<none>")
764 .collect::<Vec<_>>();
765 if image_index == 0 {
766 refs.extend(options.tags.iter().cloned());
767 }
768 refs.sort();
769 refs.dedup();
770 if refs.is_empty() {
771 break 'early_gate;
772 }
773
774 let mut cached = None;
776 for reference in &refs {
777 let Ok(parsed) = reference.parse::<Reference>() else {
778 break 'early_gate;
779 };
780 if let Some(metadata) = cache.read_image_metadata(&parsed)? {
781 let cached_diff_ids = metadata
782 .layers
783 .iter()
784 .map(|layer| layer.diff_id.clone())
785 .collect::<Vec<_>>();
786 if metadata.config_digest == config_digest && cached_diff_ids == diff_ids {
787 cached = Some(metadata);
788 break;
789 }
790 }
791 }
792 let Some(metadata) = cached else {
793 break 'early_gate;
794 };
795
796 let Ok(manifest_digest) = metadata.manifest_digest.parse::<crate::Digest>() else {
797 break 'early_gate;
798 };
799 if !crate::cache::is_valid_erofs_artifact(&cache.fsmeta_erofs_path(&manifest_digest))
800 || !cache.vmdk_path(&manifest_digest).exists()
801 {
802 break 'early_gate;
803 }
804 let mut layers_present = true;
805 for diff_id_str in &diff_ids {
806 let Ok(diff_id) = diff_id_str.parse::<crate::Digest>() else {
807 layers_present = false;
808 break;
809 };
810 if !crate::cache::is_valid_erofs_artifact(&cache.layer_erofs_path(&diff_id)) {
811 layers_present = false;
812 break;
813 }
814 }
815 if !layers_present {
816 break 'early_gate;
817 }
818
819 for reference in refs {
820 early_images.push(PreparedLoadedImage {
821 reference,
822 metadata: metadata.clone(),
823 });
824 }
825 }
826 if !archive_contains_entries(input, &required_layers)? {
827 break 'early_gate;
828 }
829 return Ok(PreparedArchiveLoad {
830 images: early_images,
831 staged_layers: Arc::new(StagedLayerGuard::new()),
832 });
833 }
834
835 let file = File::open(input).map_err(|e| ImageError::Cache {
836 path: input.to_path_buf(),
837 source: e,
838 })?;
839 let mut archive = tar::Archive::new(file);
840 let mut configs: HashMap<String, Vec<u8>> = HashMap::new();
841 let mut layers: HashMap<String, LayerBlobInfo> = HashMap::new();
842 let mut staged_layers = StagedLayerGuard::new();
843 let mut temp_counter = 0u64;
844 let mut entry_count = 0u64;
845
846 for entry in archive.entries().map_err(ImageError::Io)? {
847 let mut entry = entry.map_err(ImageError::Io)?;
848 entry_count += 1;
849 enforce_archive_entry_count(entry_count)?;
850 let path = normalized_archive_path(&entry)?;
851
852 if required_configs.contains(&path) {
853 let data = read_entry_to_vec(&mut entry, &path, ARCHIVE_METADATA_MAX_BYTES)?;
854 configs.insert(path, data);
855 continue;
856 }
857
858 if required_layers.contains(&path) {
859 let mut info = extract_layer_blob(&cache, &path, &mut entry, temp_counter)?;
860 temp_counter += 1;
861 info.path = staged_layers.track(info.digest.clone(), info.path);
862 verify_docker_layer_path_digest(&path, &info.digest)?;
863 layers.insert(path, info);
864 continue;
865 }
866 }
867
868 let mut loaded = Vec::new();
869 for (image_index, image) in manifest.into_iter().enumerate() {
870 let config_bytes = configs.get(&image.config).ok_or_else(|| {
871 ImageError::ConfigParse(format!("docker archive missing config {}", image.config))
872 })?;
873 let (config, diff_ids) = ImageConfig::parse(config_bytes)?;
874
875 if diff_ids.len() != image.layers.len() {
876 return Err(ImageError::ManifestParse(format!(
877 "layer count mismatch: config has {} diff_ids but archive manifest has {} layers",
878 diff_ids.len(),
879 image.layers.len()
880 )));
881 }
882
883 let config_digest = format!("sha256:{}", sha256_hex(config_bytes));
884 let mut layer_metadata = Vec::with_capacity(image.layers.len());
885 let mut manifest_layers = Vec::with_capacity(image.layers.len());
886
887 for (position, layer_path) in image.layers.iter().enumerate() {
888 let layer = layers.get(layer_path).ok_or_else(|| {
889 ImageError::ManifestParse(format!("docker archive missing layer {layer_path}"))
890 })?;
891 let diff_id = diff_ids[position].clone();
892 layer_metadata.push(CachedLayerMetadata {
893 digest: layer.digest.clone(),
894 media_type: Some(layer.media_type.clone()),
895 size_bytes: Some(layer.size_bytes),
896 diff_id,
897 });
898 manifest_layers.push(serde_json::json!({
899 "mediaType": layer.media_type,
900 "digest": layer.digest,
901 "size": layer.size_bytes,
902 }));
903 }
904
905 let manifest_bytes = serde_json::to_vec(&serde_json::json!({
906 "schemaVersion": 2,
907 "mediaType": OCI_MANIFEST_MEDIA_TYPE,
908 "config": {
909 "mediaType": OCI_CONFIG_MEDIA_TYPE,
910 "digest": config_digest,
911 "size": config_bytes.len(),
912 },
913 "layers": manifest_layers,
914 }))
915 .map_err(|e| ImageError::ManifestParse(format!("serialize manifest: {e}")))?;
916 let manifest_digest = format!("sha256:{}", sha256_hex(&manifest_bytes));
917
918 let metadata = CachedImageMetadata {
919 manifest_digest,
920 config_digest,
921 raw_manifest_json: json_bytes_to_string(&manifest_bytes, "docker manifest")?,
922 raw_config_json: json_bytes_to_string(config_bytes, "docker config")?,
923 config,
924 layers: layer_metadata,
925 };
926
927 let mut refs = image
928 .repo_tags
929 .unwrap_or_default()
930 .into_iter()
931 .filter(|tag| tag != "<none>:<none>")
932 .collect::<Vec<_>>();
933
934 if image_index == 0 {
935 refs.extend(options.tags.iter().cloned());
936 }
937
938 refs.sort();
939 refs.dedup();
940
941 if refs.is_empty() {
942 return Err(ImageError::ManifestParse(
943 "docker archive image has no tags; pass --tag to name it".into(),
944 ));
945 }
946
947 for reference in refs {
948 let _: Reference = reference.parse().map_err(|e| {
949 ImageError::ManifestParse(format!("invalid image reference {reference}: {e}"))
950 })?;
951 loaded.push(PreparedLoadedImage {
952 reference,
953 metadata: metadata.clone(),
954 });
955 }
956 }
957
958 Ok(PreparedArchiveLoad {
959 images: loaded,
960 staged_layers: Arc::new(staged_layers),
961 })
962}
963
964#[cfg(test)]
965fn load_oci_archive_blocking(
966 cache_dir: &Path,
967 input: &Path,
968 options: ImageLoadOptions,
969) -> ImageResult<PreparedArchiveLoad> {
970 prepare_oci_archive(cache_dir, input, options, true)
971}
972
973fn prepare_oci_archive(
974 cache_dir: &Path,
975 input: &Path,
976 options: ImageLoadOptions,
977 use_cache: bool,
978) -> ImageResult<PreparedArchiveLoad> {
979 let cache = GlobalCache::new(cache_dir)?;
980 let layout_json = read_archive_entry(input, "oci-layout")?
981 .ok_or_else(|| ImageError::ManifestParse("OCI layout missing oci-layout".into()))?;
982 serde_json::from_slice::<oci_spec::image::OciLayout>(&layout_json)
983 .map_err(|e| ImageError::ManifestParse(format!("oci-layout: {e}")))?;
984
985 let index_json = read_archive_entry(input, "index.json")?
986 .ok_or_else(|| ImageError::ManifestParse("OCI layout missing index.json".into()))?;
987 let index: oci_spec::image::ImageIndex = serde_json::from_slice(&index_json)
988 .map_err(|e| ImageError::ManifestParse(format!("OCI index.json: {e}")))?;
989 let manifest_candidates = resolve_oci_manifest_candidates(input, index.manifests())?;
990 if manifest_candidates.is_empty() {
991 return Err(ImageError::ManifestParse(
992 "OCI layout contains no image manifests for the host platform".into(),
993 ));
994 }
995
996 let manifest_paths = manifest_candidates
997 .iter()
998 .map(|candidate| blob_path_from_digest(candidate.descriptor.digest().as_ref()))
999 .collect::<ImageResult<HashSet<_>>>()?;
1000 let manifest_blobs = read_archive_entries(input, &manifest_paths)?;
1001 let mut manifests = Vec::with_capacity(manifest_candidates.len());
1002 let mut required_configs = HashSet::new();
1003 let mut required_layers = HashSet::new();
1004
1005 for candidate in manifest_candidates {
1006 let descriptor = &candidate.descriptor;
1007 let manifest_path = blob_path_from_digest(descriptor.digest().as_ref())?;
1008 let manifest_bytes = manifest_blobs.get(&manifest_path).ok_or_else(|| {
1009 ImageError::ManifestParse(format!("OCI layout missing manifest blob {manifest_path}"))
1010 })?;
1011 verify_descriptor_blob(descriptor, manifest_bytes)?;
1012 let manifest: oci_spec::image::ImageManifest = serde_json::from_slice(manifest_bytes)
1013 .map_err(|e| ImageError::ManifestParse(format!("OCI image manifest: {e}")))?;
1014
1015 required_configs.insert(blob_path_from_digest(manifest.config().digest().as_ref())?);
1016 for layer in manifest.layers() {
1017 required_layers.insert(blob_path_from_digest(layer.digest().as_ref())?);
1018 }
1019 manifests.push((candidate, manifest, manifest_bytes.clone()));
1020 }
1021
1022 'early_gate: {
1032 if !use_cache {
1033 break 'early_gate;
1034 }
1035 let config_blobs = read_archive_entries(input, &required_configs)?;
1036 let mut early_images = Vec::new();
1037 for (image_index, (candidate, manifest, manifest_bytes)) in manifests.iter().enumerate() {
1038 let config_path = blob_path_from_digest(manifest.config().digest().as_ref())?;
1039 let Some(config_bytes) = config_blobs.get(&config_path) else {
1040 break 'early_gate;
1041 };
1042 verify_descriptor_blob(manifest.config(), config_bytes)?;
1043 let (config, diff_ids) = ImageConfig::parse(config_bytes)?;
1044 if diff_ids.len() != manifest.layers().len() {
1045 break 'early_gate;
1046 }
1047 let manifest_digest_str = format!("sha256:{}", sha256_hex(manifest_bytes));
1048 let Ok(manifest_digest) = manifest_digest_str.parse::<crate::Digest>() else {
1049 break 'early_gate;
1050 };
1051 if !crate::cache::is_valid_erofs_artifact(&cache.fsmeta_erofs_path(&manifest_digest))
1052 || !cache.vmdk_path(&manifest_digest).exists()
1053 {
1054 break 'early_gate;
1055 }
1056
1057 let mut layer_metadata = Vec::with_capacity(manifest.layers().len());
1058 for (position, layer_descriptor) in manifest.layers().iter().enumerate() {
1059 let diff_id_str = diff_ids[position].clone();
1060 let Ok(diff_id) = diff_id_str.parse::<crate::Digest>() else {
1061 break 'early_gate;
1062 };
1063 if !crate::cache::is_valid_erofs_artifact(&cache.layer_erofs_path(&diff_id)) {
1064 break 'early_gate;
1065 }
1066 layer_metadata.push(CachedLayerMetadata {
1067 digest: layer_descriptor.digest().to_string(),
1068 media_type: Some(layer_descriptor.media_type().to_string()),
1069 size_bytes: Some(layer_descriptor.size()),
1070 diff_id: diff_id_str,
1071 });
1072 }
1073
1074 let metadata = CachedImageMetadata {
1075 manifest_digest: manifest_digest_str,
1076 config_digest: manifest.config().digest().to_string(),
1077 raw_manifest_json: json_bytes_to_string(manifest_bytes, "OCI manifest")?,
1078 raw_config_json: json_bytes_to_string(config_bytes, "OCI config")?,
1079 config,
1080 layers: layer_metadata,
1081 };
1082
1083 let mut refs = candidate.reference.clone().into_iter().collect::<Vec<_>>();
1084 if image_index == 0 {
1085 refs.extend(options.tags.iter().cloned());
1086 }
1087 refs.sort();
1088 refs.dedup();
1089 if refs.is_empty() {
1090 break 'early_gate;
1091 }
1092 for reference in refs {
1093 if reference.parse::<Reference>().is_err() {
1094 break 'early_gate;
1095 }
1096 early_images.push(PreparedLoadedImage {
1097 reference,
1098 metadata: metadata.clone(),
1099 });
1100 }
1101 }
1102 if !archive_contains_entries(input, &required_layers)? {
1103 break 'early_gate;
1104 }
1105 return Ok(PreparedArchiveLoad {
1106 images: early_images,
1107 staged_layers: Arc::new(StagedLayerGuard::new()),
1108 });
1109 }
1110
1111 let file = File::open(input).map_err(|e| ImageError::Cache {
1114 path: input.to_path_buf(),
1115 source: e,
1116 })?;
1117 let mut archive = tar::Archive::new(file);
1118 let mut configs: HashMap<String, Vec<u8>> = HashMap::new();
1119 let mut layers: HashMap<String, LayerBlobInfo> = HashMap::new();
1120 let mut staged_layers = StagedLayerGuard::new();
1121 let mut temp_counter = 0u64;
1122 let mut entry_count = 0u64;
1123
1124 for entry in archive.entries().map_err(ImageError::Io)? {
1125 let mut entry = entry.map_err(ImageError::Io)?;
1126 entry_count += 1;
1127 enforce_archive_entry_count(entry_count)?;
1128 let path = normalized_archive_path(&entry)?;
1129
1130 if required_configs.contains(&path) {
1131 let data = read_entry_to_vec(&mut entry, &path, ARCHIVE_METADATA_MAX_BYTES)?;
1132 configs.insert(path, data);
1133 continue;
1134 }
1135
1136 if required_layers.contains(&path) {
1137 let mut info = extract_layer_blob(&cache, &path, &mut entry, temp_counter)?;
1138 temp_counter += 1;
1139 info.path = staged_layers.track(info.digest.clone(), info.path);
1140 layers.insert(path, info);
1141 continue;
1142 }
1143 }
1144
1145 let mut loaded = Vec::new();
1146 for (image_index, (candidate, manifest, manifest_bytes)) in manifests.into_iter().enumerate() {
1147 let config_path = blob_path_from_digest(manifest.config().digest().as_ref())?;
1148 let config_bytes = configs.get(&config_path).ok_or_else(|| {
1149 ImageError::ConfigParse(format!("OCI layout missing config blob {config_path}"))
1150 })?;
1151 verify_descriptor_blob(manifest.config(), config_bytes)?;
1152 let (config, diff_ids) = ImageConfig::parse(config_bytes)?;
1153
1154 if diff_ids.len() != manifest.layers().len() {
1155 return Err(ImageError::ManifestParse(format!(
1156 "layer count mismatch: config has {} diff_ids but OCI manifest has {} layers",
1157 diff_ids.len(),
1158 manifest.layers().len()
1159 )));
1160 }
1161
1162 let mut layer_metadata = Vec::with_capacity(manifest.layers().len());
1163 for (position, layer_descriptor) in manifest.layers().iter().enumerate() {
1164 let layer_path = blob_path_from_digest(layer_descriptor.digest().as_ref())?;
1165 let layer = layers.get(&layer_path).ok_or_else(|| {
1166 ImageError::ManifestParse(format!("OCI layout missing layer blob {layer_path}"))
1167 })?;
1168 verify_layer_descriptor(layer_descriptor, layer)?;
1169 layer_metadata.push(CachedLayerMetadata {
1170 digest: layer.digest.clone(),
1171 media_type: Some(layer.media_type.clone()),
1172 size_bytes: Some(layer.size_bytes),
1173 diff_id: diff_ids[position].clone(),
1174 });
1175 }
1176
1177 let metadata = CachedImageMetadata {
1178 manifest_digest: format!("sha256:{}", sha256_hex(&manifest_bytes)),
1179 config_digest: manifest.config().digest().to_string(),
1180 raw_manifest_json: json_bytes_to_string(&manifest_bytes, "OCI manifest")?,
1181 raw_config_json: json_bytes_to_string(config_bytes, "OCI config")?,
1182 config,
1183 layers: layer_metadata,
1184 };
1185
1186 let mut refs = candidate.reference.into_iter().collect::<Vec<_>>();
1187
1188 if image_index == 0 {
1189 refs.extend(options.tags.iter().cloned());
1190 }
1191
1192 refs.sort();
1193 refs.dedup();
1194
1195 if refs.is_empty() {
1196 return Err(ImageError::ManifestParse(
1197 "OCI layout image has no ref.name annotation; pass --tag to name it".into(),
1198 ));
1199 }
1200
1201 for reference in refs {
1202 let _: Reference = reference.parse().map_err(|e| {
1203 ImageError::ManifestParse(format!("invalid image reference {reference}: {e}"))
1204 })?;
1205 loaded.push(PreparedLoadedImage {
1206 reference,
1207 metadata: metadata.clone(),
1208 });
1209 }
1210 }
1211
1212 Ok(PreparedArchiveLoad {
1213 images: loaded,
1214 staged_layers: Arc::new(staged_layers),
1215 })
1216}
1217
1218fn read_archive_entry(input: &Path, wanted_path: &str) -> ImageResult<Option<Vec<u8>>> {
1219 let file = File::open(input).map_err(|e| ImageError::Cache {
1220 path: input.to_path_buf(),
1221 source: e,
1222 })?;
1223 let mut archive = tar::Archive::new(file);
1224 let mut entry_count = 0u64;
1225
1226 for entry in archive.entries_with_seek().map_err(ImageError::Io)? {
1229 let mut entry = entry.map_err(ImageError::Io)?;
1230 entry_count += 1;
1231 enforce_archive_entry_count(entry_count)?;
1232 let path = normalized_archive_path(&entry)?;
1233 if path != wanted_path {
1234 continue;
1235 }
1236
1237 let data = read_entry_to_vec(&mut entry, &path, ARCHIVE_METADATA_MAX_BYTES)?;
1238 return Ok(Some(data));
1239 }
1240
1241 Ok(None)
1242}
1243
1244fn read_archive_entries(
1245 input: &Path,
1246 wanted_paths: &HashSet<String>,
1247) -> ImageResult<HashMap<String, Vec<u8>>> {
1248 let file = File::open(input).map_err(|e| ImageError::Cache {
1249 path: input.to_path_buf(),
1250 source: e,
1251 })?;
1252 let mut archive = tar::Archive::new(file);
1253 let mut entries = HashMap::new();
1254 let mut entry_count = 0u64;
1255
1256 for entry in archive.entries_with_seek().map_err(ImageError::Io)? {
1258 let mut entry = entry.map_err(ImageError::Io)?;
1259 entry_count += 1;
1260 enforce_archive_entry_count(entry_count)?;
1261 let path = normalized_archive_path(&entry)?;
1262 if !wanted_paths.contains(&path) {
1263 continue;
1264 }
1265
1266 let data = read_entry_to_vec(&mut entry, &path, ARCHIVE_METADATA_MAX_BYTES)?;
1267 entries.insert(path, data);
1268 if entries.len() == wanted_paths.len() {
1269 break;
1270 }
1271 }
1272
1273 Ok(entries)
1274}
1275
1276fn archive_contains_entries(input: &Path, wanted_paths: &HashSet<String>) -> ImageResult<bool> {
1277 if wanted_paths.is_empty() {
1278 return Ok(true);
1279 }
1280
1281 let file = File::open(input).map_err(|e| ImageError::Cache {
1282 path: input.to_path_buf(),
1283 source: e,
1284 })?;
1285 let mut archive = tar::Archive::new(file);
1286 let mut entries = HashSet::new();
1287 let mut entry_count = 0u64;
1288
1289 for entry in archive.entries_with_seek().map_err(ImageError::Io)? {
1293 let entry = entry.map_err(ImageError::Io)?;
1294 entry_count += 1;
1295 enforce_archive_entry_count(entry_count)?;
1296 let path = normalized_archive_path(&entry)?;
1297 if wanted_paths.contains(&path) {
1298 entries.insert(path);
1299 if entries.len() == wanted_paths.len() {
1300 return Ok(true);
1301 }
1302 }
1303 }
1304
1305 Ok(false)
1306}
1307
1308fn resolve_oci_manifest_candidates(
1309 input: &Path,
1310 descriptors: &[oci_spec::image::Descriptor],
1311) -> ImageResult<Vec<OciManifestCandidate>> {
1312 let host = Platform::host_linux();
1313 let mut pending = descriptors
1314 .iter()
1315 .cloned()
1316 .enumerate()
1317 .map(|(index, descriptor)| OciManifestCandidate {
1318 reference: descriptor
1319 .annotations()
1320 .as_ref()
1321 .and_then(|annotations| annotations.get(OCI_REF_NAME_ANNOTATION))
1322 .cloned(),
1323 descriptor,
1324 tree_path: vec![index],
1325 })
1326 .collect::<Vec<_>>();
1327
1328 let mut depth = 0usize;
1331 let mut index_count = 0usize;
1332 let mut resolved = Vec::new();
1333
1334 while !pending.is_empty() {
1335 let mut indexes = Vec::new();
1339 for candidate in pending {
1340 if !descriptor_matches_platform(&candidate.descriptor, &host) {
1341 continue;
1342 }
1343
1344 if is_oci_image_manifest_descriptor(&candidate.descriptor) {
1345 resolved.push(candidate);
1346 } else if is_oci_image_index_descriptor(&candidate.descriptor) {
1347 indexes.push(candidate);
1348 }
1349 }
1350
1351 if indexes.is_empty() {
1352 break;
1353 }
1354
1355 if depth >= OCI_INDEX_MAX_DEPTH {
1357 return Err(ImageError::ManifestParse(format!(
1358 "OCI image index nesting exceeds {OCI_INDEX_MAX_DEPTH} levels"
1359 )));
1360 }
1361
1362 index_count = index_count.saturating_add(indexes.len());
1363 if index_count > OCI_INDEX_MAX_COUNT {
1364 return Err(ImageError::ManifestParse(format!(
1365 "OCI layout contains more than {OCI_INDEX_MAX_COUNT} nested image indexes"
1366 )));
1367 }
1368
1369 let index_paths = indexes
1371 .iter()
1372 .map(|candidate| blob_path_from_digest(candidate.descriptor.digest().as_ref()))
1373 .collect::<ImageResult<HashSet<_>>>()?;
1374 let index_blobs = read_archive_entries(input, &index_paths)?;
1375 let mut next = Vec::new();
1376
1377 for candidate in indexes {
1378 let index_path = blob_path_from_digest(candidate.descriptor.digest().as_ref())?;
1379 let index_bytes = index_blobs.get(&index_path).ok_or_else(|| {
1380 ImageError::ManifestParse(format!(
1381 "OCI layout missing image index blob {index_path}"
1382 ))
1383 })?;
1384 verify_descriptor_blob(&candidate.descriptor, index_bytes)?;
1385 let index: oci_spec::image::ImageIndex = serde_json::from_slice(index_bytes)
1386 .map_err(|e| ImageError::ManifestParse(format!("OCI image index: {e}")))?;
1387
1388 for (child_index, descriptor) in index.manifests().iter().cloned().enumerate() {
1389 let mut tree_path = candidate.tree_path.clone();
1390 tree_path.push(child_index);
1391 next.push(OciManifestCandidate {
1392 descriptor,
1393 reference: candidate.reference.clone(),
1394 tree_path,
1395 });
1396 }
1397 }
1398
1399 pending = next;
1400 depth += 1;
1401 }
1402
1403 resolved.sort_by(|left, right| left.tree_path.cmp(&right.tree_path));
1404
1405 Ok(resolved)
1406}
1407
1408fn is_oci_image_manifest_descriptor(descriptor: &oci_spec::image::Descriptor) -> bool {
1409 matches!(
1410 descriptor.media_type(),
1411 oci_spec::image::MediaType::ImageManifest
1412 ) || descriptor.media_type().to_string()
1413 == "application/vnd.docker.distribution.manifest.v2+json"
1414}
1415
1416fn is_oci_image_index_descriptor(descriptor: &oci_spec::image::Descriptor) -> bool {
1417 matches!(
1418 descriptor.media_type(),
1419 oci_spec::image::MediaType::ImageIndex
1420 ) || descriptor.media_type().to_string()
1421 == "application/vnd.docker.distribution.manifest.list.v2+json"
1422}
1423
1424fn descriptor_matches_platform(descriptor: &oci_spec::image::Descriptor, host: &Platform) -> bool {
1425 let Some(platform) = descriptor.platform() else {
1426 return true;
1427 };
1428
1429 if *platform.os() != host.os || *platform.architecture() != host.arch {
1430 return false;
1431 }
1432
1433 match (&host.variant, platform.variant()) {
1434 (Some(host_variant), Some(descriptor_variant)) => host_variant == descriptor_variant,
1435 (Some(_), None) => false,
1436 (None, _) => true,
1437 }
1438}
1439
1440fn blob_path_from_digest(digest: &str) -> ImageResult<String> {
1441 let digest: Digest = digest.parse()?;
1442 Ok(format!("blobs/{}/{}", digest.algorithm(), digest.hex()))
1443}
1444
1445fn verify_descriptor_blob(
1446 descriptor: &oci_spec::image::Descriptor,
1447 bytes: &[u8],
1448) -> ImageResult<()> {
1449 if descriptor.size() != bytes.len() as u64 {
1450 return Err(ImageError::ManifestParse(format!(
1451 "OCI blob {} size mismatch: descriptor has {}, archive has {}",
1452 descriptor.digest(),
1453 descriptor.size(),
1454 bytes.len()
1455 )));
1456 }
1457
1458 verify_digest_bytes(descriptor.digest().as_ref(), bytes)
1459}
1460
1461fn verify_layer_descriptor(
1462 descriptor: &oci_spec::image::Descriptor,
1463 layer: &LayerBlobInfo,
1464) -> ImageResult<()> {
1465 if descriptor.size() != layer.size_bytes {
1466 return Err(ImageError::ManifestParse(format!(
1467 "OCI layer {} size mismatch: descriptor has {}, archive has {}",
1468 descriptor.digest(),
1469 descriptor.size(),
1470 layer.size_bytes
1471 )));
1472 }
1473
1474 if descriptor.digest().to_string() != layer.digest {
1475 return Err(ImageError::ManifestParse(format!(
1476 "OCI layer digest mismatch: descriptor has {}, archive has {}",
1477 descriptor.digest(),
1478 layer.digest
1479 )));
1480 }
1481
1482 Ok(())
1483}
1484
1485fn verify_digest_bytes(digest: &str, bytes: &[u8]) -> ImageResult<()> {
1486 let digest: Digest = digest.parse()?;
1487 if digest.algorithm() != "sha256" {
1488 return Err(ImageError::ManifestParse(format!(
1489 "unsupported OCI digest algorithm: {}",
1490 digest.algorithm()
1491 )));
1492 }
1493
1494 let actual = sha256_hex(bytes);
1495 if actual != digest.hex() {
1496 return Err(ImageError::ManifestParse(format!(
1497 "OCI blob digest mismatch: expected {}, got sha256:{actual}",
1498 digest
1499 )));
1500 }
1501
1502 Ok(())
1503}
1504
1505fn verify_docker_layer_path_digest(path: &str, digest: &str) -> ImageResult<()> {
1506 let Some(hex) = path.strip_prefix("blobs/sha256/") else {
1507 return Ok(());
1508 };
1509 if hex.contains('/') {
1510 return Ok(());
1511 }
1512
1513 let expected = format!("sha256:{hex}");
1514 if expected != digest {
1515 return Err(ImageError::ManifestParse(format!(
1516 "docker archive layer path {path} digest mismatch: expected {expected}, got {digest}"
1517 )));
1518 }
1519
1520 Ok(())
1521}
1522
1523fn create_unique_temp_file(dir: &Path, prefix: &str, suffix: &str) -> ImageResult<(File, PathBuf)> {
1524 for _ in 0..128 {
1525 let id = TEMP_FILE_COUNTER.fetch_add(1, Ordering::Relaxed);
1526 let path = dir.join(format!("{prefix}-{}-{id}{suffix}", std::process::id()));
1527 match OpenOptions::new().write(true).create_new(true).open(&path) {
1528 Ok(file) => return Ok((file, path)),
1529 Err(e) if e.kind() == io::ErrorKind::AlreadyExists => continue,
1530 Err(e) => {
1531 return Err(ImageError::Cache { path, source: e });
1532 }
1533 }
1534 }
1535
1536 Err(ImageError::Cache {
1537 path: dir.to_path_buf(),
1538 source: io::Error::new(
1539 io::ErrorKind::AlreadyExists,
1540 "could not allocate a unique temporary image archive file",
1541 ),
1542 })
1543}
1544
1545fn extract_layer_blob(
1546 cache: &GlobalCache,
1547 path: &str,
1548 entry: &mut tar::Entry<'_, File>,
1549 counter: u64,
1550) -> ImageResult<LayerBlobInfo> {
1551 let declared_size = entry.header().size().map_err(ImageError::Io)?;
1552 if declared_size > ARCHIVE_LAYER_MAX_BYTES {
1553 return Err(ImageError::ManifestParse(format!(
1554 "archive layer {path} is {declared_size} bytes; max is {ARCHIVE_LAYER_MAX_BYTES}"
1555 )));
1556 }
1557
1558 let (mut temp, temp_path) =
1559 create_unique_temp_file(cache.tmp_dir(), &format!("load-{counter}"), ".blob")?;
1560 let result = (|| {
1561 let mut hasher = Sha256::new();
1562 let mut size = 0u64;
1563 let mut magic = Vec::with_capacity(4);
1564 let mut buf = [0u8; 64 * 1024];
1565
1566 loop {
1567 let read = entry.read(&mut buf).map_err(ImageError::Io)?;
1568 if read == 0 {
1569 break;
1570 }
1571 if magic.len() < 4 {
1572 let take = (4 - magic.len()).min(read);
1573 magic.extend_from_slice(&buf[..take]);
1574 }
1575 hasher.update(&buf[..read]);
1576 temp.write_all(&buf[..read])
1577 .map_err(|e| ImageError::Cache {
1578 path: temp_path.clone(),
1579 source: e,
1580 })?;
1581 size += read as u64;
1582 if size > ARCHIVE_LAYER_MAX_BYTES {
1583 return Err(ImageError::ManifestParse(format!(
1584 "archive layer {path} exceeds {ARCHIVE_LAYER_MAX_BYTES} bytes"
1585 )));
1586 }
1587 }
1588 temp.flush().map_err(|e| ImageError::Cache {
1589 path: temp_path.clone(),
1590 source: e,
1591 })?;
1592 drop(temp);
1593
1594 let digest = Digest::new("sha256", hex::encode(hasher.finalize()));
1595 let staged_path = temp_path.clone();
1596
1597 let media_type = match Compression::detect(&magic) {
1598 Compression::None => OCI_LAYER_MEDIA_TYPE,
1599 Compression::Gzip => OCI_LAYER_GZIP_MEDIA_TYPE,
1600 Compression::Zstd => OCI_LAYER_ZSTD_MEDIA_TYPE,
1601 };
1602
1603 tracing::debug!(path, digest = %digest, size, "loaded layer blob from docker archive");
1604
1605 Ok(LayerBlobInfo {
1606 digest: digest.to_string(),
1607 media_type: media_type.to_string(),
1608 size_bytes: size,
1609 path: staged_path,
1610 })
1611 })();
1612
1613 if result.is_err() {
1614 let _ = std::fs::remove_file(&temp_path);
1615 }
1616
1617 result
1618}
1619
1620fn generate_layer_tar(cache: &GlobalCache, layer: &ImageSaveLayer) -> ImageResult<GeneratedLayer> {
1621 let diff_id: Digest = layer.diff_id.parse()?;
1622 let erofs_path = cache.layer_erofs_path(&diff_id);
1623 let file = File::open(&erofs_path).map_err(|e| ImageError::Cache {
1624 path: erofs_path.clone(),
1625 source: e,
1626 })?;
1627 let mut reader = ErofsReader::new(file).map_err(ImageError::Io)?;
1628 let (temp_file, temp_path) = create_unique_temp_file(cache.tmp_dir(), "save", ".layer.tar")?;
1629 let result = (|| {
1630 let digesting = DigestingWriter::new(BufWriter::new(temp_file));
1631 let mut builder = tar::Builder::new(digesting);
1632 let mut hardlinks: HashMap<u32, PathBuf> = HashMap::new();
1633
1634 reader.walk_entries::<ImageError, _>(|reader, entry| {
1635 if entry.path.as_os_str().is_empty() {
1636 return Ok(());
1637 }
1638
1639 if entry.kind == ErofsEntryKind::CharDevice && entry.rdev == Some((0, 0)) {
1640 append_whiteout(&mut builder, &entry)?;
1641 return Ok(());
1642 }
1643
1644 append_erofs_entry(&mut builder, reader, &entry, &mut hardlinks)?;
1645
1646 if entry.kind == ErofsEntryKind::Directory && entry.is_opaque() {
1647 append_opaque_marker(&mut builder, &entry)?;
1648 }
1649 Ok(())
1650 })?;
1651
1652 let digesting = builder.into_inner().map_err(ImageError::Io)?;
1653 let (mut file, hex, size) = digesting.finish();
1654 file.flush().map_err(|e| ImageError::Cache {
1655 path: temp_path.clone(),
1656 source: e,
1657 })?;
1658
1659 Ok(GeneratedLayer {
1660 diff_id: format!("sha256:{hex}"),
1661 hex,
1662 path: temp_path.clone(),
1663 size,
1664 })
1665 })();
1666
1667 if result.is_err() {
1668 let _ = std::fs::remove_file(&temp_path);
1669 }
1670
1671 result
1672}
1673
1674fn append_erofs_entry<W: Write>(
1675 builder: &mut tar::Builder<DigestingWriter<W>>,
1676 reader: &mut ErofsReader,
1677 entry: &crate::erofs::ErofsTreeEntry,
1678 hardlinks: &mut HashMap<u32, PathBuf>,
1679) -> ImageResult<()> {
1680 let mut header = tar::Header::new_gnu();
1681 apply_header_metadata(&mut header, entry);
1682
1683 match entry.kind {
1684 ErofsEntryKind::RegularFile => {
1685 if let Some(first_path) = hardlinks.get(&entry.nid) {
1686 header.set_entry_type(tar::EntryType::Link);
1687 header.set_size(0);
1688 builder
1689 .append_link(&mut header, &entry.path, first_path)
1690 .map_err(ImageError::Io)?;
1691 return Ok(());
1692 }
1693
1694 hardlinks.insert(entry.nid, entry.path.clone());
1695 header.set_entry_type(tar::EntryType::Regular);
1696 header.set_size(entry.size);
1697 header.set_cksum();
1698 let mut data = reader.file_data_reader(entry.nid).map_err(ImageError::Io)?;
1699 builder
1700 .append_data(&mut header, &entry.path, &mut data)
1701 .map_err(ImageError::Io)?;
1702 }
1703 ErofsEntryKind::Directory => {
1704 header.set_entry_type(tar::EntryType::Directory);
1705 header.set_size(0);
1706 header.set_cksum();
1707 builder
1708 .append_data(&mut header, &entry.path, io::empty())
1709 .map_err(ImageError::Io)?;
1710 }
1711 ErofsEntryKind::Symlink => {
1712 header.set_entry_type(tar::EntryType::Symlink);
1713 header.set_size(0);
1714 let target = reader.read_link_by_nid(entry.nid).map_err(ImageError::Io)?;
1715 builder.append_link_literal(&mut header, &entry.path, &target)?;
1716 }
1717 ErofsEntryKind::CharDevice | ErofsEntryKind::BlockDevice => {
1718 header.set_entry_type(if entry.kind == ErofsEntryKind::CharDevice {
1719 tar::EntryType::Char
1720 } else {
1721 tar::EntryType::Block
1722 });
1723 header.set_size(0);
1724 if let Some((major, minor)) = entry.rdev {
1725 header.set_device_major(major).map_err(ImageError::Io)?;
1726 header.set_device_minor(minor).map_err(ImageError::Io)?;
1727 }
1728 header.set_cksum();
1729 builder
1730 .append_data(&mut header, &entry.path, io::empty())
1731 .map_err(ImageError::Io)?;
1732 }
1733 ErofsEntryKind::Fifo => {
1734 header.set_entry_type(tar::EntryType::Fifo);
1735 header.set_size(0);
1736 header.set_cksum();
1737 builder
1738 .append_data(&mut header, &entry.path, io::empty())
1739 .map_err(ImageError::Io)?;
1740 }
1741 ErofsEntryKind::Socket => {
1742 header.set_entry_type(tar::EntryType::new(0o140));
1743 header.set_size(0);
1744 header.set_cksum();
1745 builder
1746 .append_data(&mut header, &entry.path, io::empty())
1747 .map_err(ImageError::Io)?;
1748 }
1749 }
1750
1751 Ok(())
1752}
1753
1754fn append_whiteout<W: Write>(
1755 builder: &mut tar::Builder<DigestingWriter<W>>,
1756 entry: &crate::erofs::ErofsTreeEntry,
1757) -> ImageResult<()> {
1758 let Some(file_name) = entry.path.file_name() else {
1759 return Ok(());
1760 };
1761 let mut path = entry.path.clone();
1762 let mut whiteout_name = b".wh.".to_vec();
1763 whiteout_name.extend_from_slice(os_str_bytes(file_name));
1764 path.set_file_name(os_string_from_vec(whiteout_name).map_err(ImageError::Io)?);
1765 append_empty_file(builder, &path, entry)
1766}
1767
1768fn append_opaque_marker<W: Write>(
1769 builder: &mut tar::Builder<DigestingWriter<W>>,
1770 entry: &crate::erofs::ErofsTreeEntry,
1771) -> ImageResult<()> {
1772 let path = entry.path.join(".wh..wh..opq");
1773 append_empty_file(builder, &path, entry)
1774}
1775
1776fn append_empty_file<W: Write>(
1777 builder: &mut tar::Builder<DigestingWriter<W>>,
1778 path: &Path,
1779 entry: &crate::erofs::ErofsTreeEntry,
1780) -> ImageResult<()> {
1781 let mut header = tar::Header::new_gnu();
1782 apply_header_metadata(&mut header, entry);
1783 header.set_mode(0o000);
1784 header.set_entry_type(tar::EntryType::Regular);
1785 header.set_size(0);
1786 header.set_cksum();
1787 builder
1788 .append_data(&mut header, path, io::empty())
1789 .map_err(ImageError::Io)
1790}
1791
1792fn append_layer_entries<W: Write>(
1793 archive: &mut tar::Builder<W>,
1794 layer: &GeneratedLayer,
1795) -> ImageResult<()> {
1796 append_bytes(archive, &format!("{}/VERSION", layer.hex), b"1.0\n")?;
1797 append_bytes(archive, &format!("{}/json", layer.hex), b"{}")?;
1798
1799 let mut file = File::open(&layer.path).map_err(|e| ImageError::Cache {
1800 path: layer.path.clone(),
1801 source: e,
1802 })?;
1803 let mut header = tar::Header::new_gnu();
1804 header.set_entry_type(tar::EntryType::Regular);
1805 header.set_mode(0o644);
1806 header.set_uid(0);
1807 header.set_gid(0);
1808 header.set_mtime(0);
1809 header.set_size(layer.size);
1810 header.set_cksum();
1811 archive
1812 .append_data(&mut header, format!("{}/layer.tar", layer.hex), &mut file)
1813 .map_err(ImageError::Io)
1814}
1815
1816fn append_blob_file<W: Write>(
1817 archive: &mut tar::Builder<W>,
1818 hex: &str,
1819 path: &Path,
1820 size: u64,
1821) -> ImageResult<()> {
1822 let mut file = File::open(path).map_err(|e| ImageError::Cache {
1823 path: path.to_path_buf(),
1824 source: e,
1825 })?;
1826 let mut header = tar::Header::new_gnu();
1827 header.set_entry_type(tar::EntryType::Regular);
1828 header.set_mode(0o644);
1829 header.set_uid(0);
1830 header.set_gid(0);
1831 header.set_mtime(0);
1832 header.set_size(size);
1833 header.set_cksum();
1834 archive
1835 .append_data(&mut header, format!("blobs/sha256/{hex}"), &mut file)
1836 .map_err(ImageError::Io)
1837}
1838
1839fn append_blob_bytes<W: Write>(
1840 archive: &mut tar::Builder<W>,
1841 hex: &str,
1842 bytes: &[u8],
1843) -> ImageResult<()> {
1844 append_bytes(archive, &format!("blobs/sha256/{hex}"), bytes)
1845}
1846
1847fn append_directory<W: Write>(archive: &mut tar::Builder<W>, path: &str) -> ImageResult<()> {
1848 let mut header = tar::Header::new_gnu();
1849 header.set_entry_type(tar::EntryType::Directory);
1850 header.set_mode(0o755);
1851 header.set_uid(0);
1852 header.set_gid(0);
1853 header.set_mtime(0);
1854 header.set_size(0);
1855 header.set_cksum();
1856 archive
1857 .append_data(&mut header, path, io::empty())
1858 .map_err(ImageError::Io)
1859}
1860
1861fn append_bytes<W: Write>(
1862 archive: &mut tar::Builder<W>,
1863 path: &str,
1864 bytes: &[u8],
1865) -> ImageResult<()> {
1866 let mut header = tar::Header::new_gnu();
1867 header.set_entry_type(tar::EntryType::Regular);
1868 header.set_mode(0o644);
1869 header.set_uid(0);
1870 header.set_gid(0);
1871 header.set_mtime(0);
1872 header.set_size(bytes.len() as u64);
1873 header.set_cksum();
1874 archive
1875 .append_data(&mut header, path, bytes)
1876 .map_err(ImageError::Io)
1877}
1878
1879fn enforce_archive_entry_count(count: u64) -> ImageResult<()> {
1880 if count > ARCHIVE_MAX_ENTRY_COUNT {
1881 return Err(ImageError::ManifestParse(format!(
1882 "archive has more than {ARCHIVE_MAX_ENTRY_COUNT} entries"
1883 )));
1884 }
1885
1886 Ok(())
1887}
1888
1889fn read_entry_to_vec(
1890 entry: &mut tar::Entry<'_, File>,
1891 path: &str,
1892 max_bytes: u64,
1893) -> ImageResult<Vec<u8>> {
1894 let declared_size = entry.header().size().map_err(ImageError::Io)?;
1895 if declared_size > max_bytes {
1896 return Err(ImageError::ManifestParse(format!(
1897 "archive metadata entry {path} is {declared_size} bytes; max is {max_bytes}"
1898 )));
1899 }
1900
1901 let mut data = Vec::with_capacity(declared_size as usize);
1902 entry.read_to_end(&mut data).map_err(ImageError::Io)?;
1903 Ok(data)
1904}
1905
1906fn json_bytes_to_string(bytes: &[u8], context: &str) -> ImageResult<String> {
1907 std::str::from_utf8(bytes)
1908 .map(str::to_owned)
1909 .map_err(|e| ImageError::ConfigParse(format!("{context} is not UTF-8 JSON: {e}")))
1910}
1911
1912fn docker_config_json(
1913 config: &ImageSaveConfig,
1914 raw_config_json: &str,
1915 diff_ids: &[String],
1916) -> ImageResult<Vec<u8>> {
1917 if !raw_config_json.is_empty() {
1918 let mut config_json: serde_json::Value = serde_json::from_str(raw_config_json)
1919 .map_err(|e| ImageError::ConfigParse(format!("parse raw image config: {e}")))?;
1920 let Some(object) = config_json.as_object_mut() else {
1921 return Err(ImageError::ConfigParse(
1922 "raw image config JSON is not an object".into(),
1923 ));
1924 };
1925 object.insert(
1926 "rootfs".into(),
1927 serde_json::json!({
1928 "type": "layers",
1929 "diff_ids": diff_ids,
1930 }),
1931 );
1932 object.entry("architecture").or_insert_with(|| {
1933 serde_json::json!(config.architecture.as_deref().unwrap_or("amd64"))
1934 });
1935 object
1936 .entry("os")
1937 .or_insert_with(|| serde_json::json!(config.os.as_deref().unwrap_or("linux")));
1938 return serde_json::to_vec(&config_json)
1939 .map_err(|e| ImageError::ConfigParse(format!("serialize image config: {e}")));
1940 }
1941
1942 let config_json = serde_json::json!({
1943 "architecture": config.architecture.as_deref().unwrap_or("amd64"),
1944 "os": config.os.as_deref().unwrap_or("linux"),
1945 "config": {
1946 "Env": config.env,
1947 "Entrypoint": config.entrypoint,
1948 "Cmd": config.cmd,
1949 "WorkingDir": config.working_dir,
1950 "User": config.user,
1951 "Labels": if config.labels.is_empty() {
1952 serde_json::Value::Null
1953 } else {
1954 serde_json::to_value(&config.labels)
1955 .map_err(|e| ImageError::ConfigParse(format!("serialize labels: {e}")))?
1956 },
1957 },
1958 "rootfs": {
1959 "type": "layers",
1960 "diff_ids": diff_ids,
1961 },
1962 "history": diff_ids
1963 .iter()
1964 .map(|_| serde_json::json!({"created_by": "microsandbox image save"}))
1965 .collect::<Vec<_>>(),
1966 });
1967
1968 serde_json::to_vec(&config_json)
1969 .map_err(|e| ImageError::ConfigParse(format!("serialize image config: {e}")))
1970}
1971
1972fn apply_header_metadata(header: &mut tar::Header, entry: &crate::erofs::ErofsTreeEntry) {
1973 header.set_mode((entry.metadata.mode & 0o7777) as u32);
1974 header.set_uid(entry.metadata.uid as u64);
1975 header.set_gid(entry.metadata.gid as u64);
1976 header.set_mtime(entry.metadata.mtime);
1977}
1978
1979fn normalized_archive_path(entry: &tar::Entry<'_, File>) -> ImageResult<String> {
1980 let path = entry.path().map_err(ImageError::Io)?;
1981 let bytes = path_bytes(path.as_ref());
1982 let normalized = if let Some(stripped) = bytes.strip_prefix(b"./") {
1983 stripped
1984 } else {
1985 bytes
1986 };
1987 String::from_utf8(normalized.to_vec())
1988 .map_err(|_| ImageError::ManifestParse("archive path is not valid UTF-8".into()))
1989}
1990
1991fn sha256_hex(bytes: &[u8]) -> String {
1992 hex::encode(Sha256::digest(bytes))
1993}
1994
1995#[cfg(test)]
2000mod tests {
2001 use std::collections::BTreeMap;
2002 use std::io::Cursor;
2003
2004 use tempfile::tempdir;
2005
2006 use super::*;
2007
2008 #[tokio::test]
2009 async fn warm_import_restages_evicted_layers_from_the_archive() {
2010 let temp = tempdir().unwrap();
2011 let cache = GlobalCache::new(&temp.path().join("cache")).unwrap();
2012 let first = temp.path().join("first.tar");
2013 let second = temp.path().join("second.tar");
2014 write_test_docker_archive_from_layer(
2015 &first,
2016 "example.invalid/first:latest",
2017 simple_layer_tar(),
2018 );
2019 write_test_docker_archive_from_layer(
2020 &second,
2021 "example.invalid/second:latest",
2022 complex_layer_tar(),
2023 );
2024 let mut images = load_archive_into(&cache, &first, ImageLoadOptions::default())
2025 .await
2026 .unwrap();
2027 images.extend(
2028 load_archive_into(&cache, &second, ImageLoadOptions::default())
2029 .await
2030 .unwrap(),
2031 );
2032 let archive = temp.path().join("both.tar");
2033 let requests = images
2034 .iter()
2035 .map(save_request_from_loaded)
2036 .collect::<Vec<_>>();
2037 save_docker_archive(&cache, &archive, &requests).unwrap();
2038 let warmed = load_archive_into(&cache, &archive, ImageLoadOptions::default())
2039 .await
2040 .unwrap();
2041 let evicted = cache.metadata_paths(&warmed[1].metadata).unwrap();
2042 assert!(
2043 load_archive_blocking(
2044 cache.layers_dir().parent().unwrap(),
2045 &archive,
2046 ImageLoadOptions::default()
2047 )
2048 .unwrap()
2049 .staged_layers
2050 .is_empty()
2051 );
2052 let imported = load_archive_with(&cache, &archive, ImageLoadOptions::default(), |image| {
2055 let evicted = evicted.clone();
2056 async move {
2057 if image.reference == "example.invalid/first:latest" {
2058 for path in evicted {
2059 std::fs::remove_file(path)?;
2060 }
2061 }
2062 Ok(())
2063 }
2064 })
2065 .await
2066 .unwrap();
2067 assert_eq!(imported.len(), 2);
2068 for layer in &imported[1].metadata.layers {
2069 assert!(cache.is_layer_materialized(&layer.diff_id.parse().unwrap()));
2070 }
2071 }
2072
2073 #[test]
2074 fn docker_archive_load_save_load_roundtrip() {
2075 let runtime = tokio::runtime::Builder::new_current_thread()
2076 .enable_all()
2077 .build()
2078 .unwrap();
2079 let temp = tempdir().unwrap();
2080 let input = temp.path().join("image.tar");
2081 write_test_docker_archive(&input, "tiny:latest");
2082
2083 let first_cache = temp.path().join("cache-1");
2084 let loaded = runtime
2085 .block_on(load_archive(
2086 &first_cache,
2087 &input,
2088 ImageLoadOptions::default(),
2089 ))
2090 .unwrap();
2091
2092 assert_eq!(loaded.len(), 1);
2093 assert_eq!(loaded[0].reference, "tiny:latest");
2094
2095 let saved = temp.path().join("saved.tar");
2096 let request = save_request_from_loaded(&loaded[0]);
2097 let cache = GlobalCache::new(&first_cache).unwrap();
2098 save_docker_archive(&cache, &saved, &[request]).unwrap();
2099
2100 let second_cache = temp.path().join("cache-2");
2101 let reloaded = runtime
2102 .block_on(load_archive(
2103 &second_cache,
2104 &saved,
2105 ImageLoadOptions::default(),
2106 ))
2107 .unwrap();
2108
2109 assert_eq!(reloaded.len(), 1);
2110 assert_eq!(reloaded[0].reference, "tiny:latest");
2111 assert_eq!(
2112 reloaded[0].metadata.config.cmd,
2113 Some(vec!["cat".into(), "/hello.txt".into()])
2114 );
2115 }
2116
2117 #[test]
2118 fn docker_archive_loads_manifest_blob_paths() {
2119 let runtime = tokio::runtime::Builder::new_current_thread()
2120 .enable_all()
2121 .build()
2122 .unwrap();
2123 let temp = tempdir().unwrap();
2124 let input = temp.path().join("blob-paths.tar");
2125 write_test_docker_blob_archive_from_layer(&input, "blob-paths:latest", simple_layer_tar());
2126
2127 let loaded = runtime
2128 .block_on(load_archive(
2129 &temp.path().join("cache"),
2130 &input,
2131 ImageLoadOptions::default(),
2132 ))
2133 .unwrap();
2134
2135 assert_eq!(loaded.len(), 1);
2136 assert_eq!(loaded[0].reference, "blob-paths:latest");
2137 assert_eq!(
2138 loaded[0].metadata.config.cmd,
2139 Some(vec!["cat".into(), "/hello.txt".into()])
2140 );
2141 }
2142
2143 #[test]
2144 fn docker_archive_rejects_mismatched_blob_layer_path() {
2145 let runtime = tokio::runtime::Builder::new_current_thread()
2146 .enable_all()
2147 .build()
2148 .unwrap();
2149 let temp = tempdir().unwrap();
2150 let input = temp.path().join("bad-blob-path.tar");
2151 let layer_bytes = simple_layer_tar();
2152 let diff_id = format!("sha256:{}", sha256_hex(&layer_bytes));
2153 let config_bytes = test_config_bytes(&diff_id);
2154 let config_name = format!("blobs/sha256/{}", sha256_hex(&config_bytes));
2155 let layer_name = format!("blobs/sha256/{:064x}", 1u8);
2156
2157 write_test_docker_archive_entries(
2158 &input,
2159 "bad-blob-path:latest",
2160 config_name,
2161 layer_name,
2162 config_bytes,
2163 layer_bytes,
2164 );
2165
2166 let err = runtime
2167 .block_on(load_archive(
2168 &temp.path().join("cache"),
2169 &input,
2170 ImageLoadOptions::default(),
2171 ))
2172 .unwrap_err();
2173
2174 assert!(err.to_string().contains("digest mismatch"));
2175 }
2176
2177 #[test]
2178 fn oci_layout_archive_load_save_load_roundtrip() {
2179 let runtime = tokio::runtime::Builder::new_current_thread()
2180 .enable_all()
2181 .build()
2182 .unwrap();
2183 let temp = tempdir().unwrap();
2184 let input = temp.path().join("oci-layout.tar");
2185 write_test_oci_archive_from_layer(&input, "oci-layout:latest", simple_layer_tar());
2186
2187 let first_cache = temp.path().join("cache-1");
2188 let loaded = runtime
2189 .block_on(load_archive(
2190 &first_cache,
2191 &input,
2192 ImageLoadOptions::default(),
2193 ))
2194 .unwrap();
2195
2196 assert_eq!(loaded.len(), 1);
2197 assert_eq!(loaded[0].reference, "oci-layout:latest");
2198
2199 let saved = temp.path().join("saved-oci-layout.tar");
2200 let request = save_request_from_loaded(&loaded[0]);
2201 let cache = GlobalCache::new(&first_cache).unwrap();
2202 save_archive(&cache, &saved, &[request], ImageArchiveFormat::Oci).unwrap();
2203
2204 let index_bytes = read_archive_entry(&saved, "index.json").unwrap().unwrap();
2205 let index: oci_spec::image::ImageIndex = serde_json::from_slice(&index_bytes).unwrap();
2206 assert_eq!(index.manifests().len(), 1);
2207 assert_eq!(
2208 index.manifests()[0]
2209 .annotations()
2210 .as_ref()
2211 .unwrap()
2212 .get(OCI_REF_NAME_ANNOTATION),
2213 Some(&"oci-layout:latest".to_string())
2214 );
2215
2216 let second_cache = temp.path().join("cache-2");
2217 let reloaded = runtime
2218 .block_on(load_archive(
2219 &second_cache,
2220 &saved,
2221 ImageLoadOptions::default(),
2222 ))
2223 .unwrap();
2224
2225 assert_eq!(reloaded.len(), 1);
2226 assert_eq!(reloaded[0].reference, "oci-layout:latest");
2227 }
2228
2229 #[test]
2230 fn oci_layout_archive_loads_nested_indexes() {
2231 let runtime = tokio::runtime::Builder::new_current_thread()
2232 .enable_all()
2233 .build()
2234 .unwrap();
2235 let temp = tempdir().unwrap();
2236 let input = temp.path().join("nested-oci-layout.tar");
2237 write_test_nested_oci_archive(&input, Some("nested:latest"), simple_layer_tar(), 2);
2238
2239 let loaded = runtime
2240 .block_on(load_archive(
2241 &temp.path().join("cache"),
2242 &input,
2243 ImageLoadOptions::default(),
2244 ))
2245 .unwrap();
2246
2247 assert_eq!(loaded.len(), 1);
2248 assert_eq!(loaded[0].reference, "nested:latest");
2249 assert_eq!(
2250 loaded[0].metadata.config.cmd,
2251 Some(vec!["cat".into(), "/hello.txt".into()])
2252 );
2253 }
2254
2255 #[test]
2256 fn oci_layout_nested_index_without_reference_uses_explicit_tag() {
2257 let runtime = tokio::runtime::Builder::new_current_thread()
2258 .enable_all()
2259 .build()
2260 .unwrap();
2261 let temp = tempdir().unwrap();
2262 let input = temp.path().join("untagged-nested-oci-layout.tar");
2263 write_test_nested_oci_archive(&input, None, simple_layer_tar(), 1);
2264
2265 let loaded = runtime
2266 .block_on(load_archive(
2267 &temp.path().join("cache"),
2268 &input,
2269 ImageLoadOptions {
2270 tags: vec!["explicit:latest".into()],
2271 ..ImageLoadOptions::default()
2272 },
2273 ))
2274 .unwrap();
2275
2276 assert_eq!(loaded.len(), 1);
2277 assert_eq!(loaded[0].reference, "explicit:latest");
2278 }
2279
2280 #[test]
2281 fn oci_layout_nested_index_skips_incompatible_and_unknown_siblings() {
2282 let runtime = tokio::runtime::Builder::new_current_thread()
2283 .enable_all()
2284 .build()
2285 .unwrap();
2286 let temp = tempdir().unwrap();
2287 let input = temp.path().join("mixed-nested-oci-layout.tar");
2288 write_test_mixed_nested_oci_archive(&input, "mixed-nested:latest", simple_layer_tar());
2293
2294 let loaded = runtime
2295 .block_on(load_archive(
2296 &temp.path().join("cache"),
2297 &input,
2298 ImageLoadOptions::default(),
2299 ))
2300 .unwrap();
2301
2302 assert_eq!(loaded.len(), 1);
2303 assert_eq!(loaded[0].reference, "mixed-nested:latest");
2304 assert_eq!(
2305 loaded[0].metadata.config.cmd,
2306 Some(vec!["cat".into(), "/hello.txt".into()])
2307 );
2308 }
2309
2310 #[test]
2311 fn docker_archive_save_preserves_layer_semantics() {
2312 let runtime = tokio::runtime::Builder::new_current_thread()
2313 .enable_all()
2314 .build()
2315 .unwrap();
2316 let temp = tempdir().unwrap();
2317 let input = temp.path().join("complex.tar");
2318 let layer_bytes = complex_layer_tar();
2319 write_test_docker_archive_from_layer(&input, "complex:latest", layer_bytes);
2320
2321 let first_cache = temp.path().join("cache-1");
2322 let loaded = runtime
2323 .block_on(load_archive(
2324 &first_cache,
2325 &input,
2326 ImageLoadOptions::default(),
2327 ))
2328 .unwrap();
2329
2330 let saved = temp.path().join("saved-complex.tar");
2331 let request = save_request_from_loaded(&loaded[0]);
2332 let cache = GlobalCache::new(&first_cache).unwrap();
2333 save_docker_archive(&cache, &saved, &[request]).unwrap();
2334
2335 let entries = saved_layer_entries(&saved);
2336 let config_entry = entries.get("etc/config.txt").unwrap();
2337 let config_link_entry = entries.get("etc/config.link").unwrap();
2338 let regular_config_paths = [
2339 ("etc/config.txt", config_entry),
2340 ("etc/config.link", config_link_entry),
2341 ]
2342 .into_iter()
2343 .filter(|(_, entry)| entry.entry_type == tar::EntryType::Regular)
2344 .collect::<Vec<_>>();
2345 let hardlink_config_paths = [
2346 ("etc/config.txt", config_entry),
2347 ("etc/config.link", config_link_entry),
2348 ]
2349 .into_iter()
2350 .filter(|(_, entry)| entry.entry_type == tar::EntryType::Link)
2351 .collect::<Vec<_>>();
2352
2353 assert_eq!(regular_config_paths.len(), 1);
2354 assert_eq!(hardlink_config_paths.len(), 1);
2355 assert_eq!(regular_config_paths[0].1.data, b"shared config\n");
2356 assert_eq!(
2357 hardlink_config_paths[0].1.link_name.as_deref(),
2358 Some(regular_config_paths[0].0)
2359 );
2360 assert_eq!(regular_config_paths[0].1.mode, 0o640);
2361 assert_eq!(regular_config_paths[0].1.uid, 1000);
2362 assert_eq!(regular_config_paths[0].1.gid, 1001);
2363 assert_eq!(regular_config_paths[0].1.mtime, 42);
2364
2365 let symlink_entry = entries.get("bin/config").unwrap();
2366 assert_eq!(symlink_entry.entry_type, tar::EntryType::Symlink);
2367 assert_eq!(
2368 symlink_entry.link_name.as_deref(),
2369 Some("../etc/config.txt")
2370 );
2371
2372 let whiteout_entry = entries.get("var/.wh.deleted").unwrap();
2373 assert_eq!(whiteout_entry.entry_type, tar::EntryType::Regular);
2374 assert!(whiteout_entry.data.is_empty());
2375
2376 let opaque_entry = entries.get("cache/.wh..wh..opq").unwrap();
2377 assert_eq!(opaque_entry.entry_type, tar::EntryType::Regular);
2378 assert!(opaque_entry.data.is_empty());
2379
2380 let second_cache = temp.path().join("cache-2");
2381 let reloaded = runtime
2382 .block_on(load_archive(
2383 &second_cache,
2384 &saved,
2385 ImageLoadOptions::default(),
2386 ))
2387 .unwrap();
2388
2389 assert_eq!(reloaded.len(), 1);
2390 assert_eq!(reloaded[0].reference, "complex:latest");
2391 }
2392
2393 #[test]
2394 fn docker_archive_save_preserves_long_link_targets() {
2395 let runtime = tokio::runtime::Builder::new_current_thread()
2396 .enable_all()
2397 .build()
2398 .unwrap();
2399 let temp = tempdir().unwrap();
2400 let input = temp.path().join("long-links.tar");
2401 let long_target = format!("deep/{}config.txt", "component/".repeat(12));
2402 let long_symlink_path = format!("links/{}link", "component/".repeat(12));
2403 let relative_symlink_target = "../../etc/passwd";
2404 let short_symlink_target = r"..\..\etc\passwd";
2405 let long_symlink_target = format!(r"..\{}..\etc\passwd", "component\\".repeat(12));
2406 let mut layer_bytes = Vec::new();
2407 {
2408 let mut layer = tar::Builder::new(&mut layer_bytes);
2409 append_test_file(&mut layer, &long_target, b"shared config\n", 0o644, 0, 0, 1);
2410 append_test_hardlink(&mut layer, "zz-hardlink", &long_target);
2411 append_test_symlink(&mut layer, "zz-relative-symlink", relative_symlink_target);
2412 append_test_symlink(&mut layer, "zz-short-symlink", short_symlink_target);
2413 append_test_symlink(&mut layer, &long_symlink_path, &long_symlink_target);
2414 layer.finish().unwrap();
2415 }
2416 write_test_docker_archive_from_layer(&input, "long-links:latest", layer_bytes);
2417
2418 let first_cache = temp.path().join("cache-1");
2419 let loaded = runtime
2420 .block_on(load_archive(
2421 &first_cache,
2422 &input,
2423 ImageLoadOptions::default(),
2424 ))
2425 .unwrap();
2426
2427 let saved = temp.path().join("saved-long-links.tar");
2428 let request = save_request_from_loaded(&loaded[0]);
2429 let cache = GlobalCache::new(&first_cache).unwrap();
2430 save_docker_archive(&cache, &saved, &[request]).unwrap();
2431
2432 let entries = saved_layer_entries(&saved);
2433 assert_eq!(
2434 entries.get("zz-hardlink").unwrap().link_name.as_deref(),
2435 Some(long_target.as_str())
2436 );
2437 assert_eq!(
2438 entries
2439 .get("zz-short-symlink")
2440 .unwrap()
2441 .link_name
2442 .as_deref(),
2443 Some(short_symlink_target)
2444 );
2445 assert_eq!(
2446 entries
2447 .get("zz-relative-symlink")
2448 .unwrap()
2449 .link_name
2450 .as_deref(),
2451 Some(relative_symlink_target)
2452 );
2453 assert_eq!(
2454 entries
2455 .get(&long_symlink_path)
2456 .unwrap()
2457 .link_name
2458 .as_deref(),
2459 Some(long_symlink_target.as_str())
2460 );
2461
2462 let second_cache = temp.path().join("cache-2");
2463 let reloaded = runtime
2464 .block_on(load_archive(
2465 &second_cache,
2466 &saved,
2467 ImageLoadOptions::default(),
2468 ))
2469 .unwrap();
2470 assert_eq!(reloaded[0].reference, "long-links:latest");
2471 }
2472
2473 #[test]
2474 fn docker_archive_save_preserves_raw_config_fields() {
2475 let runtime = tokio::runtime::Builder::new_current_thread()
2476 .enable_all()
2477 .build()
2478 .unwrap();
2479 let temp = tempdir().unwrap();
2480 let input = temp.path().join("config-fidelity.tar");
2481 let layer_bytes = simple_layer_tar();
2482 let diff_id = format!("sha256:{}", sha256_hex(&layer_bytes));
2483 let config_bytes = serde_json::to_vec(&serde_json::json!({
2484 "architecture": "arm64",
2485 "os": "linux",
2486 "author": "microsandbox-test",
2487 "config": {
2488 "Env": ["PATH=/usr/bin"],
2489 "Cmd": ["cat", "/hello.txt"],
2490 },
2491 "rootfs": {
2492 "type": "layers",
2493 "diff_ids": [diff_id],
2494 },
2495 "history": [{
2496 "created_by": "fixture",
2497 "comment": "keep me",
2498 }],
2499 }))
2500 .unwrap();
2501 let config_name = format!("{}.json", sha256_hex(&config_bytes));
2502
2503 write_test_docker_archive_entries(
2504 &input,
2505 "config-fidelity:latest",
2506 config_name,
2507 "layer/layer.tar".into(),
2508 config_bytes,
2509 layer_bytes,
2510 );
2511
2512 let first_cache = temp.path().join("cache-1");
2513 let loaded = runtime
2514 .block_on(load_archive(
2515 &first_cache,
2516 &input,
2517 ImageLoadOptions::default(),
2518 ))
2519 .unwrap();
2520 let saved = temp.path().join("saved-config-fidelity.tar");
2521 let request = save_request_from_loaded(&loaded[0]);
2522 let cache = GlobalCache::new(&first_cache).unwrap();
2523 save_docker_archive(&cache, &saved, &[request]).unwrap();
2524
2525 let manifest_bytes = read_archive_entry(&saved, "manifest.json")
2526 .unwrap()
2527 .unwrap();
2528 let manifest: Vec<DockerManifestEntry> = serde_json::from_slice(&manifest_bytes).unwrap();
2529 let saved_config = read_archive_entry(&saved, &manifest[0].config)
2530 .unwrap()
2531 .unwrap();
2532 let saved_config: serde_json::Value = serde_json::from_slice(&saved_config).unwrap();
2533
2534 assert_eq!(saved_config["author"], "microsandbox-test");
2535 assert_eq!(saved_config["history"][0]["comment"], "keep me");
2536 }
2537
2538 #[test]
2539 fn oci_load_hits_early_cache_gate_when_materialized() {
2540 let runtime = tokio::runtime::Builder::new_current_thread()
2541 .enable_all()
2542 .build()
2543 .unwrap();
2544 let temp = tempdir().unwrap();
2545 let input = temp.path().join("gate.tar");
2546 write_test_oci_archive_from_layer(&input, "gate:latest", simple_layer_tar());
2547 let cache = temp.path().join("cache");
2548
2549 let loaded = runtime
2551 .block_on(load_archive(&cache, &input, ImageLoadOptions::default()))
2552 .unwrap();
2553 assert_eq!(loaded.len(), 1);
2554
2555 let prepared =
2557 load_oci_archive_blocking(&cache, &input, ImageLoadOptions::default()).unwrap();
2558 assert!(
2559 prepared.staged_layers.is_empty(),
2560 "early cache gate should skip staging layer blobs on a warm cache"
2561 );
2562 assert_eq!(prepared.images.len(), 1);
2563 assert_eq!(prepared.images[0].reference, "gate:latest");
2564 assert_eq!(
2565 prepared.images[0].metadata.manifest_digest,
2566 loaded[0].metadata.manifest_digest,
2567 );
2568 }
2569
2570 #[test]
2571 fn docker_load_hits_early_cache_gate_when_materialized() {
2572 let runtime = tokio::runtime::Builder::new_current_thread()
2573 .enable_all()
2574 .build()
2575 .unwrap();
2576 let temp = tempdir().unwrap();
2577 let input = temp.path().join("gate.tar");
2578 write_test_docker_archive(&input, "gate-docker:latest");
2579 let cache = temp.path().join("cache");
2580
2581 let loaded = runtime
2583 .block_on(load_archive(&cache, &input, ImageLoadOptions::default()))
2584 .unwrap();
2585 assert_eq!(loaded.len(), 1);
2586
2587 let prepared = load_archive_blocking(&cache, &input, ImageLoadOptions::default()).unwrap();
2589 assert!(
2590 prepared.staged_layers.is_empty(),
2591 "early cache gate should skip staging layer blobs on a warm cache"
2592 );
2593 assert_eq!(prepared.images.len(), 1);
2594 assert_eq!(prepared.images[0].reference, "gate-docker:latest");
2595 assert_eq!(
2596 prepared.images[0].metadata.manifest_digest,
2597 loaded[0].metadata.manifest_digest,
2598 );
2599 }
2600
2601 #[test]
2602 fn docker_load_misses_gate_after_content_change() {
2603 let runtime = tokio::runtime::Builder::new_current_thread()
2604 .enable_all()
2605 .build()
2606 .unwrap();
2607 let temp = tempdir().unwrap();
2608 let cache = temp.path().join("cache");
2609
2610 let first = temp.path().join("first.tar");
2612 write_test_docker_archive_from_layer(&first, "app:latest", simple_layer_tar());
2613 runtime
2614 .block_on(load_archive(&cache, &first, ImageLoadOptions::default()))
2615 .unwrap();
2616
2617 let second = temp.path().join("second.tar");
2620 write_test_docker_archive_from_layer(&second, "app:latest", complex_layer_tar());
2621 let prepared = load_archive_blocking(&cache, &second, ImageLoadOptions::default()).unwrap();
2622 assert!(
2623 !prepared.staged_layers.is_empty(),
2624 "a rebuilt tag with new content must not hit the gate"
2625 );
2626 }
2627
2628 #[test]
2629 fn docker_load_misses_gate_after_config_change() {
2630 let runtime = tokio::runtime::Builder::new_current_thread()
2631 .enable_all()
2632 .build()
2633 .unwrap();
2634 let temp = tempdir().unwrap();
2635 let cache = temp.path().join("cache");
2636 let layer_bytes = simple_layer_tar();
2637 let diff_id = format!("sha256:{}", sha256_hex(&layer_bytes));
2638
2639 let first = temp.path().join("first.tar");
2641 write_test_docker_archive_from_layer(&first, "app:latest", layer_bytes.clone());
2642 runtime
2643 .block_on(load_archive(&cache, &first, ImageLoadOptions::default()))
2644 .unwrap();
2645
2646 let second = temp.path().join("second.tar");
2649 let config_bytes = test_config_bytes_with_cmd(&diff_id, &["sh", "-c", "echo changed"]);
2650 write_test_docker_archive_entries(
2651 &second,
2652 "app:latest",
2653 format!("{}.json", sha256_hex(&config_bytes)),
2654 "layer/layer.tar".into(),
2655 config_bytes,
2656 layer_bytes,
2657 );
2658
2659 let prepared = load_archive_blocking(&cache, &second, ImageLoadOptions::default()).unwrap();
2660 assert!(
2661 !prepared.staged_layers.is_empty(),
2662 "a config-only rebuild must not reuse stale cached metadata"
2663 );
2664 let expected_cmd = vec!["sh".into(), "-c".into(), "echo changed".into()];
2665 assert_eq!(
2666 prepared.images[0].metadata.config.cmd.as_ref(),
2667 Some(&expected_cmd)
2668 );
2669 }
2670
2671 #[test]
2672 fn docker_load_rejects_warm_archive_missing_layer_entry() {
2673 let runtime = tokio::runtime::Builder::new_current_thread()
2674 .enable_all()
2675 .build()
2676 .unwrap();
2677 let temp = tempdir().unwrap();
2678 let cache = temp.path().join("cache");
2679 let layer_bytes = simple_layer_tar();
2680
2681 let valid = temp.path().join("valid.tar");
2682 write_test_docker_archive_from_layer(&valid, "app:latest", layer_bytes.clone());
2683 runtime
2684 .block_on(load_archive(&cache, &valid, ImageLoadOptions::default()))
2685 .unwrap();
2686
2687 let missing_layer = temp.path().join("missing-layer.tar");
2688 write_test_docker_archive_without_layer(&missing_layer, "app:latest", layer_bytes);
2689 let err =
2690 load_archive_blocking(&cache, &missing_layer, ImageLoadOptions::default()).unwrap_err();
2691 match err {
2692 ImageError::ManifestParse(message) => {
2693 assert!(message.contains("docker archive missing layer layer/layer.tar"));
2694 }
2695 other => panic!("expected missing layer error, got {other:?}"),
2696 }
2697 }
2698
2699 #[test]
2700 fn oci_load_rejects_warm_archive_missing_layer_entry() {
2701 let runtime = tokio::runtime::Builder::new_current_thread()
2702 .enable_all()
2703 .build()
2704 .unwrap();
2705 let temp = tempdir().unwrap();
2706 let cache = temp.path().join("cache");
2707 let layer_bytes = simple_layer_tar();
2708
2709 let valid = temp.path().join("valid.tar");
2710 write_test_oci_archive_from_layer(&valid, "app:latest", layer_bytes.clone());
2711 runtime
2712 .block_on(load_archive(&cache, &valid, ImageLoadOptions::default()))
2713 .unwrap();
2714
2715 let missing_layer = temp.path().join("missing-layer.tar");
2716 write_test_oci_archive_without_layer(&missing_layer, "app:latest", layer_bytes);
2717 let err = load_oci_archive_blocking(&cache, &missing_layer, ImageLoadOptions::default())
2718 .unwrap_err();
2719 match err {
2720 ImageError::ManifestParse(message) => {
2721 assert!(message.contains("OCI layout missing layer blob"));
2722 }
2723 other => panic!("expected missing layer error, got {other:?}"),
2724 }
2725 }
2726
2727 #[test]
2728 fn oci_load_rejects_warm_archive_with_mismatched_config_blob() {
2729 let runtime = tokio::runtime::Builder::new_current_thread()
2730 .enable_all()
2731 .build()
2732 .unwrap();
2733 let temp = tempdir().unwrap();
2734 let cache = temp.path().join("cache");
2735 let layer_bytes = simple_layer_tar();
2736 let diff_id = format!("sha256:{}", sha256_hex(&layer_bytes));
2737
2738 let valid = temp.path().join("valid.tar");
2739 write_test_oci_archive_from_layer(&valid, "app:latest", layer_bytes.clone());
2740 runtime
2741 .block_on(load_archive(&cache, &valid, ImageLoadOptions::default()))
2742 .unwrap();
2743
2744 let bad_config = test_config_bytes_with_cmd(&diff_id, &["sh", "-c", "echo changed"]);
2745 let corrupt = temp.path().join("corrupt-config.tar");
2746 write_test_oci_archive_with_config_blob(&corrupt, "app:latest", layer_bytes, bad_config);
2747 let err =
2748 load_oci_archive_blocking(&cache, &corrupt, ImageLoadOptions::default()).unwrap_err();
2749 match err {
2750 ImageError::ManifestParse(message) => {
2751 assert!(message.contains("OCI blob sha256:"));
2752 }
2753 other => panic!("expected config descriptor error, got {other:?}"),
2754 }
2755 }
2756
2757 fn write_test_docker_archive(path: &Path, reference: &str) {
2758 write_test_docker_archive_from_layer(path, reference, simple_layer_tar());
2759 }
2760
2761 fn write_test_docker_archive_from_layer(path: &Path, reference: &str, layer_bytes: Vec<u8>) {
2762 let diff_id = format!("sha256:{}", sha256_hex(&layer_bytes));
2763 let config_bytes = test_config_bytes(&diff_id);
2764 let config_name = format!("{}.json", sha256_hex(&config_bytes));
2765
2766 write_test_docker_archive_entries(
2767 path,
2768 reference,
2769 config_name,
2770 "layer/layer.tar".into(),
2771 config_bytes,
2772 layer_bytes,
2773 );
2774 }
2775
2776 fn write_test_docker_archive_without_layer(path: &Path, reference: &str, layer_bytes: Vec<u8>) {
2777 let diff_id = format!("sha256:{}", sha256_hex(&layer_bytes));
2778 let config_bytes = test_config_bytes(&diff_id);
2779 let config_name = format!("{}.json", sha256_hex(&config_bytes));
2780 let layer_name = "layer/layer.tar".to_string();
2781 let manifest_bytes = serde_json::to_vec(&vec![DockerManifestOut {
2782 config: config_name.clone(),
2783 repo_tags: vec![reference.into()],
2784 layers: vec![layer_name],
2785 }])
2786 .unwrap();
2787
2788 let file = File::create(path).unwrap();
2789 let mut archive = tar::Builder::new(file);
2790 append_bytes(&mut archive, &config_name, &config_bytes).unwrap();
2791 append_bytes(&mut archive, "manifest.json", &manifest_bytes).unwrap();
2792 archive.finish().unwrap();
2793 }
2794
2795 fn write_test_docker_blob_archive_from_layer(
2796 path: &Path,
2797 reference: &str,
2798 layer_bytes: Vec<u8>,
2799 ) {
2800 let diff_id = format!("sha256:{}", sha256_hex(&layer_bytes));
2801 let config_bytes = test_config_bytes(&diff_id);
2802 let config_name = format!("blobs/sha256/{}", sha256_hex(&config_bytes));
2803 let layer_name = format!("blobs/sha256/{}", sha256_hex(&layer_bytes));
2804
2805 write_test_docker_archive_entries(
2806 path,
2807 reference,
2808 config_name,
2809 layer_name,
2810 config_bytes,
2811 layer_bytes,
2812 );
2813 }
2814
2815 fn write_test_nested_oci_archive(
2816 path: &Path,
2817 reference: Option<&str>,
2818 layer_bytes: Vec<u8>,
2819 index_depth: usize,
2820 ) {
2821 assert!(index_depth > 0);
2822
2823 let diff_id = format!("sha256:{}", sha256_hex(&layer_bytes));
2824 let config_bytes = test_config_bytes(&diff_id);
2825 let config_hex = sha256_hex(&config_bytes);
2826 let layer_hex = sha256_hex(&layer_bytes);
2827 let manifest_bytes = serde_json::to_vec(&serde_json::json!({
2828 "schemaVersion": 2,
2829 "mediaType": OCI_MANIFEST_MEDIA_TYPE,
2830 "config": {
2831 "mediaType": OCI_CONFIG_MEDIA_TYPE,
2832 "digest": format!("sha256:{config_hex}"),
2833 "size": config_bytes.len(),
2834 },
2835 "layers": [{
2836 "mediaType": OCI_LAYER_MEDIA_TYPE,
2837 "digest": format!("sha256:{layer_hex}"),
2838 "size": layer_bytes.len(),
2839 }],
2840 }))
2841 .unwrap();
2842 let manifest_hex = sha256_hex(&manifest_bytes);
2843 let host = Platform::host_linux();
2844 let mut target_descriptor = serde_json::json!({
2845 "mediaType": OCI_MANIFEST_MEDIA_TYPE,
2846 "digest": format!("sha256:{manifest_hex}"),
2847 "size": manifest_bytes.len(),
2848 "platform": {
2849 "architecture": host.arch.to_string(),
2850 "os": host.os.to_string(),
2851 },
2852 });
2853 let mut metadata_blobs = vec![(manifest_hex, manifest_bytes)];
2854
2855 for _ in 0..index_depth {
2856 let index_bytes = serde_json::to_vec(&serde_json::json!({
2857 "schemaVersion": 2,
2858 "mediaType": OCI_INDEX_MEDIA_TYPE,
2859 "manifests": [target_descriptor],
2860 }))
2861 .unwrap();
2862 let index_hex = sha256_hex(&index_bytes);
2863 target_descriptor = serde_json::json!({
2864 "mediaType": OCI_INDEX_MEDIA_TYPE,
2865 "digest": format!("sha256:{index_hex}"),
2866 "size": index_bytes.len(),
2867 });
2868 metadata_blobs.push((index_hex, index_bytes));
2869 }
2870
2871 if let Some(reference) = reference {
2872 target_descriptor["annotations"] = serde_json::json!({
2873 (OCI_REF_NAME_ANNOTATION): reference,
2874 });
2875 }
2876 let root_index_bytes = serde_json::to_vec(&serde_json::json!({
2877 "schemaVersion": 2,
2878 "mediaType": OCI_INDEX_MEDIA_TYPE,
2879 "manifests": [target_descriptor],
2880 }))
2881 .unwrap();
2882
2883 let file = File::create(path).unwrap();
2884 let mut archive = tar::Builder::new(file);
2885 append_bytes(
2886 &mut archive,
2887 "oci-layout",
2888 br#"{"imageLayoutVersion":"1.0.0"}"#,
2889 )
2890 .unwrap();
2891 append_bytes(&mut archive, "index.json", &root_index_bytes).unwrap();
2892 append_bytes(
2893 &mut archive,
2894 &format!("blobs/sha256/{config_hex}"),
2895 &config_bytes,
2896 )
2897 .unwrap();
2898 for (hex, bytes) in metadata_blobs {
2899 append_bytes(&mut archive, &format!("blobs/sha256/{hex}"), &bytes).unwrap();
2900 }
2901 append_bytes(
2902 &mut archive,
2903 &format!("blobs/sha256/{layer_hex}"),
2904 &layer_bytes,
2905 )
2906 .unwrap();
2907 archive.finish().unwrap();
2908 }
2909
2910 fn write_test_mixed_nested_oci_archive(path: &Path, reference: &str, layer_bytes: Vec<u8>) {
2916 let host = Platform::host_linux();
2917 let diff_id = format!("sha256:{}", sha256_hex(&layer_bytes));
2918 let config_bytes = test_config_bytes(&diff_id);
2919 let config_hex = sha256_hex(&config_bytes);
2920 let layer_hex = sha256_hex(&layer_bytes);
2921
2922 let manifest_bytes = serde_json::to_vec(&serde_json::json!({
2923 "schemaVersion": 2,
2924 "mediaType": OCI_MANIFEST_MEDIA_TYPE,
2925 "config": {
2926 "mediaType": OCI_CONFIG_MEDIA_TYPE,
2927 "digest": format!("sha256:{config_hex}"),
2928 "size": config_bytes.len(),
2929 },
2930 "layers": [{
2931 "mediaType": OCI_LAYER_MEDIA_TYPE,
2932 "digest": format!("sha256:{layer_hex}"),
2933 "size": layer_bytes.len(),
2934 }],
2935 }))
2936 .unwrap();
2937 let manifest_hex = sha256_hex(&manifest_bytes);
2938
2939 let inner_index_bytes = serde_json::to_vec(&serde_json::json!({
2941 "schemaVersion": 2,
2942 "mediaType": OCI_INDEX_MEDIA_TYPE,
2943 "manifests": [{
2944 "mediaType": OCI_MANIFEST_MEDIA_TYPE,
2945 "digest": format!("sha256:{manifest_hex}"),
2946 "size": manifest_bytes.len(),
2947 "platform": {
2948 "architecture": host.arch.to_string(),
2949 "os": host.os.to_string(),
2950 },
2951 }],
2952 }))
2953 .unwrap();
2954 let inner_index_hex = sha256_hex(&inner_index_bytes);
2955
2956 let absent_incompatible_hex = sha256_hex(b"absent-incompatible-platform-index");
2959 let absent_unknown_hex = sha256_hex(b"absent-unknown-media-type");
2960
2961 let mixed_index_bytes = serde_json::to_vec(&serde_json::json!({
2963 "schemaVersion": 2,
2964 "mediaType": OCI_INDEX_MEDIA_TYPE,
2965 "manifests": [
2966 {
2967 "mediaType": OCI_INDEX_MEDIA_TYPE,
2968 "digest": format!("sha256:{inner_index_hex}"),
2969 "size": inner_index_bytes.len(),
2970 },
2971 {
2972 "mediaType": OCI_INDEX_MEDIA_TYPE,
2973 "digest": format!("sha256:{absent_incompatible_hex}"),
2974 "size": 0,
2975 "platform": {
2976 "architecture": "sparc64",
2977 "os": "solaris",
2978 },
2979 },
2980 {
2981 "mediaType": "application/vnd.example.unknown.v1+json",
2982 "digest": format!("sha256:{absent_unknown_hex}"),
2983 "size": 0,
2984 },
2985 ],
2986 }))
2987 .unwrap();
2988 let mixed_index_hex = sha256_hex(&mixed_index_bytes);
2989
2990 let root_index_bytes = serde_json::to_vec(&serde_json::json!({
2993 "schemaVersion": 2,
2994 "mediaType": OCI_INDEX_MEDIA_TYPE,
2995 "manifests": [{
2996 "mediaType": OCI_INDEX_MEDIA_TYPE,
2997 "digest": format!("sha256:{mixed_index_hex}"),
2998 "size": mixed_index_bytes.len(),
2999 "annotations": {
3000 (OCI_REF_NAME_ANNOTATION): reference,
3001 },
3002 }],
3003 }))
3004 .unwrap();
3005
3006 let file = File::create(path).unwrap();
3007 let mut archive = tar::Builder::new(file);
3008 append_bytes(
3009 &mut archive,
3010 "oci-layout",
3011 br#"{"imageLayoutVersion":"1.0.0"}"#,
3012 )
3013 .unwrap();
3014 append_bytes(&mut archive, "index.json", &root_index_bytes).unwrap();
3015 for (hex, bytes) in [
3016 (config_hex, config_bytes),
3017 (manifest_hex, manifest_bytes),
3018 (inner_index_hex, inner_index_bytes),
3019 (mixed_index_hex, mixed_index_bytes),
3020 ] {
3021 append_bytes(&mut archive, &format!("blobs/sha256/{hex}"), &bytes).unwrap();
3022 }
3023 append_bytes(
3024 &mut archive,
3025 &format!("blobs/sha256/{layer_hex}"),
3026 &layer_bytes,
3027 )
3028 .unwrap();
3029 archive.finish().unwrap();
3030 }
3031
3032 fn write_test_oci_archive_from_layer(path: &Path, reference: &str, layer_bytes: Vec<u8>) {
3033 let diff_id = format!("sha256:{}", sha256_hex(&layer_bytes));
3034 let config_bytes = test_config_bytes(&diff_id);
3035 let config_hex = sha256_hex(&config_bytes);
3036 let layer_hex = sha256_hex(&layer_bytes);
3037 let manifest_bytes = serde_json::to_vec(&serde_json::json!({
3038 "schemaVersion": 2,
3039 "mediaType": OCI_MANIFEST_MEDIA_TYPE,
3040 "config": {
3041 "mediaType": OCI_CONFIG_MEDIA_TYPE,
3042 "digest": format!("sha256:{config_hex}"),
3043 "size": config_bytes.len(),
3044 },
3045 "layers": [{
3046 "mediaType": OCI_LAYER_MEDIA_TYPE,
3047 "digest": format!("sha256:{layer_hex}"),
3048 "size": layer_bytes.len(),
3049 }],
3050 }))
3051 .unwrap();
3052 let manifest_hex = sha256_hex(&manifest_bytes);
3053 let host = Platform::host_linux();
3054 let index_bytes = serde_json::to_vec(&serde_json::json!({
3055 "schemaVersion": 2,
3056 "mediaType": OCI_INDEX_MEDIA_TYPE,
3057 "manifests": [{
3058 "mediaType": OCI_MANIFEST_MEDIA_TYPE,
3059 "digest": format!("sha256:{manifest_hex}"),
3060 "size": manifest_bytes.len(),
3061 "platform": {
3062 "architecture": host.arch.to_string(),
3063 "os": host.os.to_string(),
3064 },
3065 "annotations": {
3066 (OCI_REF_NAME_ANNOTATION): reference,
3067 },
3068 }],
3069 }))
3070 .unwrap();
3071
3072 let file = File::create(path).unwrap();
3073 let mut archive = tar::Builder::new(file);
3074 append_bytes(
3075 &mut archive,
3076 "oci-layout",
3077 br#"{"imageLayoutVersion":"1.0.0"}"#,
3078 )
3079 .unwrap();
3080 append_bytes(&mut archive, "index.json", &index_bytes).unwrap();
3081 append_bytes(
3082 &mut archive,
3083 &format!("blobs/sha256/{config_hex}"),
3084 &config_bytes,
3085 )
3086 .unwrap();
3087 append_bytes(
3088 &mut archive,
3089 &format!("blobs/sha256/{manifest_hex}"),
3090 &manifest_bytes,
3091 )
3092 .unwrap();
3093 append_bytes(
3094 &mut archive,
3095 &format!("blobs/sha256/{layer_hex}"),
3096 &layer_bytes,
3097 )
3098 .unwrap();
3099 archive.finish().unwrap();
3100 }
3101
3102 fn write_test_oci_archive_without_layer(path: &Path, reference: &str, layer_bytes: Vec<u8>) {
3103 let diff_id = format!("sha256:{}", sha256_hex(&layer_bytes));
3104 let config_bytes = test_config_bytes(&diff_id);
3105 let config_hex = sha256_hex(&config_bytes);
3106 let layer_hex = sha256_hex(&layer_bytes);
3107 let manifest_bytes = serde_json::to_vec(&serde_json::json!({
3108 "schemaVersion": 2,
3109 "mediaType": OCI_MANIFEST_MEDIA_TYPE,
3110 "config": {
3111 "mediaType": OCI_CONFIG_MEDIA_TYPE,
3112 "digest": format!("sha256:{config_hex}"),
3113 "size": config_bytes.len(),
3114 },
3115 "layers": [{
3116 "mediaType": OCI_LAYER_MEDIA_TYPE,
3117 "digest": format!("sha256:{layer_hex}"),
3118 "size": layer_bytes.len(),
3119 }],
3120 }))
3121 .unwrap();
3122 let manifest_hex = sha256_hex(&manifest_bytes);
3123 let index_bytes = test_oci_index_bytes(reference, &manifest_hex, manifest_bytes.len());
3124
3125 let file = File::create(path).unwrap();
3126 let mut archive = tar::Builder::new(file);
3127 append_bytes(
3128 &mut archive,
3129 "oci-layout",
3130 br#"{"imageLayoutVersion":"1.0.0"}"#,
3131 )
3132 .unwrap();
3133 append_bytes(&mut archive, "index.json", &index_bytes).unwrap();
3134 append_bytes(
3135 &mut archive,
3136 &format!("blobs/sha256/{config_hex}"),
3137 &config_bytes,
3138 )
3139 .unwrap();
3140 append_bytes(
3141 &mut archive,
3142 &format!("blobs/sha256/{manifest_hex}"),
3143 &manifest_bytes,
3144 )
3145 .unwrap();
3146 archive.finish().unwrap();
3147 }
3148
3149 fn write_test_oci_archive_with_config_blob(
3150 path: &Path,
3151 reference: &str,
3152 layer_bytes: Vec<u8>,
3153 stored_config_bytes: Vec<u8>,
3154 ) {
3155 let diff_id = format!("sha256:{}", sha256_hex(&layer_bytes));
3156 let manifest_config_bytes = test_config_bytes(&diff_id);
3157 let config_hex = sha256_hex(&manifest_config_bytes);
3158 let layer_hex = sha256_hex(&layer_bytes);
3159 let manifest_bytes = serde_json::to_vec(&serde_json::json!({
3160 "schemaVersion": 2,
3161 "mediaType": OCI_MANIFEST_MEDIA_TYPE,
3162 "config": {
3163 "mediaType": OCI_CONFIG_MEDIA_TYPE,
3164 "digest": format!("sha256:{config_hex}"),
3165 "size": manifest_config_bytes.len(),
3166 },
3167 "layers": [{
3168 "mediaType": OCI_LAYER_MEDIA_TYPE,
3169 "digest": format!("sha256:{layer_hex}"),
3170 "size": layer_bytes.len(),
3171 }],
3172 }))
3173 .unwrap();
3174 let manifest_hex = sha256_hex(&manifest_bytes);
3175 let index_bytes = test_oci_index_bytes(reference, &manifest_hex, manifest_bytes.len());
3176
3177 let file = File::create(path).unwrap();
3178 let mut archive = tar::Builder::new(file);
3179 append_bytes(
3180 &mut archive,
3181 "oci-layout",
3182 br#"{"imageLayoutVersion":"1.0.0"}"#,
3183 )
3184 .unwrap();
3185 append_bytes(&mut archive, "index.json", &index_bytes).unwrap();
3186 append_bytes(
3187 &mut archive,
3188 &format!("blobs/sha256/{config_hex}"),
3189 &stored_config_bytes,
3190 )
3191 .unwrap();
3192 append_bytes(
3193 &mut archive,
3194 &format!("blobs/sha256/{manifest_hex}"),
3195 &manifest_bytes,
3196 )
3197 .unwrap();
3198 append_bytes(
3199 &mut archive,
3200 &format!("blobs/sha256/{layer_hex}"),
3201 &layer_bytes,
3202 )
3203 .unwrap();
3204 archive.finish().unwrap();
3205 }
3206
3207 fn simple_layer_tar() -> Vec<u8> {
3208 let mut layer_bytes = Vec::new();
3209 {
3210 let mut layer = tar::Builder::new(&mut layer_bytes);
3211 let data = b"hello from archive\n";
3212 let mut header = tar::Header::new_gnu();
3213 header.set_entry_type(tar::EntryType::Regular);
3214 header.set_mode(0o644);
3215 header.set_uid(0);
3216 header.set_gid(0);
3217 header.set_mtime(0);
3218 header.set_size(data.len() as u64);
3219 header.set_cksum();
3220 layer
3221 .append_data(&mut header, "hello.txt", Cursor::new(data))
3222 .unwrap();
3223 layer.finish().unwrap();
3224 }
3225
3226 layer_bytes
3227 }
3228
3229 fn test_config_bytes(diff_id: &str) -> Vec<u8> {
3230 test_config_bytes_with_cmd(diff_id, &["cat", "/hello.txt"])
3231 }
3232
3233 fn test_config_bytes_with_cmd(diff_id: &str, cmd: &[&str]) -> Vec<u8> {
3234 serde_json::to_vec(&serde_json::json!({
3235 "architecture": "arm64",
3236 "os": "linux",
3237 "config": {
3238 "Env": ["PATH=/usr/bin"],
3239 "Cmd": cmd,
3240 },
3241 "rootfs": {
3242 "type": "layers",
3243 "diff_ids": [diff_id],
3244 },
3245 }))
3246 .unwrap()
3247 }
3248
3249 fn test_oci_index_bytes(reference: &str, manifest_hex: &str, manifest_len: usize) -> Vec<u8> {
3250 let host = Platform::host_linux();
3251 serde_json::to_vec(&serde_json::json!({
3252 "schemaVersion": 2,
3253 "mediaType": OCI_INDEX_MEDIA_TYPE,
3254 "manifests": [{
3255 "mediaType": OCI_MANIFEST_MEDIA_TYPE,
3256 "digest": format!("sha256:{manifest_hex}"),
3257 "size": manifest_len,
3258 "platform": {
3259 "architecture": host.arch.to_string(),
3260 "os": host.os.to_string(),
3261 },
3262 "annotations": {
3263 (OCI_REF_NAME_ANNOTATION): reference,
3264 },
3265 }],
3266 }))
3267 .unwrap()
3268 }
3269
3270 fn write_test_docker_archive_entries(
3271 path: &Path,
3272 reference: &str,
3273 config_name: String,
3274 layer_name: String,
3275 config_bytes: Vec<u8>,
3276 layer_bytes: Vec<u8>,
3277 ) {
3278 let manifest_bytes = serde_json::to_vec(&vec![DockerManifestOut {
3279 config: config_name.clone(),
3280 repo_tags: vec![reference.into()],
3281 layers: vec![layer_name.clone()],
3282 }])
3283 .unwrap();
3284
3285 let file = File::create(path).unwrap();
3286 let mut archive = tar::Builder::new(file);
3287 append_bytes(&mut archive, &config_name, &config_bytes).unwrap();
3288 append_bytes(&mut archive, "manifest.json", &manifest_bytes).unwrap();
3289
3290 let mut header = tar::Header::new_gnu();
3291 header.set_entry_type(tar::EntryType::Regular);
3292 header.set_mode(0o644);
3293 header.set_uid(0);
3294 header.set_gid(0);
3295 header.set_mtime(0);
3296 header.set_size(layer_bytes.len() as u64);
3297 header.set_cksum();
3298 archive
3299 .append_data(&mut header, layer_name, Cursor::new(layer_bytes))
3300 .unwrap();
3301 archive.finish().unwrap();
3302 }
3303
3304 fn complex_layer_tar() -> Vec<u8> {
3305 let mut layer_bytes = Vec::new();
3306 {
3307 let mut layer = tar::Builder::new(&mut layer_bytes);
3308 append_test_dir(&mut layer, "bin", 0o755, 0, 0, 1);
3309 append_test_dir(&mut layer, "cache", 0o755, 0, 0, 1);
3310 append_test_dir(&mut layer, "etc", 0o755, 0, 0, 1);
3311 append_test_dir(&mut layer, "var", 0o755, 0, 0, 1);
3312 append_test_file(
3313 &mut layer,
3314 "etc/config.txt",
3315 b"shared config\n",
3316 0o640,
3317 1000,
3318 1001,
3319 42,
3320 );
3321 append_test_hardlink(&mut layer, "etc/config.link", "etc/config.txt");
3322 append_test_symlink(&mut layer, "bin/config", "../etc/config.txt");
3323 append_test_file(&mut layer, "var/.wh.deleted", b"", 0o000, 0, 0, 1);
3324 append_test_file(&mut layer, "cache/.wh..wh..opq", b"", 0o000, 0, 0, 1);
3325 layer.finish().unwrap();
3326 }
3327 layer_bytes
3328 }
3329
3330 fn append_test_dir(
3331 layer: &mut tar::Builder<&mut Vec<u8>>,
3332 path: &str,
3333 mode: u32,
3334 uid: u64,
3335 gid: u64,
3336 mtime: u64,
3337 ) {
3338 let mut header = tar::Header::new_gnu();
3339 header.set_entry_type(tar::EntryType::Directory);
3340 header.set_mode(mode);
3341 header.set_uid(uid);
3342 header.set_gid(gid);
3343 header.set_mtime(mtime);
3344 header.set_size(0);
3345 header.set_cksum();
3346 layer.append_data(&mut header, path, io::empty()).unwrap();
3347 }
3348
3349 fn append_test_file(
3350 layer: &mut tar::Builder<&mut Vec<u8>>,
3351 path: &str,
3352 data: &[u8],
3353 mode: u32,
3354 uid: u64,
3355 gid: u64,
3356 mtime: u64,
3357 ) {
3358 let mut header = tar::Header::new_gnu();
3359 header.set_entry_type(tar::EntryType::Regular);
3360 header.set_mode(mode);
3361 header.set_uid(uid);
3362 header.set_gid(gid);
3363 header.set_mtime(mtime);
3364 header.set_size(data.len() as u64);
3365 header.set_cksum();
3366 layer
3367 .append_data(&mut header, path, Cursor::new(data))
3368 .unwrap();
3369 }
3370
3371 fn append_test_hardlink(layer: &mut tar::Builder<&mut Vec<u8>>, path: &str, target: &str) {
3372 let mut header = tar::Header::new_gnu();
3373 header.set_entry_type(tar::EntryType::Link);
3374 header.set_size(0);
3375 layer.append_link(&mut header, path, target).unwrap();
3376 }
3377
3378 fn append_test_symlink(layer: &mut tar::Builder<&mut Vec<u8>>, path: &str, target: &str) {
3379 let mut header = tar::Header::new_gnu();
3380 header.set_entry_type(tar::EntryType::Symlink);
3381 header.set_mode(0o777);
3382 header.set_size(0);
3383 layer
3384 .append_link_literal(&mut header, Path::new(path), target.as_bytes())
3385 .unwrap();
3386 }
3387
3388 #[derive(Debug)]
3389 struct SavedLayerEntry {
3390 entry_type: tar::EntryType,
3391 link_name: Option<String>,
3392 mode: u32,
3393 uid: u64,
3394 gid: u64,
3395 mtime: u64,
3396 data: Vec<u8>,
3397 }
3398
3399 fn saved_layer_entries(path: &Path) -> BTreeMap<String, SavedLayerEntry> {
3400 let file = File::open(path).unwrap();
3401 let mut archive = tar::Archive::new(file);
3402 let mut layer_bytes = None;
3403
3404 for entry in archive.entries().unwrap() {
3405 let mut entry = entry.unwrap();
3406 let entry_path = entry.path().unwrap().to_string_lossy().into_owned();
3407 if entry_path.ends_with("/layer.tar") {
3408 assert!(layer_bytes.is_none());
3409 let mut data = Vec::new();
3410 entry.read_to_end(&mut data).unwrap();
3411 layer_bytes = Some(data);
3412 }
3413 }
3414
3415 let layer_bytes = layer_bytes.unwrap();
3416 let mut layer = tar::Archive::new(Cursor::new(layer_bytes));
3417 let mut entries = BTreeMap::new();
3418
3419 for entry in layer.entries().unwrap() {
3420 let mut entry = entry.unwrap();
3421 let path = entry.path().unwrap().to_string_lossy().into_owned();
3422 let header = entry.header();
3423 let entry_type = header.entry_type();
3424 let mode = header.mode().unwrap();
3425 let uid = header.uid().unwrap();
3426 let gid = header.gid().unwrap();
3427 let mtime = header.mtime().unwrap();
3428 let link_name = if matches!(entry_type, tar::EntryType::Link | tar::EntryType::Symlink)
3429 {
3430 Some(String::from_utf8_lossy(entry.link_name_bytes().unwrap().as_ref()).into())
3431 } else {
3432 None
3433 };
3434 let mut data = Vec::new();
3435 entry.read_to_end(&mut data).unwrap();
3436
3437 entries.insert(
3438 path,
3439 SavedLayerEntry {
3440 entry_type,
3441 link_name,
3442 mode,
3443 uid,
3444 gid,
3445 mtime,
3446 data,
3447 },
3448 );
3449 }
3450
3451 entries
3452 }
3453
3454 fn save_request_from_loaded(image: &LoadedImage) -> ImageSaveRequest {
3455 let host = Platform::host_linux();
3456 ImageSaveRequest {
3457 reference: image.reference.clone(),
3458 config: ImageSaveConfig {
3459 architecture: Some(host.arch.to_string()),
3460 os: Some(host.os.to_string()),
3461 env: image.metadata.config.env.clone(),
3462 entrypoint: image.metadata.config.entrypoint.clone(),
3463 cmd: image.metadata.config.cmd.clone(),
3464 working_dir: image.metadata.config.working_dir.clone(),
3465 user: image.metadata.config.user.clone(),
3466 labels: image
3467 .metadata
3468 .config
3469 .labels
3470 .iter()
3471 .map(|(key, value)| (key.clone(), value.clone()))
3472 .collect(),
3473 },
3474 raw_config_json: image.metadata.raw_config_json.clone(),
3475 layers: image
3476 .metadata
3477 .layers
3478 .iter()
3479 .map(|layer| ImageSaveLayer {
3480 diff_id: layer.diff_id.clone(),
3481 })
3482 .collect(),
3483 }
3484 }
3485}