snippets/core.lua

280 lines
7.9 KiB
Lua

--
-- Minetest snippets mod: Attempt to prevent snippets from crashing the server
--
-- Make loadstring a local variable
local loadstring
if minetest.global_exists('loadstring') then
loadstring = _G.loadstring
else
loadstring = assert(load)
end
local copy = table.copy
local safe_funcs = {}
local orig_funcs, running_snippet
function snippets.get_current_snippet()
return running_snippet
end
-- Apply "safe functions": These wrap normal registration functions so that
-- snippets can't crash them as easily.
local function apply_safe_funcs()
if orig_funcs then return end
orig_funcs = {}
for k, v in pairs(safe_funcs) do
if k ~= 'print' then
orig_funcs[k] = minetest[k]
minetest[k] = v
end
end
orig_funcs.print, print = print, safe_funcs.print
end
local function remove_safe_funcs()
if not orig_funcs then return end
for k, v in pairs(orig_funcs) do
minetest[k] = orig_funcs[k]
end
print = orig_funcs.print
orig_funcs = nil
end
-- "Break out" of wrapped functions.
local function wrap_unsafe(func)
return function(...)
if orig_funcs then
remove_safe_funcs()
local res = {func(...)}
apply_safe_funcs()
return (table.unpack or unpack)(res)
else
return func(...)
end
end
end
-- Logging
snippets.registered_on_log = {}
snippets.log_levels = {}
function snippets.log_levels.error(n)
return minetest.colorize('red', n)
end
function snippets.log_levels.warning(n)
return minetest.colorize('yellow', n)
end
function snippets.log_levels.info(n)
return n
end
snippets.log_levels.none = snippets.log_levels.info
function snippets.log_levels.debug(n)
return minetest.colorize('grey', n)
end
function snippets.log(level, msg)
local snippet = running_snippet or 'snippets:anonymous'
if msg == nil then level, msg = 'none', level end
level, msg = tostring(level), tostring(msg)
if level == 'warn' then
level = 'warning'
elseif not snippets.log_levels[level] then
level = 'none'
end
for _, func in ipairs(snippets.registered_on_log) do
if func(snippet, level, msg) then return end
end
end
snippets.log = wrap_unsafe(snippets.log)
function snippets.register_on_log(func)
assert(type(func) == 'function')
table.insert(snippets.registered_on_log, 1, func)
end
-- Create the default log action
-- Only notify the player of errors or warnings
snippets.register_on_log(function(snippet, level, msg)
local rawmsg
if level == 'warning' then
rawmsg = 'Warning'
elseif level == 'error' then
rawmsg = 'Error'
else
return
end
rawmsg = snippets.log_levels[level](rawmsg .. ' in snippet "' .. snippet ..
'": ' .. msg)
local def = snippets.registered_snippets[snippet]
if def and def.owner then
minetest.chat_send_player(def.owner, rawmsg)
else
minetest.chat_send_all(rawmsg)
end
end)
-- Create a safe print()
function safe_funcs.print(...)
local msg = ''
for i = 1, select('#', ...) do
if i > 1 then msg = msg .. '\t' end
msg = msg .. tostring(select(i, ...))
end
snippets.log('none', msg)
end
-- Mostly copied from https://stackoverflow.com/a/26367080
local function wrap_raw(snippet, func, ...)
local old_running = running_snippet
running_snippet = snippet
local use_safe_funcs = not orig_funcs
if use_safe_funcs then apply_safe_funcs() end
local good, msg = pcall(func, ...)
if use_safe_funcs then remove_safe_funcs() end
if good then
running_snippet = old_running
return msg
else
snippets.log('error', msg)
running_snippet = old_running
end
end
local function wrap(snippet, func)
if not snippet then return func end
return function(...) return wrap_raw(snippet, func, ...) end
end
-- Expose the above function to the API.
-- This will only wrap functions if called from inside a snippet.
function snippets.wrap_callback(func)
return wrap(running_snippet, func)
end
do
local after_ = minetest.after
function safe_funcs.after(after, func, ...)
after = tonumber(after)
assert(after and after == after, 'Invalid core.ater invocation')
after_(after, wrap_raw, running_snippet, func, ...)
end
function snippets.wrap_register_on(orig)
return function(func, ...)
return orig(wrap(running_snippet, func), ...)
end
end
for k, v in pairs(minetest) do
if type(k) == 'string' and k:sub(1, 12) == 'register_on_' then
safe_funcs[k] = snippets.wrap_register_on(v)
end
end
end
-- Register a snippet
snippets.registered_snippets = {}
function snippets.register_snippet(name, def)
if def == nil and type(name) == 'table' then
name, def = name.name, name
elseif type(name) ~= 'string' then
error('Invalid name passed to snippets.register_snippet!', 2)
elseif type(def) == 'string' then
def = {code=def}
elseif type(def) ~= 'table' then
error('Invalid definition passed to snippets.register_snippet!', 2)
elseif def.owner and type(def.owner) ~= 'string' then
error('Invalid owner passed to snippets.register_snippet!', 2)
end
def = table.copy(def)
def.name = name
if def.code then
-- Automatically add "return"
local msg
def.func = loadstring('return ' .. def.code, name)
if not def.func then
def.func, msg = loadstring(def.code, name)
end
if def.func then
if name ~= 'snippets:anonymous' then
local old_def = snippets.registered_snippets[name]
def.env = old_def and old_def.env
end
if not def.env then
local g = {}
def.env = setmetatable({}, {__index = function(self, key)
local res = rawget(_G, key)
if res == nil and not g[key] then
snippets.log('warning', 'Undeclared global variable "'
.. key .. '" accessed.')
g[key] = true
end
return res
end})
end
setfenv(def.func, def.env)
else
local r, s = running_snippet, snippets.registered_snippets[name]
function def.func() end
running_snippet, snippets.registered_snippets[name] = name, def
snippets.log('error', 'Load error: ' .. tostring(msg))
running_snippet, snippets.registered_snippets[name] = r, s
end
else
def.persistent = nil
end
if not def.persistent then def.code = nil end
if type(def.func) ~= 'function' then return false end
snippets.registered_snippets[name] = def
return true
end
snippets.register_snippet('snippets:anonymous', '')
-- Run a snippet
function snippets.run(snippet, ...)
local def = snippets.registered_snippets[snippet]
if not def then error('Invalid snippet specified!', 2) end
return wrap_raw(snippet, def.func, ...)
end
-- Run code as player
function snippets.exec_as_player(name, code)
if minetest.is_player(name) then name = name:get_player_name() end
local owner
if name and name ~= '' then
owner = name
name = 'snippets:player_' .. tostring(name)
else
name = 'snippets:anonymous'
end
local def = {
code = tostring(code),
owner = owner,
}
if not snippets.register_snippet(name, def) then return end
return snippets.run(name)
end
function snippets.exec(code) return snippets.exec_as_player(nil, code) end
minetest.register_on_leaveplayer(function(player)
snippets.registered_snippets['snippets:player_' ..
player:get_player_name()] = nil
end)
-- In case console.lua isn't loaded
function snippets.unregister_snippet(name)
if snippets.registered_snippets[name] ~= nil then
snippets.registered_snippets[name] = nil
end
end