Skip to content

Commit 735d209

Browse files
authored
fix(updater): Escape current_exe args for nsis installer (#2761)
* fix(updater): Escape current_exe args for nsis installer * Update plugins/updater/src/updater.rs * use std escape fn * tests * comment
1 parent e008434 commit 735d209

File tree

2 files changed

+103
-11
lines changed

2 files changed

+103
-11
lines changed

.changes/fix-nsis-updater-args.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
updater: patch
3+
updater-js: patch
4+
---
5+
6+
Fixed an issue preventing updates via the NSIS installer from succeeding when the app was launched with command line arguments containing spaces.

plugins/updater/src/updater.rs

Lines changed: 97 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -683,17 +683,25 @@ impl Update {
683683
let install_mode = self.config.install_mode();
684684
let current_args = &self.current_exe_args()[1..];
685685
let msi_args;
686+
let nsis_args;
686687

687688
let installer_args: Vec<&OsStr> = match &updater_type {
688-
WindowsUpdaterType::Nsis { .. } => install_mode
689-
.nsis_args()
690-
.iter()
691-
.map(OsStr::new)
692-
.chain(once(OsStr::new("/UPDATE")))
693-
.chain(once(OsStr::new("/ARGS")))
694-
.chain(current_args.to_vec())
695-
.chain(self.installer_args())
696-
.collect(),
689+
WindowsUpdaterType::Nsis { .. } => {
690+
nsis_args = current_args
691+
.iter()
692+
.map(escape_nsis_current_exe_arg)
693+
.collect::<Vec<_>>();
694+
695+
install_mode
696+
.nsis_args()
697+
.iter()
698+
.map(OsStr::new)
699+
.chain(once(OsStr::new("/UPDATE")))
700+
.chain(once(OsStr::new("/ARGS")))
701+
.chain(nsis_args.iter().map(OsStr::new))
702+
.chain(self.installer_args())
703+
.collect()
704+
}
697705
WindowsUpdaterType::Msi { path, .. } => {
698706
let escaped_args = current_args
699707
.iter()
@@ -1363,6 +1371,41 @@ impl PathExt for PathBuf {
13631371
}
13641372
}
13651373

1374+
// adapted from https://github.com/rust-lang/rust/blob/1c047506f94cd2d05228eb992b0a6bbed1942349/library/std/src/sys/args/windows.rs#L174
1375+
#[cfg(windows)]
1376+
fn escape_nsis_current_exe_arg(arg: &&OsStr) -> String {
1377+
let arg = arg.to_string_lossy();
1378+
let mut cmd: Vec<char> = Vec::new();
1379+
1380+
// compared to std we additionally escape `/` so that nsis won't interpret them as a beginning of an nsis argument.
1381+
let quote = arg.chars().any(|c| c == ' ' || c == '\t' || c == '/') || arg.is_empty();
1382+
let escape = true;
1383+
if quote {
1384+
cmd.push('"');
1385+
}
1386+
let mut backslashes: usize = 0;
1387+
for x in arg.chars() {
1388+
if escape {
1389+
if x == '\\' {
1390+
backslashes += 1;
1391+
} else {
1392+
if x == '"' {
1393+
// Add n+1 backslashes to total 2n+1 before internal '"'.
1394+
cmd.extend((0..=backslashes).map(|_| '\\'));
1395+
}
1396+
backslashes = 0;
1397+
}
1398+
}
1399+
cmd.push(x);
1400+
}
1401+
if quote {
1402+
// Add n backslashes to total 2n before ending '"'.
1403+
cmd.extend((0..backslashes).map(|_| '\\'));
1404+
cmd.push('"');
1405+
}
1406+
cmd.into_iter().collect()
1407+
}
1408+
13661409
#[cfg(windows)]
13671410
fn escape_msi_property_arg(arg: impl AsRef<OsStr>) -> String {
13681411
let mut arg = arg.as_ref().to_string_lossy().to_string();
@@ -1375,7 +1418,7 @@ fn escape_msi_property_arg(arg: impl AsRef<OsStr>) -> String {
13751418
}
13761419

13771420
if arg.contains('"') {
1378-
arg = arg.replace('"', r#""""""#)
1421+
arg = arg.replace('"', r#""""""#);
13791422
}
13801423

13811424
if arg.starts_with('-') {
@@ -1406,7 +1449,7 @@ mod tests {
14061449

14071450
#[test]
14081451
#[cfg(windows)]
1409-
fn it_escapes_correctly() {
1452+
fn it_escapes_correctly_for_msi() {
14101453
use crate::updater::escape_msi_property_arg;
14111454

14121455
// Explanation for quotes:
@@ -1451,4 +1494,47 @@ mod tests {
14511494
assert_eq!(escape_msi_property_arg(orig), escaped);
14521495
}
14531496
}
1497+
1498+
#[test]
1499+
#[cfg(windows)]
1500+
fn it_escapes_correctly_for_nsis() {
1501+
use crate::updater::escape_nsis_current_exe_arg;
1502+
use std::ffi::OsStr;
1503+
1504+
let cases = [
1505+
"something",
1506+
"--flag",
1507+
"--empty=",
1508+
"--arg=value",
1509+
"some space", // This simulates `./my-app "some string"`.
1510+
"--arg value", // -> This simulates `./my-app "--arg value"`. Same as above but it triggers the startsWith(`-`) logic.
1511+
"--arg=unwrapped space", // `./my-app --arg="unwrapped space"`
1512+
"--arg=\"wrapped\"", // `./my-app --args=""wrapped""`
1513+
"--arg=\"wrapped space\"", // `./my-app --args=""wrapped space""`
1514+
"--arg=midword\"wrapped space\"", // `./my-app --args=midword""wrapped""`
1515+
"", // `./my-app '""'`
1516+
];
1517+
// Note: These may not be the results we actually want (monitor this!).
1518+
// We only make sure the implementation doesn't unintentionally change.
1519+
let cases_escaped = [
1520+
"something",
1521+
"--flag",
1522+
"--empty=",
1523+
"--arg=value",
1524+
"\"some space\"",
1525+
"\"--arg value\"",
1526+
"\"--arg=unwrapped space\"",
1527+
"--arg=\\\"wrapped\\\"",
1528+
"\"--arg=\\\"wrapped space\\\"\"",
1529+
"\"--arg=midword\\\"wrapped space\\\"\"",
1530+
"\"\"",
1531+
];
1532+
1533+
// Just to be sure we didn't mess that up
1534+
assert_eq!(cases.len(), cases_escaped.len());
1535+
1536+
for (orig, escaped) in cases.iter().zip(cases_escaped) {
1537+
assert_eq!(escape_nsis_current_exe_arg(&OsStr::new(orig)), escaped);
1538+
}
1539+
}
14541540
}

0 commit comments

Comments
 (0)