]> git.lizzy.rs Git - dragonfireclient.git/blob - builtin/common/serialize.lua
cf00107c2e17862550fadaa5b7dc623f146b7afc
[dragonfireclient.git] / builtin / common / serialize.lua
1 --- Lua module to serialize values as Lua code.
2 -- From: https://github.com/fab13n/metalua/blob/no-dll/src/lib/serialize.lua
3 -- License: MIT
4 -- @copyright 2006-2997 Fabien Fleutot <metalua@gmail.com>
5 -- @author Fabien Fleutot <metalua@gmail.com>
6 -- @author ShadowNinja <shadowninja@minetest.net>
7 --------------------------------------------------------------------------------
8
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.
24         local seen = {}
25
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.
36         --
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  = {}
43         local nested = {}
44         local function mark_nest_point(parent, k, v)
45                 local nk, nv = nested[k], nested[v]
46                 local np = nest_points[parent]
47                 if not np then
48                         np = {}
49                         nest_points[parent] = np
50                 end
51                 np[k] = v
52                 seen[parent] = 2
53                 if nk then seen[k] = 2 end
54                 if nv then seen[v] = 2 end
55         end
56
57         -- First phase, list the tables and functions which appear more than
58         -- once in x.
59         local function mark_multiple_occurences(x)
60                 local tp = type(x)
61                 if tp ~= "table" and tp ~= "function" then
62                         -- No identity (comparison is done by value, not by instance)
63                         return
64                 end
65                 if seen[x] == 1 then
66                         seen[x] = 2
67                 elseif seen[x] ~= 2 then
68                         seen[x] = 1
69                 end
70
71                 if tp == "table" then
72                         nested[x] = true
73                         for k, v in pairs(x) do
74                                 if nested[k] or nested[v] then
75                                         mark_nest_point(x, k, v)
76                                 else
77                                         mark_multiple_occurences(k)
78                                         mark_multiple_occurences(v)
79                                 end
80                         end
81                         nested[x] = nil
82                 end
83         end
84
85         local dumped     = {}  -- object->varname set
86         local local_defs = {}  -- Dumped local definitions as source code lines
87
88         -- Mutually recursive local functions:
89         local dump_val, dump_or_ref_val
90
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)
95                 if seen[x] ~= 2 then
96                         return dump_val(x)
97                 end
98                 local var = dumped[x]
99                 if var then  -- Already referenced
100                         return var
101                 end
102                 -- First occurence, create and register reference
103                 local val = dump_val(x)
104                 local i = local_index
105                 local_index = local_index + 1
106                 var = "_["..i.."]"
107                 local_defs[#local_defs + 1] = var.." = "..val
108                 dumped[x] = var
109                 return var
110         end
111
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.
115         function dump_val(x)
116                 local  tp = type(x)
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)
129                         else
130                                 return tostring(x)
131                         end
132                 elseif tp == "table" then
133                         local vals = {}
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)
139                                 end
140                                 idx_dumped[i] = true
141                         end
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).."] = "
146                                                 ..dump_or_ref_val(v)
147                                 end
148                         end
149                         return "{"..table.concat(vals, ", ").."}"
150                 else
151                         error("Can't serialize data of type "..tp)
152                 end
153         end
154
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).."] = "
160                                         ..dump_or_ref_val(v)
161                         end
162                 end
163         end
164
165         mark_multiple_occurences(x)
166         local top_level = dump_or_ref_val(x)
167         dump_nest_points()
168
169         if next(local_defs) then
170                 return "local _ = {}\n"
171                         ..table.concat(local_defs, "\n")
172                         .."\nreturn "..top_level
173         else
174                 return "return "..top_level
175         end
176 end
177
178 -- Deserialization
179
180 local env = {
181         loadstring = loadstring,
182 }
183
184 local safe_env = {
185         loadstring = function() end,
186 }
187
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."
192         end
193         if str:byte(1) == 0x1B then
194                 return nil, "Bytecode prohibited"
195         end
196         local f, err = loadstring(str)
197         if not f then return nil, err end
198         setfenv(f, safe and safe_env or env)
199
200         local good, data = pcall(f)
201         if good then
202                 return data
203         else
204                 return nil, data
205         end
206 end