cleaner VFS abstraction

This commit is contained in:
osmarks 2024-09-03 12:00:47 +01:00
parent 4cbe8f81d3
commit 288ef5f03a
5 changed files with 163 additions and 123 deletions

File diff suppressed because one or more lines are too long

View File

@ -13,6 +13,11 @@ end
function sandboxlib.dispatch_if_restricted(rkey, original, restricted)
local out = {}
for k, v in pairs(restricted) do
if not original[k] then
out[k] = v
end
end
for k, v in pairs(original) do
out[k] = function(...)
if processrestriction(rkey) then

View File

@ -77,31 +77,10 @@ local function add_to_table(t1, t2)
end
end
local fscombine, fsgetname, fsgetdir = fs.combine, fs.getName, fs.getDir
-- 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, "/"
return fscombine(path, "")
end
-- Escapes lua patterns in a string. Should not be needed, but lua is stupid so the only string.replace thing is gsub
@ -114,19 +93,12 @@ 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)
table.insert(segs, 1, fsgetname(rest))
rest = fsgetdir(rest)
until rest == ""
return segs
end
@ -134,7 +106,7 @@ end
local function combine(segs)
local out = ""
for _, p in pairs(segs) do
out = fs.combine(out, p)
out = fscombine(out, p)
end
return out
end
@ -179,98 +151,154 @@ 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 fs = fs
local mappings = make_mappings(root)
local vfstree = {
mount = "potatOS",
children = {
["disk"] = { mount = "disk" },
["rom"] = { mount = "rom" },
--["virtual_test"] = { virtual = "bees" }
}
-- make virtual filesystem from files (no nested directories for simplicity)
local function vfs_from_files(files)
return {
list = function(path)
if path ~= "" then return {} end
local out = {}
for k, v in pairs(files) do
table.insert(out, k)
end
return out
end,
open = function(path, mode)
return make_handle(files[path])
end,
exists = function(path)
return files[path] ~= nil or path == ""
end,
isReadOnly = function(path)
return true
end,
isDir = function(path)
if path == "" then return true end
return false
end,
getDrive = function(_) return "memory" end,
getSize = function(path)
return #files[path]
end,
getFreeSpace = function() return 0 end,
makeDir = function() end,
delete = function() end,
move = function() end,
copy = function() end,
attributes = function(path)
return {
size = #files[path],
modification = os.epoch "utc"
}
end
}
end
local function create_FS(vfstree)
local fs = fs
local function is_usable_node(node)
return node.mount or node.vfs
end
local function resolve(sandbox_path)
local segs = segments(sandbox_path)
local current_tree = vfstree
local last_usable_node, last_segs = nil, nil
while true do
if is_usable_node(current_tree) then
last_usable_node = current_tree
last_segs = copy(segs)
end
local seg = segs[1]
if current_tree.children and current_tree.children[seg] then
if seg and current_tree.children and current_tree.children[seg] then
table.remove(segs, 1)
current_tree = current_tree.children[seg]
else break end
end
return last_usable_node, last_segs
end
local new_overlay = {}
for k, v in pairs(overlay) do
new_overlay[canonicalize(k)] = v
local function resolve_path(sandbox_path)
local node, segs = resolve(sandbox_path)
if node.mount then return fs, fscombine(node.mount, combine(segs)) end
return node.vfs, combine(segs)
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))
return function(path)
local vfs, path = resolve_path(path)
return vfs[n](path)
end
end
local new = copy_some_keys {"getDir", "getName", "combine", "complete"} (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)
local vfs, path = resolve_path(path)
return vfs.open(path, 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)
function new.move(src, dest)
local src_vfs, src_path = resolve_path(src)
local dest_vfs, dest_path = resolve_path(dest)
if src_vfs == dest_vfs then
return src_vfs.move(src_path, dest_path)
else
for _, v in pairs(ocontents) do
table.insert(contents, v)
end
return contents
if src_vfs.isReadOnly(src_path) then error "Access denied" end
new.copy(src, dest)
src_vfs.delete(src_path)
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.copy(src, dest)
local src_vfs, src_path = resolve_path(src)
local dest_vfs, dest_path = resolve_path(dest)
if src_vfs == dest_vfs then
return src_vfs.copy(src_path, dest_path)
else
if src_vfs.isDir(src_path) then
dest_vfs.makeDir(dest_path)
for _, f in pairs(src_vfs.list(src_path)) do
new.copy(fscombine(src, f), fscombine(dest, f))
end
else
local dest_fh = dest_vfs.open(dest_path, "wb")
local src_fh = src_vfs.open(src_path, "rb")
dest_fh.write(src_fh.readAll())
src_fh.close()
dest_fh.close()
end
end
end
function new.mountVFS(path, vfs)
local path = canonicalize(path)
local node, relpath = resolve(path)
while #relpath > 0 do
local seg = table.remove(relpath, 1)
if not node.children then node.children = {} end
if not node.children[seg] then node.children[seg] = {} end
node = node.children[seg]
end
node.vfs = vfs
end
function new.unmountVFS(path)
local node, relpath = resolve(path)
if #relpath == 0 then
node.vfs = nil
else
error "Not a mountpoint"
end
end
add_to_table(new, map(lift_to_sandbox, copy_some_keys {"isDir", "getDrive", "getSize", "getFreeSpace", "makeDir", "delete", "isDriveRoot", "exists", "isReadOnly", "list", "attributes"} (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
@ -310,7 +338,7 @@ local function create_FS(root, overlay)
to_add.c = new.dump(path)
to_add.t = "d"
else
local fh = new.open(path, "r")
local fh = new.open(path, "rb")
to_add.c = fh.readAll()
fh.close()
end
@ -327,7 +355,7 @@ local function create_FS(root, overlay)
new.makeDir(path)
new.load(f.c, path)
else
local fh = new.open(path, "w")
local fh = new.open(path, "wb")
fh.write(f.c)
fh.close()
end
@ -465,4 +493,4 @@ local function run(API_overrides, init, logger)
end
end
return { run = run, create_FS = create_FS }
return { run = run, create_FS = create_FS, vfs_from_files = vfs_from_files }

View File

@ -1297,26 +1297,35 @@ local function run_with_sandbox()
end or v
end
-- Provide many, many useful or not useful programs to the potatOS shell.
local FS_overlay = {
["secret/.pkey"] = fproxy "signing-key.tbl",
["secret/log"] = function() return potatOS_proxy.get_log() end,
-- The API backing this no longer exists due to excessive server load.
-----["/rom/programs/dwarf.lua"] = "print(potatOS.dwarf())",
["/secret/processes"] = function()
return tostring(process.list())
end,
["/rom/heavlisp_lib/stdlib.hvl"] = fproxy "stdlib.hvl"
local yafss = require "yafss"
local vfstree = {
mount = "potatOS",
children = {
["rom"] = {
mount = "rom",
children = {
["potatOS_xlib"] = { mount = "/xlib" },
programs = {
children = {
["potatOS"] = { mount = "/bin" }
}
},
["autorun"] = {
vfs = yafss.vfs_from_files {
["fix_path.lua"] = [[shell.setPath("/rom/programs/potatOS:"..shell.path())]],
}
},
["heavlisp_lib"] = {
vfs = yafss.vfs_from_files {
["stdlib.hvl"] = fproxy "stdlib.hvl"
}
}
}
}
}
}
for _, file in pairs(fs.list "bin") do
FS_overlay[fs.combine("rom/programs", file)] = fproxy(fs.combine("bin", file))
end
for _, file in pairs(fs.list "xlib") do
FS_overlay[fs.combine("rom/potato_xlib", file)] = fproxy(fs.combine("xlib", file))
end
local API_overrides = {
process = process,
json = json,
@ -1400,11 +1409,9 @@ local function run_with_sandbox()
require "metatable_improvements"(potatOS_proxy.add_log, potatOS_proxy.report_incident)
local yafss = require "yafss"
local fss_sentinel = sandboxlib.create_sentinel "fs-sandbox"
local debug_sentinel = sandboxlib.create_sentinel "constrained-debug"
local sandbox_filesystem = yafss.create_FS("potatOS", FS_overlay)
local sandbox_filesystem = yafss.create_FS(vfstree)
_G.fs = sandboxlib.dispatch_if_restricted(fss_sentinel, _G.fs, sandbox_filesystem)
_G.debug = sandboxlib.allow_whitelisted(debug_sentinel, _G.debug, {
"traceback",

View File

@ -573,7 +573,7 @@ local function boot_require(package)
return pkg
end
local npackage = package:gsub("%.", "/")
for _, search_path in next, {"/", "lib", "rom/modules/main", "rom/modules/turtle", "rom/modules/command", "rom/potato_xlib"} do
for _, search_path in next, {"/", "lib", "rom/modules/main", "rom/modules/turtle", "rom/modules/command", "rom/potatOS_xlib"} do
local path = try_paths(search_path, {npackage, npackage .. ".lua"})
if path then
local ok, res = pcall(dofile, path)
@ -589,7 +589,7 @@ _G.require = boot_require
_ENV.require = boot_require
local libs = {}
for _, f in pairs(fs.list "rom/potato_xlib") do
for _, f in pairs(fs.list "rom/potatOS_xlib") do
table.insert(libs, f)
end
table.sort(libs)
@ -1759,7 +1759,7 @@ if potatOS.registry.get "potatOS.immutable_global_scope" then
end
process.spawn(keyboard_shortcuts, "kbsd")
if http.websocket then process.spawn(skynet.listen, "skynetd") process.spawn(potatoNET, "systemd-potatod") end
if skynet and http.websocket then process.spawn(skynet.listen, "skynetd") process.spawn(potatoNET, "systemd-potatod") end
local autorun = potatOS.registry.get "potatOS.autorun"
if type(autorun) == "string" then
autorun = load(autorun)
@ -1781,7 +1781,7 @@ local function run_shell()
term.clear()
term.setCursorPos(1, 1)
potatOS.add_log "starting user shell"
os.run( {}, sShell )
os.run({}, sShell )
end
if potatOS.registry.get "potatOS.extended_monitoring" then process.spawn(excessive_monitoring, "extended_monitoring") end