]> git.lizzy.rs Git - minetest.git/blob - builtin/common/serialize.lua
Add keybind to swap items between hands
[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 occurrences 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 = "1", 1
65         -- [object] = reference
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 + 5 < #object) then
73                         if refnum == 1 then
74                                 write"local _={};" -- initialize reference table
75                         end
76                         write"_["
77                         write(reference)
78                         write("]=")
79                         if type_ == "table" then
80                                 write("{}")
81                         elseif type_ == "function" then
82                                 write(dump_func(object))
83                         elseif type_ == "string" then
84                                 write(quote(object))
85                         end
86                         write(";")
87                         references[object] = reference
88                         if type_ == "table" then
89                                 to_fill[object] = reference
90                         end
91                         refnum = refnum + 1
92                         reference = ("%d"):format(refnum)
93                 end
94         end
95         -- Used to decide whether we should do "key=..."
96         local function use_short_key(key)
97                 return not references[key] and type(key) == "string" and (not keywords[key]) and string_match(key, "^[%a_][%a%d_]*$")
98         end
99         local function dump(value)
100                 -- Primitive types
101                 if value == nil then
102                         return write("nil")
103                 end
104                 if value == true then
105                         return write("true")
106                 end
107                 if value == false then
108                         return write("false")
109                 end
110                 local type_ = type(value)
111                 if type_ == "number" then
112                         if value ~= value then -- nan
113                                 return write"0/0"
114                         elseif value == math_huge then
115                                 return write"1/0"
116                         elseif value == -math_huge then
117                                 return write"-1/0"
118                         else
119                                 return write(string_format("%.17g", value))
120                         end
121                 end
122                 -- Reference types: table, function and string
123                 local ref = references[value]
124                 if ref then
125                         write"_["
126                         write(ref)
127                         return write"]"
128                 end
129                 if type_ == "string" then
130                         return write(quote(value))
131                 end
132                 if type_ == "function" then
133                         return write(dump_func(value))
134                 end
135                 if type_ == "table" then
136                         write("{")
137                         -- First write list keys:
138                         -- Don't use the table length #value here as it may horribly fail
139                         -- for tables which use large integers as keys in the hash part;
140                         -- stop at the first "hole" (nil value) instead
141                         local len = 0
142                         local first = true -- whether this is the first entry, which may not have a leading comma
143                         while true do
144                                 local v = rawget(value, len + 1) -- use rawget to avoid metatables like the vector metatable
145                                 if v == nil then break end
146                                 if first then first = false else write(",") end
147                                 dump(v)
148                                 len = len + 1
149                         end
150                         -- Now write map keys ([key] = value)
151                         for k, v in next, value do
152                                 -- We have written all non-float keys in [1, len] already
153                                 if type(k) ~= "number" or k % 1 ~= 0 or k < 1 or k > len then
154                                         if first then first = false else write(",") end
155                                         if use_short_key(k) then
156                                                 write(k)
157                                         else
158                                                 write("[")
159                                                 dump(k)
160                                                 write("]")
161                                         end
162                                         write("=")
163                                         dump(v)
164                                 end
165                         end
166                         write("}")
167                         return
168                 end
169         end
170         -- Write the statements to fill circular tables
171         for table, ref in pairs(to_fill) do
172                 for k, v in pairs(table) do
173                         write("_[")
174                         write(ref)
175                         write("]")
176                         if use_short_key(k) then
177                                 write(".")
178                                 write(k)
179                         else
180                                 write("[")
181                                 dump(k)
182                                 write("]")
183                         end
184                         write("=")
185                         dump(v)
186                         write(";")
187                 end
188         end
189         write("return ")
190         dump(value)
191 end
192
193 function core.serialize(value)
194         local rope = {}
195         serialize(value, function(text)
196                  -- Faster than table.insert(rope, text) on PUC Lua 5.1
197                 rope[#rope + 1] = text
198         end)
199         return table_concat(rope)
200 end
201
202 local function dummy_func() end
203
204 function core.deserialize(str, safe)
205         -- Backwards compatibility
206         if str == nil then
207                 core.log("deprecated", "minetest.deserialize called with nil (expected string).")
208                 return nil, "Invalid type: Expected a string, got nil"
209         end
210         local t = type(str)
211         if t ~= "string" then
212                 error(("minetest.deserialize called with %s (expected string)."):format(t))
213         end
214
215         local func, err = loadstring(str)
216         if not func then return nil, err end
217
218         -- math.huge was serialized to inf and NaNs to nan by Lua in Minetest 5.6, so we have to support this here
219         local env = {inf = math_huge, nan = 0/0}
220         if safe then
221                 env.loadstring = dummy_func
222         else
223                 env.loadstring = function(str, ...)
224                         local func, err = loadstring(str, ...)
225                         if func then
226                                 setfenv(func, env)
227                                 return func
228                         end
229                         return nil, err
230                 end
231         end
232         setfenv(func, env)
233         local success, value_or_err = pcall(func)
234         if success then
235                 return value_or_err
236         end
237         return nil, value_or_err
238 end