Added handling of metatables with __tostring method.

This commit is contained in:
Paul Kulchenko 2013-03-05 10:28:26 -08:00
parent 4883768d13
commit 7f58e5899c
2 changed files with 48 additions and 18 deletions

View File

@ -1,5 +1,5 @@
local n, v = "serpent", 0.222 -- (C) 2012-13 Paul Kulchenko; MIT License
local c, d = "Paul Kulchenko", "Serializer and pretty printer of Lua data types"
local n, v = "serpent", 0.223 -- (C) 2012-13 Paul Kulchenko; MIT License
local c, d = "Paul Kulchenko", "Lua serializer and pretty printer"
local snum = {[tostring(1/0)]='1/0 --[[math.huge]]',[tostring(-1/0)]='-1/0 --[[-math.huge]]',[tostring(0/0)]='0/0'}
local badtype = {thread = true, userdata = true}
local keyword, globals, G = {}, {}, (_G or _ENV)
@ -16,7 +16,8 @@ local function s(t, opts)
local space, maxl = (opts.compact and '' or ' '), (opts.maxlevel or math.huge)
local iname, comm = '_'..(name or ''), opts.comment and (tonumber(opts.comment) or math.huge)
local seen, sref, syms, symn = {}, {'local '..iname..'={}'}, {}, 0
local function gensym(val) return '_'..(tostring(val):gsub("[^%w]",""):gsub("(%d%w+)",
local function gensym(val) return '_'..(tostring(tostring(val)):gsub("[^%w]",""):gsub("(%d%w+)",
-- tostring(val) is needed because __tostring may return a non-string value
function(s) if not syms[s] then symn = symn+1; syms[s] = symn end return syms[s] end)) end
local function safestr(s) return type(s) == "number" and (huge and snum[tostring(s)] or s)
or type(s) ~= "string" and tostring(s) -- escape NEWLINE/010 and EOF/026
@ -37,28 +38,20 @@ local function s(t, opts)
return (k[a] and 0 or to[type(a)] or 'z')..(tostring(a):gsub("%d+",padnum))
< (k[b] and 0 or to[type(b)] or 'z')..(tostring(b):gsub("%d+",padnum)) end) end
local function val2str(t, name, indent, insref, path, plainindex, level)
local ttype, level = type(t), (level or 0)
local ttype, level, mt = type(t), (level or 0), getmetatable(t)
local spath, sname = safename(path, name)
local tag = plainindex and
((type(name) == "number") and '' or name..space..'='..space) or
(name ~= nil and sname..space..'='..space or '')
if seen[t] then
if seen[t] then -- already seen this element
table.insert(sref, spath..space..'='..space..seen[t])
return tag..'nil'..comment('ref', level)
elseif badtype[ttype] then
return tag..'nil'..comment('ref', level) end
if mt and mt.__tostring then -- metatable with tostring, so substitute it
seen[t] = insref or spath
return tag..globerr(t, level)
elseif ttype == 'function' then
seen[t] = insref or spath
local ok, res = pcall(string.dump, t)
local func = ok and ((opts.nocode and "function() --[[..skipped..]] end" or
"loadstring("..safestr(res)..",'@serialized')")..comment(t, level))
return tag..(func or globerr(t, level))
elseif ttype == "table" then
t = tostring(t); ttype = type(t) end
if ttype == "table" then
if level >= maxl then return tag..'{}'..comment('max', level) end
seen[t] = insref or spath -- set path to use as reference
if getmetatable(t) and getmetatable(t).__tostring
then return tag..val2str(tostring(t),nil,indent,false,nil,nil,level+1)..comment("meta", level) end
seen[t] = insref or spath
if next(t) == nil then return tag..'{}'..comment(t, level) end -- table empty
local maxn, o, out = #t, {}, {}
for key = 1, maxn do table.insert(o, key) end
@ -87,6 +80,15 @@ local function s(t, opts)
local body = table.concat(out, ','..(indent and '\n'..prefix..indent or space))
local tail = indent and "\n"..prefix..'}' or '}'
return (custom and custom(tag,head,body,tail) or tag..head..body..tail)..comment(t, level)
elseif badtype[ttype] then
seen[t] = insref or spath
return tag..globerr(t, level)
elseif ttype == 'function' then
seen[t] = insref or spath
local ok, res = pcall(string.dump, t)
local func = ok and ((opts.nocode and "function() --[[..skipped..]] end" or
"loadstring("..safestr(res)..",'@serialized')")..comment(t, level))
return tag..(func or globerr(t, level))
else return tag..safestr(t) end -- handle all other types
end
local sepr = indent and "\n" or ";"..space

View File

@ -167,4 +167,32 @@ do
"userdata with type starting with digits: failed")
end
-- test userdata with __tostring method that returns a table
do
local userdata = newproxy(true)
getmetatable(userdata).__tostring = function() return {3,4,5} end
local a = {hi = "there", [{}] = 123, [userdata] = 23, ud = userdata}
local f = assert(loadstring(serpent.dump(a, {sparse = false, nocode = true})),
"userdata with __tostring that returns a table 1: failed")
local _a = f()
assert(_a.ud, "userdata with __tostring that returns a table 2: failed")
assert(_a[_a.ud] == 23, "userdata with __tostring that returns a table 3: failed")
end
-- test userdata with __tostring method that includes another userdata
do
local userdata1 = newproxy(true)
local userdata2 = newproxy(true)
getmetatable(userdata1).__tostring = function() return {1,2,ud = userdata2} end
getmetatable(userdata2).__tostring = function() return {3,4,ud = userdata2} end
local a = {hi = "there", [{}] = 123, [userdata1] = 23, ud = userdata1}
local f = assert(loadstring(serpent.dump(a, {sparse = false, nocode = true})),
"userdata with __tostring that returns userdata 1: failed")
local _a = f()
assert(_a.ud, "userdata with __tostring that returns userdata 2: failed")
assert(_a[_a.ud] == 23, "userdata with __tostring that returns userdata 3: failed")
end
print("All tests passed.")