ostree_ext/container/
store.rs

1//! APIs for storing (layered) container images as OSTree commits
2//!
3//! # Extension of encapsulation support
4//!
5//! This code supports ingesting arbitrary layered container images from an ostree-exported
6//! base.  See [`encapsulate`][`super::encapsulate()`] for more information on encapsulation of images.
7
8use super::*;
9use crate::chunking::{self, Chunk};
10use crate::generic_decompress::Decompressor;
11use crate::logging::system_repo_journal_print;
12use crate::refescape;
13use crate::sysroot::SysrootLock;
14use anyhow::{Context, anyhow};
15use bootc_utils::ResultExt;
16use camino::{Utf8Path, Utf8PathBuf};
17use canon_json::CanonJsonSerialize;
18use cap_std_ext::cap_std;
19use cap_std_ext::cap_std::fs::{Dir, MetadataExt};
20
21use cap_std_ext::dirext::CapStdExtDirExt;
22use containers_image_proxy::{ImageProxy, OpenedImage};
23use flate2::Compression;
24use fn_error_context::context;
25use futures_util::TryFutureExt;
26use glib::prelude::*;
27use oci_spec::image::{
28    self as oci_image, Arch, Descriptor, Digest, History, ImageConfiguration, ImageManifest,
29};
30use ocidir::oci_spec::distribution::Reference;
31use ostree::prelude::{Cast, FileEnumeratorExt, FileExt, ToVariant};
32use ostree::{gio, glib};
33use std::collections::{BTreeMap, BTreeSet, HashMap};
34use std::fmt::Write as _;
35use std::iter::FromIterator;
36use std::num::NonZeroUsize;
37use tokio::sync::mpsc::{Receiver, Sender};
38
39/// Configuration for the proxy.
40///
41/// We re-export this rather than inventing our own wrapper
42/// in the interest of avoiding duplication.
43pub use containers_image_proxy::ImageProxyConfig;
44
45/// The ostree ref prefix for blobs.
46const LAYER_PREFIX: &str = "ostree/container/blob";
47/// The ostree ref prefix for image references.
48const IMAGE_PREFIX: &str = "ostree/container/image";
49/// The ostree ref prefix for "base" image references that are used by derived images.
50/// If you maintain tooling which is locally building derived commits, write a ref
51/// with this prefix that is owned by your code.  It's a best practice to prefix the
52/// ref with the project name, so the final ref may be of the form e.g. `ostree/container/baseimage/bootc/foo`.
53pub const BASE_IMAGE_PREFIX: &str = "ostree/container/baseimage";
54
55/// The key injected into the merge commit for the manifest digest.
56pub(crate) const META_MANIFEST_DIGEST: &str = "ostree.manifest-digest";
57/// The key injected into the merge commit with the manifest serialized as JSON.
58const META_MANIFEST: &str = "ostree.manifest";
59/// The key injected into the merge commit with the image configuration serialized as JSON.
60const META_CONFIG: &str = "ostree.container.image-config";
61/// The type used to store content filtering information.
62pub type MetaFilteredData = HashMap<String, HashMap<String, u32>>;
63
64/// The ref prefixes which point to ostree deployments.  (TODO: Add an official API for this)
65const OSTREE_BASE_DEPLOYMENT_REFS: &[&str] = &["ostree/0", "ostree/1"];
66/// A layering violation we'll carry for a bit to band-aid over https://github.com/coreos/rpm-ostree/issues/4185
67const RPMOSTREE_BASE_REFS: &[&str] = &["rpmostree/base"];
68
69/// Convert e.g. sha256:12345... into `/ostree/container/blob/sha256_2B12345...`.
70fn ref_for_blob_digest(d: &str) -> Result<String> {
71    refescape::prefix_escape_for_ref(LAYER_PREFIX, d)
72}
73
74/// Convert e.g. sha256:12345... into `/ostree/container/blob/sha256_2B12345...`.
75fn ref_for_layer(l: &oci_image::Descriptor) -> Result<String> {
76    ref_for_blob_digest(&l.digest().as_ref())
77}
78
79/// Convert e.g. sha256:12345... into `/ostree/container/blob/sha256_2B12345...`.
80fn ref_for_image(l: &ImageReference) -> Result<String> {
81    refescape::prefix_escape_for_ref(IMAGE_PREFIX, &l.to_string())
82}
83
84/// Sent across a channel to track start and end of a container fetch.
85#[derive(Debug)]
86pub enum ImportProgress {
87    /// Started fetching this layer.
88    OstreeChunkStarted(Descriptor),
89    /// Successfully completed the fetch of this layer.
90    OstreeChunkCompleted(Descriptor),
91    /// Started fetching this layer.
92    DerivedLayerStarted(Descriptor),
93    /// Successfully completed the fetch of this layer.
94    DerivedLayerCompleted(Descriptor),
95}
96
97impl ImportProgress {
98    /// Returns `true` if this message signifies the start of a new layer being fetched.
99    pub fn is_starting(&self) -> bool {
100        match self {
101            ImportProgress::OstreeChunkStarted(_) => true,
102            ImportProgress::OstreeChunkCompleted(_) => false,
103            ImportProgress::DerivedLayerStarted(_) => true,
104            ImportProgress::DerivedLayerCompleted(_) => false,
105        }
106    }
107}
108
109/// Sent across a channel to track the byte-level progress of a layer fetch.
110#[derive(Clone, Debug)]
111pub struct LayerProgress {
112    /// Index of the layer in the manifest
113    pub layer_index: usize,
114    /// Number of bytes downloaded
115    pub fetched: u64,
116    /// Total number of bytes outstanding
117    pub total: u64,
118}
119
120/// State of an already pulled layered image.
121#[derive(Debug, PartialEq, Eq)]
122pub struct LayeredImageState {
123    /// The base ostree commit
124    pub base_commit: String,
125    /// The merge commit unions all layers
126    pub merge_commit: String,
127    /// The digest of the original manifest
128    pub manifest_digest: Digest,
129    /// The image manifest
130    pub manifest: ImageManifest,
131    /// The image configuration
132    pub configuration: ImageConfiguration,
133    /// Metadata for (cached, previously fetched) updates to the image, if any.
134    pub cached_update: Option<CachedImageUpdate>,
135    /// The signature verification text from libostree for the base commit;
136    /// in the future we should probably instead just proxy a signature object
137    /// instead, but this is sufficient for now.
138    pub verify_text: Option<String>,
139    /// Files that were filtered out during the import.
140    pub filtered_files: Option<MetaFilteredData>,
141}
142
143impl LayeredImageState {
144    /// Return the merged ostree commit for this image.
145    ///
146    /// This is not the same as the underlying base ostree commit.
147    pub fn get_commit(&self) -> &str {
148        self.merge_commit.as_str()
149    }
150
151    /// Retrieve the container image version.
152    pub fn version(&self) -> Option<&str> {
153        super::version_for_config(&self.configuration)
154    }
155}
156
157/// Locally cached metadata for an update to an existing image.
158#[derive(Debug, PartialEq, Eq)]
159pub struct CachedImageUpdate {
160    /// The image manifest
161    pub manifest: ImageManifest,
162    /// The image configuration
163    pub config: ImageConfiguration,
164    /// The digest of the manifest
165    pub manifest_digest: Digest,
166}
167
168impl CachedImageUpdate {
169    /// Retrieve the container image version.
170    pub fn version(&self) -> Option<&str> {
171        super::version_for_config(&self.config)
172    }
173}
174
175/// Context for importing a container image.
176#[derive(Debug)]
177pub struct ImageImporter {
178    repo: ostree::Repo,
179    pub(crate) proxy: ImageProxy,
180    imgref: OstreeImageReference,
181    target_imgref: Option<OstreeImageReference>,
182    no_imgref: bool,  // If true, do not write final image ref
183    disable_gc: bool, // If true, don't prune unused image layers
184    /// If true, require the image has the bootable flag
185    require_bootable: bool,
186    /// Do not attempt to contact the network
187    offline: bool,
188    /// If true, we have ostree v2024.3 or newer.
189    ostree_v2024_3: bool,
190
191    layer_progress: Option<Sender<ImportProgress>>,
192    layer_byte_progress: Option<tokio::sync::watch::Sender<Option<LayerProgress>>>,
193}
194
195/// Result of invoking [`ImageImporter::prepare`].
196#[derive(Debug)]
197pub enum PrepareResult {
198    /// The image reference is already present; the contained string is the OSTree commit.
199    AlreadyPresent(Box<LayeredImageState>),
200    /// The image needs to be downloaded
201    Ready(Box<PreparedImport>),
202}
203
204/// A container image layer with associated downloaded-or-not state.
205#[derive(Debug)]
206pub struct ManifestLayerState {
207    /// The underlying layer descriptor.
208    pub layer: oci_image::Descriptor,
209    // TODO semver: Make this readonly via an accessor
210    /// The ostree ref name for this layer.
211    pub ostree_ref: String,
212    // TODO semver: Make this readonly via an accessor
213    /// The ostree commit that caches this layer, if present.
214    pub commit: Option<String>,
215}
216
217impl ManifestLayerState {
218    /// Return the layer descriptor.
219    pub fn layer(&self) -> &oci_image::Descriptor {
220        &self.layer
221    }
222}
223
224/// Information about which layers need to be downloaded.
225#[derive(Debug)]
226pub struct PreparedImport {
227    /// The manifest digest that was found
228    pub manifest_digest: Digest,
229    /// The deserialized manifest.
230    pub manifest: oci_image::ImageManifest,
231    /// The deserialized configuration.
232    pub config: oci_image::ImageConfiguration,
233    /// The previous manifest
234    pub previous_state: Option<Box<LayeredImageState>>,
235    /// The previously stored manifest digest.
236    pub previous_manifest_digest: Option<Digest>,
237    /// The previously stored image ID.
238    pub previous_imageid: Option<String>,
239    /// The layers containing split objects
240    pub ostree_layers: Vec<ManifestLayerState>,
241    /// The layer for the ostree commit.
242    pub ostree_commit_layer: Option<ManifestLayerState>,
243    /// Any further non-ostree (derived) layers.
244    pub layers: Vec<ManifestLayerState>,
245    /// OSTree remote signature verification text, if enabled.
246    pub verify_text: Option<String>,
247    /// Our open image reference
248    proxy_img: OpenedImage,
249}
250
251impl PreparedImport {
252    /// Iterate over all layers; the commit layer, the ostree split object layers, and any non-ostree layers.
253    pub fn all_layers(&self) -> impl Iterator<Item = &ManifestLayerState> {
254        self.ostree_commit_layer
255            .iter()
256            .chain(self.ostree_layers.iter())
257            .chain(self.layers.iter())
258    }
259
260    /// Retrieve the container image version.
261    pub fn version(&self) -> Option<&str> {
262        super::version_for_config(&self.config)
263    }
264
265    /// If this image is using any deprecated features, return a message saying so.
266    pub fn deprecated_warning(&self) -> Option<&'static str> {
267        None
268    }
269
270    /// Iterate over all layers paired with their history entry.
271    /// An error will be returned if the history does not cover all entries.
272    pub fn layers_with_history(
273        &self,
274    ) -> impl Iterator<Item = Result<(&ManifestLayerState, &History)>> {
275        // FIXME use .filter(|h| h.empty_layer.unwrap_or_default()) after https://github.com/containers/oci-spec-rs/pull/100 lands.
276        let truncated = std::iter::once_with(|| Err(anyhow::anyhow!("Truncated history")));
277        let history = self
278            .config
279            .history()
280            .iter()
281            .flatten()
282            .map(Ok)
283            .chain(truncated);
284        self.all_layers()
285            .zip(history)
286            .map(|(s, h)| h.map(|h| (s, h)))
287    }
288
289    /// Iterate over all layers that are not present, along with their history description.
290    pub fn layers_to_fetch(&self) -> impl Iterator<Item = Result<(&ManifestLayerState, &str)>> {
291        self.layers_with_history().filter_map(|r| {
292            r.map(|(l, h)| {
293                l.commit.is_none().then(|| {
294                    let comment = h.created_by().as_deref().unwrap_or("");
295                    (l, comment)
296                })
297            })
298            .transpose()
299        })
300    }
301
302    /// Common helper to format a string for the status
303    pub(crate) fn format_layer_status(&self) -> Option<String> {
304        let (stored, to_fetch, to_fetch_size) =
305            self.all_layers()
306                .fold((0u32, 0u32, 0u64), |(stored, to_fetch, sz), v| {
307                    if v.commit.is_some() {
308                        (stored + 1, to_fetch, sz)
309                    } else {
310                        (stored, to_fetch + 1, sz + v.layer().size())
311                    }
312                });
313        (to_fetch > 0).then(|| {
314            let size = crate::glib::format_size(to_fetch_size);
315            format!("layers already present: {stored}; layers needed: {to_fetch} ({size})")
316        })
317    }
318}
319
320// Given a manifest, compute its ostree ref name and cached ostree commit
321pub(crate) fn query_layer(
322    repo: &ostree::Repo,
323    layer: oci_image::Descriptor,
324) -> Result<ManifestLayerState> {
325    let ostree_ref = ref_for_layer(&layer)?;
326    let commit = repo.resolve_rev(&ostree_ref, true)?.map(|s| s.to_string());
327    Ok(ManifestLayerState {
328        layer,
329        ostree_ref,
330        commit,
331    })
332}
333
334#[context("Reading manifest data from commit")]
335fn manifest_data_from_commitmeta(
336    commit_meta: &glib::VariantDict,
337) -> Result<(oci_image::ImageManifest, Digest)> {
338    let digest = commit_meta
339        .lookup::<String>(META_MANIFEST_DIGEST)?
340        .ok_or_else(|| anyhow!("Missing {} metadata on merge commit", META_MANIFEST_DIGEST))?;
341    let digest = Digest::from_str(&digest)?;
342    let manifest_bytes: String = commit_meta
343        .lookup::<String>(META_MANIFEST)?
344        .ok_or_else(|| anyhow!("Failed to find {} metadata key", META_MANIFEST))?;
345    let r = serde_json::from_str(&manifest_bytes)?;
346    Ok((r, digest))
347}
348
349fn image_config_from_commitmeta(commit_meta: &glib::VariantDict) -> Result<ImageConfiguration> {
350    let config = if let Some(config) = commit_meta
351        .lookup::<String>(META_CONFIG)?
352        .filter(|v| v != "null") // Format v0 apparently old versions injected `null` here sadly...
353        .map(|v| serde_json::from_str(&v).map_err(anyhow::Error::msg))
354        .transpose()?
355    {
356        config
357    } else {
358        tracing::debug!("No image configuration found");
359        Default::default()
360    };
361    Ok(config)
362}
363
364/// Return the original digest of the manifest stored in the commit metadata.
365/// This will be a string of the form e.g. `sha256:<digest>`.
366///
367/// This can be used to uniquely identify the image.  For example, it can be used
368/// in a "digested pull spec" like `quay.io/someuser/exampleos@sha256:...`.
369pub fn manifest_digest_from_commit(commit: &glib::Variant) -> Result<Digest> {
370    let commit_meta = &commit.child_value(0);
371    let commit_meta = &glib::VariantDict::new(Some(commit_meta));
372    Ok(manifest_data_from_commitmeta(commit_meta)?.1)
373}
374
375/// Given a target diffid, return its corresponding layer.  In our current model,
376/// we require a 1-to-1 mapping between the two up until the ostree level.
377/// For a bit more information on this, see https://github.com/opencontainers/image-spec/blob/main/config.md
378fn layer_from_diffid<'a>(
379    manifest: &'a ImageManifest,
380    config: &ImageConfiguration,
381    diffid: &str,
382) -> Result<&'a Descriptor> {
383    let idx = config
384        .rootfs()
385        .diff_ids()
386        .iter()
387        .position(|x| x.as_str() == diffid)
388        .ok_or_else(|| anyhow!("Missing {} {}", DIFFID_LABEL, diffid))?;
389    manifest.layers().get(idx).ok_or_else(|| {
390        anyhow!(
391            "diffid position {} exceeds layer count {}",
392            idx,
393            manifest.layers().len()
394        )
395    })
396}
397
398#[context("Parsing manifest layout")]
399pub(crate) fn parse_manifest_layout<'a>(
400    manifest: &'a ImageManifest,
401    config: &ImageConfiguration,
402) -> Result<(
403    Option<&'a Descriptor>,
404    Vec<&'a Descriptor>,
405    Vec<&'a Descriptor>,
406)> {
407    let config_labels = super::labels_of(config);
408
409    let first_layer = manifest
410        .layers()
411        .first()
412        .ok_or_else(|| anyhow!("No layers in manifest"))?;
413    let Some(target_diffid) = config_labels.and_then(|labels| labels.get(DIFFID_LABEL)) else {
414        return Ok((None, Vec::new(), manifest.layers().iter().collect()));
415    };
416
417    let target_layer = layer_from_diffid(manifest, config, target_diffid.as_str())?;
418    let mut chunk_layers = Vec::new();
419    let mut derived_layers = Vec::new();
420    let mut after_target = false;
421    // Gather the ostree layer
422    let ostree_layer = first_layer;
423    for layer in manifest.layers() {
424        if layer == target_layer {
425            if after_target {
426                anyhow::bail!("Multiple entries for {}", layer.digest());
427            }
428            after_target = true;
429            if layer != ostree_layer {
430                chunk_layers.push(layer);
431            }
432        } else if !after_target {
433            if layer != ostree_layer {
434                chunk_layers.push(layer);
435            }
436        } else {
437            derived_layers.push(layer);
438        }
439    }
440
441    Ok((Some(ostree_layer), chunk_layers, derived_layers))
442}
443
444/// Like [`parse_manifest_layout`] but requires the image has an ostree base.
445#[context("Parsing manifest layout")]
446pub(crate) fn parse_ostree_manifest_layout<'a>(
447    manifest: &'a ImageManifest,
448    config: &ImageConfiguration,
449) -> Result<(&'a Descriptor, Vec<&'a Descriptor>, Vec<&'a Descriptor>)> {
450    let (ostree_layer, component_layers, derived_layers) = parse_manifest_layout(manifest, config)?;
451    let ostree_layer = ostree_layer.ok_or_else(|| {
452        anyhow!("No {DIFFID_LABEL} label found, not an ostree encapsulated container")
453    })?;
454    Ok((ostree_layer, component_layers, derived_layers))
455}
456
457/// Find the timestamp of the manifest (or config), ignoring errors.
458fn timestamp_of_manifest_or_config(
459    manifest: &ImageManifest,
460    config: &ImageConfiguration,
461) -> Option<u64> {
462    // The manifest timestamp seems to not be widely used, but let's
463    // try it in preference to the config one.
464    let timestamp = manifest
465        .annotations()
466        .as_ref()
467        .and_then(|a| a.get(oci_image::ANNOTATION_CREATED))
468        .or_else(|| config.created().as_ref());
469    // Try to parse the timestamp
470    timestamp
471        .map(|t| {
472            chrono::DateTime::parse_from_rfc3339(t)
473                .context("Failed to parse manifest timestamp")
474                .map(|t| t.timestamp() as u64)
475        })
476        .transpose()
477        .log_err_default()
478}
479
480/// Automatically clean up files that may have been injected by container
481/// builds. xref https://github.com/containers/buildah/issues/4242
482fn cleanup_root(root: &Dir) -> Result<()> {
483    const RUNTIME_INJECTED: &[&str] = &["usr/etc/hostname", "usr/etc/resolv.conf"];
484    for ent in RUNTIME_INJECTED {
485        if let Some(meta) = root.symlink_metadata_optional(ent)? {
486            if meta.is_file() && meta.size() == 0 {
487                tracing::debug!("Removing {ent}");
488                root.remove_file(ent)?;
489            }
490        }
491    }
492    Ok(())
493}
494
495impl ImageImporter {
496    /// The metadata key used in ostree commit metadata to serialize
497    const CACHED_KEY_MANIFEST_DIGEST: &'static str = "ostree-ext.cached.manifest-digest";
498    const CACHED_KEY_MANIFEST: &'static str = "ostree-ext.cached.manifest";
499    const CACHED_KEY_CONFIG: &'static str = "ostree-ext.cached.config";
500
501    /// Create a new importer.
502    #[context("Creating importer")]
503    pub async fn new(
504        repo: &ostree::Repo,
505        imgref: &OstreeImageReference,
506        mut config: ImageProxyConfig,
507    ) -> Result<Self> {
508        if imgref.imgref.transport == Transport::ContainerStorage {
509            // Fetching from containers-storage, may require privileges to read files
510            merge_default_container_proxy_opts_with_isolation(&mut config, None)?;
511        } else {
512            // Apply our defaults to the proxy config
513            merge_default_container_proxy_opts(&mut config)?;
514        }
515        let proxy = ImageProxy::new_with_config(config).await?;
516
517        system_repo_journal_print(
518            repo,
519            libsystemd::logging::Priority::Info,
520            &format!("Fetching {imgref}"),
521        );
522
523        let repo = repo.clone();
524        Ok(ImageImporter {
525            repo,
526            proxy,
527            target_imgref: None,
528            no_imgref: false,
529            ostree_v2024_3: ostree::check_version(2024, 3),
530            disable_gc: false,
531            require_bootable: false,
532            offline: false,
533            imgref: imgref.clone(),
534            layer_progress: None,
535            layer_byte_progress: None,
536        })
537    }
538
539    /// Write cached data as if the image came from this source.
540    pub fn set_target(&mut self, target: &OstreeImageReference) {
541        self.target_imgref = Some(target.clone())
542    }
543
544    /// Do not write the final image ref, but do write refs for shared layers.
545    /// This is useful in scenarios where you want to "pre-pull" an image,
546    /// but in such a way that it does not need to be manually removed later.
547    pub fn set_no_imgref(&mut self) {
548        self.no_imgref = true;
549    }
550
551    /// Do not attempt to contact the network
552    pub fn set_offline(&mut self) {
553        self.offline = true;
554    }
555
556    /// Require that the image has the bootable metadata field
557    pub fn require_bootable(&mut self) {
558        self.require_bootable = true;
559    }
560
561    /// Override the ostree version being targeted
562    pub fn set_ostree_version(&mut self, year: u32, v: u32) {
563        self.ostree_v2024_3 = (year > 2024) || (year == 2024 && v >= 3)
564    }
565
566    /// Do not prune image layers.
567    pub fn disable_gc(&mut self) {
568        self.disable_gc = true;
569    }
570
571    /// Determine if there is a new manifest, and if so return its digest.
572    /// This will also serialize the new manifest and configuration into
573    /// metadata associated with the image, so that invocations of `[query_cached]`
574    /// can re-fetch it without accessing the network.
575    #[context("Preparing import")]
576    pub async fn prepare(&mut self) -> Result<PrepareResult> {
577        self.prepare_internal(false).await
578    }
579
580    /// Create a channel receiver that will get notifications for layer fetches.
581    pub fn request_progress(&mut self) -> Receiver<ImportProgress> {
582        assert!(self.layer_progress.is_none());
583        let (s, r) = tokio::sync::mpsc::channel(2);
584        self.layer_progress = Some(s);
585        r
586    }
587
588    /// Create a channel receiver that will get notifications for byte-level progress of layer fetches.
589    pub fn request_layer_progress(
590        &mut self,
591    ) -> tokio::sync::watch::Receiver<Option<LayerProgress>> {
592        assert!(self.layer_byte_progress.is_none());
593        let (s, r) = tokio::sync::watch::channel(None);
594        self.layer_byte_progress = Some(s);
595        r
596    }
597
598    /// Serialize the metadata about a pending fetch as detached metadata on the commit object,
599    /// so it can be retrieved later offline
600    #[context("Writing cached pending manifest")]
601    pub(crate) async fn cache_pending(
602        &self,
603        commit: &str,
604        manifest_digest: &Digest,
605        manifest: &ImageManifest,
606        config: &ImageConfiguration,
607    ) -> Result<()> {
608        let commitmeta = glib::VariantDict::new(None);
609        commitmeta.insert(
610            Self::CACHED_KEY_MANIFEST_DIGEST,
611            manifest_digest.to_string(),
612        );
613        let cached_manifest = manifest
614            .to_canon_json_string()
615            .context("Serializing manifest")?;
616        commitmeta.insert(Self::CACHED_KEY_MANIFEST, cached_manifest);
617        let cached_config = config
618            .to_canon_json_string()
619            .context("Serializing config")?;
620        commitmeta.insert(Self::CACHED_KEY_CONFIG, cached_config);
621        let commitmeta = commitmeta.to_variant();
622        // Clone these to move into blocking method
623        let commit = commit.to_string();
624        let repo = self.repo.clone();
625        crate::tokio_util::spawn_blocking_cancellable_flatten(move |cancellable| {
626            repo.write_commit_detached_metadata(&commit, Some(&commitmeta), Some(cancellable))
627                .map_err(anyhow::Error::msg)
628        })
629        .await
630    }
631
632    /// Given existing metadata (manifest, config, previous image statE) generate a PreparedImport structure
633    /// which e.g. includes a diff of the layers.
634    fn create_prepared_import(
635        &mut self,
636        manifest_digest: Digest,
637        manifest: ImageManifest,
638        config: ImageConfiguration,
639        previous_state: Option<Box<LayeredImageState>>,
640        previous_imageid: Option<String>,
641        proxy_img: OpenedImage,
642    ) -> Result<Box<PreparedImport>> {
643        let config_labels = super::labels_of(&config);
644        if self.require_bootable {
645            let bootable_key = ostree::METADATA_KEY_BOOTABLE;
646            let bootable = config_labels.is_some_and(|l| {
647                l.contains_key(bootable_key.as_str()) || l.contains_key(BOOTC_LABEL)
648            });
649            if !bootable {
650                anyhow::bail!("Target image does not have {bootable_key} label");
651            }
652            let container_arch = config.architecture();
653            let target_arch = &Arch::default();
654            if container_arch != target_arch {
655                anyhow::bail!("Image has architecture {container_arch}; expected {target_arch}");
656            }
657        }
658
659        let (commit_layer, component_layers, remaining_layers) =
660            parse_manifest_layout(&manifest, &config)?;
661
662        let query = |l: &Descriptor| query_layer(&self.repo, l.clone());
663        let commit_layer = commit_layer.map(query).transpose()?;
664        let component_layers = component_layers
665            .into_iter()
666            .map(query)
667            .collect::<Result<Vec<_>>>()?;
668        let remaining_layers = remaining_layers
669            .into_iter()
670            .map(query)
671            .collect::<Result<Vec<_>>>()?;
672
673        let previous_manifest_digest = previous_state.as_ref().map(|s| s.manifest_digest.clone());
674        let imp = PreparedImport {
675            manifest_digest,
676            manifest,
677            config,
678            previous_state,
679            previous_manifest_digest,
680            previous_imageid,
681            ostree_layers: component_layers,
682            ostree_commit_layer: commit_layer,
683            layers: remaining_layers,
684            verify_text: None,
685            proxy_img,
686        };
687        Ok(Box::new(imp))
688    }
689
690    /// Determine if there is a new manifest, and if so return its digest.
691    #[context("Fetching manifest")]
692    pub(crate) async fn prepare_internal(&mut self, verify_layers: bool) -> Result<PrepareResult> {
693        match &self.imgref.sigverify {
694            SignatureSource::ContainerPolicy if skopeo::container_policy_is_default_insecure()? => {
695                return Err(anyhow!(
696                    "containers-policy.json specifies a default of `insecureAcceptAnything`; refusing usage"
697                ));
698            }
699            SignatureSource::OstreeRemote(_) if verify_layers => {
700                return Err(anyhow!(
701                    "Cannot currently verify layered containers via ostree remote"
702                ));
703            }
704            _ => {}
705        }
706
707        // Check if we have an image already pulled
708        let previous_state = try_query_image(&self.repo, &self.imgref.imgref)?;
709
710        // Parse the target reference to see if it's a digested pull
711        let target_reference = self.imgref.imgref.name.parse::<Reference>().ok();
712        let previous_state = if let Some(target_digest) = target_reference
713            .as_ref()
714            .and_then(|v| v.digest())
715            .map(Digest::from_str)
716            .transpose()?
717        {
718            if let Some(previous_state) = previous_state {
719                // A digested pull spec, and our existing state matches.
720                if previous_state.manifest_digest == target_digest {
721                    tracing::debug!("Digest-based pullspec {:?} already present", self.imgref);
722                    return Ok(PrepareResult::AlreadyPresent(previous_state));
723                }
724                Some(previous_state)
725            } else {
726                None
727            }
728        } else {
729            previous_state
730        };
731
732        if self.offline {
733            anyhow::bail!("Manifest fetch required in offline mode");
734        }
735
736        let proxy_img = self
737            .proxy
738            .open_image(&self.imgref.imgref.to_string())
739            .await?;
740
741        let (manifest_digest, manifest) = self.proxy.fetch_manifest(&proxy_img).await?;
742        let manifest_digest = Digest::from_str(&manifest_digest)?;
743        let new_imageid = manifest.config().digest();
744
745        // Query for previous stored state
746
747        let (previous_state, previous_imageid) = if let Some(previous_state) = previous_state {
748            // If the manifest digests match, we're done.
749            if previous_state.manifest_digest == manifest_digest {
750                return Ok(PrepareResult::AlreadyPresent(previous_state));
751            }
752            // Failing that, if they have the same imageID, we're also done.
753            let previous_imageid = previous_state.manifest.config().digest();
754            if previous_imageid == new_imageid {
755                return Ok(PrepareResult::AlreadyPresent(previous_state));
756            }
757            let previous_imageid = previous_imageid.to_string();
758            (Some(previous_state), Some(previous_imageid))
759        } else {
760            (None, None)
761        };
762
763        let config = self.proxy.fetch_config(&proxy_img).await?;
764
765        // If there is a currently fetched image, cache the new pending manifest+config
766        // as detached commit metadata, so that future fetches can query it offline.
767        if let Some(previous_state) = previous_state.as_ref() {
768            self.cache_pending(
769                previous_state.merge_commit.as_str(),
770                &manifest_digest,
771                &manifest,
772                &config,
773            )
774            .await?;
775        }
776
777        let imp = self.create_prepared_import(
778            manifest_digest,
779            manifest,
780            config,
781            previous_state,
782            previous_imageid,
783            proxy_img,
784        )?;
785        Ok(PrepareResult::Ready(imp))
786    }
787
788    /// Extract the base ostree commit.
789    #[context("Unencapsulating base")]
790    pub(crate) async fn unencapsulate_base(
791        &self,
792        import: &mut store::PreparedImport,
793        require_ostree: bool,
794        write_refs: bool,
795    ) -> Result<()> {
796        tracing::debug!("Fetching base");
797        if matches!(self.imgref.sigverify, SignatureSource::ContainerPolicy)
798            && skopeo::container_policy_is_default_insecure()?
799        {
800            return Err(anyhow!(
801                "containers-policy.json specifies a default of `insecureAcceptAnything`; refusing usage"
802            ));
803        }
804        let remote = match &self.imgref.sigverify {
805            SignatureSource::OstreeRemote(remote) => Some(remote.clone()),
806            SignatureSource::ContainerPolicy | SignatureSource::ContainerPolicyAllowInsecure => {
807                None
808            }
809        };
810        let Some(commit_layer) = import.ostree_commit_layer.as_mut() else {
811            if require_ostree {
812                anyhow::bail!(
813                    "No {DIFFID_LABEL} label found, not an ostree encapsulated container"
814                );
815            }
816            return Ok(());
817        };
818        let des_layers = self.proxy.get_layer_info(&import.proxy_img).await?;
819        for layer in import.ostree_layers.iter_mut() {
820            if layer.commit.is_some() {
821                continue;
822            }
823            if let Some(p) = self.layer_progress.as_ref() {
824                p.send(ImportProgress::OstreeChunkStarted(layer.layer.clone()))
825                    .await?;
826            }
827            let (blob, driver, media_type) = fetch_layer(
828                &self.proxy,
829                &import.proxy_img,
830                &import.manifest,
831                &layer.layer,
832                self.layer_byte_progress.as_ref(),
833                des_layers.as_ref(),
834                self.imgref.imgref.transport,
835            )
836            .await?;
837            let repo = self.repo.clone();
838            let target_ref = layer.ostree_ref.clone();
839            let import_task =
840                crate::tokio_util::spawn_blocking_cancellable_flatten(move |cancellable| {
841                    let txn = repo.auto_transaction(Some(cancellable))?;
842                    let mut importer = crate::tar::Importer::new_for_object_set(&repo);
843                    let blob = tokio_util::io::SyncIoBridge::new(blob);
844                    let mut blob = Decompressor::new(&media_type, blob)?;
845                    let mut archive = tar::Archive::new(&mut blob);
846                    importer.import_objects(&mut archive, Some(cancellable))?;
847                    let commit = if write_refs {
848                        let commit = importer.finish_import_object_set()?;
849                        repo.transaction_set_ref(None, &target_ref, Some(commit.as_str()));
850                        tracing::debug!("Wrote {} => {}", target_ref, commit);
851                        Some(commit)
852                    } else {
853                        None
854                    };
855                    txn.commit(Some(cancellable))?;
856                    blob.finish()?;
857                    Ok::<_, anyhow::Error>(commit)
858                })
859                .map_err(|e| e.context(format!("Layer {}", layer.layer.digest())));
860            let commit = super::unencapsulate::join_fetch(import_task, driver).await?;
861            layer.commit = commit;
862            if let Some(p) = self.layer_progress.as_ref() {
863                p.send(ImportProgress::OstreeChunkCompleted(layer.layer.clone()))
864                    .await?;
865            }
866        }
867        if commit_layer.commit.is_none() {
868            if let Some(p) = self.layer_progress.as_ref() {
869                p.send(ImportProgress::OstreeChunkStarted(
870                    commit_layer.layer.clone(),
871                ))
872                .await?;
873            }
874            let (blob, driver, media_type) = fetch_layer(
875                &self.proxy,
876                &import.proxy_img,
877                &import.manifest,
878                &commit_layer.layer,
879                self.layer_byte_progress.as_ref(),
880                des_layers.as_ref(),
881                self.imgref.imgref.transport,
882            )
883            .await?;
884            let repo = self.repo.clone();
885            let target_ref = commit_layer.ostree_ref.clone();
886            let import_task =
887                crate::tokio_util::spawn_blocking_cancellable_flatten(move |cancellable| {
888                    let txn = repo.auto_transaction(Some(cancellable))?;
889                    let mut importer = crate::tar::Importer::new_for_commit(&repo, remote);
890                    let blob = tokio_util::io::SyncIoBridge::new(blob);
891                    let mut blob = Decompressor::new(&media_type, blob)?;
892                    let mut archive = tar::Archive::new(&mut blob);
893                    importer.import_commit(&mut archive, Some(cancellable))?;
894                    let (commit, verify_text) = importer.finish_import_commit();
895                    if write_refs {
896                        repo.transaction_set_ref(None, &target_ref, Some(commit.as_str()));
897                        tracing::debug!("Wrote {} => {}", target_ref, commit);
898                    }
899                    repo.mark_commit_partial(&commit, false)?;
900                    txn.commit(Some(cancellable))?;
901                    blob.finish()?;
902                    Ok::<_, anyhow::Error>((commit, verify_text))
903                });
904            let (commit, verify_text) =
905                super::unencapsulate::join_fetch(import_task, driver).await?;
906            commit_layer.commit = Some(commit);
907            import.verify_text = verify_text;
908            if let Some(p) = self.layer_progress.as_ref() {
909                p.send(ImportProgress::OstreeChunkCompleted(
910                    commit_layer.layer.clone(),
911                ))
912                .await?;
913            }
914        };
915        Ok(())
916    }
917
918    /// Retrieve an inner ostree commit.
919    ///
920    /// This does not write cached references for each blob, and errors out if
921    /// the image has any non-ostree layers.
922    pub async fn unencapsulate(mut self) -> Result<Import> {
923        let mut prep = match self.prepare_internal(false).await? {
924            PrepareResult::AlreadyPresent(_) => {
925                panic!("Should not have image present for unencapsulation")
926            }
927            PrepareResult::Ready(r) => r,
928        };
929        if !prep.layers.is_empty() {
930            anyhow::bail!("Image has {} non-ostree layers", prep.layers.len());
931        }
932        let deprecated_warning = prep.deprecated_warning().map(ToOwned::to_owned);
933        self.unencapsulate_base(&mut prep, true, false).await?;
934        // TODO change the imageproxy API to ensure this happens automatically when
935        // the image reference is dropped
936        self.proxy.close_image(&prep.proxy_img).await?;
937        // SAFETY: We know we have a commit
938        let ostree_commit = prep.ostree_commit_layer.unwrap().commit.unwrap();
939        let image_digest = prep.manifest_digest;
940        Ok(Import {
941            ostree_commit,
942            image_digest,
943            deprecated_warning,
944        })
945    }
946
947    /// Generate a single ostree commit that combines all layers, and also
948    /// includes container image metadata such as the manifest and config.
949    fn write_merge_commit_impl(
950        repo: &ostree::Repo,
951        base_commit: Option<&str>,
952        layer_commits: &[String],
953        have_derived_layers: bool,
954        metadata: glib::Variant,
955        timestamp: u64,
956        ostree_ref: &str,
957        no_imgref: bool,
958        disable_gc: bool,
959        cancellable: Option<&gio::Cancellable>,
960    ) -> Result<Box<LayeredImageState>> {
961        use rustix::fd::AsRawFd;
962
963        let txn = repo.auto_transaction(cancellable)?;
964
965        let devino = ostree::RepoDevInoCache::new();
966        let repodir = Dir::reopen_dir(&repo.dfd_borrow())?;
967        let repo_tmp = repodir.open_dir("tmp")?;
968        let td = cap_std_ext::cap_tempfile::TempDir::new_in(&repo_tmp)?;
969
970        let rootpath = "root";
971        let checkout_mode = if repo.mode() == ostree::RepoMode::Bare {
972            ostree::RepoCheckoutMode::None
973        } else {
974            ostree::RepoCheckoutMode::User
975        };
976        let mut checkout_opts = ostree::RepoCheckoutAtOptions {
977            mode: checkout_mode,
978            overwrite_mode: ostree::RepoCheckoutOverwriteMode::UnionFiles,
979            devino_to_csum_cache: Some(devino.clone()),
980            no_copy_fallback: true,
981            force_copy_zerosized: true,
982            process_whiteouts: false,
983            ..Default::default()
984        };
985        if let Some(base) = base_commit.as_ref() {
986            repo.checkout_at(
987                Some(&checkout_opts),
988                (*td).as_raw_fd(),
989                rootpath,
990                &base,
991                cancellable,
992            )
993            .context("Checking out base commit")?;
994        }
995
996        // Layer all subsequent commits
997        checkout_opts.process_whiteouts = true;
998        for commit in layer_commits {
999            tracing::debug!("Unpacking {commit}");
1000            repo.checkout_at(
1001                Some(&checkout_opts),
1002                (*td).as_raw_fd(),
1003                rootpath,
1004                &commit,
1005                cancellable,
1006            )
1007            .with_context(|| format!("Checking out layer {commit}"))?;
1008        }
1009
1010        let root_dir = td.open_dir(rootpath)?;
1011
1012        let modifier =
1013            ostree::RepoCommitModifier::new(ostree::RepoCommitModifierFlags::empty(), None);
1014        modifier.set_devino_cache(&devino);
1015        // If we have derived layers, then we need to handle the case where
1016        // the derived layers include custom policy. Just relabel everything
1017        // in this case.
1018        if have_derived_layers {
1019            let sepolicy = ostree::SePolicy::new_at(root_dir.as_raw_fd(), cancellable)?;
1020            tracing::debug!("labeling from merged tree");
1021            modifier.set_sepolicy(Some(&sepolicy));
1022        } else if let Some(base) = base_commit.as_ref() {
1023            tracing::debug!("labeling from base tree");
1024            // TODO: We can likely drop this; we know all labels should be pre-computed.
1025            modifier.set_sepolicy_from_commit(repo, &base, cancellable)?;
1026        } else {
1027            panic!("Unexpected state: no derived layers and no base")
1028        }
1029
1030        cleanup_root(&root_dir)?;
1031
1032        let mt = ostree::MutableTree::new();
1033        repo.write_dfd_to_mtree(
1034            (*td).as_raw_fd(),
1035            rootpath,
1036            &mt,
1037            Some(&modifier),
1038            cancellable,
1039        )
1040        .context("Writing merged filesystem to mtree")?;
1041
1042        let merged_root = repo
1043            .write_mtree(&mt, cancellable)
1044            .context("Writing mtree")?;
1045        let merged_root = merged_root.downcast::<ostree::RepoFile>().unwrap();
1046        // The merge has the base commit as a parent, if it exists. See
1047        // https://github.com/ostreedev/ostree/pull/3523
1048        let parent = base_commit.as_deref();
1049        let merged_commit = repo
1050            .write_commit_with_time(
1051                parent,
1052                None,
1053                None,
1054                Some(&metadata),
1055                &merged_root,
1056                timestamp,
1057                cancellable,
1058            )
1059            .context("Writing commit")?;
1060        if !no_imgref {
1061            repo.transaction_set_ref(None, ostree_ref, Some(merged_commit.as_str()));
1062        }
1063        txn.commit(cancellable)?;
1064
1065        if !disable_gc {
1066            let n: u32 = gc_image_layers_impl(repo, cancellable)?;
1067            tracing::debug!("pruned {n} layers");
1068        }
1069
1070        // Here we re-query state just to run through the same code path,
1071        // though it'd be cheaper to synthesize it from the data we already have.
1072        let state = query_image_commit(repo, &merged_commit)?;
1073        Ok(state)
1074    }
1075
1076    /// Import a layered container image.
1077    ///
1078    /// If enabled, this will also prune unused container image layers.
1079    #[context("Importing")]
1080    pub async fn import(
1081        mut self,
1082        mut import: Box<PreparedImport>,
1083    ) -> Result<Box<LayeredImageState>> {
1084        if let Some(status) = import.format_layer_status() {
1085            system_repo_journal_print(&self.repo, libsystemd::logging::Priority::Info, &status);
1086        }
1087        // First download all layers for the base image (if necessary) - we need the SELinux policy
1088        // there to label all following layers.
1089        self.unencapsulate_base(&mut import, false, true).await?;
1090        let des_layers = self.proxy.get_layer_info(&import.proxy_img).await?;
1091        let proxy = self.proxy;
1092        let target_imgref = self.target_imgref.as_ref().unwrap_or(&self.imgref);
1093        let base_commit = import
1094            .ostree_commit_layer
1095            .as_ref()
1096            .map(|c| c.commit.clone().unwrap());
1097
1098        let root_is_transient = if let Some(base) = base_commit.as_ref() {
1099            let rootf = self.repo.read_commit(&base, gio::Cancellable::NONE)?.0;
1100            let rootf = rootf.downcast_ref::<ostree::RepoFile>().unwrap();
1101            crate::ostree_prepareroot::overlayfs_root_enabled(rootf)?
1102        } else {
1103            // For generic images we assume they're using composefs
1104            true
1105        };
1106        tracing::debug!("Base rootfs is transient: {root_is_transient}");
1107
1108        let ostree_ref = ref_for_image(&target_imgref.imgref)?;
1109
1110        let mut layer_commits = Vec::new();
1111        let mut layer_filtered_content: Option<MetaFilteredData> = None;
1112        let have_derived_layers = !import.layers.is_empty();
1113        tracing::debug!("Processing layers: {}", import.layers.len());
1114        for layer in import.layers {
1115            if let Some(c) = layer.commit {
1116                tracing::debug!("Reusing fetched commit {}", c);
1117                layer_commits.push(c.to_string());
1118            } else {
1119                if let Some(p) = self.layer_progress.as_ref() {
1120                    p.send(ImportProgress::DerivedLayerStarted(layer.layer.clone()))
1121                        .await?;
1122                }
1123                let (blob, driver, media_type) = super::unencapsulate::fetch_layer(
1124                    &proxy,
1125                    &import.proxy_img,
1126                    &import.manifest,
1127                    &layer.layer,
1128                    self.layer_byte_progress.as_ref(),
1129                    des_layers.as_ref(),
1130                    self.imgref.imgref.transport,
1131                )
1132                .await?;
1133                // An important aspect of this is that we SELinux label the derived layers using
1134                // the base policy.
1135                let opts = crate::tar::WriteTarOptions {
1136                    base: base_commit.clone(),
1137                    selinux: true,
1138                    allow_nonusr: root_is_transient,
1139                    retain_var: self.ostree_v2024_3,
1140                };
1141                let r = crate::tar::write_tar(
1142                    &self.repo,
1143                    blob,
1144                    media_type,
1145                    layer.ostree_ref.as_str(),
1146                    Some(opts),
1147                );
1148                let r = super::unencapsulate::join_fetch(r, driver)
1149                    .await
1150                    .with_context(|| format!("Parsing layer blob {}", layer.layer.digest()))?;
1151                tracing::debug!("Imported layer: {}", r.commit.as_str());
1152                layer_commits.push(r.commit);
1153                let filtered_owned = HashMap::from_iter(r.filtered.clone());
1154                if let Some((filtered, n_rest)) = bootc_utils::collect_until(
1155                    r.filtered.iter(),
1156                    const { NonZeroUsize::new(5).unwrap() },
1157                ) {
1158                    let mut msg = String::new();
1159                    for (path, n) in filtered {
1160                        write!(msg, "{path}: {n} ").unwrap();
1161                    }
1162                    if n_rest > 0 {
1163                        write!(msg, "...and {n_rest} more").unwrap();
1164                    }
1165                    tracing::debug!("Found filtered toplevels: {msg}");
1166                    layer_filtered_content
1167                        .get_or_insert_default()
1168                        .insert(layer.layer.digest().to_string(), filtered_owned);
1169                } else {
1170                    tracing::debug!("No filtered content");
1171                }
1172                if let Some(p) = self.layer_progress.as_ref() {
1173                    p.send(ImportProgress::DerivedLayerCompleted(layer.layer.clone()))
1174                        .await?;
1175                }
1176            }
1177        }
1178
1179        // TODO change the imageproxy API to ensure this happens automatically when
1180        // the image reference is dropped
1181        proxy.close_image(&import.proxy_img).await?;
1182
1183        // We're done with the proxy, make sure it didn't have any errors.
1184        proxy.finalize().await?;
1185        tracing::debug!("finalized proxy");
1186
1187        // Disconnect progress notifiers to signal we're done with fetching.
1188        let _ = self.layer_byte_progress.take();
1189        let _ = self.layer_progress.take();
1190
1191        let mut metadata = BTreeMap::new();
1192        metadata.insert(
1193            META_MANIFEST_DIGEST,
1194            import.manifest_digest.to_string().to_variant(),
1195        );
1196        metadata.insert(
1197            META_MANIFEST,
1198            import.manifest.to_canon_json_string()?.to_variant(),
1199        );
1200        metadata.insert(
1201            META_CONFIG,
1202            import.config.to_canon_json_string()?.to_variant(),
1203        );
1204        metadata.insert(
1205            "ostree.importer.version",
1206            env!("CARGO_PKG_VERSION").to_variant(),
1207        );
1208        let metadata = metadata.to_variant();
1209
1210        let timestamp = timestamp_of_manifest_or_config(&import.manifest, &import.config)
1211            .unwrap_or_else(|| chrono::offset::Utc::now().timestamp() as u64);
1212        // Destructure to transfer ownership to thread
1213        let repo = self.repo;
1214        let mut state = crate::tokio_util::spawn_blocking_cancellable_flatten(
1215            move |cancellable| -> Result<Box<LayeredImageState>> {
1216                Self::write_merge_commit_impl(
1217                    &repo,
1218                    base_commit.as_deref(),
1219                    &layer_commits,
1220                    have_derived_layers,
1221                    metadata,
1222                    timestamp,
1223                    &ostree_ref,
1224                    self.no_imgref,
1225                    self.disable_gc,
1226                    Some(cancellable),
1227                )
1228            },
1229        )
1230        .await?;
1231        // We can at least avoid re-verifying the base commit.
1232        state.verify_text = import.verify_text;
1233        state.filtered_files = layer_filtered_content;
1234        Ok(state)
1235    }
1236}
1237
1238/// List all images stored
1239pub fn list_images(repo: &ostree::Repo) -> Result<Vec<String>> {
1240    let cancellable = gio::Cancellable::NONE;
1241    let refs = repo.list_refs_ext(
1242        Some(IMAGE_PREFIX),
1243        ostree::RepoListRefsExtFlags::empty(),
1244        cancellable,
1245    )?;
1246    refs.keys()
1247        .map(|imgname| refescape::unprefix_unescape_ref(IMAGE_PREFIX, imgname))
1248        .collect()
1249}
1250
1251/// Attempt to query metadata for a pulled image; if it is corrupted,
1252/// the error is printed to stderr and None is returned.
1253fn try_query_image(
1254    repo: &ostree::Repo,
1255    imgref: &ImageReference,
1256) -> Result<Option<Box<LayeredImageState>>> {
1257    let ostree_ref = &ref_for_image(imgref)?;
1258    if let Some(merge_rev) = repo.resolve_rev(ostree_ref, true)? {
1259        match query_image_commit(repo, merge_rev.as_str()) {
1260            Ok(r) => Ok(Some(r)),
1261            Err(e) => {
1262                eprintln!("error: failed to query image commit: {e}");
1263                Ok(None)
1264            }
1265        }
1266    } else {
1267        Ok(None)
1268    }
1269}
1270
1271/// Query metadata for a pulled image.
1272#[context("Querying image {imgref}")]
1273pub fn query_image(
1274    repo: &ostree::Repo,
1275    imgref: &ImageReference,
1276) -> Result<Option<Box<LayeredImageState>>> {
1277    let ostree_ref = &ref_for_image(imgref)?;
1278    let merge_rev = repo.resolve_rev(ostree_ref, true)?;
1279    merge_rev
1280        .map(|r| query_image_commit(repo, r.as_str()))
1281        .transpose()
1282}
1283
1284/// Given detached commit metadata, parse the data that we serialized for a pending update (if any).
1285fn parse_cached_update(meta: &glib::VariantDict) -> Result<Option<CachedImageUpdate>> {
1286    // Try to retrieve the manifest digest key from the commit detached metadata.
1287    let manifest_digest =
1288        if let Some(d) = meta.lookup::<String>(ImageImporter::CACHED_KEY_MANIFEST_DIGEST)? {
1289            d
1290        } else {
1291            // It's possible that something *else* wrote detached metadata, but without
1292            // our key; gracefully handle that.
1293            return Ok(None);
1294        };
1295    let manifest_digest = Digest::from_str(&manifest_digest)?;
1296    // If we found the cached manifest digest key, then we must have the manifest and config;
1297    // otherwise that's an error.
1298    let manifest = meta.lookup_value(ImageImporter::CACHED_KEY_MANIFEST, None);
1299    let manifest: oci_image::ImageManifest = manifest
1300        .as_ref()
1301        .and_then(|v| v.str())
1302        .map(serde_json::from_str)
1303        .transpose()?
1304        .ok_or_else(|| {
1305            anyhow!(
1306                "Expected cached manifest {}",
1307                ImageImporter::CACHED_KEY_MANIFEST
1308            )
1309        })?;
1310    let config = meta.lookup_value(ImageImporter::CACHED_KEY_CONFIG, None);
1311    let config: oci_image::ImageConfiguration = config
1312        .as_ref()
1313        .and_then(|v| v.str())
1314        .map(serde_json::from_str)
1315        .transpose()?
1316        .ok_or_else(|| {
1317            anyhow!(
1318                "Expected cached manifest {}",
1319                ImageImporter::CACHED_KEY_CONFIG
1320            )
1321        })?;
1322    Ok(Some(CachedImageUpdate {
1323        manifest,
1324        config,
1325        manifest_digest,
1326    }))
1327}
1328
1329/// Remove any cached
1330#[context("Clearing cached update {imgref}")]
1331pub fn clear_cached_update(repo: &ostree::Repo, imgref: &ImageReference) -> Result<()> {
1332    let cancellable = gio::Cancellable::NONE;
1333    let ostree_ref = ref_for_image(imgref)?;
1334    let rev = repo.require_rev(&ostree_ref)?;
1335    let Some(commitmeta) = repo.read_commit_detached_metadata(&rev, cancellable)? else {
1336        return Ok(());
1337    };
1338
1339    // SAFETY: We know this is an a{sv}
1340    let mut commitmeta: BTreeMap<String, glib::Variant> =
1341        BTreeMap::from_variant(&commitmeta).unwrap();
1342    let mut changed = false;
1343    for key in [
1344        ImageImporter::CACHED_KEY_CONFIG,
1345        ImageImporter::CACHED_KEY_MANIFEST,
1346        ImageImporter::CACHED_KEY_MANIFEST_DIGEST,
1347    ] {
1348        if commitmeta.remove(key).is_some() {
1349            changed = true;
1350        }
1351    }
1352    if !changed {
1353        return Ok(());
1354    }
1355    let commitmeta = glib::Variant::from(commitmeta);
1356    repo.write_commit_detached_metadata(&rev, Some(&commitmeta), cancellable)?;
1357    Ok(())
1358}
1359
1360/// Query metadata for a pulled image via an OSTree commit digest.
1361/// The digest must refer to a pulled container image's merge commit.
1362pub fn query_image_commit(repo: &ostree::Repo, commit: &str) -> Result<Box<LayeredImageState>> {
1363    let merge_commit = commit.to_string();
1364    let merge_commit_obj = repo.load_commit(commit)?.0;
1365    let commit_meta = &merge_commit_obj.child_value(0);
1366    let commit_meta = &ostree::glib::VariantDict::new(Some(commit_meta));
1367    let (manifest, manifest_digest) = manifest_data_from_commitmeta(commit_meta)?;
1368    let configuration = image_config_from_commitmeta(commit_meta)?;
1369    let mut layers = manifest.layers().iter().cloned();
1370    // We require a base layer.
1371    let base_layer = layers.next().ok_or_else(|| anyhow!("No layers found"))?;
1372    let base_layer = query_layer(repo, base_layer)?;
1373    let ostree_ref = base_layer.ostree_ref.as_str();
1374    let base_commit = base_layer
1375        .commit
1376        .ok_or_else(|| anyhow!("Missing base image ref {ostree_ref}"))?;
1377
1378    let detached_commitmeta =
1379        repo.read_commit_detached_metadata(&merge_commit, gio::Cancellable::NONE)?;
1380    let detached_commitmeta = detached_commitmeta
1381        .as_ref()
1382        .map(|v| glib::VariantDict::new(Some(v)));
1383    let cached_update = detached_commitmeta
1384        .as_ref()
1385        .map(parse_cached_update)
1386        .transpose()?
1387        .flatten();
1388    let state = Box::new(LayeredImageState {
1389        base_commit,
1390        merge_commit,
1391        manifest_digest,
1392        manifest,
1393        configuration,
1394        cached_update,
1395        // we can't cross-reference with a remote here
1396        verify_text: None,
1397        filtered_files: None,
1398    });
1399    tracing::debug!("Wrote merge commit {}", state.merge_commit);
1400    Ok(state)
1401}
1402
1403fn manifest_for_image(repo: &ostree::Repo, imgref: &ImageReference) -> Result<ImageManifest> {
1404    let ostree_ref = ref_for_image(imgref)?;
1405    let rev = repo.require_rev(&ostree_ref)?;
1406    let (commit_obj, _) = repo.load_commit(rev.as_str())?;
1407    let commit_meta = &glib::VariantDict::new(Some(&commit_obj.child_value(0)));
1408    Ok(manifest_data_from_commitmeta(commit_meta)?.0)
1409}
1410
1411/// Copy a downloaded image from one repository to another, while also
1412/// optionally changing the image reference type.
1413#[context("Copying image")]
1414pub async fn copy(
1415    src_repo: &ostree::Repo,
1416    src_imgref: &ImageReference,
1417    dest_repo: &ostree::Repo,
1418    dest_imgref: &ImageReference,
1419) -> Result<()> {
1420    let src_ostree_ref = ref_for_image(src_imgref)?;
1421    let src_commit = src_repo.require_rev(&src_ostree_ref)?;
1422    let manifest = manifest_for_image(src_repo, src_imgref)?;
1423    // Create a task to copy each layer, plus the final ref
1424    let layer_refs = manifest
1425        .layers()
1426        .iter()
1427        .map(ref_for_layer)
1428        .chain(std::iter::once(Ok(src_commit.to_string())));
1429    for ostree_ref in layer_refs {
1430        let ostree_ref = ostree_ref?;
1431        let src_repo = src_repo.clone();
1432        let dest_repo = dest_repo.clone();
1433        crate::tokio_util::spawn_blocking_cancellable_flatten(move |cancellable| -> Result<_> {
1434            let cancellable = Some(cancellable);
1435            let srcfd = &format!("file:///proc/self/fd/{}", src_repo.dfd());
1436            let flags = ostree::RepoPullFlags::MIRROR;
1437            let opts = glib::VariantDict::new(None);
1438            let refs = [ostree_ref.as_str()];
1439            // Some older archives may have bindings, we don't need to verify them.
1440            opts.insert("disable-verify-bindings", true);
1441            opts.insert("refs", &refs[..]);
1442            opts.insert("flags", flags.bits() as i32);
1443            let options = opts.to_variant();
1444            dest_repo.pull_with_options(srcfd, &options, None, cancellable)?;
1445            Ok(())
1446        })
1447        .await?;
1448    }
1449
1450    let dest_ostree_ref = ref_for_image(dest_imgref)?;
1451    dest_repo.set_ref_immediate(
1452        None,
1453        &dest_ostree_ref,
1454        Some(&src_commit),
1455        gio::Cancellable::NONE,
1456    )?;
1457
1458    Ok(())
1459}
1460
1461/// Options controlling commit export into OCI
1462#[derive(Clone, Debug, Default)]
1463#[non_exhaustive]
1464pub struct ExportToOCIOpts {
1465    /// If true, do not perform gzip compression of the tar layers.
1466    pub skip_compression: bool,
1467    /// Path to Docker-formatted authentication file.
1468    pub authfile: Option<std::path::PathBuf>,
1469    /// Output progress to stdout
1470    pub progress_to_stdout: bool,
1471}
1472
1473/// The way we store "chunk" layers in ostree is by writing a commit
1474/// whose filenames are their own object identifier. This function parses
1475/// what is written by the `ImporterMode::ObjectSet` logic, turning
1476/// it back into a "chunked" structure that is used by the export code.
1477fn chunking_from_layer_committed(
1478    repo: &ostree::Repo,
1479    l: &Descriptor,
1480    chunking: &mut chunking::Chunking,
1481) -> Result<()> {
1482    let mut chunk = Chunk::default();
1483    let layer_ref = &ref_for_layer(l)?;
1484    let root = repo.read_commit(layer_ref, gio::Cancellable::NONE)?.0;
1485    let e = root.enumerate_children(
1486        "standard::name,standard::size",
1487        gio::FileQueryInfoFlags::NOFOLLOW_SYMLINKS,
1488        gio::Cancellable::NONE,
1489    )?;
1490    for child in e.clone() {
1491        let child = &child?;
1492        // The name here should be a valid checksum
1493        let name = child.name();
1494        // SAFETY: ostree doesn't give us non-UTF8 filenames
1495        let name = Utf8Path::from_path(&name).unwrap();
1496        ostree::validate_checksum_string(name.as_str())?;
1497        chunking.remainder.move_obj(&mut chunk, name.as_str());
1498    }
1499    chunking.chunks.push(chunk);
1500    Ok(())
1501}
1502
1503/// Export an imported container image to a target OCI directory.
1504#[context("Copying image")]
1505pub(crate) fn export_to_oci(
1506    repo: &ostree::Repo,
1507    imgref: &ImageReference,
1508    dest_oci: &Dir,
1509    tag: Option<&str>,
1510    opts: ExportToOCIOpts,
1511) -> Result<Descriptor> {
1512    let srcinfo = query_image(repo, imgref)?.ok_or_else(|| anyhow!("No such image"))?;
1513    let (commit_layer, component_layers, remaining_layers) =
1514        parse_manifest_layout(&srcinfo.manifest, &srcinfo.configuration)?;
1515
1516    // Unfortunately today we can't guarantee we reserialize the same tar stream
1517    // or compression, so we'll need to generate a new copy of the manifest and config
1518    // with the layers reset.
1519    let mut new_manifest = srcinfo.manifest.clone();
1520    new_manifest.layers_mut().clear();
1521    let mut new_config = srcinfo.configuration.clone();
1522    if let Some(history) = new_config.history_mut() {
1523        history.clear();
1524    }
1525    new_config.rootfs_mut().diff_ids_mut().clear();
1526
1527    let opts = ExportOpts {
1528        skip_compression: opts.skip_compression,
1529        authfile: opts.authfile,
1530        ..Default::default()
1531    };
1532
1533    let mut labels = HashMap::new();
1534
1535    let mut dest_oci = ocidir::OciDir::ensure(dest_oci.try_clone()?)?;
1536
1537    let commit_chunk_ref = commit_layer
1538        .as_ref()
1539        .map(|l| ref_for_layer(l))
1540        .transpose()?;
1541    let commit_chunk_rev = commit_chunk_ref
1542        .as_ref()
1543        .map(|r| repo.require_rev(&r))
1544        .transpose()?;
1545    if let Some(commit_chunk_rev) = commit_chunk_rev {
1546        let mut chunking = chunking::Chunking::new(repo, &commit_chunk_rev)?;
1547        for layer in component_layers {
1548            chunking_from_layer_committed(repo, layer, &mut chunking)?;
1549        }
1550
1551        // Given the object chunking information we recomputed from what
1552        // we found on disk, re-serialize to layers (tarballs).
1553        export_chunked(
1554            repo,
1555            &srcinfo.base_commit,
1556            &mut dest_oci,
1557            &mut new_manifest,
1558            &mut new_config,
1559            &mut labels,
1560            chunking,
1561            &opts,
1562            "",
1563        )?;
1564    }
1565
1566    // Now, handle the non-ostree layers.
1567    let compression = opts.skip_compression.then_some(Compression::none());
1568    for (i, layer) in remaining_layers.iter().enumerate() {
1569        let layer_ref = &ref_for_layer(layer)?;
1570        let mut target_blob = dest_oci.create_gzip_layer(compression)?;
1571        // We accepted these images as raw (non-ostree) so export them the same way
1572        let export_opts = crate::tar::ExportOptions { raw: true };
1573        crate::tar::export_commit(
1574            repo,
1575            layer_ref.as_str(),
1576            &mut target_blob,
1577            Some(export_opts),
1578        )?;
1579        let layer = target_blob.complete()?;
1580        let previous_annotations = srcinfo
1581            .manifest
1582            .layers()
1583            .get(i)
1584            .and_then(|l| l.annotations().as_ref())
1585            .cloned();
1586        let history = srcinfo.configuration.history().as_ref();
1587        let history_entry = history.and_then(|v| v.get(i));
1588        let previous_description = history_entry
1589            .clone()
1590            .and_then(|h| h.comment().as_deref())
1591            .unwrap_or_default();
1592
1593        let previous_created = history_entry
1594            .and_then(|h| h.created().as_deref())
1595            .and_then(bootc_utils::try_deserialize_timestamp)
1596            .unwrap_or_default();
1597
1598        dest_oci.push_layer_full(
1599            &mut new_manifest,
1600            &mut new_config,
1601            layer,
1602            previous_annotations,
1603            previous_description,
1604            previous_created,
1605        )
1606    }
1607
1608    let new_config = dest_oci.write_config(new_config)?;
1609    new_manifest.set_config(new_config);
1610
1611    Ok(dest_oci.insert_manifest(new_manifest, tag, oci_image::Platform::default())?)
1612}
1613
1614/// Given a container image reference which is stored in `repo`, export it to the
1615/// target image location.
1616#[context("Export")]
1617pub async fn export(
1618    repo: &ostree::Repo,
1619    src_imgref: &ImageReference,
1620    dest_imgref: &ImageReference,
1621    opts: Option<ExportToOCIOpts>,
1622) -> Result<oci_image::Digest> {
1623    let opts = opts.unwrap_or_default();
1624    let target_oci = dest_imgref.transport == Transport::OciDir;
1625    let tempdir = if !target_oci {
1626        let vartmp = cap_std::fs::Dir::open_ambient_dir("/var/tmp", cap_std::ambient_authority())?;
1627        let td = cap_std_ext::cap_tempfile::TempDir::new_in(&vartmp)?;
1628        // Always skip compression when making a temporary copy
1629        let opts = ExportToOCIOpts {
1630            skip_compression: true,
1631            progress_to_stdout: opts.progress_to_stdout,
1632            ..Default::default()
1633        };
1634        export_to_oci(repo, src_imgref, &td, None, opts)?;
1635        td
1636    } else {
1637        let (path, tag) = parse_oci_path_and_tag(dest_imgref.name.as_str());
1638        tracing::debug!("using OCI path={path} tag={tag:?}");
1639        let path = Dir::open_ambient_dir(path, cap_std::ambient_authority())
1640            .with_context(|| format!("Opening {path}"))?;
1641        let descriptor = export_to_oci(repo, src_imgref, &path, tag, opts)?;
1642        return Ok(descriptor.digest().clone());
1643    };
1644    // Pass the temporary oci directory as the current working directory for the skopeo process
1645    let target_fd = 3i32;
1646    let tempoci = ImageReference {
1647        transport: Transport::OciDir,
1648        name: format!("/proc/self/fd/{target_fd}"),
1649    };
1650    let authfile = opts.authfile.as_deref();
1651    skopeo::copy(
1652        &tempoci,
1653        dest_imgref,
1654        authfile,
1655        Some((std::sync::Arc::new(tempdir.try_clone()?.into()), target_fd)),
1656        opts.progress_to_stdout,
1657    )
1658    .await
1659}
1660
1661/// Iterate over deployment commits, returning the manifests from
1662/// commits which point to a container image.
1663#[context("Listing deployment manifests")]
1664fn list_container_deployment_manifests(
1665    repo: &ostree::Repo,
1666    cancellable: Option<&gio::Cancellable>,
1667) -> Result<Vec<ImageManifest>> {
1668    // Gather all refs which start with ostree/0/ or ostree/1/ or rpmostree/base/
1669    // and create a set of the commits which they reference.
1670    let commits = OSTREE_BASE_DEPLOYMENT_REFS
1671        .iter()
1672        .chain(RPMOSTREE_BASE_REFS)
1673        .chain(std::iter::once(&BASE_IMAGE_PREFIX))
1674        .try_fold(
1675            std::collections::HashSet::new(),
1676            |mut acc, &p| -> Result<_> {
1677                let refs = repo.list_refs_ext(
1678                    Some(p),
1679                    ostree::RepoListRefsExtFlags::empty(),
1680                    cancellable,
1681                )?;
1682                for (_, v) in refs {
1683                    acc.insert(v);
1684                }
1685                Ok(acc)
1686            },
1687        )?;
1688    // Loop over the commits - if they refer to a container image, add that to our return value.
1689    let mut r = Vec::new();
1690    for commit in commits {
1691        let commit_obj = repo.load_commit(&commit)?.0;
1692        let commit_meta = &glib::VariantDict::new(Some(&commit_obj.child_value(0)));
1693        if commit_meta
1694            .lookup::<String>(META_MANIFEST_DIGEST)?
1695            .is_some()
1696        {
1697            tracing::trace!("Commit {commit} is a container image");
1698            let manifest = manifest_data_from_commitmeta(commit_meta)?.0;
1699            r.push(manifest);
1700        }
1701    }
1702    Ok(r)
1703}
1704
1705/// Garbage collect unused image layer references.
1706///
1707/// This function assumes no transaction is active on the repository.
1708/// The underlying objects are *not* pruned; that requires a separate invocation
1709/// of [`ostree::Repo::prune`].
1710pub fn gc_image_layers(repo: &ostree::Repo) -> Result<u32> {
1711    gc_image_layers_impl(repo, gio::Cancellable::NONE)
1712}
1713
1714#[context("Pruning image layers")]
1715fn gc_image_layers_impl(
1716    repo: &ostree::Repo,
1717    cancellable: Option<&gio::Cancellable>,
1718) -> Result<u32> {
1719    let all_images = list_images(repo)?;
1720    let deployment_commits = list_container_deployment_manifests(repo, cancellable)?;
1721    let all_manifests = all_images
1722        .into_iter()
1723        .map(|img| {
1724            ImageReference::try_from(img.as_str()).and_then(|ir| manifest_for_image(repo, &ir))
1725        })
1726        .chain(deployment_commits.into_iter().map(Ok))
1727        .collect::<Result<Vec<_>>>()?;
1728    tracing::debug!("Images found: {}", all_manifests.len());
1729    let mut referenced_layers = BTreeSet::new();
1730    for m in all_manifests.iter() {
1731        for layer in m.layers() {
1732            referenced_layers.insert(layer.digest().to_string());
1733        }
1734    }
1735    tracing::debug!("Referenced layers: {}", referenced_layers.len());
1736    let found_layers = repo
1737        .list_refs_ext(
1738            Some(LAYER_PREFIX),
1739            ostree::RepoListRefsExtFlags::empty(),
1740            cancellable,
1741        )?
1742        .into_iter()
1743        .map(|v| v.0);
1744    tracing::debug!("Found layers: {}", found_layers.len());
1745    let mut pruned = 0u32;
1746    for layer_ref in found_layers {
1747        let layer_digest = refescape::unprefix_unescape_ref(LAYER_PREFIX, &layer_ref)?;
1748        if referenced_layers.remove(layer_digest.as_str()) {
1749            continue;
1750        }
1751        pruned += 1;
1752        tracing::debug!("Pruning: {}", layer_ref.as_str());
1753        repo.set_ref_immediate(None, layer_ref.as_str(), None, cancellable)?;
1754    }
1755
1756    Ok(pruned)
1757}
1758
1759#[cfg(feature = "internal-testing-api")]
1760/// Return how many container blobs (layers) are stored
1761pub fn count_layer_references(repo: &ostree::Repo) -> Result<u32> {
1762    let cancellable = gio::Cancellable::NONE;
1763    let n = repo
1764        .list_refs_ext(
1765            Some(LAYER_PREFIX),
1766            ostree::RepoListRefsExtFlags::empty(),
1767            cancellable,
1768        )?
1769        .len();
1770    Ok(n as u32)
1771}
1772
1773/// Generate a suitable warning message from given list of filtered files, if any.
1774pub fn image_filtered_content_warning(
1775    filtered_files: &Option<MetaFilteredData>,
1776) -> Result<Option<String>> {
1777    use std::fmt::Write;
1778
1779    let r = filtered_files.as_ref().map(|v| {
1780        let mut filtered = BTreeMap::<&String, u32>::new();
1781        for paths in v.values() {
1782            for (k, v) in paths {
1783                let e = filtered.entry(k).or_default();
1784                *e += v;
1785            }
1786        }
1787        let mut buf = "Image contains non-ostree compatible file paths:".to_string();
1788        for (k, v) in filtered {
1789            write!(buf, " {k}: {v}").unwrap();
1790        }
1791        buf
1792    });
1793    Ok(r)
1794}
1795
1796/// Remove the specified image reference.  If the image is already
1797/// not present, this function will successfully perform no operation.
1798///
1799/// This function assumes no transaction is active on the repository.
1800/// The underlying layers are *not* pruned; that requires a separate invocation
1801/// of [`gc_image_layers`].
1802#[context("Pruning {img}")]
1803pub fn remove_image(repo: &ostree::Repo, img: &ImageReference) -> Result<bool> {
1804    let ostree_ref = &ref_for_image(img)?;
1805    let found = repo.resolve_rev(ostree_ref, true)?.is_some();
1806    // Note this API is already idempotent, but we might as well avoid another
1807    // trip into ostree.
1808    if found {
1809        repo.set_ref_immediate(None, ostree_ref, None, gio::Cancellable::NONE)?;
1810    }
1811    Ok(found)
1812}
1813
1814/// Remove the specified image references.  If an image is not found, further
1815/// images will be removed, but an error will be returned.
1816///
1817/// This function assumes no transaction is active on the repository.
1818/// The underlying layers are *not* pruned; that requires a separate invocation
1819/// of [`gc_image_layers`].
1820pub fn remove_images<'a>(
1821    repo: &ostree::Repo,
1822    imgs: impl IntoIterator<Item = &'a ImageReference>,
1823) -> Result<()> {
1824    let mut missing = Vec::new();
1825    for img in imgs.into_iter() {
1826        let found = remove_image(repo, img)?;
1827        if !found {
1828            missing.push(img);
1829        }
1830    }
1831    if !missing.is_empty() {
1832        let missing = missing.into_iter().fold("".to_string(), |mut a, v| {
1833            a.push_str(&v.to_string());
1834            a
1835        });
1836        return Err(anyhow::anyhow!("Missing images: {missing}"));
1837    }
1838    Ok(())
1839}
1840
1841#[derive(Debug, Default)]
1842struct CompareState {
1843    verified: BTreeSet<Utf8PathBuf>,
1844    inode_corrupted: BTreeSet<Utf8PathBuf>,
1845    unknown_corrupted: BTreeSet<Utf8PathBuf>,
1846}
1847
1848impl CompareState {
1849    fn is_ok(&self) -> bool {
1850        self.inode_corrupted.is_empty() && self.unknown_corrupted.is_empty()
1851    }
1852}
1853
1854fn compare_file_info(src: &gio::FileInfo, target: &gio::FileInfo) -> bool {
1855    if src.file_type() != target.file_type() {
1856        return false;
1857    }
1858    if src.size() != target.size() {
1859        return false;
1860    }
1861    for attr in ["unix::uid", "unix::gid", "unix::mode"] {
1862        if src.attribute_uint32(attr) != target.attribute_uint32(attr) {
1863            return false;
1864        }
1865    }
1866    true
1867}
1868
1869#[context("Querying object inode")]
1870fn inode_of_object(repo: &ostree::Repo, checksum: &str) -> Result<u64> {
1871    let repodir = Dir::reopen_dir(&repo.dfd_borrow())?;
1872    let (prefix, suffix) = checksum.split_at(2);
1873    let objpath = format!("objects/{prefix}/{suffix}.file");
1874    let metadata = repodir.symlink_metadata(objpath)?;
1875    Ok(metadata.ino())
1876}
1877
1878fn compare_commit_trees(
1879    repo: &ostree::Repo,
1880    root: &Utf8Path,
1881    target: &ostree::RepoFile,
1882    expected: &ostree::RepoFile,
1883    exact: bool,
1884    colliding_inodes: &BTreeSet<u64>,
1885    state: &mut CompareState,
1886) -> Result<()> {
1887    let cancellable = gio::Cancellable::NONE;
1888    let queryattrs = "standard::name,standard::type";
1889    let queryflags = gio::FileQueryInfoFlags::NOFOLLOW_SYMLINKS;
1890    let expected_iter = expected.enumerate_children(queryattrs, queryflags, cancellable)?;
1891
1892    while let Some(expected_info) = expected_iter.next_file(cancellable)? {
1893        let expected_child = expected_iter.child(&expected_info);
1894        let name = expected_info.name();
1895        let name = name.to_str().expect("UTF-8 ostree name");
1896        let path = Utf8PathBuf::from(format!("{root}{name}"));
1897        let target_child = target.child(name);
1898        let target_info = crate::diff::query_info_optional(&target_child, queryattrs, queryflags)
1899            .context("querying optional to")?;
1900        let is_dir = matches!(expected_info.file_type(), gio::FileType::Directory);
1901        if let Some(target_info) = target_info {
1902            let to_child = target_child
1903                .downcast::<ostree::RepoFile>()
1904                .expect("downcast");
1905            to_child.ensure_resolved()?;
1906            let from_child = expected_child
1907                .downcast::<ostree::RepoFile>()
1908                .expect("downcast");
1909            from_child.ensure_resolved()?;
1910
1911            if is_dir {
1912                let from_contents_checksum = from_child.tree_get_contents_checksum();
1913                let to_contents_checksum = to_child.tree_get_contents_checksum();
1914                if from_contents_checksum != to_contents_checksum {
1915                    let subpath = Utf8PathBuf::from(format!("{path}/"));
1916                    compare_commit_trees(
1917                        repo,
1918                        &subpath,
1919                        &from_child,
1920                        &to_child,
1921                        exact,
1922                        colliding_inodes,
1923                        state,
1924                    )?;
1925                }
1926            } else {
1927                let from_checksum = from_child.checksum();
1928                let to_checksum = to_child.checksum();
1929                let matches = if exact {
1930                    from_checksum == to_checksum
1931                } else {
1932                    compare_file_info(&target_info, &expected_info)
1933                };
1934                if !matches {
1935                    let from_inode = inode_of_object(repo, &from_checksum)?;
1936                    let to_inode = inode_of_object(repo, &to_checksum)?;
1937                    if colliding_inodes.contains(&from_inode)
1938                        || colliding_inodes.contains(&to_inode)
1939                    {
1940                        state.inode_corrupted.insert(path);
1941                    } else {
1942                        state.unknown_corrupted.insert(path);
1943                    }
1944                } else {
1945                    state.verified.insert(path);
1946                }
1947            }
1948        } else {
1949            eprintln!("Missing {path}");
1950            state.unknown_corrupted.insert(path);
1951        }
1952    }
1953    Ok(())
1954}
1955
1956#[context("Verifying container image state")]
1957pub(crate) fn verify_container_image(
1958    sysroot: &SysrootLock,
1959    imgref: &ImageReference,
1960    state: &LayeredImageState,
1961    colliding_inodes: &BTreeSet<u64>,
1962    verbose: bool,
1963) -> Result<bool> {
1964    let cancellable = gio::Cancellable::NONE;
1965    let repo = &sysroot.repo();
1966    let merge_commit = state.merge_commit.as_str();
1967    let merge_commit_root = repo.read_commit(merge_commit, gio::Cancellable::NONE)?.0;
1968    let merge_commit_root = merge_commit_root
1969        .downcast::<ostree::RepoFile>()
1970        .expect("downcast");
1971    merge_commit_root.ensure_resolved()?;
1972
1973    let (commit_layer, _component_layers, remaining_layers) =
1974        parse_manifest_layout(&state.manifest, &state.configuration)?;
1975
1976    let mut comparison_state = CompareState::default();
1977
1978    let query = |l: &Descriptor| query_layer(repo, l.clone());
1979
1980    let base_tree = repo
1981        .read_commit(&state.base_commit, cancellable)?
1982        .0
1983        .downcast::<ostree::RepoFile>()
1984        .expect("downcast");
1985    if let Some(commit_layer) = commit_layer {
1986        println!(
1987            "Verifying with base ostree layer {}",
1988            ref_for_layer(commit_layer)?
1989        );
1990    }
1991    compare_commit_trees(
1992        repo,
1993        "/".into(),
1994        &merge_commit_root,
1995        &base_tree,
1996        true,
1997        colliding_inodes,
1998        &mut comparison_state,
1999    )?;
2000
2001    let remaining_layers = remaining_layers
2002        .into_iter()
2003        .map(query)
2004        .collect::<Result<Vec<_>>>()?;
2005
2006    println!("Image has {} derived layers", remaining_layers.len());
2007
2008    for layer in remaining_layers.iter().rev() {
2009        let layer_ref = layer.ostree_ref.as_str();
2010        let layer_commit = layer
2011            .commit
2012            .as_deref()
2013            .ok_or_else(|| anyhow!("Missing layer {layer_ref}"))?;
2014        let layer_tree = repo
2015            .read_commit(layer_commit, cancellable)?
2016            .0
2017            .downcast::<ostree::RepoFile>()
2018            .expect("downcast");
2019        compare_commit_trees(
2020            repo,
2021            "/".into(),
2022            &merge_commit_root,
2023            &layer_tree,
2024            false,
2025            colliding_inodes,
2026            &mut comparison_state,
2027        )?;
2028    }
2029
2030    let n_verified = comparison_state.verified.len();
2031    if comparison_state.is_ok() {
2032        println!("OK image {imgref} (verified={n_verified})");
2033        println!();
2034    } else {
2035        let n_inode = comparison_state.inode_corrupted.len();
2036        let n_other = comparison_state.unknown_corrupted.len();
2037        eprintln!("warning: Found corrupted merge commit");
2038        eprintln!("  inode clashes: {n_inode}");
2039        eprintln!("  unknown:       {n_other}");
2040        eprintln!("  ok:            {n_verified}");
2041        if verbose {
2042            eprintln!("Mismatches:");
2043            for path in comparison_state.inode_corrupted {
2044                eprintln!("  inode: {path}");
2045            }
2046            for path in comparison_state.unknown_corrupted {
2047                eprintln!("  other: {path}");
2048            }
2049        }
2050        eprintln!();
2051        return Ok(false);
2052    }
2053
2054    Ok(true)
2055}
2056
2057#[cfg(test)]
2058mod tests {
2059    use cap_std_ext::cap_tempfile;
2060    use oci_image::{DescriptorBuilder, MediaType, Sha256Digest};
2061
2062    use super::*;
2063
2064    #[test]
2065    fn test_ref_for_descriptor() {
2066        let d = DescriptorBuilder::default()
2067            .size(42u64)
2068            .media_type(MediaType::ImageManifest)
2069            .digest(
2070                Sha256Digest::from_str(
2071                    "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
2072                )
2073                .unwrap(),
2074            )
2075            .build()
2076            .unwrap();
2077        assert_eq!(
2078            ref_for_layer(&d).unwrap(),
2079            "ostree/container/blob/sha256_3A_2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"
2080        );
2081    }
2082
2083    #[test]
2084    fn test_cleanup_root() -> Result<()> {
2085        let td = cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
2086        let usretc = "usr/etc";
2087        cleanup_root(&td).unwrap();
2088        td.create_dir_all(usretc)?;
2089        let usretc = &td.open_dir(usretc)?;
2090        usretc.write("hostname", b"hostname")?;
2091        cleanup_root(&td).unwrap();
2092        assert!(usretc.try_exists("hostname")?);
2093        usretc.write("hostname", b"")?;
2094        cleanup_root(&td).unwrap();
2095        assert!(!td.try_exists("hostname")?);
2096
2097        usretc.symlink_contents("../run/systemd/stub-resolv.conf", "resolv.conf")?;
2098        cleanup_root(&td).unwrap();
2099        assert!(usretc.symlink_metadata("resolv.conf")?.is_symlink());
2100        usretc.remove_file("resolv.conf")?;
2101        usretc.write("resolv.conf", b"")?;
2102        cleanup_root(&td).unwrap();
2103        assert!(!usretc.try_exists("resolv.conf")?);
2104
2105        Ok(())
2106    }
2107}