Something went wrong. Try again.
My version of https://github.com/vercel-labs/fx
Something went wrong. Try again.
26 kB · 708 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709const std = @import("std");
const whitespace = " \t\r\n";
pub const LexError = error{ EmbeddedNul, InvalidUtf8, MalformedEscape, UnbalancedQuote,};
pub const PipeSegment = struct { text: []const u8, start: usize, end: usize,};
pub const ArgvToken = struct { raw: []const u8, value: []u8, start: usize, end: usize, quoted: bool = false,};
pub const Argv = struct { tokens: []ArgvToken = &.{},
pub fn deinit(self: *Argv, alloc: std.mem.Allocator) void { for (self.tokens) |token| alloc.free(token.value); alloc.free(self.tokens); self.* = .{}; }};
pub const RedirectionKind = enum { read_path, write_path, read_write_path, unsupported,};
const ShellScan = struct { in_single: bool = false, in_double: bool = false, escaped: bool = false,
fn unquoted(self: *ShellScan, input: []const u8, index: usize) LexError!?u8 { const ch = input[index]; if (self.escaped) { self.escaped = false; return null; } if (ch == '\\' and !self.in_single) { if (index + 1 >= input.len) return error.MalformedEscape; self.escaped = true; return null; } if (ch == '\'' and !self.in_double) { self.in_single = !self.in_single; return null; } if (ch == '"' and !self.in_single) { self.in_double = !self.in_double; return null; } if (self.in_single or self.in_double) return null; return ch; }
fn finish(self: ShellScan) LexError!void { if (self.escaped) return error.MalformedEscape; if (self.in_single or self.in_double) return error.UnbalancedQuote; }};
pub fn pipe_segments(alloc: std.mem.Allocator, command: []const u8) ![]PipeSegment { try check_input(command);
var segments: std.ArrayList(PipeSegment) = .empty; errdefer segments.deinit(alloc);
var scan: ShellScan = .{}; var segment_start: usize = 0; var i: usize = 0; while (i < command.len) : (i += 1) { const ch = try scan.unquoted(command, i) orelse continue;
if (ch == '|') { if (i > 0 and command[i - 1] == '>') continue; if (i + 1 < command.len and command[i + 1] == '|') { i += 1; continue; } try segments.append(alloc, trim_segment(command, segment_start, i)); segment_start = i + 1; } }
try scan.finish(); try segments.append(alloc, trim_segment(command, segment_start, command.len)); return segments.toOwnedSlice(alloc);}
fn trim_segment(command: []const u8, raw_start: usize, raw_end: usize) PipeSegment { var start = raw_start; var end = raw_end; while (start < end and is_whitespace(command[start])) : (start += 1) {} while (end > start and is_whitespace(command[end - 1])) : (end -= 1) {} return .{ .text = command[start..end], .start = start, .end = end };}
/// Conservative shell-shape detector for syntax that needs a real shell parser./// It recognizes common unquoted compound/subshell ASCII metacharacters only;/// it intentionally does not model every POSIX shell grammar production.pub fn unsafe_compound_indicator(command: []const u8) bool { if (check_input(command)) |_| {} else |_| return true;
var scan: ShellScan = .{}; var i: usize = 0; while (i < command.len) : (i += 1) { const maybe_ch = scan.unquoted(command, i) catch return true; const ch = maybe_ch orelse continue;
if (ch == '$' and i + 1 < command.len and command[i + 1] == '(') return true; if (ch == '`') return true; if (ch == '(' or ch == ')' or ch == '{' or ch == '}') return true; if (ch == ';' or ch == '\n') return true; if (ch == '&') { if (i + 1 < command.len and command[i + 1] == '>') continue; if (i > 0 and command[i - 1] == '>') continue; return true; } if (ch == '|' and i + 1 < command.len and command[i + 1] == '|') return true; } scan.finish() catch return true; return false;}
pub fn tokenize_argv(alloc: std.mem.Allocator, command: []const u8) !Argv { try check_input(command);
var tokens: std.ArrayList(ArgvToken) = .empty; errdefer { for (tokens.items) |token| alloc.free(token.value); tokens.deinit(alloc); } var value: std.ArrayList(u8) = .empty; defer value.deinit(alloc);
var token_start: usize = 0; var token_active = false; var token_quoted = false; var in_single = false; var in_double = false;
var i: usize = 0; while (i < command.len) { const ch = command[i];
if (!in_single and !in_double) { if (is_whitespace(ch)) { if (token_active) try finish_token(alloc, &tokens, &value, command, token_start, i, token_quoted); token_active = false; token_quoted = false; i += 1; continue; }
if (redirection_operator_len(command, i)) |op_len| { if (token_active) try finish_token(alloc, &tokens, &value, command, token_start, i, token_quoted); try append_operator_token(alloc, &tokens, command, i, i + op_len); token_active = false; token_quoted = false; i += op_len; continue; }
if (control_operator_len(command, i)) |op_len| { if (token_active) try finish_token(alloc, &tokens, &value, command, token_start, i, token_quoted); try append_operator_token(alloc, &tokens, command, i, i + op_len); token_active = false; token_quoted = false; i += op_len; continue; } }
if (!token_active) { token_start = i; token_active = true; }
if (ch == '\'' and !in_double) { in_single = !in_single; token_quoted = true; i += 1; continue; } if (ch == '"' and !in_single) { in_double = !in_double; token_quoted = true; i += 1; continue; } if (ch == '\\' and !in_single) { if (i + 1 >= command.len) return error.MalformedEscape; try value.append(alloc, command[i + 1]); i += 2; continue; }
try value.append(alloc, ch); i += 1; }
if (in_single or in_double) return error.UnbalancedQuote; if (token_active) try finish_token(alloc, &tokens, &value, command, token_start, command.len, token_quoted); return .{ .tokens = try tokens.toOwnedSlice(alloc) };}
fn finish_token( alloc: std.mem.Allocator, tokens: *std.ArrayList(ArgvToken), value: *std.ArrayList(u8), command: []const u8, start: usize, end: usize, quoted: bool,) !void { const owned = try alloc.dupe(u8, value.items); value.clearRetainingCapacity(); errdefer alloc.free(owned); try tokens.append(alloc, .{ .raw = command[start..end], .value = owned, .start = start, .end = end, .quoted = quoted, });}
fn append_operator_token( alloc: std.mem.Allocator, tokens: *std.ArrayList(ArgvToken), command: []const u8, start: usize, end: usize,) !void { const owned = try alloc.dupe(u8, command[start..end]); errdefer alloc.free(owned); try tokens.append(alloc, .{ .raw = command[start..end], .value = owned, .start = start, .end = end, });}
fn redirection_operator_len(input: []const u8, start: usize) ?usize { const ch = input[start]; if (ch == '&' and start + 1 < input.len and input[start + 1] == '>') { if (start + 2 < input.len and input[start + 2] == '>') return 3; return 2; } if (ch == '<') { if (start + 2 < input.len and input[start + 1] == '<' and input[start + 2] == '<') return 3; if (start + 1 < input.len and (input[start + 1] == '<' or input[start + 1] == '&' or input[start + 1] == '>')) return 2; return 1; } if (ch == '>') { if (start + 1 < input.len and (input[start + 1] == '>' or input[start + 1] == '|' or input[start + 1] == '&')) return 2; return 1; } return null;}
pub fn redirection_kind(value: []const u8) ?RedirectionKind { if (std.mem.eql(u8, value, "<")) return .read_path; if (std.mem.eql(u8, value, "<>")) return .read_write_path; if (std.mem.eql(u8, value, "<<") or std.mem.eql(u8, value, "<<<") or std.mem.eql(u8, value, "<&")) return .unsupported;
if (std.mem.eql(u8, value, ">") or std.mem.eql(u8, value, ">>") or std.mem.eql(u8, value, ">|") or std.mem.eql(u8, value, "&>") or std.mem.eql(u8, value, "&>>") or std.mem.eql(u8, value, ">&")) { return .write_path; } return null;}
fn control_operator_len(input: []const u8, start: usize) ?usize { return switch (input[start]) { ';', '(', ')', '{', '}' => 1, '|' => if (start + 1 < input.len and input[start + 1] == '|') 2 else 1, '&' => if (start + 1 < input.len and input[start + 1] == '&') 2 else 1, else => null, };}
pub fn wrapper_strip_argv(argv: []const ArgvToken) ?[]const ArgvToken { var rest = argv; while (rest.len > 0) { if (is_safe_env_assignment(rest[0].value)) { rest = rest[1..]; continue; }
const command = rest[0].value; if (std.mem.eql(u8, command, "env")) { rest = strip_env(rest) orelse return null; continue; } if (std.mem.eql(u8, command, "nice")) { rest = strip_nice(rest) orelse return null; continue; } if (std.mem.eql(u8, command, "stdbuf")) { rest = strip_stdbuf(rest) orelse return null; continue; } if (std.mem.eql(u8, command, "unbuffer") or std.mem.eql(u8, command, "nohup")) { if (rest.len < 2) return null; rest = rest[1..]; continue; } if (std.mem.eql(u8, command, "time")) { rest = strip_time(rest) orelse return null; continue; } if (std.mem.eql(u8, command, "timeout")) { rest = strip_timeout(rest) orelse return null; continue; } return rest; } return rest;}
fn strip_env(argv: []const ArgvToken) ?[]const ArgvToken { var index: usize = 1; var saw_assignment = false; while (index < argv.len and is_safe_env_assignment(argv[index].value)) : (index += 1) { saw_assignment = true; } if (!saw_assignment or index >= argv.len) return null; return argv[index..];}
fn strip_nice(argv: []const ArgvToken) ?[]const ArgvToken { var index: usize = 1; if (index < argv.len) { const arg = argv[index].value; if (std.mem.eql(u8, arg, "-n") or std.mem.eql(u8, arg, "--adjustment")) { if (index + 1 >= argv.len or !is_signed_integer(argv[index + 1].value)) return null; index += 2; } else if (std.mem.startsWith(u8, arg, "--adjustment=")) { if (!is_signed_integer(arg["--adjustment=".len..])) return null; index += 1; } else if (arg.len > 1 and arg[0] == '-' and is_signed_integer(arg[1..])) { index += 1; } } if (index >= argv.len) return null; return argv[index..];}
fn strip_stdbuf(argv: []const ArgvToken) ?[]const ArgvToken { var index: usize = 1; var saw_option = false; while (index < argv.len) { const arg = argv[index].value; if (!std.mem.startsWith(u8, arg, "-")) break; saw_option = true; if (std.mem.eql(u8, arg, "-i") or std.mem.eql(u8, arg, "-o") or std.mem.eql(u8, arg, "-e") or std.mem.eql(u8, arg, "--input") or std.mem.eql(u8, arg, "--output") or std.mem.eql(u8, arg, "--error")) { if (index + 1 >= argv.len or has_shell_metachar(argv[index + 1].raw)) return null; index += 2; continue; } if (std.mem.startsWith(u8, arg, "-i") or std.mem.startsWith(u8, arg, "-o") or std.mem.startsWith(u8, arg, "-e") or std.mem.startsWith(u8, arg, "--input=") or std.mem.startsWith(u8, arg, "--output=") or std.mem.startsWith(u8, arg, "--error=")) { if (has_shell_metachar(argv[index].raw)) return null; index += 1; continue; } return null; } if (!saw_option or index >= argv.len) return null; return argv[index..];}
fn strip_time(argv: []const ArgvToken) ?[]const ArgvToken { var index: usize = 1; while (index < argv.len and std.mem.eql(u8, argv[index].value, "-p")) : (index += 1) {} if (index >= argv.len) return null; return argv[index..];}
fn strip_timeout(argv: []const ArgvToken) ?[]const ArgvToken { var index: usize = 1; while (index < argv.len) { const arg = argv[index].value; if (!std.mem.startsWith(u8, arg, "-") or std.mem.eql(u8, arg, "-")) break;
if (std.mem.eql(u8, arg, "--foreground") or std.mem.eql(u8, arg, "--preserve-status") or std.mem.eql(u8, arg, "--verbose") or std.mem.eql(u8, arg, "-v")) { index += 1; continue; } if (std.mem.eql(u8, arg, "-k") or std.mem.eql(u8, arg, "--kill-after")) { if (index + 1 >= argv.len or !is_duration(argv[index + 1].value) or has_shell_metachar(argv[index + 1].raw)) return null; index += 2; continue; } if (std.mem.eql(u8, arg, "-s") or std.mem.eql(u8, arg, "--signal")) { if (index + 1 >= argv.len or !is_signal(argv[index + 1].value) or has_shell_metachar(argv[index + 1].raw)) return null; index += 2; continue; } if (std.mem.startsWith(u8, arg, "--kill-after=")) { if (!is_duration(arg["--kill-after=".len..]) or has_shell_metachar(argv[index].raw)) return null; index += 1; continue; } if (std.mem.startsWith(u8, arg, "--signal=")) { if (!is_signal(arg["--signal=".len..]) or has_shell_metachar(argv[index].raw)) return null; index += 1; continue; } return null; }
if (index >= argv.len or !is_duration(argv[index].value) or has_shell_metachar(argv[index].raw)) return null; index += 1; if (index >= argv.len) return null; return argv[index..];}
pub fn is_safe_env_assignment(token: []const u8) bool { const eq = std.mem.findScalar(u8, token, '=') orelse return false; if (eq == 0) return false; return is_safe_env_name(token[0..eq]);}
fn is_safe_env_name(name: []const u8) bool { return std.mem.eql(u8, name, "CI") or std.mem.eql(u8, name, "NODE_ENV") or std.mem.eql(u8, name, "BUN_ENV") or std.mem.eql(u8, name, "DENO_ENV") or std.mem.eql(u8, name, "PYTHONUNBUFFERED") or std.mem.eql(u8, name, "PYTHONWARNINGS") or std.mem.eql(u8, name, "RUST_LOG") or std.mem.eql(u8, name, "RUST_BACKTRACE") or std.mem.eql(u8, name, "TERM") or std.mem.eql(u8, name, "COLORTERM") or std.mem.eql(u8, name, "NO_COLOR") or std.mem.eql(u8, name, "FORCE_COLOR") or std.mem.eql(u8, name, "CLICOLOR") or std.mem.eql(u8, name, "CLICOLOR_FORCE") or std.mem.eql(u8, name, "LANG") or std.mem.eql(u8, name, "LC_ALL") or std.mem.eql(u8, name, "LC_CTYPE") or std.mem.eql(u8, name, "LC_MESSAGES") or std.mem.eql(u8, name, "TZ") or std.mem.eql(u8, name, "CC") or std.mem.eql(u8, name, "CXX") or std.mem.eql(u8, name, "AR") or std.mem.eql(u8, name, "AS") or std.mem.eql(u8, name, "LD") or std.mem.eql(u8, name, "CFLAGS") or std.mem.eql(u8, name, "CXXFLAGS") or std.mem.eql(u8, name, "CPPFLAGS") or std.mem.eql(u8, name, "LDFLAGS") or std.mem.eql(u8, name, "MAKEFLAGS") or std.mem.eql(u8, name, "CMAKE_BUILD_TYPE");}
fn check_input(input: []const u8) LexError!void { if (std.mem.findScalar(u8, input, 0) != null) return error.EmbeddedNul; if (!std.unicode.utf8ValidateSlice(input)) return error.InvalidUtf8;}
fn is_whitespace(ch: u8) bool { return std.mem.findScalar(u8, whitespace, ch) != null;}
fn is_signed_integer(value: []const u8) bool { if (value.len == 0) return false; const start: usize = if (value[0] == '+' or value[0] == '-') 1 else 0; if (start >= value.len) return false; for (value[start..]) |ch| { if (!std.ascii.isDigit(ch)) return false; } return true;}
fn is_duration(value: []const u8) bool { if (value.len == 0) return false; var saw_digit = false; var saw_dot = false; for (value, 0..) |ch, i| { if (std.ascii.isDigit(ch)) { saw_digit = true; continue; } if (ch == '.' and !saw_dot and i + 1 < value.len) { saw_dot = true; continue; } if (i + 1 == value.len and (ch == 's' or ch == 'm' or ch == 'h' or ch == 'd')) return saw_digit; return false; } return saw_digit;}
fn is_signal(value: []const u8) bool { if (value.len == 0) return false; for (value) |ch| { if (!std.ascii.isAlphanumeric(ch)) return false; } return true;}
fn has_shell_metachar(value: []const u8) bool { for (value) |ch| { switch (ch) { ';', '&', '|', '(', ')', '{', '}', '<', '>', '$', '`', '\'', '"' => return true, else => {}, } } return false;}
test "pipe_segments splits unquoted pipes" { const alloc = std.testing.allocator; const segments = try pipe_segments(alloc, "printf a | grep a | wc -l"); defer alloc.free(segments);
try std.testing.expectEqual(@as(usize, 3), segments.len); try std.testing.expectEqualStrings("printf a", segments[0].text); try std.testing.expectEqualStrings("grep a", segments[1].text); try std.testing.expectEqualStrings("wc -l", segments[2].text);}
test "pipe_segments ignores quoted pipes" { const alloc = std.testing.allocator; const segments = try pipe_segments(alloc, "printf 'a|b' | grep \"c|d\""); defer alloc.free(segments);
try std.testing.expectEqual(@as(usize, 2), segments.len); try std.testing.expectEqualStrings("printf 'a|b'", segments[0].text); try std.testing.expectEqualStrings("grep \"c|d\"", segments[1].text);}
test "pipe_segments ignores escaped pipes" { const alloc = std.testing.allocator; const segments = try pipe_segments(alloc, "printf a\\|b | cat"); defer alloc.free(segments);
try std.testing.expectEqual(@as(usize, 2), segments.len); try std.testing.expectEqualStrings("printf a\\|b", segments[0].text);}
test "pipe_segments does not split logical or" { const alloc = std.testing.allocator; const segments = try pipe_segments(alloc, "grep x file || true"); defer alloc.free(segments);
try std.testing.expectEqual(@as(usize, 1), segments.len); try std.testing.expectEqualStrings("grep x file || true", segments[0].text);}
test "pipe_segments keeps overwrite pipe redirection within its segment" { const alloc = std.testing.allocator; const segments = try pipe_segments(alloc, "printf x >| out | cat"); defer alloc.free(segments);
try std.testing.expectEqual(@as(usize, 2), segments.len); try std.testing.expectEqualStrings("printf x >| out", segments[0].text); try std.testing.expectEqualStrings("cat", segments[1].text);}
test "pipe_segments reports unbalanced quotes" { try std.testing.expectError(error.UnbalancedQuote, pipe_segments(std.testing.allocator, "printf 'x | cat"));}
test "pipe_segments reports malformed trailing escapes" { try std.testing.expectError(error.MalformedEscape, pipe_segments(std.testing.allocator, "printf x\\"));}
test "pipe_segments rejects embedded nul and invalid utf8" { try std.testing.expectError(error.EmbeddedNul, pipe_segments(std.testing.allocator, "cat \x00 file")); try std.testing.expectError(error.InvalidUtf8, pipe_segments(std.testing.allocator, "cat \xff"));}
test "tokenize_argv decodes single and double quoted tokens" { var argv = try tokenize_argv(std.testing.allocator, "grep 'hello world' \"file name.txt\""); defer argv.deinit(std.testing.allocator);
try std.testing.expectEqual(@as(usize, 3), argv.tokens.len); try std.testing.expectEqualStrings("hello world", argv.tokens[1].value); try std.testing.expectEqualStrings("file name.txt", argv.tokens[2].value); try std.testing.expect(argv.tokens[1].quoted);}
test "tokenize_argv preserves raw byte offsets" { var argv = try tokenize_argv(std.testing.allocator, " cat 'a b'"); defer argv.deinit(std.testing.allocator);
try std.testing.expectEqual(@as(usize, 2), argv.tokens[0].start); try std.testing.expectEqual(@as(usize, 5), argv.tokens[0].end); try std.testing.expectEqualStrings("'a b'", argv.tokens[1].raw);}
test "tokenize_argv handles escaped spaces without splitting" { var argv = try tokenize_argv(std.testing.allocator, "cat file\\ name.txt"); defer argv.deinit(std.testing.allocator);
try std.testing.expectEqual(@as(usize, 2), argv.tokens.len); try std.testing.expectEqualStrings("file name.txt", argv.tokens[1].value); try std.testing.expectEqualStrings("file\\ name.txt", argv.tokens[1].raw);}
test "tokenize_argv emits redirection operators as tokens" { var argv = try tokenize_argv(std.testing.allocator, "printf ok>out.txt &>> log.txt >&err.txt < in.txt <> state.txt << EOF <<< value <& 0"); defer argv.deinit(std.testing.allocator);
try std.testing.expectEqualStrings(">", argv.tokens[2].value); try std.testing.expectEqualStrings("&>>", argv.tokens[4].value); try std.testing.expectEqualStrings(">&", argv.tokens[6].value); try std.testing.expectEqualStrings("err.txt", argv.tokens[7].value); try std.testing.expectEqualStrings("<", argv.tokens[8].value); try std.testing.expectEqualStrings("<>", argv.tokens[10].value); try std.testing.expectEqualStrings("<<", argv.tokens[12].value); try std.testing.expectEqualStrings("<<<", argv.tokens[14].value); try std.testing.expectEqualStrings("<&", argv.tokens[16].value);}
test "redirection_kind classifies shell redirects" { try std.testing.expectEqual(RedirectionKind.read_path, redirection_kind("<").?); try std.testing.expectEqual(RedirectionKind.read_write_path, redirection_kind("<>").?); try std.testing.expectEqual(RedirectionKind.write_path, redirection_kind(">").?); try std.testing.expectEqual(RedirectionKind.write_path, redirection_kind("&>>").?); try std.testing.expectEqual(RedirectionKind.unsupported, redirection_kind("<<").?); try std.testing.expectEqual(RedirectionKind.unsupported, redirection_kind("<<<").?); try std.testing.expectEqual(RedirectionKind.unsupported, redirection_kind("<&").?); try std.testing.expect(redirection_kind("|") == null);}
test "tokenize_argv distinguishes empty argv from tokenization failure" { var argv = try tokenize_argv(std.testing.allocator, " \t "); defer argv.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 0), argv.tokens.len);
try std.testing.expectError(error.UnbalancedQuote, tokenize_argv(std.testing.allocator, "\"unterminated"));}
test "tokenize_argv reports malformed trailing escapes" { try std.testing.expectError(error.MalformedEscape, tokenize_argv(std.testing.allocator, "printf x\\"));}
test "tokenize_argv rejects embedded nul and invalid utf8" { try std.testing.expectError(error.EmbeddedNul, tokenize_argv(std.testing.allocator, "cat \x00 file")); try std.testing.expectError(error.InvalidUtf8, tokenize_argv(std.testing.allocator, "cat \xff"));}
test "unsafe_compound_indicator detects subshells and groups" { try std.testing.expect(unsafe_compound_indicator("echo $(pwd)")); try std.testing.expect(unsafe_compound_indicator("(cd src; make)")); try std.testing.expect(unsafe_compound_indicator("{ echo hi; }")); try std.testing.expect(!unsafe_compound_indicator("grep '(' file"));}
test "unsafe_compound_indicator detects logical control syntax" { try std.testing.expect(unsafe_compound_indicator("cd src && touch x")); try std.testing.expect(unsafe_compound_indicator("grep x file || true")); try std.testing.expect(unsafe_compound_indicator("sleep 1 &")); try std.testing.expect(!unsafe_compound_indicator("printf a | grep a"));}
test "unsafe_compound_indicator ignores escaped and quoted compound characters" { try std.testing.expect(!unsafe_compound_indicator("printf \\& \\| \\;")); try std.testing.expect(!unsafe_compound_indicator("printf \"$(literal)\""));}
test "wrapper_strip_argv strips safe env and process wrappers" { var argv = try tokenize_argv(std.testing.allocator, "env NODE_ENV=prod nice -n 5 timeout --foreground 10s grep x file"); defer argv.deinit(std.testing.allocator);
const stripped = wrapper_strip_argv(argv.tokens).?; try std.testing.expectEqualStrings("grep", stripped[0].value); try std.testing.expectEqualStrings("file", stripped[2].value);}
test "wrapper_strip_argv declines ambiguous wrappers" { var env_argv = try tokenize_argv(std.testing.allocator, "env -i grep x"); defer env_argv.deinit(std.testing.allocator); try std.testing.expect(wrapper_strip_argv(env_argv.tokens) == null);
var timeout_argv = try tokenize_argv(std.testing.allocator, "timeout --kill-after '1;rm' 10s grep x"); defer timeout_argv.deinit(std.testing.allocator); try std.testing.expect(wrapper_strip_argv(timeout_argv.tokens) == null);}