diff --git a/docs/filesystem.md b/docs/filesystem.md new file mode 100644 index 0000000..bf05abc --- /dev/null +++ b/docs/filesystem.md @@ -0,0 +1,41 @@ +# Filesystem: first milestone + +Rustalis currently has a fixed-capacity, allocation-free RAM filesystem prototype in +`kernel/src/fs.rs`. It is a bring-up primitive for use before the kernel heap, +block-device layer, and persistent filesystem driver exist. + +## Current API + +- `RamFs::::new()`: create a filesystem with N file slots. +- `create(path)`: create an empty file at an absolute path. +- `write(path, bytes)`: replace a file's contents. +- `read(path)`: borrow file contents as bytes. +- `remove(path)`: remove a file and release its slot. +- `path_at(index)` and `file_count()`: enumerate occupied entries. + +Paths must be absolute, non-root paths. Empty components, trailing slashes, `.`, +and `..` are rejected. Each path is limited to 64 bytes and each file to 256 +bytes. Capacity is selected at compile time. Errors are returned as `FsError`; +the implementation does not allocate or perform disk I/O. + +## Important limitations + +This is **not yet a persistent filesystem or a complete VFS**. Data is held in +kernel memory and disappears on reboot. The current prototype does not implement +directories as independent objects, file descriptors, permissions, concurrent +access, block devices, mounting, or a disk format. It must not be used as a +security boundary. + +## Next milestones + +1. Add a VFS trait and mount-point/path resolution. +2. Add directory metadata and file metadata (size, type, timestamps, permissions). +3. Define a block-device interface with bounds-checked sector I/O. +4. Implement a small on-disk filesystem behind the VFS. +5. Add file descriptors and syscalls only after process isolation and syscall + validation are in place. +6. Test storage parsing against malformed images and run integration tests in QEMU. + +The kernel boot path performs a small smoke check by creating and reading +`/welcome.txt`. Unit tests cover CRUD behavior, invalid paths, duplicate names, +capacity limits, and oversized writes. diff --git a/kernel/src/fs.rs b/kernel/src/fs.rs new file mode 100644 index 0000000..f8e9448 --- /dev/null +++ b/kernel/src/fs.rs @@ -0,0 +1,162 @@ +//! Fixed-capacity in-memory filesystem primitives for early Rustalis bring-up. +//! +//! This is a RAM-backed prototype, not persistent storage. It deliberately avoids +//! allocation so it can be used before the kernel heap and block-device layer exist. + +const MAX_PATH_BYTES: usize = 64; +const MAX_FILE_BYTES: usize = 256; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum FsError { + InvalidPath, + PathTooLong, + FileTooLarge, + AlreadyExists, + NotFound, + NoFreeSlots, +} + +/// A small, allocation-free filesystem suitable for kernel bring-up. +pub struct RamFs { + entries: [Entry; FILES], +} + +#[derive(Clone, Copy)] +struct Entry { + used: bool, + path: [u8; MAX_PATH_BYTES], + path_len: usize, + data: [u8; MAX_FILE_BYTES], + data_len: usize, +} + +impl Entry { + const EMPTY: Self = Self { + used: false, + path: [0; MAX_PATH_BYTES], + path_len: 0, + data: [0; MAX_FILE_BYTES], + data_len: 0, + }; + + fn path(&self) -> &str { + // Paths are validated as UTF-8 before they are stored. + core::str::from_utf8(&self.path[..self.path_len]) + .expect("stored filesystem path must be valid UTF-8") + } +} + +impl RamFs { + pub const fn new() -> Self { + Self { + entries: [Entry::EMPTY; FILES], + } + } + + /// Create an empty file at an absolute path. + pub fn create(&mut self, path: &str) -> Result<(), FsError> { + let path = validate_path(path)?; + if self.find(path).is_some() { + return Err(FsError::AlreadyExists); + } + let slot = self.entries.iter().position(|entry| !entry.used) + .ok_or(FsError::NoFreeSlots)?; + let entry = &mut self.entries[slot]; + entry.path[..path.len()].copy_from_slice(path.as_bytes()); + entry.path_len = path.len(); + entry.data_len = 0; + entry.used = true; + Ok(()) + } + + /// Replace a file's contents. Fails without modifying the file if too large. + pub fn write(&mut self, path: &str, bytes: &[u8]) -> Result<(), FsError> { + let path = validate_path(path)?; + if bytes.len() > MAX_FILE_BYTES { + return Err(FsError::FileTooLarge); + } + let index = self.find(path).ok_or(FsError::NotFound)?; + let entry = &mut self.entries[index]; + entry.data[..bytes.len()].copy_from_slice(bytes); + entry.data_len = bytes.len(); + Ok(()) + } + + /// Read a file as bytes; contents need not be UTF-8. + pub fn read(&self, path: &str) -> Result<&[u8], FsError> { + let path = validate_path(path)?; + let index = self.find(path).ok_or(FsError::NotFound)?; + let entry = &self.entries[index]; + Ok(&entry.data[..entry.data_len]) + } + + /// Remove a file and release its slot. + pub fn remove(&mut self, path: &str) -> Result<(), FsError> { + let path = validate_path(path)?; + let index = self.find(path).ok_or(FsError::NotFound)?; + self.entries[index] = Entry::EMPTY; + Ok(()) + } + + /// Return the path of the Nth occupied slot, for simple directory listings. + pub fn path_at(&self, occupied_index: usize) -> Option<&str> { + self.entries.iter().filter(|entry| entry.used) + .nth(occupied_index).map(Entry::path) + } + + pub fn file_count(&self) -> usize { + self.entries.iter().filter(|entry| entry.used).count() + } + + fn find(&self, path: &str) -> Option { + self.entries.iter().position(|entry| entry.used && entry.path() == path) + } +} + +fn validate_path(path: &str) -> Result<&str, FsError> { + if !path.starts_with('/') || path == "/" || path.ends_with('/') { + return Err(FsError::InvalidPath); + } + if path.len() > MAX_PATH_BYTES { + return Err(FsError::PathTooLong); + } + if path.split('/').any(|part| part.is_empty() || part == "." || part == "..") { + return Err(FsError::InvalidPath); + } + Ok(path) +} + +#[cfg(test)] +mod tests { + use super::{FsError, RamFs}; + + #[test] + fn create_write_read_and_remove() { + let mut fs = RamFs::<4>::new(); + fs.create("/hello.txt").unwrap(); + fs.write("/hello.txt", b"Rustalis").unwrap(); + assert_eq!(fs.read("/hello.txt").unwrap(), b"Rustalis"); + assert_eq!(fs.file_count(), 1); + fs.remove("/hello.txt").unwrap(); + assert_eq!(fs.read("/hello.txt"), Err(FsError::NotFound)); + assert_eq!(fs.file_count(), 0); + } + + #[test] + fn rejects_traversal_and_duplicate_paths() { + let mut fs = RamFs::<2>::new(); + assert_eq!(fs.create("/../secret"), Err(FsError::InvalidPath)); + fs.create("/note.txt").unwrap(); + assert_eq!(fs.create("/note.txt"), Err(FsError::AlreadyExists)); + } + + #[test] + fn reports_capacity_and_size_errors_without_corrupting_data() { + let mut fs = RamFs::<1>::new(); + fs.create("/a").unwrap(); + assert_eq!(fs.create("/b"), Err(FsError::NoFreeSlots)); + fs.write("/a", b"ok").unwrap(); + assert_eq!(fs.write("/a", &[0; 257]), Err(FsError::FileTooLarge)); + assert_eq!(fs.read("/a").unwrap(), b"ok"); + } +} diff --git a/kernel/src/main.rs b/kernel/src/main.rs index 7d2fb3b..649d844 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -5,6 +5,7 @@ use bootloader_api::{entry_point, BootInfo}; use core::panic::PanicInfo; mod arch; +mod fs; mod logger; mod memory; @@ -17,17 +18,36 @@ fn kernel_main(boot_info: &'static mut BootInfo) -> ! { logger::info("Boot information received"); arch::x86_64::init(); logger::info("x86_64 architecture layer initialized"); + let summary = memory::summarize(&boot_info.memory_regions); logger::info_u64("Memory regions", summary.region_count as u64); logger::info_u64("Usable memory bytes", summary.usable_bytes); logger::info_u64("Reserved memory bytes", summary.reserved_bytes); + + // Exercise the allocation-free RAM filesystem before the heap and storage + // drivers exist. This is volatile memory and is reset on every reboot. + let mut ramfs = fs::RamFs::<8>::new(); + if ramfs.create("/welcome.txt").is_ok() + && ramfs.write("/welcome.txt", b"Welcome to Rustalis").is_ok() + && ramfs.read("/welcome.txt") == Ok(&b"Welcome to Rustalis"[..]) + { + logger::info("RAM filesystem initialized; /welcome.txt verified"); + logger::info_u64("RAM filesystem files", ramfs.file_count() as u64); + } else { + logger::error("RAM filesystem smoke test failed"); + } + logger::info("Rustalis booted successfully."); - loop { core::hint::spin_loop(); } + loop { + core::hint::spin_loop(); + } } #[panic_handler] fn panic(info: &PanicInfo) -> ! { logger::error("KERNEL PANIC"); logger::panic_info(info); - loop { core::hint::spin_loop(); } + loop { + core::hint::spin_loop(); + } }