diff options
Diffstat (limited to 'util/serialization.lua')
-rw-r--r-- | util/serialization.lua | 300 |
1 files changed, 242 insertions, 58 deletions
diff --git a/util/serialization.lua b/util/serialization.lua index 206f5fbb..dd6a2a2b 100644 --- a/util/serialization.lua +++ b/util/serialization.lua @@ -1,89 +1,271 @@ -- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain +-- Copyright (C) 2018 Kim Alvefur -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- -local string_rep = string.rep; -local type = type; -local tostring = tostring; -local t_insert = table.insert; +local getmetatable = getmetatable; +local next, type = next, type; +local s_format = string.format; +local s_gsub = string.gsub; +local s_rep = string.rep; +local s_char = string.char; +local s_match = string.match; local t_concat = table.concat; -local pairs = pairs; -local next = next; local pcall = pcall; - -local debug_traceback = debug.traceback; -local log = require "util.logger".init("serialization"); local envload = require"util.envload".envload; -local _ENV = nil; +local pos_inf, neg_inf = math.huge, -math.huge; +-- luacheck: ignore 143/math +local m_type = math.type or function (n) + return n % 1 == 0 and n <= 9007199254740992 and n >= -9007199254740992 and "integer" or "float"; +end; + +local char_to_hex = {}; +for i = 0,255 do + char_to_hex[s_char(i)] = s_format("%02x", i); +end + +local function to_hex(s) + return (s_gsub(s, ".", char_to_hex)); +end + +local function fatal_error(obj, why) + error("Can't serialize "..type(obj) .. (why and ": ".. why or "")); +end -local indent = function(i) - return string_rep("\t", i); +local function nonfatal_fallback(x, why) + return s_format("{__type=%q,__error=%q}", type(x), why or "fail"); end -local function basicSerialize (o) - if type(o) == "number" or type(o) == "boolean" then - -- no need to check for NaN, as that's not a valid table index - if o == 1/0 then return "(1/0)"; - elseif o == -1/0 then return "(-1/0)"; - else return tostring(o); end - else -- assume it is a string -- FIXME make sure it's a string. throw an error otherwise. - return (("%q"):format(tostring(o)):gsub("\\\n", "\\n")); + +local string_escapes = { + ['\a'] = [[\a]]; ['\b'] = [[\b]]; + ['\f'] = [[\f]]; ['\n'] = [[\n]]; + ['\r'] = [[\r]]; ['\t'] = [[\t]]; + ['\v'] = [[\v]]; ['\\'] = [[\\]]; + ['\"'] = [[\"]]; ['\''] = [[\']]; +} + +for i = 0, 255 do + local c = s_char(i); + if not string_escapes[c] then + string_escapes[c] = s_format("\\%03d", i); end end -local function _simplesave(o, ind, t, func) - if type(o) == "number" then - if o ~= o then func(t, "(0/0)"); - elseif o == 1/0 then func(t, "(1/0)"); - elseif o == -1/0 then func(t, "(-1/0)"); - else func(t, tostring(o)); end - elseif type(o) == "string" then - func(t, (("%q"):format(o):gsub("\\\n", "\\n"))); - elseif type(o) == "table" then - if next(o) ~= nil then - func(t, "{\n"); - for k,v in pairs(o) do - func(t, indent(ind)); - func(t, "["); - func(t, basicSerialize(k)); - func(t, "] = "); - if ind == 0 then - _simplesave(v, 0, t, func); + +local default_keywords = { + ["do"] = true; ["and"] = true; ["else"] = true; ["break"] = true; + ["if"] = true; ["end"] = true; ["goto"] = true; ["false"] = true; + ["in"] = true; ["for"] = true; ["then"] = true; ["local"] = true; + ["or"] = true; ["nil"] = true; ["true"] = true; ["until"] = true; + ["elseif"] = true; ["function"] = true; ["not"] = true; + ["repeat"] = true; ["return"] = true; ["while"] = true; +}; + +local function new(opt) + if type(opt) ~= "table" then + opt = { preset = opt }; + end + + local types = { + table = true; + string = true; + number = true; + boolean = true; + ["nil"] = true; + }; + + -- presets + if opt.preset == "debug" then + opt.preset = "oneline"; + opt.freeze = true; + opt.fatal = false; + opt.fallback = nonfatal_fallback; + opt.unquoted = true; + end + if opt.preset == "oneline" then + opt.indentwith = opt.indentwith or ""; + opt.itemstart = opt.itemstart or " "; + opt.itemlast = opt.itemlast or ""; + opt.tend = opt.tend or " }"; + elseif opt.preset == "compact" then + opt.indentwith = opt.indentwith or ""; + opt.itemstart = opt.itemstart or ""; + opt.itemlast = opt.itemlast or ""; + opt.equals = opt.equals or "="; + opt.unquoted = true; + end + + local fallback = opt.fallback or opt.fatal == false and nonfatal_fallback or fatal_error; + + local function ser(v) + return (types[type(v)] or fallback)(v); + end + + local keywords = opt.keywords or default_keywords; + + -- indented + local indentwith = opt.indentwith or "\t"; + local itemstart = opt.itemstart or "\n"; + local itemsep = opt.itemsep or ";"; + local itemlast = opt.itemlast or ";\n"; + local tstart = opt.tstart or "{"; + local tend = opt.tend or "}"; + local kstart = opt.kstart or "["; + local kend = opt.kend or "]"; + local equals = opt.equals or " = "; + local unquoted = opt.unquoted == true and "^[%a_][%w_]*$" or opt.unquoted; + local hex = opt.hex; + local freeze = opt.freeze; + local maxdepth = opt.maxdepth or 127; + local multirefs = opt.multiref; + + -- serialize one table, recursively + -- t - table being serialized + -- o - array where tokens are added, concatenate to get final result + -- - also used to detect cycles + -- l - position in o of where to insert next token + -- d - depth, used for indentation + local function serialize_table(t, o, l, d) + if o[t] then + o[l], l = fallback(t, "table has multiple references"), l + 1; + return l; + elseif d > maxdepth then + o[l], l = fallback(t, "max table depth reached"), l + 1; + return l; + end + + -- Keep track of table loops + local ot = t; -- reference pre-freeze + o[t] = true; + o[ot] = true; + + if freeze == true then + -- opportunity to do pre-serialization + local mt = getmetatable(t); + if type(mt) == "table" then + local tag = mt.__name; + local fr = mt.__freeze; + + if type(fr) == "function" then + t = fr(t); + if type(tag) == "string" then + o[l], l = tag, l + 1; + end + end + end + end + + o[l], l = tstart, l + 1; + local indent = s_rep(indentwith, d); + local numkey = 1; + local ktyp, vtyp; + for k,v in next,t do + o[l], l = itemstart, l + 1; + o[l], l = indent, l + 1; + ktyp, vtyp = type(k), type(v); + if k == numkey then + -- next index in array part + -- assuming that these are found in order + numkey = numkey + 1; + elseif unquoted and ktyp == "string" and + not keywords[k] and s_match(k, unquoted) then + -- unquoted keys + o[l], l = k, l + 1; + o[l], l = equals, l + 1; + else + -- quoted keys + o[l], l = kstart, l + 1; + if ktyp == "table" then + l = serialize_table(k, o, l, d+1); else - _simplesave(v, ind+1, t, func); + o[l], l = ser(k), l + 1; end - func(t, ";\n"); + -- = + o[l], o[l+1], l = kend, equals, l + 2; + end + + -- the value + if vtyp == "table" then + l = serialize_table(v, o, l, d+1); + else + o[l], l = ser(v), l + 1; + end + -- last item? + if next(t, k) ~= nil then + o[l], l = itemsep, l + 1; + else + o[l], l = itemlast, l + 1; end - func(t, indent(ind-1)); - func(t, "}"); - else - func(t, "{}"); end - elseif type(o) == "boolean" then - func(t, (o and "true" or "false")); + if next(t) ~= nil then + o[l], l = s_rep(indentwith, d-1), l + 1; + end + o[l], l = tend, l +1; + + if multirefs then + o[t] = nil; + o[ot] = nil; + end + + return l; + end + + function types.table(t) + local o = {}; + serialize_table(t, o, 1, 1); + return t_concat(o); + end + + local function serialize_string(s) + return '"' .. s_gsub(s, "[%z\1-\31\"\'\\\127-\255]", string_escapes) .. '"'; + end + + if type(hex) == "string" then + function types.string(s) + local esc = serialize_string(s); + if #esc > (#s*2+2+#hex) then + return hex .. '"' .. to_hex(s) .. '"'; + end + return esc; + end else - log("error", "cannot serialize a %s: %s", type(o), debug_traceback()) - func(t, "nil"); + types.string = serialize_string; end -end -local function append(t, o) - _simplesave(o, 1, t, t.write or t_insert); - return t; -end + function types.number(t) + if m_type(t) == "integer" then + return s_format("%d", t); + elseif t == pos_inf then + return "(1/0)"; + elseif t == neg_inf then + return "(-1/0)"; + elseif t ~= t then + return "(0/0)"; + end + return s_format("%.18g", t); + end + + -- Are these faster than tostring? + types["nil"] = function() + return "nil"; + end + + function types.boolean(t) + return t and "true" or "false"; + end -local function serialize(o) - return t_concat(append({}, o)); + return ser; end local function deserialize(str) if type(str) ~= "string" then return nil; end str = "return "..str; - local f, err = envload(str, "@data", {}); + local f, err = envload(str, "=serialized data", {}); if not f then return nil, err; end local success, ret = pcall(f); if not success then return nil, ret; end @@ -91,7 +273,9 @@ local function deserialize(str) end return { - append = append; - serialize = serialize; + new = new; + serialize = function (x, opt) + return new(opt)(x); + end; deserialize = deserialize; }; |