fix(windows): install version-folder wrapped portable ZIP releases

This commit is contained in:
Hasan Tan committed 2026-10-22 16:27:11 +11:00
1 parent bf39e92f30
commit 6fbc078422
1 file changed
+64
+64
View File
@@ -205,6 +205,31 @@ pub fn install(
let unpacked = staging.0.join("unpacked");
std::fs::create_dir(&unpacked).map_err(|e| e.to_string())?;
let files = extract(file, &unpacked)?;
// The official Windows packaging script uses Compress-Archive -Path $Portable,
// which places all files inside a single versioned directory in the ZIP.
// Accept that layout and traditional flat archives, but never arbitrary nesting.
let has_root_executable = files.iter().any(|relative| {
relative.parent().is_some_and(|parent| parent.as_os_str().is_empty())
&& relative
.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| app.stems().any(|stem| name.eq_ignore_ascii_case(&format!("{stem}.exe"))))
});
let (unpacked, files) = if has_root_executable {
(unpacked, files)
} else {
let prefix = files.first().and_then(|path| path.components().next()).ok_or("The portable ZIP contains no files.")?.as_os_str().to_os_string();
let prefix = PathBuf::from(prefix);
if !files.iter().all(|path| path.strip_prefix(&prefix).is_ok_and(|relative| relative.components().next().is_some())) {
return Err("The portable ZIP has an unsupported directory layout.".into());
}
let unpacked = unpacked.join(&prefix);
let files: Vec<PathBuf> = files
.into_iter()
.map(|path| path.strip_prefix(&prefix).map(Path::to_path_buf).map_err(|e| e.to_string()))
.collect::<Result<Vec<_>, _>>()?;
(unpacked, files)
};
let executable = files
.iter()
.find(|relative| {
@@ -471,6 +496,45 @@ mod tests {
assert_eq!(std::fs::read(folder.join("notes.txt")).unwrap(), b"keep");
}
#[test]
fn installs_official_single_directory_portable_zip() {
let dir = tempfile::tempdir().unwrap();
let app = CraftApp::by_id("photocraft").unwrap();
let paths = Paths::under(dir.path());
let zip = archive(
dir.path(),
"release.zip",
&[
("photocraft-0.6.1-windows-x64-portable/photocraft.exe", b"program"),
("photocraft-0.6.1-windows-x64-portable/portable.txt", b""),
("photocraft-0.6.1-windows-x64-portable/lib/runtime.dll", b"library"),
],
);
let record = install(app, &zip, "0.6.1", None, &paths, None, false, &mut |_| Ok(())).unwrap();
assert_eq!(record.path.file_name().unwrap(), "photocraft.exe");
assert_eq!(record.path.parent().unwrap().file_name().unwrap(), "photocraft");
assert_eq!(std::fs::read(&record.path).unwrap(), b"program");
assert_eq!(std::fs::read(paths.apps().join("photocraft/lib/runtime.dll")).unwrap(), b"library");
assert_eq!(record.files.len(), 3);
remove(app, &record).unwrap();
assert!(!record.path.exists());
}
#[test]
fn rejects_mixed_root_and_multiple_directory_archives() {
let dir = tempfile::tempdir().unwrap();
let app = CraftApp::by_id("photocraft").unwrap();
let paths = Paths::under(dir.path());
for (name, entries) in [
("mixed.zip", vec![("package/photocraft.exe", b"program".as_slice()), ("another/readme.txt", b"other".as_slice())]),
("nested.zip", vec![("wrapper/inner/photocraft.exe", b"program".as_slice())]),
] {
let zip = archive(dir.path(), name, &entries);
assert!(install(app, &zip, "0.6.1", None, &paths, None, false, &mut |_| Ok(())).is_err(), "{name}");
assert!(!paths.apps().join("photocraft/photocraft.exe").exists());
}
}
#[test]
fn rejects_traversal_and_case_collisions() {
let dir = tempfile::tempdir().unwrap();