use async_trait::async_trait;
use std::io::Error as IoError;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::SystemTime;
use super::backend::FsBackend;
use super::limits::{FsLimits, FsUsage};
use super::normalize_path;
use super::traits::{DirEntry, FileSystem, FileSystemExt, Metadata, fs_errors};
use crate::error::Result;
pub struct PosixFs<B: FsBackend> {
backend: B,
}
impl<B: FsBackend> PosixFs<B> {
pub fn new(backend: B) -> Self {
Self { backend }
}
pub fn backend(&self) -> &B {
&self.backend
}
fn normalize(path: &Path) -> PathBuf {
normalize_path(path)
}
async fn check_parent_exists(&self, path: &Path) -> Result<()> {
if let Some(parent) = path.parent()
&& parent != Path::new("/")
&& parent != Path::new("")
&& !self.backend.exists(parent).await?
{
return Err(fs_errors::parent_not_found());
}
Ok(())
}
}
#[async_trait]
impl<B: FsBackend + 'static> FileSystem for PosixFs<B> {
async fn read_file(&self, path: &Path) -> Result<Vec<u8>> {
let path = Self::normalize(path);
if let Ok(meta) = self.backend.stat(&path).await
&& meta.file_type.is_dir()
{
return Err(fs_errors::is_a_directory());
}
self.backend.read(&path).await
}
async fn write_file(&self, path: &Path, content: &[u8]) -> Result<()> {
let path = Self::normalize(path);
self.check_parent_exists(&path).await?;
if let Ok(meta) = self.backend.stat(&path).await
&& meta.file_type.is_dir()
{
return Err(fs_errors::is_a_directory());
}
self.backend.write(&path, content).await
}
async fn append_file(&self, path: &Path, content: &[u8]) -> Result<()> {
let path = Self::normalize(path);
self.check_parent_exists(&path).await?;
if let Ok(meta) = self.backend.stat(&path).await
&& meta.file_type.is_dir()
{
return Err(fs_errors::is_a_directory());
}
self.backend.append(&path, content).await
}
async fn mkdir(&self, path: &Path, recursive: bool) -> Result<()> {
let path = Self::normalize(path);
if let Ok(meta) = self.backend.stat(&path).await {
if meta.file_type.is_dir() {
if recursive {
return Ok(()); } else {
return Err(fs_errors::already_exists("directory exists"));
}
} else {
return Err(fs_errors::already_exists("file exists"));
}
}
if recursive {
if let Some(parent) = path.parent() {
let mut current = PathBuf::from("/");
for component in parent.components().skip(1) {
current.push(component);
if let Ok(meta) = self.backend.stat(¤t).await
&& !meta.file_type.is_dir()
{
return Err(fs_errors::already_exists("file exists"));
}
}
}
} else {
self.check_parent_exists(&path).await?;
}
self.backend.mkdir(&path, recursive).await
}
async fn remove(&self, path: &Path, recursive: bool) -> Result<()> {
let path = Self::normalize(path);
self.backend.remove(&path, recursive).await
}
async fn stat(&self, path: &Path) -> Result<Metadata> {
let path = Self::normalize(path);
self.backend.stat(&path).await
}
async fn read_dir(&self, path: &Path) -> Result<Vec<DirEntry>> {
let path = Self::normalize(path);
if let Ok(meta) = self.backend.stat(&path).await
&& !meta.file_type.is_dir()
{
return Err(fs_errors::not_a_directory());
}
self.backend.read_dir(&path).await
}
async fn exists(&self, path: &Path) -> Result<bool> {
let path = Self::normalize(path);
self.backend.exists(&path).await
}
async fn rename(&self, from: &Path, to: &Path) -> Result<()> {
let from = Self::normalize(from);
let to = Self::normalize(to);
self.backend.rename(&from, &to).await
}
async fn copy(&self, from: &Path, to: &Path) -> Result<()> {
let from = Self::normalize(from);
let to = Self::normalize(to);
if let Ok(meta) = self.backend.stat(&from).await
&& meta.file_type.is_dir()
{
return Err(IoError::other("cannot copy directory").into());
}
self.backend.copy(&from, &to).await
}
async fn symlink(&self, target: &Path, link: &Path) -> Result<()> {
let link = Self::normalize(link);
self.backend.symlink(target, &link).await
}
async fn read_link(&self, path: &Path) -> Result<PathBuf> {
let path = Self::normalize(path);
self.backend.read_link(&path).await
}
async fn chmod(&self, path: &Path, mode: u32) -> Result<()> {
let path = Self::normalize(path);
self.backend.chmod(&path, mode).await
}
async fn set_modified_time(&self, path: &Path, time: SystemTime) -> Result<()> {
let path = Self::normalize(path);
self.backend.set_modified_time(&path, time).await
}
}
#[async_trait]
impl<B: FsBackend + 'static> FileSystemExt for PosixFs<B> {
fn usage(&self) -> FsUsage {
self.backend.usage()
}
fn limits(&self) -> FsLimits {
self.backend.limits()
}
}
impl<B: FsBackend + 'static> From<PosixFs<B>> for Arc<dyn FileSystem> {
fn from(fs: PosixFs<B>) -> Self {
Arc::new(fs)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::Result;
use crate::fs::InMemoryFs;
use crate::fs::{DirEntry, FileType, FsBackend};
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
struct AppendCreatesFileBackend {
files: Mutex<HashSet<PathBuf>>,
}
impl AppendCreatesFileBackend {
fn new() -> Self {
let mut files = HashSet::new();
files.insert(PathBuf::from("/"));
files.insert(PathBuf::from("/tmp"));
Self {
files: Mutex::new(files),
}
}
}
#[async_trait]
impl FsBackend for AppendCreatesFileBackend {
async fn read(&self, _path: &Path) -> Result<Vec<u8>> {
Ok(Vec::new())
}
async fn write(&self, path: &Path, _content: &[u8]) -> Result<()> {
self.files
.lock()
.expect("backend lock poisoned")
.insert(path.to_path_buf());
Ok(())
}
async fn append(&self, path: &Path, content: &[u8]) -> Result<()> {
self.write(path, content).await
}
async fn mkdir(&self, path: &Path, _recursive: bool) -> Result<()> {
self.files
.lock()
.expect("backend lock poisoned")
.insert(path.to_path_buf());
Ok(())
}
async fn remove(&self, _path: &Path, _recursive: bool) -> Result<()> {
Ok(())
}
async fn stat(&self, path: &Path) -> Result<Metadata> {
if self
.files
.lock()
.expect("backend lock poisoned")
.contains(path)
{
Ok(Metadata {
file_type: FileType::File,
..Metadata::default()
})
} else {
Err(std::io::Error::from(std::io::ErrorKind::NotFound).into())
}
}
async fn read_dir(&self, _path: &Path) -> Result<Vec<DirEntry>> {
Ok(Vec::new())
}
async fn exists(&self, path: &Path) -> Result<bool> {
Ok(self
.files
.lock()
.expect("backend lock poisoned")
.contains(path))
}
async fn rename(&self, _from: &Path, _to: &Path) -> Result<()> {
Ok(())
}
async fn copy(&self, _from: &Path, _to: &Path) -> Result<()> {
Ok(())
}
async fn symlink(&self, _target: &Path, _link: &Path) -> Result<()> {
Ok(())
}
async fn read_link(&self, _path: &Path) -> Result<PathBuf> {
Err(std::io::Error::from(std::io::ErrorKind::NotFound).into())
}
async fn chmod(&self, _path: &Path, _mode: u32) -> Result<()> {
Ok(())
}
}
#[tokio::test]
async fn test_posix_write_to_directory_fails() {
let fs = InMemoryFs::new();
fs.mkdir(Path::new("/tmp/testdir"), false)
.await
.expect("mkdir should succeed");
let result = fs.write_file(Path::new("/tmp/testdir"), b"test").await;
assert!(result.is_err());
assert!(
result
.expect_err("write_file should fail")
.to_string()
.contains("directory")
);
}
#[tokio::test]
async fn test_posix_mkdir_on_file_fails() {
let fs = InMemoryFs::new();
fs.write_file(Path::new("/tmp/testfile"), b"test")
.await
.expect("write_file should succeed");
let result = fs.mkdir(Path::new("/tmp/testfile"), false).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_posix_normalize_dot_slash_prefix() {
let fs = InMemoryFs::new();
fs.mkdir(Path::new("/tmp/dir"), true).await.unwrap();
fs.write_file(Path::new("/tmp/dir/file.txt"), b"content")
.await
.unwrap();
let dot_path = Path::new("/tmp/dir/./file.txt");
assert!(
fs.exists(dot_path).await.unwrap(),
"exists with ./ should work"
);
let content = fs.read_file(dot_path).await.unwrap();
assert_eq!(content, b"content");
let meta = fs.stat(dot_path).await;
assert!(meta.is_ok(), "stat with ./ should work");
fs.write_file(Path::new("/tmp/dir/./new.txt"), b"new")
.await
.unwrap();
let content = fs.read_file(Path::new("/tmp/dir/new.txt")).await.unwrap();
assert_eq!(content, b"new");
}
#[tokio::test]
async fn test_posix_normalize_preserves_semantics() {
let fs = InMemoryFs::new();
let result = fs
.write_file(Path::new("/tmp/nonexistent/./file.txt"), b"content")
.await;
assert!(result.is_err(), "should fail when parent doesn't exist");
}
#[tokio::test]
async fn test_posix_append_requires_parent_directory() {
let fs = PosixFs::new(AppendCreatesFileBackend::new());
let result = fs
.append_file(Path::new("/tmp/missing-parent/file.txt"), b"content")
.await;
assert!(
result.is_err(),
"append should fail when parent doesn't exist"
);
}
}