-- decode_unsafe: codegen emits a sister <Msg>_decode_unsafe alongside
-- <Msg>_decode that skips the per-string utf8_len validation. Intended
-- for re-decoding bytes from a trusted producer (own encoder, JSON/text
-- round-trip, in-process typed RPC). Emitted in full mode only; runtime
-- mode does not currently expose the unsafe path (compiled f._reader
-- closures capture handler.decode by value, so a swap-on-call would not
-- reach them — a proper runtime-mode unsafe path would need parallel
-- _reader_unsafe closures and is intentionally deferred). (6bb)
local t = require('luatest')
local hello = require('full.hello.hello_pb')
local g = t.group('decode_unsafe.full')
-- Hand-rolled wire bytes for hello.Address{street=<s>}. Tag for field 1
-- (wire 2, LEN) is 0x0A; length-prefix is one varint byte for len<128.
local function address_with_street(s)
return string.char(0x0A, #s) .. s
end
-- Hand-rolled wire bytes for hello.Person{name=<s>}. Tag for field 1
-- is identical (0x0A).
local function person_with_name(s)
return string.char(0x0A, #s) .. s
end
g.test_valid_string_matches_safe_decode = function()
local addr = {street = 'Pushkina 1', city = 'Moscow', zip = 123456}
local bytes = hello.Address_encode(addr)
t.assert_equals(hello.Address_decode_unsafe(bytes),
hello.Address_decode(bytes))
end
g.test_safe_decode_rejects_invalid_utf8 = function()
-- 0xC0 0x80 is the classic overlong NUL — rejected by RFC 3629
-- (also banned in proto3 strings).
local bytes = address_with_street('\xC0\x80')
t.assert_error_msg_contains(
'invalid UTF-8',
function() hello.Address_decode(bytes) end)
end
g.test_unsafe_decode_accepts_invalid_utf8 = function()
local bytes = address_with_street('\xC0\x80')
local dec = hello.Address_decode_unsafe(bytes)
t.assert_equals(dec.street, '\xC0\x80')
end
g.test_unsafe_decode_repeated_string = function()
-- Person.emails is a repeated string; two entries, second is invalid.
-- Tag 0x1A = field 3 (emails), wire 2.
local good = 'alice@example.com'
local bad = '\xFF\xFE'
local bytes = string.char(0x1A, #good) .. good
.. string.char(0x1A, #bad) .. bad
t.assert_error_msg_contains(
'invalid UTF-8',
function() hello.Person_decode(bytes) end)
local dec = hello.Person_decode_unsafe(bytes)
t.assert_equals(dec.emails, {good, bad})
end
g.test_unsafe_decode_recurses_into_sub_messages = function()
-- Person{address = Address{street = '\xC0\x80'}}.
-- Tag 0x2A = field 5 (address), wire 2; payload is the Address bytes.
local inner = address_with_street('\xC0\x80')
local bytes = string.char(0x2A, #inner) .. inner
-- Safe path: nested string rejected (proves nested validation runs by
-- default).
t.assert_error_msg_contains(
'invalid UTF-8',
function() hello.Person_decode(bytes) end)
-- Unsafe path: nested call must also be the unsafe variant. If
-- Person_decode_unsafe were to call Address_decode (the safe variant)
-- for sub-messages, this would still error. The recursive dispatch is
-- emitted by inline.go and pinned by this assertion.
local dec = hello.Person_decode_unsafe(bytes)
t.assert_equals(dec.address.street, '\xC0\x80')
end
g.test_unsafe_decode_handles_long_string_fallback = function()
-- >=128 byte payload exits the 1-byte LEN inline fast path and falls
-- through to wire.decode_bytes (instead of wire.decode_string) on
-- the unsafe path. Exercises the fallback branch in the emitted code.
local big = string.rep('x', 200) .. '\xFF' -- 201 bytes, trailing bad
local bytes = string.char(0x0A) .. string.char(0xC9, 0x01) .. big
-- 201 in varint = 0xC9 0x01.
t.assert_error_msg_contains(
'invalid UTF-8',
function() hello.Address_decode(bytes) end)
local dec = hello.Address_decode_unsafe(bytes)
t.assert_equals(#dec.street, 201)
t.assert_equals(dec.street:byte(201), 0xFF)
end
-- Person.name omitted from the above explicitly to keep tests focused;
-- the singular-string scalar path is already covered by Address.street.
_ = person_with_name -- silence unused-local under future trimming