diff --git a/.gitignore b/.gitignore index 2f05f19..3e83fe1 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ target # rustc will dump stack traces when hitting an internal compiler error to PWD rustc-ice-*.txt +todo.md diff --git a/Cargo.lock b/Cargo.lock index 7298829..13d4ea4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -806,6 +806,7 @@ dependencies = [ "futures-util", "http", "mime_guess", + "rc-zip", "rc-zip-tokio", "tokio", "tokio-util", diff --git a/demo/assets/demo.zip b/demo/assets/demo.zip new file mode 100644 index 0000000..0246f3e Binary files /dev/null and b/demo/assets/demo.zip differ diff --git a/serve_zip/Cargo.toml b/serve_zip/Cargo.toml index 4df0928..399f2e3 100644 --- a/serve_zip/Cargo.toml +++ b/serve_zip/Cargo.toml @@ -13,6 +13,7 @@ futures-util = "0.3.32" http = "1.4.0" mime_guess = "2.0.5" +rc-zip = "5.4.1" rc-zip-tokio = "4.3.1" diff --git a/serve_zip/src/index.rs b/serve_zip/src/index.rs new file mode 100644 index 0000000..efc7337 --- /dev/null +++ b/serve_zip/src/index.rs @@ -0,0 +1,92 @@ +use rc_zip::parse::EntryKind; +use rc_zip_tokio::ReadZip; +use std::collections::HashMap; +use std::sync::Arc; + +pub struct EntryMeta { + pub size: u64, + pub is_dir: bool, +} + +pub struct Entries { + pub entries: HashMap, +} + +impl Entries { + pub async fn new(data: &[u8]) -> Result, rc_zip::Error> { + // The only thing zip cares about is can it be read as a zip file? Does it have a Cursor? + let records = data.read_zip().await?; + let mut entries = HashMap::::new(); + for record in records.entries() { + let name = record.sanitized_name(); + if name.is_none() { + continue; + } + + let kind = record.kind(); + if kind == EntryKind::Symlink { + continue; + } + + let name = name.unwrap(); + entries.insert( + name.to_string(), + EntryMeta { + is_dir: kind == EntryKind::Directory, + size: record.uncompressed_size, + }, + ); + } + Ok(Arc::new(Entries { entries })) + } + + pub fn get(&self, path: &str) -> Option<&EntryMeta> { + path.strip_prefix('/') + .and_then(|stripped| self.entries.get(stripped)) + .or_else(|| self.entries.get(path)) + } +} + +#[cfg(test)] +mod tests { + const ZIPPATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../demo/assets/demo.zip"); + + use super::Entries; + use crate::source::ZipSource; + + #[tokio::test] + async fn finds_zip_file_by_zip_path() { + let zipfile = ZipSource::File(ZIPPATH.into()); + let archive = zipfile.load().await.unwrap(); + let entries = Entries::new(&archive).await.unwrap(); + let index = entries.get("index.html"); + assert!(index.is_some()); + let indexfile = index.unwrap(); + assert!(indexfile.size == 150); + } + + #[tokio::test] + async fn finds_zip_file_with_rooted_path() { + let zipfile = ZipSource::File(ZIPPATH.into()); + let archive = zipfile.load().await.unwrap(); + let entries = Entries::new(&archive).await.unwrap(); + assert!(entries.get("/index.html").is_some()); + } + + #[tokio::test] + async fn returns_none_on_bad_filename() { + let zipfile = ZipSource::File(ZIPPATH.into()); + let archive = zipfile.load().await.unwrap(); + let entries = Entries::new(&archive).await.unwrap(); + assert!(entries.get("index.garbage").is_none()); + } + + #[tokio::test] + async fn returns_isdir_when_expected() { + let zipfile = ZipSource::File(ZIPPATH.into()); + let archive = zipfile.load().await.unwrap(); + let entries = Entries::new(&archive).await.unwrap(); + assert!(entries.get("docs/").is_some()); + assert!(entries.get("docs/").unwrap().is_dir); + } +} diff --git a/serve_zip/src/lib.rs b/serve_zip/src/lib.rs index b93cf3f..7f9c127 100644 --- a/serve_zip/src/lib.rs +++ b/serve_zip/src/lib.rs @@ -1,14 +1,2 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +pub mod index; +pub mod source; diff --git a/serve_zip/src/source.rs b/serve_zip/src/source.rs new file mode 100644 index 0000000..f02ad9b --- /dev/null +++ b/serve_zip/src/source.rs @@ -0,0 +1,20 @@ +use std::path::PathBuf; +use std::sync::Arc; + +#[derive(Clone, Debug)] +pub enum ZipSource { + Local(&'static [u8]), + File(PathBuf), +} + +impl ZipSource { + pub async fn load(self) -> Result, std::io::Error> { + match self { + ZipSource::Local(bytes) => Ok(Arc::from(bytes)), + ZipSource::File(path) => { + let bytes = tokio::fs::read(&path).await?; + Ok(Arc::from(bytes.as_slice())) + } + } + } +}