aboutsummaryrefslogtreecommitdiff
path: root/builtin/common
diff options
context:
space:
mode:
Diffstat (limited to 'builtin/common')
-rw-r--r--builtin/common/information_formspecs.lua4
-rw-r--r--builtin/common/misc_helpers.lua161
-rw-r--r--builtin/common/mod_storage.lua19
-rw-r--r--builtin/common/serialize.lua353
-rw-r--r--builtin/common/strict.lua37
-rw-r--r--builtin/common/tests/misc_helpers_spec.lua99
-rw-r--r--builtin/common/tests/serialize_spec.lua158
-rw-r--r--builtin/common/tests/vector_spec.lua10
-rw-r--r--builtin/common/vector.lua14
9 files changed, 591 insertions, 264 deletions
diff --git a/builtin/common/information_formspecs.lua b/builtin/common/information_formspecs.lua
index 3405263bf..1445a017c 100644
--- a/builtin/common/information_formspecs.lua
+++ b/builtin/common/information_formspecs.lua
@@ -22,7 +22,6 @@ local LIST_FORMSPEC_DESCRIPTION = [[
local F = core.formspec_escape
local S = core.get_translator("__builtin")
-local check_player_privs = core.check_player_privs
-- CHAT COMMANDS FORMSPEC
@@ -58,10 +57,11 @@ local function build_chatcommands_formspec(name, sel, copy)
.. "any entry in the list.").. "\n" ..
S("Double-click to copy the entry to the chat history.")
+ local privs = core.get_player_privs(name)
for i, data in ipairs(mod_cmds) do
rows[#rows + 1] = COLOR_BLUE .. ",0," .. F(data[1]) .. ","
for j, cmds in ipairs(data[2]) do
- local has_priv = check_player_privs(name, cmds[2].privs)
+ local has_priv = privs[cmds[2].privs]
rows[#rows + 1] = ("%s,1,%s,%s"):format(
has_priv and COLOR_GREEN or COLOR_GRAY,
cmds[1], F(cmds[2].params))
diff --git a/builtin/common/misc_helpers.lua b/builtin/common/misc_helpers.lua
index f5f89acd7..467f18804 100644
--- a/builtin/common/misc_helpers.lua
+++ b/builtin/common/misc_helpers.lua
@@ -204,7 +204,7 @@ end
--------------------------------------------------------------------------------
function string:trim()
- return (self:gsub("^%s*(.-)%s*$", "%1"))
+ return self:match("^%s*(.-)%s*$")
end
--------------------------------------------------------------------------------
@@ -245,16 +245,16 @@ function math.round(x)
return math.ceil(x - 0.5)
end
-
+local formspec_escapes = {
+ ["\\"] = "\\\\",
+ ["["] = "\\[",
+ ["]"] = "\\]",
+ [";"] = "\\;",
+ [","] = "\\,"
+}
function core.formspec_escape(text)
- if text ~= nil then
- text = string.gsub(text,"\\","\\\\")
- text = string.gsub(text,"%]","\\]")
- text = string.gsub(text,"%[","\\[")
- text = string.gsub(text,";","\\;")
- text = string.gsub(text,",","\\,")
- end
- return text
+ -- Use explicit character set instead of dot here because it doubles the performance
+ return text and string.gsub(text, "[\\%[%];,]", formspec_escapes)
end
@@ -265,18 +265,21 @@ function core.wrap_text(text, max_length, as_table)
return as_table and {text} or text
end
- for word in text:gmatch('%S+') do
- local cur_length = #table.concat(line, ' ')
- if cur_length > 0 and cur_length + #word + 1 >= max_length then
+ local line_length = 0
+ for word in text:gmatch("%S+") do
+ if line_length > 0 and line_length + #word + 1 >= max_length then
-- word wouldn't fit on current line, move to next line
- table.insert(result, table.concat(line, ' '))
- line = {}
+ table.insert(result, table.concat(line, " "))
+ line = {word}
+ line_length = #word
+ else
+ table.insert(line, word)
+ line_length = line_length + 1 + #word
end
- table.insert(line, word)
end
- table.insert(result, table.concat(line, ' '))
- return as_table and result or table.concat(result, '\n')
+ table.insert(result, table.concat(line, " "))
+ return as_table and result or table.concat(result, "\n")
end
--------------------------------------------------------------------------------
@@ -425,54 +428,50 @@ function core.string_to_pos(value)
return nil
end
- local x, y, z = string.match(value, "^([%d.-]+)[, ] *([%d.-]+)[, ] *([%d.-]+)$")
- if x and y and z then
- x = tonumber(x)
- y = tonumber(y)
- z = tonumber(z)
- return vector.new(x, y, z)
- end
- x, y, z = string.match(value, "^%( *([%d.-]+)[, ] *([%d.-]+)[, ] *([%d.-]+) *%)$")
+ value = value:match("^%((.-)%)$") or value -- strip parentheses
+
+ local x, y, z = value:trim():match("^([%d.-]+)[,%s]%s*([%d.-]+)[,%s]%s*([%d.-]+)$")
if x and y and z then
x = tonumber(x)
y = tonumber(y)
z = tonumber(z)
return vector.new(x, y, z)
end
+
return nil
end
--------------------------------------------------------------------------------
-function core.string_to_area(value)
- local p1, p2 = unpack(value:split(") ("))
- if p1 == nil or p2 == nil then
- return nil
- end
- p1 = core.string_to_pos(p1 .. ")")
- p2 = core.string_to_pos("(" .. p2)
- if p1 == nil or p2 == nil then
- return nil
+do
+ local rel_num_cap = "(~?-?%d*%.?%d*)" -- may be overly permissive as this will be tonumber'ed anyways
+ local num_delim = "[,%s]%s*"
+ local pattern = "^" .. table.concat({rel_num_cap, rel_num_cap, rel_num_cap}, num_delim) .. "$"
+
+ local function parse_area_string(pos, relative_to)
+ local pp = {}
+ pp.x, pp.y, pp.z = pos:trim():match(pattern)
+ return core.parse_coordinates(pp.x, pp.y, pp.z, relative_to)
end
- return p1, p2
-end
+ function core.string_to_area(value, relative_to)
+ local p1, p2 = value:match("^%((.-)%)%s*%((.-)%)$")
+ if not p1 then
+ return
+ end
-local function test_string_to_area()
- local p1, p2 = core.string_to_area("(10.0, 5, -2) ( 30.2, 4, -12.53)")
- assert(p1.x == 10.0 and p1.y == 5 and p1.z == -2)
- assert(p2.x == 30.2 and p2.y == 4 and p2.z == -12.53)
+ p1 = parse_area_string(p1, relative_to)
+ p2 = parse_area_string(p2, relative_to)
- p1, p2 = core.string_to_area("(10.0, 5, -2 30.2, 4, -12.53")
- assert(p1 == nil and p2 == nil)
+ if p1 == nil or p2 == nil then
+ return
+ end
- p1, p2 = core.string_to_area("(10.0, 5,) -2 fgdf2, 4, -12.53")
- assert(p1 == nil and p2 == nil)
+ return p1, p2
+ end
end
-test_string_to_area()
-
--------------------------------------------------------------------------------
function table.copy(t, seen)
local n = {}
@@ -701,3 +700,71 @@ end
function core.is_nan(number)
return number ~= number
end
+
+--[[ Helper function for parsing an optionally relative number
+of a chat command parameter, using the chat command tilde notation.
+
+Parameters:
+* arg: String snippet containing the number; possible values:
+ * "<number>": return as number
+ * "~<number>": return relative_to + <number>
+ * "~": return relative_to
+ * Anything else will return `nil`
+* relative_to: Number to which the `arg` number might be relative to
+
+Returns:
+A number or `nil`, depending on `arg.
+
+Examples:
+* `core.parse_relative_number("5", 10)` returns 5
+* `core.parse_relative_number("~5", 10)` returns 15
+* `core.parse_relative_number("~", 10)` returns 10
+]]
+function core.parse_relative_number(arg, relative_to)
+ if not arg then
+ return nil
+ elseif arg == "~" then
+ return relative_to
+ elseif string.sub(arg, 1, 1) == "~" then
+ local number = tonumber(string.sub(arg, 2))
+ if not number then
+ return nil
+ end
+ if core.is_nan(number) or number == math.huge or number == -math.huge then
+ return nil
+ end
+ return relative_to + number
+ else
+ local number = tonumber(arg)
+ if core.is_nan(number) or number == math.huge or number == -math.huge then
+ return nil
+ end
+ return number
+ end
+end
+
+--[[ Helper function to parse coordinates that might be relative
+to another position; supports chat command tilde notation.
+Intended to be used in chat command parameter parsing.
+
+Parameters:
+* x, y, z: Parsed x, y, and z coordinates as strings
+* relative_to: Position to which to compare the position
+
+Syntax of x, y and z:
+* "<number>": return as number
+* "~<number>": return <number> + player position on this axis
+* "~": return player position on this axis
+
+Returns: a vector or nil for invalid input or if player does not exist
+]]
+function core.parse_coordinates(x, y, z, relative_to)
+ if not relative_to then
+ x, y, z = tonumber(x), tonumber(y), tonumber(z)
+ return x and y and z and { x = x, y = y, z = z }
+ end
+ local rx = core.parse_relative_number(x, relative_to.x)
+ local ry = core.parse_relative_number(y, relative_to.y)
+ local rz = core.parse_relative_number(z, relative_to.z)
+ return rx and ry and rz and { x = rx, y = ry, z = rz }
+end
diff --git a/builtin/common/mod_storage.lua b/builtin/common/mod_storage.lua
new file mode 100644
index 000000000..7ccf62900
--- /dev/null
+++ b/builtin/common/mod_storage.lua
@@ -0,0 +1,19 @@
+-- Modify core.get_mod_storage to return the storage for the current mod.
+
+local get_current_modname = core.get_current_modname
+
+local old_get_mod_storage = core.get_mod_storage
+
+local storages = setmetatable({}, {
+ __mode = "v", -- values are weak references (can be garbage-collected)
+ __index = function(self, modname)
+ local storage = old_get_mod_storage(modname)
+ self[modname] = storage
+ return storage
+ end,
+})
+
+function core.get_mod_storage()
+ local modname = get_current_modname()
+ return modname and storages[modname]
+end
diff --git a/builtin/common/serialize.lua b/builtin/common/serialize.lua
index 300b394c6..caf989e69 100644
--- a/builtin/common/serialize.lua
+++ b/builtin/common/serialize.lua
@@ -1,205 +1,224 @@
--- Lua module to serialize values as Lua code.
--- From: https://github.com/fab13n/metalua/blob/no-dll/src/lib/serialize.lua
+-- From: https://github.com/appgurueu/modlib/blob/master/luon.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 = {}
+local next, rawget, pairs, pcall, error, type, setfenv, loadstring
+ = next, rawget, pairs, pcall, error, type, setfenv, loadstring
- -- 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
+local table_concat, string_dump, string_format, string_match, math_huge
+ = table.concat, string.dump, string.format, string.match, math.huge
- -- 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)
+-- Recursively counts occurences of objects (non-primitives including strings) in a table.
+local function count_objects(value)
+ local counts = {}
+ if value == nil then
+ -- Early return for nil; tables can't contain nil
+ return counts
+ end
+ local function count_values(val)
+ local type_ = type(val)
+ if type_ == "boolean" or type_ == "number" then
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)
+ local count = counts[val]
+ counts[val] = (count or 0) + 1
+ if type_ == "table" then
+ if not count then
+ for k, v in pairs(val) do
+ count_values(k)
+ count_values(v)
end
end
- nested[x] = nil
+ elseif type_ ~= "string" and type_ ~= "function" then
+ error("unsupported type: " .. type_)
end
end
+ count_values(value)
+ return counts
+end
- local dumped = {} -- object->varname set
- local local_defs = {} -- Dumped local definitions as source code lines
+-- Build a "set" of Lua keywords. These can't be used as short key names.
+-- See https://www.lua.org/manual/5.1/manual.html#2.1
+local keywords = {}
+for _, keyword in pairs({
+ "and", "break", "do", "else", "elseif",
+ "end", "false", "for", "function", "if",
+ "in", "local", "nil", "not", "or",
+ "repeat", "return", "then", "true", "until", "while",
+ "goto" -- LuaJIT, Lua 5.2+
+}) do
+ keywords[keyword] = true
+end
- -- Mutually recursive local functions:
- local dump_val, dump_or_ref_val
+local function quote(string)
+ return string_format("%q", string)
+end
- -- 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
+local function dump_func(func)
+ return string_format("loadstring(%q)", string_dump(func))
+end
+
+-- Serializes Lua nil, booleans, numbers, strings, tables and even functions
+-- Tables are referenced by reference, strings are referenced by value. Supports circular tables.
+local function serialize(value, write)
+ local reference, refnum = "r1", 1
+ -- [object] = reference string
+ local references = {}
+ -- Circular tables that must be filled using `table[key] = value` statements
+ local to_fill = {}
+ for object, count in pairs(count_objects(value)) do
+ local type_ = type(object)
+ -- Object must appear more than once. If it is a string, the reference has to be shorter than the string.
+ if count >= 2 and (type_ ~= "string" or #reference + 2 < #object) then
+ write(reference)
+ write("=")
+ if type_ == "table" then
+ write("{}")
+ elseif type_ == "function" then
+ write(dump_func(object))
+ elseif type_ == "string" then
+ write(quote(object))
+ end
+ write(";")
+ references[object] = reference
+ if type_ == "table" then
+ to_fill[object] = reference
+ end
+ refnum = refnum + 1
+ reference = ("r%X"):format(refnum)
end
- -- First occurence, create and register reference
- local val = dump_val(x)
- local i = local_index
- local_index = local_index + 1
- var = "_["..i.."]"
- local_defs[#local_defs + 1] = 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 numbers reversibly with string.format
- return string.format("%.17g", x)
- 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
- vals[#vals + 1] = dump_or_ref_val(v)
- end
- idx_dumped[i] = true
+ -- Used to decide whether we should do "key=..."
+ local function use_short_key(key)
+ return not references[key] and type(key) == "string" and (not keywords[key]) and string_match(key, "^[%a_][%a%d_]*$")
+ end
+ local function dump(value)
+ -- Primitive types
+ if value == nil then
+ return write("nil")
+ end
+ if value == true then
+ return write("true")
+ end
+ if value == false then
+ return write("false")
+ end
+ local type_ = type(value)
+ if type_ == "number" then
+ return write(string_format("%.17g", value))
+ end
+ -- Reference types: table, function and string
+ local ref = references[value]
+ if ref then
+ return write(ref)
+ end
+ if type_ == "string" then
+ return write(quote(value))
+ end
+ if type_ == "function" then
+ return write(dump_func(value))
+ end
+ if type_ == "table" then
+ write("{")
+ -- First write list keys:
+ -- Don't use the table length #value here as it may horribly fail
+ -- for tables which use large integers as keys in the hash part;
+ -- stop at the first "hole" (nil value) instead
+ local len = 0
+ local first = true -- whether this is the first entry, which may not have a leading comma
+ while true do
+ local v = rawget(value, len + 1) -- use rawget to avoid metatables like the vector metatable
+ if v == nil then break end
+ if first then first = false else write(",") end
+ dump(v)
+ len = len + 1
end
- for k, v in pairs(x) do
- if (not np or not np[k]) and
- not idx_dumped[k] then
- vals[#vals + 1] = "["..dump_or_ref_val(k).."] = "
- ..dump_or_ref_val(v)
+ -- Now write map keys ([key] = value)
+ for k, v in next, value do
+ -- We have written all non-float keys in [1, len] already
+ if type(k) ~= "number" or k % 1 ~= 0 or k < 1 or k > len then
+ if first then first = false else write(",") end
+ if use_short_key(k) then
+ write(k)
+ else
+ write("[")
+ dump(k)
+ write("]")
+ end
+ write("=")
+ dump(v)
end
end
- return "{"..table.concat(vals, ", ").."}"
- else
- error("Can't serialize data of type "..tp)
+ write("}")
+ return
end
end
-
- local function dump_nest_points()
- for parent, vals in pairs(nest_points) do
- for k, v in pairs(vals) do
- local_defs[#local_defs + 1] = dump_or_ref_val(parent)
- .."["..dump_or_ref_val(k).."] = "
- ..dump_or_ref_val(v)
+ -- Write the statements to fill circular tables
+ for table, ref in pairs(to_fill) do
+ for k, v in pairs(table) do
+ write(ref)
+ if use_short_key(k) then
+ write(".")
+ write(k)
+ else
+ write("[")
+ dump(k)
+ write("]")
end
+ write("=")
+ dump(v)
+ write(";")
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
+ write("return ")
+ dump(value)
end
--- Deserialization
-
-local function safe_loadstring(...)
- local func, err = loadstring(...)
- if func then
- setfenv(func, {})
- return func
- end
- return nil, err
+function core.serialize(value)
+ local rope = {}
+ serialize(value, function(text)
+ -- Faster than table.insert(rope, text) on PUC Lua 5.1
+ rope[#rope + 1] = text
+ end)
+ return table_concat(rope)
end
local function dummy_func() end
+local nan = (0/0)^1 -- +nan
+
function core.deserialize(str, safe)
- if type(str) ~= "string" then
- return nil, "Cannot deserialize type '"..type(str)
- .."'. Argument must be a string."
+ -- Backwards compatibility
+ if str == nil then
+ core.log("deprecated", "minetest.deserialize called with nil (expected string).")
+ return nil, "Invalid type: Expected a string, got nil"
end
- if str:byte(1) == 0x1B then
- return nil, "Bytecode prohibited"
+ local t = type(str)
+ if t ~= "string" then
+ error(("minetest.deserialize called with %s (expected string)."):format(t))
end
- local f, err = loadstring(str)
- if not f then return nil, err end
- -- The environment is recreated every time so deseralized code cannot
- -- pollute it with permanent references.
- setfenv(f, {loadstring = safe and dummy_func or safe_loadstring})
+ local func, err = loadstring(str)
+ if not func then return nil, err end
- local good, data = pcall(f)
- if good then
- return data
+ -- math.huge is serialized to inf, NaNs are serialized to nan by Lua
+ local env = {inf = math_huge, nan = nan}
+ if safe then
+ env.loadstring = dummy_func
else
- return nil, data
+ env.loadstring = function(str, ...)
+ local func, err = loadstring(str, ...)
+ if func then
+ setfenv(func, env)
+ return func
+ end
+ return nil, err
+ end
+ end
+ setfenv(func, env)
+ local success, value_or_err = pcall(func)
+ if success then
+ return value_or_err
end
+ return nil, value_or_err
end
diff --git a/builtin/common/strict.lua b/builtin/common/strict.lua
index ccde9676b..936ebb37b 100644
--- a/builtin/common/strict.lua
+++ b/builtin/common/strict.lua
@@ -1,9 +1,4 @@
-
--- Always warn when creating a global variable, even outside of a function.
--- This ignores mod namespaces (variables with the same name as the current mod).
-local WARN_INIT = false
-
-local getinfo = debug.getinfo
+local getinfo, rawget, rawset = debug.getinfo, rawget, rawset
function core.global_exists(name)
if type(name) ~= "string" then
@@ -19,39 +14,33 @@ local declared = {}
local warned = {}
function meta:__newindex(name, value)
+ if declared[name] then
+ return
+ end
local info = getinfo(2, "Sl")
local desc = ("%s:%d"):format(info.short_src, info.currentline)
- if not declared[name] then
- local warn_key = ("%s\0%d\0%s"):format(info.source,
- info.currentline, name)
- if not warned[warn_key] and info.what ~= "main" and
- info.what ~= "C" then
- core.log("warning", ("Assignment to undeclared "..
- "global %q inside a function at %s.")
+ local warn_key = ("%s\0%d\0%s"):format(info.source, info.currentline, name)
+ if not warned[warn_key] and info.what ~= "main" and info.what ~= "C" then
+ core.log("warning", ("Assignment to undeclared global %q inside a function at %s.")
:format(name, desc))
- warned[warn_key] = true
- end
- declared[name] = true
- end
- -- Ignore mod namespaces
- if WARN_INIT and name ~= core.get_current_modname() then
- core.log("warning", ("Global variable %q created at %s.")
- :format(name, desc))
+ warned[warn_key] = true
end
rawset(self, name, value)
+ declared[name] = true
end
function meta:__index(name)
+ if declared[name] then
+ return
+ end
local info = getinfo(2, "Sl")
local warn_key = ("%s\0%d\0%s"):format(info.source, info.currentline, name)
- if not declared[name] and not warned[warn_key] and info.what ~= "C" then
+ if not warned[warn_key] and info.what ~= "C" then
core.log("warning", ("Undeclared global variable %q accessed at %s:%s")
:format(name, info.short_src, info.currentline))
warned[warn_key] = true
end
- return rawget(self, name)
end
setmetatable(_G, meta)
-
diff --git a/builtin/common/tests/misc_helpers_spec.lua b/builtin/common/tests/misc_helpers_spec.lua
index b16987f0b..7d046d5b7 100644
--- a/builtin/common/tests/misc_helpers_spec.lua
+++ b/builtin/common/tests/misc_helpers_spec.lua
@@ -1,4 +1,5 @@
_G.core = {}
+_G.vector = {metatable = {}}
dofile("builtin/common/vector.lua")
dofile("builtin/common/misc_helpers.lua")
@@ -66,9 +67,107 @@ describe("pos", function()
end)
end)
+describe("area parsing", function()
+ describe("valid inputs", function()
+ it("accepts absolute numbers", function()
+ local p1, p2 = core.string_to_area("(10.0, 5, -2) ( 30.2 4 -12.53)")
+ assert(p1.x == 10 and p1.y == 5 and p1.z == -2)
+ assert(p2.x == 30.2 and p2.y == 4 and p2.z == -12.53)
+ end)
+
+ it("accepts relative numbers", function()
+ local p1, p2 = core.string_to_area("(1,2,3) (~5,~-5,~)", {x=10,y=10,z=10})
+ assert(type(p1) == "table" and type(p2) == "table")
+ assert(p1.x == 1 and p1.y == 2 and p1.z == 3)
+ assert(p2.x == 15 and p2.y == 5 and p2.z == 10)
+
+ p1, p2 = core.string_to_area("(1 2 3) (~5 ~-5 ~)", {x=10,y=10,z=10})
+ assert(type(p1) == "table" and type(p2) == "table")
+ assert(p1.x == 1 and p1.y == 2 and p1.z == 3)
+ assert(p2.x == 15 and p2.y == 5 and p2.z == 10)
+ end)
+ end)
+ describe("invalid inputs", function()
+ it("rejects too few numbers", function()
+ local p1, p2 = core.string_to_area("(1,1) (1,1,1,1)", {x=1,y=1,z=1})
+ assert(p1 == nil and p2 == nil)
+ end)
+
+ it("rejects too many numbers", function()
+ local p1, p2 = core.string_to_area("(1,1,1,1) (1,1,1,1)", {x=1,y=1,z=1})
+ assert(p1 == nil and p2 == nil)
+ end)
+
+ it("rejects nan & inf", function()
+ local p1, p2 = core.string_to_area("(1,1,1) (1,1,nan)", {x=1,y=1,z=1})
+ assert(p1 == nil and p2 == nil)
+
+ p1, p2 = core.string_to_area("(1,1,1) (1,1,~nan)", {x=1,y=1,z=1})
+ assert(p1 == nil and p2 == nil)
+
+ p1, p2 = core.string_to_area("(1,1,1) (1,~nan,1)", {x=1,y=1,z=1})
+ assert(p1 == nil and p2 == nil)
+
+ p1, p2 = core.string_to_area("(1,1,1) (1,1,inf)", {x=1,y=1,z=1})
+ assert(p1 == nil and p2 == nil)
+
+ p1, p2 = core.string_to_area("(1,1,1) (1,1,~inf)", {x=1,y=1,z=1})
+ assert(p1 == nil and p2 == nil)
+
+ p1, p2 = core.string_to_area("(1,1,1) (1,~inf,1)", {x=1,y=1,z=1})
+ assert(p1 == nil and p2 == nil)
+
+ p1, p2 = core.string_to_area("(nan,nan,nan) (nan,nan,nan)", {x=1,y=1,z=1})
+ assert(p1 == nil and p2 == nil)
+
+ p1, p2 = core.string_to_area("(nan,nan,nan) (nan,nan,nan)")
+ assert(p1 == nil and p2 == nil)
+
+ p1, p2 = core.string_to_area("(inf,inf,inf) (-inf,-inf,-inf)", {x=1,y=1,z=1})
+ assert(p1 == nil and p2 == nil)
+
+ p1, p2 = core.string_to_area("(inf,inf,inf) (-inf,-inf,-inf)")
+ assert(p1 == nil and p2 == nil)
+ end)
+
+ it("rejects words", function()
+ local p1, p2 = core.string_to_area("bananas", {x=1,y=1,z=1})
+ assert(p1 == nil and p2 == nil)
+
+ p1, p2 = core.string_to_area("bananas", "foobar")
+ assert(p1 == nil and p2 == nil)
+
+ p1, p2 = core.string_to_area("bananas")
+ assert(p1 == nil and p2 == nil)
+
+ p1, p2 = core.string_to_area("(bananas,bananas,bananas)")
+ assert(p1 == nil and p2 == nil)
+
+ p1, p2 = core.string_to_area("(bananas,bananas,bananas) (bananas,bananas,bananas)")
+ assert(p1 == nil and p2 == nil)
+ end)
+
+ it("requires parenthesis & valid numbers", function()
+ local p1, p2 = core.string_to_area("(10.0, 5, -2 30.2, 4, -12.53")
+ assert(p1 == nil and p2 == nil)
+
+ p1, p2 = core.string_to_area("(10.0, 5,) -2 fgdf2, 4, -12.53")
+ assert(p1 == nil and p2 == nil)
+ end)
+ end)
+end)
+
describe("table", function()
it("indexof()", function()
assert.equal(1, table.indexof({"foo", "bar"}, "foo"))
assert.equal(-1, table.indexof({"foo", "bar"}, "baz"))
end)
end)
+
+describe("formspec_escape", function()
+ it("escapes", function()
+ assert.equal(nil, core.formspec_escape(nil))
+ assert.equal("", core.formspec_escape(""))
+ assert.equal("\\[Hello\\\\\\[", core.formspec_escape("[Hello\\["))
+ end)
+end)
diff --git a/builtin/common/tests/serialize_spec.lua b/builtin/common/tests/serialize_spec.lua
index e46b7dcc5..340e226ee 100644
--- a/builtin/common/tests/serialize_spec.lua
+++ b/builtin/common/tests/serialize_spec.lua
@@ -1,42 +1,97 @@
_G.core = {}
+_G.vector = {metatable = {}}
_G.setfenv = require 'busted.compatibility'.setfenv
dofile("builtin/common/serialize.lua")
dofile("builtin/common/vector.lua")
+-- Supports circular tables; does not support table keys
+-- Correctly checks whether a mapping of references ("same") exists
+-- Is significantly more efficient than assert.same
+local function assert_same(a, b, same)
+ same = same or {}
+ if same[a] or same[b] then
+ assert(same[a] == b and same[b] == a)
+ return
+ end
+ if a == b then
+ return
+ end
+ if type(a) ~= "table" or type(b) ~= "table" then
+ assert(a == b)
+ return
+ end
+ same[a] = b
+ same[b] = a
+ local count = 0
+ for k, v in pairs(a) do
+ count = count + 1
+ assert(type(k) ~= "table")
+ assert_same(v, b[k], same)
+ end
+ for _ in pairs(b) do
+ count = count - 1
+ end
+ assert(count == 0)
+end
+
+local x, y = {}, {}
+local t1, t2 = {x, x, y, y}, {x, y, x, y}
+assert.same(t1, t2) -- will succeed because it only checks whether the depths match
+assert(not pcall(assert_same, t1, t2)) -- will correctly fail because it checks whether the refs match
+
describe("serialize", function()
+ local function assert_preserves(value)
+ local preserved_value = core.deserialize(core.serialize(value))
+ assert_same(value, preserved_value)
+ end
it("works", function()
- local test_in = {cat={sound="nyan", speed=400}, dog={sound="woof"}}
- local test_out = core.deserialize(core.serialize(test_in))
-
- assert.same(test_in, test_out)
+ assert_preserves({cat={sound="nyan", speed=400}, dog={sound="woof"}})
end)
it("handles characters", function()
- local test_in = {escape_chars="\n\r\t\v\\\"\'", non_european="θשׁ٩∂"}
- local test_out = core.deserialize(core.serialize(test_in))
- assert.same(test_in, test_out)
+ assert_preserves({escape_chars="\n\r\t\v\\\"\'", non_european="θשׁ٩∂"})
+ end)
+
+ it("handles NaN & infinities", function()
+ local nan = core.deserialize(core.serialize(0/0))
+ assert(nan ~= nan)
+ assert_preserves(math.huge)
+ assert_preserves(-math.huge)
end)
it("handles precise numbers", function()
- local test_in = 0.2695949158945771
- local test_out = core.deserialize(core.serialize(test_in))
- assert.same(test_in, test_out)
+ assert_preserves(0.2695949158945771)
end)
it("handles big integers", function()
- local test_in = 269594915894577
- local test_out = core.deserialize(core.serialize(test_in))
- assert.same(test_in, test_out)
+ assert_preserves(269594915894577)
end)
it("handles recursive structures", function()
local test_in = { hello = "world" }
test_in.foo = test_in
+ assert_preserves(test_in)
+ end)
+
+ it("handles cross-referencing structures", function()
+ local test_in = {
+ foo = {
+ baz = {
+ {}
+ },
+ },
+ bar = {
+ baz = {},
+ },
+ }
- local test_out = core.deserialize(core.serialize(test_in))
- assert.same(test_in, test_out)
+ test_in.foo.baz[1].foo = test_in.foo
+ test_in.foo.baz[1].bar = test_in.bar
+ test_in.bar.baz[1] = test_in.foo.baz[1]
+
+ assert_preserves(test_in)
end)
it("strips functions in safe mode", function()
@@ -46,6 +101,7 @@ describe("serialize", function()
end,
foo = "bar"
}
+ setfenv(test_in.func, _G)
local str = core.serialize(test_in)
assert.not_nil(str:find("loadstring"))
@@ -57,13 +113,77 @@ describe("serialize", function()
it("vectors work", function()
local v = vector.new(1, 2, 3)
- assert.same({{x = 1, y = 2, z = 3}}, core.deserialize(core.serialize({v})))
- assert.same({x = 1, y = 2, z = 3}, core.deserialize(core.serialize(v)))
+ assert_preserves({v})
+ assert_preserves(v)
-- abuse
v = vector.new(1, 2, 3)
v.a = "bla"
- assert.same({x = 1, y = 2, z = 3, a = "bla"},
- core.deserialize(core.serialize(v)))
+ assert_preserves(v)
+ end)
+
+ it("handles keywords as keys", function()
+ assert_preserves({["and"] = "keyword", ["for"] = "keyword"})
+ end)
+
+ describe("fuzzing", function()
+ local atomics = {true, false, math.huge, -math.huge} -- no NaN or nil
+ local function atomic()
+ return atomics[math.random(1, #atomics)]
+ end
+ local function num()
+ local sign = math.random() < 0.5 and -1 or 1
+ -- HACK math.random(a, b) requires a, b & b - a to fit within a 32-bit int
+ -- Use two random calls to generate a random number from 0 - 2^50 as lower & upper 25 bits
+ local val = math.random(0, 2^25) * 2^25 + math.random(0, 2^25 - 1)
+ local exp = math.random() < 0.5 and 1 or 2^(math.random(-120, 120))
+ return sign * val * exp
+ end
+ local function charcodes(count)
+ if count == 0 then return end
+ return math.random(0, 0xFF), charcodes(count - 1)
+ end
+ local function str()
+ return string.char(charcodes(math.random(0, 100)))
+ end
+ local primitives = {atomic, num, str}
+ local function primitive()
+ return primitives[math.random(1, #primitives)]()
+ end
+ local function tab(max_actions)
+ local root = {}
+ local tables = {root}
+ local function random_table()
+ return tables[math.random(1, #tables)]
+ end
+ for _ = 1, math.random(1, max_actions) do
+ local tab = random_table()
+ local value
+ if math.random() < 0.5 then
+ if math.random() < 0.5 then
+ value = random_table()
+ else
+ value = {}
+ table.insert(tables, value)
+ end
+ else
+ value = primitive()
+ end
+ tab[math.random() < 0.5 and (#tab + 1) or primitive()] = value
+ end
+ return root
+ end
+ it("primitives work", function()
+ for _ = 1, 1e3 do
+ assert_preserves(primitive())
+ end
+ end)
+ it("tables work", function()
+ for _ = 1, 100 do
+ local fuzzed_table = tab(1e3)
+ assert_same(fuzzed_table, table.copy(fuzzed_table))
+ assert_preserves(fuzzed_table)
+ end
+ end)
end)
end)
diff --git a/builtin/common/tests/vector_spec.lua b/builtin/common/tests/vector_spec.lua
index 2f72f3383..6a0b81a89 100644
--- a/builtin/common/tests/vector_spec.lua
+++ b/builtin/common/tests/vector_spec.lua
@@ -1,4 +1,4 @@
-_G.vector = {}
+_G.vector = {metatable = {}}
dofile("builtin/common/vector.lua")
describe("vector", function()
@@ -128,6 +128,14 @@ describe("vector", function()
assert.equal(vector.new(4.1, 5.9, 5.5), a:apply(f))
end)
+ it("combine()", function()
+ local a = vector.new(1, 2, 3)
+ local b = vector.new(3, 2, 1)
+ assert.equal(vector.add(a, b), vector.combine(a, b, function(x, y) return x + y end))
+ assert.equal(vector.new(3, 2, 3), vector.combine(a, b, math.max))
+ assert.equal(vector.new(1, 2, 1), vector.combine(a, b, math.min))
+ end)
+
it("equals()", function()
local function assertE(a, b)
assert.is_true(vector.equals(a, b))
diff --git a/builtin/common/vector.lua b/builtin/common/vector.lua
index 581d014e0..a08472e32 100644
--- a/builtin/common/vector.lua
+++ b/builtin/common/vector.lua
@@ -6,10 +6,8 @@ Note: The vector.*-functions must be able to accept old vectors that had no meta
-- localize functions
local setmetatable = setmetatable
-vector = {}
-
-local metatable = {}
-vector.metatable = metatable
+-- vector.metatable is set by C++.
+local metatable = vector.metatable
local xyz = {"x", "y", "z"}
@@ -112,6 +110,14 @@ function vector.apply(v, func)
)
end
+function vector.combine(a, b, func)
+ return fast_new(
+ func(a.x, b.x),
+ func(a.y, b.y),
+ func(a.z, b.z)
+ )
+end
+
function vector.distance(a, b)
local x = a.x - b.x
local y = a.y - b.y