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", "libc",
"tempfile", "tempfile",
"uuid", "uuid",
"walkdir",
"zerocopy", "zerocopy",
"zerocopy-derive", "zerocopy-derive",
] ]
@ -2591,6 +2592,15 @@ version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" 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]] [[package]]
name = "sandbox" name = "sandbox"
version = "0.1.0" version = "0.1.0"
@ -3197,6 +3207,16 @@ dependencies = [
"vk-parse", "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]] [[package]]
name = "wasi" name = "wasi"
version = "0.11.0+wasi-snapshot-preview1" version = "0.11.0+wasi-snapshot-preview1"

View file

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

View file

@ -8,6 +8,7 @@
mod linux { mod linux {
use std::fs::OpenOptions; use std::fs::OpenOptions;
use std::io::Write; use std::io::Write;
use std::path::Path;
use argh::FromArgs; use argh::FromArgs;
use base::MappedRegion; use base::MappedRegion;
@ -18,7 +19,11 @@ mod linux {
struct Args { struct Args {
/// path to the disk, /// path to the disk,
#[argh(option)] #[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 /// number of blocks for each group
#[argh(option, default = "1024")] #[argh(option, default = "1024")]
@ -31,20 +36,20 @@ mod linux {
pub fn main() -> anyhow::Result<()> { pub fn main() -> anyhow::Result<()> {
let args: Args = argh::from_env(); let args: Args = argh::from_env();
let src_dir = args.src.as_ref().map(|s| Path::new(s.as_str()));
let cfg = ext2::Config { let cfg = ext2::Config {
blocks_per_group: args.blocks_per_group, blocks_per_group: args.blocks_per_group,
inodes_per_group: args.inodes_per_group, inodes_per_group: args.inodes_per_group,
}; };
let mem = create_ext2_region(&cfg, src_dir)?;
let mem = create_ext2_region(&cfg)?; println!("Create {}", args.output);
println!("Create {}", args.path);
// SAFETY: `mem` has a valid pointer and its size. // SAFETY: `mem` has a valid pointer and its size.
let buf = unsafe { std::slice::from_raw_parts(mem.as_ptr(), mem.size()) }; let buf = unsafe { std::slice::from_raw_parts(mem.as_ptr(), mem.size()) };
let mut file = OpenOptions::new() let mut file = OpenOptions::new()
.write(true) .write(true)
.create(true) .create(true)
.truncate(true) .truncate(true)
.open(&args.path) .open(&args.output)
.unwrap(); .unwrap();
file.write_all(buf).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 },]); 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. /// Represents a ID of a disk block.
pub struct BlockId(u32); 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`. /// Memory arena backed by `base::MemoryMapping`.
/// ///
/// This struct takes a mutable referencet to the memory mapping so this arena won't arena the /// 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. // a filesystem in memory.
use std::collections::BTreeMap; 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::anyhow;
use anyhow::bail; use anyhow::bail;
use anyhow::Context;
use anyhow::Result; use anyhow::Result;
use base::MemoryMapping; use base::MappedRegion;
use base::MemoryMappingArena;
use base::MemoryMappingBuilder; use base::MemoryMappingBuilder;
use base::Protection;
use zerocopy::AsBytes; use zerocopy::AsBytes;
use zerocopy::FromBytes; use zerocopy::FromBytes;
use zerocopy::FromZeroes; use zerocopy::FromZeroes;
@ -21,6 +29,7 @@ use crate::arena::BlockId;
use crate::blockgroup::GroupMetaData; use crate::blockgroup::GroupMetaData;
use crate::blockgroup::BLOCK_SIZE; use crate::blockgroup::BLOCK_SIZE;
use crate::inode::Inode; use crate::inode::Inode;
use crate::inode::InodeBlock;
use crate::inode::InodeNum; use crate::inode::InodeNum;
use crate::inode::InodeType; use crate::inode::InodeType;
use crate::superblock::Config; use crate::superblock::Config;
@ -37,7 +46,7 @@ struct DirEntryRaw {
struct DirEntryWithName<'a> { struct DirEntryWithName<'a> {
de: &'a mut DirEntryRaw, de: &'a mut DirEntryRaw,
name: String, name: OsString,
} }
impl<'a> std::fmt::Debug for DirEntryWithName<'a> { impl<'a> std::fmt::Debug for DirEntryWithName<'a> {
@ -54,11 +63,11 @@ impl<'a> DirEntryWithName<'a> {
arena: &'a Arena<'a>, arena: &'a Arena<'a>,
inode: InodeNum, inode: InodeNum,
typ: InodeType, typ: InodeType,
name_str: &str, name_str: &OsStr,
dblock: &mut DirEntryBlock, dblock: &mut DirEntryBlock,
) -> Result<Self> { ) -> Result<Self> {
if name_str.len() > 255 { 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 cs = name_str.as_bytes();
let name_len = cs.len(); let name_len = cs.len();
@ -98,7 +107,7 @@ impl<'a> DirEntryWithName<'a> {
.last_mut() .last_mut()
.expect("parent_dir must not be empty"); .expect("parent_dir must not be empty");
let last_rec_len = last.de.rec_len; 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) .checked_next_multiple_of(4)
.expect("overflow to calculate rec_len"); .expect("overflow to calculate rec_len");
de.rec_len = last_rec_len - last.de.rec_len; de.rec_len = last_rec_len - last.de.rec_len;
@ -106,7 +115,7 @@ impl<'a> DirEntryWithName<'a> {
Ok(Self { Ok(Self {
de, de,
name: name_str.to_owned(), name: name_str.into(),
}) })
} }
} }
@ -118,6 +127,16 @@ struct DirEntryBlock<'a> {
entries: Vec<DirEntryWithName<'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. /// A struct to represent an ext2 filesystem.
pub struct Ext2<'a> { pub struct Ext2<'a> {
sb: &'a mut SuperBlock, sb: &'a mut SuperBlock,
@ -129,6 +148,8 @@ pub struct Ext2<'a> {
// TODO(b/331901633): To support larger directory, // TODO(b/331901633): To support larger directory,
// the value should be `Vec<DirEntryBlock>`. // the value should be `Vec<DirEntryBlock>`.
dentries: BTreeMap<InodeNum, DirEntryBlock<'a>>, dentries: BTreeMap<InodeNum, DirEntryBlock<'a>>,
fd_mappings: Vec<FileMappingInfo>,
} }
impl<'a> Ext2<'a> { impl<'a> Ext2<'a> {
@ -144,13 +165,19 @@ impl<'a> Ext2<'a> {
sb, sb,
group_metadata, group_metadata,
dentries: BTreeMap::new(), dentries: BTreeMap::new(),
fd_mappings: Vec::new(),
}; };
// Add rootdir // Add rootdir
let root_inode = InodeNum::new(2)?; 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()?; 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) Ok(ext2)
} }
@ -185,6 +212,14 @@ impl<'a> Ext2<'a> {
} }
fn allocate_block(&mut self) -> Result<BlockId> { 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 { if self.sb.free_blocks_count == 0 {
bail!( bail!(
"no free blocks: run out of s_blocks_count={}", "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. // 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 gm = &mut self.group_metadata;
let alloc_block = gm.first_free_block; let alloc_blocks = (gm.first_free_block..gm.first_free_block + n as u32)
gm.block_bitmap.set(alloc_block as usize, true)?; .map(BlockId::from)
gm.first_free_block += 1; .collect();
gm.group_desc.free_blocks_count -= 1; gm.first_free_block += n as u32;
self.sb.free_blocks_count -= 1; 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> { fn get_inode_mut(&mut self, num: InodeNum) -> Result<&mut &'a mut Inode> {
@ -220,7 +263,7 @@ impl<'a> Ext2<'a> {
parent: InodeNum, parent: InodeNum,
inode: InodeNum, inode: InodeNum,
typ: InodeType, typ: InodeType,
name: &str, name: &OsStr,
) -> Result<()> { ) -> Result<()> {
let block_size = self.block_size(); let block_size = self.block_size();
@ -230,7 +273,7 @@ impl<'a> Ext2<'a> {
if !self.dentries.contains_key(&parent) { if !self.dentries.contains_key(&parent) {
let block_id = self.allocate_block()?; let block_id = self.allocate_block()?;
let inode = self.get_inode_mut(parent)?; 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; inode.blocks = block_size as u32 / 512;
self.dentries.insert( self.dentries.insert(
parent, parent,
@ -279,12 +322,14 @@ impl<'a> Ext2<'a> {
Ok(()) 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, &mut self,
arena: &'a Arena<'a>, arena: &'a Arena<'a>,
inode_num: InodeNum, inode_num: InodeNum,
parent_inode: InodeNum, parent_inode: InodeNum,
name: &str, name: &OsStr,
) -> Result<()> { ) -> Result<()> {
let block_size = self.sb.block_size(); let block_size = self.sb.block_size();
let inode = Inode::new( let inode = Inode::new(
@ -296,8 +341,20 @@ impl<'a> Ext2<'a> {
)?; )?;
self.add_inode(inode_num, inode)?; self.add_inode(inode_num, inode)?;
self.allocate_dir_entry(arena, inode_num, inode_num, InodeType::Directory, ".")?; self.allocate_dir_entry(
self.allocate_dir_entry(arena, inode_num, parent_inode, InodeType::Directory, "..")?; 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 { if inode_num != parent_inode {
self.allocate_dir_entry(arena, parent_inode, inode_num, InodeType::Directory, name)?; self.allocate_dir_entry(arena, parent_inode, inode_num, InodeType::Directory, name)?;
@ -305,15 +362,188 @@ impl<'a> Ext2<'a> {
Ok(()) 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. /// 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 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) let mut mem = MemoryMappingBuilder::new(cfg.blocks_per_group as usize * BLOCK_SIZE * num_group)
.build()?; .build()?;
let arena = Arena::new(BLOCK_SIZE, &mut mem)?; 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()?; 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. //! Defines the inode structure.
use std::os::linux::fs::MetadataExt;
use anyhow::bail; use anyhow::bail;
use anyhow::Result; use anyhow::Result;
use enumn::N; use enumn::N;
@ -46,7 +48,7 @@ impl InodeType {
// Represents an inode number. // Represents an inode number.
// This is 1-indexed. // This is 1-indexed.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) struct InodeNum(u32); pub(crate) struct InodeNum(pub u32);
impl InodeNum { impl InodeNum {
pub fn new(inode: u32) -> Result<Self> { pub fn new(inode: u32) -> Result<Self> {
@ -91,9 +93,14 @@ impl Default for InodeBlock {
} }
impl 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. /// Set a block id at the given index.
pub fn set_block_id(&mut self, index: usize, block: BlockId) { pub fn set_block_id(&mut self, index: usize, block_id: &BlockId) {
self.0[index * 4..(index + 1) * 4].copy_from_slice(u32::from(block).as_bytes()) 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_high = (gid >> 16) as u16;
let gid_low = gid as u16; let gid_low = gid as u16;
// TODO(b/333988434): Support extended attributes.
*inode = Self { *inode = Self {
mode, mode,
size, size,
@ -197,6 +205,54 @@ impl Inode {
Ok(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> { pub fn typ(&self) -> Option<InodeType> {
InodeType::n((self.mode >> 12) as u8) InodeType::n((self.mode >> 12) as u8)
} }

View file

@ -4,8 +4,10 @@
#![cfg(target_os = "linux")] #![cfg(target_os = "linux")]
use std::collections::BTreeSet;
use std::fs::OpenOptions; use std::fs::OpenOptions;
use std::io::Write; use std::io::Write;
use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::Command; use std::process::Command;
@ -13,6 +15,8 @@ use base::MappedRegion;
use ext2::create_ext2_region; use ext2::create_ext2_region;
use ext2::Config; use ext2::Config;
use tempfile::tempdir; use tempfile::tempdir;
use tempfile::TempDir;
use walkdir::WalkDir;
const FSCK_PATH: &str = "/usr/sbin/e2fsck"; const FSCK_PATH: &str = "/usr/sbin/e2fsck";
const DEBUGFS_PATH: &str = "/usr/sbin/debugfs"; const DEBUGFS_PATH: &str = "/usr/sbin/debugfs";
@ -31,11 +35,11 @@ fn run_fsck(path: &PathBuf) {
assert!(output.status.success()); 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) let output = Command::new(DEBUGFS_PATH)
.arg("-R") .arg("-R")
.arg("ls") .args(args)
.arg(path) .arg(disk)
.output() .output()
.unwrap(); .unwrap();
@ -46,13 +50,12 @@ fn run_debugfs_ls(path: &PathBuf, expected: &str) {
println!("stderr: {stderr}"); println!("stderr: {stderr}");
assert!(output.status.success()); assert!(output.status.success());
assert_eq!(stdout.trim_start().trim_end(), expected); stdout.trim_start().trim_end().to_string()
} }
fn mkfs_empty(cfg: &Config) { fn mkfs(td: &TempDir, cfg: &Config, src_dir: Option<&Path>) -> PathBuf {
let td = tempdir().unwrap();
let path = td.path().join("empty.ext2"); 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. // SAFETY: `mem` has a valid pointer and its size.
let buf = unsafe { std::slice::from_raw_parts(mem.as_ptr(), mem.size()) }; let buf = unsafe { std::slice::from_raw_parts(mem.as_ptr(), mem.size()) };
let mut file = OpenOptions::new() let mut file = OpenOptions::new()
@ -65,26 +68,138 @@ fn mkfs_empty(cfg: &Config) {
run_fsck(&path); 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`. // Ensure the content of the generated disk image with `debugfs`.
// It contains the following entries: // It contains the following entries:
// - `.`: the rootdir whose inode is 2 and rec_len is 12. // - `.`: the rootdir whose inode is 2 and rec_len is 12.
// - `..`: this is also the rootdir with same inode and the same rec_len. // - `..`: 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). // - `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"); assert_eq!(
} run_debugfs_cmd(&["ls"], &disk),
"2 (12) . 2 (12) .. 11 (4072) lost+found"
#[test] );
fn test_mkfs_empty() {
mkfs_empty(&Config {
blocks_per_group: 1024,
inodes_per_group: 1024,
});
} }
#[test] #[test]
fn test_mkfs_empty_more_blocks() { fn test_mkfs_empty_more_blocks() {
mkfs_empty(&Config { let td = tempdir().unwrap();
blocks_per_group: 2048, let disk = mkfs(
inodes_per_group: 4096, &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);
} }