blob: c3fae6962e269b0e2c9e326bdcb47c2cf49b68a1 [file] [log] [blame]
Jiyong Park331d1ea2021-05-10 11:01:23 +09001/*
2 * Copyright (C) 2021 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17//! `zipfuse` is a FUSE filesystem for zip archives. It provides transparent access to the files
18//! in a zip archive. This filesystem does not supporting writing files back to the zip archive.
19//! The filesystem has to be mounted read only.
20
21mod inode;
22
23use anyhow::Result;
24use clap::{App, Arg};
25use fuse::filesystem::*;
26use fuse::mount::*;
27use std::collections::HashMap;
28use std::convert::TryFrom;
Jiyong Park851f68a2021-05-11 21:41:25 +090029use std::ffi::{CStr, CString};
Jiyong Park331d1ea2021-05-10 11:01:23 +090030use std::fs::{File, OpenOptions};
31use std::io;
32use std::io::Read;
Jiyong Park63a95cf2021-05-13 19:20:30 +090033use std::mem::size_of;
Jiyong Park331d1ea2021-05-10 11:01:23 +090034use std::os::unix::io::AsRawFd;
35use std::path::Path;
36use std::sync::Mutex;
37
38use crate::inode::{DirectoryEntry, Inode, InodeData, InodeKind, InodeTable};
39
40fn main() -> Result<()> {
41 let matches = App::new("zipfuse")
Jiyong Park6a762db2021-05-31 14:00:52 +090042 .arg(
43 Arg::with_name("options")
44 .short("o")
45 .takes_value(true)
46 .required(false)
Chris Wailes68c39f82021-07-27 16:03:44 -070047 .help("Comma separated list of mount options"),
Jiyong Park6a762db2021-05-31 14:00:52 +090048 )
Jiyong Park331d1ea2021-05-10 11:01:23 +090049 .arg(Arg::with_name("ZIPFILE").required(true))
50 .arg(Arg::with_name("MOUNTPOINT").required(true))
51 .get_matches();
52
53 let zip_file = matches.value_of("ZIPFILE").unwrap().as_ref();
54 let mount_point = matches.value_of("MOUNTPOINT").unwrap().as_ref();
Jiyong Park6a762db2021-05-31 14:00:52 +090055 let options = matches.value_of("options");
56 run_fuse(zip_file, mount_point, options)?;
Jiyong Park331d1ea2021-05-10 11:01:23 +090057 Ok(())
58}
59
60/// Runs a fuse filesystem by mounting `zip_file` on `mount_point`.
Jiyong Park6a762db2021-05-31 14:00:52 +090061pub fn run_fuse(zip_file: &Path, mount_point: &Path, extra_options: Option<&str>) -> Result<()> {
Jiyong Park331d1ea2021-05-10 11:01:23 +090062 const MAX_READ: u32 = 1 << 20; // TODO(jiyong): tune this
63 const MAX_WRITE: u32 = 1 << 13; // This is a read-only filesystem
64
65 let dev_fuse = OpenOptions::new().read(true).write(true).open("/dev/fuse")?;
66
Jiyong Park6a762db2021-05-31 14:00:52 +090067 let mut mount_options = vec![
68 MountOption::FD(dev_fuse.as_raw_fd()),
69 MountOption::RootMode(libc::S_IFDIR | libc::S_IXUSR | libc::S_IXGRP | libc::S_IXOTH),
70 MountOption::AllowOther,
71 MountOption::UserId(0),
72 MountOption::GroupId(0),
73 MountOption::MaxRead(MAX_READ),
74 ];
75 if let Some(value) = extra_options {
76 mount_options.push(MountOption::Extra(value));
77 }
78
Jiyong Park331d1ea2021-05-10 11:01:23 +090079 fuse::mount(
80 mount_point,
81 "zipfuse",
82 libc::MS_NOSUID | libc::MS_NODEV | libc::MS_RDONLY,
Jiyong Park6a762db2021-05-31 14:00:52 +090083 &mount_options,
Jiyong Park331d1ea2021-05-10 11:01:23 +090084 )?;
Victor Hsieh58a5e9b2022-03-09 21:57:26 +000085 let mut config = fuse::FuseConfig::new();
86 config.dev_fuse(dev_fuse).max_write(MAX_WRITE).max_read(MAX_READ);
87 Ok(config.enter_message_loop(ZipFuse::new(zip_file)?)?)
Jiyong Park331d1ea2021-05-10 11:01:23 +090088}
89
90struct ZipFuse {
91 zip_archive: Mutex<zip::ZipArchive<File>>,
Jiyong Parkf5ff33c2021-08-30 22:32:19 +090092 raw_file: Mutex<File>,
Jiyong Park331d1ea2021-05-10 11:01:23 +090093 inode_table: InodeTable,
Jiyong Parkf5ff33c2021-08-30 22:32:19 +090094 open_files: Mutex<HashMap<Handle, OpenFile>>,
Jiyong Park331d1ea2021-05-10 11:01:23 +090095 open_dirs: Mutex<HashMap<Handle, OpenDirBuf>>,
96}
97
Jiyong Parkf5ff33c2021-08-30 22:32:19 +090098/// Represents a [`ZipFile`] that is opened.
99struct OpenFile {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900100 open_count: u32, // multiple opens share the buf because this is a read-only filesystem
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900101 content: OpenFileContent,
102}
103
104/// Holds the content of a [`ZipFile`]. Depending on whether it is compressed or not, the
105/// entire content is stored, or only the zip index is stored.
106enum OpenFileContent {
107 Compressed(Box<[u8]>),
108 Uncompressed(usize), // zip index
Jiyong Park331d1ea2021-05-10 11:01:23 +0900109}
110
111/// Holds the directory entries in a directory opened by [`opendir`].
112struct OpenDirBuf {
113 open_count: u32,
114 buf: Box<[(CString, DirectoryEntry)]>,
115}
116
117type Handle = u64;
118
119fn ebadf() -> io::Error {
120 io::Error::from_raw_os_error(libc::EBADF)
121}
122
123fn timeout_max() -> std::time::Duration {
124 std::time::Duration::new(u64::MAX, 1_000_000_000 - 1)
125}
126
127impl ZipFuse {
128 fn new(zip_file: &Path) -> Result<ZipFuse> {
129 // TODO(jiyong): Use O_DIRECT to avoid double caching.
130 // `.custom_flags(nix::fcntl::OFlag::O_DIRECT.bits())` currently doesn't work.
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900131 let f = File::open(zip_file)?;
Jiyong Park331d1ea2021-05-10 11:01:23 +0900132 let mut z = zip::ZipArchive::new(f)?;
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900133 // Open the same file again so that we can directly access it when accessing
134 // uncompressed zip_file entries in it. `ZipFile` doesn't implement `Seek`.
135 let raw_file = File::open(zip_file)?;
Jiyong Park331d1ea2021-05-10 11:01:23 +0900136 let it = InodeTable::from_zip(&mut z)?;
137 Ok(ZipFuse {
138 zip_archive: Mutex::new(z),
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900139 raw_file: Mutex::new(raw_file),
Jiyong Park331d1ea2021-05-10 11:01:23 +0900140 inode_table: it,
141 open_files: Mutex::new(HashMap::new()),
142 open_dirs: Mutex::new(HashMap::new()),
143 })
144 }
145
146 fn find_inode(&self, inode: Inode) -> io::Result<&InodeData> {
147 self.inode_table.get(inode).ok_or_else(ebadf)
148 }
149
Jiyong Parkd5df9562021-05-13 00:50:23 +0900150 // TODO(jiyong) remove this. Right now this is needed to do the nlink_t to u64 conversion below
151 // on aosp_x86_64 target. That however is a useless conversion on other targets.
152 #[allow(clippy::useless_conversion)]
Jiyong Park331d1ea2021-05-10 11:01:23 +0900153 fn stat_from(&self, inode: Inode) -> io::Result<libc::stat64> {
154 let inode_data = self.find_inode(inode)?;
155 let mut st = unsafe { std::mem::MaybeUninit::<libc::stat64>::zeroed().assume_init() };
156 st.st_dev = 0;
Jiyong Parkd5df9562021-05-13 00:50:23 +0900157 st.st_nlink = if let Some(directory) = inode_data.get_directory() {
158 (2 + directory.len() as libc::nlink_t).into()
Jiyong Park331d1ea2021-05-10 11:01:23 +0900159 } else {
160 1
161 };
162 st.st_ino = inode;
163 st.st_mode = if inode_data.is_dir() { libc::S_IFDIR } else { libc::S_IFREG };
164 st.st_mode |= inode_data.mode;
165 st.st_uid = 0;
166 st.st_gid = 0;
167 st.st_size = i64::try_from(inode_data.size).unwrap_or(i64::MAX);
168 Ok(st)
169 }
170}
171
172impl fuse::filesystem::FileSystem for ZipFuse {
173 type Inode = Inode;
174 type Handle = Handle;
175 type DirIter = DirIter;
176
177 fn init(&self, _capable: FsOptions) -> std::io::Result<FsOptions> {
178 // The default options added by the fuse crate are fine. We don't have additional options.
179 Ok(FsOptions::empty())
180 }
181
182 fn lookup(&self, _ctx: Context, parent: Self::Inode, name: &CStr) -> io::Result<Entry> {
183 let inode = self.find_inode(parent)?;
184 let directory = inode.get_directory().ok_or_else(ebadf)?;
Jiyong Park331d1ea2021-05-10 11:01:23 +0900185 let entry = directory.get(name);
186 match entry {
187 Some(e) => Ok(Entry {
188 inode: e.inode,
189 generation: 0,
190 attr: self.stat_from(e.inode)?,
191 attr_timeout: timeout_max(), // this is a read-only fs
192 entry_timeout: timeout_max(),
193 }),
194 _ => Err(io::Error::from_raw_os_error(libc::ENOENT)),
195 }
196 }
197
198 fn getattr(
199 &self,
200 _ctx: Context,
201 inode: Self::Inode,
202 _handle: Option<Self::Handle>,
203 ) -> io::Result<(libc::stat64, std::time::Duration)> {
204 let st = self.stat_from(inode)?;
205 Ok((st, timeout_max()))
206 }
207
208 fn open(
209 &self,
210 _ctx: Context,
211 inode: Self::Inode,
212 _flags: u32,
213 ) -> io::Result<(Option<Self::Handle>, fuse::filesystem::OpenOptions)> {
214 let mut open_files = self.open_files.lock().unwrap();
215 let handle = inode as Handle;
216
217 // If the file is already opened, just increase the reference counter. If not, read the
218 // entire file content to the buffer. When `read` is called, a portion of the buffer is
219 // copied to the kernel.
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900220 if let Some(file) = open_files.get_mut(&handle) {
221 if file.open_count == 0 {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900222 return Err(ebadf());
223 }
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900224 file.open_count += 1;
Jiyong Park331d1ea2021-05-10 11:01:23 +0900225 } else {
226 let inode_data = self.find_inode(inode)?;
227 let zip_index = inode_data.get_zip_index().ok_or_else(ebadf)?;
228 let mut zip_archive = self.zip_archive.lock().unwrap();
229 let mut zip_file = zip_archive.by_index(zip_index)?;
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900230 let content = match zip_file.compression() {
231 zip::CompressionMethod::Stored => OpenFileContent::Uncompressed(zip_index),
232 _ => {
233 if let Some(mode) = zip_file.unix_mode() {
234 let is_reg_file = zip_file.is_file();
235 let is_executable =
236 mode & (libc::S_IXUSR | libc::S_IXGRP | libc::S_IXOTH) != 0;
237 if is_reg_file && is_executable {
238 log::warn!(
239 "Executable file {:?} is stored compressed. Consider \
240 storing it uncompressed to save memory",
241 zip_file.mangled_name()
242 );
243 }
244 }
245 let mut buf = Vec::with_capacity(inode_data.size as usize);
246 zip_file.read_to_end(&mut buf)?;
247 OpenFileContent::Compressed(buf.into_boxed_slice())
248 }
249 };
250 open_files.insert(handle, OpenFile { open_count: 1, content });
Jiyong Park331d1ea2021-05-10 11:01:23 +0900251 }
252 // Note: we don't return `DIRECT_IO` here, because then applications wouldn't be able to
253 // mmap the files.
254 Ok((Some(handle), fuse::filesystem::OpenOptions::empty()))
255 }
256
257 fn release(
258 &self,
259 _ctx: Context,
260 inode: Self::Inode,
261 _flags: u32,
262 _handle: Self::Handle,
263 _flush: bool,
264 _flock_release: bool,
265 _lock_owner: Option<u64>,
266 ) -> io::Result<()> {
267 // Releases the buffer for the `handle` when it is opened for nobody. While this is good
268 // for saving memory, this has a performance implication because we need to decompress
269 // again when the same file is opened in the future.
270 let mut open_files = self.open_files.lock().unwrap();
271 let handle = inode as Handle;
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900272 if let Some(file) = open_files.get_mut(&handle) {
273 if file.open_count.checked_sub(1).ok_or_else(ebadf)? == 0 {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900274 open_files.remove(&handle);
275 }
276 Ok(())
277 } else {
278 Err(ebadf())
279 }
280 }
281
282 fn read<W: io::Write + ZeroCopyWriter>(
283 &self,
284 _ctx: Context,
285 _inode: Self::Inode,
286 handle: Self::Handle,
287 mut w: W,
288 size: u32,
289 offset: u64,
290 _lock_owner: Option<u64>,
291 _flags: u32,
292 ) -> io::Result<usize> {
293 let open_files = self.open_files.lock().unwrap();
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900294 let file = open_files.get(&handle).ok_or_else(ebadf)?;
295 if file.open_count == 0 {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900296 return Err(ebadf());
297 }
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900298 Ok(match &file.content {
299 OpenFileContent::Uncompressed(zip_index) => {
300 let mut zip_archive = self.zip_archive.lock().unwrap();
301 let zip_file = zip_archive.by_index(*zip_index)?;
302 let start = zip_file.data_start() + offset;
303 let remaining_size = zip_file.size() - offset;
304 let size = std::cmp::min(remaining_size, size.into());
305
306 let mut raw_file = self.raw_file.lock().unwrap();
307 w.write_from(&mut raw_file, size as usize, start)?
308 }
309 OpenFileContent::Compressed(buf) => {
310 let start = offset as usize;
311 let end = start + size as usize;
312 let end = std::cmp::min(end, buf.len());
313 w.write(&buf[start..end])?
314 }
315 })
Jiyong Park331d1ea2021-05-10 11:01:23 +0900316 }
317
318 fn opendir(
319 &self,
320 _ctx: Context,
321 inode: Self::Inode,
322 _flags: u32,
323 ) -> io::Result<(Option<Self::Handle>, fuse::filesystem::OpenOptions)> {
324 let mut open_dirs = self.open_dirs.lock().unwrap();
325 let handle = inode as Handle;
326 if let Some(odb) = open_dirs.get_mut(&handle) {
327 if odb.open_count == 0 {
328 return Err(ebadf());
329 }
330 odb.open_count += 1;
331 } else {
332 let inode_data = self.find_inode(inode)?;
333 let directory = inode_data.get_directory().ok_or_else(ebadf)?;
334 let mut buf: Vec<(CString, DirectoryEntry)> = Vec::with_capacity(directory.len());
335 for (name, dir_entry) in directory.iter() {
336 let name = CString::new(name.as_bytes()).unwrap();
337 buf.push((name, dir_entry.clone()));
338 }
339 open_dirs.insert(handle, OpenDirBuf { open_count: 1, buf: buf.into_boxed_slice() });
340 }
Jiyong Park63a95cf2021-05-13 19:20:30 +0900341 Ok((Some(handle), fuse::filesystem::OpenOptions::CACHE_DIR))
Jiyong Park331d1ea2021-05-10 11:01:23 +0900342 }
343
344 fn releasedir(
345 &self,
346 _ctx: Context,
347 inode: Self::Inode,
348 _flags: u32,
349 _handle: Self::Handle,
350 ) -> io::Result<()> {
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.checked_sub(1).ok_or_else(ebadf)? == 0 {
355 open_dirs.remove(&handle);
356 }
357 Ok(())
358 } else {
359 Err(ebadf())
360 }
361 }
362
363 fn readdir(
364 &self,
365 _ctx: Context,
366 inode: Self::Inode,
367 _handle: Self::Handle,
368 size: u32,
369 offset: u64,
370 ) -> io::Result<Self::DirIter> {
371 let open_dirs = self.open_dirs.lock().unwrap();
372 let handle = inode as Handle;
373 let odb = open_dirs.get(&handle).ok_or_else(ebadf)?;
374 if odb.open_count == 0 {
375 return Err(ebadf());
376 }
377 let buf = &odb.buf;
378 let start = offset as usize;
Jiyong Park63a95cf2021-05-13 19:20:30 +0900379
380 // Estimate the size of each entry will take space in the buffer. See
381 // external/crosvm/fuse/src/server.rs#add_dirent
382 let mut estimate: usize = 0; // estimated number of bytes we will be writing
383 let mut end = start; // index in `buf`
384 while estimate < size as usize && end < buf.len() {
385 let dirent_size = size_of::<fuse::sys::Dirent>();
386 let name_size = buf[end].0.to_bytes().len();
387 estimate += (dirent_size + name_size + 7) & !7; // round to 8 byte boundary
388 end += 1;
389 }
390
Jiyong Park331d1ea2021-05-10 11:01:23 +0900391 let mut new_buf = Vec::with_capacity(end - start);
392 // The portion of `buf` is *copied* to the iterator. This is not ideal, but inevitable
393 // because the `name` field in `fuse::filesystem::DirEntry` is `&CStr` not `CString`.
394 new_buf.extend_from_slice(&buf[start..end]);
395 Ok(DirIter { inner: new_buf, offset, cur: 0 })
396 }
397}
398
399struct DirIter {
400 inner: Vec<(CString, DirectoryEntry)>,
401 offset: u64, // the offset where this iterator begins. `next` doesn't change this.
402 cur: usize, // the current index in `inner`. `next` advances this.
403}
404
405impl fuse::filesystem::DirectoryIterator for DirIter {
406 fn next(&mut self) -> Option<fuse::filesystem::DirEntry> {
407 if self.cur >= self.inner.len() {
408 return None;
409 }
410
411 let (name, entry) = &self.inner[self.cur];
412 self.cur += 1;
413 Some(fuse::filesystem::DirEntry {
414 ino: entry.inode as libc::ino64_t,
415 offset: self.offset + self.cur as u64,
416 type_: match entry.kind {
417 InodeKind::Directory => libc::DT_DIR.into(),
418 InodeKind::File => libc::DT_REG.into(),
419 },
420 name,
421 })
422 }
423}
424
425#[cfg(test)]
426mod tests {
427 use anyhow::{bail, Result};
428 use nix::sys::statfs::{statfs, FsType};
Jiyong Park63a95cf2021-05-13 19:20:30 +0900429 use std::collections::BTreeSet;
Jiyong Park331d1ea2021-05-10 11:01:23 +0900430 use std::fs;
431 use std::fs::File;
432 use std::io::Write;
433 use std::path::{Path, PathBuf};
434 use std::time::{Duration, Instant};
435 use zip::write::FileOptions;
436
437 #[cfg(not(target_os = "android"))]
438 fn start_fuse(zip_path: &Path, mnt_path: &Path) {
439 let zip_path = PathBuf::from(zip_path);
440 let mnt_path = PathBuf::from(mnt_path);
441 std::thread::spawn(move || {
Jiyong Park6a762db2021-05-31 14:00:52 +0900442 crate::run_fuse(&zip_path, &mnt_path, None).unwrap();
Jiyong Park331d1ea2021-05-10 11:01:23 +0900443 });
444 }
445
446 #[cfg(target_os = "android")]
447 fn start_fuse(zip_path: &Path, mnt_path: &Path) {
448 // Note: for some unknown reason, running a thread to serve fuse doesn't work on Android.
449 // Explicitly spawn a zipfuse process instead.
450 // TODO(jiyong): fix this
451 assert!(std::process::Command::new("sh")
452 .arg("-c")
453 .arg(format!("/data/local/tmp/zipfuse {} {}", zip_path.display(), mnt_path.display()))
454 .spawn()
455 .is_ok());
456 }
457
458 fn wait_for_mount(mount_path: &Path) -> Result<()> {
459 let start_time = Instant::now();
460 const POLL_INTERVAL: Duration = Duration::from_millis(50);
461 const TIMEOUT: Duration = Duration::from_secs(10);
462 const FUSE_SUPER_MAGIC: FsType = FsType(0x65735546);
463 loop {
464 if statfs(mount_path)?.filesystem_type() == FUSE_SUPER_MAGIC {
465 break;
466 }
467
468 if start_time.elapsed() > TIMEOUT {
469 bail!("Time out mounting zipfuse");
470 }
471 std::thread::sleep(POLL_INTERVAL);
472 }
473 Ok(())
474 }
475
476 // Creates a zip file, adds some files to the zip file, mounts it using zipfuse, runs the check
477 // routine, and finally unmounts.
478 fn run_test(add: fn(&mut zip::ZipWriter<File>), check: fn(&std::path::Path)) {
479 // Create an empty zip file
480 let test_dir = tempfile::TempDir::new().unwrap();
481 let zip_path = test_dir.path().join("test.zip");
482 let zip = File::create(&zip_path);
483 assert!(zip.is_ok());
484 let mut zip = zip::ZipWriter::new(zip.unwrap());
485
486 // Let test users add files/dirs to the zip file
487 add(&mut zip);
488 assert!(zip.finish().is_ok());
489 drop(zip);
490
491 // Mount the zip file on the "mnt" dir using zipfuse.
492 let mnt_path = test_dir.path().join("mnt");
493 assert!(fs::create_dir(&mnt_path).is_ok());
494
495 start_fuse(&zip_path, &mnt_path);
496
497 let mnt_path = test_dir.path().join("mnt");
498 // Give some time for the fuse to boot up
499 assert!(wait_for_mount(&mnt_path).is_ok());
500 // Run the check routine, and do the clean up.
501 check(&mnt_path);
502 assert!(nix::mount::umount2(&mnt_path, nix::mount::MntFlags::empty()).is_ok());
503 }
504
505 fn check_file(root: &Path, file: &str, content: &[u8]) {
506 let path = root.join(file);
507 assert!(path.exists());
508
509 let metadata = fs::metadata(&path);
510 assert!(metadata.is_ok());
511
512 let metadata = metadata.unwrap();
513 assert!(metadata.is_file());
514 assert_eq!(content.len(), metadata.len() as usize);
515
516 let read_data = fs::read(&path);
517 assert!(read_data.is_ok());
518 assert_eq!(content, read_data.unwrap().as_slice());
519 }
520
Jiyong Park63a95cf2021-05-13 19:20:30 +0900521 fn check_dir<S: AsRef<str>>(root: &Path, dir: &str, files: &[S], dirs: &[S]) {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900522 let dir_path = root.join(dir);
523 assert!(dir_path.exists());
524
525 let metadata = fs::metadata(&dir_path);
526 assert!(metadata.is_ok());
527
528 let metadata = metadata.unwrap();
529 assert!(metadata.is_dir());
530
531 let iter = fs::read_dir(&dir_path);
532 assert!(iter.is_ok());
533
534 let iter = iter.unwrap();
Jiyong Park63a95cf2021-05-13 19:20:30 +0900535 let mut actual_files = BTreeSet::new();
536 let mut actual_dirs = BTreeSet::new();
Jiyong Park331d1ea2021-05-10 11:01:23 +0900537 for de in iter {
538 let entry = de.unwrap();
539 let path = entry.path();
540 if path.is_dir() {
541 actual_dirs.insert(path.strip_prefix(&dir_path).unwrap().to_path_buf());
542 } else {
543 actual_files.insert(path.strip_prefix(&dir_path).unwrap().to_path_buf());
544 }
545 }
Jiyong Park63a95cf2021-05-13 19:20:30 +0900546 let expected_files: BTreeSet<PathBuf> =
547 files.iter().map(|s| PathBuf::from(s.as_ref())).collect();
548 let expected_dirs: BTreeSet<PathBuf> =
549 dirs.iter().map(|s| PathBuf::from(s.as_ref())).collect();
Jiyong Park331d1ea2021-05-10 11:01:23 +0900550
551 assert_eq!(expected_files, actual_files);
552 assert_eq!(expected_dirs, actual_dirs);
553 }
554
555 #[test]
556 fn empty() {
557 run_test(
558 |_| {},
559 |root| {
Jiyong Park63a95cf2021-05-13 19:20:30 +0900560 check_dir::<String>(root, "", &[], &[]);
Jiyong Park331d1ea2021-05-10 11:01:23 +0900561 },
562 );
563 }
564
565 #[test]
566 fn single_file() {
567 run_test(
568 |zip| {
569 zip.start_file("foo", FileOptions::default()).unwrap();
570 zip.write_all(b"0123456789").unwrap();
571 },
572 |root| {
573 check_dir(root, "", &["foo"], &[]);
574 check_file(root, "foo", b"0123456789");
575 },
576 );
577 }
578
579 #[test]
580 fn single_dir() {
581 run_test(
582 |zip| {
583 zip.add_directory("dir", FileOptions::default()).unwrap();
584 },
585 |root| {
586 check_dir(root, "", &[], &["dir"]);
Jiyong Park63a95cf2021-05-13 19:20:30 +0900587 check_dir::<String>(root, "dir", &[], &[]);
Jiyong Park331d1ea2021-05-10 11:01:23 +0900588 },
589 );
590 }
591
592 #[test]
593 fn complex_hierarchy() {
594 // root/
595 // a/
596 // b1/
597 // b2/
598 // c1 (file)
599 // c2/
600 // d1 (file)
601 // d2 (file)
602 // d3 (file)
603 // x/
604 // y1 (file)
605 // y2 (file)
606 // y3/
607 //
608 // foo (file)
609 // bar (file)
610 run_test(
611 |zip| {
612 let opt = FileOptions::default();
613 zip.add_directory("a/b1", opt).unwrap();
614
615 zip.start_file("a/b2/c1", opt).unwrap();
616
617 zip.start_file("a/b2/c2/d1", opt).unwrap();
618 zip.start_file("a/b2/c2/d2", opt).unwrap();
619 zip.start_file("a/b2/c2/d3", opt).unwrap();
620
621 zip.start_file("x/y1", opt).unwrap();
622 zip.start_file("x/y2", opt).unwrap();
623 zip.add_directory("x/y3", opt).unwrap();
624
625 zip.start_file("foo", opt).unwrap();
626 zip.start_file("bar", opt).unwrap();
627 },
628 |root| {
629 check_dir(root, "", &["foo", "bar"], &["a", "x"]);
630 check_dir(root, "a", &[], &["b1", "b2"]);
Jiyong Park63a95cf2021-05-13 19:20:30 +0900631 check_dir::<String>(root, "a/b1", &[], &[]);
Jiyong Park331d1ea2021-05-10 11:01:23 +0900632 check_dir(root, "a/b2", &["c1"], &["c2"]);
633 check_dir(root, "a/b2/c2", &["d1", "d2", "d3"], &[]);
634 check_dir(root, "x", &["y1", "y2"], &["y3"]);
Jiyong Park63a95cf2021-05-13 19:20:30 +0900635 check_dir::<String>(root, "x/y3", &[], &[]);
Jiyong Park331d1ea2021-05-10 11:01:23 +0900636 check_file(root, "a/b2/c1", &[]);
637 check_file(root, "a/b2/c2/d1", &[]);
638 check_file(root, "a/b2/c2/d2", &[]);
639 check_file(root, "a/b2/c2/d3", &[]);
640 check_file(root, "x/y1", &[]);
641 check_file(root, "x/y2", &[]);
642 check_file(root, "foo", &[]);
643 check_file(root, "bar", &[]);
644 },
645 );
646 }
647
648 #[test]
649 fn large_file() {
650 run_test(
651 |zip| {
652 let data = vec![10; 2 << 20];
653 zip.start_file("foo", FileOptions::default()).unwrap();
654 zip.write_all(&data).unwrap();
655 },
656 |root| {
657 let data = vec![10; 2 << 20];
658 check_file(root, "foo", &data);
659 },
660 );
661 }
Jiyong Park63a95cf2021-05-13 19:20:30 +0900662
663 #[test]
664 fn large_dir() {
665 const NUM_FILES: usize = 1 << 10;
666 run_test(
667 |zip| {
668 let opt = FileOptions::default();
669 // create 1K files. Each file has a name of length 100. So total size is at least
670 // 100KB, which is bigger than the readdir buffer size of 4K.
671 for i in 0..NUM_FILES {
672 zip.start_file(format!("dir/{:0100}", i), opt).unwrap();
673 }
674 },
675 |root| {
676 let dirs_expected: Vec<_> = (0..NUM_FILES).map(|i| format!("{:0100}", i)).collect();
677 check_dir(
678 root,
679 "dir",
680 dirs_expected.iter().map(|s| s.as_str()).collect::<Vec<&str>>().as_slice(),
681 &[],
682 );
683 },
684 );
685 }
Jiyong Parkd40f7bb2021-05-17 10:55:56 +0900686
Jiyong Parke6587ca2021-05-17 14:42:23 +0900687 fn run_fuse_and_check_test_zip(test_dir: &Path, zip_path: &Path) {
688 let mnt_path = test_dir.join("mnt");
Jiyong Parkd40f7bb2021-05-17 10:55:56 +0900689 assert!(fs::create_dir(&mnt_path).is_ok());
690
Chris Wailes68c39f82021-07-27 16:03:44 -0700691 start_fuse(zip_path, &mnt_path);
Jiyong Parkd40f7bb2021-05-17 10:55:56 +0900692
693 // Give some time for the fuse to boot up
694 assert!(wait_for_mount(&mnt_path).is_ok());
695
696 check_dir(&mnt_path, "", &[], &["dir"]);
697 check_dir(&mnt_path, "dir", &["file1", "file2"], &[]);
698 check_file(&mnt_path, "dir/file1", include_bytes!("../testdata/dir/file1"));
699 check_file(&mnt_path, "dir/file2", include_bytes!("../testdata/dir/file2"));
700 assert!(nix::mount::umount2(&mnt_path, nix::mount::MntFlags::empty()).is_ok());
701 }
Jiyong Parke6587ca2021-05-17 14:42:23 +0900702
703 #[test]
704 fn supports_deflate() {
705 let test_dir = tempfile::TempDir::new().unwrap();
706 let zip_path = test_dir.path().join("test.zip");
707 let mut zip_file = File::create(&zip_path).unwrap();
708 zip_file.write_all(include_bytes!("../testdata/test.zip")).unwrap();
709
Chris Wailes68c39f82021-07-27 16:03:44 -0700710 run_fuse_and_check_test_zip(test_dir.path(), &zip_path);
Jiyong Parke6587ca2021-05-17 14:42:23 +0900711 }
712
Jiyong Parkf5ff33c2021-08-30 22:32:19 +0900713 #[test]
714 fn supports_store() {
715 run_test(
716 |zip| {
717 let data = vec![10; 2 << 20];
718 zip.start_file(
719 "foo",
720 FileOptions::default().compression_method(zip::CompressionMethod::Stored),
721 )
722 .unwrap();
723 zip.write_all(&data).unwrap();
724 },
725 |root| {
726 let data = vec![10; 2 << 20];
727 check_file(root, "foo", &data);
728 },
729 );
730 }
731
Jiyong Parke6587ca2021-05-17 14:42:23 +0900732 #[cfg(not(target_os = "android"))] // Android doesn't have the loopdev crate
733 #[test]
734 fn supports_zip_on_block_device() {
735 // Write test.zip to the test directory
736 let test_dir = tempfile::TempDir::new().unwrap();
737 let zip_path = test_dir.path().join("test.zip");
738 let mut zip_file = File::create(&zip_path).unwrap();
739 let data = include_bytes!("../testdata/test.zip");
740 zip_file.write_all(data).unwrap();
741
742 // Pad 0 to test.zip so that its size is multiple of 4096.
743 const BLOCK_SIZE: usize = 4096;
744 let size = (data.len() + BLOCK_SIZE) & !BLOCK_SIZE;
745 let pad_size = size - data.len();
746 assert!(pad_size != 0);
747 let pad = vec![0; pad_size];
748 zip_file.write_all(pad.as_slice()).unwrap();
749 drop(zip_file);
750
751 // Attach test.zip to a loop device
752 let lc = loopdev::LoopControl::open().unwrap();
753 let ld = scopeguard::guard(lc.next_free().unwrap(), |ld| {
754 ld.detach().unwrap();
755 });
756 ld.attach_file(&zip_path).unwrap();
757
758 // Start zipfuse over to the loop device (not the zip file)
759 run_fuse_and_check_test_zip(&test_dir.path(), &ld.path().unwrap());
760 }
Jiyong Park331d1ea2021-05-10 11:01:23 +0900761}