a73x

2dadb5e0

scenario: reject size=0, out-of-bounds asserts, empty bytes-hex

a73x   2026-04-19 09:19

Commit message
scenario: reject size=0, out-of-bounds asserts, empty bytes-hex

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>

src/scenario.zig
Old New
@@ -231,6 +231,10 @@ pub fn parse(
231 diag.message = "size rows must be a positive integer"; 231 diag.message = "size rows must be a positive integer";
232 return error.ParseFailed; 232 return error.ParseFailed;
233 }; 233 };
234 if (cols == 0 or rows == 0) {
235 diag.* = .{ .line = line_num, .message = "size dimensions must be non-zero" };
236 return error.ParseFailed;
237 }
234 has_size = true; 238 has_size = true;
235 } else if (std.mem.eql(u8, kw, "timeout")) { 239 } else if (std.mem.eql(u8, kw, "timeout")) {
236 if (!has_size) { 240 if (!has_size) {
@@ -311,6 +315,10 @@ pub fn parse(
311 }; 315 };
312 try hex_buf.append(alloc, byte); 316 try hex_buf.append(alloc, byte);
313 } 317 }
318 if (hex_buf.items.len == 0) {
319 diag.* = .{ .line = line_num, .message = "bytes-hex requires at least one token" };
320 return error.ParseFailed;
321 }
314 try directives.append(alloc, .{ .bytes = try hex_buf.toOwnedSlice(alloc) }); 322 try directives.append(alloc, .{ .bytes = try hex_buf.toOwnedSlice(alloc) });
315 } else if (std.mem.eql(u8, kw, "capture")) { 323 } else if (std.mem.eql(u8, kw, "capture")) {
316 const lbl_res = nextToken(line, pos) orelse { 324 const lbl_res = nextToken(line, pos) orelse {
@@ -367,6 +375,10 @@ pub fn parse(
367 diag.message = "unknown predicate"; 375 diag.message = "unknown predicate";
368 return error.ParseFailed; 376 return error.ParseFailed;
369 }; 377 };
378 if (row >= rows or col >= cols) {
379 diag.* = .{ .line = line_num, .message = "assert-cell row/col out of bounds for scenario size" };
380 return error.ParseFailed;
381 }
370 try directives.append(alloc, .{ .assert_cell = .{ .row = row, .col = col, .pred = pred } }); 382 try directives.append(alloc, .{ .assert_cell = .{ .row = row, .col = col, .pred = pred } });
371 } else if (std.mem.eql(u8, kw, "assert-cell-at")) { 383 } else if (std.mem.eql(u8, kw, "assert-cell-at")) {
372 const lbl_res = nextToken(line, pos) orelse { 384 const lbl_res = nextToken(line, pos) orelse {
@@ -416,6 +428,10 @@ pub fn parse(
416 diag.message = "unknown predicate"; 428 diag.message = "unknown predicate";
417 return error.ParseFailed; 429 return error.ParseFailed;
418 }; 430 };
431 if (row >= rows or col >= cols) {
432 diag.* = .{ .line = line_num, .message = "assert-cell row/col out of bounds for scenario size" };
433 return error.ParseFailed;
434 }
419 try directives.append(alloc, .{ .assert_cell_at = .{ 435 try directives.append(alloc, .{ .assert_cell_at = .{
420 .label = label_copy, 436 .label = label_copy,
421 .row = row, 437 .row = row,
@@ -697,3 +713,42 @@ test "parse: unknown predicate fails at line" {
697 ); 713 );
698 try std.testing.expectEqual(@as(usize, 3), diag.line); 714 try std.testing.expectEqual(@as(usize, 3), diag.line);
699 } 715 }
716
717 test "parse: size 0 0 rejected" {
718 const diag = try parseErr(
719 \\size 0 0
720 \\timeout 500ms
721 );
722 try std.testing.expectEqual(@as(usize, 1), diag.line);
723 try std.testing.expect(std.mem.indexOf(u8, diag.message, "non-zero") != null);
724 }
725
726 test "parse: assert-cell row out of bounds" {
727 const diag = try parseErr(
728 \\size 80 24
729 \\timeout 500ms
730 \\assert-cell 24 0 cell-empty
731 );
732 try std.testing.expectEqual(@as(usize, 3), diag.line);
733 try std.testing.expect(std.mem.indexOf(u8, diag.message, "out of bounds") != null);
734 }
735
736 test "parse: assert-cell-at col out of bounds" {
737 const diag = try parseErr(
738 \\size 80 24
739 \\timeout 500ms
740 \\assert-cell-at foo 0 80 cell-empty
741 );
742 try std.testing.expectEqual(@as(usize, 3), diag.line);
743 try std.testing.expect(std.mem.indexOf(u8, diag.message, "out of bounds") != null);
744 }
745
746 test "parse: bytes-hex rejects empty token list" {
747 const diag = try parseErr(
748 \\size 80 24
749 \\timeout 500ms
750 \\bytes-hex
751 );
752 try std.testing.expectEqual(@as(usize, 3), diag.line);
753 try std.testing.expect(std.mem.indexOf(u8, diag.message, "at least one token") != null);
754 }