use anyhow::{Context, Result};
use std::path::Path;
pub fn extract_project_zip(zip_path: &Path, dest_dir: &Path) -> Result<()> {
let file = std::fs::File::open(zip_path).with_context(|| format!("Failed to open {}", zip_path.display()))?;
let mut archive = zip::ZipArchive::new(file).context("Failed to read project source zip")?;
for index in 0..archive.len() {
let mut entry = archive
.by_index(index)
.with_context(|| format!("Failed to read zip entry #{index}"))?;
let Some(relative_path) = entry.enclosed_name() else {
anyhow::bail!("Zip entry #{index} has an unsafe or invalid path: {:?}", entry.name());
};
let out_path = dest_dir.join(relative_path);
if entry.is_dir() {
std::fs::create_dir_all(&out_path)
.with_context(|| format!("Failed to create directory {}", out_path.display()))?;
continue;
}
if let Some(parent) = out_path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("Failed to create directory {}", parent.display()))?;
}
let mut out_file =
std::fs::File::create(&out_path).with_context(|| format!("Failed to create {}", out_path.display()))?;
std::io::copy(&mut entry, &mut out_file).with_context(|| format!("Failed to write {}", out_path.display()))?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::tempdir;
fn build_zip_with_entries(entries: &[(&str, &[u8])]) -> Vec<u8> {
let mut bytes = Vec::new();
{
let mut zip = zip::ZipWriter::new(std::io::Cursor::new(&mut bytes));
let options = zip::write::SimpleFileOptions::default();
for (name, contents) in entries {
zip.start_file(*name, options).unwrap();
zip.write_all(contents).unwrap();
}
zip.finish().unwrap();
}
bytes
}
#[test]
fn extract_project_zip_round_trips_a_nested_projects_files() {
let zip_bytes = build_zip_with_entries(&[
("src/main.rs", b"fn main() {}"),
("Cargo.toml", b"[package]\nname=\"x\"\n"),
]);
let src_dir = tempdir().unwrap();
let zip_path = src_dir.path().join("out.zip");
std::fs::write(&zip_path, &zip_bytes).unwrap();
let dest_dir = tempdir().unwrap();
extract_project_zip(&zip_path, dest_dir.path()).unwrap();
assert_eq!(
std::fs::read_to_string(dest_dir.path().join("src/main.rs")).unwrap(),
"fn main() {}"
);
assert_eq!(
std::fs::read_to_string(dest_dir.path().join("Cargo.toml")).unwrap(),
"[package]\nname=\"x\"\n"
);
}
#[test]
fn extract_project_zip_errors_on_a_missing_zip_file() {
let dest_dir = tempdir().unwrap();
let err = extract_project_zip(&dest_dir.path().join("missing.zip"), dest_dir.path()).unwrap_err();
assert!(err.to_string().contains("Failed to open"));
}
}