MessagePack

--
-- lua-MessagePack : <https://fperrad.frama.io/lua-MessagePack/>
--
local r, jit = pcall(require, 'jit')
if not r then
    jit = nil
end
local SIZEOF_NUMBER = string.pack and #string.pack('n', 0.0) or 8
local maxinteger
local mininteger
local assert = assert
local error = error
local pairs = pairs
local pcall = pcall
local setmetatable = setmetatable
local tostring = tostring
local type = type
local char = require'string'.char
local format = require'string'.format
local floor = require'math'.floor
local tointeger = require'math'.tointeger or floor
local frexp = require'math'.frexp or require'mathx'.frexp
local ldexp = require'math'.ldexp or require'mathx'.ldexp
local huge = require'math'.huge
local tconcat = require'table'.concat
local _ENV = nil
local m = {}
--[[ debug only
local function hexadump (s)
end
m.hexadump = hexadump
--]]
local function argerror (caller, narg, extramsg)
    error("bad argument #" .. tostring(narg) .. " to "
          .. caller .. " (" .. extramsg .. ")")
end
local function typeerror (caller, narg, arg, tname)
    argerror(caller, narg, tname .. " expected, got " .. type(arg))
end
local function checktype (caller, narg, arg, tname)
    if type(arg) ~= tname then
        typeerror(caller, narg, arg, tname)
    end
