mod format;
use std::io::{self, BufWriter, SeekFrom, Write};
use std::path::Path;
use crate::crc32c;
use crate::filetree::{DirectoryNode, FileTree, TreeNode};
use format::{
EXT4_BG_INODE_ZEROED, EXT4_BLOCK_SIZE, EXT4_BLOCKS_PER_GROUP, EXT4_DESC_SIZE, EXT4_EH_MAGIC,
EXT4_EXTENTS_FL, EXT4_FEATURE_COMPAT_DIR_INDEX, EXT4_FEATURE_COMPAT_EXT_ATTR,
EXT4_FEATURE_COMPAT_HAS_JOURNAL, EXT4_FEATURE_INCOMPAT_64BIT, EXT4_FEATURE_INCOMPAT_EXTENTS,
EXT4_FEATURE_INCOMPAT_FILETYPE, EXT4_FEATURE_RO_COMPAT_DIR_NLINK,
EXT4_FEATURE_RO_COMPAT_EXTRA_ISIZE, EXT4_FEATURE_RO_COMPAT_HUGE_FILE,
EXT4_FEATURE_RO_COMPAT_LARGE_FILE, EXT4_FEATURE_RO_COMPAT_METADATA_CSUM,
EXT4_FEATURE_RO_COMPAT_SPARSE_SUPER, EXT4_FIRST_INO, EXT4_INODE_SIZE, EXT4_INODES_PER_GROUP,
EXT4_JOURNAL_INO, EXT4_LOG_BLOCK_SIZE, EXT4_MIN_EXTRA_ISIZE, EXT4_ROOT_INO, EXT4_SUPER_MAGIC,
JBD2_MAGIC, JBD2_SUPERBLOCK_V2, S_IFCHR, S_IFDIR, S_IFLNK, S_IFREG,
};
const DEFAULT_SIZE_BYTES: u64 = 4 * 1024 * 1024 * 1024;
const DEFAULT_JOURNAL_BLOCKS: u32 = 16384;
const MAX_GROUPS: u32 = 32;
const RESERVED_GDT_BLOCKS: u32 = 0;
const EXT4_FT_DIR: u8 = 2;
#[allow(dead_code)]
const EXT4_FT_REG_FILE: u8 = 1;
const EXT4_FT_CHRDEV: u8 = 3;
const EXT4_FT_SYMLINK: u8 = 7;
const JBD2_SUPERBLOCK_SIZE: usize = 1024;
pub struct Ext4FormatOptions {
pub size_bytes: u64,
pub journal_blocks: u32,
}
#[derive(Debug)]
pub enum Ext4Error {
Io(io::Error),
TooSmall,
Layout(String),
}
struct Layout {
num_blocks: u64,
num_groups: u32,
uuid: [u8; 16],
gdt_blocks: u32,
inode_table_block: u32,
inode_table_blocks: u32,
first_data_block: u32,
journal_start_block: u32,
journal_blocks: u32,
csum_seed: u32,
feature_compat: u32,
feature_incompat: u32,
feature_ro_compat: u32,
}
struct FsStats {
group_free_blocks: Vec<u32>,
group_free_inodes: Vec<u32>,
group_used_dirs: Vec<u32>,
total_free_blocks: u64,
total_free_inodes: u64,
total_used_blocks: u64,
}
enum NodeKind {
Directory { children: u16, data: Vec<u8> },
RegularFile { data: Vec<u8> },
Symlink { target: Vec<u8>, inline: bool },
CharDevice { major: u32, minor: u32 },
}
struct NodePlan {
inode: u32,
path: String,
permissions: u16,
uid: u16,
gid: u16,
kind: NodeKind,
block_start: Option<u32>,
block_count: u32,
}
struct DraftDirectory {
children: u16,
data: Vec<u8>,
}
struct DirEntrySpec {
inode: u32,
file_type: u8,
name: Vec<u8>,
}
struct DataAllocator {
regions: Vec<(u32, u32)>,
}
impl Default for Ext4FormatOptions {
fn default() -> Self {
Self {
size_bytes: DEFAULT_SIZE_BYTES,
journal_blocks: DEFAULT_JOURNAL_BLOCKS,
}
}
}
impl std::fmt::Display for Ext4Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Ext4Error::Io(e) => write!(f, "ext4 I/O error: {e}"),
Ext4Error::TooSmall => write!(f, "image size is too small for ext4 formatting"),
Ext4Error::Layout(e) => write!(f, "ext4 layout error: {e}"),
}
}
}
impl std::error::Error for Ext4Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Ext4Error::Io(e) => Some(e),
Ext4Error::TooSmall | Ext4Error::Layout(_) => None,
}
}
}
impl From<io::Error> for Ext4Error {
fn from(e: io::Error) -> Self {
Ext4Error::Io(e)
}
}
impl Layout {
#[cfg(test)]
fn compute(opts: &Ext4FormatOptions) -> Result<Self, Ext4Error> {
Self::compute_with_root_blocks(opts, 1)
}
fn compute_with_root_blocks(
opts: &Ext4FormatOptions,
root_dir_blocks: u32,
) -> Result<Self, Ext4Error> {
let block_size = EXT4_BLOCK_SIZE as u64;
let num_blocks = opts.size_bytes / block_size;
let num_groups_raw = num_blocks.div_ceil(EXT4_BLOCKS_PER_GROUP as u64);
let num_groups = num_groups_raw.min(MAX_GROUPS as u64) as u32;
let inode_table_blocks =
(EXT4_INODES_PER_GROUP as u64 * EXT4_INODE_SIZE as u64 / block_size) as u32;
let gdt_blocks = (num_groups as u64 * EXT4_DESC_SIZE as u64).div_ceil(block_size) as u32;
let overhead_blocks = 1 + gdt_blocks + RESERVED_GDT_BLOCKS; let block_bitmap_block = overhead_blocks;
let inode_bitmap_block = block_bitmap_block + 1;
let inode_table_block = inode_bitmap_block + 1;
let first_data_block = inode_table_block + inode_table_blocks;
let journal_start_block = first_data_block + root_dir_blocks;
let min_blocks = journal_start_block as u64 + opts.journal_blocks as u64 + 1; if num_blocks < min_blocks {
return Err(Ext4Error::TooSmall);
}
let uuid = Self::generate_uuid();
let csum_seed = crc32c::crc32c_raw(0xFFFF_FFFF, &uuid);
let feature_compat = EXT4_FEATURE_COMPAT_HAS_JOURNAL
| EXT4_FEATURE_COMPAT_EXT_ATTR
| EXT4_FEATURE_COMPAT_DIR_INDEX;
let feature_incompat = EXT4_FEATURE_INCOMPAT_FILETYPE
| EXT4_FEATURE_INCOMPAT_EXTENTS
| EXT4_FEATURE_INCOMPAT_64BIT;
let feature_ro_compat = EXT4_FEATURE_RO_COMPAT_SPARSE_SUPER
| EXT4_FEATURE_RO_COMPAT_LARGE_FILE
| EXT4_FEATURE_RO_COMPAT_HUGE_FILE
| EXT4_FEATURE_RO_COMPAT_DIR_NLINK
| EXT4_FEATURE_RO_COMPAT_EXTRA_ISIZE
| EXT4_FEATURE_RO_COMPAT_METADATA_CSUM;
Ok(Layout {
num_blocks,
num_groups,
uuid,
gdt_blocks,
inode_table_block,
inode_table_blocks,
first_data_block,
journal_start_block,
journal_blocks: opts.journal_blocks,
csum_seed,
feature_compat,
feature_incompat,
feature_ro_compat,
})
}
fn generate_uuid() -> [u8; 16] {
let mut uuid = [0u8; 16];
if let Ok(mut f) = std::fs::File::open("/dev/urandom") {
use std::io::Read;
let _ = f.read_exact(&mut uuid);
} else {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
let nanos = now.as_nanos();
uuid[..8].copy_from_slice(&(nanos as u64).to_le_bytes());
uuid[8..16].copy_from_slice(&((nanos >> 64) as u64).to_le_bytes());
}
uuid[6] = (uuid[6] & 0x0F) | 0x40;
uuid[7] = (uuid[7] & 0x3F) | 0x80;
uuid
}
fn group_start_block(&self, group: u32) -> u32 {
group * EXT4_BLOCKS_PER_GROUP
}
fn blocks_in_group(&self, group: u32) -> u32 {
let group_start = self.group_start_block(group) as u64;
std::cmp::min(
EXT4_BLOCKS_PER_GROUP as u64,
self.num_blocks.saturating_sub(group_start),
) as u32
}
fn group_has_backup_super(&self, group: u32) -> bool {
group == 0 || sparse_super_group(group)
}
fn group_leading_overhead_blocks(&self, group: u32) -> u32 {
if self.group_has_backup_super(group) {
1 + self.gdt_blocks + RESERVED_GDT_BLOCKS
} else {
0
}
}
fn group_block_bitmap_block(&self, group: u32) -> u32 {
self.group_start_block(group) + self.group_leading_overhead_blocks(group)
}
fn group_inode_bitmap_block(&self, group: u32) -> u32 {
self.group_block_bitmap_block(group) + 1
}
fn group_inode_table_block(&self, group: u32) -> u32 {
self.group_inode_bitmap_block(group) + 1
}
fn group_data_start_block(&self, group: u32) -> u32 {
let mut start = self.group_start_block(group) + self.group_metadata_blocks(group);
if group == 0 {
start = self.journal_start_block + self.journal_blocks;
}
start
}
fn group_metadata_blocks(&self, group: u32) -> u32 {
self.group_leading_overhead_blocks(group) + 2 + self.inode_table_blocks
}
fn group_used_blocks(&self, group: u32) -> u32 {
let mut used = self.group_metadata_blocks(group);
if group == 0 {
used += 1 + self.journal_blocks; }
used.min(self.blocks_in_group(group))
}
fn group_free_blocks(&self, group: u32) -> u32 {
self.blocks_in_group(group)
.saturating_sub(self.group_used_blocks(group))
}
fn group_free_inodes(&self, group: u32) -> u32 {
if group == 0 {
EXT4_INODES_PER_GROUP - (EXT4_FIRST_INO - 1)
} else {
EXT4_INODES_PER_GROUP
}
}
#[cfg(test)]
fn group_used_dirs(&self, group: u32) -> u32 {
if group == 0 { 1 } else { 0 }
}
fn total_free_blocks(&self) -> u64 {
(0..self.num_groups)
.map(|group| self.group_free_blocks(group) as u64)
.sum()
}
fn total_free_inodes(&self) -> u64 {
(0..self.num_groups)
.map(|group| self.group_free_inodes(group) as u64)
.sum()
}
fn total_used_blocks(&self) -> u64 {
(0..self.num_groups)
.map(|group| self.group_used_blocks(group) as u64)
.sum()
}
}
pub fn format_ext4(path: &Path, options: &Ext4FormatOptions) -> Result<(), Ext4Error> {
let tree = FileTree::new();
format_ext4_with_tree(path, options, tree)
}
pub fn format_ext4_with_tree(
path: &Path,
options: &Ext4FormatOptions,
tree: FileTree,
) -> Result<(), Ext4Error> {
let mut next_inode = EXT4_FIRST_INO;
let mut plans = Vec::new();
let root_mode = tree.root.metadata.mode;
let root_draft = draft_directory(
"/",
tree.root,
EXT4_ROOT_INO,
EXT4_ROOT_INO,
&mut next_inode,
&mut plans,
)?;
let root_dir_blocks = blocks_for_len(root_draft.data.len());
let layout = Layout::compute_with_root_blocks(options, root_dir_blocks.max(1))?;
let mut allocator = DataAllocator::new(&layout);
for plan in &mut plans {
allocate_node_data(&mut allocator, plan)?;
}
let mut all_plans = Vec::with_capacity(plans.len() + 1);
all_plans.push(NodePlan {
inode: EXT4_ROOT_INO,
path: "/".to_string(),
permissions: normalize_dir_permissions(root_mode),
uid: 0,
gid: 0,
kind: NodeKind::Directory {
children: root_draft.children,
data: root_draft.data,
},
block_start: Some(layout.first_data_block),
block_count: root_dir_blocks.max(1),
});
all_plans.extend(plans);
all_plans.sort_by_key(|plan| plan.inode);
let block_bitmaps = build_block_bitmaps_for_plan(&layout, &all_plans);
let inode_bitmaps = build_inode_bitmaps_for_plan(&layout, &all_plans);
let stats = compute_fs_stats(&layout, &block_bitmaps, &all_plans);
let raw_file = std::fs::File::create(path)?;
raw_file.set_len(options.size_bytes)?;
let mut file = BufWriter::new(raw_file);
write_bitmaps(&mut file, &layout, &block_bitmaps, &inode_bitmaps)?;
write_tree_data(&mut file, &layout, &all_plans)?;
write_inode_table_with_plan(&mut file, &layout, &all_plans)?;
write_journal(&mut file, &layout)?;
let sb_bytes = build_superblock_with_stats(&layout, &stats)?;
write_superblock_at(&mut file, 0, &sb_bytes)?;
let gdt_bytes = build_gdt_with_stats(&layout, &stats, &block_bitmaps, &inode_bitmaps)?;
write_gdt_at(&mut file, 0, &gdt_bytes)?;
for g in 1..layout.num_groups {
if sparse_super_group(g) {
let group_start_block = g as u64 * EXT4_BLOCKS_PER_GROUP as u64;
write_superblock_at(&mut file, group_start_block, &sb_bytes)?;
write_gdt_at(&mut file, group_start_block, &gdt_bytes)?;
}
}
file.flush()?;
Ok(())
}
fn draft_directory(
path: &str,
dir: DirectoryNode,
inode: u32,
parent_inode: u32,
next_inode: &mut u32,
plans: &mut Vec<NodePlan>,
) -> Result<DraftDirectory, Ext4Error> {
if !dir.xattrs.is_empty() {
return Err(Ext4Error::Layout(format!(
"ext4 patch baking does not yet support xattrs on '{path}'"
)));
}
let mut children = Vec::new();
let mut child_dir_count = 0u16;
for (name, node) in dir.entries {
let name_bytes = name.as_os_str().as_encoded_bytes().to_vec();
let child_path = child_path(path, &name_bytes);
let child_inode = *next_inode;
if child_inode >= EXT4_INODES_PER_GROUP {
return Err(Ext4Error::Layout(
"too many upper-layer inodes for group 0 inode table".to_string(),
));
}
*next_inode += 1;
match node {
TreeNode::Directory(child_dir) => {
child_dir_count = child_dir_count.saturating_add(1);
let dir_mode = child_dir.metadata.mode;
let child_draft = draft_directory(
&child_path,
child_dir,
child_inode,
inode,
next_inode,
plans,
)?;
let block_count = blocks_for_len(child_draft.data.len());
plans.push(NodePlan {
inode: child_inode,
path: child_path.clone(),
permissions: normalize_dir_permissions(dir_mode),
uid: 0,
gid: 0,
kind: NodeKind::Directory {
children: child_draft.children,
data: child_draft.data,
},
block_start: None,
block_count,
});
children.push(DirEntrySpec {
inode: child_inode,
file_type: EXT4_FT_DIR,
name: name_bytes,
});
}
TreeNode::RegularFile(file) => {
if !file.xattrs.is_empty() {
return Err(Ext4Error::Layout(format!(
"ext4 patch baking does not yet support xattrs on '{child_path}'"
)));
}
plans.push(NodePlan {
inode: child_inode,
path: child_path.clone(),
permissions: normalize_file_permissions(file.metadata.mode),
uid: 0,
gid: 0,
block_count: blocks_for_len(file.data.len()),
kind: NodeKind::RegularFile {
data: file.data.read_all().map_err(Ext4Error::Io)?,
},
block_start: None,
});
children.push(DirEntrySpec {
inode: child_inode,
file_type: EXT4_FT_REG_FILE,
name: name_bytes,
});
}
TreeNode::Symlink(symlink) => {
let target_len = symlink.target.len();
let inline = target_len <= 59;
let block_count = if inline {
0
} else {
blocks_for_len(target_len)
};
plans.push(NodePlan {
inode: child_inode,
path: child_path.clone(),
permissions: 0o777,
uid: 0,
gid: 0,
kind: NodeKind::Symlink {
target: symlink.target,
inline,
},
block_start: None,
block_count,
});
children.push(DirEntrySpec {
inode: child_inode,
file_type: EXT4_FT_SYMLINK,
name: name_bytes,
});
}
TreeNode::CharDevice(device) => {
plans.push(NodePlan {
inode: child_inode,
path: child_path.clone(),
permissions: 0,
uid: 0,
gid: 0,
kind: NodeKind::CharDevice {
major: device.major,
minor: device.minor,
},
block_start: None,
block_count: 0,
});
children.push(DirEntrySpec {
inode: child_inode,
file_type: EXT4_FT_CHRDEV,
name: name_bytes,
});
}
_ => {
return Err(Ext4Error::Layout(format!(
"unsupported upper-layer node at '{child_path}'"
)));
}
}
}
let data = build_directory_data(inode, parent_inode, &children, path)?;
Ok(DraftDirectory {
children: child_dir_count,
data,
})
}
fn child_path(parent: &str, name: &[u8]) -> String {
let name = String::from_utf8_lossy(name);
if parent == "/" {
format!("/{name}")
} else {
format!("{parent}/{name}")
}
}
fn normalize_file_permissions(mode: u16) -> u16 {
let perms = mode & 0o7777;
if perms == 0 { 0o644 } else { perms }
}
fn normalize_dir_permissions(mode: u16) -> u16 {
let perms = mode & 0o7777;
if perms == 0 { 0o755 } else { perms }
}
fn blocks_for_len(len: usize) -> u32 {
if len == 0 {
0
} else {
(len as u64).div_ceil(EXT4_BLOCK_SIZE as u64) as u32
}
}
fn build_directory_data(
dir_inode: u32,
parent_inode: u32,
children: &[DirEntrySpec],
path: &str,
) -> Result<Vec<u8>, Ext4Error> {
let mut entries = Vec::with_capacity(children.len() + 2);
entries.push(DirEntrySpec {
inode: dir_inode,
file_type: EXT4_FT_DIR,
name: b".".to_vec(),
});
entries.push(DirEntrySpec {
inode: parent_inode,
file_type: EXT4_FT_DIR,
name: b"..".to_vec(),
});
entries.extend(children.iter().map(|entry| DirEntrySpec {
inode: entry.inode,
file_type: entry.file_type,
name: entry.name.clone(),
}));
let mut blocks = Vec::new();
let mut index = 0usize;
while index < entries.len() {
let mut block = vec![0u8; EXT4_BLOCK_SIZE as usize];
let mut pos = 0usize;
let data_limit = EXT4_BLOCK_SIZE as usize - 12;
let block_start = index;
while index < entries.len() {
let min_len = dir_entry_len(entries[index].name.len());
let needed = if pos == 0 { min_len } else { pos + min_len };
if needed > data_limit {
if pos == 0 {
return Err(Ext4Error::Layout(format!(
"directory entry too large for '{path}'"
)));
}
break;
}
pos += min_len;
index += 1;
}
let mut write_pos = 0usize;
for (entry_index, entry) in entries[block_start..index].iter().enumerate() {
let is_last = entry_index + 1 == index - block_start;
let rec_len = if is_last {
(data_limit - write_pos) as u16
} else {
dir_entry_len(entry.name.len()) as u16
};
put_le32(&mut block, write_pos, entry.inode);
put_le16(&mut block, write_pos + 4, rec_len);
block[write_pos + 6] = entry.name.len() as u8;
block[write_pos + 7] = entry.file_type;
block[write_pos + 8..write_pos + 8 + entry.name.len()].copy_from_slice(&entry.name);
write_pos += rec_len as usize;
}
let tail = data_limit;
put_le32(&mut block, tail, 0);
put_le16(&mut block, tail + 4, 12);
block[tail + 6] = 0;
block[tail + 7] = 0xDE;
blocks.extend_from_slice(&block);
}
if blocks.is_empty() {
let mut block = vec![0u8; EXT4_BLOCK_SIZE as usize];
put_le32(&mut block, 0, dir_inode);
put_le16(&mut block, 4, 12);
block[6] = 1;
block[7] = EXT4_FT_DIR;
block[8] = b'.';
put_le32(&mut block, 12, parent_inode);
put_le16(&mut block, 16, (EXT4_BLOCK_SIZE - 24) as u16);
block[18] = 2;
block[19] = EXT4_FT_DIR;
block[20] = b'.';
block[21] = b'.';
let tail = EXT4_BLOCK_SIZE as usize - 12;
put_le32(&mut block, tail, 0);
put_le16(&mut block, tail + 4, 12);
block[tail + 7] = 0xDE;
blocks = block;
}
Ok(blocks)
}
fn dir_entry_len(name_len: usize) -> usize {
(8 + name_len + 3) & !3
}
fn allocate_node_data(allocator: &mut DataAllocator, plan: &mut NodePlan) -> Result<(), Ext4Error> {
if plan.block_count == 0 {
plan.block_start = None;
return Ok(());
}
plan.block_start = allocator.allocate(plan.block_count, &plan.path)?;
Ok(())
}
impl DataAllocator {
fn new(layout: &Layout) -> Self {
let mut regions = Vec::new();
for group in 0..layout.num_groups {
let group_start = layout.group_start_block(group);
let group_end = group_start + layout.blocks_in_group(group);
let start = layout.group_data_start_block(group);
if start < group_end {
regions.push((start, group_end - start));
}
}
Self { regions }
}
fn allocate(&mut self, blocks: u32, path: &str) -> Result<Option<u32>, Ext4Error> {
if blocks == 0 {
return Ok(None);
}
for region in &mut self.regions {
if region.1 >= blocks {
let start = region.0;
region.0 += blocks;
region.1 -= blocks;
return Ok(Some(start));
}
}
Err(Ext4Error::Layout(format!(
"not enough space in upper.ext4 for '{path}'"
)))
}
}
fn build_block_bitmaps_for_plan(layout: &Layout, plans: &[NodePlan]) -> Vec<Vec<u8>> {
let mut used_extents = Vec::new();
used_extents.push((
layout.first_data_block,
layout.journal_start_block - layout.first_data_block,
));
used_extents.push((layout.journal_start_block, layout.journal_blocks));
for plan in plans {
if let Some(start) = plan.block_start
&& plan.block_count > 0
{
used_extents.push((start, plan.block_count));
}
}
(0..layout.num_groups)
.map(|group| {
let mut bitmap = vec![0u8; EXT4_BLOCK_SIZE as usize];
let group_start = layout.group_start_block(group);
let group_end = group_start + layout.blocks_in_group(group);
for bit in 0..layout.group_metadata_blocks(group) {
bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
}
for (start, len) in &used_extents {
let extent_start = *start;
let extent_end = extent_start + *len;
let overlap_start = extent_start.max(group_start);
let overlap_end = extent_end.min(group_end);
if overlap_start < overlap_end {
for block in overlap_start..overlap_end {
let bit = block - group_start;
bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
}
}
}
let blocks_in_group = layout.blocks_in_group(group);
for bit in blocks_in_group..EXT4_BLOCKS_PER_GROUP {
bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
}
bitmap
})
.collect()
}
fn build_inode_bitmaps_for_plan(layout: &Layout, plans: &[NodePlan]) -> Vec<Vec<u8>> {
let max_used_inode = plans
.iter()
.map(|plan| plan.inode)
.max()
.unwrap_or(EXT4_JOURNAL_INO)
.max(EXT4_FIRST_INO - 1);
(0..layout.num_groups)
.map(|group| {
let mut bitmap = vec![0u8; EXT4_BLOCK_SIZE as usize];
if group == 0 {
for bit in 0..max_used_inode {
bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
}
}
for bit in EXT4_INODES_PER_GROUP..(EXT4_BLOCK_SIZE * 8) {
bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
}
bitmap
})
.collect()
}
fn compute_fs_stats(layout: &Layout, block_bitmaps: &[Vec<u8>], plans: &[NodePlan]) -> FsStats {
let max_used_inode = plans
.iter()
.map(|plan| plan.inode)
.max()
.unwrap_or(EXT4_JOURNAL_INO)
.max(EXT4_FIRST_INO - 1);
let dir_count = plans
.iter()
.filter(|plan| matches!(plan.kind, NodeKind::Directory { .. }))
.count() as u32;
let mut group_free_blocks = Vec::with_capacity(layout.num_groups as usize);
let mut total_free_blocks = 0u64;
let mut total_used_blocks = 0u64;
for (group, bitmap) in block_bitmaps
.iter()
.enumerate()
.take(layout.num_groups as usize)
{
let blocks_in_group = layout.blocks_in_group(group as u32) as usize;
let used = count_used_bits(bitmap, blocks_in_group);
let free = blocks_in_group.saturating_sub(used) as u32;
group_free_blocks.push(free);
total_free_blocks += free as u64;
total_used_blocks += used as u64;
}
let mut group_free_inodes = vec![EXT4_INODES_PER_GROUP; layout.num_groups as usize];
group_free_inodes[0] = EXT4_INODES_PER_GROUP - max_used_inode;
let total_free_inodes = group_free_inodes.iter().map(|count| *count as u64).sum();
let mut group_used_dirs = vec![0u32; layout.num_groups as usize];
group_used_dirs[0] = dir_count;
FsStats {
group_free_blocks,
group_free_inodes,
group_used_dirs,
total_free_blocks,
total_free_inodes,
total_used_blocks,
}
}
fn count_used_bits(bitmap: &[u8], bits: usize) -> usize {
let full_bytes = bits / 8;
let mut used: usize = bitmap[..full_bytes]
.iter()
.map(|b| b.count_ones() as usize)
.sum();
let remaining = bits % 8;
if remaining > 0 {
let mask = (1u8 << remaining) - 1;
used += (bitmap[full_bytes] & mask).count_ones() as usize;
}
used
}
fn write_tree_data(
file: &mut (impl std::io::Write + std::io::Seek),
layout: &Layout,
plans: &[NodePlan],
) -> Result<(), Ext4Error> {
for plan in plans {
match &plan.kind {
NodeKind::Directory { data, .. } => {
let start = plan.block_start.unwrap_or(layout.first_data_block);
let mut bytes = data.clone();
update_dir_block_checksums(layout.csum_seed, plan.inode, &mut bytes);
write_extent_bytes(file, start, &bytes)?;
}
NodeKind::RegularFile { data } => {
if let Some(start) = plan.block_start {
write_extent_bytes(file, start, data)?;
}
}
NodeKind::Symlink { target, inline } => {
if !inline && let Some(start) = plan.block_start {
write_extent_bytes(file, start, target)?;
}
}
NodeKind::CharDevice { .. } => {}
}
}
Ok(())
}
fn write_extent_bytes(
file: &mut (impl std::io::Write + std::io::Seek),
start_block: u32,
data: &[u8],
) -> Result<(), Ext4Error> {
let offset = start_block as u64 * EXT4_BLOCK_SIZE as u64;
file.seek(SeekFrom::Start(offset))?;
file.write_all(data)?;
let pad = (EXT4_BLOCK_SIZE as usize - (data.len() % EXT4_BLOCK_SIZE as usize))
% EXT4_BLOCK_SIZE as usize;
if pad > 0 {
static ZEROS: [u8; 4096] = [0u8; 4096];
file.write_all(&ZEROS[..pad])?;
}
Ok(())
}
fn update_dir_block_checksums(csum_seed: u32, inode: u32, data: &mut [u8]) {
for chunk in data.chunks_exact_mut(EXT4_BLOCK_SIZE as usize) {
let tail = EXT4_BLOCK_SIZE as usize - 12;
let checksum = dir_block_checksum(csum_seed, inode, 0, &chunk[..tail]);
put_le32(chunk, tail + 8, checksum);
}
}
fn write_inode_table_with_plan(
file: &mut (impl std::io::Write + std::io::Seek),
layout: &Layout,
plans: &[NodePlan],
) -> Result<(), Ext4Error> {
let table_offset = layout.inode_table_block as u64 * EXT4_BLOCK_SIZE as u64;
let root_inode = build_inode_from_plan(layout, &plans[0])?;
let root_offset = table_offset + (EXT4_ROOT_INO as u64 - 1) * EXT4_INODE_SIZE as u64;
file.seek(SeekFrom::Start(root_offset))?;
file.write_all(&root_inode)?;
let journal_inode = build_journal_inode(layout);
let journal_offset = table_offset + (EXT4_JOURNAL_INO as u64 - 1) * EXT4_INODE_SIZE as u64;
file.seek(SeekFrom::Start(journal_offset))?;
file.write_all(&journal_inode)?;
for plan in plans.iter().filter(|plan| plan.inode >= EXT4_FIRST_INO) {
let inode_bytes = build_inode_from_plan(layout, plan)?;
let inode_offset = table_offset + (plan.inode as u64 - 1) * EXT4_INODE_SIZE as u64;
file.seek(SeekFrom::Start(inode_offset))?;
file.write_all(&inode_bytes)?;
}
Ok(())
}
fn build_inode_from_plan(layout: &Layout, plan: &NodePlan) -> Result<Vec<u8>, Ext4Error> {
let mut inode = vec![0u8; EXT4_INODE_SIZE as usize];
let (mode, size, links_count, extents) = match &plan.kind {
NodeKind::Directory { children, data } => (
S_IFDIR | normalize_dir_permissions(plan.permissions),
data.len() as u64,
2 + *children,
true,
),
NodeKind::RegularFile { data } => (
S_IFREG | normalize_file_permissions(plan.permissions),
data.len() as u64,
1,
true,
),
NodeKind::Symlink { target, inline } => (S_IFLNK | 0o777, target.len() as u64, 1, !inline),
NodeKind::CharDevice { .. } => (S_IFCHR | plan.permissions, 0, 1, false),
};
put_le16(&mut inode, 0x00, mode);
put_le16(&mut inode, 0x02, plan.uid);
put_le32(&mut inode, 0x04, size as u32);
put_le16(&mut inode, 0x18, plan.gid);
put_le16(&mut inode, 0x1A, links_count);
put_le32(&mut inode, 0x1C, plan.block_count * (EXT4_BLOCK_SIZE / 512));
if extents {
put_le32(&mut inode, 0x20, EXT4_EXTENTS_FL);
}
match &plan.kind {
NodeKind::Directory { .. } | NodeKind::RegularFile { .. } => {
if let Some(start) = plan.block_start {
write_extent_tree(&mut inode, 0x28, start, plan.block_count as u16);
} else {
write_empty_extent_tree(&mut inode, 0x28);
}
}
NodeKind::Symlink { target, inline } => {
if *inline {
inode[0x28..0x28 + target.len()].copy_from_slice(target);
} else if let Some(start) = plan.block_start {
write_extent_tree(&mut inode, 0x28, start, plan.block_count as u16);
}
}
NodeKind::CharDevice { major, minor } => {
put_le32(&mut inode, 0x28, (*minor & 0xFF) | (major << 8));
}
}
put_le32(&mut inode, 0x64, 0);
put_le32(&mut inode, 0x6C, (size >> 32) as u32);
put_le16(&mut inode, 0x80, EXT4_MIN_EXTRA_ISIZE);
let csum = inode_checksum(layout.csum_seed, plan.inode, 0, &inode);
put_le16(&mut inode, 0x7C, csum as u16);
put_le16(&mut inode, 0x82, (csum >> 16) as u16);
Ok(inode)
}
fn write_empty_extent_tree(buf: &mut [u8], offset: usize) {
put_le16(buf, offset, EXT4_EH_MAGIC);
put_le16(buf, offset + 2, 0);
put_le16(buf, offset + 4, 4);
put_le16(buf, offset + 6, 0);
put_le32(buf, offset + 8, 0);
}
fn build_superblock_with_stats(layout: &Layout, stats: &FsStats) -> Result<Vec<u8>, Ext4Error> {
let mut block = build_superblock(layout)?;
let sb = &mut block[1024..2048];
put_le32(sb, 0x0C, stats.total_free_blocks as u32);
put_le32(sb, 0x10, stats.total_free_inodes as u32);
put_le32(sb, 0x158, (stats.total_free_blocks >> 32) as u32);
put_le32(sb, 0x194, stats.total_used_blocks as u32);
put_le32(sb, 0x3FC, 0);
let checksum = crc32c::crc32c_raw(0xFFFF_FFFF, &sb[..0x3FC]);
put_le32(sb, 0x3FC, checksum);
Ok(block)
}
fn build_gdt_with_stats(
layout: &Layout,
stats: &FsStats,
block_bitmaps: &[Vec<u8>],
inode_bitmaps: &[Vec<u8>],
) -> Result<Vec<u8>, Ext4Error> {
let desc_size = EXT4_DESC_SIZE as usize;
let mut gdt = vec![0u8; layout.num_groups as usize * desc_size];
for g in 0..layout.num_groups {
let off = g as usize * desc_size;
let desc = &mut gdt[off..off + desc_size];
let bb = layout.group_block_bitmap_block(g);
let ib = layout.group_inode_bitmap_block(g);
let it = layout.group_inode_table_block(g);
let bb_csum = bitmap_checksum(
layout.csum_seed,
&block_bitmaps[g as usize],
EXT4_BLOCK_SIZE as usize,
);
let ib_csum = bitmap_checksum(
layout.csum_seed,
&inode_bitmaps[g as usize],
(EXT4_INODES_PER_GROUP / 8) as usize,
);
put_le32(desc, 0x00, bb);
put_le32(desc, 0x04, ib);
put_le32(desc, 0x08, it);
put_le16(desc, 0x0C, stats.group_free_blocks[g as usize] as u16);
put_le16(desc, 0x0E, stats.group_free_inodes[g as usize] as u16);
put_le16(desc, 0x10, stats.group_used_dirs[g as usize] as u16);
put_le16(desc, 0x12, EXT4_BG_INODE_ZEROED);
put_le16(desc, 0x18, bb_csum as u16);
put_le16(desc, 0x1A, ib_csum as u16);
put_le16(desc, 0x1C, stats.group_free_inodes[g as usize] as u16);
put_le16(desc, 0x38, (bb_csum >> 16) as u16);
put_le16(desc, 0x3A, (ib_csum >> 16) as u16);
put_le16(desc, 0x1E, 0);
let checksum = gdt_checksum(layout.csum_seed, g, desc);
put_le16(desc, 0x1E, checksum);
}
Ok(gdt)
}
#[cfg(test)]
fn build_block_bitmaps(layout: &Layout) -> Vec<Vec<u8>> {
(0..layout.num_groups)
.map(|group| build_block_bitmap(layout, group))
.collect()
}
#[cfg(test)]
fn build_inode_bitmaps(layout: &Layout) -> Vec<Vec<u8>> {
(0..layout.num_groups)
.map(|group| build_inode_bitmap(layout, group))
.collect()
}
#[cfg(test)]
fn build_block_bitmap(layout: &Layout, group: u32) -> Vec<u8> {
let mut bitmap = vec![0u8; EXT4_BLOCK_SIZE as usize];
let used = layout.group_used_blocks(group);
for bit in 0..used {
bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
}
let blocks_in_group = layout.blocks_in_group(group);
for bit in blocks_in_group..EXT4_BLOCKS_PER_GROUP {
bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
}
bitmap
}
#[cfg(test)]
fn build_inode_bitmap(_layout: &Layout, group: u32) -> Vec<u8> {
let mut bitmap = vec![0u8; EXT4_BLOCK_SIZE as usize];
if group == 0 {
for bit in 0..(EXT4_FIRST_INO - 1) {
bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
}
}
for bit in EXT4_INODES_PER_GROUP..(EXT4_BLOCK_SIZE * 8) {
bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
}
bitmap
}
fn write_bitmaps(
file: &mut (impl std::io::Write + std::io::Seek),
layout: &Layout,
block_bitmaps: &[Vec<u8>],
inode_bitmaps: &[Vec<u8>],
) -> Result<(), Ext4Error> {
for group in 0..layout.num_groups as usize {
let block_offset =
layout.group_block_bitmap_block(group as u32) as u64 * EXT4_BLOCK_SIZE as u64;
file.seek(SeekFrom::Start(block_offset))?;
file.write_all(&block_bitmaps[group])?;
let inode_offset =
layout.group_inode_bitmap_block(group as u32) as u64 * EXT4_BLOCK_SIZE as u64;
file.seek(SeekFrom::Start(inode_offset))?;
file.write_all(&inode_bitmaps[group])?;
}
Ok(())
}
fn build_journal_inode(layout: &Layout) -> Vec<u8> {
let mut inode = vec![0u8; EXT4_INODE_SIZE as usize];
let mode = S_IFREG | 0o600;
let size = layout.journal_blocks as u64 * EXT4_BLOCK_SIZE as u64;
put_le16(&mut inode, 0x00, mode);
put_le32(&mut inode, 0x04, size as u32);
put_le32(&mut inode, 0x6C, (size >> 32) as u32);
put_le16(&mut inode, 0x1A, 1);
let sectors = (layout.journal_blocks as u64 * EXT4_BLOCK_SIZE as u64) / 512;
put_le32(&mut inode, 0x1C, sectors as u32);
put_le32(&mut inode, 0x20, EXT4_EXTENTS_FL);
write_extent_tree(
&mut inode,
0x28,
layout.journal_start_block,
layout.journal_blocks as u16,
);
put_le32(&mut inode, 0x64, 0);
put_le16(&mut inode, 0x80, EXT4_MIN_EXTRA_ISIZE);
let csum = inode_checksum(layout.csum_seed, EXT4_JOURNAL_INO, 0, &inode);
put_le16(&mut inode, 0x7C, csum as u16);
put_le16(&mut inode, 0x82, (csum >> 16) as u16);
inode
}
fn write_extent_tree(buf: &mut [u8], offset: usize, start_block: u32, block_count: u16) {
put_le16(buf, offset, EXT4_EH_MAGIC); put_le16(buf, offset + 2, 1); put_le16(buf, offset + 4, 4); put_le16(buf, offset + 6, 0); put_le32(buf, offset + 8, 0);
let ext_off = offset + 12;
put_le32(buf, ext_off, 0); put_le16(buf, ext_off + 4, block_count); put_le16(buf, ext_off + 6, 0); put_le32(buf, ext_off + 8, start_block); }
fn write_journal(
file: &mut (impl std::io::Write + std::io::Seek),
layout: &Layout,
) -> Result<(), Ext4Error> {
let mut jsb = vec![0u8; EXT4_BLOCK_SIZE as usize];
put_be32(&mut jsb, 0, JBD2_MAGIC); put_be32(&mut jsb, 4, JBD2_SUPERBLOCK_V2); put_be32(&mut jsb, 8, 0);
put_be32(&mut jsb, 12, EXT4_BLOCK_SIZE); put_be32(&mut jsb, 16, layout.journal_blocks); put_be32(&mut jsb, 20, 1); put_be32(&mut jsb, 24, 1); put_be32(&mut jsb, 28, 0);
put_be32(&mut jsb, 32, 0);
put_be32(&mut jsb, 36, 0);
put_be32(&mut jsb, 40, 0x13);
put_be32(&mut jsb, 44, 0);
jsb[48..64].copy_from_slice(&layout.uuid);
put_be32(&mut jsb, 64, 1);
put_be32(&mut jsb, 68, 0);
put_be32(&mut jsb, 72, 0);
put_be32(&mut jsb, 76, 0);
jsb[0x50] = 4;
let jsb_csum = crc32c::crc32c_raw(0xFFFF_FFFF, &jsb[..JBD2_SUPERBLOCK_SIZE]);
put_be32(&mut jsb, 0xFC, jsb_csum);
let offset = layout.journal_start_block as u64 * EXT4_BLOCK_SIZE as u64;
file.seek(SeekFrom::Start(offset))?;
file.write_all(&jsb)?;
Ok(())
}
fn build_superblock(layout: &Layout) -> Result<Vec<u8>, Ext4Error> {
let mut block = vec![0u8; EXT4_BLOCK_SIZE as usize];
let sb = &mut block[1024..2048];
let total_blocks = layout.num_blocks;
let total_inodes = layout.num_groups as u64 * EXT4_INODES_PER_GROUP as u64;
let free_blocks = layout.total_free_blocks();
let free_inodes = layout.total_free_inodes();
put_le32(sb, 0x00, total_inodes as u32);
put_le32(sb, 0x04, total_blocks as u32);
put_le32(sb, 0x08, 0);
put_le32(sb, 0x0C, free_blocks as u32);
put_le32(sb, 0x10, free_inodes as u32);
put_le32(sb, 0x14, 0);
put_le32(sb, 0x18, EXT4_LOG_BLOCK_SIZE);
put_le32(sb, 0x1C, EXT4_LOG_BLOCK_SIZE);
put_le32(sb, 0x20, EXT4_BLOCKS_PER_GROUP);
put_le32(sb, 0x24, EXT4_BLOCKS_PER_GROUP);
put_le32(sb, 0x28, EXT4_INODES_PER_GROUP);
put_le16(sb, 0x34, 0);
put_le16(sb, 0x36, 0xFFFF);
put_le16(sb, 0x38, EXT4_SUPER_MAGIC);
put_le16(sb, 0x3A, 1);
put_le16(sb, 0x3C, 1);
put_le16(sb, 0x3E, 0);
put_le32(sb, 0x48, 0);
put_le32(sb, 0x4C, 1);
put_le16(sb, 0x50, 0);
put_le16(sb, 0x52, 0);
put_le32(sb, 0x54, EXT4_FIRST_INO);
put_le16(sb, 0x58, EXT4_INODE_SIZE);
put_le16(sb, 0x5A, 0);
put_le32(sb, 0x5C, layout.feature_compat);
put_le32(sb, 0x60, layout.feature_incompat);
put_le32(sb, 0x64, layout.feature_ro_compat);
sb[0x68..0x78].copy_from_slice(&layout.uuid);
put_le32(sb, 0xC8, 0);
sb[0xCC] = 0;
sb[0xCD] = 0;
put_le16(sb, 0xCE, RESERVED_GDT_BLOCKS as u16);
put_le32(sb, 0xE0, EXT4_JOURNAL_INO);
put_le32(sb, 0xE4, 0);
put_le32(sb, 0xE8, 0);
sb[0xEC..0xFC].copy_from_slice(&layout.uuid);
sb[0xFC] = 1;
sb[0xFD] = 1;
put_le16(sb, 0xFE, EXT4_DESC_SIZE);
put_le32(sb, 0x100, 0x000C);
put_le32(sb, 0x104, 0);
{
let mut extent_buf = [0u8; 60];
write_extent_tree(
&mut extent_buf,
0,
layout.journal_start_block,
layout.journal_blocks as u16,
);
sb[0x10C..0x10C + 60].copy_from_slice(&extent_buf);
let jsize = layout.journal_blocks as u64 * EXT4_BLOCK_SIZE as u64;
put_le32(sb, 0x10C + 60, jsize as u32);
put_le32(sb, 0x10C + 64, (jsize >> 32) as u32);
}
put_le32(sb, 0x150, (total_blocks >> 32) as u32);
put_le32(sb, 0x154, 0);
put_le32(sb, 0x158, (free_blocks >> 32) as u32);
put_le16(sb, 0x15C, EXT4_MIN_EXTRA_ISIZE);
put_le16(sb, 0x15E, EXT4_MIN_EXTRA_ISIZE);
put_le32(sb, 0x160, 0);
sb[0x174] = 0;
sb[0x175] = 1;
put_le32(sb, 0x194, layout.total_used_blocks() as u32);
put_le32(sb, 0x270, 0);
put_le16(sb, 0x27C, 0);
let sb_csum = crc32c::crc32c_raw(0xFFFF_FFFF, &sb[..0x3FC]);
put_le32(sb, 0x3FC, sb_csum);
Ok(block)
}
#[cfg(test)]
fn build_gdt(
layout: &Layout,
block_bitmaps: &[Vec<u8>],
inode_bitmaps: &[Vec<u8>],
) -> Result<Vec<u8>, Ext4Error> {
let desc_size = EXT4_DESC_SIZE as usize;
let mut gdt = vec![0u8; layout.num_groups as usize * desc_size];
for g in 0..layout.num_groups {
let off = g as usize * desc_size;
let desc = &mut gdt[off..off + desc_size];
let bb = layout.group_block_bitmap_block(g);
let ib = layout.group_inode_bitmap_block(g);
let it = layout.group_inode_table_block(g);
let bb_csum = bitmap_checksum(
layout.csum_seed,
&block_bitmaps[g as usize],
EXT4_BLOCK_SIZE as usize,
);
let ib_csum = bitmap_checksum(
layout.csum_seed,
&inode_bitmaps[g as usize],
(EXT4_INODES_PER_GROUP / 8) as usize,
);
put_le32(desc, 0x00, bb);
put_le32(desc, 0x04, ib);
put_le32(desc, 0x08, it);
put_le16(desc, 0x0C, layout.group_free_blocks(g) as u16);
put_le16(desc, 0x0E, layout.group_free_inodes(g) as u16);
put_le16(desc, 0x10, layout.group_used_dirs(g) as u16);
put_le16(desc, 0x12, EXT4_BG_INODE_ZEROED);
put_le32(desc, 0x14, 0);
put_le16(desc, 0x18, bb_csum as u16);
put_le16(desc, 0x1A, ib_csum as u16);
put_le16(desc, 0x1C, layout.group_free_inodes(g) as u16);
put_le32(desc, 0x20, 0);
put_le32(desc, 0x24, 0);
put_le32(desc, 0x28, 0);
put_le16(desc, 0x2C, 0);
put_le16(desc, 0x2E, 0);
put_le16(desc, 0x30, 0);
put_le16(desc, 0x32, 0);
put_le32(desc, 0x34, 0);
put_le16(desc, 0x38, (bb_csum >> 16) as u16);
put_le16(desc, 0x3A, (ib_csum >> 16) as u16);
put_le16(desc, 0x1E, 0);
let gdt_csum = gdt_checksum(layout.csum_seed, g, desc);
put_le16(desc, 0x1E, gdt_csum);
}
Ok(gdt)
}
fn write_superblock_at(
file: &mut (impl std::io::Write + std::io::Seek),
group_start_block: u64,
sb_block: &[u8],
) -> Result<(), Ext4Error> {
let offset = group_start_block * EXT4_BLOCK_SIZE as u64;
file.seek(SeekFrom::Start(offset))?;
file.write_all(sb_block)?;
Ok(())
}
fn write_gdt_at(
file: &mut (impl std::io::Write + std::io::Seek),
group_start_block: u64,
gdt: &[u8],
) -> Result<(), Ext4Error> {
let offset = (group_start_block + 1) * EXT4_BLOCK_SIZE as u64;
file.seek(SeekFrom::Start(offset))?;
file.write_all(gdt)?;
Ok(())
}
fn gdt_checksum(csum_seed: u32, group: u32, desc: &[u8]) -> u16 {
let mut crc = crc32c::crc32c_raw(csum_seed, &group.to_le_bytes());
crc = crc32c::crc32c_raw(crc, desc);
(crc & 0xFFFF) as u16
}
fn inode_checksum(csum_seed: u32, inum: u32, generation: u32, inode_bytes: &[u8]) -> u32 {
let mut crc = crc32c::crc32c_raw(csum_seed, &inum.to_le_bytes());
crc = crc32c::crc32c_raw(crc, &generation.to_le_bytes());
crc = crc32c::crc32c_raw(crc, &inode_bytes[..0x7C]);
crc = crc32c::crc32c_raw(crc, &[0u8; 2]);
crc = crc32c::crc32c_raw(crc, &inode_bytes[0x7E..0x82]);
crc = crc32c::crc32c_raw(crc, &[0u8; 2]);
crc = crc32c::crc32c_raw(crc, &inode_bytes[0x84..]);
crc
}
fn bitmap_checksum(csum_seed: u32, bitmap: &[u8], checksum_len: usize) -> u32 {
crc32c::crc32c_raw(csum_seed, &bitmap[..checksum_len])
}
fn dir_block_checksum(csum_seed: u32, inum: u32, generation: u32, data: &[u8]) -> u32 {
let mut crc = crc32c::crc32c_raw(csum_seed, &inum.to_le_bytes());
crc = crc32c::crc32c_raw(crc, &generation.to_le_bytes());
crc = crc32c::crc32c_raw(crc, data);
crc
}
fn put_le16(buf: &mut [u8], off: usize, val: u16) {
buf[off..off + 2].copy_from_slice(&val.to_le_bytes());
}
fn put_le32(buf: &mut [u8], off: usize, val: u32) {
buf[off..off + 4].copy_from_slice(&val.to_le_bytes());
}
fn put_be32(buf: &mut [u8], off: usize, val: u32) {
buf[off..off + 4].copy_from_slice(&val.to_be_bytes());
}
pub use format::sparse_super_group;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_creates_file_of_correct_size() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.ext4");
let size: u64 = 256 * 1024 * 1024; let opts = Ext4FormatOptions {
size_bytes: size,
journal_blocks: 4096, };
format_ext4(&path, &opts).unwrap();
let meta = std::fs::metadata(&path).unwrap();
assert_eq!(meta.len(), size);
}
#[test]
fn test_format_too_small() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("tiny.ext4");
let opts = Ext4FormatOptions {
size_bytes: 4096, journal_blocks: 16384,
};
let result = format_ext4(&path, &opts);
assert!(matches!(result, Err(Ext4Error::TooSmall)));
}
#[test]
fn test_format_default_options() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("default.ext4");
let opts = Ext4FormatOptions::default();
format_ext4(&path, &opts).unwrap();
let meta = std::fs::metadata(&path).unwrap();
assert_eq!(meta.len(), DEFAULT_SIZE_BYTES);
}
#[test]
fn test_superblock_magic() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("magic.ext4");
let opts = Ext4FormatOptions {
size_bytes: 256 * 1024 * 1024,
journal_blocks: 4096,
};
format_ext4(&path, &opts).unwrap();
let data = std::fs::read(&path).unwrap();
let magic = u16::from_le_bytes([data[1024 + 0x38], data[1024 + 0x39]]);
assert_eq!(magic, EXT4_SUPER_MAGIC);
}
#[test]
fn test_journal_magic() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("journal.ext4");
let opts = Ext4FormatOptions {
size_bytes: 256 * 1024 * 1024,
journal_blocks: 4096,
};
format_ext4(&path, &opts).unwrap();
let layout = Layout::compute(&opts).unwrap();
let data = std::fs::read(&path).unwrap();
let jsb_offset = layout.journal_start_block as usize * EXT4_BLOCK_SIZE as usize;
let magic = u32::from_be_bytes([
data[jsb_offset],
data[jsb_offset + 1],
data[jsb_offset + 2],
data[jsb_offset + 3],
]);
assert_eq!(magic, JBD2_MAGIC);
}
#[test]
fn test_root_dir_inode_exists() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("rootdir.ext4");
let opts = Ext4FormatOptions {
size_bytes: 256 * 1024 * 1024,
journal_blocks: 4096,
};
format_ext4(&path, &opts).unwrap();
let layout = Layout::compute(&opts).unwrap();
let data = std::fs::read(&path).unwrap();
let inode_offset = layout.inode_table_block as usize * EXT4_BLOCK_SIZE as usize
+ (EXT4_INODE_SIZE as usize);
let mode = u16::from_le_bytes([data[inode_offset], data[inode_offset + 1]]);
assert_eq!(mode, S_IFDIR | 0o755);
}
#[test]
fn test_backup_group_bitmap_starts_after_backup_metadata() {
let opts = Ext4FormatOptions {
size_bytes: 256 * 1024 * 1024,
journal_blocks: 4096,
};
let layout = Layout::compute(&opts).unwrap();
let block_bitmaps = build_block_bitmaps(&layout);
let inode_bitmaps = build_inode_bitmaps(&layout);
let gdt = build_gdt(&layout, &block_bitmaps, &inode_bitmaps).unwrap();
let desc = &gdt[EXT4_DESC_SIZE as usize..(2 * EXT4_DESC_SIZE as usize)];
let block_bitmap = u32::from_le_bytes([desc[0], desc[1], desc[2], desc[3]]);
let group_start = layout.group_start_block(1);
assert_eq!(block_bitmap, layout.group_block_bitmap_block(1));
assert!(block_bitmap > group_start + layout.gdt_blocks - 1);
}
#[test]
fn test_inode_bitmap_padding_is_marked_used() {
let layout = Layout::compute(&Ext4FormatOptions {
size_bytes: 256 * 1024 * 1024,
journal_blocks: 4096,
})
.unwrap();
let bitmap = build_inode_bitmap(&layout, 0);
for bit in EXT4_INODES_PER_GROUP..(EXT4_BLOCK_SIZE * 8) {
assert_ne!(bitmap[(bit / 8) as usize] & (1 << (bit % 8)), 0);
}
}
#[test]
fn test_journal_superblock_checksum_matches_contents() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("journal-csum.ext4");
let opts = Ext4FormatOptions {
size_bytes: 256 * 1024 * 1024,
journal_blocks: 4096,
};
format_ext4(&path, &opts).unwrap();
let layout = Layout::compute(&opts).unwrap();
let data = std::fs::read(&path).unwrap();
let offset = layout.journal_start_block as usize * EXT4_BLOCK_SIZE as usize;
let mut jsb = data[offset..offset + JBD2_SUPERBLOCK_SIZE].to_vec();
let stored = u32::from_be_bytes([jsb[0xFC], jsb[0xFD], jsb[0xFE], jsb[0xFF]]);
jsb[0xFC..0x100].fill(0);
let expected = crc32c::crc32c_raw(0xFFFF_FFFF, &jsb);
assert_eq!(stored, expected);
}
#[test]
fn test_root_dir_checksum_matches_contents() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("rootdir-csum.ext4");
let opts = Ext4FormatOptions {
size_bytes: 256 * 1024 * 1024,
journal_blocks: 4096,
};
format_ext4(&path, &opts).unwrap();
let layout = Layout::compute(&opts).unwrap();
let data = std::fs::read(&path).unwrap();
let sb = &data[1024..2048];
let uuid = &sb[0x68..0x78];
let csum_seed = crc32c::crc32c_raw(0xFFFF_FFFF, uuid);
let block_offset = layout.first_data_block as usize * EXT4_BLOCK_SIZE as usize;
let tail_offset = block_offset + EXT4_BLOCK_SIZE as usize - 12;
let stored = u32::from_le_bytes([
data[tail_offset + 8],
data[tail_offset + 9],
data[tail_offset + 10],
data[tail_offset + 11],
]);
let expected = dir_block_checksum(
csum_seed,
EXT4_ROOT_INO,
0,
&data[block_offset..tail_offset],
);
assert_eq!(stored, expected);
}
}