Skip to content

Commit

Permalink
feat(zip): ZipFS support
Browse files Browse the repository at this point in the history
  • Loading branch information
manuel-woelker committed Apr 28, 2024
1 parent 422a663 commit 57c076f
Show file tree
Hide file tree
Showing 4 changed files with 275 additions and 0 deletions.
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,15 @@ futures = {version = "0.3.28", optional = true}
async-recursion = {version = "1.0.5", optional = true}
filetime = "0.2.23"
camino = { version = "1.0.5", optional = true }
zip = { version = "1.1.1", optional = true }
ouroboros = "0.18.3"

[dev-dependencies]
uuid = { version = "=0.8.1", features = ["v4"] }
camino = "1.0.5"
anyhow = "1.0.58"
tokio-test = "0.4.3"
walkdir = { version = "2.5.0" }

[features]
embedded-fs = ["rust-embed"]
Expand Down
19 changes: 19 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
use std::{error, fmt, io};

#[cfg(feature="zip")]
use zip::result::ZipError;

/// The error type of this crate
#[derive(Debug)]
pub struct VfsError {
Expand Down Expand Up @@ -52,6 +55,22 @@ impl From<io::Error> for VfsError {
}
}

#[cfg(feature="zip")]
impl From<ZipError> for VfsError {
fn from(err: ZipError) -> Self {
VfsError::from(match err {
ZipError::Io(err) => VfsErrorKind::IoError(err),
ZipError::InvalidArchive(str) => VfsErrorKind::Other(format!("Invalid Archive: {str}")),
ZipError::UnsupportedArchive(str) => {
VfsErrorKind::Other(format!("Invalid Archive: {str}"))
}
ZipError::FileNotFound => VfsErrorKind::FileNotFound,
ZipError::InvalidPassword => VfsErrorKind::Other("Invalid Password".to_string()),
_ => VfsErrorKind::Other("Unknown error".to_string()),
})
}
}

impl VfsError {
// Path filled by the VFS crate rather than the implementations
pub(crate) fn with_path(mut self, path: impl Into<String>) -> Self {
Expand Down
2 changes: 2 additions & 0 deletions src/impls/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ pub mod embedded;
pub mod memory;
pub mod overlay;
pub mod physical;
#[cfg(feature="zip")]
pub mod zip;
251 changes: 251 additions & 0 deletions src/impls/zip.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
use std::collections::HashSet;
use crate::error::VfsErrorKind;
use crate::{FileSystem, SeekAndRead, SeekAndWrite, VfsError, VfsFileType, VfsMetadata, VfsResult};
use ouroboros::self_referencing;
use std::fmt::{Debug, Formatter};
use std::io::{Read, Seek, SeekFrom};
use zip::read::ZipFile;
use zip::ZipArchive;

/// a read-only file system view of a [ZIP archive file](https://en.wikipedia.org/wiki/ZIP_(file_format))
pub struct ZipFS {
create_fn: Box<dyn Fn() -> Box<dyn SeekAndReadAndSend> + Send + Sync>,
}

impl ZipFS {
pub fn new<T: SeekAndReadAndSend + 'static, F: (Fn() -> T) + Send + Sync + 'static>(
f: F,
) -> Self {
ZipFS {
create_fn: Box::new(move || Box::new(f())),
}
}

fn resolve_path(path: &str) -> String {
let mut path = path.to_string();
if path.starts_with("/") {
path.remove(0);
}
path
}

fn open_archive(&self) -> VfsResult<ZipArchive<Box<dyn SeekAndReadAndSend>>> {
let reader = (self.create_fn)();
let archive = ZipArchive::new(reader)?;
Ok(archive)
}
}

impl Debug for ZipFS {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "ZipFS")?;
Ok(())
}
}

pub trait SeekAndReadAndSend: Seek + Read + Send {}

impl<T> SeekAndReadAndSend for T where T: Seek + Read + Send {}

#[self_referencing]
struct SeekableZipFile {
archive: ZipArchive<Box<dyn SeekAndReadAndSend>>,

#[borrows(mut archive)]
#[not_covariant]
zip_file: ZipFile<'this>,
}

impl Read for SeekableZipFile {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.with_zip_file_mut(|zip_file| zip_file.read(buf))
}
}

impl Seek for SeekableZipFile {
fn seek(&mut self, _pos: SeekFrom) -> std::io::Result<u64> {
Err(std::io::Error::other(VfsError::from(
VfsErrorKind::NotSupported,
)))
}
}

// Should be safe since the input to the archive is Send
unsafe impl Send for SeekableZipFile {}

