authorgravatar for mail@isaacfreund.comIsaac Freund <mail@isaacfreund.com> 2026-07-03 15:54:40+02:00
committergravatar for mail@isaacfreund.comIsaac Freund <mail@isaacfreund.com> 2026-07-08 11:49:00+02:00
log080ad9a4522f7d8eb756d47ffd0c7bfa61851446
tree3f3462ab55231f3ec9082016ea3222531b46b569
parent62720ad9f9e65dd8c62579c90142cb043d60b01c
signaturelock-open Commit is signed but in an unrecognized format.

grammar: handle byte order mark

The tokenizer allows the unicode byte order mark (U+FEFF) to be present at the start of the file, but the grammar currently does not. Fixing the grammar requires some care due to how same-line doc comments are forbidden in the grammar.

3 files changed, 29 insertions(+), 4 deletions(-)

doc/langref/grammar.peg+5-2
......@@ -450,11 +450,14 @@ ExprPrefix <- ASTERISK
450450
451451# *** Tokens ***
452452
453# https://en.wikipedia.org/wiki/Byte_order_mark
454byte_order_mark <- '\357\273\277'
455
453456# Unfortunately, there is not a standard way to match the start of the file in PEG.
454457# This rule definition is compatible with the original peg(1) tool but is not
455458# portable. It is however trivial to implement an equivalent rule in hand written
456459# parsers and other PEG tooling should have similar mechanisms.
457sof <- &{ (yy->__pos == 0) }
460sof <- &{ (yy->__pos == 0) } byte_order_mark?
458461eof <- !.
459462
460463ox80_oxBF <- [\200-\277]
......@@ -536,7 +539,7 @@ line_string <- '\\\\' non_control_utf8* newline
536539# the newline terminating a multiline string literal or doc comment visible
537540# to the pre_op_white non-terminal.
538541newline <- &("\n" / "\r\n" / eof)
539skip <- ([ \n\t\r] / line_comment)*
542skip <- sof? ([ \n\t\r] / line_comment)*
540543skip_require_newline <- [ \t\r]* ([\n] / line_comment) skip
541544pre_op_white <- ([ \n\t\r] / line_comment)+
542545post_op_white <- [ \n\t\r] skip
lib/std/zig/parser_fuzz.zig+8
......@@ -119,6 +119,14 @@ test "catch capture whitespace" {
119119 try checkAgainstOracle("test{0 catch |h|0;}");
120120}
121121
122// Found using AFL++
123test "byte order mark" {
124 // https://en.wikipedia.org/wiki/Byte_order_mark
125 try checkAgainstOracle("\xef\xbb\xbf");
126 try checkAgainstOracle("\xef\xbb\xbf///\n0");
127}
128
129>>>>>>> bc46a69fba (grammar: handle byte order mark)
122130fn checkAgainstOracle(source: [:0]const u8) !void {
123131 var fba_buf: [1 << 18]u8 = undefined;
124132 var fba: std.heap.FixedBufferAllocator = .init(&fba_buf);
lib/std/zig/parser_generated_oracle.zig+16-2
......@@ -2327,10 +2327,24 @@ const Parser = struct {
23272327 break :blk_0 false;
23282328 };
23292329 }
2330 pub fn parsebyte_order_mark(p: *Parser) Error!bool {
2331 return blk_0: {
2332 const pos_0 = p.i;
2333 if (blk_1: {
2334 if (std.mem.startsWith(u8, p.source[p.i..], "\xef\xbb\xbf")) {
2335 p.i += 3;
2336 break :blk_1 true;
2337 }
2338 break :blk_1 false;
2339 }) break :blk_0 true;
2340 p.i = pos_0;
2341 break :blk_0 false;
2342 };
2343 }
23302344 pub fn parsesof(p: *Parser) Error!bool {
23312345 return blk_0: {
23322346 const pos_0 = p.i;
2333 if ((p.i == 0)) break :blk_0 true;
2347 if ((p.i == 0) and (try p.parsebyte_order_mark() or true)) break :blk_0 true;
23342348 p.i = pos_0;
23352349 break :blk_0 false;
23362350 };
......@@ -2854,7 +2868,7 @@ const Parser = struct {
28542868 pub fn parseskip(p: *Parser) Error!bool {
28552869 return blk_0: {
28562870 const pos_0 = p.i;
2857 if (blk_1: {
2871 if ((try p.parsesof() or true) and blk_1: {
28582872 var i_1: usize = 0;
28592873 while (blk_3: {
28602874 const pos_3 = p.i;