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