1use crate::{DirEntry, FileHandle, FsFileType, FsMetadata, FsProvider, OpenFlags};
2use async_trait::async_trait;
3use std::{
4 collections::BTreeMap,
5 ffi::OsString,
6 io::{self, Cursor, ErrorKind, Read, Seek, SeekFrom, Write},
7 path::{Component, Path, PathBuf},
8 sync::{Arc, Mutex},
9};
10
11#[derive(Clone, Debug)]
12pub struct MemoryFsProvider {
13 default_current_dir: PathBuf,
14 inner: Arc<Mutex<MemoryTree>>,
15}
16
17#[derive(Clone, Debug)]
18enum MemoryEntry {
19 Directory,
20 File { bytes: Vec<u8>, readonly: bool },
21}
22
23#[derive(Debug)]
24struct MemoryTree {
25 entries: BTreeMap<PathBuf, MemoryEntry>,
26}
27
28impl Default for MemoryFsProvider {
29 fn default() -> Self {
30 Self::new()
31 }
32}
33
34impl MemoryFsProvider {
35 pub fn new() -> Self {
36 Self::with_current_dir(PathBuf::from("/"))
37 }
38
39 pub fn with_current_dir(default_current_dir: impl Into<PathBuf>) -> Self {
40 let default_current_dir =
41 normalize_path(&default_current_dir.into()).unwrap_or_else(|_| PathBuf::from("/"));
42 let mut entries = BTreeMap::new();
43 entries.insert(PathBuf::from("/"), MemoryEntry::Directory);
44 let provider = Self {
45 default_current_dir,
46 inner: Arc::new(Mutex::new(MemoryTree { entries })),
47 };
48 let _ = provider.create_dir_all_sync(&provider.default_current_dir);
49 provider
50 }
51
52 pub fn reset(&self) {
53 let mut guard = self.inner.lock().expect("memory filesystem lock poisoned");
54 guard.entries.clear();
55 guard
56 .entries
57 .insert(PathBuf::from("/"), MemoryEntry::Directory);
58 ensure_dirs(&mut guard, &self.default_current_dir).expect("default cwd must be valid");
59 }
60
61 pub fn read_project_path(&self, path: impl AsRef<Path>) -> io::Result<Vec<u8>> {
62 let path = normalize_path(path.as_ref())?;
63 let guard = self.inner.lock().expect("memory filesystem lock poisoned");
64 match guard.entries.get(&path) {
65 Some(MemoryEntry::File { bytes, .. }) => Ok(bytes.clone()),
66 _ => Err(not_found(&path)),
67 }
68 }
69
70 pub fn write_project_path(&self, path: impl AsRef<Path>, data: &[u8]) -> io::Result<()> {
71 let path = normalize_path(path.as_ref())?;
72 let mut guard = self.inner.lock().expect("memory filesystem lock poisoned");
73 match guard.entries.get(&path) {
74 Some(MemoryEntry::Directory) => {
75 return Err(io::Error::new(
76 ErrorKind::InvalidInput,
77 format!("not a file: {}", path.display()),
78 ));
79 }
80 Some(MemoryEntry::File { readonly: true, .. }) => {
81 return Err(io::Error::new(
82 ErrorKind::PermissionDenied,
83 format!("file is readonly: {}", path.display()),
84 ));
85 }
86 Some(MemoryEntry::File { .. }) | None => {}
87 }
88 ensure_parent_dirs(&mut guard, &path)?;
89 guard.entries.insert(
90 path,
91 MemoryEntry::File {
92 bytes: data.to_vec(),
93 readonly: false,
94 },
95 );
96 Ok(())
97 }
98
99 pub fn list_project_path(&self, path: impl AsRef<Path>) -> io::Result<Vec<DirEntry>> {
100 let path = normalize_path(path.as_ref())?;
101 let guard = self.inner.lock().expect("memory filesystem lock poisoned");
102 match guard.entries.get(&path) {
103 Some(MemoryEntry::Directory) => {}
104 Some(MemoryEntry::File { .. }) => {
105 return Err(io::Error::new(
106 ErrorKind::InvalidInput,
107 format!("not a directory: {}", path.display()),
108 ));
109 }
110 None => return Err(not_found(&path)),
111 }
112 let mut entries = Vec::new();
113 for (candidate, entry) in guard.entries.iter() {
114 if candidate == &path || !is_direct_child(&path, candidate) {
115 continue;
116 }
117 entries.push(DirEntry::new(
118 candidate.clone(),
119 candidate
120 .file_name()
121 .map(OsString::from)
122 .unwrap_or_else(|| OsString::from("")),
123 entry_type(entry),
124 ));
125 }
126 Ok(entries)
127 }
128
129 pub fn metadata_project_path(&self, path: impl AsRef<Path>) -> io::Result<FsMetadata> {
130 let path = normalize_path(path.as_ref())?;
131 let guard = self.inner.lock().expect("memory filesystem lock poisoned");
132 metadata_for(&path, guard.entries.get(&path))
133 }
134
135 pub fn create_dir_project_path(
136 &self,
137 path: impl AsRef<Path>,
138 recursive: bool,
139 ) -> io::Result<()> {
140 let path = normalize_path(path.as_ref())?;
141 if recursive {
142 self.create_dir_all_sync(&path)
143 } else {
144 let mut guard = self.inner.lock().expect("memory filesystem lock poisoned");
145 ensure_parent_dir(&guard, &path)?;
146 match guard.entries.get(&path) {
147 Some(MemoryEntry::Directory) | Some(MemoryEntry::File { .. }) => {
148 Err(io::Error::new(
149 ErrorKind::AlreadyExists,
150 format!("entry already exists: {}", path.display()),
151 ))
152 }
153 None => {
154 guard.entries.insert(path, MemoryEntry::Directory);
155 Ok(())
156 }
157 }
158 }
159 }
160
161 pub fn remove_project_path(&self, path: impl AsRef<Path>, recursive: bool) -> io::Result<()> {
162 let path = normalize_path(path.as_ref())?;
163 if path == Path::new("/") {
164 return Err(io::Error::new(
165 ErrorKind::InvalidInput,
166 "cannot remove memory filesystem root",
167 ));
168 }
169 let mut guard = self.inner.lock().expect("memory filesystem lock poisoned");
170 match guard.entries.get(&path) {
171 Some(MemoryEntry::File { .. }) => {
172 guard.entries.remove(&path);
173 }
174 Some(MemoryEntry::Directory) => {
175 let children = guard
176 .entries
177 .keys()
178 .filter(|candidate| is_descendant(&path, candidate))
179 .cloned()
180 .collect::<Vec<_>>();
181 if !children.is_empty() && !recursive {
182 return Err(io::Error::new(
183 ErrorKind::DirectoryNotEmpty,
184 format!("directory is not empty: {}", path.display()),
185 ));
186 }
187 for child in children {
188 guard.entries.remove(&child);
189 }
190 guard.entries.remove(&path);
191 }
192 None => return Err(not_found(&path)),
193 }
194 Ok(())
195 }
196
197 pub fn rename_project_path(
198 &self,
199 from: impl AsRef<Path>,
200 to: impl AsRef<Path>,
201 ) -> io::Result<()> {
202 let from = normalize_path(from.as_ref())?;
203 let to = normalize_path(to.as_ref())?;
204 if from == Path::new("/") {
205 return Err(io::Error::new(
206 ErrorKind::InvalidInput,
207 "cannot rename memory filesystem root",
208 ));
209 }
210 let mut guard = self.inner.lock().expect("memory filesystem lock poisoned");
211 let entry = guard
212 .entries
213 .get(&from)
214 .cloned()
215 .ok_or_else(|| not_found(&from))?;
216 ensure_parent_dirs(&mut guard, &to)?;
217 if matches!(entry, MemoryEntry::Directory) {
218 let descendants = guard
219 .entries
220 .iter()
221 .filter(|(candidate, _)| is_descendant(&from, candidate))
222 .map(|(candidate, entry)| (candidate.clone(), entry.clone()))
223 .collect::<Vec<_>>();
224 guard.entries.insert(to.clone(), entry);
225 for (candidate, child) in descendants.iter() {
226 let suffix = candidate.strip_prefix(&from).unwrap_or(Path::new(""));
227 guard.entries.insert(to.join(suffix), child.clone());
228 }
229 for (candidate, _) in descendants {
230 guard.entries.remove(&candidate);
231 }
232 guard.entries.remove(&from);
233 } else {
234 guard.entries.insert(to, entry);
235 guard.entries.remove(&from);
236 }
237 Ok(())
238 }
239
240 fn create_dir_all_sync(&self, path: &Path) -> io::Result<()> {
241 let path = normalize_path(path)?;
242 let mut guard = self.inner.lock().expect("memory filesystem lock poisoned");
243 ensure_dirs(&mut guard, &path)
244 }
245}
246
247#[async_trait(?Send)]
248impl FsProvider for MemoryFsProvider {
249 fn current_dir_override(&self) -> Option<PathBuf> {
250 Some(self.default_current_dir.clone())
251 }
252
253 fn open(&self, path: &Path, flags: &OpenFlags) -> io::Result<Box<dyn FileHandle>> {
254 let path = normalize_path(path)?;
255 let existing_entry = {
256 let guard = self.inner.lock().expect("memory filesystem lock poisoned");
257 guard.entries.get(&path).cloned()
258 };
259 if flags.create_new && existing_entry.is_some() {
260 return Err(io::Error::new(
261 ErrorKind::AlreadyExists,
262 format!("entry already exists: {}", path.display()),
263 ));
264 }
265 let existing = match existing_entry {
266 Some(MemoryEntry::Directory) => {
267 return Err(io::Error::new(
268 ErrorKind::InvalidInput,
269 format!("not a file: {}", path.display()),
270 ));
271 }
272 Some(MemoryEntry::File { readonly: true, .. })
273 if flags.write || flags.append || flags.truncate =>
274 {
275 return Err(io::Error::new(
276 ErrorKind::PermissionDenied,
277 format!("file is readonly: {}", path.display()),
278 ));
279 }
280 Some(MemoryEntry::File { bytes, .. }) => Some(bytes),
281 None => None,
282 };
283 if existing.is_none() && !flags.create && !flags.create_new {
284 return Err(not_found(&path));
285 }
286 let bytes = if flags.truncate {
287 Vec::new()
288 } else {
289 existing.unwrap_or_default()
290 };
291 let mut cursor = Cursor::new(bytes);
292 if flags.append {
293 cursor.seek(SeekFrom::End(0))?;
294 }
295 Ok(Box::new(MemoryFileHandle {
296 provider: self.clone(),
297 path,
298 cursor,
299 writable: flags.write || flags.append || flags.truncate,
300 dirty: flags.truncate,
301 flushed: false,
302 }))
303 }
304
305 async fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
306 self.read_project_path(path)
307 }
308
309 async fn write(&self, path: &Path, data: &[u8]) -> io::Result<()> {
310 self.write_project_path(path, data)
311 }
312
313 async fn remove_file(&self, path: &Path) -> io::Result<()> {
314 let path = normalize_path(path)?;
315 let mut guard = self.inner.lock().expect("memory filesystem lock poisoned");
316 match guard.entries.get(&path) {
317 Some(MemoryEntry::File { .. }) => {
318 guard.entries.remove(&path);
319 Ok(())
320 }
321 Some(MemoryEntry::Directory) => Err(io::Error::new(
322 ErrorKind::InvalidInput,
323 format!("not a file: {}", path.display()),
324 )),
325 None => Err(not_found(&path)),
326 }
327 }
328
329 async fn metadata(&self, path: &Path) -> io::Result<FsMetadata> {
330 self.metadata_project_path(path)
331 }
332
333 async fn symlink_metadata(&self, path: &Path) -> io::Result<FsMetadata> {
334 self.metadata_project_path(path)
335 }
336
337 async fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>> {
338 self.list_project_path(path)
339 }
340
341 async fn canonicalize(&self, path: &Path) -> io::Result<PathBuf> {
342 let normalized = normalize_path(path)?;
343 self.metadata_project_path(&normalized)?;
344 Ok(normalized)
345 }
346
347 async fn create_dir(&self, path: &Path) -> io::Result<()> {
348 self.create_dir_project_path(path, false)
349 }
350
351 async fn create_dir_all(&self, path: &Path) -> io::Result<()> {
352 self.create_dir_project_path(path, true)
353 }
354
355 async fn remove_dir(&self, path: &Path) -> io::Result<()> {
356 let path = normalize_path(path)?;
357 {
358 let guard = self.inner.lock().expect("memory filesystem lock poisoned");
359 match guard.entries.get(&path) {
360 Some(MemoryEntry::Directory) => {}
361 Some(MemoryEntry::File { .. }) => {
362 return Err(io::Error::new(
363 ErrorKind::InvalidInput,
364 format!("not a directory: {}", path.display()),
365 ));
366 }
367 None => return Err(not_found(&path)),
368 }
369 }
370 self.remove_project_path(path, false)
371 }
372
373 async fn remove_dir_all(&self, path: &Path) -> io::Result<()> {
374 self.remove_project_path(path, true)
375 }
376
377 async fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
378 self.rename_project_path(from, to)
379 }
380
381 async fn set_readonly(&self, path: &Path, readonly: bool) -> io::Result<()> {
382 let path = normalize_path(path)?;
383 let mut guard = self.inner.lock().expect("memory filesystem lock poisoned");
384 match guard.entries.get_mut(&path) {
385 Some(MemoryEntry::File {
386 readonly: value, ..
387 }) => {
388 *value = readonly;
389 Ok(())
390 }
391 Some(MemoryEntry::Directory) => Ok(()),
392 None => Err(not_found(&path)),
393 }
394 }
395}
396
397struct MemoryFileHandle {
398 provider: MemoryFsProvider,
399 path: PathBuf,
400 cursor: Cursor<Vec<u8>>,
401 writable: bool,
402 dirty: bool,
403 flushed: bool,
404}
405
406impl MemoryFileHandle {
407 fn flush_to_provider(&mut self) -> io::Result<()> {
408 if !self.writable || !self.dirty || self.flushed {
409 return Ok(());
410 }
411 self.provider
412 .write_project_path(&self.path, self.cursor.get_ref())?;
413 self.flushed = true;
414 Ok(())
415 }
416}
417
418impl Read for MemoryFileHandle {
419 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
420 self.cursor.read(buf)
421 }
422}
423
424impl Write for MemoryFileHandle {
425 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
426 if !self.writable {
427 return Err(io::Error::new(
428 ErrorKind::PermissionDenied,
429 "file is not open for writing",
430 ));
431 }
432 self.dirty = true;
433 self.flushed = false;
434 self.cursor.write(buf)
435 }
436
437 fn flush(&mut self) -> io::Result<()> {
438 self.flush_to_provider()
439 }
440}
441
442impl Seek for MemoryFileHandle {
443 fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
444 self.cursor.seek(pos)
445 }
446}
447
448impl Drop for MemoryFileHandle {
449 fn drop(&mut self) {
450 let _ = self.flush_to_provider();
451 }
452}
453
454#[async_trait(?Send)]
455impl FileHandle for MemoryFileHandle {
456 async fn flush_async(&mut self) -> io::Result<()> {
457 self.flush_to_provider()
458 }
459
460 async fn sync_all_async(&mut self) -> io::Result<()> {
461 self.flush_to_provider()
462 }
463}
464
465fn normalize_path(path: &Path) -> io::Result<PathBuf> {
466 let mut parts = Vec::<OsString>::new();
467 for component in path.components() {
468 match component {
469 Component::Prefix(_) => {
470 return Err(io::Error::new(
471 ErrorKind::InvalidInput,
472 "path prefixes are not supported by memory filesystem",
473 ));
474 }
475 Component::RootDir => parts.clear(),
476 Component::CurDir => {}
477 Component::ParentDir => {
478 if parts.pop().is_none() {
479 return Err(io::Error::new(
480 ErrorKind::InvalidInput,
481 "parent directory traversal is not allowed",
482 ));
483 }
484 }
485 Component::Normal(part) => parts.push(part.to_os_string()),
486 }
487 }
488 let mut normalized = PathBuf::from("/");
489 for part in parts {
490 normalized.push(part);
491 }
492 Ok(normalized)
493}
494
495fn ensure_parent_dirs(tree: &mut MemoryTree, path: &Path) -> io::Result<()> {
496 let parent = path.parent().unwrap_or(Path::new("/"));
497 ensure_dirs(tree, parent)
498}
499
500fn ensure_parent_dir(tree: &MemoryTree, path: &Path) -> io::Result<()> {
501 let parent = path.parent().unwrap_or(Path::new("/"));
502 match tree.entries.get(parent) {
503 Some(MemoryEntry::Directory) => Ok(()),
504 Some(MemoryEntry::File { .. }) => Err(io::Error::new(
505 ErrorKind::InvalidInput,
506 format!("not a directory: {}", parent.display()),
507 )),
508 None => Err(not_found(parent)),
509 }
510}
511
512fn ensure_dirs(tree: &mut MemoryTree, path: &Path) -> io::Result<()> {
513 let normalized = normalize_path(path)?;
514 let mut current = PathBuf::from("/");
515 for part in normalized
516 .components()
517 .filter_map(|component| match component {
518 Component::Normal(part) => Some(part.to_os_string()),
519 _ => None,
520 })
521 {
522 current.push(part);
523 match tree.entries.get(¤t) {
524 Some(MemoryEntry::Directory) => {}
525 Some(MemoryEntry::File { .. }) => {
526 return Err(io::Error::new(
527 ErrorKind::InvalidInput,
528 format!("not a directory: {}", current.display()),
529 ));
530 }
531 None => {
532 tree.entries.insert(current.clone(), MemoryEntry::Directory);
533 }
534 }
535 }
536 Ok(())
537}
538
539fn is_direct_child(parent: &Path, candidate: &Path) -> bool {
540 if !is_descendant(parent, candidate) {
541 return false;
542 }
543 candidate
544 .strip_prefix(parent)
545 .ok()
546 .map(|relative| relative.components().count() == 1)
547 .unwrap_or(false)
548}
549
550fn is_descendant(parent: &Path, candidate: &Path) -> bool {
551 candidate != parent && candidate.starts_with(parent)
552}
553
554fn entry_type(entry: &MemoryEntry) -> FsFileType {
555 match entry {
556 MemoryEntry::Directory => FsFileType::Directory,
557 MemoryEntry::File { .. } => FsFileType::File,
558 }
559}
560
561fn metadata_for(path: &Path, entry: Option<&MemoryEntry>) -> io::Result<FsMetadata> {
562 match entry {
563 Some(MemoryEntry::Directory) => Ok(FsMetadata::new(FsFileType::Directory, 0, None, false)),
564 Some(MemoryEntry::File { bytes, readonly }) => Ok(FsMetadata::new(
565 FsFileType::File,
566 bytes.len() as u64,
567 None,
568 *readonly,
569 )),
570 None => Err(not_found(path)),
571 }
572}
573
574fn not_found(path: &Path) -> io::Error {
575 io::Error::new(ErrorKind::NotFound, path.display().to_string())
576}
577
578#[cfg(test)]
579mod tests {
580 use super::*;
581
582 #[tokio::test]
583 async fn memory_provider_supports_crud_and_dirs() {
584 let provider = MemoryFsProvider::new();
585 provider
586 .write(Path::new("/src/main.m"), b"disp('hi')")
587 .await
588 .unwrap();
589 assert_eq!(
590 provider.read(Path::new("src/main.m")).await.unwrap(),
591 b"disp('hi')"
592 );
593 let entries = provider.read_dir(Path::new("/src")).await.unwrap();
594 assert_eq!(entries.len(), 1);
595 assert_eq!(entries[0].path(), Path::new("/src/main.m"));
596 provider
597 .rename(Path::new("/src/main.m"), Path::new("/src/renamed.m"))
598 .await
599 .unwrap();
600 assert!(provider.read(Path::new("/src/main.m")).await.is_err());
601 assert_eq!(
602 provider.read(Path::new("/src/renamed.m")).await.unwrap(),
603 b"disp('hi')"
604 );
605 provider.remove_dir_all(Path::new("/src")).await.unwrap();
606 assert!(provider.metadata(Path::new("/src")).await.is_err());
607 }
608
609 #[test]
610 fn memory_provider_file_handle_flushes_to_store() {
611 let provider = MemoryFsProvider::new();
612 let flags = OpenFlags {
613 write: true,
614 create: true,
615 ..Default::default()
616 };
617 let mut file = provider.open(Path::new("/out.txt"), &flags).unwrap();
618 file.write_all(b"hello").unwrap();
619 file.flush().unwrap();
620 assert_eq!(provider.read_project_path("/out.txt").unwrap(), b"hello");
621 }
622
623 #[tokio::test]
624 async fn memory_provider_keeps_file_and_directory_operations_distinct() {
625 let provider = MemoryFsProvider::new();
626 provider.create_dir(Path::new("/src")).await.unwrap();
627 assert!(provider.remove_file(Path::new("/src")).await.is_err());
628 assert!(provider.create_dir(Path::new("/src")).await.is_err());
629
630 provider
631 .write(Path::new("/src/main.m"), b"x = 1")
632 .await
633 .unwrap();
634 provider
635 .set_readonly(Path::new("/src/main.m"), true)
636 .await
637 .unwrap();
638 assert!(matches!(
639 provider.write(Path::new("/src/main.m"), b"x = 2").await,
640 Err(error) if error.kind() == ErrorKind::PermissionDenied
641 ));
642 assert!(provider.remove_dir(Path::new("/src/main.m")).await.is_err());
643 }
644
645 #[test]
646 fn memory_provider_rejects_parent_traversal() {
647 let provider = MemoryFsProvider::new();
648 assert!(provider.write_project_path("../secret.txt", b"no").is_err());
649 }
650}