395 lines
11 KiB
Lua
395 lines
11 KiB
Lua
-------------
|
|
-- lua sandboxed environment
|
|
|
|
-- function to cross out functions and userdata.
|
|
-- modified from dump()
|
|
function atlatc.remove_invalid_data(o, nested)
|
|
if o==nil then return nil end
|
|
local valid_dt={["nil"]=true, boolean=true, number=true, string=true}
|
|
if type(o) ~= "table" then
|
|
--check valid data type
|
|
if not valid_dt[type(o)] then
|
|
return nil
|
|
end
|
|
return o
|
|
end
|
|
-- Contains table -> true/nil of currently nested tables
|
|
nested = nested or {}
|
|
if nested[o] then
|
|
return nil
|
|
end
|
|
nested[o] = true
|
|
for k, v in pairs(o) do
|
|
v = atlatc.remove_invalid_data(v, nested)
|
|
end
|
|
nested[o] = nil
|
|
return o
|
|
end
|
|
|
|
|
|
local env_proto={
|
|
load = function(self, envname, data)
|
|
self.name=envname
|
|
self.sdata=data.sdata and atlatc.remove_invalid_data(data.sdata) or {}
|
|
self.fdata={}
|
|
self.init_code=data.init_code or ""
|
|
self.subscribers=data.subscribers or {}
|
|
end,
|
|
save = function(self)
|
|
-- throw any function values out of the sdata table
|
|
self.sdata = atlatc.remove_invalid_data(self.sdata)
|
|
return {sdata = self.sdata, init_code=self.init_code, subscribers=self.subscribers}
|
|
end,
|
|
}
|
|
|
|
--Environment
|
|
--Code modified from mesecons_luacontroller (credit goes to Jeija and mesecons contributors)
|
|
|
|
local safe_globals = {
|
|
"assert", "error", "ipairs", "next", "pairs", "select",
|
|
"tonumber", "tostring", "type", "unpack", "_VERSION"
|
|
}
|
|
|
|
local function safe_date(f, t)
|
|
if not f then
|
|
-- fall back to old behavior
|
|
return(os.date("*t",os.time()))
|
|
else
|
|
--pass parameters
|
|
return os.date(f,t)
|
|
end
|
|
end
|
|
|
|
-- string.rep(str, n) with a high value for n can be used to DoS
|
|
-- the server. Therefore, limit max. length of generated string.
|
|
local function safe_string_rep(str, n)
|
|
if #str * n > 2000 then
|
|
debug.sethook() -- Clear hook
|
|
error("string.rep: string length overflow", 2)
|
|
end
|
|
|
|
return string.rep(str, n)
|
|
end
|
|
|
|
-- string.find with a pattern can be used to DoS the server.
|
|
-- Therefore, limit string.find to patternless matching.
|
|
-- Note: Disabled security since there are enough security leaks and this would be unneccessary anyway to DoS the server
|
|
local function safe_string_find(...)
|
|
--if (select(4, ...)) ~= true then
|
|
-- debug.sethook() -- Clear hook
|
|
-- error("string.find: 'plain' (fourth parameter) must always be true for security reasons.")
|
|
--end
|
|
|
|
return string.find(...)
|
|
end
|
|
|
|
local mp=minetest.get_modpath("advtrains_luaautomation")
|
|
|
|
local static_env = {
|
|
--core LUA functions
|
|
string = {
|
|
byte = string.byte,
|
|
char = string.char,
|
|
format = string.format,
|
|
len = string.len,
|
|
lower = string.lower,
|
|
upper = string.upper,
|
|
rep = safe_string_rep,
|
|
reverse = string.reverse,
|
|
sub = string.sub,
|
|
find = safe_string_find,
|
|
},
|
|
math = {
|
|
abs = math.abs,
|
|
acos = math.acos,
|
|
asin = math.asin,
|
|
atan = math.atan,
|
|
atan2 = math.atan2,
|
|
ceil = math.ceil,
|
|
cos = math.cos,
|
|
cosh = math.cosh,
|
|
deg = math.deg,
|
|
exp = math.exp,
|
|
floor = math.floor,
|
|
fmod = math.fmod,
|
|
frexp = math.frexp,
|
|
huge = math.huge,
|
|
ldexp = math.ldexp,
|
|
log = math.log,
|
|
log10 = math.log10,
|
|
max = math.max,
|
|
min = math.min,
|
|
modf = math.modf,
|
|
pi = math.pi,
|
|
pow = math.pow,
|
|
rad = math.rad,
|
|
random = math.random,
|
|
sin = math.sin,
|
|
sinh = math.sinh,
|
|
sqrt = math.sqrt,
|
|
tan = math.tan,
|
|
tanh = math.tanh,
|
|
},
|
|
table = {
|
|
concat = table.concat,
|
|
insert = table.insert,
|
|
maxn = table.maxn,
|
|
remove = table.remove,
|
|
sort = table.sort,
|
|
},
|
|
os = {
|
|
clock = os.clock,
|
|
difftime = os.difftime,
|
|
time = os.time,
|
|
date = safe_date,
|
|
},
|
|
POS = function(x,y,z) return {x=x, y=y, z=z} end,
|
|
getstate = advtrains.getstate,
|
|
setstate = advtrains.setstate,
|
|
is_passive = advtrains.is_passive,
|
|
--interrupts are handled per node, position unknown. (same goes for digilines)
|
|
--however external interrupts can be set here.
|
|
interrupt_pos = function(parpos, imesg)
|
|
local pos=atlatc.pcnaming.resolve_pos(parpos, "interrupt_pos")
|
|
atlatc.interrupt.add(0, pos, {type="ext_int", ext_int=true, message=imesg})
|
|
end,
|
|
train_parts = function(train_id)
|
|
if not train_id then return false end
|
|
local train = advtrains.trains[train_id]
|
|
if not train then return false end
|
|
return table.copy(train.trainparts or {})
|
|
end,
|
|
-- sends an atc command to train regardless of where it is in the world
|
|
atc_send_to_train = function(train_id, command)
|
|
assertt(command, "string")
|
|
local train = advtrains.trains[train_id]
|
|
if train then
|
|
advtrains.atc.train_set_command(train, command, true)
|
|
return true
|
|
else
|
|
return false
|
|
end
|
|
end,
|
|
get_slowdown = function()
|
|
return advtrains.global_slowdown
|
|
end
|
|
}
|
|
|
|
-- If interlocking is present, enable route setting functions
|
|
if advtrains.interlocking then
|
|
local function gen_checks(signal, route_name, noroutesearch)
|
|
assertt(route_name, "string")
|
|
local pos = atlatc.pcnaming.resolve_pos(signal)
|
|
local sigd = advtrains.interlocking.db.get_sigd_for_signal(pos)
|
|
if not sigd then
|
|
error("There's no signal at "..minetest.pos_to_string(pos))
|
|
end
|
|
local tcbs = advtrains.interlocking.db.get_tcbs(sigd)
|
|
if not tcbs then
|
|
error("Inconsistent configuration, no tcbs for signal at "..minetest.pos_to_string(pos))
|
|
end
|
|
|
|
local routeid, route
|
|
if not noroutesearch then
|
|
for routeidt, routet in ipairs(tcbs.routes) do
|
|
if routet.name == route_name then
|
|
routeid = routeidt
|
|
route = routet
|
|
break
|
|
end
|
|
end
|
|
if not route then
|
|
error("No route called "..route_name.." at "..minetest.pos_to_string(pos))
|
|
end
|
|
end
|
|
return pos, sigd, tcbs, routeid, route
|
|
end
|
|
|
|
|
|
static_env.can_set_route = function(signal, route_name)
|
|
local pos, sigd, tcbs, routeid, route = gen_checks(signal, route_name)
|
|
-- if route is already set on signal, return whether it's committed
|
|
if tcbs.routeset == routeid then
|
|
return tcbs.route_committed
|
|
end
|
|
-- actually try setting route (parameter 'true' designates try-run
|
|
local ok = advtrains.interlocking.route.set_route(sigd, route, true)
|
|
return ok
|
|
end
|
|
static_env.set_route = function(signal, route_name)
|
|
local pos, sigd, tcbs, routeid, route = gen_checks(signal, route_name)
|
|
return advtrains.interlocking.route.update_route(sigd, tcbs, routeid)
|
|
end
|
|
static_env.cancel_route = function(signal)
|
|
local pos, sigd, tcbs, routeid, route = gen_checks(signal, "", true)
|
|
return advtrains.interlocking.route.update_route(sigd, tcbs, nil, true)
|
|
end
|
|
static_env.get_aspect = function(signal)
|
|
local pos = atlatc.pcnaming.resolve_pos(signal)
|
|
return advtrains.interlocking.signal_get_aspect(pos)
|
|
end
|
|
static_env.set_aspect = function(signal, asp)
|
|
local pos = atlatc.pcnaming.resolve_pos(signal)
|
|
return advtrains.interlocking.signal_set_aspect(pos,asp)
|
|
end
|
|
|
|
--section_occupancy()
|
|
static_env.section_occupancy = function(ts_id)
|
|
if not ts_id then return nil end
|
|
ts_id = tostring(ts_id)
|
|
local response = advtrains.interlocking.db.get_ts(ts_id)
|
|
if not response then return false end
|
|
return (response.trains and table.copy(response.trains)) or {}
|
|
end
|
|
end
|
|
|
|
-- Lines-specific:
|
|
if advtrains.lines then
|
|
local atlrwt = advtrains.lines.rwt
|
|
static_env.rwt = {
|
|
now = atlrwt.now,
|
|
new = atlrwt.new,
|
|
copy = atlrwt.copy,
|
|
to_table = atlrwt.to_table,
|
|
to_secs = atlrwt.to_secs,
|
|
to_string = atlrwt.to_string,
|
|
add = atlrwt.add,
|
|
diff = atlrwt.diff,
|
|
sub = atlrwt.sub,
|
|
adj_diff = atlrwt.adj_diff,
|
|
adjust_cycle = atlrwt.adjust_cycle,
|
|
adjust = atlrwt.adjust,
|
|
to_string = atlrwt.to_string,
|
|
get_time_until = atlrwt.get_time_until,
|
|
next_rpt = atlrwt.next_rpt,
|
|
last_rpt = atlrwt.last_rpt,
|
|
time_from_last_rpt = atlrwt.time_from_last_rpt,
|
|
time_to_next_rpt = atlrwt.time_to_next_rpt,
|
|
}
|
|
end
|
|
|
|
|
|
atlatc.register_function = function (name, f)
|
|
static_env[name] = f
|
|
end
|
|
|
|
for _, name in pairs(safe_globals) do
|
|
static_env[name] = _G[name]
|
|
end
|
|
|
|
--The environment all code calls get is a table that has set static_env as metatable.
|
|
--In general, every variable is local to a single code chunk, but kept persistent over code re-runs. Data is also saved, but functions and userdata and circular references are removed
|
|
--Init code and step code's environments are not saved
|
|
-- S - Table that can contain any save data global to the environment. Will be saved statically. Can't contain functions or userdata or circular references.
|
|
-- F - Table global to the environment, can contain volatile data that is deleted when server quits.
|
|
-- The init code should populate this table with functions and other definitions.
|
|
|
|
local proxy_env={}
|
|
--proxy_env gets a new metatable in every run, but is the shared environment of all functions ever defined.
|
|
|
|
-- returns: true, fenv if successful; nil, error if error
|
|
function env_proto:execute_code(localenv, code, evtdata, customfct)
|
|
-- create us a print function specific for this environment
|
|
if not self.safe_print_func then
|
|
local myenv = self
|
|
self.safe_print_func = function(...)
|
|
myenv:log("info", ...)
|
|
end
|
|
end
|
|
|
|
local metatbl ={
|
|
__index = function(t, i)
|
|
if i=="S" then
|
|
return self.sdata
|
|
elseif i=="F" then
|
|
return self.fdata
|
|
elseif i=="event" then
|
|
return evtdata
|
|
elseif customfct and customfct[i] then
|
|
return customfct[i]
|
|
elseif localenv and localenv[i] then
|
|
return localenv[i]
|
|
elseif i=="print" then
|
|
return self.safe_print_func
|
|
end
|
|
return static_env[i]
|
|
end,
|
|
__newindex = function(t, i, v)
|
|
if i=="S" or i=="F" or i=="event" or (customfct and customfct[i]) or static_env[i] then
|
|
debug.sethook()
|
|
error("Trying to overwrite environment contents")
|
|
end
|
|
localenv[i]=v
|
|
end,
|
|
}
|
|
setmetatable(proxy_env, metatbl)
|
|
local fun, err=loadstring(code)
|
|
if not fun then
|
|
return false, err
|
|
end
|
|
|
|
setfenv(fun, proxy_env)
|
|
local succ, data = pcall(fun)
|
|
if succ then
|
|
data=localenv
|
|
end
|
|
return succ, data
|
|
end
|
|
|
|
function env_proto:run_initcode()
|
|
if self.init_code and self.init_code~="" then
|
|
local old_fdata=self.fdata
|
|
self.fdata = {}
|
|
--atprint("[atlatc]Running initialization code for environment '"..self.name.."'")
|
|
local succ, err = self:execute_code({}, self.init_code, {type="init", init=true})
|
|
if not succ then
|
|
self:log("error", "Executing InitCode for '"..self.name.."' failed:"..err)
|
|
self.init_err=err
|
|
if old_fdata then
|
|
self.fdata=old_fdata
|
|
self:log("warning", "The 'F' table has been restored to the previous state.")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- log to environment subscribers. severity can be "error", "warning" or "info" (used by internal print)
|
|
function env_proto:log(severity, ...)
|
|
local text=advtrains.print_concat_table({"[atlatc "..self.name.." "..severity.."]", ...})
|
|
minetest.log("action", text)
|
|
for _, pname in ipairs(self.subscribers) do
|
|
minetest.chat_send_player(pname, text)
|
|
end
|
|
end
|
|
|
|
-- env.subscribers table may be directly altered by callers.
|
|
|
|
|
|
--- class interface
|
|
|
|
function atlatc.env_new(name)
|
|
local newenv={
|
|
name=name,
|
|
init_code="",
|
|
sdata={},
|
|
subscribers={},
|
|
}
|
|
setmetatable(newenv, {__index=env_proto})
|
|
return newenv
|
|
end
|
|
function atlatc.env_load(name, data)
|
|
local newenv={}
|
|
setmetatable(newenv, {__index=env_proto})
|
|
newenv:load(name, data)
|
|
return newenv
|
|
end
|
|
|
|
function atlatc.run_initcode()
|
|
for envname, env in pairs(atlatc.envs) do
|
|
env:run_initcode()
|
|
end
|
|
end
|
|
|
|
|
|
|
|
|