virtual_filesystem/
tar_fs.rs1use 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
9pub struct TarFS {
14 memory_fs: MemoryFS,
15}
16
17pub trait FileSystemFilter {
19 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 pub fn new<R: Read>(archive: R) -> crate::Result<Self> {
38 Self::new_filtered(archive, |_: &_| true)
39 }
40
41 pub fn new_filtered<R: Read, F: FileSystemFilter>(
47 archive: R,
48 filter: F,
49 ) -> crate::Result<Self> {
50 let archive = Archive::new(archive);
52
53 Self::build_fs(archive, filter).map(|fs| Self { memory_fs: fs })
54 }
55
56 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 for entry in archive.entries()? {
68 let mut entry = entry?;
69
70 if entry.header().entry_type() != EntryType::Regular {
72 continue;
73 }
74
75 let entry_path = entry.path()?.into_owned();
76
77 if !filter.should_include(&entry_path) {
79 continue;
80 }
81
82 for parent_path in parent_iter(&entry_path).map(Path::to_string_lossy).rev() {
84 if memory_fs.exists(&parent_path)? {
86 continue;
87 }
88
89 memory_fs.create_dir(&parent_path)?;
90 }
91
92 let mut file_contents = Vec::with_capacity(entry.header().size()? as usize);
94 entry.read_to_end(&mut file_contents)?;
95
96 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}