starlit  marshal.lua at [d6efac25ef]

File mods/vtlib/marshal.lua artifact 979397aff5 part of check-in d6efac25ef


-- [ʞ] marshal.lua
--  ~ lexi hale <lexi@hale.su>
--  © EUPLv1.2
--  ? a replacement for the shitty old Sorcery marshaller
--    this marshaller focuses on reliability and extensibility;
--    it is much less space-efficient than the (broken) Sorcery
--    marshaller or the slick parvan marshaller. i don't care anymore

local lib = ...
local m = {
	t = {};
	g = {};
}

local T,G = m.t, m.g

-- a type is an object with two functions 'enc' and 'dec'

----------------
--- utilities --
----------------


local debugger = lib.dbg.debugger 'marshal'
local report = debugger.report

function m.streamReader(blob)
	local idx = 1
	local blobLen = #blob
	local function advance(ct)
		if not ct then
			--report('advancing to end of %s-byte blob',  blobLen)
			idx = blobLen+1
		else
			--report('advancing %s bytes from %s of %s', ct, idx, blobLen)
			assert(idx+ct <= blobLen + 1)
			idx = idx + ct
		end
	end
	local function dataLeft() return idx <= blobLen end
	local function consume(ct)
		if ct == 0 then return '' end
		assert(dataLeft(), string.format('wanted %s bytes but no data left: %s/%s', ct, idx, blobLen))
		local str = string.sub(blob, idx,
		  ct and idx + ct - 1 or nil)
		advance(ct)
		return str
	end
	return {
		dataLeft = dataLeft;
		advance = advance;
		consume = consume;
		dec = function(t) -- parse a fixed-size type in the stream
			assert(t.sz, 'type ' .. t.name .. ' is variably-sized')
			report('parsing type %s from stream at %s/%s', t.name, idx, blobLen)
			return t.dec(consume(t.sz))
		end;
	}
end

function m.streamEncoder(sizeType)
	local encFrags = {}
	local encoder = {}
	function encoder.push(v, ...)
		assert(type(v) == 'string')
		if v ~= nil then
			table.insert(encFrags, v)
		end
		if select('#', ...) > 0 then encoder.push(...) end
	end
	function encoder.ppush(...) -- "pascal push"
		local sz = 0
		local function szi(e, ...)
			if e then sz = sz + #e end
			if select('#') > 0 then szi(...) end
		end
		szi(...)
		encoder.push(sizeType.enc(sz), ...)
	end
	function encoder.pspush(v, ...) -- "pascal struct push"
		if v~=nil then encoder.ppush(v) end
		if select('#', ...) > 0 then encoder.prpush(...) end
	end
	function encoder.peek()
		return table.concat(encFrags)
	end
	function encoder.pull()
		local s = encoder.peek()
		encFrags = {}
		return s
	end

	return encoder
end

function m.metaStore(map, prefix)
	report('generating metaStore for %s', dump(map))
	if prefix == true then prefix = minetest.get_current_modname() end
	local function keyFor(k)
		k = map[k].key
		if prefix then return prefix .. ':' .. k end
		return k
	end

	return function(obj)
		local m = obj:get_meta()
		local store = {}
		function store.write(key, val)
			report('store: setting %q(%q)=%s (mapping %s)', key, keyFor(key), dump(val), dump(map[key]))
			local armored = lib.str.meta_armor(map[key].type.enc(val))
			m:set_string(keyFor(key), armored)
			return store
		end
		function store.read(key)
			report('store: reading %q', key)
			local dearmored = lib.str.meta_dearmor(m:get_string(keyFor(key)))
			return map[key].type.dec(dearmored)
		end
		function store.erase(key)
			m:set_string(keyFor(key), '')
			return store
		end
		function store.over(key,fn)
			local n = fn(read(key))
			if n ~= nil then write(key,n) end
			return store
		end
		return store
	end
end


-------------------------------
-- generic type constructors --
-------------------------------

