~bigbes/tarantool

tarantool-protobuf

ref: e3f067416da55e4390d1a9b3e1f410ad7a48d22f tarantool-protobuf/test/decode_unsafe_test.lua -rw-r--r-- 5.2 KiB
e3f06741 — Eugene Blikh runtime: pb.decode_unsafe + M.<Name>_decode_unsafe in runtime mode (58u) 2 months ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
-- decode_unsafe: opt-in non-validating decode for trusted producers
-- (re-decoding bytes from own encoder, JSON/text round-trips, in-process
-- typed RPC). Skips per-string utf8_len on every singular/repeated/map
-- string field; sub-message recursion stays on the unsafe path.
--
-- Full mode emits a literal sister <Msg>_decode_unsafe body (6bb).
-- Runtime mode wraps pb.decode_unsafe, which dispatches through
-- f._reader_unsafe closures compiled in pb.finalize_message against
-- a swapped scalar table (scalar.string = scalar.bytes) — see
-- codec.compile_readers_unsafe (58u).
--
-- This file runs the same suite against both modes to pin the API
-- contract (one function name, same semantics) regardless of which
-- code path executed.
local t = require('luatest')

local function address_with_street(s)
    return string.char(0x0A, #s) .. s
end

for _, mode in ipairs({'full', 'runtime'}) do
    local g = t.group('decode_unsafe.' .. mode)
    local hello = require(mode .. '.hello.hello_pb')

    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 invalid.
        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'}}.
        -- Pins that the unsafe path threads through sub-messages — a
        -- mistake here would call the safe Address decoder and error.
        local inner = address_with_street('\xC0\x80')
        local bytes = string.char(0x2A, #inner) .. inner

        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.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. Full
        -- mode falls through to wire.decode_bytes (codegen swap); runtime
        -- mode lands in the compiled _reader_unsafe scalar handler which
        -- is scalar_unsafe.string (= bytes). Both paths must accept
        -- invalid UTF-8 in the long-string regime.
        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

    g.test_unsafe_decode_map_string_value = function()
        -- Person.ages_by_nickname is map<string, int32> — string KEYS go
        -- through the map-fallback decode_one path in runtime mode (no
        -- _reader for map fields). The unsafe twin passes scalar_unsafe
        -- to decode_one so the key's utf8_len is bypassed. In full mode
        -- the inline map decoder routes through wire.decode_bytes.
        -- map<string, int32>: entry message = {1: string key, 2: int32 value}.
        local bad_key = '\xFE'
        local entry = string.char(0x0A, #bad_key) .. bad_key  -- key tag
                    .. string.char(0x10, 7)                    -- value tag + varint 7
        -- Person.ages_by_nickname id is per the .proto — check it.
        -- Use bench/dynamic introspection: look up field id from descriptor.
        local f = nil
        for _, fld in ipairs(hello.Person_descriptor.fields) do
            if fld.name == 'ages_by_nickname' then f = fld; break end
        end
        t.assert_not_equals(f, nil)
        local tag = require('pb.wire').encode_tag(f.id, 2)
        local bytes = tag .. string.char(#entry) .. entry

        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.ages_by_nickname[bad_key], 7)
    end
end