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