ostree_ext/tar/
import.rs

1//! APIs for extracting OSTree commits from container images
2
3use crate::Result;
4use anyhow::{anyhow, bail, ensure, Context};
5use camino::Utf8Path;
6use camino::Utf8PathBuf;
7use fn_error_context::context;
8use gio::glib;
9use gio::prelude::*;
10use glib::Variant;
11use ostree::gio;
12use std::collections::BTreeSet;
13use std::collections::HashMap;
14use std::io::prelude::*;
15use tracing::{event, instrument, Level};
16
17/// Arbitrary limit on xattrs to avoid RAM exhaustion attacks. The actual filesystem limits are often much smaller.
18// See https://en.wikipedia.org/wiki/Extended_file_attributes
19// For example, XFS limits to 614 KiB.
20const MAX_XATTR_SIZE: u32 = 1024 * 1024;
21/// Limit on metadata objects (dirtree/dirmeta); this is copied
22/// from ostree-core.h.  TODO: Bind this in introspection
23const MAX_METADATA_SIZE: u32 = 10 * 1024 * 1024;
24
25/// Upper size limit for "small" regular files.
26// https://stackoverflow.com/questions/258091/when-should-i-use-mmap-for-file-access
27pub(crate) const SMALL_REGFILE_SIZE: usize = 127 * 1024;
28
29// The prefix for filenames that contain content we actually look at.
30pub(crate) const REPO_PREFIX: &str = "sysroot/ostree/repo/";
31/// Statistics from import.
32#[derive(Debug, Default)]
33struct ImportStats {
34    dirtree: u32,
35    dirmeta: u32,
36    regfile_small: u32,
37    regfile_large: u32,
38    symlinks: u32,
39}
40
41enum ImporterMode {
42    Commit(Option<String>),
43    ObjectSet(BTreeSet<String>),
44}
45
46/// Importer machine.
47pub(crate) struct Importer {
48    repo: ostree::Repo,
49    remote: Option<String>,
50    // Cache of xattrs, keyed by their content checksum.
51    xattrs: HashMap<String, glib::Variant>,
52    // Reusable buffer for xattrs references. It maps a file checksum (.0)
53    // to an xattrs checksum (.1) in the `xattrs` cache above.
54    next_xattrs: Option<(String, String)>,
55
56    // Reusable buffer for reads.  See also https://github.com/rust-lang/rust/issues/78485
57    buf: Vec<u8>,
58
59    stats: ImportStats,
60
61    /// Additional state depending on whether we're importing an object set or a commit.
62    data: ImporterMode,
63}
64
65/// Validate size/type of a tar header for OSTree metadata object.
66fn validate_metadata_header(header: &tar::Header, desc: &str) -> Result<usize> {
67    if header.entry_type() != tar::EntryType::Regular {
68        return Err(anyhow!("Invalid non-regular metadata object {}", desc));
69    }
70    let size = header.size()?;
71    let max_size = MAX_METADATA_SIZE as u64;
72    if size > max_size {
73        return Err(anyhow!(
74            "object of size {} exceeds {} bytes",
75            size,
76            max_size
77        ));
78    }
79    Ok(size as usize)
80}
81
82fn header_attrs(header: &tar::Header) -> Result<(u32, u32, u32)> {
83    let uid: u32 = header.uid()?.try_into()?;
84    let gid: u32 = header.gid()?.try_into()?;
85    let mode: u32 = header.mode()?;
86    Ok((uid, gid, mode))
87}
88
89// The C function ostree_object_type_from_string aborts on
90// unknown strings, so we have a safe version here.
91fn objtype_from_string(t: &str) -> Option<ostree::ObjectType> {
92    Some(match t {
93        "commit" => ostree::ObjectType::Commit,
94        "commitmeta" => ostree::ObjectType::CommitMeta,
95        "dirtree" => ostree::ObjectType::DirTree,
96        "dirmeta" => ostree::ObjectType::DirMeta,
97        "file" => ostree::ObjectType::File,
98        _ => return None,
99    })
100}
101
102/// Given a tar entry, read it all into a GVariant
103fn entry_to_variant<R: std::io::Read, T: StaticVariantType>(
104    mut entry: tar::Entry<R>,
105    desc: &str,
106) -> Result<glib::Variant> {
107    let header = entry.header();
108    let size = validate_metadata_header(header, desc)?;
109
110    let mut buf: Vec<u8> = Vec::with_capacity(size);
111    let n = std::io::copy(&mut entry, &mut buf)?;
112    assert_eq!(n as usize, size);
113    let v = glib::Bytes::from_owned(buf);
114    let v = Variant::from_bytes::<T>(&v);
115    Ok(v.normal_form())
116}
117
118/// Parse an object path into (parent, rest, objtype).
119///
120/// Normal ostree object paths look like 00/1234.commit.
121/// In the tar format, we may also see 00/1234.file.xattrs.
122fn parse_object_entry_path(path: &Utf8Path) -> Result<(&str, &Utf8Path, &str)> {
123    // The "sharded" commit directory.
124    let parentname = path
125        .parent()
126        .and_then(|p| p.file_name())
127        .ok_or_else(|| anyhow!("Invalid path (no parent) {}", path))?;
128    if parentname.len() != 2 {
129        return Err(anyhow!("Invalid checksum parent {}", parentname));
130    }
131    let name = path
132        .file_name()
133        .map(Utf8Path::new)
134        .ok_or_else(|| anyhow!("Invalid path (dir) {}", path))?;
135    let objtype = name
136        .extension()
137        .ok_or_else(|| anyhow!("Invalid objpath {}", path))?;
138
139    Ok((parentname, name, objtype))
140}
141
142fn parse_checksum(parent: &str, name: &Utf8Path) -> Result<String> {
143    let checksum_rest = name
144        .file_stem()
145        .ok_or_else(|| anyhow!("Invalid object path part {}", name))?;
146    // Also take care of the double extension on `.file.xattrs`.
147    let checksum_rest = checksum_rest.trim_end_matches(".file");
148
149    if checksum_rest.len() != 62 {
150        return Err(anyhow!("Invalid checksum part {}", checksum_rest));
151    }
152    let reassembled = format!("{}{}", parent, checksum_rest);
153    validate_sha256(reassembled)
154}
155
156/// Parse a `.file-xattrs-link` link target into the corresponding checksum.
157fn parse_xattrs_link_target(path: &Utf8Path) -> Result<String> {
158    let (parent, rest, _objtype) = parse_object_entry_path(path)?;
159    parse_checksum(parent, rest)
160}
161
162impl Importer {
163    /// Create an importer which will import an OSTree commit object.
164    pub(crate) fn new_for_commit(repo: &ostree::Repo, remote: Option<String>) -> Self {
165        Self {
166            repo: repo.clone(),
167            remote,
168            buf: vec![0u8; 16384],
169            xattrs: Default::default(),
170            next_xattrs: None,
171            stats: Default::default(),
172            data: ImporterMode::Commit(None),
173        }
174    }
175
176    /// Create an importer to write an "object set"; a chunk of objects which is
177    /// usually streamed from a separate storage system, such as an OCI container image layer.
178    pub(crate) fn new_for_object_set(repo: &ostree::Repo) -> Self {
179        Self {
180            repo: repo.clone(),
181            remote: None,
182            buf: vec![0u8; 16384],
183            xattrs: Default::default(),
184            next_xattrs: None,
185            stats: Default::default(),
186            data: ImporterMode::ObjectSet(Default::default()),
187        }
188    }
189
190    // Given a tar entry, filter it out if it doesn't look like an object file in
191    // `/sysroot/ostree`.
192    // It is an error if the filename is invalid UTF-8.  If it is valid UTF-8, return
193    // an owned copy of the path.
194    fn filter_entry<R: std::io::Read>(
195        e: tar::Entry<R>,
196    ) -> Result<Option<(tar::Entry<R>, Utf8PathBuf)>> {
197        if e.header().entry_type() == tar::EntryType::Directory {
198            return Ok(None);
199        }
200        let orig_path = e.path()?;
201        let path = Utf8Path::from_path(&orig_path)
202            .ok_or_else(|| anyhow!("Invalid non-utf8 path {:?}", orig_path))?;
203        // Ignore the regular non-object file hardlinks we inject
204        if let Ok(path) = path.strip_prefix(REPO_PREFIX) {
205            // Filter out the repo config file and refs dir
206            if path.file_name() == Some("config") || path.starts_with("refs") {
207                return Ok(None);
208            }
209            let path = path.into();
210            Ok(Some((e, path)))
211        } else {
212            Ok(None)
213        }
214    }
215
216    pub(crate) fn parse_metadata_entry(path: &Utf8Path) -> Result<(String, ostree::ObjectType)> {
217        let (parentname, name, objtype) = parse_object_entry_path(path)?;
218        let checksum = parse_checksum(parentname, name)?;
219        let objtype = objtype_from_string(objtype)
220            .ok_or_else(|| anyhow!("Invalid object type {}", objtype))?;
221        Ok((checksum, objtype))
222    }
223
224    /// Import a metadata object.
225    #[context("Importing metadata object")]
226    fn import_metadata<R: std::io::Read>(
227        &mut self,
228        entry: tar::Entry<R>,
229        checksum: &str,
230        objtype: ostree::ObjectType,
231    ) -> Result<()> {
232        let v = match objtype {
233            ostree::ObjectType::DirTree => {
234                self.stats.dirtree += 1;
235                entry_to_variant::<_, ostree::TreeVariantType>(entry, checksum)?
236            }
237            ostree::ObjectType::DirMeta => {
238                self.stats.dirmeta += 1;
239                entry_to_variant::<_, ostree::DirmetaVariantType>(entry, checksum)?
240            }
241            o => return Err(anyhow!("Invalid metadata object type; {:?}", o)),
242        };
243        // FIXME validate here that this checksum was in the set we expected.
244        // https://github.com/ostreedev/ostree-rs-ext/issues/1
245        let actual =
246            self.repo
247                .write_metadata(objtype, Some(checksum), &v, gio::Cancellable::NONE)?;
248        assert_eq!(actual.to_hex(), checksum);
249        Ok(())
250    }
251
252    /// Import a content object, large regular file flavour.
253    #[context("Importing regfile")]
254    fn import_large_regfile_object<R: std::io::Read>(
255        &mut self,
256        mut entry: tar::Entry<R>,
257        size: usize,
258        checksum: &str,
259        xattrs: glib::Variant,
260        cancellable: Option<&gio::Cancellable>,
261    ) -> Result<()> {
262        let (uid, gid, mode) = header_attrs(entry.header())?;
263        let w = self.repo.write_regfile(
264            Some(checksum),
265            uid,
266            gid,
267            libc::S_IFREG | mode,
268            size as u64,
269            Some(&xattrs),
270        )?;
271        {
272            let w = w.clone().upcast::<gio::OutputStream>();
273            loop {
274                let n = entry
275                    .read(&mut self.buf[..])
276                    .context("Reading large regfile")?;
277                if n == 0 {
278                    break;
279                }
280                w.write(&self.buf[0..n], cancellable)
281                    .context("Writing large regfile")?;
282            }
283        }
284        let c = w.finish(cancellable)?;
285        debug_assert_eq!(c, checksum);
286        self.stats.regfile_large += 1;
287        Ok(())
288    }
289
290    /// Import a content object, small regular file flavour.
291    #[context("Importing regfile small")]
292    fn import_small_regfile_object<R: std::io::Read>(
293        &mut self,
294        mut entry: tar::Entry<R>,
295        size: usize,
296        checksum: &str,
297        xattrs: glib::Variant,
298        cancellable: Option<&gio::Cancellable>,
299    ) -> Result<()> {
300        let (uid, gid, mode) = header_attrs(entry.header())?;
301        assert!(size <= SMALL_REGFILE_SIZE);
302        let mut buf = vec![0u8; size];
303        entry.read_exact(&mut buf[..])?;
304        let c = self.repo.write_regfile_inline(
305            Some(checksum),
306            uid,
307            gid,
308            libc::S_IFREG | mode,
309            Some(&xattrs),
310            &buf,
311            cancellable,
312        )?;
313        debug_assert_eq!(c.as_str(), checksum);
314        self.stats.regfile_small += 1;
315        Ok(())
316    }
317
318    /// Import a content object, symlink flavour.
319    #[context("Importing symlink")]
320    fn import_symlink_object<R: std::io::Read>(
321        &mut self,
322        entry: tar::Entry<R>,
323        checksum: &str,
324        xattrs: glib::Variant,
325    ) -> Result<()> {
326        let (uid, gid, _) = header_attrs(entry.header())?;
327        let target = entry
328            .link_name()?
329            .ok_or_else(|| anyhow!("Invalid symlink"))?;
330        let target = target
331            .as_os_str()
332            .to_str()
333            .ok_or_else(|| anyhow!("Non-utf8 symlink"))?;
334        let c = self.repo.write_symlink(
335            Some(checksum),
336            uid,
337            gid,
338            Some(&xattrs),
339            target,
340            gio::Cancellable::NONE,
341        )?;
342        debug_assert_eq!(c.as_str(), checksum);
343        self.stats.symlinks += 1;
344        Ok(())
345    }
346
347    /// Import a content object.
348    #[context("Processing content object {}", checksum)]
349    fn import_content_object<R: std::io::Read>(
350        &mut self,
351        entry: tar::Entry<R>,
352        checksum: &str,
353        cancellable: Option<&gio::Cancellable>,
354    ) -> Result<()> {
355        let size: usize = entry.header().size()?.try_into()?;
356
357        // Pop the queued xattrs reference.
358        let (file_csum, xattrs_csum) = self
359            .next_xattrs
360            .take()
361            .ok_or_else(|| anyhow!("Missing xattrs reference"))?;
362        if checksum != file_csum {
363            return Err(anyhow!("Object mismatch, found xattrs for {}", file_csum));
364        }
365
366        if self
367            .repo
368            .has_object(ostree::ObjectType::File, checksum, cancellable)?
369        {
370            return Ok(());
371        }
372
373        // Retrieve xattrs content from the cache.
374        let xattrs = self
375            .xattrs
376            .get(&xattrs_csum)
377            .cloned()
378            .ok_or_else(|| anyhow!("Failed to find xattrs content {}", xattrs_csum,))?;
379
380        match entry.header().entry_type() {
381            tar::EntryType::Regular => {
382                if size > SMALL_REGFILE_SIZE {
383                    self.import_large_regfile_object(entry, size, checksum, xattrs, cancellable)
384                } else {
385                    self.import_small_regfile_object(entry, size, checksum, xattrs, cancellable)
386                }
387            }
388            tar::EntryType::Symlink => self.import_symlink_object(entry, checksum, xattrs),
389            o => Err(anyhow!("Invalid tar entry of type {:?}", o)),
390        }
391    }
392
393    /// Given a tar entry that looks like an object (its path is under ostree/repo/objects/),
394    /// determine its type and import it.
395    #[context("Importing object {}", path)]
396    fn import_object<R: std::io::Read>(
397        &mut self,
398        entry: tar::Entry<'_, R>,
399        path: &Utf8Path,
400        cancellable: Option<&gio::Cancellable>,
401    ) -> Result<()> {
402        let (parentname, name, suffix) = parse_object_entry_path(path)?;
403        let checksum = parse_checksum(parentname, name)?;
404
405        match suffix {
406            "commit" => Err(anyhow!("Found multiple commit objects")),
407            "file" => {
408                self.import_content_object(entry, &checksum, cancellable)?;
409                // Track the objects we wrote
410                match &mut self.data {
411                    ImporterMode::ObjectSet(imported) => {
412                        if let Some(p) = imported.replace(checksum) {
413                            anyhow::bail!("Duplicate object: {}", p);
414                        }
415                    }
416                    ImporterMode::Commit(_) => {}
417                }
418                Ok(())
419            }
420            "file-xattrs" => self.process_file_xattrs(entry, checksum),
421            "file-xattrs-link" => self.process_file_xattrs_link(entry, checksum),
422            "xattrs" => self.process_xattr_ref(entry, checksum),
423            kind => {
424                let objtype = objtype_from_string(kind)
425                    .ok_or_else(|| anyhow!("Invalid object type {}", kind))?;
426                match &mut self.data {
427                    ImporterMode::ObjectSet(_) => {
428                        anyhow::bail!(
429                            "Found metadata object {}.{} in object set mode",
430                            checksum,
431                            objtype
432                        );
433                    }
434                    ImporterMode::Commit(_) => {}
435                }
436                self.import_metadata(entry, &checksum, objtype)
437            }
438        }
439    }
440
441    /// Process a `.file-xattrs` object (v1).
442    #[context("Processing file xattrs")]
443    fn process_file_xattrs(
444        &mut self,
445        entry: tar::Entry<impl std::io::Read>,
446        checksum: String,
447    ) -> Result<()> {
448        self.cache_xattrs_content(entry, Some(checksum))?;
449        Ok(())
450    }
451
452    /// Process a `.file-xattrs-link` object (v1).
453    ///
454    /// This is an hardlink that contains extended attributes for a content object.
455    /// When the max hardlink count is reached, this object may also be encoded as
456    /// a regular file instead.
457    #[context("Processing xattrs link")]
458    fn process_file_xattrs_link(
459        &mut self,
460        entry: tar::Entry<impl std::io::Read>,
461        checksum: String,
462    ) -> Result<()> {
463        use tar::EntryType::{Link, Regular};
464        if let Some(prev) = &self.next_xattrs {
465            bail!(
466                "Found previous dangling xattrs for file object '{}'",
467                prev.0
468            );
469        }
470
471        // Extract the xattrs checksum from the link target or from the content (v1).
472        // Later, it will be used as the key for a lookup into the `self.xattrs` cache.
473        let xattrs_checksum = match entry.header().entry_type() {
474            Link => {
475                let link_target = entry
476                    .link_name()?
477                    .ok_or_else(|| anyhow!("No xattrs link content for {}", checksum))?;
478                let xattr_target = Utf8Path::from_path(&link_target)
479                    .ok_or_else(|| anyhow!("Invalid non-UTF8 xattrs link {}", checksum))?;
480                parse_xattrs_link_target(xattr_target)?
481            }
482            Regular => self.cache_xattrs_content(entry, None)?,
483            x => bail!("Unexpected xattrs type '{:?}' found for {}", x, checksum),
484        };
485
486        // Now xattrs are properly cached for the next content object in the stream,
487        // which should match `checksum`.
488        self.next_xattrs = Some((checksum, xattrs_checksum));
489
490        Ok(())
491    }
492
493    /// Process a `.file.xattrs` entry (v0).
494    ///
495    /// This is an hardlink that contains extended attributes for a content object.
496    #[context("Processing xattrs reference")]
497    fn process_xattr_ref<R: std::io::Read>(
498        &mut self,
499        entry: tar::Entry<R>,
500        target: String,
501    ) -> Result<()> {
502        if let Some(prev) = &self.next_xattrs {
503            bail!(
504                "Found previous dangling xattrs for file object '{}'",
505                prev.0
506            );
507        }
508
509        // Parse the xattrs checksum from the link target (v0).
510        // Later, it will be used as the key for a lookup into the `self.xattrs` cache.
511        let header = entry.header();
512        if header.entry_type() != tar::EntryType::Link {
513            bail!("Non-hardlink xattrs reference found for {}", target);
514        }
515        let xattr_target = entry
516            .link_name()?
517            .ok_or_else(|| anyhow!("No xattrs link content for {}", target))?;
518        let xattr_target = Utf8Path::from_path(&xattr_target)
519            .ok_or_else(|| anyhow!("Invalid non-UTF8 xattrs link {}", target))?;
520        let xattr_target = xattr_target
521            .file_name()
522            .ok_or_else(|| anyhow!("Invalid xattrs link {}", target))?
523            .to_string();
524        let xattrs_checksum = validate_sha256(xattr_target)?;
525
526        // Now xattrs are properly cached for the next content object in the stream,
527        // which should match `checksum`.
528        self.next_xattrs = Some((target, xattrs_checksum));
529
530        Ok(())
531    }
532
533    /// Process a special /xattrs/ entry, with checksum of xattrs content (v0).
534    fn process_split_xattrs_content<R: std::io::Read>(
535        &mut self,
536        entry: tar::Entry<R>,
537    ) -> Result<()> {
538        let checksum = {
539            let path = entry.path()?;
540            let name = path
541                .file_name()
542                .ok_or_else(|| anyhow!("Invalid xattrs dir: {:?}", path))?;
543            let name = name
544                .to_str()
545                .ok_or_else(|| anyhow!("Invalid non-UTF8 xattrs name: {:?}", name))?;
546            validate_sha256(name.to_string())?
547        };
548        self.cache_xattrs_content(entry, Some(checksum))?;
549        Ok(())
550    }
551
552    /// Read an xattrs entry and cache its content, optionally validating its checksum.
553    ///
554    /// This returns the computed checksum for the successfully cached content.
555    fn cache_xattrs_content<R: std::io::Read>(
556        &mut self,
557        mut entry: tar::Entry<R>,
558        expected_checksum: Option<String>,
559    ) -> Result<String> {
560        let header = entry.header();
561        if header.entry_type() != tar::EntryType::Regular {
562            return Err(anyhow!(
563                "Invalid xattr entry of type {:?}",
564                header.entry_type()
565            ));
566        }
567        let n = header.size()?;
568        if n > MAX_XATTR_SIZE as u64 {
569            return Err(anyhow!("Invalid xattr size {}", n));
570        }
571
572        let mut contents = vec![0u8; n as usize];
573        entry.read_exact(contents.as_mut_slice())?;
574        let data: glib::Bytes = contents.as_slice().into();
575        let xattrs_checksum = {
576            let digest = openssl::hash::hash(openssl::hash::MessageDigest::sha256(), &data)?;
577            hex::encode(digest)
578        };
579        if let Some(input) = expected_checksum {
580            ensure!(
581                input == xattrs_checksum,
582                "Checksum mismatch, expected '{}' but computed '{}'",
583                input,
584                xattrs_checksum
585            );
586        }
587
588        let contents = Variant::from_bytes::<&[(&[u8], &[u8])]>(&data);
589        self.xattrs.insert(xattrs_checksum.clone(), contents);
590        Ok(xattrs_checksum)
591    }
592
593    fn import_objects_impl<'a>(
594        &mut self,
595        ents: impl Iterator<Item = Result<(tar::Entry<'a, impl Read + Send + Unpin + 'a>, Utf8PathBuf)>>,
596        cancellable: Option<&gio::Cancellable>,
597    ) -> Result<()> {
598        for entry in ents {
599            let (entry, path) = entry?;
600            if let Ok(p) = path.strip_prefix("objects/") {
601                self.import_object(entry, p, cancellable)?;
602            } else if path.strip_prefix("xattrs/").is_ok() {
603                self.process_split_xattrs_content(entry)?;
604            }
605        }
606        Ok(())
607    }
608
609    #[context("Importing objects")]
610    pub(crate) fn import_objects(
611        &mut self,
612        archive: &mut tar::Archive<impl Read + Send + Unpin>,
613        cancellable: Option<&gio::Cancellable>,
614    ) -> Result<()> {
615        let ents = archive.entries()?.filter_map(|e| match e {
616            Ok(e) => Self::filter_entry(e).transpose(),
617            Err(e) => Some(Err(anyhow::Error::msg(e))),
618        });
619        self.import_objects_impl(ents, cancellable)
620    }
621
622    #[context("Importing commit")]
623    pub(crate) fn import_commit(
624        &mut self,
625        archive: &mut tar::Archive<impl Read + Send + Unpin>,
626        cancellable: Option<&gio::Cancellable>,
627    ) -> Result<()> {
628        // This can only be invoked once
629        assert!(matches!(self.data, ImporterMode::Commit(None)));
630        // Create an iterator that skips over directories; we just care about the file names.
631        let mut ents = archive.entries()?.filter_map(|e| match e {
632            Ok(e) => Self::filter_entry(e).transpose(),
633            Err(e) => Some(Err(anyhow::Error::msg(e))),
634        });
635        // Read the commit object.
636        let (commit_ent, commit_path) = ents
637            .next()
638            .ok_or_else(|| anyhow!("Commit object not found"))??;
639
640        if commit_ent.header().entry_type() != tar::EntryType::Regular {
641            return Err(anyhow!(
642                "Expected regular file for commit object, not {:?}",
643                commit_ent.header().entry_type()
644            ));
645        }
646        let (checksum, objtype) = Self::parse_metadata_entry(&commit_path)?;
647        if objtype != ostree::ObjectType::Commit {
648            return Err(anyhow!("Expected commit object, not {:?}", objtype));
649        }
650        let commit = entry_to_variant::<_, ostree::CommitVariantType>(commit_ent, &checksum)?;
651
652        let (next_ent, nextent_path) = ents
653            .next()
654            .ok_or_else(|| anyhow!("End of stream after commit object"))??;
655        let (next_checksum, next_objtype) = Self::parse_metadata_entry(&nextent_path)?;
656
657        if let Some(remote) = self.remote.as_deref() {
658            if next_objtype != ostree::ObjectType::CommitMeta {
659                return Err(anyhow!(
660                    "Using remote {} for verification; Expected commitmeta object, not {:?}",
661                    remote,
662                    next_objtype
663                ));
664            }
665            if next_checksum != checksum {
666                return Err(anyhow!(
667                    "Expected commitmeta checksum {}, found {}",
668                    checksum,
669                    next_checksum
670                ));
671            }
672            let commitmeta = entry_to_variant::<_, std::collections::HashMap<String, glib::Variant>>(
673                next_ent,
674                &next_checksum,
675            )?;
676
677            // Now that we have both the commit and detached metadata in memory, verify that
678            // the signatures in the detached metadata correctly sign the commit.
679            self.repo
680                .signature_verify_commit_data(
681                    remote,
682                    &commit.data_as_bytes(),
683                    &commitmeta.data_as_bytes(),
684                    ostree::RepoVerifyFlags::empty(),
685                )
686                .context("Verifying ostree commit in tar stream")?;
687
688            self.repo.mark_commit_partial(&checksum, true)?;
689
690            // Write the commit object, which also verifies its checksum.
691            let actual_checksum =
692                self.repo
693                    .write_metadata(objtype, Some(&checksum), &commit, cancellable)?;
694            assert_eq!(actual_checksum.to_hex(), checksum);
695            event!(Level::DEBUG, "Imported {}.commit", checksum);
696
697            // Finally, write the detached metadata.
698            self.repo
699                .write_commit_detached_metadata(&checksum, Some(&commitmeta), cancellable)?;
700        } else {
701            self.repo.mark_commit_partial(&checksum, true)?;
702
703            // We're not doing any validation of the commit, so go ahead and write it.
704            let actual_checksum =
705                self.repo
706                    .write_metadata(objtype, Some(&checksum), &commit, cancellable)?;
707            assert_eq!(actual_checksum.to_hex(), checksum);
708            event!(Level::DEBUG, "Imported {}.commit", checksum);
709
710            // Write the next object, whether it's commit metadata or not.
711            let (meta_checksum, meta_objtype) = Self::parse_metadata_entry(&nextent_path)?;
712            match meta_objtype {
713                ostree::ObjectType::CommitMeta => {
714                    let commitmeta = entry_to_variant::<
715                        _,
716                        std::collections::HashMap<String, glib::Variant>,
717                    >(next_ent, &meta_checksum)?;
718                    self.repo.write_commit_detached_metadata(
719                        &checksum,
720                        Some(&commitmeta),
721                        gio::Cancellable::NONE,
722                    )?;
723                }
724                _ => {
725                    self.import_object(next_ent, &nextent_path, cancellable)?;
726                }
727            }
728        }
729        match &mut self.data {
730            ImporterMode::Commit(c) => {
731                c.replace(checksum);
732            }
733            ImporterMode::ObjectSet(_) => unreachable!(),
734        }
735
736        self.import_objects_impl(ents, cancellable)?;
737
738        Ok(())
739    }
740
741    pub(crate) fn finish_import_commit(self) -> String {
742        tracing::debug!("Import stats: {:?}", self.stats);
743        match self.data {
744            ImporterMode::Commit(c) => c.unwrap(),
745            ImporterMode::ObjectSet(_) => unreachable!(),
746        }
747    }
748
749    pub(crate) fn default_dirmeta() -> glib::Variant {
750        let finfo = gio::FileInfo::new();
751        finfo.set_attribute_uint32("unix::uid", 0);
752        finfo.set_attribute_uint32("unix::gid", 0);
753        finfo.set_attribute_uint32("unix::mode", libc::S_IFDIR | 0o755);
754        // SAFETY: TODO: This is not a nullable return, fix it in ostree
755        ostree::create_directory_metadata(&finfo, None)
756    }
757
758    pub(crate) fn finish_import_object_set(self) -> Result<String> {
759        let objset = match self.data {
760            ImporterMode::Commit(_) => unreachable!(),
761            ImporterMode::ObjectSet(s) => s,
762        };
763        tracing::debug!("Imported {} content objects", objset.len());
764        let mtree = ostree::MutableTree::new();
765        for checksum in objset.into_iter() {
766            mtree.replace_file(&checksum, &checksum)?;
767        }
768        let dirmeta = self.repo.write_metadata(
769            ostree::ObjectType::DirMeta,
770            None,
771            &Self::default_dirmeta(),
772            gio::Cancellable::NONE,
773        )?;
774        mtree.set_metadata_checksum(&dirmeta.to_hex());
775        let tree = self.repo.write_mtree(&mtree, gio::Cancellable::NONE)?;
776        let commit = self.repo.write_commit_with_time(
777            None,
778            None,
779            None,
780            None,
781            tree.downcast_ref().unwrap(),
782            0,
783            gio::Cancellable::NONE,
784        )?;
785        Ok(commit.to_string())
786    }
787}
788
789fn validate_sha256(input: String) -> Result<String> {
790    if input.len() != 64 {
791        return Err(anyhow!("Invalid sha256 checksum (len) {}", input));
792    }
793    if !input.chars().all(|c| matches!(c, '0'..='9' | 'a'..='f')) {
794        return Err(anyhow!("Invalid sha256 checksum {}", input));
795    }
796    Ok(input)
797}
798
799/// Configuration for tar import.
800#[derive(Debug, Default)]
801#[non_exhaustive]
802pub struct TarImportOptions {
803    /// Name of the remote to use for signature verification.
804    pub remote: Option<String>,
805}
806
807/// Read the contents of a tarball and import the ostree commit inside.
808/// Returns the sha256 of the imported commit.
809#[instrument(level = "debug", skip_all)]
810pub async fn import_tar(
811    repo: &ostree::Repo,
812    src: impl tokio::io::AsyncRead + Send + Unpin + 'static,
813    options: Option<TarImportOptions>,
814) -> Result<String> {
815    let options = options.unwrap_or_default();
816    let src = tokio_util::io::SyncIoBridge::new(src);
817    let repo = repo.clone();
818    // The tar code we use today is blocking, so we spawn a thread.
819    crate::tokio_util::spawn_blocking_cancellable_flatten(move |cancellable| {
820        let mut archive = tar::Archive::new(src);
821        let txn = repo.auto_transaction(Some(cancellable))?;
822        let mut importer = Importer::new_for_commit(&repo, options.remote);
823        importer.import_commit(&mut archive, Some(cancellable))?;
824        let checksum = importer.finish_import_commit();
825        txn.commit(Some(cancellable))?;
826        repo.mark_commit_partial(&checksum, false)?;
827        Ok::<_, anyhow::Error>(checksum)
828    })
829    .await
830}
831
832/// Read the contents of a tarball and import the content objects inside.
833/// Generates a synthetic commit object referencing them.
834#[instrument(level = "debug", skip_all)]
835pub async fn import_tar_objects(
836    repo: &ostree::Repo,
837    src: impl tokio::io::AsyncRead + Send + Unpin + 'static,
838) -> Result<String> {
839    let src = tokio_util::io::SyncIoBridge::new(src);
840    let repo = repo.clone();
841    // The tar code we use today is blocking, so we spawn a thread.
842    crate::tokio_util::spawn_blocking_cancellable_flatten(move |cancellable| {
843        let mut archive = tar::Archive::new(src);
844        let mut importer = Importer::new_for_object_set(&repo);
845        let txn = repo.auto_transaction(Some(cancellable))?;
846        importer.import_objects(&mut archive, Some(cancellable))?;
847        let r = importer.finish_import_object_set()?;
848        txn.commit(Some(cancellable))?;
849        Ok::<_, anyhow::Error>(r)
850    })
851    .await
852}
853
854#[cfg(test)]
855mod tests {
856    use super::*;
857
858    #[test]
859    fn test_parse_metadata_entry() {
860        let c = "a8/6d80a3e9ff77c2e3144c787b7769b300f91ffd770221aac27bab854960b964";
861        let invalid = format!("{}.blah", c);
862        for &k in &["", "42", c, &invalid] {
863            assert!(Importer::parse_metadata_entry(k.into()).is_err())
864        }
865        let valid = format!("{}.commit", c);
866        let r = Importer::parse_metadata_entry(valid.as_str().into()).unwrap();
867        assert_eq!(r.0, c.replace('/', ""));
868        assert_eq!(r.1, ostree::ObjectType::Commit);
869    }
870
871    #[test]
872    fn test_validate_sha256() {
873        let err_cases = &[
874            "a86d80a3e9ff77c2e3144c787b7769b300f91ffd770221aac27bab854960b9644",
875            "a86d80a3E9ff77c2e3144c787b7769b300f91ffd770221aac27bab854960b964",
876        ];
877        for input in err_cases {
878            validate_sha256(input.to_string()).unwrap_err();
879        }
880
881        validate_sha256(
882            "a86d80a3e9ff77c2e3144c787b7769b300f91ffd770221aac27bab854960b964".to_string(),
883        )
884        .unwrap();
885    }
886
887    #[test]
888    fn test_parse_object_entry_path() {
889        let path =
890            "sysroot/ostree/repo/objects/b8/627e3ef0f255a322d2bd9610cfaaacc8f122b7f8d17c0e7e3caafa160f9fc7.file.xattrs";
891        let input = Utf8PathBuf::from(path);
892        let expected_parent = "b8";
893        let expected_rest =
894            "627e3ef0f255a322d2bd9610cfaaacc8f122b7f8d17c0e7e3caafa160f9fc7.file.xattrs";
895        let expected_objtype = "xattrs";
896        let output = parse_object_entry_path(&input).unwrap();
897        assert_eq!(output.0, expected_parent);
898        assert_eq!(output.1, expected_rest);
899        assert_eq!(output.2, expected_objtype);
900    }
901
902    #[test]
903    fn test_parse_checksum() {
904        let parent = "b8";
905        let name = "627e3ef0f255a322d2bd9610cfaaacc8f122b7f8d17c0e7e3caafa160f9fc7.file.xattrs";
906        let expected = "b8627e3ef0f255a322d2bd9610cfaaacc8f122b7f8d17c0e7e3caafa160f9fc7";
907        let output = parse_checksum(parent, &Utf8PathBuf::from(name)).unwrap();
908        assert_eq!(output, expected);
909    }
910
911    #[test]
912    fn test_parse_xattrs_link_target() {
913        let err_cases = &[
914            "",
915            "b8627e3ef0f255a322d2bd9610cfaaacc8f122b7f8d17c0e7e3caafa160f9fc7.file-xattrs",
916            "../b8/62.file-xattrs",
917        ];
918        for input in err_cases {
919            parse_xattrs_link_target(Utf8Path::new(input)).unwrap_err();
920        }
921
922        let ok_cases = &[
923            "../b8/627e3ef0f255a322d2bd9610cfaaacc8f122b7f8d17c0e7e3caafa160f9fc7.file-xattrs",
924            "sysroot/ostree/repo/objects/b8/627e3ef0f255a322d2bd9610cfaaacc8f122b7f8d17c0e7e3caafa160f9fc7.file-xattrs",
925        ];
926        let expected = "b8627e3ef0f255a322d2bd9610cfaaacc8f122b7f8d17c0e7e3caafa160f9fc7";
927        for input in ok_cases {
928            let output = parse_xattrs_link_target(Utf8Path::new(input)).unwrap();
929            assert_eq!(output, expected);
930        }
931    }
932}