1use std::collections::{BTreeMap, BTreeSet};
5use std::ffi::OsString;
6use std::fmt::Write as WriteFmt;
7use std::io::{BufRead, BufReader, Write as StdWrite};
8use std::iter::Peekable;
9use std::num::NonZeroUsize;
10use std::os::unix::ffi::{OsStrExt, OsStringExt};
11use std::path::{Path, PathBuf};
12
13use camino::Utf8PathBuf;
14use cap_std::fs::MetadataExt;
15use cap_std::fs::{Dir, Permissions, PermissionsExt};
16use cap_std_ext::cap_std;
17use cap_std_ext::dirext::CapStdExtDirExt;
18use rustix::fs::Mode;
19use rustix::path::Arg;
20use thiserror::Error;
21
22mod path_resolution;
23use path_resolution::{PathIdentity, PathResolver};
24
25const TMPFILESD: &str = "usr/lib/tmpfiles.d";
26const ETC_TMPFILESD: &str = "etc/tmpfiles.d";
27const BOOTC_GENERATED_PREFIX: &str = "bootc-autogenerated-var";
29
30#[derive(Debug, Default)]
32struct BootcTmpfilesGeneration(u32);
33
34impl BootcTmpfilesGeneration {
35 fn increment(&mut self) {
36 self.0 = self.0.checked_add(1).unwrap();
38 }
39
40 fn path(&self) -> Utf8PathBuf {
41 format!("{TMPFILESD}/{BOOTC_GENERATED_PREFIX}-{}.conf", self.0).into()
42 }
43}
44
45#[derive(Debug, Error)]
47#[allow(missing_docs)]
48pub enum Error {
49 #[error("I/O error: {0}")]
50 Io(#[from] std::io::Error),
51 #[error("I/O (fmt) error")]
52 Fmt(#[from] std::fmt::Error),
53 #[error("I/O error on {path}: {err}")]
54 PathIo { path: PathBuf, err: std::io::Error },
55 #[error("User not found for id {0}")]
56 UserNotFound(uzers::uid_t),
57 #[error("Group not found for id {0}")]
58 GroupNotFound(uzers::gid_t),
59 #[error("Invalid non-UTF8 username: {uid} {name}")]
60 NonUtf8User { uid: uzers::uid_t, name: String },
61 #[error("Invalid non-UTF8 groupname: {gid} {name}")]
62 NonUtf8Group { gid: uzers::gid_t, name: String },
63 #[error("Missing {TMPFILESD}")]
64 MissingTmpfilesDir {},
65 #[error("Found /var/run as a non-symlink")]
66 FoundVarRunNonSymlink {},
67 #[error("Malformed tmpfiles.d")]
68 MalformedTmpfilesPath,
69 #[error("Malformed tmpfiles.d line {0}")]
70 MalformedTmpfilesEntry(String),
71 #[error("Unsupported regular file for tmpfiles.d {0}")]
72 UnsupportedRegfile(PathBuf),
73 #[error("Unsupported file of type {ty:?} for tmpfiles.d {path}")]
74 UnsupportedFile {
75 ty: rustix::fs::FileType,
76 path: PathBuf,
77 },
78}
79
80pub type Result<T> = std::result::Result<T, Error>;
82
83fn escape_path<W: std::fmt::Write>(path: &Path, out: &mut W) -> std::fmt::Result {
84 let path_bytes = path.as_os_str().as_bytes();
85 if path_bytes.is_empty() {
86 return Err(std::fmt::Error);
87 }
88
89 if let Ok(s) = path.as_os_str().as_str() {
90 if s.chars().all(|c| c.is_ascii_alphanumeric() || c == '/') {
91 return write!(out, "{s}");
92 }
93 }
94
95 for c in path_bytes.iter().copied() {
96 let is_special = c == b'\\';
97 let is_printable = c.is_ascii_alphanumeric() || c.is_ascii_punctuation();
98 if is_printable && !is_special {
99 out.write_char(c as char)?;
100 } else {
101 match c {
102 b'\\' => out.write_str(r"\\")?,
103 b'\n' => out.write_str(r"\n")?,
104 b'\t' => out.write_str(r"\t")?,
105 b'\r' => out.write_str(r"\r")?,
106 o => write!(out, "\\x{o:02x}")?,
107 }
108 }
109 }
110 std::fmt::Result::Ok(())
111}
112
113fn impl_unescape_path_until<I>(
114 src: &mut Peekable<I>,
115 buf: &mut Vec<u8>,
116 end_of_record_is_quote: bool,
117) -> Result<()>
118where
119 I: Iterator<Item = u8>,
120{
121 let should_take_next = |c: &u8| {
122 let c = *c;
123 if end_of_record_is_quote {
124 c != b'"'
125 } else {
126 !c.is_ascii_whitespace()
127 }
128 };
129 while let Some(c) = src.next_if(should_take_next) {
130 if c != b'\\' {
131 buf.push(c);
132 continue;
133 };
134 let Some(c) = src.next() else {
135 return Err(Error::MalformedTmpfilesPath);
136 };
137 let c = match c {
138 b'\\' => b'\\',
139 b'n' => b'\n',
140 b'r' => b'\r',
141 b't' => b'\t',
142 b'x' => {
143 let mut s = String::new();
144 s.push(src.next().ok_or(Error::MalformedTmpfilesPath)?.into());
145 s.push(src.next().ok_or(Error::MalformedTmpfilesPath)?.into());
146
147 u8::from_str_radix(&s, 16).map_err(|_| Error::MalformedTmpfilesPath)?
148 }
149 _ => return Err(Error::MalformedTmpfilesPath),
150 };
151 buf.push(c);
152 }
153 Ok(())
154}
155
156fn unescape_path<I>(src: &mut Peekable<I>) -> Result<PathBuf>
157where
158 I: Iterator<Item = u8>,
159{
160 let mut r = Vec::new();
161 if src.next_if_eq(&b'"').is_some() {
162 impl_unescape_path_until(src, &mut r, true)?;
163 } else {
164 impl_unescape_path_until(src, &mut r, false)?;
165 };
166 let r = OsString::from_vec(r);
167 Ok(PathBuf::from(r))
168}
169
170fn canonicalize_escape_path<W: std::fmt::Write>(path: &Path, out: &mut W) -> std::fmt::Result {
178 if let Ok(rest) = path.strip_prefix("/var/run") {
179 let mut rewritten = PathBuf::from("/run");
180 if !rest.as_os_str().is_empty() {
181 rewritten.push(rest);
182 }
183 return escape_path(&rewritten, out);
184 }
185 escape_path(path, out)
186}
187
188enum FileMeta {
191 Directory(Mode),
192 Symlink(PathBuf),
193}
194
195impl FileMeta {
196 fn from_fs(dir: &Dir, path: &Path) -> Result<Option<Self>> {
197 let meta = dir.symlink_metadata(path)?;
198 let ftype = meta.file_type();
199 let r = if ftype.is_dir() {
200 FileMeta::Directory(Mode::from_raw_mode(meta.mode()))
201 } else if ftype.is_symlink() {
202 let target = dir.read_link_contents(path)?;
203 FileMeta::Symlink(target)
204 } else {
205 return Ok(None);
206 };
207 Ok(Some(r))
208 }
209}
210
211pub(crate) fn translate_to_tmpfiles_d(
213 abs_path: &Path,
214 meta: FileMeta,
215 username: &str,
216 groupname: &str,
217) -> Result<String> {
218 let mut bufwr = String::new();
219
220 let filetype_char = match &meta {
221 FileMeta::Directory(_) => 'd',
222 FileMeta::Symlink(_) => 'L',
223 };
224 write!(bufwr, "{filetype_char} ")?;
225 canonicalize_escape_path(abs_path, &mut bufwr)?;
226
227 match meta {
228 FileMeta::Directory(mode) => {
229 write!(bufwr, " {mode:04o} {username} {groupname} - -")?;
230 }
231 FileMeta::Symlink(target) => {
232 bufwr.push_str(" - - - - ");
233 canonicalize_escape_path(&target, &mut bufwr)?;
234 }
235 };
236
237 Ok(bufwr)
238}
239
240#[derive(Debug, Default)]
242pub struct TmpfilesWrittenResult {
243 pub generated: Option<(NonZeroUsize, Utf8PathBuf)>,
245 pub unsupported: usize,
247}
248
249pub fn var_to_tmpfiles<U: uzers::Users, G: uzers::Groups>(
251 rootfs: &Dir,
252 users: &U,
253 groups: &G,
254) -> Result<TmpfilesWrittenResult> {
255 let (existing_tmpfiles, generation) = read_tmpfiles(rootfs)?;
256
257 if let Some(meta) = rootfs.symlink_metadata_optional("var/run")? {
260 if !meta.is_symlink() {
261 return Err(Error::FoundVarRunNonSymlink {});
262 }
263 }
264
265 if !rootfs.try_exists(TMPFILESD)? {
267 return Err(Error::MissingTmpfilesDir {});
268 }
269
270 let mut entries = BTreeSet::new();
271 let mut prefix = PathBuf::from("/var");
272 let mut unsupported = Vec::new();
273 let var_identity = dir_dev_ino(rootfs, "var")?;
274 convert_path_to_tmpfiles_d_recurse(
275 &TmpfilesConvertConfig {
276 users,
277 groups,
278 rootfs,
279 existing: &existing_tmpfiles,
280 readonly: false,
281 },
282 &mut entries,
283 &mut unsupported,
284 &mut prefix,
285 var_identity,
286 )?;
287
288 let Some(entries_count) = NonZeroUsize::new(entries.len()) else {
290 return Ok(TmpfilesWrittenResult::default());
291 };
292
293 let path = generation.path();
294 assert!(!rootfs.try_exists(&path)?);
296
297 rootfs.atomic_replace_with(&path, |bufwr| -> Result<()> {
298 let mode = Permissions::from_mode(0o644);
299 bufwr.get_mut().as_file_mut().set_permissions(mode)?;
300
301 for line in entries.iter() {
302 bufwr.write_all(line.as_bytes())?;
303 writeln!(bufwr)?;
304 }
305 if !unsupported.is_empty() {
306 let (samples, rest) = bootc_utils::iterator_split(unsupported.iter(), 5);
307 for elt in samples {
308 writeln!(bufwr, "# bootc ignored: {elt:?}")?;
309 }
310 let rest = rest.count();
311 if rest > 0 {
312 writeln!(bufwr, "# bootc ignored: ...and {rest} more")?;
313 }
314 }
315 Ok(())
316 })?;
317
318 Ok(TmpfilesWrittenResult {
319 generated: Some((entries_count, path)),
320 unsupported: unsupported.len(),
321 })
322}
323
324fn dir_dev_ino(dir: &Dir, path: &str) -> Result<(u64, u64)> {
327 let meta = dir.metadata(path)?;
328 Ok((meta.dev(), meta.ino()))
329}
330
331struct TmpfilesConvertConfig<'a, U: uzers::Users, G: uzers::Groups> {
333 users: &'a U,
334 groups: &'a G,
335 rootfs: &'a Dir,
336 existing: &'a BTreeMap<PathIdentity, String>,
341 readonly: bool,
342}
343
344fn convert_path_to_tmpfiles_d_recurse<U: uzers::Users, G: uzers::Groups>(
351 config: &TmpfilesConvertConfig<'_, U, G>,
352 out_entries: &mut BTreeSet<String>,
353 out_unsupported: &mut Vec<PathBuf>,
354 prefix: &mut PathBuf,
355 dir_identity: (u64, u64),
356) -> Result<()> {
357 let relpath = prefix.strip_prefix("/").unwrap();
358 for subpath in config.rootfs.read_dir(relpath)? {
359 let subpath = subpath?;
360 let meta = subpath.metadata()?;
361 let fname = subpath.file_name();
362 prefix.push(&fname);
363
364 let key = PathIdentity::new(dir_identity.0, dir_identity.1, fname);
368 let has_tmpfiles_entry = config.existing.contains_key(&key);
369
370 if !has_tmpfiles_entry {
372 let entry = {
373 let relpath = prefix.strip_prefix("/").unwrap();
375 let Some(tmpfiles_meta) = FileMeta::from_fs(config.rootfs, &relpath)? else {
376 out_unsupported.push(relpath.into());
377 assert!(prefix.pop());
378 continue;
379 };
380 let uid = meta.uid();
381 let gid = meta.gid();
382 let user = config
383 .users
384 .get_user_by_uid(meta.uid())
385 .ok_or(Error::UserNotFound(uid))?;
386 let username = user.name();
387 let username: &str = username.to_str().ok_or_else(|| Error::NonUtf8User {
388 uid,
389 name: username.to_string_lossy().into_owned(),
390 })?;
391 let group = config
392 .groups
393 .get_group_by_gid(gid)
394 .ok_or(Error::GroupNotFound(gid))?;
395 let groupname = group.name();
396 let groupname: &str = groupname.to_str().ok_or_else(|| Error::NonUtf8Group {
397 gid,
398 name: groupname.to_string_lossy().into_owned(),
399 })?;
400 translate_to_tmpfiles_d(&prefix, tmpfiles_meta, &username, &groupname)?
401 };
402 out_entries.insert(entry);
403 }
404
405 if meta.is_dir() {
406 let relpath = prefix.strip_prefix("/").unwrap();
408 if let Some(subdir) = config.rootfs.open_dir_noxdev(relpath)? {
410 let sub_meta = subdir.dir_metadata()?;
414 convert_path_to_tmpfiles_d_recurse(
415 config,
416 out_entries,
417 out_unsupported,
418 prefix,
419 (sub_meta.dev(), sub_meta.ino()),
420 )?;
421 let relpath = prefix.strip_prefix("/").unwrap();
422 if !config.readonly {
423 config.rootfs.remove_dir_all(relpath)?;
424 }
425 }
426 } else {
427 let relpath = prefix.strip_prefix("/").unwrap();
429 if !config.readonly {
430 config.rootfs.remove_file(relpath)?;
431 }
432 }
433 assert!(prefix.pop());
434 }
435 Ok(())
436}
437
438#[allow(unsafe_code)]
440pub fn convert_var_to_tmpfiles_current_root() -> Result<TmpfilesWrittenResult> {
441 let rootfs = Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
442
443 let usergroups = unsafe { uzers::cache::UsersSnapshot::new() };
445
446 var_to_tmpfiles(&rootfs, &usergroups, &usergroups)
447}
448
449#[derive(Debug)]
451pub struct TmpfilesResult {
452 pub tmpfiles: BTreeSet<String>,
454 pub unsupported: Vec<PathBuf>,
456}
457
458#[allow(unsafe_code)]
460pub fn find_missing_tmpfiles_current_root() -> Result<TmpfilesResult> {
461 use uzers::cache::UsersSnapshot;
462
463 let rootfs = Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
464
465 let usergroups = unsafe { UsersSnapshot::new() };
467
468 let existing_tmpfiles = read_tmpfiles(&rootfs)?.0;
469
470 let mut prefix = PathBuf::from("/var");
471 let mut tmpfiles = BTreeSet::new();
472 let mut unsupported = Vec::new();
473 let var_identity = dir_dev_ino(&rootfs, "var")?;
474 convert_path_to_tmpfiles_d_recurse(
475 &TmpfilesConvertConfig {
476 users: &usergroups,
477 groups: &usergroups,
478 rootfs: &rootfs,
479 existing: &existing_tmpfiles,
480 readonly: true,
481 },
482 &mut tmpfiles,
483 &mut unsupported,
484 &mut prefix,
485 var_identity,
486 )?;
487 Ok(TmpfilesResult {
488 tmpfiles,
489 unsupported,
490 })
491}
492
493fn read_tmpfiles_from_dir(
497 rootfs: &Dir,
498 dir_path: &str,
499 resolver: &PathResolver,
500 generation: &mut BootcTmpfilesGeneration,
501) -> Result<BTreeMap<PathIdentity, String>> {
502 let Some(tmpfiles_dir) = rootfs.open_dir_optional(dir_path)? else {
503 return Ok(Default::default());
504 };
505 let mut result = BTreeMap::new();
506 for entry in tmpfiles_dir.entries()? {
507 let entry = entry?;
508 let name = entry.file_name();
509 let (Some(stem), Some(extension)) =
510 (Path::new(&name).file_stem(), Path::new(&name).extension())
511 else {
512 continue;
513 };
514 if extension != "conf" {
515 continue;
516 }
517 if let Ok(s) = stem.as_str() {
518 if s.starts_with(BOOTC_GENERATED_PREFIX) {
519 generation.increment();
520 }
521 }
522 let r = BufReader::new(entry.open()?);
523 for line in r.lines() {
524 let line = line?;
525 if line.is_empty() || line.starts_with("#") {
526 continue;
527 }
528 let path = tmpfiles_entry_get_path(&line)?;
529 let Some(identity) = resolver.resolve_parent_identity(&path)? else {
535 continue;
539 };
540 result.insert(identity, line);
541 }
542 }
543 Ok(result)
544}
545
546fn read_tmpfiles(
552 rootfs: &Dir,
553) -> Result<(BTreeMap<PathIdentity, String>, BootcTmpfilesGeneration)> {
554 let mut generation = BootcTmpfilesGeneration::default();
555 let resolver = PathResolver::new(rootfs)?;
556
557 let mut result = read_tmpfiles_from_dir(rootfs, TMPFILESD, &resolver, &mut generation)?;
559
560 let etc_result = read_tmpfiles_from_dir(rootfs, ETC_TMPFILESD, &resolver, &mut generation)?;
562 result.extend(etc_result);
564
565 Ok((result, generation))
566}
567
568fn tmpfiles_entry_get_path(line: &str) -> Result<PathBuf> {
569 let err = || Error::MalformedTmpfilesEntry(line.to_string());
570 let mut it = line.as_bytes().iter().copied().peekable();
571 while it.next_if(|c| c.is_ascii_whitespace()).is_some() {}
573 let mut found_ftype = false;
575 while it.next_if(|c| !c.is_ascii_whitespace()).is_some() {
576 found_ftype = true
577 }
578 if !found_ftype {
579 return Err(err());
580 }
581 while it.next_if(|c| c.is_ascii_whitespace()).is_some() {}
583 unescape_path(&mut it)
584}
585
586#[cfg(test)]
587mod tests {
588 use super::*;
589 use cap_std::fs::DirBuilder;
590 use cap_std_ext::cap_std::fs::DirBuilderExt as _;
591
592 #[test]
593 fn test_tmpfiles_entry_get_path() {
594 let cases = [
595 ("z /dev/kvm 0666 - kvm -", "/dev/kvm"),
596 ("d /run/lock/lvm 0700 root root -", "/run/lock/lvm"),
597 (
598 "a+ /var/lib/tpm2-tss/system/keystore - - - - default:group:tss:rwx",
599 "/var/lib/tpm2-tss/system/keystore",
600 ),
601 (
602 "d \"/run/file with spaces/foo\" 0700 root root -",
603 "/run/file with spaces/foo",
604 ),
605 (
606 r#"d /spaces\x20\x20here/foo 0700 root root -"#,
607 "/spaces here/foo",
608 ),
609 ];
610 for (input, expected) in cases {
611 let path = tmpfiles_entry_get_path(input).unwrap();
612 assert_eq!(path, Path::new(expected), "Input: {input}");
613 }
614 }
615
616 fn newroot() -> Result<cap_std_ext::cap_tempfile::TempDir> {
617 let root = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
618 root.create_dir_all(TMPFILESD)?;
619 Ok(root)
620 }
621
622 fn mock_userdb() -> uzers::mock::MockUsers {
623 let testuid = rustix::process::getuid();
624 let testgid = rustix::process::getgid();
625 let mut users = uzers::mock::MockUsers::with_current_uid(testuid.as_raw());
626 users.add_user(uzers::User::new(
627 testuid.as_raw(),
628 "testuser",
629 testgid.as_raw(),
630 ));
631 users.add_group(uzers::Group::new(testgid.as_raw(), "testgroup"));
632 users
633 }
634
635 #[test]
636 fn test_tmpfiles_d_translation() -> anyhow::Result<()> {
637 let rootfs = &newroot()?;
639 let userdb = &mock_userdb();
640
641 let mut db = DirBuilder::new();
642 db.recursive(true);
643 db.mode(0o755);
644
645 rootfs.write(
646 Path::new(TMPFILESD).join("systemd.conf"),
647 indoc::indoc! { r#"
648 d /var/lib 0755 - - -
649 d /var/lib/private 0700 root root -
650 d /var/log/private 0700 root root -
651 "#},
652 )?;
653
654 rootfs.create_dir_all(ETC_TMPFILESD)?;
656 rootfs.write(
657 Path::new(ETC_TMPFILESD).join("user.conf"),
658 "d /var/lib/user 0755 root root - -\n",
659 )?;
660
661 rootfs.ensure_dir_with("var/lib/systemd", &db)?;
663 rootfs.ensure_dir_with("var/lib/private", &db)?;
664 rootfs.ensure_dir_with("var/lib/nfs", &db)?;
665 rootfs.ensure_dir_with("var/lib/user", &db)?;
666 let global_rwx = Permissions::from_mode(0o777);
667 rootfs.ensure_dir_with("var/lib/test/nested", &db).unwrap();
668 rootfs.set_permissions("var/lib/test", global_rwx.clone())?;
669 rootfs.set_permissions("var/lib/test/nested", global_rwx)?;
670 rootfs.symlink("../", "var/lib/test/nested/symlink")?;
671 rootfs.symlink_contents("/var/lib/foo", "var/lib/test/absolute-symlink")?;
672
673 var_to_tmpfiles(rootfs, userdb, userdb).unwrap();
674
675 let mut tmp_gen = BootcTmpfilesGeneration(0);
677 let autovar_path = &tmp_gen.path();
678 assert!(rootfs.try_exists(autovar_path).unwrap());
679 let entries: Vec<String> = rootfs
680 .read_to_string(autovar_path)
681 .unwrap()
682 .lines()
683 .map(|s| s.to_owned())
684 .collect();
685 let expected = &[
686 "L /var/lib/test/absolute-symlink - - - - /var/lib/foo",
687 "L /var/lib/test/nested/symlink - - - - ../",
688 "d /var/lib/nfs 0755 testuser testgroup - -",
689 "d /var/lib/systemd 0755 testuser testgroup - -",
690 "d /var/lib/test 0777 testuser testgroup - -",
691 "d /var/lib/test/nested 0777 testuser testgroup - -",
692 ];
693 similar_asserts::assert_eq!(entries, expected);
694 assert!(!rootfs.try_exists("var/lib").unwrap());
695
696 rootfs.create_dir_all("var/lib/gen2-test")?;
699 let w = var_to_tmpfiles(rootfs, userdb, userdb).unwrap();
700 let wg = w.generated.as_ref().unwrap();
701 assert_eq!(wg.0, NonZeroUsize::new(1).unwrap());
702 assert_eq!(w.unsupported, 0);
703 tmp_gen.increment();
704 let autovar_path = &tmp_gen.path();
705 assert_eq!(autovar_path, &wg.1);
706 assert!(rootfs.try_exists(autovar_path).unwrap());
707 Ok(())
708 }
709
710 #[test]
717 fn test_tmpfiles_d_root_alias() -> anyhow::Result<()> {
718 let rootfs = &newroot()?;
720 let userdb = &mock_userdb();
721
722 rootfs.write(
723 Path::new(TMPFILESD).join("systemd.conf"),
724 indoc::indoc! { r#"
725 d /var/roothome 0700 root root -
726 d /root/.ssh 0700 root root -
727 "#},
728 )?;
729
730 rootfs.create_dir_all("var/roothome/.ssh")?;
731 rootfs.symlink("var/roothome", "root")?;
732
733 let w = var_to_tmpfiles(rootfs, userdb, userdb).unwrap();
734 assert_eq!(w.unsupported, 0);
735 assert!(w.generated.is_none());
736
737 Ok(())
738 }
739
740 #[test]
743 fn test_tmpfiles_d_home_alias() -> anyhow::Result<()> {
744 let rootfs = &newroot()?;
746 let userdb = &mock_userdb();
747
748 rootfs.write(
749 Path::new(TMPFILESD).join("systemd.conf"),
750 indoc::indoc! { r#"
751 d /var/home 0755 root root -
752 d /home/testuser 0700 testuser testuser -
753 "#},
754 )?;
755
756 rootfs.create_dir_all("var/home/testuser")?;
757 rootfs.symlink_contents("/var/home", "home")?;
758
759 let w = var_to_tmpfiles(rootfs, userdb, userdb).unwrap();
760 assert_eq!(w.unsupported, 0);
761 assert!(w.generated.is_none());
762
763 Ok(())
764 }
765
766 #[test]
771 fn test_tmpfiles_d_nested_symlink() -> anyhow::Result<()> {
772 let rootfs = &newroot()?;
774 let userdb = &mock_userdb();
775
776 rootfs.write(
777 Path::new(TMPFILESD).join("systemd.conf"),
778 indoc::indoc! { r#"
779 d /var/lib 0755 root root -
780 d /var/lib/machines 0755 root root -
781 L /var/lib/portables - - - - machines
782 d /var/lib/portables/myimage 0755 root root -
783 "#},
784 )?;
785
786 rootfs.create_dir_all("var/lib/machines/myimage")?;
787 rootfs.symlink("machines", "var/lib/portables")?;
788
789 let w = var_to_tmpfiles(rootfs, userdb, userdb).unwrap();
790 assert_eq!(w.unsupported, 0);
791 assert!(w.generated.is_none());
792
793 Ok(())
794 }
795
796 #[test]
798 fn test_log_regfile() -> anyhow::Result<()> {
799 let rootfs = &newroot()?;
801 let userdb = &mock_userdb();
802
803 rootfs.create_dir_all("var/log/dnf")?;
804 rootfs.write("var/log/dnf/dnf.log", b"some dnf log")?;
805 rootfs.create_dir_all("var/log/foo")?;
806 rootfs.write("var/log/foo/foo.log", b"some other log")?;
807
808 let tmp_gen = BootcTmpfilesGeneration(0);
809 var_to_tmpfiles(rootfs, userdb, userdb).unwrap();
810 let tmpfiles = rootfs.read_to_string(&tmp_gen.path()).unwrap();
811 let ignored = tmpfiles
812 .lines()
813 .filter(|line| line.starts_with("# bootc ignored"))
814 .count();
815 assert_eq!(ignored, 2);
816 Ok(())
817 }
818
819 #[test]
820 fn test_canonicalize_escape_path() {
821 let intact_cases = vec!["/", "/var", "/var/foo", "/run/foo"];
822 for entry in intact_cases {
823 let mut s = String::new();
824 canonicalize_escape_path(Path::new(entry), &mut s).unwrap();
825 similar_asserts::assert_eq!(&s, entry);
826 }
827
828 let quoting_cases = &[
830 ("/var/foo bar", r#"/var/foo\x20bar"#),
831 ("/var/run", "/run"),
832 ("/var/run/foo bar", r#"/run/foo\x20bar"#),
833 ];
834 for (input, expected) in quoting_cases {
835 let mut s = String::new();
836 canonicalize_escape_path(Path::new(input), &mut s).unwrap();
837 similar_asserts::assert_eq!(&s, expected);
838 }
839 }
840
841 #[test]
842 fn test_translate_to_tmpfiles_d() {
843 let path = Path::new(r#"/var/foo bar"#);
844 let username = "testuser";
845 let groupname = "testgroup";
846 {
847 let meta = FileMeta::Directory(Mode::from_raw_mode(0o721));
849 let out = translate_to_tmpfiles_d(path, meta, username, groupname).unwrap();
850 let expected = r#"d /var/foo\x20bar 0721 testuser testgroup - -"#;
851 similar_asserts::assert_eq!(out, expected);
852 }
853 {
854 let meta = FileMeta::Symlink("/mytarget".into());
856 let out = translate_to_tmpfiles_d(path, meta, username, groupname).unwrap();
857 let expected = r#"L /var/foo\x20bar - - - - /mytarget"#;
858 similar_asserts::assert_eq!(out, expected);
859 }
860 }
861}