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