|
| 1 | +local n, v = "serpent", "0.303" -- (C) 2012-18 Paul Kulchenko; MIT License |
| 2 | +local c, d = "Paul Kulchenko", "Lua serializer and pretty printer" |
| 3 | +local snum = {[tostring(1/0)]='1/0 --[[math.huge]]',[tostring(-1/0)]='-1/0 --[[-math.huge]]',[tostring(0/0)]='0/0'} |
| 4 | +local badtype = {thread = true, userdata = true, cdata = true} |
| 5 | +local getmetatable = debug and debug.getmetatable or getmetatable |
| 6 | +local pairs = function(t) return next, t end -- avoid using __pairs in Lua 5.2+ |
| 7 | +local keyword, globals, G = {}, {}, (_G or _ENV) |
| 8 | +for _,k in ipairs({'and', 'break', 'do', 'else', 'elseif', 'end', 'false', |
| 9 | + 'for', 'function', 'goto', 'if', 'in', 'local', 'nil', 'not', 'or', 'repeat', |
| 10 | + 'return', 'then', 'true', 'until', 'while'}) do keyword[k] = true end |
| 11 | +for k,v in pairs(G) do globals[v] = k end -- build func to name mapping |
| 12 | +for _,g in ipairs({'coroutine', 'debug', 'io', 'math', 'string', 'table', 'os'}) do |
| 13 | + for k,v in pairs(type(G[g]) == 'table' and G[g] or {}) do globals[v] = g..'.'..k end end |
| 14 | + |
| 15 | +local function s(t, opts) |
| 16 | + local name, indent, fatal, maxnum = opts.name, opts.indent, opts.fatal, opts.maxnum |
| 17 | + local sparse, custom, huge = opts.sparse, opts.custom, not opts.nohuge |
| 18 | + local space, maxl = (opts.compact and '' or ' '), (opts.maxlevel or math.huge) |
| 19 | + local maxlen, metatostring = tonumber(opts.maxlength), opts.metatostring |
| 20 | + local iname, comm = '_'..(name or ''), opts.comment and (tonumber(opts.comment) or math.huge) |
| 21 | + local numformat = opts.numformat or "%.17g" |
| 22 | + local seen, sref, syms, symn = {}, {'local '..iname..'={}'}, {}, 0 |
| 23 | + local function gensym(val) return '_'..(tostring(tostring(val)):gsub("[^%w]",""):gsub("(%d%w+)", |
| 24 | + -- tostring(val) is needed because __tostring may return a non-string value |
| 25 | + function(s) if not syms[s] then symn = symn+1; syms[s] = symn end return tostring(syms[s]) end)) end |
| 26 | + local function safestr(s) return type(s) == "number" and (huge and snum[tostring(s)] or numformat:format(s)) |
| 27 | + or type(s) ~= "string" and tostring(s) -- escape NEWLINE/010 and EOF/026 |
| 28 | + or ("%q"):format(s):gsub("\010","n"):gsub("\026","\\026") end |
| 29 | + -- handle radix changes in some locales |
| 30 | + if opts.fixradix and (".1f"):format(1.2) ~= "1.2" then |
| 31 | + local origsafestr = safestr |
| 32 | + safestr = function(s) return type(s) == "number" |
| 33 | + and (nohuge and snum[tostring(s)] or numformat:format(s):gsub(",",".")) or origsafestr(s) |
| 34 | + end |
| 35 | + end |
| 36 | + local function comment(s,l) return comm and (l or 0) < comm and ' --[['..select(2, pcall(tostring, s))..']]' or '' end |
| 37 | + local function globerr(s,l) return globals[s] and globals[s]..comment(s,l) or not fatal |
| 38 | + and safestr(select(2, pcall(tostring, s))) or error("Can't serialize "..tostring(s)) end |
| 39 | + local function safename(path, name) -- generates foo.bar, foo[3], or foo['b a r'] |
| 40 | + local n = name == nil and '' or name |
| 41 | + local plain = type(n) == "string" and n:match("^[%l%u_][%w_]*$") and not keyword[n] |
| 42 | + local safe = plain and n or '['..safestr(n)..']' |
| 43 | + return (path or '')..(plain and path and '.' or '')..safe, safe end |
| 44 | + local alphanumsort = type(opts.sortkeys) == 'function' and opts.sortkeys or function(k, o, n) -- k=keys, o=originaltable, n=padding |
| 45 | + local maxn, to = tonumber(n) or 12, {number = 'a', string = 'b'} |
| 46 | + local function padnum(d) return ("%0"..tostring(maxn).."d"):format(tonumber(d)) end |
| 47 | + table.sort(k, function(a,b) |
| 48 | + -- sort numeric keys first: k[key] is not nil for numerical keys |
| 49 | + return (k[a] ~= nil and 0 or to[type(a)] or 'z')..(tostring(a):gsub("%d+",padnum)) |
| 50 | + < (k[b] ~= nil and 0 or to[type(b)] or 'z')..(tostring(b):gsub("%d+",padnum)) end) end |
| 51 | + local function val2str(t, name, indent, insref, path, plainindex, level) |
| 52 | + local ttype, level, mt = type(t), (level or 0), getmetatable(t) |
| 53 | + local spath, sname = safename(path, name) |
| 54 | + local tag = plainindex and |
| 55 | + ((type(name) == "number") and '' or name..space..'='..space) or |
| 56 | + (name ~= nil and sname..space..'='..space or '') |
| 57 | + if seen[t] then -- already seen this element |
| 58 | + sref[#sref+1] = spath..space..'='..space..seen[t] |
| 59 | + return tag..'nil'..comment('ref', level) |
| 60 | + end |
| 61 | + -- protect from those cases where __tostring may fail |
| 62 | + if type(mt) == 'table' and metatostring ~= false then |
| 63 | + local to, tr = pcall(function() return mt.__tostring(t) end) |
| 64 | + local so, sr = pcall(function() return mt.__serialize(t) end) |
| 65 | + if (to or so) then -- knows how to serialize itself |
| 66 | + seen[t] = insref or spath |
| 67 | + t = so and sr or tr |
| 68 | + ttype = type(t) |
| 69 | + end -- new value falls through to be serialized |
| 70 | + end |
| 71 | + if ttype == "table" then |
| 72 | + if level >= maxl then return tag..'{}'..comment('maxlvl', level) end |
| 73 | + seen[t] = insref or spath |
| 74 | + if next(t) == nil then return tag..'{}'..comment(t, level) end -- table empty |
| 75 | + if maxlen and maxlen < 0 then return tag..'{}'..comment('maxlen', level) end |
| 76 | + local maxn, o, out = math.min(#t, maxnum or #t), {}, {} |
| 77 | + for key = 1, maxn do o[key] = key end |
| 78 | + if not maxnum or #o < maxnum then |
| 79 | + local n = #o -- n = n + 1; o[n] is much faster than o[#o+1] on large tables |
| 80 | + for key in pairs(t) do |
| 81 | + if o[key] ~= key then n = n + 1; o[n] = key end |
| 82 | + end |
| 83 | + end |
| 84 | + if maxnum and #o > maxnum then o[maxnum+1] = nil end |
| 85 | + if opts.sortkeys and #o > maxn then alphanumsort(o, t, opts.sortkeys) end |
| 86 | + local sparse = sparse and #o > maxn -- disable sparsness if only numeric keys (shorter output) |
| 87 | + for n, key in ipairs(o) do |
| 88 | + local value, ktype, plainindex = t[key], type(key), n <= maxn and not sparse |
| 89 | + if opts.valignore and opts.valignore[value] -- skip ignored values; do nothing |
| 90 | + or opts.keyallow and not opts.keyallow[key] |
| 91 | + or opts.keyignore and opts.keyignore[key] |
| 92 | + or opts.valtypeignore and opts.valtypeignore[type(value)] -- skipping ignored value types |
| 93 | + or sparse and value == nil then -- skipping nils; do nothing |
| 94 | + elseif ktype == 'table' or ktype == 'function' or badtype[ktype] then |
| 95 | + if not seen[key] and not globals[key] then |
| 96 | + sref[#sref+1] = 'placeholder' |
| 97 | + local sname = safename(iname, gensym(key)) -- iname is table for local variables |
| 98 | + sref[#sref] = val2str(key,sname,indent,sname,iname,true) |
| 99 | + end |
| 100 | + sref[#sref+1] = 'placeholder' |
| 101 | + local path = seen[t]..'['..tostring(seen[key] or globals[key] or gensym(key))..']' |
| 102 | + sref[#sref] = path..space..'='..space..tostring(seen[value] or val2str(value,nil,indent,path)) |
| 103 | + else |
| 104 | + out[#out+1] = val2str(value,key,indent,nil,seen[t],plainindex,level+1) |
| 105 | + if maxlen then |
| 106 | + maxlen = maxlen - #out[#out] |
| 107 | + if maxlen < 0 then break end |
| 108 | + end |
| 109 | + end |
| 110 | + end |
| 111 | + local prefix = string.rep(indent or '', level) |
| 112 | + local head = indent and '{\n'..prefix..indent or '{' |
| 113 | + local body = table.concat(out, ','..(indent and '\n'..prefix..indent or space)) |
| 114 | + local tail = indent and "\n"..prefix..'}' or '}' |
| 115 | + return (custom and custom(tag,head,body,tail,level) or tag..head..body..tail)..comment(t, level) |
| 116 | + elseif badtype[ttype] then |
| 117 | + seen[t] = insref or spath |
| 118 | + return tag..globerr(t, level) |
| 119 | + elseif ttype == 'function' then |
| 120 | + seen[t] = insref or spath |
| 121 | + if opts.nocode then return tag.."function() --[[..skipped..]] end"..comment(t, level) end |
| 122 | + local ok, res = pcall(string.dump, t) |
| 123 | + local func = ok and "((loadstring or load)("..safestr(res)..",'@serialized'))"..comment(t, level) |
| 124 | + return tag..(func or globerr(t, level)) |
| 125 | + else return tag..safestr(t) end -- handle all other types |
| 126 | + end |
| 127 | + local sepr = indent and "\n" or ";"..space |
| 128 | + local body = val2str(t, name, indent) -- this call also populates sref |
| 129 | + local tail = #sref>1 and table.concat(sref, sepr)..sepr or '' |
| 130 | + local warn = opts.comment and #sref>1 and space.."--[[incomplete output with shared/self-references skipped]]" or '' |
| 131 | + return not name and body..warn or "do local "..body..sepr..tail.."return "..name..sepr.."end" |
| 132 | +end |
| 133 | + |
| 134 | +local function deserialize(data, opts) |
| 135 | + local env = (opts and opts.safe == false) and G |
| 136 | + or setmetatable({}, { |
| 137 | + __index = function(t,k) return t end, |
| 138 | + __call = function(t,...) error("cannot call functions") end |
| 139 | + }) |
| 140 | + local f, res = (loadstring or load)('return '..data, nil, nil, env) |
| 141 | + if not f then f, res = (loadstring or load)(data, nil, nil, env) end |
| 142 | + if not f then return f, res end |
| 143 | + if setfenv then setfenv(f, env) end |
| 144 | + return pcall(f) |
| 145 | +end |
| 146 | + |
| 147 | +local function merge(a, b) if b then for k,v in pairs(b) do a[k] = v end end; return a; end |
| 148 | +return { _NAME = n, _COPYRIGHT = c, _DESCRIPTION = d, _VERSION = v, serialize = s, |
| 149 | + load = deserialize, |
| 150 | + dump = function(a, opts) return s(a, merge({name = '_', compact = true, sparse = true}, opts)) end, |
| 151 | + line = function(a, opts) return s(a, merge({sortkeys = true, comment = true}, opts)) end, |
| 152 | + block = function(a, opts) return s(a, merge({indent = ' ', sortkeys = true, comment = true}, opts)) end } |
0 commit comments