Skip to main content

runmat_filesystem/
memory.rs

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(&current) {
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}