potatOS/src/lib/yafss.lua

479 lines
12 KiB
Lua

-- 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 "@<input>", 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