1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
use std::path::{Component, Path, PathBuf};

use futures::future::BoxFuture;

use crate::{
    DirEntry, FileOpener, FileSystem, FsError, Metadata, OpenOptions, OpenOptionsConfig, ReadDir,
    VirtualFile,
};

/// A [`FileSystem`] implementation that is scoped to a specific directory on
/// the host.
#[derive(Debug, Clone)]
pub struct ScopedDirectoryFileSystem {
    root: PathBuf,
    inner: crate::host_fs::FileSystem,
}

impl ScopedDirectoryFileSystem {
    pub fn new(root: impl Into<PathBuf>, inner: crate::host_fs::FileSystem) -> Self {
        ScopedDirectoryFileSystem {
            root: root.into(),
            inner,
        }
    }

    /// Create a new [`ScopedDirectoryFileSystem`] using the current
    /// [`tokio::runtime::Handle`].
    ///
    /// # Panics
    ///
    /// This will panic if called outside of a `tokio` context.
    pub fn new_with_default_runtime(root: impl Into<PathBuf>) -> Self {
        let handle = tokio::runtime::Handle::current();
        let fs = crate::host_fs::FileSystem::new(handle);
        ScopedDirectoryFileSystem::new(root, fs)
    }

    fn prepare_path(&self, path: &Path) -> PathBuf {
        let path = normalize_path(path);
        let path = path.strip_prefix("/").unwrap_or(&path);

        let path = if !path.starts_with(&self.root) {
            self.root.join(path)
        } else {
            path.to_owned()
        };

        debug_assert!(path.starts_with(&self.root));
        path
    }
}

impl FileSystem for ScopedDirectoryFileSystem {
    fn read_dir(&self, path: &Path) -> Result<ReadDir, FsError> {
        let path = self.prepare_path(path);

        let mut entries = Vec::new();

        for entry in self.inner.read_dir(&path)? {
            let entry = entry?;
            let path = entry
                .path
                .strip_prefix(&self.root)
                .map_err(|_| FsError::InvalidData)?;
            entries.push(DirEntry {
                path: Path::new("/").join(path),
                ..entry
            });
        }

        Ok(ReadDir::new(entries))
    }

    fn create_dir(&self, path: &Path) -> Result<(), FsError> {
        let path = self.prepare_path(path);
        self.inner.create_dir(&path)
    }

    fn remove_dir(&self, path: &Path) -> Result<(), FsError> {
        let path = self.prepare_path(path);
        self.inner.remove_dir(&path)
    }

    fn rename<'a>(&'a self, from: &'a Path, to: &'a Path) -> BoxFuture<'a, Result<(), FsError>> {
        Box::pin(async move {
            let from = self.prepare_path(from);
            let to = self.prepare_path(to);
            self.inner.rename(&from, &to).await
        })
    }

    fn metadata(&self, path: &Path) -> Result<Metadata, FsError> {
        let path = self.prepare_path(path);
        self.inner.metadata(&path)
    }

    fn remove_file(&self, path: &Path) -> Result<(), FsError> {
        let path = self.prepare_path(path);
        self.inner.remove_file(&path)
    }

    fn new_open_options(&self) -> OpenOptions {
        OpenOptions::new(self)
    }
}

impl FileOpener for ScopedDirectoryFileSystem {
    fn open(
        &self,
        path: &Path,
        conf: &OpenOptionsConfig,
    ) -> Result<Box<dyn VirtualFile + Send + Sync + 'static>, FsError> {
        let path = self.prepare_path(path);
        self.inner
            .new_open_options()
            .options(conf.clone())
            .open(&path)
    }
}

// Copied from cargo
// https://github.com/rust-lang/cargo/blob/fede83ccf973457de319ba6fa0e36ead454d2e20/src/cargo/util/paths.rs#L61
fn normalize_path(path: &Path) -> PathBuf {
    let mut components = path.components().peekable();

    if matches!(components.peek(), Some(Component::Prefix(..))) {
        // This bit diverges from the original cargo implementation, but we want
        // to ignore the drive letter or UNC prefix on Windows. This shouldn't
        // make a difference in practice because WASI is meant to give us
        // Unix-style paths, not Windows-style ones.
        let _ = components.next();
    }

    let mut ret = PathBuf::new();

    for component in components {
        match component {
            Component::Prefix(..) => unreachable!(),
            Component::RootDir => {}
            Component::CurDir => {}
            Component::ParentDir => {
                ret.pop();
            }
            Component::Normal(c) => {
                ret.push(c);
            }
        }
    }
    ret
}

#[cfg(test)]
mod tests {
    use tempfile::TempDir;
    use tokio::io::AsyncReadExt;

    use super::*;

    #[tokio::test]
    async fn open_files() {
        let temp = TempDir::new().unwrap();
        std::fs::write(temp.path().join("file.txt"), "Hello, World!").unwrap();
        let fs = ScopedDirectoryFileSystem::new_with_default_runtime(temp.path());

        let mut f = fs.new_open_options().read(true).open("/file.txt").unwrap();
        let mut contents = String::new();
        f.read_to_string(&mut contents).await.unwrap();

        assert_eq!(contents, "Hello, World!");
    }

    #[tokio::test]
    async fn cant_access_outside_the_scoped_directory() {
        let scoped_directory = TempDir::new().unwrap();
        std::fs::write(scoped_directory.path().join("file.txt"), "").unwrap();
        std::fs::create_dir_all(scoped_directory.path().join("nested").join("dir")).unwrap();
        let fs = ScopedDirectoryFileSystem::new_with_default_runtime(scoped_directory.path());

        // Using ".." shouldn't let you escape the scoped directory
        let mut directory_entries: Vec<_> = fs
            .read_dir("/../../../".as_ref())
            .unwrap()
            .map(|e| e.unwrap().path())
            .collect();
        directory_entries.sort();
        assert_eq!(
            directory_entries,
            vec![PathBuf::from("/file.txt"), PathBuf::from("/nested")],
        );

        // Using a directory's absolute path also shouldn't work
        let other_dir = TempDir::new().unwrap();
        assert_eq!(
            fs.read_dir(other_dir.path()).unwrap_err(),
            FsError::EntryNotFound
        );
    }
}