blob: 8400a726c295a309ca7d7668cbb40e5e7047d716 [file] [log] [blame]
Jiyong Park331d1ea2021-05-10 11:01:23 +09001/*
2 * Copyright (C) 2021 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17//! `zipfuse` is a FUSE filesystem for zip archives. It provides transparent access to the files
18//! in a zip archive. This filesystem does not supporting writing files back to the zip archive.
19//! The filesystem has to be mounted read only.
20
21mod inode;
22
23use anyhow::Result;
24use clap::{App, Arg};
25use fuse::filesystem::*;
26use fuse::mount::*;
27use std::collections::HashMap;
28use std::convert::TryFrom;
Jiyong Park851f68a2021-05-11 21:41:25 +090029use std::ffi::{CStr, CString};
Jiyong Park331d1ea2021-05-10 11:01:23 +090030use std::fs::{File, OpenOptions};
31use std::io;
32use std::io::Read;
Jiyong Park63a95cf2021-05-13 19:20:30 +090033use std::mem::size_of;
Jiyong Park331d1ea2021-05-10 11:01:23 +090034use std::os::unix::io::AsRawFd;
35use std::path::Path;
36use std::sync::Mutex;
37
38use crate::inode::{DirectoryEntry, Inode, InodeData, InodeKind, InodeTable};
39
40fn main() -> Result<()> {
41 let matches = App::new("zipfuse")
Jiyong Park6a762db2021-05-31 14:00:52 +090042 .arg(
43 Arg::with_name("options")
Jeff Vander Stoepa8dc2712022-07-29 02:33:45 +020044 .short('o')
Jiyong Park6a762db2021-05-31 14:00:52 +090045 .takes_value(true)
46 .required(false)
Chris Wailes68c39f82021-07-27 16:03:44 -070047 .help("Comma separated list of mount options"),
Jiyong Park6a762db2021-05-31 14:00:52 +090048 )
Andrew Scull3854e402022-07-04 12:10:20 +000049 .arg(
50 Arg::with_name("noexec")
51 .long("noexec")
52 .takes_value(false)
53 .help("Disallow the execution of binary files"),
54 )
Jiyong Park331d1ea2021-05-10 11:01:23 +090055 .arg(Arg::with_name("ZIPFILE").required(true))
56 .arg(Arg::with_name("MOUNTPOINT").required(true))
57 .get_matches();
58
59 let zip_file = matches.value_of("ZIPFILE").unwrap().as_ref();
60 let mount_point = matches.value_of("MOUNTPOINT").unwrap().as_ref();
Jiyong Park6a762db2021-05-31 14:00:52 +090061 let options = matches.value_of("options");
Andrew Scull3854e402022-07-04 12:10:20 +000062 let noexec = matches.is_present("noexec");
63 run_fuse(zip_file, mount_point, options, noexec)?;
Jiyong Park331d1ea2021-05-10 11:01:23 +090064 Ok(())
65}
66
67/// Runs a fuse filesystem by mounting `zip_file` on `mount_point`.
Andrew Scull3854e402022-07-04 12:10:20 +000068pub fn run_fuse(
69 zip_file: &Path,
70 mount_point: &Path,
71 extra_options: Option<&str>,
72 noexec: bool,
73) -> Result<()> {
Jiyong Park331d1ea2021-05-10 11:01:23 +090074 const MAX_READ: u32 = 1 << 20; // TODO(jiyong): tune this
75 const MAX_WRITE: u32 = 1 << 13; // This is a read-only filesystem
76
77 let dev_fuse = OpenOptions::new().read(true).write(true).open("/dev/fuse")?;
78
Jiyong Park6a762db2021-05-31 14:00:52 +090079 let mut mount_options = vec![
80 MountOption::FD(dev_fuse.as_raw_fd()),
81 MountOption::RootMode(libc::S_IFDIR | libc::S_IXUSR | libc::S_IXGRP | libc::S_IXOTH),
82 MountOption::AllowOther,
83 MountOption::UserId(0),
84 MountOption::GroupId(0),
85 MountOption::MaxRead(MAX_READ),
86 ];
87 if let Some(value) = extra_options {
88 mount_options.push(MountOption::Extra(value));
89 }
90
Andrew Scull3854e402022-07-04 12:10:20 +000091 let mut mount_flags = libc::MS_NOSUID | libc::MS_NODEV | libc::MS_RDONLY;
92 if noexec {
93 mount_flags |= libc::MS_NOEXEC;
94 }
95
96 fuse::mount(mount_point, "zipfuse", mount_flags, &mount_options)?;
Victor Hsieh58a5e9b2022-03-09 21:57:26 +000097 let mut config = fuse::FuseConfig::new();
98 config.dev_fuse(dev_fuse).max_write(MAX_WRITE).max_read(MAX_READ);
99 Ok(config.enter_message_loop(ZipFuse::new(zip_file)?)?)
Jiyong Park331d1ea2021-05-10 11:01:23 +0900100}
101
102struct ZipFuse {
103 zip_archive: Mutex<zip::ZipArchive<File>>,
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900104 raw_file: Mutex<File>,
Jiyong Park331d1ea2021-05-10 11:01:23 +0900105 inode_table: InodeTable,
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900106 open_files: Mutex<HashMap<Handle, OpenFile>>,
Jiyong Park331d1ea2021-05-10 11:01:23 +0900107 open_dirs: Mutex<HashMap<Handle, OpenDirBuf>>,
108}
109
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900110/// Represents a [`ZipFile`] that is opened.
111struct OpenFile {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900112 open_count: u32, // multiple opens share the buf because this is a read-only filesystem
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900113 content: OpenFileContent,
114}
115
116/// Holds the content of a [`ZipFile`]. Depending on whether it is compressed or not, the
117/// entire content is stored, or only the zip index is stored.
118enum OpenFileContent {
119 Compressed(Box<[u8]>),
120 Uncompressed(usize), // zip index
Jiyong Park331d1ea2021-05-10 11:01:23 +0900121}
122
123/// Holds the directory entries in a directory opened by [`opendir`].
124struct OpenDirBuf {
125 open_count: u32,
126 buf: Box<[(CString, DirectoryEntry)]>,
127}
128
129type Handle = u64;
130
131fn ebadf() -> io::Error {
132 io::Error::from_raw_os_error(libc::EBADF)
133}
134
135fn timeout_max() -> std::time::Duration {
136 std::time::Duration::new(u64::MAX, 1_000_000_000 - 1)
137}
138
139impl ZipFuse {
140 fn new(zip_file: &Path) -> Result<ZipFuse> {
141 // TODO(jiyong): Use O_DIRECT to avoid double caching.
142 // `.custom_flags(nix::fcntl::OFlag::O_DIRECT.bits())` currently doesn't work.
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900143 let f = File::open(zip_file)?;
Jiyong Park331d1ea2021-05-10 11:01:23 +0900144 let mut z = zip::ZipArchive::new(f)?;
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900145 // Open the same file again so that we can directly access it when accessing
146 // uncompressed zip_file entries in it. `ZipFile` doesn't implement `Seek`.
147 let raw_file = File::open(zip_file)?;
Jiyong Park331d1ea2021-05-10 11:01:23 +0900148 let it = InodeTable::from_zip(&mut z)?;
149 Ok(ZipFuse {
150 zip_archive: Mutex::new(z),
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900151 raw_file: Mutex::new(raw_file),
Jiyong Park331d1ea2021-05-10 11:01:23 +0900152 inode_table: it,
153 open_files: Mutex::new(HashMap::new()),
154 open_dirs: Mutex::new(HashMap::new()),
155 })
156 }
157
158 fn find_inode(&self, inode: Inode) -> io::Result<&InodeData> {
159 self.inode_table.get(inode).ok_or_else(ebadf)
160 }
161
Jiyong Parkd5df9562021-05-13 00:50:23 +0900162 // TODO(jiyong) remove this. Right now this is needed to do the nlink_t to u64 conversion below
163 // on aosp_x86_64 target. That however is a useless conversion on other targets.
164 #[allow(clippy::useless_conversion)]
Jiyong Park331d1ea2021-05-10 11:01:23 +0900165 fn stat_from(&self, inode: Inode) -> io::Result<libc::stat64> {
166 let inode_data = self.find_inode(inode)?;
167 let mut st = unsafe { std::mem::MaybeUninit::<libc::stat64>::zeroed().assume_init() };
168 st.st_dev = 0;
Jiyong Parkd5df9562021-05-13 00:50:23 +0900169 st.st_nlink = if let Some(directory) = inode_data.get_directory() {
170 (2 + directory.len() as libc::nlink_t).into()
Jiyong Park331d1ea2021-05-10 11:01:23 +0900171 } else {
172 1
173 };
174 st.st_ino = inode;
175 st.st_mode = if inode_data.is_dir() { libc::S_IFDIR } else { libc::S_IFREG };
176 st.st_mode |= inode_data.mode;
177 st.st_uid = 0;
178 st.st_gid = 0;
179 st.st_size = i64::try_from(inode_data.size).unwrap_or(i64::MAX);
180 Ok(st)
181 }
182}
183
184impl fuse::filesystem::FileSystem for ZipFuse {
185 type Inode = Inode;
186 type Handle = Handle;
187 type DirIter = DirIter;
188
189 fn init(&self, _capable: FsOptions) -> std::io::Result<FsOptions> {
190 // The default options added by the fuse crate are fine. We don't have additional options.
191 Ok(FsOptions::empty())
192 }
193
194 fn lookup(&self, _ctx: Context, parent: Self::Inode, name: &CStr) -> io::Result<Entry> {
195 let inode = self.find_inode(parent)?;
196 let directory = inode.get_directory().ok_or_else(ebadf)?;
Jiyong Park331d1ea2021-05-10 11:01:23 +0900197 let entry = directory.get(name);
198 match entry {
199 Some(e) => Ok(Entry {
200 inode: e.inode,
201 generation: 0,
202 attr: self.stat_from(e.inode)?,
203 attr_timeout: timeout_max(), // this is a read-only fs
204 entry_timeout: timeout_max(),
205 }),
206 _ => Err(io::Error::from_raw_os_error(libc::ENOENT)),
207 }
208 }
209
210 fn getattr(
211 &self,
212 _ctx: Context,
213 inode: Self::Inode,
214 _handle: Option<Self::Handle>,
215 ) -> io::Result<(libc::stat64, std::time::Duration)> {
216 let st = self.stat_from(inode)?;
217 Ok((st, timeout_max()))
218 }
219
220 fn open(
221 &self,
222 _ctx: Context,
223 inode: Self::Inode,
224 _flags: u32,
225 ) -> io::Result<(Option<Self::Handle>, fuse::filesystem::OpenOptions)> {
226 let mut open_files = self.open_files.lock().unwrap();
227 let handle = inode as Handle;
228
229 // If the file is already opened, just increase the reference counter. If not, read the
230 // entire file content to the buffer. When `read` is called, a portion of the buffer is
231 // copied to the kernel.
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900232 if let Some(file) = open_files.get_mut(&handle) {
233 if file.open_count == 0 {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900234 return Err(ebadf());
235 }
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900236 file.open_count += 1;
Jiyong Park331d1ea2021-05-10 11:01:23 +0900237 } else {
238 let inode_data = self.find_inode(inode)?;
239 let zip_index = inode_data.get_zip_index().ok_or_else(ebadf)?;
240 let mut zip_archive = self.zip_archive.lock().unwrap();
241 let mut zip_file = zip_archive.by_index(zip_index)?;
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900242 let content = match zip_file.compression() {
243 zip::CompressionMethod::Stored => OpenFileContent::Uncompressed(zip_index),
244 _ => {
245 if let Some(mode) = zip_file.unix_mode() {
246 let is_reg_file = zip_file.is_file();
247 let is_executable =
248 mode & (libc::S_IXUSR | libc::S_IXGRP | libc::S_IXOTH) != 0;
249 if is_reg_file && is_executable {
250 log::warn!(
251 "Executable file {:?} is stored compressed. Consider \
252 storing it uncompressed to save memory",
253 zip_file.mangled_name()
254 );
255 }
256 }
257 let mut buf = Vec::with_capacity(inode_data.size as usize);
258 zip_file.read_to_end(&mut buf)?;
259 OpenFileContent::Compressed(buf.into_boxed_slice())
260 }
261 };
262 open_files.insert(handle, OpenFile { open_count: 1, content });
Jiyong Park331d1ea2021-05-10 11:01:23 +0900263 }
264 // Note: we don't return `DIRECT_IO` here, because then applications wouldn't be able to
265 // mmap the files.
266 Ok((Some(handle), fuse::filesystem::OpenOptions::empty()))
267 }
268
269 fn release(
270 &self,
271 _ctx: Context,
272 inode: Self::Inode,
273 _flags: u32,
274 _handle: Self::Handle,
275 _flush: bool,
276 _flock_release: bool,
277 _lock_owner: Option<u64>,
278 ) -> io::Result<()> {
279 // Releases the buffer for the `handle` when it is opened for nobody. While this is good
280 // for saving memory, this has a performance implication because we need to decompress
281 // again when the same file is opened in the future.
282 let mut open_files = self.open_files.lock().unwrap();
283 let handle = inode as Handle;
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900284 if let Some(file) = open_files.get_mut(&handle) {
285 if file.open_count.checked_sub(1).ok_or_else(ebadf)? == 0 {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900286 open_files.remove(&handle);
287 }
288 Ok(())
289 } else {
290 Err(ebadf())
291 }
292 }
293
294 fn read<W: io::Write + ZeroCopyWriter>(
295 &self,
296 _ctx: Context,
297 _inode: Self::Inode,
298 handle: Self::Handle,
299 mut w: W,
300 size: u32,
301 offset: u64,
302 _lock_owner: Option<u64>,
303 _flags: u32,
304 ) -> io::Result<usize> {
305 let open_files = self.open_files.lock().unwrap();
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900306 let file = open_files.get(&handle).ok_or_else(ebadf)?;
307 if file.open_count == 0 {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900308 return Err(ebadf());
309 }
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900310 Ok(match &file.content {
311 OpenFileContent::Uncompressed(zip_index) => {
312 let mut zip_archive = self.zip_archive.lock().unwrap();
313 let zip_file = zip_archive.by_index(*zip_index)?;
314 let start = zip_file.data_start() + offset;
315 let remaining_size = zip_file.size() - offset;
316 let size = std::cmp::min(remaining_size, size.into());
317
318 let mut raw_file = self.raw_file.lock().unwrap();
319 w.write_from(&mut raw_file, size as usize, start)?
320 }
321 OpenFileContent::Compressed(buf) => {
322 let start = offset as usize;
323 let end = start + size as usize;
324 let end = std::cmp::min(end, buf.len());
325 w.write(&buf[start..end])?
326 }
327 })
Jiyong Park331d1ea2021-05-10 11:01:23 +0900328 }
329
330 fn opendir(
331 &self,
332 _ctx: Context,
333 inode: Self::Inode,
334 _flags: u32,
335 ) -> io::Result<(Option<Self::Handle>, fuse::filesystem::OpenOptions)> {
336 let mut open_dirs = self.open_dirs.lock().unwrap();
337 let handle = inode as Handle;
338 if let Some(odb) = open_dirs.get_mut(&handle) {
339 if odb.open_count == 0 {
340 return Err(ebadf());
341 }
342 odb.open_count += 1;
343 } else {
344 let inode_data = self.find_inode(inode)?;
345 let directory = inode_data.get_directory().ok_or_else(ebadf)?;
346 let mut buf: Vec<(CString, DirectoryEntry)> = Vec::with_capacity(directory.len());
347 for (name, dir_entry) in directory.iter() {
348 let name = CString::new(name.as_bytes()).unwrap();
349 buf.push((name, dir_entry.clone()));
350 }
351 open_dirs.insert(handle, OpenDirBuf { open_count: 1, buf: buf.into_boxed_slice() });
352 }
Jiyong Park63a95cf2021-05-13 19:20:30 +0900353 Ok((Some(handle), fuse::filesystem::OpenOptions::CACHE_DIR))
Jiyong Park331d1ea2021-05-10 11:01:23 +0900354 }
355
356 fn releasedir(
357 &self,
358 _ctx: Context,
359 inode: Self::Inode,
360 _flags: u32,
361 _handle: Self::Handle,
362 ) -> io::Result<()> {
363 let mut open_dirs = self.open_dirs.lock().unwrap();
364 let handle = inode as Handle;
365 if let Some(odb) = open_dirs.get_mut(&handle) {
366 if odb.open_count.checked_sub(1).ok_or_else(ebadf)? == 0 {
367 open_dirs.remove(&handle);
368 }
369 Ok(())
370 } else {
371 Err(ebadf())
372 }
373 }
374
375 fn readdir(
376 &self,
377 _ctx: Context,
378 inode: Self::Inode,
379 _handle: Self::Handle,
380 size: u32,
381 offset: u64,
382 ) -> io::Result<Self::DirIter> {
383 let open_dirs = self.open_dirs.lock().unwrap();
384 let handle = inode as Handle;
385 let odb = open_dirs.get(&handle).ok_or_else(ebadf)?;
386 if odb.open_count == 0 {
387 return Err(ebadf());
388 }
389 let buf = &odb.buf;
390 let start = offset as usize;
Jiyong Park63a95cf2021-05-13 19:20:30 +0900391
392 // Estimate the size of each entry will take space in the buffer. See
393 // external/crosvm/fuse/src/server.rs#add_dirent
394 let mut estimate: usize = 0; // estimated number of bytes we will be writing
395 let mut end = start; // index in `buf`
396 while estimate < size as usize && end < buf.len() {
397 let dirent_size = size_of::<fuse::sys::Dirent>();
398 let name_size = buf[end].0.to_bytes().len();
399 estimate += (dirent_size + name_size + 7) & !7; // round to 8 byte boundary
400 end += 1;
401 }
402
Jiyong Park331d1ea2021-05-10 11:01:23 +0900403 let mut new_buf = Vec::with_capacity(end - start);
404 // The portion of `buf` is *copied* to the iterator. This is not ideal, but inevitable
405 // because the `name` field in `fuse::filesystem::DirEntry` is `&CStr` not `CString`.
406 new_buf.extend_from_slice(&buf[start..end]);
407 Ok(DirIter { inner: new_buf, offset, cur: 0 })
408 }
409}
410
411struct DirIter {
412 inner: Vec<(CString, DirectoryEntry)>,
413 offset: u64, // the offset where this iterator begins. `next` doesn't change this.
414 cur: usize, // the current index in `inner`. `next` advances this.
415}
416
417impl fuse::filesystem::DirectoryIterator for DirIter {
418 fn next(&mut self) -> Option<fuse::filesystem::DirEntry> {
419 if self.cur >= self.inner.len() {
420 return None;
421 }
422
423 let (name, entry) = &self.inner[self.cur];
424 self.cur += 1;
425 Some(fuse::filesystem::DirEntry {
426 ino: entry.inode as libc::ino64_t,
427 offset: self.offset + self.cur as u64,
428 type_: match entry.kind {
429 InodeKind::Directory => libc::DT_DIR.into(),
430 InodeKind::File => libc::DT_REG.into(),
431 },
432 name,
433 })
434 }
435}
436
437#[cfg(test)]
438mod tests {
439 use anyhow::{bail, Result};
440 use nix::sys::statfs::{statfs, FsType};
Jiyong Park63a95cf2021-05-13 19:20:30 +0900441 use std::collections::BTreeSet;
Jiyong Park331d1ea2021-05-10 11:01:23 +0900442 use std::fs;
443 use std::fs::File;
444 use std::io::Write;
445 use std::path::{Path, PathBuf};
446 use std::time::{Duration, Instant};
447 use zip::write::FileOptions;
448
449 #[cfg(not(target_os = "android"))]
Andrew Scull3854e402022-07-04 12:10:20 +0000450 fn start_fuse(zip_path: &Path, mnt_path: &Path, noexec: bool) {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900451 let zip_path = PathBuf::from(zip_path);
452 let mnt_path = PathBuf::from(mnt_path);
453 std::thread::spawn(move || {
Andrew Scull3854e402022-07-04 12:10:20 +0000454 crate::run_fuse(&zip_path, &mnt_path, None, noexec).unwrap();
Jiyong Park331d1ea2021-05-10 11:01:23 +0900455 });
456 }
457
458 #[cfg(target_os = "android")]
Andrew Scull3854e402022-07-04 12:10:20 +0000459 fn start_fuse(zip_path: &Path, mnt_path: &Path, noexec: bool) {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900460 // Note: for some unknown reason, running a thread to serve fuse doesn't work on Android.
461 // Explicitly spawn a zipfuse process instead.
462 // TODO(jiyong): fix this
Andrew Scull3854e402022-07-04 12:10:20 +0000463 let noexec = if noexec { "--noexec" } else { "" };
Jiyong Park331d1ea2021-05-10 11:01:23 +0900464 assert!(std::process::Command::new("sh")
465 .arg("-c")
Andrew Scull3854e402022-07-04 12:10:20 +0000466 .arg(format!(
467 "/data/local/tmp/zipfuse {} {} {}",
468 noexec,
469 zip_path.display(),
470 mnt_path.display()
471 ))
Jiyong Park331d1ea2021-05-10 11:01:23 +0900472 .spawn()
473 .is_ok());
474 }
475
476 fn wait_for_mount(mount_path: &Path) -> Result<()> {
477 let start_time = Instant::now();
478 const POLL_INTERVAL: Duration = Duration::from_millis(50);
479 const TIMEOUT: Duration = Duration::from_secs(10);
480 const FUSE_SUPER_MAGIC: FsType = FsType(0x65735546);
481 loop {
482 if statfs(mount_path)?.filesystem_type() == FUSE_SUPER_MAGIC {
483 break;
484 }
485
486 if start_time.elapsed() > TIMEOUT {
487 bail!("Time out mounting zipfuse");
488 }
489 std::thread::sleep(POLL_INTERVAL);
490 }
491 Ok(())
492 }
493
494 // Creates a zip file, adds some files to the zip file, mounts it using zipfuse, runs the check
495 // routine, and finally unmounts.
496 fn run_test(add: fn(&mut zip::ZipWriter<File>), check: fn(&std::path::Path)) {
Andrew Scull3854e402022-07-04 12:10:20 +0000497 run_test_noexec(false, add, check);
498 }
499
500 fn run_test_noexec(
501 noexec: bool,
502 add: fn(&mut zip::ZipWriter<File>),
503 check: fn(&std::path::Path),
504 ) {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900505 // Create an empty zip file
506 let test_dir = tempfile::TempDir::new().unwrap();
507 let zip_path = test_dir.path().join("test.zip");
508 let zip = File::create(&zip_path);
509 assert!(zip.is_ok());
510 let mut zip = zip::ZipWriter::new(zip.unwrap());
511
512 // Let test users add files/dirs to the zip file
513 add(&mut zip);
514 assert!(zip.finish().is_ok());
515 drop(zip);
516
517 // Mount the zip file on the "mnt" dir using zipfuse.
518 let mnt_path = test_dir.path().join("mnt");
519 assert!(fs::create_dir(&mnt_path).is_ok());
520
Andrew Scull3854e402022-07-04 12:10:20 +0000521 start_fuse(&zip_path, &mnt_path, noexec);
Jiyong Park331d1ea2021-05-10 11:01:23 +0900522
523 let mnt_path = test_dir.path().join("mnt");
524 // Give some time for the fuse to boot up
525 assert!(wait_for_mount(&mnt_path).is_ok());
526 // Run the check routine, and do the clean up.
527 check(&mnt_path);
528 assert!(nix::mount::umount2(&mnt_path, nix::mount::MntFlags::empty()).is_ok());
529 }
530
531 fn check_file(root: &Path, file: &str, content: &[u8]) {
532 let path = root.join(file);
533 assert!(path.exists());
534
535 let metadata = fs::metadata(&path);
536 assert!(metadata.is_ok());
537
538 let metadata = metadata.unwrap();
539 assert!(metadata.is_file());
540 assert_eq!(content.len(), metadata.len() as usize);
541
542 let read_data = fs::read(&path);
543 assert!(read_data.is_ok());
544 assert_eq!(content, read_data.unwrap().as_slice());
545 }
546
Jiyong Park63a95cf2021-05-13 19:20:30 +0900547 fn check_dir<S: AsRef<str>>(root: &Path, dir: &str, files: &[S], dirs: &[S]) {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900548 let dir_path = root.join(dir);
549 assert!(dir_path.exists());
550
551 let metadata = fs::metadata(&dir_path);
552 assert!(metadata.is_ok());
553
554 let metadata = metadata.unwrap();
555 assert!(metadata.is_dir());
556
557 let iter = fs::read_dir(&dir_path);
558 assert!(iter.is_ok());
559
560 let iter = iter.unwrap();
Jiyong Park63a95cf2021-05-13 19:20:30 +0900561 let mut actual_files = BTreeSet::new();
562 let mut actual_dirs = BTreeSet::new();
Jiyong Park331d1ea2021-05-10 11:01:23 +0900563 for de in iter {
564 let entry = de.unwrap();
565 let path = entry.path();
566 if path.is_dir() {
567 actual_dirs.insert(path.strip_prefix(&dir_path).unwrap().to_path_buf());
568 } else {
569 actual_files.insert(path.strip_prefix(&dir_path).unwrap().to_path_buf());
570 }
571 }
Jiyong Park63a95cf2021-05-13 19:20:30 +0900572 let expected_files: BTreeSet<PathBuf> =
573 files.iter().map(|s| PathBuf::from(s.as_ref())).collect();
574 let expected_dirs: BTreeSet<PathBuf> =
575 dirs.iter().map(|s| PathBuf::from(s.as_ref())).collect();
Jiyong Park331d1ea2021-05-10 11:01:23 +0900576
577 assert_eq!(expected_files, actual_files);
578 assert_eq!(expected_dirs, actual_dirs);
579 }
580
581 #[test]
582 fn empty() {
583 run_test(
584 |_| {},
585 |root| {
Jiyong Park63a95cf2021-05-13 19:20:30 +0900586 check_dir::<String>(root, "", &[], &[]);
Jiyong Park331d1ea2021-05-10 11:01:23 +0900587 },
588 );
589 }
590
591 #[test]
592 fn single_file() {
593 run_test(
594 |zip| {
595 zip.start_file("foo", FileOptions::default()).unwrap();
596 zip.write_all(b"0123456789").unwrap();
597 },
598 |root| {
599 check_dir(root, "", &["foo"], &[]);
600 check_file(root, "foo", b"0123456789");
601 },
602 );
603 }
604
605 #[test]
Andrew Scull3854e402022-07-04 12:10:20 +0000606 fn noexec() {
607 fn add_executable(zip: &mut zip::ZipWriter<File>) {
608 zip.start_file("executable", FileOptions::default().unix_permissions(0o755)).unwrap();
609 }
610
611 // Executables can be run when not mounting with noexec.
612 run_test(add_executable, |root| {
613 let res = std::process::Command::new(root.join("executable")).status();
614 res.unwrap();
615 });
616
617 // Mounting with noexec results in permissions denial when running an executable.
618 let noexec = true;
619 run_test_noexec(noexec, add_executable, |root| {
620 let res = std::process::Command::new(root.join("executable")).status();
621 assert!(matches!(res.unwrap_err().kind(), std::io::ErrorKind::PermissionDenied));
622 });
623 }
624
625 #[test]
Jiyong Park331d1ea2021-05-10 11:01:23 +0900626 fn single_dir() {
627 run_test(
628 |zip| {
629 zip.add_directory("dir", FileOptions::default()).unwrap();
630 },
631 |root| {
632 check_dir(root, "", &[], &["dir"]);
Jiyong Park63a95cf2021-05-13 19:20:30 +0900633 check_dir::<String>(root, "dir", &[], &[]);
Jiyong Park331d1ea2021-05-10 11:01:23 +0900634 },
635 );
636 }
637
638 #[test]
639 fn complex_hierarchy() {
640 // root/
641 // a/
642 // b1/
643 // b2/
644 // c1 (file)
645 // c2/
646 // d1 (file)
647 // d2 (file)
648 // d3 (file)
649 // x/
650 // y1 (file)
651 // y2 (file)
652 // y3/
653 //
654 // foo (file)
655 // bar (file)
656 run_test(
657 |zip| {
658 let opt = FileOptions::default();
659 zip.add_directory("a/b1", opt).unwrap();
660
661 zip.start_file("a/b2/c1", opt).unwrap();
662
663 zip.start_file("a/b2/c2/d1", opt).unwrap();
664 zip.start_file("a/b2/c2/d2", opt).unwrap();
665 zip.start_file("a/b2/c2/d3", opt).unwrap();
666
667 zip.start_file("x/y1", opt).unwrap();
668 zip.start_file("x/y2", opt).unwrap();
669 zip.add_directory("x/y3", opt).unwrap();
670
671 zip.start_file("foo", opt).unwrap();
672 zip.start_file("bar", opt).unwrap();
673 },
674 |root| {
675 check_dir(root, "", &["foo", "bar"], &["a", "x"]);
676 check_dir(root, "a", &[], &["b1", "b2"]);
Jiyong Park63a95cf2021-05-13 19:20:30 +0900677 check_dir::<String>(root, "a/b1", &[], &[]);
Jiyong Park331d1ea2021-05-10 11:01:23 +0900678 check_dir(root, "a/b2", &["c1"], &["c2"]);
679 check_dir(root, "a/b2/c2", &["d1", "d2", "d3"], &[]);
680 check_dir(root, "x", &["y1", "y2"], &["y3"]);
Jiyong Park63a95cf2021-05-13 19:20:30 +0900681 check_dir::<String>(root, "x/y3", &[], &[]);
Jiyong Park331d1ea2021-05-10 11:01:23 +0900682 check_file(root, "a/b2/c1", &[]);
683 check_file(root, "a/b2/c2/d1", &[]);
684 check_file(root, "a/b2/c2/d2", &[]);
685 check_file(root, "a/b2/c2/d3", &[]);
686 check_file(root, "x/y1", &[]);
687 check_file(root, "x/y2", &[]);
688 check_file(root, "foo", &[]);
689 check_file(root, "bar", &[]);
690 },
691 );
692 }
693
694 #[test]
695 fn large_file() {
696 run_test(
697 |zip| {
698 let data = vec![10; 2 << 20];
699 zip.start_file("foo", FileOptions::default()).unwrap();
700 zip.write_all(&data).unwrap();
701 },
702 |root| {
703 let data = vec![10; 2 << 20];
704 check_file(root, "foo", &data);
705 },
706 );
707 }
Jiyong Park63a95cf2021-05-13 19:20:30 +0900708
709 #[test]
710 fn large_dir() {
711 const NUM_FILES: usize = 1 << 10;
712 run_test(
713 |zip| {
714 let opt = FileOptions::default();
715 // create 1K files. Each file has a name of length 100. So total size is at least
716 // 100KB, which is bigger than the readdir buffer size of 4K.
717 for i in 0..NUM_FILES {
718 zip.start_file(format!("dir/{:0100}", i), opt).unwrap();
719 }
720 },
721 |root| {
722 let dirs_expected: Vec<_> = (0..NUM_FILES).map(|i| format!("{:0100}", i)).collect();
723 check_dir(
724 root,
725 "dir",
726 dirs_expected.iter().map(|s| s.as_str()).collect::<Vec<&str>>().as_slice(),
727 &[],
728 );
729 },
730 );
731 }
Jiyong Parkd40f7bb2021-05-17 10:55:56 +0900732
Jiyong Parke6587ca2021-05-17 14:42:23 +0900733 fn run_fuse_and_check_test_zip(test_dir: &Path, zip_path: &Path) {
734 let mnt_path = test_dir.join("mnt");
Jiyong Parkd40f7bb2021-05-17 10:55:56 +0900735 assert!(fs::create_dir(&mnt_path).is_ok());
736
Andrew Scull3854e402022-07-04 12:10:20 +0000737 let noexec = false;
738 start_fuse(zip_path, &mnt_path, noexec);
Jiyong Parkd40f7bb2021-05-17 10:55:56 +0900739
740 // Give some time for the fuse to boot up
741 assert!(wait_for_mount(&mnt_path).is_ok());
742
743 check_dir(&mnt_path, "", &[], &["dir"]);
744 check_dir(&mnt_path, "dir", &["file1", "file2"], &[]);
745 check_file(&mnt_path, "dir/file1", include_bytes!("../testdata/dir/file1"));
746 check_file(&mnt_path, "dir/file2", include_bytes!("../testdata/dir/file2"));
747 assert!(nix::mount::umount2(&mnt_path, nix::mount::MntFlags::empty()).is_ok());
748 }
Jiyong Parke6587ca2021-05-17 14:42:23 +0900749
750 #[test]
751 fn supports_deflate() {
752 let test_dir = tempfile::TempDir::new().unwrap();
753 let zip_path = test_dir.path().join("test.zip");
754 let mut zip_file = File::create(&zip_path).unwrap();
755 zip_file.write_all(include_bytes!("../testdata/test.zip")).unwrap();
756
Chris Wailes68c39f82021-07-27 16:03:44 -0700757 run_fuse_and_check_test_zip(test_dir.path(), &zip_path);
Jiyong Parke6587ca2021-05-17 14:42:23 +0900758 }
759
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900760 #[test]
761 fn supports_store() {
762 run_test(
763 |zip| {
764 let data = vec![10; 2 << 20];
765 zip.start_file(
766 "foo",
767 FileOptions::default().compression_method(zip::CompressionMethod::Stored),
768 )
769 .unwrap();
770 zip.write_all(&data).unwrap();
771 },
772 |root| {
773 let data = vec![10; 2 << 20];
774 check_file(root, "foo", &data);
775 },
776 );
777 }
778
Jiyong Parke6587ca2021-05-17 14:42:23 +0900779 #[cfg(not(target_os = "android"))] // Android doesn't have the loopdev crate
780 #[test]
781 fn supports_zip_on_block_device() {
782 // Write test.zip to the test directory
783 let test_dir = tempfile::TempDir::new().unwrap();
784 let zip_path = test_dir.path().join("test.zip");
785 let mut zip_file = File::create(&zip_path).unwrap();
786 let data = include_bytes!("../testdata/test.zip");
787 zip_file.write_all(data).unwrap();
788
789 // Pad 0 to test.zip so that its size is multiple of 4096.
790 const BLOCK_SIZE: usize = 4096;
791 let size = (data.len() + BLOCK_SIZE) & !BLOCK_SIZE;
792 let pad_size = size - data.len();
793 assert!(pad_size != 0);
794 let pad = vec![0; pad_size];
795 zip_file.write_all(pad.as_slice()).unwrap();
796 drop(zip_file);
797
798 // Attach test.zip to a loop device
799 let lc = loopdev::LoopControl::open().unwrap();
800 let ld = scopeguard::guard(lc.next_free().unwrap(), |ld| {
801 ld.detach().unwrap();
802 });
803 ld.attach_file(&zip_path).unwrap();
804
805 // Start zipfuse over to the loop device (not the zip file)
806 run_fuse_and_check_test_zip(&test_dir.path(), &ld.path().unwrap());
807 }
Jiyong Park331d1ea2021-05-10 11:01:23 +0900808}