1 --- Lua module to serialize values as Lua code.
2 -- From: https://github.com/fab13n/metalua/blob/no-dll/src/lib/serialize.lua
4 -- @copyright 2006-2997 Fabien Fleutot <metalua@gmail.com>
5 -- @author Fabien Fleutot <metalua@gmail.com>
6 -- @author ShadowNinja <shadowninja@minetest.net>
7 --------------------------------------------------------------------------------
9 --- Serialize an object into a source code string. This string, when passed as
10 -- an argument to deserialize(), returns an object structurally identical to
11 -- the original one. The following are currently supported:
12 -- * Booleans, numbers, strings, and nil.
13 -- * Functions; uses interpreter-dependent (and sometimes platform-dependent) bytecode!
14 -- * Tables; they can cantain multiple references and can be recursive, but metatables aren't saved.
15 -- This works in two phases:
16 -- 1. Recursively find and record multiple references and recursion.
17 -- 2. Recursively dump the value into a string.
18 -- @param x Value to serialize (nil is allowed).
19 -- @return load()able string containing the value.
20 function core.serialize(x)
21 local local_index = 1 -- Top index of the "_" local table in the dump
22 -- table->nil/1/2 set of tables seen.
23 -- nil = not seen, 1 = seen once, 2 = seen multiple times.
26 -- nest_points are places where a table appears within itself, directly
27 -- or not. For instance, all of these chunks create nest points in
28 -- table x: "x = {}; x[x] = 1", "x = {}; x[1] = x",
29 -- "x = {}; x[1] = {y = {x}}".
30 -- To handle those, two tables are used by mark_nest_point:
31 -- * nested - Transient set of tables being currently traversed.
32 -- Used for detecting nested tables.
33 -- * nest_points - parent->{key=value, ...} table cantaining the nested
34 -- keys and values in the parent. They're all dumped after all the
35 -- other table operations have been performed.
37 -- mark_nest_point(p, k, v) fills nest_points with information required
38 -- to remember that key/value (k, v) creates a nest point in table
39 -- parent. It also marks "parent" and the nested item(s) as occuring
40 -- multiple times, since several references to it will be required in
41 -- order to patch the nest points.
42 local nest_points = {}
44 local function mark_nest_point(parent, k, v)
45 local nk, nv = nested[k], nested[v]
46 local np = nest_points[parent]
49 nest_points[parent] = np
53 if nk then seen[k] = 2 end
54 if nv then seen[v] = 2 end
57 -- First phase, list the tables and functions which appear more than
59 local function mark_multiple_occurences(x)
61 if tp ~= "table" and tp ~= "function" then
62 -- No identity (comparison is done by value, not by instance)
67 elseif seen[x] ~= 2 then
73 for k, v in pairs(x) do
74 if nested[k] or nested[v] then
75 mark_nest_point(x, k, v)
77 mark_multiple_occurences(k)
78 mark_multiple_occurences(v)
85 local dumped = {} -- object->varname set
86 local local_defs = {} -- Dumped local definitions as source code lines
88 -- Mutually recursive local functions:
89 local dump_val, dump_or_ref_val
91 -- If x occurs multiple times, dump the local variable rather than
92 -- the value. If it's the first time it's dumped, also dump the
93 -- content in local_defs.
94 function dump_or_ref_val(x)
99 if var then -- Already referenced
102 -- First occurence, create and register reference
103 local val = dump_val(x)
104 local i = local_index
105 local_index = local_index + 1
107 local_defs[#local_defs + 1] = var.." = "..val
112 -- Second phase. Dump the object; subparts occuring multiple times
113 -- are dumped in local variables which can be referenced multiple
114 -- times. Care is taken to dump local vars in a sensible order.
117 if x == nil then return "nil"
118 elseif tp == "string" then return string.format("%q", x)
119 elseif tp == "boolean" then return x and "true" or "false"
120 elseif tp == "function" then
121 return string.format("loadstring(%q)", string.dump(x))
122 elseif tp == "number" then
123 -- Serialize integers with string.format to prevent
124 -- scientific notation, which doesn't preserve
125 -- precision and breaks things like node position
126 -- hashes. Serialize floats normally.
127 if math.floor(x) == x then
128 return string.format("%d", x)
132 elseif tp == "table" then
134 local idx_dumped = {}
135 local np = nest_points[x]
136 for i, v in ipairs(x) do
137 if not np or not np[i] then
138 vals[#vals + 1] = dump_or_ref_val(v)
142 for k, v in pairs(x) do
143 if (not np or not np[k]) and
144 not idx_dumped[k] then
145 vals[#vals + 1] = "["..dump_or_ref_val(k).."] = "
149 return "{"..table.concat(vals, ", ").."}"
151 error("Can't serialize data of type "..tp)
155 local function dump_nest_points()
156 for parent, vals in pairs(nest_points) do
157 for k, v in pairs(vals) do
158 local_defs[#local_defs + 1] = dump_or_ref_val(parent)
159 .."["..dump_or_ref_val(k).."] = "
165 mark_multiple_occurences(x)
166 local top_level = dump_or_ref_val(x)
169 if next(local_defs) then
170 return "local _ = {}\n"
171 ..table.concat(local_defs, "\n")
172 .."\nreturn "..top_level
174 return "return "..top_level
181 loadstring = loadstring,
185 loadstring = function() end,
188 function core.deserialize(str, safe)
189 if type(str) ~= "string" then
190 return nil, "Cannot deserialize type '"..type(str)
191 .."'. Argument must be a string."
193 if str:byte(1) == 0x1B then
194 return nil, "Bytecode prohibited"
196 local f, err = loadstring(str)
197 if not f then return nil, err end
198 setfenv(f, safe and safe_env or env)
200 local good, data = pcall(f)
210 local test_in = {cat={sound="nyan", speed=400}, dog={sound="woof"}}
211 local test_out = core.deserialize(core.serialize(test_in))
213 assert(test_in.cat.sound == test_out.cat.sound)
214 assert(test_in.cat.speed == test_out.cat.speed)
215 assert(test_in.dog.sound == test_out.dog.sound)
217 test_in = {escape_chars="\n\r\t\v\\\"\'", non_european="θשׁ٩∂"}
218 test_out = core.deserialize(core.serialize(test_in))
219 assert(test_in.escape_chars == test_out.escape_chars)
220 assert(test_in.non_european == test_out.non_european)