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