virtual_filesystem/
scoped_fs.rs1use crate::file::{DirEntry, File, Metadata, OpenOptions};
2use crate::util::{make_relative, normalize_path};
3use crate::FileSystem;
4use std::io::{self, ErrorKind};
5use std::path::PathBuf;
6
7pub struct ScopedFS<FS: FileSystem> {
10 inner: FS,
11 root: PathBuf, }
13
14impl<FS: FileSystem> ScopedFS<FS> {
15 pub fn new(root: &str, inner: FS) -> Self {
16 Self {
17 inner,
18 root: normalize_path(make_relative(root)),
19 }
20 }
21
22 fn resolve(&self, path: &str) -> crate::Result<String> {
23 let joined = normalize_path(self.root.join(make_relative(path)));
24 if !joined.starts_with(&self.root) {
25 return Err(io::Error::new(ErrorKind::PermissionDenied, "Traversal prevented"));
26 }
27 Ok(joined.to_str().unwrap().to_owned())
28 }
29}
30
31impl<FS: FileSystem> FileSystem for ScopedFS<FS> {
32 fn create_dir(&self, path: &str) -> crate::Result<()> {
33 self.inner.create_dir(&self.resolve(path)?)
34 }
35
36 fn metadata(&self, path: &str) -> crate::Result<Metadata> {
37 self.inner.metadata(&self.resolve(path)?)
38 }
39
40 fn open_file_options(&self, path: &str, options: &OpenOptions) -> crate::Result<Box<dyn File>> {
41 self.inner.open_file_options(&self.resolve(path)?, options)
42 }
43
44 fn read_dir(&self, path: &str) -> crate::Result<Box<dyn Iterator<Item = crate::Result<DirEntry>>>> {
45 self.inner.read_dir(&self.resolve(path)?)
46 }
47
48 fn remove_dir(&self, path: &str) -> crate::Result<()> {
49 self.inner.remove_dir(&self.resolve(path)?)
50 }
51
52 fn remove_file(&self, path: &str) -> crate::Result<()> {
53 self.inner.remove_file(&self.resolve(path)?)
54 }
55}
56
57#[cfg(test)]
58mod test {
59 use super::ScopedFS;
60 use crate::memory_fs::MemoryFS;
61 use crate::FileSystem;
62 use std::io::{ErrorKind, Write};
63
64 fn make_fs() -> ScopedFS<MemoryFS> {
68 let mem = MemoryFS::default();
69 mem.create_dir("src").unwrap();
70 write!(mem.create_file("src/init.luau").unwrap(), "entry").unwrap();
71 write!(mem.create_file("secret.txt").unwrap(), "secret").unwrap();
72 ScopedFS::new("src", mem)
73 }
74
75 #[test]
76 fn reads_file_inside_scope() {
77 let fs = make_fs();
78 let content = fs.open_file("init.luau").unwrap().read_into_string().unwrap();
79 assert_eq!(content, "entry");
80 }
81
82 #[test]
83 fn traversal_single_dotdot() {
84 let fs = make_fs();
85 let err = fs.open_file("../secret.txt").err().unwrap();
86 assert_eq!(err.kind(), ErrorKind::PermissionDenied);
87 }
88
89 #[test]
90 fn traversal_dotdot_chain() {
91 let fs = make_fs();
92 let err = fs.open_file("../../secret.txt").err().unwrap();
93 assert_eq!(err.kind(), ErrorKind::PermissionDenied);
94 }
95
96 #[test]
97 fn traversal_descend_then_escape() {
98 let fs = make_fs();
100 let err = fs.open_file("subdir/../../secret.txt").err().unwrap();
101 assert_eq!(err.kind(), ErrorKind::PermissionDenied);
102 }
103
104 #[test]
105 fn root_resolves_to_scope_root() {
106 let fs = make_fs();
107 assert!(fs.metadata("/").unwrap().is_directory());
109 assert!(fs.metadata("").unwrap().is_directory());
110 }
111}