cleaner VFS abstraction
This commit is contained in:
		| @@ -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 | ||||
|   | ||||
| @@ -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 } | ||||
|   | ||||
							
								
								
									
										49
									
								
								src/main.lua
									
									
									
									
									
								
							
							
						
						
									
										49
									
								
								src/main.lua
									
									
									
									
									
								
							| @@ -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", | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user