diff --git a/src-tauri/src/commands/update_portable.rs b/src-tauri/src/commands/update_portable.rs index 7cba74b43..43d6fbbce 100644 --- a/src-tauri/src/commands/update_portable.rs +++ b/src-tauri/src/commands/update_portable.rs @@ -170,6 +170,11 @@ fn sha256_hex(bytes: &[u8]) -> String { Sha256::digest(bytes).iter().map(|byte| format!("{byte:02x}")).collect() } +#[cfg(any(target_os = "windows", test))] +fn portable_update_staging_dir(executable_dir: &std::path::Path, update_id: &str) -> std::path::PathBuf { + executable_dir.join(format!(".dbx-portable-update-{update_id}")) +} + /// Windows PowerShell lives in a versioned directory that Rust's executable /// search order never visits (it only probes the `System32` root), so a machine /// whose `PATH` lost that entry fails with "program not found" even though @@ -215,7 +220,10 @@ pub(super) fn launch_portable_update_helper(archive: &[u8], version: &Version) - fs::remove_file(&write_probe) .map_err(|error| format!("Failed to finish portable directory write check: {error}"))?; - let staging_dir = std::env::temp_dir().join(format!("dbx-portable-update-{update_id}")); + // Keep the replacement and helper on the target volume. Windows + // PowerShell cannot reliably Move-Item an executable across volumes after + // the running target has already been renamed to its backup. + let staging_dir = portable_update_staging_dir(exe_dir, &update_id); fs::create_dir(&staging_dir) .map_err(|error| format!("Failed to create portable update staging directory: {error}"))?; let staged_exe = staging_dir.join("DBX.exe.new"); @@ -273,7 +281,7 @@ pub(super) fn launch_portable_update_helper(_archive: &[u8], _version: &Version) Err("Portable updates are only supported on Windows.".to_string()) } -#[cfg(target_os = "windows")] +#[cfg(any(target_os = "windows", test))] const PORTABLE_UPDATE_SCRIPT: &str = r#"param( [Parameter(Mandatory = $true)][int]$ParentProcessId, [Parameter(Mandatory = $true)][string]$SourceExe, @@ -337,9 +345,9 @@ exit 0 #[cfg(test)] mod tests { use super::{ - decode_tauri_text, portable_asset_name, sha256_hex, validate_requested_portable_version, - validated_portable_executable, PORTABLE_EXECUTABLE_NAME, PORTABLE_UPDATE_MANIFEST_NAME, - PORTABLE_UPDATE_MANIFEST_SCHEMA_VERSION, + decode_tauri_text, portable_asset_name, portable_update_staging_dir, sha256_hex, + validate_requested_portable_version, validated_portable_executable, PORTABLE_EXECUTABLE_NAME, + PORTABLE_UPDATE_MANIFEST_NAME, PORTABLE_UPDATE_MANIFEST_SCHEMA_VERSION, PORTABLE_UPDATE_SCRIPT, }; use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _}; use semver::Version; @@ -371,6 +379,38 @@ mod tests { assert!(portable_asset_name("../../0.5.64", "x86_64").is_err()); } + #[test] + fn stages_portable_replacement_beside_the_target() { + let executable_dir = std::path::Path::new("portable-install"); + let staging_dir = portable_update_staging_dir(executable_dir, "123-456"); + + assert_eq!(staging_dir.parent(), Some(executable_dir)); + assert_eq!(staging_dir.file_name().and_then(|name| name.to_str()), Some(".dbx-portable-update-123-456")); + assert_eq!(staging_dir.join("DBX.exe.new").parent(), Some(staging_dir.as_path())); + assert_eq!(staging_dir.join("apply-update.ps1").parent(), Some(staging_dir.as_path())); + } + + #[test] + fn portable_update_script_preserves_replacement_and_rollback_order() { + let backup = PORTABLE_UPDATE_SCRIPT + .find("Move-Item -LiteralPath $TargetExe -Destination $BackupExe -Force") + .expect("script must back up the running executable"); + let replacement = PORTABLE_UPDATE_SCRIPT + .find("Move-Item -LiteralPath $SourceExe -Destination $TargetExe -Force") + .expect("script must install the staged executable"); + let relaunch = PORTABLE_UPDATE_SCRIPT + .find("Start-Process -FilePath $TargetExe") + .expect("script must relaunch the portable executable"); + let cleanup = PORTABLE_UPDATE_SCRIPT + .find("Remove-Item -LiteralPath $StagingDir -Recurse -Force") + .expect("script must clean the staging directory after relaunch"); + + assert!(backup < replacement); + assert!(replacement < relaunch); + assert!(relaunch < cleanup); + assert!(PORTABLE_UPDATE_SCRIPT.contains("Copy-Item -LiteralPath $BackupExe -Destination $TargetExe -Force")); + } + #[test] fn accepts_newer_portable_update_requests() { assert_eq!(validate_requested_portable_version("0.5.64", "0.5.63").unwrap(), Version::parse("0.5.64").unwrap());