From fe0693b969b818cff1422c4a8986e47f339ee14c Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Sun, 18 Oct 2026 23:12:33 +0200 Subject: [PATCH] feat(ext4): block and inode bitmap allocation/deallocation --- .../storage-server/src/adapters/mock_block.rs | 2 +- .../storage-server/src/fs/ext4/bitmap.rs | 121 ++++++++++++++ .../servers/storage-server/src/fs/ext4/mod.rs | 21 +++ .../servers/storage-server/src/tests/ext4.rs | 154 +++++++++++------- 4 files changed, 239 insertions(+), 59 deletions(-) create mode 100644 crates/servers/storage-server/src/fs/ext4/bitmap.rs diff --git a/crates/servers/storage-server/src/adapters/mock_block.rs b/crates/servers/storage-server/src/adapters/mock_block.rs index 12cb897..d4f4b62 100644 --- a/crates/servers/storage-server/src/adapters/mock_block.rs +++ b/crates/servers/storage-server/src/adapters/mock_block.rs @@ -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], diff --git a/crates/servers/storage-server/src/fs/ext4/bitmap.rs b/crates/servers/storage-server/src/fs/ext4/bitmap.rs new file mode 100644 index 0000000..b4ee217 --- /dev/null +++ b/crates/servers/storage-server/src/fs/ext4/bitmap.rs @@ -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( + device: &mut B, + sb: &mut Superblock, + gdt_block: u32, + spb: usize, +) -> Option { + 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( + 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( + device: &mut B, + sb: &mut Superblock, + gdt_block: u32, + spb: usize, +) -> Option { + 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( + 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); + } +} diff --git a/crates/servers/storage-server/src/fs/ext4/mod.rs b/crates/servers/storage-server/src/fs/ext4/mod.rs index 91dec20..488264c 100644 --- a/crates/servers/storage-server/src/fs/ext4/mod.rs +++ b/crates/servers/storage-server/src/fs/ext4/mod.rs @@ -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 Ext4 { self.write_inode(inode_num, inode) } + pub fn alloc_block(&mut self) -> Option { + 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 { + 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 { if !inode.uses_extents() { return None; } let hdr = &inode.block_data; diff --git a/crates/servers/storage-server/src/tests/ext4.rs b/crates/servers/storage-server/src/tests/ext4.rs index f5fba55..c80797f 100644 --- a/crates/servers/storage-server/src/tests/ext4.rs +++ b/crates/servers/storage-server/src/tests/ext4.rs @@ -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();