ext2: Allow constructing a file system from a directory

BUG=b:329359333
TEST=cargo test

Change-Id: I3279426ee3ad5fa593075705acc4a5b1e8572d64
Reviewed-on: https://chromium-review.googlesource.com/c/crosvm/crosvm/+/5439156
Reviewed-by: Junichi Uekawa <uekawa@chromium.org>
Reviewed-by: Takaya Saeki <takayas@chromium.org>
Commit-Queue: Keiichi Watanabe <keiichiw@chromium.org>
This commit is contained in:
Keiichi Watanabe 2024-04-10 22:50:56 +09:00 committed by crosvm LUCI
parent 45c9c15b29
commit 0eccc54c50
7 changed files with 488 additions and 54 deletions

20
Cargo.lock generated
View file

@ -1182,6 +1182,7 @@ dependencies = [
"libc",
"tempfile",
"uuid",
"walkdir",
"zerocopy",
"zerocopy-derive",
]
@ -2591,6 +2592,15 @@ version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "sandbox"
version = "0.1.0"
@ -3197,6 +3207,16 @@ dependencies = [
"vk-parse",
]
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"

View file

@ -19,3 +19,4 @@ name = "mkfs"
[dev-dependencies]
argh = "0.1"
tempfile = "3"
walkdir = "2.3"

View file

