blob: fd7e4146f7e3b3a98ff9fba069698de737c2b785 [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
Alan Stokes60f82202022-10-07 16:40:07 +010023use anyhow::{Context as AnyhowContext, Result};
Jiyong Park331d1ea2021-05-10 11:01:23 +090024use clap::{App, Arg};
25use fuse::filesystem::*;
26use fuse::mount::*;
Alan Stokes60f82202022-10-07 16:40:07 +010027use rustutils::system_properties;
Jiyong Park331d1ea2021-05-10 11:01:23 +090028use std::collections::HashMap;
29use std::convert::TryFrom;
Jiyong Park851f68a2021-05-11 21:41:25 +090030use std::ffi::{CStr, CString};
Jiyong Park331d1ea2021-05-10 11:01:23 +090031use std::fs::{File, OpenOptions};
32use std::io;
33use std::io::Read;
Jiyong Park63a95cf2021-05-13 19:20:30 +090034use std::mem::size_of;
Jiyong Park331d1ea2021-05-10 11:01:23 +090035use std::os::unix::io::AsRawFd;
36use std::path::Path;
37use std::sync::Mutex;
38
39use crate::inode::{DirectoryEntry, Inode, InodeData, InodeKind, InodeTable};
40
41fn main() -> Result<()> {
42 let matches = App::new("zipfuse")
Jiyong Park6a762db2021-05-31 14:00:52 +090043 .arg(
44 Arg::with_name("options")
Jeff Vander Stoepa8dc2712022-07-29 02:33:45 +020045 .short('o')
Jiyong Park6a762db2021-05-31 14:00:52 +090046 .takes_value(true)
47 .required(false)
Chris Wailes68c39f82021-07-27 16:03:44 -070048 .help("Comma separated list of mount options"),
Jiyong Park6a762db2021-05-31 14:00:52 +090049 )
Andrew Scull3854e402022-07-04 12:10:20 +000050 .arg(
51 Arg::with_name("noexec")
52 .long("noexec")
53 .takes_value(false)
54 .help("Disallow the execution of binary files"),
55 )
Alan Stokes60f82202022-10-07 16:40:07 +010056 .arg(
57 Arg::with_name("readyprop")
58 .short('p')
59 .takes_value(true)
60 .help("Specify a property to be set when mount is ready"),
61 )
Jiyong Park331d1ea2021-05-10 11:01:23 +090062 .arg(Arg::with_name("ZIPFILE").required(true))
63 .arg(Arg::with_name("MOUNTPOINT").required(true))
64 .get_matches();
65
66 let zip_file = matches.value_of("ZIPFILE").unwrap().as_ref();
67 let mount_point = matches.value_of("MOUNTPOINT").unwrap().as_ref();
Jiyong Park6a762db2021-05-31 14:00:52 +090068 let options = matches.value_of("options");
Andrew Scull3854e402022-07-04 12:10:20 +000069 let noexec = matches.is_present("noexec");
Alan Stokes60f82202022-10-07 16:40:07 +010070 let ready_prop = matches.value_of("readyprop");
71 run_fuse(zip_file, mount_point, options, noexec, ready_prop)?;
Jiyong Park331d1ea2021-05-10 11:01:23 +090072 Ok(())
73}
74
75/// Runs a fuse filesystem by mounting `zip_file` on `mount_point`.
Andrew Scull3854e402022-07-04 12:10:20 +000076pub fn run_fuse(
77 zip_file: &Path,
78 mount_point: &Path,
79 extra_options: Option<&str>,
80 noexec: bool,
Alan Stokes60f82202022-10-07 16:40:07 +010081 ready_prop: Option<&str>,
Andrew Scull3854e402022-07-04 12:10:20 +000082) -> Result<()> {
Jiyong Park331d1ea2021-05-10 11:01:23 +090083 const MAX_READ: u32 = 1 << 20; // TODO(jiyong): tune this
84 const MAX_WRITE: u32 = 1 << 13; // This is a read-only filesystem
85
86 let dev_fuse = OpenOptions::new().read(true).write(true).open("/dev/fuse")?;
87
Jiyong Park6a762db2021-05-31 14:00:52 +090088 let mut mount_options = vec![
89 MountOption::FD(dev_fuse.as_raw_fd()),
Jiyong Park98ce68d2023-01-09 17:46:19 +090090 MountOption::DefaultPermissions,
Jiyong Park6a762db2021-05-31 14:00:52 +090091 MountOption::RootMode(libc::S_IFDIR | libc::S_IXUSR | libc::S_IXGRP | libc::S_IXOTH),
92 MountOption::AllowOther,
93 MountOption::UserId(0),
94 MountOption::GroupId(0),
95 MountOption::MaxRead(MAX_READ),
96 ];
97 if let Some(value) = extra_options {
98 mount_options.push(MountOption::Extra(value));
99 }
100
Andrew Scull3854e402022-07-04 12:10:20 +0000101 let mut mount_flags = libc::MS_NOSUID | libc::MS_NODEV | libc::MS_RDONLY;
102 if noexec {
103 mount_flags |= libc::MS_NOEXEC;
104 }
105
106 fuse::mount(mount_point, "zipfuse", mount_flags, &mount_options)?;
Alan Stokes60f82202022-10-07 16:40:07 +0100107
108 if let Some(property_name) = ready_prop {
109 system_properties::write(property_name, "1").context("Failed to set readyprop")?;
110 }
111
Victor Hsieh58a5e9b2022-03-09 21:57:26 +0000112 let mut config = fuse::FuseConfig::new();
113 config.dev_fuse(dev_fuse).max_write(MAX_WRITE).max_read(MAX_READ);
114 Ok(config.enter_message_loop(ZipFuse::new(zip_file)?)?)
Jiyong Park331d1ea2021-05-10 11:01:23 +0900115}
116
117struct ZipFuse {
118 zip_archive: Mutex<zip::ZipArchive<File>>,
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900119 raw_file: Mutex<File>,
Jiyong Park331d1ea2021-05-10 11:01:23 +0900120 inode_table: InodeTable,
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900121 open_files: Mutex<HashMap<Handle, OpenFile>>,
Jiyong Park331d1ea2021-05-10 11:01:23 +0900122 open_dirs: Mutex<HashMap<Handle, OpenDirBuf>>,
123}
124
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900125/// Represents a [`ZipFile`] that is opened.
126struct OpenFile {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900127 open_count: u32, // multiple opens share the buf because this is a read-only filesystem
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900128 content: OpenFileContent,
129}
130
131/// Holds the content of a [`ZipFile`]. Depending on whether it is compressed or not, the
132/// entire content is stored, or only the zip index is stored.
133enum OpenFileContent {
134 Compressed(Box<[u8]>),
135 Uncompressed(usize), // zip index
Jiyong Park331d1ea2021-05-10 11:01:23 +0900136}
137
138/// Holds the directory entries in a directory opened by [`opendir`].
139struct OpenDirBuf {
140 open_count: u32,
141 buf: Box<[(CString, DirectoryEntry)]>,
142}
143
144type Handle = u64;
145
146fn ebadf() -> io::Error {
147 io::Error::from_raw_os_error(libc::EBADF)
148}
149
150fn timeout_max() -> std::time::Duration {
151 std::time::Duration::new(u64::MAX, 1_000_000_000 - 1)
152}
153
154impl ZipFuse {
155 fn new(zip_file: &Path) -> Result<ZipFuse> {
156 // TODO(jiyong): Use O_DIRECT to avoid double caching.
157 // `.custom_flags(nix::fcntl::OFlag::O_DIRECT.bits())` currently doesn't work.
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900158 let f = File::open(zip_file)?;
Jiyong Park331d1ea2021-05-10 11:01:23 +0900159 let mut z = zip::ZipArchive::new(f)?;
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900160 // Open the same file again so that we can directly access it when accessing
161 // uncompressed zip_file entries in it. `ZipFile` doesn't implement `Seek`.
162 let raw_file = File::open(zip_file)?;
Jiyong Park331d1ea2021-05-10 11:01:23 +0900163 let it = InodeTable::from_zip(&mut z)?;
164 Ok(ZipFuse {
165 zip_archive: Mutex::new(z),
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900166 raw_file: Mutex::new(raw_file),
Jiyong Park331d1ea2021-05-10 11:01:23 +0900167 inode_table: it,
168 open_files: Mutex::new(HashMap::new()),
169 open_dirs: Mutex::new(HashMap::new()),
170 })
171 }
172
173 fn find_inode(&self, inode: Inode) -> io::Result<&InodeData> {
174 self.inode_table.get(inode).ok_or_else(ebadf)
175 }
176
Jiyong Parkd5df9562021-05-13 00:50:23 +0900177 // TODO(jiyong) remove this. Right now this is needed to do the nlink_t to u64 conversion below
178 // on aosp_x86_64 target. That however is a useless conversion on other targets.
179 #[allow(clippy::useless_conversion)]
Jiyong Park331d1ea2021-05-10 11:01:23 +0900180 fn stat_from(&self, inode: Inode) -> io::Result<libc::stat64> {
181 let inode_data = self.find_inode(inode)?;
182 let mut st = unsafe { std::mem::MaybeUninit::<libc::stat64>::zeroed().assume_init() };
183 st.st_dev = 0;
Jiyong Parkd5df9562021-05-13 00:50:23 +0900184 st.st_nlink = if let Some(directory) = inode_data.get_directory() {
185 (2 + directory.len() as libc::nlink_t).into()
Jiyong Park331d1ea2021-05-10 11:01:23 +0900186 } else {
187 1
188 };
189 st.st_ino = inode;
190 st.st_mode = if inode_data.is_dir() { libc::S_IFDIR } else { libc::S_IFREG };
191 st.st_mode |= inode_data.mode;
192 st.st_uid = 0;
193 st.st_gid = 0;
194 st.st_size = i64::try_from(inode_data.size).unwrap_or(i64::MAX);
195 Ok(st)
196 }
197}
198
199impl fuse::filesystem::FileSystem for ZipFuse {
200 type Inode = Inode;
201 type Handle = Handle;
202 type DirIter = DirIter;
203
204 fn init(&self, _capable: FsOptions) -> std::io::Result<FsOptions> {
205 // The default options added by the fuse crate are fine. We don't have additional options.
206 Ok(FsOptions::empty())
207 }
208
209 fn lookup(&self, _ctx: Context, parent: Self::Inode, name: &CStr) -> io::Result<Entry> {
210 let inode = self.find_inode(parent)?;
211 let directory = inode.get_directory().ok_or_else(ebadf)?;
Jiyong Park331d1ea2021-05-10 11:01:23 +0900212 let entry = directory.get(name);
213 match entry {
214 Some(e) => Ok(Entry {
215 inode: e.inode,
216 generation: 0,
217 attr: self.stat_from(e.inode)?,
218 attr_timeout: timeout_max(), // this is a read-only fs
219 entry_timeout: timeout_max(),
220 }),
221 _ => Err(io::Error::from_raw_os_error(libc::ENOENT)),
222 }
223 }
224
225 fn getattr(
226 &self,
227 _ctx: Context,
228 inode: Self::Inode,
229 _handle: Option<Self::Handle>,
230 ) -> io::Result<(libc::stat64, std::time::Duration)> {
231 let st = self.stat_from(inode)?;
232 Ok((st, timeout_max()))
233 }
234
235 fn open(
236 &self,
237 _ctx: Context,
238 inode: Self::Inode,
239 _flags: u32,
240 ) -> io::Result<(Option<Self::Handle>, fuse::filesystem::OpenOptions)> {
241 let mut open_files = self.open_files.lock().unwrap();
242 let handle = inode as Handle;
243
244 // If the file is already opened, just increase the reference counter. If not, read the
245 // entire file content to the buffer. When `read` is called, a portion of the buffer is
246 // copied to the kernel.
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900247 if let Some(file) = open_files.get_mut(&handle) {
248 if file.open_count == 0 {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900249 return Err(ebadf());
250 }
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900251 file.open_count += 1;
Jiyong Park331d1ea2021-05-10 11:01:23 +0900252 } else {
253 let inode_data = self.find_inode(inode)?;
254 let zip_index = inode_data.get_zip_index().ok_or_else(ebadf)?;
255 let mut zip_archive = self.zip_archive.lock().unwrap();
256 let mut zip_file = zip_archive.by_index(zip_index)?;
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900257 let content = match zip_file.compression() {
258 zip::CompressionMethod::Stored => OpenFileContent::Uncompressed(zip_index),
259 _ => {
260 if let Some(mode) = zip_file.unix_mode() {
261 let is_reg_file = zip_file.is_file();
262 let is_executable =
263 mode & (libc::S_IXUSR | libc::S_IXGRP | libc::S_IXOTH) != 0;
264 if is_reg_file && is_executable {
265 log::warn!(
266 "Executable file {:?} is stored compressed. Consider \
267 storing it uncompressed to save memory",
268 zip_file.mangled_name()
269 );
270 }
271 }
272 let mut buf = Vec::with_capacity(inode_data.size as usize);
273 zip_file.read_to_end(&mut buf)?;
274 OpenFileContent::Compressed(buf.into_boxed_slice())
275 }
276 };
277 open_files.insert(handle, OpenFile { open_count: 1, content });
Jiyong Park331d1ea2021-05-10 11:01:23 +0900278 }
279 // Note: we don't return `DIRECT_IO` here, because then applications wouldn't be able to
280 // mmap the files.
281 Ok((Some(handle), fuse::filesystem::OpenOptions::empty()))
282 }
283
284 fn release(
285 &self,
286 _ctx: Context,
287 inode: Self::Inode,
288 _flags: u32,
289 _handle: Self::Handle,
290 _flush: bool,
291 _flock_release: bool,
292 _lock_owner: Option<u64>,
293 ) -> io::Result<()> {
294 // Releases the buffer for the `handle` when it is opened for nobody. While this is good
295 // for saving memory, this has a performance implication because we need to decompress
296 // again when the same file is opened in the future.
297 let mut open_files = self.open_files.lock().unwrap();
298 let handle = inode as Handle;
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900299 if let Some(file) = open_files.get_mut(&handle) {
300 if file.open_count.checked_sub(1).ok_or_else(ebadf)? == 0 {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900301 open_files.remove(&handle);
302 }
303 Ok(())
304 } else {
305 Err(ebadf())
306 }
307 }
308
309 fn read<W: io::Write + ZeroCopyWriter>(
310 &self,
311 _ctx: Context,
312 _inode: Self::Inode,
313 handle: Self::Handle,
314 mut w: W,
315 size: u32,
316 offset: u64,
317 _lock_owner: Option<u64>,
318 _flags: u32,
319 ) -> io::Result<usize> {
320 let open_files = self.open_files.lock().unwrap();
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900321 let file = open_files.get(&handle).ok_or_else(ebadf)?;
322 if file.open_count == 0 {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900323 return Err(ebadf());
324 }
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900325 Ok(match &file.content {
326 OpenFileContent::Uncompressed(zip_index) => {
327 let mut zip_archive = self.zip_archive.lock().unwrap();
328 let zip_file = zip_archive.by_index(*zip_index)?;
329 let start = zip_file.data_start() + offset;
330 let remaining_size = zip_file.size() - offset;
331 let size = std::cmp::min(remaining_size, size.into());
332
333 let mut raw_file = self.raw_file.lock().unwrap();
334 w.write_from(&mut raw_file, size as usize, start)?
335 }
336 OpenFileContent::Compressed(buf) => {
337 let start = offset as usize;
338 let end = start + size as usize;
339 let end = std::cmp::min(end, buf.len());
340 w.write(&buf[start..end])?
341 }
342 })
Jiyong Park331d1ea2021-05-10 11:01:23 +0900343 }
344
345 fn opendir(
346 &self,
347 _ctx: Context,
348 inode: Self::Inode,
349 _flags: u32,
350 ) -> io::Result<(Option<Self::Handle>, fuse::filesystem::OpenOptions)> {
351 let mut open_dirs = self.open_dirs.lock().unwrap();
352 let handle = inode as Handle;
353 if let Some(odb) = open_dirs.get_mut(&handle) {
354 if odb.open_count == 0 {
355 return Err(ebadf());
356 }
357 odb.open_count += 1;
358 } else {
359 let inode_data = self.find_inode(inode)?;
360 let directory = inode_data.get_directory().ok_or_else(ebadf)?;
361 let mut buf: Vec<(CString, DirectoryEntry)> = Vec::with_capacity(directory.len());
362 for (name, dir_entry) in directory.iter() {
363 let name = CString::new(name.as_bytes()).unwrap();
364 buf.push((name, dir_entry.clone()));
365 }
366 open_dirs.insert(handle, OpenDirBuf { open_count: 1, buf: buf.into_boxed_slice() });
367 }
Jiyong Park63a95cf2021-05-13 19:20:30 +0900368 Ok((Some(handle), fuse::filesystem::OpenOptions::CACHE_DIR))
Jiyong Park331d1ea2021-05-10 11:01:23 +0900369 }
370
371 fn releasedir(
372 &self,
373 _ctx: Context,
374 inode: Self::Inode,
375 _flags: u32,
376 _handle: Self::Handle,
377 ) -> io::Result<()> {
378 let mut open_dirs = self.open_dirs.lock().unwrap();
379 let handle = inode as Handle;
380 if let Some(odb) = open_dirs.get_mut(&handle) {
381 if odb.open_count.checked_sub(1).ok_or_else(ebadf)? == 0 {
382 open_dirs.remove(&handle);
383 }
384 Ok(())
385 } else {
386 Err(ebadf())
387 }
388 }
389
390 fn readdir(
391 &self,
392 _ctx: Context,
393 inode: Self::Inode,
394 _handle: Self::Handle,
395 size: u32,
396 offset: u64,
397 ) -> io::Result<Self::DirIter> {
398 let open_dirs = self.open_dirs.lock().unwrap();
399 let handle = inode as Handle;
400 let odb = open_dirs.get(&handle).ok_or_else(ebadf)?;
401 if odb.open_count == 0 {
402 return Err(ebadf());
403 }
404 let buf = &odb.buf;
405 let start = offset as usize;
Jiyong Park63a95cf2021-05-13 19:20:30 +0900406
407 // Estimate the size of each entry will take space in the buffer. See
408 // external/crosvm/fuse/src/server.rs#add_dirent
409 let mut estimate: usize = 0; // estimated number of bytes we will be writing
410 let mut end = start; // index in `buf`
411 while estimate < size as usize && end < buf.len() {
412 let dirent_size = size_of::<fuse::sys::Dirent>();
413 let name_size = buf[end].0.to_bytes().len();
414 estimate += (dirent_size + name_size + 7) & !7; // round to 8 byte boundary
415 end += 1;
416 }
417
Jiyong Park331d1ea2021-05-10 11:01:23 +0900418 let mut new_buf = Vec::with_capacity(end - start);
419 // The portion of `buf` is *copied* to the iterator. This is not ideal, but inevitable
420 // because the `name` field in `fuse::filesystem::DirEntry` is `&CStr` not `CString`.
421 new_buf.extend_from_slice(&buf[start..end]);
422 Ok(DirIter { inner: new_buf, offset, cur: 0 })
423 }
424}
425
426struct DirIter {
427 inner: Vec<(CString, DirectoryEntry)>,
428 offset: u64, // the offset where this iterator begins. `next` doesn't change this.
429 cur: usize, // the current index in `inner`. `next` advances this.
430}
431
432impl fuse::filesystem::DirectoryIterator for DirIter {
433 fn next(&mut self) -> Option<fuse::filesystem::DirEntry> {
434 if self.cur >= self.inner.len() {
435 return None;
436 }
437
438 let (name, entry) = &self.inner[self.cur];
439 self.cur += 1;
440 Some(fuse::filesystem::DirEntry {
441 ino: entry.inode as libc::ino64_t,
442 offset: self.offset + self.cur as u64,
443 type_: match entry.kind {
444 InodeKind::Directory => libc::DT_DIR.into(),
445 InodeKind::File => libc::DT_REG.into(),
446 },
447 name,
448 })
449 }
450}
451
452#[cfg(test)]
453mod tests {
454 use anyhow::{bail, Result};
455 use nix::sys::statfs::{statfs, FsType};
Jiyong Park63a95cf2021-05-13 19:20:30 +0900456 use std::collections::BTreeSet;
Jiyong Park331d1ea2021-05-10 11:01:23 +0900457 use std::fs;
458 use std::fs::File;
459 use std::io::Write;
460 use std::path::{Path, PathBuf};
461 use std::time::{Duration, Instant};
462 use zip::write::FileOptions;
463
464 #[cfg(not(target_os = "android"))]
Andrew Scull3854e402022-07-04 12:10:20 +0000465 fn start_fuse(zip_path: &Path, mnt_path: &Path, noexec: bool) {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900466 let zip_path = PathBuf::from(zip_path);
467 let mnt_path = PathBuf::from(mnt_path);
468 std::thread::spawn(move || {
Andrew Scull3854e402022-07-04 12:10:20 +0000469 crate::run_fuse(&zip_path, &mnt_path, None, noexec).unwrap();
Jiyong Park331d1ea2021-05-10 11:01:23 +0900470 });
471 }
472
473 #[cfg(target_os = "android")]
Andrew Scull3854e402022-07-04 12:10:20 +0000474 fn start_fuse(zip_path: &Path, mnt_path: &Path, noexec: bool) {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900475 // Note: for some unknown reason, running a thread to serve fuse doesn't work on Android.
476 // Explicitly spawn a zipfuse process instead.
477 // TODO(jiyong): fix this
Andrew Scull3854e402022-07-04 12:10:20 +0000478 let noexec = if noexec { "--noexec" } else { "" };
Jiyong Park331d1ea2021-05-10 11:01:23 +0900479 assert!(std::process::Command::new("sh")
480 .arg("-c")
Andrew Scull3854e402022-07-04 12:10:20 +0000481 .arg(format!(
482 "/data/local/tmp/zipfuse {} {} {}",
483 noexec,
484 zip_path.display(),
485 mnt_path.display()
486 ))
Jiyong Park331d1ea2021-05-10 11:01:23 +0900487 .spawn()
488 .is_ok());
489 }
490
491 fn wait_for_mount(mount_path: &Path) -> Result<()> {
492 let start_time = Instant::now();
493 const POLL_INTERVAL: Duration = Duration::from_millis(50);
494 const TIMEOUT: Duration = Duration::from_secs(10);
495 const FUSE_SUPER_MAGIC: FsType = FsType(0x65735546);
496 loop {
497 if statfs(mount_path)?.filesystem_type() == FUSE_SUPER_MAGIC {
498 break;
499 }
500
501 if start_time.elapsed() > TIMEOUT {
502 bail!("Time out mounting zipfuse");
503 }
504 std::thread::sleep(POLL_INTERVAL);
505 }
506 Ok(())
507 }
508
509 // Creates a zip file, adds some files to the zip file, mounts it using zipfuse, runs the check
510 // routine, and finally unmounts.
511 fn run_test(add: fn(&mut zip::ZipWriter<File>), check: fn(&std::path::Path)) {
Andrew Scull3854e402022-07-04 12:10:20 +0000512 run_test_noexec(false, add, check);
513 }
514
515 fn run_test_noexec(
516 noexec: bool,
517 add: fn(&mut zip::ZipWriter<File>),
518 check: fn(&std::path::Path),
519 ) {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900520 // Create an empty zip file
521 let test_dir = tempfile::TempDir::new().unwrap();
522 let zip_path = test_dir.path().join("test.zip");
523 let zip = File::create(&zip_path);
524 assert!(zip.is_ok());
525 let mut zip = zip::ZipWriter::new(zip.unwrap());
526
527 // Let test users add files/dirs to the zip file
528 add(&mut zip);
529 assert!(zip.finish().is_ok());
530 drop(zip);
531
532 // Mount the zip file on the "mnt" dir using zipfuse.
533 let mnt_path = test_dir.path().join("mnt");
534 assert!(fs::create_dir(&mnt_path).is_ok());
535
Andrew Scull3854e402022-07-04 12:10:20 +0000536 start_fuse(&zip_path, &mnt_path, noexec);
Jiyong Park331d1ea2021-05-10 11:01:23 +0900537
538 let mnt_path = test_dir.path().join("mnt");
539 // Give some time for the fuse to boot up
540 assert!(wait_for_mount(&mnt_path).is_ok());
541 // Run the check routine, and do the clean up.
542 check(&mnt_path);
543 assert!(nix::mount::umount2(&mnt_path, nix::mount::MntFlags::empty()).is_ok());
544 }
545
546 fn check_file(root: &Path, file: &str, content: &[u8]) {
547 let path = root.join(file);
548 assert!(path.exists());
549
550 let metadata = fs::metadata(&path);
551 assert!(metadata.is_ok());
552
553 let metadata = metadata.unwrap();
554 assert!(metadata.is_file());
555 assert_eq!(content.len(), metadata.len() as usize);
556
557 let read_data = fs::read(&path);
558 assert!(read_data.is_ok());
559 assert_eq!(content, read_data.unwrap().as_slice());
560 }
561
Jiyong Park63a95cf2021-05-13 19:20:30 +0900562 fn check_dir<S: AsRef<str>>(root: &Path, dir: &str, files: &[S], dirs: &[S]) {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900563 let dir_path = root.join(dir);
564 assert!(dir_path.exists());
565
566 let metadata = fs::metadata(&dir_path);
567 assert!(metadata.is_ok());
568
569 let metadata = metadata.unwrap();
570 assert!(metadata.is_dir());
571
572 let iter = fs::read_dir(&dir_path);
573 assert!(iter.is_ok());
574
575 let iter = iter.unwrap();
Jiyong Park63a95cf2021-05-13 19:20:30 +0900576 let mut actual_files = BTreeSet::new();
577 let mut actual_dirs = BTreeSet::new();
Jiyong Park331d1ea2021-05-10 11:01:23 +0900578 for de in iter {
579 let entry = de.unwrap();
580 let path = entry.path();
581 if path.is_dir() {
582 actual_dirs.insert(path.strip_prefix(&dir_path).unwrap().to_path_buf());
583 } else {
584 actual_files.insert(path.strip_prefix(&dir_path).unwrap().to_path_buf());
585 }
586 }
Jiyong Park63a95cf2021-05-13 19:20:30 +0900587 let expected_files: BTreeSet<PathBuf> =
588 files.iter().map(|s| PathBuf::from(s.as_ref())).collect();
589 let expected_dirs: BTreeSet<PathBuf> =
590 dirs.iter().map(|s| PathBuf::from(s.as_ref())).collect();
Jiyong Park331d1ea2021-05-10 11:01:23 +0900591
592 assert_eq!(expected_files, actual_files);
593 assert_eq!(expected_dirs, actual_dirs);
594 }
595
596 #[test]
597 fn empty() {
598 run_test(
599 |_| {},
600 |root| {
Jiyong Park63a95cf2021-05-13 19:20:30 +0900601 check_dir::<String>(root, "", &[], &[]);
Jiyong Park331d1ea2021-05-10 11:01:23 +0900602 },
603 );
604 }
605
606 #[test]
607 fn single_file() {
608 run_test(
609 |zip| {
610 zip.start_file("foo", FileOptions::default()).unwrap();
611 zip.write_all(b"0123456789").unwrap();
612 },
613 |root| {
614 check_dir(root, "", &["foo"], &[]);
615 check_file(root, "foo", b"0123456789");
616 },
617 );
618 }
619
620 #[test]
Andrew Scull3854e402022-07-04 12:10:20 +0000621 fn noexec() {
622 fn add_executable(zip: &mut zip::ZipWriter<File>) {
623 zip.start_file("executable", FileOptions::default().unix_permissions(0o755)).unwrap();
624 }
625
626 // Executables can be run when not mounting with noexec.
627 run_test(add_executable, |root| {
628 let res = std::process::Command::new(root.join("executable")).status();
629 res.unwrap();
630 });
631
632 // Mounting with noexec results in permissions denial when running an executable.
633 let noexec = true;
634 run_test_noexec(noexec, add_executable, |root| {
635 let res = std::process::Command::new(root.join("executable")).status();
636 assert!(matches!(res.unwrap_err().kind(), std::io::ErrorKind::PermissionDenied));
637 });
638 }
639
640 #[test]
Jiyong Park331d1ea2021-05-10 11:01:23 +0900641 fn single_dir() {
642 run_test(
643 |zip| {
644 zip.add_directory("dir", FileOptions::default()).unwrap();
645 },
646 |root| {
647 check_dir(root, "", &[], &["dir"]);
Jiyong Park63a95cf2021-05-13 19:20:30 +0900648 check_dir::<String>(root, "dir", &[], &[]);
Jiyong Park331d1ea2021-05-10 11:01:23 +0900649 },
650 );
651 }
652
653 #[test]
654 fn complex_hierarchy() {
655 // root/
656 // a/
657 // b1/
658 // b2/
659 // c1 (file)
660 // c2/
661 // d1 (file)
662 // d2 (file)
663 // d3 (file)
664 // x/
665 // y1 (file)
666 // y2 (file)
667 // y3/
668 //
669 // foo (file)
670 // bar (file)
671 run_test(
672 |zip| {
673 let opt = FileOptions::default();
674 zip.add_directory("a/b1", opt).unwrap();
675
676 zip.start_file("a/b2/c1", opt).unwrap();
677
678 zip.start_file("a/b2/c2/d1", opt).unwrap();
679 zip.start_file("a/b2/c2/d2", opt).unwrap();
680 zip.start_file("a/b2/c2/d3", opt).unwrap();
681
682 zip.start_file("x/y1", opt).unwrap();
683 zip.start_file("x/y2", opt).unwrap();
684 zip.add_directory("x/y3", opt).unwrap();
685
686 zip.start_file("foo", opt).unwrap();
687 zip.start_file("bar", opt).unwrap();
688 },
689 |root| {
690 check_dir(root, "", &["foo", "bar"], &["a", "x"]);
691 check_dir(root, "a", &[], &["b1", "b2"]);
Jiyong Park63a95cf2021-05-13 19:20:30 +0900692 check_dir::<String>(root, "a/b1", &[], &[]);
Jiyong Park331d1ea2021-05-10 11:01:23 +0900693 check_dir(root, "a/b2", &["c1"], &["c2"]);
694 check_dir(root, "a/b2/c2", &["d1", "d2", "d3"], &[]);
695 check_dir(root, "x", &["y1", "y2"], &["y3"]);
Jiyong Park63a95cf2021-05-13 19:20:30 +0900696 check_dir::<String>(root, "x/y3", &[], &[]);
Jiyong Park331d1ea2021-05-10 11:01:23 +0900697 check_file(root, "a/b2/c1", &[]);
698 check_file(root, "a/b2/c2/d1", &[]);
699 check_file(root, "a/b2/c2/d2", &[]);
700 check_file(root, "a/b2/c2/d3", &[]);
701 check_file(root, "x/y1", &[]);
702 check_file(root, "x/y2", &[]);
703 check_file(root, "foo", &[]);
704 check_file(root, "bar", &[]);
705 },
706 );
707 }
708
709 #[test]
710 fn large_file() {
711 run_test(
712 |zip| {
713 let data = vec![10; 2 << 20];
714 zip.start_file("foo", FileOptions::default()).unwrap();
715 zip.write_all(&data).unwrap();
716 },
717 |root| {
718 let data = vec![10; 2 << 20];
719 check_file(root, "foo", &data);
720 },
721 );
722 }
Jiyong Park63a95cf2021-05-13 19:20:30 +0900723
724 #[test]
725 fn large_dir() {
726 const NUM_FILES: usize = 1 << 10;
727 run_test(
728 |zip| {
729 let opt = FileOptions::default();
730 // create 1K files. Each file has a name of length 100. So total size is at least
731 // 100KB, which is bigger than the readdir buffer size of 4K.
732 for i in 0..NUM_FILES {
733 zip.start_file(format!("dir/{:0100}", i), opt).unwrap();
734 }
735 },
736 |root| {
737 let dirs_expected: Vec<_> = (0..NUM_FILES).map(|i| format!("{:0100}", i)).collect();
738 check_dir(
739 root,
740 "dir",
741 dirs_expected.iter().map(|s| s.as_str()).collect::<Vec<&str>>().as_slice(),
742 &[],
743 );
744 },
745 );
746 }
Jiyong Parkd40f7bb2021-05-17 10:55:56 +0900747
Jiyong Parke6587ca2021-05-17 14:42:23 +0900748 fn run_fuse_and_check_test_zip(test_dir: &Path, zip_path: &Path) {
749 let mnt_path = test_dir.join("mnt");
Jiyong Parkd40f7bb2021-05-17 10:55:56 +0900750 assert!(fs::create_dir(&mnt_path).is_ok());
751
Andrew Scull3854e402022-07-04 12:10:20 +0000752 let noexec = false;
753 start_fuse(zip_path, &mnt_path, noexec);
Jiyong Parkd40f7bb2021-05-17 10:55:56 +0900754
755 // Give some time for the fuse to boot up
756 assert!(wait_for_mount(&mnt_path).is_ok());
757
758 check_dir(&mnt_path, "", &[], &["dir"]);
759 check_dir(&mnt_path, "dir", &["file1", "file2"], &[]);
760 check_file(&mnt_path, "dir/file1", include_bytes!("../testdata/dir/file1"));
761 check_file(&mnt_path, "dir/file2", include_bytes!("../testdata/dir/file2"));
762 assert!(nix::mount::umount2(&mnt_path, nix::mount::MntFlags::empty()).is_ok());
763 }
Jiyong Parke6587ca2021-05-17 14:42:23 +0900764
765 #[test]
766 fn supports_deflate() {
767 let test_dir = tempfile::TempDir::new().unwrap();
768 let zip_path = test_dir.path().join("test.zip");
769 let mut zip_file = File::create(&zip_path).unwrap();
770 zip_file.write_all(include_bytes!("../testdata/test.zip")).unwrap();
771
Chris Wailes68c39f82021-07-27 16:03:44 -0700772 run_fuse_and_check_test_zip(test_dir.path(), &zip_path);
Jiyong Parke6587ca2021-05-17 14:42:23 +0900773 }
774
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900775 #[test]
776 fn supports_store() {
777 run_test(
778 |zip| {
779 let data = vec![10; 2 << 20];
780 zip.start_file(
781 "foo",
782 FileOptions::default().compression_method(zip::CompressionMethod::Stored),
783 )
784 .unwrap();
785 zip.write_all(&data).unwrap();
786 },
787 |root| {
788 let data = vec![10; 2 << 20];
789 check_file(root, "foo", &data);
790 },
791 );
792 }
793
Jiyong Parke6587ca2021-05-17 14:42:23 +0900794 #[cfg(not(target_os = "android"))] // Android doesn't have the loopdev crate
795 #[test]
796 fn supports_zip_on_block_device() {
797 // Write test.zip to the test directory
798 let test_dir = tempfile::TempDir::new().unwrap();
799 let zip_path = test_dir.path().join("test.zip");
800 let mut zip_file = File::create(&zip_path).unwrap();
801 let data = include_bytes!("../testdata/test.zip");
802 zip_file.write_all(data).unwrap();
803
804 // Pad 0 to test.zip so that its size is multiple of 4096.
805 const BLOCK_SIZE: usize = 4096;
806 let size = (data.len() + BLOCK_SIZE) & !BLOCK_SIZE;
807 let pad_size = size - data.len();
808 assert!(pad_size != 0);
809 let pad = vec![0; pad_size];
810 zip_file.write_all(pad.as_slice()).unwrap();
811 drop(zip_file);
812
813 // Attach test.zip to a loop device
814 let lc = loopdev::LoopControl::open().unwrap();
815 let ld = scopeguard::guard(lc.next_free().unwrap(), |ld| {
816 ld.detach().unwrap();
817 });
818 ld.attach_file(&zip_path).unwrap();
819
820 // Start zipfuse over to the loop device (not the zip file)
821 run_fuse_and_check_test_zip(&test_dir.path(), &ld.path().unwrap());
822 }
Jiyong Park331d1ea2021-05-10 11:01:23 +0900823}