-- Deep-copy a table local function copy(tabl) local new = {} for k, v in pairs(tabl) do if type(v) == "table" and tabl ~= v then new[k] = copy(v) else new[k] = v end end return new end -- Deep-map all values in a table local function deepmap(table, f, path) local path = path or "" local new = {} for k, v in pairs(table) do local thisp = path .. "." .. k if type(v) == "table" and v ~= table then -- bodge it to not stackoverflow new[k] = deepmap(v, f, thisp) else new[k] = f(v, k, thisp) end end return new end -- Takes a list of keys to copy, returns a function which takes a table and copies the given keys to a new table local function copy_some_keys(keys) return function(from) local new = {} for _, key_to_copy in pairs(keys) do local x = from[key_to_copy] if type(x) == "table" then x = copy(x) end new[key_to_copy] = x end return new end end -- Simple string operations local function starts_with(s, with) return string.sub(s, 1, #with) == with end local function ends_with(s, with) return string.sub(s, -#with, -1) == with end local function contains(s, subs) return string.find(s, subs) ~= nil end -- Maps function f over table t. f is passed the value and key and can return a new value and key. local function map(f, t) local mapper = function(t) local new = {} for k, v in pairs(t) do local new_v, new_k = f(v, k) new[new_k or k] = new_v end return new end if t then return mapper(t) else return mapper end end -- Copies stuff from t2 into t1 local function add_to_table(t1, t2) for k, v in pairs(t2) do if type(v) == "table" and v ~= t2 and v ~= t1 then if not t1[k] then t1[k] = {} end add_to_table(t1[k], v) else t1[k] = v end end end -- Convert path to canonical form local function canonicalize(path) return fs.combine(path, "") end -- Checks whether a path is in a directory local function path_in(p, dir) return starts_with(canonicalize(p), canonicalize(dir)) end local function make_mappings(root) return { ["/disk"] = "/disk", ["/rom"] = "/rom", default = root } end local function get_root(path, mappings) for mapfrom, mapto in pairs(mappings) do if path_in(path, mapfrom) then return mapto, mapfrom end end return mappings.default, "/" end -- Escapes lua patterns in a string. Should not be needed, but lua is stupid so the only string.replace thing is gsub local quotepattern = '(['..("%^$().[]*+-?"):gsub("(.)", "%%%1")..'])' local function escape(str) return str:gsub(quotepattern, "%%%1") end local function strip(p, root) return p:gsub("^" .. escape(canonicalize(root)), "") end local function resolve_path(path, mappings) local root, to_strip = get_root(path, mappings) local newpath = strip(fs.combine(root, path), to_strip) if path_in(newpath, root) then return newpath end return resolve_path(newpath, mappings) end local function segments(path) local segs, rest = {}, canonicalize(path) if rest == "" then return {} end -- otherwise we'd get "root" and ".." for some broken reason repeat table.insert(segs, 1, fs.getName(rest)) rest = fs.getDir(rest) until rest == "" return segs end local function combine(segs) local out = "" for _, p in pairs(segs) do out = fs.combine(out, p) end return out end -- magic from http://lua-users.org/wiki/SplitJoin -- split string into lines local function lines(str) local t = {} local function helper(line) table.insert(t, line) return "" end helper((str:gsub("(.-)\r?\n", helper))) return t end -- Fetch the contents of URL "u" local function fetch(u) local h = http.get(u) local c = h.readAll() h.close() return c end -- Make a read handle for a string local function make_handle(text) local lines = lines(text) local h = {line = 0} function h.close() end function h.readLine() h.line = h.line + 1 return lines[h.line] end function h.readAll() return text end return h end -- Get a path from a filesystem overlay local function path_in_overlay(overlay, path) return overlay[canonicalize(path)] end local this_level_env = _G -- Create a modified FS table which confines you to root and has some extra read-only pseudofiles. local function create_FS(root, overlay) local mappings = make_mappings(root) local vfstree = { mount = "potatOS", children = { ["disk"] = { mount = "disk" }, ["rom"] = { mount = "rom" }, --["virtual_test"] = { virtual = "bees" } } } local function resolve(sandbox_path) local segs = segments(sandbox_path) local current_tree = vfstree while true do local seg = segs[1] if current_tree.children and current_tree.children[seg] then table.remove(segs, 1) current_tree = current_tree.children[seg] else break end end end local new_overlay = {} for k, v in pairs(overlay) do new_overlay[canonicalize(k)] = v end local function lift_to_sandbox(f, n) return function(...) local args = map(function(x) return resolve_path(x, mappings) end, {...}) return f(table.unpack(args)) end end local new = copy_some_keys {"getDir", "getName", "combine"} (fs) function new.isReadOnly(path) return path_in_overlay(new_overlay, path) or starts_with(canonicalize(path), "rom") end function new.open(path, mode) if (contains(mode, "w") or contains(mode, "a")) and new.isReadOnly(path) then error "Access denied" else local overlay_data = path_in_overlay(new_overlay, path) if overlay_data then if type(overlay_data) == "function" then overlay_data = overlay_data(this_level_env) end return make_handle(overlay_data), "YAFSS overlay" end return fs.open(resolve_path(path, mappings), mode) end end function new.exists(path) if path_in_overlay(new_overlay, path) ~= nil then return true end return fs.exists(resolve_path(path, mappings)) end function new.overlay() return map(function(x) if type(x) == "function" then return x(this_level_env) else return x end end, new_overlay) end function new.list(dir) local sdir = canonicalize(resolve_path(dir, mappings)) local ocontents = {} for opath in pairs(new_overlay) do if fs.getDir(opath) == sdir then table.insert(ocontents, fs.getName(opath)) end end local ok, contents = pcall(fs.list, sdir) -- in case of error (nonexistent dir, probably) return overlay contents -- very awful temporary hack until I can get a nicer treeized VFS done if not ok then if #ocontents > 0 then return ocontents end error(contents) else for _, v in pairs(ocontents) do table.insert(contents, v) end return contents end end add_to_table(new, map(lift_to_sandbox, copy_some_keys {"isDir", "getDrive", "getSize", "getFreeSpace", "makeDir", "move", "copy", "delete", "isDriveRoot"} (fs))) function new.find(wildcard) local function recurse_spec(results, path, spec) -- From here: https://github.com/Sorroko/cclite/blob/62677542ed63bd4db212f83da1357cb953e82ce3/src/emulator/native_api.lua local segment = spec:match('([^/]*)'):gsub('/', '') local pattern = '^' .. segment:gsub('[*]', '.+'):gsub('?', '.'):gsub("-", "%%-") .. '$' if new.isDir(path) then for _, file in ipairs(new.list(path)) do if file:match(pattern) then local f = new.combine(path, file) if new.isDir(f) then recurse_spec(results, f, spec:sub(#segment + 2)) end if spec == segment then table.insert(results, f) end end end end end local results = {} recurse_spec(results, '', wildcard) return results end function new.dump(dir) local dir = dir or "/" local out = {} for _, f in pairs(new.list(dir)) do local path = fs.combine(dir, f) local to_add = { n = f, t = "f" } if new.isDir(path) then to_add.c = new.dump(path) to_add.t = "d" else local fh = new.open(path, "r") to_add.c = fh.readAll() fh.close() end table.insert(out, to_add) end return out end function new.load(dump, root) local root = root or "/" for _, f in pairs(dump) do local path = fs.combine(root, f.n) if f.t == "d" then new.makeDir(path) new.load(f.c, path) else local fh = new.open(path, "w") fh.write(f.c) fh.close() end end end return new end local allowed_APIs = { "term", "http", "pairs", "ipairs", -- getfenv, getfenv are modified to prevent sandbox escapes and defined in make_environment "peripheral", "table", "string", "type", "setmetatable", "getmetatable", "os", "sleep", "pcall", "xpcall", "select", "tostring", "tonumber", "coroutine", "next", "error", "math", "redstone", "rs", "assert", "unpack", "bit", "bit32", "turtle", "pocket", "ccemux", "config", "commands", "rawget", "rawset", "rawequal", "~expect", "__inext", "periphemu", } local gf, sf = getfenv, setfenv -- Takes the root directory to allow access to, -- a map of paths to either strings containing their contents or functions returning them -- and a table of extra APIs and partial overrides for existing APIs local function make_environment(root_directory, overlay, API_overrides) local environment = copy_some_keys(allowed_APIs)(_G) environment.fs = create_FS(root_directory, overlay) -- if function is not from within the VM, return env from within sandbox function environment.getfenv(arg) local env if type(arg) == "number" then return gf() end if not env or type(env._HOST) ~= "string" or not string.match(env._HOST, "YAFSS") then return gf() else return env end end --[[ Fix PS#AD2A532C Allowing `setfenv` to operate on any function meant that privileged code could in some cases be manipulated to leak information or operate undesirably. Due to this, we restrict it, similarly to getfenv. ]] function environment.setfenv(fn, env) local nenv = gf(fn) if not nenv or type(nenv._HOST) ~= "string" or not string.match(nenv._HOST, "YAFSS") then return false end return sf(fn, env) end function environment.load(code, file, mode, env) return load(code, file or "@", mode or "t", env or environment) end if debug then environment.debug = copy_some_keys { "getmetatable", "setmetatable", "traceback", "getinfo", "getregistry" }(debug) end environment._G = environment environment._ENV = environment environment._HOST = string.format("YAFSS on %s", _HOST) function environment.os.shutdown() os.queueEvent("power_state", "shutdown") while true do coroutine.yield() end end function environment.os.reboot() os.queueEvent("power_state", "reboot") while true do coroutine.yield() end end add_to_table(environment, copy(API_overrides)) return environment end local function run(root_directory, overlay, API_overrides, init) if type(init) == "table" and init.URL then init = fetch(init.URL) end init = init or fetch "https://pastebin.com/raw/wKdMTPwQ" local running = true while running do parallel.waitForAny(function() local env = make_environment(root_directory, overlay, API_overrides) env.init_code = init local out, err = load(init, "@[init]", "t", env) if not out then error(err) end local ok, err = pcall(out) if not ok then printError(err) end end, function() while true do local event, state = coroutine.yield "power_state" if event == "power_state" then -- coroutine.yield behaves weirdly with terminate if process then local this_process = process.running.ID for _, p in pairs(process.list()) do if p.parent and p.parent.ID == this_process then process.signal(p.ID, process.signals.KILL) end end end if state == "shutdown" then running = false return elseif state == "reboot" then return end end end end) end end return run