1use std::collections::{BTreeMap, HashMap, HashSet};
4use std::ffi::OsString;
5use std::fs::{File, OpenOptions};
6use std::io::{self, BufWriter, Read, Write};
7use std::os::unix::ffi::{OsStrExt, OsStringExt};
8use std::path::{Path, PathBuf};
9use std::sync::{
10 Arc,
11 atomic::{AtomicU64, Ordering},
12};
13
14use serde::{Deserialize, Serialize};
15use sha2::{Digest as Sha2Digest, Sha256};
16
17use crate::{
18 CachedImageMetadata, CachedLayerMetadata, Digest, GlobalCache, ImageConfig, ImageError,
19 ImageResult, Platform, Reference, Registry,
20 erofs::{ErofsEntryKind, ErofsReader},
21 tar::Compression,
22};
23
24const OCI_CONFIG_MEDIA_TYPE: &str = "application/vnd.oci.image.config.v1+json";
29const OCI_MANIFEST_MEDIA_TYPE: &str = "application/vnd.oci.image.manifest.v1+json";
30const OCI_INDEX_MEDIA_TYPE: &str = "application/vnd.oci.image.index.v1+json";
31const OCI_LAYER_MEDIA_TYPE: &str = "application/vnd.oci.image.layer.v1.tar";
32const OCI_LAYER_GZIP_MEDIA_TYPE: &str = "application/vnd.oci.image.layer.v1.tar+gzip";
33const OCI_LAYER_ZSTD_MEDIA_TYPE: &str = "application/vnd.oci.image.layer.v1.tar+zstd";
34const OCI_REF_NAME_ANNOTATION: &str = "org.opencontainers.image.ref.name";
35const ARCHIVE_METADATA_MAX_BYTES: u64 = 16 * 1024 * 1024;
36const ARCHIVE_LAYER_MAX_BYTES: u64 = 10 * 1024 * 1024 * 1024;
37const ARCHIVE_MAX_ENTRY_COUNT: u64 = 1_000_000;
38static TEMP_FILE_COUNTER: AtomicU64 = AtomicU64::new(0);
39
40#[derive(Debug, Clone, Default)]
46pub struct ImageLoadOptions {
47 pub tags: Vec<String>,
49}
50
51#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
53pub enum ImageArchiveFormat {
54 #[default]
56 Docker,
57 Oci,
59}
60
61#[derive(Debug, Clone)]
63pub struct LoadedImage {
64 pub reference: String,
66 pub metadata: CachedImageMetadata,
68}
69
70#[derive(Debug, Clone)]
72pub struct ImageSaveRequest {
73 pub reference: String,
75 pub config: ImageSaveConfig,
77 pub raw_config_json: String,
79 pub layers: Vec<ImageSaveLayer>,
81}
82
83#[derive(Debug, Clone, Default)]
85pub struct ImageSaveConfig {
86 pub architecture: Option<String>,
88 pub os: Option<String>,
90 pub env: Vec<String>,
92 pub entrypoint: Option<Vec<String>>,
94 pub cmd: Option<Vec<String>>,
96 pub working_dir: Option<String>,
98 pub user: Option<String>,
100 pub labels: BTreeMap<String, String>,
102}
103
104#[derive(Debug, Clone)]
106pub struct ImageSaveLayer {
107 pub diff_id: String,
109}
110
111#[derive(Debug)]
112struct PreparedLoadedImage {
113 reference: String,
114 metadata: CachedImageMetadata,
115}
116
117#[derive(Debug)]
118struct PreparedArchiveLoad {
119 images: Vec<PreparedLoadedImage>,
120 staged_layers: HashMap<String, PathBuf>,
121}
122
123#[derive(Debug)]
124struct StagedLayerGuard {
125 paths: HashMap<String, PathBuf>,
126 cleanup_on_drop: bool,
127}
128
129#[derive(Debug)]
130struct LayerBlobInfo {
131 digest: String,
132 media_type: String,
133 size_bytes: u64,
134 path: PathBuf,
135}
136
137#[derive(Debug, Deserialize)]
138struct DockerManifestEntry {
139 #[serde(rename = "Config")]
140 config: String,
141 #[serde(rename = "RepoTags")]
142 repo_tags: Option<Vec<String>>,
143 #[serde(rename = "Layers")]
144 layers: Vec<String>,
145}
146
147#[derive(Debug, Serialize)]
148struct DockerManifestOut {
149 #[serde(rename = "Config")]
150 config: String,
151 #[serde(rename = "RepoTags")]
152 repo_tags: Vec<String>,
153 #[serde(rename = "Layers")]
154 layers: Vec<String>,
155}
156
157#[derive(Debug)]
158struct GeneratedLayer {
159 diff_id: String,
160 hex: String,
161 path: PathBuf,
162 size: u64,
163}
164
165struct DigestingWriter<W> {
166 inner: W,
167 hasher: Sha256,
168 written: u64,
169}
170
171impl<W> DigestingWriter<W> {
176 fn new(inner: W) -> Self {
177 Self {
178 inner,
179 hasher: Sha256::new(),
180 written: 0,
181 }
182 }
183
184 fn finish(self) -> (W, String, u64) {
185 (
186 self.inner,
187 hex::encode(self.hasher.finalize()),
188 self.written,
189 )
190 }
191}
192
193impl StagedLayerGuard {
194 fn new() -> Self {
195 Self {
196 paths: HashMap::new(),
197 cleanup_on_drop: true,
198 }
199 }
200
201 fn track(&mut self, digest: String, path: PathBuf) -> PathBuf {
202 if let Some(existing_path) = self.paths.get(&digest) {
203 let _ = std::fs::remove_file(&path);
204 return existing_path.clone();
205 }
206
207 self.paths.insert(digest, path.clone());
208 path
209 }
210
211 fn into_inner(mut self) -> HashMap<String, PathBuf> {
212 self.cleanup_on_drop = false;
213 std::mem::take(&mut self.paths)
214 }
215}
216
217impl<W: Write> Write for DigestingWriter<W> {
222 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
223 let written = self.inner.write(buf)?;
224 self.hasher.update(&buf[..written]);
225 self.written += written as u64;
226 Ok(written)
227 }
228
229 fn flush(&mut self) -> io::Result<()> {
230 self.inner.flush()
231 }
232}
233
234impl Drop for StagedLayerGuard {
235 fn drop(&mut self) {
236 if !self.cleanup_on_drop {
237 return;
238 }
239
240 for path in self.paths.values() {
241 let _ = std::fs::remove_file(path);
242 }
243 }
244}
245
246pub async fn load_archive(
252 cache_dir: &Path,
253 input: &Path,
254 options: ImageLoadOptions,
255) -> ImageResult<Vec<LoadedImage>> {
256 let cache_dir_for_blocking = cache_dir.to_path_buf();
257 let input = input.to_path_buf();
258 let prepared = tokio::task::spawn_blocking(move || {
259 load_archive_blocking(&cache_dir_for_blocking, &input, options)
260 })
261 .await
262 .map_err(|e| ImageError::Io(io::Error::other(e)))??;
263
264 let cache = GlobalCache::new_async(cache_dir).await?;
265 let registry = Registry::new(Platform::host_linux(), cache)?;
266 let PreparedArchiveLoad {
267 images,
268 staged_layers,
269 } = prepared;
270 let cleanup_paths = staged_layers.values().cloned().collect::<Vec<_>>();
271 let staged_layers = Arc::new(staged_layers);
272 let cache = GlobalCache::new_async(cache_dir).await?;
273 let mut loaded = Vec::with_capacity(images.len());
274
275 let result = async {
276 for image in images {
277 let reference: Reference = image
278 .reference
279 .parse()
280 .map_err(|e| ImageError::ManifestParse(format!("invalid image reference: {e}")))?;
281
282 registry
283 .materialize_cached_layers_from_paths(
284 &reference,
285 &image.metadata,
286 false,
287 Arc::clone(&staged_layers),
288 )
289 .await?;
290
291 cache
292 .write_image_metadata_async(&reference, &image.metadata)
293 .await?;
294
295 loaded.push(LoadedImage {
296 reference: image.reference,
297 metadata: image.metadata,
298 });
299 }
300
301 Ok(loaded)
302 }
303 .await;
304
305 for path in cleanup_paths {
306 let _ = tokio::fs::remove_file(path).await;
307 }
308
309 result
310}
311
312pub fn save_docker_archive(
314 cache: &GlobalCache,
315 output: &Path,
316 images: &[ImageSaveRequest],
317) -> ImageResult<()> {
318 save_archive(cache, output, images, ImageArchiveFormat::Docker)
319}
320
321pub fn save_archive(
323 cache: &GlobalCache,
324 output: &Path,
325 images: &[ImageSaveRequest],
326 format: ImageArchiveFormat,
327) -> ImageResult<()> {
328 match format {
329 ImageArchiveFormat::Docker => save_docker_archive_inner(cache, output, images),
330 ImageArchiveFormat::Oci => save_oci_archive_inner(cache, output, images),
331 }
332}
333
334fn save_docker_archive_inner(
335 cache: &GlobalCache,
336 output: &Path,
337 images: &[ImageSaveRequest],
338) -> ImageResult<()> {
339 if images.is_empty() {
340 return Err(ImageError::ManifestParse(
341 "at least one image reference is required".into(),
342 ));
343 }
344
345 let output_file = File::create(output).map_err(|e| ImageError::Cache {
346 path: output.to_path_buf(),
347 source: e,
348 })?;
349 let mut archive = tar::Builder::new(BufWriter::new(output_file));
350 let mut generated_layers: HashMap<String, GeneratedLayer> = HashMap::new();
351 let mut appended_layers: HashSet<String> = HashSet::new();
352 let mut manifest_entries = Vec::with_capacity(images.len());
353 let mut config_entries = Vec::with_capacity(images.len());
354
355 for image in images {
356 let mut layer_paths = Vec::with_capacity(image.layers.len());
357 let mut regenerated_diff_ids = Vec::with_capacity(image.layers.len());
358
359 for layer in &image.layers {
360 let generated = match generated_layers.get(&layer.diff_id) {
361 Some(generated) => generated,
362 None => {
363 let generated = generate_layer_tar(cache, layer)?;
364 generated_layers.insert(layer.diff_id.clone(), generated);
365 generated_layers.get(&layer.diff_id).unwrap()
366 }
367 };
368
369 regenerated_diff_ids.push(generated.diff_id.clone());
370 layer_paths.push(format!("{}/layer.tar", generated.hex));
371 }
372
373 let config_bytes =
374 docker_config_json(&image.config, &image.raw_config_json, ®enerated_diff_ids)?;
375 let config_hex = sha256_hex(&config_bytes);
376 let config_name = format!("{config_hex}.json");
377
378 config_entries.push((config_name.clone(), config_bytes));
379
380 manifest_entries.push(DockerManifestOut {
381 config: config_name,
382 repo_tags: vec![image.reference.clone()],
383 layers: layer_paths,
384 });
385 }
386
387 let manifest_bytes = serde_json::to_vec_pretty(&manifest_entries)
388 .map_err(|e| ImageError::ConfigParse(format!("serialize docker manifest: {e}")))?;
389 append_bytes(&mut archive, "manifest.json", &manifest_bytes)?;
390
391 for (config_name, config_bytes) in config_entries {
392 append_bytes(&mut archive, &config_name, &config_bytes)?;
393 }
394
395 for image in images {
396 for layer in &image.layers {
397 let generated = generated_layers.get(&layer.diff_id).ok_or_else(|| {
398 ImageError::ManifestParse(format!("missing generated layer {}", layer.diff_id))
399 })?;
400 if appended_layers.insert(generated.hex.clone()) {
401 append_layer_entries(&mut archive, generated)?;
402 }
403 }
404 }
405
406 archive.finish().map_err(ImageError::Io)?;
407
408 for layer in generated_layers.values() {
409 let _ = std::fs::remove_file(&layer.path);
410 }
411
412 Ok(())
413}
414
415fn save_oci_archive_inner(
416 cache: &GlobalCache,
417 output: &Path,
418 images: &[ImageSaveRequest],
419) -> ImageResult<()> {
420 if images.is_empty() {
421 return Err(ImageError::ManifestParse(
422 "at least one image reference is required".into(),
423 ));
424 }
425
426 let output_file = File::create(output).map_err(|e| ImageError::Cache {
427 path: output.to_path_buf(),
428 source: e,
429 })?;
430 let mut archive = tar::Builder::new(BufWriter::new(output_file));
431 let mut generated_layers: HashMap<String, GeneratedLayer> = HashMap::new();
432 let mut appended_metadata_blobs: HashSet<String> = HashSet::new();
433 let mut appended_layer_blobs: HashSet<String> = HashSet::new();
434 let mut layer_blob_order = Vec::new();
435 let mut metadata_blobs = Vec::new();
436 let mut index_manifests = Vec::with_capacity(images.len());
437
438 for image in images {
439 let mut layer_descriptors = Vec::with_capacity(image.layers.len());
440 let mut regenerated_diff_ids = Vec::with_capacity(image.layers.len());
441
442 for layer in &image.layers {
443 let generated = match generated_layers.get(&layer.diff_id) {
444 Some(generated) => generated,
445 None => {
446 let generated = generate_layer_tar(cache, layer)?;
447 generated_layers.insert(layer.diff_id.clone(), generated);
448 generated_layers.get(&layer.diff_id).unwrap()
449 }
450 };
451
452 regenerated_diff_ids.push(generated.diff_id.clone());
453 if appended_layer_blobs.insert(generated.hex.clone()) {
454 layer_blob_order.push(layer.diff_id.clone());
455 }
456 layer_descriptors.push(serde_json::json!({
457 "mediaType": OCI_LAYER_MEDIA_TYPE,
458 "digest": generated.diff_id,
459 "size": generated.size,
460 }));
461 }
462
463 let config_bytes =
464 docker_config_json(&image.config, &image.raw_config_json, ®enerated_diff_ids)?;
465 let config_hex = sha256_hex(&config_bytes);
466 if appended_metadata_blobs.insert(config_hex.clone()) {
467 metadata_blobs.push((config_hex.clone(), config_bytes.clone()));
468 }
469
470 let manifest_bytes = serde_json::to_vec(&serde_json::json!({
471 "schemaVersion": 2,
472 "mediaType": OCI_MANIFEST_MEDIA_TYPE,
473 "config": {
474 "mediaType": OCI_CONFIG_MEDIA_TYPE,
475 "digest": format!("sha256:{config_hex}"),
476 "size": config_bytes.len(),
477 },
478 "layers": layer_descriptors,
479 }))
480 .map_err(|e| ImageError::ManifestParse(format!("serialize OCI manifest: {e}")))?;
481 let manifest_hex = sha256_hex(&manifest_bytes);
482 if appended_metadata_blobs.insert(manifest_hex.clone()) {
483 metadata_blobs.push((manifest_hex.clone(), manifest_bytes.clone()));
484 }
485
486 index_manifests.push(serde_json::json!({
487 "mediaType": OCI_MANIFEST_MEDIA_TYPE,
488 "digest": format!("sha256:{manifest_hex}"),
489 "size": manifest_bytes.len(),
490 "platform": {
491 "architecture": image.config.architecture.as_deref().unwrap_or("amd64"),
492 "os": image.config.os.as_deref().unwrap_or("linux"),
493 },
494 "annotations": {
495 (OCI_REF_NAME_ANNOTATION): image.reference.clone(),
496 },
497 }));
498 }
499
500 let index_bytes = serde_json::to_vec_pretty(&serde_json::json!({
501 "schemaVersion": 2,
502 "mediaType": OCI_INDEX_MEDIA_TYPE,
503 "manifests": index_manifests,
504 }))
505 .map_err(|e| ImageError::ManifestParse(format!("serialize OCI index: {e}")))?;
506
507 append_bytes(
508 &mut archive,
509 "oci-layout",
510 br#"{"imageLayoutVersion":"1.0.0"}"#,
511 )?;
512 append_bytes(&mut archive, "index.json", &index_bytes)?;
513 append_directory(&mut archive, "blobs")?;
514 append_directory(&mut archive, "blobs/sha256")?;
515
516 for (hex, bytes) in metadata_blobs {
517 append_blob_bytes(&mut archive, &hex, &bytes)?;
518 }
519
520 for diff_id in layer_blob_order {
521 let generated = generated_layers.get(&diff_id).ok_or_else(|| {
522 ImageError::ManifestParse(format!("missing generated layer {diff_id}"))
523 })?;
524 append_blob_file(
525 &mut archive,
526 &generated.hex,
527 &generated.path,
528 generated.size,
529 )?;
530 }
531
532 archive.finish().map_err(ImageError::Io)?;
533
534 for layer in generated_layers.values() {
535 let _ = std::fs::remove_file(&layer.path);
536 }
537
538 Ok(())
539}
540
541fn load_archive_blocking(
542 cache_dir: &Path,
543 input: &Path,
544 options: ImageLoadOptions,
545) -> ImageResult<PreparedArchiveLoad> {
546 if let Some(manifest_json) = read_archive_entry(input, "manifest.json")? {
547 let manifest: Vec<DockerManifestEntry> = serde_json::from_slice(&manifest_json)
548 .map_err(|e| ImageError::ManifestParse(format!("docker manifest.json: {e}")))?;
549 return load_docker_archive_blocking(cache_dir, input, options, manifest);
550 }
551
552 if read_archive_entry(input, "oci-layout")?.is_some() {
553 return load_oci_archive_blocking(cache_dir, input, options);
554 }
555
556 Err(ImageError::ManifestParse(
557 "archive missing manifest.json or oci-layout".into(),
558 ))
559}
560
561fn load_docker_archive_blocking(
562 cache_dir: &Path,
563 input: &Path,
564 options: ImageLoadOptions,
565 manifest: Vec<DockerManifestEntry>,
566) -> ImageResult<PreparedArchiveLoad> {
567 let cache = GlobalCache::new(cache_dir)?;
568 if manifest.is_empty() {
569 return Err(ImageError::ManifestParse(
570 "docker archive manifest is empty".into(),
571 ));
572 }
573
574 let required_configs = manifest
575 .iter()
576 .map(|image| image.config.clone())
577 .collect::<HashSet<_>>();
578 let required_layers = manifest
579 .iter()
580 .flat_map(|image| image.layers.iter().cloned())
581 .collect::<HashSet<_>>();
582 let file = File::open(input).map_err(|e| ImageError::Cache {
583 path: input.to_path_buf(),
584 source: e,
585 })?;
586 let mut archive = tar::Archive::new(file);
587 let mut configs: HashMap<String, Vec<u8>> = HashMap::new();
588 let mut layers: HashMap<String, LayerBlobInfo> = HashMap::new();
589 let mut staged_layers = StagedLayerGuard::new();
590 let mut temp_counter = 0u64;
591 let mut entry_count = 0u64;
592
593 for entry in archive.entries().map_err(ImageError::Io)? {
594 let mut entry = entry.map_err(ImageError::Io)?;
595 entry_count += 1;
596 enforce_archive_entry_count(entry_count)?;
597 let path = normalized_archive_path(&entry)?;
598
599 if required_configs.contains(&path) {
600 let data = read_entry_to_vec(&mut entry, &path, ARCHIVE_METADATA_MAX_BYTES)?;
601 configs.insert(path, data);
602 continue;
603 }
604
605 if required_layers.contains(&path) {
606 let mut info = extract_layer_blob(&cache, &path, &mut entry, temp_counter)?;
607 temp_counter += 1;
608 info.path = staged_layers.track(info.digest.clone(), info.path);
609 verify_docker_layer_path_digest(&path, &info.digest)?;
610 layers.insert(path, info);
611 continue;
612 }
613 }
614
615 let mut loaded = Vec::new();
616 for (image_index, image) in manifest.into_iter().enumerate() {
617 let config_bytes = configs.get(&image.config).ok_or_else(|| {
618 ImageError::ConfigParse(format!("docker archive missing config {}", image.config))
619 })?;
620 let (config, diff_ids) = ImageConfig::parse(config_bytes)?;
621
622 if diff_ids.len() != image.layers.len() {
623 return Err(ImageError::ManifestParse(format!(
624 "layer count mismatch: config has {} diff_ids but archive manifest has {} layers",
625 diff_ids.len(),
626 image.layers.len()
627 )));
628 }
629
630 let config_digest = format!("sha256:{}", sha256_hex(config_bytes));
631 let mut layer_metadata = Vec::with_capacity(image.layers.len());
632 let mut manifest_layers = Vec::with_capacity(image.layers.len());
633
634 for (position, layer_path) in image.layers.iter().enumerate() {
635 let layer = layers.get(layer_path).ok_or_else(|| {
636 ImageError::ManifestParse(format!("docker archive missing layer {layer_path}"))
637 })?;
638 let diff_id = diff_ids[position].clone();
639 layer_metadata.push(CachedLayerMetadata {
640 digest: layer.digest.clone(),
641 media_type: Some(layer.media_type.clone()),
642 size_bytes: Some(layer.size_bytes),
643 diff_id,
644 });
645 manifest_layers.push(serde_json::json!({
646 "mediaType": layer.media_type,
647 "digest": layer.digest,
648 "size": layer.size_bytes,
649 }));
650 }
651
652 let manifest_bytes = serde_json::to_vec(&serde_json::json!({
653 "schemaVersion": 2,
654 "mediaType": OCI_MANIFEST_MEDIA_TYPE,
655 "config": {
656 "mediaType": OCI_CONFIG_MEDIA_TYPE,
657 "digest": config_digest,
658 "size": config_bytes.len(),
659 },
660 "layers": manifest_layers,
661 }))
662 .map_err(|e| ImageError::ManifestParse(format!("serialize manifest: {e}")))?;
663 let manifest_digest = format!("sha256:{}", sha256_hex(&manifest_bytes));
664
665 let metadata = CachedImageMetadata {
666 manifest_digest,
667 config_digest,
668 raw_manifest_json: json_bytes_to_string(&manifest_bytes, "docker manifest")?,
669 raw_config_json: json_bytes_to_string(config_bytes, "docker config")?,
670 config,
671 layers: layer_metadata,
672 };
673
674 let mut refs = image
675 .repo_tags
676 .unwrap_or_default()
677 .into_iter()
678 .filter(|tag| tag != "<none>:<none>")
679 .collect::<Vec<_>>();
680
681 if image_index == 0 {
682 refs.extend(options.tags.iter().cloned());
683 }
684
685 refs.sort();
686 refs.dedup();
687
688 if refs.is_empty() {
689 return Err(ImageError::ManifestParse(
690 "docker archive image has no tags; pass --tag to name it".into(),
691 ));
692 }
693
694 for reference in refs {
695 let _: Reference = reference.parse().map_err(|e| {
696 ImageError::ManifestParse(format!("invalid image reference {reference}: {e}"))
697 })?;
698 loaded.push(PreparedLoadedImage {
699 reference,
700 metadata: metadata.clone(),
701 });
702 }
703 }
704
705 Ok(PreparedArchiveLoad {
706 images: loaded,
707 staged_layers: staged_layers.into_inner(),
708 })
709}
710
711fn load_oci_archive_blocking(
712 cache_dir: &Path,
713 input: &Path,
714 options: ImageLoadOptions,
715) -> ImageResult<PreparedArchiveLoad> {
716 let cache = GlobalCache::new(cache_dir)?;
717 let layout_json = read_archive_entry(input, "oci-layout")?
718 .ok_or_else(|| ImageError::ManifestParse("OCI layout missing oci-layout".into()))?;
719 serde_json::from_slice::<oci_spec::image::OciLayout>(&layout_json)
720 .map_err(|e| ImageError::ManifestParse(format!("oci-layout: {e}")))?;
721
722 let index_json = read_archive_entry(input, "index.json")?
723 .ok_or_else(|| ImageError::ManifestParse("OCI layout missing index.json".into()))?;
724 let index: oci_spec::image::ImageIndex = serde_json::from_slice(&index_json)
725 .map_err(|e| ImageError::ManifestParse(format!("OCI index.json: {e}")))?;
726 let manifest_descriptors = selectable_oci_manifests(index.manifests())?;
727 if manifest_descriptors.is_empty() {
728 return Err(ImageError::ManifestParse(
729 "OCI layout contains no image manifests for the host platform".into(),
730 ));
731 }
732
733 let manifest_paths = manifest_descriptors
734 .iter()
735 .map(|descriptor| blob_path_from_digest(descriptor.digest().as_ref()))
736 .collect::<ImageResult<HashSet<_>>>()?;
737 let manifest_blobs = read_archive_entries(input, &manifest_paths)?;
738 let mut manifests = Vec::with_capacity(manifest_descriptors.len());
739 let mut required_configs = HashSet::new();
740 let mut required_layers = HashSet::new();
741
742 for descriptor in &manifest_descriptors {
743 let manifest_path = blob_path_from_digest(descriptor.digest().as_ref())?;
744 let manifest_bytes = manifest_blobs.get(&manifest_path).ok_or_else(|| {
745 ImageError::ManifestParse(format!("OCI layout missing manifest blob {manifest_path}"))
746 })?;
747 verify_descriptor_blob(descriptor, manifest_bytes)?;
748 let manifest: oci_spec::image::ImageManifest = serde_json::from_slice(manifest_bytes)
749 .map_err(|e| ImageError::ManifestParse(format!("OCI image manifest: {e}")))?;
750
751 required_configs.insert(blob_path_from_digest(manifest.config().digest().as_ref())?);
752 for layer in manifest.layers() {
753 required_layers.insert(blob_path_from_digest(layer.digest().as_ref())?);
754 }
755 manifests.push((descriptor.clone(), manifest, manifest_bytes.clone()));
756 }
757
758 let file = File::open(input).map_err(|e| ImageError::Cache {
759 path: input.to_path_buf(),
760 source: e,
761 })?;
762 let mut archive = tar::Archive::new(file);
763 let mut configs: HashMap<String, Vec<u8>> = HashMap::new();
764 let mut layers: HashMap<String, LayerBlobInfo> = HashMap::new();
765 let mut staged_layers = StagedLayerGuard::new();
766 let mut temp_counter = 0u64;
767 let mut entry_count = 0u64;
768
769 for entry in archive.entries().map_err(ImageError::Io)? {
770 let mut entry = entry.map_err(ImageError::Io)?;
771 entry_count += 1;
772 enforce_archive_entry_count(entry_count)?;
773 let path = normalized_archive_path(&entry)?;
774
775 if required_configs.contains(&path) {
776 let data = read_entry_to_vec(&mut entry, &path, ARCHIVE_METADATA_MAX_BYTES)?;
777 configs.insert(path, data);
778 continue;
779 }
780
781 if required_layers.contains(&path) {
782 let mut info = extract_layer_blob(&cache, &path, &mut entry, temp_counter)?;
783 temp_counter += 1;
784 info.path = staged_layers.track(info.digest.clone(), info.path);
785 layers.insert(path, info);
786 continue;
787 }
788 }
789
790 let mut loaded = Vec::new();
791 for (image_index, (descriptor, manifest, manifest_bytes)) in manifests.into_iter().enumerate() {
792 let config_path = blob_path_from_digest(manifest.config().digest().as_ref())?;
793 let config_bytes = configs.get(&config_path).ok_or_else(|| {
794 ImageError::ConfigParse(format!("OCI layout missing config blob {config_path}"))
795 })?;
796 verify_descriptor_blob(manifest.config(), config_bytes)?;
797 let (config, diff_ids) = ImageConfig::parse(config_bytes)?;
798
799 if diff_ids.len() != manifest.layers().len() {
800 return Err(ImageError::ManifestParse(format!(
801 "layer count mismatch: config has {} diff_ids but OCI manifest has {} layers",
802 diff_ids.len(),
803 manifest.layers().len()
804 )));
805 }
806
807 let mut layer_metadata = Vec::with_capacity(manifest.layers().len());
808 for (position, layer_descriptor) in manifest.layers().iter().enumerate() {
809 let layer_path = blob_path_from_digest(layer_descriptor.digest().as_ref())?;
810 let layer = layers.get(&layer_path).ok_or_else(|| {
811 ImageError::ManifestParse(format!("OCI layout missing layer blob {layer_path}"))
812 })?;
813 verify_layer_descriptor(layer_descriptor, layer)?;
814 layer_metadata.push(CachedLayerMetadata {
815 digest: layer.digest.clone(),
816 media_type: Some(layer.media_type.clone()),
817 size_bytes: Some(layer.size_bytes),
818 diff_id: diff_ids[position].clone(),
819 });
820 }
821
822 let metadata = CachedImageMetadata {
823 manifest_digest: format!("sha256:{}", sha256_hex(&manifest_bytes)),
824 config_digest: manifest.config().digest().to_string(),
825 raw_manifest_json: json_bytes_to_string(&manifest_bytes, "OCI manifest")?,
826 raw_config_json: json_bytes_to_string(config_bytes, "OCI config")?,
827 config,
828 layers: layer_metadata,
829 };
830
831 let mut refs = descriptor
832 .annotations()
833 .as_ref()
834 .and_then(|annotations| annotations.get(OCI_REF_NAME_ANNOTATION))
835 .cloned()
836 .into_iter()
837 .collect::<Vec<_>>();
838
839 if image_index == 0 {
840 refs.extend(options.tags.iter().cloned());
841 }
842
843 refs.sort();
844 refs.dedup();
845
846 if refs.is_empty() {
847 return Err(ImageError::ManifestParse(
848 "OCI layout image has no ref.name annotation; pass --tag to name it".into(),
849 ));
850 }
851
852 for reference in refs {
853 let _: Reference = reference.parse().map_err(|e| {
854 ImageError::ManifestParse(format!("invalid image reference {reference}: {e}"))
855 })?;
856 loaded.push(PreparedLoadedImage {
857 reference,
858 metadata: metadata.clone(),
859 });
860 }
861 }
862
863 Ok(PreparedArchiveLoad {
864 images: loaded,
865 staged_layers: staged_layers.into_inner(),
866 })
867}
868
869fn read_archive_entry(input: &Path, wanted_path: &str) -> ImageResult<Option<Vec<u8>>> {
870 let file = File::open(input).map_err(|e| ImageError::Cache {
871 path: input.to_path_buf(),
872 source: e,
873 })?;
874 let mut archive = tar::Archive::new(file);
875 let mut entry_count = 0u64;
876
877 for entry in archive.entries().map_err(ImageError::Io)? {
878 let mut entry = entry.map_err(ImageError::Io)?;
879 entry_count += 1;
880 enforce_archive_entry_count(entry_count)?;
881 let path = normalized_archive_path(&entry)?;
882 if path != wanted_path {
883 continue;
884 }
885
886 let data = read_entry_to_vec(&mut entry, &path, ARCHIVE_METADATA_MAX_BYTES)?;
887 return Ok(Some(data));
888 }
889
890 Ok(None)
891}
892
893fn read_archive_entries(
894 input: &Path,
895 wanted_paths: &HashSet<String>,
896) -> ImageResult<HashMap<String, Vec<u8>>> {
897 let file = File::open(input).map_err(|e| ImageError::Cache {
898 path: input.to_path_buf(),
899 source: e,
900 })?;
901 let mut archive = tar::Archive::new(file);
902 let mut entries = HashMap::new();
903 let mut entry_count = 0u64;
904
905 for entry in archive.entries().map_err(ImageError::Io)? {
906 let mut entry = entry.map_err(ImageError::Io)?;
907 entry_count += 1;
908 enforce_archive_entry_count(entry_count)?;
909 let path = normalized_archive_path(&entry)?;
910 if !wanted_paths.contains(&path) {
911 continue;
912 }
913
914 let data = read_entry_to_vec(&mut entry, &path, ARCHIVE_METADATA_MAX_BYTES)?;
915 entries.insert(path, data);
916 if entries.len() == wanted_paths.len() {
917 break;
918 }
919 }
920
921 Ok(entries)
922}
923
924fn selectable_oci_manifests(
925 descriptors: &[oci_spec::image::Descriptor],
926) -> ImageResult<Vec<oci_spec::image::Descriptor>> {
927 let host = Platform::host_linux();
928 let selected = descriptors
929 .iter()
930 .filter(|descriptor| is_oci_image_manifest_descriptor(descriptor))
931 .filter(|descriptor| descriptor_matches_platform(descriptor, &host))
932 .cloned()
933 .collect();
934
935 Ok(selected)
936}
937
938fn is_oci_image_manifest_descriptor(descriptor: &oci_spec::image::Descriptor) -> bool {
939 matches!(
940 descriptor.media_type(),
941 oci_spec::image::MediaType::ImageManifest
942 ) || descriptor.media_type().to_string()
943 == "application/vnd.docker.distribution.manifest.v2+json"
944}
945
946fn descriptor_matches_platform(descriptor: &oci_spec::image::Descriptor, host: &Platform) -> bool {
947 let Some(platform) = descriptor.platform() else {
948 return true;
949 };
950
951 if *platform.os() != host.os || *platform.architecture() != host.arch {
952 return false;
953 }
954
955 match (&host.variant, platform.variant()) {
956 (Some(host_variant), Some(descriptor_variant)) => host_variant == descriptor_variant,
957 (Some(_), None) => false,
958 (None, _) => true,
959 }
960}
961
962fn blob_path_from_digest(digest: &str) -> ImageResult<String> {
963 let digest: Digest = digest.parse()?;
964 Ok(format!("blobs/{}/{}", digest.algorithm(), digest.hex()))
965}
966
967fn verify_descriptor_blob(
968 descriptor: &oci_spec::image::Descriptor,
969 bytes: &[u8],
970) -> ImageResult<()> {
971 if descriptor.size() != bytes.len() as u64 {
972 return Err(ImageError::ManifestParse(format!(
973 "OCI blob {} size mismatch: descriptor has {}, archive has {}",
974 descriptor.digest(),
975 descriptor.size(),
976 bytes.len()
977 )));
978 }
979
980 verify_digest_bytes(descriptor.digest().as_ref(), bytes)
981}
982
983fn verify_layer_descriptor(
984 descriptor: &oci_spec::image::Descriptor,
985 layer: &LayerBlobInfo,
986) -> ImageResult<()> {
987 if descriptor.size() != layer.size_bytes {
988 return Err(ImageError::ManifestParse(format!(
989 "OCI layer {} size mismatch: descriptor has {}, archive has {}",
990 descriptor.digest(),
991 descriptor.size(),
992 layer.size_bytes
993 )));
994 }
995
996 if descriptor.digest().to_string() != layer.digest {
997 return Err(ImageError::ManifestParse(format!(
998 "OCI layer digest mismatch: descriptor has {}, archive has {}",
999 descriptor.digest(),
1000 layer.digest
1001 )));
1002 }
1003
1004 Ok(())
1005}
1006
1007fn verify_digest_bytes(digest: &str, bytes: &[u8]) -> ImageResult<()> {
1008 let digest: Digest = digest.parse()?;
1009 if digest.algorithm() != "sha256" {
1010 return Err(ImageError::ManifestParse(format!(
1011 "unsupported OCI digest algorithm: {}",
1012 digest.algorithm()
1013 )));
1014 }
1015
1016 let actual = sha256_hex(bytes);
1017 if actual != digest.hex() {
1018 return Err(ImageError::ManifestParse(format!(
1019 "OCI blob digest mismatch: expected {}, got sha256:{actual}",
1020 digest
1021 )));
1022 }
1023
1024 Ok(())
1025}
1026
1027fn verify_docker_layer_path_digest(path: &str, digest: &str) -> ImageResult<()> {
1028 let Some(hex) = path.strip_prefix("blobs/sha256/") else {
1029 return Ok(());
1030 };
1031 if hex.contains('/') {
1032 return Ok(());
1033 }
1034
1035 let expected = format!("sha256:{hex}");
1036 if expected != digest {
1037 return Err(ImageError::ManifestParse(format!(
1038 "docker archive layer path {path} digest mismatch: expected {expected}, got {digest}"
1039 )));
1040 }
1041
1042 Ok(())
1043}
1044
1045fn create_unique_temp_file(dir: &Path, prefix: &str, suffix: &str) -> ImageResult<(File, PathBuf)> {
1046 for _ in 0..128 {
1047 let id = TEMP_FILE_COUNTER.fetch_add(1, Ordering::Relaxed);
1048 let path = dir.join(format!("{prefix}-{}-{id}{suffix}", std::process::id()));
1049 match OpenOptions::new().write(true).create_new(true).open(&path) {
1050 Ok(file) => return Ok((file, path)),
1051 Err(e) if e.kind() == io::ErrorKind::AlreadyExists => continue,
1052 Err(e) => {
1053 return Err(ImageError::Cache { path, source: e });
1054 }
1055 }
1056 }
1057
1058 Err(ImageError::Cache {
1059 path: dir.to_path_buf(),
1060 source: io::Error::new(
1061 io::ErrorKind::AlreadyExists,
1062 "could not allocate a unique temporary image archive file",
1063 ),
1064 })
1065}
1066
1067fn extract_layer_blob(
1068 cache: &GlobalCache,
1069 path: &str,
1070 entry: &mut tar::Entry<'_, File>,
1071 counter: u64,
1072) -> ImageResult<LayerBlobInfo> {
1073 let declared_size = entry.header().size().map_err(ImageError::Io)?;
1074 if declared_size > ARCHIVE_LAYER_MAX_BYTES {
1075 return Err(ImageError::ManifestParse(format!(
1076 "archive layer {path} is {declared_size} bytes; max is {ARCHIVE_LAYER_MAX_BYTES}"
1077 )));
1078 }
1079
1080 let (mut temp, temp_path) =
1081 create_unique_temp_file(cache.tmp_dir(), &format!("load-{counter}"), ".blob")?;
1082 let result = (|| {
1083 let mut hasher = Sha256::new();
1084 let mut size = 0u64;
1085 let mut magic = Vec::with_capacity(4);
1086 let mut buf = [0u8; 64 * 1024];
1087
1088 loop {
1089 let read = entry.read(&mut buf).map_err(ImageError::Io)?;
1090 if read == 0 {
1091 break;
1092 }
1093 if magic.len() < 4 {
1094 let take = (4 - magic.len()).min(read);
1095 magic.extend_from_slice(&buf[..take]);
1096 }
1097 hasher.update(&buf[..read]);
1098 temp.write_all(&buf[..read])
1099 .map_err(|e| ImageError::Cache {
1100 path: temp_path.clone(),
1101 source: e,
1102 })?;
1103 size += read as u64;
1104 if size > ARCHIVE_LAYER_MAX_BYTES {
1105 return Err(ImageError::ManifestParse(format!(
1106 "archive layer {path} exceeds {ARCHIVE_LAYER_MAX_BYTES} bytes"
1107 )));
1108 }
1109 }
1110 temp.flush().map_err(|e| ImageError::Cache {
1111 path: temp_path.clone(),
1112 source: e,
1113 })?;
1114 drop(temp);
1115
1116 let digest = Digest::new("sha256", hex::encode(hasher.finalize()));
1117 let staged_path = temp_path.clone();
1118
1119 let media_type = match Compression::detect(&magic) {
1120 Compression::None => OCI_LAYER_MEDIA_TYPE,
1121 Compression::Gzip => OCI_LAYER_GZIP_MEDIA_TYPE,
1122 Compression::Zstd => OCI_LAYER_ZSTD_MEDIA_TYPE,
1123 };
1124
1125 tracing::debug!(path, digest = %digest, size, "loaded layer blob from docker archive");
1126
1127 Ok(LayerBlobInfo {
1128 digest: digest.to_string(),
1129 media_type: media_type.to_string(),
1130 size_bytes: size,
1131 path: staged_path,
1132 })
1133 })();
1134
1135 if result.is_err() {
1136 let _ = std::fs::remove_file(&temp_path);
1137 }
1138
1139 result
1140}
1141
1142fn generate_layer_tar(cache: &GlobalCache, layer: &ImageSaveLayer) -> ImageResult<GeneratedLayer> {
1143 let diff_id: Digest = layer.diff_id.parse()?;
1144 let erofs_path = cache.layer_erofs_path(&diff_id);
1145 let file = File::open(&erofs_path).map_err(|e| ImageError::Cache {
1146 path: erofs_path.clone(),
1147 source: e,
1148 })?;
1149 let mut reader = ErofsReader::new(file).map_err(ImageError::Io)?;
1150 let (temp_file, temp_path) = create_unique_temp_file(cache.tmp_dir(), "save", ".layer.tar")?;
1151 let result = (|| {
1152 let digesting = DigestingWriter::new(BufWriter::new(temp_file));
1153 let mut builder = tar::Builder::new(digesting);
1154 let mut hardlinks: HashMap<u32, PathBuf> = HashMap::new();
1155
1156 reader.walk_entries::<ImageError, _>(|reader, entry| {
1157 if entry.path.as_os_str().is_empty() {
1158 return Ok(());
1159 }
1160
1161 if entry.kind == ErofsEntryKind::CharDevice && entry.rdev == Some((0, 0)) {
1162 append_whiteout(&mut builder, &entry)?;
1163 return Ok(());
1164 }
1165
1166 append_erofs_entry(&mut builder, reader, &entry, &mut hardlinks)?;
1167
1168 if entry.kind == ErofsEntryKind::Directory && entry.is_opaque() {
1169 append_opaque_marker(&mut builder, &entry)?;
1170 }
1171 Ok(())
1172 })?;
1173
1174 let digesting = builder.into_inner().map_err(ImageError::Io)?;
1175 let (mut file, hex, size) = digesting.finish();
1176 file.flush().map_err(|e| ImageError::Cache {
1177 path: temp_path.clone(),
1178 source: e,
1179 })?;
1180
1181 Ok(GeneratedLayer {
1182 diff_id: format!("sha256:{hex}"),
1183 hex,
1184 path: temp_path.clone(),
1185 size,
1186 })
1187 })();
1188
1189 if result.is_err() {
1190 let _ = std::fs::remove_file(&temp_path);
1191 }
1192
1193 result
1194}
1195
1196fn append_erofs_entry<W: Write>(
1197 builder: &mut tar::Builder<DigestingWriter<W>>,
1198 reader: &mut ErofsReader,
1199 entry: &crate::erofs::ErofsTreeEntry,
1200 hardlinks: &mut HashMap<u32, PathBuf>,
1201) -> ImageResult<()> {
1202 let mut header = tar::Header::new_gnu();
1203 apply_header_metadata(&mut header, entry);
1204
1205 match entry.kind {
1206 ErofsEntryKind::RegularFile => {
1207 if let Some(first_path) = hardlinks.get(&entry.nid) {
1208 header.set_entry_type(tar::EntryType::Link);
1209 header.set_size(0);
1210 header.set_link_name(first_path).map_err(ImageError::Io)?;
1211 header.set_cksum();
1212 builder
1213 .append_data(&mut header, &entry.path, io::empty())
1214 .map_err(ImageError::Io)?;
1215 return Ok(());
1216 }
1217
1218 hardlinks.insert(entry.nid, entry.path.clone());
1219 header.set_entry_type(tar::EntryType::Regular);
1220 header.set_size(entry.size);
1221 header.set_cksum();
1222 let mut data = reader.file_data_reader(entry.nid).map_err(ImageError::Io)?;
1223 builder
1224 .append_data(&mut header, &entry.path, &mut data)
1225 .map_err(ImageError::Io)?;
1226 }
1227 ErofsEntryKind::Directory => {
1228 header.set_entry_type(tar::EntryType::Directory);
1229 header.set_size(0);
1230 header.set_cksum();
1231 builder
1232 .append_data(&mut header, &entry.path, io::empty())
1233 .map_err(ImageError::Io)?;
1234 }
1235 ErofsEntryKind::Symlink => {
1236 header.set_entry_type(tar::EntryType::Symlink);
1237 header.set_size(0);
1238 let target = reader.read_link_by_nid(entry.nid).map_err(ImageError::Io)?;
1239 header
1240 .set_link_name_literal(target)
1241 .map_err(ImageError::Io)?;
1242 header.set_cksum();
1243 builder
1244 .append_data(&mut header, &entry.path, io::empty())
1245 .map_err(ImageError::Io)?;
1246 }
1247 ErofsEntryKind::CharDevice | ErofsEntryKind::BlockDevice => {
1248 header.set_entry_type(if entry.kind == ErofsEntryKind::CharDevice {
1249 tar::EntryType::Char
1250 } else {
1251 tar::EntryType::Block
1252 });
1253 header.set_size(0);
1254 if let Some((major, minor)) = entry.rdev {
1255 header.set_device_major(major).map_err(ImageError::Io)?;
1256 header.set_device_minor(minor).map_err(ImageError::Io)?;
1257 }
1258 header.set_cksum();
1259 builder
1260 .append_data(&mut header, &entry.path, io::empty())
1261 .map_err(ImageError::Io)?;
1262 }
1263 ErofsEntryKind::Fifo => {
1264 header.set_entry_type(tar::EntryType::Fifo);
1265 header.set_size(0);
1266 header.set_cksum();
1267 builder
1268 .append_data(&mut header, &entry.path, io::empty())
1269 .map_err(ImageError::Io)?;
1270 }
1271 ErofsEntryKind::Socket => {
1272 header.set_entry_type(tar::EntryType::new(0o140));
1273 header.set_size(0);
1274 header.set_cksum();
1275 builder
1276 .append_data(&mut header, &entry.path, io::empty())
1277 .map_err(ImageError::Io)?;
1278 }
1279 }
1280
1281 Ok(())
1282}
1283
1284fn append_whiteout<W: Write>(
1285 builder: &mut tar::Builder<DigestingWriter<W>>,
1286 entry: &crate::erofs::ErofsTreeEntry,
1287) -> ImageResult<()> {
1288 let Some(file_name) = entry.path.file_name() else {
1289 return Ok(());
1290 };
1291 let mut path = entry.path.clone();
1292 let mut whiteout_name = b".wh.".to_vec();
1293 whiteout_name.extend_from_slice(file_name.as_bytes());
1294 path.set_file_name(OsString::from_vec(whiteout_name));
1295 append_empty_file(builder, &path, entry)
1296}
1297
1298fn append_opaque_marker<W: Write>(
1299 builder: &mut tar::Builder<DigestingWriter<W>>,
1300 entry: &crate::erofs::ErofsTreeEntry,
1301) -> ImageResult<()> {
1302 let path = entry.path.join(".wh..wh..opq");
1303 append_empty_file(builder, &path, entry)
1304}
1305
1306fn append_empty_file<W: Write>(
1307 builder: &mut tar::Builder<DigestingWriter<W>>,
1308 path: &Path,
1309 entry: &crate::erofs::ErofsTreeEntry,
1310) -> ImageResult<()> {
1311 let mut header = tar::Header::new_gnu();
1312 apply_header_metadata(&mut header, entry);
1313 header.set_mode(0o000);
1314 header.set_entry_type(tar::EntryType::Regular);
1315 header.set_size(0);
1316 header.set_cksum();
1317 builder
1318 .append_data(&mut header, path, io::empty())
1319 .map_err(ImageError::Io)
1320}
1321
1322fn append_layer_entries<W: Write>(
1323 archive: &mut tar::Builder<W>,
1324 layer: &GeneratedLayer,
1325) -> ImageResult<()> {
1326 append_bytes(archive, &format!("{}/VERSION", layer.hex), b"1.0\n")?;
1327 append_bytes(archive, &format!("{}/json", layer.hex), b"{}")?;
1328
1329 let mut file = File::open(&layer.path).map_err(|e| ImageError::Cache {
1330 path: layer.path.clone(),
1331 source: e,
1332 })?;
1333 let mut header = tar::Header::new_gnu();
1334 header.set_entry_type(tar::EntryType::Regular);
1335 header.set_mode(0o644);
1336 header.set_uid(0);
1337 header.set_gid(0);
1338 header.set_mtime(0);
1339 header.set_size(layer.size);
1340 header.set_cksum();
1341 archive
1342 .append_data(&mut header, format!("{}/layer.tar", layer.hex), &mut file)
1343 .map_err(ImageError::Io)
1344}
1345
1346fn append_blob_file<W: Write>(
1347 archive: &mut tar::Builder<W>,
1348 hex: &str,
1349 path: &Path,
1350 size: u64,
1351) -> ImageResult<()> {
1352 let mut file = File::open(path).map_err(|e| ImageError::Cache {
1353 path: path.to_path_buf(),
1354 source: e,
1355 })?;
1356 let mut header = tar::Header::new_gnu();
1357 header.set_entry_type(tar::EntryType::Regular);
1358 header.set_mode(0o644);
1359 header.set_uid(0);
1360 header.set_gid(0);
1361 header.set_mtime(0);
1362 header.set_size(size);
1363 header.set_cksum();
1364 archive
1365 .append_data(&mut header, format!("blobs/sha256/{hex}"), &mut file)
1366 .map_err(ImageError::Io)
1367}
1368
1369fn append_blob_bytes<W: Write>(
1370 archive: &mut tar::Builder<W>,
1371 hex: &str,
1372 bytes: &[u8],
1373) -> ImageResult<()> {
1374 append_bytes(archive, &format!("blobs/sha256/{hex}"), bytes)
1375}
1376
1377fn append_directory<W: Write>(archive: &mut tar::Builder<W>, path: &str) -> ImageResult<()> {
1378 let mut header = tar::Header::new_gnu();
1379 header.set_entry_type(tar::EntryType::Directory);
1380 header.set_mode(0o755);
1381 header.set_uid(0);
1382 header.set_gid(0);
1383 header.set_mtime(0);
1384 header.set_size(0);
1385 header.set_cksum();
1386 archive
1387 .append_data(&mut header, path, io::empty())
1388 .map_err(ImageError::Io)
1389}
1390
1391fn append_bytes<W: Write>(
1392 archive: &mut tar::Builder<W>,
1393 path: &str,
1394 bytes: &[u8],
1395) -> ImageResult<()> {
1396 let mut header = tar::Header::new_gnu();
1397 header.set_entry_type(tar::EntryType::Regular);
1398 header.set_mode(0o644);
1399 header.set_uid(0);
1400 header.set_gid(0);
1401 header.set_mtime(0);
1402 header.set_size(bytes.len() as u64);
1403 header.set_cksum();
1404 archive
1405 .append_data(&mut header, path, bytes)
1406 .map_err(ImageError::Io)
1407}
1408
1409fn enforce_archive_entry_count(count: u64) -> ImageResult<()> {
1410 if count > ARCHIVE_MAX_ENTRY_COUNT {
1411 return Err(ImageError::ManifestParse(format!(
1412 "archive has more than {ARCHIVE_MAX_ENTRY_COUNT} entries"
1413 )));
1414 }
1415
1416 Ok(())
1417}
1418
1419fn read_entry_to_vec(
1420 entry: &mut tar::Entry<'_, File>,
1421 path: &str,
1422 max_bytes: u64,
1423) -> ImageResult<Vec<u8>> {
1424 let declared_size = entry.header().size().map_err(ImageError::Io)?;
1425 if declared_size > max_bytes {
1426 return Err(ImageError::ManifestParse(format!(
1427 "archive metadata entry {path} is {declared_size} bytes; max is {max_bytes}"
1428 )));
1429 }
1430
1431 let mut data = Vec::with_capacity(declared_size as usize);
1432 entry.read_to_end(&mut data).map_err(ImageError::Io)?;
1433 Ok(data)
1434}
1435
1436fn json_bytes_to_string(bytes: &[u8], context: &str) -> ImageResult<String> {
1437 std::str::from_utf8(bytes)
1438 .map(str::to_owned)
1439 .map_err(|e| ImageError::ConfigParse(format!("{context} is not UTF-8 JSON: {e}")))
1440}
1441
1442fn docker_config_json(
1443 config: &ImageSaveConfig,
1444 raw_config_json: &str,
1445 diff_ids: &[String],
1446) -> ImageResult<Vec<u8>> {
1447 if !raw_config_json.is_empty() {
1448 let mut config_json: serde_json::Value = serde_json::from_str(raw_config_json)
1449 .map_err(|e| ImageError::ConfigParse(format!("parse raw image config: {e}")))?;
1450 let Some(object) = config_json.as_object_mut() else {
1451 return Err(ImageError::ConfigParse(
1452 "raw image config JSON is not an object".into(),
1453 ));
1454 };
1455 object.insert(
1456 "rootfs".into(),
1457 serde_json::json!({
1458 "type": "layers",
1459 "diff_ids": diff_ids,
1460 }),
1461 );
1462 object.entry("architecture").or_insert_with(|| {
1463 serde_json::json!(config.architecture.as_deref().unwrap_or("amd64"))
1464 });
1465 object
1466 .entry("os")
1467 .or_insert_with(|| serde_json::json!(config.os.as_deref().unwrap_or("linux")));
1468 return serde_json::to_vec(&config_json)
1469 .map_err(|e| ImageError::ConfigParse(format!("serialize image config: {e}")));
1470 }
1471
1472 let config_json = serde_json::json!({
1473 "architecture": config.architecture.as_deref().unwrap_or("amd64"),
1474 "os": config.os.as_deref().unwrap_or("linux"),
1475 "config": {
1476 "Env": config.env,
1477 "Entrypoint": config.entrypoint,
1478 "Cmd": config.cmd,
1479 "WorkingDir": config.working_dir,
1480 "User": config.user,
1481 "Labels": if config.labels.is_empty() {
1482 serde_json::Value::Null
1483 } else {
1484 serde_json::to_value(&config.labels)
1485 .map_err(|e| ImageError::ConfigParse(format!("serialize labels: {e}")))?
1486 },
1487 },
1488 "rootfs": {
1489 "type": "layers",
1490 "diff_ids": diff_ids,
1491 },
1492 "history": diff_ids
1493 .iter()
1494 .map(|_| serde_json::json!({"created_by": "microsandbox image save"}))
1495 .collect::<Vec<_>>(),
1496 });
1497
1498 serde_json::to_vec(&config_json)
1499 .map_err(|e| ImageError::ConfigParse(format!("serialize image config: {e}")))
1500}
1501
1502fn apply_header_metadata(header: &mut tar::Header, entry: &crate::erofs::ErofsTreeEntry) {
1503 header.set_mode((entry.metadata.mode & 0o7777) as u32);
1504 header.set_uid(entry.metadata.uid as u64);
1505 header.set_gid(entry.metadata.gid as u64);
1506 header.set_mtime(entry.metadata.mtime);
1507}
1508
1509fn normalized_archive_path(entry: &tar::Entry<'_, File>) -> ImageResult<String> {
1510 let path = entry.path().map_err(ImageError::Io)?;
1511 let bytes = path.as_os_str().as_bytes();
1512 let normalized = if let Some(stripped) = bytes.strip_prefix(b"./") {
1513 stripped
1514 } else {
1515 bytes
1516 };
1517 String::from_utf8(normalized.to_vec())
1518 .map_err(|_| ImageError::ManifestParse("archive path is not valid UTF-8".into()))
1519}
1520
1521fn sha256_hex(bytes: &[u8]) -> String {
1522 hex::encode(Sha256::digest(bytes))
1523}
1524
1525#[cfg(test)]
1530mod tests {
1531 use std::collections::BTreeMap;
1532 use std::io::Cursor;
1533
1534 use tempfile::tempdir;
1535
1536 use super::*;
1537
1538 #[test]
1539 fn docker_archive_load_save_load_roundtrip() {
1540 let runtime = tokio::runtime::Builder::new_current_thread()
1541 .enable_all()
1542 .build()
1543 .unwrap();
1544 let temp = tempdir().unwrap();
1545 let input = temp.path().join("image.tar");
1546 write_test_docker_archive(&input, "tiny:latest");
1547
1548 let first_cache = temp.path().join("cache-1");
1549 let loaded = runtime
1550 .block_on(load_archive(
1551 &first_cache,
1552 &input,
1553 ImageLoadOptions::default(),
1554 ))
1555 .unwrap();
1556
1557 assert_eq!(loaded.len(), 1);
1558 assert_eq!(loaded[0].reference, "tiny:latest");
1559
1560 let saved = temp.path().join("saved.tar");
1561 let request = save_request_from_loaded(&loaded[0]);
1562 let cache = GlobalCache::new(&first_cache).unwrap();
1563 save_docker_archive(&cache, &saved, &[request]).unwrap();
1564
1565 let second_cache = temp.path().join("cache-2");
1566 let reloaded = runtime
1567 .block_on(load_archive(
1568 &second_cache,
1569 &saved,
1570 ImageLoadOptions::default(),
1571 ))
1572 .unwrap();
1573
1574 assert_eq!(reloaded.len(), 1);
1575 assert_eq!(reloaded[0].reference, "tiny:latest");
1576 assert_eq!(
1577 reloaded[0].metadata.config.cmd,
1578 Some(vec!["cat".into(), "/hello.txt".into()])
1579 );
1580 }
1581
1582 #[test]
1583 fn docker_archive_loads_manifest_blob_paths() {
1584 let runtime = tokio::runtime::Builder::new_current_thread()
1585 .enable_all()
1586 .build()
1587 .unwrap();
1588 let temp = tempdir().unwrap();
1589 let input = temp.path().join("blob-paths.tar");
1590 write_test_docker_blob_archive_from_layer(&input, "blob-paths:latest", simple_layer_tar());
1591
1592 let loaded = runtime
1593 .block_on(load_archive(
1594 &temp.path().join("cache"),
1595 &input,
1596 ImageLoadOptions::default(),
1597 ))
1598 .unwrap();
1599
1600 assert_eq!(loaded.len(), 1);
1601 assert_eq!(loaded[0].reference, "blob-paths:latest");
1602 assert_eq!(
1603 loaded[0].metadata.config.cmd,
1604 Some(vec!["cat".into(), "/hello.txt".into()])
1605 );
1606 }
1607
1608 #[test]
1609 fn docker_archive_rejects_mismatched_blob_layer_path() {
1610 let runtime = tokio::runtime::Builder::new_current_thread()
1611 .enable_all()
1612 .build()
1613 .unwrap();
1614 let temp = tempdir().unwrap();
1615 let input = temp.path().join("bad-blob-path.tar");
1616 let layer_bytes = simple_layer_tar();
1617 let diff_id = format!("sha256:{}", sha256_hex(&layer_bytes));
1618 let config_bytes = test_config_bytes(&diff_id);
1619 let config_name = format!("blobs/sha256/{}", sha256_hex(&config_bytes));
1620 let layer_name = format!("blobs/sha256/{:064x}", 1u8);
1621
1622 write_test_docker_archive_entries(
1623 &input,
1624 "bad-blob-path:latest",
1625 config_name,
1626 layer_name,
1627 config_bytes,
1628 layer_bytes,
1629 );
1630
1631 let err = runtime
1632 .block_on(load_archive(
1633 &temp.path().join("cache"),
1634 &input,
1635 ImageLoadOptions::default(),
1636 ))
1637 .unwrap_err();
1638
1639 assert!(err.to_string().contains("digest mismatch"));
1640 }
1641
1642 #[test]
1643 fn oci_layout_archive_load_save_load_roundtrip() {
1644 let runtime = tokio::runtime::Builder::new_current_thread()
1645 .enable_all()
1646 .build()
1647 .unwrap();
1648 let temp = tempdir().unwrap();
1649 let input = temp.path().join("oci-layout.tar");
1650 write_test_oci_archive_from_layer(&input, "oci-layout:latest", simple_layer_tar());
1651
1652 let first_cache = temp.path().join("cache-1");
1653 let loaded = runtime
1654 .block_on(load_archive(
1655 &first_cache,
1656 &input,
1657 ImageLoadOptions::default(),
1658 ))
1659 .unwrap();
1660
1661 assert_eq!(loaded.len(), 1);
1662 assert_eq!(loaded[0].reference, "oci-layout:latest");
1663
1664 let saved = temp.path().join("saved-oci-layout.tar");
1665 let request = save_request_from_loaded(&loaded[0]);
1666 let cache = GlobalCache::new(&first_cache).unwrap();
1667 save_archive(&cache, &saved, &[request], ImageArchiveFormat::Oci).unwrap();
1668
1669 let index_bytes = read_archive_entry(&saved, "index.json").unwrap().unwrap();
1670 let index: oci_spec::image::ImageIndex = serde_json::from_slice(&index_bytes).unwrap();
1671 assert_eq!(index.manifests().len(), 1);
1672 assert_eq!(
1673 index.manifests()[0]
1674 .annotations()
1675 .as_ref()
1676 .unwrap()
1677 .get(OCI_REF_NAME_ANNOTATION),
1678 Some(&"oci-layout:latest".to_string())
1679 );
1680
1681 let second_cache = temp.path().join("cache-2");
1682 let reloaded = runtime
1683 .block_on(load_archive(
1684 &second_cache,
1685 &saved,
1686 ImageLoadOptions::default(),
1687 ))
1688 .unwrap();
1689
1690 assert_eq!(reloaded.len(), 1);
1691 assert_eq!(reloaded[0].reference, "oci-layout:latest");
1692 }
1693
1694 #[test]
1695 fn docker_archive_save_preserves_layer_semantics() {
1696 let runtime = tokio::runtime::Builder::new_current_thread()
1697 .enable_all()
1698 .build()
1699 .unwrap();
1700 let temp = tempdir().unwrap();
1701 let input = temp.path().join("complex.tar");
1702 let layer_bytes = complex_layer_tar();
1703 write_test_docker_archive_from_layer(&input, "complex:latest", layer_bytes);
1704
1705 let first_cache = temp.path().join("cache-1");
1706 let loaded = runtime
1707 .block_on(load_archive(
1708 &first_cache,
1709 &input,
1710 ImageLoadOptions::default(),
1711 ))
1712 .unwrap();
1713
1714 let saved = temp.path().join("saved-complex.tar");
1715 let request = save_request_from_loaded(&loaded[0]);
1716 let cache = GlobalCache::new(&first_cache).unwrap();
1717 save_docker_archive(&cache, &saved, &[request]).unwrap();
1718
1719 let entries = saved_layer_entries(&saved);
1720 let config_entry = entries.get("etc/config.txt").unwrap();
1721 let config_link_entry = entries.get("etc/config.link").unwrap();
1722 let regular_config_paths = [
1723 ("etc/config.txt", config_entry),
1724 ("etc/config.link", config_link_entry),
1725 ]
1726 .into_iter()
1727 .filter(|(_, entry)| entry.entry_type == tar::EntryType::Regular)
1728 .collect::<Vec<_>>();
1729 let hardlink_config_paths = [
1730 ("etc/config.txt", config_entry),
1731 ("etc/config.link", config_link_entry),
1732 ]
1733 .into_iter()
1734 .filter(|(_, entry)| entry.entry_type == tar::EntryType::Link)
1735 .collect::<Vec<_>>();
1736
1737 assert_eq!(regular_config_paths.len(), 1);
1738 assert_eq!(hardlink_config_paths.len(), 1);
1739 assert_eq!(regular_config_paths[0].1.data, b"shared config\n");
1740 assert_eq!(
1741 hardlink_config_paths[0].1.link_name.as_deref(),
1742 Some(regular_config_paths[0].0)
1743 );
1744 assert_eq!(regular_config_paths[0].1.mode, 0o640);
1745 assert_eq!(regular_config_paths[0].1.uid, 1000);
1746 assert_eq!(regular_config_paths[0].1.gid, 1001);
1747 assert_eq!(regular_config_paths[0].1.mtime, 42);
1748
1749 let symlink_entry = entries.get("bin/config").unwrap();
1750 assert_eq!(symlink_entry.entry_type, tar::EntryType::Symlink);
1751 assert_eq!(
1752 symlink_entry.link_name.as_deref(),
1753 Some("../etc/config.txt")
1754 );
1755
1756 let whiteout_entry = entries.get("var/.wh.deleted").unwrap();
1757 assert_eq!(whiteout_entry.entry_type, tar::EntryType::Regular);
1758 assert!(whiteout_entry.data.is_empty());
1759
1760 let opaque_entry = entries.get("cache/.wh..wh..opq").unwrap();
1761 assert_eq!(opaque_entry.entry_type, tar::EntryType::Regular);
1762 assert!(opaque_entry.data.is_empty());
1763
1764 let second_cache = temp.path().join("cache-2");
1765 let reloaded = runtime
1766 .block_on(load_archive(
1767 &second_cache,
1768 &saved,
1769 ImageLoadOptions::default(),
1770 ))
1771 .unwrap();
1772
1773 assert_eq!(reloaded.len(), 1);
1774 assert_eq!(reloaded[0].reference, "complex:latest");
1775 }
1776
1777 #[test]
1778 fn docker_archive_save_preserves_raw_config_fields() {
1779 let runtime = tokio::runtime::Builder::new_current_thread()
1780 .enable_all()
1781 .build()
1782 .unwrap();
1783 let temp = tempdir().unwrap();
1784 let input = temp.path().join("config-fidelity.tar");
1785 let layer_bytes = simple_layer_tar();
1786 let diff_id = format!("sha256:{}", sha256_hex(&layer_bytes));
1787 let config_bytes = serde_json::to_vec(&serde_json::json!({
1788 "architecture": "arm64",
1789 "os": "linux",
1790 "author": "microsandbox-test",
1791 "config": {
1792 "Env": ["PATH=/usr/bin"],
1793 "Cmd": ["cat", "/hello.txt"],
1794 },
1795 "rootfs": {
1796 "type": "layers",
1797 "diff_ids": [diff_id],
1798 },
1799 "history": [{
1800 "created_by": "fixture",
1801 "comment": "keep me",
1802 }],
1803 }))
1804 .unwrap();
1805 let config_name = format!("{}.json", sha256_hex(&config_bytes));
1806
1807 write_test_docker_archive_entries(
1808 &input,
1809 "config-fidelity:latest",
1810 config_name,
1811 "layer/layer.tar".into(),
1812 config_bytes,
1813 layer_bytes,
1814 );
1815
1816 let first_cache = temp.path().join("cache-1");
1817 let loaded = runtime
1818 .block_on(load_archive(
1819 &first_cache,
1820 &input,
1821 ImageLoadOptions::default(),
1822 ))
1823 .unwrap();
1824 let saved = temp.path().join("saved-config-fidelity.tar");
1825 let request = save_request_from_loaded(&loaded[0]);
1826 let cache = GlobalCache::new(&first_cache).unwrap();
1827 save_docker_archive(&cache, &saved, &[request]).unwrap();
1828
1829 let manifest_bytes = read_archive_entry(&saved, "manifest.json")
1830 .unwrap()
1831 .unwrap();
1832 let manifest: Vec<DockerManifestEntry> = serde_json::from_slice(&manifest_bytes).unwrap();
1833 let saved_config = read_archive_entry(&saved, &manifest[0].config)
1834 .unwrap()
1835 .unwrap();
1836 let saved_config: serde_json::Value = serde_json::from_slice(&saved_config).unwrap();
1837
1838 assert_eq!(saved_config["author"], "microsandbox-test");
1839 assert_eq!(saved_config["history"][0]["comment"], "keep me");
1840 }
1841
1842 fn write_test_docker_archive(path: &Path, reference: &str) {
1843 write_test_docker_archive_from_layer(path, reference, simple_layer_tar());
1844 }
1845
1846 fn write_test_docker_archive_from_layer(path: &Path, reference: &str, layer_bytes: Vec<u8>) {
1847 let diff_id = format!("sha256:{}", sha256_hex(&layer_bytes));
1848 let config_bytes = test_config_bytes(&diff_id);
1849 let config_name = format!("{}.json", sha256_hex(&config_bytes));
1850
1851 write_test_docker_archive_entries(
1852 path,
1853 reference,
1854 config_name,
1855 "layer/layer.tar".into(),
1856 config_bytes,
1857 layer_bytes,
1858 );
1859 }
1860
1861 fn write_test_docker_blob_archive_from_layer(
1862 path: &Path,
1863 reference: &str,
1864 layer_bytes: Vec<u8>,
1865 ) {
1866 let diff_id = format!("sha256:{}", sha256_hex(&layer_bytes));
1867 let config_bytes = test_config_bytes(&diff_id);
1868 let config_name = format!("blobs/sha256/{}", sha256_hex(&config_bytes));
1869 let layer_name = format!("blobs/sha256/{}", sha256_hex(&layer_bytes));
1870
1871 write_test_docker_archive_entries(
1872 path,
1873 reference,
1874 config_name,
1875 layer_name,
1876 config_bytes,
1877 layer_bytes,
1878 );
1879 }
1880
1881 fn write_test_oci_archive_from_layer(path: &Path, reference: &str, layer_bytes: Vec<u8>) {
1882 let diff_id = format!("sha256:{}", sha256_hex(&layer_bytes));
1883 let config_bytes = test_config_bytes(&diff_id);
1884 let config_hex = sha256_hex(&config_bytes);
1885 let layer_hex = sha256_hex(&layer_bytes);
1886 let manifest_bytes = serde_json::to_vec(&serde_json::json!({
1887 "schemaVersion": 2,
1888 "mediaType": OCI_MANIFEST_MEDIA_TYPE,
1889 "config": {
1890 "mediaType": OCI_CONFIG_MEDIA_TYPE,
1891 "digest": format!("sha256:{config_hex}"),
1892 "size": config_bytes.len(),
1893 },
1894 "layers": [{
1895 "mediaType": OCI_LAYER_MEDIA_TYPE,
1896 "digest": format!("sha256:{layer_hex}"),
1897 "size": layer_bytes.len(),
1898 }],
1899 }))
1900 .unwrap();
1901 let manifest_hex = sha256_hex(&manifest_bytes);
1902 let host = Platform::host_linux();
1903 let index_bytes = serde_json::to_vec(&serde_json::json!({
1904 "schemaVersion": 2,
1905 "mediaType": OCI_INDEX_MEDIA_TYPE,
1906 "manifests": [{
1907 "mediaType": OCI_MANIFEST_MEDIA_TYPE,
1908 "digest": format!("sha256:{manifest_hex}"),
1909 "size": manifest_bytes.len(),
1910 "platform": {
1911 "architecture": host.arch.to_string(),
1912 "os": host.os.to_string(),
1913 },
1914 "annotations": {
1915 (OCI_REF_NAME_ANNOTATION): reference,
1916 },
1917 }],
1918 }))
1919 .unwrap();
1920
1921 let file = File::create(path).unwrap();
1922 let mut archive = tar::Builder::new(file);
1923 append_bytes(
1924 &mut archive,
1925 "oci-layout",
1926 br#"{"imageLayoutVersion":"1.0.0"}"#,
1927 )
1928 .unwrap();
1929 append_bytes(&mut archive, "index.json", &index_bytes).unwrap();
1930 append_bytes(
1931 &mut archive,
1932 &format!("blobs/sha256/{config_hex}"),
1933 &config_bytes,
1934 )
1935 .unwrap();
1936 append_bytes(
1937 &mut archive,
1938 &format!("blobs/sha256/{manifest_hex}"),
1939 &manifest_bytes,
1940 )
1941 .unwrap();
1942 append_bytes(
1943 &mut archive,
1944 &format!("blobs/sha256/{layer_hex}"),
1945 &layer_bytes,
1946 )
1947 .unwrap();
1948 archive.finish().unwrap();
1949 }
1950
1951 fn simple_layer_tar() -> Vec<u8> {
1952 let mut layer_bytes = Vec::new();
1953 {
1954 let mut layer = tar::Builder::new(&mut layer_bytes);
1955 let data = b"hello from archive\n";
1956 let mut header = tar::Header::new_gnu();
1957 header.set_entry_type(tar::EntryType::Regular);
1958 header.set_mode(0o644);
1959 header.set_uid(0);
1960 header.set_gid(0);
1961 header.set_mtime(0);
1962 header.set_size(data.len() as u64);
1963 header.set_cksum();
1964 layer
1965 .append_data(&mut header, "hello.txt", Cursor::new(data))
1966 .unwrap();
1967 layer.finish().unwrap();
1968 }
1969
1970 layer_bytes
1971 }
1972
1973 fn test_config_bytes(diff_id: &str) -> Vec<u8> {
1974 serde_json::to_vec(&serde_json::json!({
1975 "architecture": "arm64",
1976 "os": "linux",
1977 "config": {
1978 "Env": ["PATH=/usr/bin"],
1979 "Cmd": ["cat", "/hello.txt"],
1980 },
1981 "rootfs": {
1982 "type": "layers",
1983 "diff_ids": [diff_id],
1984 },
1985 }))
1986 .unwrap()
1987 }
1988
1989 fn write_test_docker_archive_entries(
1990 path: &Path,
1991 reference: &str,
1992 config_name: String,
1993 layer_name: String,
1994 config_bytes: Vec<u8>,
1995 layer_bytes: Vec<u8>,
1996 ) {
1997 let manifest_bytes = serde_json::to_vec(&vec![DockerManifestOut {
1998 config: config_name.clone(),
1999 repo_tags: vec![reference.into()],
2000 layers: vec![layer_name.clone()],
2001 }])
2002 .unwrap();
2003
2004 let file = File::create(path).unwrap();
2005 let mut archive = tar::Builder::new(file);
2006 append_bytes(&mut archive, &config_name, &config_bytes).unwrap();
2007 append_bytes(&mut archive, "manifest.json", &manifest_bytes).unwrap();
2008
2009 let mut header = tar::Header::new_gnu();
2010 header.set_entry_type(tar::EntryType::Regular);
2011 header.set_mode(0o644);
2012 header.set_uid(0);
2013 header.set_gid(0);
2014 header.set_mtime(0);
2015 header.set_size(layer_bytes.len() as u64);
2016 header.set_cksum();
2017 archive
2018 .append_data(&mut header, layer_name, Cursor::new(layer_bytes))
2019 .unwrap();
2020 archive.finish().unwrap();
2021 }
2022
2023 fn complex_layer_tar() -> Vec<u8> {
2024 let mut layer_bytes = Vec::new();
2025 {
2026 let mut layer = tar::Builder::new(&mut layer_bytes);
2027 append_test_dir(&mut layer, "bin", 0o755, 0, 0, 1);
2028 append_test_dir(&mut layer, "cache", 0o755, 0, 0, 1);
2029 append_test_dir(&mut layer, "etc", 0o755, 0, 0, 1);
2030 append_test_dir(&mut layer, "var", 0o755, 0, 0, 1);
2031 append_test_file(
2032 &mut layer,
2033 "etc/config.txt",
2034 b"shared config\n",
2035 0o640,
2036 1000,
2037 1001,
2038 42,
2039 );
2040 append_test_hardlink(&mut layer, "etc/config.link", "etc/config.txt");
2041 append_test_symlink(&mut layer, "bin/config", "../etc/config.txt");
2042 append_test_file(&mut layer, "var/.wh.deleted", b"", 0o000, 0, 0, 1);
2043 append_test_file(&mut layer, "cache/.wh..wh..opq", b"", 0o000, 0, 0, 1);
2044 layer.finish().unwrap();
2045 }
2046 layer_bytes
2047 }
2048
2049 fn append_test_dir(
2050 layer: &mut tar::Builder<&mut Vec<u8>>,
2051 path: &str,
2052 mode: u32,
2053 uid: u64,
2054 gid: u64,
2055 mtime: u64,
2056 ) {
2057 let mut header = tar::Header::new_gnu();
2058 header.set_entry_type(tar::EntryType::Directory);
2059 header.set_mode(mode);
2060 header.set_uid(uid);
2061 header.set_gid(gid);
2062 header.set_mtime(mtime);
2063 header.set_size(0);
2064 header.set_cksum();
2065 layer.append_data(&mut header, path, io::empty()).unwrap();
2066 }
2067
2068 fn append_test_file(
2069 layer: &mut tar::Builder<&mut Vec<u8>>,
2070 path: &str,
2071 data: &[u8],
2072 mode: u32,
2073 uid: u64,
2074 gid: u64,
2075 mtime: u64,
2076 ) {
2077 let mut header = tar::Header::new_gnu();
2078 header.set_entry_type(tar::EntryType::Regular);
2079 header.set_mode(mode);
2080 header.set_uid(uid);
2081 header.set_gid(gid);
2082 header.set_mtime(mtime);
2083 header.set_size(data.len() as u64);
2084 header.set_cksum();
2085 layer
2086 .append_data(&mut header, path, Cursor::new(data))
2087 .unwrap();
2088 }
2089
2090 fn append_test_hardlink(layer: &mut tar::Builder<&mut Vec<u8>>, path: &str, target: &str) {
2091 let mut header = tar::Header::new_gnu();
2092 header.set_entry_type(tar::EntryType::Link);
2093 header.set_link_name(target).unwrap();
2094 header.set_size(0);
2095 header.set_cksum();
2096 layer.append_data(&mut header, path, io::empty()).unwrap();
2097 }
2098
2099 fn append_test_symlink(layer: &mut tar::Builder<&mut Vec<u8>>, path: &str, target: &str) {
2100 let mut header = tar::Header::new_gnu();
2101 header.set_entry_type(tar::EntryType::Symlink);
2102 header.set_link_name(target).unwrap();
2103 header.set_mode(0o777);
2104 header.set_size(0);
2105 header.set_cksum();
2106 layer.append_data(&mut header, path, io::empty()).unwrap();
2107 }
2108
2109 #[derive(Debug)]
2110 struct SavedLayerEntry {
2111 entry_type: tar::EntryType,
2112 link_name: Option<String>,
2113 mode: u32,
2114 uid: u64,
2115 gid: u64,
2116 mtime: u64,
2117 data: Vec<u8>,
2118 }
2119
2120 fn saved_layer_entries(path: &Path) -> BTreeMap<String, SavedLayerEntry> {
2121 let file = File::open(path).unwrap();
2122 let mut archive = tar::Archive::new(file);
2123 let mut layer_bytes = None;
2124
2125 for entry in archive.entries().unwrap() {
2126 let mut entry = entry.unwrap();
2127 let entry_path = entry.path().unwrap().to_string_lossy().into_owned();
2128 if entry_path.ends_with("/layer.tar") {
2129 assert!(layer_bytes.is_none());
2130 let mut data = Vec::new();
2131 entry.read_to_end(&mut data).unwrap();
2132 layer_bytes = Some(data);
2133 }
2134 }
2135
2136 let layer_bytes = layer_bytes.unwrap();
2137 let mut layer = tar::Archive::new(Cursor::new(layer_bytes));
2138 let mut entries = BTreeMap::new();
2139
2140 for entry in layer.entries().unwrap() {
2141 let mut entry = entry.unwrap();
2142 let path = entry.path().unwrap().to_string_lossy().into_owned();
2143 let header = entry.header();
2144 let entry_type = header.entry_type();
2145 let mode = header.mode().unwrap();
2146 let uid = header.uid().unwrap();
2147 let gid = header.gid().unwrap();
2148 let mtime = header.mtime().unwrap();
2149 let link_name = if matches!(entry_type, tar::EntryType::Link | tar::EntryType::Symlink)
2150 {
2151 Some(String::from_utf8_lossy(entry.link_name_bytes().unwrap().as_ref()).into())
2152 } else {
2153 None
2154 };
2155 let mut data = Vec::new();
2156 entry.read_to_end(&mut data).unwrap();
2157
2158 entries.insert(
2159 path,
2160 SavedLayerEntry {
2161 entry_type,
2162 link_name,
2163 mode,
2164 uid,
2165 gid,
2166 mtime,
2167 data,
2168 },
2169 );
2170 }
2171
2172 entries
2173 }
2174
2175 fn save_request_from_loaded(image: &LoadedImage) -> ImageSaveRequest {
2176 let host = Platform::host_linux();
2177 ImageSaveRequest {
2178 reference: image.reference.clone(),
2179 config: ImageSaveConfig {
2180 architecture: Some(host.arch.to_string()),
2181 os: Some(host.os.to_string()),
2182 env: image.metadata.config.env.clone(),
2183 entrypoint: image.metadata.config.entrypoint.clone(),
2184 cmd: image.metadata.config.cmd.clone(),
2185 working_dir: image.metadata.config.working_dir.clone(),
2186 user: image.metadata.config.user.clone(),
2187 labels: image
2188 .metadata
2189 .config
2190 .labels
2191 .iter()
2192 .map(|(key, value)| (key.clone(), value.clone()))
2193 .collect(),
2194 },
2195 raw_config_json: image.metadata.raw_config_json.clone(),
2196 layers: image
2197 .metadata
2198 .layers
2199 .iter()
2200 .map(|layer| ImageSaveLayer {
2201 diff_id: layer.diff_id.clone(),
2202 })
2203 .collect(),
2204 }
2205 }
2206}