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