potatOS/src/lib/binary-serialization.lua

330 lines
9.8 KiB
Lua

-- by "Yevano", tweaked due to issues with signed integer handling
--[[
BLODS - Binary Lua Object (De)Serialization
]]
--[[
Save on table access.
]]
local pairs = pairs
local type = type
local loadstring = loadstring
local mathabs = math.abs
local mathfloor = math.floor
local mathfrexp = math.frexp
local mathmodf = math.modf
local mathpow = math.pow
local stringbyte = string.byte
local stringchar = string.char
local stringdump = string.dump
local stringsub = string.sub
local tableconcat = table.concat
--[[
Float conversions. Modified from http://snippets.luacode.org/snippets/IEEE_float_conversion_144.
]]
local function double2str(value)
local s=value<0 and 1 or 0
if mathabs(value)==1/0 then
return (s==1 and "\0\0\0\0\0\0\240\255" or "\0\0\0\0\0\0\240\127")
end
if value~=value then
return "\170\170\170\170\170\170\250\255"
end
local fr,exp=mathfrexp(mathabs(value))
fr,exp=fr*2,exp-1
exp=exp+1023
return tableconcat({stringchar(mathfloor(fr*2^52)%256),
stringchar(mathfloor(fr*2^44)%256),
stringchar(mathfloor(fr*2^36)%256),
stringchar(mathfloor(fr*2^28)%256),
stringchar(mathfloor(fr*2^20)%256),
stringchar(mathfloor(fr*2^12)%256),
stringchar(mathfloor(fr*2^4)%16+mathfloor(exp)%16*16),
stringchar(mathfloor(exp/2^4)%128+128*s)})
end
local function str2double(str)
local fr=stringbyte(str, 1)/2^52+stringbyte(str, 2)/2^44+stringbyte(str, 3)/2^36+stringbyte(str, 4)/2^28+stringbyte(str, 5)/2^20+stringbyte(str, 6)/2^12+(stringbyte(str, 7)%16)/2^4+1
local exp=(stringbyte(str, 8)%128)*16+mathfloor(str:byte(7)/16)-1023
local s=mathfloor(stringbyte(str, 8)/128)
if exp==1024 then
return fr==1 and (1-2*s)/0 or 0/0
end
return (1-2*s)*fr*2^exp
end
--[[
Integer conversions. Taken from http://lua-users.org/wiki/ReadWriteFormat.
Modified to support signed ints.
]]
local function signedstringtonumber(str)
local function _b2n(exp, num, digit, ...)
if not digit then return num end
return _b2n(exp*256, num + digit*exp, ...)
end
return _b2n(256, stringbyte(str, 1, -1)) - mathpow(2, #str * 8 - 1)
end
local function stringtonumber(str)
local function _b2n(exp, num, digit, ...)
if not digit then return num end
return _b2n(exp*256, num + digit*exp, ...)
end
return _b2n(256, stringbyte(str, 1, -1))
end
local function numbertobytes(num, width)
local function _n2b(width, num, rem)
rem = rem * 256
if width == 0 then return rem end
return rem, _n2b(width-1, mathmodf(num/256))
end
return stringchar(_n2b(width-1, mathmodf((num)/256)))
end
local function log2(x)
return math.log10(x) / math.log10(2)
end
--[[
(De)Serialization for Lua types.
]]
local function intWidth(int)
local out = math.ceil((log2(int) + 1) / 8)
if out == math.huge or out == -math.huge then return 1 end
return out
end
local types = {
boolean = "b",
double = "d",
posinteger = "p",
neginteger = "n",
string = "s",
table = "t",
["function"] = "f",
["nil"] = "_"
}
local serialization = { }
local deserialization = { }
function serialization.boolean(obj)
return obj and "\1" or "\0"
end
function serialization.double(obj)
return double2str(obj)
end
function serialization.integer(obj)
local width = intWidth(obj)
return stringchar(width) .. numbertobytes(obj, width)
end
function serialization.string(obj)
local len = #obj
local width = intWidth(len)
return tableconcat({ stringchar(width), numbertobytes(len, width), obj })
end
serialization["function"] = function(obj)
local ok, s = pcall(stringdump, obj)
if not ok then return "_" end
return numbertobytes(#s, 4) .. s
end
function deserialization.b(idx, ser)
local ret = stringsub(ser[1], idx, idx) == "\1"
return ret, idx + 1
end
function deserialization.d(idx, ser)
local ret = str2double(stringsub(ser[1], idx, idx + 8))
return ret, idx + 8
end
function deserialization.p(idx, ser)
local width = stringtonumber(stringsub(ser[1], idx, idx))
local ret = stringtonumber(stringsub(ser[1], idx + 1, idx + width))
return ret, idx + width + 1
end
function deserialization.n(idx, ser)
local width = stringtonumber(stringsub(ser[1], idx, idx))
local ret = stringtonumber(stringsub(ser[1], idx + 1, idx + width))
return -ret, idx + width + 1
end
function deserialization.s(idx, ser)
local width = stringtonumber(stringsub(ser[1], idx, idx))
local len = stringtonumber(stringsub(ser[1], idx + 1, idx + width))
local ret = stringsub(ser[1], idx + width + 1, idx + width + len)
return ret, idx + width + len + 1
end
function deserialization.f(idx, ser)
local len = stringtonumber(stringsub(ser[1], idx, idx + 3))
local ret = loadstring(stringsub(ser[1], idx + 4, idx + len + 3))
return ret, idx + len + 4
end
function deserialization._(idx, ser)
return nil, idx
end
local function yield()
os.queueEvent ""
os.pullEvent ""
end
if not os.queueEvent then yield = function() end end
function serialize(obj)
-- State vars.
local ntables = 1
local tables = { }
local tableIDs = { }
local tableSerial = { }
-- Internal recursive function.
local function serialize(obj)
yield()
local t = type(obj)
if t == "table" then
local len = #obj
if tables[obj] then
-- We already serialized this table. Just return the id.
return tableIDs[obj]
end
-- Insert table info.
local id = ntables
tables[obj] = true
local width = intWidth(ntables)
local ser = "t" .. numbertobytes(width, 1) .. numbertobytes(ntables, width)
tableIDs[obj] = ser
-- Important to increment here so tables inside this one don't use the same id.
ntables = ntables + 1
-- Serialize the table.
local serialConcat = { }
-- Array part.
for i = 1, len do
if obj[i] == nil then
len = i - 1
break
end
serialConcat[#serialConcat + 1] = serialize(obj[i])
end
serialConcat[#serialConcat + 1] = "\0"
-- Table part.
for k, v in pairs(obj) do
if type(k) ~= "number" or ((k > len or k < 1) or mathfloor(k) ~= k) then
-- For each pair, serialize both the key and the value.
local idx = #serialConcat
serialConcat[idx + 1] = serialize(k)
serialConcat[idx + 2] = serialize(v)
end
end
serialConcat[#serialConcat + 1] = "\0"
-- tableconcat is way faster than normal concatenation using .. when dealing with lots of strings.
-- Add this serialization to the table of serialized tables for quick access and later more concatenation.
tableSerial[id] = tableconcat(serialConcat)
return ser
else
-- Do serialization on a non-recursive type.
if t == "number" then
-- Space optimization can be done for ints, so serialize them differently from doubles.
-- OSMARKS EDIT: handle sign in type and not actual serialization
if mathfloor(obj) == obj then
local intval = serialization.integer(math.abs(obj))
local typespec = "p"
if obj < 0 then typespec = "n" end
return typespec .. intval
end
return "d" .. serialization.double(obj)
end
local ser = types[t]
return obj == nil and ser or ser .. serialization[t](obj)
end
end
-- Either serialize for a table or for a non-recursive type.
local ser = serialize(obj)
if type(obj) == "table" then
return tableconcat({ "t", tableconcat(tableSerial) })
end
return ser
end
function deserialize(ser)
local idx = 1
local tables = { { } }
local serref = { ser }
local function getchar()
local ret = stringsub(serref[1], idx, idx)
return ret ~= "" and ret or nil
end
local function deserializeValue()
yield()
local t = getchar()
idx = idx + 1
if t == "t" then
-- Get table id.
local width = stringtonumber(getchar())
idx = idx + 1
local id = stringtonumber(stringsub(serref[1], idx, idx + width - 1))
idx = idx + width
-- Create an empty table as a placeholder.
if not tables[id] then
tables[id] = { }
end
return tables[id]
else
local ret
ret, idx = deserialization[t](idx, serref)
return ret
end
end
-- Either deserialize for a table or for a non-recursive type.
local i = 1
if getchar() == "t" then
idx = idx + 1
while getchar() do
if not tables[i] then tables[i] = { } end
local curtbl = tables[i]
-- Array part.
while getchar() ~= "\0" do
curtbl[#curtbl + 1] = deserializeValue()
end
-- Table part.
idx = idx + 1
while getchar() ~= "\0" do
curtbl[deserializeValue()] = deserializeValue()
end
i = i + 1
idx = idx + 1
end
return tables[1]
end
return deserializeValue()
end
return { serialize = serialize, deserialize = deserialize }