From 5e5b703a566c2980f0e5a67db0214af665cb829f Mon Sep 17 00:00:00 2001 From: Nilton Constantino Date: Fri, 16 Jan 2026 22:24:24 +0000 Subject: [PATCH] PR-09 - VirtualFS --- .gitignore | 3 + crates/host-desktop/src/fs_desktop_backend.rs | 112 +++++++++++++ crates/host-desktop/src/main.rs | 14 +- crates/host-desktop/src/prometeu_runner.rs | 24 ++- crates/prometeu-core/src/fs/fs_backend.rs | 12 ++ crates/prometeu-core/src/fs/fs_entry.rs | 5 + crates/prometeu-core/src/fs/fs_error.rs | 24 +++ crates/prometeu-core/src/fs/fs_state.rs | 8 + crates/prometeu-core/src/fs/mod.rs | 11 ++ crates/prometeu-core/src/fs/virtual_fs.rs | 147 ++++++++++++++++++ crates/prometeu-core/src/lib.rs | 1 + .../src/prometeu_os/prometeu_os.rs | 139 +++++++++++++++++ 12 files changed, 497 insertions(+), 3 deletions(-) create mode 100644 crates/host-desktop/src/fs_desktop_backend.rs create mode 100644 crates/prometeu-core/src/fs/fs_backend.rs create mode 100644 crates/prometeu-core/src/fs/fs_entry.rs create mode 100644 crates/prometeu-core/src/fs/fs_error.rs create mode 100644 crates/prometeu-core/src/fs/fs_state.rs create mode 100644 crates/prometeu-core/src/fs/mod.rs create mode 100644 crates/prometeu-core/src/fs/virtual_fs.rs diff --git a/.gitignore b/.gitignore index cbee5a1b..3e5f3ff2 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,6 @@ ehthumbs.db .env .env.* +mnt +mnt/** + diff --git a/crates/host-desktop/src/fs_desktop_backend.rs b/crates/host-desktop/src/fs_desktop_backend.rs new file mode 100644 index 00000000..8a65e429 --- /dev/null +++ b/crates/host-desktop/src/fs_desktop_backend.rs @@ -0,0 +1,112 @@ +use std::path::PathBuf; +use std::fs; +use prometeu_core::fs::{FsBackend, FsEntry, FsError}; + +pub struct HostDirBackend { + root: PathBuf, +} + +impl HostDirBackend { + pub fn new(root: impl Into) -> Self { + Self { root: root.into() } + } + + fn resolve(&self, path: &str) -> PathBuf { + let path = path.trim_start_matches('/'); + self.root.join(path) + } +} + +impl FsBackend for HostDirBackend { + fn mount(&mut self) -> Result<(), FsError> { + if !self.root.exists() { + return Err(FsError::NotFound); + } + + for dir in &["system", "apps", "media", "user"] { + let path = self.root.join(dir); + if !path.exists() { + fs::create_dir_all(&path).map_err(|e| FsError::IOError(e.to_string()))?; + } + } + + Ok(()) + } + + fn unmount(&mut self) {} + + fn list_dir(&self, path: &str) -> Result, FsError> { + let full_path = self.resolve(path); + let entries = fs::read_dir(full_path).map_err(|e| FsError::IOError(e.to_string()))?; + + let mut result = Vec::new(); + for entry in entries { + let entry = entry.map_err(|e| FsError::IOError(e.to_string()))?; + let metadata = entry.metadata().map_err(|e| FsError::IOError(e.to_string()))?; + result.push(FsEntry { + name: entry.file_name().to_string_lossy().into_owned(), + is_dir: metadata.is_dir(), + size: metadata.len(), + }); + } + Ok(result) + } + + fn read_file(&self, path: &str) -> Result, FsError> { + let full_path = self.resolve(path); + fs::read(full_path).map_err(|e| match e.kind() { + std::io::ErrorKind::NotFound => FsError::NotFound, + _ => FsError::IOError(e.to_string()), + }) + } + + fn write_file(&mut self, path: &str, data: &[u8]) -> Result<(), FsError> { + let full_path = self.resolve(path); + if let Some(parent) = full_path.parent() { + fs::create_dir_all(parent).map_err(|e| FsError::IOError(e.to_string()))?; + } + fs::write(full_path, data).map_err(|e| FsError::IOError(e.to_string())) + } + + fn delete(&mut self, path: &str) -> Result<(), FsError> { + let full_path = self.resolve(path); + if full_path.is_dir() { + fs::remove_dir_all(full_path).map_err(|e| FsError::IOError(e.to_string())) + } else { + fs::remove_file(full_path).map_err(|e| FsError::IOError(e.to_string())) + } + } + + fn exists(&self, path: &str) -> bool { + self.resolve(path).exists() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use std::env; + + fn get_temp_dir(name: &str) -> PathBuf { + let mut path = env::temp_dir(); + path.push(format!("prometeu_host_test_{}_{}", name, std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos())); + fs::create_dir_all(&path).unwrap(); + path + } + + #[test] + fn test_host_dir_backend_mount_and_dirs() { + let root = get_temp_dir("mount"); + let mut backend = HostDirBackend::new(root.clone()); + + backend.mount().unwrap(); + + assert!(root.join("system").is_dir()); + assert!(root.join("apps").is_dir()); + assert!(root.join("media").is_dir()); + assert!(root.join("user").is_dir()); + + let _ = fs::remove_dir_all(root); + } +} diff --git a/crates/host-desktop/src/main.rs b/crates/host-desktop/src/main.rs index cdf2241d..625ecf1f 100644 --- a/crates/host-desktop/src/main.rs +++ b/crates/host-desktop/src/main.rs @@ -1,13 +1,25 @@ mod audio_mixer; mod prometeu_runner; +mod fs_desktop_backend; use crate::prometeu_runner::PrometeuRunner; use winit::event_loop::EventLoop; fn main() -> Result<(), Box> { + let args: Vec = std::env::args().collect(); + let mut fs_root = None; + let mut i = 0; + while i < args.len() { + if args[i] == "--fs-root" && i + 1 < args.len() { + fs_root = Some(args[i + 1].clone()); + i += 1; + } + i += 1; + } + let event_loop = EventLoop::new()?; - let mut runner = PrometeuRunner::new(); + let mut runner = PrometeuRunner::new(fs_root); event_loop.run_app(&mut runner)?; Ok(()) diff --git a/crates/host-desktop/src/prometeu_runner.rs b/crates/host-desktop/src/prometeu_runner.rs index ec215002..d062c2c5 100644 --- a/crates/host-desktop/src/prometeu_runner.rs +++ b/crates/host-desktop/src/prometeu_runner.rs @@ -1,4 +1,5 @@ use crate::audio_mixer::AudioMixer; +use crate::fs_desktop_backend::HostDirBackend; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; use pixels::{Pixels, SurfaceTexture}; use prometeu_core::firmware::Firmware; @@ -23,6 +24,7 @@ pub struct PrometeuRunner { firmware: Firmware, input_signals: InputSignals, + fs_root: Option, frame_target_dt: Duration, last_frame_time: Instant, @@ -39,15 +41,22 @@ pub struct PrometeuRunner { } impl PrometeuRunner { - pub(crate) fn new() -> Self { + pub(crate) fn new(fs_root: Option) -> Self { let target_fps = 60; + let mut firmware = Firmware::new(); + if let Some(root) = &fs_root { + let backend = HostDirBackend::new(root); + firmware.os.mount_fs(Box::new(backend)); + } + Self { window: None, pixels: None, hardware: Hardware::new(), - firmware: Firmware::new(), + firmware, input_signals: InputSignals::default(), + fs_root, frame_target_dt: Duration::from_nanos(1_000_000_000 / target_fps), last_frame_time: Instant::now(), accumulator: Duration::ZERO, @@ -243,6 +252,17 @@ impl ApplicationHandler for PrometeuRunner { } fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { + // Atualiza estado do Filesystem no OS (específico do host-desktop) + if let Some(root) = &self.fs_root { + use prometeu_core::fs::FsState; + if matches!(self.firmware.os.fs_state, FsState::Unmounted | FsState::Error(_)) { + if std::path::Path::new(root).exists() { + let backend = HostDirBackend::new(root); + self.firmware.os.mount_fs(Box::new(backend)); + } + } + } + let now = Instant::now(); let mut frame_delta = now.duration_since(self.last_frame_time); diff --git a/crates/prometeu-core/src/fs/fs_backend.rs b/crates/prometeu-core/src/fs/fs_backend.rs new file mode 100644 index 00000000..00257b0d --- /dev/null +++ b/crates/prometeu-core/src/fs/fs_backend.rs @@ -0,0 +1,12 @@ +use crate::fs::{FsEntry, FsError}; + +pub trait FsBackend: Send + Sync { + fn mount(&mut self) -> Result<(), FsError>; + fn unmount(&mut self); + fn list_dir(&self, path: &str) -> Result, FsError>; + fn read_file(&self, path: &str) -> Result, FsError>; + fn write_file(&mut self, path: &str, data: &[u8]) -> Result<(), FsError>; + fn delete(&mut self, path: &str) -> Result<(), FsError>; + fn exists(&self, path: &str) -> bool; + fn is_healthy(&self) -> bool { true } +} diff --git a/crates/prometeu-core/src/fs/fs_entry.rs b/crates/prometeu-core/src/fs/fs_entry.rs new file mode 100644 index 00000000..e371f6da --- /dev/null +++ b/crates/prometeu-core/src/fs/fs_entry.rs @@ -0,0 +1,5 @@ +pub struct FsEntry { + pub name: String, + pub is_dir: bool, + pub size: u64, +} diff --git a/crates/prometeu-core/src/fs/fs_error.rs b/crates/prometeu-core/src/fs/fs_error.rs new file mode 100644 index 00000000..e0b055c0 --- /dev/null +++ b/crates/prometeu-core/src/fs/fs_error.rs @@ -0,0 +1,24 @@ +use std::fmt; + +#[derive(Debug, Clone, PartialEq)] +pub enum FsError { + NotFound, + AlreadyExists, + PermissionDenied, + NotMounted, + IOError(String), + Other(String), +} + +impl fmt::Display for FsError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + FsError::NotFound => write!(f, "File or directory not found"), + FsError::AlreadyExists => write!(f, "Already exists"), + FsError::PermissionDenied => write!(f, "Permission denied"), + FsError::NotMounted => write!(f, "Filesystem not mounted"), + FsError::IOError(s) => write!(f, "IO Error: {}", s), + FsError::Other(s) => write!(f, "Error: {}", s), + } + } +} diff --git a/crates/prometeu-core/src/fs/fs_state.rs b/crates/prometeu-core/src/fs/fs_state.rs new file mode 100644 index 00000000..26d95c96 --- /dev/null +++ b/crates/prometeu-core/src/fs/fs_state.rs @@ -0,0 +1,8 @@ +use crate::fs::FsError; + +#[derive(Debug, Clone, PartialEq)] +pub enum FsState { + Unmounted, + Mounted, + Error(FsError), +} diff --git a/crates/prometeu-core/src/fs/mod.rs b/crates/prometeu-core/src/fs/mod.rs new file mode 100644 index 00000000..7b44e66a --- /dev/null +++ b/crates/prometeu-core/src/fs/mod.rs @@ -0,0 +1,11 @@ +mod fs_error; +mod fs_state; +mod fs_entry; +mod fs_backend; +mod virtual_fs; + +pub use fs_error::FsError; +pub use fs_state::FsState; +pub use fs_entry::FsEntry; +pub use fs_backend::FsBackend; +pub use virtual_fs::VirtualFS; diff --git a/crates/prometeu-core/src/fs/virtual_fs.rs b/crates/prometeu-core/src/fs/virtual_fs.rs new file mode 100644 index 00000000..4a4cff63 --- /dev/null +++ b/crates/prometeu-core/src/fs/virtual_fs.rs @@ -0,0 +1,147 @@ +use crate::fs::{FsBackend, FsEntry, FsError}; + +pub struct VirtualFS { + backend: Option>, +} + +impl VirtualFS { + pub fn new() -> Self { + Self { backend: None } + } + + pub fn mount(&mut self, mut backend: Box) -> Result<(), FsError> { + backend.mount()?; + self.backend = Some(backend); + Ok(()) + } + + pub fn unmount(&mut self) { + if let Some(mut backend) = self.backend.take() { + backend.unmount(); + } + } + + pub fn is_mounted(&self) -> bool { + self.backend.is_some() + } + + fn normalize_path(&self, path: &str) -> String { + let mut normalized = path.replace('\\', "/"); + if !normalized.starts_with('/') { + normalized = format!("/{}", normalized); + } + normalized + } + + pub fn list_dir(&self, path: &str) -> Result, FsError> { + let normalized = self.normalize_path(path); + let backend = self.backend.as_ref().ok_or(FsError::NotMounted)?; + backend.list_dir(&normalized) + } + + pub fn read_file(&self, path: &str) -> Result, FsError> { + let normalized = self.normalize_path(path); + let backend = self.backend.as_ref().ok_or(FsError::NotMounted)?; + backend.read_file(&normalized) + } + + pub fn write_file(&mut self, path: &str, data: &[u8]) -> Result<(), FsError> { + let normalized = self.normalize_path(path); + let backend = self.backend.as_mut().ok_or(FsError::NotMounted)?; + backend.write_file(&normalized, data) + } + + pub fn delete(&mut self, path: &str) -> Result<(), FsError> { + let normalized = self.normalize_path(path); + let backend = self.backend.as_mut().ok_or(FsError::NotMounted)?; + backend.delete(&normalized) + } + + pub fn exists(&self, path: &str) -> bool { + let normalized = self.normalize_path(path); + self.backend.as_ref().map(|b| b.exists(&normalized)).unwrap_or(false) + } + + pub fn is_healthy(&self) -> bool { + self.backend.as_ref().map(|b| b.is_healthy()).unwrap_or(false) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashMap; + + struct MockBackend { + files: HashMap>, + healthy: bool, + } + + impl MockBackend { + fn new() -> Self { + Self { + files: HashMap::new(), + healthy: true, + } + } + } + + impl FsBackend for MockBackend { + fn mount(&mut self) -> Result<(), FsError> { Ok(()) } + fn unmount(&mut self) {} + fn list_dir(&self, _path: &str) -> Result, FsError> { + Ok(self.files.keys().map(|name| FsEntry { + name: name.clone(), + is_dir: false, + size: 0, + }).collect()) + } + fn read_file(&self, path: &str) -> Result, FsError> { + self.files.get(path).cloned().ok_or(FsError::NotFound) + } + fn write_file(&mut self, path: &str, data: &[u8]) -> Result<(), FsError> { + self.files.insert(path.to_string(), data.to_vec()); + Ok(()) + } + fn delete(&mut self, path: &str) -> Result<(), FsError> { + self.files.remove(path); + Ok(()) + } + fn exists(&self, path: &str) -> bool { + self.files.contains_key(path) + } + fn is_healthy(&self) -> bool { + self.healthy + } + } + + #[test] + fn test_virtual_fs_operations() { + let mut vfs = VirtualFS::new(); + let backend = MockBackend::new(); + + vfs.mount(Box::new(backend)).unwrap(); + + let test_file = "/user/test.txt"; + let content = b"hello world"; + + vfs.write_file(test_file, content).unwrap(); + assert!(vfs.exists(test_file)); + + let read_content = vfs.read_file(test_file).unwrap(); + assert_eq!(read_content, content); + + vfs.delete(test_file).unwrap(); + assert!(!vfs.exists(test_file)); + } + + #[test] + fn test_virtual_fs_health() { + let mut vfs = VirtualFS::new(); + let mut backend = MockBackend::new(); + backend.healthy = false; + + vfs.mount(Box::new(backend)).unwrap(); + assert!(!vfs.is_healthy()); + } +} diff --git a/crates/prometeu-core/src/lib.rs b/crates/prometeu-core/src/lib.rs index 98d09d44..10f5becd 100644 --- a/crates/prometeu-core/src/lib.rs +++ b/crates/prometeu-core/src/lib.rs @@ -2,6 +2,7 @@ pub mod hardware; pub mod virtual_machine; mod model; pub mod firmware; +pub mod fs; mod prometeu_os; mod prometeu_hub; diff --git a/crates/prometeu-core/src/prometeu_os/prometeu_os.rs b/crates/prometeu-core/src/prometeu_os/prometeu_os.rs index c562bf7f..71c79320 100644 --- a/crates/prometeu-core/src/prometeu_os/prometeu_os.rs +++ b/crates/prometeu-core/src/prometeu_os/prometeu_os.rs @@ -2,7 +2,9 @@ use crate::hardware::{HardwareBridge, InputSignals}; use crate::model::{Cartridge, Color, Sample}; use crate::prometeu_os::NativeInterface; use crate::virtual_machine::{Value, VirtualMachine}; +use crate::fs::{VirtualFS, FsBackend, FsState}; use std::sync::Arc; +use std::collections::HashMap; /// PrometeuOS (POS): O firmware/base do sistema. /// Autoridade máxima do boot, periféricos, execução da PVM e tratamento de falhas. @@ -17,6 +19,12 @@ pub struct PrometeuOS { pub sample_square: Option>, pub sample_kick: Option>, pub sample_snare: Option>, + + // Filesystem + pub fs: VirtualFS, + pub fs_state: FsState, + pub open_files: HashMap, + pub next_handle: u32, } impl PrometeuOS { @@ -33,6 +41,10 @@ impl PrometeuOS { sample_square: None, sample_kick: None, sample_snare: None, + fs: VirtualFS::new(), + fs_state: FsState::Unmounted, + open_files: HashMap::new(), + next_handle: 1, }; // Inicializa samples básicos (mesma lógica do LogicalHardware) @@ -41,6 +53,30 @@ impl PrometeuOS { os } + pub fn mount_fs(&mut self, backend: Box) { + match self.fs.mount(backend) { + Ok(_) => { + self.fs_state = FsState::Mounted; + } + Err(e) => { + self.fs_state = FsState::Error(e); + } + } + } + + pub fn unmount_fs(&mut self) { + self.fs.unmount(); + self.fs_state = FsState::Unmounted; + } + + fn update_fs(&mut self) { + if self.fs_state == FsState::Mounted { + if !self.fs.is_healthy() { + self.unmount_fs(); + } + } + } + pub fn reset(&mut self, vm: &mut VirtualMachine) { *vm = VirtualMachine::default(); self.tick_index = 0; @@ -60,6 +96,8 @@ impl PrometeuOS { let start = std::time::Instant::now(); self.tick_index += 1; + self.update_fs(); + if !self.logical_frame_active { self.logical_frame_active = true; self.logical_frame_remaining_cycles = Self::CYCLES_PER_LOGICAL_FRAME; @@ -311,6 +349,107 @@ impl NativeInterface for PrometeuOS { } Ok(300) } + + // --- Filesystem Syscalls (0x4000) --- + + // FS_OPEN(path) -> handle + 0x4001 => { + let path = match vm.pop()? { + Value::String(s) => s, + _ => return Err("Expected string path".into()), + }; + if self.fs_state != FsState::Mounted { + vm.push(Value::Integer(-1)); + return Ok(100); + } + let handle = self.next_handle; + self.open_files.insert(handle, path); + self.next_handle += 1; + vm.push(Value::Integer(handle as i64)); + Ok(200) + } + // FS_READ(handle) -> content + 0x4002 => { + let handle = vm.pop_integer()? as u32; + let path = self.open_files.get(&handle).ok_or("Invalid handle")?; + match self.fs.read_file(path) { + Ok(data) => { + let s = String::from_utf8_lossy(&data).into_owned(); + vm.push(Value::String(s)); + Ok(1000) + } + Err(_e) => { + vm.push(Value::Null); + Ok(100) + } + } + } + // FS_WRITE(handle, content) + 0x4003 => { + let content = match vm.pop()? { + Value::String(s) => s, + _ => return Err("Expected string content".into()), + }; + let handle = vm.pop_integer()? as u32; + let path = self.open_files.get(&handle).ok_or("Invalid handle")?; + match self.fs.write_file(path, content.as_bytes()) { + Ok(_) => { + vm.push(Value::Boolean(true)); + Ok(1000) + } + Err(_) => { + vm.push(Value::Boolean(false)); + Ok(100) + } + } + } + // FS_CLOSE(handle) + 0x4004 => { + let handle = vm.pop_integer()? as u32; + self.open_files.remove(&handle); + Ok(100) + } + // FS_LISTDIR(path) + 0x4005 => { + let path = match vm.pop()? { + Value::String(s) => s, + _ => return Err("Expected string path".into()), + }; + match self.fs.list_dir(&path) { + Ok(entries) => { + // Por enquanto, retorna uma string separada por ';' + let names: Vec = entries.into_iter().map(|e| e.name).collect(); + vm.push(Value::String(names.join(";"))); + Ok(500) + } + Err(_) => { + vm.push(Value::Null); + Ok(100) + } + } + } + // FS_EXISTS(path) -> bool + 0x4006 => { + let path = match vm.pop()? { + Value::String(s) => s, + _ => return Err("Expected string path".into()), + }; + vm.push(Value::Boolean(self.fs.exists(&path))); + Ok(100) + } + // FS_DELETE(path) + 0x4007 => { + let path = match vm.pop()? { + Value::String(s) => s, + _ => return Err("Expected string path".into()), + }; + match self.fs.delete(&path) { + Ok(_) => vm.push(Value::Boolean(true)), + Err(_) => vm.push(Value::Boolean(false)), + } + Ok(500) + } + _ => Err(format!("Unknown syscall: 0x{:08X}", id)), } }