Commit 954f8db
Eric Bower
·
2026-03-03 19:53:37 -0500 EST
parent ac857cf
fix(run): re-quote when using shell meta chars argv doesn't receive the quotes in the command line so we need to apply an algorithm to re-quote args when it uses special meta-characters. Closes: https://github.com/neurosnap/zmx/issues/72
1 files changed,
+111,
-11
+111,
-11
| ... | ... | @@ -274,7 +274,8 @@ const Daemon = struct { | |
| 274 | 274 | pub fn handleInfo(self: *Daemon, client: *Client) !void { | |
| 275 | 275 | const clients_len = self.clients.items.len - 1; | |
| 276 | 276 | ||
| 277 | - | // Build command string from args | |
| 277 | + | // Build command string from args, re-quoting args that contain | |
| 278 | + | // shell-special characters so the displayed command is copy-pasteable. | |
| 278 | 279 | var cmd_buf: [ipc.MAX_CMD_LEN]u8 = undefined; | |
| 279 | 280 | var cmd_len: u16 = 0; | |
| 280 | 281 | const cur_cmd = self.command orelse self.task_command; |
| ... | ... | @@ -286,10 +287,19 @@ const Daemon = struct { | |
| 286 | 287 | cmd_len += 1; | |
| 287 | 288 | } | |
| 288 | 289 | } | |
| 289 | - | const remaining = ipc.MAX_CMD_LEN - cmd_len; | |
| 290 | - | const copy_len: u16 = @intCast(@min(arg.len, remaining)); | |
| 291 | - | @memcpy(cmd_buf[cmd_len..][0..copy_len], arg[0..copy_len]); | |
| 292 | - | cmd_len += copy_len; | |
| 290 | + | if (shellNeedsQuoting(arg)) { | |
| 291 | + | const quoted = shellQuote(self.alloc, arg) catch arg; | |
| 292 | + | defer if (quoted.ptr != arg.ptr) self.alloc.free(quoted); | |
| 293 | + | const remaining = ipc.MAX_CMD_LEN - cmd_len; | |
| 294 | + | const copy_len: u16 = @intCast(@min(quoted.len, remaining)); | |
| 295 | + | @memcpy(cmd_buf[cmd_len..][0..copy_len], quoted[0..copy_len]); | |
| 296 | + | cmd_len += copy_len; | |
| 297 | + | } else { | |
| 298 | + | const remaining = ipc.MAX_CMD_LEN - cmd_len; | |
| 299 | + | const copy_len: u16 = @intCast(@min(arg.len, remaining)); | |
| 300 | + | @memcpy(cmd_buf[cmd_len..][0..copy_len], arg[0..copy_len]); | |
| 301 | + | cmd_len += copy_len; | |
| 302 | + | } | |
| 293 | 303 | } | |
| 294 | 304 | } | |
| 295 | 305 |
| ... | ... | @@ -967,6 +977,79 @@ fn attach(daemon: *Daemon) !void { | |
| 967 | 977 | try clientLoop(daemon.cfg, client_sock); | |
| 968 | 978 | } | |
| 969 | 979 | ||
| 980 | + | fn shellNeedsQuoting(arg: []const u8) bool { | |
| 981 | + | if (arg.len == 0) return true; | |
| 982 | + | for (arg) |ch| { | |
| 983 | + | switch (ch) { | |
| 984 | + | ' ', '\t', '"', '\'', '\\', '$', '`', '!', '(', ')', '{', '}', '[', ']', '|', '&', ';', '<', '>', '?', '*', '~', '#', '\n' => return true, | |
| 985 | + | else => {}, | |
| 986 | + | } | |
| 987 | + | } | |
| 988 | + | return false; | |
| 989 | + | } | |
| 990 | + | ||
| 991 | + | fn shellQuote(alloc: std.mem.Allocator, arg: []const u8) ![]u8 { | |
| 992 | + | // Prefer double quotes when the arg has no double quotes (cleaner output). | |
| 993 | + | // Fall back to single quotes with '\'' escaping for embedded single quotes. | |
| 994 | + | const has_double_quote = std.mem.indexOfScalar(u8, arg, '"') != null; | |
| 995 | + | ||
| 996 | + | if (!has_double_quote) { | |
| 997 | + | // Double-quote style: escape only $ ` \ ! " | |
| 998 | + | var len: usize = 2; // opening and closing " | |
| 999 | + | for (arg) |ch| { | |
| 1000 | + | if (ch == '$' or ch == '`' or ch == '\\' or ch == '!') { | |
| 1001 | + | len += 2; // backslash + char | |
| 1002 | + | } else { | |
| 1003 | + | len += 1; | |
| 1004 | + | } | |
| 1005 | + | } | |
| 1006 | + | const buf = try alloc.alloc(u8, len); | |
| 1007 | + | var i: usize = 0; | |
| 1008 | + | buf[i] = '"'; | |
| 1009 | + | i += 1; | |
| 1010 | + | for (arg) |ch| { | |
| 1011 | + | if (ch == '$' or ch == '`' or ch == '\\' or ch == '!') { | |
| 1012 | + | buf[i] = '\\'; | |
| 1013 | + | buf[i + 1] = ch; | |
| 1014 | + | i += 2; | |
| 1015 | + | } else { | |
| 1016 | + | buf[i] = ch; | |
| 1017 | + | i += 1; | |
| 1018 | + | } | |
| 1019 | + | } | |
| 1020 | + | buf[i] = '"'; | |
| 1021 | + | return buf; | |
| 1022 | + | } | |
| 1023 | + | ||
| 1024 | + | // Single-quote style: escape embedded single quotes as '\'' | |
| 1025 | + | var len: usize = 2; | |
| 1026 | + | for (arg) |ch| { | |
| 1027 | + | if (ch == '\'') { | |
| 1028 | + | len += 4; | |
| 1029 | + | } else { | |
| 1030 | + | len += 1; | |
| 1031 | + | } | |
| 1032 | + | } | |
| 1033 | + | const buf = try alloc.alloc(u8, len); | |
| 1034 | + | var i: usize = 0; | |
| 1035 | + | buf[i] = '\''; | |
| 1036 | + | i += 1; | |
| 1037 | + | for (arg) |ch| { | |
| 1038 | + | if (ch == '\'') { | |
| 1039 | + | buf[i] = '\''; | |
| 1040 | + | buf[i + 1] = '\\'; | |
| 1041 | + | buf[i + 2] = '\''; | |
| 1042 | + | buf[i + 3] = '\''; | |
| 1043 | + | i += 4; | |
| 1044 | + | } else { | |
| 1045 | + | buf[i] = ch; | |
| 1046 | + | i += 1; | |
| 1047 | + | } | |
| 1048 | + | } | |
| 1049 | + | buf[i] = '\''; | |
| 1050 | + | return buf; | |
| 1051 | + | } | |
| 1052 | + | ||
| 970 | 1053 | fn run(daemon: *Daemon, command_args: [][]const u8) !void { | |
| 971 | 1054 | const alloc = daemon.alloc; | |
| 972 | 1055 | var buf: [4096]u8 = undefined; |
| ... | ... | @@ -985,19 +1068,36 @@ fn run(daemon: *Daemon, command_args: [][]const u8) !void { | |
| 985 | 1068 | defer if (allocated_cmd) |cmd| alloc.free(cmd); | |
| 986 | 1069 | ||
| 987 | 1070 | if (command_args.len > 0) { | |
| 1071 | + | var parts: std.ArrayList([]const u8) = .empty; | |
| 1072 | + | defer { | |
| 1073 | + | for (parts.items) |part| alloc.free(part); | |
| 1074 | + | parts.deinit(alloc); | |
| 1075 | + | } | |
| 1076 | + | ||
| 988 | 1077 | var total_len: usize = 0; | |
| 989 | - | for (command_args) |arg| { | |
| 990 | - | total_len += arg.len + 1; | |
| 1078 | + | for (command_args, 0..) |arg, i| { | |
| 1079 | + | // Last arg is the sentinel (e.g. "; echo ZMX_TASK_COMPLETED:$?") | |
| 1080 | + | // which must not be quoted so the shell interprets it. | |
| 1081 | + | const is_last = i == command_args.len - 1; | |
| 1082 | + | if (!is_last and shellNeedsQuoting(arg)) { | |
| 1083 | + | const quoted = try shellQuote(alloc, arg); | |
| 1084 | + | try parts.append(alloc, quoted); | |
| 1085 | + | total_len += quoted.len + 1; | |
| 1086 | + | } else { | |
| 1087 | + | const duped = try alloc.dupe(u8, arg); | |
| 1088 | + | try parts.append(alloc, duped); | |
| 1089 | + | total_len += duped.len + 1; | |
| 1090 | + | } | |
| 991 | 1091 | } | |
| 992 | 1092 | ||
| 993 | 1093 | const cmd_buf = try alloc.alloc(u8, total_len); | |
| 994 | 1094 | allocated_cmd = cmd_buf; | |
| 995 | 1095 | ||
| 996 | 1096 | var offset: usize = 0; | |
| 997 | - | for (command_args, 0..) |arg, i| { | |
| 998 | - | @memcpy(cmd_buf[offset .. offset + arg.len], arg); | |
| 999 | - | offset += arg.len; | |
| 1000 | - | if (i < command_args.len - 1) { | |
| 1097 | + | for (parts.items, 0..) |part, i| { | |
| 1098 | + | @memcpy(cmd_buf[offset .. offset + part.len], part); | |
| 1099 | + | offset += part.len; | |
| 1100 | + | if (i < parts.items.len - 1) { | |
| 1001 | 1101 | cmd_buf[offset] = ' '; | |
| 1002 | 1102 | } else { | |
| 1003 | 1103 | cmd_buf[offset] = '\n'; |