const std = @import("std");
const testing = std.testing;

/// 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.
///
/// T should be a Zig type that follows the `std.io.Writer` interface.
pub fn ShellEscapeWriter(comptime T: type) type {
    return struct {
        child_writer: T,

        fn write(self: *ShellEscapeWriter(T), data: []const u8) error{Error}!usize {
            var count: usize = 0;
            for (data) |byte| {
                const buf = switch (byte) {
                    '\\',
                    '"',
                    '\'',
                    '$',
                    '`',
                    '*',
                    '?',
                    ' ',
                    '|',
                    '(',
                    ')',
                    => &[_]u8{ '\\', byte },
                    else => &[_]u8{byte},
                };
                self.child_writer.writeAll(buf) catch return error.Error;
                count += 1;
            }
            return count;
        }

        const Writer = std.io.Writer(*ShellEscapeWriter(T), error{Error}, write);

        pub fn init(child_writer: T) ShellEscapeWriter(T) {
            return .{ .child_writer = child_writer };
        }

        pub fn writer(self: *ShellEscapeWriter(T)) Writer {
            return .{ .context = self };
        }
    };
}

test "shell escape 1" {
    var buf: [128]u8 = undefined;
    var fmt = std.io.fixedBufferStream(&buf);
    var shell: ShellEscapeWriter(@TypeOf(fmt).Writer) = .{ .child_writer = fmt.writer() };
    const writer = shell.writer();
    try writer.writeAll("abc");
    try testing.expectEqualStrings("abc", fmt.getWritten());
}

test "shell escape 2" {
    var buf: [128]u8 = undefined;
    var fmt = std.io.fixedBufferStream(&buf);
    var shell: ShellEscapeWriter(@TypeOf(fmt).Writer) = .{ .child_writer = fmt.writer() };
    const writer = shell.writer();
    try writer.writeAll("a c");
    try testing.expectEqualStrings("a\\ c", fmt.getWritten());
}

test "shell escape 3" {
    var buf: [128]u8 = undefined;
    var fmt = std.io.fixedBufferStream(&buf);
    var shell: ShellEscapeWriter(@TypeOf(fmt).Writer) = .{ .child_writer = fmt.writer() };
    const writer = shell.writer();
    try writer.writeAll("a?c");
    try testing.expectEqualStrings("a\\?c", fmt.getWritten());
}

test "shell escape 4" {
    var buf: [128]u8 = undefined;
    var fmt = std.io.fixedBufferStream(&buf);
    var shell: ShellEscapeWriter(@TypeOf(fmt).Writer) = .{ .child_writer = fmt.writer() };
    const writer = shell.writer();
    try writer.writeAll("a\\c");
    try testing.expectEqualStrings("a\\\\c", fmt.getWritten());
}

test "shell escape 5" {
    var buf: [128]u8 = undefined;
    var fmt = std.io.fixedBufferStream(&buf);
    var shell: ShellEscapeWriter(@TypeOf(fmt).Writer) = .{ .child_writer = fmt.writer() };
    const writer = shell.writer();
    try writer.writeAll("a|c");
    try testing.expectEqualStrings("a\\|c", fmt.getWritten());
}

test "shell escape 6" {
    var buf: [128]u8 = undefined;
    var fmt = std.io.fixedBufferStream(&buf);
    var shell: ShellEscapeWriter(@TypeOf(fmt).Writer) = .{ .child_writer = fmt.writer() };
    const writer = shell.writer();
    try writer.writeAll("a\"c");
    try testing.expectEqualStrings("a\\\"c", fmt.getWritten());
}

test "shell escape 7" {
    var buf: [128]u8 = undefined;
    var fmt = std.io.fixedBufferStream(&buf);
    var shell: ShellEscapeWriter(@TypeOf(fmt).Writer) = .{ .child_writer = fmt.writer() };
    const writer = shell.writer();
    try writer.writeAll("a(1)");
    try testing.expectEqualStrings("a\\(1\\)", fmt.getWritten());
}
