fix(commands): substitute parsing bug

There was a bug in the parsing logic that caused a substitution
command like `s/typo/correction` to crash the bot.

Correct command is of course `s/typo/correction/`, but now it at least
shouldn't crash.
This commit is contained in:
Jacob Jonsson 2026-03-11 01:19:33 +01:00
parent 4e11cc9ea1
commit 8b15398196
Signed by: Jassob
GPG key ID: 7E30B9B047F7202E
2 changed files with 33 additions and 12 deletions

View file

@ -1,6 +1,6 @@
const std = @import("std");
const parser = @import("root.zig").parser;
const Parser = @import("root.zig").parser.Parser;
/// UserCommand represents the commands that ordinary IRC users can use.
pub const UserCommand = union(enum) {
@ -10,23 +10,34 @@ pub const UserCommand = union(enum) {
help: void,
pub fn parse(nick: []const u8, text: []const u8) ?UserCommand {
const original = parser.init(text);
const original = Parser.init(text);
if (original.consume_str("!help")) |_| {
return .help;
}
if (original.consume_char('s')) |substitute| {
const delim_parser, const delim = substitute.take_char();
const log_prefix = "parsing substitute command";
var parser = substitute;
parser, const delim = parser.take_char();
if (std.ascii.isAlphanumeric(delim)) {
std.log.debug("parsing substitute command: delimiter cannot be a whitespace: \"{s}\"", .{text});
std.log.debug("{s}: delimiter cannot be a whitespace: \"{s}\"", .{ log_prefix, 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});
var result = parser.take_until_char(delim);
if (result == null) {
std.log.debug(
"{s}: cannot find typo, expecting a message on the form 's{}TYPO{}CORRECTION{}', but got {s}",
.{ log_prefix, delim, delim, delim, text },
);
return null;
}
parser, const typo = result.?;
result = parser.consume_char(delim).?.take_until_char(delim);
if (result == null) {
std.log.debug("{s}: missing an ending '/' in \"{s}\"", .{ log_prefix, text });
return null;
}
parser, const correction = result.?;
return .{
.substitute = .{
.author = nick,
@ -50,7 +61,7 @@ pub const AdminCommand = union(enum) {
err: struct { message: []const u8 },
pub fn parse(text: []const u8) ?AdminCommand {
const original = parser.init(text);
const original = Parser.init(text);
if (original.consume_char('!')) |command| {
if (command.consume_str("status")) |_| {
return .status;
@ -97,6 +108,14 @@ test "can parse s/hello/world/ successful" {
);
}
test "can parse s/hello/world and report failure" {
try std.testing.expectEqualDeep(null, UserCommand.parse("jassob", "s/hello/world"));
}
test "can parse s/hello|world| and report failure" {
try std.testing.expectEqualDeep(null, UserCommand.parse("jassob", "s/hello/world"));
}
test "correctly ignores non-messages when trying to parse" {
try std.testing.expectEqualDeep(null, UserCommand.parse("jassob", "Hello, world"));
}

View file

@ -61,9 +61,11 @@ pub const Parser = struct {
//
// Returns a new parser window that starts after idx and the
// extracted byte slice.
pub 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] };
pub fn take_until_char(self: *const Parser, c: u8) ?struct { Parser, []const u8 } {
if (std.mem.indexOfScalar(u8, self.rest, c)) |idx| {
return .{ self.seek(idx), self.rest[0..idx] };
}
return null;
}
// Take the current character and advance the parser one step.