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 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`. 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 101 fn ebadf() -> io::Error { 102 io::Error::from_raw_os_error(libc::EBADF) 103 } 104 105 fn timeout_max() -> std::time::Duration { 106 std::time::Duration::new(u64::MAX, 1_000_000_000 - 1) 107 } 108 109 impl 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 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)] 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 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 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 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 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 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 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 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 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 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 { 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"))] 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")] 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 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. 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 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 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] 505 fn empty() { 506 run_test( 507 |_| {}, 508 |root| { 509 check_dir::<String>(root, "", &[], &[]); 510 }, 511 ); 512 } 513 514 #[test] 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] 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] 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] 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] 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