Advertisement
osmarks

YAFSS

Aug 14th, 2018 (edited)
3,093
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 11.69 KB | None | 0 0
  1. -- Deep-copy a table
  2. local function copy(tabl)
  3.     local new = {}
  4.     for k, v in pairs(tabl) do
  5.         if type(v) == "table" and tabl ~= v then
  6.             new[k] = copy(v)
  7.         else
  8.             new[k] = v
  9.         end
  10.     end
  11.     return new
  12. end
  13.  
  14. -- Deep-map all values in a table
  15. local function deepmap(table, f, path)
  16.     local path = path or ""
  17.     local new = {}
  18.     for k, v in pairs(table) do
  19.         local thisp = path .. "." .. k
  20.         if type(v) == "table" and v ~= table then -- bodge it to not stackoverflow
  21.             new[k] = deepmap(v, f, thisp)
  22.         else
  23.             new[k] = f(v, k, thisp)
  24.         end
  25.     end
  26.     return new
  27. end
  28.  
  29. -- Takes a list of keys to copy, returns a function which takes a table and copies the given keys to a new table
  30. local function copy_some_keys(keys)
  31.     return function(from)
  32.         local new = {}
  33.         for _, key_to_copy in pairs(keys) do
  34.             local x = from[key_to_copy]
  35.             if type(x) == "table" then
  36.                 x = copy(x)
  37.             end
  38.             new[key_to_copy] = x
  39.         end
  40.         return new
  41.     end
  42. end
  43.  
  44. -- Simple string operations
  45. local function starts_with(s, with)
  46.     return string.sub(s, 1, #with) == with
  47. end
  48. local function ends_with(s, with)
  49.     return string.sub(s, -#with, -1) == with
  50. end
  51. local function contains(s, subs)
  52.     return string.find(s, subs) ~= nil
  53. end
  54.  
  55. -- Maps function f over table t. f is passed the value and key and can return a new value and key.
  56. local function map(f, t)
  57.     local mapper = function(t)
  58.         local new = {}
  59.         for k, v in pairs(t) do
  60.             local new_v, new_k = f(v, k)
  61.             new[new_k or k] = new_v
  62.         end
  63.         return new
  64.     end
  65.     if t then return mapper(t) else return mapper end
  66. end
  67.  
  68. -- Copies stuff from t2 into t1
  69. local function add_to_table(t1, t2)
  70.     for k, v in pairs(t2) do
  71.         if type(v) == "table" and v ~= t2 and v ~= t1 then
  72.             if not t1[k] then t1[k] = {} end
  73.             add_to_table(t1[k], v)
  74.         else
  75.             t1[k] = v
  76.         end
  77.     end
  78. end
  79.  
  80. -- Convert path to canonical form
  81. local function canonicalize(path)
  82.     return fs.combine(path, "")
  83. end
  84.  
  85. -- Checks whether a path is in a directory
  86. local function path_in(p, dir)
  87.     return starts_with(canonicalize(p), canonicalize(dir))
  88. end
  89.  
  90. local function make_mappings(root)
  91.     return {
  92.         ["/disk"] = "/disk",
  93.         ["/rom"] = "/rom",
  94.         default = root
  95.     }
  96. end
  97.  
  98. local function get_root(path, mappings)
  99.     for mapfrom, mapto in pairs(mappings) do
  100.         if path_in(path, mapfrom) then
  101.             return mapto, mapfrom
  102.         end
  103.     end
  104.     return mappings.default, "/"
  105. end
  106.  
  107. -- Escapes lua patterns in a string. Should not be needed, but lua is stupid so the only string.replace thing is gsub
  108. local quotepattern = '(['..("%^$().[]*+-?"):gsub("(.)", "%%%1")..'])'
  109. local function escape(str)
  110.     return str:gsub(quotepattern, "%%%1")
  111. end
  112.  
  113. local function strip(p, root)
  114.     return p:gsub("^" .. escape(canonicalize(root)), "")
  115. end
  116.  
  117. local function resolve_path(path, mappings)
  118.     local root, to_strip = get_root(path, mappings)
  119.     local newpath = strip(fs.combine(root, path), to_strip)
  120.     if path_in(newpath, root) then return newpath end
  121.     return resolve_path(newpath, mappings)
  122. end
  123.  
  124. local function segments(path)
  125.     local segs, rest = {}, ""
  126.     repeat
  127.         table.insert(segs, 1, fs.getName(rest))
  128.         rest = fs.getDir(rest)
  129.     until rest == ""
  130.     return segs
  131. end
  132.  
  133. local function combine(segs)
  134.     local out = ""
  135.     for _, p in pairs(segs) do
  136.         out = fs.combine(out, p)
  137.     end
  138.     return out
  139. end
  140.  
  141. local function difference(p1, p2)
  142.     local s1, s2 = segments(p1), segments(p2)
  143.     if #s2 == 0 then return combine(s1) end
  144.     local segs = {}
  145.     for _, p in pairs(s1) do
  146.         local item = table.remove(s1, 1)
  147.         table.insert(segs, item)
  148.         if p == s2[1] then break end
  149.     end
  150.     return combine(segs)
  151. end
  152.  
  153. -- magic from http://lua-users.org/wiki/SplitJoin
  154. -- split string into lines
  155. local function lines(str)
  156.     local t = {}
  157.     local function helper(line)
  158.         table.insert(t, line)
  159.         return ""
  160.     end
  161.     helper((str:gsub("(.-)\r?\n", helper)))
  162.     return t
  163. end
  164.  
  165. -- Fetch the contents of URL "u"
  166. local function fetch(u)
  167.     local h = http.get(u)
  168.     local c = h.readAll()
  169.     h.close()
  170.     return c
  171. end
  172.  
  173. -- Make a read handle for a string
  174. local function make_handle(text)
  175.     local lines = lines(text)
  176.     local h = {line = 0}
  177.     function h.close() end
  178.     function h.readLine() h.line = h.line + 1 return lines[h.line] end
  179.     function h.readAll() return text end
  180.     return h
  181. end
  182.  
  183. -- Get a path from a filesystem overlay
  184. local function path_in_overlay(overlay, path)
  185.     return overlay[canonicalize(path)]
  186. end
  187.  
  188. local this_level_env = _G
  189.  
  190. -- Create a modified FS table which confines you to root and has some extra read-only pseudofiles.
  191. local function create_FS(root, overlay)
  192.     local mappings = make_mappings(root)
  193.  
  194.     local new_overlay = {}
  195.     for k, v in pairs(overlay) do
  196.         new_overlay[canonicalize(k)] = v
  197.     end
  198.  
  199.     local function lift_to_sandbox(f, n)
  200.         return function(...)
  201.             local args = map(function(x) return resolve_path(x, mappings) end, {...})
  202.             return f(table.unpack(args))
  203.         end
  204.     end
  205.  
  206.     local new = copy_some_keys {"getDir", "getName", "combine"} (fs)
  207.  
  208.     function new.isReadOnly(path)
  209.         return path_in_overlay(new_overlay, path) or starts_with(canonicalize(path), "rom")
  210.     end
  211.  
  212.     function new.open(path, mode)
  213.         if (contains(mode, "w") or contains(mode, "a")) and new.isReadOnly(path) then
  214.             error "Access denied"
  215.         else
  216.             local overlay_data = path_in_overlay(new_overlay, path)
  217.             if overlay_data then
  218.                 if type(overlay_data) == "function" then overlay_data = overlay_data(this_level_env) end
  219.                 return make_handle(overlay_data), "YAFSS overlay"
  220.             end
  221.             return fs.open(resolve_path(path, mappings), mode)
  222.         end
  223.     end
  224.  
  225.     function new.exists(path)
  226.         if path_in_overlay(new_overlay, path) ~= nil then return true end
  227.         return fs.exists(resolve_path(path, mappings))
  228.     end
  229.  
  230.     function new.overlay()
  231.         return map(function(x)
  232.             if type(x) == "function" then return x(this_level_env)
  233.             else return x end
  234.         end, new_overlay)
  235.     end
  236.  
  237.     function new.list(dir)
  238.         local sdir = canonicalize(resolve_path(dir, mappings))
  239.         local contents = fs.list(sdir)
  240.         for opath in pairs(new_overlay) do
  241.             if fs.getDir(opath) == sdir then
  242.                 table.insert(contents, fs.getName(opath))
  243.             end
  244.         end
  245.         return contents
  246.     end
  247.  
  248.     add_to_table(new, map(lift_to_sandbox, copy_some_keys {"isDir", "getDrive", "getSize", "getFreeSpace", "makeDir", "move", "copy", "delete", "isDriveRoot"} (fs)))
  249.  
  250.     function new.find(wildcard)
  251.         local function recurse_spec(results, path, spec) -- From here: https://github.com/Sorroko/cclite/blob/62677542ed63bd4db212f83da1357cb953e82ce3/src/emulator/native_api.lua
  252.             local segment = spec:match('([^/]*)'):gsub('/', '')
  253.             local pattern = '^' .. segment:gsub('[*]', '.+'):gsub('?', '.'):gsub("-", "%%-") .. '$'
  254.  
  255.             if new.isDir(path) then
  256.                 for _, file in ipairs(new.list(path)) do
  257.                     if file:match(pattern) then
  258.                         local f = new.combine(path, file)
  259.  
  260.                         if new.isDir(f) then
  261.                             recurse_spec(results, f, spec:sub(#segment + 2))
  262.                         end
  263.                         if spec == segment then
  264.                             table.insert(results, f)
  265.                         end
  266.                     end
  267.                 end
  268.             end
  269.         end
  270.         local results = {}
  271.         recurse_spec(results, '', wildcard)
  272.         return results
  273.     end
  274.  
  275.     function new.dump(dir)
  276.         local dir = dir or "/"
  277.         local out = {}
  278.         for _, f in pairs(new.list(dir)) do
  279.             local path = fs.combine(dir, f)
  280.             local to_add = {
  281.                 n = f,
  282.                 t = "f"
  283.             }
  284.             if new.isDir(path) then
  285.                 to_add.c = new.dump(path)
  286.                 to_add.t = "d"
  287.             else
  288.                 local fh = new.open(path, "r")
  289.                 to_add.c = fh.readAll()
  290.                 fh.close()
  291.             end
  292.             table.insert(out, to_add)
  293.         end
  294.         return out
  295.     end
  296.  
  297.     function new.load(dump, root)
  298.         local root = root or "/"
  299.         for _, f in pairs(dump) do
  300.             local path = fs.combine(root, f.n)
  301.             if f.t == "d" then
  302.                 new.makeDir(path)
  303.                 new.load(f.c, path)
  304.             else
  305.                 local fh = new.open(path, "w")
  306.                 fh.write(f.c)
  307.                 fh.close()
  308.             end
  309.         end
  310.     end
  311.  
  312.     return new
  313. end
  314.  
  315. local allowed_APIs = {
  316.     "term",
  317.     "http",
  318.     "pairs",
  319.     "ipairs",
  320.     -- getfenv, getfenv are modified to prevent sandbox escapes and defined in make_environment
  321.     "peripheral",
  322.     "table",
  323.     "string",
  324.     "type",
  325.     "setmetatable",
  326.     "getmetatable",
  327.     "os",
  328.     "sleep",
  329.     "pcall",
  330.     "xpcall",
  331.     "select",
  332.     "tostring",
  333.     "tonumber",
  334.     "coroutine",
  335.     "next",
  336.     "error",
  337.     "math",
  338.     "redstone",
  339.     "rs",
  340.     "assert",
  341.     "unpack",
  342.     "bit",
  343.     "bit32",
  344.     "turtle",
  345.     "pocket",
  346.     "ccemux",
  347.     "config",
  348.     "commands",
  349.     "rawget",
  350.     "rawset",
  351.     "rawequal",
  352.     "~expect",
  353.     "__inext",
  354.     "periphemu",
  355. }
  356.  
  357. local gf, sf = getfenv, setfenv
  358.  
  359. -- Takes the root directory to allow access to,
  360. -- a map of paths to either strings containing their contents or functions returning them
  361. -- and a table of extra APIs and partial overrides for existing APIs
  362. local function make_environment(root_directory, overlay, API_overrides)
  363.     local environment = copy_some_keys(allowed_APIs)(_G)
  364.  
  365.     environment.fs = create_FS(root_directory, overlay)
  366.  
  367.     -- if function is not from within the VM, return env from within sandbox
  368.     function environment.getfenv(arg)
  369.         local env
  370.         if type(arg) == "number" then return gf() end
  371.         if not env or type(env._HOST) ~= "string" or not string.match(env._HOST, "YAFSS") then
  372.             return gf()
  373.         else
  374.             return env
  375.         end
  376.     end
  377.  
  378.     --[[
  379. Fix PS#AD2A532C
  380. 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.
  381.     ]]
  382.     function environment.setfenv(fn, env)
  383.         local nenv = gf(fn)
  384.         if not nenv or type(nenv._HOST) ~= "string" or not string.match(nenv._HOST, "YAFSS") then
  385.             return false
  386.         end
  387.         return sf(fn, env)
  388.     end
  389.  
  390.     function environment.load(code, file, mode, env)
  391.         return load(code, file or "@<input>", mode or "t", env or environment)
  392.     end
  393.  
  394.     if debug then
  395.         environment.debug = copy_some_keys {
  396.             "getmetatable",
  397.             "setmetatable",
  398.             "traceback",
  399.             "getinfo",
  400.             "getregistry"
  401.         }(debug)
  402.     end
  403.  
  404.     environment._G = environment
  405.     environment._ENV = environment
  406.     environment._HOST = string.format("YAFSS on %s", _HOST)
  407.  
  408.     local upper = _G.hypercalls
  409.     environment.hypercalls = {}
  410.     function environment.hypercalls.upper()
  411.         return upper
  412.     end
  413.     function environment.hypercalls.layers()
  414.         if upper then return upper.layers() + 1
  415.         else return 1 end
  416.     end
  417.  
  418.     function environment.os.shutdown()
  419.         os.queueEvent("power_state", "shutdown")
  420.         while true do coroutine.yield() end
  421.     end
  422.  
  423.     function environment.os.reboot()
  424.         os.queueEvent("power_state", "reboot")
  425.         while true do coroutine.yield() end
  426.     end
  427.  
  428.     add_to_table(environment, copy(API_overrides))
  429.  
  430.     return environment
  431. end
  432.  
  433. local function run(root_directory, overlay, API_overrides, init)
  434.     if type(init) == "table" and init.URL then init = fetch(init.URL) end
  435.     init = init or fetch "https://pastebin.com/raw/wKdMTPwQ"
  436.    
  437.     local running = true
  438.     while running do
  439.         parallel.waitForAny(function()
  440.             local env = make_environment(root_directory, overlay, API_overrides)
  441.             env.init_code = init
  442.  
  443.             local out, err = load(init, "@[init]", "t", env)
  444.             env.hypercalls.run = out
  445.             if not out then error(err) end
  446.             local ok, err = pcall(out)
  447.             if not ok then printError(err) end
  448.         end,
  449.         function()
  450.             while true do
  451.                 local event, state = coroutine.yield "power_state"
  452.                 if event == "power_state" then -- coroutine.yield behaves weirdly with terminate
  453.                     if process then
  454.                         local this_process = process.running.ID
  455.                         for _, p in pairs(process.list()) do
  456.                             if p.parent and p.parent.ID == this_process then
  457.                                 process.signal(p.ID, process.signals.KILL)
  458.                             end
  459.                         end
  460.                     end
  461.                     if state == "shutdown" then running = false return
  462.                     elseif state == "reboot" then return end
  463.                 end
  464.             end
  465.         end)
  466.     end
  467. end
  468.  
  469. return run
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement