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