function G.int(bits,signed)
	local bytes = math.ceil(bits / 8)
	local max = 2 ^ bits
	local spoint = math.floor(max/2)
	local name = string.format("%sint<%s>",
			signed and 's' or 'u', bits
		);
	return {
		name = name;
		sz = bytes;
		enc = function(obj)
			report('encoding %s value=%s', name, dump(obj))
			obj = obj or 0
			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 error('domain error') end
				if obj < 0 then val = val + spoint end
				-- e.g. for 8bit: 0x80 == -1; 0xFF = -128
			else
				if val > max then error('domain error') 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
		end;
	}
end

local size = G.int(32, false)


function G.struct(...)
	-- struct record {
	--		uint< 8> keySz;
	--		uint<32> valSz;
	--		string[keySz] name;
	--		string[valSz] data;
	-- }
	-- struct struct {
	--		uint<32>         nRecords;
	--		record[nRecords] records;
	-- }
	local def, name
	if select('#', ...) >= 2 then
		name, def = ...
	else
		def = ...
	end
	name = 'struct' .. (name and (':' .. name) or '');
	report('defining struct name=%q fields=%s', name, dump(def))
	return {
		name = name;
		enc = function(obj)
		report('encoding struct name=%q vals=%s', name, dump(obj))
			local enc = m.streamEncoder()
			local n = 0
			for k,ty in pairs(def) do n=n+1
				if obj[k] == nil then
					error(string.format("missing %s field %q for %s", ty.name, k, name))
				end
				local encoded = ty.enc(obj[k])
				enc.push(T.u8.enc(#k), size.enc(#encoded), k, encoded)
			end
			return size.enc(n) .. enc.peek()
		end;
		dec = debugger.wrap(function(blob)
			if blob == '' then
				-- struct is more likely to be used directly, as the top of a serialization
				-- tree, which means it is more likely to be exposed to ill-formed input.
				-- a particularly common case will be the empty string, returned when a
				-- get_string is performed on an empty key. i think the most sensible behavior
				-- here is to return nil, rather than just crashing
				return nil
			end
			local s = m.streamReader(blob)
			local obj = {}
			report('struct.dec: decoding type %s; reading string %s', name, dump(blob))
			local nRecords = s.dec(size)
			while s.dataLeft() and nRecords > 0 do
				report('%s records left', nRecords)
				local	ksz = s.dec(T.u8)
				local	vsz = s.dec(size)
				local k = s.consume(ksz)
				local v = s.consume(vsz)
				local ty = def[k]
				report('decoding field %s of type %s, %s bytes', k, ty.name, vsz)
				if not ty then
					report('warning: unfamiliar record %q found in struct', k)
				else
					obj[k] = ty.dec(v)
				end
				nRecords = nRecords - 1
			end
			if s.dataLeft() then
				report('warning: junk at end of struct %q',s.consume())
			end
			report('returning object %s', dump(obj))
			return obj
		end);
	}
end

function G.fixed(bits, base, prec, sign)
	local c = G.int(bits, sign)
	local mul = base ^ prec
	return {
		sz = c.sz;
		name = string.format("%sfixed<%s,%s,%s>",
			sign and 's' or 'u',
			bits, base, prec
		);
		enc = function(v)
			return c.enc(v * mul)
		end;
		dec = function(s)
			local v = c.dec(s)
			return v / mul
		end;
	}
end

function G.range(min, max, bits)
	local d = max-min
	local precType = G.fixed(bits, d, 1, false)
	return {
		sz = precType.sz;
		name = string.format("range<%s,%s~%s>",
			bits, min, max
		);
		enc = function(v)
			return precType.enc((v - min) / d)
		end;
		dec = function(s)
			local v = precType.dec(s)
			return d*v + min
		end;
	}
end

T.str = {
	name = 'str';
	enc = function(s) return s end;
	dec = function(s) return s end;
}


function G.array(bitlen,t)
	local sz = G.int(bitlen,false)
	local name = string.format("array<%s,%s>",
		sz.name, t.name
	);
	return {
		name = name;
		enc = debugger.wrap(function(obj)
			local s = m.streamEncoder(size)
			report('encoding array of type %s', name)
			local nVals = (obj and #obj) or 0
			s.push(sz.enc(nVals))
			for i=1,nVals do
				report('encoding value %s: %s', i, dump(obj[i]))
				s.ppush(t.enc(obj[i]) or '')
			end
			report('returning blob %q', s.peek())
			return s.peek()
		end);
		dec = debugger.wrap(function(blob)
			local s = m.streamReader(blob)
			local ct = s.dec(sz)
			local obj = {}
			report('decoding array %s of size %s', name, ct)
			for i=1,ct do
				report('decoding elt [%s] of %s', i, ct)
				local eltsz = s.dec(size)
				obj[i] = t.dec(s.consume(eltsz))
			end
			if s.dataLeft() then
				print('warning: junk at end of array', dump(s.consume))
			end
			return obj
		end);
	}
end

function G.enum(values)
	local n = #values
	local bits = 8
	if     n > 65536 then bits = 32 -- don't think we really need any more
	elseif n > 256   then bits = 16 end
	local t = G.int(bits, false)
	local map = {}
	for k,v in pairs(values) do map[v] = k end
	return {
		name = string.format("enum<[%s]>", dump(values) );
		sz = t.sz;
		enc = function(v)
			local iv = map[v] or error('value ' .. v .. ' not allowed in enum')
			return t.enc(iv)
		end;
		dec = function(s)
			return values[t.dec(s)]
		end;
	}
end

function G.class(struct, extract, construct)
	return {
		sz = struct.sz;
		name = string.format("class<%s>",
			struct.name
		);
		enc = debugger.wrap(function(v)
			report('encoding class<%s>', struct.name)
			return struct.enc(extract(v))
		end);
		dec = debugger.wrap(function(s)
			report('decoding class<%s>', struct.name)
			return construct(s ~= '' and struct.dec(s) or nil)
				-- allow classes to handle empty metastrings after their own fashion
		end);
	}
end



-------------------------
-- common type aliases --
-------------------------

for _, sz in pairs{8,16,32,64} do
	T['u' .. tostring(sz)] = G.int(sz,false)
	T['s' .. tostring(sz)] = G.int(sz,true)
end


T.factor  = G.fixed(16, 10.0, 3, false);
T.decimal = G.fixed(32, 10.0, 5, true);
T.fixed   = G.fixed(32, 2.0, 16, true);
T.double  = G.fixed(64, 10.0, 10, true);
T.wide    = G.fixed(64, 2.0, 32, true);

T.angle     = G.range(   0, 360, 16);
T.turn      = G.range(-360, 360, 16);
T.clamp     = G.range(   0, 1.0, 16);
T.tinyClamp = G.range(   0, 1.0, 8);

-----------------------------------
-- abstractions over engine data --
-----------------------------------

T.inventoryList = G.class(
	G.array(16, G.struct ('inventoryItem', {
		index = T.u16;
		itemString = T.str;
	})),
	function(lst)
		report('encoding inventory list %s', dump(lst))
		local ary = {}
		for i, s in pairs(lst) do
			if not s:is_empty() then
				table.insert(ary, {index = i, itemString = s:to_string()})
			end
		end
		report('list structure: %s', dump(ary))
		return ary
	end,
	function(ary)
		report('decoding inventory list %s', dump(ary))
		if not ary then return {} end
		local tbl = {}
		for _, s in pairs(ary) do
			tbl[s.index] = ItemStack(s.itemString)
		end
		return tbl
	end
);

T.inventory = G.class(
	G.array(8, G.struct('inventory', {
		name = T.str;
		items = T.inventoryList;
	})),
	function (inv)
		if inv.get_lists then
			inv = inv:get_lists()
		end
		local lst = {}
		for name, items in pairs(inv) do table.insert(lst, {name=name,items=items}) end
		return lst
	end,
	function (tbl)
		if not tbl then return {} end
		local inv = {}
		for _, e in pairs(tbl) do
			inv[e.name] = e.items
		end
		return inv
	end
);

-------------------
-- legacy compat --
-------------------

-- strings are now fixed at 32-bit sizetype
T.text   = T.str
T.phrase = T.str
function G.blob() return T.str end

function m.transcoder(tbl)
	local ty = G.struct(tbl)
	return ty.enc, ty.dec
end

return m