use super::constants::{GROUP_DESC_SIZE, GROUP_DESC_SIZE_64, INODE_SIZE_DYNAMIC};
#[derive(Debug, Clone)]
pub struct Layout {
pub block_size: u32,
pub blocks_count: u32,
pub inodes_count: u32,
pub first_data_block: u32,
pub blocks_per_group: u32,
pub inodes_per_group: u32,
pub inode_size: u16,
pub desc_size: usize,
pub inode_table_blocks: u32,
pub gdt_blocks: u32,
pub log_groups_per_flex: u8,
pub groups: Vec<GroupLayout>,
}
impl Layout {
pub fn num_groups(&self) -> u32 {
self.groups.len() as u32
}
pub fn flex_size(&self) -> u32 {
if self.log_groups_per_flex == 0 {
1
} else {
1u32 << self.log_groups_per_flex
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct GroupLayout {
pub start_block: u32,
pub end_block: u32,
pub has_superblock: bool,
pub block_bitmap: u32,
pub inode_bitmap: u32,
pub inode_table: u32,
pub data_start: u32,
pub meta_blocks: u32,
}
pub fn group_has_sparse_super(g: u32) -> bool {
if g <= 1 {
return true;
}
is_power_of(g, 3) || is_power_of(g, 5) || is_power_of(g, 7)
}
fn is_power_of(mut n: u32, base: u32) -> bool {
if n == 0 {
return false;
}
while n % base == 0 {
n /= base;
}
n == 1
}
pub fn plan(block_size: u32, blocks_count: u32, inodes_count: u32) -> crate::Result<Layout> {
plan_with(block_size, blocks_count, inodes_count, false)
}
pub fn plan_with(
block_size: u32,
blocks_count: u32,
inodes_count: u32,
sparse_super: bool,
) -> crate::Result<Layout> {
plan_full(block_size, blocks_count, inodes_count, sparse_super, 0)
}
#[derive(Debug, Clone, Copy, Default)]
pub enum SparseSuperMode {
#[default]
All,
Classic,
Two([u32; 2]),
}
impl SparseSuperMode {
pub fn group_has_backup(self, g: u32) -> bool {
if g == 0 {
return true;
}
match self {
SparseSuperMode::All => true,
SparseSuperMode::Classic => group_has_sparse_super(g),
SparseSuperMode::Two([a, b]) => g == a || g == b,
}
}
}
pub fn plan_full(
block_size: u32,
blocks_count: u32,
inodes_count: u32,
sparse_super: bool,
log_groups_per_flex: u8,
) -> crate::Result<Layout> {
let mode = if sparse_super {
SparseSuperMode::Classic
} else {
SparseSuperMode::All
};
plan_layout(
block_size,
blocks_count,
inodes_count,
mode,
log_groups_per_flex,
false,
)
}
pub fn plan_layout(
block_size: u32,
blocks_count: u32,
inodes_count: u32,
sparse_super_mode: SparseSuperMode,
log_groups_per_flex: u8,
use_64bit: bool,
) -> crate::Result<Layout> {
if !block_size.is_power_of_two() || block_size < 1024 {
return Err(crate::Error::InvalidArgument(format!(
"ext: block_size must be a power of two ≥ 1024, got {block_size}"
)));
}
if blocks_count < 32 {
return Err(crate::Error::InvalidArgument(format!(
"ext: blocks_count {blocks_count} too small"
)));
}
if inodes_count < 11 {
return Err(crate::Error::InvalidArgument(format!(
"ext: inodes_count must include the reserved range (≥ 11), got {inodes_count}"
)));
}
if log_groups_per_flex > 5 {
return Err(crate::Error::InvalidArgument(format!(
"ext: log_groups_per_flex {log_groups_per_flex} > 5 (max flex unit = 32 groups)"
)));
}
let first_data_block: u32 = if block_size == 1024 { 1 } else { 0 };
let max_per_group = 8 * block_size;
let blocks_per_group = max_per_group.min(blocks_count);
if blocks_per_group % 8 != 0 {
return Err(crate::Error::InvalidArgument(format!(
"ext: blocks_count {blocks_count} must be a multiple of 8 for a \
single-group filesystem (blocks_per_group must be byte-aligned)"
)));
}
let group_input_blocks = blocks_count - first_data_block;
let num_groups = group_input_blocks.div_ceil(blocks_per_group);
let mut inodes_per_group = inodes_count.div_ceil(num_groups);
let max_inodes_per_group = 8 * block_size;
if inodes_per_group > max_inodes_per_group {
return Err(crate::Error::InvalidArgument(format!(
"ext: too many inodes per group ({inodes_per_group}, max {max_inodes_per_group})"
)));
}
inodes_per_group = inodes_per_group.div_ceil(8) * 8;
let inodes_count = inodes_per_group * num_groups;
let inode_size = INODE_SIZE_DYNAMIC;
let inode_table_bytes = inodes_per_group as u64 * inode_size as u64;
let inode_table_blocks = inode_table_bytes.div_ceil(block_size as u64) as u32;
let desc_size = if use_64bit {
GROUP_DESC_SIZE_64
} else {
GROUP_DESC_SIZE
};
let gdt_bytes = num_groups as u64 * desc_size as u64;
let gdt_blocks = gdt_bytes.div_ceil(block_size as u64) as u32;
let flex_size: u32 = if log_groups_per_flex == 0 {
1
} else {
1u32 << log_groups_per_flex
};
let mut groups: Vec<GroupLayout> = Vec::with_capacity(num_groups as usize);
for g in 0..num_groups {
let start = first_data_block + g * blocks_per_group;
let nominal_end = start + blocks_per_group - 1;
let end = nominal_end.min(blocks_count - 1);
let has_sb = sparse_super_mode.group_has_backup(g);
let sb_gdt_blocks: u32 = if has_sb { 1 + gdt_blocks } else { 0 };
let local_meta_start = start + sb_gdt_blocks;
let (block_bitmap, inode_bitmap, inode_table, data_start, meta_blocks);
if log_groups_per_flex == 0 {
block_bitmap = local_meta_start;
inode_bitmap = local_meta_start + 1;
inode_table = local_meta_start + 2;
data_start = inode_table + inode_table_blocks;
meta_blocks = data_start - start;
} else {
let flex_first = (g / flex_size) * flex_size;
let pos_in_flex = g - flex_first;
let (first_start, first_has_sb) = if pos_in_flex == 0 {
(start, has_sb)
} else {
let prev = &groups[flex_first as usize];
(prev.start_block, prev.has_superblock)
};
let packed_base = first_start + if first_has_sb { 1 + gdt_blocks } else { 0 };
let bbm_base = packed_base;
let ibm_base = bbm_base + flex_size;
let table_base = ibm_base + flex_size;
block_bitmap = bbm_base + pos_in_flex;
inode_bitmap = ibm_base + pos_in_flex;
inode_table = table_base + pos_in_flex * inode_table_blocks;
if pos_in_flex == 0 {
let packed_end = table_base + flex_size * inode_table_blocks;
data_start = packed_end;
meta_blocks = data_start - start;
} else {
data_start = local_meta_start;
meta_blocks = sb_gdt_blocks;
}
}
groups.push(GroupLayout {
start_block: start,
end_block: end,
has_superblock: has_sb,
block_bitmap,
inode_bitmap,
inode_table,
data_start,
meta_blocks,
});
}
if log_groups_per_flex != 0 {
for first in (0..num_groups).step_by(flex_size as usize) {
let g0 = &groups[first as usize];
if g0.data_start > g0.end_block + 1 {
return Err(crate::Error::InvalidArgument(format!(
"ext: flex_bg metadata for unit starting at group {} \
({} blocks) exceeds its capacity ({} blocks); try a \
smaller log_groups_per_flex.",
first,
g0.data_start - g0.start_block,
g0.end_block + 1 - g0.start_block,
)));
}
}
}
Ok(Layout {
block_size,
blocks_count,
inodes_count,
first_data_block,
blocks_per_group,
inodes_per_group,
inode_size,
desc_size,
inode_table_blocks,
gdt_blocks,
log_groups_per_flex,
groups,
})
}
pub fn from_superblock(sb: &super::superblock::Superblock) -> crate::Result<Layout> {
let block_size = sb.block_size();
if !block_size.is_power_of_two() || block_size < 1024 {
return Err(crate::Error::InvalidImage(format!(
"ext: bad block_size {block_size}"
)));
}
let group_input_blocks = sb.blocks_count - sb.first_data_block;
let num_groups = group_input_blocks.div_ceil(sb.blocks_per_group);
let inode_table_blocks =
(sb.inodes_per_group as u64 * sb.inode_size as u64).div_ceil(block_size as u64) as u32;
let desc_size = sb.group_desc_size();
let gdt_blocks = (num_groups as u64 * desc_size as u64).div_ceil(block_size as u64) as u32;
let sparse_super2_on = sb.feature_compat & super::constants::feature::COMPAT_SPARSE_SUPER2 != 0;
let sparse_super_on =
sb.feature_ro_compat & super::constants::feature::RO_COMPAT_SPARSE_SUPER != 0;
let sparse_super_mode = if sparse_super2_on {
SparseSuperMode::Two(sb.backup_bgs)
} else if sparse_super_on {
SparseSuperMode::Classic
} else {
SparseSuperMode::All
};
let flex_bg_on = sb.feature_incompat & super::constants::feature::INCOMPAT_FLEX_BG != 0;
let log_groups_per_flex = if flex_bg_on {
sb.log_groups_per_flex
} else {
0
};
let flex_size: u32 = if log_groups_per_flex == 0 {
1
} else {
1u32 << log_groups_per_flex
};
let mut groups: Vec<GroupLayout> = Vec::with_capacity(num_groups as usize);
for g in 0..num_groups {
let start = sb.first_data_block + g * sb.blocks_per_group;
let nominal_end = start + sb.blocks_per_group - 1;
let end = nominal_end.min(sb.blocks_count - 1);
let has_sb = sparse_super_mode.group_has_backup(g);
let sb_gdt_blocks: u32 = if has_sb { 1 + gdt_blocks } else { 0 };
let local_meta_start = start + sb_gdt_blocks;
let (block_bitmap, inode_bitmap, inode_table, data_start, meta_blocks);
if log_groups_per_flex == 0 {
block_bitmap = local_meta_start;
inode_bitmap = local_meta_start + 1;
inode_table = local_meta_start + 2;
data_start = inode_table + inode_table_blocks;
meta_blocks = data_start - start;
} else {
let flex_first = (g / flex_size) * flex_size;
let pos_in_flex = g - flex_first;
let (first_start, first_has_sb) = if pos_in_flex == 0 {
(start, has_sb)
} else {
let prev = &groups[flex_first as usize];
(prev.start_block, prev.has_superblock)
};
let packed_base = first_start + if first_has_sb { 1 + gdt_blocks } else { 0 };
let bbm_base = packed_base;
let ibm_base = bbm_base + flex_size;
let table_base = ibm_base + flex_size;
block_bitmap = bbm_base + pos_in_flex;
inode_bitmap = ibm_base + pos_in_flex;
inode_table = table_base + pos_in_flex * inode_table_blocks;
if pos_in_flex == 0 {
let packed_end = table_base + flex_size * inode_table_blocks;
data_start = packed_end;
meta_blocks = data_start - start;
} else {
data_start = local_meta_start;
meta_blocks = sb_gdt_blocks;
}
}
groups.push(GroupLayout {
start_block: start,
end_block: end,
has_superblock: has_sb,
block_bitmap,
inode_bitmap,
inode_table,
data_start,
meta_blocks,
});
}
Ok(Layout {
block_size,
blocks_count: sb.blocks_count,
inodes_count: sb.inodes_count,
first_data_block: sb.first_data_block,
blocks_per_group: sb.blocks_per_group,
inodes_per_group: sb.inodes_per_group,
inode_size: sb.inode_size,
desc_size,
inode_table_blocks,
gdt_blocks,
log_groups_per_flex,
groups,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn single_group_1kib() {
let layout = plan(1024, 1024, 16).unwrap();
assert_eq!(layout.first_data_block, 1);
assert_eq!(layout.blocks_per_group, 1024);
assert_eq!(layout.num_groups(), 1);
assert_eq!(layout.inodes_per_group, 16);
assert_eq!(layout.inode_size, 128);
assert_eq!(layout.inode_table_blocks, 2);
assert_eq!(layout.gdt_blocks, 1);
let g0 = &layout.groups[0];
assert_eq!(g0.start_block, 1);
assert_eq!(g0.end_block, 1023);
assert!(g0.has_superblock);
assert_eq!(g0.block_bitmap, 3); assert_eq!(g0.inode_bitmap, 4);
assert_eq!(g0.inode_table, 5);
assert_eq!(g0.data_start, 7); assert_eq!(g0.meta_blocks, 6);
}
#[test]
fn multiple_groups_1kib() {
let layout = plan(1024, 32 * 1024, 256).unwrap();
assert_eq!(layout.num_groups(), 4);
assert_eq!(layout.blocks_per_group, 8192);
let g0 = &layout.groups[0];
let g1 = &layout.groups[1];
assert_eq!(g0.start_block, 1);
assert_eq!(g0.end_block, 8192);
assert_eq!(g1.start_block, 8193);
assert_eq!(g1.end_block, 16384);
}
#[test]
fn block_size_4096() {
let layout = plan(4096, 1024, 64).unwrap();
assert_eq!(layout.first_data_block, 0);
assert_eq!(layout.blocks_per_group, 1024);
assert_eq!(layout.num_groups(), 1);
}
#[test]
fn rejects_invalid_block_size() {
assert!(plan(512, 1024, 16).is_err());
assert!(plan(3000, 1024, 16).is_err()); }
#[test]
fn rejects_too_few_blocks() {
assert!(plan(1024, 8, 16).is_err());
}
#[test]
fn inodes_round_up_to_multiple_of_8() {
let layout = plan(1024, 1024, 11).unwrap();
assert_eq!(layout.inodes_per_group % 8, 0);
assert!(layout.inodes_per_group >= 11);
}
#[test]
fn flex_bg_packs_metadata_into_first_group() {
let blocks_per_group = 32768u32;
let total_blocks = 4 * blocks_per_group; let layout = plan_full(4096, total_blocks, 1024, false, 2).unwrap();
assert_eq!(layout.num_groups(), 4);
assert_eq!(layout.log_groups_per_flex, 2);
assert_eq!(layout.flex_size(), 4);
let g0 = &layout.groups[0];
let g1 = &layout.groups[1];
let g2 = &layout.groups[2];
let g3 = &layout.groups[3];
let gdt = layout.gdt_blocks;
let packed_base = 1 + gdt; assert_eq!(g0.block_bitmap, packed_base);
assert_eq!(g1.block_bitmap, packed_base + 1);
assert_eq!(g2.block_bitmap, packed_base + 2);
assert_eq!(g3.block_bitmap, packed_base + 3);
assert_eq!(g0.inode_bitmap, packed_base + 4);
assert_eq!(g1.inode_bitmap, packed_base + 5);
assert_eq!(g2.inode_bitmap, packed_base + 6);
assert_eq!(g3.inode_bitmap, packed_base + 7);
let table_base = packed_base + 8;
assert_eq!(g0.inode_table, table_base);
assert_eq!(g1.inode_table, table_base + layout.inode_table_blocks);
assert_eq!(g2.inode_table, table_base + 2 * layout.inode_table_blocks);
assert_eq!(g3.inode_table, table_base + 3 * layout.inode_table_blocks);
assert_eq!(g1.meta_blocks, 1 + gdt);
assert_eq!(g1.data_start, g1.start_block + 1 + gdt);
assert_eq!(g2.meta_blocks, 1 + gdt);
assert_eq!(g3.meta_blocks, 1 + gdt);
}
#[test]
fn flex_bg_with_sparse_super_skips_backups() {
let blocks_per_group = 32768u32;
let total_blocks = 4 * blocks_per_group;
let layout = plan_full(4096, total_blocks, 1024, true, 2).unwrap();
let g2 = &layout.groups[2];
let g3 = &layout.groups[3];
assert!(!g2.has_superblock);
assert!(g3.has_superblock);
assert_eq!(g2.meta_blocks, 0);
assert_eq!(g2.data_start, g2.start_block);
assert_eq!(g3.meta_blocks, 1 + layout.gdt_blocks);
}
#[test]
fn flex_bg_rejects_too_large_log() {
let err = plan_full(4096, 32 * 1024, 256, false, 6).unwrap_err();
assert!(matches!(err, crate::Error::InvalidArgument(_)));
}
#[test]
fn flex_bg_off_matches_classic_plan() {
let a = plan_full(1024, 32 * 1024, 256, false, 0).unwrap();
let b = plan_with(1024, 32 * 1024, 256, false).unwrap();
assert_eq!(a.num_groups(), b.num_groups());
for i in 0..a.groups.len() {
assert_eq!(a.groups[i].block_bitmap, b.groups[i].block_bitmap);
assert_eq!(a.groups[i].inode_bitmap, b.groups[i].inode_bitmap);
assert_eq!(a.groups[i].inode_table, b.groups[i].inode_table);
assert_eq!(a.groups[i].data_start, b.groups[i].data_start);
assert_eq!(a.groups[i].meta_blocks, b.groups[i].meta_blocks);
}
}
#[test]
fn use_64bit_widens_desc_size_and_gdt() {
let a = plan_layout(4096, 4 * 32768, 1024, SparseSuperMode::All, 0, false).unwrap();
let b = plan_layout(4096, 4 * 32768, 1024, SparseSuperMode::All, 0, true).unwrap();
assert_eq!(a.desc_size, GROUP_DESC_SIZE);
assert_eq!(b.desc_size, GROUP_DESC_SIZE_64);
assert_eq!(a.num_groups(), b.num_groups());
}
#[test]
fn sparse_super_mode_two_keeps_only_listed_groups() {
let layout = plan_layout(
4096,
4 * 32768,
1024,
SparseSuperMode::Two([1, 3]),
0,
false,
)
.unwrap();
assert!(layout.groups[0].has_superblock);
assert!(layout.groups[1].has_superblock);
assert!(!layout.groups[2].has_superblock);
assert!(layout.groups[3].has_superblock);
let sb_gdt = 1 + layout.gdt_blocks;
assert_eq!(
layout.groups[1].meta_blocks - layout.groups[2].meta_blocks,
sb_gdt,
"group 2 saves exactly 1 + gdt_blocks of metadata vs a group with a backup",
);
}
#[test]
fn sparse_super_mode_group_zero_is_always_primary() {
let mode = SparseSuperMode::Two([5, 9]);
assert!(mode.group_has_backup(0));
assert!(!mode.group_has_backup(2));
assert!(mode.group_has_backup(5));
assert!(mode.group_has_backup(9));
}
}