1use 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
17const MAX_XATTR_SIZE: u32 = 1024 * 1024;
21const MAX_METADATA_SIZE: u32 = 10 * 1024 * 1024;
24
25pub(crate) const SMALL_REGFILE_SIZE: usize = 127 * 1024;
28
29pub(crate) const REPO_PREFIX: &str = "sysroot/ostree/repo/";
31#[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
46pub(crate) struct Importer {
48 repo: ostree::Repo,
49 remote: Option<String>,
50 xattrs: HashMap<String, glib::Variant>,
52 next_xattrs: Option<(String, String)>,
55
56 buf: Vec<u8>,
58
59 stats: ImportStats,
60
61 data: ImporterMode,
63}
64
65fn 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
89fn 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
102fn 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
118fn parse_object_entry_path(path: &Utf8Path) -> Result<(&str, &Utf8Path, &str)> {
123 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 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
156fn 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 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 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 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 if let Ok(path) = path.strip_prefix(REPO_PREFIX) {
205 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 #[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 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 #[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 #[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 #[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 #[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 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 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 #[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 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 #[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 #[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 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 self.next_xattrs = Some((checksum, xattrs_checksum));
489
490 Ok(())
491 }
492
493 #[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 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 self.next_xattrs = Some((target, xattrs_checksum));
529
530 Ok(())
531 }
532
533 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 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 assert!(matches!(self.data, ImporterMode::Commit(None)));
630 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 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 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 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 self.repo
699 .write_commit_detached_metadata(&checksum, Some(&commitmeta), cancellable)?;
700 } else {
701 self.repo.mark_commit_partial(&checksum, true)?;
702
703 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 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 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#[derive(Debug, Default)]
801#[non_exhaustive]
802pub struct TarImportOptions {
803 pub remote: Option<String>,
805}
806
807#[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 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#[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 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}