Hey, some tests are passing. I can open a zip file and check it for validity, presence, file size, and kind.

This commit is contained in:
Elf M. Sternberg 2026-05-10 12:10:49 -07:00
parent 841fa7e91e
commit e85da1cf34
7 changed files with 117 additions and 14 deletions

1
.gitignore vendored
View File

@ -9,3 +9,4 @@ target
# rustc will dump stack traces when hitting an internal compiler error to PWD
rustc-ice-*.txt
todo.md

1
Cargo.lock generated
View File

@ -806,6 +806,7 @@ dependencies = [
"futures-util",
"http",
"mime_guess",
"rc-zip",
"rc-zip-tokio",
"tokio",
"tokio-util",

BIN
demo/assets/demo.zip Normal file

Binary file not shown.

View File

@ -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"

92
serve_zip/src/index.rs Normal file
View File

@ -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<String, EntryMeta>,
}
impl Entries {
pub async fn new(data: &[u8]) -> Result<Arc<Self>, 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::<String, EntryMeta>::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);
}
}

View File

@ -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;

20
serve_zip/src/source.rs Normal file
View File

@ -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<Arc<[u8]>, 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()))
}
}
}
}