Files
2026-07-13 12:32:21 +08:00

181 lines
5.0 KiB
Lua

--- Stream class.
-- Decodes AWS response-stream types, currently application/vnd.amazon.eventstream
-- @classmod Stream
local buf = require("string.buffer")
local to_hex = require("resty.string").to_hex
local Stream = {}
Stream.__index = Stream
local _HEADER_EXTRACTORS = {
-- bool true
[0] = function(stream)
return true, 0
end,
-- bool false
[1] = function(stream)
return false, 0
end,
-- string type
[7] = function(stream)
local header_value_len = stream:next_int(16)
return stream:next_utf_8(header_value_len), header_value_len + 2 -- add the 2 bits read for the length
end,
-- TODO ADD THE REST OF THE DATA TYPES
-- EVEN THOUGH THEY'RE NOT REALLY USED
}
--- Constructor.
-- @function aws:Stream
-- @param chunk string complete AWS response stream chunk for decoding
-- @param is_hex boolean specify if the chunk bytes are already decoded to hex
-- @usage
-- local stream_parser = stream:new("00000120af0310f.......", true)
-- local next, err = stream_parser:next_message()
function Stream:new(chunk, is_hex)
local self = {} -- override 'self' to be the new object/class
setmetatable(self, Stream)
if #chunk < ((is_hex and 32) or 16) then
return nil, "cannot parse a chunk less than 16 bytes long"
end
self.read_count = 0
self.chunk = buf.new()
self.chunk:put((is_hex and chunk) or to_hex(chunk))
return self
end
--- return the next `count` ascii bytes from the front of the chunk
--- and then trims the chunk of those bytes
-- @param count number whole utf-8 bytes to return
-- @return string resulting utf-8 string
function Stream:next_utf_8(count)
local utf_bytes = self:next_bytes(count)
local ascii_string = ""
for i = 1, #utf_bytes, 2 do
local hex_byte = utf_bytes:sub(i, i + 1)
local ascii_byte = string.char(tonumber(hex_byte, 16))
ascii_string = ascii_string .. ascii_byte
end
return ascii_string
end
--- returns the next `count` bytes from the front of the chunk
--- and then trims the chunk of those bytes
-- @param count number whole integer of bytes to return
-- @return string hex-encoded next `count` bytes
function Stream:next_bytes(count)
if not self.chunk then
return nil, "function cannot be called on its own - initialise a chunk reader with :new(chunk)"
end
local bytes = self.chunk:get(count * 2)
self.read_count = (count) + self.read_count
return bytes
end
--- returns the next unsigned int from the front of the chunk
--- and then trims the chunk of those bytes
-- @param size integer bit length (8, 16, 32, etc)
-- @return number whole integer of size specified
-- @return string the original bytes, for reference/checksums
function Stream:next_int(size)
if not self.chunk then
return nil, nil, "function cannot be called on its own - initialise a chunk reader with :new(chunk)"
end
if size < 8 then
return nil, nil, "cannot work on integers smaller than 8 bits long"
end
local int, err = self:next_bytes(size / 8)
if err then
return nil, nil, err
end
return tonumber(int, 16), int
end
--- returns the next message in the chunk, as a table.
--- can be used as an iterator.
-- @return table formatted next message from the given constructor chunk
function Stream:next_message()
if not self.chunk then
return nil, "function cannot be called on its own - initialise a chunk reader with :new(chunk)"
end
if #self.chunk < 1 then
return false
end
-- get the message length and pull that many bytes
--
-- this is a chicken and egg problem, because we need to
-- read the message to get the length, to then re-read the
-- whole message at correct offset
local msg_len, _, err = self:next_int(32)
if err then
return err
end
-- get the headers length
local headers_len, _, err = self:next_int(32)
if err then
return err
end
-- get the preamble checksum
-- skip it because we're not using UDP
self:next_int(32)
-- pull the headers from the buf
local headers = {}
local headers_bytes_read = 0
while headers_bytes_read < headers_len do
-- the next 8-bit int is the "header key length"
local header_key_len = self:next_int(8)
local header_key = self:next_utf_8(header_key_len)
headers_bytes_read = 1 + header_key_len + headers_bytes_read
-- next 8-bits is the header type, which is an enum
local header_type = self:next_int(8)
headers_bytes_read = 1 + headers_bytes_read
-- depending on the header type, depends on how long the header should max out at
local header_value, header_value_len = _HEADER_EXTRACTORS[header_type](self)
headers_bytes_read = header_value_len + headers_bytes_read
headers[header_key] = header_value
end
-- finally, extract the body as a string by
-- subtracting what's read so far from the
-- total length obtained right at the start
local body = self:next_utf_8(msg_len - self.read_count - 4)
-- last 4 bytes is a body checksum
-- skip it because we're not using UDP
self:next_int(32)
-- rewind the tape
self.read_count = 0
return {
headers = headers,
body = body,
}
end
return Stream