2dadb5e0
scenario: reject size=0, out-of-bounds asserts, empty bytes-hex
a73x 2026-04-19 09:19
Three parse-time validations the parser was silently accepting. All three surface a line-numbered Diagnostic and error.ParseFailed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
diff --git a/src/scenario.zig b/src/scenario.zig index 2d22461..2dd3f50 100644 --- a/src/scenario.zig +++ b/src/scenario.zig @@ -231,6 +231,10 @@ pub fn parse( diag.message = "size rows must be a positive integer"; return error.ParseFailed; }; if (cols == 0 or rows == 0) { diag.* = .{ .line = line_num, .message = "size dimensions must be non-zero" }; return error.ParseFailed; } has_size = true; } else if (std.mem.eql(u8, kw, "timeout")) { if (!has_size) { @@ -311,6 +315,10 @@ pub fn parse( }; try hex_buf.append(alloc, byte); } if (hex_buf.items.len == 0) { diag.* = .{ .line = line_num, .message = "bytes-hex requires at least one token" }; return error.ParseFailed; } try directives.append(alloc, .{ .bytes = try hex_buf.toOwnedSlice(alloc) }); } else if (std.mem.eql(u8, kw, "capture")) { const lbl_res = nextToken(line, pos) orelse { @@ -367,6 +375,10 @@ pub fn parse( diag.message = "unknown predicate"; return error.ParseFailed; }; if (row >= rows or col >= cols) { diag.* = .{ .line = line_num, .message = "assert-cell row/col out of bounds for scenario size" }; return error.ParseFailed; } try directives.append(alloc, .{ .assert_cell = .{ .row = row, .col = col, .pred = pred } }); } else if (std.mem.eql(u8, kw, "assert-cell-at")) { const lbl_res = nextToken(line, pos) orelse { @@ -416,6 +428,10 @@ pub fn parse( diag.message = "unknown predicate"; return error.ParseFailed; }; if (row >= rows or col >= cols) { diag.* = .{ .line = line_num, .message = "assert-cell row/col out of bounds for scenario size" }; return error.ParseFailed; } try directives.append(alloc, .{ .assert_cell_at = .{ .label = label_copy, .row = row, @@ -697,3 +713,42 @@ test "parse: unknown predicate fails at line" { ); try std.testing.expectEqual(@as(usize, 3), diag.line); } test "parse: size 0 0 rejected" { const diag = try parseErr( \\size 0 0 \\timeout 500ms ); try std.testing.expectEqual(@as(usize, 1), diag.line); try std.testing.expect(std.mem.indexOf(u8, diag.message, "non-zero") != null); } test "parse: assert-cell row out of bounds" { const diag = try parseErr( \\size 80 24 \\timeout 500ms \\assert-cell 24 0 cell-empty ); try std.testing.expectEqual(@as(usize, 3), diag.line); try std.testing.expect(std.mem.indexOf(u8, diag.message, "out of bounds") != null); } test "parse: assert-cell-at col out of bounds" { const diag = try parseErr( \\size 80 24 \\timeout 500ms \\assert-cell-at foo 0 80 cell-empty ); try std.testing.expectEqual(@as(usize, 3), diag.line); try std.testing.expect(std.mem.indexOf(u8, diag.message, "out of bounds") != null); } test "parse: bytes-hex rejects empty token list" { const diag = try parseErr( \\size 80 24 \\timeout 500ms \\bytes-hex ); try std.testing.expectEqual(@as(usize, 3), diag.line); try std.testing.expect(std.mem.indexOf(u8, diag.message, "at least one token") != null); }