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