aboutsummaryrefslogtreecommitdiffstats
path: root/util/serialization.lua
diff options
context:
space:
mode:
Diffstat (limited to 'util/serialization.lua')
-rw-r--r--util/serialization.lua300
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;
};