Skip to main content

virtual_filesystem/physical_fs/
mod.rs

1mod path_resolver;
2
3use crate::file::{DirEntry, File, Metadata, OpenOptions};
4use crate::physical_fs::path_resolver::{
5    PathResolver, SandboxedPathResolver, UnrestrictedPathResolver,
6};
7use crate::util::invalid_path;
8use crate::FileSystem;
9use normalize_path::NormalizePath;
10use std::fs;
11use std::marker::PhantomData;
12use std::path::{Path, PathBuf};
13
14/// The physical filesystem, backed by a root on the drive.
15pub struct PhysicalFSImpl<R: PathResolver> {
16    root: PathBuf,
17    _marker: PhantomData<R>,
18}
19
20/// The physical filesystem, backed by a root on the drive. This filesystem will not protect against
21/// directory traversal and very simply appends the target path to the root.
22pub type PhysicalFS = PhysicalFSImpl<UnrestrictedPathResolver>;
23/// The physical filesystem, backed by a root on the drive. This filesystem will perform basic
24/// protections against directory traversal in the form of returning an error if a user tries to
25/// escape the current directory.
26pub type SandboxedPhysicalFS = PhysicalFSImpl<SandboxedPathResolver>;
27
28impl<R: PathResolver> PhysicalFSImpl<R> {
29    /// Creates a new physical file system at the given root.
30    pub fn new<P: AsRef<Path>>(root: P) -> Self {
31        Self {
32            root: root.as_ref().normalize(),
33            _marker: PhantomData,
34        }
35    }
36}
37
38impl<R: PathResolver> FileSystem for PhysicalFSImpl<R> {
39    fn create_dir(&self, path: &str) -> crate::Result<()> {
40        fs::create_dir(R::resolve_path(&self.root, path)?.normalize())
41    }
42
43    fn metadata(&self, path: &str) -> crate::Result<Metadata> {
44        fs::metadata(R::resolve_path(&self.root, path)?.normalize()).map(Metadata::from)
45    }
46
47    fn open_file_options(&self, path: &str, options: &OpenOptions) -> crate::Result<Box<dyn File>> {
48        fs::OpenOptions::from(options)
49            .open(R::resolve_path(&self.root, path)?.normalize())
50            .map::<Box<dyn File>, _>(|file| Box::new(file))
51    }
52
53    fn read_dir(
54        &self,
55        path: &str,
56    ) -> crate::Result<Box<dyn Iterator<Item = crate::Result<DirEntry>>>> {
57        let resolved = R::resolve_path(&self.root, path)?;
58        let abs_root = if resolved.is_absolute() {
59            resolved.clone()
60        } else {
61            std::env::current_dir()?.join(&resolved).normalize()
62        };
63        let abs_root = abs_root.normalize();
64        Ok(Box::new(
65            fs::read_dir(&abs_root)?.map({
66                move |entry| {
67                    entry.and_then({
68                        |entry| {
69                            Ok(DirEntry {
70                                // strip the root
71                                path: entry
72                                    .path()
73                                    .strip_prefix(&abs_root)
74                                    .map_err(|_| invalid_path())?
75                                    .into(),
76                                metadata: entry.metadata()?.into(),
77                            })
78                        }
79                    })
80                }
81            }),
82        ))
83    }
84
85    fn remove_dir(&self, path: &str) -> crate::Result<()> {
86        fs::remove_dir(R::resolve_path(&self.root, path)?.normalize())
87    }
88
89    fn remove_file(&self, path: &str) -> crate::Result<()> {
90        fs::remove_file(R::resolve_path(&self.root, path)?.normalize())
91    }
92}
93
94impl File for fs::File {
95    fn metadata(&self) -> crate::Result<Metadata> {
96        self.metadata().map(Metadata::from)
97    }
98}
99
100#[cfg(test)]
101mod test {
102    use crate::file::FileType;
103    use crate::physical_fs::{PhysicalFS, SandboxedPhysicalFS};
104    use crate::FileSystem;
105    use std::path::Path;
106
107    fn physical_fs<P: AsRef<Path>>(root: P) -> (PhysicalFS, SandboxedPhysicalFS) {
108        (
109            PhysicalFS::new(root.as_ref()),
110            SandboxedPhysicalFS::new(root.as_ref()),
111        )
112    }
113
114    #[test]
115    fn read_dir() {
116        let (unrestricted_fs, sandboxed_fs) = physical_fs("test");
117
118        // basic traversal
119        let dir = sandboxed_fs.read_dir(".").unwrap();
120        assert!(dir.count() > 0);
121        let dir = unrestricted_fs.read_dir(".").unwrap();
122        assert!(dir.count() > 0);
123
124        // project root traversal
125        assert!(sandboxed_fs.read_dir("..").is_err());
126        let dir = unrestricted_fs.read_dir("..").unwrap();
127        assert!(dir.count() > 0);
128
129        // fancy project root traversal
130        assert!(sandboxed_fs.read_dir("test/something/../../..").is_err());
131        let dir = unrestricted_fs.read_dir("test/something/../../..").unwrap();
132        assert!(dir.count() > 0);
133    }
134
135    #[test]
136    fn metadata() {
137        let (unrestricted_fs, sandboxed_fs) = physical_fs("test/folder_a");
138
139        // basic traversal
140        let md = sandboxed_fs.metadata(".").unwrap();
141        assert_eq!(md.file_type, FileType::Directory);
142        assert_eq!(md.len, 0);
143        let md = unrestricted_fs.metadata(".").unwrap();
144        assert_eq!(md.file_type, FileType::Directory);
145        assert_eq!(md.len, 0);
146        let md = sandboxed_fs.metadata("file_a").unwrap();
147        assert_eq!(md.file_type, FileType::File);
148        assert_eq!(md.len, 6);
149        let md = unrestricted_fs.metadata("file_a").unwrap();
150        assert_eq!(md.file_type, FileType::File);
151        assert_eq!(md.len, 6);
152
153        // project root traversal
154        assert!(sandboxed_fs.metadata("../deep_fs.zip").is_err());
155        let md = unrestricted_fs.metadata("../deep_fs.zip").unwrap();
156        assert_eq!(md.file_type, FileType::File);
157        assert_eq!(md.len, 2691);
158
159        // fancy project root traversal
160        assert!(sandboxed_fs.metadata("test/../../deep_fs.zip").is_err());
161        let md = unrestricted_fs.metadata("test/../../deep_fs.zip").unwrap();
162        assert_eq!(md.file_type, FileType::File);
163        assert_eq!(md.len, 2691);
164    }
165
166    #[test]
167    fn open_file() {
168        let (unrestricted_fs, sandboxed_fs) = physical_fs("test/folder_a");
169
170        // basic traversal
171        let mut file = sandboxed_fs.open_file("file_a").unwrap();
172        let md = file.metadata().unwrap();
173        assert_eq!(md.file_type, FileType::File);
174        assert_eq!(md.len, 6);
175        assert_eq!(file.read_into_string().unwrap(), "file a");
176        let mut file = unrestricted_fs.open_file("file_a").unwrap();
177        let md = file.metadata().unwrap();
178        assert_eq!(md.file_type, FileType::File);
179        assert_eq!(md.len, 6);
180        assert_eq!(file.read_into_string().unwrap(), "file a");
181
182        // project root traversal
183        assert!(sandboxed_fs.read_dir("../bad.tar.xz").is_err());
184        let mut file = unrestricted_fs.open_file("../bad.tar.xz").unwrap();
185        let md = file.metadata().unwrap();
186        assert_eq!(md.file_type, FileType::File);
187        assert_eq!(md.len, 4);
188        assert_eq!(file.read_into_string().unwrap(), "abcd");
189
190        // fancy project root traversal
191        assert!(sandboxed_fs.read_dir("test/../../bad.tar.xz").is_err());
192        let mut file = unrestricted_fs.open_file("test/../../bad.tar.xz").unwrap();
193        let md = file.metadata().unwrap();
194        assert_eq!(md.file_type, FileType::File);
195        assert_eq!(md.len, 4);
196        assert_eq!(file.read_into_string().unwrap(), "abcd");
197    }
198
199    #[test]
200    fn exists() {
201        let (unrestricted_fs, sandboxed_fs) = physical_fs("test");
202
203        assert!(unrestricted_fs.exists("").unwrap());
204        assert!(sandboxed_fs.exists("").unwrap());
205        assert!(unrestricted_fs.exists(".").unwrap());
206        assert!(sandboxed_fs.exists(".").unwrap());
207        assert!(unrestricted_fs.exists("///").unwrap());
208        assert!(sandboxed_fs.exists("///").unwrap());
209        assert!(unrestricted_fs.exists("\\\\").unwrap());
210        assert!(sandboxed_fs.exists("\\\\").unwrap());
211        assert!(unrestricted_fs.exists("folder_a").unwrap());
212        assert!(sandboxed_fs.exists("folder_a").unwrap());
213        assert!(!unrestricted_fs.exists("folder_c").unwrap());
214        assert!(!sandboxed_fs.exists("folder_c").unwrap());
215        assert!(unrestricted_fs.exists("bad.tar.xz").unwrap());
216        assert!(sandboxed_fs.exists("bad.tar.xz").unwrap());
217        assert!(unrestricted_fs.exists("../Cargo.toml").unwrap());
218        assert!(sandboxed_fs.exists("../Cargo.toml").is_err());
219        assert!(!unrestricted_fs.exists("../Cargo.toml2").unwrap());
220        // TODO: fix this case
221        // assert!(sandboxed_fs.exists("../Cargo.toml2").is_err());
222        assert!(unrestricted_fs.exists("folder_a/../../Cargo.toml").unwrap());
223        assert!(sandboxed_fs.exists("folder_a/../../Cargo.toml").is_err());
224    }
225}