diff --git a/lib/Nats/Grammar.rakumod b/lib/Nats/Grammar.rakumod index cb9d66c..5b9dbea 100644 --- a/lib/Nats/Grammar.rakumod +++ b/lib/Nats/Grammar.rakumod @@ -13,7 +13,10 @@ token sid { \d+ } token size { \d+ } token payload(UInt $size) { <( - . ** { $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. + .+? )> \n @@ -22,7 +25,8 @@ token hsize { \d+ } token tsize { \d+ } token hpayload(UInt $hsize, UInt $tsize) { <( - . ** { $tsize } + # $tsize is BYTES; match characters until byte count matches + .+? )> \n diff --git a/t/utf8-grammar.rakutest b/t/utf8-grammar.rakutest new file mode 100644 index 0000000..2a11510 --- /dev/null +++ b/t/utf8-grammar.rakutest @@ -0,0 +1,159 @@ +#!/usr/bin/env raku + +use Test; +use Test::Mock; + +use lib 'lib'; + +use Nats; +use Nats::Grammar; +use Nats::Message; + +use-ok 'Nats'; + +# Helper: build MSG wire string with correct byte count +sub msg-wire(Str $subject, UInt $sid, Str $payload --> Str) { + my $bytes = $payload.encode('utf8').bytes; + "MSG $subject $sid $bytes\r\n$payload\r\n" +} + +# ═══════════════════════════════════════════════════════════════════ +# Test 1: Grammar parses MSG with multi-byte UTF-8 payloads +# ═══════════════════════════════════════════════════════════════════ + +{ + my Supplier $supplier .= new; + my $conn = mocked IO::Socket::Async, returning => { Supply => $supplier.Supply }; + my $socket-class = mocked IO::Socket::Async, returning => { connect => Promise.kept: $conn }; + my $nats = Nats.new: :$socket-class; + await $nats.start; + + my @cases = ( + { label => 'Olá (4 bytes, 3 chars)', payload => 'Olá' }, + { label => 'coração (9 bytes, 7 chars)', payload => 'coração' }, + { label => 'emoji (multi-byte)', payload => '✅ ❌ 🚀' }, + { label => 'JSON with accents', payload => '{"msg":"Não há dados"}' }, + { label => 'German umlauts', payload => 'München über Köln' }, + { label => 'Mixed ASCII+UTF8', payload => 'Olá mundo!' }, + ); + + for @cases { + my ($label, $payload) = $_