123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218 |
- --- Lua module to serialize values as Lua code.
- -- From: https://github.com/fab13n/metalua/blob/no-dll/src/lib/serialize.lua
- -- License: MIT
- -- @copyright 2006-2997 Fabien Fleutot <metalua@gmail.com>
- -- @author Fabien Fleutot <metalua@gmail.com>
- -- @author ShadowNinja <shadowninja@minetest.net>
- --------------------------------------------------------------------------------
- --- 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:
- -- * Booleans, numbers, strings, and nil.
- -- * Functions; uses interpreter-dependent (and sometimes platform-dependent) bytecode!
- -- * Tables; they can cantain multiple references and can be recursive, but metatables aren't saved.
- -- This works in two phases:
- -- 1. Recursively find and record multiple references and recursion.
- -- 2. Recursively dump the value into a string.
- -- @param x Value to serialize (nil is allowed).
- -- @return load()able string containing the value.
- function core.serialize(x)
- local local_index = 1 -- Top index of the "_" local table in the dump
- -- table->nil/1/2 set of tables seen.
- -- nil = not seen, 1 = seen once, 2 = seen multiple times.
- local seen = {}
- -- 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 used by mark_nest_point:
- -- * nested - Transient set of tables being currently traversed.
- -- Used for detecting nested tables.
- -- * nest_points - parent->{key=value, ...} table cantaining the nested
- -- keys and values in the parent. They're all dumped after all the
- -- other table operations have been performed.
- --
- -- mark_nest_point(p, k, v) fills nest_points with information required
- -- to remember that key/value (k, v) creates a nest point in table
- -- parent. It also marks "parent" and the nested item(s) as occuring
- -- multiple times, since several references to it will be required in
- -- order to patch the nest points.
- local nest_points = {}
- local nested = {}
- local function mark_nest_point(parent, k, v)
- local nk, nv = nested[k], nested[v]
- local np = nest_points[parent]
- if not np then
- np = {}
- nest_points[parent] = np
- end
- np[k] = v
- seen[parent] = 2
- if nk then seen[k] = 2 end
- if nv then seen[v] = 2 end
- end
- -- First phase, list the tables and functions which appear more than
- -- once in x.
- local function mark_multiple_occurences(x)
- local tp = type(x)
- if tp ~= "table" and tp ~= "function" then
- -- No identity (comparison is done by value, not by instance)
- return
- end
- if seen[x] == 1 then
- seen[x] = 2
- elseif seen[x] ~= 2 then
- seen[x] = 1
- end
- if tp == "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 = {} -- object->varname set
- local local_defs = {} -- Dumped local definitions as source code lines
- -- Mutually recursive local functions:
- local dump_val, dump_or_ref_val
- -- If x occurs multiple times, dump the local variable rather than
- -- the value. If it's the first time it's dumped, also dump the
- -- content in local_defs.
- function dump_or_ref_val(x)
- if seen[x] ~= 2 then
- return dump_val(x)
- end
- local var = dumped[x]
- if var then -- Already referenced
- return var
- end
- -- First occurence, create and register reference
- local val = dump_val(x)
- local i = local_index
- local_index = local_index + 1
- var = "_["..i.."]"
- table.insert(local_defs, var.." = "..val)
- dumped[x] = var
- return var
- end
- -- Second phase. Dump the object; subparts occuring multiple times
- -- are dumped in local variables which can be referenced multiple
- -- times. Care is taken to dump local vars in a sensible order.
- function dump_val(x)
- local tp = type(x)
- if x == nil then return "nil"
- elseif tp == "string" then return string.format("%q", x)
- elseif tp == "boolean" then return x and "true" or "false"
- elseif tp == "function" then
- return string.format("loadstring(%q)", string.dump(x))
- elseif tp == "number" then
- -- Serialize integers with string.format to prevent
- -- scientific notation, which doesn't preserve
- -- precision and breaks things like node position
- -- hashes. Serialize floats normally.
- if math.floor(x) == x then
- return string.format("%d", x)
- else
- return tostring(x)
- end
- elseif tp == "table" then
- local vals = {}
- local idx_dumped = {}
- local np = nest_points[x]
- for i, v in ipairs(x) do
- if not np or not np[i] then
- table.insert(vals, dump_or_ref_val(v))
- end
- idx_dumped[i] = true
- end
- for k, v in pairs(x) do
- if (not np or not np[k]) and
- not idx_dumped[k] then
- table.insert(vals,
- "["..dump_or_ref_val(k).."] = "
- ..dump_or_ref_val(v))
- end
- end
- return "{"..table.concat(vals, ", ").."}"
- else
- error("Can't serialize data of type "..tp)
- end
- end
- local function dump_nest_points()
- for parent, vals in pairs(nest_points) do
- for k, v in pairs(vals) do
- table.insert(local_defs, dump_or_ref_val(parent)
- .."["..dump_or_ref_val(k).."] = "
- ..dump_or_ref_val(v))
- end
- end
- end
- mark_multiple_occurences(x)
- local top_level = dump_or_ref_val(x)
- dump_nest_points()
- if next(local_defs) then
- return "local _ = {}\n"
- ..table.concat(local_defs, "\n")
- .."\nreturn "..top_level
- else
- return "return "..top_level
- end
- end
- -- Deserialization
- local env = {
- loadstring = loadstring,
- }
- local safe_env = {
- loadstring = function() end,
- }
- function core.deserialize(str, safe)
- if str:byte(1) == 0x1B then
- return nil, "Bytecode prohibited"
- end
- local f, err = loadstring(str)
- if not f then return nil, err end
- setfenv(f, safe and safe_env or env)
- local good, data = pcall(f)
- if good then
- return data
- else
- return nil, data
- end
- end
- -- Unit tests
- local test_in = {cat={sound="nyan", speed=400}, dog={sound="woof"}}
- local test_out = core.deserialize(core.serialize(test_in))
- assert(test_in.cat.sound == test_out.cat.sound)
- assert(test_in.cat.speed == test_out.cat.speed)
- assert(test_in.dog.sound == test_out.dog.sound)
- test_in = {escape_chars="\n\r\t\v\\\"\'", non_european="θשׁ٩∂"}
- test_out = core.deserialize(core.serialize(test_in))
- assert(test_in.escape_chars == test_out.escape_chars)
- assert(test_in.non_european == test_out.non_european)
|