-- Test for bd-wyp / ra6 3j: C-side unknown-fields capture + re-emission. -- -- A C-runtime decoder that meets fields not in the plan must capture -- their raw bytes into result._unknown_fields, and a subsequent C -- encode must re-emit them verbatim — same contract as -- test/unknown_test.lua against the pure-Lua paths. -- -- Gated on PB_ENABLE_C=1 + a loadable c_runtime module, same as the -- sibling c_runtime_*_test.lua files. local t = require('luatest') local pb = require('pb') local c_runtime = pb.c_runtime local function skip_if_no_c() if c_runtime == nil then t.skip('PB_ENABLE_C not set or pb.c_runtime not available') end end local function hex(s) local out = {} for i = 1, #s do out[i] = string.format('%02x', s:byte(i)) end return table.concat(out) end -- Wire-tag for (field_id, wire_type). Multi-byte varint emission for -- field ids > 15 — Address has fields 1..4 declared, so 50/51/52/53 are -- guaranteed unknown. local function tag_bytes(id, wt) local v = id * 8 + wt local out = {} while v >= 0x80 do out[#out + 1] = string.char(v % 0x80 + 0x80) v = math.floor(v / 0x80) end out[#out + 1] = string.char(v) return table.concat(out) end -- One byte-slice per wire type. Field ids picked outside Address (1..4). local UNK_VARINT = tag_bytes(50, 0) .. '\x2a' -- value 42 local UNK_I32 = tag_bytes(51, 5) .. '\x01\x00\x00\x00' -- value 1 local UNK_I64 = tag_bytes(52, 1) .. '\x02\x00\x00\x00\x00\x00\x00\x00' local UNK_LEN = tag_bytes(53, 2) .. '\x03foo' -- 3-byte string local ALL_UNK = UNK_VARINT .. UNK_I32 .. UNK_I64 .. UNK_LEN local full_hello for _, mode in ipairs({'full', 'runtime'}) do local g = t.group('c_runtime_unknown.' .. mode) local hello local plan g.before_all(function() skip_if_no_c() hello = require(mode .. '.hello.hello_pb') full_hello = require('full.hello.hello_pb') plan = c_runtime.compile_plan(hello.Address_descriptor) end) g.before_each(skip_if_no_c) function g.test_no_unknown_means_field_absent() local dec = c_runtime.decode(plan, c_runtime.encode(plan, {street = 'X'})) t.assert_equals(dec.street, 'X') t.assert_equals(dec._unknown_fields, nil, '_unknown_fields must be absent when input had only known fields') end function g.test_varint_unknown_round_trips() local known = c_runtime.encode(plan, {street = 'X', zip = 7}) local mixed = known .. UNK_VARINT local dec = c_runtime.decode(plan, mixed) t.assert_equals(dec.street, 'X') t.assert_equals(dec.zip, 7) t.assert_equals(hex(dec._unknown_fields), hex(UNK_VARINT)) end function g.test_all_wire_types_captured_in_order() local known = c_runtime.encode(plan, {street = 'X'}) local mixed = known .. ALL_UNK local dec = c_runtime.decode(plan, mixed) t.assert_equals(dec.street, 'X') t.assert_equals(hex(dec._unknown_fields), hex(ALL_UNK), 'all four wire types must be captured verbatim in source order') end function g.test_unknowns_interleaved_with_knowns() -- Bytes order: unknown, known, unknown — capture must preserve -- the two unknown chunks in encounter order, knowns stay parsed. local known1 = c_runtime.encode(plan, {street = 'A'}) local known2 = c_runtime.encode(plan, {zip = 99}) local mixed = UNK_VARINT .. known1 .. UNK_I64 .. known2 local dec = c_runtime.decode(plan, mixed) t.assert_equals(dec.street, 'A') t.assert_equals(dec.zip, 99) t.assert_equals(hex(dec._unknown_fields), hex(UNK_VARINT .. UNK_I64)) end function g.test_re_encode_preserves_unknown_bytes() local mixed = c_runtime.encode(plan, {street = 'X'}) .. ALL_UNK local dec = c_runtime.decode(plan, mixed) local re_enc = c_runtime.encode(plan, dec) local expected = c_runtime.encode(plan, {street = 'X'}) .. ALL_UNK t.assert_equals(hex(re_enc), hex(expected), 'unknown bytes must be re-emitted verbatim at the tail') end function g.test_re_encode_decode_idempotent() local mixed = c_runtime.encode(plan, {street = 'X', zip = 1}) .. UNK_LEN local dec1 = c_runtime.decode(plan, mixed) local dec2 = c_runtime.decode(plan, c_runtime.encode(plan, dec1)) t.assert_equals(dec2.street, 'X') t.assert_equals(dec2.zip, 1) t.assert_equals(hex(dec2._unknown_fields), hex(UNK_LEN)) end function g.test_empty_unknown_string_treated_as_absent() -- User explicitly sets _unknown_fields = '' on encode; must be -- a no-op (mirrors codec.lua's nil/'' short-circuit). local enc = c_runtime.encode(plan, {street = 'X', _unknown_fields = ''}) t.assert_equals(hex(enc), hex(c_runtime.encode(plan, {street = 'X'}))) end function g.test_capture_matches_pure_lua_decode() -- Acceptance per bd-wyp: shape-for-shape parity with the pure-Lua -- decode for the same mixed input. local mixed = full_hello.Address_encode({street = 'X'}) .. ALL_UNK local c_dec = c_runtime.decode(plan, mixed) local lua_dec = full_hello.Address_decode(mixed) t.assert_equals(hex(c_dec._unknown_fields), hex(lua_dec._unknown_fields)) end function g.test_reencode_matches_pure_lua_with_seeded_unknown_bytes() -- Table with _unknown_fields set — encode under C must agree -- byte-for-byte with the pure-Lua encoder. local tbl = {street = 'X', zip = 9, _unknown_fields = ALL_UNK} t.assert_equals( hex(c_runtime.encode(plan, tbl)), hex(full_hello.Address_encode(tbl))) end end -- --------------------------------------------------------------------------- -- Nested-message coverage: each recursive decode_body gets its own -- _unknown_fields buffer — parent and child must not bleed into each -- other. -- --------------------------------------------------------------------------- local g_nested = t.group('c_runtime_unknown.nested') local full_hello_n g_nested.before_all(function() skip_if_no_c() full_hello_n = require('full.hello.hello_pb') end) g_nested.before_each(skip_if_no_c) g_nested.test_inner_unknown_isolated_from_outer = function() -- Build a Person whose nested address payload includes an unknown -- trailing tag. The outer Person decode must NOT inherit the inner -- _unknown_fields. Acceptance for the per-frame buffer isolation. local person_plan = c_runtime.compile_plan( full_hello_n.Person_descriptor) local addr_plan = c_runtime.compile_plan( full_hello_n.Address_descriptor) -- inner address body = encode({street = 'X'}) plus an unknown tag local addr_body = c_runtime.encode(addr_plan, {street = 'X'}) .. UNK_VARINT -- outer person body wraps that as a length-delimited field id=5 -- (Person.address). Tag = (5<<3)|2 = 0x2A. local function uvarint(v) local out = {} while v >= 0x80 do out[#out + 1] = string.char(v % 0x80 + 0x80) v = math.floor(v / 0x80) end out[#out + 1] = string.char(v) return table.concat(out) end local person_body = '\x2a' .. uvarint(#addr_body) .. addr_body local dec = c_runtime.decode(person_plan, person_body) t.assert_equals(dec._unknown_fields, nil, 'outer Person must not carry the inner Address unknown bytes') t.assert_equals(dec.address.street, 'X') t.assert_equals(hex(dec.address._unknown_fields), hex(UNK_VARINT)) end