diff --git a/.gdbinit b/.gdbinit index 2a135ce..29ef6e0 100644 --- a/.gdbinit +++ b/.gdbinit @@ -3,4 +3,4 @@ target remote localhost:1234 # break machine_mode_entry break *0x800bf000 add-symbol-file target/riscv64/debug/test_pic 0x800bf000 -c +# c diff --git a/Cargo.toml b/Cargo.toml index 1676e3c..8123588 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,8 @@ embedded-alloc = "0.7" kernel-macros = { path = "crates/kernel-macros" } log = "0.4" critical-section = { version = "1", features = ["restore-state-bool"] } -bffs = { path = "../../../code/bffs", features = ["alloc"] } -shared = { path = "crates/shared" } +bffs = { path = "crates/bffs", features = ["alloc"] } +shared = { path = "crates/shared", features = ["kernel"] } # ELF parsing helper goblin = { version = "0.7", default-features = false, features = ["elf32", "elf64", "endian_fd"] } +hashbrown = "0.16" diff --git a/crates/bffs/Cargo.toml b/crates/bffs/Cargo.toml new file mode 100644 index 0000000..dc2f319 --- /dev/null +++ b/crates/bffs/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "bffs" +version = "0.1.0" +edition = "2024" + +[dependencies] +bitflags = "2" + +[features] +alloc = [] +std = ["alloc"] diff --git a/crates/bffs/justfile b/crates/bffs/justfile new file mode 100644 index 0000000..e738719 --- /dev/null +++ b/crates/bffs/justfile @@ -0,0 +1,2 @@ +run: + cargo r --features std diff --git a/crates/bffs/src/boot_sector.rs b/crates/bffs/src/boot_sector.rs new file mode 100644 index 0000000..3d29d6e --- /dev/null +++ b/crates/bffs/src/boot_sector.rs @@ -0,0 +1,165 @@ +use crate::{error::Error, io::{Read, ReadLeExt}}; + +#[derive(Debug, Clone, Copy)] +pub struct Fat32BootSector { + // --- Jump + OEM --- + pub jump_boot: [u8; 3], + pub oem_name: [u8; 8], + + // --- BPB (BIOS Parameter Block) --- + pub bytes_per_sector: u16, + pub sectors_per_cluster: u8, + pub reserved_sector_count: u16, + pub num_fats: u8, + pub root_entry_count: u16, + pub total_sectors_16: u16, + pub media: u8, + pub fat_size_16: u16, + pub sectors_per_track: u16, + pub num_heads: u16, + pub hidden_sectors: u32, + pub total_sectors_32: u32, + + // --- FAT32 Extended BPB --- + pub fat_size_32: u32, + pub ext_flags: u16, + pub fs_version: u16, + pub root_cluster: u32, + pub fs_info: u16, + pub bk_boot_sec: u16, + pub reserved: [u8; 12], + + // --- FAT32 Drive Info --- + pub drive_number: u8, + pub reserved1: u8, + pub boot_signature: u8, + pub volume_id: u32, + pub volume_label: [u8; 11], + pub fs_type: [u8; 8], + + // --- Boot code --- + pub boot_code: [u8; 420], + + // --- Signature --- + pub boot_sector_signature: u16, // 0xAA55 +} + +impl core::fmt::Display for Fat32BootSector { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("Fat32BootSector") + .field("jump_boot", &self.jump_boot) + .field("oem_name", &self.get_oem_name()) + .field("bytes_per_sector", &self.bytes_per_sector) + .field("sectors_per_cluster", &self.sectors_per_cluster) + .field("reserved_sector_count", &self.reserved_sector_count) + .field("num_fats", &self.num_fats) + .field("root_entry_count", &self.root_entry_count) + .field("total_sectors_16", &self.total_sectors_16) + .field("media", &self.media) + .field("fat_size_16", &self.fat_size_16) + .field("sectors_per_track", &self.sectors_per_track) + .field("num_heads", &self.num_heads) + .field("hidden_sectors", &self.hidden_sectors) + .field("total_sectors_32", &self.total_sectors_32) + .field("fat_size_32", &self.fat_size_32) + .field("ext_flags", &self.ext_flags) + .field("fs_version", &self.fs_version) + .field("root_cluster", &self.root_cluster) + .field("fs_info", &self.fs_info) + .field("bk_boot_sec", &self.bk_boot_sec) + .field("reserved", &self.reserved) + .field("drive_number", &self.drive_number) + .field("reserved1", &self.reserved1) + .field("boot_signature", &self.boot_signature) + .field("volume_id", &self.volume_id) + .field("volume_label", &self.get_volume_label()) + .field("fs_type", &self.get_fs_type()) + .field("boot_code", &"[_; 420]") + .field("boot_sector_signature", &self.boot_sector_signature) + .finish() + } +} + +impl Fat32BootSector { + pub fn get_oem_name(&self) -> Result<&str, core::str::Utf8Error> { + str::from_utf8(&self.oem_name) + } + pub fn get_volume_label(&self) -> Result<&str, core::str::Utf8Error> { + str::from_utf8(&self.volume_label) + } + pub fn get_fs_type(&self) -> Result<&str, core::str::Utf8Error> { + str::from_utf8(&self.fs_type) + } +} + +impl Fat32BootSector { + pub fn deserialize(disk: &mut T) -> Result> { + let mut jump_boot = [0u8; _]; + disk.read_exact(&mut jump_boot)?; + let mut oem_name = [0u8; _]; + disk.read_exact(&mut oem_name)?; + let bytes_per_sector = disk.read_u16_le()?; + let sectors_per_cluster = disk.read_u8()?; + let reserved_sector_count = disk.read_u16_le()?; + let num_fats = disk.read_u8()?; + let root_entry_count = disk.read_u16_le()?; + let total_sectors_16 = disk.read_u16_le()?; + let media = disk.read_u8()?; + let fat_size_16 = disk.read_u16_le()?; + let sectors_per_track = disk.read_u16_le()?; + let num_heads = disk.read_u16_le()?; + let hidden_sectors = disk.read_u32_le()?; + let total_sectors_32 = disk.read_u32_le()?; + let fat_size_32 = disk.read_u32_le()?; + let ext_flags = disk.read_u16_le()?; + let fs_version = disk.read_u16_le()?; + let root_cluster = disk.read_u32_le()?; + let fs_info = disk.read_u16_le()?; + let bk_boot_sec = disk.read_u16_le()?; + let mut reserved = [0u8; _]; + disk.read_exact(&mut reserved)?; + let drive_number = disk.read_u8()?; + let reserved1 = disk.read_u8()?; + let boot_signature = disk.read_u8()?; + let volume_id = disk.read_u32_le()?; + let mut volume_label = [0u8; _]; + disk.read_exact(&mut volume_label)?; + let mut fs_type = [0u8; _]; + disk.read_exact(&mut fs_type)?; + let mut boot_code = [0u8; _]; + disk.read_exact(&mut boot_code)?; + let boot_sector_signature = disk.read_u16_le()?; + + Ok(Self { + jump_boot, + oem_name, + bytes_per_sector, + sectors_per_cluster, + reserved_sector_count, + num_fats, + root_entry_count, + total_sectors_16, + media, + fat_size_16, + sectors_per_track, + num_heads, + hidden_sectors, + total_sectors_32, + fat_size_32, + ext_flags, + fs_version, + root_cluster, + fs_info, + bk_boot_sec, + reserved, + drive_number, + reserved1, + boot_signature, + volume_id, + volume_label, + fs_type, + boot_code, + boot_sector_signature, + }) + } +} diff --git a/crates/bffs/src/consts.rs b/crates/bffs/src/consts.rs new file mode 100644 index 0000000..e71410e --- /dev/null +++ b/crates/bffs/src/consts.rs @@ -0,0 +1,18 @@ +use bitflags::bitflags; + +bitflags! { + pub struct FATAttr: u8 { + const READ_ONLY = 0x01; + const HIDDEN = 0x02; + const SYSTEM = 0x04; + const VOLUME_ID = 0x08; + const DIRECTORY = 0x10; + const ARCHIVE = 0x20; + const LFN = 0x0F; + } +} + +pub const FAT32_CLUSTER_MASK: u32 = 0x0FFFFFFF; +pub const FAT32_FREE_CLUSTER: u32 = 0x00000000; +pub const FAT32_BAD_CLUSTER: u32 = 0x0FFFFFF7; +pub const FAT32_END_OF_CHAIN: u32 = 0x0FFFFFF8; diff --git a/crates/bffs/src/dir.rs b/crates/bffs/src/dir.rs new file mode 100644 index 0000000..cf606f6 --- /dev/null +++ b/crates/bffs/src/dir.rs @@ -0,0 +1,103 @@ +use crate::{ + entry::{DirEntry, DirectoryIterator}, + error::Error, + file::{File, RawFile}, + io::{self, IoBase, Read, Seek, Write}, + path::Path, + Fat32FileSystem, ReadSeek, ReadWriteSeek, +}; + +pub struct Dir<'a, T> { + raw: RawFile<'a, T>, + fs: &'a Fat32FileSystem, +} + +impl<'a, T: IoBase> IoBase for Dir<'a, T> { + type Error = T::Error; +} +impl<'a, T: ReadSeek> Seek for Dir<'a, T> { + fn seek(&mut self, pos: io::SeekFrom) -> Result> { + self.raw.seek(pos) + } +} +impl<'a, T: ReadSeek> Read for Dir<'a, T> { + fn read(&mut self, buf: &mut [u8]) -> Result> { + self.raw.read(buf) + } +} +impl<'a, T: ReadWriteSeek> Write for Dir<'a, T> { + fn write(&mut self, buf: &[u8]) -> Result> { + self.raw.write(buf) + } + + fn flush(&mut self) -> Result<(), Error> { + self.raw.flush() + } +} + +impl<'a, T> Dir<'a, T> { + pub fn new(raw: RawFile<'a, T>, fs: &'a Fat32FileSystem) -> Self { + Self { raw, fs } + } + pub fn iter(&self) -> DirectoryIterator<'a, T> { + DirectoryIterator::new(RawFile::new(self.fs, self.raw.first_cluster(), None)) + } +} +impl<'a, T: ReadSeek> Dir<'a, T> { + pub fn open_entry<'b, P: Into>>( + &self, + path: P, + ) -> Result, Error> { + let path = path.into(); + if path.is_absolute() { + return self.fs.open_entry(path); + } + for file in self.iter() { + let f = file?; + if f.name_is(path.as_str()) { + return Ok(f); + } + } + Err(Error::NotFound) + } + pub fn open_file<'b, P: Into>>( + &self, + path: P, + ) -> Result, Error> { + let path = path.into(); + if path.is_absolute() { + return self.fs.open_file(path); + } + let (start, dirname) = path.split_path(); + let entry = self.open_entry(start)?; + match dirname { + Some(name) => { + if !entry.is_dir() { + return Err(Error::NotFound); + } + entry.to_dir().open_file(name) + } + None => { + if !entry.is_file() { + return Err(Error::NotFound); + } + Ok(entry.to_file()) + } + } + } + pub fn open_dir<'b, P: Into>>(&self, path: P) -> Result> { + let path = path.into(); + if path.is_absolute() { + return self.fs.open_dir(path); + } + let (start, dirname) = path.split_path(); + let entry = self.open_entry(start)?; + if !entry.is_dir() { + return Err(Error::NotFound); + } + match dirname { + Some(name) => entry.to_dir().open_dir(name), + None => Ok(entry.to_dir()), + } + } +} diff --git a/crates/bffs/src/entry.rs b/crates/bffs/src/entry.rs new file mode 100644 index 0000000..c4f70b9 --- /dev/null +++ b/crates/bffs/src/entry.rs @@ -0,0 +1,346 @@ +use core::{array::IntoIter, iter::Copied, str::Utf8Error}; +use core::{char::DecodeUtf16, marker::PhantomData, slice::Iter}; + +use crate::{ + consts::FATAttr, + dir::Dir, + error::Error, + file::{File, RawFile}, + io::{IoBase, Read, ReadLeExt}, + Fat32FileSystem, ReadSeek, +}; + +#[cfg(feature = "alloc")] +use alloc::borrow::ToOwned; +#[cfg(feature = "alloc")] +use alloc::string::String; + +#[derive(Debug, Clone, Copy)] +pub struct FatDirEntry { + pub name: [u8; 11], + pub attr: u8, + pub nt_reserved: u8, + pub creation_time_tenth: u8, + pub creation_time: u16, + pub creation_date: u16, + pub last_access_date: u16, + pub first_cluster_high: u16, + pub write_time: u16, + pub write_date: u16, + pub first_cluster_low: u16, + pub file_size: u32, +} + +impl FatDirEntry { + pub fn deserialize(reader: &mut T) -> Result> { + let mut name = [0u8; _]; + reader.read_exact(&mut name)?; + let attr = reader.read_u8()?; + let nt_reserved = reader.read_u8()?; + let creation_time_tenth = reader.read_u8()?; + let creation_time = reader.read_u16_le()?; + let creation_date = reader.read_u16_le()?; + let last_access_date = reader.read_u16_le()?; + let first_cluster_high = reader.read_u16_le()?; + let write_time = reader.read_u16_le()?; + let write_date = reader.read_u16_le()?; + let first_cluster_low = reader.read_u16_le()?; + let file_size = reader.read_u32_le()?; + Ok(Self { + name, + attr, + nt_reserved, + creation_time_tenth, + creation_time, + creation_date, + last_access_date, + first_cluster_high, + write_time, + write_date, + first_cluster_low, + file_size, + }) + } + + pub fn is_dir(&self) -> bool { + (self.attr & FATAttr::DIRECTORY.bits()) != 0 + } + + pub fn is_file(&self) -> bool { + !self.is_dir() + } + + pub fn first_cluster(&self) -> u32 { + ((self.first_cluster_high as u32) << 16) | (self.first_cluster_low as u32) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct FatLfnEntry { + pub sequence_number: u8, + pub name_part1: [u16; 5], + pub attr: u8, + pub type_u: u8, + pub checksum: u8, + pub name_part2: [u16; 6], + pub first_cluster: u16, + pub name_part3: [u16; 2], +} + +impl From for FatLfnEntry { + fn from(value: FatDirEntry) -> Self { + let sequence_number = value.name[0]; + let name_part1 = [ + u16::from_le_bytes([value.name[1], value.name[2]]), + u16::from_le_bytes([value.name[3], value.name[4]]), + u16::from_le_bytes([value.name[5], value.name[6]]), + u16::from_le_bytes([value.name[7], value.name[8]]), + u16::from_le_bytes([value.name[9], value.name[10]]), + ]; + let name_part2 = [ + value.creation_time, + value.creation_date, + value.last_access_date, + value.first_cluster_high, + value.write_time, + value.write_date, + ]; + let name_part3 = [ + value.file_size as u16, + (value.file_size >> u16::BITS) as u16, + ]; + Self { + sequence_number, + name_part1, + attr: value.attr, + type_u: value.nt_reserved, + checksum: value.creation_time_tenth, + name_part2, + first_cluster: value.first_cluster_low, + name_part3, + } + } +} + +pub type FatEntry = u32; + +#[derive(Debug, Clone, Copy)] +pub struct LongFileNameBuilder { + inner: [u16; 256], + _phantom: PhantomData, +} + +impl LongFileNameBuilder { + pub fn new() -> Self { + Self { + inner: [0; _], + _phantom: PhantomData, + } + } + pub fn push(&mut self, entry: &FatLfnEntry, index: usize) { + // Each name entry are 13 bytes long + let char_offset = index * 13; + + if char_offset <= 256 { + // First segment + self.inner[char_offset..char_offset + 5].copy_from_slice(&entry.name_part1); + + if char_offset + 13 <= 256 { + // Second segment + self.inner[char_offset + 5..char_offset + 11].copy_from_slice(&entry.name_part2); + + // Third segment + self.inner[char_offset + 11..char_offset + 13].copy_from_slice(&entry.name_part3); + } else { + // If the name is longer than 247 characters, the last entry is only 9 characters long. + self.inner[char_offset + 5..char_offset + 9] + .copy_from_slice(&entry.name_part2[..4]); + } + } + } + #[cfg(feature = "alloc")] + pub fn to_string(&self) -> Result> { + self.iter().try_collect::() + } +} + +impl IntoIterator for LongFileNameBuilder { + type Item = > as Iterator>::Item; + + type IntoIter = LongFileNameIterator>; + + fn into_iter(self) -> Self::IntoIter { + LongFileNameIterator { + iterator: char::decode_utf16(self.inner), + _phantom: PhantomData, + } + } +} + +impl LongFileNameBuilder { + pub fn iter(&self) -> LongFileNameIterator>> { + LongFileNameIterator { + iterator: char::decode_utf16(self.inner.iter().copied()), + _phantom: PhantomData, + } + } +} + +pub struct LongFileNameIterator> { + iterator: DecodeUtf16, + _phantom: PhantomData, +} + +impl> Iterator for LongFileNameIterator { + type Item = Result>; + + fn next(&mut self) -> Option { + match self.iterator.next()? { + Ok(value) => { + if value == '\0' { + None + } else { + Some(Ok(value)) + } + } + Err(_) => Some(Err(Error::UnsupportedFileNameCharacter)), + } + } +} + +impl Default for LongFileNameBuilder { + fn default() -> Self { + Self::new() + } +} + +pub struct DirectoryIterator<'a, T> { + directory_file: RawFile<'a, T>, +} + +impl<'a, T> DirectoryIterator<'a, T> { + pub fn new(directory_file: RawFile<'a, T>) -> Self { + Self { directory_file } + } +} + +impl<'a, T: ReadSeek> Iterator for DirectoryIterator<'a, T> { + type Item = Result, Error>; + + fn next(&mut self) -> Option { + let mut lfn_builder = LongFileNameBuilder::new(); + let mut has_lfn = false; + + loop { + let entry = match FatDirEntry::deserialize(&mut self.directory_file) { + Ok(v) => v, + Err(e) => return Some(Err(e)), + }; + + // There is no entry after this one + if entry.name[0] == 0x00 { + return None; + } + + // The file was suppressed + if entry.name[0] == 0xE5 { + return self.next(); + } + + if entry.attr == FATAttr::LFN.bits() { + // Current entry is a LongFileName entry + let lfn: FatLfnEntry = entry.into(); + + let index = ((lfn.sequence_number & 0x1F) - 1) as usize; + lfn_builder.push(&lfn, index); + has_lfn = true; + } else { + // Check if the entry is "." or ".." + if entry.name[0] == b'.' + && (entry.name[1] == b'.' || entry.name[1] == b' ') + && entry.name.iter().skip(2).all(|c| *c == b' ') + { + return self.next(); + } else { + return Some(Ok(DirEntry::new( + entry, + entry.name, + has_lfn.then_some(lfn_builder), + self.directory_file.fs, + ))); + } + } + } + } +} + +#[derive(Debug, Clone)] +pub struct DirEntry<'a, T> { + entry: FatDirEntry, + short_name: [u8; 11], + long_name: Option>, + fs: &'a Fat32FileSystem, +} + +impl<'a, T: IoBase> DirEntry<'a, T> { + fn new( + entry: FatDirEntry, + short_name: [u8; 11], + long_name: Option>, + fs: &'a Fat32FileSystem, + ) -> Self { + Self { + entry, + short_name, + long_name, + fs, + } + } + #[cfg(feature = "alloc")] + pub fn name(&self) -> Result> { + if let Some(long_name) = self.long_name() { + long_name.to_string() + } else { + self.short_name() + .map(|n| n.to_owned()) + .map_err(|_| Error::UnsupportedFileNameCharacter) + } + } + pub fn long_name(&self) -> Option<&LongFileNameBuilder> { + self.long_name.as_ref() + } + pub fn short_name(&self) -> Result<&str, Utf8Error> { + str::from_utf8(&self.short_name) + } + pub fn name_is(&self, name: &str) -> bool { + if let Some(long_name) = self.long_name() { + long_name + .iter() + .eq_by(name.chars(), |a, b| a.is_ok_and(|a| a == b)) + } else { + self.short_name().is_ok_and(|n| n == name) + } + } + pub fn is_dir(&self) -> bool { + self.entry.is_dir() + } + pub fn is_file(&self) -> bool { + self.entry.is_file() + } + pub fn to_dir(&self) -> Dir<'a, T> { + Dir::new( + RawFile::new(self.fs, self.entry.first_cluster(), None), + self.fs, + ) + } + pub fn to_file(&self) -> File<'a, T> { + File::new( + RawFile::new( + self.fs, + self.entry.first_cluster(), + Some(self.entry.file_size), + ), + self.fs, + ) + } +} diff --git a/crates/bffs/src/error.rs b/crates/bffs/src/error.rs new file mode 100644 index 0000000..1fa5c6b --- /dev/null +++ b/crates/bffs/src/error.rs @@ -0,0 +1,117 @@ +/// Error enum with all errors that can be returned by functions from this crate +/// +/// Generic parameter `T` is a type of external error returned by the user provided storage +#[derive(Debug)] +#[non_exhaustive] +pub enum Error { + /// A user provided storage instance returned an error during an input/output operation. + Io(T), + /// A read operation cannot be completed because an end of a file has been reached prematurely. + UnexpectedEof, + /// A write operation cannot be completed because `Write::write` returned 0. + WriteZero, + /// A parameter was incorrect. + InvalidInput, + /// A requested file or directory has not been found. + NotFound, + /// A file or a directory with the same name already exists. + AlreadyExists, + /// An operation cannot be finished because a directory is not empty. + DirectoryIsNotEmpty, + /// File system internal structures are corrupted/invalid. + CorruptedFileSystem, + /// There is not enough free space on the storage to finish the requested operation. + NotEnoughSpace, + /// The provided file name is either too long or empty. + InvalidFileNameLength, + /// The provided file name contains an invalid character. + UnsupportedFileNameCharacter, + /// The file content contains invalid UTF-8 characters. + InvalidUTF8, +} + +impl From for Error { + fn from(error: T) -> Self { + Error::Io(error) + } +} + +impl core::fmt::Display for Error { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Error::Io(io_error) => write!(f, "IO error: {}", io_error), + Error::UnexpectedEof => write!(f, "Unexpected end of file"), + Error::NotEnoughSpace => write!(f, "Not enough space"), + Error::WriteZero => write!(f, "Write zero"), + Error::InvalidInput => write!(f, "Invalid input"), + Error::InvalidFileNameLength => write!(f, "Invalid file name length"), + Error::UnsupportedFileNameCharacter => write!(f, "Unsupported file name character"), + Error::DirectoryIsNotEmpty => write!(f, "Directory is not empty"), + Error::NotFound => write!(f, "No such file or directory"), + Error::AlreadyExists => write!(f, "File or directory already exists"), + Error::CorruptedFileSystem => write!(f, "Corrupted file system"), + Error::InvalidUTF8 => write!(f, "File contains invalid UTF-8 characters"), + } + } +} + +/// Trait that should be implemented by errors returned from the user supplied storage. +/// +/// Implementations for `std::io::Error` and `()` are provided by this crate. +pub trait IoError: core::fmt::Debug { + fn is_interrupted(&self) -> bool; + fn new_unexpected_eof_error() -> Self; + fn new_write_zero_error() -> Self; +} + +impl IoError for Error { + fn is_interrupted(&self) -> bool { + match self { + Error::::Io(io_error) => io_error.is_interrupted(), + _ => false, + } + } + + fn new_unexpected_eof_error() -> Self { + Error::::UnexpectedEof + } + + fn new_write_zero_error() -> Self { + Error::::WriteZero + } +} + +impl IoError for () { + fn is_interrupted(&self) -> bool { + false + } + + fn new_unexpected_eof_error() -> Self { + // empty + } + + fn new_write_zero_error() -> Self { + // empty + } +} + +#[cfg(all(feature = "std", not(target_arch = "riscv64")))] +impl IoError for std::io::Error { + fn is_interrupted(&self) -> bool { + self.kind() == std::io::ErrorKind::Interrupted + } + + fn new_unexpected_eof_error() -> Self { + Self::new( + std::io::ErrorKind::UnexpectedEof, + "failed to fill whole buffer", + ) + } + + fn new_write_zero_error() -> Self { + Self::new( + std::io::ErrorKind::WriteZero, + "failed to write whole buffer", + ) + } +} diff --git a/crates/bffs/src/file.rs b/crates/bffs/src/file.rs new file mode 100644 index 0000000..e38ef53 --- /dev/null +++ b/crates/bffs/src/file.rs @@ -0,0 +1,160 @@ +use crate::{ + consts::{FAT32_BAD_CLUSTER, FAT32_END_OF_CHAIN}, + error::Error, + io::{self, IoBase, Read, Seek, Write}, + Fat32FileSystem, ReadSeek, ReadWriteSeek, +}; + +#[derive(Debug, Clone)] +pub struct RawFile<'a, T> { + pub(crate) fs: &'a Fat32FileSystem, + first_cluster: u32, + current_cluster: u32, + pos: u64, + size: Option, +} + +impl<'a, T> RawFile<'a, T> { + pub fn new(fs: &'a Fat32FileSystem, start_cluster: u32, size: Option) -> Self { + Self { + fs, + first_cluster: start_cluster, + current_cluster: start_cluster, + pos: 0, + size, + } + } + pub fn first_cluster(&self) -> u32 { + self.first_cluster + } +} + +impl<'a, T: IoBase> IoBase for RawFile<'a, T> { + type Error = T::Error; +} + +impl<'a, T: ReadSeek> Seek for RawFile<'a, T> { + fn seek(&mut self, pos: io::SeekFrom) -> Result> { + let new_pos = match pos { + io::SeekFrom::Start(s) => s, + io::SeekFrom::Current(c) => (self.pos as i64 + c) as u64, + io::SeekFrom::End(e) => { + let s = self.size.unwrap_or(0) as i64; + (s + e) as u64 + } + }; + + // Position changed, compute the new cluster + if new_pos != self.pos { + let cluster_size = self.fs.cluster_size(); + + let cluster_index = new_pos / cluster_size; + + // Find the new cluster from the start + let mut c = self.first_cluster; + for _ in 0..cluster_index { + c = self.fs.get_next_cluster(c)?; + if c >= FAT32_BAD_CLUSTER { + break; + } + } + self.current_cluster = c; + self.pos = new_pos; + } + Ok(self.pos) + } +} + +impl<'a, T: ReadSeek> Read for RawFile<'a, T> { + fn read(&mut self, buf: &mut [u8]) -> Result> { + let max_len = self + .size + .map(|size| core::cmp::min(buf.len(), size as usize - self.pos as usize)) + .unwrap_or(buf.len()); + let mut total_read = 0; + let cluster_size = self.fs.cluster_size(); + + while total_read < max_len { + // Compute how much can be read in the current cluster + let offset_in_cluster = self.pos % cluster_size; + let remaining_in_cluster = cluster_size - offset_in_cluster; + + let to_read = core::cmp::min(max_len - total_read, remaining_in_cluster as usize); + + // Read from disk + let physical_offset = + self.fs.cluster_to_offset(self.current_cluster) + offset_in_cluster; + self.fs + .device + .borrow_mut() + .seek(io::SeekFrom::Start(physical_offset))?; + let read = self + .fs + .device + .borrow_mut() + .read(&mut buf[total_read..total_read + to_read])?; + + total_read += read; + self.pos += read as u64; + + if read != to_read { + // The read call didn't read all bytes to fill buf, we can return early + return Ok(total_read); + } + + // At the end of a cluster, go to the next one for the next read operation + if self.pos.is_multiple_of(cluster_size) { + if self.current_cluster >= FAT32_END_OF_CHAIN { + break; // End of the fat file + } + self.current_cluster = self.fs.get_next_cluster(self.current_cluster)?; + } + } + + Ok(total_read) + } +} +impl<'a, T: ReadWriteSeek> Write for RawFile<'a, T> { + fn write(&mut self, _buf: &[u8]) -> Result> { + todo!() + } + + fn flush(&mut self) -> Result<(), Error> { + todo!() + } +} + +#[allow(unused)] +pub struct File<'a, T> { + raw: RawFile<'a, T>, + fs: &'a Fat32FileSystem, +} + +impl<'a, T: IoBase> IoBase for File<'a, T> { + type Error = T::Error; +} +impl<'a, T: ReadSeek> Seek for File<'a, T> { + fn seek(&mut self, pos: io::SeekFrom) -> Result> { + self.raw.seek(pos) + } +} +impl<'a, T: ReadSeek> Read for File<'a, T> { + fn read(&mut self, buf: &mut [u8]) -> Result> { + self.raw.read(buf) + } +} +impl<'a, T: ReadWriteSeek> Write for File<'a, T> { + fn write(&mut self, buf: &[u8]) -> Result> { + self.raw.write(buf) + } + + fn flush(&mut self) -> Result<(), Error> { + self.raw.flush() + } +} + +impl<'a, T> File<'a, T> { + pub fn new(raw: RawFile<'a, T>, fs: &'a Fat32FileSystem) -> Self { + Self { raw, fs } + } +} diff --git a/crates/bffs/src/io.rs b/crates/bffs/src/io.rs new file mode 100644 index 0000000..c328aef --- /dev/null +++ b/crates/bffs/src/io.rs @@ -0,0 +1,293 @@ +use crate::error::{Error, IoError}; + +#[cfg(feature = "alloc")] +use alloc::string::String; +#[cfg(feature = "alloc")] +use alloc::vec::Vec; + +/// Provides IO error as an associated type. +/// +/// Must be implemented for all types that also implement at least one of the following traits: `Read`, `Write`, +/// `Seek`. +pub trait IoBase { + /// Type of errors returned by input/output operations. + type Error: IoError; +} + +/// The `Read` trait allows for reading bytes from a source. +/// +/// It is based on the `std::io::Read` trait. +pub trait Read: IoBase { + /// Pull some bytes from this source into the specified buffer, returning how many bytes were read. + /// + /// This function does not provide any guarantees about whether it blocks waiting for data, but if an object needs + /// to block for a read and cannot, it will typically signal this via an Err return value. + /// + /// If the return value of this method is `Ok(n)`, then it must be guaranteed that `0 <= n <= buf.len()`. A nonzero + /// `n` value indicates that the buffer buf has been filled in with n bytes of data from this source. If `n` is + /// `0`, then it can indicate one of two scenarios: + /// + /// 1. This reader has reached its "end of file" and will likely no longer be able to produce bytes. Note that this + /// does not mean that the reader will always no longer be able to produce bytes. + /// 2. The buffer specified was 0 bytes in length. + /// + /// It is not an error if the returned value `n` is smaller than the buffer size, even when the reader is not at + /// the end of the stream yet. This may happen for example because fewer bytes are actually available right now + /// (e. g. being close to end-of-file) or because `read()` was interrupted by a signal. + /// + /// # Errors + /// + /// If this function encounters any form of I/O or other error, an error will be returned. If an error is returned + /// then it must be guaranteed that no bytes were read. + /// An error for which `IoError::is_interrupted` returns true is non-fatal and the read operation should be retried + /// if there is nothing else to do. + fn read(&mut self, buf: &mut [u8]) -> Result>; + + /// Read the exact number of bytes required to fill `buf`. + /// + /// This function reads as many bytes as necessary to completely fill the specified buffer `buf`. + /// + /// # Errors + /// + /// If this function encounters an error for which `IoError::is_interrupted` returns true then the error is ignored + /// and the operation will continue. + /// + /// If this function encounters an end of file before completely filling the buffer, it returns an error + /// instantiated by a call to `IoError::new_unexpected_eof_error`. The contents of `buf` are unspecified in this + /// case. + /// + /// If this function returns an error, it is unspecified how many bytes it has read, but it will never read more + /// than would be necessary to completely fill the buffer. + fn read_exact(&mut self, mut buf: &mut [u8]) -> Result<(), Error> { + while !buf.is_empty() { + match self.read(buf) { + Ok(0) => break, + Ok(n) => { + let tmp = buf; + buf = &mut tmp[n..]; + } + Err(ref e) if e.is_interrupted() => {} + Err(e) => return Err(e), + } + } + if buf.is_empty() { + Ok(()) + } else { + Err(Error::::new_unexpected_eof_error()) + } + } + + #[cfg(feature = "alloc")] + fn read_to_end(&mut self, buf: &mut Vec) -> Result> { + const CHUNK_SIZE: usize = 32; + let start_len = buf.len(); + loop { + let mut chunk_buf = [0; CHUNK_SIZE]; + let read = self.read(&mut chunk_buf)?; + buf.extend_from_slice(&chunk_buf[..read]); + + if read == 0 { + return Ok(buf.len() - start_len); + } + } + } + + #[cfg(feature = "alloc")] + fn read_to_string(&mut self, buf: &mut String) -> Result> { + let read = self.read_to_end(unsafe { buf.as_mut_vec() })?; + + if str::from_utf8(buf.as_bytes()).is_err() { + Err(Error::InvalidUTF8) + } else { + Ok(read) + } + } +} + +/// The `Write` trait allows for writing bytes into the sink. +/// +/// It is based on the `std::io::Write` trait. +pub trait Write: IoBase { + /// Write a buffer into this writer, returning how many bytes were written. + /// + /// # Errors + /// + /// Each call to write may generate an I/O error indicating that the operation could not be completed. If an error + /// is returned then no bytes in the buffer were written to this writer. + /// It is not considered an error if the entire buffer could not be written to this writer. + fn write(&mut self, buf: &[u8]) -> Result>; + + /// Attempts to write an entire buffer into this writer. + /// + /// This method will continuously call `write` until there is no more data to be written or an error is returned. + /// Errors for which `IoError::is_interrupted` method returns true are being skipped. This method will not return + /// until the entire buffer has been successfully written or such an error occurs. + /// If `write` returns 0 before the entire buffer has been written this method will return an error instantiated by + /// a call to `IoError::new_write_zero_error`. + /// + /// # Errors + /// + /// This function will return the first error for which `IoError::is_interrupted` method returns false that `write` + /// returns. + fn write_all(&mut self, mut buf: &[u8]) -> Result<(), Error> { + while !buf.is_empty() { + match self.write(buf) { + Ok(0) => { + return Err(Error::::new_write_zero_error()); + } + Ok(n) => buf = &buf[n..], + Err(ref e) if e.is_interrupted() => {} + Err(e) => return Err(e), + } + } + Ok(()) + } + + /// Flush this output stream, ensuring that all intermediately buffered contents reach their destination. + /// + /// # Errors + /// + /// It is considered an error if not all bytes could be written due to I/O errors or EOF being reached. + fn flush(&mut self) -> Result<(), Error>; +} + +/// Enumeration of possible methods to seek within an I/O object. +/// +/// It is based on the `std::io::SeekFrom` enum. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SeekFrom { + /// Sets the offset to the provided number of bytes. + Start(u64), + /// Sets the offset to the size of this object plus the specified number of bytes. + End(i64), + /// Sets the offset to the current position plus the specified number of bytes. + Current(i64), +} + +/// The `Seek` trait provides a cursor which can be moved within a stream of bytes. +/// +/// It is based on the `std::io::Seek` trait. +pub trait Seek: IoBase { + /// Seek to an offset, in bytes, in a stream. + /// + /// A seek beyond the end of a stream or to a negative position is not allowed. + /// + /// If the seek operation completed successfully, this method returns the new position from the start of the + /// stream. That position can be used later with `SeekFrom::Start`. + /// + /// # Errors + /// Seeking to a negative offset is considered an error. + fn seek(&mut self, pos: SeekFrom) -> Result>; +} + +#[cfg(all(feature = "std", not(target_arch = "riscv64")))] +impl From for std::io::SeekFrom { + fn from(from: SeekFrom) -> Self { + match from { + SeekFrom::Start(n) => std::io::SeekFrom::Start(n), + SeekFrom::End(n) => std::io::SeekFrom::End(n), + SeekFrom::Current(n) => std::io::SeekFrom::Current(n), + } + } +} + +#[cfg(all(feature = "std", not(target_arch = "riscv64")))] +impl From for SeekFrom { + fn from(from: std::io::SeekFrom) -> Self { + match from { + std::io::SeekFrom::Start(n) => SeekFrom::Start(n), + std::io::SeekFrom::End(n) => SeekFrom::End(n), + std::io::SeekFrom::Current(n) => SeekFrom::Current(n), + } + } +} + +#[cfg(all(feature = "std", not(target_arch = "riscv64")))] +impl IoBase for std::fs::File { + type Error = std::io::Error; +} + +#[cfg(all(feature = "std", not(target_arch = "riscv64")))] +impl> Read for T { + fn read(&mut self, buf: &mut [u8]) -> Result> { + self.read(buf).map_err(Error::Io) + } + fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), Error> { + self.read_exact(buf).map_err(Error::Io) + } +} + +#[cfg(all(feature = "std", not(target_arch = "riscv64")))] +impl> Write for T { + fn write(&mut self, buf: &[u8]) -> Result> { + self.write(buf).map_err(Error::Io) + } + + fn write_all(&mut self, buf: &[u8]) -> Result<(), Error> { + self.write_all(buf).map_err(Error::Io) + } + + fn flush(&mut self) -> Result<(), Error> { + self.flush().map_err(Error::Io) + } +} + +#[cfg(all(feature = "std", not(target_arch = "riscv64")))] +impl> Seek for T { + fn seek(&mut self, pos: SeekFrom) -> Result> { + self.seek(pos.into()).map_err(Error::Io) + } +} + +pub(crate) trait ReadLeExt { + type Error; + fn read_u8(&mut self) -> Result>; + fn read_u16_le(&mut self) -> Result>; + fn read_u32_le(&mut self) -> Result>; +} + +impl ReadLeExt for T { + type Error = ::Error; + + fn read_u8(&mut self) -> Result> { + let mut buf = [0_u8; 1]; + self.read_exact(&mut buf)?; + Ok(buf[0]) + } + + fn read_u16_le(&mut self) -> Result> { + let mut buf = [0_u8; 2]; + self.read_exact(&mut buf)?; + Ok(u16::from_le_bytes(buf)) + } + + fn read_u32_le(&mut self) -> Result> { + let mut buf = [0_u8; 4]; + self.read_exact(&mut buf)?; + Ok(u32::from_le_bytes(buf)) + } +} + +#[allow(unused)] +pub(crate) trait WriteLeExt { + type Error; + fn write_u8(&mut self, n: u8) -> Result<(), Error>; + fn write_u16_le(&mut self, n: u16) -> Result<(), Error>; + fn write_u32_le(&mut self, n: u32) -> Result<(), Error>; +} + +impl WriteLeExt for T { + type Error = ::Error; + + fn write_u8(&mut self, n: u8) -> Result<(), Error> { + self.write_all(&[n]) + } + + fn write_u16_le(&mut self, n: u16) -> Result<(), Error> { + self.write_all(&n.to_le_bytes()) + } + + fn write_u32_le(&mut self, n: u32) -> Result<(), Error> { + self.write_all(&n.to_le_bytes()) + } +} diff --git a/crates/bffs/src/lib.rs b/crates/bffs/src/lib.rs new file mode 100644 index 0000000..96ed21e --- /dev/null +++ b/crates/bffs/src/lib.rs @@ -0,0 +1,157 @@ +#![feature(iterator_try_collect, iter_order_by)] +#![cfg_attr(any(not(feature = "std"), target_arch = "riscv64"), no_std)] + +use core::cell::RefCell; +use core::fmt::Display; + +use crate::{ + boot_sector::Fat32BootSector, + consts::FAT32_CLUSTER_MASK, + dir::Dir, + entry::{DirEntry, FatEntry}, + error::Error, + file::{File, RawFile}, + io::{Read, ReadLeExt, Seek, Write}, + path::Path, +}; + +#[cfg(feature = "alloc")] +extern crate alloc; + +pub mod boot_sector; +pub mod consts; +pub mod dir; +pub mod entry; +pub mod error; +pub mod file; +pub mod io; +pub mod path; + +pub trait ReadSeek: Read + Seek {} +impl ReadSeek for T {} +pub trait WriteSeek: Write + Seek {} +impl WriteSeek for T {} +pub trait ReadWriteSeek: Read + Write + Seek {} +impl ReadWriteSeek for T {} + +#[derive(Debug, Clone, Copy)] +pub struct Fat32FsInfo { + pub lead_signature: u32, + pub reserved1: [u8; 480], + pub struct_signature: u32, + pub free_count: u32, + pub next_free: u32, + pub reserved2: [u8; 12], + pub trail_signature: u32, +} + +impl Fat32FsInfo { + pub fn deserialize(disk: &mut T) -> Result> { + let lead_signature = disk.read_u32_le()?; + let mut reserved1 = [0u8; _]; + disk.read_exact(&mut reserved1)?; + let struct_signature = disk.read_u32_le()?; + let free_count = disk.read_u32_le()?; + let next_free = disk.read_u32_le()?; + let mut reserved2 = [0u8; _]; + disk.read_exact(&mut reserved2)?; + let trail_signature = disk.read_u32_le()?; + Ok(Self { + lead_signature, + reserved1, + struct_signature, + free_count, + next_free, + reserved2, + trail_signature, + }) + } +} + +#[derive(Debug)] +pub struct Fat32FileSystem { + device: RefCell, + pub boot_sector: Fat32BootSector, +} + +impl Display for Fat32FileSystem { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.boot_sector.fmt(f) + } +} + +impl Fat32FileSystem { + pub fn new(mut device: T) -> Result> { + device.seek(io::SeekFrom::Start(0))?; + + let boot_sector = Fat32BootSector::deserialize(&mut device)?; + Ok(Self { + device: device.into(), + boot_sector, + }) + } + /// Get the next cluster from the current one + fn get_next_cluster(&self, current_cluster: u32) -> Result> { + let fat_offset = + self.fat_start_offset() + (current_cluster as u64 * size_of::() as u64); + self.device + .borrow_mut() + .seek(io::SeekFrom::Start(fat_offset))?; + + let next = self.device.borrow_mut().read_u32_le()? & FAT32_CLUSTER_MASK; + Ok(next) + } +} +impl Fat32FileSystem { + /// Start offset of the FAT + pub fn fat_start_offset(&self) -> u64 { + (self.boot_sector.reserved_sector_count as u64) * (self.boot_sector.bytes_per_sector as u64) + } + /// Start offset for data + fn data_start_offset(&self) -> u64 { + let fat_size = self.boot_sector.fat_size_32 as u64; + let fats = (self.boot_sector.num_fats as u64) * fat_size; + + fats * self.boot_sector.bytes_per_sector as u64 + self.fat_start_offset() + } + + /// Convert a cluster number to an offset + fn cluster_to_offset(&self, cluster: u32) -> u64 { + let cluster_offset = (cluster.saturating_sub(2)) as u64 + * self.boot_sector.sectors_per_cluster as u64 + * self.boot_sector.bytes_per_sector as u64; + + self.data_start_offset() + cluster_offset + } + + fn cluster_size(&self) -> u64 { + (self.boot_sector.sectors_per_cluster as u64) * (self.boot_sector.bytes_per_sector as u64) + } + + pub fn root_directory(&self) -> Dir<'_, T> { + Dir::new( + RawFile::new(self, self.boot_sector.root_cluster, None), + self, + ) + } +} +impl Fat32FileSystem { + pub fn open_entry<'b, P: Into>>( + &self, + path: P, + ) -> Result, Error> { + let path = path.into().as_str().trim_start_matches("/"); + self.root_directory().open_entry(path) + } + pub fn open_dir<'b, P: Into>>(&self, path: P) -> Result, Error> { + let path = path.into().as_str().trim_start_matches("/"); + self.root_directory().open_dir(path) + } + pub fn open_file<'b, P: Into>>( + &self, + path: P, + ) -> Result, Error> { + let path = path.into().as_str().trim_start_matches("/"); + self.root_directory().open_file(path) + } +} diff --git a/crates/bffs/src/main.rs b/crates/bffs/src/main.rs new file mode 100644 index 0000000..f389a6a --- /dev/null +++ b/crates/bffs/src/main.rs @@ -0,0 +1,34 @@ +#![cfg_attr(any(not(feature = "std"), target_arch = "riscv64"), no_std)] + +#[cfg(all(feature = "std", not(target_arch = "riscv64")))] +pub fn main() { + use bffs::{dir::Dir, io::Read, Fat32FileSystem, ReadSeek}; + let file = std::fs::File::open("disk.img").unwrap(); + let fs = Fat32FileSystem::new(file).unwrap(); + // println!("{:#?}", fs); + + // walk(fs.root_directory()); + + let mut f = fs.open_file("/usr/bin/test_pic").unwrap(); + let mut content = Vec::new(); + f.read_to_end(&mut content).unwrap(); + println!("file content len: {}", content.len()); + + pub fn walk(dir: Dir<'_, T>) { + for entry in dir.iter() { + let entry = entry.unwrap(); + println!("file {}", entry.name().unwrap()); + if entry.is_dir() { + walk(entry.to_dir()) + } else { + let mut file = entry.to_file(); + let mut content = String::new(); + file.read_to_string(&mut content).unwrap(); + println!("file content {}", content); + } + } + } +} + +#[cfg(any(not(feature = "std"), target_arch = "riscv64"))] +pub fn main() {} diff --git a/crates/bffs/src/path.rs b/crates/bffs/src/path.rs new file mode 100644 index 0000000..bb90751 --- /dev/null +++ b/crates/bffs/src/path.rs @@ -0,0 +1,33 @@ +pub struct Path<'a> { + inner: &'a str, +} + +impl<'a> From<&'a str> for Path<'a> { + fn from(value: &'a str) -> Self { + Self { inner: value } + } +} +impl<'a> AsRef for Path<'a> { + fn as_ref(&self) -> &str { + self.inner + } +} + +impl<'a> Path<'a> { + pub fn split_path(&self) -> (&'a str, Option>) { + if let Some((start, end)) = self.inner.split_once("/") { + (start, Some(end.into())) + } else { + (self.inner, None) + } + } + pub fn as_str(&self) -> &'a str { + self.inner + } + pub fn is_absolute(&self) -> bool { + self.inner.starts_with("/") + } + pub fn is_relative(&self) -> bool { + !self.is_absolute() + } +} diff --git a/crates/os-std/Cargo.toml b/crates/os-std/Cargo.toml index 9c2ffae..4074eab 100644 --- a/crates/os-std/Cargo.toml +++ b/crates/os-std/Cargo.toml @@ -5,4 +5,4 @@ edition = "2024" [dependencies] os-std-macros = { path = "../os-std-macros" } -shared = { path = "../shared" } +shared = { path = "../shared", features = ["user"] } diff --git a/crates/os-std/src/lib.rs b/crates/os-std/src/lib.rs index 6d7f864..211a0a8 100644 --- a/crates/os-std/src/lib.rs +++ b/crates/os-std/src/lib.rs @@ -4,6 +4,7 @@ extern crate alloc; pub mod prelude; +pub use shared::fs; pub use shared::syscall; #[macro_export] diff --git a/crates/shared/Cargo.toml b/crates/shared/Cargo.toml index 5e22169..081c69b 100644 --- a/crates/shared/Cargo.toml +++ b/crates/shared/Cargo.toml @@ -4,3 +4,8 @@ version = "0.1.0" edition = "2024" [dependencies] +bffs = { path = "../bffs" } + +[features] +kernel = [] +user = [] diff --git a/crates/shared/src/fs.rs b/crates/shared/src/fs.rs new file mode 100644 index 0000000..49badb7 --- /dev/null +++ b/crates/shared/src/fs.rs @@ -0,0 +1,9 @@ +pub struct File { + fd: u64, +} + +impl File { + pub unsafe fn new(fd: u64) -> Self { + Self { fd } + } +} diff --git a/crates/shared/src/lib.rs b/crates/shared/src/lib.rs index 7b3b5ed..866f9c0 100644 --- a/crates/shared/src/lib.rs +++ b/crates/shared/src/lib.rs @@ -1,3 +1,4 @@ #![no_std] +pub mod fs; pub mod syscall; diff --git a/crates/shared/src/syscall.rs b/crates/shared/src/syscall.rs index ccf37e4..9c96ab5 100644 --- a/crates/shared/src/syscall.rs +++ b/crates/shared/src/syscall.rs @@ -1,7 +1,10 @@ use core::{alloc::Layout, time::Duration}; +use bffs::path::Path; + #[repr(u64)] pub enum SysCall { + Open = 2, Alloc = 40, Dealloc = 41, Exit = 60, @@ -14,6 +17,7 @@ pub enum SysCall { impl From for SysCall { fn from(value: u64) -> Self { match value { + 2 => SysCall::Open, 40 => SysCall::Alloc, 41 => SysCall::Dealloc, 60 => SysCall::Exit, @@ -124,3 +128,12 @@ pub fn dealloc(ptr: *mut u8, layout: core::alloc::Layout) { syscall!(SysCall::Dealloc, ptr as u64, size as u64, align as u64); } } +pub fn open<'a, P: Into>>(path: P) -> u64 { + unsafe { + let path_str = path.into().as_str(); + let ptr = path_str.as_ptr(); + let size = path_str.len(); + let (fd, ..) = syscall!(SysCall::Open, ptr as u64, size as u64); + fd + } +} diff --git a/justfile b/justfile index 4116fe9..1c966f4 100644 --- a/justfile +++ b/justfile @@ -13,6 +13,7 @@ sync_filesystem: build_user_prog prog: RUSTFLAGS="-C relocation-model=pic -C link-arg=-Tuser.ld -C link-arg=-pie" cargo b {{ cargo_flags }} --package {{ prog }} + riscv64-elf-strip {{ bin_path / prog }} cp {{ bin_path / prog }} {{ "mnt/usr/bin" / prog }} # riscv64-elf-objcopy -O binary {{ bin_path / prog }} {{ "mnt/usr/bin" / prog }} @@ -30,6 +31,11 @@ map_dir dir recipe: qemu := "qemu-system-riscv64 -machine virt -device bochs-display -bios none -m 512M -device loader,file=disk.img,addr=0x90000000" +perf: build + {{ qemu }} -perfmap -kernel {{ bin_path / "kernel-rust" }}& + perf record --output=/tmp/perf.data --call-graph=dwarf -F 999 -p $(pidof qemu-system-riscv64) -- sleep 20; exit 0 + cd /tmp && hotspot perf.data + gdb: build {{ qemu }} -s -S -kernel {{ bin_path / "kernel-rust" }}& gf2 diff --git a/src/fs.rs b/src/fs.rs index 0f7b70b..44602ab 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -2,12 +2,17 @@ //! //! Implements a minimal disk backend and exposes a global FILE_SYSTEM used by //! the kernel to load user binaries. -use core::{cell::UnsafeCell, ops::Deref}; +use core::{ + cell::{LazyCell, UnsafeCell}, + ops::Deref, +}; use bffs::{ io::{IoBase, Read, Seek}, + path::Path, Fat32FileSystem, }; +use hashbrown::HashMap; const DISK_ADDR: *const u8 = 0x9000_0000 as *const _; @@ -79,10 +84,20 @@ impl Read for Disk { return Ok(0); } let size = usize::min(buf.len(), (self.size - self.pos) as usize); - (0..size).for_each(|i| { - buf[i] = unsafe { *DISK_ADDR.byte_add(i + self.pos as usize) }; - }); + unsafe { + core::ptr::copy_nonoverlapping( + DISK_ADDR.byte_add(self.pos as usize), + buf.as_mut_ptr(), + size, + ); + } self.pos += size as u64; Ok(size) } } + +pub struct KernelFDTable(LazyCell>>); + +unsafe impl Sync for KernelFDTable {} + +pub static KERNEL_FILE_DESCRIPTOR_TABLE: KernelFDTable = KernelFDTable(LazyCell::new(HashMap::new)); diff --git a/src/interrupt.rs b/src/interrupt.rs index 7326be1..5a56c05 100644 --- a/src/interrupt.rs +++ b/src/interrupt.rs @@ -14,7 +14,7 @@ use crate::{ read_csr, riscv::disable_interrupt, scheduler::scheduler_without_ret, - set_csr, + set_csr, syscall, time::{setup_next_timer_interrupt, IRQ_M_TIMER}, write_csr, }; @@ -57,7 +57,6 @@ unsafe extern "C" fn machine_trap_handler( c if c == EextensionID::Time as usize => match fid { c if c == TimerFunctionID::SetTimer as usize => { clear_csr!(mip, 1 << 5); - // setup_next_timer_interrupt(); } _ => {} }, @@ -122,16 +121,21 @@ unsafe extern "C" fn supervisor_trap_handler( let a3: u64 = unsafe { (*interrupt_state).a[3] }; let syscall: SysCall = syscall_u64.into(); match syscall { + SysCall::Open => { + let path = unsafe { str::from_raw_parts(a1 as *const u8, a2 as usize) }; + let fd = syscall::open(path, false); + unsafe { (*interrupt_state).a[0] = fd.unwrap() }; + } SysCall::Alloc => { let layout = Layout::from_size_align(a1 as usize, a2 as usize).unwrap(); // Allocate memory and put the pointer in a0 - unsafe { (*interrupt_state).a[0] = alloc::alloc::alloc(layout) as u64 }; + unsafe { (*interrupt_state).a[0] = syscall::alloc(layout) as u64 }; } SysCall::Dealloc => { let ptr = a1 as *mut u8; let layout = Layout::from_size_align(a2 as usize, a3 as usize).unwrap(); // Free memory - unsafe { alloc::alloc::dealloc(ptr, layout) }; + unsafe { syscall::dealloc(ptr, layout) }; } SysCall::Exit => exit_process(&mut interrupt_state), SysCall::NanoSleep => sleep(Duration::new(a1, a2 as u32), &mut interrupt_state), diff --git a/src/main.rs b/src/main.rs index 5efd99d..b151f39 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,6 +15,7 @@ ptr_metadata )] +use alloc::boxed::Box; use embedded_alloc::LlffHeap as Heap; use log::info; @@ -38,6 +39,7 @@ mod panic_handler; mod process; mod riscv; mod scheduler; +mod syscall; mod time; mod uart; mod user; @@ -63,8 +65,8 @@ pub extern "C" fn supervisor_mode_entry() { info!("Hello World !"); unsafe { Vga::draw_string(10, 10, "Hello World !", Color::WHITE, Color::BLACK) }; - create_process(&test, "proc1"); - create_process(&proc2, "proc2"); + create_process(Box::new(test), "proc1"); + create_process(Box::new(proc2), "proc2"); create_process_from_file("/usr/bin/test_pic"); diff --git a/src/process.rs b/src/process.rs index 6e223ee..90798db 100644 --- a/src/process.rs +++ b/src/process.rs @@ -12,6 +12,7 @@ use core::time::Duration; use alloc::{boxed::Box, format, string::String, vec::Vec}; use bffs::{io::Read, path::Path}; use goblin::elf::reloc::R_RISCV_RELATIVE; +use hashbrown::HashMap; use shared::syscall::exit; use crate::{ @@ -82,13 +83,15 @@ pub struct Process { /// Current state of the process. pub state: ProcessState, /// Optional entry point for the process code. - pub entry: Option<&'static dyn Fn()>, + pub entry: Option>, /// Wake time for sleeping processes. pub wake_time: Duration, /// Saved execution context. pub ctx: ExecutionContext, /// Process stack. pub stack: [u64; STACK_SIZE], + /// File descriptor table. + pub fd_table: HashMap> } impl core::fmt::Debug for Process { @@ -130,13 +133,19 @@ pub fn create_process_from_file<'a, T: Into>>(path: T) -> i64 { // Open and read the binary file let mut bin = FILE_SYSTEM.open_file(path).unwrap(); + println!("Creating process"); let mut content: Vec = Vec::new(); bin.read_to_end(&mut content).unwrap(); - println!("Loading binary at address: {:x?}", content.as_ptr()); + println!( + "Loading binary at address: {:x?}, length: {}", + content.as_ptr(), + content.len() + ); // If ELF, use goblin to load PT_LOAD segments and apply relocations if let Ok(gelf) = goblin::elf::Elf::parse(&content) { + println!("Parsed"); // Determine memory bounds from program headers let mut min_vaddr = u64::MAX; let mut max_vaddr = 0u64; @@ -179,6 +188,7 @@ pub fn create_process_from_file<'a, T: Into>>(path: T) -> i64 { } } } + println!("Copied"); // Apply relocations using our parser (handles RELA entries) for rela in gelf.dynrelas.iter() { @@ -193,15 +203,16 @@ pub fn create_process_from_file<'a, T: Into>>(path: T) -> i64 { _ => {} } } + println!("Relocated"); // Entry point let entry_va = gelf.entry; let entry_addr = unsafe { base.add((entry_va - min_vaddr) as usize) } as *const u8; let entry_fn = unsafe { core::mem::transmute::<*const u8, extern "C" fn()>(entry_addr) }; - let wrapper = Box::leak(Box::new(move || { + let wrapper = Box::new(move || { entry_fn(); - })); + }); println!("Program loaded at : {:x?}", entry_addr); return create_process(wrapper, name); } @@ -210,9 +221,9 @@ pub fn create_process_from_file<'a, T: Into>>(path: T) -> i64 { // Fallback: treat the file as a raw binary blob and execute in-place let entry_point = unsafe { core::mem::transmute::<*const u8, extern "C" fn()>(Vec::leak(content).as_ptr()) }; - let wrapper = Box::leak(Box::new(move || { + let wrapper = Box::new(move || { entry_point(); - })); + }); create_process(wrapper, name) } @@ -236,7 +247,7 @@ pub fn create_process_from_file<'a, T: Into>>(path: T) -> i64 { /// /// The provided `code` function will be executed when the process is first /// scheduled. Returns the new PID, or -1 if the process table is full. -pub fn create_process, F: Fn()>(code: &'static F, name: T) -> i64 { +pub fn create_process, F: Fn() + 'static>(code: Box, name: T) -> i64 { // Search for a free slot in the process table let mut next_pid = 0; while next_pid < PROCESS_COUNT && unsafe { PROCESS_TABLE[next_pid].state != ProcessState::Dead } @@ -262,7 +273,7 @@ pub fn create_process, F: Fn()>(code: &'static F, name: T) -> i6 // Configure execution context // a0 contains the pointer to the function to execute - process.ctx.a[0] = process.entry.as_ref().unwrap_unchecked() as *const &dyn Fn() as u64; + process.ctx.a[0] = &raw const *process.entry.as_ref().unwrap_unchecked() as u64; // mepc points to process_launcher which will call the function process.ctx.mepc = process_launcher as *const _; @@ -295,7 +306,7 @@ pub fn create_process, F: Fn()>(code: &'static F, name: T) -> i6 /// This function is installed into the process `mepc` so that when the new /// process is scheduled it will run this launcher which calls the user /// function and ensures the process exits cleanly. -extern "C" fn process_launcher(code: *const &dyn Fn()) { +extern "C" fn process_launcher(code: *const Box) { // SAFETY: The code pointer was initialized in create_process // and points to a valid function. unsafe { (*code)() }; diff --git a/src/riscv.rs b/src/riscv.rs index 791464f..50f5406 100644 --- a/src/riscv.rs +++ b/src/riscv.rs @@ -105,7 +105,7 @@ macro_rules! mret { } #[unsafe(naked)] -pub extern "C" fn exit_qemu() { +pub extern "C" fn exit_qemu() -> ! { naked_asm!( " li a0, 0x100000 diff --git a/src/scheduler.rs b/src/scheduler.rs index bab27d6..1d9c896 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -5,7 +5,8 @@ //! and a simple round-robin scheduler used by the kernel. use core::{arch::riscv64::wfi, array, cell::LazyCell, time::Duration}; -use alloc::string::String; +use alloc::{boxed::Box, string::String}; +use hashbrown::HashMap; use log::info; use crate::{ @@ -43,6 +44,7 @@ pub static mut PROCESS_TABLE: LazyCell<[Process; PROCESS_COUNT]> = LazyCell::new }, stack: [0; _], entry: None, + fd_table: HashMap::new() }) }); @@ -71,7 +73,7 @@ pub fn scheduler_init() { } } - create_process(&idle, "idle"); + create_process(Box::new(idle), "idle"); unsafe { PROCESS_TABLE[0].state = ProcessState::Active; } diff --git a/src/syscall.rs b/src/syscall.rs new file mode 100644 index 0000000..aa08ab7 --- /dev/null +++ b/src/syscall.rs @@ -0,0 +1,27 @@ +use core::alloc::Layout; + +use bffs::{error::Error, path::Path}; + +use crate::fs::{Disk, FILE_SYSTEM}; + +pub unsafe fn alloc(layout: Layout) -> *mut u8 { + unsafe { alloc::alloc::alloc(layout) } +} + +pub unsafe fn dealloc(ptr: *mut u8, layout: core::alloc::Layout) { + unsafe { alloc::alloc::dealloc(ptr, layout) } +} + +pub fn open<'a, P: Into>>( + path: P, + in_kernel: bool, +) -> Result::Error>> { + let path = path.into(); + let file = match path.split_path() { + ("dev", path) => { + unimplemented!() + } + _ => FILE_SYSTEM.open_file(path)?, + }; + Ok(42) +} diff --git a/user/test_pic/src/main.rs b/user/test_pic/src/main.rs index a8d9d06..7ee438e 100644 --- a/user/test_pic/src/main.rs +++ b/user/test_pic/src/main.rs @@ -6,8 +6,8 @@ fn main() { let mut test = String::new(); test.push('A'); test.push('B'); - for _ in 0..50 { + for _ in 0..100 { test.push('C'); } - println!("Hello from PIC program loaded dynamically with custom std and a better justfile, and syscalls !"); + println!("Hello from PIC program loaded dynamically with custom std and a better justfile, and syscalls ! {}", test); }