end
local packers = setmetatable({}, {
    __index = function (t, k)
        if k == 1 then return end   -- allows ipairs
        error("pack '" .. k .. "' is unimplemented")
    end
})
m.packers = packers
packers['nil'] = function (buffer)
    buffer[#buffer+1] = char(0xC0)              -- nil
end
packers['boolean'] = function (buffer, bool)
    if bool then
        buffer[#buffer+1] = char(0xC3)          -- true
    else
        buffer[#buffer+1] = char(0xC2)          -- false
    end
end
packers['string_compat'] = function (buffer, str)
    local n = #str
    if n <= 0x1F then
        buffer[#buffer+1] = char(0xA0 + n)      -- fixstr
    elseif n <= 0xFFFF then
        buffer[#buffer+1] = char(0xDA,          -- str16
                                 floor(n / 0x100),
                                 n % 0x100)
    elseif n <= 4294967295.0 then
        buffer[#buffer+1] = char(0xDB,          -- str32
                                 floor(n / 0x1000000),
                                 floor(n / 0x10000) % 0x100,
                                 floor(n / 0x100) % 0x100,
                                 n % 0x100)
    else
        error"overflow in pack 'string_compat'"
    end
    buffer[#buffer+1] = str
end
packers['_string'] = function (buffer, str)
    local n = #str
    if n <= 0x1F then
        buffer[#buffer+1] = char(0xA0 + n)      -- fixstr
    elseif n <= 0xFF then
        buffer[#buffer+1] = char(0xD9,          -- str8
                                 n)
    elseif n <= 0xFFFF then
        buffer[#buffer+1] = char(0xDA,          -- str16
                                 floor(n / 0x100),
                                 n % 0x100)
    elseif n <= 4294967295.0 then
        buffer[#buffer+1] = char(0xDB,          -- str32
                                 floor(n / 0x1000000),
                                 floor(n / 0x10000) % 0x100,
                                 floor(n / 0x100) % 0x100,
                                 n % 0x100)
    else
        error"overflow in pack 'string'"
    end
    buffer[#buffer+1] = str
end
packers['binary'] = function (buffer, str)
    local n = #str
    if n <= 0xFF then
        buffer[#buffer+1] = char(0xC4,          -- bin8
                                 n)
    elseif n <= 0xFFFF then
        buffer[#buffer+1] = char(0xC5,          -- bin16
                                 floor(n / 0x100),
                                 n % 0x100)
    elseif n <= 4294967295.0 then
        buffer[#buffer+1] = char(0xC6,          -- bin32
                                 floor(n / 0x1000000),
                                 floor(n / 0x10000) % 0x100,
                                 floor(n / 0x100) % 0x100,
                                 n % 0x100)
    else
        error"overflow in pack 'binary'"
    end
    buffer[#buffer+1] = str
end
local set_string = function (str)
    if str == 'string_compat' then
        packers['string'] = packers['string_compat']
    elseif str == 'string' then
        packers['string'] = packers['_string']
    elseif str == 'binary' then
        packers['string'] = packers['binary']
    else
        argerror('set_string', 1, "invalid option '" .. str .."'")
    end
end
m.set_string = set_string
packers['map'] = function (buffer, tbl, n)
    if n <= 0x0F then
        buffer[#buffer+1] = char(0x80 + n)      -- fixmap
    elseif n <= 0xFFFF then
        buffer[#buffer+1] = char(0xDE,          -- map16
                                 floor(n / 0x100),
                                 n % 0x100)
    elseif n <= 4294967295.0 then
        buffer[#buffer+1] = char(0xDF,          -- map32
                                 floor(n / 0x1000000),
                                 floor(n / 0x10000) % 0x100,
                                 floor(n / 0x100) % 0x100,
                                 n % 0x100)
    else
        error"overflow in pack 'map'"
    end
    for k, v in pairs(tbl) do
        packers[type(k)](buffer, k)
        packers[type(v)](buffer, v)
    end
end
packers['array'] = function (buffer, tbl, n)
    if n <= 0x0F then
        buffer[#buffer+1] = char(0x90 + n)      -- fixarray
    elseif n <= 0xFFFF then
        buffer[#buffer+1] = char(0xDC,          -- array16
                                 floor(n / 0x100),
                                 n % 0x100)
    elseif n <= 4294967295.0 then
        buffer[#buffer+1] = char(0xDD,          -- array32
                                 floor(n / 0x1000000),
                                 floor(n / 0x10000) % 0x100,
                                 floor(n / 0x100) % 0x100,
                                 n % 0x100)
    else
        error"overflow in pack 'array'"
    end
    for i = 1, n do
        local v = tbl[i]
        packers[type(v)](buffer, v)
    end
end
local set_array = function (array)
    if array == 'without_hole' then
        packers['_table'] = function (buffer, tbl)
            local is_map, n, max = false, 0, 0
            for k in pairs(tbl) do
                if type(k) == 'number' and k > 0 then
                    if k > max then
                        max = k
                    end
                else
                    is_map = true
                end
                n = n + 1
            end
            if max ~= n then    -- there are holes
                is_map = true
            end
            if is_map then
                packers['map'](buffer, tbl, n)
            else
                packers['array'](buffer, tbl, n)
            end
        end
    elseif array == 'with_hole' then
        packers['_table'] = function (buffer, tbl)
            local is_map, n, max = false, 0, 0
            for k in pairs(tbl) do
                if type(k) == 'number' and k > 0 then
                    if k > max then
                        max = k
                    end
                else
                    is_map = true
                end
                n = n + 1
            end
            if is_map then
                packers['map'](buffer, tbl, n)
            else
                packers['array'](buffer, tbl, max)
            end
        end
    elseif array == 'always_as_map' then
        packers['_table'] = function(buffer, tbl)
            local n = 0
            for k in pairs(tbl) do
                n = n + 1
            end
            packers['map'](buffer, tbl, n)
        end
    else
        argerror('set_array', 1, "invalid option '" .. array .."'")
    end
end
m.set_array = set_array
packers['table'] = function (buffer, tbl)
    packers['_table'](buffer, tbl)
end
packers['unsigned'] = function (buffer, n)
    if n >= 0 then
        if n <= 0x7F then
            buffer[#buffer+1] = char(n)         -- fixnum_pos
        elseif n <= 0xFF then
            buffer[#buffer+1] = char(0xCC,      -- uint8
                                     n)
        elseif n <= 0xFFFF then
            buffer[#buffer+1] = char(0xCD,      -- uint16
                                     floor(n / 0x100),
                                     n % 0x100)
        elseif n <= 4294967295.0 then
            buffer[#buffer+1] = char(0xCE,      -- uint32
                                     floor(n / 0x1000000),
                                     floor(n / 0x10000) % 0x100,
                                     floor(n / 0x100) % 0x100,
                                     n % 0x100)
        else
            buffer[#buffer+1] = char(0xCF,      -- uint64
                                     0,         -- only 53 bits from double
                                     floor(n / 0x1000000000000) % 0x100,
                                     floor(n / 0x10000000000) % 0x100,
                                     floor(n / 0x100000000) % 0x100,
                                     floor(n / 0x1000000) % 0x100,
                                     floor(n / 0x10000) % 0x100,
                                     floor(n / 0x100) % 0x100,
                                     n % 0x100)
        end
    else
        if n >= -0x20 then
            buffer[#buffer+1] = char(0x100 + n) -- fixnum_neg
        elseif n >= -0x80 then
            buffer[#buffer+1] = char(0xD0,      -- int8
                                     0x100 + n)
        elseif n >= -0x8000 then
            n = 0x10000 + n
            buffer[#buffer+1] = char(0xD1,      -- int16
                                     floor(n / 0x100),
                                     n % 0x100)
        elseif n >= -0x80000000 then
            n = 4294967296.0 + n
            buffer[#buffer+1] = char(0xD2,      -- int32
                                     floor(n / 0x1000000),
                                     floor(n / 0x10000) % 0x100,
                                     floor(n / 0x100) % 0x100,
                                     n % 0x100)
        else
            buffer[#buffer+1] = char(0xD3,      -- int64
                                     0xFF,      -- only 53 bits from double
                                     floor(n / 0x1000000000000) % 0x100,
                                     floor(n / 0x10000000000) % 0x100,
                                     floor(n / 0x100000000) % 0x100,
                                     floor(n / 0x1000000) % 0x100,
                                     floor(n / 0x10000) % 0x100,
                                     floor(n / 0x100) % 0x100,
                                     n % 0x100)
        end
    end
end
packers['signed'] = function (buffer, n)
    if n >= 0 then
        if n <= 0x7F then
            buffer[#buffer+1] = char(n)         -- fixnum_pos
        elseif n <= 0x7FFF then
            buffer[#buffer+1] = char(0xD1,      -- int16
                                     floor(n / 0x100),
                                     n % 0x100)
        elseif n <= 0x7FFFFFFF then
            buffer[#buffer+1] = char(0xD2,      -- int32
                                     floor(n / 0x1000000),
                                     floor(n / 0x10000) % 0x100,
                                     floor(n / 0x100) % 0x100,
                                     n % 0x100)
        else
            buffer[#buffer+1] = char(0xD3,      -- int64
                                     0,         -- only 53 bits from double
                                     floor(n / 0x1000000000000) % 0x100,
                                     floor(n / 0x10000000000) % 0x100,
                                     floor(n / 0x100000000) % 0x100,
                                     floor(n / 0x1000000) % 0x100,
                                     floor(n / 0x10000) % 0x100,
                                     floor(n / 0x100) % 0x100,
                                     n % 0x100)
        end
    else
        if n >= -0x20 then
            buffer[#buffer+1] = char(0xE0 + 0x20 + n)   -- fixnum_neg
        elseif n >= -0x80 then
            buffer[#buffer+1] = char(0xD0,      -- int8
                                     0x100 + n)
        elseif n >= -0x8000 then
            n = 0x10000 + n
            buffer[#buffer+1] = char(0xD1,      -- int16
                                     floor(n / 0x100),
                                     n % 0x100)
        elseif n >= -0x80000000 then
            n = 4294967296.0 + n
            buffer[#buffer+1] = char(0xD2,      -- int32
                                     floor(n / 0x1000000),
                                     floor(n / 0x10000) % 0x100,
                                     floor(n / 0x100) % 0x100,
                                     n % 0x100)
        else
            buffer[#buffer+1] = char(0xD3,      -- int64
                                     0xFF,      -- only 53 bits from double
                                     floor(n / 0x1000000000000) % 0x100,
                                     floor(n / 0x10000000000) % 0x100,
                                     floor(n / 0x100000000) % 0x100,
                                     floor(n / 0x1000000) % 0x100,
                                     floor(n / 0x10000) % 0x100,
                                     floor(n / 0x100) % 0x100,
                                     n % 0x100)
        end
    end
end
local set_integer = function (integer)
    if integer == 'unsigned' then
        packers['integer'] = packers['unsigned']
    elseif integer == 'signed' then
        packers['integer'] = packers['signed']
    else
        argerror('set_integer', 1, "invalid option '" .. integer .."'")
    end
end
m.set_integer = set_integer
packers['float'] = function (buffer, n)
    local sign = 0
    if n < 0.0 then
        sign = 0x80
        n = -n
    end
    local mant, expo = frexp(n)
    if mant ~= mant then
        buffer[#buffer+1] = char(0xCA,  -- nan
                                 0xFF, 0x88, 0x00, 0x00)
    elseif mant == huge or expo > 0x80 then
        if sign == 0 then
            buffer[#buffer+1] = char(0xCA,      -- inf
                                     0x7F, 0x80, 0x00, 0x00)
        else
            buffer[#buffer+1] = char(0xCA,      -- -inf
                                     0xFF, 0x80, 0x00, 0x00)
        end
    elseif (mant == 0.0 and expo == 0) or expo < -0x7E then
        buffer[#buffer+1] = char(0xCA,  -- zero
                                 sign, 0x00, 0x00, 0x00)
    else
        expo = expo + 0x7E
        mant = floor((mant * 2.0 - 1.0) * ldexp(0.5, 24))
        buffer[#buffer+1] = char(0xCA,
                                 sign + floor(expo / 0x2),
                                 (expo % 0x2) * 0x80 + floor(mant / 0x10000),
                                 floor(mant / 0x100) % 0x100,
                                 mant % 0x100)
    end
end
packers['double'] = function (buffer, n)
    local sign = 0
    if n < 0.0 then
        sign = 0x80
        n = -n
    end
    local mant, expo = frexp(n)
    if mant ~= mant then
        buffer[#buffer+1] = char(0xCB,  -- nan
                                 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
    elseif mant == huge or expo > 0x400 then
        if sign == 0 then
            buffer[#buffer+1] = char(0xCB,      -- inf
                                     0x7F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
        else
            buffer[#buffer+1] = char(0xCB,      -- -inf
                                     0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
        end
    elseif (mant == 0.0 and expo == 0) or expo < -0x3FE then
        buffer[#buffer+1] = char(0xCB,  -- zero
                                 sign, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
    else
        expo = expo + 0x3FE
        mant = floor((mant * 2.0 - 1.0) * ldexp(0.5, 53))
        buffer[#buffer+1] = char(0xCB,
                                 sign + floor(expo / 0x10),
                                 (expo % 0x10) * 0x10 + floor(mant / 0x1000000000000),
                                 floor(mant / 0x10000000000) % 0x100,
                                 floor(mant / 0x100000000) % 0x100,
                                 floor(mant / 0x1000000) % 0x100,
                                 floor(mant / 0x10000) % 0x100,
                                 floor(mant / 0x100) % 0x100,
                                 mant % 0x100)
    end
end
local set_number = function (number)
    if number == 'float' then
        packers['number'] = function (buffer, n)
            if floor(n) == n and n < maxinteger and n > mininteger then
                packers['integer'](buffer, n)
            else
                packers['float'](buffer, n)
            end
        end
    elseif number == 'double' then
        packers['number'] = function (buffer, n)
            if floor(n) == n and n < maxinteger and n > mininteger then
                packers['integer'](buffer, n)
            else
                packers['double'](buffer, n)
            end
        end
    else
        argerror('set_number', 1, "invalid option '" .. number .."'")
    end
end
m.set_number = set_number
for k = 0, 4 do
    local n = tointeger(2^k)
    local fixext = 0xD4 + k
    packers['fixext' .. tostring(n)] = function (buffer, tag, data)
        assert(#data == n, "bad length for fixext" .. tostring(n))
        buffer[#buffer+1] = char(fixext,
                                 tag < 0 and tag + 0x100 or tag)
        buffer[#buffer+1] = data
    end
end
packers['ext'] = function (buffer, tag, data)
    local n = #data
    if n <= 0xFF then
        buffer[#buffer+1] = char(0xC7,          -- ext8
                                 n,
                                 tag < 0 and tag + 0x100 or tag)
    elseif n <= 0xFFFF then
        buffer[#buffer+1] = char(0xC8,          -- ext16
                                 floor(n / 0x100),
                                 n % 0x100,
                                 tag < 0 and tag + 0x100 or tag)
    elseif n <= 4294967295.0 then
        buffer[#buffer+1] = char(0xC9,          -- ext&32
                                 floor(n / 0x1000000),
                                 floor(n / 0x10000) % 0x100,
                                 floor(n / 0x100) % 0x100,
                                 n % 0x100,
                                 tag < 0 and tag + 0x100 or tag)
    else
        error"overflow in pack 'ext'"
    end
    buffer[#buffer+1] = data
end
function m.pack (data)
    local buffer = {}
    packers[type(data)](buffer, data)
    return tconcat(buffer)
end
local unpackers         -- forward declaration
    local s, i, j = c.s, c.i, c.j
    if i > j then
        c:underflow(i)
        s, i, j = c.s, c.i, c.j
    end
    local val = s:byte(i)
    c.i = i+1
    return unpackers[val](c, val)
end
m.unpack_cursor = unpack_cursor
local function unpack_str (c, n)
    local s, i, j = c.s, c.i, c.j
    local e = i+n-1
    if e > j or n < 0 then
        c:underflow(e)
        s, i, j = c.s, c.i, c.j
        e = i+n-1
    end
    c.i = i+n
    return s:sub(i, e)
end
local function unpack_array (c, n)
    local t = {}
    for i = 1, n do
        if k == nil or k ~= k then
            k = m.sentinel
        end
        if k ~= nil then
            t[k] = val
        end
    end
    return t
end
    local s, i, j = c.s, c.i, c.j
    if i+3 > j then
        c:underflow(i+3)
        s, i, j = c.s, c.i, c.j
    end
    local b1, b2, b3, b4 = s:byte(i, i+3)
    local sign = b1 > 0x7F
    local expo = (b1 % 0x80) * 0x2 + floor(b2 / 0x80)
    local mant = ((b2 % 0x80) * 0x100 + b3) * 0x100 + b4
    if sign then
        sign = -1
    else
        sign = 1
    end
    local n
    if mant == 0 and expo == 0 then
        n = sign * 0.0
    elseif expo == 0xFF then
        if mant == 0 then
            n = sign * huge
        else
            n = 0.0/0.0
        end
    else
        n = sign * ldexp(1.0 + mant / 0x800000, expo - 0x7F)
    end
    c.i = i+4
    return n
end
    local s, i, j = c.s, c.i, c.j
    if i+7 > j then
        c:underflow(i+7)
        s, i, j = c.s, c.i, c.j
    end
    local b1, b2, b3, b4, b5, b6, b7, b8 = s:byte(i, i+7)
    local sign = b1 > 0x7F
    local expo = (b1 % 0x80) * 0x10 + floor(b2 / 0x10)
    local mant = ((((((b2 % 0x10) * 0x100 + b3) * 0x100 + b4) * 0x100 + b5) * 0x100 + b6) * 0x100 + b7) * 0x100 + b8
    if sign then
        sign = -1
    else
        sign = 1
    end
    local n
    if mant == 0 and expo == 0 then
        n = sign * 0.0
    elseif expo == 0x7FF then
        if mant == 0 then
            n = sign * huge
        else
            n = 0.0/0.0
        end
    else
        n = sign * ldexp(1.0 + mant / 4503599627370496.0, expo - 0x3FF)
    end
    c.i = i+8
    return n
end
    local s, i, j = c.s, c.i, c.j
    if i+1 > j then
        c:underflow(i+1)
        s, i, j = c.s, c.i, c.j
    end
    local b1, b2 = s:byte(i, i+1)
    c.i = i+2
    return b1 * 0x100 + b2
end
    local s, i, j = c.s, c.i, c.j
    if i+7 > j then
        c:underflow(i+7)
        s, i, j = c.s, c.i, c.j
    end
    local b1, b2, b3, b4, b5, b6, b7, b8 = s:byte(i, i+7)
    c.i = i+8
    return ((((((b1 * 0x100 + b2) * 0x100 + b3) * 0x100 + b4) * 0x100 + b5) * 0x100 + b6) * 0x100 + b7) * 0x100 + b8
end
    local s, i, j = c.s, c.i, c.j
    if i > j then
        c:underflow(i)
        s, i, j = c.s, c.i, c.j
    end
    local b1 = s:byte(i)
    c.i = i+1
    if b1 < 0x80 then
        return b1
    else
        return b1 - 0x100
    end
end
    local s, i, j = c.s, c.i, c.j
    if i+1 > j then
        c:underflow(i+1)
        s, i, j = c.s, c.i, c.j
    end
    local b1, b2 = s:byte(i, i+1)
    c.i = i+2
    if b1 < 0x80 then
        return b1 * 0x100 + b2
    else
        return ((b1 - 0xFF) * 0x100 + (b2 - 0xFF)) - 1
    end
end
    local s, i, j = c.s, c.i, c.j
    if i+3 > j then
        c:underflow(i+3)
        s, i, j = c.s, c.i, c.j
    end
    local b1, b2, b3, b4 = s:byte(i, i+3)
    c.i = i+4
    if b1 < 0x80 then
        return ((b1 * 0x100 + b2) * 0x100 + b3) * 0x100 + b4
    else
        return ((((b1 - 0xFF) * 0x100 + (b2 - 0xFF)) * 0x100 + (b3 - 0xFF)) * 0x100 + (b4 - 0xFF)) - 1
    end
end
    local s, i, j = c.s, c.i, c.j
    if i+7 > j then
        c:underflow(i+7)
        s, i, j = c.s, c.i, c.j
    end
    local b1, b2, b3, b4, b5, b6, b7, b8 = s:byte(i, i+7)
    c.i = i+8
    if b1 < 0x80 then
        return ((((((b1 * 0x100 + b2) * 0x100 + b3) * 0x100 + b4) * 0x100 + b5) * 0x100 + b6) * 0x100 + b7) * 0x100 + b8
    else
        return ((((((((b1 - 0xFF) * 0x100 + (b2 - 0xFF)) * 0x100 + (b3 - 0xFF)) * 0x100 + (b4 - 0xFF)) * 0x100 + (b5 - 0xFF)) * 0x100 + (b6 - 0xFF)) * 0x100 + (b7 - 0xFF)) * 0x100 + (b8 - 0xFF)) - 1
    end
end
function m.build_ext (tag, data)
    return nil
end
local function unpack_ext (c, n, tag)
    local s, i, j = c.s, c.i, c.j
    local e = i+n-1
    if e > j or n < 0 then
        c:underflow(e)
        s, i, j = c.s, c.i, c.j
        e = i+n-1
    end
    c.i = i+n
    return m.build_ext(tag, s:sub(i, e))
end
unpackers = setmetatable({
    [0xC0] = function () return nil end,
    [0xC2] = function () return false end,
    [0xC3] = function () return true end,
}, {
    __index = function (t, k)
        if k < 0xC0 then
            if k < 0x80 then
                return function (c, val) return val end
            elseif k < 0x90 then
                return function (c, val) return unpack_map(c, val % 0x10) end
            elseif k < 0xA0 then
                return function (c, val) return unpack_array(c, val % 0x10) end
            else
                return function (c, val) return unpack_str(c, val % 0x20) end
            end
        elseif k > 0xDF then
            return function (c, val) return val - 0x100 end
        else
            return function () error("unpack '" .. format('%#x', k) .. "' is unimplemented") end
        end
    end
})
local function cursor_string (str)
    return {
        s = str,
        i = 1,
        j = #str,
        underflow = function ()
                        error "missing bytes"
                    end,
    }
end
local function cursor_loader (ld)
    return {
        s = '',
        i = 1,
        j = 0,
        underflow = function (self, e)
                        self.s = self.s:sub(self.i)
                        e = e - self.i + 1
                        self.i = 1
                        self.j = 0
                        while e > self.j do
                            local chunk = ld()
                            if not chunk then
                                error "missing bytes"
                            end
                            self.s = self.s .. chunk
                            self.j = #self.s
                        end
                    end,
    }
end
function m.unpack (s)
    checktype('unpack', 1, s, 'string')
    local cursor = cursor_string(s)
    local data = unpack_cursor(cursor)
    if cursor.i <= cursor.j then
        error "extra bytes"
    end
    return data
end
function m.unpacker (src)
    if type(src) == 'string' then
        local cursor = cursor_string(src)
        return function ()
            if cursor.i <= cursor.j then
                return cursor.i, unpack_cursor(cursor)
            end
        end
    elseif type(src) == 'function' then
        local cursor = cursor_loader(src)
        return function ()
            if cursor.i > cursor.j then
                pcall(cursor.underflow, cursor, cursor.i)
            end
            if cursor.i <= cursor.j then
                return true, unpack_cursor(cursor)
            end
        end
    else
        argerror('unpacker', 1, "string or function expected, got " .. type(src))
    end
end
set_string'string_compat'
set_integer'unsigned'
if SIZEOF_NUMBER == 4 then
    maxinteger = 16777215
    mininteger = -maxinteger
    m.small_lua = true
    unpackers[0xCB] = nil       -- double
    unpackers[0xCF] = nil       -- uint64
    unpackers[0xD3] = nil       -- int64
    set_number'float'
else
    maxinteger = 9007199254740991
    mininteger = -maxinteger
    set_number'double'
    if SIZEOF_NUMBER > 8 then
        m.long_double = true
    end
end
set_array'without_hole'
m._VERSION = '0.5.2'
m._DESCRIPTION = "lua-MessagePack : a pure Lua implementation"
-- like Lua itself.
--


account

-- expect to have PB and Utils passed in
local PB, Utils, Policy = ...
assert(PB)
assert(Utils)
assert(Policy)
local Account = {
  -- Account key => {
  --   key: (redis key),
  --   pb: PB go.chromium.org.luci.server.quota.Account,
  --   account_status: AccountStatus string,
  -- }
  CACHE = {},
}
-- TODO(iannucci) - make local references for all readonly dot-access stuff?
local math_floor = math.floor
local math_min = math.min
local math_max = math.max
local NOW = Utils.NOW
local redis_call = redis.call
local AccountPB = "go.chromium.org.luci.server.quota.quotapb.Account"
function Account:get(key)
  assert(key, "Account:get called with <nil>")
  assert(key ~= "", "Account:get called with ''")
  local entry = Account.CACHE[key]
  if not entry then
    local raw = redis_call("GET", key)
    entry = {
      key = key,
      -- NOTE: could keep track of whether this Account needs to be written, but
      -- if we are loading it, it means that the user has elected to perform an
      -- Op on this Account. Only in rather pathological cases would this result
      -- in a total no op (no refill, Op e.g. subtracts 0 to the balance). So,
      -- we elect to just always write all loaded Accounts at the end of the
      -- update script.
    }
    if raw then
      local ok, pb = pcall(PB.unmarshal, AccountPB, raw)
      if ok then
        entry.pb = pb
        entry.account_status = "ALREADY_EXISTS"
      else
        -- NOTE: not-ok implies that this account is broken; we treat this the
        -- same as a non-existant account (i.e. we'll make a new one).
        entry.account_status = "RECREATED"
      end
    else
      entry.account_status = "CREATED"
    end
    if not entry.pb then
      entry.pb = PB.new(AccountPB, {
        -- This Account will be (re)created, so it's updated_ts is NOW.
        updated_ts = NOW,
      })
    end
    setmetatable(entry, self)
    self.__index = self
    self.CACHE[key] = entry
    -- The entry was not new; it may have some refill policy to apply.
    --
    -- We set updated_ts here and not in applyRefill because the way refill is
    -- defined; We do the refill, under the existing policy, exactly once when
    -- the Account is loaded. Note that the existing policy could be missing,
    -- infinite, or just a regular refill policy. In ALL of those cases, we want
    -- any future policy application which happens in this invocation to
    -- calculate vs NOW, not vs any previous updated_ts time.
    --
    -- We could move this to applyRefill... but here it is a clearer indicator
    -- that NO applyRefill calls (e.g. in the case of applying an infinite
    -- refill policy via an Op) in this process will use any previous
    -- updated_ts.
    --
    -- You will note that updated_ts is only ever set to NOW in this whole
    -- program, and both of those assignments happen here, in this constructor.
    if entry.account_status == "ALREADY_EXISTS" then
      entry:applyRefill()
      entry.pb.updated_ts = NOW
    end
  end
  return entry
end
local RELATIVE_TO_CURRENT_BALANCE = "CURRENT_BALANCE"
local RELATIVE_TO_DEFAULT = "DEFAULT"
local RELATIVE_TO_ZERO = "ZERO"
-- isInfiniteRefill determines if the policy has a refill policy, and if so, if
-- it is positively infinite.
--
-- An 'infinite refill policy' is one where the `interval` (i.e. how frequently
-- `units` apply is 0) and `units` is positive. This is because the policy would
-- logically fill the account at a rate of `units / interval` (thus answering
-- the age-old question of what happens when you divide by zero).
--
-- It is not valid to have an interval of 0 with units <= 0. "Negative Infinity"
-- policies are better represented by just setting the policy limit to 0. This
-- function raises an error in such a case.
local isInfiniteRefill = function(policy)
  if not policy then
    return false
  end
  if not policy.refill then
    return false
  end
  if policy.refill.interval ~= 0 then
    return false
  end
  if policy.refill.units <= 0 then
    error("invalid zero-interval refill policy")
  end
  return true
end
local opts = PB.E["go.chromium.org.luci.server.quota.quotapb.Op.Options"]
local IGNORE_POLICY_BOUNDS = opts.IGNORE_POLICY_BOUNDS
local DO_NOT_CAP_PROPOSED = opts.DO_NOT_CAP_PROPOSED
-- computeProposed computes the new, proposed, balance value for an account.
--
-- Args:
--   * op -- The Op message
--   * new_account (bool) -- True if this acccount is 'new' (i.e. prior to this
--     Op, the Account did not exist)
--   * current (number) -- The balance of the account (undefined for new
--     accounts).
--   * limit (number or nil) -- The limit of the account's policy, or nil if the
--     account has no policy.
--   * default (number or nil) -- The default for a new account, or nil if the
--     account has no policy.
--
-- Returns the proposed account balance (number) plus a status string (if there
-- was a error). If `op` is malformed, this raises an error.
local computeProposed = function(op, new_account, current, policy)
  local relative_to = op.relative_to
  if relative_to == RELATIVE_TO_ZERO then
    return op.delta
  end
  if policy == nil and new_account then
    -- no policy and this account didn't exist? `current` is undefined, since we
    -- don't know what its default should be.
    return 0, "ERR_POLICY_REQUIRED"
  end
  if relative_to == RELATIVE_TO_CURRENT_BALANCE then
    return current + op.delta
  end
  if policy == nil then
    return 0, "ERR_POLICY_REQUIRED"
  end
    return policy.limit + op.delta
  end
  if relative_to == RELATIVE_TO_DEFAULT then
    return policy.default + op.delta
  end
  error("invalid `relative_to` value: "..op.relative_to)
end
function Account:applyOp(op, result)
  -- If there is a policy_ref attempt to set the policy.
  if op.policy_ref ~= nil then
    local policy_raw = Policy.get(op.policy_ref)
    if not policy_raw then
      result.status = "ERR_UNKNOWN_POLICY"
      return
    end
    self:setPolicy(policy_raw)
  end
  local pb = self.pb
  local policy = pb.policy
  -- this weird construction checks if the bit is set in options.
  local options = op.options
  local ignore_bounds = (options/IGNORE_POLICY_BOUNDS)%2 >= 1
  local no_cap = (options/DO_NOT_CAP_PROPOSED)%2 >= 1
  if ignore_bounds and no_cap then
    error("IGNORE_POLICY_BOUNDS and DO_NOT_CAP_PROPOSED both set")
  end
  local current = pb.balance
  -- step 1; figure out what value they want to set the account to.
  -- NOTE: computeProposed will return an error status if the op wants to compute
  -- something relative to a value we don't have (e.g. the balance for a `new`
  -- Account, or the limit/default for a policy-less Account).
  local proposed, status = computeProposed(op, self.account_status ~= "ALREADY_EXISTS", current, policy)
  if status ~= nil then
    result.status = status
    return
  end
  local limit = nil
  if policy then
    limit = policy.limit
  end
  if not (no_cap or ignore_bounds) then
    -- We haven't been instructed to cap the proposed value by policy.limit, and
    -- we also haven't been instructed to completely ignore the bounds.
    proposed = math_min(proposed, limit)
  end
  -- step 2, figure out how to apply the proposed value.
  if ignore_bounds then
    -- No boundaries matter, use the proposed value as-is.
    pb.balance = proposed
  elseif policy == nil then
    -- You cannot apply a value to an Account lacking a policy, without using
    -- the ignore_bounds flag, so check that here.
    result.status = "ERR_POLICY_REQUIRED"
    return
  elseif proposed >= 0 and proposed <= limit then
    -- We are respecting policy bounds, and this proposed value is "in bounds".
    if isInfiniteRefill(policy) then
      -- setting a value 'in bounds' with an infinite policy means that the
      -- balance automatically replenishes to policy.limit. Note that pb.balance
      -- could, itself, be out of bounds here (i.e. balance could be negative or
      -- over the limit), but the proposal puts us in [0, limit], whereupon the
      -- policy.refill now applies.
      pb.balance = limit
    else
      pb.balance = proposed
    end
  else
    -- We are respecting policy bounds, and this proposed value is "out of bounds"
    -- (either below 0 or above policy.limit).
    --
    -- We allow updates which bring the account balance back towards [0, limit].
    -- That is, we allow a credit to a negative account balance, and we allow
    -- a debit from an overflowed account balance.
    --
    -- However we wouldn't allow making a negative balance MORE negative or an
    -- overflowed balance MORE overflowed.
    --
    -- NOTE: Even with an infinite refill policy, you are not allowed to reduce
    -- the balance below 0 without UNDERFLOW-ing.
    if proposed < 0 and proposed < current then
      result.status = "ERR_UNDERFLOW"
      return
    end
    if proposed > limit and proposed > current then
      result.status = "ERR_OVERFLOW"
      return
    end
    pb.balance = proposed
  end
  -- We applied an op without error; the Account is now "ALREADY_EXISTS" for any
  -- subsequent ops.
  self.account_status = "ALREADY_EXISTS"
end
function Account:applyRefill()
  -- NOTE: in this function we assume that policy has already been validated and
  -- follows all validation rules set in `policy.proto`.
  local policy = self.pb.policy
  if policy == nil then
    return
  end
  local limit = policy.limit
  local refill = policy.refill
  if refill == nil then
    return
  end
  local curBalance = self.pb.balance
  if isInfiniteRefill(policy) then
    self.pb.balance = math_max(curBalance, limit)
    return
  end
  local units = refill.units
  local interval = refill.interval
  -- we subtract offset from all timestamps for this calculation; this has the
  -- effect of shifting midnight forwards by pushing all the timestamps
  -- backwards.
  --
  -- NOTE - Because interval and offset are defined as UTC midnight aligned
  -- seconds, we can completely ignore the nanos field in both updated_ts and
  -- NOW, since they would be obliterated by the % interval logic below anyway.
  local offset = refill.offset
  local updated_unix = self.pb.updated_ts.seconds - offset
  local now_unix = NOW.seconds - offset
  -- Intervals:  A     B     C     D
  -- Timestamps:    U          N
  --
  -- first_event_unix == B
  -- last_event_unix == C
  --
  -- num_events == 2
  -- find first refill event after updated_ts
  local first_event_unix = (updated_unix - (updated_unix % interval)) + interval
  -- find last refill event which happened before NOW
  local last_event_unix = (now_unix - (now_unix % interval))
  if last_event_unix < first_event_unix then
    return
  end
  local num_events = ((last_event_unix - first_event_unix) / interval) + 1
  -- we should always have an integer number of events
  assert(math_floor(num_events) == num_events)
  local delta = num_events * units
  if delta > 0 and curBalance < limit then
    self.pb.balance = math_min(curBalance+delta, limit)
  elseif delta < 0 and curBalance > 0 then
    self.pb.balance = math_max(curBalance+delta, 0)
  end
end
local policyRefEq = function(a, b)
  if a == nil and b == nil then
    return true
  end
  if a == nil and b ~= nil then
    return false
  end
  if a ~= nil and b == nil then
    return false
  end
  return a.config == b.config and a.key == b.key
end
function Account:setPolicy(policy)
  if policy then
    -- sets Policy on this Account, updating its policy entry and replenishing it.
    if not policyRefEq(self.pb.policy_ref, policy.policy_ref) then
      self.pb.policy = policy.pb
      self.pb.policy_ref = policy.policy_ref
      self.pb.policy_change_ts = NOW
    end
    -- this account didn't exist before; set the initial value.
    if self.account_status ~= "ALREADY_EXISTS" then
      -- if multiple ops apply to this Account, treat the account as existing
      -- from this point on.
      self.pb.balance = policy.pb.default
    end
    self:applyRefill() -- in case this policy is infinite, this will set the balance to limit.
  else
    -- explicitly unset the policy; no further action is needed.
    self.pb.policy = nil
    self.pb.policy_ref = nil
    self.pb.policy_change_ts = NOW
  end
end
function Account:write()
  local lifetime_ms = nil
  if self.pb.policy then
    lifetime_ms = Utils.Millis(self.pb.policy.lifetime)
  end
  local raw = PB.marshal(self.pb)
  if lifetime_ms then
    redis_call('SET', self.key, raw, 'PX', lifetime_ms)
  else
    redis_call('SET', self.key, raw)
  end
end
function Account.ApplyOps(oplist)
  local ret = PB.new("go.chromium.org.luci.server.quota.quotapb.ApplyOpsResponse")
  local allOK = true
  for i, op in ipairs(oplist) do
    local account = Account:get(op.account_ref)
    local result = PB.new("go.chromium.org.luci.server.quota.quotapb.OpResult", {
      status = "SUCCESS",  -- by default; applyOp can overwrite this.
      account_status = account.account_status,
      previous_balance = account.pb.balance,
    })
    ret.results[i] = result
    -- applyOp should never raise an error, so any caught here are unknown.
    local ok, err = pcall(account.applyOp, account, op, result)
    if not ok then
      result.status = "ERR_UNKNOWN"
      result.status_msg = tostring(err)
    end
    if result.status == "SUCCESS" then
      result.new_balance = account.pb.balance
    else
      allOK = false
    end
  end
  if allOK then
    for key, account in pairs(Account.CACHE) do
      account:write()
    end
    ret.originally_set = NOW
  end
  return ret, allOK
end
return Account


utils

-- expect these to be passed in
local PB = ...
assert(PB)
local redis = redis
local Utils = {}
-- Get the current time; We treat the script execution as atomic, so all
-- accounts will be updated at exactly the same timestamp.
--
-- Note that time returns seconds and MICRO seconds (not milliseconds) within
-- that second.
local nowRaw = redis.call('TIME')
Utils.NOW = PB.new('google.protobuf.Timestamp', {
  seconds = tonumber(nowRaw[1]),
  nanos = tonumber(nowRaw[2]) * 1000,
})
local math_floor = math.floor
function Utils.Millis(duration)
  if duration == nil then
    return nil
  end
  return math_floor(duration.seconds * 1000 + duration.nanos / 1000000)
end
function Utils.WithRequestID(req, callback)
  local req_id_key = req.request_key
  if req_id_key ~= "" then
    local hash_scheme, hash, response = unpack(redis.call(
       "HMGET", req_id_key, "hash_scheme", "hash", "response"))
    if hash_scheme ~= nil then
      local my_scheme = tonumber(hash_scheme)
      if response then
        if req.hash_scheme ~= my_scheme then
          -- hash scheme mismatch; just return the response
          return response
        elseif req.hash_scheme == my_scheme and req.hash == hash then
          -- hash match; return response.
          return response
        else
          return redis.error_reply('REQUEST_HASH')
        end
      end
    end
  end
  -- either there was no request_key, or this is the first call.
  local ret, allOK = callback()
  local response = PB.marshal(ret)
  if allOK and req_id_key ~= "" then
    redis.call(
      "HSET", req_id_key,
      "hash_scheme", tostring(req.hash_scheme),
      "hash", req.hash,
      "response", response
    )
    local ttl = Utils.Millis(req.request_key_ttl)
    if ttl == nil then
      -- 2 hours * 60 minutes * 60 seconds * 1000 milliseconds.
      ttl = 2 * 60 * 60 * 1000
    end
    if ttl > 0 then
      redis.call("PEXPIRE", req_id_key, ttl)
    end
  end
  return response
end
return Utils


update-accounts

-- Updates multiple quota accounts in one go.
-- cmsgpack will either be global (production) or will be passed via
-- quotatestmonkeypatch.
local PB = loadfile('../../quotapb/quotapb.lua')(cmsgpack)
PB.setInternTable(KEYS)
local Utils = loadfile('utils.lua')(PB)
local req = PB.unmarshal("go.chromium.org.luci.server.quota.quotapb.UpdateAccountsInput", ARGV[1])
return Utils.WithRequestID(req, function()
  -- We load these 'lazily' because WithRequestID may return early without ever
  -- calling this function.
  local Policy = loadfile('policy.lua')(PB, Utils)
  local Account = loadfile('account.lua')(PB, Utils, Policy)
  return Account.ApplyOps(req.ops)
end)


policy

-- expect to have PB and Utils passed in
local PB, Utils = ...
assert(PB)
assert(Utils)
local Policy = {
  -- config => key => {
  --   policy_ref: PB go.chromium.org.luci.server.quota.PolicyRef,
  --   pb: PB go.chromium.org.luci.server.quota.Policy,
  -- }
  CACHE = {},
}
local PolicyPB = "go.chromium.org.luci.server.quota.quotapb.Policy"
local PolicyRefPB = "go.chromium.org.luci.server.quota.quotapb.PolicyRef"
function Policy.get(policy_ref)
  assert(policy_ref ~= nil, "Policy:get called with <nil>")
  local configkey = policy_ref.config
  local policykey = policy_ref.key
  if configkey == "" and policykey == "" then
    return nil
  end
  local configTable = Policy.CACHE[configkey]
  if not configTable then
    configTable = {}
    Policy.CACHE[configkey] = configTable
  end
  local entry = configTable[policykey]
  if not entry then
    local raw = redis.call("HGET", configkey, policykey)
    if not raw then
      return nil -- will result in "ERR_UNKNOWN_POLICY"
    end
    entry = {
      policy_ref = policy_ref,
      pb = PB.unmarshal(PolicyPB, raw),
    }
    configTable[policykey] = entry
  end
  return entry
end
return Policy