mirror of
https://github.com/storytold/craft-launcher.git
synced 2026-11-12 05:03:07 +00:00
fix(windows): install version-folder wrapped portable ZIP releases
This commit is contained in:
1 parent
bf39e92f30
commit
6fbc078422
1 file changed
+64
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user