diff --git a/src/bot.zig b/src/bot.zig index c9267b6..8f0827c 100644 --- a/src/bot.zig +++ b/src/bot.zig @@ -1,6 +1,55 @@ const std = @import("std"); const zircon = @import("zircon"); +pub const Parser = struct { + original: []const u8, + rest: []const u8, + end_idx: usize, + + fn seek(self: *const Parser, skip: usize) Parser { + return .{ .original = self.original, .rest = self.rest[skip..], .end_idx = self.end_idx + skip }; + } + + fn init(s: []const u8) Parser { + return .{ + .original = s, + .end_idx = 0, + .rest = s, + }; + } + + fn consume_char(self: *const Parser, c: u8) ?Parser { + if (self.rest[0] != c) { + return null; + } + return self.seek(1); + } + + fn consume_str(self: *const Parser, s: []const u8) ?Parser { + const len = s.len; + if (self.rest.len < len) { + return null; + } + if (!std.mem.eql(u8, self.rest[0..len], s)) { + return null; + } + return self.seek(len); + } + + fn take_until_char(self: *const Parser, c: u8) struct { Parser, []const u8 } { + const idx = std.mem.indexOfScalar(u8, self.rest, c) orelse unreachable; + return .{ self.seek(idx), self.rest[0..idx] }; + } + + fn take_char(self: *const Parser) struct { Parser, u8 } { + return .{ self.seek(1), self.rest[0] }; + } + + fn parsed(self: *const Parser) []const u8 { + return self.original[0..self.end_idx]; + } +}; + /// AdminCommand are commands useful for debugging zigeru, since they /// are more spammy than others they are separated and only sent to /// #eru-admin. @@ -9,18 +58,19 @@ pub const AdminCommand = union(enum) { status: void, pub fn parse(text: []const u8) ?AdminCommand { - if (text.len < 2) return null; - - if (std.mem.eql(u8, text, "!status")) { - return .status; - } - - if (text.len > 8 and std.mem.eql(u8, text[0..8], "!backlog")) { - const history = std.fmt.parseInt(u16, text[9..], 10) catch |err| { - std.debug.print("failed to parse int ('{s}') with error: {}\n", .{ text[8..], err }); - return null; - }; - return .{ .backlog = .{ .history = history } }; + const original = Parser.init(text); + if (original.consume_char('!')) |command| { + if (command.consume_str("status")) |_| { + return .status; + } + if (command.consume_str("backlog")) |backlog| { + const history = std.fmt.parseInt(u16, backlog.rest, 10) catch |err| { + std.debug.print("failed to parse int ('{s}') with error: {}\n", .{ backlog.rest, err }); + return null; + }; + return .{ .backlog = .{ .history = history } }; + } + std.log.debug("unknown command: \"{s}\"", .{command.rest}); } return null; } @@ -34,32 +84,29 @@ pub const Command = union(enum) { help: void, pub fn parse(nick: []const u8, text: []const u8) ?Command { - if (std.mem.eql(u8, text, "!help")) { + const original = Parser.init(text); + if (original.consume_str("!help")) |_| { return .help; } - if (text[0] == 's') { - if (text.len == 1) return null; - const delim = switch (text[1]) { - '/', '|', '#' => text[1], - else => { - return null; - }, - }; - if (std.mem.count(u8, text, &.{delim}) != 3) { - // invalid format, we expect three delimiters + if (original.consume_char('s')) |substitute| { + const delim_parser, const delim = substitute.take_char(); + if (std.ascii.isAlphanumeric(delim)) { + std.log.debug("parsing substitute command: delimiter cannot be a whitespace: \"{s}\"", .{text}); + return null; + } + const typo_parser, const typo = delim_parser.take_until_char(delim); + const correction_parser, const correction = typo_parser.consume_char(delim).?.take_until_char(delim); + if (correction_parser.consume_char(delim) == null) { + std.log.debug("parsing substitute command: missing an ending '/' in \"{s}\"", .{text}); return null; } - var parts = std.mem.splitScalar(u8, text, delim); - _ = parts.next() orelse return null; - const needle = parts.next().?; - const replacement = parts.next().?; return .{ .substitute = .{ .author = nick, - .needle = needle, - .replacement = replacement, - .all = if (parts.next()) |flags| std.mem.eql(u8, flags, "g") else false, + .needle = typo, + .replacement = correction, + .all = false, }, }; }