Merge branch 'feature/pcall-protection' into dev

This commit is contained in:
Starbeamrainbowlabs 2024-10-15 20:15:17 +01:00
commit 57d09a766b
Signed by: sbrl
GPG key ID: 1BE5172E637709C2
9 changed files with 214 additions and 39 deletions

View file

@ -53,12 +53,14 @@ local f = function(val) end
-- Table tweaks (because this is for Minetest)
--- @class table
local table = table
if not table.unpack then table.unpack = unpack end
table.join = function(tbl, sep)
local function fn_iter(tbl,sep,i)
if i < #tbl then
return (tostring(tbl[i]) or "").. sep .. fn_iter(tbl,sep,i+1)
else return (tostring(tbl[i]) or "") end
-- @diagnostic disable-next-line
if not table.unpack then table.unpack = unpack end --luacheck: ignore
-- @diagnostic disable-next-line
table.join = function(tbl, sep) --luacheck: ignore
local function fn_iter(tbl_inner,sep_inner,i)
if i < #tbl_inner then
return (tostring(tbl_inner[i]) or "").. sep_inner .. fn_iter(tbl_inner,sep_inner,i+1)
else return (tostring(tbl_inner[i]) or "") end
end
return fn_iter(tbl,sep,1)
end
@ -93,7 +95,7 @@ local function_type_warn = function(called_from, position, arg_name, must_be, se
end
local type_enforce = function(called_from, args)
local err_str = nil
local err_str
for i, arg in ipairs(args) do
local is_err = true
for _, should_be in ipairs(arg.should_be) do
@ -105,7 +107,7 @@ local type_enforce = function(called_from, args)
end
end
if is_err then
err_str = function_type_warn(called_from, i, arg.name, table.join(arg.should_be, " or "), arg.name == "self" and true or false)
err_str = function_type_warn(called_from, i, arg.name, table.join(arg.should_be, " or "), arg.name == "self" and true or false) --luacheck: ignore
if arg.error then error(err_str)
else warn(err_str) end
end

View file

@ -71,13 +71,14 @@ wea_c.register_command("maze", {
return success, replace_node, seed, path_length, path_width
end,
nodes_needed = function(name)
-- Note that we could take in additional parameters from the return value of parse (minue the success bool there), but we don't actually need them here
return worldedit.volume(worldedit.pos1[name], worldedit.pos2[name])
-- Note that we could take in additional parameters from the return value of parse (minus the success bool there), but we don't actually need them here
local pos1, pos2 = wea_c.pos.get12(name)
return worldedit.volume(pos1, pos2)
end,
func = function(name, replace_node, seed, path_length, path_width)
local start_time = wea_c.get_ms_time()
local pos1, pos2 = Vector3.sort(worldedit.pos1[name], worldedit.pos2[name])
local pos1, pos2 = wea_c.pos.get12(name)
local replaced = wea.maze2d(
pos1, pos2,
replace_node,
@ -114,7 +115,7 @@ wea_c.register_command("maze3d", {
end,
func = function(name, replace_node, seed, path_length, path_width, path_depth)
local start_time = wea_c.get_ms_time()
local pos1, pos2 = Vector3.sort(worldedit.pos1[name], worldedit.pos2[name])
local pos1, pos2 = Vector3.sort(wea_c.pos.get12(name))
local replaced = wea.maze3d(
pos1, pos2,
replace_node,

View file

@ -2,13 +2,14 @@
-- @module worldeditadditions_core
-- WARNING: safe_region MUST NOT be imported more than once, as it defines chat commands. If you want to import it again elsewhere, check first that multiple dofile() calls don't execute a file more than once.
local wea_c = worldeditadditions_core
local safe_region = dofile(wea_c.modpath.."/core/safe_region.lua")
local human_size = wea_c.format.human_size
local weac = worldeditadditions_core
local safe_region = dofile(weac.modpath.."/core/safe_region.lua")
local human_size = weac.format.human_size
local safe_function = weac.safe_function
-- TODO: Reimplement worldedit.player_notify(player_name, msg_text)
--- Actually runs the command in question.
--- Actually runs the command in question. [HIDDEN]
-- Unfortunately needed to keep the codebase clena because Lua sucks.
-- @internal
-- @param player_name string The name of the player executing the function.
@ -17,11 +18,17 @@ local human_size = wea_c.format.human_size
-- @param tbl_event table Internal event table used when calling `worldeditadditions_core.emit(event_name, tbl_event)`.
-- @returns nil
local function run_command_stage2(player_name, func, parse_result, tbl_event)
wea_c:emit("pre-execute", tbl_event)
local success, result_message = func(player_name, wea_c.table.unpack(parse_result))
weac:emit("pre-execute", tbl_event)
local success_safefn, retvals = safe_function(func, { player_name, weac.table.unpack(parse_result) }, player_name, "The function crashed during execution.", tbl_event.cmdname)
if not success_safefn then return false end
if #retvals ~= 2 then
worldedit.player_notify(player_name, "[//"..tostring(tbl_event.cmdname).."] The main execution function for this chat command returned "..tostring(#retvals).." arguments instead of the expected 2 (success, message), so it is unclear whether it succeeded or not. This is a bug!")
end
local success, result_message = retvals[1], retvals[2]
print("DEBUG:run_command_stage2 SUCCESS", success, "RESULT_MESSAGE", result_message)
if not success then
result_message = "[//"..tostring(tbl_event.cmdname).."] "..result_message
end
@ -31,9 +38,11 @@ local function run_command_stage2(player_name, func, parse_result, tbl_event)
end
tbl_event.success = success
tbl_event.result = result_message
wea_c:emit("post-execute", tbl_event)
weac:emit("post-execute", tbl_event)
end
--- Command execution pipeline: before `paramtext` parsing but after validation.
--
-- See `worldeditadditions_core.run_command`
@ -89,15 +98,15 @@ end
-- @param player_name string The name of the player to execute the command for.
-- @param paramtext string The unparsed argument string to pass to the command when executing it.
local function run_command(cmdname, options, player_name, paramtext)
if options.require_pos > 0 and not worldedit.pos1[player_name] and not wea_c.pos.get1(player_name) then
if options.require_pos > 0 and not worldedit.pos1[player_name] and not weac.pos.get1(player_name) then
worldedit.player_notify(player_name, "Error: pos1 must be selected to use this command.")
return false
end
if options.require_pos > 1 and not worldedit.pos2[player_name] and not wea_c.pos.get2(player_name) then
if options.require_pos > 1 and not worldedit.pos2[player_name] and not weac.pos.get2(player_name) then
worldedit.player_notify(player_name, "Error: Both pos1 and pos2 must be selected (together making a region) to use this command.")
return false
end
local pos_count = wea_c.pos.count(player_name)
local pos_count = weac.pos.count(player_name)
if options.require_pos > 2 and pos_count < options.require_pos then
worldedit.player_notify(player_name, "Error: At least "..options.require_pos.."positions must be defined to use this command, but you only have "..pos_count.." defined (try using the multiwand).")
return false
@ -110,33 +119,52 @@ local function run_command(cmdname, options, player_name, paramtext)
player_name = player_name
}
wea_c:emit("pre-parse", tbl_event)
weac:emit("pre-parse", tbl_event)
local parse_result = { options.parse(paramtext) }
local success = table.remove(parse_result, 1)
if not success then
worldedit.player_notify(player_name, ("[//"..tostring(cmdname).."] "..tostring(parse_result[1])) or "Invalid usage (no further error message was provided by the command. This is probably a bug.)")
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-- local did_error = false
local success_safefn, parse_result = safe_function(options.parse, { paramtext }, player_name, "The command crashed when parsing the arguments.", cmdname)
if not success_safefn then return false end -- error already sent to the player above
if #parse_result == 0 then
worldedit.player_notify(player_name, "[//"..tostring(cmdname).."] No return values at all were returned by the parsing function - not even a success boolean. This is a bug - please report it :D")
return false
end
local success = table.remove(parse_result, 1)
if not success then
worldedit.player_notify(player_name, "[//"..tostring(cmdname).."] "..(tostring(parse_result[1]) or "Invalid usage (no further error message was provided by the command. This is probably a bug.)"))
return false
end
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
tbl_event.paramargs = parse_result
wea_c:emit("post-parse", tbl_event)
weac:emit("post-parse", tbl_event)
if options.nodes_needed then
local potential_changes = options.nodes_needed(player_name, wea_c.table.unpack(parse_result))
local success_xpcall_nn, retvals_nn = safe_function(options.nodes_needed, { player_name, weac.table.unpack(parse_result) }, player_name, "The nodes_needed function crashed!", cmdname)
if not success_xpcall_nn then return false end
if #retvals_nn == 0 then
worldedit.player_notify(player_name, "[//"..tostring(cmdname).."] Error: The nodes_needed function didn't return any values. This is a bug!")
return false
end
local potential_changes = retvals_nn[1]
tbl_event.potential_changes = potential_changes
wea_c:emit("post-nodesneeded", tbl_event)
weac:emit("post-nodesneeded", tbl_event)
if type(potential_changes) ~= "number" then
worldedit.player_notify(player_name, "Error: The command '"..cmdname.."' returned a "..type(potential_changes).." instead of a number when asked how many nodes might be changed. Abort. This is a bug.")
return
end
local limit = wea_c.safe_region_limit_default
if wea_c.safe_region_limits[player_name] then
limit = wea_c.safe_region_limits[player_name]
local limit = weac.safe_region_limit_default
if weac.safe_region_limits[player_name] then
limit = weac.safe_region_limits[player_name]
end
if type(potential_changes) == "string" then
worldedit.player_notify(player_name, "/"..cmdname.." "..paramtext.." "..potential_changes..". Type //y to continue, or //n to cancel (in this specific situation, your configured limit via the //saferegion command does not apply).")

View file

@ -0,0 +1,98 @@
local weac = worldeditadditions_core
---
-- @module worldeditadditions_core
-- ███████ █████ ███████ ███████ ███████ ███ ██
-- ██ ██ ██ ██ ██ ██ ████ ██
-- ███████ ███████ █████ █████ █████ ██ ██ ██
-- ██ ██ ██ ██ ██ ██ ██ ██ ██
-- ███████ ██ ██ ██ ███████ ███████ ██ ██ ████
local function send_error(player_name, cmdname, msg, stack_trace)
print("DEBUG:HAI SEND_ERROR")
local msg_compiled = table.concat({
"[//", cmdname, "] Error: ",
msg,
"\n",
"Please report this by opening an issue on GitHub! Bug report link (ctrl + click):\n",
"https://github.com/sbrl/Minetest-WorldEditAdditions/issues/new?title=",
weac.format.escape(stack_trace:match("^[^\n]+")), -- extract 1st line & escape
"&body=",
weac.format.escape(table.concat({
[[## Describe the bug
What's the bug? Be clear and detailed but concise in our explanation. Don't forget to include any context, error messages, logs, and screenshots required to understand the issue if applicable.
## Reproduction steps
Steps to reproduce the behaviour:
1. Go to '...'
2. Click on '....'
3. Enter this command to '....'
4. See error
## System information (please complete the following information)
- **Operating system and version:** [e.g. iOS]
- **Minetest version:** [e.g. 5.8.0]
- **WorldEdit version:**
- **WorldEditAdditions version:**
Please add any other additional specific system information here too if you think it would help.
## Stack trace
- **Command name:** ]],
cmdname,
"\n",
"```\n",
stack_trace,
"\n",
"```\n",
}, "")),
"\n",
"-------------------------------------\n",
"*** Stack trace ***\n",
stack_trace,
"\n",
"-------------------------------------\n"
}, "")
print("DEBUG:player_notify player_name", player_name, "msg_compiled", msg_compiled)
worldedit.player_notify(player_name, msg_compiled)
end
--- Calls the given function `fn` with the UNPACKED arguments from `args`, catching errors and sending the calling player a nice error message with a report link.
--
-- WARNING: Do NOT nest `safe_function()` calls!!!
-- @param fn function The function to call
-- @param args table The table of args to unpack and send to `fn` as arguments
-- @param string|nil player_name The name of the player affected. If nil then no message is sent to the player.
-- @param string error_msg The error message to send when `fn` inevitably crashes.
-- @param string|nil cmdname Optional. The name of the command being run.
-- @returns bool,any,... A success bool (true == success), and then if success == true the rest of the arguments are the (unpacked) return values from the function called. If success == false, then the 2nd argument will be the stack trace.
local function safe_function(fn, args, player_name, error_msg, cmdname)
local retvals
local success_xpcall, stack_trace = xpcall(function()
retvals = { fn(weac.table.unpack(args)) }
end, debug.traceback)
if not success_xpcall then
send_error(player_name, cmdname, error_msg, stack_trace)
weac:emit("error", {
fn = fn,
args = args,
player_name = player_name,
cmdname = cmdname,
stack_trace = stack_trace,
error_msg = error_msg
})
minetest.log("error", "[//"..tostring(cmdname).."] Caught error from running function ", fn, "with args", weac.inspect(args), "for player ", player_name, "with provided error message", error_msg, ". Stack trace: ", stack_trace)
return false, stack_trace
end
return true, retvals
end
return safe_function

View file

@ -11,14 +11,33 @@ local modpath = minetest.get_modpath("worldeditadditions_core")
local EventEmitter = dofile(modpath .. "/utils/EventEmitter.lua")
local directory_separator = "/"
if package and package.config then
directory_separator = package.config:sub(1,1)
end
worldeditadditions_core = EventEmitter.new({
version = "1.15-dev",
--- The directory separator on the current host system
-- @value string
dirsep = directory_separator,
--- The full absolute filepath to the mod worldeditadditions_core
-- @value
modpath = modpath,
--- The full absolute filepath to the data directory WorldEditAdditions can store miscellaneous data in.
-- @value
datapath = minetest.get_worldpath() .. directory_separator .."worldeditadditions",
--- A table containing the definitions for all commands registered in WorldEditAdditions.
-- Keys are the command name SANS any forward slashes! So //replacemix would be registered as simply replacemix.
-- @value table<string, table>
registered_commands = {},
-- Storage for per-player node limits before safe_region kicks in.
--- Storage for per-player node limits before safe_region kicks in.
-- TODO: Persist these to disk.
-- @value table<string, number>
safe_region_limits = {},
-- The default limit for new players on the number of potential nodes changed before safe_region kicks in.
--- The default limit for new players on the number of potential nodes changed before safe_region kicks in.
-- TODO make this configurable
-- @value number
safe_region_limit_default = 100000,
})
local wea_c = worldeditadditions_core
@ -68,6 +87,7 @@ dofile(wea_c.modpath.."/utils/player.lua") -- Player info functions
wea_c.setting_handler = dofile(wea_c.modpath.."/utils/setting_handler.lua") -- AFTER parser
wea_c.pos = dofile(modpath.."/core/pos.lua") -- AFTER EventEmitter
wea_c.safe_function = dofile(modpath.."/core/safe_function.lua")
wea_c.register_command = dofile(modpath.."/core/register_command.lua")
wea_c.command_exists = dofile(modpath.."/core/command_exists.lua")
wea_c.fetch_command_def = dofile(modpath.."/core/fetch_command_def.lua")

View file

@ -0,0 +1,25 @@
---
-- @module worldeditadditions_core
-- decodeURIComponent() implementation
-- Ref https://stackoverflow.com/a/78225561/1460422
-- Adapted by @sbrl to:
-- - Print leading 0 behind escape codes as it should
-- - Also escape ' and #
-- TODO this doesn't work. It replaces \n with %A instead of %0A, though we don't know if that's a problem or not
-- it also doesn't handle quotes even though we've clearly got them in the Lua pattern
local function _escape_char(char)
return string.format('%%%02X', string.byte(char))
end
--- Escape the given string for use in a url.
-- In other words, like a space turns into %20.
-- Similar to Javascript's `encodeURIComponent()`.
-- @param string str The string to escape.
-- @returns string The escaped string.
local function escape(str)
return (string.gsub(str, "[^%a%d%-_%.!~%*%(%);/%?:@&=%+%$,]", _escape_char))
end
return escape

View file

@ -8,5 +8,6 @@ wea_c.format = {
node_distribution = dofile(wea_c.modpath.."/utils/format/node_distribution.lua"),
make_ascii_table = dofile(wea_c.modpath.."/utils/format/make_ascii_table.lua"),
map = dofile(wea_c.modpath.."/utils/format/map.lua"),
escape = dofile(wea_c.modpath.."/utils/format/escape.lua")
}

View file

@ -4,10 +4,10 @@ local wea_c = worldeditadditions_core
wea_c.settings = {}
-- Initialize wea world folder if not already existing
local path = minetest.get_worldpath() .. "/worldeditadditions"
local path = minetest.get_worldpath() .. wea_c.dirsep .. "worldeditadditions"
minetest.mkdir(path)
--- A wrapper to simultaniously handle global and world settings.
--- A wrapper to simultaneously handle global and world settings.
-- @namespace worldeditadditions_core.setting_handler
local setting_handler = {}

View file

@ -62,7 +62,7 @@ end
-- @param plain boolean If true (or truthy), pattern is interpreted as a
-- plain string, not a Lua pattern
-- @returns table A sequence table containing the substrings
local function split(str,dlm,plain)
local function split(str, dlm, plain)
if not dlm then dlm = "%s+" end
local pos, ret = 0, {}
local ins, i = str:find(dlm,pos,plain)