@ -8,6 +8,7 @@
mod linux {
use std::fs::OpenOptions;
use std::io::Write;
use std::path::Path;
use argh::FromArgs;
use base::MappedRegion;
@ -18,7 +19,11 @@ mod linux {
struct Args {
/// path to the disk,
#[argh(option)]
path: String,
output: String,
/// path to the source directory to copy files from,
#[argh(option)]
src: Option<String>,
/// number of blocks for each group
#[argh(option, default = "1024")]
@ -31,20 +36,20 @@ mod linux {
pub fn main() -> anyhow::Result<()> {
let args: Args = argh::from_env();
let src_dir = args.src.as_ref().map(|s| Path::new(s.as_str()));
let cfg = ext2::Config {
blocks_per_group: args.blocks_per_group,
inodes_per_group: args.inodes_per_group,
};
let mem = create_ext2_region(&cfg)?;
println!("Create {}", args.path);
let mem = create_ext2_region(&cfg, src_dir)?;
println!("Create {}", args.output);
// SAFETY: `mem` has a valid pointer and its size.
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(&args.path)
.open(&args.output)
.unwrap();
file.write_all(buf).unwrap();

View file

@ -142,7 +142,8 @@ fn test_region_manager() {
assert_eq!(rm.to_vec(), vec![&Region { start: 0, len: 30 },]);
}
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, AsBytes)]
#[repr(C)]
/// Represents a ID of a disk block.
pub struct BlockId(u32);
@ -158,6 +159,12 @@ impl From<BlockId> for u32 {
}
}
impl BlockId {
pub fn as_bytes(&self) -> &[u8] {
self.0.as_bytes()
}
}
/// Memory arena backed by `base::MemoryMapping`.
///
/// This struct takes a mutable referencet to the memory mapping so this arena won't arena the

View file

@ -6,12 +6,20 @@
// a filesystem in memory.
use std::collections::BTreeMap;
use std::ffi::OsStr;
use std::ffi::OsString;
use std::fs::File;
use std::os::unix::ffi::OsStrExt;
use std::path::Path;
use anyhow::anyhow;
use anyhow::bail;
use anyhow::Context;
use anyhow::Result;
use base::MemoryMapping;
use base::MappedRegion;
use base::MemoryMappingArena;
use base::MemoryMappingBuilder;
use base::Protection;
use zerocopy::AsBytes;
use zerocopy::FromBytes;
use zerocopy::FromZeroes;
@ -21,6 +29,7 @@ use crate::arena::BlockId;
use crate::blockgroup::GroupMetaData;
use crate::blockgroup::BLOCK_SIZE;
use crate::inode::Inode;
use crate::inode::InodeBlock;
use crate::inode::InodeNum;
use crate::inode::InodeType;
use crate::superblock::Config;
@ -37,7 +46,7 @@ struct DirEntryRaw {
struct DirEntryWithName<'a> {
de: &'a mut DirEntryRaw,
name: String,
name: OsString,
}
impl<'a> std::fmt::Debug for DirEntryWithName<'a> {
@ -54,11 +63,11 @@ impl<'a> DirEntryWithName<'a> {
arena: &'a Arena<'a>,
inode: InodeNum,
typ: InodeType,
name_str: &str,
name_str: &OsStr,
dblock: &mut DirEntryBlock,
) -> Result<Self> {
if name_str.len() > 255 {
anyhow::bail!("name length must not exceed 255: {}", name_str);
anyhow::bail!("name length must not exceed 255: {:?}", name_str);
}
let cs = name_str.as_bytes();
let name_len = cs.len();
@ -98,7 +107,7 @@ impl<'a> DirEntryWithName<'a> {
.last_mut()
.expect("parent_dir must not be empty");
let last_rec_len = last.de.rec_len;
last.de.rec_len = (8 + last.name.as_bytes().len() as u16)
last.de.rec_len = (8 + last.name.as_os_str().as_bytes().len() as u16)
.checked_next_multiple_of(4)
.expect("overflow to calculate rec_len");
de.rec_len = last_rec_len - last.de.rec_len;
@ -106,7 +115,7 @@ impl<'a> DirEntryWithName<'a> {
Ok(Self {
de,
name: name_str.to_owned(),
name: name_str.into(),
})
}
}
@ -118,6 +127,16 @@ struct DirEntryBlock<'a> {
entries: Vec<DirEntryWithName<'a>>,
}
/// Information on how to mmap a host file to ext2 blocks.
struct FileMappingInfo {
/// The ext2 disk block id that the memory region maps to.
start_block: BlockId,
/// The file to be mmap'd.
file: File,
/// The size of the file to be mmap'd.
file_size: usize,
}
/// A struct to represent an ext2 filesystem.
pub struct Ext2<'a> {
sb: &'a mut SuperBlock,
@ -129,6 +148,8 @@ pub struct Ext2<'a> {
// TODO(b/331901633): To support larger directory,
// the value should be `Vec<DirEntryBlock>`.
dentries: BTreeMap<InodeNum, DirEntryBlock<'a>>,
fd_mappings: Vec<FileMappingInfo>,
}
impl<'a> Ext2<'a> {
@ -144,13 +165,19 @@ impl<'a> Ext2<'a> {
sb,
group_metadata,
dentries: BTreeMap::new(),
fd_mappings: Vec::new(),
};
// Add rootdir
let root_inode = InodeNum::new(2)?;
ext2.add_dir(arena, root_inode, root_inode, "/")?;
ext2.add_reserved_dir(arena, root_inode, root_inode, OsStr::new("/"))?;
let lost_found_inode = ext2.allocate_inode()?;
ext2.add_dir(arena, lost_found_inode, root_inode, "lost+found")?;
ext2.add_reserved_dir(
arena,
lost_found_inode,
root_inode,
OsStr::new("lost+found"),
)?;
Ok(ext2)
}
@ -185,6 +212,14 @@ impl<'a> Ext2<'a> {
}
fn allocate_block(&mut self) -> Result<BlockId> {
self.allocate_contiguous_blocks(1).map(|v| v[0])
}
fn allocate_contiguous_blocks(&mut self, n: u16) -> Result<Vec<BlockId>> {
if n == 0 {
bail!("n must be positive");
}
if self.sb.free_blocks_count == 0 {
bail!(
"no free blocks: run out of s_blocks_count={}",
@ -192,19 +227,27 @@ impl<'a> Ext2<'a> {
);
}
if self.group_metadata.group_desc.free_blocks_count == 0 {
if self.group_metadata.group_desc.free_blocks_count < n {
// TODO(b/331764754): Support multiple block groups.
bail!("no free blocks in group 0. No multiple group support");
bail!(
"not enough free blocks in group 0.: {} < {}",
self.group_metadata.group_desc.free_blocks_count,
n
);
}
let gm = &mut self.group_metadata;
let alloc_block = gm.first_free_block;
gm.block_bitmap.set(alloc_block as usize, true)?;
gm.first_free_block += 1;
gm.group_desc.free_blocks_count -= 1;
self.sb.free_blocks_count -= 1;
let alloc_blocks = (gm.first_free_block..gm.first_free_block + n as u32)
.map(BlockId::from)
.collect();
gm.first_free_block += n as u32;
gm.group_desc.free_blocks_count -= n;
self.sb.free_blocks_count -= n as u32;
for &b in &alloc_blocks {
gm.block_bitmap.set(u32::from(b) as usize, true)?;
}
Ok(BlockId::from(alloc_block))
Ok(alloc_blocks)
}
fn get_inode_mut(&mut self, num: InodeNum) -> Result<&mut &'a mut Inode> {
@ -220,7 +263,7 @@ impl<'a> Ext2<'a> {
parent: InodeNum,
inode: InodeNum,
typ: InodeType,
name: &str,
name: &OsStr,
) -> Result<()> {
let block_size = self.block_size();
@ -230,7 +273,7 @@ impl<'a> Ext2<'a> {
if !self.dentries.contains_key(&parent) {
let block_id = self.allocate_block()?;
let inode = self.get_inode_mut(parent)?;
inode.block.set_block_id(0, block_id);
inode.block.set_block_id(0, &block_id);
inode.blocks = block_size as u32 / 512;
self.dentries.insert(
parent,
@ -279,12 +322,14 @@ impl<'a> Ext2<'a> {
Ok(())
}
fn add_dir(
// Creates a reserved directory such as "root" or "lost+found".
// So, inode is constructed from scratch.
fn add_reserved_dir(
&mut self,
arena: &'a Arena<'a>,
inode_num: InodeNum,
parent_inode: InodeNum,
name: &str,
name: &OsStr,
) -> Result<()> {
let block_size = self.sb.block_size();
let inode = Inode::new(
@ -296,8 +341,20 @@ impl<'a> Ext2<'a> {
)?;
self.add_inode(inode_num, inode)?;
self.allocate_dir_entry(arena, inode_num, inode_num, InodeType::Directory, ".")?;
self.allocate_dir_entry(arena, inode_num, parent_inode, InodeType::Directory, "..")?;
self.allocate_dir_entry(
arena,
inode_num,
inode_num,
InodeType::Directory,
OsStr::new("."),
)?;
self.allocate_dir_entry(
arena,
inode_num,
parent_inode,
InodeType::Directory,
OsStr::new(".."),
)?;
if inode_num != parent_inode {
self.allocate_dir_entry(arena, parent_inode, inode_num, InodeType::Directory, name)?;
@ -305,15 +362,188 @@ impl<'a> Ext2<'a> {
Ok(())
}
fn add_dir(
&mut self,
arena: &'a Arena<'a>,
inode_num: InodeNum,
parent_inode: InodeNum,
path: &Path,
) -> Result<()> {
let block_size = self.sb.block_size();
let inode = Inode::from_metadata(
arena,
&mut self.group_metadata,
inode_num,
&std::fs::metadata(path)?,
block_size as u32,
0,
0,
InodeBlock::default(),
)?;
self.add_inode(inode_num, inode)?;
self.allocate_dir_entry(
arena,
inode_num,
inode_num,
InodeType::Directory,
OsStr::new("."),
)?;
self.allocate_dir_entry(
arena,
inode_num,
parent_inode,
InodeType::Directory,
OsStr::new(".."),
)?;
if inode_num != parent_inode {
let name = path
.file_name()
.ok_or_else(|| anyhow!("failed to get directory name"))?;
self.allocate_dir_entry(arena, parent_inode, inode_num, InodeType::Directory, name)?;
}
Ok(())
}
fn add_file(
&mut self,
arena: &'a Arena<'a>,
parent_inode: InodeNum,
path: &Path,
) -> Result<()> {
let inode_num = self.allocate_inode()?;
let name = path
.file_name()
.ok_or_else(|| anyhow!("failed to get directory name"))?;
let file = File::open(path)?;
let file_size = file.metadata()?.len() as usize;
let block_size = self.block_size() as usize;
let mut block = InodeBlock::default();
let block_num = file_size.div_ceil(block_size);
if block_num > 12 {
// TODO(b/342937441): Support indirect blocks.
bail!("indirect data block are not yet supported");
}
if block_num > 0 {
let blocks = self.allocate_contiguous_blocks(block_num as u16)?;
self.fd_mappings.push(FileMappingInfo {
start_block: blocks[0],
file_size,
file,
});
block.copy_from_slice(0, blocks.as_bytes());
}
// The spec says that the `blocks` field is a "32-bit value representing the total number
// of 512-bytes blocks". This `512` is a fixed number regardless of the actual block size,
// which is usuaully 4KB.
let blocks = block_num as u32 * (block_size as u32 / 512);
let size = file_size as u32;
let inode = Inode::from_metadata(
arena,
&mut self.group_metadata,
inode_num,
&std::fs::metadata(path)?,
size,
1,
blocks,
block,
)?;
self.add_inode(inode_num, inode)?;
self.allocate_dir_entry(arena, parent_inode, inode_num, InodeType::Regular, name)?;
Ok(())
}
/// Walks through `src_dir` and copies directories and files to the new file system.
fn copy_dirtree<P: AsRef<Path>>(&mut self, arena: &'a Arena<'a>, src_dir: P) -> Result<()> {
self.copy_dirtree_rec(arena, InodeNum(2), src_dir)
}
fn copy_dirtree_rec<P: AsRef<Path>>(
&mut self,
arena: &'a Arena<'a>,
parent_inode: InodeNum,
src_dir: P,
) -> Result<()> {
for entry in std::fs::read_dir(src_dir)? {
let entry = entry?;
let ftype = entry.file_type()?;
if ftype.is_dir() {
let inode = self.allocate_inode()?;
self.add_dir(arena, inode, parent_inode, &entry.path())
.with_context(|| {
format!(
"failed to add directory {:?} as inode={:?}",
entry.path(),
inode
)
})?;
self.copy_dirtree_rec(arena, inode, entry.path())?;
} else if ftype.is_file() {
self.add_file(arena, parent_inode, &entry.path())
.with_context(|| {
format!(
"failed to add file {:?} in inode={:?}",
entry.path(),
parent_inode
)
})?;
} else if ftype.is_symlink() {
let src = entry.path();
let dst = std::fs::read_link(&src)?;
// TODO(b/342937495): support symlink
bail!("symlink is not supported yet: {src:?} -> {dst:?}");
} else {
panic!("unknown file type: {:?}", ftype);
}
}
Ok(())
}
fn into_fd_mappings(self) -> Vec<FileMappingInfo> {
self.fd_mappings
}
}
/// Creates a memory mapping region where an ext2 filesystem is constructed.
pub fn create_ext2_region(cfg: &Config) -> Result<MemoryMapping> {
pub fn create_ext2_region(cfg: &Config, src_dir: Option<&Path>) -> Result<MemoryMappingArena> {
let num_group = 1; // TODO(b/329359333): Support more than 1 group.
let mut mem = MemoryMappingBuilder::new(cfg.blocks_per_group as usize * BLOCK_SIZE * num_group)
.build()?;
let arena = Arena::new(BLOCK_SIZE, &mut mem)?;
let _ext2 = Ext2::new(cfg, &arena)?;
let mut ext2 = Ext2::new(cfg, &arena)?;
if let Some(dir) = src_dir {
ext2.copy_dirtree(&arena, dir)?;
}
let file_mappings = ext2.into_fd_mappings();
mem.msync()?;
Ok(mem)
let mut mmap_arena = MemoryMappingArena::from(mem);
for FileMappingInfo {
start_block,
file_size,
file,
} in file_mappings
{
mmap_arena.add_fd_mapping(
u32::from(start_block) as usize * BLOCK_SIZE,
file_size,
&file,
0, /* fd_offset */
Protection::read(),
)?;
}
Ok(mmap_arena)
}

View file

@ -4,6 +4,8 @@
//! Defines the inode structure.
use std::os::linux::fs::MetadataExt;
use anyhow::bail;
use anyhow::Result;
use enumn::N;
@ -46,7 +48,7 @@ impl InodeType {
// Represents an inode number.
// This is 1-indexed.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) struct InodeNum(u32);
pub(crate) struct InodeNum(pub u32);
impl InodeNum {
pub fn new(inode: u32) -> Result<Self> {
@ -91,9 +93,14 @@ impl Default for InodeBlock {
}
impl InodeBlock {
/// Copies the given slice to `self.0.[offset..]`.
pub fn copy_from_slice(&mut self, offset: usize, src: &[u8]) {
self.0[offset..offset + src.len()].copy_from_slice(src)
}
/// Set a block id at the given index.
pub fn set_block_id(&mut self, index: usize, block: BlockId) {
self.0[index * 4..(index + 1) * 4].copy_from_slice(u32::from(block).as_bytes())
pub fn set_block_id(&mut self, index: usize, block_id: &BlockId) {
self.copy_from_slice(index * 4, block_id.as_bytes())
}
}
@ -182,6 +189,7 @@ impl Inode {
let gid_high = (gid >> 16) as u16;
let gid_low = gid as u16;
// TODO(b/333988434): Support extended attributes.
*inode = Self {
mode,
size,
@ -197,6 +205,54 @@ impl Inode {
Ok(inode)
}
pub fn from_metadata<'a>(
arena: &'a Arena<'a>,
group: &mut GroupMetaData,
inode_num: InodeNum,
m: &std::fs::Metadata,
size: u32,
links_count: u16,
blocks: u32,
block: InodeBlock,
) -> Result<&'a mut Self> {
// (inode_num - 1) because inode is 1-indexed.
let inode_offset = (usize::from(inode_num) - 1) * Inode::inode_record_size() as usize;
let inode =
arena.allocate::<Inode>(BlockId::from(group.group_desc.inode_table), inode_offset)?;
let mode = m.st_mode() as u16;
let uid = m.st_uid();
let uid_high = (uid >> 16) as u16;
let uid_low: u16 = uid as u16;
let gid = m.st_gid();
let gid_high = (gid >> 16) as u16;
let gid_low: u16 = gid as u16;
let atime = m.st_atime() as u32;
let ctime = m.st_ctime() as u32;
let mtime = m.st_mtime() as u32;
*inode = Inode {
mode,
_uid: uid_low,
_gid: gid_low,
size,
atime,
ctime,
mtime,
links_count,
blocks,
block,
_uid_high: uid_high,
_gid_high: gid_high,
..Default::default()
};
Ok(inode)
}
pub fn typ(&self) -> Option<InodeType> {
InodeType::n((self.mode >> 12) as u8)
}

View file

@ -4,8 +4,10 @@
#![cfg(target_os = "linux")]
use std::collections::BTreeSet;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
@ -13,6 +15,8 @@ use base::MappedRegion;
use ext2::create_ext2_region;
use ext2::Config;
use tempfile::tempdir;
use tempfile::TempDir;
use walkdir::WalkDir;
const FSCK_PATH: &str = "/usr/sbin/e2fsck";
const DEBUGFS_PATH: &str = "/usr/sbin/debugfs";
@ -31,11 +35,11 @@ fn run_fsck(path: &PathBuf) {
assert!(output.status.success());
}
fn run_debugfs_ls(path: &PathBuf, expected: &str) {
fn run_debugfs_cmd(args: &[&str], disk: &PathBuf) -> String {
let output = Command::new(DEBUGFS_PATH)
.arg("-R")
.arg("ls")
.arg(path)
.args(args)
.arg(disk)
.output()
.unwrap();
@ -46,13 +50,12 @@ fn run_debugfs_ls(path: &PathBuf, expected: &str) {
println!("stderr: {stderr}");
assert!(output.status.success());
assert_eq!(stdout.trim_start().trim_end(), expected);
stdout.trim_start().trim_end().to_string()
}
fn mkfs_empty(cfg: &Config) {
let td = tempdir().unwrap();
fn mkfs(td: &TempDir, cfg: &Config, src_dir: Option<&Path>) -> PathBuf {
let path = td.path().join("empty.ext2");
let mem = create_ext2_region(cfg).unwrap();
let mem = create_ext2_region(cfg, src_dir).unwrap();
// SAFETY: `mem` has a valid pointer and its size.
let buf = unsafe { std::slice::from_raw_parts(mem.as_ptr(), mem.size()) };
let mut file = OpenOptions::new()
@ -65,26 +68,138 @@ fn mkfs_empty(cfg: &Config) {
run_fsck(&path);
path
}
#[test]
fn test_mkfs_empty() {
let td = tempdir().unwrap();
let disk = mkfs(
&td,
&Config {
blocks_per_group: 1024,
inodes_per_group: 1024,
},
None,
);
// Ensure the content of the generated disk image with `debugfs`.
// It contains the following entries:
// - `.`: the rootdir whose inode is 2 and rec_len is 12.
// - `..`: this is also the rootdir with same inode and the same rec_len.
// - `lost+found`: inode is 11 and rec_len is 4072 (= block_size - 2*12).
run_debugfs_ls(&path, "2 (12) . 2 (12) .. 11 (4072) lost+found");
}
#[test]
fn test_mkfs_empty() {
mkfs_empty(&Config {
blocks_per_group: 1024,
inodes_per_group: 1024,
});
assert_eq!(
run_debugfs_cmd(&["ls"], &disk),
"2 (12) . 2 (12) .. 11 (4072) lost+found"
);
}
#[test]
fn test_mkfs_empty_more_blocks() {
mkfs_empty(&Config {
blocks_per_group: 2048,
inodes_per_group: 4096,
});
let td = tempdir().unwrap();
let disk = mkfs(
&td,
&Config {
blocks_per_group: 2048,
inodes_per_group: 4096,
},
None,
);
assert_eq!(
run_debugfs_cmd(&["ls"], &disk),
"2 (12) . 2 (12) .. 11 (4072) lost+found"
);
}
fn collect_paths(dir: &Path) -> 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() || name == "lost+found" {
return None;
}
Some((name, path))
})
})
.collect()
}
fn assert_eq_dirs(dir1: &Path, dir2: &Path) {
let paths1 = collect_paths(dir1);
let paths2 = collect_paths(dir2);
if paths1.len() != paths2.len() {
panic!(
"number of entries mismatch: {:?}={:?}, {:?}={:?}",
dir1,
paths1.len(),
dir2,
paths2.len()
);
}
for ((name1, path1), (name2, path2)) in paths1.iter().zip(paths2.iter()) {
assert_eq!(name1, name2);
let m1 = std::fs::metadata(path1).unwrap();
let m2 = std::fs::metadata(path2).unwrap();
assert_eq!(
m1.file_type(),
m2.file_type(),
"file type mismatch ({name1})"
);
assert_eq!(m1.len(), m2.len(), "length mismatch ({name1})");
assert_eq!(
m1.permissions(),
m2.permissions(),
"permissions mismatch ({name1})"
);
}
}
fn create_test_data(root: &Path) {
// root
// ├── a.txt
// ├── b.txt
// └── dir
// └── c.txt
std::fs::create_dir(root).unwrap();
std::fs::File::create(root.join("a.txt")).unwrap();
std::fs::File::create(root.join("b.txt")).unwrap();
std::fs::create_dir(root.join("dir")).unwrap();
std::fs::File::create(root.join("dir/c.txt")).unwrap();
}
#[test]
fn test_mkfs_dir() {
let td = tempdir().unwrap();
let testdata_dir = td.path().join("testdata");
create_test_data(&testdata_dir);
let disk = mkfs(
&td,
&Config {
blocks_per_group: 2048,
inodes_per_group: 4096,
},
Some(&testdata_dir),
);
// dump the disk contents to `dump_dir`.
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,
);
assert_eq_dirs(&testdata_dir, &dump_dir);
}