]> git.lizzy.rs Git - minetest.git/blob - builtin/common/serialize.lua
Deserialization: Restore backwards compat (#12519)
[minetest.git] / builtin / common / serialize.lua
1 --- Lua module to serialize values as Lua code.
2 -- From: https://github.com/appgurueu/modlib/blob/master/luon.lua
3 -- License: MIT
4
5 local next, rawget, pairs, pcall, error, type, setfenv, loadstring
6         = next, rawget, pairs, pcall, error, type, setfenv, loadstring
7
8 local table_concat, string_dump, string_format, string_match, math_huge
9         = table.concat, string.dump, string.format, string.match, math.huge
10
11 -- Recursively counts occurences of objects (non-primitives including strings) in a table.
12 local function count_objects(value)
13         local counts = {}
14         if value == nil then
15                 -- Early return for nil; tables can't contain nil
16                 return counts
17         end
18         local function count_values(val)
19                 local type_ = type(val)
20                 if type_ == "boolean" or type_ == "number" then
21                         return
22                 end
23                 local count = counts[val]
24                 counts[val] = (count or 0) + 1
25                 if type_ == "table" then
26                         if not count then
27                                 for k, v in pairs(val) do
28                                         count_values(k)
29                                         count_values(v)
30                                 end
31                         end
32                 elseif type_ ~= "string" and type_ ~= "function" then
33                         error("unsupported type: " .. type_)
34                 end
35         end
36         count_values(value)
37         return counts
38 end
39
40 -- Build a "set" of Lua keywords. These can't be used as short key names.
41 -- See https://www.lua.org/manual/5.1/manual.html#2.1
42 local keywords = {}
43 for _, keyword in pairs({
44         "and", "break", "do", "else", "elseif",
45         "end", "false", "for", "function", "if",
46         "in", "local", "nil", "not", "or",
47         "repeat", "return", "then", "true", "until", "while",
48         "goto" -- LuaJIT, Lua 5.2+
49 }) do
50         keywords[keyword] = true
51 end
52
53 local function quote(string)
54         return string_format("%q", string)
55 end
56
57 local function dump_func(func)
58         return string_format("loadstring(%q)", string_dump(func))
59 end
60
61 -- Serializes Lua nil, booleans, numbers, strings, tables and even functions
62 -- Tables are referenced by reference, strings are referenced by value. Supports circular tables.
63 local function serialize(value, write)
64         local reference, refnum = "r1", 1
65         -- [object] = reference string
66         local references = {}
67         -- Circular tables that must be filled using `table[key] = value` statements
68         local to_fill = {}
69         for object, count in pairs(count_objects(value)) do
70                 local type_ = type(object)
71                 -- Object must appear more than once. If it is a string, the reference has to be shorter than the string.
72                 if count >= 2 and (type_ ~= "string" or #reference + 2 < #object) then
73                         write(reference)
74                         write("=")
75                         if type_ == "table" then
76                                 write("{}")
77                         elseif type_ == "function" then
78                                 write(dump_func(object))
79                         elseif type_ == "string" then
80                                 write(quote(object))
81                         end
82                         write(";")
83                         references[object] = reference
84                         if type_ == "table" then
85                                 to_fill[object] = reference
86                         end
87                         refnum = refnum + 1
88                         reference = ("r%X"):format(refnum)
89                 end
90         end
91         -- Used to decide whether we should do "key=..."
92         local function use_short_key(key)
93                 return not references[key] and type(key) == "string" and (not keywords[key]) and string_match(key, "^[%a_][%a%d_]*$")
94         end
95         local function dump(value)
96                 -- Primitive types
97                 if value == nil then
98                         return write("nil")
99                 end
100                 if value == true then
101                         return write("true")
102                 end
103                 if value == false then
104                         return write("false")
105                 end
106                 local type_ = type(value)
107                 if type_ == "number" then
108                         return write(string_format("%.17g", value))
109                 end
110                 -- Reference types: table, function and string
111                 local ref = references[value]
112                 if ref then
113                         return write(ref)
114                 end
115                 if type_ == "string" then
116                         return write(quote(value))
117                 end
118                 if type_ == "function" then
119                         return write(dump_func(value))
120                 end
121                 if type_ == "table" then
122                         write("{")
123                         -- First write list keys:
124                         -- Don't use the table length #value here as it may horribly fail
125                         -- for tables which use large integers as keys in the hash part;
126                         -- stop at the first "hole" (nil value) instead
127                         local len = 0
128                         local first = true -- whether this is the first entry, which may not have a leading comma
129                         while true do
130                                 local v = rawget(value, len + 1) -- use rawget to avoid metatables like the vector metatable
131                                 if v == nil then break end
132                                 if first then first = false else write(",") end
133                                 dump(v)
134                                 len = len + 1
135                         end
136                         -- Now write map keys ([key] = value)
137                         for k, v in next, value do
138                                 -- We have written all non-float keys in [1, len] already
139                                 if type(k) ~= "number" or k % 1 ~= 0 or k < 1 or k > len then
140                                         if first then first = false else write(",") end
141                                         if use_short_key(k) then
142                                                 write(k)
143                                         else
144                                                 write("[")
145                                                 dump(k)
146                                                 write("]")
147                                         end
148                                         write("=")
149                                         dump(v)
150                                 end
151                         end
152                         write("}")
153                         return
154                 end
155         end
156         -- Write the statements to fill circular tables
157         for table, ref in pairs(to_fill) do
158                 for k, v in pairs(table) do
159                         write(ref)
160                         if use_short_key(k) then
161                                 write(".")
162                                 write(k)
163                         else
164                                 write("[")
165                                 dump(k)
166                                 write("]")
167                         end
168                         write("=")
169                         dump(v)
170                         write(";")
171                 end
172         end
173         write("return ")
174         dump(value)
175 end
176
177 function core.serialize(value)
178         local rope = {}
179         serialize(value, function(text)
180                  -- Faster than table.insert(rope, text) on PUC Lua 5.1
181                 rope[#rope + 1] = text
182         end)
183         return table_concat(rope)
184 end
185
186 local function dummy_func() end
187
188 local nan = (0/0)^1 -- +nan
189
190 function core.deserialize(str, safe)
191         -- Backwards compatibility
192         if str == nil then
193                 core.log("deprecated", "minetest.deserialize called with nil (expected string).")
194                 return nil, "Invalid type: Expected a string, got nil"
195         end
196         local t = type(str)
197         if t ~= "string" then
198                 error(("minetest.deserialize called with %s (expected string)."):format(t))
199         end
200
201         local func, err = loadstring(str)
202         if not func then return nil, err end
203
204         -- math.huge is serialized to inf, NaNs are serialized to nan by Lua
205         local env = {inf = math_huge, nan = nan}
206         if safe then
207                 env.loadstring = dummy_func
208         else
209                 env.loadstring = function(str, ...)
210                         local func, err = loadstring(str, ...)
211                         if func then
212                                 setfenv(func, env)
213                                 return func
214                         end
215                         return nil, err
216                 end
217         end
218         setfenv(func, env)
219         local success, value_or_err = pcall(func)
220         if success then
221                 return value_or_err
222         end
223         return nil, value_or_err
224 end