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!(
332 "xattr name too long; max={} found={}",
333 XATTR_NAME_MAX,
334 keylen
335 );
336 }
337 let value = unescape(value)?;
338 let valuelen = value.len();
339 if valuelen > XATTR_SIZE_MAX {
340 anyhow::bail!(
341 "xattr value too long; max={} found={}",
342 XATTR_SIZE_MAX,
343 keylen
344 );
345 }
346 Ok(Self { key, value })
347 }
348}
349
350impl<'p> Entry<'p> {
351 fn check_nonregfile(content: Option<&str>, fsverity_digest: Option<&str>) -> Result<()> {
352 if content.is_some() {
353 anyhow::bail!("entry cannot have content");
354 }
355 if fsverity_digest.is_some() {
356 anyhow::bail!("entry cannot have fsverity digest");
357 }
358 Ok(())
359 }
360
361 fn check_rdev(rdev: u64) -> Result<()> {
362 if rdev != 0 {
363 anyhow::bail!("entry cannot have device (rdev) {rdev}");
364 }
365 Ok(())
366 }
367
368 pub fn parse(s: &'p str) -> Result<Entry<'p>> {
370 let mut components = s.split(' ');
371 let mut next = |name: &str| components.next().ok_or_else(|| anyhow!("Missing {name}"));
372 let path = unescape_to_path_canonical(next("path")?)?;
373 let size = u64::from_str(next("size")?)?;
374 let modeval = next("mode")?;
375 let (is_hardlink, mode) = if let Some((_, rest)) = modeval.split_once('@') {
376 (true, u32::from_str_radix(rest, 8)?)
377 } else {
378 (false, u32::from_str_radix(modeval, 8)?)
379 };
380 let nlink = u32::from_str(next("nlink")?)?;
381 let uid = u32::from_str(next("uid")?)?;
382 let gid = u32::from_str(next("gid")?)?;
383 let rdev = u64::from_str(next("rdev")?)?;
384 let mtime = Mtime::from_str(next("mtime")?)?;
385 let payload = optional_str(next("payload")?);
386 let content = optional_str(next("content")?);
387 let fsverity_digest = optional_str(next("digest")?);
388 let xattrs = components
389 .try_fold((Vec::new(), 0usize), |(mut acc, total_namelen), line| {
390 let xattr = Xattr::parse(line)?;
391 let total_namelen = total_namelen.saturating_add(xattr.key.len());
393 if total_namelen > XATTR_LIST_MAX {
394 anyhow::bail!("Too many xattrs");
395 }
396 acc.push(xattr);
397 Ok((acc, total_namelen))
398 })?
399 .0;
400
401 let ty = FileType::from_raw_mode(mode);
402 let item = if is_hardlink {
403 if ty == FileType::Directory {
404 anyhow::bail!("Invalid hardlinked directory");
405 }
406 let target =
407 unescape_to_path_canonical(payload.ok_or_else(|| anyhow!("Missing payload"))?)?;
408 Item::Hardlink { target }
410 } else {
411 match ty {
412 FileType::RegularFile => {
413 Self::check_rdev(rdev)?;
414 if let Some(path) = payload.as_ref() {
415 let path = unescape_to_path(path)?;
416 Item::Regular {
417 size,
418 nlink,
419 path,
420 fsverity_digest: fsverity_digest.map(ToOwned::to_owned),
421 }
422 } else {
423 let content = content.unwrap_or_default();
425 let content = unescape_limited(content, MAX_INLINE_CONTENT.into())?;
426 if fsverity_digest.is_some() {
427 anyhow::bail!("Inline file cannot have fsverity digest");
428 }
429 Item::RegularInline { nlink, content }
430 }
431 }
432 FileType::Symlink => {
433 Self::check_nonregfile(content, fsverity_digest)?;
434 Self::check_rdev(rdev)?;
435
436 let target =
440 unescape_to_path(payload.ok_or_else(|| anyhow!("Missing payload"))?)?;
441 let targetlen = target.as_os_str().as_bytes().len();
442 if targetlen > PATH_MAX as usize {
443 anyhow::bail!("Target length too large {}", targetlen);
444 }
445 Item::Symlink { nlink, target }
446 }
447 FileType::Fifo => {
448 Self::check_nonregfile(content, fsverity_digest)?;
449 Self::check_rdev(rdev)?;
450
451 Item::Fifo { nlink }
452 }
453 FileType::CharacterDevice | FileType::BlockDevice => {
454 Self::check_nonregfile(content, fsverity_digest)?;
455 Item::Device { nlink, rdev }
456 }
457 FileType::Directory => {
458 Self::check_nonregfile(content, fsverity_digest)?;
459 Self::check_rdev(rdev)?;
460
461 Item::Directory { size, nlink }
462 }
463 FileType::Socket => {
464 anyhow::bail!("sockets are not supported");
465 }
466 FileType::Unknown => {
467 anyhow::bail!("Unhandled file type from raw mode: {mode}")
468 }
469 }
470 };
471 Ok(Entry {
472 path,
473 uid,
474 gid,
475 mode,
476 mtime,
477 item,
478 xattrs,
479 })
480 }
481
482 pub fn filter_special(mut self) -> Self {
485 self.xattrs.retain(|v| {
486 !matches!(
487 (v.key.as_bytes(), &*v.value),
488 (b"trusted.overlay.opaque" | b"user.overlay.opaque", b"x")
489 )
490 });
491 self
492 }
493}
494
495impl Item<'_> {
496 pub(crate) fn size(&self) -> u64 {
497 match self {
498 Item::Regular { size, .. } | Item::Directory { size, .. } => *size,
499 Item::RegularInline { content, .. } => content.len() as u64,
500 _ => 0,
501 }
502 }
503
504 pub(crate) fn nlink(&self) -> u32 {
505 match self {
506 Item::RegularInline { nlink, .. } => *nlink,
507 Item::Regular { nlink, .. } => *nlink,
508 Item::Device { nlink, .. } => *nlink,
509 Item::Symlink { nlink, .. } => *nlink,
510 Item::Directory { nlink, .. } => *nlink,
511 Item::Fifo { nlink, .. } => *nlink,
512 _ => 0,
513 }
514 }
515
516 pub(crate) fn rdev(&self) -> u64 {
517 match self {
518 Item::Device { rdev, .. } => *rdev,
519 _ => 0,
520 }
521 }
522
523 pub(crate) fn payload(&self) -> Option<&Path> {
524 match self {
525 Item::Regular { path, .. } => Some(path),
526 Item::Symlink { target, .. } => Some(target),
527 Item::Hardlink { target } => Some(target),
528 _ => None,
529 }
530 }
531}
532
533impl Display for Mtime {
534 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
535 write!(f, "{}.{}", self.sec, self.nsec)
536 }
537}
538
539impl Display for Entry<'_> {
540 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
541 escape(f, self.path.as_os_str().as_bytes(), EscapeMode::Standard)?;
542 write!(
543 f,
544 " {} {:o} {} {} {} {} {} ",
545 self.item.size(),
546 self.mode,
547 self.item.nlink(),
548 self.uid,
549 self.gid,
550 self.item.rdev(),
551 self.mtime,
552 )?;
553 if let Some(payload) = self.item.payload() {
555 escape(f, payload.as_os_str().as_bytes(), EscapeMode::Standard)?;
556 f.write_char(' ')?;
557 } else {
558 write!(f, "- ")?;
559 }
560 match &self.item {
561 Item::RegularInline { content, .. } => {
562 escape(f, content, EscapeMode::Standard)?;
563 write!(f, " -")?;
564 }
565 Item::Regular {
566 fsverity_digest, ..
567 } => {
568 let fsverity_digest = fsverity_digest.as_deref().unwrap_or("-");
569 write!(f, "- {fsverity_digest}")?;
570 }
571 _ => {
572 write!(f, "- -")?;
573 }
574 }
575 for xattr in self.xattrs.iter() {
576 f.write_char(' ')?;
577 escape(f, xattr.key.as_bytes(), EscapeMode::XattrKey)?;
578 f.write_char('=')?;
579 escape(f, &xattr.value, EscapeMode::Standard)?;
580 }
581 std::fmt::Result::Ok(())
582 }
583}
584
585#[derive(Debug, Default)]
587pub struct DumpConfig<'a> {
588 pub filters: Option<&'a [&'a str]>,
590}
591
592pub fn dump<F>(input: File, config: DumpConfig, mut handler: F) -> Result<()>
594where
595 F: FnMut(Entry<'_>) -> Result<()> + Send,
596{
597 let mut proc = Command::new("composefs-info");
598 proc.arg("dump");
599 if let Some(filter) = config.filters {
600 proc.args(filter.iter().flat_map(|f| ["--filter", f]));
601 }
602 proc.args(["/dev/stdin"])
603 .stdin(std::process::Stdio::from(input))
604 .stderr(std::process::Stdio::piped())
605 .stdout(std::process::Stdio::piped());
606 let mut proc = proc.spawn().context("Spawning composefs-info")?;
607
608 let child_stdout = proc.stdout.take().unwrap();
610 let child_stderr = proc.stderr.take().unwrap();
611
612 std::thread::scope(|s| {
613 let stderr_copier = s.spawn(move || {
614 let mut child_stderr = std::io::BufReader::new(child_stderr);
615 let mut buf = Vec::new();
616 std::io::copy(&mut child_stderr, &mut buf)?;
617 anyhow::Ok(buf)
618 });
619
620 let child_stdout = std::io::BufReader::new(child_stdout);
621 for line in child_stdout.lines() {
622 let line = line.context("Reading dump stdout")?;
623 let entry = Entry::parse(&line)?.filter_special();
624 handler(entry)?;
625 }
626
627 let r = proc.wait()?;
628 let stderr = stderr_copier.join().unwrap()?;
629 if !r.success() {
630 let stderr = String::from_utf8_lossy(&stderr);
631 let stderr = stderr.trim();
632 anyhow::bail!("composefs-info dump failed: {r}: {stderr}")
633 }
634
635 Ok(())
636 })
637}
638
639#[cfg(test)]
640mod tests {
641 use std::{
642 fs::File,
643 io::{BufWriter, Seek},
644 process::Stdio,
645 };
646
647 use super::*;
648
649 const SPECIAL_DUMP: &str = include_str!("tests/assets/special.dump");
650 const SPECIALS: &[&str] = &["foo=bar=baz", r"\x01\x02", "-"];
651 const UNQUOTED: &[&str] = &["foo!bar", "hello-world", "--"];
652
653 fn mkcomposefs(dumpfile: &str, out: &mut File) -> Result<()> {
654 let mut tf = tempfile::tempfile().map(BufWriter::new)?;
655 tf.write_all(dumpfile.as_bytes())?;
656 let mut tf = tf.into_inner()?;
657 tf.seek(std::io::SeekFrom::Start(0))?;
658 let mut mkcomposefs = Command::new("mkcomposefs")
659 .args(["--from-file", "-", "-"])
660 .stdin(Stdio::from(tf))
661 .stdout(Stdio::from(out.try_clone()?))
662 .stderr(Stdio::inherit())
663 .spawn()?;
664
665 let st = mkcomposefs.wait()?;
666 if !st.success() {
667 anyhow::bail!("mkcomposefs failed: {st}");
668 };
669
670 Ok(())
671 }
672
673 #[test]
674 fn test_escape_specials() {
675 let cases = [("", "-"), ("-", r"\x2d")];
676 for (source, expected) in cases {
677 let mut buf = String::new();
678 escape(&mut buf, source.as_bytes(), EscapeMode::Standard).unwrap();
679 assert_eq!(&buf, expected);
680 }
681 }
682
683 #[test]
684 fn test_escape_roundtrip() {
685 let cases = SPECIALS.iter().chain(UNQUOTED);
686 for case in cases {
687 let mut buf = String::new();
688 escape(&mut buf, case.as_bytes(), EscapeMode::Standard).unwrap();
689 let case2 = unescape(&buf).unwrap();
690 assert_eq!(case, &String::from_utf8(case2.into()).unwrap());
691 }
692 }
693
694 #[test]
695 fn test_escape_unquoted() {
696 let cases = UNQUOTED;
697 for case in cases {
698 let mut buf = String::new();
699 escape(&mut buf, case.as_bytes(), EscapeMode::Standard).unwrap();
700 assert_eq!(case, &buf);
701 }
702 }
703
704 #[test]
705 fn test_escape_quoted() {
706 {
708 let mut buf = String::new();
709 escape(&mut buf, b"=", EscapeMode::Standard).unwrap();
710 assert_eq!(buf, "=");
711 }
712 let cases = &[("=", r"\x3d"), ("-", r"\x2d")];
714 for (src, expected) in cases {
715 let mut buf = String::new();
716 escape(&mut buf, src.as_bytes(), EscapeMode::XattrKey).unwrap();
717 assert_eq!(expected, &buf);
718 }
719 }
720
721 #[test]
722 fn test_unescape() {
723 assert_eq!(unescape("").unwrap().len(), 0);
724 assert_eq!(unescape_limited("", 0).unwrap().len(), 0);
725 assert!(unescape_limited("foobar", 3).is_err());
726 assert!(matches!(
728 unescape_limited("foobar", 6).unwrap(),
729 Cow::Borrowed(_)
730 ));
731 assert!(matches!(unescape_limited("→", 6).unwrap(), Cow::Owned(_)));
733 assert!(unescape_limited("foo→bar", 3).is_err());
734 }
735
736 #[test]
737 fn test_unescape_path() {
738 assert!(unescape_to_path("").is_err());
740 assert!(unescape_to_path("\0").is_err());
742 assert!(unescape_to_path("foo\0bar").is_err());
743 assert!(unescape_to_path("\0foobar").is_err());
744 assert!(unescape_to_path("foobar\0").is_err());
745 assert!(unescape_to_path("foo\\x00bar").is_err());
746 let mut p = "a".repeat(PATH_MAX.try_into().unwrap());
747 assert!(unescape_to_path(&p).is_ok());
748 p.push('a');
749 assert!(unescape_to_path(&p).is_err());
750 }
751
752 #[test]
753 fn test_unescape_path_canonical() {
754 assert!(unescape_to_path_canonical("").is_err());
756 assert!(unescape_to_path_canonical("foo").is_err());
757 assert!(unescape_to_path_canonical("../blah").is_err());
758 assert!(unescape_to_path_canonical("/foo/..").is_err());
759 assert!(unescape_to_path_canonical("/foo/../blah").is_err());
760 assert!(matches!(
762 unescape_to_path_canonical("/foo").unwrap(),
763 Cow::Borrowed(v) if v.to_str() == Some("/foo")
764 ));
765 assert!(matches!(
767 unescape_to_path_canonical(r#"/\x66oo"#).unwrap(),
768 Cow::Owned(v) if v.to_str() == Some("/foo")
769 ));
770 assert_eq!(
772 unescape_to_path_canonical("///foo/bar//baz")
773 .unwrap()
774 .to_str()
775 .unwrap(),
776 "/foo/bar/baz"
777 );
778 assert_eq!(
779 unescape_to_path_canonical("/.").unwrap().to_str().unwrap(),
780 "/"
781 );
782 }
783
784 #[test]
785 fn test_xattr() {
786 let v = Xattr::parse("foo=bar").unwrap();
787 similar_asserts::assert_eq!(v.key.as_bytes(), b"foo");
788 similar_asserts::assert_eq!(&*v.value, b"bar");
789 assert!(Xattr::parse("foo\0bar=baz").is_err());
791 assert!(Xattr::parse("foo\x00bar=baz").is_err());
792 let v = Xattr::parse("security.selinux=bar\x00").unwrap();
794 similar_asserts::assert_eq!(v.key.as_bytes(), b"security.selinux");
795 similar_asserts::assert_eq!(&*v.value, b"bar\0");
796 }
797
798 #[test]
799 fn long_xattrs() {
800 let mut s = String::from("/file 0 100755 1 0 0 0 0.0 00/26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae - -");
801 Entry::parse(&s).unwrap();
802 let xattrs_to_fill = XATTR_LIST_MAX / XATTR_NAME_MAX;
803 let xattr_name_remainder = XATTR_LIST_MAX % XATTR_NAME_MAX;
804 assert_eq!(xattr_name_remainder, 0);
805 let uniqueidlen = 8u8;
806 let xattr_prefix_len = XATTR_NAME_MAX.checked_sub(uniqueidlen.into()).unwrap();
807 let push_long_xattr = |s: &mut String, n| {
808 s.push(' ');
809 for _ in 0..xattr_prefix_len {
810 s.push('a');
811 }
812 write!(s, "{n:08x}=x").unwrap();
813 };
814 for i in 0..xattrs_to_fill {
815 push_long_xattr(&mut s, i);
816 }
817 Entry::parse(&s).unwrap();
818 push_long_xattr(&mut s, xattrs_to_fill);
819 assert!(Entry::parse(&s).is_err());
820 }
821
822 #[test]
823 fn test_parse() {
824 const CONTENT: &str = include_str!("tests/assets/special.dump");
825 for line in CONTENT.lines() {
826 let e = Entry::parse(line).unwrap();
828 let serialized = e.to_string();
829 if line != serialized {
830 dbg!(&line, &e, &serialized);
831 }
832 similar_asserts::assert_eq!(line, serialized);
833 let e2 = Entry::parse(&serialized).unwrap();
834 similar_asserts::assert_eq!(e, e2);
835 }
836 }
837
838 fn parse_all(name: &str, s: &str) -> Result<()> {
839 for line in s.lines() {
840 if line.is_empty() {
841 continue;
842 }
843 let _: Entry =
844 Entry::parse(line).with_context(|| format!("Test case={name:?} line={line:?}"))?;
845 }
846 Ok(())
847 }
848
849 #[test]
850 fn test_should_fail() {
851 const CASES: &[(&str, &str)] = &[
852 (
853 "content in fifo",
854 "/ 4096 40755 2 0 0 0 0.0 - - -\n/fifo 0 10777 1 0 0 0 0.0 - foobar -",
855 ),
856 ("root with rdev", "/ 4096 40755 2 0 0 42 0.0 - - -"),
857 ("root with fsverity", "/ 4096 40755 2 0 0 0 0.0 - - 35d02f81325122d77ec1d11baba655bc9bf8a891ab26119a41c50fa03ddfb408"),
858 ];
859 for (name, case) in CASES.iter().copied() {
860 assert!(
861 parse_all(name, case).is_err(),
862 "Expected case {name} to fail"
863 );
864 }
865 }
866
867 #[test_with::executable(mkcomposefs)]
868 #[test]
869 fn test_load_cfs() -> Result<()> {
870 let mut tmpf = tempfile::tempfile()?;
871 mkcomposefs(SPECIAL_DUMP, &mut tmpf).unwrap();
872 let mut entries = String::new();
873 tmpf.seek(std::io::SeekFrom::Start(0))?;
874 dump(tmpf, DumpConfig::default(), |e| {
875 writeln!(entries, "{e}")?;
876 Ok(())
877 })
878 .unwrap();
879 similar_asserts::assert_eq!(SPECIAL_DUMP, &entries);
880 Ok(())
881 }
882
883 #[test_with::executable(mkcomposefs)]
884 #[test]
885 fn test_load_cfs_filtered() -> Result<()> {
886 const FILTERED: &str =
887 "/ 4096 40555 2 0 0 0 1633950376.0 - - - trusted.foo1=bar-1 user.foo2=bar-2\n\
888/blockdev 0 60777 1 0 0 107690 1633950376.0 - - - trusted.bar=bar-2\n\
889/inline 15 100777 1 0 0 0 1633950376.0 - FOOBAR\\nINAFILE\\n - user.foo=bar-2\n";
890 let mut tmpf = tempfile::tempfile()?;
891 mkcomposefs(SPECIAL_DUMP, &mut tmpf).unwrap();
892 let mut entries = String::new();
893 tmpf.seek(std::io::SeekFrom::Start(0))?;
894 let filter = DumpConfig {
895 filters: Some(&["blockdev", "inline"]),
896 };
897 dump(tmpf, filter, |e| {
898 writeln!(entries, "{e}")?;
899 Ok(())
900 })
901 .unwrap();
902 assert_eq!(FILTERED, &entries);
903 Ok(())
904 }
905}