Files
ccphone/libs/containers.lua
2025-12-24 12:55:15 -08:00

1540 lines
51 KiB
Lua

local deflate = dofile("libs/deflate.lua")
local expect = dofile("rom/modules/main/cc/expect.lua")
local expect, field = expect.expect, expect.field
settings.define("containers.compression_level", {
description = "The level of compression for the file systems on a scale of 1-8",
default = 8,
type = "number",
})
settings.save()
local compression_level = settings.get("containers.compression_level")
local lib = {}
local function deepcopy(o, seen)
seen = seen or {}
if o == nil then return nil end
if seen[o] then return seen[o] end
local no
if type(o) == 'table' then
no = {}
seen[o] = no
for k, v in next, o, nil do
no[deepcopy(k, seen)] = deepcopy(v, seen)
end
setmetatable(no, deepcopy(getmetatable(o), seen))
else -- number, string, boolean, etc
no = o
end
return no
end
local function buildRom(path)
local out = {}
for _, file in ipairs(fs.list(path)) do
local fullPath = fs.combine(path, file)
if fs.isDir(fullPath) then
out[file] = buildRom(fullPath)
else
local handle = fs.open(fullPath, "r")
out[file] = handle.readAll()
handle.close()
end
end
return out
end
local function getRom()
if not _G.rom_cache then
_G.rom_cache = buildRom("/rom")
_G.rom_cache.modules.main.globals = buildRom("/global-libraries")
end
return _G.rom_cache
end
function lib.getENV(fspath, term_override, perms)
local filesystem = nil
print(fspath)
if not perms then perms = {} end
local global = deepcopy(_G)
global.require = nil
local native = global.term.native and global.term.native() or global.term
local redirectTarget = native
local function wrap(_sFunction)
return function(...)
return redirectTarget[_sFunction](...)
end
end
local term = {}
--- Redirects terminal output to a monitor, a [`window`], or any other custom
-- terminal object. Once the redirect is performed, any calls to a "term"
-- function - or to a function that makes use of a term function, as [`print`] -
-- will instead operate with the new terminal object.
--
-- A "terminal object" is simply a table that contains functions with the same
-- names - and general features - as those found in the term table. For example,
-- a wrapped monitor is suitable.
--
-- The redirect can be undone by pointing back to the previous terminal object
-- (which this function returns whenever you switch).
--
-- @tparam Redirect target The terminal redirect the [`term`] API will draw to.
-- @treturn Redirect The previous redirect object, as returned by
-- [`term.current`].
-- @since 1.31
-- @usage
-- Redirect to a monitor on the right of the computer.
--
-- term.redirect(peripheral.wrap("right"))
term.redirect = function(target)
expect(1, target, "table")
if target == term or target == global.term then
error("term is not a recommended redirect target, try term.current() instead", 2)
end
for k, v in pairs(native) do
if type(k) == "string" and type(v) == "function" then
if type(target[k]) ~= "function" then
target[k] = function()
error("Redirect object is missing method " .. k .. ".", 2)
end
end
end
end
redirectTarget = target
return oldRedirectTarget
end
--- Returns the current terminal object of the computer.
--
-- @treturn Redirect The current terminal redirect
-- @since 1.6
-- @usage
-- Create a new [`window`] which draws to the current redirect target.
--
-- window.create(term.current(), 1, 1, 10, 10)
term.current = function()
return redirectTarget
end
--- Get the native terminal object of the current computer.
--
-- It is recommended you do not use this function unless you absolutely have
-- to. In a multitasked environment, [`term.native`] will _not_ be the current
-- terminal object, and so drawing may interfere with other programs.
--
-- @treturn Redirect The native terminal redirect.
-- @since 1.6
term.native = function()
return native
end
-- Some methods shouldn't go through redirects, so we move them to the main
-- term API.
for _, method in ipairs { "nativePaletteColor", "nativePaletteColour" } do
term[method] = native[method]
native[method] = nil
end
for k, v in pairs(native) do
if type(k) == "string" and type(v) == "function" and rawget(term, k) == nil then
term[k] = wrap(k)
end
end
local function write(sText)
expect(1, sText, "string", "number")
local w, h = term.getSize()
local x, y = term.getCursorPos()
local nLinesPrinted = 0
local function newLine()
if y + 1 <= h then
term.setCursorPos(1, y + 1)
else
term.setCursorPos(1, h)
term.scroll(1)
end
x, y = term.getCursorPos()
nLinesPrinted = nLinesPrinted + 1
end
-- Print the line with proper word wrapping
sText = tostring(sText)
while #sText > 0 do
local whitespace = string.match(sText, "^[ \t]+")
if whitespace then
-- Print whitespace
term.write(whitespace)
x, y = term.getCursorPos()
sText = string.sub(sText, #whitespace + 1)
end
local newline = string.match(sText, "^\n")
if newline then
-- Print newlines
newLine()
sText = string.sub(sText, 2)
end
local text = string.match(sText, "^[^ \t\n]+")
if text then
sText = string.sub(sText, #text + 1)
if #text > w then
-- Print a multiline word
while #text > 0 do
if x > w then
newLine()
end
term.write(text)
text = string.sub(text, w - x + 2)
x, y = term.getCursorPos()
end
else
-- Print a word normally
if x + #text - 1 > w then
newLine()
end
term.write(text)
x, y = term.getCursorPos()
end
end
end
return nLinesPrinted
end
local function print(...)
local nLinesPrinted = 0
local nLimit = select("#", ...)
for n = 1, nLimit do
local s = tostring(select(n, ...))
if n < nLimit then
s = s .. "\t"
end
nLinesPrinted = nLinesPrinted + write(s)
end
nLinesPrinted = nLinesPrinted + write("\n")
return nLinesPrinted
end
local function printError(...)
local oldColour
if term.isColour() then
oldColour = term.getTextColour()
term.setTextColour(colors.red)
end
print(...)
if term.isColour() then
term.setTextColour(oldColour)
end
end
local function read(_sReplaceChar, _tHistory, _fnComplete, _sDefault)
expect(1, _sReplaceChar, "string", "nil")
expect(2, _tHistory, "table", "nil")
expect(3, _fnComplete, "function", "nil")
expect(4, _sDefault, "string", "nil")
term.setCursorBlink(true)
local sLine
if type(_sDefault) == "string" then
sLine = _sDefault
else
sLine = ""
end
local nHistoryPos
local nPos, nScroll = #sLine, 0
if _sReplaceChar then
_sReplaceChar = string.sub(_sReplaceChar, 1, 1)
end
local tCompletions
local nCompletion
local function recomplete()
if _fnComplete and nPos == #sLine then
tCompletions = _fnComplete(sLine)
if tCompletions and #tCompletions > 0 then
nCompletion = 1
else
nCompletion = nil
end
else
tCompletions = nil
nCompletion = nil
end
end
local function uncomplete()
tCompletions = nil
nCompletion = nil
end
local w = term.getSize()
local sx = term.getCursorPos()
local function redraw(_bClear)
local cursor_pos = nPos - nScroll
if sx + cursor_pos >= w then
-- We've moved beyond the RHS, ensure we're on the edge.
nScroll = sx + nPos - w
elseif cursor_pos < 0 then
-- We've moved beyond the LHS, ensure we're on the edge.
nScroll = nPos
end
local _, cy = term.getCursorPos()
term.setCursorPos(sx, cy)
local sReplace = _bClear and " " or _sReplaceChar
if sReplace then
term.write(string.rep(sReplace, math.max(#sLine - nScroll, 0)))
else
term.write(string.sub(sLine, nScroll + 1))
end
if nCompletion then
local sCompletion = tCompletions[nCompletion]
local oldText, oldBg
if not _bClear then
oldText = term.getTextColor()
oldBg = term.getBackgroundColor()
term.setTextColor(colors.white)
term.setBackgroundColor(colors.gray)
end
if sReplace then
term.write(string.rep(sReplace, #sCompletion))
else
term.write(sCompletion)
end
if not _bClear then
term.setTextColor(oldText)
term.setBackgroundColor(oldBg)
end
end
term.setCursorPos(sx + nPos - nScroll, cy)
end
local function clear()
redraw(true)
end
recomplete()
redraw()
local function acceptCompletion()
if nCompletion then
-- Clear
clear()
-- Find the common prefix of all the other suggestions which start with the same letter as the current one
local sCompletion = tCompletions[nCompletion]
sLine = sLine .. sCompletion
nPos = #sLine
-- Redraw
recomplete()
redraw()
end
end
while true do
local sEvent, param, param1, param2 = os.pullEvent()
if sEvent == "char" then
-- Typed key
clear()
sLine = string.sub(sLine, 1, nPos) .. param .. string.sub(sLine, nPos + 1)
nPos = nPos + 1
recomplete()
redraw()
elseif sEvent == "paste" then
-- Pasted text
clear()
sLine = string.sub(sLine, 1, nPos) .. param .. string.sub(sLine, nPos + 1)
nPos = nPos + #param
recomplete()
redraw()
elseif sEvent == "key" then
if param == keys.enter or param == keys.numPadEnter then
-- Enter/Numpad Enter
if nCompletion then
clear()
uncomplete()
redraw()
end
break
elseif param == keys.left then
-- Left
if nPos > 0 then
clear()
nPos = nPos - 1
recomplete()
redraw()
end
elseif param == keys.right then
-- Right
if nPos < #sLine then
-- Move right
clear()
nPos = nPos + 1
recomplete()
redraw()
else
-- Accept autocomplete
acceptCompletion()
end
elseif param == keys.up or param == keys.down then
-- Up or down
if nCompletion then
-- Cycle completions
clear()
if param == keys.up then
nCompletion = nCompletion - 1
if nCompletion < 1 then
nCompletion = #tCompletions
end
elseif param == keys.down then
nCompletion = nCompletion + 1
if nCompletion > #tCompletions then
nCompletion = 1
end
end
redraw()
elseif _tHistory then
-- Cycle history
clear()
if param == keys.up then
-- Up
if nHistoryPos == nil then
if #_tHistory > 0 then
nHistoryPos = #_tHistory
end
elseif nHistoryPos > 1 then
nHistoryPos = nHistoryPos - 1
end
else
-- Down
if nHistoryPos == #_tHistory then
nHistoryPos = nil
elseif nHistoryPos ~= nil then
nHistoryPos = nHistoryPos + 1
end
end
if nHistoryPos then
sLine = _tHistory[nHistoryPos]
nPos, nScroll = #sLine, 0
else
sLine = ""
nPos, nScroll = 0, 0
end
uncomplete()
redraw()
end
elseif param == keys.backspace then
-- Backspace
if nPos > 0 then
clear()
sLine = string.sub(sLine, 1, nPos - 1) .. string.sub(sLine, nPos + 1)
nPos = nPos - 1
if nScroll > 0 then nScroll = nScroll - 1 end
recomplete()
redraw()
end
elseif param == keys.home then
-- Home
if nPos > 0 then
clear()
nPos = 0
recomplete()
redraw()
end
elseif param == keys.delete then
-- Delete
if nPos < #sLine then
clear()
sLine = string.sub(sLine, 1, nPos) .. string.sub(sLine, nPos + 2)
recomplete()
redraw()
end
elseif param == keys["end"] then
-- End
if nPos < #sLine then
clear()
nPos = #sLine
recomplete()
redraw()
end
elseif param == keys.tab then
-- Tab (accept autocomplete)
acceptCompletion()
end
elseif sEvent == "mouse_click" or sEvent == "mouse_drag" and param == 1 then
local _, cy = term.getCursorPos()
if param1 >= sx and param1 <= w and param2 == cy then
-- Ensure we don't scroll beyond the current line
nPos = math.min(math.max(nScroll + param1 - sx, 0), #sLine)
redraw()
end
elseif sEvent == "term_resize" then
-- Terminal resized
w = term.getSize()
redraw()
end
end
local _, cy = term.getCursorPos()
term.setCursorBlink(false)
term.setCursorPos(w + 1, cy)
print()
return sLine
end
local function getFileSystem()
if filesystem then filesystem.rom = getRom() return filesystem end
if not fs.exists(fspath) then
return {rom=getRom()}
end
local file = fs.open(fspath, "r")
local compressedData = file.readAll()
file.close()
local decompressedData = deflate:DecompressDeflate(compressedData,{level=compression_level})
local data = textutils.unserialize(decompressedData) or {}
filesystem = data
data.rom = getRom()
return data
end
local function setFileSystem(data)
filesystem = data
data.rom = nil
data = textutils.serialize(data)
local file = fs.open(fspath, "w")
file.write(deflate:CompressDeflate(data,{level=compression_level}))
file.close()
end
local function normalizeParts(path)
local parts = {}
for part in string.gmatch(path, "[^/]+") do
if part == "" or part == "." then
-- skip
elseif part == ".." then
if #parts > 0 then table.remove(parts) end
else
parts[#parts + 1] = part
end
end
return parts
end
local function getDataAt(path)
expect(1, path, "string")
local parts = normalizeParts(path)
local current = getFileSystem()
if #parts == 0 then return current end
for i = 1, #parts do
local part = parts[i]
if type(current) ~= "table" then
-- trying to index into a file
return nil
end
current = current[part]
if current == nil then return nil end
end
return current
end
local function setDataAt(path, data)
expect(1, path, "string")
local parts = normalizeParts(path)
local filesys = getFileSystem()
local current = filesys
for i, part in ipairs(parts) do
if i == #parts then
current[part] = data
elseif type(current[part]) == "table" then
current = current[part]
elseif type(current[part]) == "string" then
return
end
end
setFileSystem(filesys)
end
function global.fs.list(path)
expect(1, path, "string")
local out = {}
local data = getDataAt(path)
if data then
for k, v in pairs(data) do
table.insert(out, k)
end
end
return out
end
function global.fs.isDir(path)
expect(1, path, "string")
local data = getDataAt(path)
return type(data) == "table"
end
function global.fs.isReadOnly(path)
if path == "" then return false end
expect(1, path, "string")
local offset = string.find(path, "rom")
return offset ~= nil and offset < 3
end
function global.fs.exists(path)
expect(1, path, "string")
local data = getDataAt(path)
return data ~= nil
end
function global.fs.complete(sPath, sLocation, bIncludeFiles, bIncludeDirs)
expect(1, sPath, "string")
expect(2, sLocation, "string")
local bIncludeHidden = nil
if type(bIncludeFiles) == "table" then
bIncludeDirs = field(bIncludeFiles, "include_dirs", "boolean", "nil")
bIncludeHidden = field(bIncludeFiles, "include_hidden", "boolean", "nil")
bIncludeFiles = field(bIncludeFiles, "include_files", "boolean", "nil")
else
expect(3, bIncludeFiles, "boolean", "nil")
expect(4, bIncludeDirs, "boolean", "nil")
end
bIncludeHidden = bIncludeHidden ~= false
bIncludeFiles = bIncludeFiles ~= false
bIncludeDirs = bIncludeDirs ~= false
local sDir = sLocation
local nStart = 1
local nSlash = string.find(sPath, "[/\\]", nStart)
if nSlash == 1 then
sDir = ""
nStart = 2
end
local sName
while not sName do
local nSlash = string.find(sPath, "[/\\]", nStart)
if nSlash then
local sPart = string.sub(sPath, nStart, nSlash - 1)
sDir = global.fs.combine(sDir, sPart)
nStart = nSlash + 1
else
sName = string.sub(sPath, nStart)
end
end
if global.fs.isDir(sDir) then
local tResults = {}
if bIncludeDirs and sPath == "" then
table.insert(tResults, ".")
end
if sDir ~= "" then
if sPath == "" then
table.insert(tResults, bIncludeDirs and ".." or "../")
elseif sPath == "." then
table.insert(tResults, bIncludeDirs and "." or "./")
end
end
local tFiles = global.fs.list(sDir)
for n = 1, #tFiles do
local sFile = tFiles[n]
if #sFile >= #sName and string.sub(sFile, 1, #sName) == sName and (
bIncludeHidden or sFile:sub(1, 1) ~= "." or sName:sub(1, 1) == "."
) then
local bIsDir = global.fs.isDir(fs.combine(sDir, sFile))
local sResult = string.sub(sFile, #sName + 1)
if bIsDir then
table.insert(tResults, sResult .. "/")
if bIncludeDirs and #sResult > 0 then
table.insert(tResults, sResult)
end
else
if bIncludeFiles and #sResult > 0 then
table.insert(tResults, sResult)
end
end
end
end
return tResults
end
return {}
end
local function getReadHandle(path, isBinary)
expect(1, path, "string")
local data = getDataAt(path) or ""
local len = #data
local seek = 1
return {
readAll = function()
if data == nil then return nil end
data = data:sub(seek)
seek = len + 1
return data
end,
readLine = function(includeTrailing)
if data == nil then return nil end
if seek > len then return nil end -- EOF guard
local start = seek
local nl = data:find("\n", start, true)
local line
if nl then
if includeTrailing then
line = data:sub(start, nl) -- include "\n"
else
line = data:sub(start, nl - 1) -- exclude "\n"
end
seek = nl + 1
else
-- last line (no trailing newline)
line = data:sub(start)
seek = len + 1
end
-- NOTE: do NOT treat "" as EOF; blank lines are valid
return line
end,
read = function(count)
if type(count) == "table" then
count = 1
end
expect(1, count, "number", "nil")
if data == nil then return nil end
if not count then count = 1 end
if seek > len then return nil end
local toReturn = string.sub(data, seek, seek + count - 1)
seek = seek + #toReturn
if toReturn == "" then
return nil
elseif isBinary and count == 1 then
return string.byte(toReturn)
end
return toReturn
end,
seek = function (offset, whence)
if data == nil then return nil, "File is closed" end
offset = offset or 0
whence = whence or "cur"
if whence == "set" then
seek = math.min(math.max(0, offset), len) + 1
elseif whence == "cur" then
seek = math.min(math.max(1, seek + offset), len + 1)
elseif whence == "end" then
seek = math.min(math.max(0, len + offset), len) + 1
end
return seek - 1
end,
close = function()
data = nil
end,
}
end
local function getWriteHandle(path, isBinary, append_mode)
local data = ""
local seek = 1
if append_mode then
data = getDataAt(path) or ""
seek = #data + 1
end
local handle -- forward declared so writeLine can see it
local function hwrite(str)
if data == nil then error("File is closed", 2) end
if type(str) == "number" and isBinary then
str = string.char(str)
elseif type(str) ~= "string" then
error("bad argument #1 (string or number expected, got " .. type(str) .. " )", 2)
end
data = string.sub(data, 1, seek - 1) .. str .. string.sub(data, seek + #str)
seek = seek + #str
end
handle = {
write = hwrite,
writeLine = function (str)
if data == nil then error("File is closed", 2) end
expect(1, str, "string")
hwrite(str .. "\n") -- **no global write**
end,
flush = function()
if data == nil then error("File is closed", 2) end
setDataAt(path, data)
end,
close = function()
if data == nil then error("File is already closed", 2) end
setDataAt(path, data)
data = nil
end,
}
return handle
end
local function getReadWriteHandle(path, isBinary, erase_data)
expect(1, path, "string")
local data = erase_data and "" or (getDataAt(path) or "")
local len = #data
local seek = 1
local handle = {}
handle.readAll = function()
if data == nil then return nil end
data = data:sub(seek)
seek = len + 1
return data
end
handle.readLine = function(includeTrailing)
if data == nil then return nil end
if seek > len then return nil end -- EOF guard
local start = seek
local nl = data:find("\n", start, true)
local line
if nl then
if includeTrailing then
line = data:sub(start, nl)
else
line = data:sub(start, nl - 1)
end
seek = nl + 1
else
line = data:sub(start)
seek = len + 1
end
return line
end
function handle.read(count)
if type(count) == "table" then
count = 1
end
expect(1, count, "number", "nil")
if data == nil then error("File is closed", 2) end
if not count then count = 1 end
if seek > len then return nil end
local toReturn = data:sub(seek, seek + count - 1)
seek = seek + #toReturn
if toReturn == "" then
return nil
elseif isBinary and count == 1 then
return string.byte(toReturn)
end
return toReturn
end
function handle.seek(offset, whence)
if data == nil then return nil, "File is closed" end
offset = offset or 0
whence = whence or "cur"
if whence == "set" then
seek = math.min(math.max(0, offset), len) + 1
elseif whence == "cur" then
seek = math.min(math.max(1, seek + offset), len + 1)
elseif whence == "end" then
seek = math.min(math.max(0, len + offset), len) + 1
else
error("bad argument #2 (invalid whence)", 2)
end
return seek - 1
end
function handle.write(str)
if type(str) == "number" and isBinary then
str = string.char(str)
elseif type(str) ~= "string" then
error("bad argument #1 (string or number expected, got " .. type(str) .. " )", 2)
end
if data == nil then error("File is closed", 2) end
data = string.sub(data, 1, seek - 1) .. str .. string.sub(data, seek + #str)
seek = seek + #str
len = #data
end
function handle.writeLine(str)
expect(1, str, "string")
handle.write(str .. "\n")
end
function handle.flush()
if data == nil then error("File is closed", 2) end
setDataAt(path, data)
end
function handle.close()
if data == nil then error("File is already closed", 2) end
setDataAt(path, data)
data = nil
end
return handle
end
local function ioHandleWrapper(handle)
local closed = false
return {
read = function (self, mode)
if closed then error("File is closed", 2) end
expect(2, mode, "string", "nil")
if not mode then mode = "l" end
if type(self) ~= "table" then
error("bad argument #1 (FILE expected, got " .. type(self) .. " )", 2)
end
if string.find(mode, "%*") == 1 then
mode = mode:gsub("%*", "")
end
if mode == "l" then
return handle.readLine(false)
elseif mode == "L" then
return handle.readLine(true)
elseif mode == "a" then
return handle.readAll()
else
error("Unsupported read mode", 2)
end
end,
seek = function (self, whence, offset)
if closed then error("File is closed", 2) end
expect(2, whence, "string", "nil")
expect(3, offset, "number", "nil")
if type(self) ~= "table" then
error("bad argument #1 (FILE expected, got " .. type(self) .. " )", 2)
end
return handle.seek(offset, whence)
end,
write = function (self, ...)
if closed then error("File is closed", 2) end
if type(self) ~= "table" then
error("bad argument #1 (FILE expected, got " .. type(self) .. " )", 2)
end
local args = { ... }
for i = 1, #args do
local arg = args[i]
if type(arg) ~= "string" and type(arg) ~= "number" then
error("bad argument #" .. (i + 1) ..
" (string or number expected, got " .. type(arg) .. " )", 2)
end
-- preserve your existing semantics: underlying handle decides what to do
handle.write(arg)
end
end,
close = function (self)
if closed then error("File is already closed", 2) end
if type(self) ~= "table" then
error("bad argument #1 (FILE expected, got " .. type(self) .. " )", 2)
end
handle.close()
closed = true
end,
flush = function (self)
if closed then error("File is closed", 2) end
if type(self) ~= "table" then
error("bad argument #1 (FILE expected, got " .. type(self) .. " )", 2)
end
handle.flush()
end,
lines = function (self)
if closed then error("File is closed", 2) end
if type(self) ~= "table" then
error("bad argument #1 (FILE expected, got " .. type(self) .. " )", 2)
end
return function()
if closed then error("file is already closed", 2) end
return handle.readLine(false)
end
end,
}
end
function global.fs.open(path,mode)
expect(1, path, "string")
expect(2, mode, "string")
if mode == "r" or mode == "rb" then
if global.fs.exists(path) then
return getReadHandle(path, mode == "rb")
else
return nil, "File not found"
end
elseif mode == "w" or mode == "wb" then
return getWriteHandle(path, mode == "wb", false)
elseif mode == "a" or mode == "ab" then
return getWriteHandle(path, mode == "ab", true)
elseif mode == "r+" or mode == "rb+" or mode == "r+b" then
if global.fs.exists(path) then
return getReadWriteHandle(path, mode == "rb+" or mode == "r+b", false)
else
return nil, "File not found"
end
elseif mode == "w+" or mode == "wb+" or mode == "w+b" then
return getReadWriteHandle(path, mode == "wb+" or mode == "w+b", true)
elseif mode == "a+" or mode == "ab+" or mode == "a+b" then
return getReadWriteHandle(path, mode == "ab+" or mode == "a+b", true)
else
return nil, "Invalid read mode"
end
end
function global.fs.move(srcPath, dstPath)
expect(1, srcPath, "string")
expect(2, dstPath, "string")
if not global.fs.exists(srcPath) then
error("Source does not exist", 2)
end
if global.fs.exists(dstPath) then
error("Destination already exists", 2)
end
local data = getDataAt(srcPath)
setDataAt(dstPath, data)
setDataAt(srcPath, nil)
end
function global.fs.copy(srcPath, dstPath)
expect(1, srcPath, "string")
expect(2, dstPath, "string")
if not global.fs.exists(srcPath) then
error("Source does not exist", 2)
end
if global.fs.exists(dstPath) then
error("Destination already exists", 2)
end
local data = getDataAt(srcPath)
setDataAt(dstPath, data)
end
local function find_aux(path, parts, i, out)
local part = parts[i]
if not part then
-- If we're at the end of the pattern, ensure our path exists and append it.
if global.fs.exists(path) then out[#out + 1] = path end
elseif part.exact then
-- If we're an exact match, just recurse into this directory.
return find_aux(global.fs.combine(path, part.contents), parts, i + 1, out)
else
-- Otherwise we're a pattern. Check we're a directory, then recurse into each
-- matching file.
if not global.fs.isDir(path) then return end
local files = global.fs.list(path)
for j = 1, #files do
local file = files[j]
if file:find(part.contents) then find_aux(global.fs.combine(path, file), parts, i + 1, out) end
end
end
end
local find_escape = {
-- Escape standard Lua pattern characters
["^"] = "%^", ["$"] = "%$", ["("] = "%(", [")"] = "%)", ["%"] = "%%",
["."] = "%.", ["["] = "%[", ["]"] = "%]", ["+"] = "%+", ["-"] = "%-",
-- Aside from our wildcards.
["*"] = ".*",
["?"] = ".",
}
function global.fs.find(pattern)
expect(1, pattern, "string")
pattern = global.fs.combine(pattern) -- Normalise the path, removing ".."s.
-- If the pattern is trying to search outside the computer root, just abort.
-- This will fail later on anyway.
if pattern == ".." or pattern:sub(1, 3) == "../" then
error("/" .. pattern .. ": Invalid Path", 2)
end
-- If we've no wildcards, just check the file exists.
if not pattern:find("[*?]") then
if global.fs.exists(pattern) then return { pattern } else return {} end
end
local parts = {}
for part in pattern:gmatch("[^/]+") do
if part:find("[*?]") then
parts[#parts + 1] = {
exact = false,
contents = "^" .. part:gsub(".", find_escape) .. "$",
}
else
parts[#parts + 1] = { exact = true, contents = part }
end
end
local out = {}
find_aux("", parts, 1, out)
return out
end
function global.io.open(path, mode)
expect(1, path, "string")
expect(2, mode, "string", "nil")
if not mode then mode = "r" end
local handle
if mode == "r" or mode == "rb" then
if global.fs.exists(path) and not global.fs.isDir(path) then
handle = getReadHandle(path, mode == "rb")
elseif global.fs.isDir(path) then
return nil, "Attempt to open a directory"
else
return nil, "File not found"
end
elseif mode == "w" or mode == "wb" then
handle = getWriteHandle(path, mode == "wb", false)
elseif mode == "a" or mode == "ab" then
handle = getWriteHandle(path, mode == "ab", true)
elseif mode == "r+" or mode == "rb+" or mode == "r+b" then
if global.fs.exists(path) and not global.fs.isDir(path) then
handle = getReadWriteHandle(path, mode == "rb+" or mode == "r+b", false)
elseif global.fs.isDir(path) then
return nil, "Attempt to open a directory"
else
return nil, "File not found"
end
elseif mode == "w+" or mode == "wb+" or mode == "w+b" then
handle = getReadWriteHandle(path, mode == "wb+" or mode == "w+b", true)
elseif mode == "a+" or mode == "ab+" or mode == "a+b" then
handle = getReadWriteHandle(path, mode == "ab+" or mode == "a+b", true)
else
return nil, "Invalid mode"
end
return ioHandleWrapper(handle)
end
function global.io.lines(path,...)
local args = {...}
expect(1, path, "string")
local handle = global.io.open(path, table.unpack(args))
local count = 0
return function ()
count = count + 1
local line = handle:read(args[count%#args + 1] or "l")
if line == nil then
handle:close()
end
return line
end
end
function global.fs.delete(path)
expect(1, path, "string")
if global.fs.exists(path) then
setDataAt(path, nil)
end
end
function global.fs.getSize(path)
expect(1, path, "string")
if not global.fs.exists(path) then
error("File not found", 2)
end
if global.fs.isDir(path) then
return 0
else
return #getDataAt(path)
end
end
function global.fs.makeDir(path)
expect(1, path, "string")
if global.fs.exists(path) then
return
end
setDataAt(path, {})
end
local tAPIsLoading = {}
local bAPIError = false
function global.loadfile(filename, mode, env)
-- Support the previous `loadfile(filename, env)` form instead.
if type(mode) == "table" and env == nil then
mode, env = nil, mode
end
expect(1, filename, "string")
expect(2, mode, "string", "nil")
expect(3, env, "table", "nil")
local file = global.fs.open(filename, "r")
if not file then return nil, "File not found" end
local func, err = load(file.readAll(), "@/" .. fs.combine(filename), mode, env)
file.close()
return func, err
end
function global.dofile(_sFile)
expect(1, _sFile, "string")
local fnFile, e = global.loadfile(_sFile, nil, global)
if fnFile then
return fnFile()
else
error(e, 2)
end
end
function global.loadAPI(_sPath)
expect(1, _sPath, "string")
local sName = fs.getName(_sPath)
if sName:sub(-4) == ".lua" then
sName = sName:sub(1, -5)
end
if sName == "term" then
return true
end
if tAPIsLoading[sName] == true then
printError("API " .. sName .. " is already being loaded")
return false
end
tAPIsLoading[sName] = true
local tEnv = {}
setmetatable(tEnv, { __index = global })
local fnAPI, err = global.loadfile(_sPath, nil, tEnv)
if fnAPI then
local ok, err = pcall(fnAPI)
if not ok then
tAPIsLoading[sName] = nil
return error("Failed to load API " .. sName .. " due to " .. err, 1)
end
else
tAPIsLoading[sName] = nil
return error("Failed to load API " .. sName .. " due to " .. err, 1)
end
local tAPI = {}
for k, v in pairs(tEnv) do
if k ~= "_ENV" then
tAPI[k] = v
end
end
global[sName] = tAPI
tAPIsLoading[sName] = nil
return true
end
local function load_apis(dir)
if not global.fs.isDir(dir) then return end
for _, file in ipairs(global.fs.list(dir)) do
if file:sub(1, 1) ~= "." then
local path = fs.combine(dir, file)
if not fs.isDir(path) then
if not global.loadAPI(path) then
bAPIError = true
end
end
end
end
end
function global.os.run(_tEnv, _sPath, ...)
expect(1, _tEnv, "table")
expect(2, _sPath, "string")
local tEnv = _tEnv
setmetatable(tEnv, { __index = global })
if settings.get("bios.strict_globals", false) then
-- load will attempt to set _ENV on this environment, which
-- throws an error with this protection enabled. Thus we set it here first.
tEnv._ENV = tEnv
getmetatable(tEnv).__newindex = function(_, name)
error("Attempt to create global " .. tostring(name), 2)
end
end
local fnFile, err = global.loadfile(_sPath, nil, tEnv)
if fnFile then
local ok, err = pcall(fnFile, ...)
if not ok then
if err and err ~= "" then
printError(err)
end
return false
end
return true
end
if err and err ~= "" then
printError(err)
end
return false
end
if network and perms.network then
global.network = network
end
if app and perms.app then
global.app = deepcopy(app)
global.app.launch = function(id,app_perms) app.launch(id,app_perms,perms) end
end
if global and not perms.peripheral then
global.peripheral.wrap = function () end
global.peripheral.find = function () end
global.peripheral.getNames = function () return {} end
global.peripheral.getType = function () end
global.peripheral.isPresent = function () return false end
global.peripheral.hasType = function () end
global.peripheral.call = function () end
global.peripheral.getMethods = function () end
end
if repo and perms.repo then
global.repo = repo
end
global.settings = settings
global._ENV = global
global._G = global
global.shell = nil
global.multishell = nil
global.term = term
global.write = write
global.read = read
global.print = print
global.printError = printError
if type(term_override) == "table" then
global.term.redirect(term_override)
end
load_apis("rom/apis")
if http and perms.http then global.http = http load_apis("rom/apis/http") end
return global
end
local exception = dofile("rom/modules/main/cc/internal/tiny_require.lua")("cc.internal.exception")
local function create(...)
local barrier_ctx = { co = coroutine.running() }
local functions = table.pack(...)
local threads = {}
for i = 1, functions.n, 1 do
local fn = functions[i]
if type(fn) ~= "function" then
error("bad argument #" .. i .. " (function expected, got " .. type(fn) .. ")", 3)
end
threads[i] = { co = coroutine.create(function() return exception.try_barrier(barrier_ctx, fn) end), filter = nil }
end
return threads
end
local function runUntilLimit(threads, limit)
local count = #threads
if count < 1 then return 0 end
local living = count
local event = { n = 0 }
while true do
for i = 1, count do
local thread = threads[i]
if thread and (thread.filter == nil or thread.filter == event[1] or event[1] == "terminate") then
local ok, param = coroutine.resume(thread.co, table.unpack(event, 1, event.n))
if ok then
if param == "thread_shutdown" then
return {command=true,type="shutdown"}
elseif param == "thread_reboot" then
print("reboot")
return {command=true,type="reboot"}
else
thread.filter = param
end
elseif type(param) == "string" and exception.can_wrap_errors() then
printError(exception.make_exception(param, thread.co))
else
printError(param)
end
if coroutine.status(thread.co) == "dead" then
threads[i] = false
living = living - 1
if living <= limit then
return i
end
end
end
end
event = table.pack(os.pullEventRaw())
end
end
function lib.start(env)
local command = nil
env.os.reboot = function() command = "reboot" sleep() end
env.os.shutdown = function() command = "shutdown" sleep() end
local func = function ()
command = nil
settings.define("shell.allow_startup", {
default = true,
description = "Run startup files when the computer turns on.",
type = "boolean",
})
settings.define("shell.allow_disk_startup", {
default = commands == nil,
description = "Run startup files from disk drives when the computer turns on.",
type = "boolean",
})
settings.define("shell.autocomplete", {
default = true,
description = "Autocomplete program and arguments in the shell.",
type = "boolean",
})
settings.define("edit.autocomplete", {
default = true,
description = "Autocomplete API and function names in the editor.",
type = "boolean",
})
settings.define("lua.autocomplete", {
default = true,
description = "Autocomplete API and function names in the Lua REPL.",
type = "boolean",
})
settings.define("edit.default_extension", {
default = "lua",
description = [[The file extension the editor will use if none is given. Set to "" to disable.]],
type = "string",
})
settings.define("paint.default_extension", {
default = "nfp",
description = [[The file extension the paint program will use if none is given. Set to "" to disable.]],
type = "string",
})
settings.define("list.show_hidden", {
default = false,
description = [[Whether the list program show hidden files (those starting with ".").]],
type = "boolean",
})
settings.define("motd.enable", {
default = pocket == nil,
description = "Display a random message when the computer starts up.",
type = "boolean",
})
settings.define("motd.path", {
default = "/rom/motd.txt:/motd.txt",
description = [[The path to load random messages from. Should be a colon (":") separated string of file paths.]],
type = "string",
})
settings.define("lua.warn_against_use_of_local", {
default = true,
description = [[Print a message when input in the Lua REPL starts with the word 'local'. Local variables defined in the Lua REPL are be inaccessible on the next input.]],
type = "boolean",
})
settings.define("lua.function_args", {
default = true,
description = "Show function arguments when printing functions.",
type = "boolean",
})
settings.define("lua.function_source", {
default = false,
description = "Show where a function was defined when printing functions.",
type = "boolean",
})
settings.define("bios.strict_globals", {
default = false,
description = "Prevents assigning variables into a program's environment. Make sure you use the local keyword or assign to _G explicitly.",
type = "boolean",
})
settings.define("bios.use_multishell", {
default = true,
description = "Allow running multiple program at once, through the use of the \"fg\" and \"bg\" programs..",
type = "boolean",
})
settings.define("shell.autocomplete_hidden", {
default = false,
description = [[Autocomplete hidden files and folders (those starting with ".").]],
type = "boolean",
})
print("running container!")
return runUntilLimit(create(function()
local sShell
if term.isColour() and settings.get("bios.use_multishell") then
sShell = "rom/programs/advanced/multishell.lua"
else
sShell = "rom/programs/shell.lua"
end
os.run({}, sShell)
os.run({}, "rom/programs/shutdown.lua")
end,function ()
while true do
sleep()
if command == "shutdown" then
while true do coroutine.yield("thread_shutdown") end
elseif command == "reboot" then
while true do coroutine.yield("thread_reboot") end
end
end
end),0)
end
local command = "starting"
while command == "starting" or (type(command) == "table" and command.command and command.type=="reboot") do
command = setfenv(func,deepcopy(env))()
end
end
return lib