markdownfs / src /fs /mod.rs
Subramanya97's picture
Deploy from markdownfs@1a8d486
4b8758b
Raw
History Blame Contribute Delete
43.7 kB
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<InodeId, Inode>,
root: InodeId,
cwd: InodeId,
next_id: InodeId,
cwd_path: Vec<(String, InodeId)>,
options: FsOptions,
next_handle: HandleId,
handles: HashMap<HandleId, OpenHandle>,
pending_delete: HashSet<InodeId>,
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!("<inode {id}>"),
})
}
fn get_inode_mut(&mut self, id: InodeId) -> Result<&mut Inode, VfsError> {
self.inodes.get_mut(&id).ok_or_else(|| VfsError::NotFound {
path: format!("<inode {id}>"),
})
}
fn dir_entries(&self, id: InodeId) -> Result<&BTreeMap<String, InodeId>, VfsError> {
match &self.get_inode(id)?.kind {
InodeKind::Directory { entries } => Ok(entries),
_ => Err(VfsError::NotDirectory {
path: format!("<inode {id}>"),
}),
}
}
fn dir_entries_mut(
&mut self,
id: InodeId,
) -> Result<&mut BTreeMap<String, InodeId>, VfsError> {
match &mut self.get_inode_mut(id)?.kind {
InodeKind::Directory { entries } => Ok(entries),
_ => Err(VfsError::NotDirectory {
path: format!("<inode {id}>"),
}),
}
}
/// Resolve a path without permission checks (for internal/VCS use).
pub fn resolve_path(&self, path: &str) -> Result<InodeId, 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)?;
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<InodeId, VfsError> {
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<String>) {
let (start, path_str) = if path.starts_with('/') {
(self.root, &path[1..])
} else {
(self.cwd, path)
};
let components: Vec<String> = 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<Vec<LsEntry>, 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<u16>,
) -> Result<InodeId, VfsError> {
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<Vec<u8>, VfsError> {
self.cat(path).map(|b| b.to_vec())
}
pub fn write_file(&mut self, path: &str, content: Vec<u8>) -> 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<Vec<u8>, 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<usize, 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 } => {
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<_>>(),
_ => 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<StatInfo, VfsError> {
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<String, VfsError> {
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<HandleId, VfsError> {
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<HandleId, VfsError> {
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<Vec<u8>, 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!("<handle {handle}>"),
});
}
(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<usize, VfsError> {
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!("<handle {handle}>"),
});
}
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<usize, VfsError> {
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!("<inode {inode_id}>"),
}),
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<Vec<u8>, 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!("<inode {inode_id}>"),
}),
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<String, VfsError> {
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<Vec<String>, 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<String>,
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<Vec<GrepResult>, 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: &regex::Regex,
results: &mut Vec<GrepResult>,
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<InodeId, Inode> {
&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<InodeId, Inode>,
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,
}