Files
thrift/lib/lua/TCompactProtocol.lua
Jens GeyerandClaude Opus 5.5 eddc8a4439 THRIFT-6365: Add a container-size limit to the Lua protocols
Client: lua

The binary, compact and JSON protocols refused only a negative list, set or
map size, and the generated code reads one element per declared element.
Lua has no MaxMessageSize that would bound the count otherwise, so the new
limit gets a finite default instead of the "no limit of its own" default
THRIFT-6291 gives MaxContainerSize.

- TProtocol.lua: DEFAULT_MAX_CONTAINER_SIZE = 16384000, the value of the
  string and frame size limits, and TProtocolBase:checkContainerSize().
  maxContainerSize on a protocol instance overrides the default; zero or
  less switches the limit off.
- readListBegin, readSetBegin and readMapBegin of TBinaryProtocol,
  TCompactProtocol and TJSONProtocol call it before any element is read.
- doc/specs/thrift-tconfiguration.md records the finite Lua default and the
  reason.
- test/test_container_limits.lua covers the three protocols for list, set
  and map, at the limit and one over it, the default, the off values and
  skip. Its pure-Lua stand-in for libluabpack gains the int8_t wrap of 'c'
  and toVarint32, which the compact protocol needs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 00:00:24 +02:00

506 lines
14 KiB
Lua

