blob: 365d2366a54201b8a4e6c8c366c8bb4027711a1d [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 Park231f9692023-01-09 19:36:21 +090062 .arg(
63 Arg::with_name("uid")
64 .short('u')
65 .takes_value(true)
66 .help("numeric UID who's the owner of the files"),
67 )
68 .arg(
69 Arg::with_name("gid")
70 .short('g')
71 .takes_value(true)
72 .help("numeric GID who's the group of the files"),
73 )
Jiyong Park331d1ea2021-05-10 11:01:23 +090074 .arg(Arg::with_name("ZIPFILE").required(true))
75 .arg(Arg::with_name("MOUNTPOINT").required(true))
76 .get_matches();
77
78 let zip_file = matches.value_of("ZIPFILE").unwrap().as_ref();
79 let mount_point = matches.value_of("MOUNTPOINT").unwrap().as_ref();
Jiyong Park6a762db2021-05-31 14:00:52 +090080 let options = matches.value_of("options");
Andrew Scull3854e402022-07-04 12:10:20 +000081 let noexec = matches.is_present("noexec");
Alan Stokes60f82202022-10-07 16:40:07 +010082 let ready_prop = matches.value_of("readyprop");
Jiyong Park231f9692023-01-09 19:36:21 +090083 let uid: u32 = matches.value_of("uid").map_or(0, |s| s.parse().unwrap());
84 let gid: u32 = matches.value_of("gid").map_or(0, |s| s.parse().unwrap());
85 run_fuse(zip_file, mount_point, options, noexec, ready_prop, uid, gid)?;
Jiyong Park331d1ea2021-05-10 11:01:23 +090086 Ok(())
87}
88
89/// Runs a fuse filesystem by mounting `zip_file` on `mount_point`.
Andrew Scull3854e402022-07-04 12:10:20 +000090pub fn run_fuse(
91 zip_file: &Path,
92 mount_point: &Path,
93 extra_options: Option<&str>,
94 noexec: bool,
Alan Stokes60f82202022-10-07 16:40:07 +010095 ready_prop: Option<&str>,
Jiyong Park231f9692023-01-09 19:36:21 +090096 uid: u32,
97 gid: u32,
Andrew Scull3854e402022-07-04 12:10:20 +000098) -> Result<()> {
Jiyong Park331d1ea2021-05-10 11:01:23 +090099 const MAX_READ: u32 = 1 << 20; // TODO(jiyong): tune this
100 const MAX_WRITE: u32 = 1 << 13; // This is a read-only filesystem
101
102 let dev_fuse = OpenOptions::new().read(true).write(true).open("/dev/fuse")?;
103
Jiyong Park6a762db2021-05-31 14:00:52 +0900104 let mut mount_options = vec![
105 MountOption::FD(dev_fuse.as_raw_fd()),
Jiyong Park98ce68d2023-01-09 17:46:19 +0900106 MountOption::DefaultPermissions,
Jiyong Park6a762db2021-05-31 14:00:52 +0900107 MountOption::RootMode(libc::S_IFDIR | libc::S_IXUSR | libc::S_IXGRP | libc::S_IXOTH),
108 MountOption::AllowOther,
109 MountOption::UserId(0),
110 MountOption::GroupId(0),
111 MountOption::MaxRead(MAX_READ),
112 ];
113 if let Some(value) = extra_options {
114 mount_options.push(MountOption::Extra(value));
115 }
116
Andrew Scull3854e402022-07-04 12:10:20 +0000117 let mut mount_flags = libc::MS_NOSUID | libc::MS_NODEV | libc::MS_RDONLY;
118 if noexec {
119 mount_flags |= libc::MS_NOEXEC;
120 }
121
122 fuse::mount(mount_point, "zipfuse", mount_flags, &mount_options)?;
Alan Stokes60f82202022-10-07 16:40:07 +0100123
124 if let Some(property_name) = ready_prop {
125 system_properties::write(property_name, "1").context("Failed to set readyprop")?;
126 }
127
Victor Hsieh58a5e9b2022-03-09 21:57:26 +0000128 let mut config = fuse::FuseConfig::new();
129 config.dev_fuse(dev_fuse).max_write(MAX_WRITE).max_read(MAX_READ);
Jiyong Park231f9692023-01-09 19:36:21 +0900130 Ok(config.enter_message_loop(ZipFuse::new(zip_file, uid, gid)?)?)
Jiyong Park331d1ea2021-05-10 11:01:23 +0900131}
132
133struct ZipFuse {
134 zip_archive: Mutex<zip::ZipArchive<File>>,
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900135 raw_file: Mutex<File>,
Jiyong Park331d1ea2021-05-10 11:01:23 +0900136 inode_table: InodeTable,
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900137 open_files: Mutex<HashMap<Handle, OpenFile>>,
Jiyong Park331d1ea2021-05-10 11:01:23 +0900138 open_dirs: Mutex<HashMap<Handle, OpenDirBuf>>,
Jiyong Park231f9692023-01-09 19:36:21 +0900139 uid: u32,
140 gid: u32,
Jiyong Park331d1ea2021-05-10 11:01:23 +0900141}
142
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900143/// Represents a [`ZipFile`] that is opened.
144struct OpenFile {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900145 open_count: u32, // multiple opens share the buf because this is a read-only filesystem
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900146 content: OpenFileContent,
147}
148
149/// Holds the content of a [`ZipFile`]. Depending on whether it is compressed or not, the
150/// entire content is stored, or only the zip index is stored.
151enum OpenFileContent {
152 Compressed(Box<[u8]>),
153 Uncompressed(usize), // zip index
Jiyong Park331d1ea2021-05-10 11:01:23 +0900154}
155
156/// Holds the directory entries in a directory opened by [`opendir`].
157struct OpenDirBuf {
158 open_count: u32,
159 buf: Box<[(CString, DirectoryEntry)]>,
160}
161
162type Handle = u64;
163
164fn ebadf() -> io::Error {
165 io::Error::from_raw_os_error(libc::EBADF)
166}
167
168fn timeout_max() -> std::time::Duration {
169 std::time::Duration::new(u64::MAX, 1_000_000_000 - 1)
170}
171
172impl ZipFuse {
Jiyong Park231f9692023-01-09 19:36:21 +0900173 fn new(zip_file: &Path, uid: u32, gid: u32) -> Result<ZipFuse> {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900174 // TODO(jiyong): Use O_DIRECT to avoid double caching.
175 // `.custom_flags(nix::fcntl::OFlag::O_DIRECT.bits())` currently doesn't work.
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900176 let f = File::open(zip_file)?;
Jiyong Park331d1ea2021-05-10 11:01:23 +0900177 let mut z = zip::ZipArchive::new(f)?;
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900178 // Open the same file again so that we can directly access it when accessing
179 // uncompressed zip_file entries in it. `ZipFile` doesn't implement `Seek`.
180 let raw_file = File::open(zip_file)?;
Jiyong Park331d1ea2021-05-10 11:01:23 +0900181 let it = InodeTable::from_zip(&mut z)?;
182 Ok(ZipFuse {
183 zip_archive: Mutex::new(z),
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900184 raw_file: Mutex::new(raw_file),
Jiyong Park331d1ea2021-05-10 11:01:23 +0900185 inode_table: it,
186 open_files: Mutex::new(HashMap::new()),
187 open_dirs: Mutex::new(HashMap::new()),
Jiyong Park231f9692023-01-09 19:36:21 +0900188 uid,
189 gid,
Jiyong Park331d1ea2021-05-10 11:01:23 +0900190 })
191 }
192
193 fn find_inode(&self, inode: Inode) -> io::Result<&InodeData> {
194 self.inode_table.get(inode).ok_or_else(ebadf)
195 }
196
Jiyong Parkd5df9562021-05-13 00:50:23 +0900197 // TODO(jiyong) remove this. Right now this is needed to do the nlink_t to u64 conversion below
198 // on aosp_x86_64 target. That however is a useless conversion on other targets.
199 #[allow(clippy::useless_conversion)]
Jiyong Park331d1ea2021-05-10 11:01:23 +0900200 fn stat_from(&self, inode: Inode) -> io::Result<libc::stat64> {
201 let inode_data = self.find_inode(inode)?;
202 let mut st = unsafe { std::mem::MaybeUninit::<libc::stat64>::zeroed().assume_init() };
203 st.st_dev = 0;
Jiyong Parkd5df9562021-05-13 00:50:23 +0900204 st.st_nlink = if let Some(directory) = inode_data.get_directory() {
205 (2 + directory.len() as libc::nlink_t).into()
Jiyong Park331d1ea2021-05-10 11:01:23 +0900206 } else {
207 1
208 };
209 st.st_ino = inode;
210 st.st_mode = if inode_data.is_dir() { libc::S_IFDIR } else { libc::S_IFREG };
211 st.st_mode |= inode_data.mode;
Jiyong Park231f9692023-01-09 19:36:21 +0900212 st.st_uid = self.uid;
213 st.st_gid = self.gid;
Jiyong Park331d1ea2021-05-10 11:01:23 +0900214 st.st_size = i64::try_from(inode_data.size).unwrap_or(i64::MAX);
215 Ok(st)
216 }
217}
218
219impl fuse::filesystem::FileSystem for ZipFuse {
220 type Inode = Inode;
221 type Handle = Handle;
222 type DirIter = DirIter;
223
224 fn init(&self, _capable: FsOptions) -> std::io::Result<FsOptions> {
225 // The default options added by the fuse crate are fine. We don't have additional options.
226 Ok(FsOptions::empty())
227 }
228
229 fn lookup(&self, _ctx: Context, parent: Self::Inode, name: &CStr) -> io::Result<Entry> {
230 let inode = self.find_inode(parent)?;
231 let directory = inode.get_directory().ok_or_else(ebadf)?;
Jiyong Park331d1ea2021-05-10 11:01:23 +0900232 let entry = directory.get(name);
233 match entry {
234 Some(e) => Ok(Entry {
235 inode: e.inode,
236 generation: 0,
237 attr: self.stat_from(e.inode)?,
238 attr_timeout: timeout_max(), // this is a read-only fs
239 entry_timeout: timeout_max(),
240 }),
241 _ => Err(io::Error::from_raw_os_error(libc::ENOENT)),
242 }
243 }
244
245 fn getattr(
246 &self,
247 _ctx: Context,
248 inode: Self::Inode,
249 _handle: Option<Self::Handle>,
250 ) -> io::Result<(libc::stat64, std::time::Duration)> {
251 let st = self.stat_from(inode)?;
252 Ok((st, timeout_max()))
253 }
254
255 fn open(
256 &self,
257 _ctx: Context,
258 inode: Self::Inode,
259 _flags: u32,
260 ) -> io::Result<(Option<Self::Handle>, fuse::filesystem::OpenOptions)> {
261 let mut open_files = self.open_files.lock().unwrap();
262 let handle = inode as Handle;
263
264 // If the file is already opened, just increase the reference counter. If not, read the
265 // entire file content to the buffer. When `read` is called, a portion of the buffer is
266 // copied to the kernel.
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900267 if let Some(file) = open_files.get_mut(&handle) {
268 if file.open_count == 0 {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900269 return Err(ebadf());
270 }
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900271 file.open_count += 1;
Jiyong Park331d1ea2021-05-10 11:01:23 +0900272 } else {
273 let inode_data = self.find_inode(inode)?;
274 let zip_index = inode_data.get_zip_index().ok_or_else(ebadf)?;
275 let mut zip_archive = self.zip_archive.lock().unwrap();
276 let mut zip_file = zip_archive.by_index(zip_index)?;
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900277 let content = match zip_file.compression() {
278 zip::CompressionMethod::Stored => OpenFileContent::Uncompressed(zip_index),
279 _ => {
280 if let Some(mode) = zip_file.unix_mode() {
281 let is_reg_file = zip_file.is_file();
282 let is_executable =
283 mode & (libc::S_IXUSR | libc::S_IXGRP | libc::S_IXOTH) != 0;
284 if is_reg_file && is_executable {
285 log::warn!(
286 "Executable file {:?} is stored compressed. Consider \
287 storing it uncompressed to save memory",
288 zip_file.mangled_name()
289 );
290 }
291 }
292 let mut buf = Vec::with_capacity(inode_data.size as usize);
293 zip_file.read_to_end(&mut buf)?;
294 OpenFileContent::Compressed(buf.into_boxed_slice())
295 }
296 };
297 open_files.insert(handle, OpenFile { open_count: 1, content });
Jiyong Park331d1ea2021-05-10 11:01:23 +0900298 }
299 // Note: we don't return `DIRECT_IO` here, because then applications wouldn't be able to
300 // mmap the files.
301 Ok((Some(handle), fuse::filesystem::OpenOptions::empty()))
302 }
303
304 fn release(
305 &self,
306 _ctx: Context,
307 inode: Self::Inode,
308 _flags: u32,
309 _handle: Self::Handle,
310 _flush: bool,
311 _flock_release: bool,
312 _lock_owner: Option<u64>,
313 ) -> io::Result<()> {
314 // Releases the buffer for the `handle` when it is opened for nobody. While this is good
315 // for saving memory, this has a performance implication because we need to decompress
316 // again when the same file is opened in the future.
317 let mut open_files = self.open_files.lock().unwrap();
318 let handle = inode as Handle;
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900319 if let Some(file) = open_files.get_mut(&handle) {
320 if file.open_count.checked_sub(1).ok_or_else(ebadf)? == 0 {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900321 open_files.remove(&handle);
322 }
323 Ok(())
324 } else {
325 Err(ebadf())
326 }
327 }
328
329 fn read<W: io::Write + ZeroCopyWriter>(
330 &self,
331 _ctx: Context,
332 _inode: Self::Inode,
333 handle: Self::Handle,
334 mut w: W,
335 size: u32,
336 offset: u64,
337 _lock_owner: Option<u64>,
338 _flags: u32,
339 ) -> io::Result<usize> {
340 let open_files = self.open_files.lock().unwrap();
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900341 let file = open_files.get(&handle).ok_or_else(ebadf)?;
342 if file.open_count == 0 {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900343 return Err(ebadf());
344 }
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900345 Ok(match &file.content {
346 OpenFileContent::Uncompressed(zip_index) => {
347 let mut zip_archive = self.zip_archive.lock().unwrap();
348 let zip_file = zip_archive.by_index(*zip_index)?;
349 let start = zip_file.data_start() + offset;
350 let remaining_size = zip_file.size() - offset;
351 let size = std::cmp::min(remaining_size, size.into());
352
353 let mut raw_file = self.raw_file.lock().unwrap();
354 w.write_from(&mut raw_file, size as usize, start)?
355 }
356 OpenFileContent::Compressed(buf) => {
357 let start = offset as usize;
358 let end = start + size as usize;
359 let end = std::cmp::min(end, buf.len());
360 w.write(&buf[start..end])?
361 }
362 })
Jiyong Park331d1ea2021-05-10 11:01:23 +0900363 }
364
365 fn opendir(
366 &self,
367 _ctx: Context,
368 inode: Self::Inode,
369 _flags: u32,
370 ) -> io::Result<(Option<Self::Handle>, fuse::filesystem::OpenOptions)> {
371 let mut open_dirs = self.open_dirs.lock().unwrap();
372 let handle = inode as Handle;
373 if let Some(odb) = open_dirs.get_mut(&handle) {
374 if odb.open_count == 0 {
375 return Err(ebadf());
376 }
377 odb.open_count += 1;
378 } else {
379 let inode_data = self.find_inode(inode)?;
380 let directory = inode_data.get_directory().ok_or_else(ebadf)?;
381 let mut buf: Vec<(CString, DirectoryEntry)> = Vec::with_capacity(directory.len());
382 for (name, dir_entry) in directory.iter() {
383 let name = CString::new(name.as_bytes()).unwrap();
384 buf.push((name, dir_entry.clone()));
385 }
386 open_dirs.insert(handle, OpenDirBuf { open_count: 1, buf: buf.into_boxed_slice() });
387 }
Jiyong Park63a95cf2021-05-13 19:20:30 +0900388 Ok((Some(handle), fuse::filesystem::OpenOptions::CACHE_DIR))
Jiyong Park331d1ea2021-05-10 11:01:23 +0900389 }
390
391 fn releasedir(
392 &self,
393 _ctx: Context,
394 inode: Self::Inode,
395 _flags: u32,
396 _handle: Self::Handle,
397 ) -> io::Result<()> {
398 let mut open_dirs = self.open_dirs.lock().unwrap();
399 let handle = inode as Handle;
400 if let Some(odb) = open_dirs.get_mut(&handle) {
401 if odb.open_count.checked_sub(1).ok_or_else(ebadf)? == 0 {
402 open_dirs.remove(&handle);
403 }
404 Ok(())
405 } else {
406 Err(ebadf())
407 }
408 }
409
410 fn readdir(
411 &self,
412 _ctx: Context,
413 inode: Self::Inode,
414 _handle: Self::Handle,
415 size: u32,
416 offset: u64,
417 ) -> io::Result<Self::DirIter> {
418 let open_dirs = self.open_dirs.lock().unwrap();
419 let handle = inode as Handle;
420 let odb = open_dirs.get(&handle).ok_or_else(ebadf)?;
421 if odb.open_count == 0 {
422 return Err(ebadf());
423 }
424 let buf = &odb.buf;
425 let start = offset as usize;
Jiyong Park63a95cf2021-05-13 19:20:30 +0900426
427 // Estimate the size of each entry will take space in the buffer. See
428 // external/crosvm/fuse/src/server.rs#add_dirent
429 let mut estimate: usize = 0; // estimated number of bytes we will be writing
430 let mut end = start; // index in `buf`
431 while estimate < size as usize && end < buf.len() {
432 let dirent_size = size_of::<fuse::sys::Dirent>();
433 let name_size = buf[end].0.to_bytes().len();
434 estimate += (dirent_size + name_size + 7) & !7; // round to 8 byte boundary
435 end += 1;
436 }
437
Jiyong Park331d1ea2021-05-10 11:01:23 +0900438 let mut new_buf = Vec::with_capacity(end - start);
439 // The portion of `buf` is *copied* to the iterator. This is not ideal, but inevitable
440 // because the `name` field in `fuse::filesystem::DirEntry` is `&CStr` not `CString`.
441 new_buf.extend_from_slice(&buf[start..end]);
442 Ok(DirIter { inner: new_buf, offset, cur: 0 })
443 }
444}
445
446struct DirIter {
447 inner: Vec<(CString, DirectoryEntry)>,
448 offset: u64, // the offset where this iterator begins. `next` doesn't change this.
449 cur: usize, // the current index in `inner`. `next` advances this.
450}
451
452impl fuse::filesystem::DirectoryIterator for DirIter {
453 fn next(&mut self) -> Option<fuse::filesystem::DirEntry> {
454 if self.cur >= self.inner.len() {
455 return None;
456 }
457
458 let (name, entry) = &self.inner[self.cur];
459 self.cur += 1;
460 Some(fuse::filesystem::DirEntry {
461 ino: entry.inode as libc::ino64_t,
462 offset: self.offset + self.cur as u64,
463 type_: match entry.kind {
464 InodeKind::Directory => libc::DT_DIR.into(),
465 InodeKind::File => libc::DT_REG.into(),
466 },
467 name,
468 })
469 }
470}
471
472#[cfg(test)]
473mod tests {
474 use anyhow::{bail, Result};
475 use nix::sys::statfs::{statfs, FsType};
Jiyong Park63a95cf2021-05-13 19:20:30 +0900476 use std::collections::BTreeSet;
Jiyong Park331d1ea2021-05-10 11:01:23 +0900477 use std::fs;
478 use std::fs::File;
479 use std::io::Write;
Jiyong Park0e74d9f2023-01-10 14:12:20 +0900480 use std::os::unix::fs::MetadataExt;
Jiyong Park331d1ea2021-05-10 11:01:23 +0900481 use std::path::{Path, PathBuf};
482 use std::time::{Duration, Instant};
483 use zip::write::FileOptions;
484
Jiyong Park0e74d9f2023-01-10 14:12:20 +0900485 #[derive(Default)]
486 struct Options {
487 noexec: bool,
488 uid: u32,
489 gid: u32,
490 }
491
Jiyong Park331d1ea2021-05-10 11:01:23 +0900492 #[cfg(not(target_os = "android"))]
Jiyong Park0e74d9f2023-01-10 14:12:20 +0900493 fn start_fuse(zip_path: &Path, mnt_path: &Path, opt: Options) {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900494 let zip_path = PathBuf::from(zip_path);
495 let mnt_path = PathBuf::from(mnt_path);
496 std::thread::spawn(move || {
Jiyong Park0e74d9f2023-01-10 14:12:20 +0900497 crate::run_fuse(&zip_path, &mnt_path, None, opt.noexec, opt.uid, opt.gid).unwrap();
Jiyong Park331d1ea2021-05-10 11:01:23 +0900498 });
499 }
500
501 #[cfg(target_os = "android")]
Jiyong Park0e74d9f2023-01-10 14:12:20 +0900502 fn start_fuse(zip_path: &Path, mnt_path: &Path, opt: Options) {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900503 // Note: for some unknown reason, running a thread to serve fuse doesn't work on Android.
504 // Explicitly spawn a zipfuse process instead.
505 // TODO(jiyong): fix this
Jiyong Park0e74d9f2023-01-10 14:12:20 +0900506 let noexec = if opt.noexec { "--noexec" } else { "" };
Jiyong Park331d1ea2021-05-10 11:01:23 +0900507 assert!(std::process::Command::new("sh")
508 .arg("-c")
Andrew Scull3854e402022-07-04 12:10:20 +0000509 .arg(format!(
Jiyong Park0e74d9f2023-01-10 14:12:20 +0900510 "/data/local/tmp/zipfuse {} -u {} -g {} {} {}",
Andrew Scull3854e402022-07-04 12:10:20 +0000511 noexec,
Jiyong Park0e74d9f2023-01-10 14:12:20 +0900512 opt.uid,
513 opt.gid,
Andrew Scull3854e402022-07-04 12:10:20 +0000514 zip_path.display(),
515 mnt_path.display()
516 ))
Jiyong Park331d1ea2021-05-10 11:01:23 +0900517 .spawn()
518 .is_ok());
519 }
520
521 fn wait_for_mount(mount_path: &Path) -> Result<()> {
522 let start_time = Instant::now();
523 const POLL_INTERVAL: Duration = Duration::from_millis(50);
524 const TIMEOUT: Duration = Duration::from_secs(10);
525 const FUSE_SUPER_MAGIC: FsType = FsType(0x65735546);
526 loop {
527 if statfs(mount_path)?.filesystem_type() == FUSE_SUPER_MAGIC {
528 break;
529 }
530
531 if start_time.elapsed() > TIMEOUT {
532 bail!("Time out mounting zipfuse");
533 }
534 std::thread::sleep(POLL_INTERVAL);
535 }
536 Ok(())
537 }
538
539 // Creates a zip file, adds some files to the zip file, mounts it using zipfuse, runs the check
540 // routine, and finally unmounts.
541 fn run_test(add: fn(&mut zip::ZipWriter<File>), check: fn(&std::path::Path)) {
Jiyong Park0e74d9f2023-01-10 14:12:20 +0900542 run_test_with_options(Default::default(), add, check);
Andrew Scull3854e402022-07-04 12:10:20 +0000543 }
544
Jiyong Park0e74d9f2023-01-10 14:12:20 +0900545 fn run_test_with_options(
546 opt: Options,
Andrew Scull3854e402022-07-04 12:10:20 +0000547 add: fn(&mut zip::ZipWriter<File>),
548 check: fn(&std::path::Path),
549 ) {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900550 // Create an empty zip file
551 let test_dir = tempfile::TempDir::new().unwrap();
552 let zip_path = test_dir.path().join("test.zip");
553 let zip = File::create(&zip_path);
554 assert!(zip.is_ok());
555 let mut zip = zip::ZipWriter::new(zip.unwrap());
556
557 // Let test users add files/dirs to the zip file
558 add(&mut zip);
559 assert!(zip.finish().is_ok());
560 drop(zip);
561
562 // Mount the zip file on the "mnt" dir using zipfuse.
563 let mnt_path = test_dir.path().join("mnt");
564 assert!(fs::create_dir(&mnt_path).is_ok());
565
Jiyong Park0e74d9f2023-01-10 14:12:20 +0900566 start_fuse(&zip_path, &mnt_path, opt);
Jiyong Park331d1ea2021-05-10 11:01:23 +0900567
568 let mnt_path = test_dir.path().join("mnt");
569 // Give some time for the fuse to boot up
570 assert!(wait_for_mount(&mnt_path).is_ok());
571 // Run the check routine, and do the clean up.
572 check(&mnt_path);
573 assert!(nix::mount::umount2(&mnt_path, nix::mount::MntFlags::empty()).is_ok());
574 }
575
576 fn check_file(root: &Path, file: &str, content: &[u8]) {
577 let path = root.join(file);
578 assert!(path.exists());
579
580 let metadata = fs::metadata(&path);
581 assert!(metadata.is_ok());
582
583 let metadata = metadata.unwrap();
584 assert!(metadata.is_file());
585 assert_eq!(content.len(), metadata.len() as usize);
586
587 let read_data = fs::read(&path);
588 assert!(read_data.is_ok());
589 assert_eq!(content, read_data.unwrap().as_slice());
590 }
591
Jiyong Park63a95cf2021-05-13 19:20:30 +0900592 fn check_dir<S: AsRef<str>>(root: &Path, dir: &str, files: &[S], dirs: &[S]) {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900593 let dir_path = root.join(dir);
594 assert!(dir_path.exists());
595
596 let metadata = fs::metadata(&dir_path);
597 assert!(metadata.is_ok());
598
599 let metadata = metadata.unwrap();
600 assert!(metadata.is_dir());
601
602 let iter = fs::read_dir(&dir_path);
603 assert!(iter.is_ok());
604
605 let iter = iter.unwrap();
Jiyong Park63a95cf2021-05-13 19:20:30 +0900606 let mut actual_files = BTreeSet::new();
607 let mut actual_dirs = BTreeSet::new();
Jiyong Park331d1ea2021-05-10 11:01:23 +0900608 for de in iter {
609 let entry = de.unwrap();
610 let path = entry.path();
611 if path.is_dir() {
612 actual_dirs.insert(path.strip_prefix(&dir_path).unwrap().to_path_buf());
613 } else {
614 actual_files.insert(path.strip_prefix(&dir_path).unwrap().to_path_buf());
615 }
616 }
Jiyong Park63a95cf2021-05-13 19:20:30 +0900617 let expected_files: BTreeSet<PathBuf> =
618 files.iter().map(|s| PathBuf::from(s.as_ref())).collect();
619 let expected_dirs: BTreeSet<PathBuf> =
620 dirs.iter().map(|s| PathBuf::from(s.as_ref())).collect();
Jiyong Park331d1ea2021-05-10 11:01:23 +0900621
622 assert_eq!(expected_files, actual_files);
623 assert_eq!(expected_dirs, actual_dirs);
624 }
625
626 #[test]
627 fn empty() {
628 run_test(
629 |_| {},
630 |root| {
Jiyong Park63a95cf2021-05-13 19:20:30 +0900631 check_dir::<String>(root, "", &[], &[]);
Jiyong Park331d1ea2021-05-10 11:01:23 +0900632 },
633 );
634 }
635
636 #[test]
637 fn single_file() {
638 run_test(
639 |zip| {
640 zip.start_file("foo", FileOptions::default()).unwrap();
641 zip.write_all(b"0123456789").unwrap();
642 },
643 |root| {
644 check_dir(root, "", &["foo"], &[]);
645 check_file(root, "foo", b"0123456789");
646 },
647 );
648 }
649
650 #[test]
Andrew Scull3854e402022-07-04 12:10:20 +0000651 fn noexec() {
652 fn add_executable(zip: &mut zip::ZipWriter<File>) {
653 zip.start_file("executable", FileOptions::default().unix_permissions(0o755)).unwrap();
654 }
655
656 // Executables can be run when not mounting with noexec.
657 run_test(add_executable, |root| {
658 let res = std::process::Command::new(root.join("executable")).status();
659 res.unwrap();
660 });
661
662 // Mounting with noexec results in permissions denial when running an executable.
Jiyong Park0e74d9f2023-01-10 14:12:20 +0900663 let opt = Options { noexec: true, ..Default::default() };
664 run_test_with_options(opt, add_executable, |root| {
Andrew Scull3854e402022-07-04 12:10:20 +0000665 let res = std::process::Command::new(root.join("executable")).status();
666 assert!(matches!(res.unwrap_err().kind(), std::io::ErrorKind::PermissionDenied));
667 });
668 }
669
670 #[test]
Jiyong Park0e74d9f2023-01-10 14:12:20 +0900671 fn uid_gid() {
672 const UID: u32 = 100;
673 const GID: u32 = 200;
674 run_test_with_options(
675 Options { noexec: true, uid: UID, gid: GID },
676 |zip| {
677 zip.start_file("foo", FileOptions::default()).unwrap();
678 zip.write_all(b"0123456789").unwrap();
679 },
680 |root| {
681 let path = root.join("foo");
682
683 let metadata = fs::metadata(&path);
684 assert!(metadata.is_ok());
685 let metadata = metadata.unwrap();
686
687 assert_eq!(UID, metadata.uid());
688 assert_eq!(GID, metadata.gid());
689 },
690 );
691 }
692
693 #[test]
Jiyong Park331d1ea2021-05-10 11:01:23 +0900694 fn single_dir() {
695 run_test(
696 |zip| {
697 zip.add_directory("dir", FileOptions::default()).unwrap();
698 },
699 |root| {
700 check_dir(root, "", &[], &["dir"]);
Jiyong Park63a95cf2021-05-13 19:20:30 +0900701 check_dir::<String>(root, "dir", &[], &[]);
Jiyong Park331d1ea2021-05-10 11:01:23 +0900702 },
703 );
704 }
705
706 #[test]
707 fn complex_hierarchy() {
708 // root/
709 // a/
710 // b1/
711 // b2/
712 // c1 (file)
713 // c2/
714 // d1 (file)
715 // d2 (file)
716 // d3 (file)
717 // x/
718 // y1 (file)
719 // y2 (file)
720 // y3/
721 //
722 // foo (file)
723 // bar (file)
724 run_test(
725 |zip| {
726 let opt = FileOptions::default();
727 zip.add_directory("a/b1", opt).unwrap();
728
729 zip.start_file("a/b2/c1", opt).unwrap();
730
731 zip.start_file("a/b2/c2/d1", opt).unwrap();
732 zip.start_file("a/b2/c2/d2", opt).unwrap();
733 zip.start_file("a/b2/c2/d3", opt).unwrap();
734
735 zip.start_file("x/y1", opt).unwrap();
736 zip.start_file("x/y2", opt).unwrap();
737 zip.add_directory("x/y3", opt).unwrap();
738
739 zip.start_file("foo", opt).unwrap();
740 zip.start_file("bar", opt).unwrap();
741 },
742 |root| {
743 check_dir(root, "", &["foo", "bar"], &["a", "x"]);
744 check_dir(root, "a", &[], &["b1", "b2"]);
Jiyong Park63a95cf2021-05-13 19:20:30 +0900745 check_dir::<String>(root, "a/b1", &[], &[]);
Jiyong Park331d1ea2021-05-10 11:01:23 +0900746 check_dir(root, "a/b2", &["c1"], &["c2"]);
747 check_dir(root, "a/b2/c2", &["d1", "d2", "d3"], &[]);
748 check_dir(root, "x", &["y1", "y2"], &["y3"]);
Jiyong Park63a95cf2021-05-13 19:20:30 +0900749 check_dir::<String>(root, "x/y3", &[], &[]);
Jiyong Park331d1ea2021-05-10 11:01:23 +0900750 check_file(root, "a/b2/c1", &[]);
751 check_file(root, "a/b2/c2/d1", &[]);
752 check_file(root, "a/b2/c2/d2", &[]);
753 check_file(root, "a/b2/c2/d3", &[]);
754 check_file(root, "x/y1", &[]);
755 check_file(root, "x/y2", &[]);
756 check_file(root, "foo", &[]);
757 check_file(root, "bar", &[]);
758 },
759 );
760 }
761
762 #[test]
763 fn large_file() {
764 run_test(
765 |zip| {
766 let data = vec![10; 2 << 20];
767 zip.start_file("foo", FileOptions::default()).unwrap();
768 zip.write_all(&data).unwrap();
769 },
770 |root| {
771 let data = vec![10; 2 << 20];
772 check_file(root, "foo", &data);
773 },
774 );
775 }
Jiyong Park63a95cf2021-05-13 19:20:30 +0900776
777 #[test]
778 fn large_dir() {
779 const NUM_FILES: usize = 1 << 10;
780 run_test(
781 |zip| {
782 let opt = FileOptions::default();
783 // create 1K files. Each file has a name of length 100. So total size is at least
784 // 100KB, which is bigger than the readdir buffer size of 4K.
785 for i in 0..NUM_FILES {
786 zip.start_file(format!("dir/{:0100}", i), opt).unwrap();
787 }
788 },
789 |root| {
790 let dirs_expected: Vec<_> = (0..NUM_FILES).map(|i| format!("{:0100}", i)).collect();
791 check_dir(
792 root,
793 "dir",
794 dirs_expected.iter().map(|s| s.as_str()).collect::<Vec<&str>>().as_slice(),
795 &[],
796 );
797 },
798 );
799 }
Jiyong Parkd40f7bb2021-05-17 10:55:56 +0900800
Jiyong Parke6587ca2021-05-17 14:42:23 +0900801 fn run_fuse_and_check_test_zip(test_dir: &Path, zip_path: &Path) {
802 let mnt_path = test_dir.join("mnt");
Jiyong Parkd40f7bb2021-05-17 10:55:56 +0900803 assert!(fs::create_dir(&mnt_path).is_ok());
804
Jiyong Park0e74d9f2023-01-10 14:12:20 +0900805 let opt = Options { noexec: false, ..Default::default() };
806 start_fuse(zip_path, &mnt_path, opt);
Jiyong Parkd40f7bb2021-05-17 10:55:56 +0900807
808 // Give some time for the fuse to boot up
809 assert!(wait_for_mount(&mnt_path).is_ok());
810
811 check_dir(&mnt_path, "", &[], &["dir"]);
812 check_dir(&mnt_path, "dir", &["file1", "file2"], &[]);
813 check_file(&mnt_path, "dir/file1", include_bytes!("../testdata/dir/file1"));
814 check_file(&mnt_path, "dir/file2", include_bytes!("../testdata/dir/file2"));
815 assert!(nix::mount::umount2(&mnt_path, nix::mount::MntFlags::empty()).is_ok());
816 }
Jiyong Parke6587ca2021-05-17 14:42:23 +0900817
818 #[test]
819 fn supports_deflate() {
820 let test_dir = tempfile::TempDir::new().unwrap();
821 let zip_path = test_dir.path().join("test.zip");
822 let mut zip_file = File::create(&zip_path).unwrap();
823 zip_file.write_all(include_bytes!("../testdata/test.zip")).unwrap();
824
Chris Wailes68c39f82021-07-27 16:03:44 -0700825 run_fuse_and_check_test_zip(test_dir.path(), &zip_path);
Jiyong Parke6587ca2021-05-17 14:42:23 +0900826 }
827
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900828 #[test]
829 fn supports_store() {
830 run_test(
831 |zip| {
832 let data = vec![10; 2 << 20];
833 zip.start_file(
834 "foo",
835 FileOptions::default().compression_method(zip::CompressionMethod::Stored),
836 )
837 .unwrap();
838 zip.write_all(&data).unwrap();
839 },
840 |root| {
841 let data = vec![10; 2 << 20];
842 check_file(root, "foo", &data);
843 },
844 );
845 }
846
Jiyong Parke6587ca2021-05-17 14:42:23 +0900847 #[cfg(not(target_os = "android"))] // Android doesn't have the loopdev crate
848 #[test]
849 fn supports_zip_on_block_device() {
850 // Write test.zip to the test directory
851 let test_dir = tempfile::TempDir::new().unwrap();
852 let zip_path = test_dir.path().join("test.zip");
853 let mut zip_file = File::create(&zip_path).unwrap();
854 let data = include_bytes!("../testdata/test.zip");
855 zip_file.write_all(data).unwrap();
856
857 // Pad 0 to test.zip so that its size is multiple of 4096.
858 const BLOCK_SIZE: usize = 4096;
859 let size = (data.len() + BLOCK_SIZE) & !BLOCK_SIZE;
860 let pad_size = size - data.len();
861 assert!(pad_size != 0);
862 let pad = vec![0; pad_size];
863 zip_file.write_all(pad.as_slice()).unwrap();
864 drop(zip_file);
865
866 // Attach test.zip to a loop device
867 let lc = loopdev::LoopControl::open().unwrap();
868 let ld = scopeguard::guard(lc.next_free().unwrap(), |ld| {
869 ld.detach().unwrap();
870 });
871 ld.attach_file(&zip_path).unwrap();
872
873 // Start zipfuse over to the loop device (not the zip file)
874 run_fuse_and_check_test_zip(&test_dir.path(), &ld.path().unwrap());
875 }
Jiyong Park331d1ea2021-05-10 11:01:23 +0900876}