From eb9e3efaac720cdbaa480c1c2aa15e0f79ef4588 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Tue, 27 Oct 2026 23:53:13 +0100 Subject: [PATCH] feat(ext4): seek, rmdir (e2fsck-clean) --- .../storage-server/src/adapters/mock_fs.rs | 9 ++++ .../servers/storage-server/src/filesystem.rs | 4 ++ .../servers/storage-server/src/fs/ext4/mod.rs | 45 +++++++++++++++++++ .../storage-server/src/tests/ext4_real.rs | 35 +++++++++++++++ 4 files changed, 93 insertions(+) diff --git a/crates/servers/storage-server/src/adapters/mock_fs.rs b/crates/servers/storage-server/src/adapters/mock_fs.rs index 442aa28..a8f0f41 100644 --- a/crates/servers/storage-server/src/adapters/mock_fs.rs +++ b/crates/servers/storage-server/src/adapters/mock_fs.rs @@ -130,4 +130,13 @@ impl FileSystem for MockFileSystem { } fn mkdir(&mut self, _path: &[u8]) -> bool { true } + + /// Files are read from the start and written at the end only. + fn seek(&mut self, _handle: FileHandle, _offset: usize) -> bool { + false + } + + fn rmdir(&mut self, _path: &[u8]) -> bool { + false + } } diff --git a/crates/servers/storage-server/src/filesystem.rs b/crates/servers/storage-server/src/filesystem.rs index 13b9619..892a38e 100644 --- a/crates/servers/storage-server/src/filesystem.rs +++ b/crates/servers/storage-server/src/filesystem.rs @@ -74,6 +74,10 @@ pub trait FileSystem { fn delete(&mut self, path: &[u8]) -> bool; fn file_size(&mut self, handle: FileHandle) -> Option; fn mkdir(&mut self, path: &[u8]) -> bool; + /// Moves `handle`'s position to `offset` (at most the file's size). + fn seek(&mut self, handle: FileHandle, offset: usize) -> bool; + /// Removes the directory at `path`, which must be empty. + fn rmdir(&mut self, path: &[u8]) -> bool; /// Whether anything can be written at all; the server refuses /// writing requests up front when not. fn writable(&self) -> bool { diff --git a/crates/servers/storage-server/src/fs/ext4/mod.rs b/crates/servers/storage-server/src/fs/ext4/mod.rs index 743255c..2a7c1bc 100644 --- a/crates/servers/storage-server/src/fs/ext4/mod.rs +++ b/crates/servers/storage-server/src/fs/ext4/mod.rs @@ -60,6 +60,15 @@ impl Ext4 { } } + /// One directory fewer in `inode`'s block group. + fn uncount_dir(&mut self, inode: u32) { + let (bg, spb) = (self.sb.inode_block_group(inode), self.sb.sectors_per_block()); + if let Some(mut d) = bgd::BlockGroupDescriptor::read(&mut self.device, self.gdt_block, bg, spb) { + d.used_dirs_count = d.used_dirs_count.saturating_sub(1); + d.write_back(&mut self.device, self.gdt_block, bg, spb); + } + } + /// The disk, for a test to look at what was written. pub fn into_device(self) -> B { self.device @@ -361,4 +370,40 @@ impl FileSystem for Ext4 { true } + + fn seek(&mut self, handle: FileHandle, offset: usize) -> bool { + match self.open_files.get_mut(handle.raw() as usize).and_then(Option::as_mut) { + Some(f) if offset <= f.size => { + f.offset = offset; + true + } + _ => false, + } + } + + fn rmdir(&mut self, path: &[u8]) -> bool { + let Some((parent_num, name)) = self.resolve_parent(path) else { return false }; + let Some(parent) = self.read_inode(parent_num) else { return false }; + let Some(child_num) = dir::find_entry(&mut self.device, &self.sb, &parent, name) else { return false }; + let Some(child) = self.read_inode(child_num).filter(Inode::is_dir) else { return false }; + // Empty: nothing but `.` and `..`. + let mut entries = [DirEntry::empty(); 3]; + let Some(n) = dir::list_entries(&mut self.device, &self.sb, &child, &mut entries) else { return false }; + if entries[..n].iter().any(|e| e.name.as_bytes() != b"." && e.name.as_bytes() != b"..") { + return false; + } + if dir::remove_entry(&mut self.device, &self.sb, &parent, name).is_none() { + return false; + } + self.free_all_blocks(&child); + self.clear_inode(child_num); + self.free_inode(child_num); + self.uncount_dir(child_num); + // Its `..` linked the parent. + if let Some(mut p) = self.read_inode(parent_num) { + p.links_count = p.links_count.saturating_sub(1); + self.write_inode(parent_num, &p); + } + true + } } diff --git a/crates/servers/storage-server/src/tests/ext4_real.rs b/crates/servers/storage-server/src/tests/ext4_real.rs index dd29cea..e01f3fc 100644 --- a/crates/servers/storage-server/src/tests/ext4_real.rs +++ b/crates/servers/storage-server/src/tests/ext4_real.rs @@ -183,3 +183,38 @@ fn holes_read_as_zeros() { let mut fs = Ext4::mount(FileDisk(img)).expect("mount"); assert_eq!(read_all(&mut fs, b"sparse.bin"), data); } + +#[test] +fn an_empty_directory_goes_and_e2fsck_agrees() { + let Some((dir, img)) = setup("rmdir") else { return }; + let mut fs = Ext4::mount(FileDisk(img)).expect("mount"); + assert!(fs.mkdir(b"/gone")); + assert!(fs.mkdir(b"/gone/deeper")); + let h = fs.create(b"/gone/deeper/f").expect("create"); + fs.close(h); + assert!(!fs.rmdir(b"/gone"), "not empty"); + assert!(fs.delete(b"/gone/deeper/f")); + assert!(fs.rmdir(b"/gone/deeper")); + assert!(fs.rmdir(b"/gone")); + assert!(!names(&mut fs, b"/").iter().any(|n| n == "gone")); + assert!(!fs.rmdir(b"/hello.txt"), "a file is not a directory"); + let (clean, report) = fsck_clean(&dir, &fs.into_device().0); + assert!(clean, "e2fsck: {report}"); +} + +#[test] +fn writes_land_where_seek_says() { + let Some((dir, img)) = setup("seek") else { return }; + let mut fs = Ext4::mount(FileDisk(img)).expect("mount"); + let h = fs.create(b"/f").expect("create"); + assert_eq!(fs.write(h, &[b'a'; 5000]), Some(5000)); + assert!(fs.seek(h, 4090)); + assert_eq!(fs.write(h, b"XYZXYZXYZXYZ"), Some(12)); + assert!(!fs.seek(h, 6000), "not past the end"); + fs.close(h); + let back = read_all(&mut fs, b"/f"); + assert_eq!(back.len(), 5000); + assert_eq!(&back[4088..4104], b"aaXYZXYZXYZXYZaa"); + let (clean, report) = fsck_clean(&dir, &fs.into_device().0); + assert!(clean, "e2fsck: {report}"); +}