-- [ʞ] 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('missing key '..dump(k)..' for type '..ty.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