use crate::ipc::GrepMatch;
use async_trait::async_trait;
use std::path::{Path, PathBuf};
pub mod local;
#[cfg(unix)]
pub mod sandboxed;
pub use local::LocalFileSystem;
#[cfg(unix)]
pub use sandboxed::SandboxedFileSystem;
#[async_trait]
pub trait FileSystem: Send + Sync {
async fn read(&self, path: &Path, max_bytes: Option<usize>) -> FsResult<Vec<u8>>;
async fn write(&self, path: &Path, content: &[u8]) -> FsResult<usize>;
async fn edit(
&self,
path: &Path,
old_string: &str,
new_string: &str,
all: bool,
) -> FsResult<usize>;
async fn glob(&self, pattern: &str, root: &Path) -> FsResult<Vec<PathBuf>>;
async fn grep(
&self,
pattern: &str,
root: &Path,
include: Option<&str>,
) -> FsResult<Vec<GrepMatch>>;
async fn stat(&self, path: &Path) -> FsResult<Metadata>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Metadata {
pub size: u64,
pub is_dir: bool,
pub is_symlink: bool,
}
pub type FsResult<T> = Result<T, FsError>;
#[derive(Debug)]
pub enum FsError {
Io(std::io::Error),
PolicyDenied {
message: String,
},
EditNotFound {
path: PathBuf,
},
InvalidPattern {
message: String,
},
Transport {
message: String,
},
}
impl std::fmt::Display for FsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FsError::Io(e) => write!(f, "io error: {e}"),
FsError::PolicyDenied { message } => write!(f, "policy denied: {message}"),
FsError::EditNotFound { path } => {
write!(f, "old_string not found in {}", path.display())
}
FsError::InvalidPattern { message } => write!(f, "invalid pattern: {message}"),
FsError::Transport { message } => write!(f, "fs worker transport: {message}"),
}
}
}
impl std::error::Error for FsError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
FsError::Io(e) => Some(e),
_ => None,
}
}
}
impl From<std::io::Error> for FsError {
fn from(e: std::io::Error) -> Self {
FsError::Io(e)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::error::Error as _;
#[test]
fn display_io() {
let e = FsError::Io(std::io::Error::new(std::io::ErrorKind::NotFound, "nope"));
assert_eq!(e.to_string(), "io error: nope");
}
#[test]
fn display_policy_denied() {
let e = FsError::PolicyDenied {
message: "outside write root".into(),
};
assert_eq!(e.to_string(), "policy denied: outside write root");
}
#[test]
fn display_edit_not_found() {
let e = FsError::EditNotFound {
path: PathBuf::from("/tmp/foo.rs"),
};
assert_eq!(e.to_string(), "old_string not found in /tmp/foo.rs");
}
#[test]
fn display_invalid_pattern() {
let e = FsError::InvalidPattern {
message: "unclosed bracket".into(),
};
assert_eq!(e.to_string(), "invalid pattern: unclosed bracket");
}
#[test]
fn display_transport() {
let e = FsError::Transport {
message: "broken pipe".into(),
};
assert_eq!(e.to_string(), "fs worker transport: broken pipe");
}
#[test]
fn source_io_returns_some() {
let inner = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied");
let e = FsError::Io(inner);
assert!(e.source().is_some());
}
#[test]
fn source_non_io_variants_return_none() {
let variants: &[FsError] = &[
FsError::PolicyDenied {
message: "x".into(),
},
FsError::EditNotFound {
path: PathBuf::from("/x"),
},
FsError::InvalidPattern {
message: "x".into(),
},
FsError::Transport {
message: "x".into(),
},
];
for v in variants {
assert!(v.source().is_none(), "{v} should have no source");
}
}
#[test]
fn from_io_error_wraps_in_io_variant() {
let io_err = std::io::Error::new(std::io::ErrorKind::TimedOut, "timed out");
let fs_err: FsError = io_err.into();
assert!(fs_err.to_string().contains("timed out"), "got: {fs_err}");
assert!(fs_err.source().is_some(), "Io variant must chain source");
}
}