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