1use crate::chunking;
4use crate::objgv::*;
5use anyhow::{Context, Result, anyhow, ensure};
6use camino::{Utf8Path, Utf8PathBuf};
7use fn_error_context::context;
8use gio::glib;
9use gio::prelude::*;
10use gvariant::aligned_bytes::TryAsAligned;
11use gvariant::{Marker, Structure};
12use ostree::gio;
13use std::borrow::Borrow;
14use std::borrow::Cow;
15use std::collections::HashSet;
16use std::ffi::CStr;
17use std::io::BufReader;
18
19pub const BARE_SPLIT_XATTRS_MODE: &str = "bare-split-xattrs";
21
22pub(crate) const SECURITY_SELINUX_XATTR_C: &CStr = c"security.selinux";
25pub(crate) const SECURITY_SELINUX_XATTR: &str = const {
27 match SECURITY_SELINUX_XATTR_C.to_str() {
28 Ok(r) => r,
29 Err(_) => unreachable!(),
30 }
31};
32
33const SYSROOT: &str = "sysroot";
35const OSTREEDIR: &str = "sysroot/ostree";
37#[allow(dead_code)]
39const OSTREEREF: &str = "encapsulated";
40
41const TAR_PATH_PREFIX_V0: &str = "./";
47
48const REPO_CONFIG: &str = r#"[core]
51repo_version=1
52mode=bare-split-xattrs
53"#;
54
55const BUF_CAPACITY: usize = 131072;
58
59fn map_path_inner<'p>(
61 p: &'p Utf8Path,
62 from: &'_ str,
63 to: &'_ str,
64) -> std::borrow::Cow<'p, Utf8Path> {
65 match p.strip_prefix(from) {
66 Ok(r) => {
67 if r.components().count() > 0 {
68 Cow::Owned(Utf8Path::new(to).join(r))
69 } else {
70 Cow::Owned(Utf8PathBuf::from(to))
71 }
72 }
73 _ => Cow::Borrowed(p),
74 }
75}
76
77fn map_path(p: &Utf8Path) -> std::borrow::Cow<'_, Utf8Path> {
79 map_path_inner(p, "./usr/etc", "./etc")
80}
81
82fn unmap_path(p: &Utf8Path) -> std::borrow::Cow<'_, Utf8Path> {
85 map_path_inner(p, "etc", "usr/etc")
86}
87
88fn map_path_v1(p: &Utf8Path) -> &Utf8Path {
90 debug_assert!(!p.starts_with("/") && !p.starts_with("."));
91 if p.starts_with("usr/etc") {
92 p.strip_prefix("usr/").unwrap()
93 } else {
94 p
95 }
96}
97
98pub(crate) fn path_equivalent_for_tar(a: impl AsRef<Utf8Path>, b: impl AsRef<Utf8Path>) -> bool {
103 fn strip_prefix(p: &Utf8Path) -> &Utf8Path {
104 if let Ok(p) = p.strip_prefix("/") {
105 return p;
106 } else if let Ok(p) = p.strip_prefix("./") {
107 return p;
108 }
109 p
110 }
111 strip_prefix(a.as_ref()) == strip_prefix(b.as_ref())
112}
113
114struct OstreeTarWriter<'a, W: std::io::Write> {
115 repo: &'a ostree::Repo,
116 commit_checksum: &'a str,
117 commit_object: glib::Variant,
118 out: &'a mut tar::Builder<W>,
119 #[allow(dead_code)]
120 options: ExportOptions,
121 wrote_initdirs: bool,
122 structure_only: bool,
124 wrote_vartmp: bool, wrote_dirtree: HashSet<String>,
126 wrote_dirmeta: HashSet<String>,
127 wrote_content: HashSet<String>,
128}
129
130pub(crate) fn object_path(objtype: ostree::ObjectType, checksum: &str) -> Utf8PathBuf {
131 let suffix = match objtype {
132 ostree::ObjectType::Commit => "commit",
133 ostree::ObjectType::CommitMeta => "commitmeta",
134 ostree::ObjectType::DirTree => "dirtree",
135 ostree::ObjectType::DirMeta => "dirmeta",
136 ostree::ObjectType::File => "file",
137 o => panic!("Unexpected object type: {o:?}"),
138 };
139 let (first, rest) = checksum.split_at(2);
140 format!("{OSTREEDIR}/repo/objects/{first}/{rest}.{suffix}").into()
141}
142
143fn v1_xattrs_object_path(checksum: &str) -> Utf8PathBuf {
144 let (first, rest) = checksum.split_at(2);
145 format!("{OSTREEDIR}/repo/objects/{first}/{rest}.file-xattrs-link").into()
146}
147
148fn symlink_is_denormal(target: &str) -> bool {
156 target.contains("//")
157}
158
159pub(crate) fn tar_append_default_data(
160 out: &mut tar::Builder<impl std::io::Write>,
161 path: &Utf8Path,
162 buf: &[u8],
163) -> Result<()> {
164 let mut h = tar::Header::new_gnu();
165 h.set_entry_type(tar::EntryType::Regular);
166 h.set_uid(0);
167 h.set_gid(0);
168 h.set_mode(0o644);
169 h.set_size(buf.len() as u64);
170 out.append_data(&mut h, path, buf).map_err(Into::into)
171}
172
173impl<'a, W: std::io::Write> OstreeTarWriter<'a, W> {
174 fn new(
175 repo: &'a ostree::Repo,
176 commit_checksum: &'a str,
177 out: &'a mut tar::Builder<W>,
178 options: ExportOptions,
179 ) -> Result<Self> {
180 let commit_object = repo.load_commit(commit_checksum)?.0;
181 let r = Self {
182 repo,
183 commit_checksum,
184 commit_object,
185 out,
186 options,
187 wrote_initdirs: false,
188 structure_only: false,
189 wrote_vartmp: false,
190 wrote_dirmeta: HashSet::new(),
191 wrote_dirtree: HashSet::new(),
192 wrote_content: HashSet::new(),
193 };
194 Ok(r)
195 }
196
197 fn filter_mode(&self, mode: u32) -> u32 {
201 mode & !libc::S_IFMT
202 }
203
204 fn append_default_dir(&mut self, path: &Utf8Path) -> Result<()> {
206 let mut h = tar::Header::new_gnu();
207 h.set_entry_type(tar::EntryType::Directory);
208 h.set_uid(0);
209 h.set_gid(0);
210 h.set_mode(0o755);
211 h.set_size(0);
212 self.out.append_data(&mut h, path, &mut std::io::empty())?;
213 Ok(())
214 }
215
216 fn append_default_data(&mut self, path: &Utf8Path, buf: &[u8]) -> Result<()> {
218 tar_append_default_data(self.out, path, buf)
219 }
220
221 fn write_repo_structure(&mut self) -> Result<()> {
223 if self.wrote_initdirs {
224 return Ok(());
225 }
226
227 let objdir: Utf8PathBuf = format!("{OSTREEDIR}/repo/objects").into();
228 let parent_dirs = {
230 let mut parts: Vec<_> = objdir.ancestors().collect();
231 parts.reverse();
232 parts
233 };
234 for path in parent_dirs {
235 match path.as_str() {
236 "/" | "" => continue,
237 _ => {}
238 }
239 self.append_default_dir(path)?;
240 }
241 for d in 0..=0xFF {
243 let path: Utf8PathBuf = format!("{objdir}/{d:02x}").into();
244 self.append_default_dir(&path)?;
245 }
246 let subdirs = [
248 "extensions",
249 "refs",
250 "refs/heads",
251 "refs/mirrors",
252 "refs/remotes",
253 "state",
254 "tmp",
255 "tmp/cache",
256 ];
257 for d in subdirs {
258 let path: Utf8PathBuf = format!("{OSTREEDIR}/repo/{d}").into();
259 self.append_default_dir(&path)?;
260 }
261
262 {
264 let path = format!("{OSTREEDIR}/repo/config");
265 self.append_default_data(Utf8Path::new(&path), REPO_CONFIG.as_bytes())?;
266 }
267
268 self.wrote_initdirs = true;
269 Ok(())
270 }
271
272 fn write_commit(&mut self) -> Result<()> {
274 let cancellable = gio::Cancellable::NONE;
275
276 let commit_bytes = self.commit_object.data_as_bytes();
277 let commit_bytes = commit_bytes.try_as_aligned()?;
278 let commit = gv_commit!().cast(commit_bytes);
279 let commit = commit.to_tuple();
280 let contents = hex::encode(commit.6);
281 let metadata_checksum = &hex::encode(commit.7);
282 let metadata_v = self
283 .repo
284 .load_variant(ostree::ObjectType::DirMeta, metadata_checksum)?;
285 let metadata = &ostree::DirMetaParsed::from_variant(&metadata_v).unwrap();
287 let rootpath = Utf8Path::new(TAR_PATH_PREFIX_V0);
288
289 self.append_dir(rootpath, metadata)?;
292
293 if !self.options.raw {
294 self.write_repo_structure()?;
296
297 self.append_commit_object()?;
298
299 self.append(ostree::ObjectType::DirMeta, metadata_checksum, &metadata_v)?;
301 }
302
303 self.append_dirtree(
305 Utf8Path::new(TAR_PATH_PREFIX_V0),
306 contents,
307 true,
308 cancellable,
309 )?;
310
311 self.append_standard_var(cancellable)?;
312
313 Ok(())
314 }
315
316 fn append_commit_object(&mut self) -> Result<()> {
317 self.append(
318 ostree::ObjectType::Commit,
319 self.commit_checksum,
320 &self.commit_object.clone(),
321 )?;
322 if let Some(commitmeta) = self
323 .repo
324 .read_commit_detached_metadata(self.commit_checksum, gio::Cancellable::NONE)?
325 {
326 self.append(
327 ostree::ObjectType::CommitMeta,
328 self.commit_checksum,
329 &commitmeta,
330 )?;
331 }
332 Ok(())
333 }
334
335 fn append(
336 &mut self,
337 objtype: ostree::ObjectType,
338 checksum: &str,
339 v: &glib::Variant,
340 ) -> Result<()> {
341 let set = match objtype {
342 ostree::ObjectType::Commit | ostree::ObjectType::CommitMeta => None,
343 ostree::ObjectType::DirTree => Some(&mut self.wrote_dirtree),
344 ostree::ObjectType::DirMeta => Some(&mut self.wrote_dirmeta),
345 o => panic!("Unexpected object type: {o:?}"),
346 };
347 if let Some(set) = set {
348 if set.contains(checksum) {
349 return Ok(());
350 }
351 let inserted = set.insert(checksum.to_string());
352 debug_assert!(inserted);
353 }
354
355 let data = v.data_as_bytes();
356 let data = data.as_ref();
357 self.append_default_data(&object_path(objtype, checksum), data)
358 .with_context(|| format!("Writing object {checksum}"))?;
359 Ok(())
360 }
361
362 #[context("Writing xattrs")]
368 fn append_ostree_xattrs(&mut self, checksum: &str, xattrs: &glib::Variant) -> Result<bool> {
369 let xattrs_data = xattrs.data_as_bytes();
370 let xattrs_data = xattrs_data.as_ref();
371
372 let path = v1_xattrs_object_path(&checksum);
373 self.append_default_data(&path, xattrs_data)?;
374
375 Ok(true)
376 }
377
378 #[context("Writing tar xattrs")]
383 fn append_tarstream_xattrs(&mut self, xattrs: &glib::Variant) -> Result<()> {
384 let v = xattrs.data_as_bytes();
385 let v = v.try_as_aligned().unwrap();
386 let v = gvariant::gv!("a(ayay)").cast(v);
387 let mut pax_extensions = Vec::new();
388 for entry in v {
389 let (k, v) = entry.to_tuple();
390 let k = CStr::from_bytes_with_nul(k).unwrap();
391 let k = k
392 .to_str()
393 .with_context(|| format!("Found non-UTF8 xattr: {k:?}"))?;
394 if k == SECURITY_SELINUX_XATTR {
395 continue;
396 }
397 pax_extensions.push((format!("SCHILY.xattr.{k}"), v));
398 }
399 self.out
400 .append_pax_extensions(pax_extensions.iter().map(|(k, v)| (k.as_str(), *v)))?;
401 Ok(())
402 }
403
404 fn append_content(&mut self, checksum: &str) -> Result<(Utf8PathBuf, tar::Header)> {
407 let path = object_path(ostree::ObjectType::File, checksum);
408
409 let (instream, meta, xattrs) = self.repo.load_file(checksum, gio::Cancellable::NONE)?;
410
411 let mut h = tar::Header::new_gnu();
412 h.set_uid(meta.attribute_uint32("unix::uid") as u64);
413 h.set_gid(meta.attribute_uint32("unix::gid") as u64);
414 let mode = meta.attribute_uint32("unix::mode");
415 h.set_mode(self.filter_mode(mode));
416 if instream.is_some() {
417 h.set_entry_type(tar::EntryType::Regular);
418 h.set_size(meta.size() as u64);
419 } else {
420 h.set_entry_type(tar::EntryType::Symlink);
421 h.set_size(0);
422 }
423 if !self.wrote_content.contains(checksum) {
424 let inserted = self.wrote_content.insert(checksum.to_string());
425 debug_assert!(inserted);
426
427 self.append_ostree_xattrs(checksum, &xattrs)?;
431 self.append_tarstream_xattrs(&xattrs)?;
432
433 if let Some(instream) = instream {
434 ensure!(meta.file_type() == gio::FileType::Regular);
435
436 let mut instream = BufReader::with_capacity(BUF_CAPACITY, instream.into_read());
437 self.out
438 .append_data(&mut h, &path, &mut instream)
439 .with_context(|| format!("Writing regfile {checksum}"))?;
440 } else {
441 ensure!(meta.file_type() == gio::FileType::SymbolicLink);
442
443 let target = meta
444 .symlink_target()
445 .ok_or_else(|| anyhow!("Missing symlink target"))?;
446 let target = target
447 .to_str()
448 .ok_or_else(|| anyhow!("Invalid UTF-8 symlink target: {target:?}"))?;
449 let context = || format!("Writing content symlink: {checksum}");
450 if symlink_is_denormal(target) {
452 h.set_link_name_literal(target).with_context(context)?;
453 self.out
454 .append_data(&mut h, &path, &mut std::io::empty())
455 .with_context(context)?;
456 } else {
457 self.out
458 .append_link(&mut h, &path, target)
459 .with_context(context)?;
460 }
461 }
462 }
463
464 Ok((path, h))
465 }
466
467 fn append_dir(&mut self, dirpath: &Utf8Path, meta: &ostree::DirMetaParsed) -> Result<()> {
469 let mut header = tar::Header::new_gnu();
470 header.set_entry_type(tar::EntryType::Directory);
471 header.set_size(0);
472 header.set_uid(meta.uid as u64);
473 header.set_gid(meta.gid as u64);
474 header.set_mode(self.filter_mode(meta.mode));
475 self.out
476 .append_data(&mut header, dirpath, std::io::empty())?;
477 Ok(())
478 }
479
480 fn append_content_hardlink(
483 &mut self,
484 srcpath: &Utf8Path,
485 mut h: tar::Header,
486 dest: &Utf8Path,
487 ) -> Result<()> {
488 let is_regular_zerosized = if h.entry_type() == tar::EntryType::Regular {
494 let size = h.size().context("Querying size for hardlink append")?;
495 size == 0
496 } else {
497 false
498 };
499 h.set_size(0);
501 if is_regular_zerosized {
502 self.out.append_data(&mut h, dest, &mut std::io::empty())?;
503 } else {
504 h.set_entry_type(tar::EntryType::Link);
505 h.set_link_name(srcpath)?;
506 self.out.append_data(&mut h, dest, &mut std::io::empty())?;
507 }
508 Ok(())
509 }
510
511 fn append_dirtree<C: IsA<gio::Cancellable>>(
513 &mut self,
514 dirpath: &Utf8Path,
515 checksum: String,
516 is_root: bool,
517 cancellable: Option<&C>,
518 ) -> Result<()> {
519 let v = &self
520 .repo
521 .load_variant(ostree::ObjectType::DirTree, &checksum)?;
522 self.append(ostree::ObjectType::DirTree, &checksum, v)?;
523 drop(checksum);
524 let v = v.data_as_bytes();
525 let v = v.try_as_aligned()?;
526 let v = gv_dirtree!().cast(v);
527 let (files, dirs) = v.to_tuple();
528
529 if let Some(c) = cancellable {
530 c.set_error_if_cancelled()?;
531 }
532
533 if !self.structure_only {
534 for file in files {
535 let (name, csum) = file.to_tuple();
536 let name = name.to_str();
537 let checksum = &hex::encode(csum);
538 let (objpath, h) = self.append_content(checksum)?;
539 let subpath = &dirpath.join(name);
540 let subpath = map_path(subpath);
541 self.append_content_hardlink(&objpath, h, &subpath)
542 .with_context(|| format!("Hardlinking {checksum} to {subpath}"))?;
543 }
544 }
545
546 if path_equivalent_for_tar(dirpath, "var/tmp") {
549 self.wrote_vartmp = true;
550 }
551
552 for item in dirs {
553 let (name, contents_csum, meta_csum) = item.to_tuple();
554 let name = name.to_str();
555 let metadata = {
556 let meta_csum = &hex::encode(meta_csum);
557 let meta_v = &self
558 .repo
559 .load_variant(ostree::ObjectType::DirMeta, meta_csum)?;
560 self.append(ostree::ObjectType::DirMeta, meta_csum, meta_v)?;
561 ostree::DirMetaParsed::from_variant(meta_v).unwrap()
563 };
564 if is_root && name == SYSROOT {
566 continue;
567 }
568 let dirtree_csum = hex::encode(contents_csum);
569 let subpath = &dirpath.join(name);
570 let subpath = map_path(subpath);
571 self.append_dir(&subpath, &metadata)?;
572 self.append_dirtree(&subpath, dirtree_csum, false, cancellable)?;
573 }
574
575 Ok(())
576 }
577
578 fn append_standard_var(&mut self, cancellable: Option<&gio::Cancellable>) -> Result<()> {
585 if self.wrote_vartmp {
587 return Ok(());
588 }
589 if let Some(c) = cancellable {
590 c.set_error_if_cancelled()?;
591 }
592 let mut header = tar::Header::new_gnu();
593 header.set_entry_type(tar::EntryType::Directory);
594 header.set_size(0);
595 header.set_uid(0);
596 header.set_gid(0);
597 header.set_mode(self.filter_mode(libc::S_IFDIR | 0o1777));
598 self.out
599 .append_data(&mut header, "var/tmp", std::io::empty())?;
600 Ok(())
601 }
602
603 fn write_parents_of(
604 &mut self,
605 path: &Utf8Path,
606 root: &gio::File,
607 cache: &mut HashSet<Utf8PathBuf>,
608 ) -> Result<()> {
609 let Some(parent) = path.parent() else {
610 return Ok(());
611 };
612
613 if parent.components().count() == 0 {
614 return Ok(());
615 }
616
617 if cache.contains(parent) {
618 return Ok(());
619 }
620
621 self.write_parents_of(parent, root, cache)?;
622
623 let inserted = cache.insert(parent.to_owned());
624 debug_assert!(inserted);
625
626 let parent_file = root.resolve_relative_path(unmap_path(parent).as_ref());
627 let queryattrs = "unix::*";
628 let queryflags = gio::FileQueryInfoFlags::NOFOLLOW_SYMLINKS;
629 let stat = parent_file.query_info(&queryattrs, queryflags, gio::Cancellable::NONE)?;
630 let uid = stat.attribute_uint32(gio::FILE_ATTRIBUTE_UNIX_UID);
631 let gid = stat.attribute_uint32(gio::FILE_ATTRIBUTE_UNIX_GID);
632 let orig_mode = stat.attribute_uint32(gio::FILE_ATTRIBUTE_UNIX_MODE);
633 let mode = self.filter_mode(orig_mode);
634
635 let mut header = tar::Header::new_gnu();
636 header.set_entry_type(tar::EntryType::Directory);
637 header.set_size(0);
638 header.set_uid(uid as u64);
639 header.set_gid(gid as u64);
640 header.set_mode(mode);
641 self.out
642 .append_data(&mut header, parent, std::io::empty())?;
643 Ok(())
644 }
645}
646
647#[context("Writing tar xattrs")]
651fn append_pax_xattrs<W: std::io::Write>(
652 out: &mut tar::Builder<W>,
653 xattrs: &glib::Variant,
654) -> Result<()> {
655 let v = xattrs.data_as_bytes();
656 let v = v.try_as_aligned().unwrap();
657 let v = gvariant::gv!("a(ayay)").cast(v);
658 let mut pax_extensions = Vec::new();
659 for entry in v {
660 let (k, v) = entry.to_tuple();
661 let k = CStr::from_bytes_with_nul(k).unwrap();
662 let k = k
663 .to_str()
664 .with_context(|| format!("Found non-UTF8 xattr: {k:?}"))?;
665 if k == SECURITY_SELINUX_XATTR {
666 continue;
667 }
668 pax_extensions.push((format!("SCHILY.xattr.{k}"), v));
669 }
670 if !pax_extensions.is_empty() {
671 out.append_pax_extensions(pax_extensions.iter().map(|(k, v)| (k.as_str(), *v)))?;
672 }
673 Ok(())
674}
675
676fn impl_export<W: std::io::Write>(
681 repo: &ostree::Repo,
682 commit_checksum: &str,
683 out: &mut tar::Builder<W>,
684 options: ExportOptions,
685) -> Result<()> {
686 if options.raw {
687 return impl_raw_export(repo, commit_checksum, out);
688 }
689 let writer = &mut OstreeTarWriter::new(repo, commit_checksum, out, options)?;
690 writer.write_commit()?;
691 Ok(())
692}
693
694fn impl_raw_export<W: std::io::Write>(
697 repo: &ostree::Repo,
698 commit_checksum: &str,
699 out: &mut tar::Builder<W>,
700) -> Result<()> {
701 let cancellable = gio::Cancellable::NONE;
702 let (root, _) = repo.read_commit(commit_checksum, cancellable)?;
703 let root = root
704 .downcast::<ostree::RepoFile>()
705 .expect("read_commit returns RepoFile");
706 root.ensure_resolved()?;
707 raw_export_dir(repo, out, &root, Utf8Path::new(""))
708}
709
710fn raw_export_dir<W: std::io::Write>(
712 repo: &ostree::Repo,
713 out: &mut tar::Builder<W>,
714 dir: &ostree::RepoFile,
715 path: &Utf8Path,
716) -> Result<()> {
717 let cancellable = gio::Cancellable::NONE;
718 let queryattrs = "standard::name,standard::type";
719 let queryflags = gio::FileQueryInfoFlags::NOFOLLOW_SYMLINKS;
720 let e = dir.enumerate_children(queryattrs, queryflags, cancellable)?;
721
722 while let Some(info) = e.next_file(cancellable)? {
723 let name = info.name();
724 let name = name
725 .to_str()
726 .ok_or_else(|| anyhow!("Invalid UTF-8 filename: {:?}", name))?;
727 let child_path = path.join(name);
728
729 let output_path = map_path_v1(&child_path);
731
732 let child = dir.child(name);
734 let child = child
735 .downcast::<ostree::RepoFile>()
736 .expect("child of RepoFile is RepoFile");
737 child.ensure_resolved()?;
738
739 let file_type = info.file_type();
740 match file_type {
741 gio::FileType::Regular | gio::FileType::SymbolicLink => {
742 let checksum = child.checksum();
744 let (instream, meta, xattrs) = repo.load_file(&checksum, cancellable)?;
745
746 append_pax_xattrs(out, &xattrs)?;
748
749 let mut h = tar::Header::new_gnu();
750 h.set_uid(meta.attribute_uint32("unix::uid") as u64);
751 h.set_gid(meta.attribute_uint32("unix::gid") as u64);
752 h.set_mode(meta.attribute_uint32("unix::mode") & !libc::S_IFMT);
754
755 if let Some(instream) = instream {
756 h.set_entry_type(tar::EntryType::Regular);
758 h.set_size(meta.size() as u64);
759 let mut instream = BufReader::with_capacity(BUF_CAPACITY, instream.into_read());
760 out.append_data(&mut h, output_path, &mut instream)
761 .with_context(|| format!("Writing {child_path}"))?;
762 } else {
763 h.set_entry_type(tar::EntryType::Symlink);
765 h.set_size(0);
766
767 let target = meta
768 .symlink_target()
769 .ok_or_else(|| anyhow!("Missing symlink target for {child_path}"))?;
770 let target = target
771 .to_str()
772 .ok_or_else(|| anyhow!("Invalid UTF-8 symlink target: {target:?}"))?;
773
774 if symlink_is_denormal(target) {
776 h.set_link_name_literal(target)
777 .with_context(|| format!("Setting symlink target for {child_path}"))?;
778 out.append_data(&mut h, output_path, &mut std::io::empty())
779 .with_context(|| format!("Writing symlink {child_path}"))?;
780 } else {
781 out.append_link(&mut h, output_path, target)
782 .with_context(|| format!("Writing symlink {child_path}"))?;
783 }
784 }
785 }
786 gio::FileType::Directory => {
787 let dir_meta_checksum = child.tree_get_metadata_checksum().ok_or_else(|| {
789 anyhow!("Missing metadata checksum for directory {child_path}")
790 })?;
791 let meta_v = repo.load_variant(ostree::ObjectType::DirMeta, &dir_meta_checksum)?;
792 let metadata =
793 ostree::DirMetaParsed::from_variant(&meta_v).context("Parsing dirmeta")?;
794
795 let mut h = tar::Header::new_gnu();
796 h.set_entry_type(tar::EntryType::Directory);
797 h.set_uid(metadata.uid as u64);
798 h.set_gid(metadata.gid as u64);
799 h.set_mode(metadata.mode & !libc::S_IFMT);
800 h.set_size(0);
801 out.append_data(&mut h, output_path, std::io::empty())
802 .with_context(|| format!("Writing directory {child_path}"))?;
803
804 raw_export_dir(repo, out, &child, &child_path)?;
805 }
806 o => anyhow::bail!("Unsupported file type {o:?} for {child_path}"),
807 }
808 }
809 Ok(())
810}
811
812#[derive(Debug, PartialEq, Eq, Default)]
814pub struct ExportOptions {
815 pub raw: bool,
820}
821
822#[context("Exporting commit")]
824pub fn export_commit(
825 repo: &ostree::Repo,
826 rev: &str,
827 out: impl std::io::Write,
828 options: Option<ExportOptions>,
829) -> Result<()> {
830 let commit = repo.require_rev(rev)?;
831 let mut tar = tar::Builder::new(out);
832 let options = options.unwrap_or_default();
833 impl_export(repo, commit.as_str(), &mut tar, options)?;
834 tar.finish()?;
835 Ok(())
836}
837
838fn path_for_tar_v1(p: &Utf8Path) -> &Utf8Path {
840 debug_assert!(!p.starts_with("."));
841 map_path_v1(p.strip_prefix("/").unwrap_or(p))
842}
843
844fn write_chunk<W: std::io::Write>(
847 writer: &mut OstreeTarWriter<W>,
848 chunk: chunking::ChunkMapping,
849 create_parent_dirs: bool,
850) -> Result<()> {
851 let mut cache = std::collections::HashSet::new();
852 let root = writer
853 .repo
854 .read_commit(&writer.commit_checksum, gio::Cancellable::NONE)?
855 .0;
856 for (checksum, (_size, paths)) in chunk.into_iter() {
857 let (objpath, h) = writer.append_content(checksum.borrow())?;
858 for path in paths.iter() {
859 let path = path_for_tar_v1(path);
860 let h = h.clone();
861 if create_parent_dirs {
862 writer.write_parents_of(&path, &root, &mut cache)?;
863 }
864 writer.append_content_hardlink(&objpath, h, path)?;
865 }
866 }
867 Ok(())
868}
869
870pub(crate) fn export_chunk<W: std::io::Write>(
872 repo: &ostree::Repo,
873 commit: &str,
874 chunk: chunking::ChunkMapping,
875 out: &mut tar::Builder<W>,
876 create_parent_dirs: bool,
877) -> Result<()> {
878 #[allow(clippy::needless_update)]
880 let opts = ExportOptions::default();
881 let writer = &mut OstreeTarWriter::new(repo, commit, out, opts)?;
882 writer.write_repo_structure()?;
883 write_chunk(writer, chunk, create_parent_dirs)
884}
885
886#[context("Exporting final chunk")]
888pub(crate) fn export_final_chunk<W: std::io::Write>(
889 repo: &ostree::Repo,
890 commit_checksum: &str,
891 remainder: chunking::Chunk,
892 out: &mut tar::Builder<W>,
893 create_parent_dirs: bool,
894) -> Result<()> {
895 let options = ExportOptions::default();
896 let writer = &mut OstreeTarWriter::new(repo, commit_checksum, out, options)?;
897 writer.structure_only = true;
900 writer.write_commit()?;
901 writer.structure_only = false;
902 write_chunk(writer, remainder.content, create_parent_dirs)
903}
904
905#[allow(clippy::while_let_on_iterator)]
907#[context("Replacing detached metadata")]
908pub(crate) fn reinject_detached_metadata<C: IsA<gio::Cancellable>>(
909 src: &mut tar::Archive<impl std::io::Read>,
910 dest: &mut tar::Builder<impl std::io::Write>,
911 detached_buf: Option<&[u8]>,
912 cancellable: Option<&C>,
913) -> Result<()> {
914 let mut entries = src.entries()?;
915 let mut commit_ent = None;
916 while let Some(entry) = entries.next() {
919 if let Some(c) = cancellable {
920 c.set_error_if_cancelled()?;
921 }
922 let entry = entry?;
923 let header = entry.header();
924 let path = entry.path()?;
925 let path: &Utf8Path = (&*path).try_into()?;
926 if !(header.entry_type() == tar::EntryType::Regular && path.as_str().ends_with(".commit")) {
927 crate::tar::write::copy_entry(entry, dest, None)?;
928 } else {
929 commit_ent = Some(entry);
930 break;
931 }
932 }
933 let commit_ent = commit_ent.ok_or_else(|| anyhow!("Missing commit object"))?;
934 let commit_path = commit_ent.path()?;
935 let commit_path = Utf8Path::from_path(&commit_path)
936 .ok_or_else(|| anyhow!("Invalid non-utf8 path {:?}", commit_path))?;
937 let (checksum, objtype) = crate::tar::import::Importer::parse_metadata_entry(commit_path)?;
938 assert_eq!(objtype, ostree::ObjectType::Commit); crate::tar::write::copy_entry(commit_ent, dest, None)?;
940
941 if let Some(detached_buf) = detached_buf {
943 let detached_path = object_path(ostree::ObjectType::CommitMeta, &checksum);
944 tar_append_default_data(dest, &detached_path, detached_buf)?;
945 }
946
947 let next_ent = entries
949 .next()
950 .ok_or_else(|| anyhow!("Expected metadata object after commit"))??;
951 let next_ent_path = next_ent.path()?;
952 let next_ent_path: &Utf8Path = (&*next_ent_path).try_into()?;
953 let objtype = crate::tar::import::Importer::parse_metadata_entry(next_ent_path)?.1;
954 if objtype != ostree::ObjectType::CommitMeta {
955 crate::tar::write::copy_entry(next_ent, dest, None)?;
956 }
957
958 while let Some(entry) = entries.next() {
960 if let Some(c) = cancellable {
961 c.set_error_if_cancelled()?;
962 }
963 crate::tar::write::copy_entry(entry?, dest, None)?;
964 }
965
966 Ok(())
967}
968
969pub fn update_detached_metadata<D: std::io::Write, C: IsA<gio::Cancellable>>(
971 src: impl std::io::Read,
972 dest: D,
973 detached_buf: Option<&[u8]>,
974 cancellable: Option<&C>,
975) -> Result<D> {
976 let mut src = tar::Archive::new(src);
977 let mut dest = tar::Builder::new(dest);
978 reinject_detached_metadata(&mut src, &mut dest, detached_buf, cancellable)?;
979 dest.into_inner().map_err(Into::into)
980}
981
982#[cfg(test)]
983mod tests {
984 use super::*;
985
986 #[test]
987 fn test_path_equivalent() {
988 assert!(path_equivalent_for_tar("var/tmp", "./var/tmp"));
989 assert!(path_equivalent_for_tar("./var/tmp", "var/tmp"));
990 assert!(path_equivalent_for_tar("/var/tmp", "var/tmp"));
991 assert!(!path_equivalent_for_tar("var/tmp", "var"));
992 }
993
994 #[test]
995 fn test_map_path() {
996 assert_eq!(
997 map_path("/".into()).as_os_str(),
998 Utf8Path::new("/").as_os_str()
999 );
1000 assert_eq!(
1001 map_path("./usr/etc/blah".into()).as_os_str(),
1002 Utf8Path::new("./etc/blah").as_os_str()
1003 );
1004 for unchanged in ["boot", "usr/bin", "usr/lib/foo"].iter().map(Utf8Path::new) {
1005 assert_eq!(unchanged.as_os_str(), map_path_v1(unchanged).as_os_str());
1006 }
1007
1008 assert_eq!(
1009 Utf8Path::new("etc").as_os_str(),
1010 map_path_v1(Utf8Path::new("usr/etc")).as_os_str()
1011 );
1012 assert_eq!(
1013 Utf8Path::new("etc/foo").as_os_str(),
1014 map_path_v1(Utf8Path::new("usr/etc/foo")).as_os_str()
1015 );
1016 }
1017
1018 #[test]
1019 fn test_unmap_path() {
1020 assert_eq!(
1021 unmap_path("/".into()).as_os_str(),
1022 Utf8Path::new("/").as_os_str()
1023 );
1024 assert_eq!(
1025 unmap_path("/etc".into()).as_os_str(),
1026 Utf8Path::new("/etc").as_os_str()
1027 );
1028 assert_eq!(
1029 unmap_path("/usr/etc".into()).as_os_str(),
1030 Utf8Path::new("/usr/etc").as_os_str()
1031 );
1032 assert_eq!(
1033 unmap_path("usr/etc".into()).as_os_str(),
1034 Utf8Path::new("usr/etc").as_os_str()
1035 );
1036 assert_eq!(
1037 unmap_path("etc".into()).as_os_str(),
1038 Utf8Path::new("usr/etc").as_os_str()
1039 );
1040 assert_eq!(
1041 unmap_path("etc/blah".into()).as_os_str(),
1042 Utf8Path::new("usr/etc/blah").as_os_str()
1043 );
1044 }
1045
1046 #[test]
1047 fn test_denormal_symlink() {
1048 let normal = ["/", "/usr", "../usr/bin/blah"];
1049 let denormal = ["../../usr/sbin//chkconfig", "foo//bar/baz"];
1050 for path in normal {
1051 assert!(!symlink_is_denormal(path));
1052 }
1053 for path in denormal {
1054 assert!(symlink_is_denormal(path));
1055 }
1056 }
1057
1058 #[test]
1059 fn test_v1_xattrs_object_path() {
1060 let checksum = "b8627e3ef0f255a322d2bd9610cfaaacc8f122b7f8d17c0e7e3caafa160f9fc7";
1061 let expected = "sysroot/ostree/repo/objects/b8/627e3ef0f255a322d2bd9610cfaaacc8f122b7f8d17c0e7e3caafa160f9fc7.file-xattrs-link";
1062 let output = v1_xattrs_object_path(checksum);
1063 assert_eq!(&output, expected);
1064 }
1065}