use crate::bgd::{BgdFlags, BlockGroupDescriptor};
use crate::error::{Error, Result};
use crate::superblock::Superblock;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BitmapWrite {
pub bitmap_block: u64,
pub bit_start: u32,
pub count: u32,
pub set: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BgdCounterUpdate {
pub group_idx: u32,
pub free_blocks_delta: i32,
pub free_inodes_delta: i32,
pub used_dirs_delta: i32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SuperblockCounterUpdate {
pub free_blocks_delta: i64,
pub free_inodes_delta: i32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BlockAllocationPlan {
pub first_block: u64,
pub count: u32,
pub bitmap: BitmapWrite,
pub bgd: BgdCounterUpdate,
pub sb: SuperblockCounterUpdate,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InodeAllocationPlan {
pub inode: u32,
pub is_dir: bool,
pub bitmap: BitmapWrite,
pub bgd: BgdCounterUpdate,
pub sb: SuperblockCounterUpdate,
}
#[inline]
pub fn bit_is_set(bitmap: &[u8], idx: u32) -> bool {
let byte = (idx / 8) as usize;
let mask = 1u8 << (idx % 8);
byte < bitmap.len() && bitmap[byte] & mask != 0
}
pub fn find_first_free(bitmap: &[u8], start: u32, max_bits: u32) -> Option<u32> {
let max_bits = max_bits.min(u32::try_from(bitmap.len().saturating_mul(8)).unwrap_or(u32::MAX));
if start >= max_bits {
return None;
}
let mut i = start;
while i < max_bits && !i.is_multiple_of(64) {
if !bit_is_set(bitmap, i) {
return Some(i);
}
i += 1;
}
while i + 64 <= max_bits {
let byte = (i as usize) / 8;
if byte + 8 > bitmap.len() {
break;
}
let word = u64::from_le_bytes(bitmap[byte..byte + 8].try_into().unwrap());
if word != u64::MAX {
let bit = word.trailing_ones();
let cand = i + bit;
if cand < max_bits {
return Some(cand);
}
return None;
}
i += 64;
}
while i < max_bits {
if !bit_is_set(bitmap, i) {
return Some(i);
}
i += 1;
}
None
}
pub fn find_free_run(bitmap: &[u8], start: u32, max_bits: u32, count: u32) -> Option<u32> {
if count == 0 {
return None;
}
let mut i = start;
while i + count <= max_bits {
let run_start = find_first_free(bitmap, i, max_bits)?;
if run_start + count > max_bits {
return None;
}
let mut j = run_start + 1;
while j < run_start + count && !bit_is_set(bitmap, j) {
j += 1;
}
if j - run_start >= count {
return Some(run_start);
}
i = j + 1;
}
None
}
pub fn plan_block_allocation<F>(
sb: &Superblock,
groups: &[BlockGroupDescriptor],
count: u32,
hint_group: u32,
mut bitmap_reader: F,
) -> Result<BlockAllocationPlan>
where
F: FnMut(u64) -> Result<Vec<u8>>,
{
if count == 0 {
return Err(Error::Corrupt("plan_block_allocation: count == 0"));
}
let blocks_per_group = sb.blocks_per_group;
let ngroups = groups.len() as u32;
if ngroups == 0 {
return Err(Error::Corrupt("no block groups"));
}
let hint = hint_group.min(ngroups.saturating_sub(1));
for step in 0..ngroups {
let gi = (hint + step) % ngroups;
let bgd = &groups[gi as usize];
if bgd.free_blocks_count < count {
continue;
}
let max_bits = blocks_in_group(sb, gi);
let bitmap_bytes: Vec<u8> = if bgd.flags().contains(BgdFlags::BLOCK_UNINIT) {
vec![0u8; sb.block_size() as usize]
} else {
bitmap_reader(bgd.block_bitmap)?
};
let Some(bit_start) = find_free_run(&bitmap_bytes, 0, max_bits, count) else {
continue;
};
let group_first_block =
(gi as u64) * (blocks_per_group as u64) + sb.first_data_block as u64;
let first_block = group_first_block + bit_start as u64;
return Ok(BlockAllocationPlan {
first_block,
count,
bitmap: BitmapWrite {
bitmap_block: bgd.block_bitmap,
bit_start,
count,
set: true,
},
bgd: BgdCounterUpdate {
group_idx: gi,
free_blocks_delta: -(count as i32),
free_inodes_delta: 0,
used_dirs_delta: 0,
},
sb: SuperblockCounterUpdate {
free_blocks_delta: -(count as i64),
free_inodes_delta: 0,
},
});
}
Err(Error::Corrupt(
"no group has a contiguous free run of this size",
))
}
fn blocks_in_group(sb: &Superblock, gi: u32) -> u32 {
let ngroups = sb.block_group_count() as u32;
if gi + 1 < ngroups {
return sb.blocks_per_group;
}
let remainder =
sb.blocks_count.saturating_sub(sb.first_data_block as u64) % sb.blocks_per_group as u64;
if remainder == 0 {
sb.blocks_per_group
} else {
remainder as u32
}
}
pub fn plan_inode_allocation<F>(
sb: &Superblock,
groups: &[BlockGroupDescriptor],
is_dir: bool,
hint_group: u32,
mut bitmap_reader: F,
) -> Result<InodeAllocationPlan>
where
F: FnMut(u64) -> Result<Vec<u8>>,
{
let ngroups = groups.len() as u32;
if ngroups == 0 {
return Err(Error::Corrupt("no block groups"));
}
let start_group = if is_dir {
orlov_select_group(groups, hint_group)
} else {
hint_group.min(ngroups.saturating_sub(1))
};
for step in 0..ngroups {
let gi = (start_group + step) % ngroups;
let bgd = &groups[gi as usize];
if bgd.free_inodes_count == 0 {
continue;
}
let max_bits = sb.inodes_per_group;
let bitmap_bytes: Vec<u8> = if bgd.flags().contains(BgdFlags::INODE_UNINIT) {
vec![0u8; sb.block_size() as usize]
} else {
bitmap_reader(bgd.inode_bitmap)?
};
let floor = if gi == 0 {
sb.first_inode.saturating_sub(1)
} else {
0
};
let Some(bit_start) = find_first_free(&bitmap_bytes, floor, max_bits) else {
continue;
};
let inode = u64::from(gi)
.checked_mul(u64::from(sb.inodes_per_group))
.and_then(|base| base.checked_add(u64::from(bit_start) + 1))
.filter(|n| *n <= u64::from(sb.inodes_count))
.ok_or(Error::Corrupt(
"the group's inode range does not fit in the filesystem",
))? as u32;
return Ok(InodeAllocationPlan {
inode,
is_dir,
bitmap: BitmapWrite {
bitmap_block: bgd.inode_bitmap,
bit_start,
count: 1,
set: true,
},
bgd: BgdCounterUpdate {
group_idx: gi,
free_blocks_delta: 0,
free_inodes_delta: -1,
used_dirs_delta: if is_dir { 1 } else { 0 },
},
sb: SuperblockCounterUpdate {
free_blocks_delta: 0,
free_inodes_delta: -1,
},
});
}
Err(Error::Corrupt("no group has a free inode"))
}
fn orlov_select_group(groups: &[BlockGroupDescriptor], hint: u32) -> u32 {
let ngroups = groups.len() as u32;
if ngroups == 0 {
return 0;
}
let hint = hint.min(ngroups.saturating_sub(1));
let total_free_inodes: u64 = groups.iter().map(|g| g.free_inodes_count as u64).sum();
let total_used_dirs: u64 = groups.iter().map(|g| g.used_dirs_count as u64).sum();
let avg_free_inodes = total_free_inodes / ngroups as u64;
let avg_used_dirs = total_used_dirs / ngroups as u64;
let mut best: Option<u32> = None;
let mut best_score: i64 = i64::MIN;
for step in 0..ngroups {
let gi = (hint + step) % ngroups;
let g = &groups[gi as usize];
let fi = g.free_inodes_count as i64;
let ud = g.used_dirs_count as i64;
let mut score = fi - ud;
if fi >= avg_free_inodes as i64 {
score += 1000;
}
if ud <= avg_used_dirs as i64 {
score += 500;
}
if score > best_score && g.free_inodes_count > 0 {
best_score = score;
best = Some(gi);
}
}
best.unwrap_or(hint)
}
pub fn apply_bitmap_write(buf: &mut [u8], w: &BitmapWrite) {
for b in 0..w.count {
let idx = (w.bit_start + b) as usize;
let byte = idx / 8;
let mask = 1u8 << (idx % 8);
if byte >= buf.len() {
break;
}
if w.set {
buf[byte] |= mask;
} else {
buf[byte] &= !mask;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn mk_sb(
block_size: u32,
blocks_per_group: u32,
inodes_per_group: u32,
total_blocks: u64,
) -> Superblock {
let mut raw = vec![0u8; crate::superblock::SUPERBLOCK_SIZE];
raw[0x38..0x3A].copy_from_slice(&(crate::superblock::EXT4_MAGIC).to_le_bytes());
raw[0x00..0x04].copy_from_slice(&(inodes_per_group * 4).to_le_bytes());
raw[0x04..0x08].copy_from_slice(&(total_blocks as u32).to_le_bytes());
raw[0x14..0x18].copy_from_slice(&1u32.to_le_bytes()); raw[0x18..0x1C]
.copy_from_slice(&(block_size.trailing_zeros().saturating_sub(10)).to_le_bytes());
raw[0x20..0x24].copy_from_slice(&blocks_per_group.to_le_bytes());
raw[0x28..0x2C].copy_from_slice(&inodes_per_group.to_le_bytes());
raw[0x4C..0x50].copy_from_slice(&1u32.to_le_bytes()); raw[0x58..0x5A].copy_from_slice(&256u16.to_le_bytes()); raw[0xFE..0x100].copy_from_slice(&64u16.to_le_bytes()); crate::superblock::Superblock::parse(raw).unwrap()
}
fn mk_bgd(
free_blocks: u32,
free_inodes: u32,
used_dirs: u32,
flags: u16,
) -> BlockGroupDescriptor {
BlockGroupDescriptor {
block_bitmap: 100,
inode_bitmap: 200,
inode_table: 300,
free_blocks_count: free_blocks,
free_inodes_count: free_inodes,
used_dirs_count: used_dirs,
flags,
itable_unused: 0,
block_bitmap_csum: 0,
inode_bitmap_csum: 0,
checksum: 0,
}
}
#[test]
fn a_bit_outside_the_bitmap_is_not_a_free_bit() {
let full = vec![0xFFu8];
assert_eq!(find_first_free(&full, 0, 1 << 31), None);
let partly = vec![0x0Fu8];
assert_eq!(find_first_free(&partly, 0, 1 << 31), Some(4));
assert_eq!(find_first_free(&partly, 8, 1 << 31), None);
assert_eq!(find_first_free(&[], 0, 1 << 31), None);
}
#[test]
fn a_reserved_inode_is_never_allocated() {
let sb = mk_sb(1024, 8192, 128, 1024);
let groups = vec![mk_bgd(100, 128, 0, BgdFlags::INODE_UNINIT.bits())];
assert_eq!(
sb.first_inode,
crate::superblock::GOOD_OLD_FIRST_INODE,
"the fixture superblock leaves s_first_ino zero; the floor comes from the parser"
);
let plan = plan_inode_allocation(&sb, &groups, false, 0, |_| {
panic!("an uninitialised group is not read")
})
.expect("a group with free inodes");
assert!(
plan.inode >= sb.first_inode,
"allocated inode {} , where the first non-reserved one is {} -- \
writing it puts the new file's inode over the root directory's",
plan.inode,
sb.first_inode
);
let groups = vec![
mk_bgd(100, 0, 0, 0),
mk_bgd(100, 128, 0, BgdFlags::INODE_UNINIT.bits()),
];
let plan = plan_inode_allocation(&sb, &groups, false, 1, |_| unreachable!()).unwrap();
assert_eq!(plan.inode, 128 + 1);
}
#[test]
fn find_first_free_walks_bits() {
let buf = vec![0xFF, 0x0F, 0x00]; assert_eq!(find_first_free(&buf, 0, 24), Some(12));
assert_eq!(find_first_free(&buf, 20, 24), Some(20));
}
#[test]
fn find_first_free_word_aligned_fast_path() {
let mut buf = vec![0xFFu8; 8];
buf.extend_from_slice(&[0xFFu8; 2]); buf.push(0x00); buf.extend_from_slice(&[0xFFu8; 5]);
assert_eq!(find_first_free(&buf, 0, 128), Some(80));
}
#[test]
fn find_first_free_all_ones_in_range() {
let buf = vec![0xFFu8; 32]; assert_eq!(find_first_free(&buf, 0, 256), None);
assert_eq!(find_first_free(&buf, 63, 256), None);
assert_eq!(find_first_free(&buf, 64, 256), None);
}
#[test]
fn find_first_free_respects_max_bits_mid_word() {
let mut buf = vec![0xFFu8; 8]; buf.push(0x00); buf.extend_from_slice(&[0xFFu8; 7]);
assert_eq!(find_first_free(&buf, 0, 65), Some(64));
assert_eq!(find_first_free(&buf, 0, 64), None);
}
#[test]
fn find_first_free_unaligned_start_matches_per_bit() {
let buf: Vec<u8> = (0..128u8).collect(); let max = (buf.len() as u32) * 8;
for start in [0u32, 1, 7, 8, 63, 64, 65, 127, 200, 511] {
let fast = find_first_free(&buf, start, max);
let slow = {
let mut i = start;
loop {
if i >= max {
break None;
}
if !bit_is_set(&buf, i) {
break Some(i);
}
i += 1;
}
};
assert_eq!(fast, slow, "start={start}");
}
}
#[test]
fn find_free_run_handles_gaps() {
let buf = vec![0b0000_0011, 0xFF, 0x00];
assert_eq!(find_free_run(&buf, 0, 24, 5), Some(2));
assert_eq!(find_free_run(&buf, 0, 24, 7), Some(16));
}
#[test]
fn find_free_run_exact_fit() {
let buf = vec![0x00];
assert_eq!(find_free_run(&buf, 0, 8, 8), Some(0));
}
#[test]
fn find_free_run_rejects_too_short() {
let buf = vec![0xFE]; assert_eq!(find_free_run(&buf, 0, 8, 2), None);
}
#[test]
fn block_allocation_uses_first_group_with_room() {
let sb = mk_sb(4096, 32768, 8192, 65536);
let g0 = mk_bgd(100, 8000, 0, 0);
let g1 = mk_bgd(20000, 8000, 0, 0);
let groups = vec![g0, g1];
let read = |_block: u64| -> Result<Vec<u8>> { Ok(vec![0u8; 4096]) };
let plan = plan_block_allocation(&sb, &groups, 10, 0, read).unwrap();
assert_eq!(plan.first_block, 1); assert_eq!(plan.count, 10);
assert_eq!(plan.bgd.group_idx, 0);
assert_eq!(plan.bgd.free_blocks_delta, -10);
assert_eq!(plan.sb.free_blocks_delta, -10);
}
#[test]
fn block_allocation_skips_full_group() {
let sb = mk_sb(4096, 32768, 8192, 65536);
let g0 = mk_bgd(5, 8000, 0, 0); let g1 = mk_bgd(20000, 8000, 0, 0);
let groups = vec![g0, g1];
let read = |_b| Ok(vec![0u8; 4096]);
let plan = plan_block_allocation(&sb, &groups, 10, 0, read).unwrap();
assert_eq!(plan.bgd.group_idx, 1);
assert_eq!(plan.first_block, 1 + 32768);
}
#[test]
fn block_allocation_honours_block_uninit_flag() {
let sb = mk_sb(4096, 32768, 8192, 65536);
let g0 = mk_bgd(32768, 8000, 0, BgdFlags::BLOCK_UNINIT.bits());
let groups = vec![g0];
let mut call_count = 0;
let read = |_b| {
call_count += 1;
Ok(vec![0xFFu8; 4096])
};
let plan = plan_block_allocation(&sb, &groups, 4, 0, read).unwrap();
assert_eq!(plan.count, 4);
assert_eq!(call_count, 0, "UNINIT group should not read bitmap");
}
#[test]
fn inode_allocation_returns_one_based_number() {
let sb = mk_sb(4096, 32768, 8192, 65536);
let g0 = mk_bgd(1000, 8000, 0, 0);
let groups = vec![g0];
let read = |_b| Ok(vec![0u8; 4096]);
let plan = plan_inode_allocation(&sb, &groups, false, 0, read).unwrap();
assert_eq!(
plan.inode, sb.first_inode,
"group 0's first available inode is s_first_ino, not inode 1"
);
assert_eq!(plan.bitmap.bit_start, sb.first_inode - 1, "one-based");
assert!(!plan.is_dir);
assert_eq!(plan.bgd.used_dirs_delta, 0);
}
#[test]
fn inode_allocation_dir_bumps_used_dirs() {
let sb = mk_sb(4096, 32768, 8192, 65536);
let g0 = mk_bgd(1000, 8000, 0, 0);
let groups = vec![g0];
let read = |_b| Ok(vec![0u8; 4096]);
let plan = plan_inode_allocation(&sb, &groups, true, 0, read).unwrap();
assert!(plan.is_dir);
assert_eq!(plan.bgd.used_dirs_delta, 1);
assert_eq!(plan.bgd.free_inodes_delta, -1);
}
#[test]
fn orlov_prefers_group_with_fewer_dirs() {
let groups = vec![
mk_bgd(100, 500, 30, 0),
mk_bgd(100, 1000, 2, 0),
mk_bgd(100, 800, 10, 0),
];
assert_eq!(orlov_select_group(&groups, 0), 1);
}
#[test]
fn apply_bitmap_write_sets_and_clears_bits() {
let mut buf = vec![0u8; 2];
apply_bitmap_write(
&mut buf,
&BitmapWrite {
bitmap_block: 0,
bit_start: 0,
count: 10,
set: true,
},
);
assert_eq!(buf, vec![0xFF, 0x03]);
apply_bitmap_write(
&mut buf,
&BitmapWrite {
bitmap_block: 0,
bit_start: 5,
count: 3,
set: false,
},
);
assert_eq!(buf, vec![0b0001_1111, 0x03]);
}
#[test]
fn blocks_in_group_full_groups_return_blocks_per_group() {
let sb = mk_sb(4096, 32768, 8192, 3 * 32768 + 1);
assert_eq!(blocks_in_group(&sb, 0), 32768);
assert_eq!(blocks_in_group(&sb, 1), 32768);
}
#[test]
fn blocks_in_group_last_group_exact_multiple_returns_full() {
let sb = mk_sb(4096, 32768, 8192, 2 * 32768 + 1);
assert_eq!(blocks_in_group(&sb, 1), 32768);
}
#[test]
fn blocks_in_group_short_last_group() {
let sb = mk_sb(4096, 32768, 8192, 32769 + 100);
assert_eq!(blocks_in_group(&sb, 0), 32768); assert_eq!(blocks_in_group(&sb, 1), 100); }
}