• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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