vfs/async_vfs/impls/
memory.rs

1//! An ephemeral in-memory file system, intended mainly for unit tests
2use crate::async_vfs::{AsyncFileSystem, SeekAndRead};
3use crate::error::VfsErrorKind;
4use crate::path::VfsFileType;
5use crate::{VfsMetadata, VfsResult};
6
7use async_std::io::{prelude::SeekExt, Cursor, Read, Seek, SeekFrom, Write};
8use async_std::sync::{Arc, RwLock};
9use async_trait::async_trait;
10use futures::task::{Context, Poll};
11use futures::{Stream, StreamExt};
12use std::collections::hash_map::Entry;
13use std::collections::HashMap;
14use std::fmt;
15use std::fmt::{Debug, Formatter};
16use std::mem::swap;
17use std::pin::Pin;
18
19type AsyncMemoryFsHandle = Arc<RwLock<AsyncMemoryFsImpl>>;
20
21/// An ephemeral in-memory file system, intended mainly for unit tests
22pub struct AsyncMemoryFS {
23    handle: AsyncMemoryFsHandle,
24}
25
26impl Debug for AsyncMemoryFS {
27    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
28        f.write_str("In Memory File System")
29    }
30}
31
32impl AsyncMemoryFS {
33    /// Create a new in-memory filesystem
34    pub fn new() -> Self {
35        AsyncMemoryFS {
36            handle: Arc::new(RwLock::new(AsyncMemoryFsImpl::new())),
37        }
38    }
39
40    async fn ensure_has_parent(&self, path: &str) -> VfsResult<()> {
41        let separator = path.rfind('/');
42        if let Some(index) = separator {
43            if self.exists(&path[..index]).await? {
44                return Ok(());
45            }
46        }
47        Err(VfsErrorKind::Other("Parent path does not exist".into()).into())
48    }
49}
50
51impl Default for AsyncMemoryFS {
52    fn default() -> Self {
53        Self::new()
54    }
55}
56
57struct AsyncWritableFile {
58    content: Cursor<Vec<u8>>,
59    destination: String,
60    fs: AsyncMemoryFsHandle,
61}
62
63impl Write for AsyncWritableFile {
64    fn poll_write(
65        self: Pin<&mut Self>,
66        cx: &mut Context<'_>,
67        buf: &[u8],
68    ) -> Poll<Result<usize, async_std::io::Error>> {
69        let this = self.get_mut();
70        let file = Pin::new(&mut this.content);
71        file.poll_write(cx, buf)
72    }
73    // Flush any bytes left in the write buffer to the virtual file
74    fn poll_flush(
75        self: Pin<&mut Self>,
76        cx: &mut Context<'_>,
77    ) -> Poll<Result<(), async_std::io::Error>> {
78        let this = self.get_mut();
79        let file = Pin::new(&mut this.content);
80        file.poll_flush(cx)
81    }
82    fn poll_close(
83        self: Pin<&mut Self>,
84        cx: &mut Context<'_>,
85    ) -> Poll<Result<(), async_std::io::Error>> {
86        let this = self.get_mut();
87        let file = Pin::new(&mut this.content);
88        file.poll_close(cx)
89    }
90}
91
92impl Drop for AsyncWritableFile {
93    fn drop(&mut self) {
94        let mut content = vec![];
95        swap(&mut content, self.content.get_mut());
96        futures::executor::block_on(self.fs.write()).files.insert(
97            self.destination.clone(),
98            AsyncMemoryFile {
99                file_type: VfsFileType::File,
100                content: Arc::new(content),
101            },
102        );
103    }
104}
105
106struct AsyncReadableFile {
107    #[allow(clippy::rc_buffer)] // to allow accessing the same object as writable
108    content: Arc<Vec<u8>>,
109    // Position of the read cursor in the "file"
110    cursor_pos: u64,
111}
112
113impl AsyncReadableFile {
114    fn len(&self) -> u64 {
115        self.content.len() as u64
116    }
117}
118
119impl Read for AsyncReadableFile {
120    fn poll_read(
121        self: Pin<&mut Self>,
122        _cx: &mut Context<'_>,
123        buf: &mut [u8],
124    ) -> Poll<Result<usize, async_std::io::Error>> {
125        let this = self.get_mut();
126        let bytes_left = this.len() - this.cursor_pos;
127        let bytes_read = std::cmp::min(buf.len() as u64, bytes_left);
128        if bytes_left == 0 {
129            return Poll::Ready(Ok(0));
130        }
131        buf[..bytes_read as usize].copy_from_slice(
132            &this.content[this.cursor_pos as usize..(this.cursor_pos + bytes_read) as usize],
133        );
134        this.cursor_pos += bytes_read;
135        Poll::Ready(Ok(bytes_read as usize))
136    }
137}
138
139impl Seek for AsyncReadableFile {
140    fn poll_seek(
141        self: Pin<&mut Self>,
142        _cx: &mut Context<'_>,
143        pos: SeekFrom,
144    ) -> Poll<Result<u64, async_std::io::Error>> {
145        let this = self.get_mut();
146        let new_pos = match pos {
147            SeekFrom::Start(offset) => offset as i64,
148            SeekFrom::End(offset) => this.cursor_pos as i64 - offset,
149            SeekFrom::Current(offset) => this.cursor_pos as i64 + offset,
150        };
151        if new_pos < 0 || new_pos >= this.len() as i64 {
152            Poll::Ready(Err(async_std::io::Error::new(
153                async_std::io::ErrorKind::InvalidData,
154                "Requested offset is outside the file!",
155            )))
156        } else {
157            this.cursor_pos = new_pos as u64;
158            Poll::Ready(Ok(new_pos as u64))
159        }
160    }
161}
162
163#[async_trait]
164impl AsyncFileSystem for AsyncMemoryFS {
165    async fn read_dir(
166        &self,
167        path: &str,
168    ) -> VfsResult<Box<dyn Unpin + Stream<Item = String> + Send>> {
169        let prefix = format!("{}/", path);
170        let handle = self.handle.read().await;
171        let mut found_directory = false;
172        #[allow(clippy::needless_collect)] // need collect to satisfy lifetime requirements
173        let entries: Vec<String> = handle
174            .files
175            .iter()
176            .filter_map(|(candidate_path, _)| {
177                if candidate_path == path {
178                    found_directory = true;
179                }
180                if candidate_path.starts_with(&prefix) {
181                    let rest = &candidate_path[prefix.len()..];
182                    if !rest.contains('/') {
183                        return Some(rest.to_string());
184                    }
185                }
186                None
187            })
188            .collect();
189        if !found_directory {
190            return Err(VfsErrorKind::FileNotFound.into());
191        }
192        Ok(Box::new(futures::stream::iter(entries)))
193    }
194
195    async fn create_dir(&self, path: &str) -> VfsResult<()> {
196        self.ensure_has_parent(path).await?;
197        let map = &mut self.handle.write().await.files;
198        let entry = map.entry(path.to_string());
199        match entry {
200            Entry::Occupied(file) => {
201                return match file.get().file_type {
202                    VfsFileType::File => Err(VfsErrorKind::FileExists.into()),
203                    VfsFileType::Directory => Err(VfsErrorKind::DirectoryExists.into()),
204                }
205            }
206            Entry::Vacant(_) => {
207                map.insert(
208                    path.to_string(),
209                    AsyncMemoryFile {
210                        file_type: VfsFileType::Directory,
211                        content: Default::default(),
212                    },
213                );
214            }
215        }
216        Ok(())
217    }
218
219    async fn open_file(&self, path: &str) -> VfsResult<Box<dyn SeekAndRead + Send + Unpin>> {
220        let handle = self.handle.read().await;
221        let file = handle.files.get(path).ok_or(VfsErrorKind::FileNotFound)?;
222        ensure_file(file)?;
223        Ok(Box::new(AsyncReadableFile {
224            content: file.content.clone(),
225            cursor_pos: 0,
226        }))
227    }
228
229    async fn create_file(&self, path: &str) -> VfsResult<Box<dyn Write + Send + Unpin>> {
230        self.ensure_has_parent(path).await?;
231        let content = Arc::new(Vec::<u8>::new());
232        self.handle.write().await.files.insert(
233            path.to_string(),
234            AsyncMemoryFile {
235                file_type: VfsFileType::File,
236                content,
237            },
238        );
239        let writer = AsyncWritableFile {
240            content: Cursor::new(vec![]),
241            destination: path.to_string(),
242            fs: self.handle.clone(),
243        };
244        Ok(Box::new(writer))
245    }
246
247    async fn append_file(&self, path: &str) -> VfsResult<Box<dyn Write + Send + Unpin>> {
248        let handle = self.handle.write().await;
249        let file = handle.files.get(path).ok_or(VfsErrorKind::FileNotFound)?;
250        let mut content = Cursor::new(file.content.as_ref().clone());
251        content.seek(SeekFrom::End(0)).await?;
252        let writer = AsyncWritableFile {
253            content,
254            destination: path.to_string(),
255            fs: self.handle.clone(),
256        };
257        Ok(Box::new(writer))
258    }
259
260    async fn metadata(&self, path: &str) -> VfsResult<VfsMetadata> {
261        let guard = self.handle.read().await;
262        let files = &guard.files;
263        let file = files.get(path).ok_or(VfsErrorKind::FileNotFound)?;
264        Ok(VfsMetadata {
265            file_type: file.file_type,
266            len: file.content.len() as u64,
267            modified: None,
268            created: None,
269            accessed: None,
270        })
271    }
272
273    async fn exists(&self, path: &str) -> VfsResult<bool> {
274        Ok(self.handle.read().await.files.contains_key(path))
275    }
276
277    async fn remove_file(&self, path: &str) -> VfsResult<()> {
278        let mut handle = self.handle.write().await;
279        handle
280            .files
281            .remove(path)
282            .ok_or(VfsErrorKind::FileNotFound)?;
283        Ok(())
284    }
285
286    async fn remove_dir(&self, path: &str) -> VfsResult<()> {
287        if self.read_dir(path).await?.next().await.is_some() {
288            return Err(VfsErrorKind::Other("Directory to remove is not empty".into()).into());
289        }
290        let mut handle = self.handle.write().await;
291        handle
292            .files
293            .remove(path)
294            .ok_or(VfsErrorKind::FileNotFound)?;
295        Ok(())
296    }
297}
298
299#[derive(Debug)]
300struct AsyncMemoryFsImpl {
301    files: HashMap<String, AsyncMemoryFile>,
302}
303
304impl AsyncMemoryFsImpl {
305    pub fn new() -> Self {
306        let mut files = HashMap::new();
307        // Add root directory
308        files.insert(
309            "".to_string(),
310            AsyncMemoryFile {
311                file_type: VfsFileType::Directory,
312                content: Arc::new(vec![]),
313            },
314        );
315        Self { files }
316    }
317}
318
319#[derive(Debug)]
320struct AsyncMemoryFile {
321    file_type: VfsFileType,
322    #[allow(clippy::rc_buffer)] // to allow accessing the same object as writable
323    content: Arc<Vec<u8>>,
324}
325
326#[cfg(test)]
327mod tests {
328    use super::*;
329    use crate::async_vfs::AsyncVfsPath;
330    use async_std::io::{ReadExt, WriteExt};
331    test_async_vfs!(AsyncMemoryFS::new());
332
333    #[tokio::test]
334    async fn write_and_read_file() -> VfsResult<()> {
335        let root = AsyncVfsPath::new(AsyncMemoryFS::new());
336        let path = root.join("foobar.txt").unwrap();
337        let _send = &path as &dyn Send;
338        {
339            let mut file = path.create_file().await.unwrap();
340            write!(file, "Hello world").await.unwrap();
341            write!(file, "!").await.unwrap();
342        }
343        {
344            let mut file = path.open_file().await.unwrap();
345            let mut string: String = String::new();
346            file.read_to_string(&mut string).await.unwrap();
347            assert_eq!(string, "Hello world!");
348        }
349        assert!(path.exists().await?);
350        assert!(!root.join("foo").unwrap().exists().await?);
351        let metadata = path.metadata().await.unwrap();
352        assert_eq!(metadata.len, 12);
353        assert_eq!(metadata.file_type, VfsFileType::File);
354        Ok(())
355    }
356
357    #[tokio::test]
358    async fn append_file() {
359        let root = AsyncVfsPath::new(AsyncMemoryFS::new());
360        let _string = String::new();
361        let path = root.join("test_append.txt").unwrap();
362        path.create_file()
363            .await
364            .unwrap()
365            .write_all(b"Testing 1")
366            .await
367            .unwrap();
368        path.append_file()
369            .await
370            .unwrap()
371            .write_all(b"Testing 2")
372            .await
373            .unwrap();
374        {
375            let mut file = path.open_file().await.unwrap();
376            let mut string: String = String::new();
377            file.read_to_string(&mut string).await.unwrap();
378            assert_eq!(string, "Testing 1Testing 2");
379        }
380    }
381
382    #[tokio::test]
383    async fn create_dir() {
384        let root = AsyncVfsPath::new(AsyncMemoryFS::new());
385        let _string = String::new();
386        let path = root.join("foo").unwrap();
387        path.create_dir().await.unwrap();
388        let metadata = path.metadata().await.unwrap();
389        assert_eq!(metadata.file_type, VfsFileType::Directory);
390    }
391
392    #[tokio::test]
393    async fn remove_dir_error_message() {
394        let root = AsyncVfsPath::new(AsyncMemoryFS::new());
395        let path = root.join("foo").unwrap();
396        let result = path.remove_dir().await;
397        assert_eq!(
398            format!("{}", result.unwrap_err()),
399            "Could not remove directory for '/foo': The file or directory could not be found"
400        );
401    }
402
403    #[tokio::test]
404    async fn read_dir_error_message() {
405        let root = AsyncVfsPath::new(AsyncMemoryFS::new());
406        let path = root.join("foo").unwrap();
407        let result = path.read_dir().await;
408        match result {
409            Ok(_) => panic!("Error expected"),
410            Err(err) => {
411                assert_eq!(
412                    format!("{}", err),
413                    "Could not read directory for '/foo': The file or directory could not be found"
414                );
415            }
416        }
417    }
418
419    #[tokio::test]
420    async fn copy_file_across_filesystems() -> VfsResult<()> {
421        let root_a = AsyncVfsPath::new(AsyncMemoryFS::new());
422        let root_b = AsyncVfsPath::new(AsyncMemoryFS::new());
423        let src = root_a.join("a.txt")?;
424        let dest = root_b.join("b.txt")?;
425        src.create_file().await?.write_all(b"Hello World").await?;
426        src.copy_file(&dest).await?;
427        assert_eq!(&dest.read_to_string().await?, "Hello World");
428        Ok(())
429    }
430}
431
432fn ensure_file(file: &AsyncMemoryFile) -> VfsResult<()> {
433    if file.file_type != VfsFileType::File {
434        return Err(VfsErrorKind::Other("Not a file".into()).into());
435    }
436    Ok(())
437}