• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2024 The ChromiumOS Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #![cfg(target_os = "linux")]
6 
7 use std::collections::BTreeMap;
8 use std::collections::BTreeSet;
9 use std::fs;
10 use std::fs::create_dir;
11 use std::fs::read_link;
12 use std::fs::symlink_metadata;
13 use std::fs::File;
14 use std::fs::OpenOptions;
15 use std::io::BufWriter;
16 use std::io::Seek;
17 use std::io::SeekFrom;
18 use std::io::Write;
19 use std::os::unix::fs::symlink;
20 use std::path::Path;
21 use std::path::PathBuf;
22 use std::process::Command;
23 
24 use base::test_utils::call_test_with_sudo;
25 use base::MappedRegion;
26 use ext2::Builder;
27 use tempfile::tempdir;
28 use tempfile::tempdir_in;
29 use tempfile::TempDir;
30 use walkdir::WalkDir;
31 
32 const FSCK_PATH: &str = "/usr/sbin/e2fsck";
33 const DEBUGFS_PATH: &str = "/usr/sbin/debugfs";
34 
35 const BLOCK_SIZE: u32 = 4096;
36 
run_fsck(path: &PathBuf)37 fn run_fsck(path: &PathBuf) {
38     // Run fsck and scheck its exit code is 0.
39     // Passing 'y' to stop attempting interactive repair.
40     let output = Command::new(FSCK_PATH)
41         .arg("-fvy")
42         .arg(path)
43         .output()
44         .unwrap();
45     println!("status: {}", output.status);
46     println!("stdout: {}", String::from_utf8_lossy(&output.stdout));
47     println!("stderr: {}", String::from_utf8_lossy(&output.stderr));
48     assert!(output.status.success());
49 }
50 
run_debugfs_cmd(args: &[&str], disk: &PathBuf) -> String51 fn run_debugfs_cmd(args: &[&str], disk: &PathBuf) -> String {
52     let output = Command::new(DEBUGFS_PATH)
53         .arg("-R")
54         .args(args)
55         .arg(disk)
56         .output()
57         .unwrap();
58 
59     let stdout = String::from_utf8_lossy(&output.stdout);
60     let stderr = String::from_utf8_lossy(&output.stderr);
61     println!("status: {}", output.status);
62     println!("stdout: {stdout}");
63     println!("stderr: {stderr}");
64     assert!(output.status.success());
65 
66     stdout.trim_start().trim_end().to_string()
67 }
68 
mkfs(td: &TempDir, builder: Builder) -> PathBuf69 fn mkfs(td: &TempDir, builder: Builder) -> PathBuf {
70     let path = td.path().join("empty.ext2");
71     let mem = builder
72         .allocate_memory()
73         .unwrap()
74         .build_mmap_info()
75         .unwrap()
76         .do_mmap()
77         .unwrap();
78     // SAFETY: `mem` has a valid pointer and its size.
79     let buf = unsafe { std::slice::from_raw_parts(mem.as_ptr(), mem.size()) };
80     let mut file = OpenOptions::new()
81         .write(true)
82         .create(true)
83         .truncate(true)
84         .open(&path)
85         .unwrap();
86     file.write_all(buf).unwrap();
87 
88     run_fsck(&path);
89 
90     path
91 }
92 
93 #[test]
test_mkfs_empty()94 fn test_mkfs_empty() {
95     let td = tempdir().unwrap();
96     let disk = mkfs(
97         &td,
98         Builder {
99             blocks_per_group: 1024,
100             inodes_per_group: 1024,
101             ..Default::default()
102         },
103     );
104 
105     // Ensure the content of the generated disk image with `debugfs`.
106     // It contains the following entries:
107     // - `.`: the rootdir whose inode is 2 and rec_len is 12.
108     // - `..`: this is also the rootdir with same inode and the same rec_len.
109     // - `lost+found`: inode is 11 and rec_len is 4072 (= block_size - 2*12).
110     assert_eq!(
111         run_debugfs_cmd(&["ls"], &disk),
112         "2  (12) .    2  (12) ..    11  (4072) lost+found"
113     );
114 }
115 
116 #[test]
test_mkfs_empty_multi_block_groups()117 fn test_mkfs_empty_multi_block_groups() {
118     let td = tempdir().unwrap();
119     let blocks_per_group = 2048;
120     let num_groups = 2;
121     let disk = mkfs(
122         &td,
123         Builder {
124             blocks_per_group,
125             inodes_per_group: 4096,
126             size: 4096 * blocks_per_group * num_groups,
127             ..Default::default()
128         },
129     );
130     assert_eq!(
131         run_debugfs_cmd(&["ls"], &disk),
132         "2  (12) .    2  (12) ..    11  (4072) lost+found"
133     );
134 }
135 
collect_paths(dir: &Path, skip_lost_found: bool) -> BTreeSet<(String, PathBuf)>136 fn collect_paths(dir: &Path, skip_lost_found: bool) -> BTreeSet<(String, PathBuf)> {
137     WalkDir::new(dir)
138         .into_iter()
139         .filter_map(|entry| {
140             entry.ok().and_then(|e| {
141                 let name = e
142                     .path()
143                     .strip_prefix(dir)
144                     .unwrap()
145                     .to_string_lossy()
146                     .into_owned();
147                 let path = e.path().to_path_buf();
148                 if name.is_empty() {
149                     return None;
150                 }
151                 if skip_lost_found && name == "lost+found" {
152                     return None;
153                 }
154 
155                 Some((name, path))
156             })
157         })
158         .collect()
159 }
160 
assert_eq_dirs( td: &TempDir, dir: &Path, disk: &PathBuf, xattr_map: Option<BTreeMap<String, Vec<(&str, &str)>>>, )161 fn assert_eq_dirs(
162     td: &TempDir,
163     dir: &Path,
164     disk: &PathBuf,
165     // Check the correct xattr is set and any unexpected one isn't set.
166     // Pass None to skip this check for test cases where many files are created.
167     xattr_map: Option<BTreeMap<String, Vec<(&str, &str)>>>,
168 ) {
169     // dump the disk contents to `dump_dir`.
170     let dump_dir = td.path().join("dump");
171     std::fs::create_dir(&dump_dir).unwrap();
172     run_debugfs_cmd(
173         &[&format!(
174             "rdump / {}",
175             dump_dir.as_os_str().to_str().unwrap()
176         )],
177         disk,
178     );
179 
180     let paths1 = collect_paths(dir, true);
181     let paths2 = collect_paths(&dump_dir, true);
182     if paths1.len() != paths2.len() {
183         panic!(
184             "number of entries mismatch: {:?}={:?}, {:?}={:?}",
185             dir,
186             paths1.len(),
187             dump_dir,
188             paths2.len()
189         );
190     }
191 
192     for ((name1, path1), (name2, path2)) in paths1.iter().zip(paths2.iter()) {
193         assert_eq!(name1, name2);
194         let m1 = symlink_metadata(path1).unwrap();
195         let m2 = symlink_metadata(path2).unwrap();
196         assert_eq!(
197             m1.file_type(),
198             m2.file_type(),
199             "file type mismatch ({name1})"
200         );
201 
202         if m1.file_type().is_symlink() {
203             let dst1 = read_link(path1).unwrap();
204             let dst2 = read_link(path2).unwrap();
205             assert_eq!(
206                 dst1, dst2,
207                 "symlink mismatch ({name1}): {:?}->{:?} vs {:?}->{:?}",
208                 path1, dst1, path2, dst2
209             );
210         } else {
211             assert_eq!(m1.len(), m2.len(), "length mismatch ({name1})");
212         }
213 
214         assert_eq!(
215             m1.permissions(),
216             m2.permissions(),
217             "permissions mismatch ({name1})"
218         );
219 
220         if m1.file_type().is_file() {
221             // Check contents
222             let c1 = std::fs::read_to_string(path1).unwrap();
223             let c2 = std::fs::read_to_string(path2).unwrap();
224             assert_eq!(c1, c2, "content mismatch: ({name1})");
225         }
226 
227         // Check xattr
228         if let Some(mp) = &xattr_map {
229             match mp.get(name1) {
230                 Some(expected_xattrs) if !expected_xattrs.is_empty() => {
231                     for (key, value) in expected_xattrs {
232                         let s = run_debugfs_cmd(&[&format!("ea_get -V {name1} {key}",)], disk);
233                         assert_eq!(&s, value);
234                     }
235                 }
236                 // If no xattr is specified, any value must not be set.
237                 _ => {
238                     let s = run_debugfs_cmd(&[&format!("ea_list {}", name1,)], disk);
239                     assert_eq!(s, "");
240                 }
241             }
242         }
243     }
244 }
245 
246 #[test]
test_simple_dir()247 fn test_simple_dir() {
248     // testdata
249     // ├── a.txt
250     // ├── b.txt
251     // └── dir
252     //     └── c.txt
253     let td = tempdir().unwrap();
254     let dir = td.path().join("testdata");
255     create_dir(&dir).unwrap();
256     File::create(dir.join("a.txt")).unwrap();
257     File::create(dir.join("b.txt")).unwrap();
258     create_dir(dir.join("dir")).unwrap();
259     File::create(dir.join("dir/c.txt")).unwrap();
260     let disk = mkfs(
261         &td,
262         Builder {
263             blocks_per_group: 2048,
264             inodes_per_group: 4096,
265             root_dir: Some(dir.clone()),
266             ..Default::default()
267         },
268     );
269 
270     assert_eq_dirs(&td, &dir, &disk, Some(Default::default()));
271 
272     td.close().unwrap(); // make sure that tempdir is properly deleted.
273 }
274 
275 #[test]
test_nested_dirs()276 fn test_nested_dirs() {
277     // testdata
278     // └── dir1
279     //     ├── a.txt
280     //     └── dir2
281     //         ├── b.txt
282     //         └── dir3
283     let td = tempdir().unwrap();
284     let dir = td.path().join("testdata");
285     create_dir(&dir).unwrap();
286     let dir1 = &dir.join("dir1");
287     create_dir(dir1).unwrap();
288     File::create(dir1.join("a.txt")).unwrap();
289     let dir2 = dir1.join("dir2");
290     create_dir(&dir2).unwrap();
291     File::create(dir2.join("b.txt")).unwrap();
292     let dir3 = dir2.join("dir3");
293     create_dir(dir3).unwrap();
294     let disk = mkfs(
295         &td,
296         Builder {
297             blocks_per_group: 2048,
298             inodes_per_group: 4096,
299             root_dir: Some(dir.clone()),
300             ..Default::default()
301         },
302     );
303 
304     assert_eq_dirs(&td, &dir, &disk, Some(Default::default()));
305 }
306 
307 #[test]
test_file_contents()308 fn test_file_contents() {
309     // testdata
310     // ├── hello.txt (content: "Hello!\n")
311     // └── big.txt (content: 10KB of data, which doesn't fit in one block)
312     let td = tempdir().unwrap();
313     let dir = td.path().join("testdata");
314     create_dir(&dir).unwrap();
315     let mut hello = File::create(dir.join("hello.txt")).unwrap();
316     hello.write_all(b"Hello!\n").unwrap();
317     let mut big = BufWriter::new(File::create(dir.join("big.txt")).unwrap());
318     let data = b"123456789\n";
319     for _ in 0..1024 {
320         big.write_all(data).unwrap();
321     }
322 
323     let disk = mkfs(
324         &td,
325         Builder {
326             blocks_per_group: 2048,
327             inodes_per_group: 4096,
328             root_dir: Some(dir.clone()),
329             ..Default::default()
330         },
331     );
332 
333     assert_eq_dirs(&td, &dir, &disk, Some(Default::default()));
334 }
335 
336 #[test]
test_max_file_name()337 fn test_max_file_name() {
338     // testdata
339     // └── aa..aa (whose file name length is 255, which is the ext2/3/4's maximum file name length)
340     let td = tempdir().unwrap();
341     let dir = td.path().join("testdata");
342     create_dir(&dir).unwrap();
343     let long_name = "a".repeat(255);
344     File::create(dir.join(long_name)).unwrap();
345 
346     let disk = mkfs(
347         &td,
348         Builder {
349             blocks_per_group: 2048,
350             inodes_per_group: 4096,
351             root_dir: Some(dir.clone()),
352             ..Default::default()
353         },
354     );
355 
356     assert_eq_dirs(&td, &dir, &disk, Some(Default::default()));
357 }
358 
359 #[test]
test_mkfs_indirect_block()360 fn test_mkfs_indirect_block() {
361     // testdata
362     // ├── big.txt (80KiB), which requires indirect blocks
363     // └── huge.txt (8MiB), which requires doubly indirect blocks
364     let td = tempdir().unwrap();
365     let dir = td.path().join("testdata");
366     std::fs::create_dir(&dir).unwrap();
367     let mut big = std::fs::File::create(dir.join("big.txt")).unwrap();
368     big.seek(SeekFrom::Start(80 * 1024)).unwrap();
369     big.write_all(&[0]).unwrap();
370 
371     let mut huge = std::fs::File::create(dir.join("huge.txt")).unwrap();
372     huge.seek(SeekFrom::Start(8 * 1024 * 1024)).unwrap();
373     huge.write_all(&[0]).unwrap();
374 
375     let disk = mkfs(
376         &td,
377         Builder {
378             blocks_per_group: 4096,
379             inodes_per_group: 4096,
380             root_dir: Some(dir.clone()),
381             ..Default::default()
382         },
383     );
384 
385     assert_eq_dirs(&td, &dir, &disk, Some(Default::default()));
386 }
387 
388 #[test]
test_mkfs_symlink()389 fn test_mkfs_symlink() {
390     // testdata
391     // ├── a.txt
392     // ├── self -> ./self
393     // ├── symlink0 -> ./a.txt
394     // ├── symlink1 -> ./symlink0
395     // └── dir
396     //     └── upper-a -> ../a.txt
397     let td = tempdir().unwrap();
398     let dir = td.path().join("testdata");
399     create_dir(&dir).unwrap();
400 
401     let mut f = File::create(dir.join("a.txt")).unwrap();
402     f.write_all("Hello".as_bytes()).unwrap();
403 
404     symlink("./self", dir.join("self")).unwrap();
405 
406     symlink("./a.txt", dir.join("symlink0")).unwrap();
407     symlink("./symlink0", dir.join("symlink1")).unwrap();
408 
409     create_dir(dir.join("dir")).unwrap();
410     symlink("../a.txt", dir.join("dir/upper-a")).unwrap();
411 
412     let disk = mkfs(
413         &td,
414         Builder {
415             blocks_per_group: 2048,
416             inodes_per_group: 4096,
417             root_dir: Some(dir.clone()),
418             ..Default::default()
419         },
420     );
421 
422     assert_eq_dirs(&td, &dir, &disk, Some(Default::default()));
423 }
424 
425 #[test]
test_mkfs_abs_symlink()426 fn test_mkfs_abs_symlink() {
427     // testdata
428     // ├── a.txt
429     // ├── a -> /testdata/a
430     // ├── self -> /testdata/self
431     // ├── tmp -> /tmp
432     // └── abc -> /a/b/c
433     let td = tempdir().unwrap();
434     let dir = td.path().join("testdata");
435 
436     std::fs::create_dir(&dir).unwrap();
437     File::create(dir.join("a.txt")).unwrap();
438     symlink(dir.join("a.txt"), dir.join("a")).unwrap();
439     symlink(dir.join("self"), dir.join("self")).unwrap();
440     symlink("/tmp/", dir.join("tmp")).unwrap();
441     symlink("/a/b/c", dir.join("abc")).unwrap();
442 
443     let disk = mkfs(
444         &td,
445         Builder {
446             blocks_per_group: 2048,
447             inodes_per_group: 4096,
448             root_dir: Some(dir.clone()),
449             ..Default::default()
450         },
451     );
452 
453     assert_eq_dirs(&td, &dir, &disk, Some(Default::default()));
454 }
455 
456 #[test]
test_mkfs_symlink_to_deleted()457 fn test_mkfs_symlink_to_deleted() {
458     // testdata
459     // ├── (deleted)
460     // └── symlink_to_deleted -> (deleted)
461     let td = tempdir().unwrap();
462     let dir = td.path().join("testdata");
463 
464     std::fs::create_dir(&dir).unwrap();
465     File::create(dir.join("deleted")).unwrap();
466     symlink("./deleted", dir.join("symlink_to_deleted")).unwrap();
467     fs::remove_file(dir.join("deleted")).unwrap();
468 
469     let disk = mkfs(
470         &td,
471         Builder {
472             blocks_per_group: 2048,
473             inodes_per_group: 4096,
474             root_dir: Some(dir.clone()),
475             ..Default::default()
476         },
477     );
478 
479     assert_eq_dirs(&td, &dir, &disk, Some(Default::default()));
480 }
481 
482 #[test]
test_mkfs_long_symlink()483 fn test_mkfs_long_symlink() {
484     // testdata
485     // ├── /(long name directory)/a.txt
486     // └── symlink -> /(long name directory)/a.txt
487     // ├── (60-byte filename)
488     // └── symlink60 -> (60-byte filename)
489 
490     let td = tempdir().unwrap();
491     let dir = td.path().join("testdata");
492 
493     create_dir(&dir).unwrap();
494 
495     const LONG_DIR_NAME: &str =
496         "this_is_a_very_long_directory_name_so_that_name_cannoot_fit_in_60_characters_in_inode";
497     assert!(LONG_DIR_NAME.len() > 60);
498 
499     let long_dir = dir.join(LONG_DIR_NAME);
500     create_dir(&long_dir).unwrap();
501     File::create(long_dir.join("a.txt")).unwrap();
502     symlink(long_dir.join("a.txt"), dir.join("symlink")).unwrap();
503 
504     const SIXTY_CHAR_DIR_NAME: &str =
505         "./this_is_just_60_byte_long_so_it_can_work_as_a_corner_case.";
506     assert_eq!(SIXTY_CHAR_DIR_NAME.len(), 60);
507     File::create(dir.join(SIXTY_CHAR_DIR_NAME)).unwrap();
508     symlink(SIXTY_CHAR_DIR_NAME, dir.join("symlink60")).unwrap();
509 
510     let disk = mkfs(
511         &td,
512         Builder {
513             blocks_per_group: 2048,
514             inodes_per_group: 4096,
515             root_dir: Some(dir.clone()),
516             ..Default::default()
517         },
518     );
519 
520     assert_eq_dirs(&td, &dir, &disk, Some(Default::default()));
521 }
522 
523 #[test]
test_ignore_lost_found()524 fn test_ignore_lost_found() {
525     // Ignore /lost+found/ directory in source to avoid conflict.
526     //
527     // testdata
528     // ├── lost+found (ignored and recreated as an empty dir)
529     // │   └── should_be_ignored.txt
530     // └── sub
531     //     └── lost+found (not ignored)
532     //         └── a.txt
533 
534     let td = tempdir().unwrap();
535     let dir = td.path().join("testdata");
536 
537     create_dir(&dir).unwrap();
538     create_dir(dir.join("lost+found")).unwrap();
539     File::create(dir.join("lost+found").join("should_be_ignored.txt")).unwrap();
540     create_dir(dir.join("sub")).unwrap();
541     create_dir(dir.join("sub").join("lost+found")).unwrap();
542     File::create(dir.join("sub").join("lost+found").join("a.txt")).unwrap();
543 
544     let disk = mkfs(
545         &td,
546         Builder {
547             blocks_per_group: 2048,
548             inodes_per_group: 4096,
549             root_dir: Some(dir.clone()),
550             ..Default::default()
551         },
552     );
553 
554     // dump the disk contents to `dump_dir`.
555     let dump_dir = td.path().join("dump");
556     std::fs::create_dir(&dump_dir).unwrap();
557     run_debugfs_cmd(
558         &[&format!(
559             "rdump / {}",
560             dump_dir.as_os_str().to_str().unwrap()
561         )],
562         &disk,
563     );
564 
565     let paths = collect_paths(&dump_dir, false /* skip_lost_found */)
566         .into_iter()
567         .map(|(path, _)| path)
568         .collect::<BTreeSet<_>>();
569     assert_eq!(
570         paths,
571         BTreeSet::from([
572             "lost+found".to_string(),
573             // 'lost+found/should_be_ignored.txt' must not in the result.
574             "sub".to_string(),
575             "sub/lost+found".to_string(),
576             "sub/lost+found/a.txt".to_string()
577         ])
578     );
579 }
580 
581 #[test]
test_multiple_block_directory_entry()582 fn test_multiple_block_directory_entry() {
583     // Creates a many files in a directory.
584     // So the sum of the sizes of directory entries exceeds 4KB and they need to be stored in
585     // multiple blocks.
586     //
587     // testdata
588     // ├─  0.txt
589     // ├─  1.txt
590     // ...
591     // └── 999.txt
592     let td = tempdir().unwrap();
593     let dir = td.path().join("testdata");
594 
595     std::fs::create_dir(&dir).unwrap();
596 
597     for i in 0..1000 {
598         let path = dir.join(format!("{i}.txt"));
599         File::create(&path).unwrap();
600     }
601 
602     let disk = mkfs(
603         &td,
604         Builder {
605             blocks_per_group: 2048,
606             inodes_per_group: 4096,
607             root_dir: Some(dir.clone()),
608             ..Default::default()
609         },
610     );
611 
612     assert_eq_dirs(&td, &dir, &disk, None); // skip xattr check
613 }
614 
615 // Test a case where the inode tables spans multiple block groups.
616 #[test]
test_multiple_bg_multi_inode_bitmap()617 fn test_multiple_bg_multi_inode_bitmap() {
618     // testdata
619     // ├─  0.txt
620     // ├─  1.txt
621     // ...
622     // └── 999.txt
623     let td = tempdir().unwrap();
624     let dir = td.path().join("testdata");
625 
626     std::fs::create_dir(&dir).unwrap();
627 
628     for i in 0..1000 {
629         let fname = format!("{i}.txt");
630         let path = dir.join(&fname);
631         let mut f = File::create(&path).unwrap();
632         // Write a file name to the file.
633         f.write_all(fname.as_bytes()).unwrap();
634     }
635 
636     let blocks_per_group = 1024;
637     // Set `inodes_per_group` to a smaller value than the number of files.
638     // So, the inode table in the 2nd block group will be used.
639     let inodes_per_group = 512;
640     let num_groups = 2;
641     let disk = mkfs(
642         &td,
643         Builder {
644             blocks_per_group,
645             inodes_per_group,
646             size: BLOCK_SIZE * blocks_per_group * num_groups,
647             root_dir: Some(dir.clone()),
648         },
649     );
650 
651     assert_eq_dirs(&td, &dir, &disk, None);
652 }
653 
654 /// Test a case where the block tables spans multiple block groups.
655 #[test]
test_multiple_bg_multi_block_bitmap()656 fn test_multiple_bg_multi_block_bitmap() {
657     // testdata
658     // ├─  0.txt
659     // ├─  1.txt
660     // ...
661     // └── 999.txt
662     let td = tempdir().unwrap();
663     let dir = td.path().join("testdata");
664 
665     std::fs::create_dir(&dir).unwrap();
666 
667     for i in 0..1000 {
668         let fname = format!("{i}.txt");
669         let path = dir.join(&fname);
670         let mut f = File::create(&path).unwrap();
671         // Write a file name to the file.
672         f.write_all(fname.as_bytes()).unwrap();
673     }
674 
675     // Set `blocks_per_group` to a smaller value than the number of files.
676     // So, the block table in the 2nd block group will be used.
677     let blocks_per_group = 512;
678     let inodes_per_group = 2048;
679     let num_groups = 4;
680     let disk = mkfs(
681         &td,
682         Builder {
683             blocks_per_group,
684             inodes_per_group,
685             size: BLOCK_SIZE * blocks_per_group * num_groups,
686             root_dir: Some(dir.clone()),
687         },
688     );
689 
690     assert_eq_dirs(&td, &dir, &disk, None);
691 }
692 
693 // Test a case where a file spans multiple block groups.
694 #[test]
test_multiple_bg_big_files()695 fn test_multiple_bg_big_files() {
696     // testdata
697     // ├─  0.txt (200 * 5000 bytes)
698     // ├─  1.txt (200 * 5000 bytes)
699     // ...
700     // └── 9.txt (200 * 5000 bytes)
701     let td = tempdir().unwrap();
702     let dir = td.path().join("testdata");
703 
704     std::fs::create_dir(&dir).unwrap();
705 
706     // Prepare a large data.
707     let data = vec!["0123456789"; 5000 * 20].concat();
708     for i in 0..10 {
709         let path = dir.join(format!("{i}.txt"));
710         let mut f = File::create(&path).unwrap();
711         f.write_all(data.as_bytes()).unwrap();
712     }
713 
714     // Set `blocks_per_group` to a value smaller than |size of a file| / 4K.
715     // So, each file spans multiple block groups.
716     let blocks_per_group = 128;
717     let num_groups = 50;
718     let disk = mkfs(
719         &td,
720         Builder {
721             blocks_per_group,
722             inodes_per_group: 1024,
723             size: BLOCK_SIZE * blocks_per_group * num_groups,
724             root_dir: Some(dir.clone()),
725         },
726     );
727 
728     assert_eq_dirs(&td, &dir, &disk, Some(Default::default()));
729 }
730 
731 #[test]
732 #[ignore = "Called by test_mkfs_xattr"]
test_mkfs_xattr_impl()733 fn test_mkfs_xattr_impl() {
734     // Since tmpfs doesn't support xattr, use the current directory.
735     let td = tempdir_in(".").unwrap();
736     let dir = td.path().join("testdata");
737     // testdata
738     // ├── a.txt ("user.foo"="a", "user.bar"="0123456789")
739     // ├── b.txt ("security.selinux"="unconfined_u:object_r:user_home_t:s0")
740     // ├── c.txt (no xattr)
741     // └── dir/ ("user.foo"="directory")
742     //     └─ d.txt ("user.foo"="in_directory")
743     std::fs::create_dir(&dir).unwrap();
744 
745     let dir_xattrs = vec![("dir".to_string(), vec![("user.foo", "directory")])];
746     let file_xattrs = vec![
747         (
748             "a.txt".to_string(),
749             vec![("user.foo", "a"), ("user.number", "0123456789")],
750         ),
751         (
752             "b.txt".to_string(),
753             vec![("security.selinux", "unconfined_u:object_r:user_home_t:s0")],
754         ),
755         ("c.txt".to_string(), vec![]),
756         ("dir/d.txt".to_string(), vec![("user.foo", "in_directory")]),
757     ];
758 
759     // Create dirs
760     for (fname, xattrs) in &dir_xattrs {
761         let f_path = dir.join(fname);
762         std::fs::create_dir(&f_path).unwrap();
763         for (key, value) in xattrs {
764             ext2::set_xattr(&f_path, key, value).unwrap();
765         }
766     }
767     // Create files
768     for (fname, xattrs) in &file_xattrs {
769         let f_path = dir.join(fname);
770         File::create(&f_path).unwrap();
771         for (key, value) in xattrs {
772             ext2::set_xattr(&f_path, key, value).unwrap();
773         }
774     }
775 
776     let xattr_map: BTreeMap<String, Vec<(&str, &str)>> =
777         file_xattrs.into_iter().chain(dir_xattrs).collect();
778 
779     let builder = Builder {
780         root_dir: Some(dir.clone()),
781         ..Default::default()
782     };
783     let disk = mkfs(&td, builder);
784 
785     assert_eq_dirs(&td, &dir, &disk, Some(xattr_map));
786 }
787 
788 #[test]
test_mkfs_xattr()789 fn test_mkfs_xattr() {
790     call_test_with_sudo("test_mkfs_xattr_impl")
791 }
792