xtask_todo_lib/devshell/workspace/
backend.rs1use std::cell::RefCell;
6use std::process::ExitStatus;
7use std::rc::Rc;
8
9use crate::devshell::vfs::{resolve_path_with_cwd, Vfs, VfsError};
10use crate::devshell::vm::{
11 guest_path_is_under_mount, guest_project_dir_on_guest, normalize_guest_path, GuestFsError,
12 GuestFsOps, SessionHolder,
13};
14
15#[derive(Debug)]
17pub enum WorkspaceBackendError {
18 Vfs(VfsError),
19 Guest(GuestFsError),
20 Vm(crate::devshell::vm::VmError),
21 PathOutsideWorkspace,
23 ModeSOnly,
25 Unsupported(&'static str),
27}
28
29impl std::fmt::Display for WorkspaceBackendError {
30 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31 match self {
32 Self::Vfs(e) => write!(f, "{e}"),
33 Self::Guest(e) => write!(f, "{e}"),
34 Self::Vm(e) => write!(f, "{e}"),
35 Self::PathOutsideWorkspace => f.write_str("path outside workspace cwd"),
36 Self::ModeSOnly => f.write_str("guest path resolution not available in Mode S"),
37 Self::Unsupported(msg) => write!(f, "unsupported: {msg}"),
38 }
39 }
40}
41
42impl std::error::Error for WorkspaceBackendError {
43 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
44 match self {
45 Self::Vfs(e) => Some(e),
46 Self::Guest(e) => Some(e),
47 Self::Vm(e) => Some(e),
48 _ => None,
49 }
50 }
51}
52
53impl From<VfsError> for WorkspaceBackendError {
54 fn from(e: VfsError) -> Self {
55 Self::Vfs(e)
56 }
57}
58
59impl From<GuestFsError> for WorkspaceBackendError {
60 fn from(e: GuestFsError) -> Self {
61 Self::Guest(e)
62 }
63}
64
65impl From<crate::devshell::vm::VmError> for WorkspaceBackendError {
66 fn from(e: crate::devshell::vm::VmError) -> Self {
67 Self::Vm(e)
68 }
69}
70
71pub fn logical_path_to_guest(
77 guest_mount: &str,
78 logical_cwd: &str,
79 logical_path: &str,
80) -> Result<String, WorkspaceBackendError> {
81 let abs_cwd = resolve_path_with_cwd("/", logical_cwd);
82 let abs_path = resolve_path_with_cwd(logical_cwd, logical_path);
83 let prefix = if abs_cwd.ends_with('/') {
84 abs_cwd.clone()
85 } else {
86 format!("{abs_cwd}/")
87 };
88 if abs_path != abs_cwd && !abs_path.starts_with(&prefix) {
89 return Err(WorkspaceBackendError::PathOutsideWorkspace);
90 }
91 let rel = if abs_path == abs_cwd {
92 ""
93 } else {
94 &abs_path[prefix.len()..]
95 };
96 let guest_root = guest_project_dir_on_guest(guest_mount, logical_cwd);
97 if !guest_path_is_under_mount(guest_mount, &guest_root) {
98 return Err(WorkspaceBackendError::PathOutsideWorkspace);
99 }
100 let guest_path = if rel.is_empty() {
101 guest_root
102 } else {
103 format!("{guest_root}/{rel}")
104 };
105 let guest_path =
106 normalize_guest_path(&guest_path).ok_or(WorkspaceBackendError::PathOutsideWorkspace)?;
107 if !guest_path_is_under_mount(guest_mount, &guest_path) {
108 return Err(WorkspaceBackendError::PathOutsideWorkspace);
109 }
110 Ok(guest_path)
111}
112
113pub trait WorkspaceBackend {
115 fn logical_cwd(&self) -> String;
116 fn set_logical_cwd(&mut self, path: &str) -> Result<(), WorkspaceBackendError>;
119 fn read_file(&mut self, path: &str) -> Result<Vec<u8>, WorkspaceBackendError>;
122 fn write_file(&mut self, path: &str, data: &[u8]) -> Result<(), WorkspaceBackendError>;
125 fn list_dir(&mut self, path: &str) -> Result<Vec<String>, WorkspaceBackendError>;
128 fn mkdir(&mut self, path: &str) -> Result<(), WorkspaceBackendError>;
131 fn remove(&mut self, path: &str) -> Result<(), WorkspaceBackendError>;
134 fn exists(&mut self, path: &str) -> Result<bool, WorkspaceBackendError>;
137
138 fn try_resolve_guest_path(&self, logical_path: &str) -> Result<String, WorkspaceBackendError>;
143
144 fn run_rust_tool(
149 &mut self,
150 vm_session: &mut SessionHolder,
151 program: &str,
152 args: &[String],
153 ) -> Result<ExitStatus, WorkspaceBackendError>;
154}
155
156pub struct MemoryVfsBackend {
158 vfs: Rc<RefCell<Vfs>>,
159}
160
161impl MemoryVfsBackend {
162 #[must_use]
163 pub const fn new(vfs: Rc<RefCell<Vfs>>) -> Self {
164 Self { vfs }
165 }
166}
167
168impl WorkspaceBackend for MemoryVfsBackend {
169 fn logical_cwd(&self) -> String {
170 self.vfs.borrow().cwd().to_string()
171 }
172
173 fn set_logical_cwd(&mut self, path: &str) -> Result<(), WorkspaceBackendError> {
174 self.vfs.borrow_mut().set_cwd(path)?;
175 Ok(())
176 }
177
178 fn read_file(&mut self, path: &str) -> Result<Vec<u8>, WorkspaceBackendError> {
179 Ok(self.vfs.borrow().read_file(path)?)
180 }
181
182 fn write_file(&mut self, path: &str, data: &[u8]) -> Result<(), WorkspaceBackendError> {
183 self.vfs.borrow_mut().write_file(path, data)?;
184 Ok(())
185 }
186
187 fn list_dir(&mut self, path: &str) -> Result<Vec<String>, WorkspaceBackendError> {
188 Ok(self.vfs.borrow().list_dir(path)?)
189 }
190
191 fn mkdir(&mut self, path: &str) -> Result<(), WorkspaceBackendError> {
192 self.vfs.borrow_mut().mkdir(path)?;
193 Ok(())
194 }
195
196 fn remove(&mut self, _path: &str) -> Result<(), WorkspaceBackendError> {
197 Err(WorkspaceBackendError::Unsupported(
198 "MemoryVfsBackend::remove — add Vfs::remove or use dispatch path",
199 ))
200 }
201
202 fn exists(&mut self, path: &str) -> Result<bool, WorkspaceBackendError> {
203 let vfs = self.vfs.borrow();
204 let abs = resolve_path_with_cwd(vfs.cwd(), path);
205 Ok(vfs.resolve_absolute(&abs).is_ok())
206 }
207
208 fn try_resolve_guest_path(&self, _logical_path: &str) -> Result<String, WorkspaceBackendError> {
209 Err(WorkspaceBackendError::ModeSOnly)
210 }
211
212 fn run_rust_tool(
213 &mut self,
214 vm_session: &mut SessionHolder,
215 program: &str,
216 args: &[String],
217 ) -> Result<ExitStatus, WorkspaceBackendError> {
218 let vfs_cwd = self.vfs.borrow().cwd().to_string();
219 let mut vfs = self.vfs.borrow_mut();
220 Ok(vm_session.run_rust_tool(&mut vfs, &vfs_cwd, program, args)?)
221 }
222}
223
224pub struct GuestPrimaryBackend {
226 ops: Box<dyn GuestFsOps>,
227 guest_mount: String,
228 logical_cwd: String,
229}
230
231impl GuestPrimaryBackend {
232 #[must_use]
233 pub fn new(guest_mount: String, logical_cwd: String, ops: Box<dyn GuestFsOps>) -> Self {
234 Self {
235 ops,
236 guest_mount,
237 logical_cwd,
238 }
239 }
240
241 #[must_use]
242 pub fn guest_mount(&self) -> &str {
243 &self.guest_mount
244 }
245}
246
247impl WorkspaceBackend for GuestPrimaryBackend {
248 fn logical_cwd(&self) -> String {
249 self.logical_cwd.clone()
250 }
251
252 fn set_logical_cwd(&mut self, path: &str) -> Result<(), WorkspaceBackendError> {
253 self.logical_cwd = resolve_path_with_cwd(&self.logical_cwd, path);
254 Ok(())
255 }
256
257 fn read_file(&mut self, path: &str) -> Result<Vec<u8>, WorkspaceBackendError> {
258 let g = logical_path_to_guest(&self.guest_mount, &self.logical_cwd, path)?;
259 Ok(self.ops.read_file(&g)?)
260 }
261
262 fn write_file(&mut self, path: &str, data: &[u8]) -> Result<(), WorkspaceBackendError> {
263 let g = logical_path_to_guest(&self.guest_mount, &self.logical_cwd, path)?;
264 Ok(self.ops.write_file(&g, data)?)
265 }
266
267 fn list_dir(&mut self, path: &str) -> Result<Vec<String>, WorkspaceBackendError> {
268 let g = logical_path_to_guest(&self.guest_mount, &self.logical_cwd, path)?;
269 Ok(self.ops.list_dir(&g)?)
270 }
271
272 fn mkdir(&mut self, path: &str) -> Result<(), WorkspaceBackendError> {
273 let g = logical_path_to_guest(&self.guest_mount, &self.logical_cwd, path)?;
274 Ok(self.ops.mkdir(&g)?)
275 }
276
277 fn remove(&mut self, path: &str) -> Result<(), WorkspaceBackendError> {
278 let g = logical_path_to_guest(&self.guest_mount, &self.logical_cwd, path)?;
279 Ok(self.ops.remove(&g)?)
280 }
281
282 fn exists(&mut self, path: &str) -> Result<bool, WorkspaceBackendError> {
283 let g = logical_path_to_guest(&self.guest_mount, &self.logical_cwd, path)?;
284 match self.ops.read_file(&g) {
285 Ok(_) | Err(GuestFsError::IsADirectory(_)) => Ok(true),
286 Err(GuestFsError::NotFound(_)) => match self.ops.list_dir(&g) {
287 Ok(_) => Ok(true),
288 Err(GuestFsError::NotFound(_) | GuestFsError::NotADirectory(_)) => Ok(false),
289 Err(e) => Err(WorkspaceBackendError::Guest(e)),
290 },
291 Err(e) => Err(WorkspaceBackendError::Guest(e)),
292 }
293 }
294
295 fn try_resolve_guest_path(&self, logical_path: &str) -> Result<String, WorkspaceBackendError> {
296 logical_path_to_guest(&self.guest_mount, &self.logical_cwd, logical_path)
297 }
298
299 fn run_rust_tool(
300 &mut self,
301 _vm_session: &mut SessionHolder,
302 _program: &str,
303 _args: &[String],
304 ) -> Result<ExitStatus, WorkspaceBackendError> {
305 Err(WorkspaceBackendError::Unsupported(
306 "GuestPrimaryBackend::run_rust_tool (Sprint 3: push/pull skip)",
307 ))
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314 use crate::devshell::vm::MockGuestFsOps;
315
316 #[test]
317 fn logical_path_to_guest_under_cwd() {
318 let g = logical_path_to_guest("/workspace", "/projects/hello", "/projects/hello/src/a.rs")
319 .unwrap();
320 assert_eq!(g, "/workspace/hello/src/a.rs");
321 }
322
323 #[test]
324 fn logical_path_to_guest_rejects_escape() {
325 assert!(logical_path_to_guest("/workspace", "/projects/hello", "/etc/passwd").is_err());
326 }
327
328 #[test]
329 fn memory_backend_roundtrip() {
330 let vfs = Rc::new(RefCell::new(Vfs::new()));
331 let mut b = MemoryVfsBackend::new(Rc::clone(&vfs));
332 b.mkdir("/a").unwrap();
333 b.write_file("/a/f", b"x").unwrap();
334 assert_eq!(b.read_file("/a/f").unwrap(), b"x");
335 assert!(b.exists("/a/f").unwrap());
336 assert!(b.try_resolve_guest_path("/a/f").is_err());
337 }
338
339 #[test]
340 fn guest_primary_backend_mock_resolves_and_writes() {
341 let mut b = GuestPrimaryBackend::new(
342 "/workspace".to_string(),
343 "/projects/foo".to_string(),
344 Box::new(MockGuestFsOps::new()),
345 );
346 b.write_file("/projects/foo/x.txt", b"hi").unwrap();
347 assert_eq!(b.read_file("/projects/foo/x.txt").unwrap(), b"hi");
348 assert_eq!(
349 b.try_resolve_guest_path("/projects/foo/x.txt").unwrap(),
350 "/workspace/foo/x.txt"
351 );
352 }
353}