pub mod inode; use crate::auth::perms::{has_setgid, Access}; use crate::auth::registry::UserRegistry; use crate::auth::session::Session; use crate::auth::{Gid, Uid, ROOT_GID, ROOT_UID}; use crate::config::CompatibilityTarget; use crate::error::VfsError; use inode::{Inode, InodeId, InodeKind}; use std::collections::BTreeMap; use std::collections::{HashMap, HashSet}; pub type HandleId = u64; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct FsOptions { pub compatibility_target: CompatibilityTarget, } impl Default for FsOptions { fn default() -> Self { Self { compatibility_target: CompatibilityTarget::Markdown, } } } #[derive(Debug, Clone)] struct OpenHandle { inode_id: InodeId, cursor: usize, writable: bool, directory: bool, } #[derive(Clone)] pub struct VirtualFs { inodes: HashMap, root: InodeId, cwd: InodeId, next_id: InodeId, cwd_path: Vec<(String, InodeId)>, options: FsOptions, next_handle: HandleId, handles: HashMap, pending_delete: HashSet, pub registry: UserRegistry, } impl VirtualFs { pub fn new() -> Self { Self::new_with_options(FsOptions::default()) } pub fn new_posix() -> Self { Self::new_with_options(FsOptions { compatibility_target: CompatibilityTarget::Posix, }) } pub fn new_with_options(options: FsOptions) -> Self { let root_id = 0; let root = Inode::new_dir(root_id, ROOT_UID, ROOT_GID); let mut inodes = HashMap::new(); inodes.insert(root_id, root); VirtualFs { inodes, root: root_id, cwd: root_id, next_id: 1, cwd_path: Vec::new(), options, next_handle: 1, handles: HashMap::new(), pending_delete: HashSet::new(), registry: UserRegistry::new(), } } fn alloc_id(&mut self) -> InodeId { let id = self.next_id; self.next_id += 1; id } fn alloc_handle_id(&mut self) -> HandleId { let id = self.next_handle; self.next_handle += 1; id } pub fn options(&self) -> FsOptions { self.options } pub fn compatibility_target(&self) -> CompatibilityTarget { self.options.compatibility_target } pub fn pwd(&self) -> String { if self.cwd_path.is_empty() { "/".to_string() } else { let mut path = String::new(); for (name, _) in &self.cwd_path { path.push('/'); path.push_str(name); } path } } pub fn get_inode(&self, id: InodeId) -> Result<&Inode, VfsError> { self.inodes.get(&id).ok_or_else(|| VfsError::NotFound { path: format!(""), }) } fn get_inode_mut(&mut self, id: InodeId) -> Result<&mut Inode, VfsError> { self.inodes.get_mut(&id).ok_or_else(|| VfsError::NotFound { path: format!(""), }) } fn dir_entries(&self, id: InodeId) -> Result<&BTreeMap, VfsError> { match &self.get_inode(id)?.kind { InodeKind::Directory { entries } => Ok(entries), _ => Err(VfsError::NotDirectory { path: format!(""), }), } } fn dir_entries_mut( &mut self, id: InodeId, ) -> Result<&mut BTreeMap, VfsError> { match &mut self.get_inode_mut(id)?.kind { InodeKind::Directory { entries } => Ok(entries), _ => Err(VfsError::NotDirectory { path: format!(""), }), } } /// Resolve a path without permission checks (for internal/VCS use). pub fn resolve_path(&self, path: &str) -> Result { let (start, components) = self.parse_path(path); let mut current = start; for component in &components { match component.as_str() { "." => {} ".." => { current = self.parent_of(current); } name => { let entries = self.dir_entries(current)?; current = *entries.get(name).ok_or_else(|| VfsError::NotFound { path: path.to_string(), })?; } } } Ok(current) } /// Resolve a path with Execute permission checks on every directory. pub fn resolve_path_checked( &self, path: &str, session: &Session, ) -> Result { let (start, components) = self.parse_path(path); let mut current = start; // Check execute on starting directory let start_inode = self.get_inode(current)?; if !session.has_permission(start_inode, Access::Execute) { return Err(VfsError::PermissionDenied { path: path.to_string(), }); } for component in &components { match component.as_str() { "." => {} ".." => { current = self.parent_of(current); } name => { let entries = self.dir_entries(current)?; current = *entries.get(name).ok_or_else(|| VfsError::NotFound { path: path.to_string(), })?; // Check execute on intermediate directories let inode = self.get_inode(current)?; if inode.is_dir() && !session.has_permission(inode, Access::Execute) { return Err(VfsError::PermissionDenied { path: path.to_string(), }); } } } } Ok(current) } /// Resolve parent with permission checks. pub fn resolve_parent_checked( &self, path: &str, session: &Session, ) -> Result<(InodeId, String), VfsError> { let (start, components) = self.parse_path(path); if components.is_empty() { return Err(VfsError::InvalidPath { path: path.to_string(), }); } let name = components.last().unwrap().clone(); let parent_path = if components.len() == 1 { if path.starts_with('/') { "/".to_string() } else { ".".to_string() } } else { let parent_components = &components[..components.len() - 1]; if path.starts_with('/') { format!("/{}", parent_components.join("/")) } else { parent_components.join("/") } }; let _ = start; // used via parse_path in resolve_path_checked let parent_id = self.resolve_path_checked(&parent_path, session)?; Ok((parent_id, name)) } fn resolve_parent(&self, path: &str) -> Result<(InodeId, String), VfsError> { let (start, components) = self.parse_path(path); if components.is_empty() { return Err(VfsError::InvalidPath { path: path.to_string(), }); } let name = components.last().unwrap().clone(); let mut current = start; for component in &components[..components.len() - 1] { match component.as_str() { "." => {} ".." => { current = self.parent_of(current); } n => { let entries = self.dir_entries(current)?; current = *entries.get(n).ok_or_else(|| VfsError::NotFound { path: path.to_string(), })?; } } } Ok((current, name)) } fn parse_path(&self, path: &str) -> (InodeId, Vec) { let (start, path_str) = if path.starts_with('/') { (self.root, &path[1..]) } else { (self.cwd, path) }; let components: Vec = path_str .split('/') .filter(|s| !s.is_empty()) .map(String::from) .collect(); (start, components) } fn parent_of(&self, id: InodeId) -> InodeId { if id == self.root { return self.root; } for i in (0..self.cwd_path.len()).rev() { if self.cwd_path[i].1 == id && i > 0 { return self.cwd_path[i - 1].1; } } for inode in self.inodes.values() { if let InodeKind::Directory { entries } = &inode.kind { if entries.values().any(|&child| child == id) { return inode.id; } } } self.root } /// Determine the gid for a new child in this directory. /// If the parent has setgid, inherit parent's gid; otherwise use caller's gid. fn effective_gid(&self, parent_id: InodeId, caller_gid: Gid) -> Gid { if let Ok(parent) = self.get_inode(parent_id) { if has_setgid(parent.mode) { return parent.gid; } } caller_gid } fn validate_regular_path(&self, path: &str) -> Result<(), VfsError> { if self.compatibility_target() == CompatibilityTarget::Posix { return Ok(()); } validate_markdown_filename(path) } fn mark_accessed(&mut self, id: InodeId) -> Result<(), VfsError> { let inode = self.get_inode_mut(id)?; inode.touch_access(); Ok(()) } fn unlink_inode_if_needed(&mut self, id: InodeId) { let open = self .handles .values() .any(|handle| handle.inode_id == id); if let Some(inode) = self.inodes.get(&id) { if inode.nlink == 0 { if open { self.pending_delete.insert(id); } else { self.pending_delete.remove(&id); self.inodes.remove(&id); } } } } fn release_orphan_if_needed(&mut self, id: InodeId) { if !self.pending_delete.contains(&id) { return; } let still_open = self .handles .values() .any(|handle| handle.inode_id == id); if !still_open { self.pending_delete.remove(&id); self.inodes.remove(&id); } } // ───── Commands ───── pub fn ls(&self, path: Option<&str>) -> Result, VfsError> { let id = match path { Some(p) => self.resolve_path(p)?, None => self.cwd, }; let inode = self.get_inode(id)?; match &inode.kind { InodeKind::Directory { entries } => { let mut result = Vec::with_capacity(entries.len()); for (name, &child_id) in entries { let child = self.get_inode(child_id)?; result.push(LsEntry { name: name.clone(), is_dir: child.is_dir(), is_symlink: matches!(child.kind, InodeKind::Symlink { .. }), size: child.size(), mode: child.mode, uid: child.uid, gid: child.gid, modified: child.modified, }); } Ok(result) } InodeKind::File { .. } => Ok(vec![LsEntry { name: path.unwrap_or("").to_string(), is_dir: false, is_symlink: false, size: inode.size(), mode: inode.mode, uid: inode.uid, gid: inode.gid, modified: inode.modified, }]), InodeKind::Symlink { .. } => Ok(vec![LsEntry { name: path.unwrap_or("").to_string(), is_dir: false, is_symlink: true, size: inode.size(), mode: inode.mode, uid: inode.uid, gid: inode.gid, modified: inode.modified, }]), } } pub fn cd(&mut self, path: &str) -> Result<(), VfsError> { if path == "/" { self.cwd = self.root; self.cwd_path.clear(); return Ok(()); } let target = self.resolve_path(path)?; let inode = self.get_inode(target)?; if !inode.is_dir() { return Err(VfsError::NotDirectory { path: path.to_string(), }); } if path.starts_with('/') { self.cwd_path.clear(); } for component in path.split('/').filter(|s| !s.is_empty()) { match component { "." => {} ".." => { self.cwd_path.pop(); } name => { let current_dir = self .cwd_path .last() .map(|(_, id)| *id) .unwrap_or(self.root); let entries = self.dir_entries(current_dir)?; if let Some(&child_id) = entries.get(name) { self.cwd_path.push((name.to_string(), child_id)); } } } } self.cwd = target; Ok(()) } pub fn mkdir(&mut self, path: &str, uid: Uid, gid: Gid) -> Result<(), VfsError> { let (parent_id, name) = self.resolve_parent(path)?; let entries = self.dir_entries(parent_id)?; if entries.contains_key(&name) { return Err(VfsError::AlreadyExists { path: path.to_string(), }); } let effective_gid = self.effective_gid(parent_id, gid); let new_id = self.alloc_id(); let mut new_dir = Inode::new_dir(new_id, uid, effective_gid); // Inherit setgid bit from parent if let Ok(parent) = self.get_inode(parent_id) { if has_setgid(parent.mode) { new_dir.mode |= 0o2000; } } self.inodes.insert(new_id, new_dir); self.dir_entries_mut(parent_id)?.insert(name, new_id); let parent = self.get_inode_mut(parent_id)?; parent.nlink += 1; parent.touch_change(); Ok(()) } pub fn mkdir_p(&mut self, path: &str, uid: Uid, gid: Gid) -> Result<(), VfsError> { let (start, components) = self.parse_path(path); let mut current = start; for component in &components { match component.as_str() { "." => {} ".." => { current = self.parent_of(current); } name => { let entries = self.dir_entries(current)?; if let Some(&existing) = entries.get(name) { if !self.get_inode(existing)?.is_dir() { return Err(VfsError::NotDirectory { path: name.to_string(), }); } current = existing; } else { let effective_gid = self.effective_gid(current, gid); let new_id = self.alloc_id(); let new_dir = Inode::new_dir(new_id, uid, effective_gid); self.inodes.insert(new_id, new_dir); self.dir_entries_mut(current)? .insert(name.to_string(), new_id); let parent = self.get_inode_mut(current)?; parent.nlink += 1; parent.touch_change(); current = new_id; } } } } Ok(()) } pub fn touch(&mut self, path: &str, uid: Uid, gid: Gid) -> Result<(), VfsError> { self.validate_regular_path(path)?; if let Ok(id) = self.resolve_path(path) { let inode = self.get_inode_mut(id)?; inode.touch_modify(); return Ok(()); } let (parent_id, name) = self.resolve_parent(path)?; let effective_gid = self.effective_gid(parent_id, gid); let new_id = self.alloc_id(); let new_file = Inode::new_file(new_id, uid, effective_gid); self.inodes.insert(new_id, new_file); self.dir_entries_mut(parent_id)?.insert(name, new_id); Ok(()) } pub fn create_file( &mut self, path: &str, uid: Uid, gid: Gid, mode: Option, ) -> Result { self.validate_regular_path(path)?; if self.resolve_path(path).is_ok() { return Err(VfsError::AlreadyExists { path: path.to_string(), }); } let (parent_id, name) = self.resolve_parent(path)?; let effective_gid = self.effective_gid(parent_id, gid); let new_id = self.alloc_id(); let mut new_file = Inode::new_file(new_id, uid, effective_gid); if let Some(mode) = mode { new_file.mode = mode; } self.inodes.insert(new_id, new_file); self.dir_entries_mut(parent_id)?.insert(name, new_id); Ok(new_id) } pub fn cat(&self, path: &str) -> Result<&[u8], VfsError> { let id = self.resolve_path(path)?; let inode = self.get_inode(id)?; match &inode.kind { InodeKind::File { content } => Ok(content), InodeKind::Directory { .. } => Err(VfsError::IsDirectory { path: path.to_string(), }), InodeKind::Symlink { target } => self.cat(target), } } pub fn cat_owned(&self, path: &str) -> Result, VfsError> { self.cat(path).map(|b| b.to_vec()) } pub fn write_file(&mut self, path: &str, content: Vec) -> Result<(), VfsError> { self.validate_regular_path(path)?; let id = self.resolve_path(path)?; let inode = self.get_inode_mut(id)?; match &mut inode.kind { InodeKind::File { content: c, .. } => { *c = content; inode.touch_modify(); Ok(()) } InodeKind::Directory { .. } => Err(VfsError::IsDirectory { path: path.to_string(), }), InodeKind::Symlink { target } => { let target = target.clone(); self.write_file(&target, content) } } } pub fn read_file_at( &mut self, path: &str, offset: usize, size: usize, ) -> Result, VfsError> { let id = self.resolve_path(path)?; self.read_inode_at(id, offset, size) } pub fn write_file_at( &mut self, path: &str, offset: usize, data: &[u8], ) -> Result { self.validate_regular_path(path)?; let id = self.resolve_path(path)?; let inode = self.get_inode_mut(id)?; match &mut inode.kind { InodeKind::File { content } => { if offset > content.len() { content.resize(offset, 0); } let end = offset.saturating_add(data.len()); if end > content.len() { content.resize(end, 0); } content[offset..end].copy_from_slice(data); inode.touch_modify(); Ok(data.len()) } InodeKind::Directory { .. } => Err(VfsError::IsDirectory { path: path.to_string(), }), InodeKind::Symlink { target } => { let target = target.clone(); self.write_file_at(&target, offset, data) } } } pub fn truncate(&mut self, path: &str, size: usize) -> Result<(), VfsError> { let id = self.resolve_path(path)?; let inode = self.get_inode_mut(id)?; match &mut inode.kind { InodeKind::File { content } => { content.resize(size, 0); inode.touch_modify(); Ok(()) } InodeKind::Directory { .. } => Err(VfsError::IsDirectory { path: path.to_string(), }), InodeKind::Symlink { target } => { let target = target.clone(); self.truncate(&target, size) } } } pub fn rm(&mut self, path: &str) -> Result<(), VfsError> { let id = self.resolve_path(path)?; let inode = self.get_inode(id)?; if inode.is_dir() { return Err(VfsError::IsDirectory { path: path.to_string(), }); } let (parent_id, name) = self.resolve_parent(path)?; self.dir_entries_mut(parent_id)?.remove(&name); let inode = self.get_inode_mut(id)?; inode.nlink = inode.nlink.saturating_sub(1); inode.touch_change(); self.unlink_inode_if_needed(id); Ok(()) } pub fn rmdir(&mut self, path: &str) -> Result<(), VfsError> { let id = self.resolve_path(path)?; let inode = self.get_inode(id)?; match &inode.kind { InodeKind::Directory { entries } => { if !entries.is_empty() { return Err(VfsError::NotEmpty { path: path.to_string(), }); } } _ => { return Err(VfsError::NotDirectory { path: path.to_string(), }) } } if id == self.root { return Err(VfsError::InvalidPath { path: "cannot remove root".to_string(), }); } let (parent_id, name) = self.resolve_parent(path)?; self.dir_entries_mut(parent_id)?.remove(&name); let parent = self.get_inode_mut(parent_id)?; parent.nlink = parent.nlink.saturating_sub(1); parent.touch_change(); self.inodes.remove(&id); Ok(()) } pub fn rm_rf(&mut self, path: &str) -> Result<(), VfsError> { let id = self.resolve_path(path)?; if id == self.root { return Err(VfsError::InvalidPath { path: "cannot remove root".to_string(), }); } let (parent_id, name) = self.resolve_parent(path)?; self.dir_entries_mut(parent_id)?.remove(&name); self.remove_inode_recursive(id)?; let parent = self.get_inode_mut(parent_id)?; parent.touch_change(); Ok(()) } fn remove_inode_recursive(&mut self, id: InodeId) -> Result<(), VfsError> { let child_entries = match &self.get_inode(id)?.kind { InodeKind::Directory { entries } => entries.values().copied().collect::>(), _ => Vec::new(), }; for child_id in child_entries { self.remove_inode_recursive(child_id)?; } let nlink = self.get_inode(id)?.nlink; if nlink > 0 { let inode = self.get_inode_mut(id)?; inode.nlink = inode.nlink.saturating_sub(1); inode.touch_change(); } self.unlink_inode_if_needed(id); Ok(()) } pub fn mv(&mut self, src: &str, dst: &str) -> Result<(), VfsError> { self.rename(src, dst) } pub fn rename(&mut self, src: &str, dst: &str) -> Result<(), VfsError> { let src_id = self.resolve_path(src)?; if src_id == self.root { return Err(VfsError::InvalidPath { path: "cannot rename root".to_string(), }); } if self.get_inode(src_id)?.is_file() { let dst_name = dst.rsplit('/').next().unwrap_or(dst); self.validate_regular_path(dst_name)?; } let (src_parent, src_name) = self.resolve_parent(src)?; if let Ok(dst_id) = self.resolve_path(dst) { if self.get_inode(dst_id)?.is_dir() { self.dir_entries_mut(src_parent)?.remove(&src_name); self.dir_entries_mut(dst_id)?.insert(src_name, src_id); let inode = self.get_inode_mut(src_id)?; inode.touch_change(); return Ok(()); } else { self.rm(dst)?; } } let (dst_parent, dst_name) = self.resolve_parent(dst)?; self.dir_entries_mut(src_parent)?.remove(&src_name); self.dir_entries_mut(dst_parent)?.insert(dst_name, src_id); let inode = self.get_inode_mut(src_id)?; inode.touch_change(); Ok(()) } pub fn cp(&mut self, src: &str, dst: &str, uid: Uid, gid: Gid) -> Result<(), VfsError> { let src_id = self.resolve_path(src)?; let src_inode = self.get_inode(src_id)?.clone(); if src_inode.is_dir() { return Err(VfsError::IsDirectory { path: src.to_string(), }); } let dst_name = dst.rsplit('/').next().unwrap_or(dst); if src_inode.is_file() { self.validate_regular_path(dst_name)?; } let (parent_id, name) = if let Ok(dst_id) = self.resolve_path(dst) { if self.get_inode(dst_id)?.is_dir() { let src_name = src.rsplit('/').next().unwrap_or(src); (dst_id, src_name.to_string()) } else { self.resolve_parent(dst)? } } else { self.resolve_parent(dst)? }; let new_id = self.alloc_id(); let mut new_inode = src_inode; new_inode.id = new_id; new_inode.uid = uid; // Copy is owned by the caller new_inode.gid = self.effective_gid(parent_id, gid); new_inode.nlink = 1; new_inode.touch_change(); self.inodes.insert(new_id, new_inode); self.dir_entries_mut(parent_id)?.insert(name, new_id); Ok(()) } pub fn stat(&self, path: &str) -> Result { let id = self.resolve_path(path)?; let inode = self.get_inode(id)?; Ok(StatInfo { inode_id: id, kind: match &inode.kind { InodeKind::File { .. } => "file", InodeKind::Directory { .. } => "directory", InodeKind::Symlink { .. } => "symlink", }, size: inode.size(), mode: inode.mode, uid: inode.uid, gid: inode.gid, nlink: inode.nlink, block_size: inode.block_size, blocks: inode.blocks(), created: inode.created_at.secs, modified: inode.modified_at.secs, accessed: inode.accessed_at.secs, changed: inode.changed_at.secs, created_nanos: inode.created_at.nanos, modified_nanos: inode.modified_at.nanos, accessed_nanos: inode.accessed_at.nanos, changed_nanos: inode.changed_at.nanos, }) } pub fn chmod(&mut self, path: &str, mode: u16) -> Result<(), VfsError> { let id = self.resolve_path(path)?; let inode = self.get_inode_mut(id)?; inode.mode = mode; inode.touch_change(); Ok(()) } pub fn chown(&mut self, path: &str, uid: Uid, gid: Gid) -> Result<(), VfsError> { let id = self.resolve_path(path)?; let inode = self.get_inode_mut(id)?; inode.uid = uid; inode.gid = gid; inode.touch_change(); Ok(()) } pub fn ln_s(&mut self, target: &str, link_path: &str, uid: Uid, gid: Gid) -> Result<(), VfsError> { let (parent_id, name) = self.resolve_parent(link_path)?; let entries = self.dir_entries(parent_id)?; if entries.contains_key(&name) { return Err(VfsError::AlreadyExists { path: link_path.to_string(), }); } let effective_gid = self.effective_gid(parent_id, gid); let new_id = self.alloc_id(); let symlink = Inode::new_symlink(new_id, target.to_string(), uid, effective_gid); self.inodes.insert(new_id, symlink); self.dir_entries_mut(parent_id)?.insert(name, new_id); Ok(()) } pub fn link(&mut self, target: &str, link_path: &str) -> Result<(), VfsError> { let target_id = self.resolve_path(target)?; if self.get_inode(target_id)?.is_dir() { return Err(VfsError::NotSupported { message: "hard links to directories".to_string(), }); } let (parent_id, name) = self.resolve_parent(link_path)?; if self.dir_entries(parent_id)?.contains_key(&name) { return Err(VfsError::AlreadyExists { path: link_path.to_string(), }); } self.dir_entries_mut(parent_id)?.insert(name, target_id); let inode = self.get_inode_mut(target_id)?; inode.nlink += 1; inode.touch_change(); Ok(()) } pub fn readlink(&self, path: &str) -> Result { let id = self.resolve_path(path)?; let inode = self.get_inode(id)?; match &inode.kind { InodeKind::Symlink { target } => Ok(target.clone()), _ => Err(VfsError::InvalidArgs { message: format!("not a symlink: {path}"), }), } } pub fn open(&mut self, path: &str, writable: bool) -> Result { let inode_id = self.resolve_path(path)?; let directory = self.get_inode(inode_id)?.is_dir(); let handle_id = self.alloc_handle_id(); self.handles.insert( handle_id, OpenHandle { inode_id, cursor: 0, writable, directory, }, ); self.mark_accessed(inode_id)?; Ok(handle_id) } pub fn opendir(&mut self, path: &str) -> Result { let inode_id = self.resolve_path(path)?; if !self.get_inode(inode_id)?.is_dir() { return Err(VfsError::NotDirectory { path: path.to_string(), }); } self.open(path, false) } pub fn release_handle(&mut self, handle: HandleId) -> Result<(), VfsError> { let open = self .handles .remove(&handle) .ok_or(VfsError::InvalidHandle { handle })?; self.release_orphan_if_needed(open.inode_id); Ok(()) } pub fn read_handle(&mut self, handle: HandleId, size: usize) -> Result, VfsError> { let (inode_id, cursor) = { let open = self .handles .get(&handle) .ok_or(VfsError::InvalidHandle { handle })?; if open.directory { return Err(VfsError::IsDirectory { path: format!(""), }); } (open.inode_id, open.cursor) }; let data = self.read_inode_at(inode_id, cursor, size)?; if let Some(open) = self.handles.get_mut(&handle) { open.cursor = open.cursor.saturating_add(data.len()); } Ok(data) } pub fn write_handle(&mut self, handle: HandleId, data: &[u8]) -> Result { let (inode_id, cursor, writable) = { let open = self .handles .get(&handle) .ok_or(VfsError::InvalidHandle { handle })?; (open.inode_id, open.cursor, open.writable) }; if !writable { return Err(VfsError::PermissionDenied { path: format!(""), }); } let written = self.write_inode_at(inode_id, cursor, data)?; if let Some(open) = self.handles.get_mut(&handle) { open.cursor = open.cursor.saturating_add(written); } Ok(written) } fn write_inode_at( &mut self, inode_id: InodeId, offset: usize, data: &[u8], ) -> Result { let inode = self.get_inode_mut(inode_id)?; match &mut inode.kind { InodeKind::File { content } => { if offset > content.len() { content.resize(offset, 0); } let end = offset.saturating_add(data.len()); if end > content.len() { content.resize(end, 0); } content[offset..end].copy_from_slice(data); inode.touch_modify(); Ok(data.len()) } InodeKind::Directory { .. } => Err(VfsError::IsDirectory { path: format!(""), }), InodeKind::Symlink { target } => { let target = target.clone(); self.write_file_at(&target, offset, data) } } } fn read_inode_at( &mut self, inode_id: InodeId, offset: usize, size: usize, ) -> Result, VfsError> { let inode = self.get_inode_mut(inode_id)?; match &mut inode.kind { InodeKind::File { content } => { let start = offset.min(content.len()); let end = start.saturating_add(size).min(content.len()); let out = content[start..end].to_vec(); inode.touch_access(); Ok(out) } InodeKind::Directory { .. } => Err(VfsError::IsDirectory { path: format!(""), }), InodeKind::Symlink { target } => { let target = target.clone(); self.read_file_at(&target, offset, size) } } } pub fn tree( &self, path: Option<&str>, prefix: &str, session: Option<&Session>, ) -> Result { let id = match path { Some(p) => self.resolve_path(p)?, None => self.cwd, }; let mut output = String::new(); if prefix.is_empty() { output.push_str(".\n"); } self.tree_recursive(id, prefix, &mut output, session)?; Ok(output) } fn tree_recursive( &self, id: InodeId, prefix: &str, output: &mut String, session: Option<&Session>, ) -> Result<(), VfsError> { let entries = self.dir_entries(id)?; // Filter entries by read permission let visible: Vec<_> = entries .iter() .filter(|&(_, child_id)| self.is_visible(*child_id, session)) .collect(); let count = visible.len(); for (i, (name, child_id)) in visible.iter().enumerate() { let is_last = i == count - 1; let connector = if is_last { "\u{2514}\u{2500}\u{2500} " } else { "\u{251c}\u{2500}\u{2500} " }; let child = self.get_inode(**child_id)?; output.push_str(prefix); output.push_str(connector); output.push_str(name); if child.is_dir() { output.push('/'); } output.push('\n'); if child.is_dir() { let new_prefix = if is_last { format!("{prefix} ") } else { format!("{prefix}\u{2502} ") }; self.tree_recursive(**child_id, &new_prefix, output, session)?; } } Ok(()) } pub fn find( &self, path: Option<&str>, pattern: Option<&str>, session: Option<&Session>, ) -> Result, VfsError> { let id = match path { Some(p) => self.resolve_path(p)?, None => self.cwd, }; let base = match path { Some(p) => p.to_string(), None => ".".to_string(), }; let mut results = Vec::new(); self.find_recursive(id, &base, pattern, &mut results, session)?; Ok(results) } fn find_recursive( &self, id: InodeId, current_path: &str, pattern: Option<&str>, results: &mut Vec, session: Option<&Session>, ) -> Result<(), VfsError> { let entries = self.dir_entries(id)?; for (name, &child_id) in entries { if !self.is_visible(child_id, session) { continue; } let child_path = if current_path == "." { format!("./{name}") } else { format!("{current_path}/{name}") }; let child = self.get_inode(child_id)?; let matches = match pattern { Some(pat) => glob_match(pat, name), None => true, }; if matches { results.push(child_path.clone()); } if child.is_dir() { self.find_recursive(child_id, &child_path, pattern, results, session)?; } } Ok(()) } pub fn grep( &self, pattern: &str, path: Option<&str>, recursive: bool, session: Option<&Session>, ) -> Result, VfsError> { let re = regex::Regex::new(pattern).map_err(|e| VfsError::InvalidArgs { message: format!("invalid regex: {e}"), })?; let id = match path { Some(p) => self.resolve_path(p)?, None => self.cwd, }; let base = path.unwrap_or("."); let mut results = Vec::new(); let inode = self.get_inode(id)?; match &inode.kind { InodeKind::File { content } => { let text = String::from_utf8_lossy(content); for (line_num, line) in text.lines().enumerate() { if re.is_match(line) { results.push(GrepResult { file: base.to_string(), line_num: line_num + 1, line: line.to_string(), }); } } } InodeKind::Directory { .. } if recursive => { self.grep_recursive(id, base, &re, &mut results, session)?; } InodeKind::Directory { .. } => { return Err(VfsError::IsDirectory { path: base.to_string(), }); } _ => {} } Ok(results) } fn grep_recursive( &self, id: InodeId, base: &str, re: ®ex::Regex, results: &mut Vec, session: Option<&Session>, ) -> Result<(), VfsError> { let entries = self.dir_entries(id)?; for (name, &child_id) in entries { if !self.is_visible(child_id, session) { continue; } let child_path = if base == "." { format!("./{name}") } else { format!("{base}/{name}") }; let child = self.get_inode(child_id)?; match &child.kind { InodeKind::File { content } => { let text = String::from_utf8_lossy(content); for (line_num, line) in text.lines().enumerate() { if re.is_match(line) { results.push(GrepResult { file: child_path.clone(), line_num: line_num + 1, line: line.to_string(), }); } } } InodeKind::Directory { .. } => { self.grep_recursive(child_id, &child_path, re, results, session)?; } _ => {} } } Ok(()) } /// Check if a user can see this inode (has read permission). /// If session is None, everything is visible (internal/root use). /// Respects delegation: both principal and delegate must have read access. fn is_visible(&self, id: InodeId, session: Option<&Session>) -> bool { let session = match session { Some(s) => s, None => return true, }; if session.can_read_anywhere() { return self.get_inode(id).is_ok(); } let inode = match self.get_inode(id) { Ok(i) => i, Err(_) => return false, }; session.has_permission(inode, Access::Read) } pub fn all_inodes(&self) -> &HashMap { &self.inodes } pub fn root_id(&self) -> InodeId { self.root } pub fn cwd_id(&self) -> InodeId { self.cwd } pub fn next_inode_id(&self) -> InodeId { self.next_id } pub fn cwd_path_clone(&self) -> Vec<(String, InodeId)> { self.cwd_path.clone() } pub fn from_persisted( inodes: HashMap, root: InodeId, cwd: InodeId, next_id: InodeId, cwd_path: Vec<(String, InodeId)>, options: FsOptions, registry: UserRegistry, ) -> Self { VirtualFs { inodes, root, cwd, next_id, cwd_path, options, next_handle: 1, handles: HashMap::new(), pending_delete: HashSet::new(), registry, } } } fn glob_match(pattern: &str, name: &str) -> bool { let pat = pattern .replace('.', "\\.") .replace('*', ".*") .replace('?', "."); regex::Regex::new(&format!("^{pat}$")) .map(|re| re.is_match(name)) .unwrap_or(false) } fn validate_markdown_filename(path: &str) -> Result<(), VfsError> { let filename = path.rsplit('/').next().unwrap_or(path); if !filename.ends_with(".md") { return Err(VfsError::InvalidExtension { name: filename.to_string(), }); } Ok(()) } #[derive(Debug)] pub struct LsEntry { pub name: String, pub is_dir: bool, pub is_symlink: bool, pub size: u64, pub mode: u16, pub uid: u32, pub gid: u32, pub modified: u64, } #[derive(Debug, Clone)] pub struct StatInfo { pub inode_id: InodeId, pub kind: &'static str, pub size: u64, pub mode: u16, pub uid: u32, pub gid: u32, pub nlink: u64, pub block_size: u64, pub blocks: u64, pub created: u64, pub modified: u64, pub accessed: u64, pub changed: u64, pub created_nanos: u32, pub modified_nanos: u32, pub accessed_nanos: u32, pub changed_nanos: u32, } #[derive(Debug, Clone)] pub struct GrepResult { pub file: String, pub line_num: usize, pub line: String, }