-- minetest provides its own built-in serializer mechanisms. however,
-- these leave much to be desired: the first works by converting
-- values to *lua source code* and 'deserializes' these values by
-- executing it in a mildly sandboxed env. it's LSON basically. the
-- other serializer is JSON. clearly we need something better. if this
-- was lua 5.3, we'd just use string.pack, but alas, they had to use
-- luajit, which is stuck on an ancient version because it no longer
-- exists. if we built against moonjit, we could use pack, but then
-- that would raise compat issues for other users who might want to
-- use these mods. so we need to write our own.
--
-- good news is, it's very easy to do better than both the minetest
-- people and the clowns at PUC-Rio. (if only we had general purpose
-- bitops, it would be even easier)
--
-- WARNING: when storing binary data in minetest metadata stores, the
-- bytes 0x01-0x03 MUST be avoided or they will break the kvstore
-- format and the item/node's metadata will become corrupt. use the
-- lib.str.meta_{,de}armor functions on the output of pack/unpack to
-- safely store and retrieve data structures from meta storage.
local m = {
err = {
unmarshalled = {
exp = 'the bytes passed are not a marshalled data structure';
};
corrupt = {
exp = 'the marshalled data is corrupt';
};
domain = {
exp = 'a value is outside the allowed domain';
};
};
}
m.wrong = function(e)
for k,v in pairs(m.err) do
if v == e then return k end
end
return false
end;
local proto = {
header = string.char(0xFE,0x99)
}
local intcoder = function(bits,signed)
local bytes = math.ceil(bits / 8)
local max = 2 ^ bits
local spoint = math.floor(max/2)
return {
enc = function(obj)
local val = math.abs(obj)
local str = ''
if signed then
local max = math.floor(max / 2)
if (obj > max) or (obj < (0-(max+1))) then
return m.err.domain end
if obj < 0 then val = val + spoint end
-- e.g. for 8bit: 0x80 == -1; 0xFF = -128
else
if val > max then return m.err.domain end
end
for i=1,bytes do
local n = math.fmod(val, 0x100)
str = str .. string.char(n)
val = math.floor(val / 0x100)
end
return str
end;
dec = function(str)
local val = 0
for i = 0, bytes-1 do
local b = string.byte(str,bytes - i)
val = (val * 0x100) + (b or 0)
end
if signed then
if val > spoint then val = 0 - (val - spoint) end
end
return val, string.sub(str, 1 + bytes)
end;
}
end
local blobcoder = function(bitlen)
return {
enc = function(obj)
obj = obj or ''
return intcoder(bitlen,false).enc(string.len(obj)) .. obj
end;
dec = function(str)
local len, ns = intcoder(bitlen,false).dec(str)
local val = string.sub(ns, 1, len)
return val, string.sub(ns, len + 1)
end;
}
end
local arycoder = function(bitlen,t)
return {
enc = function(obj)
local sz = (obj and #obj) or 0
local str = intcoder(bitlen,false).enc(sz)
for i=1,sz do
str = str .. t.enc(obj[i])
end
return str
end;
dec = function(str)
local ct, body = intcoder(bitlen,false).dec(str)
local obj = {}
for i=1,ct do
obj[i], body = t.dec(body)
end
return obj, body
end;
}
end
m.t = {
u8 = intcoder( 8,false), s8 = intcoder( 8,true);
u16 = intcoder(16,false), s16 = intcoder(16,true);
u32 = intcoder(32,false), s32 = intcoder(32,true);
u64 = intcoder(64,false), s64 = intcoder(64,true);
-- technically this is sort of just a 52-bit integer
-- type since while the logic could handle 64-bit ints
-- with no problem, lua 5.2 only has doubles, so we
-- only have 52 bits of mantissa to work with. but in
-- lua 5.3 this will work as a full i64
str = blobcoder(16,false);
text = blobcoder(32,false);
}
m.transcoder = function(format)
local keys = {}
for k in pairs(format) do keys[#keys + 1] = k end
table.sort(keys)
local encoder = function(struct)
struct = struct or {}
local str = proto.header -- identify the marshalling format
-- via a magic number so we can gracefully detect marshalled
-- data, and possibly handle breaking upgrades
for i=1,#keys do
local fld = keys[i]
local ty = format[fld]
local code = ty.enc(struct[fld])
-- propagate error conditions
if m.wrong(code) then return code end
str = str .. code
end
return str
end
local decoder = function(s)
local obj = {}
if string.sub(s,1,2) ~= proto.header then
return m.err.unmarshalled end
local str = string.sub(s,3)
for i=1,#keys do
local fld = keys[i]
local ty = format[fld]
local code, newstr = ty.dec(str)
if m.wrong(code) then return code end
obj[fld], str = code, newstr
end
return obj, str
end
return encoder, decoder
end
m.g = {
blob = blobcoder;
int = intcoder;
array = arycoder;
struct = function(format)
-- okay, this is a bit of an abuse and a little
-- inefficient, because this is the raw transcoder
-- function, meaning it stores nested copies of
-- the protocol header. i can't be arsed to care
local encoder, decoder = m.transcoder(format)
return { enc = encoder, dec = decoder };
end;
}
-- example:
--- local pack, unpack = m.transcoder {
--- version = m.t.u16;
--- name = m.t.str;
--- time = m.t.u64;
--- blob = m.t.text;
--- }
return m