origin_platform/
workspace.rs1use async_trait::async_trait;
9use origin_domain::{AppError, Result};
10use std::fmt::Debug;
11use std::path::{Component, Path, PathBuf};
12
13#[derive(Debug, Clone, PartialEq, Eq, Hash)]
17pub struct WorkspaceRoot(PathBuf);
18
19impl WorkspaceRoot {
20 pub fn new(path: PathBuf) -> Result<Self> {
21 if !path.is_absolute() {
22 return Err(AppError::validation(
23 "workspace root must be an absolute path",
24 ));
25 }
26 Ok(Self(path))
27 }
28
29 pub fn as_path(&self) -> &Path {
30 &self.0
31 }
32
33 pub fn into_inner(self) -> PathBuf {
34 self.0
35 }
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Hash)]
45pub struct RelPath(PathBuf);
46
47impl RelPath {
48 pub fn new(path: impl AsRef<Path>) -> Result<Self> {
49 let path = path.as_ref();
50 if path.is_absolute() {
51 return Err(AppError::validation("a relative path must not be absolute"));
52 }
53
54 for component in path.components() {
55 match component {
56 Component::Normal(_) | Component::CurDir => {}
57 _ => {
58 return Err(AppError::validation(format!(
59 "a relative path must not contain `..`: `{}`",
60 path.display()
61 )));
62 }
63 }
64 }
65
66 Ok(Self(path.to_path_buf()))
67 }
68
69 pub fn as_path(&self) -> &Path {
70 &self.0
71 }
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct DirEntry {
77 pub name: String,
78 pub is_dir: bool,
79}
80
81impl DirEntry {
82 pub fn file(name: impl Into<String>) -> Self {
83 Self {
84 name: name.into(),
85 is_dir: false,
86 }
87 }
88
89 pub fn directory(name: impl Into<String>) -> Self {
90 Self {
91 name: name.into(),
92 is_dir: true,
93 }
94 }
95}
96
97#[async_trait]
103pub trait WorkspaceFs: Debug + Send + Sync + 'static {
104 async fn list_dir(&self, root: &WorkspaceRoot, path: &RelPath) -> Result<Vec<DirEntry>>;
106
107 async fn read_file(&self, root: &WorkspaceRoot, path: &RelPath) -> Result<Vec<u8>>;
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn workspace_root_rejects_relative_paths() {
117 let result = WorkspaceRoot::new(PathBuf::from("relative/path"));
118 match result {
119 Err(AppError::Validation(_)) => {} other => panic!("expected Validation error for relative path, got {other:?}"),
121 }
122 }
123
124 #[test]
125 fn workspace_root_accepts_absolute_paths() {
126 WorkspaceRoot::new(PathBuf::from("/Users/test/repo")).expect("absolute path must be ok");
127 }
128
129 #[test]
130 fn rel_path_rejects_absolute_paths() {
131 let result = RelPath::new("/absolute/file");
132 match result {
133 Err(AppError::Validation(_)) => {} other => panic!("expected Validation error for absolute rel path, got {other:?}"),
135 }
136 }
137
138 #[test]
139 fn rel_path_rejects_parent_traversal() {
140 let result = RelPath::new("../escape");
141 match result {
142 Err(AppError::Validation(_)) => {} other => panic!("expected Validation error for parent traversal, got {other:?}"),
144 }
145 }
146
147 #[test]
148 fn rel_path_accepts_normal_relative_paths() {
149 let rel = RelPath::new("src/main.rs").expect("normal relative path must be ok");
150 assert_eq!(rel.as_path(), Path::new("src/main.rs"));
151 }
152
153 #[test]
154 fn rel_path_accepts_current_dir_prefix() {
155 let rel = RelPath::new("./file.txt").expect("./ prefix must be ok");
156 assert_eq!(rel.as_path(), Path::new("./file.txt"));
157 }
158}