use prometeu_system::fs::{FsBackend, FsEntry, FsError}; use std::fs; use std::path::{Component, Path, PathBuf}; pub struct HostDirBackend { root: PathBuf, } impl HostDirBackend { pub fn new(root: impl Into) -> Self { Self { root: root.into() } } fn resolve(&self, path: &str) -> Result { let normalized = path.replace('\\', "/"); let relative = normalized.strip_prefix('/').unwrap_or(&normalized); let mut resolved = self.root.clone(); for component in Path::new(relative).components() { match component { Component::Normal(segment) => resolved.push(segment), Component::ParentDir => { return Err(FsError::InvalidPath( "parent traversal '..' is not allowed".to_string(), )); } Component::CurDir => { return Err(FsError::InvalidPath( "current directory '.' is not allowed".to_string(), )); } Component::RootDir => { return Err(FsError::InvalidPath("unexpected root component".to_string())); } Component::Prefix(prefix) => { return Err(FsError::InvalidPath(format!( "unexpected platform path prefix: {:?}", prefix ))); } } } if !resolved.starts_with(&self.root) { return Err(FsError::InvalidPath("resolved path escaped the mounted root".to_string())); } Ok(resolved) } } 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 == self.root { return Err(FsError::PermissionDenied); } 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).map(|resolved| resolved.exists()).unwrap_or(false) } } #[cfg(test)] mod tests { use super::*; use std::env; use std::fs; 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); } #[test] fn test_host_dir_backend_round_trip() { let root = get_temp_dir("round_trip"); let mut backend = HostDirBackend::new(root.clone()); backend.mount().unwrap(); let path = "/user/test.txt"; let content = b"hello world"; backend.write_file(path, content).unwrap(); assert!(backend.exists(path)); assert_eq!(backend.read_file(path).unwrap(), content); backend.delete(path).unwrap(); assert!(!backend.exists(path)); let _ = fs::remove_dir_all(root); } #[test] fn test_host_dir_backend_rejects_traversal_write_and_read() { let root = get_temp_dir("rejects_traversal"); let mut backend = HostDirBackend::new(root.clone()); backend.mount().unwrap(); let outside_name = format!( "outside_{}.txt", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos() ); let traversal_path = format!("/user/../../{}", outside_name); let outside_path = root.parent().unwrap().join(&outside_name); assert!(matches!( backend.write_file(&traversal_path, b"escape"), Err(FsError::InvalidPath(_)) )); assert!(matches!(backend.read_file(&traversal_path), Err(FsError::InvalidPath(_)))); assert!(!outside_path.exists()); let _ = fs::remove_dir_all(root); } #[test] fn test_host_dir_backend_rejects_traversal_for_exists_and_list_dir() { let root = get_temp_dir("rejects_bool_and_list"); let mut backend = HostDirBackend::new(root.clone()); backend.mount().unwrap(); let traversal_path = "/user/../../outside"; assert!(!backend.exists(traversal_path)); assert!(matches!(backend.list_dir(traversal_path), Err(FsError::InvalidPath(_)))); let _ = fs::remove_dir_all(root); } #[test] fn test_host_dir_backend_rejects_delete_root() { let root = get_temp_dir("rejects_delete_root"); let mut backend = HostDirBackend::new(root.clone()); backend.mount().unwrap(); assert!(matches!(backend.delete("/"), Err(FsError::PermissionDenied))); assert!(root.exists()); let _ = fs::remove_dir_all(root); } }