--
-- Licensed to the Apache Software Foundation (ASF) under one
-- or more contributor license agreements. See the NOTICE file
-- distributed with this work for additional information
-- regarding copyright ownership. The ASF licenses this file
-- to you under the Apache License, Version 2.0 (the
-- "License"); you may not use this file except in compliance
-- with the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing,
-- software distributed under the License is distributed on an
-- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-- KIND, either express or implied. See the License for the
-- specific language governing permissions and limitations
-- under the License.
--
require 'TProtocol'
local libluabpack = require 'libluabpack'
local libluabitwise = require 'libluabitwise'
local liblualongnumber = require 'liblualongnumber'
TCompactProtocol = __TObject.new(TProtocolBase, {
__type = 'TCompactProtocol',
COMPACT_PROTOCOL_ID = 0x82,
COMPACT_VERSION = 1,
COMPACT_VERSION_MASK = 0x1f,
COMPACT_TYPE_MASK = 0xE0,
COMPACT_TYPE_BITS = 0x07,
COMPACT_TYPE_SHIFT_AMOUNT = 5,
COMPACT_MAX_VARINT_BYTES = 10, -- ceil(64/7); matches protobuf wire format
-- Used to keep track of the last field for the current and previous structs,
-- so we can do the delta stuff.
lastField = {},
lastFieldId = 0,
lastFieldIndex = 1,
-- If we encounter a boolean field begin, save the TField here so it can
-- have the value incorporated.
booleanFieldName = "",
booleanFieldId = 0,
booleanFieldPending = false,
-- If we read a field header, and it's a boolean field, save the boolean
-- value here so that readBool can use it.
boolValue = false,
boolValueIsNotNull = false,
})
TCompactType = {
COMPACT_BOOLEAN_TRUE = 0x01,
COMPACT_BOOLEAN_FALSE = 0x02,
COMPACT_BYTE = 0x03,
COMPACT_I16 = 0x04,
COMPACT_I32 = 0x05,
COMPACT_I64 = 0x06,
COMPACT_DOUBLE = 0x07,
COMPACT_BINARY = 0x08,
COMPACT_LIST = 0x09,
COMPACT_SET = 0x0A,
COMPACT_MAP = 0x0B,
COMPACT_STRUCT = 0x0C,
COMPACT_UUID = 0x0D,
}
TTypeToCompactType = {}
TTypeToCompactType[TType.STOP] = TType.STOP
TTypeToCompactType[TType.BOOL] = TCompactType.COMPACT_BOOLEAN_TRUE
TTypeToCompactType[TType.BYTE] = TCompactType.COMPACT_BYTE
TTypeToCompactType[TType.I16] = TCompactType.COMPACT_I16
TTypeToCompactType[TType.I32] = TCompactType.COMPACT_I32
TTypeToCompactType[TType.I64] = TCompactType.COMPACT_I64
TTypeToCompactType[TType.DOUBLE] = TCompactType.COMPACT_DOUBLE
TTypeToCompactType[TType.STRING] = TCompactType.COMPACT_BINARY
TTypeToCompactType[TType.LIST] = TCompactType.COMPACT_LIST
TTypeToCompactType[TType.SET] = TCompactType.COMPACT_SET
TTypeToCompactType[TType.MAP] = TCompactType.COMPACT_MAP
TTypeToCompactType[TType.STRUCT] = TCompactType.COMPACT_STRUCT
TTypeToCompactType[TType.UUID] = TCompactType.COMPACT_UUID
CompactTypeToTType = {}
CompactTypeToTType[TType.STOP] = TType.STOP
CompactTypeToTType[TCompactType.COMPACT_BOOLEAN_TRUE] = TType.BOOL
CompactTypeToTType[TCompactType.COMPACT_BOOLEAN_FALSE] = TType.BOOL
CompactTypeToTType[TCompactType.COMPACT_BYTE] = TType.BYTE
CompactTypeToTType[TCompactType.COMPACT_I16] = TType.I16
CompactTypeToTType[TCompactType.COMPACT_I32] = TType.I32
CompactTypeToTType[TCompactType.COMPACT_I64] = TType.I64
CompactTypeToTType[TCompactType.COMPACT_DOUBLE] = TType.DOUBLE
CompactTypeToTType[TCompactType.COMPACT_BINARY] = TType.STRING
CompactTypeToTType[TCompactType.COMPACT_LIST] = TType.LIST
CompactTypeToTType[TCompactType.COMPACT_SET] = TType.SET
CompactTypeToTType[TCompactType.COMPACT_MAP] = TType.MAP
CompactTypeToTType[TCompactType.COMPACT_STRUCT] = TType.STRUCT
CompactTypeToTType[TCompactType.COMPACT_UUID] = TType.UUID
function TCompactProtocol:resetLastField()
self.lastField = {}
self.lastFieldId = 0
self.lastFieldIndex = 1
end
function TCompactProtocol:packCompactType(ktype, vtype)
return libluabitwise.bor(libluabitwise.shiftl(ktype, 4), vtype)
end
function TCompactProtocol:writeMessageBegin(name, ttype, seqid)
self:writeByte(TCompactProtocol.COMPACT_PROTOCOL_ID)
self:writeByte(libluabpack.packMesgType(TCompactProtocol.COMPACT_VERSION,
TCompactProtocol.COMPACT_VERSION_MASK,ttype,
TCompactProtocol.COMPACT_TYPE_SHIFT_AMOUNT,
TCompactProtocol.COMPACT_TYPE_MASK))
self:writeVarint32(seqid)
self:writeString(name)
self:resetLastField()
end
function TCompactProtocol:writeMessageEnd()
end
function TCompactProtocol:writeStructBegin(name)
self.lastField[self.lastFieldIndex] = self.lastFieldId
self.lastFieldIndex = self.lastFieldIndex + 1
self.lastFieldId = 0
end
function TCompactProtocol:writeStructEnd()
self.lastFieldIndex = self.lastFieldIndex - 1
self.lastFieldId = self.lastField[self.lastFieldIndex]
end
function TCompactProtocol:writeFieldBegin(name, ttype, id)
if ttype == TType.BOOL then
self.booleanFieldName = name
self.booleanFieldId = id
self.booleanFieldPending = true
else
self:writeFieldBeginInternal(name, ttype, id, -1)
end
end
function TCompactProtocol:writeFieldEnd()
end
function TCompactProtocol:writeFieldStop()
self:writeByte(TType.STOP);
end
function TCompactProtocol:writeMapBegin(ktype, vtype, size)
if size == 0 then
self:writeByte(0)
else
self:writeVarint32(size)
self:writeByte(self:packCompactType(TTypeToCompactType[ktype], TTypeToCompactType[vtype]))
end
end
function TCompactProtocol:writeMapEnd()
end
function TCompactProtocol:writeListBegin(etype, size)
self:writeCollectionBegin(etype, size)
end
function TCompactProtocol:writeListEnd()
end
function TCompactProtocol:writeSetBegin(etype, size)
self:writeCollectionBegin(etype, size)
end
function TCompactProtocol:writeSetEnd()
end
function TCompactProtocol:writeBool(bool)
local value = TCompactType.COMPACT_BOOLEAN_FALSE
if bool then
value = TCompactType.COMPACT_BOOLEAN_TRUE
end
if self.booleanFieldPending then
self:writeFieldBeginInternal(self.booleanFieldName, TType.BOOL, self.booleanFieldId, value)
self.booleanFieldPending = false
else
self:writeByte(value)
end
end
function TCompactProtocol:writeByte(byte)
local buff = libluabpack.bpack('c', byte)
self.trans:write(buff)
end
function TCompactProtocol:writeI16(i16)
self:writeVarint32(libluabpack.i32ToZigzag(i16))
end
function TCompactProtocol:writeI32(i32)
self:writeVarint32(libluabpack.i32ToZigzag(i32))
end
function TCompactProtocol:writeUI32(i32)
local buff = libluabpack.bpack('I', i32)
self.trans:write(buff)
end
function TCompactProtocol:writeI64(i64)
self:writeVarint64(libluabpack.i64ToZigzag(i64))
end
function TCompactProtocol:writeDouble(dub)
local buff = libluabpack.bpack('d', dub)
self.trans:write(buff)
end
function TCompactProtocol:writeString(str)
-- Should be utf-8
self:writeBinary(str)
end
function TCompactProtocol:writeUuid(uuid)
self:writeUI32(uuid.two)
self:writeUI32(uuid.three)
self:writeUI32(uuid.zero)
self:writeUI32(uuid.one)
end
function TCompactProtocol:writeBinary(str)
-- Should be utf-8
self:writeVarint32(string.len(str))
self.trans:write(str)
end
function TCompactProtocol:writeFieldBeginInternal(name, ttype, id, typeOverride)
if typeOverride == -1 then
typeOverride = TTypeToCompactType[ttype]
end
local offset = id - self.lastFieldId
if id > self.lastFieldId and offset <= 15 then
self:writeByte(libluabitwise.bor(libluabitwise.shiftl(offset, 4), typeOverride))
else
self:writeByte(typeOverride)
self:writeI16(id)
end
self.lastFieldId = id
end
function TCompactProtocol:writeCollectionBegin(etype, size)
if size <= 14 then
self:writeByte(libluabitwise.bor(libluabitwise.shiftl(size, 4), TTypeToCompactType[etype]))
else
self:writeByte(libluabitwise.bor(0xf0, TTypeToCompactType[etype]))
self:writeVarint32(size)
end
end
function TCompactProtocol:writeVarint32(i32)
-- Should be utf-8
local str = libluabpack.toVarint32(i32)
self.trans:write(str)
end
function TCompactProtocol:writeVarint64(i64)
-- Should be utf-8
local str = libluabpack.toVarint64(i64)
self.trans:write(str)
end
function TCompactProtocol:readMessageBegin()
local protocolId = self:readSignByte()
if protocolId ~= self.COMPACT_PROTOCOL_ID then
terror(TProtocolException:new{
message = "Expected protocol id " .. self.COMPACT_PROTOCOL_ID .. " but got " .. protocolId})
end
local versionAndType = self:readSignByte()
local version = libluabitwise.band(versionAndType, self.COMPACT_VERSION_MASK)
local ttype = libluabitwise.band(libluabitwise.shiftr(versionAndType,
self.COMPACT_TYPE_SHIFT_AMOUNT), self.COMPACT_TYPE_BITS)
if version ~= self.COMPACT_VERSION then
terror(TProtocolException:new{
message = "Expected version " .. self.COMPACT_VERSION .. " but got " .. version})
end
local seqid = self:readVarint32()
local name = self:readString()
return name, ttype, seqid
end
function TCompactProtocol:readMessageEnd()
end
function TCompactProtocol:readStructBegin()
self.lastField[self.lastFieldIndex] = self.lastFieldId
self.lastFieldIndex = self.lastFieldIndex + 1
self.lastFieldId = 0
return nil
end
function TCompactProtocol:readStructEnd()
self.lastFieldIndex = self.lastFieldIndex - 1
self.lastFieldId = self.lastField[self.lastFieldIndex]
end
function TCompactProtocol:readFieldBegin()
local field_and_ttype = self:readSignByte()
local ttype = self:getTType(field_and_ttype)
if ttype == TType.STOP then
return nil, ttype, 0
end
local modifier = libluabitwise.shiftr(field_and_ttype, 4)
local id = 0
if modifier == 0 then
id = self:readI16()
else
id = self.lastFieldId + modifier
end
local type = libluabitwise.band(field_and_ttype, 0x0f)
if type == TCompactType.COMPACT_BOOLEAN_TRUE then
self.boolValue = true
self.boolValueIsNotNull = true
elseif type == TCompactType.COMPACT_BOOLEAN_FALSE then
self.boolValue = false
self.boolValueIsNotNull = true
end
self.lastFieldId = id
return nil, ttype, id
end
function TCompactProtocol:readFieldEnd()
end
function TCompactProtocol:readMapBegin()
local size = self:readVarint32()
if size < 0 then
terror(TProtocolException:new{errorCode = TProtocolException.NEGATIVE_SIZE})
end
self:checkContainerSize(size)
local kvtype = 0
if size > 0 then
kvtype = self:readSignByte()
end
local ktype = self:getTType(libluabitwise.shiftr(kvtype, 4))
local vtype = self:getTType(kvtype)
return ktype, vtype, size
end
function TCompactProtocol:readMapEnd()
end
function TCompactProtocol:readListBegin()
local size_and_type = self:readSignByte()
local size = libluabitwise.band(libluabitwise.shiftr(size_and_type, 4), 0x0f)
if size == 15 then
size = self:readVarint32()
end
if size < 0 then
terror(TProtocolException:new{errorCode = TProtocolException.NEGATIVE_SIZE})
end
self:checkContainerSize(size)
local etype = self:getTType(libluabitwise.band(size_and_type, 0x0f))
return etype, size
end
function TCompactProtocol:readListEnd()
end
function TCompactProtocol:readSetBegin()
return self:readListBegin()
end
function TCompactProtocol:readSetEnd()
end
function TCompactProtocol:readBool()
if self.boolValueIsNotNull then
self.boolValueIsNotNull = false
return self.boolValue
end
local val = self:readSignByte()
if val == TCompactType.COMPACT_BOOLEAN_TRUE then
return true
end
return false
end
function TCompactProtocol:readByte()
local buff = self.trans:readAll(1)
local val = libluabpack.bunpack('c', buff)
return val
end
function TCompactProtocol:readSignByte()
local buff = self.trans:readAll(1)
local val = libluabpack.bunpack('C', buff)
return val
end
function TCompactProtocol:readI16()
return self:readI32()
end
function TCompactProtocol:readI32()
local v = self:readVarint32()
local value = libluabpack.zigzagToI32(v)
return value
end
function TCompactProtocol:readUI32()
local buff = self.trans:readAll(4)
local val = libluabpack.bunpack('I', buff)
return val
end
function TCompactProtocol:readI64()
local value = self:readVarint64()
return value
end
function TCompactProtocol:readDouble()
local buff = self.trans:readAll(8)
local val = libluabpack.bunpack('d', buff)
return val
end
function TCompactProtocol:readString()
return self:readBinary()
end
function TCompactProtocol:readUuid()
local a = self:readUI32()
local b = self:readUI32()
local c = self:readUI32()
local d = self:readUI32()
return TUUID:new {
zero = c,
one = d,
two = a,
three = b
}
end
function TCompactProtocol:readBinary()
local size = self:readVarint32()
if size < 0 then
terror(TProtocolException:new{errorCode = TProtocolException.NEGATIVE_SIZE})
end
if size == 0 then
return ""
end
self:checkStringSize(size)
return self.trans:readAll(size)
end
function TCompactProtocol:readVarint32()
local shiftl = 0
local result = 0
for idx = 0, self.COMPACT_MAX_VARINT_BYTES - 1 do
b = self:readByte()
result = libluabitwise.bor(result,
libluabitwise.shiftl(libluabitwise.band(b, 0x7f), shiftl))
if libluabitwise.band(b, 0x80) ~= 0x80 then
return result
end
shiftl = shiftl + 7
end
terror(TProtocolException:new{errorCode = TProtocolException.INVALID_DATA,
message = 'Variable-length int over 10 bytes.'})
end
function TCompactProtocol:readVarint64()
local result = liblualongnumber.new
local data = result(0)
local shiftl = 0
for idx = 0, self.COMPACT_MAX_VARINT_BYTES - 1 do
b = self:readSignByte()
endFlag, data = libluabpack.fromVarint64(b, shiftl, data)
shiftl = shiftl + 7
if endFlag == 0 then
return data
end
end
terror(TProtocolException:new{errorCode = TProtocolException.INVALID_DATA,
message = 'Variable-length int over 10 bytes.'})
end
function TCompactProtocol:getTType(ctype)
return CompactTypeToTType[libluabitwise.band(ctype, 0x0f)]
end
TCompactProtocolFactory = TProtocolFactory:new{
__type = 'TCompactProtocolFactory',
}
function TCompactProtocolFactory:getProtocol(trans)
-- TODO Enforce that this must be a transport class (ie not a bool)
if not trans then
terror(TProtocolException:new{
message = 'Must supply a transport to ' .. ttype(self)
})
end
return TCompactProtocol:new{
trans = trans
}
end