1use super::*;
129use crate::chunking::{self, Chunk};
130use crate::generic_decompress::Decompressor;
131use crate::logging::system_repo_journal_print;
132use crate::refescape;
133use crate::sysroot::SysrootLock;
134use anyhow::{Context, anyhow};
135use bootc_utils::ResultExt;
136use camino::{Utf8Path, Utf8PathBuf};
137use canon_json::CanonJsonSerialize;
138use cap_std_ext::cap_std;
139use cap_std_ext::cap_std::fs::{Dir, MetadataExt};
140
141use cap_std_ext::dirext::CapStdExtDirExt;
142use containers_image_proxy::{ImageProxy, OpenedImage};
143use flate2::Compression;
144use fn_error_context::context;
145use futures_util::TryFutureExt;
146use glib::prelude::*;
147use oci_spec::image::{
148 self as oci_image, Arch, Descriptor, Digest, History, ImageConfiguration, ImageManifest,
149};
150use ocidir::oci_spec::distribution::Reference;
151use ostree::prelude::{Cast, FileEnumeratorExt, FileExt, ToVariant};
152use ostree::{gio, glib};
153use std::collections::{BTreeMap, BTreeSet, HashMap};
154use std::fmt::Write as _;
155use std::iter::FromIterator;
156use std::num::NonZeroUsize;
157use tokio::sync::mpsc::{Receiver, Sender};
158
159pub use containers_image_proxy::ImageProxyConfig;
164
165const LAYER_PREFIX: &str = "ostree/container/blob";
167const IMAGE_PREFIX: &str = "ostree/container/image";
169pub const BASE_IMAGE_PREFIX: &str = "ostree/container/baseimage";
174
175pub(crate) const META_MANIFEST_DIGEST: &str = "ostree.manifest-digest";
177const META_MANIFEST: &str = "ostree.manifest";
179const META_CONFIG: &str = "ostree.container.image-config";
181pub type MetaFilteredData = HashMap<String, HashMap<String, u32>>;
183
184const OSTREE_BASE_DEPLOYMENT_REFS: &[&str] = &["ostree/0", "ostree/1"];
186const RPMOSTREE_BASE_REFS: &[&str] = &["rpmostree/base"];
188
189fn ref_for_blob_digest(d: &str) -> Result<String> {
191 refescape::prefix_escape_for_ref(LAYER_PREFIX, d)
192}
193
194fn ref_for_layer(l: &oci_image::Descriptor) -> Result<String> {
196 ref_for_blob_digest(&l.digest().as_ref())
197}
198
199fn ref_for_image(l: &ImageReference) -> Result<String> {
201 refescape::prefix_escape_for_ref(IMAGE_PREFIX, &l.to_string())
202}
203
204#[derive(Debug)]
206pub enum ImportProgress {
207 OstreeChunkStarted(Descriptor),
209 OstreeChunkCompleted(Descriptor),
211 DerivedLayerStarted(Descriptor),
213 DerivedLayerCompleted(Descriptor),
215}
216
217impl ImportProgress {
218 pub fn is_starting(&self) -> bool {
220 match self {
221 ImportProgress::OstreeChunkStarted(_) => true,
222 ImportProgress::OstreeChunkCompleted(_) => false,
223 ImportProgress::DerivedLayerStarted(_) => true,
224 ImportProgress::DerivedLayerCompleted(_) => false,
225 }
226 }
227}
228
229#[derive(Clone, Debug)]
231pub struct LayerProgress {
232 pub layer_index: usize,
234 pub fetched: u64,
236 pub total: u64,
238}
239
240#[derive(Debug, PartialEq, Eq)]
242pub struct LayeredImageState {
243 pub base_commit: String,
245 pub merge_commit: String,
247 pub manifest_digest: Digest,
249 pub manifest: ImageManifest,
251 pub configuration: ImageConfiguration,
253 pub cached_update: Option<CachedImageUpdate>,
255 pub verify_text: Option<String>,
259 pub filtered_files: Option<MetaFilteredData>,
261}
262
263impl LayeredImageState {
264 pub fn get_commit(&self) -> &str {
268 self.merge_commit.as_str()
269 }
270
271 pub fn version(&self) -> Option<&str> {
273 super::version_for_config(&self.configuration)
274 }
275}
276
277#[derive(Debug, PartialEq, Eq)]
279pub struct CachedImageUpdate {
280 pub manifest: ImageManifest,
282 pub config: ImageConfiguration,
284 pub manifest_digest: Digest,
286}
287
288impl CachedImageUpdate {
289 pub fn version(&self) -> Option<&str> {
291 super::version_for_config(&self.config)
292 }
293}
294
295struct LayerRef {
297 ostree_ref: String,
298 commit: String,
299}
300
301#[derive(Debug)]
303pub struct ImageImporter {
304 repo: ostree::Repo,
305 pub(crate) proxy: ImageProxy,
306 imgref: OstreeImageReference,
307 target_imgref: Option<OstreeImageReference>,
308 no_imgref: bool, disable_gc: bool, sepolicy_commit: Option<String>,
312 require_bootable: bool,
314 offline: bool,
316 ostree_v2024_3: bool,
318 diffid_to_digest: HashMap<String, String>,
320
321 layer_progress: Option<Sender<ImportProgress>>,
322 layer_byte_progress: Option<tokio::sync::watch::Sender<Option<LayerProgress>>>,
323}
324
325#[derive(Debug)]
327pub enum PrepareResult {
328 AlreadyPresent(Box<LayeredImageState>),
330 Ready(Box<PreparedImport>),
332}
333
334#[derive(Debug)]
336pub struct ManifestLayerState {
337 pub layer: oci_image::Descriptor,
339 pub ostree_ref: String,
342 pub commit: Option<String>,
345}
346
347impl ManifestLayerState {
348 pub fn layer(&self) -> &oci_image::Descriptor {
350 &self.layer
351 }
352}
353
354#[derive(Debug)]
356pub struct PreparedImport {
357 pub manifest_digest: Digest,
359 pub manifest: oci_image::ImageManifest,
361 pub config: oci_image::ImageConfiguration,
363 pub previous_state: Option<Box<LayeredImageState>>,
365 pub previous_manifest_digest: Option<Digest>,
367 pub previous_imageid: Option<String>,
369 pub ostree_layers: Vec<ManifestLayerState>,
371 pub ostree_commit_layer: Option<ManifestLayerState>,
373 pub layers: Vec<ManifestLayerState>,
375 pub verify_text: Option<String>,
377 proxy_img: OpenedImage,
379}
380
381impl PreparedImport {
382 pub fn all_layers(&self) -> impl Iterator<Item = &ManifestLayerState> {
384 self.ostree_commit_layer
385 .iter()
386 .chain(self.ostree_layers.iter())
387 .chain(self.layers.iter())
388 }
389
390 pub fn version(&self) -> Option<&str> {
392 super::version_for_config(&self.config)
393 }
394
395 pub fn deprecated_warning(&self) -> Option<&'static str> {
397 None
398 }
399
400 pub fn layers_with_history(
403 &self,
404 ) -> impl Iterator<Item = Result<(&ManifestLayerState, &History)>> {
405 let truncated = std::iter::once_with(|| Err(anyhow::anyhow!("Truncated history")));
407 let history = self
408 .config
409 .history()
410 .iter()
411 .flatten()
412 .map(Ok)
413 .chain(truncated);
414 self.all_layers()
415 .zip(history)
416 .map(|(s, h)| h.map(|h| (s, h)))
417 }
418
419 pub fn layers_to_fetch(&self) -> impl Iterator<Item = Result<(&ManifestLayerState, &str)>> {
421 self.layers_with_history().filter_map(|r| {
422 r.map(|(l, h)| {
423 l.commit.is_none().then(|| {
424 let comment = h.created_by().as_deref().unwrap_or("");
425 (l, comment)
426 })
427 })
428 .transpose()
429 })
430 }
431
432 pub(crate) fn format_layer_status(&self) -> Option<String> {
434 let (stored, to_fetch, to_fetch_size) =
435 self.all_layers()
436 .fold((0u32, 0u32, 0u64), |(stored, to_fetch, sz), v| {
437 if v.commit.is_some() {
438 (stored + 1, to_fetch, sz)
439 } else {
440 (stored, to_fetch + 1, sz + v.layer().size())
441 }
442 });
443 (to_fetch > 0).then(|| {
444 let size = crate::glib::format_size(to_fetch_size);
445 format!("layers already present: {stored}; layers needed: {to_fetch} ({size})")
446 })
447 }
448}
449
450pub(crate) fn query_layer(
452 repo: &ostree::Repo,
453 layer: oci_image::Descriptor,
454) -> Result<ManifestLayerState> {
455 let ostree_ref = ref_for_layer(&layer)?;
456 let commit = repo.resolve_rev(&ostree_ref, true)?.map(|s| s.to_string());
457 Ok(ManifestLayerState {
458 layer,
459 ostree_ref,
460 commit,
461 })
462}
463
464#[context("Reading manifest data from commit")]
465fn manifest_data_from_commitmeta(
466 commit_meta: &glib::VariantDict,
467) -> Result<(oci_image::ImageManifest, Digest)> {
468 let digest = commit_meta
469 .lookup::<String>(META_MANIFEST_DIGEST)?
470 .ok_or_else(|| anyhow!("Missing {} metadata on merge commit", META_MANIFEST_DIGEST))?;
471 let digest = Digest::from_str(&digest)?;
472 let manifest_bytes: String = commit_meta
473 .lookup::<String>(META_MANIFEST)?
474 .ok_or_else(|| anyhow!("Failed to find {} metadata key", META_MANIFEST))?;
475 let r = serde_json::from_str(&manifest_bytes)?;
476 Ok((r, digest))
477}
478
479fn image_config_from_commitmeta(commit_meta: &glib::VariantDict) -> Result<ImageConfiguration> {
480 let config = if let Some(config) = commit_meta
481 .lookup::<String>(META_CONFIG)?
482 .filter(|v| v != "null") .map(|v| serde_json::from_str(&v).map_err(anyhow::Error::msg))
484 .transpose()?
485 {
486 config
487 } else {
488 tracing::debug!("No image configuration found");
489 Default::default()
490 };
491 Ok(config)
492}
493
494pub fn manifest_digest_from_commit(commit: &glib::Variant) -> Result<Digest> {
500 let commit_meta = &commit.child_value(0);
501 let commit_meta = &glib::VariantDict::new(Some(commit_meta));
502 Ok(manifest_data_from_commitmeta(commit_meta)?.1)
503}
504
505fn layer_from_diffid<'a>(
509 manifest: &'a ImageManifest,
510 config: &ImageConfiguration,
511 diffid: &str,
512) -> Result<&'a Descriptor> {
513 let idx = config
514 .rootfs()
515 .diff_ids()
516 .iter()
517 .position(|x| x.as_str() == diffid)
518 .ok_or_else(|| anyhow!("Missing {} {}", DIFFID_LABEL, diffid))?;
519 manifest.layers().get(idx).ok_or_else(|| {
520 anyhow!(
521 "diffid position {} exceeds layer count {}",
522 idx,
523 manifest.layers().len()
524 )
525 })
526}
527
528#[context("Parsing manifest layout")]
529pub(crate) fn parse_manifest_layout<'a>(
530 manifest: &'a ImageManifest,
531 config: &ImageConfiguration,
532) -> Result<(
533 Option<&'a Descriptor>,
534 Vec<&'a Descriptor>,
535 Vec<&'a Descriptor>,
536)> {
537 let config_labels = super::labels_of(config);
538
539 let first_layer = manifest
540 .layers()
541 .first()
542 .ok_or_else(|| anyhow!("No layers in manifest"))?;
543 let Some(target_diffid) = config_labels.and_then(|labels| labels.get(DIFFID_LABEL)) else {
544 return Ok((None, Vec::new(), manifest.layers().iter().collect()));
545 };
546
547 let target_layer = layer_from_diffid(manifest, config, target_diffid.as_str())?;
548 let mut chunk_layers = Vec::new();
549 let mut derived_layers = Vec::new();
550 let mut after_target = false;
551 let ostree_layer = first_layer;
553 for layer in manifest.layers() {
554 if layer == target_layer {
555 if after_target {
556 anyhow::bail!("Multiple entries for {}", layer.digest());
557 }
558 after_target = true;
559 if layer != ostree_layer {
560 chunk_layers.push(layer);
561 }
562 } else if !after_target {
563 if layer != ostree_layer {
564 chunk_layers.push(layer);
565 }
566 } else {
567 derived_layers.push(layer);
568 }
569 }
570
571 Ok((Some(ostree_layer), chunk_layers, derived_layers))
572}
573
574#[context("Parsing manifest layout")]
576pub(crate) fn parse_ostree_manifest_layout<'a>(
577 manifest: &'a ImageManifest,
578 config: &ImageConfiguration,
579) -> Result<(&'a Descriptor, Vec<&'a Descriptor>, Vec<&'a Descriptor>)> {
580 let (ostree_layer, component_layers, derived_layers) = parse_manifest_layout(manifest, config)?;
581 let ostree_layer = ostree_layer.ok_or_else(|| {
582 anyhow!("No {DIFFID_LABEL} label found, not an ostree encapsulated container")
583 })?;
584 Ok((ostree_layer, component_layers, derived_layers))
585}
586
587fn timestamp_of_manifest_or_config(
589 manifest: &ImageManifest,
590 config: &ImageConfiguration,
591) -> Option<u64> {
592 let timestamp = manifest
595 .annotations()
596 .as_ref()
597 .and_then(|a| a.get(oci_image::ANNOTATION_CREATED))
598 .or_else(|| config.created().as_ref());
599 timestamp
601 .map(|t| {
602 chrono::DateTime::parse_from_rfc3339(t)
603 .context("Failed to parse manifest timestamp")
604 .map(|t| t.timestamp() as u64)
605 })
606 .transpose()
607 .log_err_default()
608}
609
610fn cleanup_root(root: &Dir) -> Result<()> {
613 const RUNTIME_INJECTED: &[&str] = &["usr/etc/hostname", "usr/etc/resolv.conf"];
614 for ent in RUNTIME_INJECTED {
615 if let Some(meta) = root.symlink_metadata_optional(ent)? {
616 if meta.is_file() && meta.size() == 0 {
617 tracing::debug!("Removing {ent}");
618 root.remove_file(ent)?;
619 }
620 }
621 }
622 Ok(())
623}
624
625impl ImageImporter {
626 const CACHED_KEY_MANIFEST_DIGEST: &'static str = "ostree-ext.cached.manifest-digest";
628 const CACHED_KEY_MANIFEST: &'static str = "ostree-ext.cached.manifest";
629 const CACHED_KEY_CONFIG: &'static str = "ostree-ext.cached.config";
630
631 #[context("Creating importer")]
633 pub async fn new(
634 repo: &ostree::Repo,
635 imgref: &OstreeImageReference,
636 mut config: ImageProxyConfig,
637 ) -> Result<Self> {
638 if imgref.imgref.transport == Transport::ContainerStorage {
639 merge_default_container_proxy_opts_with_isolation(&mut config, None)?;
641 } else {
642 merge_default_container_proxy_opts(&mut config)?;
644 }
645 let proxy = ImageProxy::new_with_config(config).await?;
646
647 system_repo_journal_print(
648 repo,
649 libsystemd::logging::Priority::Info,
650 &format!("Fetching {imgref}"),
651 );
652
653 let repo = repo.clone();
654
655 let diffid_to_digest = Self::build_diffid_to_digest_map(&repo)?;
656
657 Ok(ImageImporter {
658 repo,
659 proxy,
660 target_imgref: None,
661 no_imgref: false,
662 ostree_v2024_3: ostree::check_version(2024, 3),
663 disable_gc: false,
664 sepolicy_commit: None,
665 require_bootable: false,
666 offline: false,
667 imgref: imgref.clone(),
668 diffid_to_digest,
669 layer_progress: None,
670 layer_byte_progress: None,
671 })
672 }
673
674 pub fn set_target(&mut self, target: &OstreeImageReference) {
676 self.target_imgref = Some(target.clone())
677 }
678
679 pub fn set_no_imgref(&mut self) {
683 self.no_imgref = true;
684 }
685
686 pub fn set_offline(&mut self) {
688 self.offline = true;
689 }
690
691 pub fn require_bootable(&mut self) {
693 self.require_bootable = true;
694 }
695
696 pub fn set_ostree_version(&mut self, year: u32, v: u32) {
698 self.ostree_v2024_3 = (year > 2024) || (year == 2024 && v >= 3)
699 }
700
701 pub fn disable_gc(&mut self) {
703 self.disable_gc = true;
704 }
705
706 pub fn set_sepolicy_commit(&mut self, commit: String) {
710 self.sepolicy_commit = Some(commit);
711 }
712
713 #[context("Preparing import")]
718 pub async fn prepare(&mut self) -> Result<PrepareResult> {
719 self.prepare_internal(false).await
720 }
721
722 pub fn request_progress(&mut self) -> Receiver<ImportProgress> {
724 assert!(self.layer_progress.is_none());
725 let (s, r) = tokio::sync::mpsc::channel(2);
726 self.layer_progress = Some(s);
727 r
728 }
729
730 pub fn request_layer_progress(
732 &mut self,
733 ) -> tokio::sync::watch::Receiver<Option<LayerProgress>> {
734 assert!(self.layer_byte_progress.is_none());
735 let (s, r) = tokio::sync::watch::channel(None);
736 self.layer_byte_progress = Some(s);
737 r
738 }
739
740 #[context("Writing cached pending manifest")]
743 pub(crate) async fn cache_pending(
744 &self,
745 commit: &str,
746 manifest_digest: &Digest,
747 manifest: &ImageManifest,
748 config: &ImageConfiguration,
749 ) -> Result<()> {
750 let commitmeta = glib::VariantDict::new(None);
751 commitmeta.insert(
752 Self::CACHED_KEY_MANIFEST_DIGEST,
753 manifest_digest.to_string(),
754 );
755 let cached_manifest = manifest
756 .to_canon_json_string()
757 .context("Serializing manifest")?;
758 commitmeta.insert(Self::CACHED_KEY_MANIFEST, cached_manifest);
759 let cached_config = config
760 .to_canon_json_string()
761 .context("Serializing config")?;
762 commitmeta.insert(Self::CACHED_KEY_CONFIG, cached_config);
763 let commitmeta = commitmeta.to_variant();
764 let commit = commit.to_string();
766 let repo = self.repo.clone();
767 crate::tokio_util::spawn_blocking_cancellable_flatten(move |cancellable| {
768 repo.write_commit_detached_metadata(&commit, Some(&commitmeta), Some(cancellable))
769 .map_err(anyhow::Error::msg)
770 })
771 .await
772 }
773
774 fn build_diffid_to_digest_map(repo: &ostree::Repo) -> Result<HashMap<String, String>> {
777 let mut map = HashMap::new();
778 let all_images = list_images(repo)?;
779
780 for imgref_str in all_images {
781 let imgref = match ImageReference::try_from(imgref_str.as_str()) {
782 Ok(r) => r,
783 Err(e) => {
784 tracing::warn!("Failed to parse image reference {}: {}", imgref_str, e);
785 continue;
786 }
787 };
788
789 let state = match query_image(repo, &imgref)? {
790 Some(s) => s,
791 None => continue,
792 };
793
794 for (layer_desc, diff_id) in state
796 .manifest
797 .layers()
798 .iter()
799 .zip(state.configuration.rootfs().diff_ids())
800 {
801 let diff_id_str = diff_id.to_string();
802 map.entry(diff_id_str)
804 .or_insert_with(|| layer_desc.digest().to_string());
805 }
806 }
807
808 Ok(map)
809 }
810
811 fn find_digest_by_diffid(
812 &self,
813 manifest: &oci_image::ImageManifest,
814 config: &oci_image::ImageConfiguration,
815 layer: &oci_image::Descriptor,
816 ) -> Option<&String> {
817 let idx = manifest
818 .layers()
819 .iter()
820 .position(|l| l.digest() == layer.digest())?;
821 let layer_diffid = config.rootfs().diff_ids().get(idx)?;
822 self.diffid_to_digest.get(layer_diffid.as_str())
823 }
824
825 fn resolve_commit_by_diffid(
827 &self,
828 manifest: &oci_image::ImageManifest,
829 config: &oci_image::ImageConfiguration,
830 layer: &oci_image::Descriptor,
831 ) -> Result<Option<String>> {
832 let Some(existing_digest) = self.find_digest_by_diffid(manifest, config, layer) else {
833 return Ok(None);
834 };
835 let existing_ref = ref_for_blob_digest(existing_digest)?;
836 Ok(self
837 .repo
838 .resolve_rev(&existing_ref, true)?
839 .map(|s| s.to_string()))
840 }
841
842 fn query_layer(
845 &self,
846 manifest: &oci_image::ImageManifest,
847 config: &oci_image::ImageConfiguration,
848 layer: &oci_image::Descriptor,
849 ) -> Result<ManifestLayerState> {
850 let ostree_ref = ref_for_layer(layer)?;
851 let commit = self
852 .repo
853 .resolve_rev(&ostree_ref, true)?
854 .map(|s| s.to_string());
855 let commit = match commit {
858 Some(c) => Some(c),
859 None => self.resolve_commit_by_diffid(manifest, config, layer)?,
860 };
861
862 Ok(ManifestLayerState {
863 layer: layer.clone(),
864 ostree_ref,
865 commit,
866 })
867 }
868
869 fn ensure_ref_for_layer(repo: &ostree::Repo, ostree_ref: &str, commit: &str) -> Result<()> {
871 let ref_exists = repo.resolve_rev(ostree_ref, true)?.is_some();
872 if !ref_exists {
873 tracing::debug!("Creating ref {} for reused commit {}", ostree_ref, commit);
874 repo.set_ref_immediate(None, ostree_ref, Some(commit), gio::Cancellable::NONE)?;
875 }
876 Ok(())
877 }
878
879 fn create_prepared_import(
882 &mut self,
883 manifest_digest: Digest,
884 manifest: ImageManifest,
885 config: ImageConfiguration,
886 previous_state: Option<Box<LayeredImageState>>,
887 previous_imageid: Option<String>,
888 proxy_img: OpenedImage,
889 ) -> Result<Box<PreparedImport>> {
890 let config_labels = super::labels_of(&config);
891 if self.require_bootable {
892 let bootable_key = ostree::METADATA_KEY_BOOTABLE;
893 let bootable = config_labels.is_some_and(|l| {
894 l.contains_key(bootable_key.as_str()) || l.contains_key(BOOTC_LABEL)
895 });
896 if !bootable {
897 anyhow::bail!("Target image does not have {bootable_key} label");
898 }
899 let container_arch = config.architecture();
900 let target_arch = &Arch::default();
901 if container_arch != target_arch {
902 anyhow::bail!("Image has architecture {container_arch}; expected {target_arch}");
903 }
904 }
905
906 let (commit_layer, component_layers, remaining_layers) =
907 parse_manifest_layout(&manifest, &config)?;
908
909 let commit_layer = commit_layer
910 .map(|layer| self.query_layer(&manifest, &config, layer))
911 .transpose()?;
912
913 let component_layers = component_layers
914 .into_iter()
915 .map(|l| self.query_layer(&manifest, &config, l))
916 .collect::<Result<Vec<_>>>()?;
917
918 let remaining_layers = remaining_layers
919 .into_iter()
920 .map(|l| self.query_layer(&manifest, &config, l))
921 .collect::<Result<Vec<_>>>()?;
922
923 let previous_manifest_digest = previous_state.as_ref().map(|s| s.manifest_digest.clone());
924 let imp = PreparedImport {
925 manifest_digest,
926 manifest,
927 config,
928 previous_state,
929 previous_manifest_digest,
930 previous_imageid,
931 ostree_layers: component_layers,
932 ostree_commit_layer: commit_layer,
933 layers: remaining_layers,
934 verify_text: None,
935 proxy_img,
936 };
937 Ok(Box::new(imp))
938 }
939
940 #[context("Fetching manifest")]
942 pub(crate) async fn prepare_internal(&mut self, verify_layers: bool) -> Result<PrepareResult> {
943 match &self.imgref.sigverify {
944 SignatureSource::ContainerPolicy if skopeo::container_policy_is_default_insecure()? => {
945 return Err(anyhow!(
946 "containers-policy.json specifies a default of `insecureAcceptAnything`; refusing usage"
947 ));
948 }
949 SignatureSource::OstreeRemote(_) if verify_layers => {
950 return Err(anyhow!(
951 "Cannot currently verify layered containers via ostree remote"
952 ));
953 }
954 _ => {}
955 }
956
957 let previous_state = try_query_image(&self.repo, &self.imgref.imgref)?;
959
960 let target_reference = self.imgref.imgref.name.parse::<Reference>().ok();
962 let previous_state = if let Some(target_digest) = target_reference
963 .as_ref()
964 .and_then(|v| v.digest())
965 .map(Digest::from_str)
966 .transpose()?
967 {
968 if let Some(previous_state) = previous_state {
969 if previous_state.manifest_digest == target_digest {
971 tracing::debug!("Digest-based pullspec {:?} already present", self.imgref);
972 return Ok(PrepareResult::AlreadyPresent(previous_state));
973 }
974 Some(previous_state)
975 } else {
976 None
977 }
978 } else {
979 previous_state
980 };
981
982 if self.offline {
983 anyhow::bail!("Manifest fetch required in offline mode");
984 }
985
986 let proxy_img = self
987 .proxy
988 .open_image(&self.imgref.imgref.to_string())
989 .await?;
990
991 let (manifest_digest, manifest) = self.proxy.fetch_manifest(&proxy_img).await?;
992 let manifest_digest = Digest::from_str(&manifest_digest)?;
993 let new_imageid = manifest.config().digest();
994
995 let (previous_state, previous_imageid) = if let Some(previous_state) = previous_state {
998 if previous_state.manifest_digest == manifest_digest {
1000 return Ok(PrepareResult::AlreadyPresent(previous_state));
1001 }
1002 let previous_imageid = previous_state.manifest.config().digest();
1004 if previous_imageid == new_imageid {
1005 return Ok(PrepareResult::AlreadyPresent(previous_state));
1006 }
1007 let previous_imageid = previous_imageid.to_string();
1008 (Some(previous_state), Some(previous_imageid))
1009 } else {
1010 (None, None)
1011 };
1012
1013 let config = self.proxy.fetch_config(&proxy_img).await?;
1014
1015 if let Some(previous_state) = previous_state.as_ref() {
1018 self.cache_pending(
1019 previous_state.merge_commit.as_str(),
1020 &manifest_digest,
1021 &manifest,
1022 &config,
1023 )
1024 .await?;
1025 }
1026
1027 let imp = self.create_prepared_import(
1028 manifest_digest,
1029 manifest,
1030 config,
1031 previous_state,
1032 previous_imageid,
1033 proxy_img,
1034 )?;
1035 Ok(PrepareResult::Ready(imp))
1036 }
1037
1038 #[context("Unencapsulating base")]
1040 pub(crate) async fn unencapsulate_base(
1041 &self,
1042 import: &mut store::PreparedImport,
1043 require_ostree: bool,
1044 write_refs: bool,
1045 ) -> Result<()> {
1046 tracing::debug!("Fetching base");
1047 if matches!(self.imgref.sigverify, SignatureSource::ContainerPolicy)
1048 && skopeo::container_policy_is_default_insecure()?
1049 {
1050 return Err(anyhow!(
1051 "containers-policy.json specifies a default of `insecureAcceptAnything`; refusing usage"
1052 ));
1053 }
1054 let remote = match &self.imgref.sigverify {
1055 SignatureSource::OstreeRemote(remote) => Some(remote.clone()),
1056 SignatureSource::ContainerPolicy | SignatureSource::ContainerPolicyAllowInsecure => {
1057 None
1058 }
1059 };
1060 let Some(commit_layer) = import.ostree_commit_layer.as_mut() else {
1061 if require_ostree {
1062 anyhow::bail!(
1063 "No {DIFFID_LABEL} label found, not an ostree encapsulated container"
1064 );
1065 }
1066 return Ok(());
1067 };
1068 let des_layers = self.proxy.get_layer_info(&import.proxy_img).await?;
1069 for layer in import.ostree_layers.iter_mut() {
1070 if let Some(commit) = layer.commit.as_ref() {
1071 if write_refs {
1072 Self::ensure_ref_for_layer(&self.repo, &layer.ostree_ref, commit)?;
1073 }
1074 continue;
1075 }
1076 if let Some(p) = self.layer_progress.as_ref() {
1077 p.send(ImportProgress::OstreeChunkStarted(layer.layer.clone()))
1078 .await?;
1079 }
1080 let (blob, driver, media_type) = fetch_layer(
1081 &self.proxy,
1082 &import.proxy_img,
1083 &import.manifest,
1084 &layer.layer,
1085 self.layer_byte_progress.as_ref(),
1086 des_layers.as_ref(),
1087 self.imgref.imgref.transport,
1088 )
1089 .await?;
1090 let repo = self.repo.clone();
1091 let target_ref = layer.ostree_ref.clone();
1092 let import_task =
1093 crate::tokio_util::spawn_blocking_cancellable_flatten(move |cancellable| {
1094 let txn = repo.auto_transaction(Some(cancellable))?;
1095 let mut importer = crate::tar::Importer::new_for_object_set(&repo);
1096 let blob = tokio_util::io::SyncIoBridge::new(blob);
1097 let mut blob = Decompressor::new(&media_type, blob)?;
1098 let mut archive = tar::Archive::new(&mut blob);
1099 importer.import_objects(&mut archive, Some(cancellable))?;
1100 let commit = if write_refs {
1101 let commit = importer.finish_import_object_set()?;
1102 repo.transaction_set_ref(None, &target_ref, Some(commit.as_str()));
1103 tracing::debug!("Wrote {} => {}", target_ref, commit);
1104 Some(commit)
1105 } else {
1106 None
1107 };
1108 txn.commit(Some(cancellable))?;
1109 blob.finish()?;
1110 Ok::<_, anyhow::Error>(commit)
1111 })
1112 .map_err(|e| e.context(format!("Layer {}", layer.layer.digest())));
1113 let commit = super::unencapsulate::join_fetch(import_task, driver).await?;
1114 layer.commit = commit;
1115 if let Some(p) = self.layer_progress.as_ref() {
1116 p.send(ImportProgress::OstreeChunkCompleted(layer.layer.clone()))
1117 .await?;
1118 }
1119 }
1120 if let Some(commit) = commit_layer.commit.as_ref() {
1121 if write_refs {
1122 Self::ensure_ref_for_layer(&self.repo, &commit_layer.ostree_ref, commit)?;
1123 }
1124 } else {
1125 if let Some(p) = self.layer_progress.as_ref() {
1126 p.send(ImportProgress::OstreeChunkStarted(
1127 commit_layer.layer.clone(),
1128 ))
1129 .await?;
1130 }
1131 let (blob, driver, media_type) = fetch_layer(
1132 &self.proxy,
1133 &import.proxy_img,
1134 &import.manifest,
1135 &commit_layer.layer,
1136 self.layer_byte_progress.as_ref(),
1137 des_layers.as_ref(),
1138 self.imgref.imgref.transport,
1139 )
1140 .await?;
1141 let repo = self.repo.clone();
1142 let target_ref = commit_layer.ostree_ref.clone();
1143 let import_task =
1144 crate::tokio_util::spawn_blocking_cancellable_flatten(move |cancellable| {
1145 let txn = repo.auto_transaction(Some(cancellable))?;
1146 let mut importer = crate::tar::Importer::new_for_commit(&repo, remote);
1147 let blob = tokio_util::io::SyncIoBridge::new(blob);
1148 let mut blob = Decompressor::new(&media_type, blob)?;
1149 let mut archive = tar::Archive::new(&mut blob);
1150 importer.import_commit(&mut archive, Some(cancellable))?;
1151 let (commit, verify_text) = importer.finish_import_commit();
1152 if write_refs {
1153 repo.transaction_set_ref(None, &target_ref, Some(commit.as_str()));
1154 tracing::debug!("Wrote {} => {}", target_ref, commit);
1155 }
1156 repo.mark_commit_partial(&commit, false)?;
1157 txn.commit(Some(cancellable))?;
1158 blob.finish()?;
1159 Ok::<_, anyhow::Error>((commit, verify_text))
1160 });
1161 let (commit, verify_text) =
1162 super::unencapsulate::join_fetch(import_task, driver).await?;
1163 commit_layer.commit = Some(commit);
1164 import.verify_text = verify_text;
1165 if let Some(p) = self.layer_progress.as_ref() {
1166 p.send(ImportProgress::OstreeChunkCompleted(
1167 commit_layer.layer.clone(),
1168 ))
1169 .await?;
1170 }
1171 }
1172 Ok(())
1173 }
1174
1175 pub async fn unencapsulate(mut self) -> Result<Import> {
1180 let mut prep = match self.prepare_internal(false).await? {
1181 PrepareResult::AlreadyPresent(_) => {
1182 panic!("Should not have image present for unencapsulation")
1183 }
1184 PrepareResult::Ready(r) => r,
1185 };
1186 if !prep.layers.is_empty() {
1187 anyhow::bail!("Image has {} non-ostree layers", prep.layers.len());
1188 }
1189 let deprecated_warning = prep.deprecated_warning().map(ToOwned::to_owned);
1190 self.unencapsulate_base(&mut prep, true, false).await?;
1191 self.proxy.close_image(&prep.proxy_img).await?;
1194 let ostree_commit = prep.ostree_commit_layer.unwrap().commit.unwrap();
1196 let image_digest = prep.manifest_digest;
1197 Ok(Import {
1198 ostree_commit,
1199 image_digest,
1200 deprecated_warning,
1201 })
1202 }
1203
1204 fn write_merge_commit_impl(
1207 repo: &ostree::Repo,
1208 base_commit: Option<&str>,
1209 layer_commits: &[LayerRef],
1210 have_derived_layers: bool,
1211 metadata: glib::Variant,
1212 timestamp: u64,
1213 ostree_ref: &str,
1214 no_imgref: bool,
1215 disable_gc: bool,
1216 cancellable: Option<&gio::Cancellable>,
1217 ) -> Result<Box<LayeredImageState>> {
1218 use rustix::fd::AsRawFd;
1219
1220 let txn = repo.auto_transaction(cancellable)?;
1221
1222 let devino = ostree::RepoDevInoCache::new();
1223 let repodir = Dir::reopen_dir(&repo.dfd_borrow())?;
1224 let repo_tmp = repodir.open_dir("tmp")?;
1225 let td = cap_std_ext::cap_tempfile::TempDir::new_in(&repo_tmp)?;
1226
1227 let rootpath = "root";
1228 let checkout_mode = if repo.mode() == ostree::RepoMode::Bare {
1229 ostree::RepoCheckoutMode::None
1230 } else {
1231 ostree::RepoCheckoutMode::User
1232 };
1233 let mut checkout_opts = ostree::RepoCheckoutAtOptions {
1234 mode: checkout_mode,
1235 overwrite_mode: ostree::RepoCheckoutOverwriteMode::UnionFiles,
1236 devino_to_csum_cache: Some(devino.clone()),
1237 no_copy_fallback: true,
1238 force_copy_zerosized: true,
1239 process_whiteouts: false,
1240 ..Default::default()
1241 };
1242 if let Some(base) = base_commit.as_ref() {
1243 repo.checkout_at(
1244 Some(&checkout_opts),
1245 (*td).as_raw_fd(),
1246 rootpath,
1247 &base,
1248 cancellable,
1249 )
1250 .context("Checking out base commit")?;
1251 }
1252
1253 checkout_opts.process_whiteouts = true;
1255 for lc in layer_commits {
1256 let commit = &lc.commit;
1257 tracing::debug!("Unpacking {commit}");
1258 repo.checkout_at(
1259 Some(&checkout_opts),
1260 (*td).as_raw_fd(),
1261 rootpath,
1262 commit,
1263 cancellable,
1264 )
1265 .with_context(|| format!("Checking out layer {commit}"))?;
1266 }
1267
1268 let root_dir = td.open_dir(rootpath)?;
1269
1270 let modifier =
1271 ostree::RepoCommitModifier::new(ostree::RepoCommitModifierFlags::empty(), None);
1272 modifier.set_devino_cache(&devino);
1273 let should_relabel;
1281 if have_derived_layers {
1282 let sepolicy = ostree::SePolicy::new_at(root_dir.as_raw_fd(), cancellable)?;
1283 should_relabel = sepolicy.name().map_or(false, |s| !s.is_empty());
1284 if should_relabel {
1285 tracing::debug!("labeling from merged tree");
1286 modifier.set_sepolicy(Some(&sepolicy));
1287 }
1288 } else if let Some(base) = base_commit.as_ref() {
1289 tracing::debug!("labeling from base tree");
1290 should_relabel = false;
1291 modifier.set_sepolicy_from_commit(repo, &base, cancellable)?;
1293 } else {
1294 panic!("Unexpected state: no derived layers and no base")
1295 }
1296
1297 cleanup_root(&root_dir)?;
1298
1299 let mt = ostree::MutableTree::new();
1300 repo.write_dfd_to_mtree(
1301 (*td).as_raw_fd(),
1302 rootpath,
1303 &mt,
1304 Some(&modifier),
1305 cancellable,
1306 )
1307 .context("Writing merged filesystem to mtree")?;
1308
1309 let merged_root = repo
1310 .write_mtree(&mt, cancellable)
1311 .context("Writing mtree")?;
1312 let merged_root = merged_root.downcast::<ostree::RepoFile>().unwrap();
1313 let parent = base_commit.as_deref();
1316 let merged_commit = repo
1317 .write_commit_with_time(
1318 parent,
1319 None,
1320 None,
1321 Some(&metadata),
1322 &merged_root,
1323 timestamp,
1324 cancellable,
1325 )
1326 .context("Writing commit")?;
1327 if !no_imgref {
1328 repo.transaction_set_ref(None, ostree_ref, Some(merged_commit.as_str()));
1329 }
1330
1331 let n_relabeled_layers = if should_relabel {
1337 let n =
1338 Self::relabel_layers(repo, layer_commits, &modifier, checkout_mode, cancellable)?;
1339 tracing::debug!("relabeled {n} layer commits");
1340 n
1341 } else {
1342 0
1343 };
1344
1345 txn.commit(cancellable)?;
1346
1347 if !disable_gc {
1348 let n: u32 = gc_image_layers_impl(repo, cancellable)?;
1349 tracing::debug!("pruned {n} layers");
1350 if n_relabeled_layers > 0 {
1352 let (_, n_pruned, _) =
1353 repo.prune(ostree::RepoPruneFlags::REFS_ONLY, 0, cancellable)?;
1354 tracing::debug!("pruned {n_pruned} orphaned objects after relabeling");
1355 }
1356 }
1357
1358 let state = query_image_commit(repo, &merged_commit)?;
1361 Ok(state)
1362 }
1363
1364 fn relabel_layers(
1370 repo: &ostree::Repo,
1371 layer_commits: &[LayerRef],
1372 modifier: &ostree::RepoCommitModifier,
1373 checkout_mode: ostree::RepoCheckoutMode,
1374 cancellable: Option<&gio::Cancellable>,
1375 ) -> Result<u32> {
1376 use rustix::fd::AsRawFd;
1377
1378 let repodir = Dir::reopen_dir(&repo.dfd_borrow())?;
1379 let repo_tmp = repodir.open_dir("tmp")?;
1380 let rootpath = "root";
1381 let mut n_relabeled = 0u32;
1382 let checkout_opts = ostree::RepoCheckoutAtOptions {
1383 mode: checkout_mode,
1384 no_copy_fallback: true,
1385 force_copy_zerosized: true,
1386 ..Default::default()
1387 };
1388
1389 for lc in layer_commits {
1390 let (layer_ref, old_commit) = (&lc.ostree_ref, &lc.commit);
1391 let td = cap_std_ext::cap_tempfile::TempDir::new_in(&repo_tmp)?;
1392 repo.checkout_at(
1393 Some(&checkout_opts),
1394 (*td).as_raw_fd(),
1395 rootpath,
1396 old_commit,
1397 cancellable,
1398 )
1399 .with_context(|| format!("Checking out layer {old_commit} for relabeling"))?;
1400
1401 let mt = ostree::MutableTree::new();
1402 repo.write_dfd_to_mtree(
1403 (*td).as_raw_fd(),
1404 rootpath,
1405 &mt,
1406 Some(modifier),
1407 cancellable,
1408 )
1409 .with_context(|| format!("Writing relabeled layer {old_commit} to mtree"))?;
1410
1411 let root = repo
1412 .write_mtree(&mt, cancellable)
1413 .context("Writing mtree")?;
1414 let root = root.downcast::<ostree::RepoFile>().unwrap();
1415
1416 let (commit_v, _) = repo.load_commit(old_commit)?;
1418 let old_metadata = commit_v.child_value(0);
1419 let old_timestamp = ostree::commit_get_timestamp(&commit_v);
1420
1421 let new_commit = repo
1422 .write_commit_with_time(
1423 None,
1424 None,
1425 None,
1426 Some(&old_metadata),
1427 &root,
1428 old_timestamp,
1429 cancellable,
1430 )
1431 .with_context(|| format!("Writing relabeled commit for layer {layer_ref}"))?;
1432
1433 if new_commit != *old_commit {
1434 repo.transaction_set_ref(None, layer_ref, Some(new_commit.as_str()));
1435 n_relabeled += 1;
1436 tracing::debug!("Relabeled layer {layer_ref}: {old_commit} -> {new_commit}");
1437 }
1438 }
1439
1440 Ok(n_relabeled)
1441 }
1442
1443 #[context("Importing")]
1447 pub async fn import(
1448 mut self,
1449 mut import: Box<PreparedImport>,
1450 ) -> Result<Box<LayeredImageState>> {
1451 if let Some(status) = import.format_layer_status() {
1452 system_repo_journal_print(&self.repo, libsystemd::logging::Priority::Info, &status);
1453 }
1454 self.unencapsulate_base(&mut import, false, true).await?;
1457 let des_layers = self.proxy.get_layer_info(&import.proxy_img).await?;
1458 let proxy = self.proxy;
1459 let target_imgref = self.target_imgref.as_ref().unwrap_or(&self.imgref);
1460 let base_commit = import
1461 .ostree_commit_layer
1462 .as_ref()
1463 .map(|c| c.commit.clone().unwrap());
1464
1465 let root_is_transient = if let Some(base) = base_commit.as_ref() {
1466 let rootf = self.repo.read_commit(&base, gio::Cancellable::NONE)?.0;
1467 let rootf = rootf.downcast_ref::<ostree::RepoFile>().unwrap();
1468 crate::ostree_prepareroot::overlayfs_root_enabled(rootf)?
1469 } else {
1470 true
1472 };
1473 tracing::debug!("Base rootfs is transient: {root_is_transient}");
1474
1475 let ostree_ref = ref_for_image(&target_imgref.imgref)?;
1476
1477 let mut layer_commits: Vec<LayerRef> = Vec::new();
1478 let mut layer_filtered_content: Option<MetaFilteredData> = None;
1479 let have_derived_layers = !import.layers.is_empty();
1480 tracing::debug!("Processing layers: {}", import.layers.len());
1481 for layer in import.layers {
1482 if let Some(c) = layer.commit {
1483 tracing::debug!("Reusing fetched commit {}", c);
1484 Self::ensure_ref_for_layer(&self.repo, &layer.ostree_ref, &c)?;
1485
1486 layer_commits.push(LayerRef {
1487 ostree_ref: layer.ostree_ref.clone(),
1488 commit: c.to_string(),
1489 });
1490 } else {
1491 if let Some(p) = self.layer_progress.as_ref() {
1492 p.send(ImportProgress::DerivedLayerStarted(layer.layer.clone()))
1493 .await?;
1494 }
1495 let (blob, driver, media_type) = super::unencapsulate::fetch_layer(
1496 &proxy,
1497 &import.proxy_img,
1498 &import.manifest,
1499 &layer.layer,
1500 self.layer_byte_progress.as_ref(),
1501 des_layers.as_ref(),
1502 self.imgref.imgref.transport,
1503 )
1504 .await?;
1505 let opts = crate::tar::WriteTarOptions {
1509 base: base_commit.clone().or(self.sepolicy_commit.clone()),
1510 selinux: true,
1511 allow_nonusr: root_is_transient,
1512 retain_var: self.ostree_v2024_3,
1513 };
1514 let r = crate::tar::write_tar(
1515 &self.repo,
1516 blob,
1517 media_type,
1518 layer.ostree_ref.as_str(),
1519 Some(opts),
1520 );
1521 let r = super::unencapsulate::join_fetch(r, driver)
1522 .await
1523 .with_context(|| format!("Parsing layer blob {}", layer.layer.digest()))?;
1524 tracing::debug!("Imported layer: {}", r.commit.as_str());
1525 layer_commits.push(LayerRef {
1526 ostree_ref: layer.ostree_ref.clone(),
1527 commit: r.commit,
1528 });
1529 let filtered_owned = HashMap::from_iter(r.filtered.clone());
1530 if let Some((filtered, n_rest)) = bootc_utils::collect_until(
1531 r.filtered.iter(),
1532 const { NonZeroUsize::new(5).unwrap() },
1533 ) {
1534 let mut msg = String::new();
1535 for (path, n) in filtered {
1536 write!(msg, "{path}: {n} ").unwrap();
1537 }
1538 if n_rest > 0 {
1539 write!(msg, "...and {n_rest} more").unwrap();
1540 }
1541 tracing::debug!("Found filtered toplevels: {msg}");
1542 layer_filtered_content
1543 .get_or_insert_default()
1544 .insert(layer.layer.digest().to_string(), filtered_owned);
1545 } else {
1546 tracing::debug!("No filtered content");
1547 }
1548 if let Some(p) = self.layer_progress.as_ref() {
1549 p.send(ImportProgress::DerivedLayerCompleted(layer.layer.clone()))
1550 .await?;
1551 }
1552 }
1553 }
1554
1555 proxy.close_image(&import.proxy_img).await?;
1558
1559 proxy.finalize().await?;
1561 tracing::debug!("finalized proxy");
1562
1563 let _ = self.layer_byte_progress.take();
1565 let _ = self.layer_progress.take();
1566
1567 let mut metadata = BTreeMap::new();
1568 metadata.insert(
1569 META_MANIFEST_DIGEST,
1570 import.manifest_digest.to_string().to_variant(),
1571 );
1572 metadata.insert(
1573 META_MANIFEST,
1574 import.manifest.to_canon_json_string()?.to_variant(),
1575 );
1576 metadata.insert(
1577 META_CONFIG,
1578 import.config.to_canon_json_string()?.to_variant(),
1579 );
1580 metadata.insert(
1581 "ostree.importer.version",
1582 env!("CARGO_PKG_VERSION").to_variant(),
1583 );
1584 let metadata = metadata.to_variant();
1585
1586 let timestamp = timestamp_of_manifest_or_config(&import.manifest, &import.config)
1587 .unwrap_or_else(|| chrono::offset::Utc::now().timestamp() as u64);
1588 let repo = self.repo;
1590 let mut state = crate::tokio_util::spawn_blocking_cancellable_flatten(
1591 move |cancellable| -> Result<Box<LayeredImageState>> {
1592 Self::write_merge_commit_impl(
1593 &repo,
1594 base_commit.as_deref(),
1595 &layer_commits,
1596 have_derived_layers,
1597 metadata,
1598 timestamp,
1599 &ostree_ref,
1600 self.no_imgref,
1601 self.disable_gc,
1602 Some(cancellable),
1603 )
1604 },
1605 )
1606 .await?;
1607 state.verify_text = import.verify_text;
1609 state.filtered_files = layer_filtered_content;
1610 Ok(state)
1611 }
1612}
1613
1614pub fn list_images(repo: &ostree::Repo) -> Result<Vec<String>> {
1616 let cancellable = gio::Cancellable::NONE;
1617 let refs = repo.list_refs_ext(
1618 Some(IMAGE_PREFIX),
1619 ostree::RepoListRefsExtFlags::empty(),
1620 cancellable,
1621 )?;
1622 refs.keys()
1623 .map(|imgname| refescape::unprefix_unescape_ref(IMAGE_PREFIX, imgname))
1624 .collect()
1625}
1626
1627fn try_query_image(
1630 repo: &ostree::Repo,
1631 imgref: &ImageReference,
1632) -> Result<Option<Box<LayeredImageState>>> {
1633 let ostree_ref = &ref_for_image(imgref)?;
1634 if let Some(merge_rev) = repo.resolve_rev(ostree_ref, true)? {
1635 match query_image_commit(repo, merge_rev.as_str()) {
1636 Ok(r) => Ok(Some(r)),
1637 Err(e) => {
1638 eprintln!("error: failed to query image commit: {e}");
1639 Ok(None)
1640 }
1641 }
1642 } else {
1643 Ok(None)
1644 }
1645}
1646
1647#[context("Querying image {imgref}")]
1649pub fn query_image(
1650 repo: &ostree::Repo,
1651 imgref: &ImageReference,
1652) -> Result<Option<Box<LayeredImageState>>> {
1653 let ostree_ref = &ref_for_image(imgref)?;
1654 let merge_rev = repo.resolve_rev(ostree_ref, true)?;
1655 merge_rev
1656 .map(|r| query_image_commit(repo, r.as_str()))
1657 .transpose()
1658}
1659
1660fn parse_cached_update(meta: &glib::VariantDict) -> Result<Option<CachedImageUpdate>> {
1662 let manifest_digest =
1664 if let Some(d) = meta.lookup::<String>(ImageImporter::CACHED_KEY_MANIFEST_DIGEST)? {
1665 d
1666 } else {
1667 return Ok(None);
1670 };
1671 let manifest_digest = Digest::from_str(&manifest_digest)?;
1672 let manifest = meta.lookup_value(ImageImporter::CACHED_KEY_MANIFEST, None);
1675 let manifest: oci_image::ImageManifest = manifest
1676 .as_ref()
1677 .and_then(|v| v.str())
1678 .map(serde_json::from_str)
1679 .transpose()?
1680 .ok_or_else(|| {
1681 anyhow!(
1682 "Expected cached manifest {}",
1683 ImageImporter::CACHED_KEY_MANIFEST
1684 )
1685 })?;
1686 let config = meta.lookup_value(ImageImporter::CACHED_KEY_CONFIG, None);
1687 let config: oci_image::ImageConfiguration = config
1688 .as_ref()
1689 .and_then(|v| v.str())
1690 .map(serde_json::from_str)
1691 .transpose()?
1692 .ok_or_else(|| {
1693 anyhow!(
1694 "Expected cached manifest {}",
1695 ImageImporter::CACHED_KEY_CONFIG
1696 )
1697 })?;
1698 Ok(Some(CachedImageUpdate {
1699 manifest,
1700 config,
1701 manifest_digest,
1702 }))
1703}
1704
1705#[context("Clearing cached update {imgref}")]
1707pub fn clear_cached_update(repo: &ostree::Repo, imgref: &ImageReference) -> Result<()> {
1708 let cancellable = gio::Cancellable::NONE;
1709 let ostree_ref = ref_for_image(imgref)?;
1710 let rev = repo.require_rev(&ostree_ref)?;
1711 let Some(commitmeta) = repo.read_commit_detached_metadata(&rev, cancellable)? else {
1712 return Ok(());
1713 };
1714
1715 let mut commitmeta: BTreeMap<String, glib::Variant> =
1717 BTreeMap::from_variant(&commitmeta).unwrap();
1718 let mut changed = false;
1719 for key in [
1720 ImageImporter::CACHED_KEY_CONFIG,
1721 ImageImporter::CACHED_KEY_MANIFEST,
1722 ImageImporter::CACHED_KEY_MANIFEST_DIGEST,
1723 ] {
1724 if commitmeta.remove(key).is_some() {
1725 changed = true;
1726 }
1727 }
1728 if !changed {
1729 return Ok(());
1730 }
1731 let commitmeta = glib::Variant::from(commitmeta);
1732 repo.write_commit_detached_metadata(&rev, Some(&commitmeta), cancellable)?;
1733 Ok(())
1734}
1735
1736pub fn query_image_commit(repo: &ostree::Repo, commit: &str) -> Result<Box<LayeredImageState>> {
1739 let merge_commit = commit.to_string();
1740 let merge_commit_obj = repo.load_commit(commit)?.0;
1741 let commit_meta = &merge_commit_obj.child_value(0);
1742 let commit_meta = &ostree::glib::VariantDict::new(Some(commit_meta));
1743 let (manifest, manifest_digest) = manifest_data_from_commitmeta(commit_meta)?;
1744 let configuration = image_config_from_commitmeta(commit_meta)?;
1745 let mut layers = manifest.layers().iter().cloned();
1746 let base_layer = layers.next().ok_or_else(|| anyhow!("No layers found"))?;
1748 let base_layer = query_layer(repo, base_layer)?;
1749 let ostree_ref = base_layer.ostree_ref.as_str();
1750 let base_commit = base_layer
1751 .commit
1752 .ok_or_else(|| anyhow!("Missing base image ref {ostree_ref}"))?;
1753
1754 let detached_commitmeta =
1755 repo.read_commit_detached_metadata(&merge_commit, gio::Cancellable::NONE)?;
1756 let detached_commitmeta = detached_commitmeta
1757 .as_ref()
1758 .map(|v| glib::VariantDict::new(Some(v)));
1759 let cached_update = detached_commitmeta
1760 .as_ref()
1761 .map(parse_cached_update)
1762 .transpose()?
1763 .flatten();
1764 let state = Box::new(LayeredImageState {
1765 base_commit,
1766 merge_commit,
1767 manifest_digest,
1768 manifest,
1769 configuration,
1770 cached_update,
1771 verify_text: None,
1773 filtered_files: None,
1774 });
1775 tracing::debug!("Wrote merge commit {}", state.merge_commit);
1776 Ok(state)
1777}
1778
1779fn manifest_for_image(repo: &ostree::Repo, imgref: &ImageReference) -> Result<ImageManifest> {
1780 let ostree_ref = ref_for_image(imgref)?;
1781 let rev = repo.require_rev(&ostree_ref)?;
1782 let (commit_obj, _) = repo.load_commit(rev.as_str())?;
1783 let commit_meta = &glib::VariantDict::new(Some(&commit_obj.child_value(0)));
1784 Ok(manifest_data_from_commitmeta(commit_meta)?.0)
1785}
1786
1787#[context("Copying image")]
1790pub async fn copy(
1791 src_repo: &ostree::Repo,
1792 src_imgref: &ImageReference,
1793 dest_repo: &ostree::Repo,
1794 dest_imgref: &ImageReference,
1795) -> Result<()> {
1796 let src_ostree_ref = ref_for_image(src_imgref)?;
1797 let src_commit = src_repo.require_rev(&src_ostree_ref)?;
1798 let manifest = manifest_for_image(src_repo, src_imgref)?;
1799 let layer_refs = manifest
1801 .layers()
1802 .iter()
1803 .map(ref_for_layer)
1804 .chain(std::iter::once(Ok(src_commit.to_string())));
1805 for ostree_ref in layer_refs {
1806 let ostree_ref = ostree_ref?;
1807 let src_repo = src_repo.clone();
1808 let dest_repo = dest_repo.clone();
1809 crate::tokio_util::spawn_blocking_cancellable_flatten(move |cancellable| -> Result<_> {
1810 let cancellable = Some(cancellable);
1811 let srcfd = &format!("file:///proc/self/fd/{}", src_repo.dfd());
1812 let flags = ostree::RepoPullFlags::MIRROR;
1813 let opts = glib::VariantDict::new(None);
1814 let refs = [ostree_ref.as_str()];
1815 opts.insert("disable-verify-bindings", true);
1817 opts.insert("refs", &refs[..]);
1818 opts.insert("flags", flags.bits() as i32);
1819 let options = opts.to_variant();
1820 dest_repo.pull_with_options(srcfd, &options, None, cancellable)?;
1821 Ok(())
1822 })
1823 .await?;
1824 }
1825
1826 let dest_ostree_ref = ref_for_image(dest_imgref)?;
1827 dest_repo.set_ref_immediate(
1828 None,
1829 &dest_ostree_ref,
1830 Some(&src_commit),
1831 gio::Cancellable::NONE,
1832 )?;
1833
1834 Ok(())
1835}
1836
1837#[derive(Clone, Debug, Default)]
1839#[non_exhaustive]
1840pub struct ExportToOCIOpts {
1841 pub skip_compression: bool,
1843 pub authfile: Option<std::path::PathBuf>,
1845 pub progress_to_stdout: bool,
1847}
1848
1849fn chunking_from_layer_committed(
1854 repo: &ostree::Repo,
1855 l: &Descriptor,
1856 chunking: &mut chunking::Chunking,
1857) -> Result<()> {
1858 let mut chunk = Chunk::default();
1859 let layer_ref = &ref_for_layer(l)?;
1860 let root = repo.read_commit(layer_ref, gio::Cancellable::NONE)?.0;
1861 let e = root.enumerate_children(
1862 "standard::name,standard::size",
1863 gio::FileQueryInfoFlags::NOFOLLOW_SYMLINKS,
1864 gio::Cancellable::NONE,
1865 )?;
1866 for child in e.clone() {
1867 let child = &child?;
1868 let name = child.name();
1870 let name = Utf8Path::from_path(&name).unwrap();
1872 ostree::validate_checksum_string(name.as_str())?;
1873 chunking.remainder.move_obj(&mut chunk, name.as_str());
1874 }
1875 chunking.chunks.push(chunk);
1876 Ok(())
1877}
1878
1879#[context("Copying image")]
1881pub(crate) fn export_to_oci(
1882 repo: &ostree::Repo,
1883 imgref: &ImageReference,
1884 dest_oci: &Dir,
1885 tag: Option<&str>,
1886 opts: ExportToOCIOpts,
1887) -> Result<Descriptor> {
1888 let srcinfo = query_image(repo, imgref)?.ok_or_else(|| anyhow!("No such image"))?;
1889 let (commit_layer, component_layers, remaining_layers) =
1890 parse_manifest_layout(&srcinfo.manifest, &srcinfo.configuration)?;
1891
1892 let mut new_manifest = srcinfo.manifest.clone();
1896 new_manifest.layers_mut().clear();
1897 let mut new_config = srcinfo.configuration.clone();
1898 if let Some(history) = new_config.history_mut() {
1899 history.clear();
1900 }
1901 new_config.rootfs_mut().diff_ids_mut().clear();
1902
1903 let opts = ExportOpts {
1904 skip_compression: opts.skip_compression,
1905 authfile: opts.authfile,
1906 ..Default::default()
1907 };
1908
1909 let mut labels = HashMap::new();
1910
1911 let mut dest_oci = ocidir::OciDir::ensure(dest_oci.try_clone()?)?;
1912
1913 let commit_chunk_ref = commit_layer
1914 .as_ref()
1915 .map(|l| ref_for_layer(l))
1916 .transpose()?;
1917 let commit_chunk_rev = commit_chunk_ref
1918 .as_ref()
1919 .map(|r| repo.require_rev(&r))
1920 .transpose()?;
1921 if let Some(commit_chunk_rev) = commit_chunk_rev {
1922 let mut chunking = chunking::Chunking::new(repo, &commit_chunk_rev)?;
1923 for layer in component_layers {
1924 chunking_from_layer_committed(repo, layer, &mut chunking)?;
1925 }
1926
1927 export_chunked(
1930 repo,
1931 &srcinfo.base_commit,
1932 &mut dest_oci,
1933 &mut new_manifest,
1934 &mut new_config,
1935 &mut labels,
1936 chunking,
1937 &opts,
1938 "",
1939 )?;
1940 }
1941
1942 let compression = opts.skip_compression.then_some(Compression::none());
1944 for (i, layer) in remaining_layers.iter().enumerate() {
1945 let layer_ref = &ref_for_layer(layer)?;
1946 let mut target_blob = dest_oci.create_gzip_layer(compression)?;
1947 let export_opts = crate::tar::ExportOptions { raw: true };
1949 crate::tar::export_commit(
1950 repo,
1951 layer_ref.as_str(),
1952 &mut target_blob,
1953 Some(export_opts),
1954 )?;
1955 let layer = target_blob.complete()?;
1956 let previous_annotations = srcinfo
1957 .manifest
1958 .layers()
1959 .get(i)
1960 .and_then(|l| l.annotations().as_ref())
1961 .cloned();
1962 let history = srcinfo.configuration.history().as_ref();
1963 let history_entry = history.and_then(|v| v.get(i));
1964 let previous_description = history_entry
1965 .clone()
1966 .and_then(|h| h.comment().as_deref())
1967 .unwrap_or_default();
1968
1969 let previous_created = history_entry
1970 .and_then(|h| h.created().as_deref())
1971 .and_then(bootc_utils::try_deserialize_timestamp)
1972 .unwrap_or_default();
1973
1974 dest_oci.push_layer_full(
1975 &mut new_manifest,
1976 &mut new_config,
1977 layer,
1978 previous_annotations,
1979 previous_description,
1980 previous_created,
1981 )
1982 }
1983
1984 let new_config = dest_oci.write_config(new_config)?;
1985 new_manifest.set_config(new_config);
1986
1987 Ok(dest_oci.insert_manifest(new_manifest, tag, oci_image::Platform::default())?)
1988}
1989
1990#[context("Export")]
1993pub async fn export(
1994 repo: &ostree::Repo,
1995 src_imgref: &ImageReference,
1996 dest_imgref: &ImageReference,
1997 opts: Option<ExportToOCIOpts>,
1998) -> Result<oci_image::Digest> {
1999 let opts = opts.unwrap_or_default();
2000 let target_oci = dest_imgref.transport == Transport::OciDir;
2001 let tempdir = if !target_oci {
2002 let vartmp = cap_std::fs::Dir::open_ambient_dir("/var/tmp", cap_std::ambient_authority())?;
2003 let td = cap_std_ext::cap_tempfile::TempDir::new_in(&vartmp)?;
2004 let opts = ExportToOCIOpts {
2006 skip_compression: true,
2007 progress_to_stdout: opts.progress_to_stdout,
2008 ..Default::default()
2009 };
2010 export_to_oci(repo, src_imgref, &td, None, opts)?;
2011 td
2012 } else {
2013 let (path, tag) = parse_oci_path_and_tag(dest_imgref.name.as_str());
2014 tracing::debug!("using OCI path={path} tag={tag:?}");
2015 let path = Dir::open_ambient_dir(path, cap_std::ambient_authority())
2016 .with_context(|| format!("Opening {path}"))?;
2017 let descriptor = export_to_oci(repo, src_imgref, &path, tag, opts)?;
2018 return Ok(descriptor.digest().clone());
2019 };
2020 let target_fd = 3i32;
2022 let tempoci = ImageReference {
2023 transport: Transport::OciDir,
2024 name: format!("/proc/self/fd/{target_fd}"),
2025 };
2026 let authfile = opts.authfile.as_deref();
2027 skopeo::copy(
2028 &tempoci,
2029 dest_imgref,
2030 authfile,
2031 Some((std::sync::Arc::new(tempdir.try_clone()?.into()), target_fd)),
2032 opts.progress_to_stdout,
2033 )
2034 .await
2035}
2036
2037#[context("Listing deployment manifests")]
2040fn list_container_deployment_manifests(
2041 repo: &ostree::Repo,
2042 cancellable: Option<&gio::Cancellable>,
2043) -> Result<Vec<ImageManifest>> {
2044 let commits = OSTREE_BASE_DEPLOYMENT_REFS
2047 .iter()
2048 .chain(RPMOSTREE_BASE_REFS)
2049 .chain(std::iter::once(&BASE_IMAGE_PREFIX))
2050 .try_fold(
2051 std::collections::HashSet::new(),
2052 |mut acc, &p| -> Result<_> {
2053 let refs = repo.list_refs_ext(
2054 Some(p),
2055 ostree::RepoListRefsExtFlags::empty(),
2056 cancellable,
2057 )?;
2058 for (_, v) in refs {
2059 acc.insert(v);
2060 }
2061 Ok(acc)
2062 },
2063 )?;
2064 let mut r = Vec::new();
2066 for commit in commits {
2067 let commit_obj = repo.load_commit(&commit)?.0;
2068 let commit_meta = &glib::VariantDict::new(Some(&commit_obj.child_value(0)));
2069 if commit_meta
2070 .lookup::<String>(META_MANIFEST_DIGEST)?
2071 .is_some()
2072 {
2073 tracing::trace!("Commit {commit} is a container image");
2074 let manifest = manifest_data_from_commitmeta(commit_meta)?.0;
2075 r.push(manifest);
2076 }
2077 }
2078 Ok(r)
2079}
2080
2081pub fn gc_image_layers(repo: &ostree::Repo) -> Result<u32> {
2087 gc_image_layers_impl(repo, gio::Cancellable::NONE)
2088}
2089
2090#[context("Pruning image layers")]
2091fn gc_image_layers_impl(
2092 repo: &ostree::Repo,
2093 cancellable: Option<&gio::Cancellable>,
2094) -> Result<u32> {
2095 let all_images = list_images(repo)?;
2096 let deployment_commits = list_container_deployment_manifests(repo, cancellable)?;
2097 let all_manifests = all_images
2098 .into_iter()
2099 .map(|img| {
2100 ImageReference::try_from(img.as_str()).and_then(|ir| manifest_for_image(repo, &ir))
2101 })
2102 .chain(deployment_commits.into_iter().map(Ok))
2103 .collect::<Result<Vec<_>>>()?;
2104 tracing::debug!("Images found: {}", all_manifests.len());
2105 let mut referenced_layers = BTreeSet::new();
2106 for m in all_manifests.iter() {
2107 for layer in m.layers() {
2108 referenced_layers.insert(layer.digest().to_string());
2109 }
2110 }
2111 tracing::debug!("Referenced layers: {}", referenced_layers.len());
2112 let found_layers = repo
2113 .list_refs_ext(
2114 Some(LAYER_PREFIX),
2115 ostree::RepoListRefsExtFlags::empty(),
2116 cancellable,
2117 )?
2118 .into_iter()
2119 .map(|v| v.0);
2120 tracing::debug!("Found layers: {}", found_layers.len());
2121 let mut pruned = 0u32;
2122 for layer_ref in found_layers {
2123 let layer_digest = refescape::unprefix_unescape_ref(LAYER_PREFIX, &layer_ref)?;
2124 if referenced_layers.remove(layer_digest.as_str()) {
2125 continue;
2126 }
2127 pruned += 1;
2128 tracing::debug!("Pruning: {}", layer_ref.as_str());
2129 repo.set_ref_immediate(None, layer_ref.as_str(), None, cancellable)?;
2130 }
2131
2132 Ok(pruned)
2133}
2134
2135#[cfg(feature = "internal-testing-api")]
2136pub fn count_layer_references(repo: &ostree::Repo) -> Result<u32> {
2138 let cancellable = gio::Cancellable::NONE;
2139 let n = repo
2140 .list_refs_ext(
2141 Some(LAYER_PREFIX),
2142 ostree::RepoListRefsExtFlags::empty(),
2143 cancellable,
2144 )?
2145 .len();
2146 Ok(n as u32)
2147}
2148
2149pub fn image_filtered_content_warning(
2151 filtered_files: &Option<MetaFilteredData>,
2152) -> Result<Option<String>> {
2153 use std::fmt::Write;
2154
2155 let r = filtered_files.as_ref().map(|v| {
2156 let mut filtered = BTreeMap::<&String, u32>::new();
2157 for paths in v.values() {
2158 for (k, v) in paths {
2159 let e = filtered.entry(k).or_default();
2160 *e += v;
2161 }
2162 }
2163 let mut buf = "Image contains non-ostree compatible file paths:".to_string();
2164 for (k, v) in filtered {
2165 write!(buf, " {k}: {v}").unwrap();
2166 }
2167 buf
2168 });
2169 Ok(r)
2170}
2171
2172#[context("Pruning {img}")]
2179pub fn remove_image(repo: &ostree::Repo, img: &ImageReference) -> Result<bool> {
2180 let ostree_ref = &ref_for_image(img)?;
2181 let found = repo.resolve_rev(ostree_ref, true)?.is_some();
2182 if found {
2185 repo.set_ref_immediate(None, ostree_ref, None, gio::Cancellable::NONE)?;
2186 }
2187 Ok(found)
2188}
2189
2190pub fn remove_images<'a>(
2197 repo: &ostree::Repo,
2198 imgs: impl IntoIterator<Item = &'a ImageReference>,
2199) -> Result<()> {
2200 let mut missing = Vec::new();
2201 for img in imgs.into_iter() {
2202 let found = remove_image(repo, img)?;
2203 if !found {
2204 missing.push(img);
2205 }
2206 }
2207 if !missing.is_empty() {
2208 let missing = missing.into_iter().fold("".to_string(), |mut a, v| {
2209 a.push_str(&v.to_string());
2210 a
2211 });
2212 return Err(anyhow::anyhow!("Missing images: {missing}"));
2213 }
2214 Ok(())
2215}
2216
2217#[derive(Debug, Default)]
2218struct CompareState {
2219 verified: BTreeSet<Utf8PathBuf>,
2220 inode_corrupted: BTreeSet<Utf8PathBuf>,
2221 unknown_corrupted: BTreeSet<Utf8PathBuf>,
2222}
2223
2224impl CompareState {
2225 fn is_ok(&self) -> bool {
2226 self.inode_corrupted.is_empty() && self.unknown_corrupted.is_empty()
2227 }
2228}
2229
2230fn compare_file_info(src: &gio::FileInfo, target: &gio::FileInfo) -> bool {
2231 if src.file_type() != target.file_type() {
2232 return false;
2233 }
2234 if src.size() != target.size() {
2235 return false;
2236 }
2237 for attr in ["unix::uid", "unix::gid", "unix::mode"] {
2238 if src.attribute_uint32(attr) != target.attribute_uint32(attr) {
2239 return false;
2240 }
2241 }
2242 true
2243}
2244
2245#[context("Querying object inode")]
2246fn inode_of_object(repo: &ostree::Repo, checksum: &str) -> Result<u64> {
2247 let repodir = Dir::reopen_dir(&repo.dfd_borrow())?;
2248 let (prefix, suffix) = checksum.split_at(2);
2249 let objpath = format!("objects/{prefix}/{suffix}.file");
2250 let metadata = repodir.symlink_metadata(objpath)?;
2251 Ok(metadata.ino())
2252}
2253
2254fn compare_commit_trees(
2255 repo: &ostree::Repo,
2256 root: &Utf8Path,
2257 target: &ostree::RepoFile,
2258 expected: &ostree::RepoFile,
2259 exact: bool,
2260 colliding_inodes: &BTreeSet<u64>,
2261 state: &mut CompareState,
2262) -> Result<()> {
2263 let cancellable = gio::Cancellable::NONE;
2264 let queryattrs = "standard::name,standard::type";
2265 let queryflags = gio::FileQueryInfoFlags::NOFOLLOW_SYMLINKS;
2266 let expected_iter = expected.enumerate_children(queryattrs, queryflags, cancellable)?;
2267
2268 while let Some(expected_info) = expected_iter.next_file(cancellable)? {
2269 let expected_child = expected_iter.child(&expected_info);
2270 let name = expected_info.name();
2271 let name = name.to_str().expect("UTF-8 ostree name");
2272 let path = Utf8PathBuf::from(format!("{root}{name}"));
2273 let target_child = target.child(name);
2274 let target_info = crate::diff::query_info_optional(&target_child, queryattrs, queryflags)
2275 .context("querying optional to")?;
2276 let is_dir = matches!(expected_info.file_type(), gio::FileType::Directory);
2277 if let Some(target_info) = target_info {
2278 let to_child = target_child
2279 .downcast::<ostree::RepoFile>()
2280 .expect("downcast");
2281 to_child.ensure_resolved()?;
2282 let from_child = expected_child
2283 .downcast::<ostree::RepoFile>()
2284 .expect("downcast");
2285 from_child.ensure_resolved()?;
2286
2287 if is_dir {
2288 let from_contents_checksum = from_child.tree_get_contents_checksum();
2289 let to_contents_checksum = to_child.tree_get_contents_checksum();
2290 if from_contents_checksum != to_contents_checksum {
2291 let subpath = Utf8PathBuf::from(format!("{path}/"));
2292 compare_commit_trees(
2293 repo,
2294 &subpath,
2295 &from_child,
2296 &to_child,
2297 exact,
2298 colliding_inodes,
2299 state,
2300 )?;
2301 }
2302 } else {
2303 let from_checksum = from_child.checksum();
2304 let to_checksum = to_child.checksum();
2305 let matches = if exact {
2306 from_checksum == to_checksum
2307 } else {
2308 compare_file_info(&target_info, &expected_info)
2309 };
2310 if !matches {
2311 let from_inode = inode_of_object(repo, &from_checksum)?;
2312 let to_inode = inode_of_object(repo, &to_checksum)?;
2313 if colliding_inodes.contains(&from_inode)
2314 || colliding_inodes.contains(&to_inode)
2315 {
2316 state.inode_corrupted.insert(path);
2317 } else {
2318 state.unknown_corrupted.insert(path);
2319 }
2320 } else {
2321 state.verified.insert(path);
2322 }
2323 }
2324 } else {
2325 eprintln!("Missing {path}");
2326 state.unknown_corrupted.insert(path);
2327 }
2328 }
2329 Ok(())
2330}
2331
2332#[context("Verifying container image state")]
2333pub(crate) fn verify_container_image(
2334 sysroot: &SysrootLock,
2335 imgref: &ImageReference,
2336 state: &LayeredImageState,
2337 colliding_inodes: &BTreeSet<u64>,
2338 verbose: bool,
2339) -> Result<bool> {
2340 let cancellable = gio::Cancellable::NONE;
2341 let repo = &sysroot.repo();
2342 let merge_commit = state.merge_commit.as_str();
2343 let merge_commit_root = repo.read_commit(merge_commit, gio::Cancellable::NONE)?.0;
2344 let merge_commit_root = merge_commit_root
2345 .downcast::<ostree::RepoFile>()
2346 .expect("downcast");
2347 merge_commit_root.ensure_resolved()?;
2348
2349 let (commit_layer, _component_layers, remaining_layers) =
2350 parse_manifest_layout(&state.manifest, &state.configuration)?;
2351
2352 let mut comparison_state = CompareState::default();
2353
2354 let query = |l: &Descriptor| query_layer(repo, l.clone());
2355
2356 let base_tree = repo
2357 .read_commit(&state.base_commit, cancellable)?
2358 .0
2359 .downcast::<ostree::RepoFile>()
2360 .expect("downcast");
2361 if let Some(commit_layer) = commit_layer {
2362 println!(
2363 "Verifying with base ostree layer {}",
2364 ref_for_layer(commit_layer)?
2365 );
2366 }
2367 compare_commit_trees(
2368 repo,
2369 "/".into(),
2370 &merge_commit_root,
2371 &base_tree,
2372 true,
2373 colliding_inodes,
2374 &mut comparison_state,
2375 )?;
2376
2377 let remaining_layers = remaining_layers
2378 .into_iter()
2379 .map(query)
2380 .collect::<Result<Vec<_>>>()?;
2381
2382 println!("Image has {} derived layers", remaining_layers.len());
2383
2384 for layer in remaining_layers.iter().rev() {
2385 let layer_ref = layer.ostree_ref.as_str();
2386 let layer_commit = layer
2387 .commit
2388 .as_deref()
2389 .ok_or_else(|| anyhow!("Missing layer {layer_ref}"))?;
2390 let layer_tree = repo
2391 .read_commit(layer_commit, cancellable)?
2392 .0
2393 .downcast::<ostree::RepoFile>()
2394 .expect("downcast");
2395 compare_commit_trees(
2396 repo,
2397 "/".into(),
2398 &merge_commit_root,
2399 &layer_tree,
2400 false,
2401 colliding_inodes,
2402 &mut comparison_state,
2403 )?;
2404 }
2405
2406 let n_verified = comparison_state.verified.len();
2407 if comparison_state.is_ok() {
2408 println!("OK image {imgref} (verified={n_verified})");
2409 println!();
2410 } else {
2411 let n_inode = comparison_state.inode_corrupted.len();
2412 let n_other = comparison_state.unknown_corrupted.len();
2413 eprintln!("warning: Found corrupted merge commit");
2414 eprintln!(" inode clashes: {n_inode}");
2415 eprintln!(" unknown: {n_other}");
2416 eprintln!(" ok: {n_verified}");
2417 if verbose {
2418 eprintln!("Mismatches:");
2419 for path in comparison_state.inode_corrupted {
2420 eprintln!(" inode: {path}");
2421 }
2422 for path in comparison_state.unknown_corrupted {
2423 eprintln!(" other: {path}");
2424 }
2425 }
2426 eprintln!();
2427 return Ok(false);
2428 }
2429
2430 Ok(true)
2431}
2432
2433#[cfg(test)]
2434mod tests {
2435 use cap_std_ext::cap_tempfile;
2436 use oci_image::{DescriptorBuilder, MediaType, Sha256Digest};
2437
2438 use super::*;
2439
2440 #[test]
2441 fn test_ref_for_descriptor() {
2442 let d = DescriptorBuilder::default()
2443 .size(42u64)
2444 .media_type(MediaType::ImageManifest)
2445 .digest(
2446 Sha256Digest::from_str(
2447 "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
2448 )
2449 .unwrap(),
2450 )
2451 .build()
2452 .unwrap();
2453 assert_eq!(
2454 ref_for_layer(&d).unwrap(),
2455 "ostree/container/blob/sha256_3A_2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"
2456 );
2457 }
2458
2459 #[test]
2460 fn test_cleanup_root() -> Result<()> {
2461 let td = cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
2462 let usretc = "usr/etc";
2463 cleanup_root(&td).unwrap();
2464 td.create_dir_all(usretc)?;
2465 let usretc = &td.open_dir(usretc)?;
2466 usretc.write("hostname", b"hostname")?;
2467 cleanup_root(&td).unwrap();
2468 assert!(usretc.try_exists("hostname")?);
2469 usretc.write("hostname", b"")?;
2470 cleanup_root(&td).unwrap();
2471 assert!(!td.try_exists("hostname")?);
2472
2473 usretc.symlink_contents("../run/systemd/stub-resolv.conf", "resolv.conf")?;
2474 cleanup_root(&td).unwrap();
2475 assert!(usretc.symlink_metadata("resolv.conf")?.is_symlink());
2476 usretc.remove_file("resolv.conf")?;
2477 usretc.write("resolv.conf", b"")?;
2478 cleanup_root(&td).unwrap();
2479 assert!(!usretc.try_exists("resolv.conf")?);
2480
2481 Ok(())
2482 }
2483}