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