feat(storage-server): implement ext4 read-only filesystem with superblock, inode, extent, and directory parsing
This commit is contained in:
166
crates/servers/storage-server/src/tests/ext4.rs
Normal file
166
crates/servers/storage-server/src/tests/ext4.rs
Normal 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);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
mod ext4;
|
||||
mod fat16;
|
||||
mod protocol;
|
||||
mod server;
|
||||
|
||||
Reference in New Issue
Block a user