use super::traits::{DirEntry, DirEntryKind, Filesystem};
use async_trait::async_trait;
use std::collections::HashMap;
use std::io;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use tokio::sync::RwLock;
#[derive(Debug, Clone)]
enum Entry {
File { data: Vec<u8>, modified: SystemTime },
Directory { modified: SystemTime },
Symlink { target: PathBuf, modified: SystemTime },
}
#[derive(Debug)]
pub struct MemoryFs {
entries: RwLock<HashMap<PathBuf, Entry>>,
}
impl Default for MemoryFs {
fn default() -> Self {
Self::new()
}
}
impl MemoryFs {
pub fn new() -> Self {
let mut entries = HashMap::new();
entries.insert(
PathBuf::from(""),
Entry::Directory {
modified: SystemTime::now(),
},
);
Self {
entries: RwLock::new(entries),
}
}
fn normalize(path: &Path) -> PathBuf {
let mut result = PathBuf::new();
for component in path.components() {
match component {
std::path::Component::RootDir => {}
std::path::Component::CurDir => {}
std::path::Component::ParentDir => {
result.pop();
}
std::path::Component::Normal(s) => {
result.push(s);
}
std::path::Component::Prefix(_) => {}
}
}
result
}
const MAX_SYMLINK_DEPTH: usize = 40;
fn read_inner(&self, path: &Path, depth: usize) -> std::pin::Pin<Box<dyn std::future::Future<Output = io::Result<Vec<u8>>> + Send + '_>> {
let path = path.to_path_buf();
Box::pin(async move {
if depth > Self::MAX_SYMLINK_DEPTH {
return Err(io::Error::other(
"too many levels of symbolic links",
));
}
let normalized = Self::normalize(&path);
let entries = self.entries.read().await;
match entries.get(&normalized) {
Some(Entry::File { data, .. }) => Ok(data.clone()),
Some(Entry::Directory { .. }) => Err(io::Error::new(
io::ErrorKind::IsADirectory,
format!("is a directory: {}", path.display()),
)),
Some(Entry::Symlink { target, .. }) => {
let target = target.clone();
drop(entries);
self.read_inner(&target, depth + 1).await
}
None => Err(io::Error::new(
io::ErrorKind::NotFound,
format!("not found: {}", path.display()),
)),
}
})
}
fn stat_inner(&self, path: &Path, depth: usize) -> std::pin::Pin<Box<dyn std::future::Future<Output = io::Result<DirEntry>> + Send + '_>> {
let path = path.to_path_buf();
Box::pin(async move {
if depth > Self::MAX_SYMLINK_DEPTH {
return Err(io::Error::other(
"too many levels of symbolic links",
));
}
let normalized = Self::normalize(&path);
if normalized.as_os_str().is_empty() {
return Ok(DirEntry {
name: String::new(),
kind: DirEntryKind::Directory,
size: 0,
modified: Some(SystemTime::now()),
permissions: None,
symlink_target: None,
});
}
let entry_info: Option<(DirEntry, Option<PathBuf>)> = {
let entries = self.entries.read().await;
match entries.get(&normalized) {
Some(Entry::File { data, modified }) => Some((
DirEntry {
name: String::new(),
kind: DirEntryKind::File,
size: data.len() as u64,
modified: Some(*modified),
permissions: None,
symlink_target: None,
},
None,
)),
Some(Entry::Directory { modified }) => Some((
DirEntry {
name: String::new(),
kind: DirEntryKind::Directory,
size: 0,
modified: Some(*modified),
permissions: None,
symlink_target: None,
},
None,
)),
Some(Entry::Symlink { target, .. }) => Some((
DirEntry {
name: String::new(),
kind: DirEntryKind::File, size: 0,
modified: None,
permissions: None,
symlink_target: None,
},
Some(target.clone()),
)),
None => None,
}
};
match entry_info {
Some((entry, None)) => Ok(entry),
Some((_, Some(target))) => self.stat_inner(&target, depth + 1).await,
None => Err(io::Error::new(
io::ErrorKind::NotFound,
format!("not found: {}", path.display()),
)),
}
})
}
async fn ensure_parents(&self, path: &Path) -> io::Result<()> {
let mut entries = self.entries.write().await;
let mut current = PathBuf::new();
for component in path.parent().into_iter().flat_map(|p| p.components()) {
if let std::path::Component::Normal(s) = component {
current.push(s);
match entries.entry(current.clone()) {
std::collections::hash_map::Entry::Occupied(e) => {
if matches!(e.get(), Entry::File { .. }) {
return Err(io::Error::new(
io::ErrorKind::NotADirectory,
format!("not a directory: {}", current.display()),
));
}
}
std::collections::hash_map::Entry::Vacant(e) => {
e.insert(Entry::Directory {
modified: SystemTime::now(),
});
}
}
}
}
Ok(())
}
}
#[async_trait]
impl Filesystem for MemoryFs {
async fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
self.read_inner(path, 0).await
}
async fn write(&self, path: &Path, data: &[u8]) -> io::Result<()> {
let normalized = Self::normalize(path);
self.ensure_parents(&normalized).await?;
let mut entries = self.entries.write().await;
if let Some(Entry::Directory { .. }) = entries.get(&normalized) {
return Err(io::Error::new(
io::ErrorKind::IsADirectory,
format!("is a directory: {}", path.display()),
));
}
entries.insert(
normalized,
Entry::File {
data: data.to_vec(),
modified: SystemTime::now(),
},
);
Ok(())
}
async fn list(&self, path: &Path) -> io::Result<Vec<DirEntry>> {
let normalized = Self::normalize(path);
let entries = self.entries.read().await;
match entries.get(&normalized) {
Some(Entry::Directory { .. }) => {}
Some(Entry::File { .. }) => {
return Err(io::Error::new(
io::ErrorKind::NotADirectory,
format!("not a directory: {}", path.display()),
))
}
Some(Entry::Symlink { .. }) => {
return Err(io::Error::new(
io::ErrorKind::NotADirectory,
format!("not a directory: {}", path.display()),
))
}
None if normalized.as_os_str().is_empty() => {
}
None => {
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!("not found: {}", path.display()),
))
}
}
let prefix = if normalized.as_os_str().is_empty() {
PathBuf::new()
} else {
normalized.clone()
};
let mut result = Vec::new();
for (entry_path, entry) in entries.iter() {
if let Some(parent) = entry_path.parent()
&& parent == prefix && entry_path != &normalized
&& let Some(name) = entry_path.file_name() {
let (kind, size, modified, symlink_target) = match entry {
Entry::File { data, modified } => (DirEntryKind::File, data.len() as u64, Some(*modified), None),
Entry::Directory { modified } => (DirEntryKind::Directory, 0, Some(*modified), None),
Entry::Symlink { target, modified } => (DirEntryKind::Symlink, 0, Some(*modified), Some(target.clone())),
};
result.push(DirEntry {
name: name.to_string_lossy().into_owned(),
kind,
size,
modified,
permissions: None,
symlink_target,
});
}
}
result.sort_by(|a, b| a.name.cmp(&b.name));
Ok(result)
}
async fn stat(&self, path: &Path) -> io::Result<DirEntry> {
let mut entry = self.stat_inner(path, 0).await?;
let normalized = Self::normalize(path);
entry.name = normalized
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "/".to_string());
Ok(entry)
}
async fn lstat(&self, path: &Path) -> io::Result<DirEntry> {
let normalized = Self::normalize(path);
let name = normalized
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "/".to_string());
let entries = self.entries.read().await;
if normalized.as_os_str().is_empty() {
return Ok(DirEntry {
name,
kind: DirEntryKind::Directory,
size: 0,
modified: Some(SystemTime::now()),
permissions: None,
symlink_target: None,
});
}
match entries.get(&normalized) {
Some(Entry::File { data, modified }) => Ok(DirEntry {
name,
kind: DirEntryKind::File,
size: data.len() as u64,
modified: Some(*modified),
permissions: None,
symlink_target: None,
}),
Some(Entry::Directory { modified }) => Ok(DirEntry {
name,
kind: DirEntryKind::Directory,
size: 0,
modified: Some(*modified),
permissions: None,
symlink_target: None,
}),
Some(Entry::Symlink { target, modified }) => Ok(DirEntry {
name,
kind: DirEntryKind::Symlink,
size: 0,
modified: Some(*modified),
permissions: None,
symlink_target: Some(target.clone()),
}),
None => Err(io::Error::new(
io::ErrorKind::NotFound,
format!("not found: {}", path.display()),
)),
}
}
async fn read_link(&self, path: &Path) -> io::Result<PathBuf> {
let normalized = Self::normalize(path);
let entries = self.entries.read().await;
match entries.get(&normalized) {
Some(Entry::Symlink { target, .. }) => Ok(target.clone()),
Some(_) => Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("not a symbolic link: {}", path.display()),
)),
None => Err(io::Error::new(
io::ErrorKind::NotFound,
format!("not found: {}", path.display()),
)),
}
}
async fn symlink(&self, target: &Path, link: &Path) -> io::Result<()> {
let normalized = Self::normalize(link);
self.ensure_parents(&normalized).await?;
let mut entries = self.entries.write().await;
if entries.contains_key(&normalized) {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("file exists: {}", link.display()),
));
}
entries.insert(
normalized,
Entry::Symlink {
target: target.to_path_buf(),
modified: SystemTime::now(),
},
);
Ok(())
}
async fn mkdir(&self, path: &Path) -> io::Result<()> {
let normalized = Self::normalize(path);
self.ensure_parents(&normalized).await?;
let mut entries = self.entries.write().await;
if let Some(existing) = entries.get(&normalized) {
return match existing {
Entry::Directory { .. } => Ok(()), Entry::File { .. } | Entry::Symlink { .. } => Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("file exists: {}", path.display()),
)),
};
}
entries.insert(
normalized,
Entry::Directory {
modified: SystemTime::now(),
},
);
Ok(())
}
async fn remove(&self, path: &Path) -> io::Result<()> {
let normalized = Self::normalize(path);
if normalized.as_os_str().is_empty() {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"cannot remove root directory",
));
}
let mut entries = self.entries.write().await;
if let Some(Entry::Directory { .. }) = entries.get(&normalized) {
let has_children = entries.keys().any(|k| {
k.parent() == Some(&normalized) && k != &normalized
});
if has_children {
return Err(io::Error::new(
io::ErrorKind::DirectoryNotEmpty,
format!("directory not empty: {}", path.display()),
));
}
}
entries.remove(&normalized).ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
format!("not found: {}", path.display()),
)
})?;
Ok(())
}
async fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
let from_normalized = Self::normalize(from);
let to_normalized = Self::normalize(to);
if from_normalized.as_os_str().is_empty() {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"cannot rename root directory",
));
}
if from_normalized == to_normalized {
return Ok(());
}
if to_normalized.starts_with(&from_normalized) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("cannot move '{}' into itself", from.display()),
));
}
drop(self.ensure_parents(&to_normalized).await);
let mut entries = self.entries.write().await;
let entry = entries.remove(&from_normalized).ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
format!("not found: {}", from.display()),
)
})?;
if let Some(existing) = entries.get(&to_normalized) {
match (&entry, existing) {
(Entry::File { .. }, Entry::Directory { .. }) => {
entries.insert(from_normalized, entry);
return Err(io::Error::new(
io::ErrorKind::IsADirectory,
format!("destination is a directory: {}", to.display()),
));
}
(Entry::Directory { .. }, Entry::File { .. }) => {
entries.insert(from_normalized, entry);
return Err(io::Error::new(
io::ErrorKind::NotADirectory,
format!("destination is not a directory: {}", to.display()),
));
}
_ => {}
}
}
if matches!(entry, Entry::Directory { .. }) {
let children_to_rename: Vec<(PathBuf, Entry)> = entries
.iter()
.filter(|(k, _)| k.starts_with(&from_normalized) && *k != &from_normalized)
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
for (old_path, child_entry) in children_to_rename {
entries.remove(&old_path);
let Ok(relative) = old_path.strip_prefix(&from_normalized) else {
continue;
};
let new_path = to_normalized.join(relative);
entries.insert(new_path, child_entry);
}
}
entries.insert(to_normalized, entry);
Ok(())
}
fn read_only(&self) -> bool {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_write_and_read() {
let fs = MemoryFs::new();
fs.write(Path::new("test.txt"), b"hello world").await.unwrap();
let data = fs.read(Path::new("test.txt")).await.unwrap();
assert_eq!(data, b"hello world");
}
#[tokio::test]
async fn test_read_not_found() {
let fs = MemoryFs::new();
let result = fs.read(Path::new("nonexistent.txt")).await;
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::NotFound);
}
#[tokio::test]
async fn test_nested_directories() {
let fs = MemoryFs::new();
fs.write(Path::new("a/b/c/file.txt"), b"nested").await.unwrap();
let entry = fs.stat(Path::new("a")).await.unwrap();
assert_eq!(entry.kind, DirEntryKind::Directory);
let entry = fs.stat(Path::new("a/b")).await.unwrap();
assert_eq!(entry.kind, DirEntryKind::Directory);
let entry = fs.stat(Path::new("a/b/c")).await.unwrap();
assert_eq!(entry.kind, DirEntryKind::Directory);
let data = fs.read(Path::new("a/b/c/file.txt")).await.unwrap();
assert_eq!(data, b"nested");
}
#[tokio::test]
async fn test_list_directory() {
let fs = MemoryFs::new();
fs.write(Path::new("a.txt"), b"a").await.unwrap();
fs.write(Path::new("b.txt"), b"b").await.unwrap();
fs.mkdir(Path::new("subdir")).await.unwrap();
let entries = fs.list(Path::new("")).await.unwrap();
assert_eq!(entries.len(), 3);
let names: Vec<_> = entries.iter().map(|e| &e.name).collect();
assert!(names.contains(&&"a.txt".to_string()));
assert!(names.contains(&&"b.txt".to_string()));
assert!(names.contains(&&"subdir".to_string()));
}
#[tokio::test]
async fn test_mkdir_and_stat() {
let fs = MemoryFs::new();
fs.mkdir(Path::new("mydir")).await.unwrap();
let entry = fs.stat(Path::new("mydir")).await.unwrap();
assert_eq!(entry.kind, DirEntryKind::Directory);
}
#[tokio::test]
async fn test_remove_file() {
let fs = MemoryFs::new();
fs.write(Path::new("file.txt"), b"data").await.unwrap();
fs.remove(Path::new("file.txt")).await.unwrap();
let result = fs.stat(Path::new("file.txt")).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_remove_empty_directory() {
let fs = MemoryFs::new();
fs.mkdir(Path::new("emptydir")).await.unwrap();
fs.remove(Path::new("emptydir")).await.unwrap();
let result = fs.stat(Path::new("emptydir")).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_remove_non_empty_directory_fails() {
let fs = MemoryFs::new();
fs.write(Path::new("dir/file.txt"), b"data").await.unwrap();
let result = fs.remove(Path::new("dir")).await;
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::DirectoryNotEmpty);
}
#[tokio::test]
async fn test_path_normalization() {
let fs = MemoryFs::new();
fs.write(Path::new("/a/b/c.txt"), b"data").await.unwrap();
let data1 = fs.read(Path::new("a/b/c.txt")).await.unwrap();
let data2 = fs.read(Path::new("/a/b/c.txt")).await.unwrap();
let data3 = fs.read(Path::new("a/./b/c.txt")).await.unwrap();
let data4 = fs.read(Path::new("a/b/../b/c.txt")).await.unwrap();
assert_eq!(data1, data2);
assert_eq!(data2, data3);
assert_eq!(data3, data4);
}
#[tokio::test]
async fn test_overwrite_file() {
let fs = MemoryFs::new();
fs.write(Path::new("file.txt"), b"first").await.unwrap();
fs.write(Path::new("file.txt"), b"second").await.unwrap();
let data = fs.read(Path::new("file.txt")).await.unwrap();
assert_eq!(data, b"second");
}
#[tokio::test]
async fn test_exists() {
let fs = MemoryFs::new();
assert!(!fs.exists(Path::new("nope.txt")).await);
fs.write(Path::new("yes.txt"), b"here").await.unwrap();
assert!(fs.exists(Path::new("yes.txt")).await);
}
#[tokio::test]
async fn test_rename_file() {
let fs = MemoryFs::new();
fs.write(Path::new("old.txt"), b"content").await.unwrap();
fs.rename(Path::new("old.txt"), Path::new("new.txt")).await.unwrap();
let data = fs.read(Path::new("new.txt")).await.unwrap();
assert_eq!(data, b"content");
assert!(!fs.exists(Path::new("old.txt")).await);
}
#[tokio::test]
async fn test_rename_directory() {
let fs = MemoryFs::new();
fs.write(Path::new("dir/a.txt"), b"a").await.unwrap();
fs.write(Path::new("dir/b.txt"), b"b").await.unwrap();
fs.write(Path::new("dir/sub/c.txt"), b"c").await.unwrap();
fs.rename(Path::new("dir"), Path::new("renamed")).await.unwrap();
assert!(fs.exists(Path::new("renamed")).await);
assert!(fs.exists(Path::new("renamed/a.txt")).await);
assert!(fs.exists(Path::new("renamed/b.txt")).await);
assert!(fs.exists(Path::new("renamed/sub/c.txt")).await);
assert!(!fs.exists(Path::new("dir")).await);
assert!(!fs.exists(Path::new("dir/a.txt")).await);
let data = fs.read(Path::new("renamed/a.txt")).await.unwrap();
assert_eq!(data, b"a");
}
#[tokio::test]
async fn test_rename_not_found() {
let fs = MemoryFs::new();
let result = fs.rename(Path::new("nonexistent"), Path::new("dest")).await;
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::NotFound);
}
#[tokio::test]
async fn test_symlink_create_and_read_link() {
let fs = MemoryFs::new();
fs.write(Path::new("target.txt"), b"content").await.unwrap();
fs.symlink(Path::new("target.txt"), Path::new("link.txt")).await.unwrap();
let target = fs.read_link(Path::new("link.txt")).await.unwrap();
assert_eq!(target, Path::new("target.txt"));
}
#[tokio::test]
async fn test_symlink_read_follows_link() {
let fs = MemoryFs::new();
fs.write(Path::new("target.txt"), b"hello from target").await.unwrap();
fs.symlink(Path::new("target.txt"), Path::new("link.txt")).await.unwrap();
let data = fs.read(Path::new("link.txt")).await.unwrap();
assert_eq!(data, b"hello from target");
}
#[tokio::test]
async fn test_symlink_stat_follows_link() {
let fs = MemoryFs::new();
fs.write(Path::new("target.txt"), b"12345").await.unwrap();
fs.symlink(Path::new("target.txt"), Path::new("link.txt")).await.unwrap();
let entry = fs.stat(Path::new("link.txt")).await.unwrap();
assert_eq!(entry.kind, DirEntryKind::File);
assert_eq!(entry.size, 5);
}
#[tokio::test]
async fn test_symlink_lstat_returns_symlink_info() {
let fs = MemoryFs::new();
fs.write(Path::new("target.txt"), b"content").await.unwrap();
fs.symlink(Path::new("target.txt"), Path::new("link.txt")).await.unwrap();
let entry = fs.lstat(Path::new("link.txt")).await.unwrap();
assert_eq!(entry.kind, DirEntryKind::Symlink);
}
#[tokio::test]
async fn test_symlink_in_list() {
let fs = MemoryFs::new();
fs.write(Path::new("file.txt"), b"content").await.unwrap();
fs.symlink(Path::new("file.txt"), Path::new("link.txt")).await.unwrap();
fs.mkdir(Path::new("dir")).await.unwrap();
let entries = fs.list(Path::new("")).await.unwrap();
assert_eq!(entries.len(), 3);
let link_entry = entries.iter().find(|e| e.name == "link.txt").unwrap();
assert_eq!(link_entry.kind, DirEntryKind::Symlink);
assert_eq!(link_entry.symlink_target, Some(PathBuf::from("file.txt")));
}
#[tokio::test]
async fn test_symlink_broken_link() {
let fs = MemoryFs::new();
fs.symlink(Path::new("nonexistent.txt"), Path::new("broken.txt")).await.unwrap();
let target = fs.read_link(Path::new("broken.txt")).await.unwrap();
assert_eq!(target, Path::new("nonexistent.txt"));
let entry = fs.lstat(Path::new("broken.txt")).await.unwrap();
assert_eq!(entry.kind, DirEntryKind::Symlink);
let result = fs.stat(Path::new("broken.txt")).await;
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::NotFound);
let result = fs.read(Path::new("broken.txt")).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_symlink_read_link_on_non_symlink_fails() {
let fs = MemoryFs::new();
fs.write(Path::new("file.txt"), b"content").await.unwrap();
let result = fs.read_link(Path::new("file.txt")).await;
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidInput);
}
#[tokio::test]
async fn test_symlink_already_exists() {
let fs = MemoryFs::new();
fs.write(Path::new("existing.txt"), b"content").await.unwrap();
let result = fs.symlink(Path::new("target"), Path::new("existing.txt")).await;
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::AlreadyExists);
}
#[tokio::test]
async fn test_symlink_chain() {
let fs = MemoryFs::new();
fs.write(Path::new("file.txt"), b"end of chain").await.unwrap();
fs.symlink(Path::new("file.txt"), Path::new("c")).await.unwrap();
fs.symlink(Path::new("c"), Path::new("b")).await.unwrap();
fs.symlink(Path::new("b"), Path::new("a")).await.unwrap();
let data = fs.read(Path::new("a")).await.unwrap();
assert_eq!(data, b"end of chain");
let entry = fs.stat(Path::new("a")).await.unwrap();
assert_eq!(entry.kind, DirEntryKind::File);
}
#[tokio::test]
async fn test_symlink_to_directory() {
let fs = MemoryFs::new();
fs.mkdir(Path::new("realdir")).await.unwrap();
fs.write(Path::new("realdir/file.txt"), b"inside dir").await.unwrap();
fs.symlink(Path::new("realdir"), Path::new("linkdir")).await.unwrap();
let entry = fs.stat(Path::new("linkdir")).await.unwrap();
assert_eq!(entry.kind, DirEntryKind::Directory);
}
#[tokio::test]
async fn test_symlink_relative_path_stored_as_is() {
let fs = MemoryFs::new();
fs.mkdir(Path::new("subdir")).await.unwrap();
fs.write(Path::new("subdir/target.txt"), b"content").await.unwrap();
fs.symlink(Path::new("../subdir/target.txt"), Path::new("subdir/link.txt")).await.unwrap();
let target = fs.read_link(Path::new("subdir/link.txt")).await.unwrap();
assert_eq!(target.to_string_lossy(), "../subdir/target.txt");
}
#[tokio::test]
async fn test_symlink_absolute_path() {
let fs = MemoryFs::new();
fs.write(Path::new("target.txt"), b"content").await.unwrap();
fs.symlink(Path::new("/target.txt"), Path::new("link.txt")).await.unwrap();
let target = fs.read_link(Path::new("link.txt")).await.unwrap();
assert_eq!(target.to_string_lossy(), "/target.txt");
let data = fs.read(Path::new("link.txt")).await.unwrap();
assert_eq!(data, b"content");
}
#[tokio::test]
async fn test_symlink_remove() {
let fs = MemoryFs::new();
fs.write(Path::new("target.txt"), b"content").await.unwrap();
fs.symlink(Path::new("target.txt"), Path::new("link.txt")).await.unwrap();
fs.remove(Path::new("link.txt")).await.unwrap();
assert!(!fs.exists(Path::new("link.txt")).await);
assert!(fs.exists(Path::new("target.txt")).await);
}
#[tokio::test]
async fn test_symlink_overwrite_target_content() {
let fs = MemoryFs::new();
fs.write(Path::new("target.txt"), b"original").await.unwrap();
fs.symlink(Path::new("target.txt"), Path::new("link.txt")).await.unwrap();
fs.write(Path::new("target.txt"), b"modified").await.unwrap();
let data = fs.read(Path::new("link.txt")).await.unwrap();
assert_eq!(data, b"modified");
}
#[tokio::test]
async fn test_symlink_empty_name() {
let fs = MemoryFs::new();
fs.write(Path::new("target.txt"), b"content").await.unwrap();
fs.symlink(Path::new("./target.txt"), Path::new("link.txt")).await.unwrap();
let target = fs.read_link(Path::new("link.txt")).await.unwrap();
assert_eq!(target.to_string_lossy(), "./target.txt");
}
#[tokio::test]
async fn test_symlink_nested_creation() {
let fs = MemoryFs::new();
fs.symlink(Path::new("target"), Path::new("a/b/c/link")).await.unwrap();
let entry = fs.stat(Path::new("a/b")).await.unwrap();
assert_eq!(entry.kind, DirEntryKind::Directory);
let entry = fs.lstat(Path::new("a/b/c/link")).await.unwrap();
assert_eq!(entry.kind, DirEntryKind::Symlink);
}
#[tokio::test]
async fn test_symlink_read_link_not_found() {
let fs = MemoryFs::new();
let result = fs.read_link(Path::new("nonexistent")).await;
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::NotFound);
}
#[tokio::test]
async fn test_symlink_read_link_on_directory() {
let fs = MemoryFs::new();
fs.mkdir(Path::new("dir")).await.unwrap();
let result = fs.read_link(Path::new("dir")).await;
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidInput);
}
#[tokio::test]
async fn test_symlink_circular_read_returns_error() {
let fs = MemoryFs::new();
fs.symlink(Path::new("b"), Path::new("a")).await.unwrap();
fs.symlink(Path::new("a"), Path::new("b")).await.unwrap();
let result = fs.read(Path::new("a")).await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.to_string().contains("symbolic links"),
"expected symlink loop error, got: {}",
err
);
}
#[tokio::test]
async fn test_symlink_circular_stat_returns_error() {
let fs = MemoryFs::new();
fs.symlink(Path::new("b"), Path::new("a")).await.unwrap();
fs.symlink(Path::new("a"), Path::new("b")).await.unwrap();
let result = fs.stat(Path::new("a")).await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.to_string().contains("symbolic links"),
"expected symlink loop error, got: {}",
err
);
}
#[tokio::test]
async fn test_rename_into_self_errors() {
let fs = MemoryFs::new();
fs.mkdir(Path::new("a")).await.unwrap();
let result = fs.rename(Path::new("a"), Path::new("a/b")).await;
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidInput);
}
#[tokio::test]
async fn test_rename_identity_noop() {
let fs = MemoryFs::new();
fs.write(Path::new("a"), b"data").await.unwrap();
fs.rename(Path::new("a"), Path::new("a")).await.unwrap();
let data = fs.read(Path::new("a")).await.unwrap();
assert_eq!(data, b"data");
}
#[tokio::test]
async fn test_ensure_parents_rejects_file_as_dir() {
let fs = MemoryFs::new();
fs.write(Path::new("a"), b"I am a file").await.unwrap();
let result = fs.write(Path::new("a/b"), b"child").await;
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::NotADirectory);
}
}