Add minetest.serialize() and minetest.deserialize()
authorPerttu Ahola <celeron55@gmail.com>
Wed, 6 Jun 2012 21:03:42 +0000 (00:03 +0300)
committerPerttu Ahola <celeron55@gmail.com>
Wed, 6 Jun 2012 21:05:00 +0000 (00:05 +0300)
builtin/builtin.lua
builtin/serialize.lua [new file with mode: 0644]
doc/lua_api.txt

index 13c1c09d4dca41ce1d27d9fd67a84d0898adad94..bd5adf9e715668b7200c18156f9d3ba44f1462cf 100644 (file)
@@ -10,6 +10,7 @@ print = minetest.debug
 math.randomseed(os.time())
 
 -- Load other files
+dofile(minetest.get_modpath("__builtin").."/serialize.lua")
 dofile(minetest.get_modpath("__builtin").."/misc_helpers.lua")
 dofile(minetest.get_modpath("__builtin").."/item.lua")
 dofile(minetest.get_modpath("__builtin").."/misc_register.lua")
diff --git a/builtin/serialize.lua b/builtin/serialize.lua
new file mode 100644 (file)
index 0000000..ecb2438
--- /dev/null
@@ -0,0 +1,207 @@
+-- Minetest: builtin/serialize.lua
+
+-- https://github.com/fab13n/metalua/blob/no-dll/src/lib/serialize.lua
+-- Copyright (c) 2006-2997 Fabien Fleutot <metalua@gmail.com>
+-- License: MIT
+--------------------------------------------------------------------------------
+-- Serialize an object into a source code string. This string, when passed as
+-- an argument to deserialize(), returns an object structurally identical
+-- to the original one. The following are currently supported:
+-- * strings, numbers, booleans, nil
+-- * tables thereof. Tables can have shared part, but can't be recursive yet.
+-- Caveat: metatables and environments aren't saved.
+--------------------------------------------------------------------------------
+
+local no_identity = { number=1, boolean=1, string=1, ['nil']=1 }
+
+function minetest.serialize(x)
+
+       local gensym_max   =  0  -- index of the gensym() symbol generator
+       local seen_once    = { } -- element->true set of elements seen exactly once in the table
+       local multiple     = { } -- element->varname set of elements seen more than once
+       local nested       = { } -- transient, set of elements currently being traversed
+       local nest_points  = { }
+       local nest_patches = { }
+       
+       local function gensym()
+               gensym_max = gensym_max + 1 ;  return gensym_max
+       end
+
+       -----------------------------------------------------------------------------
+       -- nest_points are places where a table appears within itself, directly or not.
+       -- for instance, all of these chunks create nest points in table x:
+       -- "x = { }; x[x] = 1", "x = { }; x[1] = x", "x = { }; x[1] = { y = { x } }".
+       -- To handle those, two tables are created by mark_nest_point:
+       -- * nest_points [parent] associates all keys and values in table parent which
+       --   create a nest_point with boolean `true'
+       -- * nest_patches contain a list of { parent, key, value } tuples creating
+       --   a nest point. They're all dumped after all the other table operations
+       --   have been performed.
+       --
+       -- mark_nest_point (p, k, v) fills tables nest_points and nest_patches with
+       -- informations required to remember that key/value (k,v) create a nest point
+       -- in table parent. It also marks `parent' as occuring multiple times, since
+       -- several references to it will be required in order to patch the nest
+       -- points.
+       -----------------------------------------------------------------------------
+       local function mark_nest_point (parent, k, v)
+               local nk, nv = nested[k], nested[v]
+               assert (not nk or seen_once[k] or multiple[k])
+               assert (not nv or seen_once[v] or multiple[v])
+               local mode = (nk and nv and "kv") or (nk and "k") or ("v")
+               local parent_np = nest_points [parent]
+               local pair = { k, v }
+               if not parent_np then parent_np = { }; nest_points [parent] = parent_np end
+               parent_np [k], parent_np [v] = nk, nv
+               table.insert (nest_patches, { parent, k, v })
+               seen_once [parent], multiple [parent]  = nil, true
+       end
+
+       -----------------------------------------------------------------------------
+       -- First pass, list the tables and functions which appear more than once in x
+       -----------------------------------------------------------------------------
+       local function mark_multiple_occurences (x)
+               if no_identity [type(x)] then return end
+               if     seen_once [x]     then seen_once [x], multiple [x] = nil, true
+               elseif multiple  [x]     then -- pass
+               else   seen_once [x] = true end
+               
+               if type (x) == 'table' then
+                       nested [x] = true
+                       for k, v in pairs (x) do
+                               if nested[k] or nested[v] then mark_nest_point (x, k, v) else
+                                       mark_multiple_occurences (k)
+                                       mark_multiple_occurences (v)
+                               end
+                       end
+                       nested [x] = nil
+               end
+       end
+
+       local dumped    = { } -- multiply occuring values already dumped in localdefs
+       local localdefs = { } -- already dumped local definitions as source code lines
+
+       -- mutually recursive functions:
+       local dump_val, dump_or_ref_val
+
+       --------------------------------------------------------------------
+       -- if x occurs multiple times, dump the local var rather than the
+       -- value. If it's the first time it's dumped, also dump the content
+       -- in localdefs.
+       --------------------------------------------------------------------
+       function dump_or_ref_val (x)
+               if nested[x] then return 'false' end -- placeholder for recursive reference
+               if not multiple[x] then return dump_val (x) end
+               local var = dumped [x]
+               if var then return "_[" .. var .. "]" end -- already referenced
+               local val = dump_val(x) -- first occurence, create and register reference
+               var = gensym()
+               table.insert(localdefs, "_["..var.."]="..val)
+               dumped [x] = var
+               return "_[" .. var .. "]"
+       end
+
+       -----------------------------------------------------------------------------
+       -- Second pass, dump the object; subparts occuring multiple times are dumped
+       -- in local variables which can be referenced multiple times;
+       -- care is taken to dump locla vars in asensible order.
+       -----------------------------------------------------------------------------
+       function dump_val(x)
+               local  t = type(x)
+               if     x==nil        then return 'nil'
+               elseif t=="number"   then return tostring(x)
+               elseif t=="string"   then return string.format("%q", x)
+               elseif t=="boolean"  then return x and "true" or "false"
+               elseif t=="table" then
+                       local acc        = { }
+                       local idx_dumped = { }
+                       local np         = nest_points [x]
+                       for i, v in ipairs(x) do
+                               if np and np[v] then
+                                       table.insert (acc, 'false') -- placeholder
+                               else
+                                       table.insert (acc, dump_or_ref_val(v))
+                               end
+                               idx_dumped[i] = true
+                       end
+                       for k, v in pairs(x) do
+                               if np and (np[k] or np[v]) then
+                                       --check_multiple(k); check_multiple(v) -- force dumps in localdefs
+                               elseif not idx_dumped[k] then
+                                       table.insert (acc, "[" .. dump_or_ref_val(k) .. "] = " .. dump_or_ref_val(v))
+                               end
+                       end
+                       return "{ "..table.concat(acc,", ").." }"
+               else
+                       error ("Can't serialize data of type "..t)
+               end
+       end
+       
+       local function dump_nest_patches()
+               for _, entry in ipairs(nest_patches) do
+                       local p, k, v = unpack (entry)
+                       assert (multiple[p])
+                       local set = dump_or_ref_val (p) .. "[" .. dump_or_ref_val (k) .. "] = " .. 
+                               dump_or_ref_val (v) .. " -- rec "
+                       table.insert (localdefs, set)
+               end
+       end
+
+       mark_multiple_occurences (x)
+       local toplevel = dump_or_ref_val (x)
+       dump_nest_patches()
+
+       if next (localdefs) then
+               return "local _={ }\n" ..
+                       table.concat (localdefs, "\n") .. 
+                       "\nreturn " .. toplevel
+       else
+               return "return " .. toplevel
+       end
+end
+
+-- Deserialization.
+-- http://stackoverflow.com/questions/5958818/loading-serialized-data-into-a-table
+--
+
+local function stringtotable(sdata)
+       if sdata:byte(1) == 27 then return nil, "binary bytecode prohibited" end
+       local f, message = assert(loadstring(sdata))
+       if not f then return nil, message end
+       setfenv(f, table)
+       return f()
+end
+
+function minetest.deserialize(sdata)
+       local table = {}
+       local okay,results = pcall(stringtotable, sdata)
+       if okay then
+               return results
+       end
+       print('error:'.. results)
+       return nil
+end
+
+-- Run some unit tests
+local function unit_test()
+       function unitTest(name, success)
+               if not success then
+                       error(name .. ': failed')
+               end
+       end
+
+       unittest_input = {cat={sound="nyan", speed=400}, dog={sound="woof"}}
+       unittest_output = minetest.deserialize(minetest.serialize(unittest_input))
+
+       unitTest("test 1a", unittest_input.cat.sound == unittest_output.cat.sound)
+       unitTest("test 1b", unittest_input.cat.speed == unittest_output.cat.speed)
+       unitTest("test 1c", unittest_input.dog.sound == unittest_output.dog.sound)
+
+       unittest_input = {escapechars="\n\r\t\v\\\"\'\[\]", noneuropean="θשׁ٩∂"}
+       unittest_output = minetest.deserialize(minetest.serialize(unittest_input))
+       unitTest("test 3a", unittest_input.escapechars == unittest_output.escapechars)
+       unitTest("test 3b", unittest_input.noneuropean == unittest_output.noneuropean)
+end
+unit_test() -- Run it
+unit_test = nil -- Hide it
+
index 16587144d4c48d8b3c5f3714f9d9984a9713ca1d..61bc8e1c22b98a01d5babe4f72dad54aef42c63b 100644 (file)
@@ -805,6 +805,17 @@ minetest.get_item_group(name, group) -> rating
 ^ Get rating of a group of an item. (0 = not in group)
 minetest.get_node_group(name, group) -> rating
 ^ Deprecated: An alias for the former.
+minetest.serialize(table) -> string
+^ Convert a table containing tables, strings, numbers, booleans and nils
+  into string form readable by minetest.deserialize
+^ Example: serialize({foo='bar'}) -> 'return { ["foo"] = "bar" }'
+minetest.deserialize(string) -> table
+^ Convert a string returned by minetest.deserialize into a table
+^ String is loaded in an empty sandbox environment.
+^ Will load functions, but they cannot access the global environment.
+^ Example: deserialize('return { ["foo"] = "bar" }') -> {foo='bar'}
+^ Example: deserialize('print("foo")') -> nil (function call fails)
+  ^ error:[string "print("foo")"]:1: attempt to call global 'print' (a nil value)
 
 Global objects:
 minetest.env - EnvRef of the server environment and world.