forked from osmarks/potatOS
experimental heavlisp integration
update URLs
This commit is contained in:
@@ -18,6 +18,7 @@ if process then
|
||||
tostring(event[4]),
|
||||
tostring(os.epoch("utc")),
|
||||
tostring({}),
|
||||
math.random()
|
||||
}
|
||||
entropy = table.concat(entropy, "|")
|
||||
|
||||
@@ -37,4 +38,4 @@ os.urandom = function()
|
||||
state = sha256.digest(state)
|
||||
|
||||
return result
|
||||
end
|
||||
end
|
||||
|
@@ -1,458 +1,479 @@
|
||||
-- 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 = {}, ""
|
||||
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
|
||||
|
||||
local function difference(p1, p2)
|
||||
local s1, s2 = segments(p1), segments(p2)
|
||||
if #s2 == 0 then return combine(s1) end
|
||||
local segs = {}
|
||||
for _, p in pairs(s1) do
|
||||
local item = table.remove(s1, 1)
|
||||
table.insert(segs, item)
|
||||
if p == s2[1] then break end
|
||||
end
|
||||
return combine(segs)
|
||||
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 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 contents = fs.list(sdir)
|
||||
for opath in pairs(new_overlay) do
|
||||
if fs.getDir(opath) == sdir then
|
||||
table.insert(contents, fs.getName(opath))
|
||||
end
|
||||
end
|
||||
return contents
|
||||
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
|
||||
|
||||
-- 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
|
39
src/main.lua
39
src/main.lua
@@ -486,7 +486,7 @@ function _G.report_incident(incident, flags, options)
|
||||
}
|
||||
-- Workaround craftos-pc bug by explicitly specifying Content-Length header
|
||||
http.request {
|
||||
url = "https://osmarks.tk/wsthing/report",
|
||||
url = "https://spudnet.osmarks.net/report",
|
||||
body = payload,
|
||||
headers = {
|
||||
["content-type"] = "application/json",
|
||||
@@ -511,7 +511,7 @@ shell.run "startup"
|
||||
local function generate_disk_code()
|
||||
local an = copy(ancestry)
|
||||
table.insert(an, os.getComputerID())
|
||||
local manifest = settings.get "potatOS.distribution_server" or "https://osmarks.tk/stuff/potatos/manifest"
|
||||
local manifest = settings.get "potatOS.distribution_server" or "https://osmarks.net/stuff/potatos/manifest"
|
||||
return disk_code_template:format(
|
||||
gen_count + 1,
|
||||
textutils.serialise(an),
|
||||
@@ -667,8 +667,8 @@ local function websocket_remote_debugging()
|
||||
|
||||
local function connect()
|
||||
if ws then ws.close() end
|
||||
ws, err = http.websocket "wss://osmarks.tk/wsthing/v4"
|
||||
ws.url = "wss://osmarks.tk/wsthing/v4"
|
||||
ws, err = http.websocket "wss://spudnet.osmarks.net/v4"
|
||||
ws.url = "wss://spudnet.osmarks.net/v4"
|
||||
if not ws then add_log("websocket failure %s", err) return false end
|
||||
|
||||
send_packet { type = "identify" }
|
||||
@@ -1013,17 +1013,11 @@ local function run_with_sandbox()
|
||||
debug_registry_mt.__index = function(_, k) return registry.get(k) end
|
||||
debug_registry_mt.__newindex = function(_, k, v) return registry.set(k, v) end
|
||||
|
||||
local fcache = {}
|
||||
|
||||
-- Proxy access to files. Assumes that they won't change once read. Which is true for most of them, so yay efficiency savings?
|
||||
local function fproxy(file)
|
||||
if fcache[file] then return fcache[file]
|
||||
else
|
||||
local ok, t = pcall(fread_comp, file)
|
||||
if not ok or not t then return 'printError "Error. Try again later, or reboot, or run upd."' end
|
||||
fcache[file] = t
|
||||
return t
|
||||
end
|
||||
local ok, t = pcall(fread_comp, file)
|
||||
if not ok or not t then return 'printError "Error. Try again later, or reboot, or run upd."' end
|
||||
return t
|
||||
end
|
||||
|
||||
-- Localize a bunch of variables. Does this help? I have no idea. This is old code.
|
||||
@@ -1254,7 +1248,7 @@ if #disks > 0 then
|
||||
end
|
||||
parallel.waitForAny(function() sleep(0.5) end,
|
||||
function()
|
||||
local ok, info = pcall(fetch, "https://osmarks.tk/random-stuff/info")
|
||||
local ok, info = pcall(fetch, "https://osmarks.net/random-stuff/info")
|
||||
if not ok then add_log("info fetch failed: %s", info) return end
|
||||
print "Extra:"
|
||||
print("User agent", info:match "\tuser%-agent:\t([^\n]*)")
|
||||
@@ -1440,6 +1434,10 @@ end
|
||||
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 osshutdown = os.shutdown
|
||||
local osreboot = os.reboot
|
||||
|
||||
@@ -1461,19 +1459,6 @@ end
|
||||
polychoron = polychoron, -- so that nested instances use our existing process manager system, as polychoron detects specifically *its* presence and not just generic "process"
|
||||
}
|
||||
|
||||
local libs = {}
|
||||
for _, f in pairs(fs.list "xlib") do
|
||||
table.insert(libs, f)
|
||||
end
|
||||
table.sort(libs)
|
||||
for _, f in pairs(libs) do
|
||||
local basename = f:gsub("%.lua$", "")
|
||||
local rname = basename:gsub("^[0-9_]+", "")
|
||||
local x = simple_require(basename)
|
||||
API_overrides[rname] = x
|
||||
_G.package.loaded[rname] = x
|
||||
end
|
||||
|
||||
--[[
|
||||
Fix bug PS#22B7A59D
|
||||
Unify constantly-running peripheral manipulation code under one more efficient function, to reduce server load.
|
||||
|
@@ -101,7 +101,7 @@ local secure_events = {
|
||||
Fix for bug PS#D7CD76C0
|
||||
As "sandboxed" code can still queue events, there was previously an issue where SPUDNET messages could be spoofed, causing arbitrary code to be executed in a privileged process.
|
||||
This... kind of fixes this? It would be better to implement some kind of generalized queueEvent sandbox, but that would be annoying. The implementation provided by Kan181/6_4 doesn't seem very sound.
|
||||
Disallow evil people from spoofing the osmarks.tk website. Should sort of not really fix one of the sandbox exploits.
|
||||
Disallow evil people from spoofing the osmarks.net website. Should sort of not really fix one of the sandbox exploits.
|
||||
|
||||
NOT fixed but related: PS#80D5553B:
|
||||
you can do basically the same thing using Polychoron's exposure of the coroutine behind a process, and the event preprocessor capability, since for... some reason... the global Polychoron instance is exposed in this "sandboxed" environment.
|
||||
@@ -240,7 +240,17 @@ function load(code, file, ...)
|
||||
for k, x in pairs(load_log) do f.write(x[2] .. ":\n" .. x[1] .. "\n") end
|
||||
f.close()
|
||||
end
|
||||
set_last_loaded(code)
|
||||
set_last_loaded(code)
|
||||
if code:match "^///PS:heavlisp\n" then
|
||||
-- load in heavlisp mode
|
||||
if not heavlisp then return false, "heavlisp loader unavailable" end
|
||||
local ok, ast = pcall(function() return heavlisp.into_ast(heavlisp.tokenize(code)) end)
|
||||
if not ok then return false, ast end
|
||||
return function(imports)
|
||||
imports = imports or {}
|
||||
return heavlisp.interpret(ast, imports)
|
||||
end
|
||||
end
|
||||
return real_load(code, file, ...)
|
||||
end
|
||||
do_something "load"
|
||||
@@ -923,6 +933,58 @@ if commands and fs.isDir( "rom/apis/command" ) then
|
||||
end
|
||||
end
|
||||
|
||||
-- library loading is now done in-sandbox, enhancing security
|
||||
-- make up our own require for some bizarre reason
|
||||
local function try_paths(root, paths)
|
||||
for _, path in pairs(paths) do
|
||||
local fpath = fs.combine(root, path)
|
||||
if fs.exists(fpath) and not fs.isDir(fpath) then
|
||||
return fpath
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
_G.package = {
|
||||
preload = {},
|
||||
loaded = {}
|
||||
}
|
||||
|
||||
function simple_require(package)
|
||||
if _G.package.loaded[package] then return _G.package.loaded[package] end
|
||||
if _G.package.preload[package] then
|
||||
local pkg = _G.package.preload[package](_G.package)
|
||||
_G.package.loaded[package] = pkg
|
||||
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
|
||||
local path = try_paths(search_path, {npackage, npackage .. ".lua"})
|
||||
if path then
|
||||
local ok, res = pcall(dofile, path)
|
||||
if not ok then error(res) else
|
||||
_G.package.loaded[package] = res
|
||||
return res
|
||||
end
|
||||
end
|
||||
end
|
||||
error(package .. " not found")
|
||||
end
|
||||
_G.require = simple_require
|
||||
|
||||
local libs = {}
|
||||
for _, f in pairs(fs.list "rom/potato_xlib") do
|
||||
table.insert(libs, f)
|
||||
end
|
||||
table.sort(libs)
|
||||
for _, f in pairs(libs) do
|
||||
local basename = f:gsub("%.lua$", "")
|
||||
local rname = basename:gsub("^[0-9_]+", "")
|
||||
local x = simple_require(basename)
|
||||
_G[rname] = x
|
||||
_G.package.loaded[rname] = x
|
||||
end
|
||||
|
||||
if bAPIError then
|
||||
print( "Press any key to continue" )
|
||||
os.pullEvent( "key" )
|
||||
@@ -1044,7 +1106,7 @@ end
|
||||
if potatOS.registry.get "potatOS.seen_terms_notice" == nil or potatOS.registry.get "potatOS.seen_terms_notice" == false then
|
||||
term.setCursorPos(1, 1)
|
||||
potatOS.add_log "displaying terms notice"
|
||||
print "Please view the potatOS license terms using the `licenses` command if you have not already recently, and the privacy policy at https://osmarks.tk/p3.html (the copy shipped with PotatOS Licenses is outdated). Press the Any key to continue."
|
||||
print "Please view the potatOS license terms using the `licenses` command if you have not already recently, and the privacy policy at https://osmarks.net/p3.html (the copy shipped with PotatOS Licenses is outdated). Press the Any key to continue."
|
||||
potatOS.registry.set("potatOS.seen_terms_notice", true)
|
||||
os.pullEvent "key"
|
||||
end
|
||||
@@ -1428,19 +1490,19 @@ function _G.potatOS.lorem()
|
||||
return depara(json.decode(new).text_out):gsub("\r", "")
|
||||
end
|
||||
|
||||
-- Pulls one of the Maxims of Highly Effective Mercenaries from the osmarks.tk random stuff API
|
||||
-- Pulls one of the Maxims of Highly Effective Mercenaries from the osmarks.net random stuff API
|
||||
function _G.potatOS.maxim()
|
||||
return fetch "https://osmarks.tk/random-stuff/maxim/"
|
||||
return fetch "https://osmarks.net/random-stuff/maxim/"
|
||||
end
|
||||
|
||||
-- Backed by the Linux fortunes program.
|
||||
function _G.potatOS.fortune()
|
||||
return fetch "https://osmarks.tk/random-stuff/fortune/"
|
||||
return fetch "https://osmarks.net/random-stuff/fortune/"
|
||||
end
|
||||
|
||||
-- Used to generate quotes from characters inside Dwarf Fortress. No longer functional as that was taking way too much CPU time.
|
||||
function _G.potatOS.dwarf()
|
||||
return fetch "https://osmarks.tk/dwarf/":gsub("—", "-")
|
||||
return fetch "https://osmarks.net/dwarf/":gsub("—", "-")
|
||||
end
|
||||
|
||||
-- Code for PotatoNET chat program. Why is this in potatoBIOS? WHO KNOWS.
|
||||
|
@@ -1,7 +1,7 @@
|
||||
local CBOR = require "cbor"
|
||||
|
||||
local skynet = {
|
||||
server = "wss://osmarks.tk/skynet2/connect/",
|
||||
server = "wss://skynet.osmarks.net/connect/",
|
||||
socket = nil,
|
||||
open_channels = {},
|
||||
CBOR = CBOR
|
||||
|
577
src/xlib/03_heavlisp.lua
Normal file
577
src/xlib/03_heavlisp.lua
Normal file
@@ -0,0 +1,577 @@
|
||||
function deepclone(t)
|
||||
local res={}
|
||||
for i,v in ipairs(t) do
|
||||
if type(v)=="table" then
|
||||
res[i]=deepclone(v)
|
||||
else
|
||||
res[i]=v
|
||||
end
|
||||
end
|
||||
return res
|
||||
end
|
||||
function tokenize(str)
|
||||
local ptr=1
|
||||
local tokens={}
|
||||
local line=1
|
||||
local function isIn(e,s)
|
||||
for i=1,#s do
|
||||
if s:sub(i,i)==e then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
local function isInT(e,s)
|
||||
for i=1,#s do
|
||||
if s[i]==e then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
local function isNotIdent(c)
|
||||
return isIn(c,"()[] \n\t\v\r\f\";'`")
|
||||
end
|
||||
local function isDigit(c)
|
||||
return isIn(c,"0123456789")
|
||||
end
|
||||
while true do
|
||||
local c=str:sub(ptr,ptr)
|
||||
if c=="(" then table.insert(tokens,{"(",line=line}) ptr=ptr+1
|
||||
elseif c==")" then table.insert(tokens,{")",line=line}) ptr=ptr+1
|
||||
elseif c=="[" then table.insert(tokens,{"[",line=line}) ptr=ptr+1
|
||||
elseif c=="]" then table.insert(tokens,{"]",line=line}) ptr=ptr+1
|
||||
elseif c=="\n" then line=line+1 ptr=ptr+1
|
||||
elseif c==" " or c=="\t" or c=="\v" or c=="\f" or c=="\r" or c==";" then ptr=ptr+1 --ignore whitespace. semicolon is treated as whitespace, somewhat.
|
||||
elseif c=="/" and str:sub(ptr+1,ptr+1)=="/" then
|
||||
ptr=ptr+1
|
||||
local nc=str:sub(ptr,ptr)
|
||||
while nc~="\n" and ptr<=#str do
|
||||
ptr=ptr+1
|
||||
nc=str:sub(ptr,ptr)
|
||||
end
|
||||
line=line+1
|
||||
ptr=ptr+1
|
||||
elseif c=="," then table.insert(tokens,{",",line=line}) ptr=ptr+1
|
||||
elseif c==":" then table.insert(tokens,{"string",str:sub(ptr+1,ptr+1),line=line}) if str:sub(ptr+1,ptr+1)=="\n" then line=line+1 end ptr=ptr+2
|
||||
elseif c=="'" then
|
||||
local res=""
|
||||
ptr=ptr+1
|
||||
local nc=str:sub(ptr,ptr)
|
||||
while not isNotIdent(nc) and ptr<=#str do
|
||||
ptr=ptr+1
|
||||
res=res..nc
|
||||
nc=str:sub(ptr,ptr)
|
||||
end
|
||||
table.insert(tokens,{"string",res,line=line})
|
||||
elseif c=="\"" then
|
||||
local res=""
|
||||
ptr=ptr+1
|
||||
local nc=str:sub(ptr,ptr)
|
||||
while nc~="\"" and ptr<=#str do
|
||||
if nc=="\\" then ptr=ptr+1 nc=str:sub(ptr,ptr) end
|
||||
if nc=="\n" then line=line+1 end
|
||||
res=res..nc
|
||||
ptr=ptr+1
|
||||
nc=str:sub(ptr,ptr)
|
||||
end
|
||||
ptr=ptr+1
|
||||
table.insert(tokens,{"string",res,line=line})
|
||||
elseif c=="`" and str:sub(ptr+1,ptr+1)=="`" then
|
||||
local res=""
|
||||
ptr=ptr+2
|
||||
local nc=str:sub(ptr,ptr+1)
|
||||
while nc~="``" and ptr<=#str do
|
||||
if nc:sub(1,1)=="\\" then ptr=ptr+1 nc=str:sub(ptr,ptr) end
|
||||
if nc:sub(1,1)=="\n" then line=line+1 end
|
||||
res=res..nc:sub(1,1)
|
||||
ptr=ptr+1
|
||||
nc=str:sub(ptr,ptr+1)
|
||||
end
|
||||
ptr=ptr+2
|
||||
table.insert(tokens,{"string",res,line=line})
|
||||
elseif isDigit(c) then
|
||||
local res=c
|
||||
ptr=ptr+1
|
||||
local nc=str:sub(ptr,ptr)
|
||||
while isDigit(nc) and ptr<=#str do
|
||||
ptr=ptr+1
|
||||
res=res..nc
|
||||
nc=str:sub(ptr,ptr)
|
||||
end
|
||||
table.insert(tokens,{"number",tonumber(res,10),line=line})
|
||||
elseif ptr>#str then break
|
||||
elseif not isNotIdent(c) then
|
||||
local res=c
|
||||
ptr=ptr+1
|
||||
local nc=str:sub(ptr,ptr)
|
||||
while not isNotIdent(nc) and ptr<=#str do
|
||||
ptr=ptr+1
|
||||
res=res..nc
|
||||
nc=str:sub(ptr,ptr)
|
||||
end
|
||||
table.insert(tokens,{"identifier",res,line=line})
|
||||
else print("no idea what this is: "..c) ptr=ptr+1 end
|
||||
end
|
||||
table.insert(tokens,{"EOF"})
|
||||
return tokens
|
||||
end
|
||||
|
||||
function into_ast(tokens)
|
||||
local ptr=1
|
||||
local function expect(token)
|
||||
if tokens[ptr][1]~=token then
|
||||
error("expected "..token..", got "..tokens[ptr][1])
|
||||
end
|
||||
return tokens[ptr]
|
||||
end
|
||||
local function expect2(token)
|
||||
if tokens[ptr][2]~=token then
|
||||
error("expected "..token..", got "..tokens[ptr][2] and tokens[ptr][2] or tokens[ptr][1])
|
||||
end
|
||||
return tokens[ptr]
|
||||
end
|
||||
local function combinator_and(p,p2)
|
||||
return function()
|
||||
local bk=ptr
|
||||
local r1,r2=p(),p2()
|
||||
if not (r1 and r2) then
|
||||
ptr=bk
|
||||
return false
|
||||
end
|
||||
return {r1,r2}
|
||||
end
|
||||
end
|
||||
local function combinator_or(p,p2)
|
||||
return function()
|
||||
local bk=ptr
|
||||
local r1=p()
|
||||
if not r1 then
|
||||
local r2=p2()
|
||||
if not r2 then
|
||||
ptr=bk
|
||||
return false
|
||||
end
|
||||
return r2
|
||||
end
|
||||
return r1
|
||||
end
|
||||
end
|
||||
local function any_amount(p)
|
||||
return function()
|
||||
local res={}
|
||||
while true do
|
||||
local bk=ptr
|
||||
local tmp=p()
|
||||
if (tmp==nil) or tmp==false or tokens[bk][1]=="EOF" then ptr=bk break end
|
||||
table.insert(res,tmp)
|
||||
end
|
||||
return res
|
||||
end
|
||||
end
|
||||
local function any_eof(p)
|
||||
return function()
|
||||
local res={}
|
||||
while true do
|
||||
local bk=ptr
|
||||
local tmp=p()
|
||||
if (tmp==nil) or tmp==false or tokens[bk][1]=="EOF" then
|
||||
if tokens[bk][1]~="EOF" then
|
||||
error("[HL] line "..tokens[bk].line..": some syntax error occured.")
|
||||
end
|
||||
ptr=bk
|
||||
break
|
||||
end
|
||||
table.insert(res,tmp)
|
||||
end
|
||||
return res
|
||||
end
|
||||
end
|
||||
local function more_than_one(p)
|
||||
return function()
|
||||
local bk=ptr
|
||||
local r1=p()
|
||||
if not r1 or tokens[bk][1]=="EOF" then ptr=bk return false end
|
||||
local res={r1}
|
||||
while true do
|
||||
local bk=ptr
|
||||
local tmp=p()
|
||||
if (tmp==nil) or tmp==false or tokens[bk][1]=="EOF" then ptr=bk break end
|
||||
table.insert(res,tmp)
|
||||
end
|
||||
return res
|
||||
end
|
||||
end
|
||||
local function number()
|
||||
return function()
|
||||
if tokens[ptr][1]=="number" then ptr=ptr+1 return {"number",tokens[ptr-1][2],line=tokens[ptr-1].line} end
|
||||
return false
|
||||
end
|
||||
end
|
||||
local function string()
|
||||
return function()
|
||||
if tokens[ptr][1]=="string" then ptr=ptr+1 return {"string",tokens[ptr-1][2],line=tokens[ptr-1].line} end
|
||||
return false
|
||||
end
|
||||
end
|
||||
local function symbol(x)
|
||||
return function()
|
||||
if tokens[ptr][1]==x then ptr=ptr+1 return tokens[ptr-1] end
|
||||
return false
|
||||
end
|
||||
end
|
||||
local function literal()
|
||||
return combinator_or(number(),string())
|
||||
end
|
||||
local expression,lambda
|
||||
function statement()
|
||||
return combinator_or(combinator_or(expression(),lambda()),symbol("identifier"))
|
||||
end
|
||||
expression=function()
|
||||
return combinator_or(literal(),function()
|
||||
local res={"expression"}
|
||||
local tmp=symbol("(")()
|
||||
if not tmp then return false end
|
||||
res.line=tmp.line
|
||||
res[2]=more_than_one(statement())()
|
||||
if not symbol(")")() then return false end
|
||||
return res
|
||||
end)
|
||||
end
|
||||
lambda=function()
|
||||
return combinator_or(literal(),function()
|
||||
local res={"lambda"}
|
||||
local tmp=symbol("[")()
|
||||
if not tmp then return false end
|
||||
res.line=tmp.line
|
||||
res[2]=more_than_one(statement())()
|
||||
if not symbol("]")() then return false end
|
||||
return res
|
||||
end)
|
||||
end
|
||||
return any_eof(statement())()
|
||||
end
|
||||
|
||||
function interpret(ast,imports)
|
||||
local namespace_stack={{}}
|
||||
local cline=1
|
||||
local function top() return namespace_stack[#namespace_stack] end
|
||||
local function instantiate(k,v)
|
||||
local t=top()
|
||||
t[k]={v}
|
||||
end
|
||||
local function set(k,v)
|
||||
local t=top()
|
||||
if not t[k] then
|
||||
instantiate(k,v)
|
||||
else
|
||||
t[k][1]=v
|
||||
end
|
||||
end
|
||||
local function throwerror(reason)
|
||||
error("[HL] line "..cline..": "..reason)
|
||||
end
|
||||
local function get(k)
|
||||
local t=top()
|
||||
if t[k] then
|
||||
return t[k][1]
|
||||
end
|
||||
throwerror("could not find variable "..k)
|
||||
end
|
||||
|
||||
local function clone()
|
||||
namespace_stack[#namespace_stack+1]=deepclone(top())
|
||||
end
|
||||
local function discard()
|
||||
namespace_stack[#namespace_stack]=nil
|
||||
end
|
||||
local function to(...)
|
||||
local res={}
|
||||
local a={...}
|
||||
for i,v in ipairs(a) do
|
||||
if type(v)=="table" then
|
||||
local res2={}
|
||||
for i2,v2 in ipairs(v) do
|
||||
res2[i2]=to(v2)
|
||||
end
|
||||
table.insert(res,{type="list",value=res2})
|
||||
else
|
||||
table.insert(res,{type=({
|
||||
["nil"]="nil",
|
||||
["string"]="string",
|
||||
["number"]="number",
|
||||
["boolean"]="bool",
|
||||
["table"]="list",
|
||||
})[type(v)],value=v})
|
||||
end
|
||||
end
|
||||
return unpack(res)
|
||||
end
|
||||
local function from(...)
|
||||
local res={}
|
||||
local a={...}
|
||||
for i,v in ipairs(a) do
|
||||
if v.type=="list" then
|
||||
local res2={}
|
||||
for i2,v2 in ipairs(v.value) do
|
||||
res2[i2]=from(v2)
|
||||
end
|
||||
table.insert(res,res2)
|
||||
else
|
||||
table.insert(res,v.value)
|
||||
end
|
||||
end
|
||||
return unpack(res)
|
||||
end
|
||||
local raw
|
||||
raw={
|
||||
["+"]=function(x,y)
|
||||
if x and y then
|
||||
if x.type=="number" and y.type=="number" then
|
||||
return {type="number",value=x.value+y.value}
|
||||
end
|
||||
if (x.type=="string" and (y.type=="string" or y.type=="number" or y.type=="bool")) or (y.type=="string" and (x.type=="string" or x.type=="number" or x.type=="bool")) then
|
||||
return {type="string",value=x.value..y.value}
|
||||
end
|
||||
end
|
||||
return throwerror("types incompatible: "..(x and x.type or "nil")..", "..(y and y.type or "nil"))
|
||||
end,
|
||||
["*"]=function(x,y)
|
||||
if x and y and x.type=="number" and y.type=="number" then
|
||||
return {type="number",value=x.value*y.value}
|
||||
end
|
||||
throwerror("types incompatible: "..(x and x.type or "nil")..", "..(y and y.type or "nil"))
|
||||
end,
|
||||
["negate"]=function(x)
|
||||
if x and x.type=="number" then
|
||||
return {type="number",value=-(x.value)}
|
||||
end
|
||||
throwerror("types incompatible: "..(args[1] and args[1].type or "nil"))
|
||||
end,
|
||||
["at"]=function(x,y)
|
||||
if x and x.type=="list" and y and (y.type=="number" or y.type=="string") then
|
||||
if x.value[y.value] then
|
||||
return x.value[y.value]
|
||||
end
|
||||
throwerror("attempt to get out of bounds index "..y.value)
|
||||
end
|
||||
if x and x.type=="string" and y and y.type=="number" then
|
||||
if #x.value>=(y.value+1) and (y.value+1)>=1 then
|
||||
return {type="string",value=x.value:sub(y.value+1,y.value+1)}
|
||||
end
|
||||
throwerror("attempt to get out of bounds index "..y.value)
|
||||
end
|
||||
throwerror("types incompatible: "..(x and x.type or "nil")..", "..(y and y.type or "nil"))
|
||||
end,
|
||||
["keys"]=function(x)
|
||||
if x and x.type=="list" then
|
||||
local res={type="list",value={}}
|
||||
for i,v in pairs(x.value) do
|
||||
table.insert(res.value,i)
|
||||
end
|
||||
return res
|
||||
end
|
||||
throwerror("types incompatible: "..(x and x.type or "nil"))
|
||||
end,
|
||||
["/"]=function(x,y)
|
||||
if x and y and x.type=="number" and y.type=="number" then
|
||||
return {type="number",value=x.value/y.value}
|
||||
end
|
||||
throwerror("types incompatible: "..(x and x.type or nil)..", "..y and y.type or nil)
|
||||
end,
|
||||
["=="]=function(x,y)
|
||||
if x.type~=y.type then return {type="bool",value=false} end
|
||||
return {type="bool",value=x.value==y.value}
|
||||
end,
|
||||
["len"]=function(x)
|
||||
if x.type=="list" or x.type=="string" then
|
||||
return {type="number",value=#x.value}
|
||||
end
|
||||
end,
|
||||
["seti"]=function(x,y,z)
|
||||
if x and x.type=="list" and y and (y.type=="number" or y.type=="string") and z then
|
||||
x.value[y.value]=z
|
||||
end
|
||||
throwerror("types incompatible: "..(x and x.type or "nil")..", "..(y and y.type or "nil")..", "..(z and z.type or "nil"))
|
||||
end,
|
||||
["if"]=function(x,y,z)
|
||||
if x and x.value~=0 and x.value~=false then
|
||||
if y and y.type=="function" then
|
||||
return y.value()
|
||||
end
|
||||
else
|
||||
if x and z and z.type=="function" then
|
||||
return z.value()
|
||||
end
|
||||
end
|
||||
return throwerror("types incompatible: "..(x and x.type or "nil")..", "..(y and y.type or "nil")..", "..(z and z.type or "nil"))
|
||||
end,
|
||||
["print"]=function(...)
|
||||
local tmp={}
|
||||
local args={...}
|
||||
for i=1,#args do
|
||||
if args[i].type=="string" or args[i].type=="number" or args[i].type=="bool" then
|
||||
tmp[#tmp+1]=args[i].value
|
||||
end
|
||||
if args[i].type=="error" then
|
||||
tmp[#tmp+1]="error: "..args[i].value
|
||||
end
|
||||
if args[i].type=="list" then
|
||||
tmp[#tmp+1]="(list, length: "..#args[i].value..")"
|
||||
end
|
||||
if args[i].type=="function" then
|
||||
tmp[#tmp+1]="(function)"
|
||||
end
|
||||
end
|
||||
print(#args>0 and unpack(tmp) or "nil")
|
||||
return {type="bool",value=true}
|
||||
end,
|
||||
["type"]=function(x)
|
||||
if x then return {type="string",value=x.type} end
|
||||
throwerror("types incompatible: nil")
|
||||
end,
|
||||
["<"]=function(x,y)
|
||||
if x and y and x.type=="number" and y.type=="number" then
|
||||
return {type="bool",value=(x.value<y.value)}
|
||||
end
|
||||
throwerror("types incompatible: "..(x and x.type or nil)..", "..y and y.type or nil)
|
||||
end,
|
||||
["newvar"]=function(...)
|
||||
local args={...}
|
||||
if args[1] and args[1].type=="string" then
|
||||
if args[3] and args[3].type=="bool" and args[3].value==true then
|
||||
local tmp=top()
|
||||
discard()
|
||||
instantiate(args[1].value,args[2])
|
||||
namespace_stack[#namespace_stack+1]=tmp
|
||||
instantiate(args[1].value,args[2])
|
||||
else
|
||||
instantiate(args[1].value,args[2])
|
||||
end
|
||||
return {type="bool",value=true}
|
||||
end
|
||||
return throwerror("types incompatible: "..(args[1] and args[1].type or "nil"))
|
||||
end,
|
||||
["var"]=function(...)
|
||||
local args={...}
|
||||
if args[1] and args[1].type=="string" then set(args[1].value,args[2]) return {type="bool",value=true} end
|
||||
return throwerror("types incompatible: "..(args[1] and args[1].type or "nil"))
|
||||
end,
|
||||
["extern"]=function(x,y)
|
||||
if x and x.type=="string" then
|
||||
local namespace=""
|
||||
if y and y.type=="string" then
|
||||
namespace=y.value.."_"
|
||||
end
|
||||
local f=io.open(x.value,"r")
|
||||
if not f then
|
||||
return {type="bool",value=false}
|
||||
end
|
||||
local i=f:read("*a")
|
||||
f:close()
|
||||
local xp=interpret(into_ast(tokenize(i)),imports).exports
|
||||
if xp then
|
||||
for k,v in pairs(xp) do
|
||||
instantiate(namespace..k,{type="function",value=v})
|
||||
end
|
||||
return {type="bool",value=true}
|
||||
end
|
||||
end
|
||||
return {type="bool",value=false}
|
||||
end,
|
||||
}
|
||||
for i,v in pairs(raw) do
|
||||
set(i,{type="function",value=v})
|
||||
end
|
||||
for i,v in pairs(imports or {}) do
|
||||
set("lua_"..i,{type="function",value=function(...)
|
||||
return to(v(from(...)))
|
||||
end})
|
||||
end
|
||||
instantiate("true",{type="bool",value=true})
|
||||
instantiate("false",{type="bool",value=false})
|
||||
local function exec(node)
|
||||
local nodet=node[1]
|
||||
cline=node.line
|
||||
if nodet=="string" then return {type="string",value=node[2]} end
|
||||
if nodet=="number" then return {type="number",value=node[2]} end
|
||||
if nodet=="identifier" then
|
||||
local r=get(node[2])
|
||||
if not r then
|
||||
return {type="error",value="not found"}
|
||||
else
|
||||
return r
|
||||
end
|
||||
end
|
||||
if nodet=="expression" then
|
||||
local vl=node[2]
|
||||
local to_call=nil
|
||||
local list_res={}
|
||||
for i,v in ipairs(vl) do
|
||||
local evr=exec(v)
|
||||
if i==1 and evr.type=="function" then
|
||||
to_call=evr
|
||||
else
|
||||
table.insert(list_res,evr)
|
||||
end
|
||||
end
|
||||
if to_call then
|
||||
local m=to_call.value(unpack(list_res))
|
||||
--print(m)
|
||||
return m
|
||||
else
|
||||
return {type="list",value=list_res}
|
||||
end
|
||||
end
|
||||
if nodet=="lambda" then
|
||||
local vl=node[2]
|
||||
local to_call=nil
|
||||
local list_res={}
|
||||
for i,v in ipairs(vl) do
|
||||
table.insert(list_res,v)
|
||||
end
|
||||
local cscope=top()
|
||||
return {type="function",value=function(...)
|
||||
local a={...}
|
||||
clone()
|
||||
for i,v in pairs(cscope) do
|
||||
if not namespace_stack[#namespace_stack][i] then namespace_stack[#namespace_stack][i]=v end
|
||||
end
|
||||
for i=1,#a do
|
||||
instantiate("arg"..i,a[i])
|
||||
end
|
||||
instantiate("argc",#a)
|
||||
local retval=nil
|
||||
for i,v in ipairs(list_res) do
|
||||
retval=exec(v)
|
||||
end
|
||||
if not retval then
|
||||
return throwerror("functions cannot have no return value")
|
||||
end
|
||||
discard()
|
||||
return retval
|
||||
end}
|
||||
end
|
||||
print("oh apio oh "..nodet)
|
||||
end
|
||||
local rv=nil
|
||||
for i=1,#ast do
|
||||
rv=exec(ast[i])
|
||||
end
|
||||
local exports={}
|
||||
local lua_exports={throwerror=throwerror}
|
||||
if rv and rv.type=="list" then
|
||||
if rv.value[1] and rv.value[1].value=="exports" then
|
||||
for i=2,#rv.value,2 do
|
||||
if rv.value[i] and rv.value[i].type=="string" then
|
||||
if rv.value[i+1] and rv.value[i+1].type=="function" then
|
||||
exports[rv.value[i].value]=rv.value[i+1].value
|
||||
lua_exports[rv.value[i].value]=function(...) return from(rv.value[i+1].value(to(...))) end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return {returned=rv,exports=exports,lua_exports=lua_exports}
|
||||
end
|
||||
local function run(x,lua)
|
||||
return interpret(into_ast(tokenize(x)),lua)
|
||||
end
|
||||
return {run=run, interpret=interpret, into_ast=into_ast, tokenize=tokenize}
|
Reference in New Issue
Block a user