fix: grammar payload token matches bytes, not characters - #4
Conversation
The NATS wire protocol uses byte counts, but . ** {$size} in Raku
regex matches CHARACTERS (graphemes), not bytes. For multi-byte
UTF-8 payloads like 'Olá' (3 chars, 4 bytes), this caused the
grammar to fail and corrupt the buffer permanently.
Fix: use .+? <?{ $/.Str.encode('utf8').bytes == $size }> which
matches characters non-greedily until the UTF-8 encoded byte count
equals the wire protocol size.
Adds t/utf8-grammar.rakutest with 37 tests covering:
- Grammar parsing of multi-byte payloads (Olá, coração, emoji, etc.)
- Split-frame buffer reassembly with UTF-8
- Multiple mixed-payload frames in one chunk
- Publish wire format uses correct byte counts
- HPUB with multi-byte payloads
There was a problem hiding this comment.
Pull request overview
Fixes NATS wire-protocol parsing for MSG/HMSG payloads where the protocol’s $size/$tsize fields are byte counts, but Raku regex . quantifiers operate on characters (graphemes)—which breaks parsing for multi-byte UTF-8 payloads and can corrupt the input buffer.
Changes:
- Update
Nats::Grammarpayload/hpayloadtokens to stop matching by character count and instead match until the UTF-8 encoded byte count equals the protocol size. - Add a new UTF-8-focused test suite covering grammar parsing, buffer reassembly, multi-frame chunks, and publish byte-count regression checks.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
lib/Nats/Grammar.rakumod |
Adjust payload matching logic to align with NATS byte-count sizes for UTF-8 payloads. |
t/utf8-grammar.rakutest |
Add regression tests for multi-byte UTF-8 payload parsing and publish formatting. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <( | ||
| . ** { $size } | ||
| # $size is BYTES from NATS wire protocol. . ** {$size} matches | ||
| # CHARACTERS which fails for multi-byte UTF-8 (Olá: 3 chars ≠ 4 bytes). | ||
| # Match characters until their UTF-8 encoded byte count equals $size. | ||
| .+? <?{ $/.Str.encode('utf8').bytes == $size }> | ||
| )> |
| token hpayload(UInt $hsize, UInt $tsize) { | ||
| <( | ||
| . ** { $tsize } | ||
| # $tsize is BYTES; match characters until byte count matches | ||
| .+? <?{ $/.Str.encode('utf8').bytes == $tsize }> | ||
| )> |
| my $nats = Nats.new: :$socket-class; | ||
| $nats.start; | ||
|
|
||
| $nats.publish: "hdr", "café", :header({ :Content-Type<text/plain> }); |
Code Review —
|
Problem
Nats::Grammartokenspayloadandhpayloadused. ** { $size }to match payload content, where$sizeis the byte count from the NATS wire protocol. However,.in Raku regex matches characters (graphemes), not bytes.For ASCII payloads, 1 character = 1 byte → works fine.
For multi-byte UTF-8 payloads like
Olá(3 chars, 4 bytes), the match fails permanently and corrupts the buffer, blocking all subsequent message processing.Fix
Replace
. ** { $size }with:This matches characters non-greedily until the UTF-8 encoded byte count equals the wire protocol size.
Test Coverage
New file
t/utf8-grammar.rakutestwith 37 tests:Full test suite: 261 tests passing (all 13 test files green).