feat(ext4): block and inode bitmap allocation/deallocation

This commit is contained in:
2026-10-18 23:12:33 +02:00
parent 381669d9dd
commit fe0693b969
4 changed files with 239 additions and 59 deletions

View File

@@ -1,7 +1,7 @@
use port_block_device::{BlockDevice, SectorId};
const SECTOR_SIZE: usize = 512;
const MAX_SECTORS: usize = 64;
const MAX_SECTORS: usize = 256;
pub struct MockBlockDevice {
data: [[u8; SECTOR_SIZE]; MAX_SECTORS],

View File

@@ -0,0 +1,121 @@
use port_block_device::BlockDevice;
use super::io::{read_block, write_block};
use super::superblock::Superblock;
use super::bgd::BlockGroupDescriptor;
pub fn alloc_block<B: BlockDevice>(
device: &mut B,
sb: &mut Superblock,
gdt_block: u32,
spb: usize,
) -> Option<u32> {
let bg = 0u32;
let mut bgd = BlockGroupDescriptor::read(device, gdt_block, bg, spb)?;
if bgd.free_blocks_count == 0 { return None; }
let mut bitmap = [0u8; 4096];
if !read_block(device, bgd.block_bitmap as u64, spb, &mut bitmap) { return None; }
for byte_idx in 0..sb.blocks_count as usize {
let byte = byte_idx / 8;
let bit = byte_idx % 8;
if byte >= bitmap.len() { break; }
if bitmap[byte] & (1 << bit) == 0 {
bitmap[byte] |= 1 << bit;
if !write_block(device, bgd.block_bitmap as u64, spb, &bitmap) { return None; }
bgd.free_blocks_count -= 1;
bgd.write_back(device, gdt_block, bg, spb);
sb.free_blocks_count -= 1;
sb.write_back(device);
return Some(byte_idx as u32);
}
}
None
}
pub fn free_block<B: BlockDevice>(
device: &mut B,
sb: &mut Superblock,
gdt_block: u32,
block: u32,
spb: usize,
) {
let bg = 0u32;
let mut bgd = match BlockGroupDescriptor::read(device, gdt_block, bg, spb) {
Some(b) => b,
None => return,
};
let mut bitmap = [0u8; 4096];
if !read_block(device, bgd.block_bitmap as u64, spb, &mut bitmap) { return; }
let byte = block as usize / 8;
let bit = block as usize % 8;
if byte < bitmap.len() {
bitmap[byte] &= !(1 << bit);
write_block(device, bgd.block_bitmap as u64, spb, &bitmap);
bgd.free_blocks_count += 1;
bgd.write_back(device, gdt_block, bg, spb);
sb.free_blocks_count += 1;
sb.write_back(device);
}
}
pub fn alloc_inode<B: BlockDevice>(
device: &mut B,
sb: &mut Superblock,
gdt_block: u32,
spb: usize,
) -> Option<u32> {
let bg = 0u32;
let mut bgd = BlockGroupDescriptor::read(device, gdt_block, bg, spb)?;
if bgd.free_inodes_count == 0 { return None; }
let mut bitmap = [0u8; 4096];
if !read_block(device, bgd.inode_bitmap as u64, spb, &mut bitmap) { return None; }
for bit_idx in 0..sb.inodes_count as usize {
let byte = bit_idx / 8;
let bit = bit_idx % 8;
if byte >= bitmap.len() { break; }
if bitmap[byte] & (1 << bit) == 0 {
bitmap[byte] |= 1 << bit;
if !write_block(device, bgd.inode_bitmap as u64, spb, &bitmap) { return None; }
bgd.free_inodes_count -= 1;
bgd.write_back(device, gdt_block, bg, spb);
sb.free_inodes_count -= 1;
sb.write_back(device);
return Some(bit_idx as u32 + 1); // inodes are 1-based
}
}
None
}
pub fn free_inode<B: BlockDevice>(
device: &mut B,
sb: &mut Superblock,
gdt_block: u32,
inode: u32,
spb: usize,
) {
let bg = 0u32;
let mut bgd = match BlockGroupDescriptor::read(device, gdt_block, bg, spb) {
Some(b) => b,
None => return,
};
let mut bitmap = [0u8; 4096];
if !read_block(device, bgd.inode_bitmap as u64, spb, &mut bitmap) { return; }
let bit_idx = (inode - 1) as usize; // inodes are 1-based
let byte = bit_idx / 8;
let bit = bit_idx % 8;
if byte < bitmap.len() {
bitmap[byte] &= !(1 << bit);
write_block(device, bgd.inode_bitmap as u64, spb, &bitmap);
bgd.free_inodes_count += 1;
bgd.write_back(device, gdt_block, bg, spb);
sb.free_inodes_count += 1;
sb.write_back(device);
}
}

View File

@@ -1,6 +1,7 @@
pub mod io;
pub mod superblock;
pub mod bgd;
pub mod bitmap;
use port_block_device::BlockDevice;
use crate::filesystem::{DirEntry, FileHandle, FileName, FileSystem};
@@ -173,6 +174,26 @@ impl<B: BlockDevice> Ext4<B> {
self.write_inode(inode_num, inode)
}
pub fn alloc_block(&mut self) -> Option<u32> {
let spb = self.sb.sectors_per_block();
bitmap::alloc_block(&mut self.device, &mut self.sb, self.gdt_block, spb)
}
pub fn free_block(&mut self, block: u32) {
let spb = self.sb.sectors_per_block();
bitmap::free_block(&mut self.device, &mut self.sb, self.gdt_block, block, spb);
}
pub fn alloc_inode(&mut self) -> Option<u32> {
let spb = self.sb.sectors_per_block();
bitmap::alloc_inode(&mut self.device, &mut self.sb, self.gdt_block, spb)
}
pub fn free_inode(&mut self, inode: u32) {
let spb = self.sb.sectors_per_block();
bitmap::free_inode(&mut self.device, &mut self.sb, self.gdt_block, inode, spb);
}
fn read_extents(&self, inode: &Inode) -> Option<Extent> {
if !inode.uses_extents() { return None; }
let hdr = &inode.block_data;

View File

@@ -12,57 +12,79 @@ fn write_u16(buf: &mut [u8], offset: usize, val: u16) {
}
fn format_ext4_disk() -> MockBlockDevice {
let mut dev = MockBlockDevice::new(64);
let mut dev = MockBlockDevice::new(256);
let mut buf = [0u8; 512];
// Superblock at sectors 2-3 (byte offset 1024)
// Sector 2: first 512 bytes of superblock
write_u32(&mut buf, 0x00, 32); // s_inodes_count
write_u32(&mut buf, 0x04, 64); // s_blocks_count
write_u32(&mut buf, 0x14, 1); // s_first_data_block = 1 (1K blocks would, but we use 0 for 4K)
write_u32(&mut buf, 0x18, 2); // s_log_block_size = 2 → 1024 << 2 = 4096
write_u32(&mut buf, 0x20, 64); // s_blocks_per_group
write_u32(&mut buf, 0x28, 32); // s_inodes_per_group
write_u16(&mut buf, 0x38, 0xEF53); // s_magic
write_u16(&mut buf, 0x58, 128); // s_inode_size
write_u32(&mut buf, 0x14, 0); // s_first_data_block = 0 for 4K blocks
// Layout (4K blocks):
// Block 0: superblock (at byte offset 1024, sectors 2-3)
// Block 1: GDT (sectors 8-15)
// Block 2: block bitmap (sectors 16-23)
// Block 3: inode bitmap (sectors 24-31)
// Block 4: inode table (sectors 32-39), 32 inodes * 128 bytes = 4096
// Block 5: root dir data (sectors 40-47)
// Block 6: hello file data (sectors 48-55)
// Blocks 7-31: free (25 blocks)
// Superblock at sectors 2-3
write_u32(&mut buf, 0x00, 32); // s_inodes_count
write_u32(&mut buf, 0x04, 32); // s_blocks_count
write_u32(&mut buf, 0x0C, 25); // s_free_blocks_count (blocks 7-31)
write_u32(&mut buf, 0x10, 29); // s_free_inodes_count (inodes 4-32)
write_u32(&mut buf, 0x14, 0); // s_first_data_block = 0 for 4K blocks
write_u32(&mut buf, 0x18, 2); // s_log_block_size = 2 → 4096
write_u32(&mut buf, 0x20, 32); // s_blocks_per_group
write_u32(&mut buf, 0x28, 32); // s_inodes_per_group
write_u16(&mut buf, 0x38, 0xEF53); // s_magic
write_u16(&mut buf, 0x58, 128); // s_inode_size
dev.write_sector(SectorId::new(2), &buf);
// Block group descriptor at block 1 (sectors 8-15 for 4K blocks)
// Block group descriptor at block 1 (sector 8)
let mut gdt = [0u8; 512];
write_u32(&mut gdt, 8, 3); // bg_inode_table = block 3
write_u32(&mut gdt, 0, 2); // bg_block_bitmap = block 2
write_u32(&mut gdt, 4, 3); // bg_inode_bitmap = block 3
write_u32(&mut gdt, 8, 4); // bg_inode_table = block 4
write_u16(&mut gdt, 12, 25); // bg_free_blocks_count
write_u16(&mut gdt, 14, 29); // bg_free_inodes_count
dev.write_sector(SectorId::new(8), &gdt);
// Inode table at block 3 (sectors 24-31)
// Root inode (inode 2) at offset 128 in the table
// Block bitmap at block 2 (sector 16): blocks 0-6 used, 7-31 free
let mut bbitmap = [0u8; 512];
bbitmap[0] = 0x7F; // bits 0-6 set = blocks 0-6 used
dev.write_sector(SectorId::new(16), &bbitmap);
// Inode bitmap at block 3 (sector 24): inodes 1-3 used, 4-32 free
let mut ibitmap = [0u8; 512];
ibitmap[0] = 0x07; // bits 0-2 set = inodes 1-3 used
dev.write_sector(SectorId::new(24), &ibitmap);
// Inode table at block 4 (sector 32)
let mut inode_sector = [0u8; 512];
// Inode 2 (root dir) at byte offset 128 = still in first sector
let ioff = 128; // inode 2 index = 1, * 128 bytes = 128
write_u16(&mut inode_sector, ioff + 0x00, 0x4000 | 0o755); // mode = directory
write_u32(&mut inode_sector, ioff + 0x04, 4096); // size_lo
write_u32(&mut inode_sector, ioff + 0x20, 0x80000); // flags = uses extents
// Extent header at inode+0x28
// Inode 2 (root dir) at offset 128
let ioff = 128;
write_u16(&mut inode_sector, ioff + 0x00, 0x4000 | 0o755);
write_u32(&mut inode_sector, ioff + 0x04, 4096);
write_u16(&mut inode_sector, ioff + 0x1A, 2); // links_count
write_u32(&mut inode_sector, ioff + 0x1C, 8); // blocks_count (512-byte units: 1 block = 8)
write_u32(&mut inode_sector, ioff + 0x20, 0x80000);
let eh = ioff + 0x28;
write_u16(&mut inode_sector, eh + 0, 0xF30A); // magic
write_u16(&mut inode_sector, eh + 2, 1); // entries
write_u16(&mut inode_sector, eh + 4, 4); // max
write_u16(&mut inode_sector, eh + 6, 0); // depth
// Extent leaf at inode+0x28+12
write_u16(&mut inode_sector, eh + 0, 0xF30A);
write_u16(&mut inode_sector, eh + 2, 1);
write_u16(&mut inode_sector, eh + 4, 4);
write_u16(&mut inode_sector, eh + 6, 0);
let ext = eh + 12;
write_u32(&mut inode_sector, ext + 0, 0); // logical block
write_u16(&mut inode_sector, ext + 4, 1); // length
write_u16(&mut inode_sector, ext + 6, 0); // physical hi
write_u32(&mut inode_sector, ext + 8, 4); // physical lo → block 4
write_u32(&mut inode_sector, ext + 0, 0);
write_u16(&mut inode_sector, ext + 4, 1);
write_u16(&mut inode_sector, ext + 6, 0);
write_u32(&mut inode_sector, ext + 8, 5); // root dir data at block 5
// Inode 3 (a regular file "hello") at byte offset 256
// Inode 3 (file "hello") at offset 256
let ioff2 = 256;
write_u16(&mut inode_sector, ioff2 + 0x00, 0x8000 | 0o644); // mode = regular file
write_u32(&mut inode_sector, ioff2 + 0x04, 13); // size = 13 bytes
write_u32(&mut inode_sector, ioff2 + 0x20, 0x80000); // flags = extents
write_u16(&mut inode_sector, ioff2 + 0x00, 0x8000 | 0o644);
write_u32(&mut inode_sector, ioff2 + 0x04, 13);
write_u16(&mut inode_sector, ioff2 + 0x1A, 1); // links_count
write_u32(&mut inode_sector, ioff2 + 0x1C, 8); // blocks_count
write_u32(&mut inode_sector, ioff2 + 0x20, 0x80000);
let eh2 = ioff2 + 0x28;
write_u16(&mut inode_sector, eh2 + 0, 0xF30A);
write_u16(&mut inode_sector, eh2 + 2, 1);
@@ -72,23 +94,19 @@ fn format_ext4_disk() -> MockBlockDevice {
write_u32(&mut inode_sector, ext2 + 0, 0);
write_u16(&mut inode_sector, ext2 + 4, 1);
write_u16(&mut inode_sector, ext2 + 6, 0);
write_u32(&mut inode_sector, ext2 + 8, 5); // data at block 5
write_u32(&mut inode_sector, ext2 + 8, 6); // file data at block 6
dev.write_sector(SectorId::new(24), &inode_sector);
dev.write_sector(SectorId::new(32), &inode_sector);
// Root directory at block 4 (sectors 32-39)
// Root directory at block 5 (sector 40)
let mut dir = [0u8; 512];
let mut pos = 0;
// Entry: "." → inode 2
write_u32(&mut dir, pos + 0, 2);
write_u16(&mut dir, pos + 4, 12); // rec_len
dir[pos + 6] = 1; // name_len
dir[pos + 7] = 2; // file_type = dir
write_u16(&mut dir, pos + 4, 12);
dir[pos + 6] = 1;
dir[pos + 7] = 2;
dir[pos + 8] = b'.';
pos += 12;
// Entry: ".." → inode 2
write_u32(&mut dir, pos + 0, 2);
write_u16(&mut dir, pos + 4, 12);
dir[pos + 6] = 2;
@@ -96,20 +114,17 @@ fn format_ext4_disk() -> MockBlockDevice {
dir[pos + 8] = b'.';
dir[pos + 9] = b'.';
pos += 12;
// Entry: "hello" → inode 3
write_u32(&mut dir, pos + 0, 3);
write_u16(&mut dir, pos + 4, 488); // rec_len = rest of block
dir[pos + 6] = 5; // name_len
dir[pos + 7] = 1; // file_type = regular
write_u16(&mut dir, pos + 4, 488);
dir[pos + 6] = 5;
dir[pos + 7] = 1;
dir[pos + 8..pos + 13].copy_from_slice(b"hello");
dev.write_sector(SectorId::new(40), &dir);
dev.write_sector(SectorId::new(32), &dir);
// File data at block 5 (sectors 40-47)
// File data at block 6 (sector 48)
let mut data = [0u8; 512];
data[..13].copy_from_slice(b"Hello, ext4!\n");
dev.write_sector(SectorId::new(40), &data);
dev.write_sector(SectorId::new(48), &data);
dev
}
@@ -242,7 +257,7 @@ fn format_ext4_two_extents() -> MockBlockDevice {
dev.write_sector(SectorId::new(32), &dir);
// Block 5: filled with 'A'
let mut data_a = [b'A'; 512];
let data_a = [b'A'; 512];
for s in 0..8 {
dev.write_sector(SectorId::new(40 + s), &data_a);
}
@@ -304,6 +319,29 @@ fn init_empty_inode_has_valid_extent_header() {
assert_eq!(inode.block_data[3], 0);
}
#[test]
fn alloc_block_returns_first_free() {
let mut fs = Ext4::mount(format_ext4_disk()).unwrap();
let block = fs.alloc_block().unwrap();
assert_eq!(block, 7);
}
#[test]
fn alloc_inode_returns_first_free() {
let mut fs = Ext4::mount(format_ext4_disk()).unwrap();
let inode = fs.alloc_inode().unwrap();
assert_eq!(inode, 4);
}
#[test]
fn free_block_makes_it_available_again() {
let mut fs = Ext4::mount(format_ext4_disk()).unwrap();
let b1 = fs.alloc_block().unwrap();
fs.free_block(b1);
let b2 = fs.alloc_block().unwrap();
assert_eq!(b1, b2);
}
#[test]
fn read_past_eof_returns_zero() {
let mut fs = Ext4::mount(format_ext4_disk()).unwrap();