use std::path::{Path, PathBuf};
use super::copy_to_host::{copy_host_path_to_host_dir, copy_node_to_host};
use super::error::VfsError;
use super::node::Node;
use super::path::resolve_path_with_cwd;
pub struct Vfs {
root: Node,
cwd: String,
host_root: Option<PathBuf>,
}
impl Default for Vfs {
fn default() -> Self {
Self::new()
}
}
impl Vfs {
#[must_use]
pub fn new() -> Self {
Self {
root: Node::Dir {
name: String::new(),
children: vec![],
},
cwd: "/".to_string(),
host_root: None,
}
}
pub fn new_host_root(root: impl AsRef<Path>) -> std::io::Result<Self> {
let root = root.as_ref();
std::fs::create_dir_all(root)?;
let root = root.canonicalize()?;
Ok(Self {
root: Node::Dir {
name: String::new(),
children: vec![],
},
cwd: "/".to_string(),
host_root: Some(root),
})
}
#[must_use]
pub const fn is_host_backed(&self) -> bool {
self.host_root.is_some()
}
#[must_use]
pub const fn from_parts(root: Node, cwd: String) -> Self {
Self {
root,
cwd,
host_root: None,
}
}
fn logical_to_host_path(&self, abs_logical: &str) -> PathBuf {
let root = self.host_root.as_ref().unwrap();
let abs_logical = abs_logical.trim_end_matches('/');
let mut p = root.clone();
if abs_logical.is_empty() || abs_logical == "/" {
return p;
}
for seg in abs_logical.split('/').filter(|s| !s.is_empty()) {
p.push(seg);
}
p
}
#[must_use]
pub fn cwd(&self) -> &str {
&self.cwd
}
#[must_use]
pub const fn root(&self) -> &Node {
&self.root
}
pub fn resolve_absolute(&self, path: &str) -> Result<Node, VfsError> {
if self.host_root.is_some() {
let path = path.trim_end_matches('/');
let p = self.logical_to_host_path(path);
let meta = std::fs::metadata(&p).map_err(|_| VfsError::InvalidPath)?;
if meta.is_file() {
let content = std::fs::read(&p).map_err(VfsError::Io)?;
let name = p
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default();
return Ok(Node::File { name, content });
}
if meta.is_dir() {
let name = p
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default();
return Ok(Node::Dir {
name,
children: vec![],
});
}
return Err(VfsError::InvalidPath);
}
let path = path.trim_end_matches('/');
if path.is_empty() || path == "/" {
return Ok(self.root.clone());
}
let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
let mut current = &self.root;
for segment in segments {
current = current.child(segment).ok_or(VfsError::InvalidPath)?;
}
Ok(current.clone())
}
#[must_use]
pub fn resolve_to_absolute(&self, path: &str) -> String {
resolve_path_with_cwd(&self.cwd, path)
}
pub fn mkdir(&mut self, path: &str) -> Result<(), VfsError> {
if self.host_root.is_some() {
let abs = self.resolve_to_absolute(path);
let p = self.logical_to_host_path(&abs);
std::fs::create_dir_all(&p).map_err(VfsError::Io)?;
return Ok(());
}
let abs = self.resolve_to_absolute(path);
let segments: Vec<&str> = abs.split('/').filter(|s| !s.is_empty()).collect();
if segments.is_empty() {
return Ok(());
}
let mut indices: Vec<usize> = vec![];
for segment in segments {
let current = Self::get_mut_at(&mut self.root, &indices);
match current {
Node::Dir { children, .. } => {
let pos = children.iter().position(|c| c.name() == segment);
if let Some(i) = pos {
if !children[i].is_dir() {
return Err(VfsError::InvalidPath);
}
indices.push(i);
} else {
children.push(Node::Dir {
name: segment.to_string(),
children: vec![],
});
indices.push(children.len() - 1);
}
}
Node::File { .. } => return Err(VfsError::InvalidPath),
}
}
Ok(())
}
pub fn write_file(&mut self, path: &str, content: &[u8]) -> Result<(), VfsError> {
if self.host_root.is_some() {
let abs = self.resolve_to_absolute(path);
let p = self.logical_to_host_path(&abs);
if let Some(parent) = p.parent() {
std::fs::create_dir_all(parent).map_err(VfsError::Io)?;
}
std::fs::write(&p, content).map_err(VfsError::Io)?;
return Ok(());
}
let abs = self.resolve_to_absolute(path);
let segments: Vec<&str> = abs.split('/').filter(|s| !s.is_empty()).collect();
let (parent_segments, file_name) = match segments.split_last() {
Some((last, rest)) => (rest, *last),
None => return Err(VfsError::InvalidPath), };
let mut indices: Vec<usize> = vec![];
for segment in parent_segments {
let current = Self::get_mut_at(&mut self.root, &indices);
match current {
Node::Dir { children, .. } => {
let pos = children.iter().position(|c| c.name() == *segment);
match pos {
Some(i) => {
if !children[i].is_dir() {
return Err(VfsError::InvalidPath);
}
indices.push(i);
}
None => return Err(VfsError::InvalidPath), }
}
Node::File { .. } => return Err(VfsError::InvalidPath),
}
}
let parent = Self::get_mut_at(&mut self.root, &indices);
match parent {
Node::Dir { children, .. } => {
let pos = children.iter().position(|c| c.name() == file_name);
let node = Node::File {
name: file_name.to_string(),
content: content.to_vec(),
};
match pos {
Some(i) => children[i] = node,
None => children.push(node),
}
Ok(())
}
Node::File { .. } => Err(VfsError::InvalidPath),
}
}
pub fn touch(&mut self, path: &str) -> Result<(), VfsError> {
self.write_file(path, &[])
}
pub fn read_file(&self, path: &str) -> Result<Vec<u8>, VfsError> {
if self.host_root.is_some() {
let abs = self.resolve_to_absolute(path);
let p = self.logical_to_host_path(&abs);
if !p.is_file() {
return Err(VfsError::InvalidPath);
}
return std::fs::read(&p).map_err(VfsError::Io);
}
let abs = self.resolve_to_absolute(path);
let n = self.resolve_absolute(&abs)?;
match n {
Node::File { content, .. } => Ok(content),
Node::Dir { .. } => Err(VfsError::InvalidPath),
}
}
pub fn list_dir(&self, path: &str) -> Result<Vec<String>, VfsError> {
if self.host_root.is_some() {
let abs = self.resolve_to_absolute(path);
let p = self.logical_to_host_path(&abs);
if !p.is_dir() {
return Err(VfsError::InvalidPath);
}
let mut out = Vec::new();
for e in std::fs::read_dir(&p).map_err(VfsError::Io)? {
let e = e.map_err(VfsError::Io)?;
out.push(e.file_name().to_string_lossy().into_owned());
}
out.sort();
return Ok(out);
}
let abs = self.resolve_to_absolute(path);
let n = self.resolve_absolute(&abs)?;
match n {
Node::Dir { children, .. } => {
Ok(children.iter().map(|c| c.name().to_string()).collect())
}
Node::File { .. } => Err(VfsError::InvalidPath),
}
}
pub fn set_cwd(&mut self, path: &str) -> Result<(), VfsError> {
if self.host_root.is_some() {
let abs = self.resolve_to_absolute(path);
let p = self.logical_to_host_path(&abs);
let meta = std::fs::metadata(&p).map_err(|_| VfsError::InvalidPath)?;
if !meta.is_dir() {
return Err(VfsError::InvalidPath);
}
self.cwd = if abs == "/" { "/".to_string() } else { abs };
return Ok(());
}
let abs = self.resolve_to_absolute(path);
let n = self.resolve_absolute(&abs)?;
if !n.is_dir() {
return Err(VfsError::InvalidPath);
}
self.cwd = if abs == "/" { "/".to_string() } else { abs };
Ok(())
}
fn get_mut_at<'a>(node: &'a mut Node, path: &[usize]) -> &'a mut Node {
if path.is_empty() {
return node;
}
match node {
Node::Dir { children, .. } => {
let i = path[0];
Self::get_mut_at(&mut children[i], &path[1..])
}
Node::File { .. } => unreachable!("path must follow dirs only"),
}
}
pub fn copy_tree_to_host(&self, vfs_path: &str, host_dir: &Path) -> Result<(), VfsError> {
let abs = self.resolve_to_absolute(vfs_path);
if self.host_root.is_some() {
let src = self.logical_to_host_path(&abs);
copy_host_path_to_host_dir(&src, host_dir)
} else {
let node = self.resolve_absolute(&abs)?;
copy_node_to_host(&node, host_dir)
}
}
}