#![cfg(target_os = "linux")]
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::fs;
use std::fs::create_dir;
use std::fs::read_link;
use std::fs::symlink_metadata;
use std::fs::File;
use std::fs::OpenOptions;
use std::io::BufWriter;
use std::io::Seek;
use std::io::SeekFrom;
use std::io::Write;
use std::os::unix::fs::symlink;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use base::test_utils::call_test_with_sudo;
use base::MappedRegion;
use ext2::Builder;
use tempfile::tempdir;
use tempfile::tempdir_in;
use tempfile::TempDir;
use walkdir::WalkDir;
const FSCK_PATH: &str = "/usr/sbin/e2fsck";
const DEBUGFS_PATH: &str = "/usr/sbin/debugfs";
const BLOCK_SIZE: u32 = 4096;
fn run_fsck(path: &PathBuf) {
let output = Command::new(FSCK_PATH)
.arg("-fvy")
.arg(path)
.output()
.unwrap();
println!("status: {}", output.status);
println!("stdout: {}", String::from_utf8_lossy(&output.stdout));
println!("stderr: {}", String::from_utf8_lossy(&output.stderr));
assert!(output.status.success());
}
fn run_debugfs_cmd(args: &[&str], disk: &PathBuf) -> String {
let output = Command::new(DEBUGFS_PATH)
.arg("-R")
.args(args)
.arg(disk)
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
println!("status: {}", output.status);
println!("stdout: {stdout}");
println!("stderr: {stderr}");
assert!(output.status.success());
stdout.trim_start().trim_end().to_string()
}
fn mkfs(td: &TempDir, builder: Builder) -> PathBuf {
let path = td.path().join("empty.ext2");
let mem = builder
.allocate_memory()
.unwrap()
.build_mmap_info()
.unwrap()
.do_mmap()
.unwrap();
let buf = unsafe { std::slice::from_raw_parts(mem.as_ptr(), mem.size()) };
let mut file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&path)
.unwrap();
file.write_all(buf).unwrap();
run_fsck(&path);
path
}
#[test]
fn test_mkfs_empty() {
let td = tempdir().unwrap();
let disk = mkfs(
&td,
Builder {
blocks_per_group: 1024,
inodes_per_group: 1024,
..Default::default()
},
);
assert_eq!(
run_debugfs_cmd(&["ls"], &disk),
"2 (12) . 2 (12) .. 11 (4072) lost+found"
);
}
#[test]
fn test_mkfs_empty_multi_block_groups() {
let td = tempdir().unwrap();
let blocks_per_group = 2048;
let num_groups = 2;
let disk = mkfs(
&td,
Builder {
blocks_per_group,
inodes_per_group: 4096,
size: 4096 * blocks_per_group * num_groups,
..Default::default()
},
);
assert_eq!(
run_debugfs_cmd(&["ls"], &disk),
"2 (12) . 2 (12) .. 11 (4072) lost+found"
);
}
fn collect_paths(dir: &Path, skip_lost_found: bool) -> BTreeSet<(String, PathBuf)> {
WalkDir::new(dir)
.into_iter()
.filter_map(|entry| {
entry.ok().and_then(|e| {
let name = e
.path()
.strip_prefix(dir)
.unwrap()
.to_string_lossy()
.into_owned();
let path = e.path().to_path_buf();
if name.is_empty() {
return None;
}
if skip_lost_found && name == "lost+found" {
return None;
}
Some((name, path))
})
})
.collect()
}
fn assert_eq_dirs(
td: &TempDir,
dir: &Path,
disk: &PathBuf,
xattr_map: Option<BTreeMap<String, Vec<(&str, &str)>>>,
) {
let dump_dir = td.path().join("dump");
std::fs::create_dir(&dump_dir).unwrap();
run_debugfs_cmd(
&[&format!(
"rdump / {}",
dump_dir.as_os_str().to_str().unwrap()
)],
disk,
);
let paths1 = collect_paths(dir, true);
let paths2 = collect_paths(&dump_dir, true);
if paths1.len() != paths2.len() {
panic!(
"number of entries mismatch: {:?}={:?}, {:?}={:?}",
dir,
paths1.len(),
dump_dir,
paths2.len()
);
}
for ((name1, path1), (name2, path2)) in paths1.iter().zip(paths2.iter()) {
assert_eq!(name1, name2);
let m1 = symlink_metadata(path1).unwrap();
let m2 = symlink_metadata(path2).unwrap();
assert_eq!(
m1.file_type(),
m2.file_type(),
"file type mismatch ({name1})"
);
if m1.file_type().is_symlink() {
let dst1 = read_link(path1).unwrap();
let dst2 = read_link(path2).unwrap();
assert_eq!(
dst1, dst2,
"symlink mismatch ({name1}): {path1:?}->{dst1:?} vs {path2:?}->{dst2:?}"
);
} else {
assert_eq!(m1.len(), m2.len(), "length mismatch ({name1})");
}
assert_eq!(
m1.permissions(),
m2.permissions(),
"permissions mismatch ({name1})"
);
if m1.file_type().is_file() {
let c1 = std::fs::read_to_string(path1).unwrap();
let c2 = std::fs::read_to_string(path2).unwrap();
assert_eq!(c1, c2, "content mismatch: ({name1})");
}
if let Some(mp) = &xattr_map {
match mp.get(name1) {
Some(expected_xattrs) if !expected_xattrs.is_empty() => {
for (key, value) in expected_xattrs {
let s = run_debugfs_cmd(&[&format!("ea_get -V {name1} {key}",)], disk);
assert_eq!(&s, value);
}
}
_ => {
let s = run_debugfs_cmd(&[&format!("ea_list {name1}",)], disk);
assert_eq!(s, "");
}
}
}
}
}
#[test]
fn test_simple_dir() {
let td = tempdir().unwrap();
let dir = td.path().join("testdata");
create_dir(&dir).unwrap();
File::create(dir.join("a.txt")).unwrap();
File::create(dir.join("b.txt")).unwrap();
create_dir(dir.join("dir")).unwrap();
File::create(dir.join("dir/c.txt")).unwrap();
let disk = mkfs(
&td,
Builder {
blocks_per_group: 2048,
inodes_per_group: 4096,
root_dir: Some(dir.clone()),
..Default::default()
},
);
assert_eq_dirs(&td, &dir, &disk, Some(Default::default()));
td.close().unwrap();
}
#[test]
fn test_nested_dirs() {
let td = tempdir().unwrap();
let dir = td.path().join("testdata");
create_dir(&dir).unwrap();
let dir1 = &dir.join("dir1");
create_dir(dir1).unwrap();
File::create(dir1.join("a.txt")).unwrap();
let dir2 = dir1.join("dir2");
create_dir(&dir2).unwrap();
File::create(dir2.join("b.txt")).unwrap();
let dir3 = dir2.join("dir3");
create_dir(dir3).unwrap();
let disk = mkfs(
&td,
Builder {
blocks_per_group: 2048,
inodes_per_group: 4096,
root_dir: Some(dir.clone()),
..Default::default()
},
);
assert_eq_dirs(&td, &dir, &disk, Some(Default::default()));
}
#[test]
fn test_file_contents() {
let td = tempdir().unwrap();
let dir = td.path().join("testdata");
create_dir(&dir).unwrap();
let mut hello = File::create(dir.join("hello.txt")).unwrap();
hello.write_all(b"Hello!\n").unwrap();
let mut big = BufWriter::new(File::create(dir.join("big.txt")).unwrap());
let data = b"123456789\n";
for _ in 0..1024 {
big.write_all(data).unwrap();
}
let disk = mkfs(
&td,
Builder {
blocks_per_group: 2048,
inodes_per_group: 4096,
root_dir: Some(dir.clone()),
..Default::default()
},
);
assert_eq_dirs(&td, &dir, &disk, Some(Default::default()));
}
#[test]
fn test_max_file_name() {
let td = tempdir().unwrap();
let dir = td.path().join("testdata");
create_dir(&dir).unwrap();
let long_name = "a".repeat(255);
File::create(dir.join(long_name)).unwrap();
let disk = mkfs(
&td,
Builder {
blocks_per_group: 2048,
inodes_per_group: 4096,
root_dir: Some(dir.clone()),
..Default::default()
},
);
assert_eq_dirs(&td, &dir, &disk, Some(Default::default()));
}
#[test]
fn test_mkfs_indirect_block() {
let td = tempdir().unwrap();
let dir = td.path().join("testdata");
std::fs::create_dir(&dir).unwrap();
let mut big = std::fs::File::create(dir.join("big.txt")).unwrap();
big.seek(SeekFrom::Start(80 * 1024)).unwrap();
big.write_all(&[0]).unwrap();
let mut huge = std::fs::File::create(dir.join("huge.txt")).unwrap();
huge.seek(SeekFrom::Start(8 * 1024 * 1024)).unwrap();
huge.write_all(&[0]).unwrap();
let disk = mkfs(
&td,
Builder {
blocks_per_group: 4096,
inodes_per_group: 4096,
root_dir: Some(dir.clone()),
..Default::default()
},
);
assert_eq_dirs(&td, &dir, &disk, Some(Default::default()));
}
#[test]
fn test_mkfs_symlink() {
let td = tempdir().unwrap();
let dir = td.path().join("testdata");
create_dir(&dir).unwrap();
let mut f = File::create(dir.join("a.txt")).unwrap();
f.write_all("Hello".as_bytes()).unwrap();
symlink("./self", dir.join("self")).unwrap();
symlink("./a.txt", dir.join("symlink0")).unwrap();
symlink("./symlink0", dir.join("symlink1")).unwrap();
create_dir(dir.join("dir")).unwrap();
symlink("../a.txt", dir.join("dir/upper-a")).unwrap();
let disk = mkfs(
&td,
Builder {
blocks_per_group: 2048,
inodes_per_group: 4096,
root_dir: Some(dir.clone()),
..Default::default()
},
);
assert_eq_dirs(&td, &dir, &disk, Some(Default::default()));
}
#[test]
fn test_mkfs_abs_symlink() {
let td = tempdir().unwrap();
let dir = td.path().join("testdata");
std::fs::create_dir(&dir).unwrap();
File::create(dir.join("a.txt")).unwrap();
symlink(dir.join("a.txt"), dir.join("a")).unwrap();
symlink(dir.join("self"), dir.join("self")).unwrap();
symlink("/tmp/", dir.join("tmp")).unwrap();
symlink("/a/b/c", dir.join("abc")).unwrap();
let disk = mkfs(
&td,
Builder {
blocks_per_group: 2048,
inodes_per_group: 4096,
root_dir: Some(dir.clone()),
..Default::default()
},
);
assert_eq_dirs(&td, &dir, &disk, Some(Default::default()));
}
#[test]
fn test_mkfs_symlink_to_deleted() {
let td = tempdir().unwrap();
let dir = td.path().join("testdata");
std::fs::create_dir(&dir).unwrap();
File::create(dir.join("deleted")).unwrap();
symlink("./deleted", dir.join("symlink_to_deleted")).unwrap();
fs::remove_file(dir.join("deleted")).unwrap();
let disk = mkfs(
&td,
Builder {
blocks_per_group: 2048,
inodes_per_group: 4096,
root_dir: Some(dir.clone()),
..Default::default()
},
);
assert_eq_dirs(&td, &dir, &disk, Some(Default::default()));
}
#[test]
fn test_mkfs_long_symlink() {
let td = tempdir().unwrap();
let dir = td.path().join("testdata");
create_dir(&dir).unwrap();
const LONG_DIR_NAME: &str =
"this_is_a_very_long_directory_name_so_that_name_cannoot_fit_in_60_characters_in_inode";
assert!(LONG_DIR_NAME.len() > 60);
let long_dir = dir.join(LONG_DIR_NAME);
create_dir(&long_dir).unwrap();
File::create(long_dir.join("a.txt")).unwrap();
symlink(long_dir.join("a.txt"), dir.join("symlink")).unwrap();
const SIXTY_CHAR_DIR_NAME: &str =
"./this_is_just_60_byte_long_so_it_can_work_as_a_corner_case.";
assert_eq!(SIXTY_CHAR_DIR_NAME.len(), 60);
File::create(dir.join(SIXTY_CHAR_DIR_NAME)).unwrap();
symlink(SIXTY_CHAR_DIR_NAME, dir.join("symlink60")).unwrap();
let disk = mkfs(
&td,
Builder {
blocks_per_group: 2048,
inodes_per_group: 4096,
root_dir: Some(dir.clone()),
..Default::default()
},
);
assert_eq_dirs(&td, &dir, &disk, Some(Default::default()));
}
#[test]
fn test_ignore_lost_found() {
let td = tempdir().unwrap();
let dir = td.path().join("testdata");
create_dir(&dir).unwrap();
create_dir(dir.join("lost+found")).unwrap();
File::create(dir.join("lost+found").join("should_be_ignored.txt")).unwrap();
create_dir(dir.join("sub")).unwrap();
create_dir(dir.join("sub").join("lost+found")).unwrap();
File::create(dir.join("sub").join("lost+found").join("a.txt")).unwrap();
let disk = mkfs(
&td,
Builder {
blocks_per_group: 2048,
inodes_per_group: 4096,
root_dir: Some(dir.clone()),
..Default::default()
},
);
let dump_dir = td.path().join("dump");
std::fs::create_dir(&dump_dir).unwrap();
run_debugfs_cmd(
&[&format!(
"rdump / {}",
dump_dir.as_os_str().to_str().unwrap()
)],
&disk,
);
let paths = collect_paths(&dump_dir, false )
.into_iter()
.map(|(path, _)| path)
.collect::<BTreeSet<_>>();
assert_eq!(
paths,
BTreeSet::from([
"lost+found".to_string(),
"sub".to_string(),
"sub/lost+found".to_string(),
"sub/lost+found/a.txt".to_string()
])
);
}
#[test]
fn test_multiple_block_directory_entry() {
let td = tempdir().unwrap();
let dir = td.path().join("testdata");
std::fs::create_dir(&dir).unwrap();
for i in 0..1000 {
let path = dir.join(format!("{i}.txt"));
File::create(&path).unwrap();
}
let disk = mkfs(
&td,
Builder {
blocks_per_group: 2048,
inodes_per_group: 4096,
root_dir: Some(dir.clone()),
..Default::default()
},
);
assert_eq_dirs(&td, &dir, &disk, None);
}
#[test]
fn test_multiple_bg_multi_inode_bitmap() {
let td = tempdir().unwrap();
let dir = td.path().join("testdata");
std::fs::create_dir(&dir).unwrap();
for i in 0..1000 {
let fname = format!("{i}.txt");
let path = dir.join(&fname);
let mut f = File::create(&path).unwrap();
f.write_all(fname.as_bytes()).unwrap();
}
let blocks_per_group = 1024;
let inodes_per_group = 512;
let num_groups = 2;
let disk = mkfs(
&td,
Builder {
blocks_per_group,
inodes_per_group,
size: BLOCK_SIZE * blocks_per_group * num_groups,
root_dir: Some(dir.clone()),
},
);
assert_eq_dirs(&td, &dir, &disk, None);
}
#[test]
fn test_multiple_bg_multi_block_bitmap() {
let td = tempdir().unwrap();
let dir = td.path().join("testdata");
std::fs::create_dir(&dir).unwrap();
for i in 0..1000 {
let fname = format!("{i}.txt");
let path = dir.join(&fname);
let mut f = File::create(&path).unwrap();
f.write_all(fname.as_bytes()).unwrap();
}
let blocks_per_group = 512;
let inodes_per_group = 2048;
let num_groups = 4;
let disk = mkfs(
&td,
Builder {
blocks_per_group,
inodes_per_group,
size: BLOCK_SIZE * blocks_per_group * num_groups,
root_dir: Some(dir.clone()),
},
);
assert_eq_dirs(&td, &dir, &disk, None);
}
#[test]
fn test_multiple_bg_big_files() {
let td = tempdir().unwrap();
let dir = td.path().join("testdata");
std::fs::create_dir(&dir).unwrap();
let data = vec!["0123456789"; 5000 * 20].concat();
for i in 0..10 {
let path = dir.join(format!("{i}.txt"));
let mut f = File::create(&path).unwrap();
f.write_all(data.as_bytes()).unwrap();
}
let blocks_per_group = 128;
let num_groups = 50;
let disk = mkfs(
&td,
Builder {
blocks_per_group,
inodes_per_group: 1024,
size: BLOCK_SIZE * blocks_per_group * num_groups,
root_dir: Some(dir.clone()),
},
);
assert_eq_dirs(&td, &dir, &disk, Some(Default::default()));
}
#[test]
#[ignore = "Called by test_mkfs_xattr"]
fn test_mkfs_xattr_impl() {
let td = tempdir_in(".").unwrap();
let dir = td.path().join("testdata");
std::fs::create_dir(&dir).unwrap();
let dir_xattrs = vec![("dir".to_string(), vec![("user.foo", "directory")])];
let file_xattrs = vec![
(
"a.txt".to_string(),
vec![("user.foo", "a"), ("user.number", "0123456789")],
),
(
"b.txt".to_string(),
vec![("security.selinux", "unconfined_u:object_r:user_home_t:s0")],
),
("c.txt".to_string(), vec![]),
("dir/d.txt".to_string(), vec![("user.foo", "in_directory")]),
];
for (fname, xattrs) in &dir_xattrs {
let f_path = dir.join(fname);
std::fs::create_dir(&f_path).unwrap();
for (key, value) in xattrs {
ext2::set_xattr(&f_path, key, value).unwrap();
}
}
for (fname, xattrs) in &file_xattrs {
let f_path = dir.join(fname);
File::create(&f_path).unwrap();
for (key, value) in xattrs {
ext2::set_xattr(&f_path, key, value).unwrap();
}
}
let xattr_map: BTreeMap<String, Vec<(&str, &str)>> =
file_xattrs.into_iter().chain(dir_xattrs).collect();
let builder = Builder {
root_dir: Some(dir.clone()),
..Default::default()
};
let disk = mkfs(&td, builder);
assert_eq_dirs(&td, &dir, &disk, Some(xattr_map));
}
#[test]
fn test_mkfs_xattr() {
call_test_with_sudo("test_mkfs_xattr_impl")
}