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 max_bits = max_bits.min(u32::try_from(bitmap.len().saturating_mul(8)).unwrap_or(u32::MAX));
let mut i = start;
while i.checked_add(count).is_some_and(|end| end <= max_bits) {
let run_start = find_first_free(bitmap, i, max_bits)?;
let run_end = run_start
.checked_add(count)
.filter(|&end| end <= max_bits)?;
let mut j = run_start + 1;
while j < run_end && !bit_is_set(bitmap, j) {
j += 1;
}
if j == run_end {
return Some(run_start);
}
i = j.checked_add(1)?;
}
None
}
pub fn plan_block_allocation<F>(
sb: &Superblock,
groups: &[BlockGroupDescriptor],
count: u32,
hint_group: u32,
bitmap_reader: F,
) -> Result<BlockAllocationPlan>
where
F: FnMut(u64) -> Result<Vec<u8>>,
{
plan_block_allocation_excluding(sb, groups, count, hint_group, &[], bitmap_reader)
}
fn mark_reserved_in_group(
sb: &Superblock,
gi: u32,
max_bits: u32,
reserved: &[u64],
bitmap: &mut [u8],
) {
if reserved.is_empty() {
return;
}
let group_first_block = (gi as u64) * (sb.blocks_per_group as u64) + sb.first_data_block as u64;
for &block in reserved {
let Some(offset) = block.checked_sub(group_first_block) else {
continue;
};
if offset >= max_bits as u64 {
continue;
}
let bit = offset as usize;
let byte = bit / 8;
if byte < bitmap.len() {
bitmap[byte] |= 1u8 << (bit % 8);
}
}
}
pub(crate) fn reserved_blocks(
data: &BlockAllocationPlan,
handed_out: &[BlockAllocationPlan],
) -> Vec<u64> {
std::iter::once(data)
.chain(handed_out)
.flat_map(|plan| (0..u64::from(plan.count)).map(move |i| plan.first_block + i))
.collect()
}
pub fn plan_block_allocation_excluding<F>(
sb: &Superblock,
groups: &[BlockGroupDescriptor],
count: u32,
hint_group: u32,
reserved: &[u64],
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 mut bitmap_bytes: Vec<u8> = if bgd.flags().contains(BgdFlags::BLOCK_UNINIT) {
uninit_block_bitmap(sb, groups, gi)
} else {
bitmap_reader(bgd.block_bitmap)?
};
mark_reserved_in_group(sb, gi, max_bits, reserved, &mut bitmap_bytes);
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",
))
}
pub(crate) fn uninit_group_used_ranges(
sb: &Superblock,
groups: &[BlockGroupDescriptor],
gi: usize,
) -> Vec<std::ops::Range<u64>> {
let group_first = sb.first_data_block as u64 + gi as u64 * sb.blocks_per_group as u64;
let group_len = u64::from(blocks_in_group(sb, gi as u32));
let mut used = Vec::new();
let head = sb.group_head_metadata_blocks(gi as u64);
if head > 0 {
used.push(0..head.min(group_len));
}
if let Some(desc) = groups.get(gi) {
let table_len = (u64::from(sb.inodes_per_group) * u64::from(sb.inode_size))
.div_ceil(u64::from(sb.block_size()));
for (start, len) in [
(desc.block_bitmap, 1),
(desc.inode_bitmap, 1),
(desc.inode_table, table_len),
] {
let Some(relative) = start.checked_sub(group_first) else {
continue;
};
if relative < group_len {
used.push(relative..(relative + len).min(group_len));
}
}
}
used
}
pub(crate) fn uninit_block_bitmap(
sb: &Superblock,
groups: &[BlockGroupDescriptor],
gi: u32,
) -> Vec<u8> {
let mut bitmap = vec![0u8; sb.block_size() as usize];
let capacity = bitmap.len() as u64 * 8;
let padding = u64::from(blocks_in_group(sb, gi)).min(capacity)..capacity;
for range in uninit_group_used_ranges(sb, groups, gi as usize)
.into_iter()
.chain(std::iter::once(padding))
{
for bit in range.start.min(capacity)..range.end.min(capacity) {
bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
}
}
bitmap
}
pub(crate) 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 before_group = u64::from(gi) * u64::from(sb.inodes_per_group);
let floor = u64::from(sb.first_inode.saturating_sub(1)).saturating_sub(before_group);
let floor = u32::try_from(floor).unwrap_or(u32::MAX);
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_plan(first_block: u64, count: u32) -> BlockAllocationPlan {
BlockAllocationPlan {
first_block,
count,
bitmap: BitmapWrite {
bitmap_block: 0,
bit_start: 0,
count,
set: true,
},
bgd: BgdCounterUpdate {
group_idx: 0,
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,
},
}
}
#[test]
fn the_data_plan_is_reserved() {
assert_eq!(reserved_blocks(&mk_plan(517, 1), &[]), vec![517]);
}
#[test]
fn a_multi_block_plan_reserves_its_whole_run() {
assert_eq!(
reserved_blocks(&mk_plan(100, 3), &[]),
vec![100, 101, 102],
"reserving only first_block leaves 101 and 102 free to be handed out again"
);
assert_eq!(
reserved_blocks(&mk_plan(100, 1), &[mk_plan(200, 4)]),
vec![100, 200, 201, 202, 203],
"a handed-out meta plan contributes its whole run too"
);
}
#[test]
fn every_meta_plan_handed_out_so_far_is_reserved() {
let data = mk_plan(517, 1);
let first = reserved_blocks(&data, &[]);
assert_eq!(first, vec![517]);
let handed_out = vec![mk_plan(518, 1)];
let second = reserved_blocks(&data, &handed_out);
assert!(
second.contains(&518),
"the block the first call was given must not be offered again: {second:?}"
);
assert!(
second.contains(&517),
"and the data block stays reserved across calls: {second:?}"
);
let handed_out = vec![mk_plan(518, 1), mk_plan(519, 1)];
assert_eq!(reserved_blocks(&data, &handed_out), vec![517, 518, 519]);
}
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 the_reserved_range_reaches_past_group_0() {
let sb = mk_sb(1024, 8192, 8, 1024);
assert_eq!((sb.inodes_per_group, sb.first_inode), (8, 11));
let groups = vec![
mk_bgd(100, 0, 0, 0),
mk_bgd(100, 8, 0, BgdFlags::INODE_UNINIT.bits()),
];
let plan = plan_inode_allocation(&sb, &groups, false, 0, |_| {
panic!("group 0 is full and group 1 uninitialised: nothing is read")
})
.expect("group 1 has free inodes");
assert_eq!(
plan.inode, 11,
"inodes 9 and 10 are below s_first_ino, in group 1"
);
assert_eq!((plan.bgd.group_idx, plan.bitmap.bit_start), (1, 2));
let groups = vec![
mk_bgd(100, 0, 0, 0),
mk_bgd(100, 8, 0, BgdFlags::INODE_UNINIT.bits()),
];
let mut sb = sb;
sb.first_inode = 17;
let plan = plan_inode_allocation(&sb, &groups, false, 1, |_| unreachable!());
assert!(plan.is_err(), "inodes 9..=16 are all reserved: {plan:?}");
}
#[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 find_free_run_stops_at_the_end_of_the_bitmap() {
assert_eq!(find_free_run(&[0], 0, 100, 9), None);
assert_eq!(find_free_run(&[0], 0, 100, 8), Some(0));
assert_eq!(find_free_run(&[0x0F], 0, u32::MAX, 5), None);
assert_eq!(find_free_run(&[0], u32::MAX - 1, u32::MAX, 4), 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 consecutive_plans_repeat_without_reservations_and_advance_with_them() {
let sb = mk_sb(4096, 32768, 8192, 65536);
let groups = vec![mk_bgd(32768, 8000, 0, 0)];
let read = |_b| Ok(vec![0u8; 4096]);
let unreserved: Vec<u64> = (0..3)
.map(|_| {
plan_block_allocation(&sb, &groups, 1, 0, read)
.unwrap()
.first_block
})
.collect();
assert_eq!(
unreserved,
vec![1, 1, 1],
"nothing was committed, so the scan sees the same bytes each time"
);
let mut reserved: Vec<u64> = Vec::new();
let mut handed_out: Vec<u64> = Vec::new();
for _ in 0..3 {
let plan =
plan_block_allocation_excluding(&sb, &groups, 1, 0, &reserved, read).unwrap();
reserved.push(plan.first_block);
handed_out.push(plan.first_block);
}
assert_eq!(
handed_out,
vec![1, 2, 3],
"each plan must avoid the blocks the ones before it took"
);
}
#[test]
fn a_reserved_run_is_skipped_entirely() {
let sb = mk_sb(4096, 32768, 8192, 65536);
let groups = vec![mk_bgd(32768, 8000, 0, 0)];
let read = |_b| Ok(vec![0u8; 4096]);
let reserved: Vec<u64> = (1..=4).collect();
let plan = plan_block_allocation_excluding(&sb, &groups, 1, 0, &reserved, read).unwrap();
assert_eq!(plan.first_block, 5, "blocks 1..=4 are spoken for");
}
#[test]
fn a_reservation_is_honoured_in_an_uninit_group() {
let sb = mk_sb(4096, 32768, 8192, 65536);
let groups = vec![mk_bgd(32768, 8000, 0, BgdFlags::BLOCK_UNINIT.bits())];
let mut call_count = 0;
let read = |_b| {
call_count += 1;
Ok(vec![0u8; 4096])
};
let plan = plan_block_allocation_excluding(&sb, &groups, 1, 0, &[3], read).unwrap();
assert_eq!(plan.first_block, 4, "block 3 is reserved");
assert_eq!(call_count, 0, "an UNINIT group still reads no bitmap");
}
#[test]
fn an_uninit_group_keeps_its_own_metadata() {
let mut sb = mk_sb(4096, 32768, 8192, 65536);
sb.reserved_gdt_blocks = 3;
let g0 = mk_bgd(0, 8000, 0, 0);
let mut g1 = mk_bgd(32768, 8000, 0, BgdFlags::BLOCK_UNINIT.bits());
g1.block_bitmap = 32774;
g1.inode_bitmap = 32775;
g1.inode_table = 32776;
let groups = vec![g0, g1];
let read = |_b| Ok(vec![0u8; 4096]);
let plan = plan_block_allocation(&sb, &groups, 1, 1, read).unwrap();
assert_eq!(
plan.first_block,
32776 + 512,
"first block past the group's metadata"
);
let used = uninit_group_used_ranges(&sb, &groups, 1);
assert_eq!(used, vec![0..5, 5..6, 6..7, 7..519]);
}
#[test]
fn a_reservation_outside_this_group_does_not_narrow_it() {
let sb = mk_sb(4096, 32768, 8192, 65536);
let groups = vec![mk_bgd(32768, 8000, 0, 0), mk_bgd(32768, 8000, 0, 0)];
let read = |_b| Ok(vec![0u8; 4096]);
let reserved = [1 + 32768, 999_999];
let plan = plan_block_allocation_excluding(&sb, &groups, 1, 0, &reserved, read).unwrap();
assert_eq!(
plan.first_block, 1,
"neither reservation is in group 0, so group 0 is untouched"
);
let full_g0 = vec![mk_bgd(0, 8000, 0, 0), mk_bgd(32768, 8000, 0, 0)];
let plan = plan_block_allocation_excluding(&sb, &full_g0, 1, 0, &reserved, read).unwrap();
assert_eq!(
plan.first_block,
2 + 32768,
"group 0 has no room, and group 1's first block is reserved"
);
}
#[test]
fn the_unreserved_wrapper_agrees_with_the_general_form() {
let sb = mk_sb(4096, 32768, 8192, 65536);
let groups = vec![mk_bgd(5, 8000, 0, 0), mk_bgd(20000, 8000, 0, 0)];
let read = |_b| Ok(vec![0u8; 4096]);
let a = plan_block_allocation(&sb, &groups, 10, 0, read).unwrap();
let b = plan_block_allocation_excluding(&sb, &groups, 10, 0, &[], read).unwrap();
assert_eq!(a, b);
}
#[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); }
}
#[cfg(test)]
mod uninit_bitmap_oracle {
mod needs_host {
use crate::alloc::{blocks_in_group, uninit_block_bitmap};
use crate::bgd::BgdFlags;
use crate::block_io::FileDevice;
use crate::fs::Filesystem;
use std::collections::BTreeMap;
use std::sync::Arc;
fn dumpe2fs_free(image: &str) -> BTreeMap<usize, Vec<(u64, u64)>> {
let out = fs_ext4_test_support::oracle("dumpe2fs")
.arg(image)
.judged()
.clean("dumpe2fs");
let text = String::from_utf8_lossy(&out.stdout).into_owned();
let mut free = BTreeMap::new();
let mut group = None;
for line in text.lines() {
if let Some(rest) = line.strip_prefix("Group ") {
group = rest.split(':').next().and_then(|g| g.parse().ok());
} else if let (Some(g), Some(list)) =
(group, line.trim().strip_prefix("Free blocks:"))
{
let ranges = list
.split(',')
.map(str::trim)
.filter(|r| !r.is_empty())
.map(|r| match r.split_once('-') {
Some((a, b)) => (a.parse().unwrap(), b.parse().unwrap()),
None => (r.parse().unwrap(), r.parse().unwrap()),
})
.collect();
free.insert(g, ranges);
}
}
free
}
fn check(
tag: &str,
block_size: u32,
blocks_per_group: u32,
kib: u64,
features: &str,
force_last_group: bool,
) {
let image =
fs_ext4_test_support::temp_path!("fs_ext4_uninit_{tag}_{}.img", std::process::id());
std::fs::File::create(&image)
.and_then(|f| f.set_len(kib << 10))
.unwrap();
let mut mkfs = fs_ext4_test_support::oracle("mkfs.ext4").args([
"-q",
"-F",
"-b",
&block_size.to_string(),
"-g",
&blocks_per_group.to_string(),
]);
if !features.is_empty() {
mkfs = mkfs.args(["-O", features]);
}
let out = mkfs.arg(&image).output();
assert!(
out.status.success(),
"[{tag}] {}",
String::from_utf8_lossy(&out.stderr)
);
if force_last_group {
let last = {
let fs =
Filesystem::mount(Arc::new(FileDevice::open(&image).unwrap())).unwrap();
let last = fs.groups.len() - 1;
(last, fs.groups[last].flags | BgdFlags::BLOCK_UNINIT.bits())
};
let script = format!(
"set_bg {0} flags {1}\nset_bg {0} checksum calc\n",
last.0, last.1
);
let out = fs_ext4_test_support::oracle("debugfs")
.args(["-w", "-f", "-", &image])
.stdin(script)
.output();
assert!(out.status.success(), "[{tag}] debugfs set_bg");
}
let free = dumpe2fs_free(&image);
let fs = Filesystem::mount(Arc::new(FileDevice::open(&image).unwrap())).unwrap();
let bpg = u64::from(fs.sb.blocks_per_group);
let mut checked = 0;
for (gi, desc) in fs.groups.iter().enumerate() {
if !desc.flags().contains(BgdFlags::BLOCK_UNINIT) {
continue;
}
checked += 1;
let first = u64::from(fs.sb.first_data_block) + gi as u64 * bpg;
let len = u64::from(blocks_in_group(&fs.sb, gi as u32));
let bitmap = uninit_block_bitmap(&fs.sb, &fs.groups, gi as u32);
let is_free = |block: u64| free[&gi].iter().any(|&(a, b)| (a..=b).contains(&block));
for bit in 0..bitmap.len() as u64 * 8 {
let set = bitmap[(bit / 8) as usize] & (1 << (bit % 8)) != 0;
if bit >= len {
assert!(set, "[{tag}] group {gi}: padding bit {bit} is clear");
} else {
assert_eq!(
!set,
is_free(first + bit),
"[{tag}] group {gi}, block {}",
first + bit
);
}
}
assert_eq!(
free[&gi]
.iter()
.filter(|&&(a, _)| a < first + len)
.map(|&(a, b)| b.min(first + len - 1) - a + 1)
.sum::<u64>(),
u64::from(desc.free_blocks_count),
"[{tag}] group {gi}: dumpe2fs's list and the descriptor"
);
}
assert!(checked >= 3, "[{tag}] only {checked} BLOCK_UNINIT groups");
drop(fs);
let _ = std::fs::remove_file(&image);
}
#[test]
fn a_1k_flex_bg_volume() {
check("1k_flex", 1024, 1024, 64 << 10, "", false);
}
#[test]
fn a_1k_volume_without_flex_bg() {
check("1k_noflex", 1024, 1024, 64 << 10, "^flex_bg", false);
}
#[test]
fn a_4k_flex_bg_volume() {
check("4k_flex", 4096, 2048, 256 << 10, "", false);
}
#[test]
fn a_4k_volume_without_flex_bg() {
check(
"4k_noflex",
4096,
2048,
256 << 10,
"^flex_bg,^resize_inode",
false,
);
}
#[test]
fn a_1k_meta_bg_volume_without_flex_bg() {
check(
"1k_meta",
1024,
1024,
64 << 10,
"meta_bg,^resize_inode,^flex_bg",
false,
);
}
#[test]
fn a_short_last_group() {
check("1k_short", 1024, 1024, (64 << 10) + 300, "^flex_bg", true);
}
}
}