mirror of
https://chromium.googlesource.com/crosvm/crosvm
synced 2024-11-24 12:34:31 +00:00
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:
parent
45c9c15b29
commit
0eccc54c50
7 changed files with 488 additions and 54 deletions
20
Cargo.lock
generated
20
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -19,3 +19,4 @@ name = "mkfs"
|
|||
[dev-dependencies]
|
||||
argh = "0.1"
|
||||
tempfile = "3"
|
||||
walkdir = "2.3"
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
280
ext2/src/fs.rs
280
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<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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue