feat(storage-server): implement ext4 read-only filesystem with superblock, inode, extent, and directory parsing

This commit is contained in:
2026-10-18 03:49:24 +02:00
parent dd5c2c7268
commit 29c3841679
4 changed files with 511 additions and 0 deletions

View File

@@ -0,0 +1,166 @@
use crate::adapters::mock_block::MockBlockDevice;
use crate::block_device::{BlockDevice, SectorId};
use crate::filesystem::{DirEntry, FileSystem};
use crate::fs::ext4::Ext4;
fn write_u32(buf: &mut [u8], offset: usize, val: u32) {
buf[offset..offset + 4].copy_from_slice(&val.to_le_bytes());
}
fn write_u16(buf: &mut [u8], offset: usize, val: u16) {
buf[offset..offset + 2].copy_from_slice(&val.to_le_bytes());
}
fn format_ext4_disk() -> MockBlockDevice {
let mut dev = MockBlockDevice::new(64);
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
dev.write_sector(SectorId::new(2), &buf);
// Block group descriptor at block 1 (sectors 8-15 for 4K blocks)
let mut gdt = [0u8; 512];
write_u32(&mut gdt, 8, 3); // bg_inode_table = block 3
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
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
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
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
// Inode 3 (a regular file "hello") at byte 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
let eh2 = ioff2 + 0x28;
write_u16(&mut inode_sector, eh2 + 0, 0xF30A);
write_u16(&mut inode_sector, eh2 + 2, 1);
write_u16(&mut inode_sector, eh2 + 4, 4);
write_u16(&mut inode_sector, eh2 + 6, 0);
let ext2 = eh2 + 12;
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
dev.write_sector(SectorId::new(24), &inode_sector);
// Root directory at block 4 (sectors 32-39)
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
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;
dir[pos + 7] = 2;
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
dir[pos + 8..pos + 13].copy_from_slice(b"hello");
dev.write_sector(SectorId::new(32), &dir);
// File data at block 5 (sectors 40-47)
let mut data = [0u8; 512];
data[..13].copy_from_slice(b"Hello, ext4!\n");
dev.write_sector(SectorId::new(40), &data);
dev
}
#[test]
fn mount_valid_ext4() {
let dev = format_ext4_disk();
assert!(Ext4::mount(dev).is_some());
}
#[test]
fn mount_invalid_disk() {
let dev = MockBlockDevice::new(64);
assert!(Ext4::mount(dev).is_none());
}
#[test]
fn list_root_directory() {
let mut fs = Ext4::mount(format_ext4_disk()).unwrap();
let mut entries = [DirEntry::empty(); 8];
let count = fs.list_dir(b"/", &mut entries).unwrap();
assert_eq!(count, 1); // "hello" (. and .. filtered)
assert_eq!(entries[0].name.as_bytes(), b"hello");
}
#[test]
fn open_and_read_file() {
let mut fs = Ext4::mount(format_ext4_disk()).unwrap();
let h = fs.open(b"hello").unwrap();
assert_eq!(fs.file_size(h), Some(13));
let mut buf = [0u8; 32];
let read = fs.read(h, &mut buf).unwrap();
assert_eq!(read, 13);
assert_eq!(&buf[..13], b"Hello, ext4!\n");
fs.close(h);
}
#[test]
fn open_nonexistent_returns_none() {
let mut fs = Ext4::mount(format_ext4_disk()).unwrap();
assert!(fs.open(b"nope").is_none());
}
#[test]
fn read_past_eof_returns_zero() {
let mut fs = Ext4::mount(format_ext4_disk()).unwrap();
let h = fs.open(b"hello").unwrap();
let mut buf = [0u8; 32];
fs.read(h, &mut buf).unwrap(); // read all 13 bytes
let read = fs.read(h, &mut buf).unwrap();
assert_eq!(read, 0);
fs.close(h);
}

View File

@@ -1,3 +1,4 @@
mod ext4;
mod fat16;
mod protocol;
mod server;