virtual_fs/
webc_volume_fs.rs

1use std::{
2    convert::{TryFrom, TryInto},
3    io::Cursor,
4    path::{Path, PathBuf},
5    pin::Pin,
6    result::Result,
7    task::Poll,
8};
9
10use futures::future::BoxFuture;
11use tokio::io::{AsyncRead, AsyncSeek, AsyncWrite};
12use webc::{
13    Container, Metadata as WebcMetadata, PathSegmentError, PathSegments, ToPathSegments, Volume,
14    compat::SharedBytes,
15};
16
17use crate::{
18    DirEntry, EmptyFileSystem, FileOpener, FileSystem, FileType, FsError, Metadata,
19    OpenOptionsConfig, OverlayFileSystem, ReadDir, VirtualFile,
20};
21
22#[derive(Debug, Clone)]
23pub struct WebcVolumeFileSystem {
24    volume: Volume,
25}
26
27impl WebcVolumeFileSystem {
28    pub fn new(volume: Volume) -> Self {
29        WebcVolumeFileSystem { volume }
30    }
31
32    pub fn volume(&self) -> &Volume {
33        &self.volume
34    }
35
36    /// Get a filesystem where all [`Volume`]s in a [`Container`] are mounted to
37    /// the root directory.
38    pub fn mount_all(
39        container: &Container,
40    ) -> OverlayFileSystem<EmptyFileSystem, Vec<WebcVolumeFileSystem>> {
41        let mut filesystems = Vec::new();
42
43        for volume in container.volumes().into_values() {
44            filesystems.push(WebcVolumeFileSystem::new(volume));
45        }
46
47        OverlayFileSystem::new(EmptyFileSystem::default(), filesystems)
48    }
49}
50
51impl FileSystem for WebcVolumeFileSystem {
52    fn readlink(&self, path: &Path) -> crate::Result<PathBuf> {
53        let path = normalize(path).map_err(|_| FsError::InvalidInput)?;
54
55        match self.volume().metadata(&path) {
56            Some(meta) if !meta.is_symlink() => Err(FsError::InvalidInput),
57            Some(_) => self
58                .volume()
59                .read_link(&path)
60                .map(|(target, _)| PathBuf::from(target))
61                .ok_or(FsError::EntryNotFound),
62            None => Err(FsError::EntryNotFound),
63        }
64    }
65
66    fn read_dir(&self, path: &Path) -> Result<crate::ReadDir, FsError> {
67        let meta = self.metadata(path)?;
68
69        if !meta.is_dir() {
70            return Err(FsError::BaseNotDirectory);
71        }
72
73        let path = normalize(path).map_err(|_| FsError::InvalidInput)?;
74
75        let mut entries = Vec::new();
76
77        for (name, _, meta) in self
78            .volume()
79            .read_dir(&path)
80            .ok_or(FsError::EntryNotFound)?
81        {
82            let path = PathBuf::from(path.join(name).to_string());
83            entries.push(DirEntry {
84                path,
85                metadata: Ok(compat_meta(meta)),
86            });
87        }
88
89        Ok(ReadDir::new(entries))
90    }
91
92    fn create_dir(&self, path: &Path) -> Result<(), FsError> {
93        // the directory shouldn't exist yet
94        if self.metadata(path).is_ok() {
95            return Err(FsError::AlreadyExists);
96        }
97
98        // it's parent should exist
99        let parent = path.parent().unwrap_or_else(|| Path::new("/"));
100
101        match self.metadata(parent) {
102            Ok(parent_meta) if parent_meta.is_dir() => {
103                // The operation would normally be doable... but we're a readonly
104                // filesystem
105                Err(FsError::PermissionDenied)
106            }
107            Ok(_) | Err(FsError::EntryNotFound) => Err(FsError::BaseNotDirectory),
108            Err(other) => Err(other),
109        }
110    }
111
112    fn remove_dir(&self, path: &Path) -> Result<(), FsError> {
113        // The original directory should exist
114        let meta = self.metadata(path)?;
115
116        // and it should be a directory
117        if !meta.is_dir() {
118            return Err(FsError::BaseNotDirectory);
119        }
120
121        // but we are a readonly filesystem, so you can't modify anything
122        Err(FsError::PermissionDenied)
123    }
124
125    fn rename<'a>(&'a self, from: &'a Path, to: &'a Path) -> BoxFuture<'a, Result<(), FsError>> {
126        Box::pin(async {
127            // The original file should exist
128            let _ = self.metadata(from)?;
129
130            // we also want to make sure the destination's folder exists, too
131            let dest_parent = to.parent().unwrap_or_else(|| Path::new("/"));
132            let parent_meta = self.metadata(dest_parent)?;
133            if !parent_meta.is_dir() {
134                return Err(FsError::BaseNotDirectory);
135            }
136
137            // but we are a readonly filesystem, so you can't modify anything
138            Err(FsError::PermissionDenied)
139        })
140    }
141
142    fn metadata(&self, path: &Path) -> Result<Metadata, FsError> {
143        let path = normalize(path).map_err(|_| FsError::InvalidInput)?;
144
145        self.volume()
146            .metadata(path)
147            .map(compat_meta)
148            .ok_or(FsError::EntryNotFound)
149    }
150
151    fn symlink_metadata(&self, path: &Path) -> crate::Result<Metadata> {
152        self.metadata(path)
153    }
154
155    fn remove_file(&self, path: &Path) -> Result<(), FsError> {
156        let meta = self.metadata(path)?;
157
158        if !meta.is_file() {
159            return Err(FsError::NotAFile);
160        }
161
162        Err(FsError::PermissionDenied)
163    }
164
165    fn new_open_options(&self) -> crate::OpenOptions<'_> {
166        crate::OpenOptions::new(self)
167    }
168}
169
170impl FileOpener for WebcVolumeFileSystem {
171    fn open(
172        &self,
173        path: &Path,
174        conf: &OpenOptionsConfig,
175    ) -> crate::Result<Box<dyn crate::VirtualFile + Send + Sync + 'static>> {
176        if let Some(parent) = path.parent() {
177            let parent_meta = self.metadata(parent)?;
178            if !parent_meta.is_dir() {
179                return Err(FsError::BaseNotDirectory);
180            }
181        }
182
183        let timestamps = match self.volume().metadata(path) {
184            Some(m) if m.is_file() => m.timestamps(),
185            Some(_) => return Err(FsError::NotAFile),
186            None if conf.create() || conf.create_new() => {
187                // The file would normally be created, but we are a readonly fs.
188                return Err(FsError::PermissionDenied);
189            }
190            None => return Err(FsError::EntryNotFound),
191        };
192
193        match self.volume().read_file(path) {
194            Some((bytes, _)) => Ok(Box::new(File {
195                timestamps,
196                content: Cursor::new(bytes),
197            })),
198            None => {
199                // The metadata() call should guarantee this, so something
200                // probably went wrong internally
201                Err(FsError::UnknownError)
202            }
203        }
204    }
205}
206
207#[derive(Debug, Clone, PartialEq)]
208struct File {
209    timestamps: Option<webc::Timestamps>,
210    content: Cursor<SharedBytes>,
211}
212
213impl VirtualFile for File {
214    fn last_accessed(&self) -> u64 {
215        0
216    }
217
218    fn last_modified(&self) -> u64 {
219        self.timestamps
220            .map(|t| t.modified())
221            .unwrap_or_else(|| get_modified(None))
222    }
223
224    fn created_time(&self) -> u64 {
225        0
226    }
227
228    fn size(&self) -> u64 {
229        self.content.get_ref().len().try_into().unwrap()
230    }
231
232    fn set_len(&mut self, _new_size: u64) -> crate::Result<()> {
233        Err(FsError::PermissionDenied)
234    }
235
236    fn unlink(&mut self) -> crate::Result<()> {
237        Err(FsError::PermissionDenied)
238    }
239
240    fn poll_read_ready(
241        self: Pin<&mut Self>,
242        _cx: &mut std::task::Context<'_>,
243    ) -> Poll<std::io::Result<usize>> {
244        let bytes_remaining =
245            self.content.get_ref().len() - usize::try_from(self.content.position()).unwrap();
246        Poll::Ready(Ok(bytes_remaining))
247    }
248
249    fn poll_write_ready(
250        self: Pin<&mut Self>,
251        _cx: &mut std::task::Context<'_>,
252    ) -> Poll<std::io::Result<usize>> {
253        Poll::Ready(Err(std::io::ErrorKind::PermissionDenied.into()))
254    }
255
256    fn as_owned_buffer(&self) -> Option<SharedBytes> {
257        Some(self.content.get_ref().clone())
258    }
259}
260
261impl AsyncRead for File {
262    fn poll_read(
263        mut self: Pin<&mut Self>,
264        cx: &mut std::task::Context<'_>,
265        buf: &mut tokio::io::ReadBuf<'_>,
266    ) -> Poll<std::io::Result<()>> {
267        AsyncRead::poll_read(Pin::new(&mut self.content), cx, buf)
268    }
269}
270
271impl AsyncSeek for File {
272    fn start_seek(mut self: Pin<&mut Self>, position: std::io::SeekFrom) -> std::io::Result<()> {
273        AsyncSeek::start_seek(Pin::new(&mut self.content), position)
274    }
275
276    fn poll_complete(
277        mut self: Pin<&mut Self>,
278        cx: &mut std::task::Context<'_>,
279    ) -> Poll<std::io::Result<u64>> {
280        AsyncSeek::poll_complete(Pin::new(&mut self.content), cx)
281    }
282}
283
284impl AsyncWrite for File {
285    fn poll_write(
286        self: Pin<&mut Self>,
287        _cx: &mut std::task::Context<'_>,
288        _buf: &[u8],
289    ) -> Poll<Result<usize, std::io::Error>> {
290        Poll::Ready(Err(std::io::ErrorKind::PermissionDenied.into()))
291    }
292
293    fn poll_flush(
294        self: Pin<&mut Self>,
295        _cx: &mut std::task::Context<'_>,
296    ) -> Poll<Result<(), std::io::Error>> {
297        Poll::Ready(Err(std::io::ErrorKind::PermissionDenied.into()))
298    }
299
300    fn poll_shutdown(
301        self: Pin<&mut Self>,
302        _cx: &mut std::task::Context<'_>,
303    ) -> Poll<Result<(), std::io::Error>> {
304        Poll::Ready(Err(std::io::ErrorKind::PermissionDenied.into()))
305    }
306}
307
308// HACK: WebC v2 doesn't have timestamps, and WebC v3 files sometimes
309// have directories with a zero timestamp as well. Since some programs
310// interpret a zero timestamp as the absence of a value, we return
311// 1 second past epoch instead.
312fn get_modified(timestamps: Option<webc::Timestamps>) -> u64 {
313    let modified = timestamps.map(|t| t.modified()).unwrap_or_default();
314    // 1 billion nanoseconds = 1 second
315    modified.max(1_000_000_000)
316}
317
318fn compat_meta(meta: WebcMetadata) -> Metadata {
319    match meta {
320        WebcMetadata::Dir { timestamps } => Metadata {
321            ft: FileType {
322                dir: true,
323                ..Default::default()
324            },
325            modified: get_modified(timestamps),
326            ..Default::default()
327        },
328        WebcMetadata::File {
329            length, timestamps, ..
330        } => Metadata {
331            ft: FileType {
332                file: true,
333                ..Default::default()
334            },
335            len: length.try_into().unwrap(),
336            modified: get_modified(timestamps),
337            ..Default::default()
338        },
339        WebcMetadata::Symlink {
340            target_length,
341            timestamps,
342        } => Metadata {
343            ft: FileType {
344                symlink: true,
345                ..Default::default()
346            },
347            len: target_length.try_into().unwrap(),
348            modified: get_modified(timestamps),
349            ..Default::default()
350        },
351    }
352}
353
354/// Normalize a [`Path`] into a [`PathSegments`], dealing with things like `..`
355/// and skipping `.`'s.
356fn normalize(path: &Path) -> Result<PathSegments, PathSegmentError> {
357    // normalization is handled by the ToPathSegments impl for Path
358    let result = path.to_path_segments();
359
360    if let Err(e) = &result {
361        tracing::debug!(
362            error = e as &dyn std::error::Error,
363            path=%path.display(),
364            "Unable to normalize a path",
365        );
366    }
367
368    result
369}
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374    use crate::DirEntry;
375    use std::collections::BTreeMap;
376    use std::convert::TryFrom;
377    use tokio::io::AsyncReadExt;
378    use wasmer_package::utils::from_bytes;
379    use webc::PathSegment;
380
381    const PYTHON_WEBC: &[u8] =
382        include_bytes!("../../../wasmer-test-files/examples/python-0.1.0.wasmer");
383
384    fn symlink_fs() -> WebcVolumeFileSystem {
385        let timestamps = webc::v3::Timestamps::default();
386        let dir = webc::v3::write::Directory::new(
387            BTreeMap::from_iter([
388                (
389                    PathSegment::parse("target.txt").unwrap(),
390                    webc::v3::write::DirEntry::File(webc::v3::write::FileEntry::borrowed(
391                        b"target", timestamps,
392                    )),
393                ),
394                (
395                    PathSegment::parse("link").unwrap(),
396                    webc::v3::write::DirEntry::Symlink(webc::v3::write::SymlinkEntry::borrowed(
397                        "target.txt",
398                        timestamps,
399                    )),
400                ),
401            ]),
402            timestamps,
403        );
404        let manifest = webc::metadata::Manifest::default();
405        let mut writer = webc::v3::write::Writer::new(webc::v3::ChecksumAlgorithm::Sha256)
406            .write_manifest(&manifest)
407            .unwrap()
408            .write_atoms(BTreeMap::new())
409            .unwrap();
410        writer.write_volume("atom", dir).unwrap();
411        let webc = writer.finish(webc::v3::SignatureAlgorithm::None).unwrap();
412        let container = from_bytes(webc).unwrap();
413        let volume = container.volumes()["atom"].clone();
414
415        WebcVolumeFileSystem::new(volume)
416    }
417
418    #[test]
419    fn normalize_paths() {
420        let inputs: Vec<(&str, &[&str])> = vec![
421            ("/", &[]),
422            ("/path/to/", &["path", "to"]),
423            ("/path/to/file.txt", &["path", "to", "file.txt"]),
424            ("/folder/..", &[]),
425            ("/.hidden", &[".hidden"]),
426            ("/folder/../../../../../../../file.txt", &["file.txt"]),
427            #[cfg(windows)]
428            (r"C:\path\to\file.txt", &["path", "to", "file.txt"]),
429        ];
430
431        for (path, expected) in inputs {
432            let normalized = normalize(path.as_ref()).unwrap();
433            assert_eq!(normalized, expected.to_path_segments().unwrap());
434        }
435    }
436
437    #[test]
438    #[cfg_attr(not(windows), ignore = "Only works with PathBuf's Windows logic")]
439    fn normalize_windows_paths() {
440        let inputs: Vec<(&str, &[&str])> = vec![
441            (r"C:\path\to\file.txt", &["path", "to", "file.txt"]),
442            (r"C:/path/to/file.txt", &["path", "to", "file.txt"]),
443            (r"\\system07\C$\", &[]),
444            (r"c:\temp\test-file.txt", &["temp", "test-file.txt"]),
445            (
446                r"\\127.0.0.1\c$\temp\test-file.txt",
447                &["temp", "test-file.txt"],
448            ),
449            (r"\\.\c:\temp\test-file.txt", &["temp", "test-file.txt"]),
450            (r"\\?\c:\temp\test-file.txt", &["temp", "test-file.txt"]),
451            (
452                r"\\127.0.0.1\c$\temp\test-file.txt",
453                &["temp", "test-file.txt"],
454            ),
455            (
456                r"\\.\Volume{b75e2c83-0000-0000-0000-602f00000000}\temp\test-file.txt",
457                &["temp", "test-file.txt"],
458            ),
459        ];
460
461        for (path, expected) in inputs {
462            let normalized = normalize(path.as_ref()).unwrap();
463            assert_eq!(normalized, expected.to_path_segments().unwrap(), "{}", path);
464        }
465    }
466
467    #[test]
468    fn invalid_paths() {
469        let paths = [".", "..", "./file.txt", ""];
470
471        for path in paths {
472            assert!(normalize(path.as_ref()).is_err(), "{}", path);
473        }
474    }
475
476    #[test]
477    fn symlink_metadata_and_readlink() {
478        let fs = symlink_fs();
479
480        let link = fs.symlink_metadata("/link".as_ref()).unwrap();
481        assert!(link.ft.is_symlink());
482        assert_eq!(link.len(), "target.txt".len() as u64);
483        assert_eq!(
484            fs.readlink("/link".as_ref()).unwrap(),
485            Path::new("target.txt")
486        );
487        assert_eq!(
488            fs.new_open_options().read(true).open("/link").unwrap_err(),
489            FsError::NotAFile,
490        );
491
492        let entries: Vec<_> = fs
493            .read_dir("/".as_ref())
494            .unwrap()
495            .map(|entry| entry.unwrap())
496            .collect();
497        let link_entry = entries
498            .iter()
499            .find(|entry| entry.path == Path::new("/link"))
500            .unwrap();
501        assert!(link_entry.metadata().unwrap().ft.is_symlink());
502
503        assert_eq!(
504            fs.readlink("/target.txt".as_ref()).unwrap_err(),
505            FsError::InvalidInput
506        );
507        assert_eq!(
508            fs.readlink("/missing".as_ref()).unwrap_err(),
509            FsError::EntryNotFound
510        );
511    }
512
513    #[test]
514    fn mount_all_volumes_in_python() {
515        let container = from_bytes(PYTHON_WEBC).unwrap();
516
517        let fs = WebcVolumeFileSystem::mount_all(&container);
518
519        // We should now have access to the python directory
520        let lib_meta = fs.metadata("/lib/python3.6/".as_ref()).unwrap();
521        assert!(lib_meta.is_dir());
522    }
523
524    #[test]
525    fn read_dir() {
526        let container = from_bytes(PYTHON_WEBC).unwrap();
527        let volumes = container.volumes();
528        let volume = volumes["atom"].clone();
529
530        let fs = WebcVolumeFileSystem::new(volume);
531
532        let entries: Vec<_> = fs
533            .read_dir("/lib".as_ref())
534            .unwrap()
535            .map(|r| r.unwrap())
536            .collect();
537
538        let modified = get_modified(None);
539        let expected = vec![
540            DirEntry {
541                path: "/lib/.DS_Store".into(),
542                metadata: Ok(Metadata {
543                    ft: FileType {
544                        file: true,
545                        ..Default::default()
546                    },
547                    accessed: 0,
548                    created: 0,
549                    modified,
550                    len: 6148,
551                }),
552            },
553            DirEntry {
554                path: "/lib/Parser".into(),
555                metadata: Ok(Metadata {
556                    ft: FileType {
557                        dir: true,
558                        ..Default::default()
559                    },
560                    accessed: 0,
561                    created: 0,
562                    modified,
563                    len: 0,
564                }),
565            },
566            DirEntry {
567                path: "/lib/python.wasm".into(),
568                metadata: Ok(crate::Metadata {
569                    ft: crate::FileType {
570                        file: true,
571                        ..Default::default()
572                    },
573                    accessed: 0,
574                    created: 0,
575                    modified,
576                    len: 4694941,
577                }),
578            },
579            DirEntry {
580                path: "/lib/python3.6".into(),
581                metadata: Ok(crate::Metadata {
582                    ft: crate::FileType {
583                        dir: true,
584                        ..Default::default()
585                    },
586                    accessed: 0,
587                    created: 0,
588                    modified,
589                    len: 0,
590                }),
591            },
592        ];
593        assert_eq!(entries, expected);
594    }
595
596    #[test]
597    fn metadata() {
598        let container = from_bytes(PYTHON_WEBC).unwrap();
599        let volumes = container.volumes();
600        let volume = volumes["atom"].clone();
601
602        let fs = WebcVolumeFileSystem::new(volume);
603
604        let modified = get_modified(None);
605        let python_wasm = crate::Metadata {
606            ft: crate::FileType {
607                file: true,
608                ..Default::default()
609            },
610            accessed: 0,
611            created: 0,
612            modified,
613            len: 4694941,
614        };
615        assert_eq!(
616            fs.metadata("/lib/python.wasm".as_ref()).unwrap(),
617            python_wasm,
618        );
619        assert_eq!(
620            fs.metadata("/../../../../lib/python.wasm".as_ref())
621                .unwrap(),
622            python_wasm,
623        );
624        assert_eq!(
625            fs.metadata("/lib/python3.6/../python3.6/../python.wasm".as_ref())
626                .unwrap(),
627            python_wasm,
628        );
629        assert_eq!(
630            fs.metadata("/lib/python3.6".as_ref()).unwrap(),
631            crate::Metadata {
632                ft: crate::FileType {
633                    dir: true,
634                    ..Default::default()
635                },
636                accessed: 0,
637                created: 0,
638                modified,
639                len: 0,
640            },
641        );
642        assert_eq!(
643            fs.metadata("/this/does/not/exist".as_ref()).unwrap_err(),
644            FsError::EntryNotFound
645        );
646    }
647
648    #[tokio::test]
649    async fn file_opener() {
650        let container = from_bytes(PYTHON_WEBC).unwrap();
651        let volumes = container.volumes();
652        let volume = volumes["atom"].clone();
653
654        let fs = WebcVolumeFileSystem::new(volume);
655
656        assert_eq!(
657            fs.new_open_options()
658                .create(true)
659                .write(true)
660                .open("/file.txt")
661                .unwrap_err(),
662            FsError::PermissionDenied,
663        );
664        assert_eq!(
665            fs.new_open_options().read(true).open("/lib").unwrap_err(),
666            FsError::NotAFile,
667        );
668        assert_eq!(
669            fs.new_open_options()
670                .read(true)
671                .open("/this/does/not/exist.txt")
672                .unwrap_err(),
673            FsError::EntryNotFound,
674        );
675
676        // We should be able to actually read the file
677        let mut f = fs
678            .new_open_options()
679            .read(true)
680            .open("/lib/python.wasm")
681            .unwrap();
682        let mut buffer = Vec::new();
683        f.read_to_end(&mut buffer).await.unwrap();
684        assert!(buffer.starts_with(b"\0asm"));
685        assert_eq!(
686            fs.metadata("/lib/python.wasm".as_ref()).unwrap().len(),
687            u64::try_from(buffer.len()).unwrap(),
688        );
689    }
690
691    #[test]
692    fn remove_dir_is_not_allowed() {
693        let container = from_bytes(PYTHON_WEBC).unwrap();
694        let volumes = container.volumes();
695        let volume = volumes["atom"].clone();
696
697        let fs = WebcVolumeFileSystem::new(volume);
698
699        assert_eq!(
700            fs.remove_dir("/lib".as_ref()).unwrap_err(),
701            FsError::PermissionDenied,
702        );
703        assert_eq!(
704            fs.remove_dir("/this/does/not/exist".as_ref()).unwrap_err(),
705            FsError::EntryNotFound,
706        );
707        assert_eq!(
708            fs.remove_dir("/lib/python.wasm".as_ref()).unwrap_err(),
709            FsError::BaseNotDirectory,
710        );
711    }
712
713    #[test]
714    fn remove_file_is_not_allowed() {
715        let container = from_bytes(PYTHON_WEBC).unwrap();
716        let volumes = container.volumes();
717        let volume = volumes["atom"].clone();
718
719        let fs = WebcVolumeFileSystem::new(volume);
720
721        assert_eq!(
722            fs.remove_file("/lib".as_ref()).unwrap_err(),
723            FsError::NotAFile,
724        );
725        assert_eq!(
726            fs.remove_file("/this/does/not/exist".as_ref()).unwrap_err(),
727            FsError::EntryNotFound,
728        );
729        assert_eq!(
730            fs.remove_file("/lib/python.wasm".as_ref()).unwrap_err(),
731            FsError::PermissionDenied,
732        );
733    }
734
735    #[test]
736    fn create_dir_is_not_allowed() {
737        let container = from_bytes(PYTHON_WEBC).unwrap();
738        let volumes = container.volumes();
739        let volume = volumes["atom"].clone();
740
741        let fs = WebcVolumeFileSystem::new(volume);
742
743        assert_eq!(
744            fs.create_dir("/lib".as_ref()).unwrap_err(),
745            FsError::AlreadyExists,
746        );
747        assert_eq!(
748            fs.create_dir("/this/does/not/exist".as_ref()).unwrap_err(),
749            FsError::BaseNotDirectory,
750        );
751        assert_eq!(
752            fs.create_dir("/lib/nested/".as_ref()).unwrap_err(),
753            FsError::PermissionDenied,
754        );
755    }
756
757    #[tokio::test]
758    async fn rename_is_not_allowed() {
759        let container = from_bytes(PYTHON_WEBC).unwrap();
760        let volumes = container.volumes();
761        let volume = volumes["atom"].clone();
762
763        let fs = WebcVolumeFileSystem::new(volume);
764
765        assert_eq!(
766            fs.rename("/lib".as_ref(), "/other".as_ref())
767                .await
768                .unwrap_err(),
769            FsError::PermissionDenied,
770        );
771        assert_eq!(
772            fs.rename("/this/does/not/exist".as_ref(), "/another".as_ref())
773                .await
774                .unwrap_err(),
775            FsError::EntryNotFound,
776        );
777        assert_eq!(
778            fs.rename("/lib/python.wasm".as_ref(), "/lib/another.wasm".as_ref())
779                .await
780                .unwrap_err(),
781            FsError::PermissionDenied,
782        );
783    }
784}