sorcery  marshal.lua at [f9d8ede806]

File lib/marshal.lua artifact 30a22c2236 part of check-in f9d8ede806


-- 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