use async_trait::async_trait;
use origin_domain::{AppError, Result};
use std::fmt::Debug;
use std::path::{Component, Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct WorkspaceRoot(PathBuf);
impl WorkspaceRoot {
pub fn new(path: PathBuf) -> Result<Self> {
if !path.is_absolute() {
return Err(AppError::validation(
"workspace root must be an absolute path",
));
}
Ok(Self(path))
}
pub fn as_path(&self) -> &Path {
&self.0
}
pub fn into_inner(self) -> PathBuf {
self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct RelPath(PathBuf);
impl RelPath {
pub fn new(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref();
if path.is_absolute() {
return Err(AppError::validation("a relative path must not be absolute"));
}
for component in path.components() {
match component {
Component::Normal(_) | Component::CurDir => {}
_ => {
return Err(AppError::validation(format!(
"a relative path must not contain `..`: `{}`",
path.display()
)));
}
}
}
Ok(Self(path.to_path_buf()))
}
pub fn as_path(&self) -> &Path {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DirEntry {
pub name: String,
pub is_dir: bool,
}
impl DirEntry {
pub fn file(name: impl Into<String>) -> Self {
Self {
name: name.into(),
is_dir: false,
}
}
pub fn directory(name: impl Into<String>) -> Self {
Self {
name: name.into(),
is_dir: true,
}
}
}
#[async_trait]
pub trait WorkspaceFs: Debug + Send + Sync + 'static {
async fn list_dir(&self, root: &WorkspaceRoot, path: &RelPath) -> Result<Vec<DirEntry>>;
async fn read_file(&self, root: &WorkspaceRoot, path: &RelPath) -> Result<Vec<u8>>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn workspace_root_rejects_relative_paths() {
let result = WorkspaceRoot::new(PathBuf::from("relative/path"));
match result {
Err(AppError::Validation(_)) => {} other => panic!("expected Validation error for relative path, got {other:?}"),
}
}
#[test]
fn workspace_root_accepts_absolute_paths() {
WorkspaceRoot::new(PathBuf::from("/Users/test/repo")).expect("absolute path must be ok");
}
#[test]
fn rel_path_rejects_absolute_paths() {
let result = RelPath::new("/absolute/file");
match result {
Err(AppError::Validation(_)) => {} other => panic!("expected Validation error for absolute rel path, got {other:?}"),
}
}
#[test]
fn rel_path_rejects_parent_traversal() {
let result = RelPath::new("../escape");
match result {
Err(AppError::Validation(_)) => {} other => panic!("expected Validation error for parent traversal, got {other:?}"),
}
}
#[test]
fn rel_path_accepts_normal_relative_paths() {
let rel = RelPath::new("src/main.rs").expect("normal relative path must be ok");
assert_eq!(rel.as_path(), Path::new("src/main.rs"));
}
#[test]
fn rel_path_accepts_current_dir_prefix() {
let rel = RelPath::new("./file.txt").expect("./ prefix must be ok");
assert_eq!(rel.as_path(), Path::new("./file.txt"));
}
}