virtual_filesystem/
zip_fs.rs

1use crate::file::{DirEntry, File, FileType, Metadata, OpenOptions};
2use crate::util::{make_relative, not_found, not_supported, parent_iter};
3use crate::{util, FileSystem};
4use itertools::Itertools;
5use parking_lot::Mutex;
6use std::collections::{HashMap, HashSet};
7use std::fmt::Debug;
8use std::io;
9use std::io::{Cursor, ErrorKind, Read, Seek, SeekFrom, Write};
10use std::path::{Path, PathBuf};
11use zip::read::ZipFile;
12use zip::result::{ZipError, ZipResult};
13use zip::ZipArchive;
14
15/// A virtual FileSystem backed by a ZIP file. Only supports read operations for now.
16#[derive(Debug)]
17pub struct ZipFS<R: Read + Seek> {
18    zip_file: Mutex<ZipArchive<R>>,
19    directories: HashSet<PathBuf>,
20    normalized_lower_to_path: HashMap<PathBuf, PathBuf>,
21}
22
23impl<R: Read + Seek> ZipFS<R> {
24    /// Mounts a ZIP file onto the local filesystem.
25    pub fn new(zip_file: R) -> ZipResult<Self> {
26        let zip_file = ZipArchive::new(zip_file)?;
27
28        // collect folders
29        let mut directories = HashSet::from_iter([Path::new("").to_owned()]);
30        let mut normalized_lower_to_path = HashMap::new();
31        for file_name in zip_file.file_names() {
32            for parent in parent_iter(Path::new(&file_name.to_lowercase())) {
33                directories.insert(parent.to_owned());
34            }
35
36            let normalized = Self::normalize_path(file_name);
37            let lower = PathBuf::from(
38                normalized
39                    .to_str()
40                    .ok_or_else(not_supported)?
41                    .to_lowercase(),
42            );
43
44            normalized_lower_to_path.insert(lower, normalized);
45        }
46
47        Ok(Self {
48            zip_file: Mutex::new(zip_file),
49            directories,
50            normalized_lower_to_path,
51        })
52    }
53
54    fn convert_error<T>(maybe_error: ZipResult<T>) -> crate::Result<T> {
55        maybe_error.map_err(|err| match err {
56            ZipError::FileNotFound => {
57                io::Error::new(ErrorKind::NotFound, "File not found in zip archive")
58            }
59            ZipError::Io(io_error) => io_error,
60            ZipError::InvalidArchive(error_str) => {
61                io::Error::new(ErrorKind::InvalidData, error_str)
62            }
63            ZipError::UnsupportedArchive(error_str) => {
64                io::Error::new(ErrorKind::Unsupported, error_str)
65            }
66        })
67    }
68
69    /// Returns the cased path for the given normalized path.
70    fn get_cased_path(&self, normalized_path: &Path) -> Option<&PathBuf> {
71        // find the cased path
72        let lowercase_path = PathBuf::from(normalized_path.to_str()?.to_lowercase());
73        self.normalized_lower_to_path.get(&lowercase_path)
74    }
75
76    fn normalize_path<P: AsRef<Path>>(path: P) -> PathBuf {
77        // as far as I can tell, zip files are relative from the root
78        make_relative(util::normalize_path(path))
79    }
80
81    fn with_file<RV, F: FnOnce(ZipFile) -> RV>(
82        &self,
83        normalized_path: &Path,
84        f: F,
85    ) -> crate::Result<RV> {
86        // find the cased path
87        let cased_path = self.get_cased_path(normalized_path).ok_or_else(not_found)?;
88
89        let mut zip_file = self.zip_file.lock();
90
91        let entry =
92            Self::convert_error(zip_file.by_name(cased_path.to_str().ok_or_else(not_supported)?))?;
93        Ok(f(entry))
94    }
95}
96
97impl<R: Read + Seek> FileSystem for ZipFS<R> {
98    fn create_dir(&self, _path: &str) -> crate::Result<()> {
99        Err(not_supported())
100    }
101
102    fn metadata(&self, path: &str) -> crate::Result<Metadata> {
103        let normalized_path = Self::normalize_path(path);
104
105        // try directories first, which are lowercase
106        let lowercase_path = PathBuf::from(
107            normalized_path
108                .as_path()
109                .to_str()
110                .ok_or_else(not_supported)?
111                .to_lowercase(),
112        );
113        if self.directories.get(&lowercase_path).is_some() {
114            return Ok(Metadata {
115                file_type: FileType::Directory,
116                len: 0,
117            });
118        }
119
120        // now files
121        self.with_file(normalized_path.as_path(), |file| Metadata {
122            file_type: FileType::File,
123            len: file.size(),
124        })
125    }
126
127    fn open_file_options(&self, path: &str, options: &OpenOptions) -> crate::Result<Box<dyn File>> {
128        // ensure we only want to read
129        if !options.read || options.write {
130            return Err(not_supported());
131        }
132
133        // open the file and read into a readable buffer
134        self.with_file::<crate::Result<Box<dyn File>>, _>(
135            &Self::normalize_path(path),
136            |mut entry| {
137                let mut contents = Vec::with_capacity(entry.size() as usize);
138                entry.read_to_end(&mut contents)?;
139                Ok(Box::new(ZipFileContents {
140                    inner: Cursor::new(contents),
141                }))
142            },
143        )?
144    }
145
146    fn read_dir(
147        &self,
148        path: &str,
149    ) -> crate::Result<Box<dyn Iterator<Item = crate::Result<DirEntry>>>> {
150        let directory = Self::normalize_path(path);
151
152        // if there are no folders with this path, error out
153        if !self.directories.contains(&directory) {
154            return Err(not_found());
155        }
156
157        let mut zip_file = self.zip_file.lock();
158        let mut files = HashMap::new();
159        for file in zip_file
160            .file_names()
161            .map(|file_name| file_name.to_owned())
162            .collect_vec()
163        {
164            let normalized_file = Self::normalize_path(&file);
165
166            let mut add_parent = |normalized_path: &Path, metadata| {
167                if normalized_path.parent()? == directory {
168                    files.insert(PathBuf::from(normalized_path.file_name()?), metadata);
169                }
170
171                Some(())
172            };
173
174            // if the file's parent is the directory, it's in the directory
175            add_parent(
176                &normalized_file,
177                Metadata::file(zip_file.by_name(&file)?.size()),
178            );
179
180            // if the file's parent directory is in the directory, add it
181            if let Some(file_parent) = normalized_file.parent() {
182                add_parent(file_parent, Metadata::directory());
183            }
184        }
185
186        Ok(Box::new(
187            files
188                .into_iter()
189                .map(|(path, metadata)| Ok(DirEntry { path, metadata })),
190        ))
191    }
192
193    fn remove_dir(&self, _path: &str) -> crate::Result<()> {
194        Err(not_supported())
195    }
196
197    fn remove_file(&self, _path: &str) -> crate::Result<()> {
198        Err(not_supported())
199    }
200}
201
202struct ZipFileContents {
203    inner: Cursor<Vec<u8>>,
204}
205
206impl Read for ZipFileContents {
207    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
208        self.inner.read(buf)
209    }
210}
211
212impl Seek for ZipFileContents {
213    fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
214        self.inner.seek(pos)
215    }
216}
217
218impl Write for ZipFileContents {
219    fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
220        Err(not_supported())
221    }
222
223    fn flush(&mut self) -> io::Result<()> {
224        Err(not_supported())
225    }
226}
227
228impl File for ZipFileContents {
229    fn metadata(&self) -> crate::Result<Metadata> {
230        Ok(Metadata::file(self.inner.get_ref().len() as u64))
231    }
232}
233
234#[cfg(test)]
235mod test {
236    use crate::file::{FileType, Metadata};
237    use crate::zip_fs::ZipFS;
238    use crate::FileSystem;
239    use std::collections::BTreeMap;
240    use std::fs::File;
241
242    fn read_directory(fs: &ZipFS<File>, path: &str) -> crate::Result<BTreeMap<String, Metadata>> {
243        Ok(fs
244            .read_dir(path)?
245            .map(|entry| {
246                let entry = entry.unwrap();
247                (entry.path.to_str().unwrap().to_owned(), entry.metadata)
248            })
249            .collect::<BTreeMap<_, _>>())
250    }
251
252    fn zip_fs() -> ZipFS<File> {
253        ZipFS::new(File::open("test/deep_fs.zip").unwrap()).unwrap()
254    }
255
256    #[test]
257    fn read_dir() {
258        let fs = zip_fs();
259
260        let root = read_directory(&fs, "").unwrap();
261        itertools::assert_equal(root.keys(), vec!["file", "folder"]);
262        itertools::assert_equal(
263            root.values().map(|md| md.file_type),
264            vec![FileType::File, FileType::Directory],
265        );
266        itertools::assert_equal(root.values().map(|md| md.len), vec![2571, 0]);
267
268        let another_root = read_directory(&fs, ".").unwrap();
269        assert_eq!(root, another_root);
270
271        let another_root = read_directory(&fs, "///").unwrap();
272        assert_eq!(root, another_root);
273
274        let another_root = read_directory(&fs, "\\").unwrap();
275        assert_eq!(root, another_root);
276
277        let another_root = read_directory(&fs, "///test/../").unwrap();
278        assert_eq!(root, another_root);
279
280        let deeper_root = read_directory(&fs, "folder/and/it").unwrap();
281        itertools::assert_equal(deeper_root.keys(), vec!["desc", "goes"]);
282
283        assert!(read_directory(&fs, "file").is_err());
284        assert!(read_directory(&fs, "not_a_real_path").is_err());
285    }
286
287    #[test]
288    fn open_file() {
289        let fs = zip_fs();
290
291        let mut file = fs.open_file("file").unwrap();
292        let md = file.metadata().unwrap();
293        assert_eq!(md.file_type, FileType::File);
294        assert_eq!(md.len, 2571);
295
296        let file = file.read_into_string().unwrap();
297        assert!(file.starts_with("Lorem ipsum dolor"));
298
299        let indirect_file = fs
300            .open_file("///something/..\\file")
301            .unwrap()
302            .read_into_string()
303            .unwrap();
304        assert_eq!(indirect_file, file);
305
306        let nested_file = fs
307            .open_file("folder/and/it/goes/deeper/desc")
308            .unwrap()
309            .read_into_string()
310            .unwrap();
311        assert_eq!(nested_file, "deeper\n")
312    }
313
314    #[test]
315    fn metadata() {
316        let fs = zip_fs();
317
318        let md = fs.metadata("file").unwrap();
319        assert_eq!(md.file_type, FileType::File);
320        assert_eq!(md.len, 2571);
321
322        let md = fs.metadata("folder").unwrap();
323        assert_eq!(md.file_type, FileType::Directory);
324        assert_eq!(md.len, 0);
325
326        let md = fs.metadata("folder/and/it/goes/desc").unwrap();
327        assert_eq!(md.file_type, FileType::File);
328        assert_eq!(md.len, 5);
329    }
330
331    #[test]
332    fn exists() {
333        let fs = zip_fs();
334
335        assert!(fs.exists("/").unwrap());
336        assert!(fs.exists("").unwrap());
337        assert!(fs.exists("file").unwrap());
338        assert!(fs.exists("FiLe").unwrap());
339        assert!(!fs.exists("no_file").unwrap());
340        assert!(fs.exists("folder").unwrap());
341        assert!(fs.exists("folDeR").unwrap());
342        assert!(fs.exists("folder/and/it").unwrap());
343        assert!(fs.exists("folder/anD/iT").unwrap());
344        assert!(fs.exists("folder/and/it/desc").unwrap());
345        assert!(!fs.exists("folder/and/it/does/not").unwrap());
346        assert!(fs.exists("///test/something_else/../../file").unwrap());
347        assert!(fs.exists("///test/something_elsE/../../file").unwrap());
348    }
349}