blob: ef31a0c844804ddf9cf05b4cd99940f478477f5e [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};
Andrew Walbranaa1efc42022-08-10 13:33:57 +000024use clap::{builder::ValueParser, Arg, ArgAction, Command};
Jiyong Park331d1ea2021-05-10 11:01:23 +090025use 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;
Andrew Walbranae3350d2023-07-21 19:01:18 +010034use std::mem::{size_of, MaybeUninit};
Jiyong Park331d1ea2021-05-10 11:01:23 +090035use std::os::unix::io::AsRawFd;
36use std::path::Path;
Andrew Walbranaa1efc42022-08-10 13:33:57 +000037use std::path::PathBuf;
Jiyong Park331d1ea2021-05-10 11:01:23 +090038use std::sync::Mutex;
39
40use crate::inode::{DirectoryEntry, Inode, InodeData, InodeKind, InodeTable};
41
42fn main() -> Result<()> {
Andrew Walbranaa1efc42022-08-10 13:33:57 +000043 let matches = clap_command().get_matches();
44
45 let zip_file = matches.get_one::<PathBuf>("ZIPFILE").unwrap();
46 let mount_point = matches.get_one::<PathBuf>("MOUNTPOINT").unwrap();
47 let options = matches.get_one::<String>("options");
48 let noexec = matches.get_flag("noexec");
49 let ready_prop = matches.get_one::<String>("readyprop");
50 let uid: u32 = matches.get_one::<String>("uid").map_or(0, |s| s.parse().unwrap());
51 let gid: u32 = matches.get_one::<String>("gid").map_or(0, |s| s.parse().unwrap());
52 run_fuse(zip_file, mount_point, options, noexec, ready_prop, uid, gid)?;
53
54 Ok(())
55}
56
57fn clap_command() -> Command {
58 Command::new("zipfuse")
Jiyong Park6a762db2021-05-31 14:00:52 +090059 .arg(
Andrew Walbranaa1efc42022-08-10 13:33:57 +000060 Arg::new("options")
Jeff Vander Stoepa8dc2712022-07-29 02:33:45 +020061 .short('o')
Jiyong Park6a762db2021-05-31 14:00:52 +090062 .required(false)
Chris Wailes68c39f82021-07-27 16:03:44 -070063 .help("Comma separated list of mount options"),
Jiyong Park6a762db2021-05-31 14:00:52 +090064 )
Andrew Scull3854e402022-07-04 12:10:20 +000065 .arg(
Andrew Walbranaa1efc42022-08-10 13:33:57 +000066 Arg::new("noexec")
Andrew Scull3854e402022-07-04 12:10:20 +000067 .long("noexec")
Andrew Walbranaa1efc42022-08-10 13:33:57 +000068 .action(ArgAction::SetTrue)
Andrew Scull3854e402022-07-04 12:10:20 +000069 .help("Disallow the execution of binary files"),
70 )
Alan Stokes60f82202022-10-07 16:40:07 +010071 .arg(
Andrew Walbranaa1efc42022-08-10 13:33:57 +000072 Arg::new("readyprop")
Alan Stokes60f82202022-10-07 16:40:07 +010073 .short('p')
Alan Stokes60f82202022-10-07 16:40:07 +010074 .help("Specify a property to be set when mount is ready"),
75 )
Andrew Walbranaa1efc42022-08-10 13:33:57 +000076 .arg(Arg::new("uid").short('u').help("numeric UID who's the owner of the files"))
77 .arg(Arg::new("gid").short('g').help("numeric GID who's the group of the files"))
78 .arg(Arg::new("ZIPFILE").value_parser(ValueParser::path_buf()).required(true))
79 .arg(Arg::new("MOUNTPOINT").value_parser(ValueParser::path_buf()).required(true))
Jiyong Park331d1ea2021-05-10 11:01:23 +090080}
81
82/// Runs a fuse filesystem by mounting `zip_file` on `mount_point`.
Andrew Scull3854e402022-07-04 12:10:20 +000083pub fn run_fuse(
84 zip_file: &Path,
85 mount_point: &Path,
Andrew Walbranaa1efc42022-08-10 13:33:57 +000086 extra_options: Option<&String>,
Andrew Scull3854e402022-07-04 12:10:20 +000087 noexec: bool,
Andrew Walbranaa1efc42022-08-10 13:33:57 +000088 ready_prop: Option<&String>,
Jiyong Park231f9692023-01-09 19:36:21 +090089 uid: u32,
90 gid: u32,
Andrew Scull3854e402022-07-04 12:10:20 +000091) -> Result<()> {
Jiyong Park331d1ea2021-05-10 11:01:23 +090092 const MAX_READ: u32 = 1 << 20; // TODO(jiyong): tune this
93 const MAX_WRITE: u32 = 1 << 13; // This is a read-only filesystem
94
95 let dev_fuse = OpenOptions::new().read(true).write(true).open("/dev/fuse")?;
96
Jiyong Park6a762db2021-05-31 14:00:52 +090097 let mut mount_options = vec![
98 MountOption::FD(dev_fuse.as_raw_fd()),
Jiyong Park98ce68d2023-01-09 17:46:19 +090099 MountOption::DefaultPermissions,
Jiyong Park6a762db2021-05-31 14:00:52 +0900100 MountOption::RootMode(libc::S_IFDIR | libc::S_IXUSR | libc::S_IXGRP | libc::S_IXOTH),
101 MountOption::AllowOther,
102 MountOption::UserId(0),
103 MountOption::GroupId(0),
104 MountOption::MaxRead(MAX_READ),
105 ];
106 if let Some(value) = extra_options {
107 mount_options.push(MountOption::Extra(value));
108 }
109
Andrew Scull3854e402022-07-04 12:10:20 +0000110 let mut mount_flags = libc::MS_NOSUID | libc::MS_NODEV | libc::MS_RDONLY;
111 if noexec {
112 mount_flags |= libc::MS_NOEXEC;
113 }
114
115 fuse::mount(mount_point, "zipfuse", mount_flags, &mount_options)?;
Alan Stokes60f82202022-10-07 16:40:07 +0100116
117 if let Some(property_name) = ready_prop {
118 system_properties::write(property_name, "1").context("Failed to set readyprop")?;
119 }
120
Victor Hsieh58a5e9b2022-03-09 21:57:26 +0000121 let mut config = fuse::FuseConfig::new();
122 config.dev_fuse(dev_fuse).max_write(MAX_WRITE).max_read(MAX_READ);
Jiyong Park231f9692023-01-09 19:36:21 +0900123 Ok(config.enter_message_loop(ZipFuse::new(zip_file, uid, gid)?)?)
Jiyong Park331d1ea2021-05-10 11:01:23 +0900124}
125
126struct ZipFuse {
127 zip_archive: Mutex<zip::ZipArchive<File>>,
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900128 raw_file: Mutex<File>,
Jiyong Park331d1ea2021-05-10 11:01:23 +0900129 inode_table: InodeTable,
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900130 open_files: Mutex<HashMap<Handle, OpenFile>>,
Jiyong Park331d1ea2021-05-10 11:01:23 +0900131 open_dirs: Mutex<HashMap<Handle, OpenDirBuf>>,
Jiyong Park231f9692023-01-09 19:36:21 +0900132 uid: u32,
133 gid: u32,
Jiyong Park331d1ea2021-05-10 11:01:23 +0900134}
135
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900136/// Represents a [`ZipFile`] that is opened.
137struct OpenFile {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900138 open_count: u32, // multiple opens share the buf because this is a read-only filesystem
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900139 content: OpenFileContent,
140}
141
142/// Holds the content of a [`ZipFile`]. Depending on whether it is compressed or not, the
143/// entire content is stored, or only the zip index is stored.
144enum OpenFileContent {
145 Compressed(Box<[u8]>),
146 Uncompressed(usize), // zip index
Jiyong Park331d1ea2021-05-10 11:01:23 +0900147}
148
149/// Holds the directory entries in a directory opened by [`opendir`].
150struct OpenDirBuf {
151 open_count: u32,
152 buf: Box<[(CString, DirectoryEntry)]>,
153}
154
155type Handle = u64;
156
157fn ebadf() -> io::Error {
158 io::Error::from_raw_os_error(libc::EBADF)
159}
160
161fn timeout_max() -> std::time::Duration {
162 std::time::Duration::new(u64::MAX, 1_000_000_000 - 1)
163}
164
165impl ZipFuse {
Jiyong Park231f9692023-01-09 19:36:21 +0900166 fn new(zip_file: &Path, uid: u32, gid: u32) -> Result<ZipFuse> {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900167 // TODO(jiyong): Use O_DIRECT to avoid double caching.
168 // `.custom_flags(nix::fcntl::OFlag::O_DIRECT.bits())` currently doesn't work.
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900169 let f = File::open(zip_file)?;
Jiyong Park331d1ea2021-05-10 11:01:23 +0900170 let mut z = zip::ZipArchive::new(f)?;
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900171 // Open the same file again so that we can directly access it when accessing
172 // uncompressed zip_file entries in it. `ZipFile` doesn't implement `Seek`.
173 let raw_file = File::open(zip_file)?;
Jiyong Park331d1ea2021-05-10 11:01:23 +0900174 let it = InodeTable::from_zip(&mut z)?;
175 Ok(ZipFuse {
176 zip_archive: Mutex::new(z),
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900177 raw_file: Mutex::new(raw_file),
Jiyong Park331d1ea2021-05-10 11:01:23 +0900178 inode_table: it,
179 open_files: Mutex::new(HashMap::new()),
180 open_dirs: Mutex::new(HashMap::new()),
Jiyong Park231f9692023-01-09 19:36:21 +0900181 uid,
182 gid,
Jiyong Park331d1ea2021-05-10 11:01:23 +0900183 })
184 }
185
186 fn find_inode(&self, inode: Inode) -> io::Result<&InodeData> {
187 self.inode_table.get(inode).ok_or_else(ebadf)
188 }
189
Jiyong Parkd5df9562021-05-13 00:50:23 +0900190 // TODO(jiyong) remove this. Right now this is needed to do the nlink_t to u64 conversion below
191 // on aosp_x86_64 target. That however is a useless conversion on other targets.
192 #[allow(clippy::useless_conversion)]
Jiyong Park331d1ea2021-05-10 11:01:23 +0900193 fn stat_from(&self, inode: Inode) -> io::Result<libc::stat64> {
194 let inode_data = self.find_inode(inode)?;
Andrew Walbranae3350d2023-07-21 19:01:18 +0100195 // SAFETY: All fields of stat64 are valid for zero byte patterns.
196 let mut st = unsafe { MaybeUninit::<libc::stat64>::zeroed().assume_init() };
Jiyong Park331d1ea2021-05-10 11:01:23 +0900197 st.st_dev = 0;
Jiyong Parkd5df9562021-05-13 00:50:23 +0900198 st.st_nlink = if let Some(directory) = inode_data.get_directory() {
199 (2 + directory.len() as libc::nlink_t).into()
Jiyong Park331d1ea2021-05-10 11:01:23 +0900200 } else {
201 1
202 };
203 st.st_ino = inode;
204 st.st_mode = if inode_data.is_dir() { libc::S_IFDIR } else { libc::S_IFREG };
205 st.st_mode |= inode_data.mode;
Jiyong Park231f9692023-01-09 19:36:21 +0900206 st.st_uid = self.uid;
207 st.st_gid = self.gid;
Jiyong Park331d1ea2021-05-10 11:01:23 +0900208 st.st_size = i64::try_from(inode_data.size).unwrap_or(i64::MAX);
209 Ok(st)
210 }
211}
212
213impl fuse::filesystem::FileSystem for ZipFuse {
214 type Inode = Inode;
215 type Handle = Handle;
216 type DirIter = DirIter;
217
218 fn init(&self, _capable: FsOptions) -> std::io::Result<FsOptions> {
219 // The default options added by the fuse crate are fine. We don't have additional options.
220 Ok(FsOptions::empty())
221 }
222
223 fn lookup(&self, _ctx: Context, parent: Self::Inode, name: &CStr) -> io::Result<Entry> {
224 let inode = self.find_inode(parent)?;
225 let directory = inode.get_directory().ok_or_else(ebadf)?;
Jiyong Park331d1ea2021-05-10 11:01:23 +0900226 let entry = directory.get(name);
227 match entry {
228 Some(e) => Ok(Entry {
229 inode: e.inode,
230 generation: 0,
231 attr: self.stat_from(e.inode)?,
232 attr_timeout: timeout_max(), // this is a read-only fs
233 entry_timeout: timeout_max(),
234 }),
235 _ => Err(io::Error::from_raw_os_error(libc::ENOENT)),
236 }
237 }
238
239 fn getattr(
240 &self,
241 _ctx: Context,
242 inode: Self::Inode,
243 _handle: Option<Self::Handle>,
244 ) -> io::Result<(libc::stat64, std::time::Duration)> {
245 let st = self.stat_from(inode)?;
246 Ok((st, timeout_max()))
247 }
248
249 fn open(
250 &self,
251 _ctx: Context,
252 inode: Self::Inode,
253 _flags: u32,
254 ) -> io::Result<(Option<Self::Handle>, fuse::filesystem::OpenOptions)> {
255 let mut open_files = self.open_files.lock().unwrap();
256 let handle = inode as Handle;
257
258 // If the file is already opened, just increase the reference counter. If not, read the
259 // entire file content to the buffer. When `read` is called, a portion of the buffer is
260 // copied to the kernel.
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900261 if let Some(file) = open_files.get_mut(&handle) {
262 if file.open_count == 0 {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900263 return Err(ebadf());
264 }
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900265 file.open_count += 1;
Jiyong Park331d1ea2021-05-10 11:01:23 +0900266 } else {
267 let inode_data = self.find_inode(inode)?;
268 let zip_index = inode_data.get_zip_index().ok_or_else(ebadf)?;
269 let mut zip_archive = self.zip_archive.lock().unwrap();
270 let mut zip_file = zip_archive.by_index(zip_index)?;
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900271 let content = match zip_file.compression() {
272 zip::CompressionMethod::Stored => OpenFileContent::Uncompressed(zip_index),
273 _ => {
274 if let Some(mode) = zip_file.unix_mode() {
275 let is_reg_file = zip_file.is_file();
276 let is_executable =
277 mode & (libc::S_IXUSR | libc::S_IXGRP | libc::S_IXOTH) != 0;
278 if is_reg_file && is_executable {
279 log::warn!(
280 "Executable file {:?} is stored compressed. Consider \
281 storing it uncompressed to save memory",
282 zip_file.mangled_name()
283 );
284 }
285 }
286 let mut buf = Vec::with_capacity(inode_data.size as usize);
287 zip_file.read_to_end(&mut buf)?;
288 OpenFileContent::Compressed(buf.into_boxed_slice())
289 }
290 };
291 open_files.insert(handle, OpenFile { open_count: 1, content });
Jiyong Park331d1ea2021-05-10 11:01:23 +0900292 }
293 // Note: we don't return `DIRECT_IO` here, because then applications wouldn't be able to
294 // mmap the files.
295 Ok((Some(handle), fuse::filesystem::OpenOptions::empty()))
296 }
297
298 fn release(
299 &self,
300 _ctx: Context,
301 inode: Self::Inode,
302 _flags: u32,
303 _handle: Self::Handle,
304 _flush: bool,
305 _flock_release: bool,
306 _lock_owner: Option<u64>,
307 ) -> io::Result<()> {
308 // Releases the buffer for the `handle` when it is opened for nobody. While this is good
309 // for saving memory, this has a performance implication because we need to decompress
310 // again when the same file is opened in the future.
311 let mut open_files = self.open_files.lock().unwrap();
312 let handle = inode as Handle;
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900313 if let Some(file) = open_files.get_mut(&handle) {
314 if file.open_count.checked_sub(1).ok_or_else(ebadf)? == 0 {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900315 open_files.remove(&handle);
316 }
317 Ok(())
318 } else {
319 Err(ebadf())
320 }
321 }
322
Charisee13fc9ee2024-03-20 19:05:10 +0000323 #[allow(clippy::unused_io_amount)]
Jiyong Park331d1ea2021-05-10 11:01:23 +0900324 fn read<W: io::Write + ZeroCopyWriter>(
325 &self,
326 _ctx: Context,
327 _inode: Self::Inode,
328 handle: Self::Handle,
329 mut w: W,
330 size: u32,
331 offset: u64,
332 _lock_owner: Option<u64>,
333 _flags: u32,
334 ) -> io::Result<usize> {
335 let open_files = self.open_files.lock().unwrap();
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900336 let file = open_files.get(&handle).ok_or_else(ebadf)?;
337 if file.open_count == 0 {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900338 return Err(ebadf());
339 }
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900340 Ok(match &file.content {
341 OpenFileContent::Uncompressed(zip_index) => {
342 let mut zip_archive = self.zip_archive.lock().unwrap();
343 let zip_file = zip_archive.by_index(*zip_index)?;
344 let start = zip_file.data_start() + offset;
345 let remaining_size = zip_file.size() - offset;
346 let size = std::cmp::min(remaining_size, size.into());
347
348 let mut raw_file = self.raw_file.lock().unwrap();
349 w.write_from(&mut raw_file, size as usize, start)?
350 }
351 OpenFileContent::Compressed(buf) => {
352 let start = offset as usize;
353 let end = start + size as usize;
354 let end = std::cmp::min(end, buf.len());
355 w.write(&buf[start..end])?
356 }
357 })
Jiyong Park331d1ea2021-05-10 11:01:23 +0900358 }
359
360 fn opendir(
361 &self,
362 _ctx: Context,
363 inode: Self::Inode,
364 _flags: u32,
365 ) -> io::Result<(Option<Self::Handle>, fuse::filesystem::OpenOptions)> {
366 let mut open_dirs = self.open_dirs.lock().unwrap();
367 let handle = inode as Handle;
368 if let Some(odb) = open_dirs.get_mut(&handle) {
369 if odb.open_count == 0 {
370 return Err(ebadf());
371 }
372 odb.open_count += 1;
373 } else {
374 let inode_data = self.find_inode(inode)?;
375 let directory = inode_data.get_directory().ok_or_else(ebadf)?;
376 let mut buf: Vec<(CString, DirectoryEntry)> = Vec::with_capacity(directory.len());
377 for (name, dir_entry) in directory.iter() {
378 let name = CString::new(name.as_bytes()).unwrap();
379 buf.push((name, dir_entry.clone()));
380 }
381 open_dirs.insert(handle, OpenDirBuf { open_count: 1, buf: buf.into_boxed_slice() });
382 }
Jiyong Park63a95cf2021-05-13 19:20:30 +0900383 Ok((Some(handle), fuse::filesystem::OpenOptions::CACHE_DIR))
Jiyong Park331d1ea2021-05-10 11:01:23 +0900384 }
385
386 fn releasedir(
387 &self,
388 _ctx: Context,
389 inode: Self::Inode,
390 _flags: u32,
391 _handle: Self::Handle,
392 ) -> io::Result<()> {
393 let mut open_dirs = self.open_dirs.lock().unwrap();
394 let handle = inode as Handle;
395 if let Some(odb) = open_dirs.get_mut(&handle) {
396 if odb.open_count.checked_sub(1).ok_or_else(ebadf)? == 0 {
397 open_dirs.remove(&handle);
398 }
399 Ok(())
400 } else {
401 Err(ebadf())
402 }
403 }
404
405 fn readdir(
406 &self,
407 _ctx: Context,
408 inode: Self::Inode,
409 _handle: Self::Handle,
410 size: u32,
411 offset: u64,
412 ) -> io::Result<Self::DirIter> {
413 let open_dirs = self.open_dirs.lock().unwrap();
414 let handle = inode as Handle;
415 let odb = open_dirs.get(&handle).ok_or_else(ebadf)?;
416 if odb.open_count == 0 {
417 return Err(ebadf());
418 }
419 let buf = &odb.buf;
420 let start = offset as usize;
Jiyong Park63a95cf2021-05-13 19:20:30 +0900421
422 // Estimate the size of each entry will take space in the buffer. See
423 // external/crosvm/fuse/src/server.rs#add_dirent
424 let mut estimate: usize = 0; // estimated number of bytes we will be writing
425 let mut end = start; // index in `buf`
426 while estimate < size as usize && end < buf.len() {
427 let dirent_size = size_of::<fuse::sys::Dirent>();
428 let name_size = buf[end].0.to_bytes().len();
429 estimate += (dirent_size + name_size + 7) & !7; // round to 8 byte boundary
430 end += 1;
431 }
432
Jiyong Park331d1ea2021-05-10 11:01:23 +0900433 let mut new_buf = Vec::with_capacity(end - start);
434 // The portion of `buf` is *copied* to the iterator. This is not ideal, but inevitable
435 // because the `name` field in `fuse::filesystem::DirEntry` is `&CStr` not `CString`.
436 new_buf.extend_from_slice(&buf[start..end]);
437 Ok(DirIter { inner: new_buf, offset, cur: 0 })
438 }
439}
440
441struct DirIter {
442 inner: Vec<(CString, DirectoryEntry)>,
443 offset: u64, // the offset where this iterator begins. `next` doesn't change this.
444 cur: usize, // the current index in `inner`. `next` advances this.
445}
446
447impl fuse::filesystem::DirectoryIterator for DirIter {
448 fn next(&mut self) -> Option<fuse::filesystem::DirEntry> {
449 if self.cur >= self.inner.len() {
450 return None;
451 }
452
453 let (name, entry) = &self.inner[self.cur];
454 self.cur += 1;
455 Some(fuse::filesystem::DirEntry {
456 ino: entry.inode as libc::ino64_t,
457 offset: self.offset + self.cur as u64,
458 type_: match entry.kind {
459 InodeKind::Directory => libc::DT_DIR.into(),
460 InodeKind::File => libc::DT_REG.into(),
461 },
462 name,
463 })
464 }
465}
466
467#[cfg(test)]
468mod tests {
Andrew Walbranaa1efc42022-08-10 13:33:57 +0000469 use super::*;
470 use anyhow::bail;
Jiyong Park331d1ea2021-05-10 11:01:23 +0900471 use nix::sys::statfs::{statfs, FsType};
Jiyong Park63a95cf2021-05-13 19:20:30 +0900472 use std::collections::BTreeSet;
Jiyong Park331d1ea2021-05-10 11:01:23 +0900473 use std::fs;
Jiyong Park331d1ea2021-05-10 11:01:23 +0900474 use std::io::Write;
Jiyong Park0e74d9f2023-01-10 14:12:20 +0900475 use std::os::unix::fs::MetadataExt;
Jiyong Park331d1ea2021-05-10 11:01:23 +0900476 use std::path::{Path, PathBuf};
477 use std::time::{Duration, Instant};
478 use zip::write::FileOptions;
479
Jiyong Park0e74d9f2023-01-10 14:12:20 +0900480 #[derive(Default)]
481 struct Options {
482 noexec: bool,
483 uid: u32,
484 gid: u32,
485 }
486
Jiyong Park331d1ea2021-05-10 11:01:23 +0900487 #[cfg(not(target_os = "android"))]
Jiyong Park0e74d9f2023-01-10 14:12:20 +0900488 fn start_fuse(zip_path: &Path, mnt_path: &Path, opt: Options) {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900489 let zip_path = PathBuf::from(zip_path);
490 let mnt_path = PathBuf::from(mnt_path);
491 std::thread::spawn(move || {
Jiyong Park0e74d9f2023-01-10 14:12:20 +0900492 crate::run_fuse(&zip_path, &mnt_path, None, opt.noexec, opt.uid, opt.gid).unwrap();
Jiyong Park331d1ea2021-05-10 11:01:23 +0900493 });
494 }
495
496 #[cfg(target_os = "android")]
Jiyong Park0e74d9f2023-01-10 14:12:20 +0900497 fn start_fuse(zip_path: &Path, mnt_path: &Path, opt: Options) {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900498 // Note: for some unknown reason, running a thread to serve fuse doesn't work on Android.
499 // Explicitly spawn a zipfuse process instead.
500 // TODO(jiyong): fix this
Jiyong Park0e74d9f2023-01-10 14:12:20 +0900501 let noexec = if opt.noexec { "--noexec" } else { "" };
Jiyong Park331d1ea2021-05-10 11:01:23 +0900502 assert!(std::process::Command::new("sh")
503 .arg("-c")
Andrew Scull3854e402022-07-04 12:10:20 +0000504 .arg(format!(
Jiyong Park0e74d9f2023-01-10 14:12:20 +0900505 "/data/local/tmp/zipfuse {} -u {} -g {} {} {}",
Andrew Scull3854e402022-07-04 12:10:20 +0000506 noexec,
Jiyong Park0e74d9f2023-01-10 14:12:20 +0900507 opt.uid,
508 opt.gid,
Andrew Scull3854e402022-07-04 12:10:20 +0000509 zip_path.display(),
510 mnt_path.display()
511 ))
Jiyong Park331d1ea2021-05-10 11:01:23 +0900512 .spawn()
513 .is_ok());
514 }
515
516 fn wait_for_mount(mount_path: &Path) -> Result<()> {
517 let start_time = Instant::now();
518 const POLL_INTERVAL: Duration = Duration::from_millis(50);
519 const TIMEOUT: Duration = Duration::from_secs(10);
520 const FUSE_SUPER_MAGIC: FsType = FsType(0x65735546);
521 loop {
522 if statfs(mount_path)?.filesystem_type() == FUSE_SUPER_MAGIC {
523 break;
524 }
525
526 if start_time.elapsed() > TIMEOUT {
527 bail!("Time out mounting zipfuse");
528 }
529 std::thread::sleep(POLL_INTERVAL);
530 }
531 Ok(())
532 }
533
534 // Creates a zip file, adds some files to the zip file, mounts it using zipfuse, runs the check
535 // routine, and finally unmounts.
536 fn run_test(add: fn(&mut zip::ZipWriter<File>), check: fn(&std::path::Path)) {
Jiyong Park0e74d9f2023-01-10 14:12:20 +0900537 run_test_with_options(Default::default(), add, check);
Andrew Scull3854e402022-07-04 12:10:20 +0000538 }
539
Jiyong Park0e74d9f2023-01-10 14:12:20 +0900540 fn run_test_with_options(
541 opt: Options,
Andrew Scull3854e402022-07-04 12:10:20 +0000542 add: fn(&mut zip::ZipWriter<File>),
543 check: fn(&std::path::Path),
544 ) {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900545 // Create an empty zip file
546 let test_dir = tempfile::TempDir::new().unwrap();
547 let zip_path = test_dir.path().join("test.zip");
548 let zip = File::create(&zip_path);
549 assert!(zip.is_ok());
550 let mut zip = zip::ZipWriter::new(zip.unwrap());
551
552 // Let test users add files/dirs to the zip file
553 add(&mut zip);
554 assert!(zip.finish().is_ok());
555 drop(zip);
556
557 // Mount the zip file on the "mnt" dir using zipfuse.
558 let mnt_path = test_dir.path().join("mnt");
559 assert!(fs::create_dir(&mnt_path).is_ok());
560
Jiyong Park0e74d9f2023-01-10 14:12:20 +0900561 start_fuse(&zip_path, &mnt_path, opt);
Jiyong Park331d1ea2021-05-10 11:01:23 +0900562
563 let mnt_path = test_dir.path().join("mnt");
564 // Give some time for the fuse to boot up
565 assert!(wait_for_mount(&mnt_path).is_ok());
566 // Run the check routine, and do the clean up.
567 check(&mnt_path);
568 assert!(nix::mount::umount2(&mnt_path, nix::mount::MntFlags::empty()).is_ok());
569 }
570
571 fn check_file(root: &Path, file: &str, content: &[u8]) {
572 let path = root.join(file);
573 assert!(path.exists());
574
575 let metadata = fs::metadata(&path);
576 assert!(metadata.is_ok());
577
578 let metadata = metadata.unwrap();
579 assert!(metadata.is_file());
580 assert_eq!(content.len(), metadata.len() as usize);
581
582 let read_data = fs::read(&path);
583 assert!(read_data.is_ok());
584 assert_eq!(content, read_data.unwrap().as_slice());
585 }
586
Jiyong Park63a95cf2021-05-13 19:20:30 +0900587 fn check_dir<S: AsRef<str>>(root: &Path, dir: &str, files: &[S], dirs: &[S]) {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900588 let dir_path = root.join(dir);
589 assert!(dir_path.exists());
590
591 let metadata = fs::metadata(&dir_path);
592 assert!(metadata.is_ok());
593
594 let metadata = metadata.unwrap();
595 assert!(metadata.is_dir());
596
597 let iter = fs::read_dir(&dir_path);
598 assert!(iter.is_ok());
599
600 let iter = iter.unwrap();
Jiyong Park63a95cf2021-05-13 19:20:30 +0900601 let mut actual_files = BTreeSet::new();
602 let mut actual_dirs = BTreeSet::new();
Jiyong Park331d1ea2021-05-10 11:01:23 +0900603 for de in iter {
604 let entry = de.unwrap();
605 let path = entry.path();
606 if path.is_dir() {
607 actual_dirs.insert(path.strip_prefix(&dir_path).unwrap().to_path_buf());
608 } else {
609 actual_files.insert(path.strip_prefix(&dir_path).unwrap().to_path_buf());
610 }
611 }
Jiyong Park63a95cf2021-05-13 19:20:30 +0900612 let expected_files: BTreeSet<PathBuf> =
613 files.iter().map(|s| PathBuf::from(s.as_ref())).collect();
614 let expected_dirs: BTreeSet<PathBuf> =
615 dirs.iter().map(|s| PathBuf::from(s.as_ref())).collect();
Jiyong Park331d1ea2021-05-10 11:01:23 +0900616
617 assert_eq!(expected_files, actual_files);
618 assert_eq!(expected_dirs, actual_dirs);
619 }
620
621 #[test]
622 fn empty() {
623 run_test(
624 |_| {},
625 |root| {
Jiyong Park63a95cf2021-05-13 19:20:30 +0900626 check_dir::<String>(root, "", &[], &[]);
Jiyong Park331d1ea2021-05-10 11:01:23 +0900627 },
628 );
629 }
630
631 #[test]
632 fn single_file() {
633 run_test(
634 |zip| {
635 zip.start_file("foo", FileOptions::default()).unwrap();
636 zip.write_all(b"0123456789").unwrap();
637 },
638 |root| {
639 check_dir(root, "", &["foo"], &[]);
640 check_file(root, "foo", b"0123456789");
641 },
642 );
643 }
644
645 #[test]
Andrew Scull3854e402022-07-04 12:10:20 +0000646 fn noexec() {
647 fn add_executable(zip: &mut zip::ZipWriter<File>) {
648 zip.start_file("executable", FileOptions::default().unix_permissions(0o755)).unwrap();
649 }
650
651 // Executables can be run when not mounting with noexec.
652 run_test(add_executable, |root| {
653 let res = std::process::Command::new(root.join("executable")).status();
654 res.unwrap();
655 });
656
657 // Mounting with noexec results in permissions denial when running an executable.
Jiyong Park0e74d9f2023-01-10 14:12:20 +0900658 let opt = Options { noexec: true, ..Default::default() };
659 run_test_with_options(opt, add_executable, |root| {
Andrew Scull3854e402022-07-04 12:10:20 +0000660 let res = std::process::Command::new(root.join("executable")).status();
661 assert!(matches!(res.unwrap_err().kind(), std::io::ErrorKind::PermissionDenied));
662 });
663 }
664
665 #[test]
Jiyong Park0e74d9f2023-01-10 14:12:20 +0900666 fn uid_gid() {
667 const UID: u32 = 100;
668 const GID: u32 = 200;
669 run_test_with_options(
670 Options { noexec: true, uid: UID, gid: GID },
671 |zip| {
672 zip.start_file("foo", FileOptions::default()).unwrap();
673 zip.write_all(b"0123456789").unwrap();
674 },
675 |root| {
676 let path = root.join("foo");
677
Charisee96113f32023-01-26 09:00:42 +0000678 let metadata = fs::metadata(path);
Jiyong Park0e74d9f2023-01-10 14:12:20 +0900679 assert!(metadata.is_ok());
680 let metadata = metadata.unwrap();
681
682 assert_eq!(UID, metadata.uid());
683 assert_eq!(GID, metadata.gid());
684 },
685 );
686 }
687
688 #[test]
Jiyong Park331d1ea2021-05-10 11:01:23 +0900689 fn single_dir() {
690 run_test(
691 |zip| {
692 zip.add_directory("dir", FileOptions::default()).unwrap();
693 },
694 |root| {
695 check_dir(root, "", &[], &["dir"]);
Jiyong Park63a95cf2021-05-13 19:20:30 +0900696 check_dir::<String>(root, "dir", &[], &[]);
Jiyong Park331d1ea2021-05-10 11:01:23 +0900697 },
698 );
699 }
700
701 #[test]
702 fn complex_hierarchy() {
703 // root/
704 // a/
705 // b1/
706 // b2/
707 // c1 (file)
708 // c2/
709 // d1 (file)
710 // d2 (file)
711 // d3 (file)
712 // x/
713 // y1 (file)
714 // y2 (file)
715 // y3/
716 //
717 // foo (file)
718 // bar (file)
719 run_test(
720 |zip| {
721 let opt = FileOptions::default();
722 zip.add_directory("a/b1", opt).unwrap();
723
724 zip.start_file("a/b2/c1", opt).unwrap();
725
726 zip.start_file("a/b2/c2/d1", opt).unwrap();
727 zip.start_file("a/b2/c2/d2", opt).unwrap();
728 zip.start_file("a/b2/c2/d3", opt).unwrap();
729
730 zip.start_file("x/y1", opt).unwrap();
731 zip.start_file("x/y2", opt).unwrap();
732 zip.add_directory("x/y3", opt).unwrap();
733
734 zip.start_file("foo", opt).unwrap();
735 zip.start_file("bar", opt).unwrap();
736 },
737 |root| {
738 check_dir(root, "", &["foo", "bar"], &["a", "x"]);
739 check_dir(root, "a", &[], &["b1", "b2"]);
Jiyong Park63a95cf2021-05-13 19:20:30 +0900740 check_dir::<String>(root, "a/b1", &[], &[]);
Jiyong Park331d1ea2021-05-10 11:01:23 +0900741 check_dir(root, "a/b2", &["c1"], &["c2"]);
742 check_dir(root, "a/b2/c2", &["d1", "d2", "d3"], &[]);
743 check_dir(root, "x", &["y1", "y2"], &["y3"]);
Jiyong Park63a95cf2021-05-13 19:20:30 +0900744 check_dir::<String>(root, "x/y3", &[], &[]);
Jiyong Park331d1ea2021-05-10 11:01:23 +0900745 check_file(root, "a/b2/c1", &[]);
746 check_file(root, "a/b2/c2/d1", &[]);
747 check_file(root, "a/b2/c2/d2", &[]);
748 check_file(root, "a/b2/c2/d3", &[]);
749 check_file(root, "x/y1", &[]);
750 check_file(root, "x/y2", &[]);
751 check_file(root, "foo", &[]);
752 check_file(root, "bar", &[]);
753 },
754 );
755 }
756
757 #[test]
758 fn large_file() {
759 run_test(
760 |zip| {
761 let data = vec![10; 2 << 20];
762 zip.start_file("foo", FileOptions::default()).unwrap();
763 zip.write_all(&data).unwrap();
764 },
765 |root| {
766 let data = vec![10; 2 << 20];
767 check_file(root, "foo", &data);
768 },
769 );
770 }
Jiyong Park63a95cf2021-05-13 19:20:30 +0900771
772 #[test]
773 fn large_dir() {
774 const NUM_FILES: usize = 1 << 10;
775 run_test(
776 |zip| {
777 let opt = FileOptions::default();
778 // create 1K files. Each file has a name of length 100. So total size is at least
779 // 100KB, which is bigger than the readdir buffer size of 4K.
780 for i in 0..NUM_FILES {
781 zip.start_file(format!("dir/{:0100}", i), opt).unwrap();
782 }
783 },
784 |root| {
785 let dirs_expected: Vec<_> = (0..NUM_FILES).map(|i| format!("{:0100}", i)).collect();
786 check_dir(
787 root,
788 "dir",
789 dirs_expected.iter().map(|s| s.as_str()).collect::<Vec<&str>>().as_slice(),
790 &[],
791 );
792 },
793 );
794 }
Jiyong Parkd40f7bb2021-05-17 10:55:56 +0900795
Jiyong Parke6587ca2021-05-17 14:42:23 +0900796 fn run_fuse_and_check_test_zip(test_dir: &Path, zip_path: &Path) {
797 let mnt_path = test_dir.join("mnt");
Jiyong Parkd40f7bb2021-05-17 10:55:56 +0900798 assert!(fs::create_dir(&mnt_path).is_ok());
799
Jiyong Park0e74d9f2023-01-10 14:12:20 +0900800 let opt = Options { noexec: false, ..Default::default() };
801 start_fuse(zip_path, &mnt_path, opt);
Jiyong Parkd40f7bb2021-05-17 10:55:56 +0900802
803 // Give some time for the fuse to boot up
804 assert!(wait_for_mount(&mnt_path).is_ok());
805
806 check_dir(&mnt_path, "", &[], &["dir"]);
807 check_dir(&mnt_path, "dir", &["file1", "file2"], &[]);
808 check_file(&mnt_path, "dir/file1", include_bytes!("../testdata/dir/file1"));
809 check_file(&mnt_path, "dir/file2", include_bytes!("../testdata/dir/file2"));
810 assert!(nix::mount::umount2(&mnt_path, nix::mount::MntFlags::empty()).is_ok());
811 }
Jiyong Parke6587ca2021-05-17 14:42:23 +0900812
813 #[test]
814 fn supports_deflate() {
815 let test_dir = tempfile::TempDir::new().unwrap();
816 let zip_path = test_dir.path().join("test.zip");
817 let mut zip_file = File::create(&zip_path).unwrap();
818 zip_file.write_all(include_bytes!("../testdata/test.zip")).unwrap();
819
Chris Wailes68c39f82021-07-27 16:03:44 -0700820 run_fuse_and_check_test_zip(test_dir.path(), &zip_path);
Jiyong Parke6587ca2021-05-17 14:42:23 +0900821 }
822
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900823 #[test]
824 fn supports_store() {
825 run_test(
826 |zip| {
827 let data = vec![10; 2 << 20];
828 zip.start_file(
829 "foo",
830 FileOptions::default().compression_method(zip::CompressionMethod::Stored),
831 )
832 .unwrap();
833 zip.write_all(&data).unwrap();
834 },
835 |root| {
836 let data = vec![10; 2 << 20];
837 check_file(root, "foo", &data);
838 },
839 );
840 }
841
Jiyong Parke6587ca2021-05-17 14:42:23 +0900842 #[cfg(not(target_os = "android"))] // Android doesn't have the loopdev crate
843 #[test]
844 fn supports_zip_on_block_device() {
845 // Write test.zip to the test directory
846 let test_dir = tempfile::TempDir::new().unwrap();
847 let zip_path = test_dir.path().join("test.zip");
848 let mut zip_file = File::create(&zip_path).unwrap();
849 let data = include_bytes!("../testdata/test.zip");
850 zip_file.write_all(data).unwrap();
851
852 // Pad 0 to test.zip so that its size is multiple of 4096.
853 const BLOCK_SIZE: usize = 4096;
854 let size = (data.len() + BLOCK_SIZE) & !BLOCK_SIZE;
855 let pad_size = size - data.len();
856 assert!(pad_size != 0);
857 let pad = vec![0; pad_size];
858 zip_file.write_all(pad.as_slice()).unwrap();
859 drop(zip_file);
860
861 // Attach test.zip to a loop device
862 let lc = loopdev::LoopControl::open().unwrap();
863 let ld = scopeguard::guard(lc.next_free().unwrap(), |ld| {
864 ld.detach().unwrap();
865 });
866 ld.attach_file(&zip_path).unwrap();
867
868 // Start zipfuse over to the loop device (not the zip file)
869 run_fuse_and_check_test_zip(&test_dir.path(), &ld.path().unwrap());
870 }
Andrew Walbranaa1efc42022-08-10 13:33:57 +0000871
872 #[test]
873 fn verify_command() {
874 // Check that the command parsing has been configured in a valid way.
875 clap_command().debug_assert();
876 }
Jiyong Park331d1ea2021-05-10 11:01:23 +0900877}