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) function sandboxlib.dispatch_if_restricted(rkey, original, restricted)
local out = {} 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 for k, v in pairs(original) do
out[k] = function(...) out[k] = function(...)
if processrestriction(rkey) then if processrestriction(rkey) then

View File

@ -77,31 +77,10 @@ local function add_to_table(t1, t2)
end end
end end
local fscombine, fsgetname, fsgetdir = fs.combine, fs.getName, fs.getDir
-- Convert path to canonical form -- Convert path to canonical form
local function canonicalize(path) local function canonicalize(path)
return fs.combine(path, "") return fscombine(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 end
-- Escapes lua patterns in a string. Should not be needed, but lua is stupid so the only string.replace thing is gsub -- 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)), "") return p:gsub("^" .. escape(canonicalize(root)), "")
end 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 function segments(path)
local segs, rest = {}, canonicalize(path) local segs, rest = {}, canonicalize(path)
if rest == "" then return {} end -- otherwise we'd get "root" and ".." for some broken reason if rest == "" then return {} end -- otherwise we'd get "root" and ".." for some broken reason
repeat repeat
table.insert(segs, 1, fs.getName(rest)) table.insert(segs, 1, fsgetname(rest))
rest = fs.getDir(rest) rest = fsgetdir(rest)
until rest == "" until rest == ""
return segs return segs
end end
@ -134,7 +106,7 @@ end
local function combine(segs) local function combine(segs)
local out = "" local out = ""
for _, p in pairs(segs) do for _, p in pairs(segs) do
out = fs.combine(out, p) out = fscombine(out, p)
end end
return out return out
end end
@ -179,98 +151,154 @@ end
local this_level_env = _G local this_level_env = _G
-- Create a modified FS table which confines you to root and has some extra read-only pseudofiles. -- make virtual filesystem from files (no nested directories for simplicity)
local function create_FS(root, overlay) local function vfs_from_files(files)
local fs = fs return {
local mappings = make_mappings(root) 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 vfstree = { local function create_FS(vfstree)
mount = "potatOS", local fs = fs
children = {
["disk"] = { mount = "disk" }, local function is_usable_node(node)
["rom"] = { mount = "rom" }, return node.mount or node.vfs
--["virtual_test"] = { virtual = "bees" } end
}
}
local function resolve(sandbox_path) local function resolve(sandbox_path)
local segs = segments(sandbox_path) local segs = segments(sandbox_path)
local current_tree = vfstree local current_tree = vfstree
local last_usable_node, last_segs = nil, nil
while true do while true do
if is_usable_node(current_tree) then
last_usable_node = current_tree
last_segs = copy(segs)
end
local seg = segs[1] 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) table.remove(segs, 1)
current_tree = current_tree.children[seg] current_tree = current_tree.children[seg]
else break end else break end
end end
return last_usable_node, last_segs
end end
local new_overlay = {} local function resolve_path(sandbox_path)
for k, v in pairs(overlay) do local node, segs = resolve(sandbox_path)
new_overlay[canonicalize(k)] = v if node.mount then return fs, fscombine(node.mount, combine(segs)) end
return node.vfs, combine(segs)
end end
local function lift_to_sandbox(f, n) local function lift_to_sandbox(f, n)
return function(...) return function(path)
local args = map(function(x) return resolve_path(x, mappings) end, {...}) local vfs, path = resolve_path(path)
return f(table.unpack(args)) return vfs[n](path)
end end
end end
local new = copy_some_keys {"getDir", "getName", "combine", "complete"} (fs) 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) function new.open(path, mode)
if (contains(mode, "w") or contains(mode, "a")) and new.isReadOnly(path) then if (contains(mode, "w") or contains(mode, "a")) and new.isReadOnly(path) then
error "Access denied" error "Access denied"
else else
local overlay_data = path_in_overlay(new_overlay, path) local vfs, path = resolve_path(path)
if overlay_data then return vfs.open(path, mode)
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
end end
function new.exists(path) function new.move(src, dest)
if path_in_overlay(new_overlay, path) ~= nil then return true end local src_vfs, src_path = resolve_path(src)
return fs.exists(resolve_path(path, mappings)) local dest_vfs, dest_path = resolve_path(dest)
end if src_vfs == dest_vfs then
return src_vfs.move(src_path, dest_path)
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 else
for _, v in pairs(ocontents) do if src_vfs.isReadOnly(src_path) then error "Access denied" end
table.insert(contents, v) new.copy(src, dest)
end src_vfs.delete(src_path)
return contents
end end
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) 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 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.c = new.dump(path)
to_add.t = "d" to_add.t = "d"
else else
local fh = new.open(path, "r") local fh = new.open(path, "rb")
to_add.c = fh.readAll() to_add.c = fh.readAll()
fh.close() fh.close()
end end
@ -327,7 +355,7 @@ local function create_FS(root, overlay)
new.makeDir(path) new.makeDir(path)
new.load(f.c, path) new.load(f.c, path)
else else
local fh = new.open(path, "w") local fh = new.open(path, "wb")
fh.write(f.c) fh.write(f.c)
fh.close() fh.close()
end end
@ -465,4 +493,4 @@ local function run(API_overrides, init, logger)
end end
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,25 +1297,34 @@ local function run_with_sandbox()
end or v end or v
end end
-- Provide many, many useful or not useful programs to the potatOS shell. local yafss = require "yafss"
local FS_overlay = {
["secret/.pkey"] = fproxy "signing-key.tbl", local vfstree = {
["secret/log"] = function() return potatOS_proxy.get_log() end, mount = "potatOS",
-- The API backing this no longer exists due to excessive server load. children = {
-----["/rom/programs/dwarf.lua"] = "print(potatOS.dwarf())", ["rom"] = {
["/secret/processes"] = function() mount = "rom",
return tostring(process.list()) children = {
end, ["potatOS_xlib"] = { mount = "/xlib" },
["/rom/heavlisp_lib/stdlib.hvl"] = fproxy "stdlib.hvl" 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 = { local API_overrides = {
process = process, process = process,
@ -1400,11 +1409,9 @@ local function run_with_sandbox()
require "metatable_improvements"(potatOS_proxy.add_log, potatOS_proxy.report_incident) require "metatable_improvements"(potatOS_proxy.add_log, potatOS_proxy.report_incident)
local yafss = require "yafss"
local fss_sentinel = sandboxlib.create_sentinel "fs-sandbox" local fss_sentinel = sandboxlib.create_sentinel "fs-sandbox"
local debug_sentinel = sandboxlib.create_sentinel "constrained-debug" 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.fs = sandboxlib.dispatch_if_restricted(fss_sentinel, _G.fs, sandbox_filesystem)
_G.debug = sandboxlib.allow_whitelisted(debug_sentinel, _G.debug, { _G.debug = sandboxlib.allow_whitelisted(debug_sentinel, _G.debug, {
"traceback", "traceback",

View File

@ -573,7 +573,7 @@ local function boot_require(package)
return pkg return pkg
end end
local npackage = package:gsub("%.", "/") 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"}) local path = try_paths(search_path, {npackage, npackage .. ".lua"})
if path then if path then
local ok, res = pcall(dofile, path) local ok, res = pcall(dofile, path)
@ -589,7 +589,7 @@ _G.require = boot_require
_ENV.require = boot_require _ENV.require = boot_require
local libs = {} 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) table.insert(libs, f)
end end
table.sort(libs) table.sort(libs)
@ -1759,7 +1759,7 @@ if potatOS.registry.get "potatOS.immutable_global_scope" then
end end
process.spawn(keyboard_shortcuts, "kbsd") 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" local autorun = potatOS.registry.get "potatOS.autorun"
if type(autorun) == "string" then if type(autorun) == "string" then
autorun = load(autorun) autorun = load(autorun)