From 02d7e42151216e72c8a8b1597d33c0263ae5c504 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg <5861166+CompleteDotTech@users.noreply.github.com> Date: Fri, 24 Jul 2026 23:35:11 -0400 Subject: [PATCH] fix(updater): quote MSI log argument once --- src-tauri/release-runtime.test.mjs | 9 +++- src-tauri/src/tauri_setup.rs | 78 ++++++++++++++++++++++++++++-- 2 files changed, 81 insertions(+), 6 deletions(-) diff --git a/src-tauri/release-runtime.test.mjs b/src-tauri/release-runtime.test.mjs index c76d08923..83ea67c88 100644 --- a/src-tauri/release-runtime.test.mjs +++ b/src-tauri/release-runtime.test.mjs @@ -228,8 +228,13 @@ test("native updater cleanup stops the sidecar before Windows exits", async () = ); assert.match( launcher, - /installer_args\(\[[\s\S]*OsString::from\("\/L\*V"\)/, - "updater-driven MSI installs must retain a verbose per-run diagnostic log", + /fn msi_verbose_log_installer_args\([\s\S]*quoted_log_path\.push\(log_path\.as_os_str\(\)\)[\s\S]*OsString::from\("\/L\*V"\)/, + "the extracted updater helper must retain a verbose MSI log argument while preserving the native path", + ); + assert.match( + launcher, + /updater_builder\.installer_args\(msi_verbose_log_installer_args\(&log_path\)\)/, + "updater-driven MSI installs must use the verbose per-run diagnostic log helper", ); }); diff --git a/src-tauri/src/tauri_setup.rs b/src-tauri/src/tauri_setup.rs index f62e2986e..4e2de8650 100644 --- a/src-tauri/src/tauri_setup.rs +++ b/src-tauri/src/tauri_setup.rs @@ -1,5 +1,78 @@ use super::*; +/// Builds the verbose MSI log arguments as command-line segments. +/// +/// `tauri-plugin-updater` joins `installer_args` into the raw parameter +/// string supplied to `ShellExecuteW`, rather than passing them through a +/// process argument API. Keep exactly one pair of quotes around the log path +/// so msiexec receives a single `/L*V` path even when the app log directory +/// contains spaces. +#[cfg(any(target_os = "windows", test))] +fn msi_verbose_log_installer_args(log_path: &std::path::Path) -> [std::ffi::OsString; 2] { + let mut quoted_log_path = std::ffi::OsString::from("\""); + quoted_log_path.push(log_path.as_os_str()); + quoted_log_path.push("\""); + + [std::ffi::OsString::from("/L*V"), quoted_log_path] +} + +#[cfg(test)] +mod updater_argument_tests { + use super::msi_verbose_log_installer_args; + use std::{ffi::OsStr, path::Path}; + + const UPDATER_JOIN_CONTRACT_VERSION: &str = "2.10.1"; + + fn locked_updater_package_entry() -> &'static str { + let mut entries = include_str!("../Cargo.lock") + .split("[[package]]") + .filter(|entry| entry.contains("name = \"tauri-plugin-updater\"")); + let entry = entries + .next() + .expect("Cargo.lock must contain tauri-plugin-updater"); + assert!( + entries.next().is_none(), + "the updater join-contract test must be revisited when multiple updater versions are locked" + ); + entry + } + + #[test] + fn msi_verbose_log_path_is_quoted_once_in_shell_execute_parameters() { + assert!( + locked_updater_package_entry().contains(&format!( + "version = \"{UPDATER_JOIN_CONTRACT_VERSION}\"" + )), + "tauri-plugin-updater changed; revalidate its MSI ShellExecuteW argument construction before updating this contract" + ); + + let log_path = Path::new( + r"C:\Users\A Coven\AppData\Local\Coven Cave\logs\msi-upgrade-from-0.1.6-123.log", + ); + + let installer_args = msi_verbose_log_installer_args(log_path); + assert_eq!(installer_args[0], OsStr::new("/L*V")); + assert_eq!( + installer_args[1], + OsStr::new( + r#""C:\Users\A Coven\AppData\Local\Coven Cave\logs\msi-upgrade-from-0.1.6-123.log""# + ) + ); + + // This mirrors tauri-plugin-updater 2.10.1's `ShellExecuteW` + // parameter construction. No quotes would split this spaced path, and + // a second pair would end quote mode and split it as well. + let parameters = installer_args.join(OsStr::new(" ")); + assert_eq!( + parameters, + OsStr::new( + r#"/L*V "C:\Users\A Coven\AppData\Local\Coven Cave\logs\msi-upgrade-from-0.1.6-123.log""# + ) + ); + assert_eq!(parameters.to_string_lossy().matches('"').count(), 2); + } +} + /// Show or hide the macOS traffic lights (close/minimize/zoom) on the /// invoking window. The main window's title bar is an Overlay (see the main /// window builder), so the buttons float over web content — when the app's @@ -229,10 +302,7 @@ pub fn run() { std::process::id() )); log::info!("[cave] updater MSI log -> {}", log_path.display()); - updater_builder.installer_args([ - std::ffi::OsString::from("/L*V"), - std::ffi::OsString::from(format!("\"{}\"", log_path.display())), - ]) + updater_builder.installer_args(msi_verbose_log_installer_args(&log_path)) }; app.handle().plugin(updater_builder.build())?; app.handle().plugin(tauri_plugin_process::init())?;