PranitInitial commit439f83c2 months agoHistory
708 lines · 25.5 KB · Zig
RawDownload
const 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;}

Lines 1–500 of 708.