Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197const std = @import("std");const testing = std.testing;const Allocator = std.mem.Allocator;const Writer = std.Io.Writer;
/// Builder for constructing space-separated shell command strings./// Uses a caller-provided allocator (typically with stackFallback).pub const ShellCommandBuilder = struct { buffer: std.Io.Writer.Allocating,
pub fn init(allocator: Allocator) ShellCommandBuilder { return .{ .buffer = .init(allocator) }; }
pub fn deinit(self: *ShellCommandBuilder) void { self.buffer.deinit(); }
/// Append an argument to the command with automatic space separation. pub fn appendArg(self: *ShellCommandBuilder, arg: []const u8) (Allocator.Error || Writer.Error)!void { if (arg.len == 0) return; if (self.buffer.written().len > 0) { try self.buffer.writer.writeByte(' '); } try self.buffer.writer.writeAll(arg); }
/// Get the final null-terminated command string, transferring ownership to caller. /// Calling deinit() after this is safe but unnecessary. pub fn toOwnedSlice(self: *ShellCommandBuilder) Allocator.Error![:0]const u8 { return try self.buffer.toOwnedSliceSentinel(0); }};
test ShellCommandBuilder { // Empty command { var cmd = ShellCommandBuilder.init(testing.allocator); defer cmd.deinit(); try testing.expectEqualStrings("", cmd.buffer.written()); }
// Single arg { var cmd = ShellCommandBuilder.init(testing.allocator); defer cmd.deinit(); try cmd.appendArg("bash"); try testing.expectEqualStrings("bash", cmd.buffer.written()); }
// Multiple args { var cmd = ShellCommandBuilder.init(testing.allocator); defer cmd.deinit(); try cmd.appendArg("bash"); try cmd.appendArg("--posix"); try cmd.appendArg("-l"); try testing.expectEqualStrings("bash --posix -l", cmd.buffer.written()); }
// Empty arg { var cmd = ShellCommandBuilder.init(testing.allocator); defer cmd.deinit(); try cmd.appendArg("bash"); try cmd.appendArg(""); try testing.expectEqualStrings("bash", cmd.buffer.written()); }
// toOwnedSlice { var cmd = ShellCommandBuilder.init(testing.allocator); try cmd.appendArg("bash"); try cmd.appendArg("--posix"); const result = try cmd.toOwnedSlice(); defer testing.allocator.free(result); try testing.expectEqualStrings("bash --posix", result); try testing.expectEqual(@as(u8, 0), result[result.len]); }}
/// Writer that escapes characters that shells treat specially to reduce the/// risk of injection attacks or other such weirdness. Specifically excludes/// linefeeds so that they can be used to delineate lists of file paths.pub const ShellEscapeWriter = struct { writer: Writer, child: *Writer,
pub fn init(child: *Writer) ShellEscapeWriter { return .{ .writer = .{ // TODO: Actually use a buffer here .buffer = &.{}, .vtable = &.{ .drain = ShellEscapeWriter.drain }, }, .child = child, }; }
fn drain(w: *Writer, data: []const []const u8, splat: usize) Writer.Error!usize { const self: *ShellEscapeWriter = @fieldParentPtr("writer", w);
// TODO: This is a very naive implementation and does not really make // full use of the post-Writergate API. However, since we know that // this is going into an Allocating writer anyways, we can be a bit // less strict here.
var count: usize = 0; for (data[0 .. data.len - 1]) |chunk| try self.writeEscaped(chunk, &count);
for (0..splat) |_| try self.writeEscaped(data[data.len - 1], &count); return count; }
fn writeEscaped( self: *ShellEscapeWriter, s: []const u8, count: *usize, ) Writer.Error!void { for (s) |byte| { const buf = switch (byte) { '\\', '"', '\'', '$', '`', '*', '?', ' ', '|', '(', ')', => &[_]u8{ '\\', byte }, else => &[_]u8{byte}, }; try self.child.writeAll(buf); count.* += 1; } }};
test "shell escape 1" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); var shell: ShellEscapeWriter = .init(&writer); try shell.writer.writeAll("abc"); try testing.expectEqualStrings("abc", writer.buffered());}
test "shell escape 2" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); var shell: ShellEscapeWriter = .init(&writer); try shell.writer.writeAll("a c"); try testing.expectEqualStrings("a\\ c", writer.buffered());}
test "shell escape 3" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); var shell: ShellEscapeWriter = .init(&writer); try shell.writer.writeAll("a?c"); try testing.expectEqualStrings("a\\?c", writer.buffered());}
test "shell escape 4" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); var shell: ShellEscapeWriter = .init(&writer); try shell.writer.writeAll("a\\c"); try testing.expectEqualStrings("a\\\\c", writer.buffered());}
test "shell escape 5" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); var shell: ShellEscapeWriter = .init(&writer); try shell.writer.writeAll("a|c"); try testing.expectEqualStrings("a\\|c", writer.buffered());}
test "shell escape 6" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); var shell: ShellEscapeWriter = .init(&writer); try shell.writer.writeAll("a\"c"); try testing.expectEqualStrings("a\\\"c", writer.buffered());}
test "shell escape 7" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); var shell: ShellEscapeWriter = .init(&writer); try shell.writer.writeAll("a(1)"); try testing.expectEqualStrings("a\\(1\\)", writer.buffered());}