Last active
September 5, 2024 17:15
-
-
Save MCJack123/e634347fe7a3025d19d9f7fcf7e01c24 to your computer and use it in GitHub Desktop.
YellowBox: Virtual machines for ComputerCraft
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--- YellowBox 1.0 | |
-- By JackMacWindows | |
-- | |
-- @module yellowbox | |
-- | |
-- This module allows you to easily create and manage special ComputerCraft | |
-- containers called "boxes". These boxes contain their own separate execution | |
-- environment, completely sequestered from the caller's environment. This allows | |
-- effective "virtual machines" running CraftOS. | |
-- | |
-- YellowBox supports: | |
-- * Custom BIOSes | |
-- * Virtual filesystems (including a basic serialize/deserialize storage format) | |
-- * Custom terminals | |
-- * Queueing arbitrary events outside the box | |
-- * Mounts between the real FS and inner FS | |
-- * Peripheral exports | |
-- * API exports | |
-- * Custom configuration (for some options) | |
-- * Redstone interception | |
-- | |
-- To create a new box, use yellowbox:new(). Then call box:resume() to start | |
-- execution. The resume method exits either when the computer shuts down or | |
-- there are no more events waiting in the queue. To replenish the events and | |
-- continue execution, use box:queueEvent(event, ...) to push more events to the | |
-- queue (usually from os.pullEventRaw). The box.running property can be used to | |
-- determine whether the environment is running/waiting for events. | |
-- | |
-- To load the root filesystem from a file, you can use box:loadVFS(path). This | |
-- takes the path to a serialized table file, and sets that path as the location | |
-- to write new data to. It also initializes the contents of the disk with the | |
-- current contents of the file if available. If you want to write your own | |
-- filesystem instead of using the built-in VFS functionality, you can set | |
-- box.disk to a table containing the filesystem hierarchy as a key-value table, | |
-- and box:syncfs to a function that is called each time the filesystem is | |
-- written to. | |
-- | |
-- Any changes to the environment from the default (such as terminals) should be | |
-- done before calling box:resume() for the first time. If this is not possible, | |
-- you can call box:reloadenv() to reload the environment (however, this may or | |
-- may not work properly depending on the workings of the embedded OS). | |
-- | |
-- Here's an example program demonstrating how to use YellowBox: | |
-- | |
-- local file = fs.open("bios.lua", "rb") | |
-- local vm = yellowbox:new(file.readAll()) | |
-- file.close() | |
-- vm:loadVFS("filesystem.vfs") | |
-- vm:mount("/realFS", "/") | |
-- vm:resume() | |
-- while vm.running do | |
-- vm:queueEvent(os.pullEventRaw()) | |
-- vm:resume() | |
-- end | |
-- MIT License | |
-- | |
-- Copyright (c) 2021 JackMacWindows | |
-- | |
-- Permission is hereby granted, free of charge, to any person obtaining a copy | |
-- of this software and associated documentation files (the "Software"), to deal | |
-- in the Software without restriction, including without limitation the rights | |
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
-- copies of the Software, and to permit persons to whom the Software is | |
-- furnished to do so, subject to the following conditions: | |
-- | |
-- The above copyright notice and this permission notice shall be included in all | |
-- copies or substantial portions of the Software. | |
-- | |
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
-- SOFTWARE. | |
local expect = require "cc.expect".expect | |
local yellowbox = {} | |
--- This table converts side names to side numbers. | |
yellowbox.sideNames = { | |
top = 1, | |
bottom = 2, | |
left = 3, | |
right = 4, | |
front = 5, | |
back = 6 | |
} | |
--- This table converts side numbers to side names. | |
yellowbox.sideNumbers = { | |
"top", | |
"bottom", | |
"left", | |
"right", | |
"front", | |
"back" | |
} | |
--- Creates a new box. | |
-- @tparam string|nil bios The BIOS to use. If unset, use loadBIOS later. | |
-- @tparam table|nil The disk data. If unset, defaults to an empty disk. | |
-- @treturn yellowbox A new box instance. | |
function yellowbox:new(bios, disk) | |
expect(1, bios, "string", "nil") | |
expect(2, disk, "table", "nil") | |
local obj = setmetatable({ | |
coro = nil, | |
bios = bios, | |
disk = disk or {}, | |
syncfs = function() end, | |
term = term.current(), | |
eventQueue = {}, | |
mounts = {[{"rom"}] = "rom"}, | |
peripherals = {}, | |
apis = {bit32 = bit32}, | |
config = { | |
http_enable = http ~= nil, | |
http_websocket_enable = http ~= nil and http.websocket ~= nil, | |
disable_lua51_features = _CC_DISABLE_LUA51_FEATURES, | |
default_computer_settings = _CC_DEFAULT_SETTINGS, | |
maximumFilesOpen = 128 | |
}, | |
redstone = { | |
input = {0, 0, 0, 0, 0, 0}, | |
output = {0, 0, 0, 0, 0, 0}, | |
bundledInput = {0, 0, 0, 0, 0, 0}, | |
bundledOutput = {0, 0, 0, 0, 0, 0} | |
}, | |
timers = {}, | |
alarms = {}, | |
running = false, | |
id = 0, | |
label = nil, | |
startTime = nil, | |
filter = nil, | |
openFiles = 0 | |
}, {__index = yellowbox}) | |
if bios then | |
obj.fn = load(bios, "=bios.lua", "t", obj:makeenv()) | |
obj.coro = coroutine.create(obj.fn) | |
end | |
return obj | |
end | |
local function checkMount(mounts, path) | |
local parts = {} | |
for p in fs.combine(path):gmatch("[^/]+") do parts[#parts+1] = p end | |
for k,v in pairs(mounts) do | |
local ok = true | |
for i,p in ipairs(k) do if parts[i] ~= p then ok = false break end end | |
if ok then | |
for i = 1, #k do table.remove(parts, 1) end | |
return true, fs.combine(v, table.concat(parts, "/")) | |
end | |
end | |
return false | |
end | |
local function getPath(tab, path) | |
for p in fs.combine(path):gmatch("[^/]+") do | |
tab = tab[p] | |
if tab == nil then return nil end | |
end | |
return tab | |
end | |
local function getPath_mkdir(tab, path) | |
for p in fs.combine(path):gmatch("[^/]+") do | |
if type(tab) == "string" then return nil | |
elseif tab[p] == nil then tab[p] = {} end | |
tab = tab[p] | |
end | |
return tab | |
end | |
local function deepcopy(orig) | |
local orig_type = type(orig) | |
local copy | |
if orig_type == 'table' then | |
copy = {} | |
for orig_key, orig_value in next, orig, nil do | |
copy[deepcopy(orig_key)] = deepcopy(orig_value) | |
end | |
setmetatable(copy, deepcopy(getmetatable(orig))) | |
else -- number, string, boolean, etc | |
copy = orig | |
end | |
return copy | |
end | |
local function aux_find(parts, t, mounts) | |
if #parts == 0 then return type(t) == "table" and "" or t elseif type(t) ~= "table" then return nil end | |
local parts2 = {} | |
for i,v in ipairs(parts) do parts2[i] = v end | |
local name = table.remove(parts2, 1) | |
local retval = {} | |
if t then for k, v in pairs(t) do if k:match("^" .. name:gsub("([%%%.])", "%%%1"):gsub("%*", "%.%*") .. "$") then retval[k] = aux_find(parts2, v, mounts[k]) end end end | |
if mounts then for k, v in pairs(mounts) do if k[1]:match("^" .. name:gsub("([%%%.])", "%%%1"):gsub("%*", "%.%*") .. "$") then | |
if #k == 1 then | |
local r = fs.find(fs.combine(v, table.concat(parts2, "/"))) | |
retval[k[1]] = {} | |
for _,w in ipairs(r) do retval[k[1]][fs.combine((w:gsub(v, "")))] = "" end | |
else retval[k[1]] = aux_find(parts2, t[k[1]], {table.unpack(k, 2)}) end | |
end end end | |
return retval | |
end | |
local function combineKeys(t, prefix) | |
prefix = prefix or "" | |
if t == nil then return {} end | |
local retval = {} | |
for k,v in pairs(t) do | |
if type(v) == "string" then table.insert(retval, prefix .. k) | |
else for _,w in ipairs(combineKeys(v, prefix .. k .. "/")) do table.insert(retval, w) end end | |
end | |
return retval | |
end | |
local fs, os, peripheral, getfenv, setfenv, load, loadstring = fs, os, peripheral, getfenv, setfenv, load, loadstring | |
--- Creates the environment for the box. This is mostly an internal function, | |
-- but it's exported as part of the class. | |
-- @treturn table A new environment for the box. | |
function yellowbox:makeenv() | |
local env | |
env = { | |
assert = assert, | |
error = error, | |
getfenv = function(n) | |
local e = getfenv(n) | |
if e and e._G == _G then return nil end | |
return e | |
end, | |
getmetatable = getmetatable, | |
ipairs = ipairs, | |
load = function(_chunk, _name, _mode, _env) | |
if not _env then _env = env end | |
return load(_chunk, _name, _mode, _env) | |
end, | |
loadstring = function(chunk, name) | |
local fn, err = loadstring(chunk, name) | |
if fn then setfenv(fn, env) end | |
return fn, err | |
end, | |
next = next, | |
pairs = pairs, | |
pcall = pcall, | |
rawequal = rawequal, | |
rawget = rawget, | |
rawset = rawset, | |
select = select, | |
setfenv = setfenv, | |
setmetatable = setmetatable, | |
tonumber = tonumber, | |
tostring = tostring, | |
type = type, | |
unpack = unpack, | |
_VERSION = _VERSION, | |
xpcall = xpcall, | |
_HOST = "ComputerCraft 1.95.2 (YellowBox 1.0)", | |
_CC_DEFAULT_SETTINGS = self.config.default_computer_settings, | |
_CC_DISABLE_LUA51_FEATURES = self.config.disable_lua51_features, | |
coroutine = coroutine, | |
math = math, | |
string = string, | |
table = table, | |
term = self.term, | |
utf8 = utf8, | |
fs = { | |
list = function(path) | |
expect(1, path, "string") | |
local isMount, mountPath = checkMount(self.mounts, path) | |
if isMount then return fs.list(mountPath) end | |
local tab = getPath(self.disk, path) | |
if type(tab) ~= "table" then error(path .. ": Not a directory", 2) end | |
local retval = {} | |
for k in pairs(tab) do retval[#retval+1] = k end | |
local parts = {} | |
for p in fs.combine(path):gmatch("[^/]+") do parts[#parts+1] = p end | |
for k,v in pairs(self.mounts) do | |
if #parts == #k - 1 then | |
local ok = true | |
for i,p in ipairs(k) do if parts[i] ~= p and i < #k then ok = false break end end | |
if ok then retval[#retval+1] = k[#k] end | |
end | |
end | |
table.sort(retval) | |
return retval | |
end, | |
exists = function(path) | |
expect(1, path, "string") | |
local isMount, mountPath = checkMount(self.mounts, path) | |
if isMount then return fs.exists(mountPath) end | |
return getPath(self.disk, path) ~= nil | |
end, | |
isDir = function(path) | |
expect(1, path, "string") | |
local isMount, mountPath = checkMount(self.mounts, path) | |
if isMount then return fs.isDir(mountPath) end | |
local tab = getPath(self.disk, path) | |
return type(tab) == "table" | |
end, | |
isReadOnly = function(path) | |
expect(1, path, "string") | |
local isMount, mountPath = checkMount(self.mounts, path) | |
if isMount then return fs.isReadOnly(mountPath) end | |
return false -- todo | |
end, | |
getName = fs.getName, | |
getDrive = function(path) | |
expect(1, path, "string") | |
local isMount, mountPath = checkMount(self.mounts, path) | |
if isMount then return fs.getDrive(mountPath) end | |
return "hdd" | |
end, | |
getSize = function(path) | |
expect(1, path, "string") | |
local isMount, mountPath = checkMount(self.mounts, path) | |
if isMount then return fs.getSize(mountPath) end | |
local tab = getPath(self.disk, path) | |
if tab == nil then error(path .. ": No such file") end | |
if type(tab) == "table" then return 0 | |
else return #tab end | |
end, | |
getFreeSpace = function(path) | |
expect(1, path, "string") | |
local isMount, mountPath = checkMount(self.mounts, path) | |
if isMount then return fs.getFreeSpace(mountPath) end | |
return 1000000 | |
end, | |
makeDir = function(path) | |
expect(1, path, "string") | |
local isMount, mountPath = checkMount(self.mounts, path) | |
if isMount then return fs.makeDir(mountPath) end | |
local tab = self.disk | |
for p in fs.combine(path):gmatch("[^/]+") do | |
if tab[p] == nil then tab[p] = {} | |
elseif type(tab[p]) == "string" then | |
self:syncfs() | |
error(path .. ": File already exists", 2) | |
end | |
tab = tab[p] | |
end | |
self:syncfs() | |
end, | |
move = function(fromPath, toPath) | |
expect(1, fromPath, "string") | |
local isFromMount, fromMountPath = checkMount(self.mounts, fromPath) | |
local isToMount, toMountPath = checkMount(self.mounts, toPath) | |
if isFromMount and isToMount then return fs.move(fromMountPath, toMountPath) | |
elseif isFromMount and not isToMount then | |
local function move(from, toTab, idx, level) | |
if fs.isReadOnly(from) then error(fromPath .. ": Access denied", level) end | |
if fs.isDir(from) then | |
toTab[idx] = {} | |
for _,v in ipairs(fs.list(from)) do | |
move(fs.combine(from, v), toTab[idx], level + 1) | |
end | |
fs.delete(from) | |
else | |
local file, err = fs.open(from, "rb") | |
if file == nil then self:syncfs() error(err, level) end | |
toTab[idx] = file.readAll() | |
file.close() | |
fs.delete(from) | |
end | |
end | |
move(fromMountPath, getPath_mkdir(self.disk, fs.getDir(toPath)), fs.getName(toPath), 3) | |
self:syncfs() | |
elseif not isFromMount and isToMount then | |
local function move(fromTab, idx, to, level) | |
if fs.isReadOnly(to) then error(toPath .. ": Access denied", level) end | |
if type(fromTab[idx]) == "table" then | |
fs.makeDir(to) | |
for k in pairs(fromTab[idx]) do | |
move(fromTab[idx], k, fs.combine(to, k), level + 1) | |
end | |
fromTab[idx] = nil | |
else | |
local file, err = fs.open(to, "wb") | |
if file == nil then self:syncfs() error(err, level) end | |
file.write(fromTab[idx]) | |
file.close() | |
fromTab[idx] = nil | |
end | |
end | |
move(getPath(self.disk, fs.getDir(fromPath)), fs.getName(fromPath), toPath, 3) | |
self:syncfs() | |
else | |
getPath_mkdir(self.disk, fs.getDir(toPath))[fs.getName(toPath)] = getPath(self.disk, fromPath) | |
getPath(self.disk, fs.getDir(fromPath))[fs.getName(fromPath)] = nil | |
self:syncfs() | |
end | |
end, | |
copy = function(fromPath, toPath) | |
expect(1, fromPath, "string") | |
expect(2, toPath, "string") | |
local isFromMount, fromMountPath = checkMount(self.mounts, fromPath) | |
local isToMount, toMountPath = checkMount(self.mounts, toPath) | |
if isFromMount and isToMount then return fs.copy(fromMountPath, toMountPath) | |
elseif isFromMount and not isToMount then | |
local function copy(from, toTab, idx, level) | |
if fs.isDir(from) then | |
toTab[idx] = {} | |
for _,v in ipairs(fs.list(from)) do | |
copy(fs.combine(from, v), toTab[idx], level + 1) | |
end | |
else | |
local file, err = fs.open(from, "rb") | |
if file == nil then self:syncfs() error(err, level) end | |
toTab[idx] = file.readAll() | |
file.close() | |
end | |
end | |
copy(fromMountPath, getPath_mkdir(self.disk, fs.getDir(toPath)), fs.getName(toPath), 3) | |
self:syncfs() | |
elseif not isFromMount and isToMount then | |
local function copy(fromTab, idx, to, level) | |
if fs.isReadOnly(to) then error(toPath .. ": Access denied", level) end | |
if type(fromTab[idx]) == "table" then | |
fs.makeDir(to) | |
for k in pairs(fromTab[idx]) do | |
copy(fromTab[idx], k, fs.combine(to, k), level + 1) | |
end | |
else | |
local file, err = fs.open(to, "wb") | |
if file == nil then self:syncfs() error(err, level) end | |
file.write(fromTab[idx]) | |
file.close() | |
end | |
end | |
copy(getPath(self.disk, fs.getDir(fromPath)), fs.getName(fromPath), toPath, 3) | |
self:syncfs() | |
else | |
getPath_mkdir(self.disk, fs.getDir(toPath))[fs.getName(toPath)] = deepcopy(getPath(self.disk, fromPath)) | |
self:syncfs() | |
end | |
end, | |
delete = function(path) | |
expect(1, path, "string") | |
local isMount, mountPath = checkMount(self.mounts, path) | |
if isMount then return fs.delete(mountPath) end | |
local tab = self.disk | |
for p in fs.combine(path, ".."):gmatch("[^/]+") do | |
if tab[p] == nil or type(tab[p]) == "string" then error(path .. ": No such file or directory", 2) end | |
tab = tab[p] | |
end | |
tab[fs.getName(path)] = nil | |
self:syncfs() | |
end, | |
combine = fs.combine, | |
open = function(path, mode) | |
expect(1, path, "string") | |
expect(2, mode, "string") | |
local isMount, mountPath = checkMount(self.mounts, path) | |
if isMount then return fs.open(mountPath, mode) end | |
if self.openFiles >= self.config.maximumFilesOpen then return nil, "Too many open files" end | |
self.openFiles = self.openFiles + 1 | |
if mode == "r" then | |
local tab = getPath(self.disk, path) | |
if type(tab) ~= "string" then return nil, "No such file" end | |
local oldtab = tab | |
tab = "" | |
for _, c in utf8.codes(oldtab) do tab = tab .. (c > 255 and "?" or string.char(c)) end | |
tab = tab:gsub("\r\n", "\n") | |
local pos = 1 | |
local closed = false | |
return { | |
readLine = function(withTrailing) | |
if closed then error("file is already closed", 2) end | |
if pos > #tab then return end | |
local str, endPos = tab:match(withTrailing and "([^\n]*\n?)()" or "([^\n]*)\n?()", pos) | |
pos = str and endPos or #tab + 1 | |
return str | |
end, | |
readAll = function() | |
if closed then error("file is already closed", 2) end | |
if #tab == 0 and pos == 1 then | |
pos = 2 | |
return "" | |
end | |
if pos > #tab then return end | |
local oldPos = pos | |
pos = #tab + 1 | |
return tab:sub(oldPos) | |
end, | |
read = function(count) | |
if closed then error("file is already closed", 2) end | |
if pos > #tab then return end | |
expect(1, count, "number", "nil") | |
count = count or 1 | |
local oldPos = pos | |
pos = pos + count | |
return tab:sub(oldPos, pos - 1) | |
end, | |
close = function() | |
if closed then error("file is already closed", 2) end | |
closed = true | |
self.openFiles = self.openFiles - 1 | |
end | |
} | |
elseif mode == "w" or mode == "a" then | |
local dir = getPath_mkdir(self.disk, fs.getDir(path)) | |
if dir == nil then return nil, "File exists" end | |
local name = fs.getName(path) | |
if type(dir[name]) == "table" then return nil, "Directory exists" end | |
local data = "" | |
local closed = false | |
if mode == "w" or dir[name] == nil then | |
dir[name] = data | |
self:syncfs() | |
elseif mode == "a" then | |
if not dir[name] then return nil, "No such file" end | |
data = dir[name] | |
local oldtab = data | |
data = "" | |
for _, c in utf8.codes(oldtab) do data = data .. (c > 255 and "?" or string.char(c)) end | |
data = data:gsub("\r\n", "\n") | |
end | |
return { | |
write = function(value) | |
if closed then error("file is already closed", 2) end | |
data = data .. utf8.char(tostring(value):byte(1, -1)) | |
end, | |
writeLine = function(value) | |
if closed then error("file is already closed", 2) end | |
data = data .. utf8.char(tostring(value):byte(1, -1)) .. "\n" | |
end, | |
flush = function() | |
if closed then error("file is already closed", 2) end | |
dir[name] = data | |
self:syncfs() | |
end, | |
close = function() | |
if closed then error("file is already closed", 2) end | |
dir[name] = data | |
self:syncfs() | |
closed = true | |
self.openFiles = self.openFiles - 1 | |
end | |
} | |
elseif mode == "rb" then | |
local tab = getPath(self.disk, path) | |
if type(tab) ~= "string" then return nil, "No such file" end | |
local pos = 1 | |
local closed = false | |
return { | |
readLine = function(withTrailing) | |
if closed then error("file is already closed", 2) end | |
if pos > #tab then return end | |
local str, endPos = tab:match(withTrailing and "([^\n]*\n?)()" or "([^\n]*)\n?()", pos) | |
pos = str and endPos or #tab + 1 | |
return str | |
end, | |
readAll = function() | |
if closed then error("file is already closed", 2) end | |
if #tab == 0 and pos == 1 then | |
pos = 2 | |
return "" | |
end | |
if pos > #tab then return end | |
local oldPos = pos | |
pos = #tab + 1 | |
return tab:sub(oldPos) | |
end, | |
read = function(count) | |
expect(1, count, "number", "nil") | |
if closed then error("file is already closed", 2) end | |
if pos > #tab then return end | |
if count == nil then | |
pos = pos + 1 | |
return tab:byte(pos - 1) | |
else | |
local oldPos = pos | |
pos = pos + count | |
return tab:sub(oldPos, pos - 1) | |
end | |
end, | |
close = function() | |
if closed then error("file is already closed", 2) end | |
closed = true | |
self.openFiles = self.openFiles - 1 | |
end, | |
seek = function(whence, offset) | |
if closed then error("file is already closed", 2) end | |
expect(1, whence, "string", "nil") | |
expect(2, offset, "number", "nil") | |
whence = whence or "cur" | |
offset = offset or 0 | |
if whence == "set" then pos = offset | |
elseif whence == "cur" then pos = pos + offset | |
elseif whence == "end" then pos = #tab - offset | |
else error("bad argument #1 (invalid option " .. whence .. ")", 2) end | |
return pos | |
end | |
} | |
elseif mode == "wb" or mode == "ab" then | |
local dir = getPath_mkdir(self.disk, fs.getDir(path)) | |
if dir == nil then return nil, "File exists" end | |
local name = fs.getName(path) | |
if type(dir[name]) == "table" then return nil, "Directory exists" end | |
local data = "" | |
local closed = false | |
if mode == "wb" or dir[name] == nil then | |
dir[name] = data | |
self:syncfs() | |
elseif mode == "ab" then | |
if not dir[name] then return nil, "No such file" end | |
data = dir[name] | |
end | |
local pos = #data | |
return { | |
write = function(value) | |
if closed then error("file is already closed", 2) end | |
if type(value) == "number" then value = string.char(value) end | |
data = data:sub(1, pos) .. value .. data:sub(pos + #value + 1) | |
pos = pos + #value | |
end, | |
writeLine = function(value) | |
if closed then error("file is already closed", 2) end | |
if type(value) == "number" then value = string.char(value) end | |
data = data:sub(1, pos) .. value .. "\n" .. data:sub(pos + #value + 2) | |
pos = pos + #value + 1 | |
end, | |
flush = function() | |
if closed then error("file is already closed", 2) end | |
dir[name] = data | |
self:syncfs() | |
end, | |
close = function() | |
if closed then error("file is already closed", 2) end | |
dir[name] = data | |
self:syncfs() | |
closed = true | |
self.openFiles = self.openFiles - 1 | |
end, | |
seek = function(whence, offset) | |
if closed then error("file is already closed", 2) end | |
expect(1, whence, "string", "nil") | |
expect(2, offset, "number", "nil") | |
whence = whence or "cur" | |
offset = offset or 0 | |
if whence == "set" then pos = offset | |
elseif whence == "cur" then pos = pos + offset | |
elseif whence == "end" then pos = #tab - offset | |
else error("bad argument #1 (invalid option " .. whence .. ")", 2) end | |
return pos | |
end | |
} | |
else return nil, "Invalid mode" end | |
end, | |
find = function(wildcard) | |
expect(1, wildcard, "string") | |
local parts = {} | |
for p in wildcard:gmatch("[^/]+") do parts[#parts+1] = p end | |
local retval = {} | |
for _,v in ipairs(combineKeys(aux_find(parts, self.disk, self.mounts))) do table.insert(retval, v) end | |
table.sort(retval) | |
return retval | |
end, | |
getDir = fs.getDir, | |
attributes = function(path) | |
expect(1, path, "string") | |
local isMount, mountPath = checkMount(self.mounts, path) | |
if isMount then return fs.attributes(mountPath) end | |
local tab = getPath(self.disk, path) | |
return { | |
size = type(tab) == "table" and 0 or #tab, | |
isDir = type(tab) == "table", | |
isReadOnly = false, | |
created = 0, | |
modified = 0 | |
} | |
end, | |
getCapacity = function(path) | |
expect(1, path, "string") | |
local isMount, mountPath = checkMount(self.mounts, path) | |
if isMount then return fs.getCapacity(mountPath) end | |
return 1000000 | |
end | |
}, | |
http = self.config.http_enable and { | |
request = http.request, | |
checkURL = http.checkURLAsync, | |
websocket = self.config.http_websocket_enable and http.websocketAsync or nil | |
} or nil, | |
os = { | |
queueEvent = function(event, ...) | |
expect(1, event, "string") | |
self.eventQueue[#self.eventQueue+1] = {event, ...} | |
end, | |
startTimer = function(timeout) | |
expect(1, timeout, "number") | |
local id = os.startTimer(timeout) | |
if id ~= nil then self.timers[id] = true end | |
return id | |
end, | |
cancelTimer = function(id) | |
expect(1, id, "number") | |
if not self.timers[id] then return end | |
os.cancelTimer(id) | |
self.timers[id] = nil | |
end, | |
setAlarm = function(timeout) | |
expect(1, timeout, "number") | |
local id = os.setAlarm(timeout) | |
if id ~= nil then self.alarms[id] = true end | |
return id | |
end, | |
cancelAlarm = function(id) | |
expect(1, id, "number") | |
if not self.alarms[id] then return end | |
os.cancelAlarm(id) | |
self.alarms[id] = nil | |
end, | |
shutdown = function() self.running = false end, | |
reboot = function() self.running = 2 end, | |
getComputerID = function() return self.id end, | |
getComputerLabel = function() return self.label end, | |
setComputerLabel = function(label) | |
expect(1, label, "string", "nil") | |
self.label = label | |
end, | |
clock = function() return os.clock() - self.startTime end, | |
time = os.time, | |
day = os.day, | |
epoch = os.epoch, | |
date = os.date | |
}, | |
peripheral = { | |
isPresent = function(side) | |
expect(1, side, "string") | |
return self.peripherals[side] ~= nil | |
end, | |
getType = function(side) | |
expect(1, side, "string") | |
if self.peripherals[side] then return peripheral.getType(self.peripherals[side]) end | |
return nil | |
end, | |
hasType = function(side, type) | |
expect(1, side, "string") | |
expect(2, type, "string") | |
if self.peripherals[side] then return peripheral.hasType(self.peripherals[side], type) end | |
return nil | |
end, | |
getMethods = function(side) | |
expect(1, side, "string") | |
return self.peripherals[side] and peripheral.getMethods(self.peripherals[side]) or nil | |
end, | |
call = function(side, method, ...) | |
expect(1, side, "string") | |
expect(2, method, "string") | |
if self.peripherals[side] then return peripheral.call(self.peripherals[side], method, ...) end | |
error("No such peripheral", 2) | |
end | |
}, | |
redstone = { | |
getSides = function() return yellowbox.sideNumbers end, | |
getInput = function(side) | |
expect(1, side, "string") | |
if yellowbox.sideNames[side] == nil then error("bad argument #1 (invalid option " .. side .. ")", 2) end | |
return self.redstone.input[yellowbox.sideNames[side]] ~= 0 | |
end, | |
getOutput = function(side) | |
expect(1, side, "string") | |
if yellowbox.sideNames[side] == nil then error("bad argument #1 (invalid option " .. side .. ")", 2) end | |
return self.redstone.output[yellowbox.sideNames[side]] ~= 0 | |
end, | |
setOutput = function(side, output) | |
expect(1, side, "string") | |
expect(2, output, "boolean") | |
if yellowbox.sideNames[side] == nil then error("bad argument #1 (invalid option " .. side .. ")", 2) end | |
self.redstone.output[yellowbox.sideNames[side]] = output and 15 or 0 | |
end, | |
getAnalogInput = function(side) | |
expect(1, side, "string") | |
if yellowbox.sideNames[side] == nil then error("bad argument #1 (invalid option " .. side .. ")", 2) end | |
return self.redstone.input[yellowbox.sideNames[side]] | |
end, | |
getAnalogOutput = function(side) | |
expect(1, side, "string") | |
if yellowbox.sideNames[side] == nil then error("bad argument #1 (invalid option " .. side .. ")", 2) end | |
return self.redstone.output[yellowbox.sideNames[side]] | |
end, | |
setAnalogOutput = function(side, output) | |
expect(1, side, "string") | |
expect(2, output, "number") | |
if yellowbox.sideNames[side] == nil then error("bad argument #1 (invalid option " .. side .. ")", 2) end | |
if output < 0 or output > 15 then error("Expected number in range 0-15", 2) end | |
self.redstone.output[yellowbox.sideNames[side]] = output | |
end, | |
getBundledInput = function(side) | |
expect(1, side, "string") | |
if yellowbox.sideNames[side] == nil then error("bad argument #1 (invalid option " .. side .. ")", 2) end | |
return self.redstone.bundledInput[yellowbox.sideNames[side]] | |
end, | |
getBundledOutput = function(side) | |
expect(1, side, "string") | |
if yellowbox.sideNames[side] == nil then error("bad argument #1 (invalid option " .. side .. ")", 2) end | |
return self.redstone.bundledOutput[yellowbox.sideNames[side]] | |
end, | |
setBundledOutput = function(side, output) | |
expect(1, side, "string") | |
expect(2, output, "number") | |
if yellowbox.sideNames[side] == nil then error("bad argument #1 (invalid option " .. side .. ")", 2) end | |
if output < 0 or output > 65535 then error("Expected number in range 0-65535", 2) end | |
self.redstone.bundledOutput[yellowbox.sideNames[side]] = output | |
end, | |
testBundledInput = function(side, mask) | |
expect(1, side, "string") | |
expect(2, mask, "number") | |
if yellowbox.sideNames[side] == nil then error("bad argument #1 (invalid option " .. side .. ")", 2) end | |
if mask < 0 or mask > 65535 then error("Expected number in range 0-65535", 2) end | |
return bit32.btest(self.redstone.bundledInput[yellowbox.sideNames[side]], mask) | |
end | |
} | |
} | |
env._G = env | |
env.os.computerID = env.os.getComputerID | |
env.os.computerLabel = env.os.getComputerLabel | |
env.rs = redstone | |
env.redstone.getAnalogueInput = env.redstone.getAnalogInput | |
env.redstone.getAnalogueOutput = env.redstone.getAnalogOutput | |
env.redstone.setAnalogueOutput = env.redstone.setAnalogOutput | |
env.term.nativePaletteColor = term.nativePaletteColor | |
env.term.nativePaletteColour = term.nativePaletteColour | |
for k,v in pairs(self.apis) do env[k] = v end | |
return env | |
end | |
--- Returns the global environment for the box. | |
-- @treturn table The box's global environment. | |
function yellowbox:getfenv() | |
return getfenv(self.fn) | |
end | |
--- Loads the BIOS if necessary and sets a new environment. | |
function yellowbox:reloadenv() | |
if self.bios == nil then error("No BIOS was specified. Please set self.bios to the contents of the BIOS script.", 2) end | |
if self.fn == nil then self.fn = load(self.bios, "=bios.lua", "t", self:makeenv()) | |
else setfenv(self.fn, self:makeenv()) end | |
end | |
--- Resumes the box's execution, or starts it if it's not running. | |
function yellowbox:resume() | |
local ok | |
repeat | |
if not self.coro then | |
if self.bios == nil then error("No BIOS was specified. Please set self.bios to the contents of the BIOS script.", 2) end | |
if self.fn == nil then self.fn = load(self.bios, "=bios.lua", "t", self:makeenv()) end | |
self.coro = coroutine.create(self.fn) | |
end | |
if self.running ~= true then | |
self.running = true | |
self.startTime = os.clock() | |
ok, self.filter = coroutine.resume(self.coro) | |
if not ok then | |
local err = self.filter | |
self.filter = nil | |
self.coro = nil | |
self.running = false | |
self.startTime = nil | |
self.eventQueue = {} | |
self:syncfs() | |
error("YellowBox environment threw an exception: " .. err, 2) | |
end | |
end | |
while #self.eventQueue > 0 and self.running == true do | |
local ev = table.remove(self.eventQueue, 1) | |
if self.filter == nil or self.filter == ev[1] or ev[1] == "terminate" then | |
if ev[1] == "timer" then self.timers[ev[2]] = nil | |
elseif ev[1] == "alarm" then self.alarms[ev[2]] = nil end | |
ok, self.filter = coroutine.resume(self.coro, table.unpack(ev)) | |
if not ok then | |
local err = self.filter | |
self.filter = nil | |
self.coro = nil | |
self.running = false | |
self.startTime = nil | |
self.eventQueue = {} | |
self:syncfs() | |
error("YellowBox environment threw an exception: " .. err, 2) | |
end | |
end | |
end | |
if self.running ~= true then | |
self.filter = nil | |
self.coro = nil | |
self.startTime = nil | |
self.eventQueue = {} | |
self:syncfs() | |
end | |
until self.running ~= 2 | |
end | |
--- Queues an event inside the box. | |
-- @tparam string event The event name to queue. | |
-- @param ... The event's arguments. | |
function yellowbox:queueEvent(event, ...) | |
expect(1, event, "string") | |
if (event == "timer" and self.timers[...] == nil) or (event == "alarm" and self.alarms[...] == nil) then return end | |
self.eventQueue[#self.eventQueue+1] = {event, ...} | |
end | |
--- Halts the box, deleting any coroutines and resetting the state. | |
function yellowbox:halt() | |
self.running = false | |
self.coro = nil | |
self.startTime = nil | |
self.eventQueue = {} | |
self:syncfs() | |
end | |
--- Mounts a path from outside the box inside the environment. | |
-- @tparam string innerPath The path to the mount inside the box. | |
-- @tparam string outerPath The path to the mounted folder outside the box. | |
function yellowbox:mount(innerPath, outerPath) | |
expect(1, innerPath, "string") | |
expect(2, outerPath, "string") | |
local parts = {} | |
for p in fs.combine(innerPath):gmatch("[^/]+") do parts[#parts+1] = p end | |
self.mounts[parts] = fs.combine(outerPath) | |
end | |
--- Unmounts a previously mounted directory. | |
-- @tparam string innerPath The path to the mount inside the box. | |
function yellowbox:unmount(innerPath) | |
expect(1, innerPath, "string") | |
local parts = {} | |
for p in fs.combine(innerPath):gmatch("[^/]+") do parts[#parts+1] = p end | |
for k in pairs(self.mounts) do | |
if #k == #p and table.concat(k, "/") == table.concat(p, "/") then | |
self.mounts[k] = nil | |
break | |
end | |
end | |
end | |
--- Loads a BIOS file from a path. | |
-- @tparam string path The path to the BIOS. | |
function yellowbox:loadBIOS(path) | |
expect(1, path, "string") | |
local file, err = fs.open(path, "rb") | |
if file == nil then error(err, 2) end | |
self.bios = file.readAll() | |
file.close() | |
end | |
--- Loads a VFS disk and optionally sets up write-backs. | |
-- This uses a basic serialized table to store data. More efficient methods are | |
-- available, but these are left to the user to implement. | |
-- @tparam string path The path to the VFS. | |
-- @tparam boolean|nil readOnly Whether the disk is read-only (won't sync changes to the file). | |
function yellowbox:loadVFS(path, readOnly) | |
expect(1, path, "string") | |
expect(2, readOnly, "boolean", "nil") | |
local file, err = fs.open(path, "rb") | |
if file ~= nil then | |
local data = file.readAll() | |
file.close() | |
self.disk = textutils.unserialize(data) | |
else self.disk = {} end | |
if not readOnly then | |
self.syncfs = function(self) | |
local file = fs.open(path, "wb") | |
if file == nil then return end | |
file.write(textutils.serialize(self.disk, {compact = true})) | |
file.close() | |
end | |
else self.syncfs = function() end end | |
end | |
--- Exports a peripheral from outside the box in. | |
-- @tparam string name The name of the peripheral as available outside the box. | |
-- @tparam string|nil innerName The name of the peripheral as it will appear inside the box. Defaults to the same name as outside. | |
function yellowbox:exportPeripheral(name, innerName) | |
expect(1, name, "string") | |
expect(2, innerName, "string", "nil") | |
self.peripherals[innerName or name] = name | |
end | |
return yellowbox |
how do you add the debug API without ruining security as my OS requires it
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
sandbox go brrrrr