blob: 4ab934d4151937d5baed5141cdf4ced1749154f2 [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 )?;
85 Ok(fuse::worker::start_message_loop(dev_fuse, MAX_READ, MAX_WRITE, ZipFuse::new(zip_file)?)?)
86}
87
88struct ZipFuse {
89 zip_archive: Mutex<zip::ZipArchive<File>>,
90 inode_table: InodeTable,
91 open_files: Mutex<HashMap<Handle, OpenFileBuf>>,
92 open_dirs: Mutex<HashMap<Handle, OpenDirBuf>>,
93}
94
95/// Holds the (decompressed) contents of a [`ZipFile`].
96///
97/// This buf is needed because `ZipFile` is in general not seekable due to the compression.
98///
99/// TODO(jiyong): do this only for compressed `ZipFile`s. Uncompressed (store) files don't need
100/// this; they can be directly read from `zip_archive`.
101struct OpenFileBuf {
102 open_count: u32, // multiple opens share the buf because this is a read-only filesystem
103 buf: Box<[u8]>,
104}
105
106/// Holds the directory entries in a directory opened by [`opendir`].
107struct OpenDirBuf {
108 open_count: u32,
109 buf: Box<[(CString, DirectoryEntry)]>,
110}
111
112type Handle = u64;
113
114fn ebadf() -> io::Error {
115 io::Error::from_raw_os_error(libc::EBADF)
116}
117
118fn timeout_max() -> std::time::Duration {
119 std::time::Duration::new(u64::MAX, 1_000_000_000 - 1)
120}
121
122impl ZipFuse {
123 fn new(zip_file: &Path) -> Result<ZipFuse> {
124 // TODO(jiyong): Use O_DIRECT to avoid double caching.
125 // `.custom_flags(nix::fcntl::OFlag::O_DIRECT.bits())` currently doesn't work.
126 let f = OpenOptions::new().read(true).open(zip_file)?;
127 let mut z = zip::ZipArchive::new(f)?;
128 let it = InodeTable::from_zip(&mut z)?;
129 Ok(ZipFuse {
130 zip_archive: Mutex::new(z),
131 inode_table: it,
132 open_files: Mutex::new(HashMap::new()),
133 open_dirs: Mutex::new(HashMap::new()),
134 })
135 }
136
137 fn find_inode(&self, inode: Inode) -> io::Result<&InodeData> {
138 self.inode_table.get(inode).ok_or_else(ebadf)
139 }
140
Jiyong Parkd5df9562021-05-13 00:50:23 +0900141 // TODO(jiyong) remove this. Right now this is needed to do the nlink_t to u64 conversion below
142 // on aosp_x86_64 target. That however is a useless conversion on other targets.
143 #[allow(clippy::useless_conversion)]
Jiyong Park331d1ea2021-05-10 11:01:23 +0900144 fn stat_from(&self, inode: Inode) -> io::Result<libc::stat64> {
145 let inode_data = self.find_inode(inode)?;
146 let mut st = unsafe { std::mem::MaybeUninit::<libc::stat64>::zeroed().assume_init() };
147 st.st_dev = 0;
Jiyong Parkd5df9562021-05-13 00:50:23 +0900148 st.st_nlink = if let Some(directory) = inode_data.get_directory() {
149 (2 + directory.len() as libc::nlink_t).into()
Jiyong Park331d1ea2021-05-10 11:01:23 +0900150 } else {
151 1
152 };
153 st.st_ino = inode;
154 st.st_mode = if inode_data.is_dir() { libc::S_IFDIR } else { libc::S_IFREG };
155 st.st_mode |= inode_data.mode;
156 st.st_uid = 0;
157 st.st_gid = 0;
158 st.st_size = i64::try_from(inode_data.size).unwrap_or(i64::MAX);
159 Ok(st)
160 }
161}
162
163impl fuse::filesystem::FileSystem for ZipFuse {
164 type Inode = Inode;
165 type Handle = Handle;
166 type DirIter = DirIter;
167
168 fn init(&self, _capable: FsOptions) -> std::io::Result<FsOptions> {
169 // The default options added by the fuse crate are fine. We don't have additional options.
170 Ok(FsOptions::empty())
171 }
172
173 fn lookup(&self, _ctx: Context, parent: Self::Inode, name: &CStr) -> io::Result<Entry> {
174 let inode = self.find_inode(parent)?;
175 let directory = inode.get_directory().ok_or_else(ebadf)?;
Jiyong Park331d1ea2021-05-10 11:01:23 +0900176 let entry = directory.get(name);
177 match entry {
178 Some(e) => Ok(Entry {
179 inode: e.inode,
180 generation: 0,
181 attr: self.stat_from(e.inode)?,
182 attr_timeout: timeout_max(), // this is a read-only fs
183 entry_timeout: timeout_max(),
184 }),
185 _ => Err(io::Error::from_raw_os_error(libc::ENOENT)),
186 }
187 }
188
189 fn getattr(
190 &self,
191 _ctx: Context,
192 inode: Self::Inode,
193 _handle: Option<Self::Handle>,
194 ) -> io::Result<(libc::stat64, std::time::Duration)> {
195 let st = self.stat_from(inode)?;
196 Ok((st, timeout_max()))
197 }
198
199 fn open(
200 &self,
201 _ctx: Context,
202 inode: Self::Inode,
203 _flags: u32,
204 ) -> io::Result<(Option<Self::Handle>, fuse::filesystem::OpenOptions)> {
205 let mut open_files = self.open_files.lock().unwrap();
206 let handle = inode as Handle;
207
208 // If the file is already opened, just increase the reference counter. If not, read the
209 // entire file content to the buffer. When `read` is called, a portion of the buffer is
210 // copied to the kernel.
211 // TODO(jiyong): do this only for compressed zip files. Files that are not compressed
212 // (store) can be directly read from zip_archive. That will help reduce the memory usage.
213 if let Some(ofb) = open_files.get_mut(&handle) {
214 if ofb.open_count == 0 {
215 return Err(ebadf());
216 }
217 ofb.open_count += 1;
218 } else {
219 let inode_data = self.find_inode(inode)?;
220 let zip_index = inode_data.get_zip_index().ok_or_else(ebadf)?;
221 let mut zip_archive = self.zip_archive.lock().unwrap();
222 let mut zip_file = zip_archive.by_index(zip_index)?;
223 let mut buf = Vec::with_capacity(inode_data.size as usize);
224 zip_file.read_to_end(&mut buf)?;
225 open_files.insert(handle, OpenFileBuf { open_count: 1, buf: buf.into_boxed_slice() });
226 }
227 // Note: we don't return `DIRECT_IO` here, because then applications wouldn't be able to
228 // mmap the files.
229 Ok((Some(handle), fuse::filesystem::OpenOptions::empty()))
230 }
231
232 fn release(
233 &self,
234 _ctx: Context,
235 inode: Self::Inode,
236 _flags: u32,
237 _handle: Self::Handle,
238 _flush: bool,
239 _flock_release: bool,
240 _lock_owner: Option<u64>,
241 ) -> io::Result<()> {
242 // Releases the buffer for the `handle` when it is opened for nobody. While this is good
243 // for saving memory, this has a performance implication because we need to decompress
244 // again when the same file is opened in the future.
245 let mut open_files = self.open_files.lock().unwrap();
246 let handle = inode as Handle;
247 if let Some(ofb) = open_files.get_mut(&handle) {
248 if ofb.open_count.checked_sub(1).ok_or_else(ebadf)? == 0 {
249 open_files.remove(&handle);
250 }
251 Ok(())
252 } else {
253 Err(ebadf())
254 }
255 }
256
257 fn read<W: io::Write + ZeroCopyWriter>(
258 &self,
259 _ctx: Context,
260 _inode: Self::Inode,
261 handle: Self::Handle,
262 mut w: W,
263 size: u32,
264 offset: u64,
265 _lock_owner: Option<u64>,
266 _flags: u32,
267 ) -> io::Result<usize> {
268 let open_files = self.open_files.lock().unwrap();
269 let ofb = open_files.get(&handle).ok_or_else(ebadf)?;
270 if ofb.open_count == 0 {
271 return Err(ebadf());
272 }
273 let start = offset as usize;
274 let end = start + size as usize;
275 let end = std::cmp::min(end, ofb.buf.len());
276 let read_len = w.write(&ofb.buf[start..end])?;
277 Ok(read_len)
278 }
279
280 fn opendir(
281 &self,
282 _ctx: Context,
283 inode: Self::Inode,
284 _flags: u32,
285 ) -> io::Result<(Option<Self::Handle>, fuse::filesystem::OpenOptions)> {
286 let mut open_dirs = self.open_dirs.lock().unwrap();
287 let handle = inode as Handle;
288 if let Some(odb) = open_dirs.get_mut(&handle) {
289 if odb.open_count == 0 {
290 return Err(ebadf());
291 }
292 odb.open_count += 1;
293 } else {
294 let inode_data = self.find_inode(inode)?;
295 let directory = inode_data.get_directory().ok_or_else(ebadf)?;
296 let mut buf: Vec<(CString, DirectoryEntry)> = Vec::with_capacity(directory.len());
297 for (name, dir_entry) in directory.iter() {
298 let name = CString::new(name.as_bytes()).unwrap();
299 buf.push((name, dir_entry.clone()));
300 }
301 open_dirs.insert(handle, OpenDirBuf { open_count: 1, buf: buf.into_boxed_slice() });
302 }
Jiyong Park63a95cf2021-05-13 19:20:30 +0900303 Ok((Some(handle), fuse::filesystem::OpenOptions::CACHE_DIR))
Jiyong Park331d1ea2021-05-10 11:01:23 +0900304 }
305
306 fn releasedir(
307 &self,
308 _ctx: Context,
309 inode: Self::Inode,
310 _flags: u32,
311 _handle: Self::Handle,
312 ) -> io::Result<()> {
313 let mut open_dirs = self.open_dirs.lock().unwrap();
314 let handle = inode as Handle;
315 if let Some(odb) = open_dirs.get_mut(&handle) {
316 if odb.open_count.checked_sub(1).ok_or_else(ebadf)? == 0 {
317 open_dirs.remove(&handle);
318 }
319 Ok(())
320 } else {
321 Err(ebadf())
322 }
323 }
324
325 fn readdir(
326 &self,
327 _ctx: Context,
328 inode: Self::Inode,
329 _handle: Self::Handle,
330 size: u32,
331 offset: u64,
332 ) -> io::Result<Self::DirIter> {
333 let open_dirs = self.open_dirs.lock().unwrap();
334 let handle = inode as Handle;
335 let odb = open_dirs.get(&handle).ok_or_else(ebadf)?;
336 if odb.open_count == 0 {
337 return Err(ebadf());
338 }
339 let buf = &odb.buf;
340 let start = offset as usize;
Jiyong Park63a95cf2021-05-13 19:20:30 +0900341
342 // Estimate the size of each entry will take space in the buffer. See
343 // external/crosvm/fuse/src/server.rs#add_dirent
344 let mut estimate: usize = 0; // estimated number of bytes we will be writing
345 let mut end = start; // index in `buf`
346 while estimate < size as usize && end < buf.len() {
347 let dirent_size = size_of::<fuse::sys::Dirent>();
348 let name_size = buf[end].0.to_bytes().len();
349 estimate += (dirent_size + name_size + 7) & !7; // round to 8 byte boundary
350 end += 1;
351 }
352
Jiyong Park331d1ea2021-05-10 11:01:23 +0900353 let mut new_buf = Vec::with_capacity(end - start);
354 // The portion of `buf` is *copied* to the iterator. This is not ideal, but inevitable
355 // because the `name` field in `fuse::filesystem::DirEntry` is `&CStr` not `CString`.
356 new_buf.extend_from_slice(&buf[start..end]);
357 Ok(DirIter { inner: new_buf, offset, cur: 0 })
358 }
359}
360
361struct DirIter {
362 inner: Vec<(CString, DirectoryEntry)>,
363 offset: u64, // the offset where this iterator begins. `next` doesn't change this.
364 cur: usize, // the current index in `inner`. `next` advances this.
365}
366
367impl fuse::filesystem::DirectoryIterator for DirIter {
368 fn next(&mut self) -> Option<fuse::filesystem::DirEntry> {
369 if self.cur >= self.inner.len() {
370 return None;
371 }
372
373 let (name, entry) = &self.inner[self.cur];
374 self.cur += 1;
375 Some(fuse::filesystem::DirEntry {
376 ino: entry.inode as libc::ino64_t,
377 offset: self.offset + self.cur as u64,
378 type_: match entry.kind {
379 InodeKind::Directory => libc::DT_DIR.into(),
380 InodeKind::File => libc::DT_REG.into(),
381 },
382 name,
383 })
384 }
385}
386
387#[cfg(test)]
388mod tests {
389 use anyhow::{bail, Result};
390 use nix::sys::statfs::{statfs, FsType};
Jiyong Park63a95cf2021-05-13 19:20:30 +0900391 use std::collections::BTreeSet;
Jiyong Park331d1ea2021-05-10 11:01:23 +0900392 use std::fs;
393 use std::fs::File;
394 use std::io::Write;
395 use std::path::{Path, PathBuf};
396 use std::time::{Duration, Instant};
397 use zip::write::FileOptions;
398
399 #[cfg(not(target_os = "android"))]
400 fn start_fuse(zip_path: &Path, mnt_path: &Path) {
401 let zip_path = PathBuf::from(zip_path);
402 let mnt_path = PathBuf::from(mnt_path);
403 std::thread::spawn(move || {
Jiyong Park6a762db2021-05-31 14:00:52 +0900404 crate::run_fuse(&zip_path, &mnt_path, None).unwrap();
Jiyong Park331d1ea2021-05-10 11:01:23 +0900405 });
406 }
407
408 #[cfg(target_os = "android")]
409 fn start_fuse(zip_path: &Path, mnt_path: &Path) {
410 // Note: for some unknown reason, running a thread to serve fuse doesn't work on Android.
411 // Explicitly spawn a zipfuse process instead.
412 // TODO(jiyong): fix this
413 assert!(std::process::Command::new("sh")
414 .arg("-c")
415 .arg(format!("/data/local/tmp/zipfuse {} {}", zip_path.display(), mnt_path.display()))
416 .spawn()
417 .is_ok());
418 }
419
420 fn wait_for_mount(mount_path: &Path) -> Result<()> {
421 let start_time = Instant::now();
422 const POLL_INTERVAL: Duration = Duration::from_millis(50);
423 const TIMEOUT: Duration = Duration::from_secs(10);
424 const FUSE_SUPER_MAGIC: FsType = FsType(0x65735546);
425 loop {
426 if statfs(mount_path)?.filesystem_type() == FUSE_SUPER_MAGIC {
427 break;
428 }
429
430 if start_time.elapsed() > TIMEOUT {
431 bail!("Time out mounting zipfuse");
432 }
433 std::thread::sleep(POLL_INTERVAL);
434 }
435 Ok(())
436 }
437
438 // Creates a zip file, adds some files to the zip file, mounts it using zipfuse, runs the check
439 // routine, and finally unmounts.
440 fn run_test(add: fn(&mut zip::ZipWriter<File>), check: fn(&std::path::Path)) {
441 // Create an empty zip file
442 let test_dir = tempfile::TempDir::new().unwrap();
443 let zip_path = test_dir.path().join("test.zip");
444 let zip = File::create(&zip_path);
445 assert!(zip.is_ok());
446 let mut zip = zip::ZipWriter::new(zip.unwrap());
447
448 // Let test users add files/dirs to the zip file
449 add(&mut zip);
450 assert!(zip.finish().is_ok());
451 drop(zip);
452
453 // Mount the zip file on the "mnt" dir using zipfuse.
454 let mnt_path = test_dir.path().join("mnt");
455 assert!(fs::create_dir(&mnt_path).is_ok());
456
457 start_fuse(&zip_path, &mnt_path);
458
459 let mnt_path = test_dir.path().join("mnt");
460 // Give some time for the fuse to boot up
461 assert!(wait_for_mount(&mnt_path).is_ok());
462 // Run the check routine, and do the clean up.
463 check(&mnt_path);
464 assert!(nix::mount::umount2(&mnt_path, nix::mount::MntFlags::empty()).is_ok());
465 }
466
467 fn check_file(root: &Path, file: &str, content: &[u8]) {
468 let path = root.join(file);
469 assert!(path.exists());
470
471 let metadata = fs::metadata(&path);
472 assert!(metadata.is_ok());
473
474 let metadata = metadata.unwrap();
475 assert!(metadata.is_file());
476 assert_eq!(content.len(), metadata.len() as usize);
477
478 let read_data = fs::read(&path);
479 assert!(read_data.is_ok());
480 assert_eq!(content, read_data.unwrap().as_slice());
481 }
482
Jiyong Park63a95cf2021-05-13 19:20:30 +0900483 fn check_dir<S: AsRef<str>>(root: &Path, dir: &str, files: &[S], dirs: &[S]) {
Jiyong Park331d1ea2021-05-10 11:01:23 +0900484 let dir_path = root.join(dir);
485 assert!(dir_path.exists());
486
487 let metadata = fs::metadata(&dir_path);
488 assert!(metadata.is_ok());
489
490 let metadata = metadata.unwrap();
491 assert!(metadata.is_dir());
492
493 let iter = fs::read_dir(&dir_path);
494 assert!(iter.is_ok());
495
496 let iter = iter.unwrap();
Jiyong Park63a95cf2021-05-13 19:20:30 +0900497 let mut actual_files = BTreeSet::new();
498 let mut actual_dirs = BTreeSet::new();
Jiyong Park331d1ea2021-05-10 11:01:23 +0900499 for de in iter {
500 let entry = de.unwrap();
501 let path = entry.path();
502 if path.is_dir() {
503 actual_dirs.insert(path.strip_prefix(&dir_path).unwrap().to_path_buf());
504 } else {
505 actual_files.insert(path.strip_prefix(&dir_path).unwrap().to_path_buf());
506 }
507 }
Jiyong Park63a95cf2021-05-13 19:20:30 +0900508 let expected_files: BTreeSet<PathBuf> =
509 files.iter().map(|s| PathBuf::from(s.as_ref())).collect();
510 let expected_dirs: BTreeSet<PathBuf> =
511 dirs.iter().map(|s| PathBuf::from(s.as_ref())).collect();
Jiyong Park331d1ea2021-05-10 11:01:23 +0900512
513 assert_eq!(expected_files, actual_files);
514 assert_eq!(expected_dirs, actual_dirs);
515 }
516
517 #[test]
518 fn empty() {
519 run_test(
520 |_| {},
521 |root| {
Jiyong Park63a95cf2021-05-13 19:20:30 +0900522 check_dir::<String>(root, "", &[], &[]);
Jiyong Park331d1ea2021-05-10 11:01:23 +0900523 },
524 );
525 }
526
527 #[test]
528 fn single_file() {
529 run_test(
530 |zip| {
531 zip.start_file("foo", FileOptions::default()).unwrap();
532 zip.write_all(b"0123456789").unwrap();
533 },
534 |root| {
535 check_dir(root, "", &["foo"], &[]);
536 check_file(root, "foo", b"0123456789");
537 },
538 );
539 }
540
541 #[test]
542 fn single_dir() {
543 run_test(
544 |zip| {
545 zip.add_directory("dir", FileOptions::default()).unwrap();
546 },
547 |root| {
548 check_dir(root, "", &[], &["dir"]);
Jiyong Park63a95cf2021-05-13 19:20:30 +0900549 check_dir::<String>(root, "dir", &[], &[]);
Jiyong Park331d1ea2021-05-10 11:01:23 +0900550 },
551 );
552 }
553
554 #[test]
555 fn complex_hierarchy() {
556 // root/
557 // a/
558 // b1/
559 // b2/
560 // c1 (file)
561 // c2/
562 // d1 (file)
563 // d2 (file)
564 // d3 (file)
565 // x/
566 // y1 (file)
567 // y2 (file)
568 // y3/
569 //
570 // foo (file)
571 // bar (file)
572 run_test(
573 |zip| {
574 let opt = FileOptions::default();
575 zip.add_directory("a/b1", opt).unwrap();
576
577 zip.start_file("a/b2/c1", opt).unwrap();
578
579 zip.start_file("a/b2/c2/d1", opt).unwrap();
580 zip.start_file("a/b2/c2/d2", opt).unwrap();
581 zip.start_file("a/b2/c2/d3", opt).unwrap();
582
583 zip.start_file("x/y1", opt).unwrap();
584 zip.start_file("x/y2", opt).unwrap();
585 zip.add_directory("x/y3", opt).unwrap();
586
587 zip.start_file("foo", opt).unwrap();
588 zip.start_file("bar", opt).unwrap();
589 },
590 |root| {
591 check_dir(root, "", &["foo", "bar"], &["a", "x"]);
592 check_dir(root, "a", &[], &["b1", "b2"]);
Jiyong Park63a95cf2021-05-13 19:20:30 +0900593 check_dir::<String>(root, "a/b1", &[], &[]);
Jiyong Park331d1ea2021-05-10 11:01:23 +0900594 check_dir(root, "a/b2", &["c1"], &["c2"]);
595 check_dir(root, "a/b2/c2", &["d1", "d2", "d3"], &[]);
596 check_dir(root, "x", &["y1", "y2"], &["y3"]);
Jiyong Park63a95cf2021-05-13 19:20:30 +0900597 check_dir::<String>(root, "x/y3", &[], &[]);
Jiyong Park331d1ea2021-05-10 11:01:23 +0900598 check_file(root, "a/b2/c1", &[]);
599 check_file(root, "a/b2/c2/d1", &[]);
600 check_file(root, "a/b2/c2/d2", &[]);
601 check_file(root, "a/b2/c2/d3", &[]);
602 check_file(root, "x/y1", &[]);
603 check_file(root, "x/y2", &[]);
604 check_file(root, "foo", &[]);
605 check_file(root, "bar", &[]);
606 },
607 );
608 }
609
610 #[test]
611 fn large_file() {
612 run_test(
613 |zip| {
614 let data = vec![10; 2 << 20];
615 zip.start_file("foo", FileOptions::default()).unwrap();
616 zip.write_all(&data).unwrap();
617 },
618 |root| {
619 let data = vec![10; 2 << 20];
620 check_file(root, "foo", &data);
621 },
622 );
623 }
Jiyong Park63a95cf2021-05-13 19:20:30 +0900624
625 #[test]
626 fn large_dir() {
627 const NUM_FILES: usize = 1 << 10;
628 run_test(
629 |zip| {
630 let opt = FileOptions::default();
631 // create 1K files. Each file has a name of length 100. So total size is at least
632 // 100KB, which is bigger than the readdir buffer size of 4K.
633 for i in 0..NUM_FILES {
634 zip.start_file(format!("dir/{:0100}", i), opt).unwrap();
635 }
636 },
637 |root| {
638 let dirs_expected: Vec<_> = (0..NUM_FILES).map(|i| format!("{:0100}", i)).collect();
639 check_dir(
640 root,
641 "dir",
642 dirs_expected.iter().map(|s| s.as_str()).collect::<Vec<&str>>().as_slice(),
643 &[],
644 );
645 },
646 );
647 }
Jiyong Parkd40f7bb2021-05-17 10:55:56 +0900648
Jiyong Parke6587ca2021-05-17 14:42:23 +0900649 fn run_fuse_and_check_test_zip(test_dir: &Path, zip_path: &Path) {
650 let mnt_path = test_dir.join("mnt");
Jiyong Parkd40f7bb2021-05-17 10:55:56 +0900651 assert!(fs::create_dir(&mnt_path).is_ok());
652
Chris Wailes68c39f82021-07-27 16:03:44 -0700653 start_fuse(zip_path, &mnt_path);
Jiyong Parkd40f7bb2021-05-17 10:55:56 +0900654
655 // Give some time for the fuse to boot up
656 assert!(wait_for_mount(&mnt_path).is_ok());
657
658 check_dir(&mnt_path, "", &[], &["dir"]);
659 check_dir(&mnt_path, "dir", &["file1", "file2"], &[]);
660 check_file(&mnt_path, "dir/file1", include_bytes!("../testdata/dir/file1"));
661 check_file(&mnt_path, "dir/file2", include_bytes!("../testdata/dir/file2"));
662 assert!(nix::mount::umount2(&mnt_path, nix::mount::MntFlags::empty()).is_ok());
663 }
Jiyong Parke6587ca2021-05-17 14:42:23 +0900664
665 #[test]
666 fn supports_deflate() {
667 let test_dir = tempfile::TempDir::new().unwrap();
668 let zip_path = test_dir.path().join("test.zip");
669 let mut zip_file = File::create(&zip_path).unwrap();
670 zip_file.write_all(include_bytes!("../testdata/test.zip")).unwrap();
671
Chris Wailes68c39f82021-07-27 16:03:44 -0700672 run_fuse_and_check_test_zip(test_dir.path(), &zip_path);
Jiyong Parke6587ca2021-05-17 14:42:23 +0900673 }
674
675 #[cfg(not(target_os = "android"))] // Android doesn't have the loopdev crate
676 #[test]
677 fn supports_zip_on_block_device() {
678 // Write test.zip to the test directory
679 let test_dir = tempfile::TempDir::new().unwrap();
680 let zip_path = test_dir.path().join("test.zip");
681 let mut zip_file = File::create(&zip_path).unwrap();
682 let data = include_bytes!("../testdata/test.zip");
683 zip_file.write_all(data).unwrap();
684
685 // Pad 0 to test.zip so that its size is multiple of 4096.
686 const BLOCK_SIZE: usize = 4096;
687 let size = (data.len() + BLOCK_SIZE) & !BLOCK_SIZE;
688 let pad_size = size - data.len();
689 assert!(pad_size != 0);
690 let pad = vec![0; pad_size];
691 zip_file.write_all(pad.as_slice()).unwrap();
692 drop(zip_file);
693
694 // Attach test.zip to a loop device
695 let lc = loopdev::LoopControl::open().unwrap();
696 let ld = scopeguard::guard(lc.next_free().unwrap(), |ld| {
697 ld.detach().unwrap();
698 });
699 ld.attach_file(&zip_path).unwrap();
700
701 // Start zipfuse over to the loop device (not the zip file)
702 run_fuse_and_check_test_zip(&test_dir.path(), &ld.path().unwrap());
703 }
Jiyong Park331d1ea2021-05-10 11:01:23 +0900704}