-- Low-level protobuf wire format (proto3). -- Pure Lua + LuaJIT FFI; no Tarantool-specific dependencies. local ffi = require('ffi') local bit = require('bit') local M = {} -- Wire type constants (https://protobuf.dev/programming-guides/encoding/#structure) M.WIRE_VARINT = 0 M.WIRE_I64 = 1 M.WIRE_LEN = 2 -- Wire 3/4 are proto2 groups. We never emit them, but unknown-field -- skip must tolerate them so proto2-shaped payloads round-trip through -- a proto3 decoder (conformance suite exercises this). M.WIRE_SGROUP = 3 M.WIRE_EGROUP = 4 M.WIRE_I32 = 5 local UINT64 = ffi.typeof('uint64_t') local INT64 = ffi.typeof('int64_t') local UINT64_ZERO = UINT64(0) local CONT_MASK = UINT64(bit.bnot(0x7f)) -- 0xFFFFFFFFFFFFFF80 -- FFI scratch unions used by the fixed-width decoders. Allocated once -- and reused — `ffi.copy` from a `uint8_t*` cast over the Lua string body -- writes into the union's byte view; reading back via `.u` / `.d` / -- `.u32[i]` reinterprets the bytes without further allocation. ffi.cdef[[ typedef union { float f; uint32_t u; uint8_t b[4]; } pb_f32_u_t; typedef union { double d; uint64_t u; uint32_t u32[2]; /* [0] = low half, [1] = high half (LE) */ uint8_t b[8]; } pb_f64_u_t; typedef union { uint64_t u; uint32_t u32[2]; uint8_t b[8]; } pb_u64_u_t; ]] local U8CP = ffi.typeof('const uint8_t *') local F32 = ffi.new('pb_f32_u_t') local F64 = ffi.new('pb_f64_u_t') local U64 = ffi.new('pb_u64_u_t') -- Coerce any integer-like value to uint64_t cdata. -- Negative Lua numbers are sign-extended via int64_t (protobuf wire spec). local function to_uint64(v) local t = type(v) if t == 'number' then if v < 0 then return UINT64(INT64(v)) end return UINT64(v) elseif t == 'cdata' then return UINT64(v) elseif t == 'boolean' then return v and UINT64(1) or UINT64_ZERO end error("cannot coerce " .. t .. " to uint64", 0) end M.to_uint64 = to_uint64 local function to_int64(v) local t = type(v) if t == 'number' or t == 'cdata' then return INT64(v) end error("cannot coerce " .. t .. " to int64", 0) end M.to_int64 = to_int64 -- Truncate a uint64 varint payload to proto3 int32/uint32 Lua numbers. -- Per spec, int32/uint32/enum/sint32 fields must keep only the low 32 bits -- of an over-range varint; int32/sint32 additionally sign-extend from bit 31. local function varint_to_uint32(u) return tonumber(bit.band(u, 0xFFFFFFFF)) end local function varint_to_int32(u) local n = tonumber(bit.band(u, 0xFFFFFFFF)) if n >= 0x80000000 then n = n - 0x100000000 end return n end M.varint_to_uint32 = varint_to_uint32 M.varint_to_int32 = varint_to_int32 -- --------------------------------------------------------------------------- -- Varint -- --------------------------------------------------------------------------- -- encode_varint(n) -> string -- Accepts uint64_t/int64_t cdata, Lua number, or boolean. -- -- Fast path: small non-negative Lua numbers (0..127) become a single -- string.char(n) call with no cdata allocation, no `out` table, no -- table.concat. Covers most length prefixes for short strings, many -- enum ordinals, and most small int values in typical RPC payloads. -- -- The outer function is kept tiny on purpose — adding 2/3/4-byte -- branches to `encode_varint` itself pushes it past LuaJIT's inline -- budget so parent traces stop inlining it, regressing the -- 1-byte-dominant workloads by ~30%. Multi-byte Lua-number fast paths -- live in `encode_varint_slow` below, which is not inlined into hot -- traces anyway — so its size doesn't matter. local encode_varint_slow -- encode_varint_slow: handles every input that doesn't fit the -- 1-byte fast path in `encode_varint`. Three layers, falling through: -- -- 1. Non-negative Lua numbers < 2^28 are encoded via `bit.rshift` / -- `bit.band` / `string.char` — bit ops on Lua numbers operate as -- uint32 and don't allocate cdata. Values up to 4 bytes covered. -- 2. Non-negative Lua numbers in [2^28, 2^53) are still exact in -- double precision but exceed uint32 — emit one byte through bit -- ops, then recurse with `n / 128` on the smaller residue. -- 3. Everything else (cdata uint64/int64, negative Lua numbers — the -- latter get sign-extended to 10-byte varints) goes through the -- uint64 cdata loop. Pre-change measurement: 18x slower than the -- 1-byte fast path; the Lua-number paths above bring 2-byte to -- ~2x of the fast path instead. encode_varint_slow = function(n) if type(n) == 'number' and n >= 0 then if n < 0x4000 then -- 2-byte return string.char( bit.bor(bit.band(n, 0x7f), 0x80), bit.rshift(n, 7)) end if n < 0x200000 then -- 3-byte return string.char( bit.bor(bit.band(n, 0x7f), 0x80), bit.bor(bit.band(bit.rshift(n, 7), 0x7f), 0x80), bit.rshift(n, 14)) end if n < 0x10000000 then -- 4-byte (< 2^28, fits in uint32) return string.char( bit.bor(bit.band(n, 0x7f), 0x80), bit.bor(bit.band(bit.rshift(n, 7), 0x7f), 0x80), bit.bor(bit.band(bit.rshift(n, 14), 0x7f), 0x80), bit.rshift(n, 21)) end -- 2^28 <= n < 2^53: still exact in double, but past uint32. -- Emit one byte via Lua-number bit op, then recurse on the -- residue (which is now < n/128 and may now fit a fast path). if n < 2^53 then return string.char(bit.bor(bit.band(n, 0x7f), 0x80)) .. encode_varint_slow(math.floor(n / 128)) end end -- Fallback: uint64 cdata path. Reached by cdata inputs, negative -- Lua numbers (sign-extended to 10-byte varint), and the (rare) -- case of a Lua number larger than 2^53. n = to_uint64(n) local out = {} local i = 1 while bit.band(n, CONT_MASK) ~= UINT64_ZERO do out[i] = string.char(tonumber(bit.bor(bit.band(n, 0x7f), 0x80))) n = bit.rshift(n, 7) i = i + 1 end out[i] = string.char(tonumber(n)) return table.concat(out) end local function encode_varint(n) if type(n) == 'number' and n >= 0 and n < 0x80 then return string.char(n) end return encode_varint_slow(n) end M.encode_varint = encode_varint -- decode_varint(buf, pos) -> uint64_t cdata, new_pos (1-based) -- -- Fast path is inlined: 1-byte varints (field tags for ids 1..15 and -- many small values) take a straight-line branch with no loop, which -- keeps the JIT trace single-rooted across hot decode callers. The -- multi-byte tail still loops, but it's only entered for the small -- minority of values that don't fit in 7 bits. local function decode_varint(buf, pos) local b = buf:byte(pos) if b == nil then error("truncated varint at offset " .. pos, 0) end if b < 0x80 then return UINT64(b), pos + 1 end local result = UINT64(bit.band(b, 0x7f)) local shift = 7 pos = pos + 1 while true do b = buf:byte(pos) if b == nil then error("truncated varint at offset " .. pos, 0) end pos = pos + 1 result = bit.bor(result, bit.lshift(UINT64(bit.band(b, 0x7f)), shift)) if b < 0x80 then return result, pos end shift = shift + 7 if shift >= 70 then error("varint exceeds 10 bytes", 0) end end end M.decode_varint = decode_varint -- --------------------------------------------------------------------------- -- Tag -- --------------------------------------------------------------------------- local function encode_tag(field_id, wire_type) -- field_id < 2^29, fits in Lua double exactly. return encode_varint(field_id * 8 + wire_type) end M.encode_tag = encode_tag -- The 1-byte varint fast path is duplicated at every hot decode call -- site (decode_tag, decode_len, decode_int32 / int64 / uint32 / uint64 / -- sint32 / sint64 / bool, skip_field VARINT branch) instead of being -- factored into a helper. Reason: LuaJIT 2.1 inlines a small called -- function into the caller's trace, so when the parent guard exits to -- a side trace for the multi-byte case, the side trace has to return -- from the inlined frame — and LuaJIT can't stitch that return back -- to the parent, dropping to interpreter dispatch. Inlining the fast -- path literally keeps the side trace inside the caller's own frame, -- where stitching works. local function decode_tag(buf, pos) local b = buf:byte(pos) if b == nil then error("truncated varint at offset " .. pos, 0) end if b < 0x80 then local wt = bit.band(b, 7) if wt >= 6 then error("illegal wire type " .. wt, 0) end local fn = bit.rshift(b, 3) if fn == 0 then error("illegal field number 0", 0) end return fn, wt, pos + 1 end local u, npos = decode_varint(buf, pos) -- A tag must use the minimum number of bytes to encode its value. -- The trailing byte of a multi-byte varint contributes 0 high bits -- only when the encoding is overlong (any prior byte already covered -- the value). if buf:byte(npos - 1) == 0 then error("overlong tag varint at offset " .. pos, 0) end -- Bit ops on the uint64 cdata preserve 64-bit width; going through -- tonumber first would truncate field numbers above 2^32. local wt = tonumber(bit.band(u, 7)) if wt >= 6 then error("illegal wire type " .. wt, 0) end local fn = tonumber(bit.rshift(u, 3)) if fn == 0 then error("illegal field number 0", 0) end -- Field numbers are 29-bit per the protobuf spec. if fn > 0x1FFFFFFF then error("field number out of range: " .. fn, 0) end return fn, wt, npos end M.decode_tag = decode_tag -- --------------------------------------------------------------------------- -- ZigZag (sint32 / sint64) -- --------------------------------------------------------------------------- -- 32-bit zigzag stays in Lua number range. local function zigzag_encode32(n) n = tonumber(n) if n >= 0 then return n * 2 else return -n * 2 - 1 end end M.zigzag_encode32 = zigzag_encode32 local function zigzag_decode32(u) u = tonumber(u) if u % 2 == 0 then return u / 2 else return -((u + 1) / 2) end end M.zigzag_decode32 = zigzag_decode32 -- 64-bit zigzag uses cdata. local function zigzag_encode64(n) local i = to_int64(n) local doubled = bit.lshift(UINT64(i), 1) if i >= 0 then return doubled end return bit.bnot(doubled) end M.zigzag_encode64 = zigzag_encode64 local function zigzag_decode64(u) u = to_uint64(u) local half = bit.rshift(u, 1) if bit.band(u, 1) == UINT64_ZERO then return INT64(half) end return INT64(bit.bnot(half)) end M.zigzag_decode64 = zigzag_decode64 -- --------------------------------------------------------------------------- -- Fixed32 / Fixed64 (little-endian) -- --------------------------------------------------------------------------- local function encode_fixed32(v) -- Accepts uint32 (Lua number 0..2^32-1) or any integer cdata. local u if type(v) == 'cdata' then u = tonumber(bit.band(UINT64(v), 0xffffffff)) else u = tonumber(v) if u < 0 then u = u + 0x100000000 end end return string.char( bit.band(u, 0xff), bit.band(bit.rshift(u, 8), 0xff), bit.band(bit.rshift(u, 16), 0xff), bit.band(bit.rshift(u, 24), 0xff)) end M.encode_fixed32 = encode_fixed32 -- decode_fixed32(buf, pos) -> Lua number (0..2^32-1), new_pos local function decode_fixed32(buf, pos) local b1, b2, b3, b4 = buf:byte(pos, pos + 3) if b4 == nil then error("truncated fixed32", 0) end return b1 + b2 * 0x100 + b3 * 0x10000 + b4 * 0x1000000, pos + 4 end M.decode_fixed32 = decode_fixed32 local function encode_fixed64(v) local u = to_uint64(v) local lo = tonumber(bit.band(u, 0xffffffff)) local hi = tonumber(bit.rshift(u, 32)) return string.char( bit.band(lo, 0xff), bit.band(bit.rshift(lo, 8), 0xff), bit.band(bit.rshift(lo, 16), 0xff), bit.band(bit.rshift(lo, 24), 0xff), bit.band(hi, 0xff), bit.band(bit.rshift(hi, 8), 0xff), bit.band(bit.rshift(hi, 16), 0xff), bit.band(bit.rshift(hi, 24), 0xff)) end M.encode_fixed64 = encode_fixed64 -- decode_fixed64(buf, pos) -> uint64_t cdata, new_pos -- Reading via the FFI union avoids the multi-step `UINT64(lo) + -- bit.lshift(UINT64(hi), 32)` cdata arithmetic, which allocated 2-3 -- intermediate cdata per call. ffi.copy from a uint8_t* cast over the -- Lua string body skips the buf:sub allocation entirely. local function decode_fixed64(buf, pos) if pos + 7 > #buf then error("truncated fixed64", 0) end ffi.copy(U64.b, ffi.cast(U8CP, buf) + (pos - 1), 8) return U64.u, pos + 8 end M.decode_fixed64 = decode_fixed64 -- --------------------------------------------------------------------------- -- Float / Double (IEEE 754 little-endian) -- --------------------------------------------------------------------------- local function encode_float(n) F32.f = n return ffi.string(F32.b, 4) end M.encode_float = encode_float -- ffi.cast over the Lua string body (`U8CP`) avoids the buf:sub -- allocation (saves ~40 bytes per fixed-width read on the GC, and one -- alloc per call). Lua strings are immutable + zero-terminated, so the -- pointer is valid for the duration of the cast. local function decode_float(buf, pos) if pos + 3 > #buf then error("truncated float", 0) end ffi.copy(F32.b, ffi.cast(U8CP, buf) + (pos - 1), 4) -- Detect Inf/NaN from the raw bit pattern before going through -- tonumber(). LuaJIT 2.1 NaN-boxes Lua values, so some IEEE NaN -- payloads collide with internal type tags (nil/function/etc.) and -- `tonumber(F32.f)` yields a non-number. Reading via F32.u (uint32) -- keeps us in the integer domain until we decide what to return. local u = F32.u local exp = bit.band(bit.rshift(u, 23), 0xff) if exp == 0xff then if bit.band(u, 0x7fffff) == 0 then if bit.band(u, 0x80000000) ~= 0 then return -math.huge, pos + 4 end return math.huge, pos + 4 end return 0/0, pos + 4 end return tonumber(F32.f), pos + 4 end M.decode_float = decode_float local function encode_double(n) F64.d = n return ffi.string(F64.b, 8) end M.encode_double = encode_double local function decode_double(buf, pos) if pos + 7 > #buf then error("truncated double", 0) end ffi.copy(F64.b, ffi.cast(U8CP, buf) + (pos - 1), 8) -- Detect Inf/NaN from the raw bit pattern before going through -- tonumber(). LuaJIT 2.1 NaN-boxes Lua values, so some IEEE NaN -- payloads collide with internal type tags (nil/function/etc.) and -- `tonumber(F64.d)` yields a non-number. Splitting the uint64 into -- two uint32 halves lets the bit ops stay in Lua-number space — -- avoids per-op uint64 cdata allocation in the hot path. local hi = F64.u32[1] if bit.band(bit.rshift(hi, 20), 0x7ff) == 0x7ff then local frac_hi = bit.band(hi, 0xfffff) if frac_hi == 0 and F64.u32[0] == 0 then if bit.band(hi, 0x80000000) ~= 0 then return -math.huge, pos + 8 end return math.huge, pos + 8 end return 0/0, pos + 8 end return tonumber(F64.d), pos + 8 end M.decode_double = decode_double -- --------------------------------------------------------------------------- -- Length-delimited (LEN) -- --------------------------------------------------------------------------- local function encode_len(s) return encode_varint(#s) .. s end M.encode_len = encode_len -- decode_len(buf, pos) -> string, new_pos local function decode_len(buf, pos) local b = buf:byte(pos) if b == nil then error("truncated varint at offset " .. pos, 0) end local len, npos if b < 0x80 then len = b; npos = pos + 1 else local v v, npos = decode_varint(buf, pos) len = tonumber(v) end if npos + len - 1 > #buf then error("truncated LEN payload", 0) end return buf:sub(npos, npos + len - 1), npos + len end M.decode_len = decode_len -- --------------------------------------------------------------------------- -- Typed scalar encoders/decoders (one per proto3 scalar type). -- -- Both code paths (descriptor-driven runtime and inline codegen) call into -- these. They take the application-side Lua value and produce/consume only -- the value's wire bytes — the field tag is the caller's responsibility. -- --------------------------------------------------------------------------- -- Encoders -------------------------------------------------------------------- M.encode_int32 = encode_varint M.encode_int64 = encode_varint M.encode_uint32 = encode_varint M.encode_uint64 = encode_varint local function encode_sint32(v) return encode_varint(zigzag_encode32(v)) end local function encode_sint64(v) return encode_varint(zigzag_encode64(v)) end local function encode_bool(v) return encode_varint(v and 1 or 0) end M.encode_sint32 = encode_sint32 M.encode_sint64 = encode_sint64 M.encode_bool = encode_bool M.encode_sfixed32 = encode_fixed32 -- bits are identical, only interpretation differs M.encode_sfixed64 = encode_fixed64 M.encode_string = encode_len M.encode_bytes = encode_len -- (encode_fixed32, encode_fixed64, encode_float, encode_double already on M) -- Decoders -------------------------------------------------------------------- -- The 1-byte fast path is inlined at every varint-based scalar decoder -- (see comment above decode_tag). Each decoder reads the first byte, -- handles the common 0..127 case in straight-line code, and falls -- through to decode_varint only for multi-byte values. local function decode_int32(buf, pos) local b = buf:byte(pos) if b == nil then error("truncated varint at offset " .. pos, 0) end if b < 0x80 then return b, pos + 1 end -- 0..127 fits int32 directly local u, np = decode_varint(buf, pos) return varint_to_int32(u), np end local function decode_int64(buf, pos) local b = buf:byte(pos) if b == nil then error("truncated varint at offset " .. pos, 0) end if b < 0x80 then return INT64(b), pos + 1 end local u, np = decode_varint(buf, pos) return INT64(u), np end local function decode_uint32(buf, pos) local b = buf:byte(pos) if b == nil then error("truncated varint at offset " .. pos, 0) end if b < 0x80 then return b, pos + 1 end local u, np = decode_varint(buf, pos) return varint_to_uint32(u), np end local function decode_uint64(buf, pos) local b = buf:byte(pos) if b == nil then error("truncated varint at offset " .. pos, 0) end if b < 0x80 then return UINT64(b), pos + 1 end local u, np = decode_varint(buf, pos) return UINT64(u), np end local function decode_sint32(buf, pos) local b = buf:byte(pos) if b == nil then error("truncated varint at offset " .. pos, 0) end if b < 0x80 then return zigzag_decode32(b), pos + 1 end local u, np = decode_varint(buf, pos) return zigzag_decode32(varint_to_uint32(u)), np end local function decode_sint64(buf, pos) local b = buf:byte(pos) if b == nil then error("truncated varint at offset " .. pos, 0) end if b < 0x80 then return zigzag_decode64(b), pos + 1 end local u, np = decode_varint(buf, pos) return zigzag_decode64(u), np end local function decode_bool(buf, pos) local b = buf:byte(pos) if b == nil then error("truncated varint at offset " .. pos, 0) end if b < 0x80 then return b ~= 0, pos + 1 end local u, np = decode_varint(buf, pos) return u ~= UINT64_ZERO, np end local function decode_sfixed32(buf, pos) local n, np = decode_fixed32(buf, pos) if n > 0x7fffffff then n = n - 0x100000000 end return n, np end local function decode_sfixed64(buf, pos) local u, np = decode_fixed64(buf, pos) return INT64(u), np end M.decode_int32 = decode_int32 M.decode_int64 = decode_int64 M.decode_uint32 = decode_uint32 M.decode_uint64 = decode_uint64 M.decode_sint32 = decode_sint32 M.decode_sint64 = decode_sint64 M.decode_bool = decode_bool M.decode_sfixed32 = decode_sfixed32 M.decode_sfixed64 = decode_sfixed64 -- RFC 3629 UTF-8 validator. Rejects: out-of-range continuation bytes, -- truncated multi-byte sequences, overlong encodings, UTF-16 surrogate -- code points (U+D800..U+DFFF), and code points above U+10FFFF. -- -- Tarantool ships an ICU-backed `utf8.len` (src/lua/utf8.c:165) that uses -- `U8_NEXT` for validation — it returns nil on the first invalid sequence -- and matches every proto3 rejection case (overlong, surrogate, > U+10FFFF, -- 5-byte). Empirically 15-30x faster than a pure-Lua `string.byte` loop on -- ASCII payloads (decode throughput on string-heavy 1KB Person: ~1.9x). local utf8_len = require('utf8').len local function is_valid_utf8(s) return utf8_len(s) ~= nil end M.is_valid_utf8 = is_valid_utf8 -- Inlined 1-byte LEN fast path. Same rationale as the inlined varint -- fast paths above (see decode_tag): strings ≤127 bytes are the common -- RPC case; folding their length-prefix read in here skips two function -- call layers (decode_string → decode_len → decode_varint) and lets the -- JIT keep the trace single-rooted across the caller. local function decode_string(buf, pos) local b = buf:byte(pos) if b == nil then error("truncated varint at offset " .. pos, 0) end if b < 0x80 then local np = pos + 1 local epos = np + b if epos - 1 > #buf then error("truncated LEN payload", 0) end local s = buf:sub(np, epos - 1) if utf8_len(s) == nil then error("invalid UTF-8 in string field at offset " .. pos, 0) end return s, epos end local s, np = decode_len(buf, pos) if utf8_len(s) == nil then error("invalid UTF-8 in string field at offset " .. pos, 0) end return s, np end M.decode_string = decode_string local function decode_bytes(buf, pos) local b = buf:byte(pos) if b == nil then error("truncated varint at offset " .. pos, 0) end if b < 0x80 then local np = pos + 1 local epos = np + b if epos - 1 > #buf then error("truncated LEN payload", 0) end return buf:sub(np, epos - 1), epos end return decode_len(buf, pos) end M.decode_bytes = decode_bytes -- (decode_fixed32, decode_fixed64, decode_float, decode_double already on M -- and have the right semantics for their proto types: fixed32 -> uint32 Lua -- number 0..2^32-1, fixed64 -> uint64 cdata.) -- --------------------------------------------------------------------------- -- TYPE_INFO — single-source-of-truth metadata for the codec layer and codegen. -- Each entry carries the wire type, packed-list eligibility, and the typed -- encode/decode functions defined above. Adapted from tarantool-etcd's -- types.lua pattern. -- --------------------------------------------------------------------------- M.TYPE_INFO = { int32 = {wire = M.WIRE_VARINT, packable = true, encode = M.encode_int32, decode = M.decode_int32 }, int64 = {wire = M.WIRE_VARINT, packable = true, encode = M.encode_int64, decode = M.decode_int64 }, uint32 = {wire = M.WIRE_VARINT, packable = true, encode = M.encode_uint32, decode = M.decode_uint32 }, uint64 = {wire = M.WIRE_VARINT, packable = true, encode = M.encode_uint64, decode = M.decode_uint64 }, sint32 = {wire = M.WIRE_VARINT, packable = true, encode = M.encode_sint32, decode = M.decode_sint32 }, sint64 = {wire = M.WIRE_VARINT, packable = true, encode = M.encode_sint64, decode = M.decode_sint64 }, bool = {wire = M.WIRE_VARINT, packable = true, encode = M.encode_bool, decode = M.decode_bool }, fixed32 = {wire = M.WIRE_I32, packable = true, encode = M.encode_fixed32, decode = M.decode_fixed32 }, sfixed32 = {wire = M.WIRE_I32, packable = true, encode = M.encode_sfixed32, decode = M.decode_sfixed32}, float = {wire = M.WIRE_I32, packable = true, encode = M.encode_float, decode = M.decode_float }, fixed64 = {wire = M.WIRE_I64, packable = true, encode = M.encode_fixed64, decode = M.decode_fixed64 }, sfixed64 = {wire = M.WIRE_I64, packable = true, encode = M.encode_sfixed64, decode = M.decode_sfixed64}, double = {wire = M.WIRE_I64, packable = true, encode = M.encode_double, decode = M.decode_double }, string = {wire = M.WIRE_LEN, packable = false, encode = M.encode_string, decode = M.decode_string }, bytes = {wire = M.WIRE_LEN, packable = false, encode = M.encode_bytes, decode = M.decode_bytes }, } -- --------------------------------------------------------------------------- -- Skip an unknown field (used by decoder when an unrecognized id appears). -- skip_field(buf, pos, wire_type, field_id) -> new_pos -- -- field_id is required for SGROUP (wire 3) so the closing EGROUP can -- be matched. Other wire types ignore it. -- --------------------------------------------------------------------------- local function skip_field(buf, pos, wire_type, field_id) if wire_type == M.WIRE_VARINT then local b = buf:byte(pos) if b == nil then error("truncated varint at offset " .. pos, 0) end if b < 0x80 then return pos + 1 end local _, npos = decode_varint(buf, pos) return npos elseif wire_type == M.WIRE_I64 then local np = pos + 8 if np > #buf + 1 then error("truncated I64 at offset " .. pos, 0) end return np elseif wire_type == M.WIRE_LEN then local b = buf:byte(pos) if b == nil then error("truncated varint at offset " .. pos, 0) end if b < 0x80 then local np = pos + 1 + b if np > #buf + 1 then error("truncated LEN at offset " .. pos, 0) end return np end local len, npos = decode_varint(buf, pos) local np = npos + tonumber(len) if np > #buf + 1 then error("truncated LEN at offset " .. pos, 0) end return np elseif wire_type == M.WIRE_I32 then local np = pos + 4 if np > #buf + 1 then error("truncated I32 at offset " .. pos, 0) end return np elseif wire_type == M.WIRE_SGROUP then -- Read inner tags until the matching EGROUP; recurse on nested -- groups. EGROUP id mismatch is a hard error per spec. if field_id == nil then error("skip_field SGROUP requires field_id", 0) end while true do local iid, iwt iid, iwt, pos = decode_tag(buf, pos) if iwt == M.WIRE_EGROUP then if iid ~= field_id then error(("EGROUP id %d does not match SGROUP id %d"): format(iid, field_id), 0) end return pos end pos = skip_field(buf, pos, iwt, iid) end elseif wire_type == M.WIRE_EGROUP then error("unexpected EGROUP for field " .. tostring(field_id), 0) end error("unknown wire type " .. tostring(wire_type), 0) end M.skip_field = skip_field return M