#[path = "../build_zip.rs"]
mod build_zip;
use std::path::{Path, PathBuf};
fn scripts_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("pasta_scripts")
}
fn copy_tree(src: &Path, dst: &Path) {
std::fs::create_dir_all(dst).expect("create dst dir");
for entry in std::fs::read_dir(src).expect("read src dir") {
let entry = entry.expect("dir entry");
let ty = entry.file_type().expect("file type");
let from = entry.path();
let to = dst.join(entry.file_name());
if ty.is_dir() {
copy_tree(&from, &to);
} else if ty.is_file() {
std::fs::copy(&from, &to).expect("copy file");
}
}
}
fn md5_hex(bytes: &[u8]) -> String {
format!("{:x}", md5::compute(bytes))
}
#[test]
fn zip_is_byte_and_md5_deterministic() {
let root = scripts_root();
assert!(root.is_dir(), "pasta_scripts source not found: {}", root.display());
let first = build_zip::build_deterministic_zip(&root);
let second = build_zip::build_deterministic_zip(&root);
assert_eq!(first, second, "two builds from same source produced different bytes");
assert_eq!(
md5_hex(&first),
md5_hex(&second),
"two builds from same source produced different MD5"
);
assert!(!first.is_empty(), "generated zip is unexpectedly empty");
}
#[test]
fn zip_md5_matches_embedded_expected() {
let zip = build_zip::build_deterministic_zip(&scripts_root());
let actual = md5_hex(&zip);
let expected = env!("PASTA_SCRIPTS_MD5");
assert_eq!(
actual, expected,
"generated zip MD5 diverged from embedded PASTA_SCRIPTS_MD5 \
(refactor must be byte-preserving)"
);
}
#[test]
fn md5_changes_when_a_single_file_changes() {
let root = scripts_root();
let original_md5 = md5_hex(&build_zip::build_deterministic_zip(&root));
let tmp = tempfile::tempdir().expect("create tempdir");
let copy_root = tmp.path().join("pasta_scripts");
copy_tree(&root, ©_root);
let copy_md5 = md5_hex(&build_zip::build_deterministic_zip(©_root));
assert_eq!(
copy_md5, original_md5,
"verbatim copy of source produced a different MD5"
);
let mut target: Option<PathBuf> = None;
find_first_file(©_root, &mut target);
let target = target.expect("no file found in copied tree to mutate");
let mut content = std::fs::read(&target).expect("read target");
content.push(b'\n');
std::fs::write(&target, &content).expect("write mutated target");
let mutated_md5 = md5_hex(&build_zip::build_deterministic_zip(©_root));
assert_ne!(
mutated_md5, original_md5,
"MD5 did not change after mutating a single file ({})",
target.display()
);
}
fn zip_entry_names(zip_bytes: &[u8]) -> Vec<String> {
let reader = std::io::Cursor::new(zip_bytes.to_vec());
let mut archive = zip::ZipArchive::new(reader).expect("open generated zip");
let mut names = Vec::with_capacity(archive.len());
for i in 0..archive.len() {
let entry = archive.by_index(i).expect("read zip entry");
names.push(entry.name().to_string());
}
names
}
#[test]
fn legacy_luasocket_debug_assets_absent_from_zip() {
let zip = build_zip::build_deterministic_zip(&scripts_root());
let names = zip_entry_names(&zip);
let removed = [
"vscode-debuggee.lua",
"socket/core.dll",
"mime/core.dll",
"dkjson.lua",
];
for asset in removed {
assert!(
!names.contains(&asset.to_string()),
"removed legacy luasocket debug asset still present in embedded zip: {asset}\n\
zip entries: {names:?}"
);
}
}
fn find_first_file(dir: &Path, out: &mut Option<PathBuf>) {
if out.is_some() {
return;
}
let mut entries: Vec<PathBuf> = std::fs::read_dir(dir)
.expect("read dir")
.map(|e| e.expect("entry").path())
.collect();
entries.sort();
for path in entries {
if out.is_some() {
return;
}
let ty = std::fs::symlink_metadata(&path).expect("stat").file_type();
if ty.is_dir() {
find_first_file(&path, out);
} else if ty.is_file() {
*out = Some(path);
return;
}
}
}