aboutsummaryrefslogtreecommitdiffstats
path: root/zellij-server
diff options
context:
space:
mode:
authordivens <divens@users.noreply.github.com>2026-07-07 16:05:31 +0200
committerGitHub <noreply@github.com>2026-07-07 16:05:31 +0200
commitd1a85aedfed81bc8d6af9b8aab6231d240379c77 (patch)
tree68313cac21bb92b0a1c2b71394bae848b239eb02 /zellij-server
parentfix(windows): route Windows Terminal through the VT input path (#5335) (diff)
downloadsl0ppij-d1a85aedfed81bc8d6af9b8aab6231d240379c77.tar.gz
sl0ppij-d1a85aedfed81bc8d6af9b8aab6231d240379c77.tar.xz
sl0ppij-d1a85aedfed81bc8d6af9b8aab6231d240379c77.zip
fix(windows): preserve backslashes when quoting spawned-process arguments (#5336)
`build_command_line` in the ConPTY backend accumulated runs of backslashes inside a quoted argument but never emitted them before non-quote characters, so `C:\Users\me\Folder With Space\README.md` reached the child as `C:UsersmeFolder With SpaceREADME.md`. Only args routed through the quoted branch were affected, which matches the reported `zellij edit` failure on Windows backslash paths containing spaces. Rewrite the loop to follow `CommandLineToArgvW` rules exactly: n backslashes not followed by a quote emit literally, 2n+1 escape an embedded quote, and trailing backslashes are doubled before the closing quote. Add unit tests for each case, including a regression test for the issue #5293 repro.
Diffstat (limited to 'zellij-server')
-rw-r--r--zellij-server/src/os_input_output_windows.rs140
1 files changed, 111 insertions, 29 deletions
diff --git a/zellij-server/src/os_input_output_windows.rs b/zellij-server/src/os_input_output_windows.rs
index 410dec2b..068750a4 100644
--- a/zellij-server/src/os_input_output_windows.rs
+++ b/zellij-server/src/os_input_output_windows.rs
@@ -123,9 +123,49 @@ fn to_wide(s: &str) -> Vec<u16> {
.collect()
}
+/// Quote a single argument per the `CommandLineToArgvW` convention.
+///
+/// Rules:
+/// * n backslashes not followed by a quote are n literal backslashes.
+/// * 2n backslashes followed by a quote produce n backslashes and a
+/// delimiting quote.
+/// * 2n+1 backslashes followed by a quote produce n backslashes and
+/// a literal quote.
+fn append_quoted_arg(cmdline: &mut String, arg: &str) {
+ if !(arg.is_empty() || arg.contains(' ') || arg.contains('\t') || arg.contains('"')) {
+ cmdline.push_str(arg);
+ return;
+ }
+ cmdline.push('"');
+ let mut backslashes: usize = 0;
+ for ch in arg.chars() {
+ if ch == '\\' {
+ backslashes += 1;
+ } else if ch == '"' {
+ for _ in 0..(backslashes * 2 + 1) {
+ cmdline.push('\\');
+ }
+ backslashes = 0;
+ cmdline.push('"');
+ } else {
+ for _ in 0..backslashes {
+ cmdline.push('\\');
+ }
+ backslashes = 0;
+ cmdline.push(ch);
+ }
+ }
+ // Double any trailing backslashes so the closing quote still terminates
+ // the argument rather than being escaped.
+ for _ in 0..(backslashes * 2) {
+ cmdline.push('\\');
+ }
+ cmdline.push('"');
+}
+
/// Build a Windows command-line string from a `RunCommand`, following the
/// `CommandLineToArgvW` quoting convention.
-fn build_command_line(cmd: &RunCommand) -> Vec<u16> {
+fn build_command_line_string(cmd: &RunCommand) -> String {
let mut cmdline = String::new();
// Executable — always quote to handle spaces in paths.
@@ -136,36 +176,14 @@ fn build_command_line(cmd: &RunCommand) -> Vec<u16> {
for arg in &cmd.args {
cmdline.push(' ');
- if arg.is_empty() || arg.contains(' ') || arg.contains('\t') || arg.contains('"') {
- cmdline.push('"');
- let mut backslashes: usize = 0;
- for ch in arg.chars() {
- if ch == '\\' {
- backslashes += 1;
- } else if ch == '"' {
- // Double backslashes preceding a quote, then escape the quote.
- for _ in 0..backslashes {
- cmdline.push('\\');
- }
- backslashes = 0;
- cmdline.push('\\');
- cmdline.push('"');
- } else {
- backslashes = 0;
- cmdline.push(ch);
- }
- }
- // Double trailing backslashes before the closing quote.
- for _ in 0..backslashes {
- cmdline.push('\\');
- }
- cmdline.push('"');
- } else {
- cmdline.push_str(arg);
- }
+ append_quoted_arg(&mut cmdline, arg);
}
- to_wide(&cmdline)
+ cmdline
+}
+
+fn build_command_line(cmd: &RunCommand) -> Vec<u16> {
+ to_wide(&build_command_line_string(cmd))
}
/// Build a UTF-16 environment block (each entry `KEY=VALUE\0`, terminated by
@@ -647,3 +665,67 @@ impl WindowsPtyBackend {
)
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::path::PathBuf;
+
+ fn cmd(exe: &str, args: &[&str]) -> RunCommand {
+ RunCommand {
+ command: PathBuf::from(exe),
+ args: args.iter().map(|s| s.to_string()).collect(),
+ ..Default::default()
+ }
+ }
+
+ #[test]
+ fn plain_arg_is_left_unquoted() {
+ let s = build_command_line_string(&cmd("nvim", &["hello"]));
+ assert_eq!(s, "\"nvim\" hello");
+ }
+
+ #[test]
+ fn arg_with_space_is_quoted() {
+ let s = build_command_line_string(&cmd("nvim", &["hello world"]));
+ assert_eq!(s, "\"nvim\" \"hello world\"");
+ }
+
+ // Regression: zellij edit with a Windows backslash path plus a space in
+ // the path used to eat every backslash inside the quoted arg. See
+ // https://github.com/zellij-org/zellij/issues/5293.
+ #[test]
+ fn backslash_path_with_space_preserves_backslashes() {
+ let path = r"C:\Users\me\Folder With Space\README.md";
+ let s = build_command_line_string(&cmd("nvim", &[path]));
+ assert_eq!(
+ s,
+ "\"nvim\" \"C:\\Users\\me\\Folder With Space\\README.md\""
+ );
+ }
+
+ #[test]
+ fn embedded_quote_is_escaped_with_odd_backslashes() {
+ // Input arg: a"b (contains a literal quote). It should be emitted
+ // as a\"b inside a quoted arg.
+ let s = build_command_line_string(&cmd("prog", &["a\"b"]));
+ assert_eq!(s, "\"prog\" \"a\\\"b\"");
+ }
+
+ #[test]
+ fn backslash_before_embedded_quote_gets_doubled() {
+ // Input arg: a\"b — one literal backslash, then a literal quote.
+ // Must emit 3 backslashes then a quote so CommandLineToArgvW parses
+ // back to a\"b.
+ let s = build_command_line_string(&cmd("prog", &["a\\\"b c"]));
+ assert_eq!(s, "\"prog\" \"a\\\\\\\"b c\"");
+ }
+
+ #[test]
+ fn trailing_backslash_before_closing_quote_gets_doubled() {
+ // "path with space\" would let the trailing backslash escape the
+ // closing quote. The trailing backslash must be doubled.
+ let s = build_command_line_string(&cmd("prog", &["C:\\my path\\"]));
+ assert_eq!(s, "\"prog\" \"C:\\my path\\\\\"");
+ }
+}