impl FileSystem for ZipFS {
fn read_dir(&self, path: &str) -> VfsResult<Box<dyn Iterator<Item = String> + Send>> {
let mut resolved_path = Self::resolve_path(path);
if resolved_path != "" {
resolved_path += "/";
}
let mut archive = self.open_archive()?;
let mut entries = HashSet::<String>::new();
let size = archive.len();
for i in 0..size {
let file = archive.by_index(i)?;
if let Some(rest) = file.name().strip_prefix(&resolved_path) {
if rest == "" {
continue;
}
if let Some((entry, _)) = rest.split_once("/") {
if entry == "" {
continue;
}
entries.insert(entry.to_string());
} else {
entries.insert(rest.to_string());
}
}
}
if entries.is_empty() {
// Maybe directory does not exist
if !self.exists(&path)? {
return Err(VfsError::from(VfsErrorKind::FileNotFound))
}
}
Ok(Box::new(entries.into_iter()))
}

fn create_dir(&self, _path: &str) -> VfsResult<()> {
Err(VfsErrorKind::NotSupported.into())
}

fn open_file(&self, path: &str) -> VfsResult<Box<dyn SeekAndRead + Send>> {
let mut archive = self.open_archive()?;
let path = Self::resolve_path(path);
// return Ok(Box::new(Cursor::new(vec![])));
archive.by_name(&path)?;
let file = SeekableZipFileBuilder {
archive: archive,
zip_file_builder: |archive: &mut ZipArchive<Box<dyn SeekAndReadAndSend>>| {
archive.by_name(&path).unwrap()
},
}
.build();
Ok(Box::new(file))
}

fn create_file(&self, _path: &str) -> VfsResult<Box<dyn SeekAndWrite + Send>> {
Err(VfsErrorKind::NotSupported.into())
}

fn append_file(&self, _path: &str) -> VfsResult<Box<dyn SeekAndWrite + Send>> {
Err(VfsErrorKind::NotSupported.into())
}

fn metadata(&self, path: &str) -> VfsResult<VfsMetadata> {
if path == "" {
return Ok(VfsMetadata {
file_type: VfsFileType::Directory,
len: 0,
modified: None,
created: None,
accessed: None,
});
}
let mut archive = self.open_archive()?;
let path = Self::resolve_path(path);
let zipfile = {
let mut result = archive.by_name(&path);
if result.is_err() {
drop(result);
result = archive.by_name(&(path + "/"));
}
result
}?;
Ok(VfsMetadata {
file_type: if zipfile.is_dir() {
VfsFileType::Directory
} else {
VfsFileType::File
},
len: zipfile.size(),
modified: None,
created: None,
accessed: None,
})
}

fn exists(&self, path: &str) -> VfsResult<bool> {
if path == "" {
return Ok(true);
}
let mut archive = self.open_archive()?;
let path = Self::resolve_path(path);
let zipfile = archive.by_name(&path);
if zipfile.is_err() {
drop(zipfile);
let zipfile = archive.by_name(&(path + "/"));
return Ok(zipfile.is_ok());
}
Ok(true)
}

fn remove_file(&self, _path: &str) -> VfsResult<()> {
Err(VfsErrorKind::NotSupported.into())
}

fn remove_dir(&self, _path: &str) -> VfsResult<()> {
Err(VfsErrorKind::NotSupported.into())
}
}

#[cfg(test)]
mod tests {
use super::*;
use ::zip::write::SimpleFileOptions;
use ::zip::ZipWriter;
use std::fs;
use std::fs::File;
use std::io::Cursor;
use std::sync::Arc;
use walkdir::WalkDir;

#[derive(Clone)]
pub struct LargeData {
data: Arc<Vec<u8>>,
}

impl AsRef<[u8]> for LargeData {
fn as_ref(&self) -> &[u8] {
&self.data
}
}

impl LargeData {
fn open_for_read(&self) -> Cursor<LargeData> {
Cursor::new(self.clone())
}
}

fn get_test_fs() -> ZipFS {
let mut buf = Vec::new();
let mut zip = ZipWriter::new(std::io::Cursor::new(&mut buf));
let base_path = "test/test_directory";
for entry in WalkDir::new(base_path) {
let entry = entry.unwrap();
let path = entry.path().strip_prefix(base_path).unwrap().to_str().unwrap().to_string().replace("\\", "/");
if fs::metadata(entry.path()).unwrap().is_dir() {
let options =
SimpleFileOptions::default().compression_method(zip::CompressionMethod::Zstd);
zip.add_directory(path, options).unwrap();
continue;
}
let options =
SimpleFileOptions::default().compression_method(zip::CompressionMethod::Zstd);
zip.start_file(path, options)
.unwrap();
let mut file = File::open(entry.path()).unwrap();
std::io::copy(&mut file, &mut zip).unwrap();
}
zip.finish().unwrap();
drop(zip);
let data = LargeData {
data: Arc::new(buf),
};
ZipFS::new(move || data.open_for_read())
}

test_vfs_readonly!({ get_test_fs() });
}

0 comments on commit 57c076f

Please sign in to comment.