mirror of
https://chromium.googlesource.com/crosvm/crosvm
synced 2024-11-28 01:16:50 +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",
|
"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"
|
||||||
|
|
|
@ -19,3 +19,4 @@ name = "mkfs"
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
argh = "0.1"
|
argh = "0.1"
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
walkdir = "2.3"
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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
|
||||||
|
|
280
ext2/src/fs.rs
280
ext2/src/fs.rs
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue