blob: a91642cc5623ffed839e411e0268ae4c7e2a748b [file] [log] [blame]
Jiyong Park331d1ea2021-05-10 11:01:23 +09001/*
2 * Copyright (C) 2021 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17//! `zipfuse` is a FUSE filesystem for zip archives. It provides transparent access to the files
18//! in a zip archive. This filesystem does not supporting writing files back to the zip archive.
19//! The filesystem has to be mounted read only.
20
21mod inode;
22
23use anyhow::Result;
24use clap::{App, Arg};
25use fuse::filesystem::*;
26use fuse::mount::*;
27use std::collections::HashMap;
28use std::convert::TryFrom;
Jiyong Park851f68a2021-05-11 21:41:25 +090029use std::ffi::{CStr, CString};
Jiyong Park331d1ea2021-05-10 11:01:23 +090030use std::fs::{File, OpenOptions};
31use std::io;
32use std::io::Read;
Jiyong Park63a95cf2021-05-13 19:20:30 +090033use std::mem::size_of;
Jiyong Park331d1ea2021-05-10 11:01:23 +090034use std::os::unix::io::AsRawFd;
35use std::path::Path;
36use std::sync::Mutex;
37
38use crate::inode::{DirectoryEntry, Inode, InodeData, InodeKind, InodeTable};
39
40fn main() -> Result<()> {
41 let matches = App::new("zipfuse")
Jiyong Park6a762db2021-05-31 14:00:52 +090042 .arg(
43 Arg::with_name("options")
44 .short("o")
45 .takes_value(true)
46 .required(false)
Chris Wailes68c39f82021-07-27 16:03:44 -070047 .help("Comma separated list of mount options"),
Jiyong Park6a762db2021-05-31 14:00:52 +090048 )
Jiyong Park331d1ea2021-05-10 11:01:23 +090049 .arg(Arg::with_name("ZIPFILE").required(true))
50 .arg(Arg::with_name("MOUNTPOINT").required(true))
51 .get_matches();
52
53 let zip_file = matches.value_of("ZIPFILE").unwrap().as_ref();
54 let mount_point = matches.value_of("MOUNTPOINT").unwrap().as_ref();
Jiyong Park6a762db2021-05-31 14:00:52 +090055 let options = matches.value_of("options");
56 run_fuse(zip_file, mount_point, options)?;
Jiyong Park331d1ea2021-05-10 11:01:23 +090057 Ok(())
58}
59
60/// Runs a fuse filesystem by mounting `zip_file` on `mount_point`.
Jiyong Park6a762db2021-05-31 14:00:52 +090061pub fn run_fuse(zip_file: &Path, mount_point: &Path, extra_options: Option<&str>) -> Result<()> {
Jiyong Park331d1ea2021-05-10 11:01:23 +090062 const MAX_READ: u32 = 1 << 20; // TODO(jiyong): tune this
63 const MAX_WRITE: u32 = 1 << 13; // This is a read-only filesystem
64
65 let dev_fuse = OpenOptions::new().read(true).write(true).open("/dev/fuse")?;
66
Jiyong Park6a762db2021-05-31 14:00:52 +090067 let mut mount_options = vec![
68 MountOption::FD(dev_fuse.as_raw_fd()),
69 MountOption::RootMode(libc::S_IFDIR | libc::S_IXUSR | libc::S_IXGRP | libc::S_IXOTH),
70 MountOption::AllowOther,
71 MountOption::UserId(0),
72 MountOption::GroupId(0),
73 MountOption::MaxRead(MAX_READ),
74 ];
75 if let Some(value) = extra_options {
76 mount_options.push(MountOption::Extra(value));
77 }
78
Jiyong Park331d1ea2021-05-10 11:01:23 +090079 fuse::mount(
80 mount_point,
81 "zipfuse",
82 libc::MS_NOSUID | libc::MS_NODEV | libc::MS_RDONLY,
Jiyong Park6a762db2021-05-31 14:00:52 +090083 &mount_options,
Jiyong Park331d1ea2021-05-10 11:01:23 +090084 )?;
85 Ok(fuse::worker::start_message_loop(dev_fuse, MAX_READ, MAX_WRITE, ZipFuse::new(zip_file)?)?)
86}
87
88struct ZipFuse {
89 zip_archive: Mutex<zip::ZipArchive<File>>,
Jiyong Parkf5ff33c2021-08-30 22:32:19 +090090 raw_file: Mutex<File>,
Jiyong Park331d1ea2021-05-10 11:01:23 +090091 inode_table: InodeTable,
Jiyong Parkf5ff33c2021-08-30 22:32:19 +090092 open_files: Mutex<HashMap<Handle, OpenFile>>,
Jiyong Park331d1ea2021-05-10 11:01:23 +090093 open_dirs: Mutex<HashMap<Handle, OpenDirBuf>>,
94}
95
Jiyong Parkf5ff33c2021-08-30 22:32:19 +090096/// Represents a [`ZipFile`] that is opened.
97struct OpenFile {
Jiyong Park331d1ea2021-05-10 11:01:23 +090098 open_count: u32, // multiple opens share the buf because this is a read-only filesystem
Jiyong Parkf5ff33c2021-08-30 22:32:19 +090099 content: OpenFileContent,
100}
101
102/// Holds the content of a [`ZipFile`]. Depending on whether it is compressed or not, the
103/// entire content is stored, or only the zip index is stored.
104enum OpenFileContent {
105 Compressed(Box<[u8]>),
106 Uncompressed(usize), // zip index
Jiyong Park331d1ea2021-05-10 11:01:23 +0900107}
108
109/// Holds the directory entries in a directory opened by [`opendir`].
110struct OpenDirBuf {
111 open_count: u32,
112 buf: Box<[(CString, DirectoryEntry)]>,
113}
114
115type Handle = u64;
116
117fn ebadf() -> io::Error {
118 io::Error::from_raw_os_error(libc::EBADF)
119}
120
121fn timeout_max() -> std::time::Duration {
122 std::time::Duration::new(u64::MAX, 1_000_000_000 - 1)
123}
124
125impl ZipFuse {
126 fn new(zip_file: &Path) -> Result<ZipFuse> {
127 // TODO(jiyong): Use O_DIRECT to avoid double caching.
128 // `.custom_flags(nix::fcntl::OFlag::O_DIRECT.bits())` currently doesn't work.
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900129 let f = File::open(zip_file)?;
Jiyong Park331d1ea2021-05-10 11:01:23 +0900130 let mut z = zip::ZipArchive::new(f)?;
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900131 // Open the same file again so that we can directly access it when accessing
132 // uncompressed zip_file entries in it. `ZipFile` doesn't implement `Seek`.
133 let raw_file = File::open(zip_file)?;
Jiyong Park331d1ea2021-05-10 11:01:23 +0900134 let it = InodeTable::from_zip(&mut z)?;
135 Ok(ZipFuse {
136 zip_archive: Mutex::new(z),
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900137 raw_file: Mutex::new(raw_file),
Jiyong Park331d1ea2021-05-10 11:01:23 +0900138 inode_table: it,
139 open_files: Mutex::new(HashMap::new()),
140 open_dirs: Mutex::new(HashMap::new()),
141 })
142 }
143
144 fn find_inode(&self, inode: Inode) -> io::Result<&InodeData> {
145 self.inode_table.get(inode).ok_or_else(ebadf)
146 }
147
Jiyong Parkd5df9562021-05-13 00:50:23 +0900148 // TODO(jiyong) remove this. Right now this is needed to do the nlink_t to u64 conversion below
149 // on aosp_x86_64 target. That however is a useless conversion on other targets.
150 #[allow(clippy::useless_conversion)]
Jiyong Park331d1ea2021-05-10 11:01:23 +0900151 fn stat_from(&self, inode: Inode) -> io::Result<libc::stat64> {
152 let inode_data = self.find_inode(inode)?;
153 let mut st = unsafe { std::mem::MaybeUninit::<libc::stat64>::zeroed().assume_init() };
154 st.st_dev = 0;
Jiyong Parkd5df9562021-05-13 00:50:23 +0900155 st.st_nlink = if let Some(directory) = inode_data.get_directory() {
156 (2 + directory.len() as libc::nlink_t).into()
Jiyong Park331d1ea2021-05-10 11:01:23 +0900157 } else {
158 1
159 };
160 st.st_ino = inode;
161 st.st_mode = if inode_data.is_dir() { libc::S_IFDIR } else { libc::S_IFREG };
162 st.st_mode |= inode_data.mode;
163 st.st_uid = 0;
164 st.st_gid = 0;
165 st.st_size = i64::try_from(inode_data.size).unwrap_or(i64::MAX);
166 Ok(st)
167 }
168}
169
170impl fuse::filesystem::FileSystem for ZipFuse {
171 type Inode = Inode;
172 type Handle = Handle;
173 type DirIter = DirIter;
174
175 fn init(&self, _capable: FsOptions) -> std::io::Result<FsOptions> {
176 // The default options added by the fuse crate are fine. We don't have additional options.
177 Ok(FsOptions::empty())
178 }
179
180 fn lookup(&self, _ctx: Context, parent: Self::Inode, name: &CStr) -> io::Result<Entry> {
181 let inode = self.find_inode(parent)?;
182 let directory = inode.get_directory().ok_or_else(ebadf)?;
Jiyong Park331d1ea2021-05-10 11:01:23 +0900183 let entry = directory.get(name);
184 match entry {
185 Some(e) => Ok(Entry {
186 inode: e.inode,
187 generation: 0,
188 attr: self.stat_from(e.inode)?,
189 attr_timeout: timeout_max(), // this is a read-only fs
190 entry_timeout: timeout_max(),
191 }),
192 _ => Err(io::Error::from_raw_os_error(libc::ENOENT)),
193 }
194 }
195
196 fn getattr(
197 &self,
198 _ctx: Context,
199 inode: Self::Inode,
200 _handle: Option<Self::Handle>,
201 ) -> io::Result<(libc::stat64, std::time::Duration)> {
202 let st = self.stat_from(inode)?;
203 Ok((st, timeout_max()))
204 }
205
206 fn open(
207 &self,
208 _ctx: Context,
209 inode: Self::Inode,
210 _flags: u32,
211 ) -> io::Result<(Option<Self::Handle>, fuse::filesystem::OpenOptions)> {
212 let mut open_files = self.open_files.lock().unwrap();
213 let handle = inode as Handle;
214
215 // If the file is already opened, just increase the reference counter. If not, read the
216 // entire file content to the buffer. When `read` is called, a portion of the buffer is
217 // copied to the kernel.
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900218 if let Some(file) = open_files.get_mut(&handle) {
219 if file.open_count == 0 {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900220 return Err(ebadf());
221 }
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900222 file.open_count += 1;
Jiyong Park331d1ea2021-05-10 11:01:23 +0900223 } else {
224 let inode_data = self.find_inode(inode)?;
225 let zip_index = inode_data.get_zip_index().ok_or_else(ebadf)?;
226 let mut zip_archive = self.zip_archive.lock().unwrap();
227 let mut zip_file = zip_archive.by_index(zip_index)?;
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900228 let content = match zip_file.compression() {
229 zip::CompressionMethod::Stored => OpenFileContent::Uncompressed(zip_index),
230 _ => {
231 if let Some(mode) = zip_file.unix_mode() {
232 let is_reg_file = zip_file.is_file();
233 let is_executable =
234 mode & (libc::S_IXUSR | libc::S_IXGRP | libc::S_IXOTH) != 0;
235 if is_reg_file && is_executable {
236 log::warn!(
237 "Executable file {:?} is stored compressed. Consider \
238 storing it uncompressed to save memory",
239 zip_file.mangled_name()
240 );
241 }
242 }
243 let mut buf = Vec::with_capacity(inode_data.size as usize);
244 zip_file.read_to_end(&mut buf)?;
245 OpenFileContent::Compressed(buf.into_boxed_slice())
246 }
247 };
248 open_files.insert(handle, OpenFile { open_count: 1, content });
Jiyong Park331d1ea2021-05-10 11:01:23 +0900249 }
250 // Note: we don't return `DIRECT_IO` here, because then applications wouldn't be able to
251 // mmap the files.
252 Ok((Some(handle), fuse::filesystem::OpenOptions::empty()))
253 }
254
255 fn release(
256 &self,
257 _ctx: Context,
258 inode: Self::Inode,
259 _flags: u32,
260 _handle: Self::Handle,
261 _flush: bool,
262 _flock_release: bool,
263 _lock_owner: Option<u64>,
264 ) -> io::Result<()> {
265 // Releases the buffer for the `handle` when it is opened for nobody. While this is good
266 // for saving memory, this has a performance implication because we need to decompress
267 // again when the same file is opened in the future.
268 let mut open_files = self.open_files.lock().unwrap();
269 let handle = inode as Handle;
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900270 if let Some(file) = open_files.get_mut(&handle) {
271 if file.open_count.checked_sub(1).ok_or_else(ebadf)? == 0 {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900272 open_files.remove(&handle);
273 }
274 Ok(())
275 } else {
276 Err(ebadf())
277 }
278 }
279
280 fn read<W: io::Write + ZeroCopyWriter>(
281 &self,
282 _ctx: Context,
283 _inode: Self::Inode,
284 handle: Self::Handle,
285 mut w: W,
286 size: u32,
287 offset: u64,
288 _lock_owner: Option<u64>,
289 _flags: u32,
290 ) -> io::Result<usize> {
291 let open_files = self.open_files.lock().unwrap();
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900292 let file = open_files.get(&handle).ok_or_else(ebadf)?;
293 if file.open_count == 0 {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900294 return Err(ebadf());
295 }
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900296 Ok(match &file.content {
297 OpenFileContent::Uncompressed(zip_index) => {
298 let mut zip_archive = self.zip_archive.lock().unwrap();
299 let zip_file = zip_archive.by_index(*zip_index)?;
300 let start = zip_file.data_start() + offset;
301 let remaining_size = zip_file.size() - offset;
302 let size = std::cmp::min(remaining_size, size.into());
303
304 let mut raw_file = self.raw_file.lock().unwrap();
305 w.write_from(&mut raw_file, size as usize, start)?
306 }
307 OpenFileContent::Compressed(buf) => {
308 let start = offset as usize;
309 let end = start + size as usize;
310 let end = std::cmp::min(end, buf.len());
311 w.write(&buf[start..end])?
312 }
313 })
Jiyong Park331d1ea2021-05-10 11:01:23 +0900314 }
315
316 fn opendir(
317 &self,
318 _ctx: Context,
319 inode: Self::Inode,
320 _flags: u32,
321 ) -> io::Result<(Option<Self::Handle>, fuse::filesystem::OpenOptions)> {
322 let mut open_dirs = self.open_dirs.lock().unwrap();
323 let handle = inode as Handle;
324 if let Some(odb) = open_dirs.get_mut(&handle) {
325 if odb.open_count == 0 {
326 return Err(ebadf());
327 }
328 odb.open_count += 1;
329 } else {
330 let inode_data = self.find_inode(inode)?;
331 let directory = inode_data.get_directory().ok_or_else(ebadf)?;
332 let mut buf: Vec<(CString, DirectoryEntry)> = Vec::with_capacity(directory.len());
333 for (name, dir_entry) in directory.iter() {
334 let name = CString::new(name.as_bytes()).unwrap();
335 buf.push((name, dir_entry.clone()));
336 }
337 open_dirs.insert(handle, OpenDirBuf { open_count: 1, buf: buf.into_boxed_slice() });
338 }
Jiyong Park63a95cf2021-05-13 19:20:30 +0900339 Ok((Some(handle), fuse::filesystem::OpenOptions::CACHE_DIR))
Jiyong Park331d1ea2021-05-10 11:01:23 +0900340 }
341
342 fn releasedir(
343 &self,
344 _ctx: Context,
345 inode: Self::Inode,
346 _flags: u32,
347 _handle: Self::Handle,
348 ) -> io::Result<()> {
349 let mut open_dirs = self.open_dirs.lock().unwrap();
350 let handle = inode as Handle;
351 if let Some(odb) = open_dirs.get_mut(&handle) {
352 if odb.open_count.checked_sub(1).ok_or_else(ebadf)? == 0 {
353 open_dirs.remove(&handle);
354 }
355 Ok(())
356 } else {
357 Err(ebadf())
358 }
359 }
360
361 fn readdir(
362 &self,
363 _ctx: Context,
364 inode: Self::Inode,
365 _handle: Self::Handle,
366 size: u32,
367 offset: u64,
368 ) -> io::Result<Self::DirIter> {
369 let open_dirs = self.open_dirs.lock().unwrap();
370 let handle = inode as Handle;
371 let odb = open_dirs.get(&handle).ok_or_else(ebadf)?;
372 if odb.open_count == 0 {
373 return Err(ebadf());
374 }
375 let buf = &odb.buf;
376 let start = offset as usize;
Jiyong Park63a95cf2021-05-13 19:20:30 +0900377
378 // Estimate the size of each entry will take space in the buffer. See
379 // external/crosvm/fuse/src/server.rs#add_dirent
380 let mut estimate: usize = 0; // estimated number of bytes we will be writing
381 let mut end = start; // index in `buf`
382 while estimate < size as usize && end < buf.len() {
383 let dirent_size = size_of::<fuse::sys::Dirent>();
384 let name_size = buf[end].0.to_bytes().len();
385 estimate += (dirent_size + name_size + 7) & !7; // round to 8 byte boundary
386 end += 1;
387 }
388
Jiyong Park331d1ea2021-05-10 11:01:23 +0900389 let mut new_buf = Vec::with_capacity(end - start);
390 // The portion of `buf` is *copied* to the iterator. This is not ideal, but inevitable
391 // because the `name` field in `fuse::filesystem::DirEntry` is `&CStr` not `CString`.
392 new_buf.extend_from_slice(&buf[start..end]);
393 Ok(DirIter { inner: new_buf, offset, cur: 0 })
394 }
395}
396
397struct DirIter {
398 inner: Vec<(CString, DirectoryEntry)>,
399 offset: u64, // the offset where this iterator begins. `next` doesn't change this.
400 cur: usize, // the current index in `inner`. `next` advances this.
401}
402
403impl fuse::filesystem::DirectoryIterator for DirIter {
404 fn next(&mut self) -> Option<fuse::filesystem::DirEntry> {
405 if self.cur >= self.inner.len() {
406 return None;
407 }
408
409 let (name, entry) = &self.inner[self.cur];
410 self.cur += 1;
411 Some(fuse::filesystem::DirEntry {
412 ino: entry.inode as libc::ino64_t,
413 offset: self.offset + self.cur as u64,
414 type_: match entry.kind {
415 InodeKind::Directory => libc::DT_DIR.into(),
416 InodeKind::File => libc::DT_REG.into(),
417 },
418 name,
419 })
420 }
421}
422
423#[cfg(test)]
424mod tests {
425 use anyhow::{bail, Result};
426 use nix::sys::statfs::{statfs, FsType};
Jiyong Park63a95cf2021-05-13 19:20:30 +0900427 use std::collections::BTreeSet;
Jiyong Park331d1ea2021-05-10 11:01:23 +0900428 use std::fs;
429 use std::fs::File;
430 use std::io::Write;
431 use std::path::{Path, PathBuf};
432 use std::time::{Duration, Instant};
433 use zip::write::FileOptions;
434
435 #[cfg(not(target_os = "android"))]
436 fn start_fuse(zip_path: &Path, mnt_path: &Path) {
437 let zip_path = PathBuf::from(zip_path);
438 let mnt_path = PathBuf::from(mnt_path);
439 std::thread::spawn(move || {
Jiyong Park6a762db2021-05-31 14:00:52 +0900440 crate::run_fuse(&zip_path, &mnt_path, None).unwrap();
Jiyong Park331d1ea2021-05-10 11:01:23 +0900441 });
442 }
443
444 #[cfg(target_os = "android")]
445 fn start_fuse(zip_path: &Path, mnt_path: &Path) {
446 // Note: for some unknown reason, running a thread to serve fuse doesn't work on Android.
447 // Explicitly spawn a zipfuse process instead.
448 // TODO(jiyong): fix this
449 assert!(std::process::Command::new("sh")
450 .arg("-c")
451 .arg(format!("/data/local/tmp/zipfuse {} {}", zip_path.display(), mnt_path.display()))
452 .spawn()
453 .is_ok());
454 }
455
456 fn wait_for_mount(mount_path: &Path) -> Result<()> {
457 let start_time = Instant::now();
458 const POLL_INTERVAL: Duration = Duration::from_millis(50);
459 const TIMEOUT: Duration = Duration::from_secs(10);
460 const FUSE_SUPER_MAGIC: FsType = FsType(0x65735546);
461 loop {
462 if statfs(mount_path)?.filesystem_type() == FUSE_SUPER_MAGIC {
463 break;
464 }
465
466 if start_time.elapsed() > TIMEOUT {
467 bail!("Time out mounting zipfuse");
468 }
469 std::thread::sleep(POLL_INTERVAL);
470 }
471 Ok(())
472 }
473
474 // Creates a zip file, adds some files to the zip file, mounts it using zipfuse, runs the check
475 // routine, and finally unmounts.
476 fn run_test(add: fn(&mut zip::ZipWriter<File>), check: fn(&std::path::Path)) {
477 // Create an empty zip file
478 let test_dir = tempfile::TempDir::new().unwrap();
479 let zip_path = test_dir.path().join("test.zip");
480 let zip = File::create(&zip_path);
481 assert!(zip.is_ok());
482 let mut zip = zip::ZipWriter::new(zip.unwrap());
483
484 // Let test users add files/dirs to the zip file
485 add(&mut zip);
486 assert!(zip.finish().is_ok());
487 drop(zip);
488
489 // Mount the zip file on the "mnt" dir using zipfuse.
490 let mnt_path = test_dir.path().join("mnt");
491 assert!(fs::create_dir(&mnt_path).is_ok());
492
493 start_fuse(&zip_path, &mnt_path);
494
495 let mnt_path = test_dir.path().join("mnt");
496 // Give some time for the fuse to boot up
497 assert!(wait_for_mount(&mnt_path).is_ok());
498 // Run the check routine, and do the clean up.
499 check(&mnt_path);
500 assert!(nix::mount::umount2(&mnt_path, nix::mount::MntFlags::empty()).is_ok());
501 }
502
503 fn check_file(root: &Path, file: &str, content: &[u8]) {
504 let path = root.join(file);
505 assert!(path.exists());
506
507 let metadata = fs::metadata(&path);
508 assert!(metadata.is_ok());
509
510 let metadata = metadata.unwrap();
511 assert!(metadata.is_file());
512 assert_eq!(content.len(), metadata.len() as usize);
513
514 let read_data = fs::read(&path);
515 assert!(read_data.is_ok());
516 assert_eq!(content, read_data.unwrap().as_slice());
517 }
518
Jiyong Park63a95cf2021-05-13 19:20:30 +0900519 fn check_dir<S: AsRef<str>>(root: &Path, dir: &str, files: &[S], dirs: &[S]) {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900520 let dir_path = root.join(dir);
521 assert!(dir_path.exists());
522
523 let metadata = fs::metadata(&dir_path);
524 assert!(metadata.is_ok());
525
526 let metadata = metadata.unwrap();
527 assert!(metadata.is_dir());
528
529 let iter = fs::read_dir(&dir_path);
530 assert!(iter.is_ok());
531
532 let iter = iter.unwrap();
Jiyong Park63a95cf2021-05-13 19:20:30 +0900533 let mut actual_files = BTreeSet::new();
534 let mut actual_dirs = BTreeSet::new();
Jiyong Park331d1ea2021-05-10 11:01:23 +0900535 for de in iter {
536 let entry = de.unwrap();
537 let path = entry.path();
538 if path.is_dir() {
539 actual_dirs.insert(path.strip_prefix(&dir_path).unwrap().to_path_buf());
540 } else {
541 actual_files.insert(path.strip_prefix(&dir_path).unwrap().to_path_buf());
542 }
543 }
Jiyong Park63a95cf2021-05-13 19:20:30 +0900544 let expected_files: BTreeSet<PathBuf> =
545 files.iter().map(|s| PathBuf::from(s.as_ref())).collect();
546 let expected_dirs: BTreeSet<PathBuf> =
547 dirs.iter().map(|s| PathBuf::from(s.as_ref())).collect();
Jiyong Park331d1ea2021-05-10 11:01:23 +0900548
549 assert_eq!(expected_files, actual_files);
550 assert_eq!(expected_dirs, actual_dirs);
551 }
552
553 #[test]
554 fn empty() {
555 run_test(
556 |_| {},
557 |root| {
Jiyong Park63a95cf2021-05-13 19:20:30 +0900558 check_dir::<String>(root, "", &[], &[]);
Jiyong Park331d1ea2021-05-10 11:01:23 +0900559 },
560 );
561 }
562
563 #[test]
564 fn single_file() {
565 run_test(
566 |zip| {
567 zip.start_file("foo", FileOptions::default()).unwrap();
568 zip.write_all(b"0123456789").unwrap();
569 },
570 |root| {
571 check_dir(root, "", &["foo"], &[]);
572 check_file(root, "foo", b"0123456789");
573 },
574 );
575 }
576
577 #[test]
578 fn single_dir() {
579 run_test(
580 |zip| {
581 zip.add_directory("dir", FileOptions::default()).unwrap();
582 },
583 |root| {
584 check_dir(root, "", &[], &["dir"]);
Jiyong Park63a95cf2021-05-13 19:20:30 +0900585 check_dir::<String>(root, "dir", &[], &[]);
Jiyong Park331d1ea2021-05-10 11:01:23 +0900586 },
587 );
588 }
589
590 #[test]
591 fn complex_hierarchy() {
592 // root/
593 // a/
594 // b1/
595 // b2/
596 // c1 (file)
597 // c2/
598 // d1 (file)
599 // d2 (file)
600 // d3 (file)
601 // x/
602 // y1 (file)
603 // y2 (file)
604 // y3/
605 //
606 // foo (file)
607 // bar (file)
608 run_test(
609 |zip| {
610 let opt = FileOptions::default();
611 zip.add_directory("a/b1", opt).unwrap();
612
613 zip.start_file("a/b2/c1", opt).unwrap();
614
615 zip.start_file("a/b2/c2/d1", opt).unwrap();
616 zip.start_file("a/b2/c2/d2", opt).unwrap();
617 zip.start_file("a/b2/c2/d3", opt).unwrap();
618
619 zip.start_file("x/y1", opt).unwrap();
620 zip.start_file("x/y2", opt).unwrap();
621 zip.add_directory("x/y3", opt).unwrap();
622
623 zip.start_file("foo", opt).unwrap();
624 zip.start_file("bar", opt).unwrap();
625 },
626 |root| {
627 check_dir(root, "", &["foo", "bar"], &["a", "x"]);
628 check_dir(root, "a", &[], &["b1", "b2"]);
Jiyong Park63a95cf2021-05-13 19:20:30 +0900629 check_dir::<String>(root, "a/b1", &[], &[]);
Jiyong Park331d1ea2021-05-10 11:01:23 +0900630 check_dir(root, "a/b2", &["c1"], &["c2"]);
631 check_dir(root, "a/b2/c2", &["d1", "d2", "d3"], &[]);
632 check_dir(root, "x", &["y1", "y2"], &["y3"]);
Jiyong Park63a95cf2021-05-13 19:20:30 +0900633 check_dir::<String>(root, "x/y3", &[], &[]);
Jiyong Park331d1ea2021-05-10 11:01:23 +0900634 check_file(root, "a/b2/c1", &[]);
635 check_file(root, "a/b2/c2/d1", &[]);
636 check_file(root, "a/b2/c2/d2", &[]);
637 check_file(root, "a/b2/c2/d3", &[]);
638 check_file(root, "x/y1", &[]);
639 check_file(root, "x/y2", &[]);
640 check_file(root, "foo", &[]);
641 check_file(root, "bar", &[]);
642 },
643 );
644 }
645
646 #[test]
647 fn large_file() {
648 run_test(
649 |zip| {
650 let data = vec![10; 2 << 20];
651 zip.start_file("foo", FileOptions::default()).unwrap();
652 zip.write_all(&data).unwrap();
653 },
654 |root| {
655 let data = vec![10; 2 << 20];
656 check_file(root, "foo", &data);
657 },
658 );
659 }
Jiyong Park63a95cf2021-05-13 19:20:30 +0900660
661 #[test]
662 fn large_dir() {
663 const NUM_FILES: usize = 1 << 10;
664 run_test(
665 |zip| {
666 let opt = FileOptions::default();
667 // create 1K files. Each file has a name of length 100. So total size is at least
668 // 100KB, which is bigger than the readdir buffer size of 4K.
669 for i in 0..NUM_FILES {
670 zip.start_file(format!("dir/{:0100}", i), opt).unwrap();
671 }
672 },
673 |root| {
674 let dirs_expected: Vec<_> = (0..NUM_FILES).map(|i| format!("{:0100}", i)).collect();
675 check_dir(
676 root,
677 "dir",
678 dirs_expected.iter().map(|s| s.as_str()).collect::<Vec<&str>>().as_slice(),
679 &[],
680 );
681 },
682 );
683 }
Jiyong Parkd40f7bb2021-05-17 10:55:56 +0900684
Jiyong Parke6587ca2021-05-17 14:42:23 +0900685 fn run_fuse_and_check_test_zip(test_dir: &Path, zip_path: &Path) {
686 let mnt_path = test_dir.join("mnt");
Jiyong Parkd40f7bb2021-05-17 10:55:56 +0900687 assert!(fs::create_dir(&mnt_path).is_ok());
688
Chris Wailes68c39f82021-07-27 16:03:44 -0700689 start_fuse(zip_path, &mnt_path);
Jiyong Parkd40f7bb2021-05-17 10:55:56 +0900690
691 // Give some time for the fuse to boot up
692 assert!(wait_for_mount(&mnt_path).is_ok());
693
694 check_dir(&mnt_path, "", &[], &["dir"]);
695 check_dir(&mnt_path, "dir", &["file1", "file2"], &[]);
696 check_file(&mnt_path, "dir/file1", include_bytes!("../testdata/dir/file1"));
697 check_file(&mnt_path, "dir/file2", include_bytes!("../testdata/dir/file2"));
698 assert!(nix::mount::umount2(&mnt_path, nix::mount::MntFlags::empty()).is_ok());
699 }
Jiyong Parke6587ca2021-05-17 14:42:23 +0900700
701 #[test]
702 fn supports_deflate() {
703 let test_dir = tempfile::TempDir::new().unwrap();
704 let zip_path = test_dir.path().join("test.zip");
705 let mut zip_file = File::create(&zip_path).unwrap();
706 zip_file.write_all(include_bytes!("../testdata/test.zip")).unwrap();
707
Chris Wailes68c39f82021-07-27 16:03:44 -0700708 run_fuse_and_check_test_zip(test_dir.path(), &zip_path);
Jiyong Parke6587ca2021-05-17 14:42:23 +0900709 }
710
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900711 #[test]
712 fn supports_store() {
713 run_test(
714 |zip| {
715 let data = vec![10; 2 << 20];
716 zip.start_file(
717 "foo",
718 FileOptions::default().compression_method(zip::CompressionMethod::Stored),
719 )
720 .unwrap();
721 zip.write_all(&data).unwrap();
722 },
723 |root| {
724 let data = vec![10; 2 << 20];
725 check_file(root, "foo", &data);
726 },
727 );
728 }
729
Jiyong Parke6587ca2021-05-17 14:42:23 +0900730 #[cfg(not(target_os = "android"))] // Android doesn't have the loopdev crate
731 #[test]
732 fn supports_zip_on_block_device() {
733 // Write test.zip to the test directory
734 let test_dir = tempfile::TempDir::new().unwrap();
735 let zip_path = test_dir.path().join("test.zip");
736 let mut zip_file = File::create(&zip_path).unwrap();
737 let data = include_bytes!("../testdata/test.zip");
738 zip_file.write_all(data).unwrap();
739
740 // Pad 0 to test.zip so that its size is multiple of 4096.
741 const BLOCK_SIZE: usize = 4096;
742 let size = (data.len() + BLOCK_SIZE) & !BLOCK_SIZE;
743 let pad_size = size - data.len();
744 assert!(pad_size != 0);
745 let pad = vec![0; pad_size];
746 zip_file.write_all(pad.as_slice()).unwrap();
747 drop(zip_file);
748
749 // Attach test.zip to a loop device
750 let lc = loopdev::LoopControl::open().unwrap();
751 let ld = scopeguard::guard(lc.next_free().unwrap(), |ld| {
752 ld.detach().unwrap();
753 });
754 ld.attach_file(&zip_path).unwrap();
755
756 // Start zipfuse over to the loop device (not the zip file)
757 run_fuse_and_check_test_zip(&test_dir.path(), &ld.path().unwrap());
758 }
Jiyong Park331d1ea2021-05-10 11:01:23 +0900759}