1use std::borrow::Cow;
7use std::ffi::OsStr;
8use std::ffi::OsString;
9use std::fmt::Display;
10use std::fmt::Write as WriteFmt;
11use std::fs::File;
12use std::io::BufRead;
13use std::io::Write;
14use std::os::unix::ffi::{OsStrExt, OsStringExt};
15use std::path::{Path, PathBuf};
16use std::process::Command;
17use std::str::FromStr;
18
19use anyhow::Context;
20use anyhow::{anyhow, Result};
21use rustix::fs::FileType;
22
23const PATH_MAX: u32 = 4096;
25const MAX_INLINE_CONTENT: u16 = 5000;
27const XATTR_NAME_MAX: usize = 255;
31const XATTR_LIST_MAX: usize = u16::MAX as usize;
33const XATTR_SIZE_MAX: usize = u16::MAX as usize;
35
36#[derive(Debug, PartialEq, Eq)]
37pub struct Xattr<'k> {
39 pub key: Cow<'k, OsStr>,
41 pub value: Cow<'k, [u8]>,
43}
44pub type Xattrs<'k> = Vec<Xattr<'k>>;
46
47#[derive(Debug, PartialEq, Eq)]
49pub struct Mtime {
50 pub sec: u64,
52 pub nsec: u64,
54}
55
56#[derive(Debug, PartialEq, Eq)]
58pub struct Entry<'p> {
59 pub path: Cow<'p, Path>,
61 pub uid: u32,
63 pub gid: u32,
65 pub mode: u32,
67 pub mtime: Mtime,
69 pub item: Item<'p>,
71 pub xattrs: Xattrs<'p>,
73}
74
75#[derive(Debug, PartialEq, Eq)]
76pub enum Item<'p> {
81 RegularInline {
83 nlink: u32,
85 content: Cow<'p, [u8]>,
87 },
88 Regular {
90 size: u64,
92 nlink: u32,
94 path: Cow<'p, Path>,
96 fsverity_digest: Option<String>,
98 },
99 Device {
101 nlink: u32,
103 rdev: u64,
105 },
106 Symlink {
108 nlink: u32,
110 target: Cow<'p, Path>,
112 },
113 Hardlink {
115 target: Cow<'p, Path>,
117 },
118 Fifo {
120 nlink: u32,
122 },
123 Directory {
125 size: u64,
127 nlink: u32,
129 },
130}
131
132fn unescape_limited(s: &str, max: usize) -> Result<Cow<'_, [u8]>> {
135 if !s.contains('\\') && s.is_ascii() {
139 let len = s.len();
140 if len > max {
141 anyhow::bail!("Input {len} exceeded maximum length {max}");
142 }
143 return Ok(Cow::Borrowed(s.as_bytes()));
144 }
145 let mut it = s.chars();
146 let mut r = Vec::new();
147 while let Some(c) = it.next() {
148 if r.len() == max {
149 anyhow::bail!("Input exceeded maximum length {max}");
150 }
151 if c != '\\' {
152 write!(r, "{c}").unwrap();
153 continue;
154 }
155 let c = it.next().ok_or_else(|| anyhow!("Unterminated escape"))?;
156 let c = match c {
157 '\\' => b'\\',
158 'n' => b'\n',
159 'r' => b'\r',
160 't' => b'\t',
161 'x' => {
162 let mut s = String::new();
163 s.push(
164 it.next()
165 .ok_or_else(|| anyhow!("Unterminated hex escape"))?,
166 );
167 s.push(
168 it.next()
169 .ok_or_else(|| anyhow!("Unterminated hex escape"))?,
170 );
171
172 u8::from_str_radix(&s, 16).with_context(|| anyhow!("Invalid hex escape {s}"))?
173 }
174 o => anyhow::bail!("Invalid escape {o}"),
175 };
176 r.push(c);
177 }
178 Ok(r.into())
179}
180
181fn unescape(s: &str) -> Result<Cow<'_, [u8]>> {
183 unescape_limited(s, usize::MAX)
184}
185
186fn unescape_to_osstr(s: &str) -> Result<Cow<'_, OsStr>> {
189 let v = unescape(s)?;
190 if v.contains(&0u8) {
191 anyhow::bail!("Invalid embedded NUL");
192 }
193 let r = match v {
194 Cow::Borrowed(v) => Cow::Borrowed(OsStr::from_bytes(v)),
195 Cow::Owned(v) => Cow::Owned(OsString::from_vec(v)),
196 };
197 Ok(r)
198}
199
200fn unescape_to_path(s: &str) -> Result<Cow<'_, Path>> {
205 let v = unescape_to_osstr(s).and_then(|v| {
206 if v.is_empty() {
207 anyhow::bail!("Invalid empty path");
208 }
209 let l = v.len();
210 if l > PATH_MAX as usize {
211 anyhow::bail!("Path is too long: {l} bytes");
212 }
213 Ok(v)
214 })?;
215 let r = match v {
216 Cow::Borrowed(v) => Cow::Borrowed(Path::new(v)),
217 Cow::Owned(v) => Cow::Owned(PathBuf::from(v)),
218 };
219 Ok(r)
220}
221
222fn unescape_to_path_canonical(s: &str) -> Result<Cow<'_, Path>> {
228 let p = unescape_to_path(s)?;
229 let mut components = p.components();
230 let mut r = std::path::PathBuf::new();
231 let Some(first) = components.next() else {
232 anyhow::bail!("Invalid empty path");
233 };
234 if first != std::path::Component::RootDir {
235 anyhow::bail!("Invalid non-absolute path");
236 }
237 r.push(first);
238 for component in components {
239 match component {
240 std::path::Component::Prefix(_)
243 | std::path::Component::RootDir
244 | std::path::Component::CurDir => {
245 anyhow::bail!("Internal error in unescape_to_path_canonical");
246 }
247 std::path::Component::ParentDir => {
248 anyhow::bail!("Invalid \"..\" in path");
249 }
250 std::path::Component::Normal(_) => {
251 r.push(component);
252 }
253 }
254 }
255 if r.as_os_str().as_bytes() == p.as_os_str().as_bytes() {
259 Ok(p)
260 } else {
261 Ok(r.into())
263 }
264}
265
266#[derive(Debug, Clone, Copy, PartialEq, Eq)]
267enum EscapeMode {
268 Standard,
269 XattrKey,
270}
271
272fn escape<W: std::fmt::Write>(out: &mut W, s: &[u8], mode: EscapeMode) -> std::fmt::Result {
274 if s.is_empty() {
276 return out.write_char('-');
277 }
278 if s == b"-" {
280 return out.write_str(r"\x2d");
281 }
282 for c in s.iter().copied() {
283 let is_special = c == b'\\' || (matches!((mode, c), (EscapeMode::XattrKey, b'=')));
285 let is_printable = c.is_ascii_alphanumeric() || c.is_ascii_punctuation();
286 if is_printable && !is_special {
287 out.write_char(c as char)?;
288 } else {
289 match c {
290 b'\\' => out.write_str(r"\\")?,
291 b'\n' => out.write_str(r"\n")?,
292 b'\t' => out.write_str(r"\t")?,
293 b'\r' => out.write_str(r"\r")?,
294 o => write!(out, "\\x{o:02x}")?,
295 }
296 }
297 }
298 std::fmt::Result::Ok(())
299}
300
301fn optional_str(s: &str) -> Option<&str> {
303 match s {
304 "-" => None,
305 o => Some(o),
306 }
307}
308
309impl FromStr for Mtime {
310 type Err = anyhow::Error;
311
312 fn from_str(s: &str) -> Result<Self> {
313 let (sec, nsec) = s
314 .split_once('.')
315 .ok_or_else(|| anyhow!("Missing . in mtime"))?;
316 Ok(Self {
317 sec: u64::from_str(sec)?,
318 nsec: u64::from_str(nsec)?,
319 })
320 }
321}
322
323impl<'k> Xattr<'k> {
324 fn parse(s: &'k str) -> Result<Self> {
325 let (key, value) = s
326 .split_once('=')
327 .ok_or_else(|| anyhow!("Missing = in xattrs"))?;
328 let key = unescape_to_osstr(key)?;
329 let keylen = key.as_bytes().len();
330 if keylen > XATTR_NAME_MAX {
331 anyhow::bail!("xattr name too long; max={XATTR_NAME_MAX} found={keylen}");
332 }
333 let value = unescape(value)?;
334 let valuelen = value.len();
335 if valuelen > XATTR_SIZE_MAX {
336 anyhow::bail!("xattr value too long; max={XATTR_SIZE_MAX} found={keylen}");
337 }
338 Ok(Self { key, value })
339 }
340}
341
342impl<'p> Entry<'p> {
343 fn check_nonregfile(content: Option<&str>, fsverity_digest: Option<&str>) -> Result<()> {
344 if content.is_some() {
345 anyhow::bail!("entry cannot have content");
346 }
347 if fsverity_digest.is_some() {
348 anyhow::bail!("entry cannot have fsverity digest");
349 }
350 Ok(())
351 }
352
353 fn check_rdev(rdev: u64) -> Result<()> {
354 if rdev != 0 {
355 anyhow::bail!("entry cannot have device (rdev) {rdev}");
356 }
357 Ok(())
358 }
359
360 pub fn parse(s: &'p str) -> Result<Entry<'p>> {
362 let mut components = s.split(' ');
363 let mut next = |name: &str| components.next().ok_or_else(|| anyhow!("Missing {name}"));
364 let path = unescape_to_path_canonical(next("path")?)?;
365 let size = u64::from_str(next("size")?)?;
366 let modeval = next("mode")?;
367 let (is_hardlink, mode) = if let Some((_, rest)) = modeval.split_once('@') {
368 (true, u32::from_str_radix(rest, 8)?)
369 } else {
370 (false, u32::from_str_radix(modeval, 8)?)
371 };
372 let nlink = u32::from_str(next("nlink")?)?;
373 let uid = u32::from_str(next("uid")?)?;
374 let gid = u32::from_str(next("gid")?)?;
375 let rdev = u64::from_str(next("rdev")?)?;
376 let mtime = Mtime::from_str(next("mtime")?)?;
377 let payload = optional_str(next("payload")?);
378 let content = optional_str(next("content")?);
379 let fsverity_digest = optional_str(next("digest")?);
380 let xattrs = components
381 .try_fold((Vec::new(), 0usize), |(mut acc, total_namelen), line| {
382 let xattr = Xattr::parse(line)?;
383 let total_namelen = total_namelen.saturating_add(xattr.key.len());
385 if total_namelen > XATTR_LIST_MAX {
386 anyhow::bail!("Too many xattrs");
387 }
388 acc.push(xattr);
389 Ok((acc, total_namelen))
390 })?
391 .0;
392
393 let ty = FileType::from_raw_mode(mode);
394 let item = if is_hardlink {
395 if ty == FileType::Directory {
396 anyhow::bail!("Invalid hardlinked directory");
397 }
398 let target =
399 unescape_to_path_canonical(payload.ok_or_else(|| anyhow!("Missing payload"))?)?;
400 Item::Hardlink { target }
402 } else {
403 match ty {
404 FileType::RegularFile => {
405 Self::check_rdev(rdev)?;
406 if let Some(path) = payload.as_ref() {
407 let path = unescape_to_path(path)?;
408 Item::Regular {
409 size,
410 nlink,
411 path,
412 fsverity_digest: fsverity_digest.map(ToOwned::to_owned),
413 }
414 } else {
415 let content = content.unwrap_or_default();
417 let content = unescape_limited(content, MAX_INLINE_CONTENT.into())?;
418 if fsverity_digest.is_some() {
419 anyhow::bail!("Inline file cannot have fsverity digest");
420 }
421 Item::RegularInline { nlink, content }
422 }
423 }
424 FileType::Symlink => {
425 Self::check_nonregfile(content, fsverity_digest)?;
426 Self::check_rdev(rdev)?;
427
428 let target =
432 unescape_to_path(payload.ok_or_else(|| anyhow!("Missing payload"))?)?;
433 let targetlen = target.as_os_str().as_bytes().len();
434 if targetlen > PATH_MAX as usize {
435 anyhow::bail!("Target length too large {targetlen}");
436 }
437 Item::Symlink { nlink, target }
438 }
439 FileType::Fifo => {
440 Self::check_nonregfile(content, fsverity_digest)?;
441 Self::check_rdev(rdev)?;
442
443 Item::Fifo { nlink }
444 }
445 FileType::CharacterDevice | FileType::BlockDevice => {
446 Self::check_nonregfile(content, fsverity_digest)?;
447 Item::Device { nlink, rdev }
448 }
449 FileType::Directory => {
450 Self::check_nonregfile(content, fsverity_digest)?;
451 Self::check_rdev(rdev)?;
452
453 Item::Directory { size, nlink }
454 }
455 FileType::Socket => {
456 anyhow::bail!("sockets are not supported");
457 }
458 FileType::Unknown => {
459 anyhow::bail!("Unhandled file type from raw mode: {mode}")
460 }
461 }
462 };
463 Ok(Entry {
464 path,
465 uid,
466 gid,
467 mode,
468 mtime,
469 item,
470 xattrs,
471 })
472 }
473
474 pub fn filter_special(mut self) -> Self {
477 self.xattrs.retain(|v| {
478 !matches!(
479 (v.key.as_bytes(), &*v.value),
480 (b"trusted.overlay.opaque" | b"user.overlay.opaque", b"x")
481 )
482 });
483 self
484 }
485}
486
487impl Item<'_> {
488 pub(crate) fn size(&self) -> u64 {
489 match self {
490 Item::Regular { size, .. } | Item::Directory { size, .. } => *size,
491 Item::RegularInline { content, .. } => content.len() as u64,
492 _ => 0,
493 }
494 }
495
496 pub(crate) fn nlink(&self) -> u32 {
497 match self {
498 Item::RegularInline { nlink, .. } => *nlink,
499 Item::Regular { nlink, .. } => *nlink,
500 Item::Device { nlink, .. } => *nlink,
501 Item::Symlink { nlink, .. } => *nlink,
502 Item::Directory { nlink, .. } => *nlink,
503 Item::Fifo { nlink, .. } => *nlink,
504 _ => 0,
505 }
506 }
507
508 pub(crate) fn rdev(&self) -> u64 {
509 match self {
510 Item::Device { rdev, .. } => *rdev,
511 _ => 0,
512 }
513 }
514
515 pub(crate) fn payload(&self) -> Option<&Path> {
516 match self {
517 Item::Regular { path, .. } => Some(path),
518 Item::Symlink { target, .. } => Some(target),
519 Item::Hardlink { target } => Some(target),
520 _ => None,
521 }
522 }
523}
524
525impl Display for Mtime {
526 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
527 write!(f, "{}.{}", self.sec, self.nsec)
528 }
529}
530
531impl Display for Entry<'_> {
532 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
533 escape(f, self.path.as_os_str().as_bytes(), EscapeMode::Standard)?;
534 write!(
535 f,
536 " {} {:o} {} {} {} {} {} ",
537 self.item.size(),
538 self.mode,
539 self.item.nlink(),
540 self.uid,
541 self.gid,
542 self.item.rdev(),
543 self.mtime,
544 )?;
545 if let Some(payload) = self.item.payload() {
547 escape(f, payload.as_os_str().as_bytes(), EscapeMode::Standard)?;
548 f.write_char(' ')?;
549 } else {
550 write!(f, "- ")?;
551 }
552 match &self.item {
553 Item::RegularInline { content, .. } => {
554 escape(f, content, EscapeMode::Standard)?;
555 write!(f, " -")?;
556 }
557 Item::Regular {
558 fsverity_digest, ..
559 } => {
560 let fsverity_digest = fsverity_digest.as_deref().unwrap_or("-");
561 write!(f, "- {fsverity_digest}")?;
562 }
563 _ => {
564 write!(f, "- -")?;
565 }
566 }
567 for xattr in self.xattrs.iter() {
568 f.write_char(' ')?;
569 escape(f, xattr.key.as_bytes(), EscapeMode::XattrKey)?;
570 f.write_char('=')?;
571 escape(f, &xattr.value, EscapeMode::Standard)?;
572 }
573 std::fmt::Result::Ok(())
574 }
575}
576
577#[derive(Debug, Default)]
579pub struct DumpConfig<'a> {
580 pub filters: Option<&'a [&'a str]>,
582}
583
584pub fn dump<F>(input: File, config: DumpConfig, mut handler: F) -> Result<()>
586where
587 F: FnMut(Entry<'_>) -> Result<()> + Send,
588{
589 let mut proc = Command::new("composefs-info");
590 proc.arg("dump");
591 if let Some(filter) = config.filters {
592 proc.args(filter.iter().flat_map(|f| ["--filter", f]));
593 }
594 proc.args(["/dev/stdin"])
595 .stdin(std::process::Stdio::from(input))
596 .stderr(std::process::Stdio::piped())
597 .stdout(std::process::Stdio::piped());
598 let mut proc = proc.spawn().context("Spawning composefs-info")?;
599
600 let child_stdout = proc.stdout.take().unwrap();
602 let child_stderr = proc.stderr.take().unwrap();
603
604 std::thread::scope(|s| {
605 let stderr_copier = s.spawn(move || {
606 let mut child_stderr = std::io::BufReader::new(child_stderr);
607 let mut buf = Vec::new();
608 std::io::copy(&mut child_stderr, &mut buf)?;
609 anyhow::Ok(buf)
610 });
611
612 let child_stdout = std::io::BufReader::new(child_stdout);
613 for line in child_stdout.lines() {
614 let line = line.context("Reading dump stdout")?;
615 let entry = Entry::parse(&line)?.filter_special();
616 handler(entry)?;
617 }
618
619 let r = proc.wait()?;
620 let stderr = stderr_copier.join().unwrap()?;
621 if !r.success() {
622 let stderr = String::from_utf8_lossy(&stderr);
623 let stderr = stderr.trim();
624 anyhow::bail!("composefs-info dump failed: {r}: {stderr}")
625 }
626
627 Ok(())
628 })
629}
630
631#[cfg(test)]
632mod tests {
633 use std::{
634 fs::File,
635 io::{BufWriter, Seek},
636 process::Stdio,
637 };
638
639 use super::*;
640
641 const SPECIAL_DUMP: &str = include_str!("tests/assets/special.dump");
642 const SPECIALS: &[&str] = &["foo=bar=baz", r"\x01\x02", "-"];
643 const UNQUOTED: &[&str] = &["foo!bar", "hello-world", "--"];
644
645 fn mkcomposefs(dumpfile: &str, out: &mut File) -> Result<()> {
646 let mut tf = tempfile::tempfile().map(BufWriter::new)?;
647 tf.write_all(dumpfile.as_bytes())?;
648 let mut tf = tf.into_inner()?;
649 tf.seek(std::io::SeekFrom::Start(0))?;
650 let mut mkcomposefs = Command::new("mkcomposefs")
651 .args(["--from-file", "-", "-"])
652 .stdin(Stdio::from(tf))
653 .stdout(Stdio::from(out.try_clone()?))
654 .stderr(Stdio::inherit())
655 .spawn()?;
656
657 let st = mkcomposefs.wait()?;
658 if !st.success() {
659 anyhow::bail!("mkcomposefs failed: {st}");
660 };
661
662 Ok(())
663 }
664
665 #[test]
666 fn test_escape_specials() {
667 let cases = [("", "-"), ("-", r"\x2d")];
668 for (source, expected) in cases {
669 let mut buf = String::new();
670 escape(&mut buf, source.as_bytes(), EscapeMode::Standard).unwrap();
671 assert_eq!(&buf, expected);
672 }
673 }
674
675 #[test]
676 fn test_escape_roundtrip() {
677 let cases = SPECIALS.iter().chain(UNQUOTED);
678 for case in cases {
679 let mut buf = String::new();
680 escape(&mut buf, case.as_bytes(), EscapeMode::Standard).unwrap();
681 let case2 = unescape(&buf).unwrap();
682 assert_eq!(case, &String::from_utf8(case2.into()).unwrap());
683 }
684 }
685
686 #[test]
687 fn test_escape_unquoted() {
688 let cases = UNQUOTED;
689 for case in cases {
690 let mut buf = String::new();
691 escape(&mut buf, case.as_bytes(), EscapeMode::Standard).unwrap();
692 assert_eq!(case, &buf);
693 }
694 }
695
696 #[test]
697 fn test_escape_quoted() {
698 {
700 let mut buf = String::new();
701 escape(&mut buf, b"=", EscapeMode::Standard).unwrap();
702 assert_eq!(buf, "=");
703 }
704 let cases = &[("=", r"\x3d"), ("-", r"\x2d")];
706 for (src, expected) in cases {
707 let mut buf = String::new();
708 escape(&mut buf, src.as_bytes(), EscapeMode::XattrKey).unwrap();
709 assert_eq!(expected, &buf);
710 }
711 }
712
713 #[test]
714 fn test_unescape() {
715 assert_eq!(unescape("").unwrap().len(), 0);
716 assert_eq!(unescape_limited("", 0).unwrap().len(), 0);
717 assert!(unescape_limited("foobar", 3).is_err());
718 assert!(matches!(
720 unescape_limited("foobar", 6).unwrap(),
721 Cow::Borrowed(_)
722 ));
723 assert!(matches!(unescape_limited("→", 6).unwrap(), Cow::Owned(_)));
725 assert!(unescape_limited("foo→bar", 3).is_err());
726 }
727
728 #[test]
729 fn test_unescape_path() {
730 assert!(unescape_to_path("").is_err());
732 assert!(unescape_to_path("\0").is_err());
734 assert!(unescape_to_path("foo\0bar").is_err());
735 assert!(unescape_to_path("\0foobar").is_err());
736 assert!(unescape_to_path("foobar\0").is_err());
737 assert!(unescape_to_path("foo\\x00bar").is_err());
738 let mut p = "a".repeat(PATH_MAX.try_into().unwrap());
739 assert!(unescape_to_path(&p).is_ok());
740 p.push('a');
741 assert!(unescape_to_path(&p).is_err());
742 }
743
744 #[test]
745 fn test_unescape_path_canonical() {
746 assert!(unescape_to_path_canonical("").is_err());
748 assert!(unescape_to_path_canonical("foo").is_err());
749 assert!(unescape_to_path_canonical("../blah").is_err());
750 assert!(unescape_to_path_canonical("/foo/..").is_err());
751 assert!(unescape_to_path_canonical("/foo/../blah").is_err());
752 assert!(matches!(
754 unescape_to_path_canonical("/foo").unwrap(),
755 Cow::Borrowed(v) if v.to_str() == Some("/foo")
756 ));
757 assert!(matches!(
759 unescape_to_path_canonical(r#"/\x66oo"#).unwrap(),
760 Cow::Owned(v) if v.to_str() == Some("/foo")
761 ));
762 assert_eq!(
764 unescape_to_path_canonical("///foo/bar//baz")
765 .unwrap()
766 .to_str()
767 .unwrap(),
768 "/foo/bar/baz"
769 );
770 assert_eq!(
771 unescape_to_path_canonical("/.").unwrap().to_str().unwrap(),
772 "/"
773 );
774 }
775
776 #[test]
777 fn test_xattr() {
778 let v = Xattr::parse("foo=bar").unwrap();
779 similar_asserts::assert_eq!(v.key.as_bytes(), b"foo");
780 similar_asserts::assert_eq!(&*v.value, b"bar");
781 assert!(Xattr::parse("foo\0bar=baz").is_err());
783 assert!(Xattr::parse("foo\x00bar=baz").is_err());
784 let v = Xattr::parse("security.selinux=bar\x00").unwrap();
786 similar_asserts::assert_eq!(v.key.as_bytes(), b"security.selinux");
787 similar_asserts::assert_eq!(&*v.value, b"bar\0");
788 }
789
790 #[test]
791 fn long_xattrs() {
792 let mut s = String::from("/file 0 100755 1 0 0 0 0.0 00/26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae - -");
793 Entry::parse(&s).unwrap();
794 let xattrs_to_fill = XATTR_LIST_MAX / XATTR_NAME_MAX;
795 let xattr_name_remainder = XATTR_LIST_MAX % XATTR_NAME_MAX;
796 assert_eq!(xattr_name_remainder, 0);
797 let uniqueidlen = 8u8;
798 let xattr_prefix_len = XATTR_NAME_MAX.checked_sub(uniqueidlen.into()).unwrap();
799 let push_long_xattr = |s: &mut String, n| {
800 s.push(' ');
801 for _ in 0..xattr_prefix_len {
802 s.push('a');
803 }
804 write!(s, "{n:08x}=x").unwrap();
805 };
806 for i in 0..xattrs_to_fill {
807 push_long_xattr(&mut s, i);
808 }
809 Entry::parse(&s).unwrap();
810 push_long_xattr(&mut s, xattrs_to_fill);
811 assert!(Entry::parse(&s).is_err());
812 }
813
814 #[test]
815 fn test_parse() {
816 const CONTENT: &str = include_str!("tests/assets/special.dump");
817 for line in CONTENT.lines() {
818 let e = Entry::parse(line).unwrap();
820 let serialized = e.to_string();
821 if line != serialized {
822 dbg!(&line, &e, &serialized);
823 }
824 similar_asserts::assert_eq!(line, serialized);
825 let e2 = Entry::parse(&serialized).unwrap();
826 similar_asserts::assert_eq!(e, e2);
827 }
828 }
829
830 fn parse_all(name: &str, s: &str) -> Result<()> {
831 for line in s.lines() {
832 if line.is_empty() {
833 continue;
834 }
835 let _: Entry =
836 Entry::parse(line).with_context(|| format!("Test case={name:?} line={line:?}"))?;
837 }
838 Ok(())
839 }
840
841 #[test]
842 fn test_should_fail() {
843 const CASES: &[(&str, &str)] = &[
844 (
845 "content in fifo",
846 "/ 4096 40755 2 0 0 0 0.0 - - -\n/fifo 0 10777 1 0 0 0 0.0 - foobar -",
847 ),
848 ("root with rdev", "/ 4096 40755 2 0 0 42 0.0 - - -"),
849 ("root with fsverity", "/ 4096 40755 2 0 0 0 0.0 - - 35d02f81325122d77ec1d11baba655bc9bf8a891ab26119a41c50fa03ddfb408"),
850 ];
851 for (name, case) in CASES.iter().copied() {
852 assert!(
853 parse_all(name, case).is_err(),
854 "Expected case {name} to fail"
855 );
856 }
857 }
858
859 #[test_with::executable(mkcomposefs)]
860 #[test]
861 fn test_load_cfs() -> Result<()> {
862 let mut tmpf = tempfile::tempfile()?;
863 mkcomposefs(SPECIAL_DUMP, &mut tmpf).unwrap();
864 let mut entries = String::new();
865 tmpf.seek(std::io::SeekFrom::Start(0))?;
866 dump(tmpf, DumpConfig::default(), |e| {
867 writeln!(entries, "{e}")?;
868 Ok(())
869 })
870 .unwrap();
871 similar_asserts::assert_eq!(SPECIAL_DUMP, &entries);
872 Ok(())
873 }
874
875 #[test_with::executable(mkcomposefs)]
876 #[test]
877 fn test_load_cfs_filtered() -> Result<()> {
878 const FILTERED: &str =
879 "/ 4096 40555 2 0 0 0 1633950376.0 - - - trusted.foo1=bar-1 user.foo2=bar-2\n\
880/blockdev 0 60777 1 0 0 107690 1633950376.0 - - - trusted.bar=bar-2\n\
881/inline 15 100777 1 0 0 0 1633950376.0 - FOOBAR\\nINAFILE\\n - user.foo=bar-2\n";
882 let mut tmpf = tempfile::tempfile()?;
883 mkcomposefs(SPECIAL_DUMP, &mut tmpf).unwrap();
884 let mut entries = String::new();
885 tmpf.seek(std::io::SeekFrom::Start(0))?;
886 let filter = DumpConfig {
887 filters: Some(&["blockdev", "inline"]),
888 };
889 dump(tmpf, filter, |e| {
890 writeln!(entries, "{e}")?;
891 Ok(())
892 })
893 .unwrap();
894 assert_eq!(FILTERED, &entries);
895 Ok(())
896 }
897}