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