1use std::{
10 cell::RefCell,
11 collections::{BTreeMap, HashMap},
12 ffi::{OsStr, OsString},
13 fmt,
14 io::{BufWriter, Write},
15 os::unix::ffi::OsStrExt,
16 path::{Path, PathBuf},
17 rc::Rc,
18};
19
20use anyhow::{ensure, Context, Result};
21use rustix::fs::FileType;
22
23use crate::{
24 dumpfile_parse::{Entry, Item},
25 fsverity::FsVerityHashValue,
26 tree::{Directory, FileSystem, Inode, Leaf, LeafContent, RegularFile, Stat},
27};
28
29fn write_empty(writer: &mut impl fmt::Write) -> fmt::Result {
30 writer.write_str("-")
31}
32
33fn write_escaped(writer: &mut impl fmt::Write, bytes: &[u8]) -> fmt::Result {
34 if bytes.is_empty() {
35 return write_empty(writer);
36 }
37
38 for c in bytes {
39 let c = *c;
40
41 if c < b'!' || c == b'=' || c == b'\\' || c > b'~' {
42 write!(writer, "\\x{c:02x}")?;
43 } else {
44 writer.write_char(c as char)?;
45 }
46 }
47
48 Ok(())
49}
50
51#[allow(clippy::too_many_arguments)]
52fn write_entry(
53 writer: &mut impl fmt::Write,
54 path: &Path,
55 stat: &Stat,
56 ifmt: FileType,
57 size: u64,
58 nlink: usize,
59 rdev: u64,
60 payload: impl AsRef<OsStr>,
61 content: &[u8],
62 digest: Option<&str>,
63) -> fmt::Result {
64 let mode = stat.st_mode | ifmt.as_raw_mode();
65 let uid = stat.st_uid;
66 let gid = stat.st_gid;
67 let mtim_sec = stat.st_mtim_sec;
68
69 write_escaped(writer, path.as_os_str().as_bytes())?;
70 write!(
71 writer,
72 " {size} {mode:o} {nlink} {uid} {gid} {rdev} {mtim_sec}.0 "
73 )?;
74 write_escaped(writer, payload.as_ref().as_bytes())?;
75 write!(writer, " ")?;
76 write_escaped(writer, content)?;
77 write!(writer, " ")?;
78 if let Some(id) = digest {
79 write!(writer, "{id}")?;
80 } else {
81 write_empty(writer)?;
82 }
83
84 for (key, value) in &*stat.xattrs.borrow() {
85 write!(writer, " ")?;
86 write_escaped(writer, key.as_bytes())?;
87 write!(writer, "=")?;
88 write_escaped(writer, value)?;
89 }
90
91 Ok(())
92}
93
94pub fn write_directory(
99 writer: &mut impl fmt::Write,
100 path: &Path,
101 stat: &Stat,
102 nlink: usize,
103) -> fmt::Result {
104 write_entry(
105 writer,
106 path,
107 stat,
108 FileType::Directory,
109 0,
110 nlink,
111 0,
112 "",
113 &[],
114 None,
115 )
116}
117
118pub fn write_leaf(
123 writer: &mut impl fmt::Write,
124 path: &Path,
125 stat: &Stat,
126 content: &LeafContent<impl FsVerityHashValue>,
127 nlink: usize,
128) -> fmt::Result {
129 match content {
130 LeafContent::Regular(RegularFile::Inline(ref data)) => write_entry(
131 writer,
132 path,
133 stat,
134 FileType::RegularFile,
135 data.len() as u64,
136 nlink,
137 0,
138 "",
139 data,
140 None,
141 ),
142 LeafContent::Regular(RegularFile::External(id, size)) => write_entry(
143 writer,
144 path,
145 stat,
146 FileType::RegularFile,
147 *size,
148 nlink,
149 0,
150 id.to_object_pathname(),
151 &[],
152 Some(&id.to_hex()),
153 ),
154 LeafContent::BlockDevice(rdev) => write_entry(
155 writer,
156 path,
157 stat,
158 FileType::BlockDevice,
159 0,
160 nlink,
161 *rdev,
162 "",
163 &[],
164 None,
165 ),
166 LeafContent::CharacterDevice(rdev) => write_entry(
167 writer,
168 path,
169 stat,
170 FileType::CharacterDevice,
171 0,
172 nlink,
173 *rdev,
174 "",
175 &[],
176 None,
177 ),
178 LeafContent::Fifo => write_entry(
179 writer,
180 path,
181 stat,
182 FileType::Fifo,
183 0,
184 nlink,
185 0,
186 "",
187 &[],
188 None,
189 ),
190 LeafContent::Socket => write_entry(
191 writer,
192 path,
193 stat,
194 FileType::Socket,
195 0,
196 nlink,
197 0,
198 "",
199 &[],
200 None,
201 ),
202 LeafContent::Symlink(ref target) => write_entry(
203 writer,
204 path,
205 stat,
206 FileType::Symlink,
207 target.as_bytes().len() as u64,
208 nlink,
209 0,
210 target,
211 &[],
212 None,
213 ),
214 }
215}
216
217pub fn write_hardlink(writer: &mut impl fmt::Write, path: &Path, target: &OsStr) -> fmt::Result {
222 write_escaped(writer, path.as_os_str().as_bytes())?;
223 write!(writer, " 0 @120000 - - - - 0.0 ")?;
224 write_escaped(writer, target.as_bytes())?;
225 write!(writer, " - -")?;
226 Ok(())
227}
228
229struct DumpfileWriter<'a, W: Write, ObjectID: FsVerityHashValue> {
230 hardlinks: HashMap<*const Leaf<ObjectID>, OsString>,
231 writer: &'a mut W,
232}
233
234fn writeln_fmt(writer: &mut impl Write, f: impl Fn(&mut String) -> fmt::Result) -> Result<()> {
235 let mut tmp = String::with_capacity(256);
236 f(&mut tmp)?;
237 Ok(writeln!(writer, "{tmp}")?)
238}
239
240impl<'a, W: Write, ObjectID: FsVerityHashValue> DumpfileWriter<'a, W, ObjectID> {
241 fn new(writer: &'a mut W) -> Self {
242 Self {
243 hardlinks: HashMap::new(),
244 writer,
245 }
246 }
247
248 fn write_dir(&mut self, path: &mut PathBuf, dir: &Directory<ObjectID>) -> Result<()> {
249 let nlink = dir.inodes().fold(2, |count, inode| {
252 count + {
253 match inode {
254 Inode::Directory(..) => 1,
255 _ => 0,
256 }
257 }
258 });
259
260 writeln_fmt(self.writer, |fmt| {
261 write_directory(fmt, path, &dir.stat, nlink)
262 })?;
263
264 for (name, inode) in dir.sorted_entries() {
265 path.push(name);
266
267 match inode {
268 Inode::Directory(ref dir) => {
269 self.write_dir(path, dir)?;
270 }
271 Inode::Leaf(ref leaf) => {
272 self.write_leaf(path, leaf)?;
273 }
274 }
275
276 path.pop();
277 }
278 Ok(())
279 }
280
281 fn write_leaf(&mut self, path: &Path, leaf: &Rc<Leaf<ObjectID>>) -> Result<()> {
282 let nlink = Rc::strong_count(leaf);
283
284 if nlink > 1 {
285 let ptr = Rc::as_ptr(leaf);
287 if let Some(target) = self.hardlinks.get(&ptr) {
288 return writeln_fmt(self.writer, |fmt| write_hardlink(fmt, path, target));
289 }
290
291 self.hardlinks.insert(ptr, OsString::from(&path));
293 }
294
295 writeln_fmt(self.writer, |fmt| {
296 write_leaf(fmt, path, &leaf.stat, &leaf.content, nlink)
297 })
298 }
299}
300
301pub fn write_dumpfile(
306 writer: &mut impl Write,
307 fs: &FileSystem<impl FsVerityHashValue>,
308) -> Result<()> {
309 let mut buffer = BufWriter::with_capacity(32768, writer);
312 let mut dfw = DumpfileWriter::new(&mut buffer);
313 let mut path = PathBuf::from("/");
314
315 dfw.write_dir(&mut path, &fs.root)?;
316 buffer.flush()?;
317
318 Ok(())
319}
320
321pub fn add_entry_to_filesystem<ObjectID: FsVerityHashValue>(
325 fs: &mut FileSystem<ObjectID>,
326 entry: Entry<'_>,
327 hardlinks: &mut HashMap<PathBuf, Rc<Leaf<ObjectID>>>,
328) -> Result<()> {
329 let path = entry.path.as_ref();
330
331 if path == Path::new("/") {
333 let stat = entry_to_stat(&entry);
334 fs.set_root_stat(stat);
335 return Ok(());
336 }
337
338 let parent = path.parent().unwrap_or_else(|| Path::new("/"));
340 let filename = path
341 .file_name()
342 .ok_or_else(|| anyhow::anyhow!("Path has no filename: {path:?}"))?;
343
344 let parent_dir = if parent == Path::new("/") {
346 &mut fs.root
347 } else {
348 fs.root
349 .get_directory_mut(parent.as_os_str())
350 .with_context(|| format!("Parent directory not found: {parent:?}"))?
351 };
352
353 let inode = match entry.item {
355 Item::Directory { .. } => {
356 let stat = entry_to_stat(&entry);
357 Inode::Directory(Box::new(Directory::new(stat)))
358 }
359 Item::Hardlink { ref target } => {
360 let target_leaf = hardlinks
362 .get(target.as_ref())
363 .ok_or_else(|| anyhow::anyhow!("Hardlink target not found: {target:?}"))?
364 .clone();
365 Inode::Leaf(target_leaf)
366 }
367 Item::RegularInline { ref content, .. } => {
368 let stat = entry_to_stat(&entry);
369 let data: Box<[u8]> = match content {
370 std::borrow::Cow::Borrowed(d) => Box::from(*d),
371 std::borrow::Cow::Owned(d) => d.clone().into_boxed_slice(),
372 };
373 let content = LeafContent::Regular(RegularFile::Inline(data));
374 Inode::Leaf(Rc::new(Leaf { stat, content }))
375 }
376 Item::Regular {
377 size,
378 ref fsverity_digest,
379 ..
380 } => {
381 let stat = entry_to_stat(&entry);
382 let digest = fsverity_digest
383 .as_ref()
384 .ok_or_else(|| anyhow::anyhow!("External file missing fsverity digest"))?;
385 let object_id = ObjectID::from_hex(digest)?;
386 let content = LeafContent::Regular(RegularFile::External(object_id, size));
387 Inode::Leaf(Rc::new(Leaf { stat, content }))
388 }
389 Item::Device { rdev, .. } => {
390 let stat = entry_to_stat(&entry);
391 let content = if entry.mode & 0o170000 == 0o60000 {
393 LeafContent::BlockDevice(rdev)
394 } else {
395 LeafContent::CharacterDevice(rdev)
396 };
397 Inode::Leaf(Rc::new(Leaf { stat, content }))
398 }
399 Item::Symlink { ref target, .. } => {
400 let stat = entry_to_stat(&entry);
401 let target_os: Box<OsStr> = match target {
402 std::borrow::Cow::Borrowed(t) => Box::from(t.as_os_str()),
403 std::borrow::Cow::Owned(t) => Box::from(t.as_os_str()),
404 };
405 let content = LeafContent::Symlink(target_os);
406 Inode::Leaf(Rc::new(Leaf { stat, content }))
407 }
408 Item::Fifo { .. } => {
409 let stat = entry_to_stat(&entry);
410 let content = LeafContent::Fifo;
411 Inode::Leaf(Rc::new(Leaf { stat, content }))
412 }
413 };
414
415 if let Inode::Leaf(ref leaf) = inode {
417 hardlinks.insert(path.to_path_buf(), leaf.clone());
418 }
419
420 parent_dir.insert(filename, inode);
421 Ok(())
422}
423
424fn entry_to_stat(entry: &Entry<'_>) -> Stat {
426 let mut xattrs = BTreeMap::new();
427 for xattr in &entry.xattrs {
428 let key: Box<OsStr> = match &xattr.key {
429 std::borrow::Cow::Borrowed(k) => Box::from(*k),
430 std::borrow::Cow::Owned(k) => Box::from(k.as_os_str()),
431 };
432 let value: Box<[u8]> = match &xattr.value {
433 std::borrow::Cow::Borrowed(v) => Box::from(*v),
434 std::borrow::Cow::Owned(v) => v.clone().into_boxed_slice(),
435 };
436 xattrs.insert(key, value);
437 }
438
439 Stat {
440 st_mode: entry.mode & 0o7777, st_uid: entry.uid,
442 st_gid: entry.gid,
443 st_mtim_sec: entry.mtime.sec as i64,
444 xattrs: RefCell::new(xattrs),
445 }
446}
447
448pub fn dumpfile_to_filesystem<ObjectID: FsVerityHashValue>(
453 dumpfile: &str,
454) -> Result<FileSystem<ObjectID>> {
455 let mut lines = dumpfile.lines().peekable();
456 let mut hardlinks = HashMap::new();
457
458 let root_stat = loop {
460 match lines.next() {
461 Some(line) if line.trim().is_empty() => continue,
462 Some(line) => {
463 let entry = Entry::parse(line)
464 .with_context(|| format!("Failed to parse dumpfile line: {line}"))?;
465 ensure!(
466 entry.path.as_ref() == Path::new("/"),
467 "Dumpfile must start with root directory entry, found: {:?}",
468 entry.path
469 );
470 break entry_to_stat(&entry);
471 }
472 None => anyhow::bail!("Dumpfile is empty, expected root directory entry"),
473 }
474 };
475
476 let mut fs = FileSystem::new(root_stat);
477
478 for line in lines {
480 if line.trim().is_empty() {
481 continue;
482 }
483 let entry =
484 Entry::parse(line).with_context(|| format!("Failed to parse dumpfile line: {line}"))?;
485 add_entry_to_filesystem(&mut fs, entry, &mut hardlinks)?;
486 }
487
488 Ok(fs)
489}
490
491#[cfg(test)]
492mod tests {
493 use super::*;
494 use crate::fsverity::Sha256HashValue;
495
496 const SIMPLE_DUMP: &str = r#"/ 4096 40755 2 0 0 0 1000.0 - - -
497/empty_file 0 100644 1 0 0 0 1000.0 - - -
498/small_file 5 100644 1 0 0 0 1000.0 - hello -
499/symlink 7 120777 1 0 0 0 1000.0 /target - -
500"#;
501
502 #[test]
503 fn test_simple_dumpfile_conversion() -> Result<()> {
504 let fs = dumpfile_to_filesystem::<Sha256HashValue>(SIMPLE_DUMP)?;
505
506 assert!(fs.root.lookup(OsStr::new("empty_file")).is_some());
508 assert!(fs.root.lookup(OsStr::new("small_file")).is_some());
509 assert!(fs.root.lookup(OsStr::new("symlink")).is_some());
510
511 let small_file = fs.root.get_file(OsStr::new("small_file"))?;
513 if let RegularFile::Inline(data) = small_file {
514 assert_eq!(&**data, b"hello");
515 } else {
516 panic!("Expected inline file");
517 }
518
519 Ok(())
520 }
521
522 #[test]
523 fn test_hardlinks() -> Result<()> {
524 let dumpfile = r#"/ 4096 40755 2 0 0 0 1000.0 - - -
525/original 11 100644 2 0 0 0 1000.0 - hello_world -
526/hardlink1 0 @120000 2 0 0 0 0.0 /original - -
527/dir1 4096 40755 2 0 0 0 1000.0 - - -
528/dir1/hardlink2 0 @120000 2 0 0 0 0.0 /original - -
529"#;
530
531 let fs = dumpfile_to_filesystem::<Sha256HashValue>(dumpfile)?;
532
533 let original = fs.root.lookup(OsStr::new("original")).unwrap();
535 let hardlink1 = fs.root.lookup(OsStr::new("hardlink1")).unwrap();
536
537 let dir1 = fs.root.get_directory(OsStr::new("dir1"))?;
539 let hardlink2 = dir1.lookup(OsStr::new("hardlink2")).unwrap();
540
541 let original_leaf = match original {
543 Inode::Leaf(ref l) => l,
544 _ => panic!("Expected Leaf inode"),
545 };
546 let hardlink1_leaf = match hardlink1 {
547 Inode::Leaf(ref l) => l,
548 _ => panic!("Expected Leaf inode"),
549 };
550 let hardlink2_leaf = match hardlink2 {
551 Inode::Leaf(ref l) => l,
552 _ => panic!("Expected Leaf inode"),
553 };
554
555 assert!(Rc::ptr_eq(original_leaf, hardlink1_leaf));
557 assert!(Rc::ptr_eq(original_leaf, hardlink2_leaf));
558
559 assert_eq!(Rc::strong_count(original_leaf), 3);
561
562 if let LeafContent::Regular(RegularFile::Inline(data)) = &original_leaf.content {
564 assert_eq!(&**data, b"hello_world");
565 } else {
566 panic!("Expected inline regular file");
567 }
568
569 Ok(())
570 }
571}