use std::collections::BTreeMap;
use std::io::Write;
use std::path::{Path, PathBuf};
use zip::write::{SimpleFileOptions, ZipWriter};
use zip::{CompressionMethod, DateTime};
const FIXED_COMPRESSION_LEVEL: i64 = 6;
pub fn build_deterministic_zip(root: &Path) -> Vec<u8> {
let mut files: BTreeMap<String, PathBuf> = BTreeMap::new();
let mut dirs: Vec<PathBuf> = Vec::new();
collect(root, root, &mut files, &mut dirs);
pack(&files)
}
pub fn collect(
root: &Path,
current: &Path,
files: &mut BTreeMap<String, PathBuf>,
dirs: &mut Vec<PathBuf>,
) {
let read = std::fs::read_dir(current)
.unwrap_or_else(|e| panic!("failed to read dir {}: {e}", current.display()));
let mut entries: Vec<PathBuf> = read
.map(|e| {
e.unwrap_or_else(|err| panic!("failed to read dir entry in {}: {err}", current.display()))
.path()
})
.collect();
entries.sort();
for path in entries {
let file_type = std::fs::symlink_metadata(&path)
.unwrap_or_else(|e| panic!("failed to stat {}: {e}", path.display()))
.file_type();
if file_type.is_symlink() {
continue;
}
if file_type.is_dir() {
dirs.push(path.clone());
collect(root, &path, files, dirs);
} else if file_type.is_file() {
let rel = path
.strip_prefix(root)
.unwrap_or_else(|e| panic!("strip_prefix failed for {}: {e}", path.display()))
.to_string_lossy()
.replace('\\', "/");
files.insert(rel, path);
}
}
}
fn pack(files: &BTreeMap<String, PathBuf>) -> Vec<u8> {
let buf = std::io::Cursor::new(Vec::<u8>::new());
let mut zw = ZipWriter::new(buf);
let options = SimpleFileOptions::default()
.compression_method(CompressionMethod::Deflated)
.compression_level(Some(FIXED_COMPRESSION_LEVEL))
.last_modified_time(DateTime::default())
.unix_permissions(0o644);
for (entry_name, abs_path) in files {
let bytes = std::fs::read(abs_path)
.unwrap_or_else(|e| panic!("failed to read {}: {e}", abs_path.display()));
zw.start_file(entry_name.as_str(), options)
.unwrap_or_else(|e| panic!("failed to start zip entry {entry_name}: {e}"));
zw.write_all(&bytes)
.unwrap_or_else(|e| panic!("failed to write zip entry {entry_name}: {e}"));
}
let cursor = zw
.finish()
.unwrap_or_else(|e| panic!("failed to finish zip: {e}"));
cursor.into_inner()
}