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