use crate::error::FsError;
use std::fmt::Display;
use std::fs::{File, OpenOptions};
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::Path;
use std::time::SystemTime;
use std::{fs, str};
fn get_components(path: &str) -> Vec<&str> {
path.split('/').filter(|s| !s.is_empty()).collect()
}
fn get_components_string(path: &str) -> Vec<String> {
path.split('/')
.filter(|s| !s.is_empty())
.map(String::from)
.collect()
}
fn path_to_str(vp: &VirtPath) -> String {
String::from_utf8_lossy(vp.bytes()).to_string()
}
fn normalize_path(path: &str) -> String {
let is_absolute = path.starts_with('/');
let comps = path.split('/'); let mut stack = Vec::new();
for comp in comps {
if comp.is_empty() || comp == "." {
continue;
}
if comp == ".." {
if stack.pop().is_some() {
} else if !is_absolute {
stack.push("..");
}
} else {
stack.push(comp);
}
}
if is_absolute {
format!("/{}", stack.join("/"))
} else {
stack.join("/")
}
}
#[derive(Clone, Debug)]
pub struct VirtPermissions {
pub mode: u16,
}
impl VirtPermissions {
pub const S_IRUSR: u16 = 0o400;
pub const S_IWUSR: u16 = 0o200;
pub const S_IXUSR: u16 = 0o100;
pub const S_IRGRP: u16 = 0o040;
pub const S_IWGRP: u16 = 0o020;
pub const S_IXGRP: u16 = 0o010;
pub const S_IROTH: u16 = 0o004;
pub const S_IWOTH: u16 = 0o002;
pub const S_IXOTH: u16 = 0o001;
#[must_use]
pub fn new(mode: u16) -> Self {
VirtPermissions { mode }
}
}
impl Display for VirtPermissions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut s = String::with_capacity(9);
for &(bit, r, w, x) in &[
(Self::S_IRUSR, 'r', 'w', 'x'),
(Self::S_IRGRP, 'r', 'w', 'x'),
(Self::S_IROTH, 'r', 'w', 'x'),
] {
s.push(if self.mode & bit != 0 { r } else { '-' });
s.push(if self.mode & (bit >> 1) != 0 { w } else { '-' });
s.push(if self.mode & (bit >> 2) != 0 { x } else { '-' });
}
write!(f, "{s}")
}
}
#[derive(Clone)]
pub struct VirtMetadata {
pub permissions: VirtPermissions,
pub owner: String,
pub group: String,
pub created: SystemTime,
pub modified: SystemTime,
}
impl VirtMetadata {
#[must_use]
pub fn new(default_mode: u16) -> Self {
let now = SystemTime::now();
VirtMetadata {
permissions: VirtPermissions::new(default_mode),
owner: "root".to_string(),
group: "root".to_string(),
created: now,
modified: now,
}
}
}
#[derive(Clone)]
pub enum VirtPath {
Relative(Vec<u8>),
Absolute(Vec<u8>),
}
impl From<&str> for VirtPath {
fn from(s: &str) -> Self {
if s.starts_with('/') {
VirtPath::Absolute(s.as_bytes().to_vec())
} else {
VirtPath::Relative(s.as_bytes().to_vec())
}
}
}
impl From<String> for VirtPath {
fn from(s: String) -> Self {
if s.starts_with('/') {
VirtPath::Absolute(s.as_bytes().to_vec())
} else {
VirtPath::Relative(s.as_bytes().to_vec())
}
}
}
impl AsRef<VirtPath> for VirtPath {
fn as_ref(&self) -> &VirtPath {
self
}
}
impl VirtPath {
#[must_use]
pub fn bytes(&self) -> &[u8] {
match self {
VirtPath::Absolute(ref v) | VirtPath::Relative(ref v) => v.as_slice(),
}
}
pub fn bytes_mut(&mut self) -> &mut Vec<u8> {
match self {
VirtPath::Absolute(ref mut v) | VirtPath::Relative(ref mut v) => v,
}
}
#[must_use]
pub fn nav_rel<P: Into<VirtPath>>(&self, rhs: P) -> VirtPath {
let rhs = rhs.into();
let mut base = self.clone();
if !base.bytes().ends_with(b"/") {
base.bytes_mut().push(b'/');
}
base.bytes_mut().extend_from_slice(rhs.bytes());
base
}
}
pub struct VirtFS {
root: VirtDir,
current_dir: VirtPath,
}
#[derive(Clone)]
pub struct VirtDir {
pub path: VirtPath,
pub files: Vec<VirtFile>,
pub dirs: Vec<VirtDir>,
pub metadata: VirtMetadata,
}
#[derive(Clone)]
pub struct VirtFile {
pub path: VirtPath,
pub content: Vec<u8>,
pub metadata: VirtMetadata,
cursor: usize,
}
impl VirtFile {
pub fn new<P: Into<VirtPath>>(path: P, metadata: VirtMetadata) -> Self {
VirtFile {
path: path.into().clone(),
content: Vec::new(),
metadata,
cursor: 0,
}
}
pub fn reset_cursor(&mut self) {
self.cursor = 0;
}
pub fn try_from_real_path<P: AsRef<Path>, VP: Into<VirtPath>>(
path: P,
new_path: VP,
) -> std::io::Result<Self> {
let path = path.as_ref();
match fs::read(path) {
Ok(b) => Ok(Self {
path: new_path.into().clone(),
content: b,
metadata: VirtMetadata::new(0o755),
cursor: 0,
}),
Err(e) => Err(e),
}
}
pub fn try_from_real<VP: Into<VirtPath>>(
file: &mut File,
new_path: VP,
) -> std::io::Result<Self> {
let mut buf = Vec::new();
file.seek(SeekFrom::Start(0))?;
file.read_to_end(&mut buf)?;
Ok(Self {
path: new_path.into().clone(),
content: buf,
metadata: VirtMetadata::new(0o755),
cursor: 0,
})
}
pub fn into_real<P: AsRef<Path>>(self, export_path: P) -> std::io::Result<File> {
let mut file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(export_path)?;
file.write_all(&self.content)?;
Ok(file)
}
#[must_use]
pub fn content(&self) -> &[u8] {
&self.content
}
#[must_use]
pub fn metadata(&self) -> &VirtMetadata {
&self.metadata
}
pub fn content_mut(&mut self) -> &mut Vec<u8> {
&mut self.content
}
}
impl Default for VirtFS {
fn default() -> Self {
Self::new()
}
}
impl VirtFS {
#[must_use]
pub fn new() -> VirtFS {
let root_dir = VirtDir {
path: VirtPath::Absolute(b"/".to_vec()),
files: Vec::new(),
dirs: Vec::new(),
metadata: VirtMetadata::new(0o755),
};
VirtFS {
root: root_dir,
current_dir: VirtPath::Absolute(b"/".to_vec()),
}
}
fn resolve_path<P: Into<VirtPath>>(&self, path: P) -> VirtPath {
let p = path_to_str(&path.into());
if p.starts_with('/') {
let norm = normalize_path(&p);
VirtPath::Absolute(norm.into_bytes())
} else {
let cur = path_to_str(&self.current_dir);
let joined = if cur.ends_with('/') {
format!("{cur}{p}")
} else {
format!("{cur}/{p}")
};
let norm = normalize_path(&joined);
VirtPath::Absolute(norm.into_bytes())
}
}
pub fn cd<P: Into<VirtPath>>(&mut self, path: P) {
let path = path.into();
match path {
VirtPath::Absolute(_) => {
self.current_dir = path.clone();
}
VirtPath::Relative(_) => {
self.current_dir = self.current_dir.nav_rel(path);
}
}
let normalized = normalize_path(&path_to_str(&self.current_dir));
self.current_dir = VirtPath::Absolute(normalized.into_bytes());
}
#[must_use]
pub fn pwd(&self) -> String {
path_to_str(&self.current_dir)
}
pub fn mkdir<P: Into<VirtPath>>(&mut self, path: P) -> Result<(), FsError> {
let abs = self.resolve_path(path);
let comps = get_components_string(&path_to_str(&abs));
let mut current = &mut self.root;
let mut current_path = String::from("/");
for comp in comps {
if let Some(dir) = current.find_dir(&comp) {
*current = dir.clone();
} else {
if current_path != "/" {
current_path.push('/');
}
current_path.push_str(&comp);
let new_dir = VirtDir {
path: VirtPath::Absolute(current_path.as_bytes().to_vec()),
files: Vec::new(),
dirs: Vec::new(),
metadata: VirtMetadata::new(0o755),
};
current.dirs.push(new_dir);
current = current.find_dir_mut(&comp).unwrap();
}
}
Ok(())
}
pub fn touch<P: Into<VirtPath>>(&mut self, path: P) -> Result<(), FsError> {
let abs = self.resolve_path(path);
let comps = get_components_string(&path_to_str(&abs));
if comps.is_empty() {
return Err(FsError::InvalidPath("Empty file name".to_string()));
}
let file_name = comps.last().unwrap();
let dir_path = if comps.len() == 1 {
"/".to_string()
} else {
format!("/{}", comps[..comps.len() - 1].join("/"))
};
let dir_comps = get_components(&dir_path);
let mut current = &mut self.root;
for comp in dir_comps {
current = current
.find_dir_mut(comp)
.ok_or_else(|| FsError::NotFound(format!("Directory {comp} not found")))?;
}
if current.find_file_mut(file_name).is_some() {
return Ok(());
}
let file_full_path = if dir_path == "/" {
format!("/{file_name}")
} else {
format!("{dir_path}/{file_name}")
};
let new_file = VirtFile::new(
VirtPath::Absolute(file_full_path.as_bytes().to_vec()),
VirtMetadata::new(0o644),
);
current.files.push(new_file);
Ok(())
}
pub fn open<P: Into<VirtPath> + Clone>(&mut self, path: P) -> Result<&mut VirtFile, FsError> {
self.touch(path.clone().into())?;
self.open_file_mut(path)
}
pub fn open_file_mut<P: Into<VirtPath>>(&mut self, path: P) -> Result<&mut VirtFile, FsError> {
let abs = self.resolve_path(path);
let comps = get_components_string(&path_to_str(&abs));
if comps.is_empty() {
return Err(FsError::InvalidPath("Empty file name".to_string()));
}
let file_name = comps.last().unwrap();
let dir_path = if comps.len() == 1 {
"/".to_string()
} else {
format!("/{}", comps[..comps.len() - 1].join("/"))
};
let dir_comps = get_components(&dir_path);
let mut current = &mut self.root;
for comp in dir_comps {
current = current
.find_dir_mut(comp)
.ok_or_else(|| FsError::NotFound(format!("Directory {comp} not found")))?;
}
current
.find_file_mut(file_name)
.ok_or_else(|| FsError::NotFound(format!("File {file_name} not found")))
}
pub fn ls<P: Into<VirtPath>>(&self, path: Option<P>) -> Result<Vec<String>, FsError> {
let target_path = if let Some(p) = path {
self.resolve_path(p)
} else {
self.current_dir.clone()
};
let comps = get_components_string(&path_to_str(&target_path));
let mut current = &self.root;
for comp in comps {
current = current
.find_dir(&comp)
.ok_or_else(|| FsError::NotFound(format!("Directory {comp} not found")))?;
}
let mut entries = Vec::new();
for d in ¤t.dirs {
entries.push(format!("{}/", d.name()));
}
for f in ¤t.files {
let full = path_to_str(&f.path);
let comps = get_components(&full);
if let Some(name) = comps.last() {
entries.push((*name).to_string());
}
}
Ok(entries)
}
pub fn rm<P: Into<VirtPath>>(&mut self, path: P) -> Result<(), FsError> {
let abs = self.resolve_path(path);
let comps = get_components_string(&path_to_str(&abs));
if comps.is_empty() {
return Err(FsError::InvalidPath("Empty file name".to_string()));
}
let file_name = comps.last().unwrap();
let dir_path = if comps.len() == 1 {
"/".to_string()
} else {
format!("/{}", comps[..comps.len() - 1].join("/"))
};
let dir_comps = get_components(&dir_path);
let mut current = &mut self.root;
for comp in dir_comps {
current = current
.find_dir_mut(comp)
.ok_or_else(|| FsError::NotFound(format!("Directory {comp} not found")))?;
}
let initial_len = current.files.len();
current.files.retain(|f| {
let full = path_to_str(&f.path);
let comps = get_components(&full);
comps.last().is_none_or(|s| *s != *file_name)
});
if current.files.len() == initial_len {
return Err(FsError::NotFound(format!("File {file_name} not found")));
}
Ok(())
}
pub fn rmdir<P: Into<VirtPath>>(&mut self, path: P) -> Result<(), FsError> {
let abs = self.resolve_path(path);
let comps = get_components_string(&path_to_str(&abs));
if comps.is_empty() {
return Err(FsError::InvalidPath("Cannot remove root".to_string()));
}
let target_dir = comps.last().unwrap();
let parent_path = format!("/{}", comps[..comps.len() - 1].join("/"));
let parent_comps = get_components(&parent_path);
let mut parent = &mut self.root;
for comp in parent_comps {
parent = parent
.find_dir_mut(comp)
.ok_or_else(|| FsError::NotFound(format!("Directory {comp} not found")))?;
}
if let Some(dir) = parent.find_dir(target_dir) {
if !dir.files.is_empty() || !dir.dirs.is_empty() {
return Err(FsError::AlreadyExists(format!(
"Directory {target_dir} is not empty"
)));
}
}
let initial_len = parent.dirs.len();
parent.dirs.retain(|d| d.name() != *target_dir);
if parent.dirs.len() == initial_len {
return Err(FsError::NotFound(format!(
"Directory {target_dir} not found"
)));
}
Ok(())
}
pub fn chmod<P: Into<VirtPath>>(&mut self, path: P, mode: u16) -> Result<(), FsError> {
let abs = self.resolve_path(path);
let comps = get_components_string(&path_to_str(&abs));
if comps.is_empty() {
return Err(FsError::InvalidPath("Empty path".to_string()));
}
let name = comps.last().unwrap();
let dir_path = if comps.len() == 1 {
"/".to_string()
} else {
format!("/{}", comps[..comps.len() - 1].join("/"))
};
let dir_comps = get_components(&dir_path);
let mut current = &mut self.root;
for comp in dir_comps {
current = current
.find_dir_mut(comp)
.ok_or_else(|| FsError::NotFound(format!("Directory {comp} not found")))?;
}
if let Some(file) = current.find_file_mut(name) {
file.metadata.permissions.mode = mode;
file.metadata.modified = SystemTime::now();
return Ok(());
}
if let Some(dir) = current.find_dir_mut(name) {
dir.metadata.permissions.mode = mode;
dir.metadata.modified = SystemTime::now();
return Ok(());
}
Err(FsError::NotFound(format!("Entry {name} not found",)))
}
pub fn chown<P: Into<VirtPath>>(
&mut self,
path: P,
owner: &str,
group: &str,
) -> Result<(), FsError> {
let abs = self.resolve_path(path);
let comps = get_components_string(&path_to_str(&abs));
if comps.is_empty() {
return Err(FsError::InvalidPath("Empty path".to_string()));
}
let name = comps.last().unwrap();
let dir_path = if comps.len() == 1 {
"/".to_string()
} else {
format!("/{}", comps[..comps.len() - 1].join("/"))
};
let dir_comps = get_components(&dir_path);
let mut current = &mut self.root;
for comp in dir_comps {
current = current
.find_dir_mut(comp)
.ok_or_else(|| FsError::NotFound(format!("Directory {comp} not found")))?;
}
if let Some(file) = current.find_file_mut(name) {
file.metadata.owner = owner.to_string();
file.metadata.group = group.to_string();
file.metadata.modified = SystemTime::now();
return Ok(());
}
if let Some(dir) = current.find_dir_mut(name) {
dir.metadata.owner = owner.to_string();
dir.metadata.group = group.to_string();
dir.metadata.modified = SystemTime::now();
return Ok(());
}
Err(FsError::NotFound(format!("Entry {name} not found")))
}
pub fn stat<P: Into<VirtPath>>(&self, path: P) -> Result<VirtMetadata, FsError> {
let abs = self.resolve_path(path);
let comps = get_components_string(&path_to_str(&abs));
if comps.is_empty() {
return Err(FsError::InvalidPath("Empty path".to_string()));
}
let name = comps.last().unwrap();
let dir_path = if comps.len() == 1 {
"/".to_string()
} else {
format!("/{}", comps[..comps.len() - 1].join("/"))
};
let dir_comps = get_components(&dir_path);
let mut current = &self.root;
for comp in dir_comps {
current = current
.find_dir(comp)
.ok_or_else(|| FsError::NotFound(format!("Directory {comp} not found")))?;
}
if let Some(file) = current.find_file(name) {
return Ok(file.metadata.clone());
}
if let Some(dir) = current.find_dir(name) {
return Ok(dir.metadata.clone());
}
Err(FsError::NotFound(format!("Entry {name} not found")))
}
pub fn rename<P: Into<VirtPath>, P2: Into<VirtPath>>(
&mut self,
src: P,
dst: P2,
) -> Result<(), FsError> {
let src_abs = self.resolve_path(src);
let dst_abs = self.resolve_path(dst);
let src_comps = get_components_string(&path_to_str(&src_abs));
let dst_comps = get_components_string(&path_to_str(&dst_abs));
if src_comps.is_empty() || dst_comps.is_empty() {
return Err(FsError::InvalidPath("Empty path".to_string()));
}
let src_file_name = src_comps.last().unwrap();
let src_parent_path = if src_comps.len() == 1 {
"/".to_string()
} else {
format!("/{}", src_comps[..src_comps.len() - 1].join("/"))
};
let src_parent_comps = get_components(&src_parent_path);
let mut src_parent = &mut self.root;
for comp in src_parent_comps {
src_parent = src_parent
.find_dir_mut(comp)
.ok_or_else(|| FsError::NotFound(format!("Directory {comp} not found")))?;
}
if let Some(pos) = src_parent.files.iter().position(|f| {
let full = path_to_str(&f.path);
let comps = get_components(&full);
comps.last().is_some_and(|s| *s == *src_file_name)
}) {
let mut file = src_parent.files.remove(pos);
file.path = VirtPath::Absolute(path_to_str(&dst_abs).as_bytes().to_vec());
let dst_parent_path = if dst_comps.len() == 1 {
"/".to_string()
} else {
format!("/{}", dst_comps[..dst_comps.len() - 1].join("/"))
};
let dst_parent_comps = get_components(&dst_parent_path);
let mut dst_parent = &mut self.root;
for comp in dst_parent_comps {
dst_parent = dst_parent
.find_dir_mut(comp)
.ok_or_else(|| FsError::NotFound(format!("Directory {comp} not found")))?;
}
dst_parent.files.push(file);
return Ok(());
}
if let Some(pos) = src_parent
.dirs
.iter()
.position(|d| d.name() == *src_file_name)
{
let mut dir = src_parent.dirs.remove(pos);
dir.update_path::<String>(path_to_str(&dst_abs));
let dst_parent_path = if dst_comps.len() == 1 {
"/".to_string()
} else {
format!("/{}", dst_comps[..dst_comps.len() - 1].join("/"))
};
let dst_parent_comps = get_components(&dst_parent_path);
let mut dst_parent = &mut self.root;
for comp in dst_parent_comps {
dst_parent = dst_parent
.find_dir_mut(comp)
.ok_or_else(|| FsError::NotFound(format!("Directory {comp} not found")))?;
}
dst_parent.dirs.push(dir);
return Ok(());
}
Err(FsError::NotFound("Source entry not found".to_string()))
}
}
impl VirtDir {
#[must_use]
pub fn name(&self) -> String {
let full = path_to_str(&self.path);
if full == "/" {
"/".to_string()
} else {
let comps = get_components(&full);
(*comps.last().unwrap_or(&full.as_str())).to_string()
}
}
pub fn find_dir_mut(&mut self, name: &str) -> Option<&mut VirtDir> {
self.dirs.iter_mut().find(|d| d.name() == name)
}
#[must_use]
pub fn find_dir(&self, name: &str) -> Option<&VirtDir> {
self.dirs.iter().find(|d| d.name() == name)
}
pub fn find_file_mut(&mut self, name: &str) -> Option<&mut VirtFile> {
self.files.iter_mut().find(|f| {
let full = path_to_str(&f.path);
let comps = get_components(&full);
comps.last().is_some_and(|s| *s == name)
})
}
#[must_use]
pub fn find_file(&self, name: &str) -> Option<&VirtFile> {
self.files.iter().find(|f| {
let full = path_to_str(&f.path);
let comps = get_components(&full);
comps.last().is_some_and(|s| *s == name)
})
}
pub fn insert_file(&mut self, file: VirtFile) {
self.files.push(file);
}
pub fn update_path<P: Into<VirtPath>>(&mut self, new_path: P) {
let new_path = new_path.into();
self.path = new_path.clone();
for f in &mut self.files {
let comps = get_components_string(&path_to_str(&f.path));
if let Some(name) = comps.last() {
let full = if path_to_str(&new_path) == "/" {
format!("/{name}")
} else {
format!("{}/{}", path_to_str(&new_path), name)
};
f.path = VirtPath::Absolute(full.as_bytes().to_vec());
}
}
for d in &mut self.dirs {
let comps = get_components_string(&path_to_str(&d.path));
if let Some(name) = comps.last() {
let full = if path_to_str(&new_path) == "/" {
format!("/{name}")
} else {
format!("{}/{name}", path_to_str(&new_path))
};
d.update_path(VirtPath::Absolute(full.as_bytes().to_vec()));
}
}
}
}
impl Read for VirtFile {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
if self.cursor >= self.content.len() {
return Ok(0); }
let available = self.content.len() - self.cursor;
let to_read = available.min(buf.len());
buf[..to_read].copy_from_slice(&self.content[self.cursor..self.cursor + to_read]);
self.cursor += to_read;
Ok(to_read)
}
}
impl Write for VirtFile {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
if self.cursor > self.content.len() {
self.content.resize(self.cursor, 0);
}
let end = self.cursor + buf.len();
if end > self.content.len() {
self.content.resize(end, 0);
}
self.content[self.cursor..end].copy_from_slice(buf);
self.cursor = end;
self.metadata.modified = SystemTime::now();
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl Seek for VirtFile {
#[allow(clippy::cast_possible_wrap)]
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_sign_loss)]
fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
let new_pos = match pos {
SeekFrom::Start(offset) => offset as i64,
SeekFrom::Current(offset) => self.cursor as i64 + offset,
SeekFrom::End(offset) => self.content.len() as i64 + offset,
};
if new_pos < 0 {
Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Invalid seek",
))
} else {
self.cursor = new_pos as usize;
Ok(self.cursor as u64)
}
}
}
#[cfg(feature = "display_files")]
impl Display for VirtFile {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(
f,
"{}",
sew::infallible::InfallibleString::from(self.content.clone())
)
}
}