From 0eccc54c50ec773230d1638c4bbeca76a4135663 Mon Sep 17 00:00:00 2001 From: Keiichi Watanabe Date: Wed, 10 Apr 2024 22:50:56 +0900 Subject: [PATCH] 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 Reviewed-by: Takaya Saeki Commit-Queue: Keiichi Watanabe --- Cargo.lock | 20 +++ ext2/Cargo.toml | 1 + ext2/examples/mkfs.rs | 15 ++- ext2/src/arena.rs | 9 +- ext2/src/fs.rs | 280 ++++++++++++++++++++++++++++++++++++++---- ext2/src/inode.rs | 62 +++++++++- ext2/tests/tests.rs | 155 ++++++++++++++++++++--- 7 files changed, 488 insertions(+), 54 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a01e3153f8..c75b77d811 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/ext2/Cargo.toml b/ext2/Cargo.toml index 4a331fbd9e..dd201eb545 100644 --- a/ext2/Cargo.toml +++ b/ext2/Cargo.toml @@ -19,3 +19,4 @@ name = "mkfs" [dev-dependencies] argh = "0.1" tempfile = "3" +walkdir = "2.3" diff --git a/ext2/examples/mkfs.rs b/ext2/examples/mkfs.rs index ca419b1389..1e1a22a6ae 100644 --- a/ext2/examples/mkfs.rs +++ b/ext2/examples/mkfs.rs @@ -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, /// 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(); diff --git a/ext2/src/arena.rs b/ext2/src/arena.rs index de65025e71..4d3a51ec4b 100644 --- a/ext2/src/arena.rs +++ b/ext2/src/arena.rs @@ -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 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 diff --git a/ext2/src/fs.rs b/ext2/src/fs.rs index 7533b142a6..2ce4e1d1cf 100644 --- a/ext2/src/fs.rs +++ b/ext2/src/fs.rs @@ -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 { 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>, } +/// 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`. dentries: BTreeMap>, + + fd_mappings: Vec, } 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 { + self.allocate_contiguous_blocks(1).map(|v| v[0]) + } + + fn allocate_contiguous_blocks(&mut self, n: u16) -> Result> { + 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>(&mut self, arena: &'a Arena<'a>, src_dir: P) -> Result<()> { + self.copy_dirtree_rec(arena, InodeNum(2), src_dir) + } + + fn copy_dirtree_rec>( + &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 { + self.fd_mappings + } } /// Creates a memory mapping region where an ext2 filesystem is constructed. -pub fn create_ext2_region(cfg: &Config) -> Result { +pub fn create_ext2_region(cfg: &Config, src_dir: Option<&Path>) -> Result { 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) } diff --git a/ext2/src/inode.rs b/ext2/src/inode.rs index 1f06aebde0..e2761aa0d1 100644 --- a/ext2/src/inode.rs +++ b/ext2/src/inode.rs @@ -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 { @@ -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::(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::n((self.mode >> 12) as u8) } diff --git a/ext2/tests/tests.rs b/ext2/tests/tests.rs index e5f8033c52..526ecaa1a4 100644 --- a/ext2/tests/tests.rs +++ b/ext2/tests/tests.rs @@ -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); }