virtual_filesystem/
tar_fs.rs

1use crate::file::{DirEntry, File, Metadata, OpenOptions};
2use crate::memory_fs::MemoryFS;
3use crate::util::{not_supported, parent_iter};
4use crate::FileSystem;
5use std::io::{Read, Write};
6use std::path::Path;
7use tar::{Archive, EntryType};
8
9/// A filesystem mounted on a Tarball archive, backed by a Memory FS.
10/// Because the FS is backed by memory, all files are immediately loaded
11/// into memory, so `filtered` variants of constructors should be used
12/// to avoid large files that may not need to be accessed.
13pub struct TarFS {
14    memory_fs: MemoryFS,
15}
16
17/// Filters over filesystems.
18pub trait FileSystemFilter {
19    /// Returns true if the path should be included in the filesystem.
20    ///
21    /// # Arguments
22    /// `path`: THe path to the file.  
23    fn should_include(&self, path: &Path) -> bool;
24}
25
26impl<F: Fn(&Path) -> bool> FileSystemFilter for F {
27    fn should_include(&self, path: &Path) -> bool {
28        self(path)
29    }
30}
31
32impl TarFS {
33    /// Creates a new tar-backed filesystem.
34    ///
35    /// # Arguments
36    /// `archive`: The tarball archive itself.
37    pub fn new<R: Read>(archive: R) -> crate::Result<Self> {
38        Self::new_filtered(archive, |_: &_| true)
39    }
40
41    /// Creates a new tar-backed filesystem with filtered contents.
42    ///
43    /// # Arguments
44    /// `archive`: The tarball archive itself.  
45    /// `filter`: A filter that determines which entries are included in the filesystem.  
46    pub fn new_filtered<R: Read, F: FileSystemFilter>(
47        archive: R,
48        filter: F,
49    ) -> crate::Result<Self> {
50        // iterate through each entry and build the memory FS
51        let archive = Archive::new(archive);
52
53        Self::build_fs(archive, filter).map(|fs| Self { memory_fs: fs })
54    }
55
56    /// Builds the memory file system from the archive.
57    ///
58    /// # Arguments
59    /// `archive`: The archive itself.
60    fn build_fs<R: Read, F: FileSystemFilter>(
61        mut archive: Archive<R>,
62        filter: F,
63    ) -> crate::Result<MemoryFS> {
64        let memory_fs = MemoryFS::default();
65
66        // iterate over the archive and read in any files that don't already exist
67        for entry in archive.entries()? {
68            let mut entry = entry?;
69
70            // ignore anything that isn't a regular folder
71            if entry.header().entry_type() != EntryType::Regular {
72                continue;
73            }
74
75            let entry_path = entry.path()?.into_owned();
76
77            // ignore filtered files
78            if !filter.should_include(&entry_path) {
79                continue;
80            }
81
82            // recursively create parent directories
83            for parent_path in parent_iter(&entry_path).map(Path::to_string_lossy).rev() {
84                // only care about directories that exist
85                if memory_fs.exists(&parent_path)? {
86                    continue;
87                }
88
89                memory_fs.create_dir(&parent_path)?;
90            }
91
92            // read the entire entry to a vec
93            let mut file_contents = Vec::with_capacity(entry.header().size()? as usize);
94            entry.read_to_end(&mut file_contents)?;
95
96            // create the file and write all of the contents
97            let mut file = memory_fs.create_file(&format!("/{}", entry_path.to_string_lossy()))?;
98            file.write_all(&file_contents)?;
99        }
100
101        Ok(memory_fs)
102    }
103}
104
105impl FileSystem for TarFS {
106    fn create_dir(&self, _path: &str) -> crate::Result<()> {
107        Err(not_supported())
108    }
109
110    fn metadata(&self, path: &str) -> crate::Result<Metadata> {
111        self.memory_fs.metadata(path)
112    }
113
114    fn open_file_options(&self, path: &str, options: &OpenOptions) -> crate::Result<Box<dyn File>> {
115        if options.write {
116            return Err(not_supported());
117        }
118
119        self.memory_fs.open_file_options(path, options)
120    }
121
122    fn read_dir(
123        &self,
124        path: &str,
125    ) -> crate::Result<Box<dyn Iterator<Item = crate::Result<DirEntry>>>> {
126        self.memory_fs.read_dir(path)
127    }
128
129    fn remove_dir(&self, _path: &str) -> crate::Result<()> {
130        Err(not_supported())
131    }
132
133    fn remove_file(&self, _path: &str) -> crate::Result<()> {
134        Err(not_supported())
135    }
136}
137
138#[cfg(test)]
139mod test {
140    use std::fs::File;
141    use std::io::Read;
142
143    use crate::FileSystem;
144    use xz::read::XzDecoder;
145
146    use super::TarFS;
147
148    #[test]
149    fn bad_xz() {
150        let file = File::open("test/bad.tar.xz").unwrap();
151        let bad_archive = TarFS::new(XzDecoder::new(file));
152
153        assert!(bad_archive.is_err());
154    }
155
156    #[test]
157    fn single_file_xz_empty() {
158        let file = File::open("test/empty.tar.xz").unwrap();
159        let archive = TarFS::new(XzDecoder::new(file)).unwrap();
160
161        let files = archive.read_dir("").unwrap().collect::<Vec<_>>();
162
163        assert_eq!(files.len(), 1);
164
165        let mut empty_file = archive.open_file("/empty").unwrap();
166        let mut file_contents = vec![];
167        empty_file.read_to_end(&mut file_contents).unwrap();
168
169        assert_eq!(file_contents.len(), 0);
170    }
171
172    #[test]
173    fn single_file_xz_not_empty() {
174        let file = File::open("test/not_empty.tar.xz").unwrap();
175        let archive = TarFS::new(XzDecoder::new(file)).unwrap();
176
177        let files = archive.read_dir("").unwrap().collect::<Vec<_>>();
178
179        assert_eq!(files.len(), 1);
180
181        let mut file = archive.open_file("/not_empty").unwrap();
182        let mut file_contents = String::new();
183        file.read_to_string(&mut file_contents).unwrap();
184
185        assert_eq!(file_contents, "something interesting\n");
186    }
187
188    #[test]
189    fn deep_fs_xz() {
190        let file = File::open("test/deep_fs.tar.xz").unwrap();
191        let archive = TarFS::new(XzDecoder::new(file)).unwrap();
192
193        let files = archive.read_dir("folder").unwrap().collect::<Vec<_>>();
194
195        assert_eq!(files.len(), 2);
196
197        let mut file = archive.open_file("/folder/and/it/desc").unwrap();
198        let mut file_contents = String::new();
199        file.read_to_string(&mut file_contents).unwrap();
200
201        assert_eq!(file_contents, "it\n");
202    }
203}