diff --git a/repository/apps/app-metamachine.lua b/repository/apps/app-metamachine.lua new file mode 100644 index 0000000..c45c7e3 --- /dev/null +++ b/repository/apps/app-metamachine.lua @@ -0,0 +1,538 @@ +-- This is released into the public domain. +-- No warranty is provided, implied or otherwise. + +-- app-metamachine.lua : Virtual Machine +-- Authors: 20kdc + +local loaderPkg, loaderPid, vmName = ... + +local icecap = neo.requireAccess("x.neo.pub.base", "fs") + +local libVGPU = require("metamachine-vgpu") + +local vmBaseCoroutineWrap +local vmComponent, vmComputer, vmOs +local vmEnvironment +local vmSelfdestruct = false +local vmSuperVM = true +local signalStack = {} +local postVMRList = {} + +-- File structure: +-- vm-* : Virtual machine configuration +-- vm- + +local vmConfiguration = { + -- true : Physical + -- {type, ...} : Virtual + -- NOTE : The following rules are set. + -- k-computer always exists + -- k-gpu always exists + -- k-log always exists + -- k-tmpfs always exists in non-Super VMs + ["world"] = {"filesystem", "/", false}, + ["eeprom"] = {"eeprom", "/confboot.lua", "/confdata.bin", "Configurator", true}, + ["screen"] = {"screen", "configurator", 50, 15, 8} +} + +if vmName then + neo.ensurePathComponent("vm-" .. vmName) + vmSuperVM = false + local f = icecap.open("/vm-" .. vmName, false) + vmConfiguration = require("serial").deserialize(f.read("*a")) + f.close() + if not vmConfiguration then error("The VM configuration was unloadable.") end + vmConfiguration["k-tmpfs"] = {"filesystem", "/vt-" .. vmName .. "/", false} +end + +local function clone(t) + if type(t) == "table" then + local b = {} + for k, v in pairs(t) do + b[k] = v + end + return b + end + return t +end + +-- by window ID = {address, internal} +local screensInt = { +} +-- by component address = callback +local screensAll = { +} + +local tmpAddress = "k-tmpfs" +local passthroughs = {} +local components = { + ["k-computer"] = { + type = "computer", + beep = function () + end, + start = function () + return false + end, + stop = function () + vmSelfdestruct = true + coroutine.yield(0.05) + end, + isRunning = function () + return true + end, + getProgramLocations = function () + -- Entries of {"file", "lootdisk"} + return {} + end + }, + ["k-gpu"] = libVGPU.newGPU(screensAll), + ["k-log"] = { + type = "ocemu", + log = neo.emergency + } +} +-- Clones of components made on-demand. +local proxies = {} +setmetatable(proxies, {__mode = "v"}) + +vmComponent = { + list = function (filter, exact) + -- This is an iterator :( + local t = {} + for k, v in pairs(components) do + local ok = false + if filter then + if v.type == filter or ((not exact) and v.type:match(filter, 1, true)) then + ok = true + end + else + ok = true + end + if ok then + table.insert(t, {k, v.type}) + end + end + return function () + local tr1 = table.remove(t, 1) + if not tr1 then return end + return table.unpack(tr1) + end, 9, nil + end, + invoke = function (com, me, ...) + if not components[com] then error("no component " .. com) end + if not components[com][me] then error("no method " .. com .. "." .. me) end + return components[com][me](...) + end, + proxy = function (com) + if not components[com] then + return nil, "no such component" + end + local p = proxies[com] + if p then return p end + p = clone(components[com]) + p.address = com + p.fields = {} + p.slot = 0 + proxies[com] = p + return p + end, + type = function (com) + if not components[com] then + return nil, "no such component" + end + return components[com].type + end, + methods = function (com) + local mt = {} + for k, v in pairs(components[address]) do + if type(v) == "function" then + mt[k] = true + end + end + return mt + end, + fields = function (com) + -- This isn't actually supported, + -- because fields are bad-sec nonsense. + -- Luckily, everybody knows this, so nobody uses them. + return {} + end, + doc = function (address, method) + if not components[address] then + error("No such component " .. address) + end + if not components[address][method] then + error("No such method " .. method) + end + return tostring(components[address][method]) + end +} + +-- Prepare configured components +local insertionCallbacks = { + ["screen"] = function (address, title, w, h, d) + local activeLines = {} + local scrW = neo.requireAccess("x.neo.pub.window", "primary window")(w, h, title) + local gpuC, scrI, scrC + gpuC, scrI, scrC = libVGPU.newBuffer(scrW, {address .. "-kb"}, w, h, function (nw, nh) + table.insert(signalStack, {"screen_resized", address, nw, nh}) + end, function (l) + if activeLines[l] then + return + end + activeLines[l] = true + table.insert(postVMRList, function () + scrI.line(l) + activeLines = {} + end) + end) + components[address] = scrC + components[address .. "-kb"] = {type = "keyboard"} + screensInt[scrW.id] = {address, scrI} + screensAll[address] = gpuC + end, + ["eeprom"] = function (address, boot, data, name, ro) + local codeSize = 4096 + local dataSize = 256 + local function getCore(fd) + local f = icecap.open(fd, false) + if not f then return "" end + local contents = f.read("*a") + f.close() + return contents + end + local function setCore(fd, size, contents) + checkArg(1, contents, "string") + if #contents > size then return nil, "too large" end + if ro then + return nil, "storage is readonly" + end + local f = icecap.open(fd, true) + if not f then return nil, "storage is readonly" end + f.write(contents) + f.close() + return true + end + components[address] = { + type = "eeprom", + get = function () + return getCore(boot) + end, + set = function (contents) + return setCore(boot, codeSize, contents) + end, + makeReadonly = function () + ro = true + return true + end, + getChecksum = function () + return "00000000" + end, + getSize = function () + return codeSize + end, + getDataSize = function () + return dataSize + end, + getData = function () + return getCore(data) + end, + setData = function () + return setCore(data, dataSize, contents) + end + } + end, + ["filesystem"] = function (address, path, ro) + components[address] = require("metamachine-vfs")(icecap, address, path, ro) + end +} + +for k, v in pairs(vmConfiguration) do + if type(v) == "string" then + local root = neo.requireAccess("k.root", "component passthrough") + local ty = root.component.type(k) + if ty then + passthroughs[k] = true + components[k] = root.component.proxy(k) + if ty == "screen" then + -- Need to ensure the screen in question is for the taking + local div = neo.requireAccess("x.neo.sys.session", "ability to divorce screens") + div.disclaimMonitor(k) + local div2 = neo.requireAccess("x.neo.sys.screens", "ability to claim screens") + screensAll[k] = div2.claim(k) + assert(screensAll[k], "Hardware screen " .. k .. " unavailable.") + end + end + else + assert(insertionCallbacks[v[1]], "Cannot insert virtual " .. v[1]) + insertionCallbacks[v[1]](k, table.unpack(v, 2)) + end +end + +vmOs = clone(os) + +vmComputer = {} +vmComputer.shutdown = function (...) + vmSelfdestruct = true + coroutine.yield(0.05) +end +vmComputer.pushSignal = function (...) + table.insert(signalStack, {...}) +end +vmComputer.pullSignal = function (time) + if not signalStack[1] then + if type(time) == "number" then + time = time + os.uptime() + coroutine.yield(time) + if not signalStack[1] then + return + end + else + while not signalStack[1] do + coroutine.yield(math.huge) + end + end + end + return table.unpack(table.remove(signalStack, 1)) +end + +vmComputer.totalMemory = os.totalMemory +vmOs.totalMemory = nil +vmComputer.freeMemory = os.freeMemory +vmOs.freeMemory = nil +vmComputer.energy = os.energy +vmOs.energy = nil +vmComputer.maxEnergy = os.maxEnergy +vmOs.maxEnergy = nil +vmComputer.uptime = os.uptime +vmOs.uptime = nil +vmComputer.address = os.address +vmOs.address = nil + +vmComputer.isRobot = function () + return false +end +vmComputer.address = function () + return "k-computer" +end +vmComputer.tmpAddress = function () + return tmpAddress +end + +vmComputer.getBootAddress = function () + return "k-eeprom" +end +vmComputer.setBootAddress = function () +end +vmComputer.users = function () + return {} +end +vmComputer.addUser = function () + return false, "user support not available" +end +vmComputer.removeUser = function () + return false, "user support not available" +end +vmComputer.beep = function (...) + return vmComponent.invoke("k-computer", "beep", ...) +end +vmComputer.getDeviceInfo = function (...) + return vmComponent.invoke("k-computer", "getDeviceInfo", ...) +end +vmComputer.getProgramLocations = function (...) + return vmComponent.invoke("k-computer", "getProgramLocations", ...) +end +vmComputer.getArchitectures = function (...) + return vmComponent.invoke("k-computer", "getArchitectures", ...) +end +vmComputer.getArchitecture = function (...) + return vmComponent.invoke("k-computer", "getArchitecture", ...) +end +vmComputer.setArchitecture = function (...) + return vmComponent.invoke("k-computer", "setArchitecture", ...) +end + +vmUnicode = clone(unicode) +vmUnicode.safeTextSupport = nil +vmUnicode.undoSafeTextSupport = nil + +vmEnvironment = { + _VERSION = _VERSION, + component = vmComponent, + computer = vmComputer, + table = clone(table), + math = clone(math), + string = clone(string), + unicode = vmUnicode, + -- Scheme here: + -- A yield's first argument is nil for an actual yield, + -- or the time to add a timer at (math.huge if no timeout) for a pullSignal. + -- This is not exactly the same, but is very similar, to that of machine.lua, + -- differing mainly in how pullSignal timeout scheduling occurs. + coroutine = { + yield = function (...) + return coroutine.yield(nil, ...) + end, + -- The way this is defined by machine.lua makes it true even when it arguably shouldn't be. Oh well. + isyieldable = coroutine.isyieldable, + status = coroutine.status, + create = function (f) + return coroutine.create(function (...) + return nil, f(...) + end) + end, + running = coroutine.running, + wrap = function (f) + local pf = coroutine.wrap(function (...) + return nil, f(...) + end) + return function (...) + local last = {...} + while true do + local tabpack = {pf(table.unpack(last))} + if not tabpack[1] then + return table.unpack(tabpack, 2) + end + last = {coroutine.yield(tabpack[1])} + end + end + end, + resume = function (co, ...) + local last = {...} + while true do + local tabpack = {coroutine.resume(co, table.unpack(last))} + if not tabpack[1] then + neo.emergency(co, table.unpack(tabpack)) + return table.unpack(tabpack) + elseif not tabpack[2] then + return tabpack[1], table.unpack(tabpack, 3) + end + last = {coroutine.yield(tabpack[2])} + end + end + }, + os = vmOs, + debug = clone(debug), + bit32 = clone(bit32), + utf8 = clone(utf8), + assert = assert, + ipairs = ipairs, + next = next, + load = function (a, b, c, d) + if rawequal(d, nil) then + d = vmEnvironment + end + return load(a, b, c, d) + end, + + pairs = pairs, pcall = function (...) + local r = {pcall(...)} + if not r[1] then + neo.emergency("pcall error:", table.unpack(r, 2)) + end + return table.unpack(r) + end, + xpcall = xpcall, select = select, + type = type, error = error, + tonumber = tonumber, tostring = tostring, + + setmetatable = setmetatable, getmetatable = getmetatable, + rawset = rawset, rawget = rawget, + rawlen = rawlen, rawequal = rawequal, + checkArg = checkArg +} +vmEnvironment._G = vmEnvironment + +if vmSuperVM then + vmEnvironment._MMstartVM = function (vmName) + neo.executeAsync("app-metamachine", vmName) + end + vmEnvironment._MMserial = function (...) + return require("serial").serialize(...) + end + vmEnvironment._MMdeserial = function (...) + return require("serial").deserialize(...) + end + vmEnvironment.print = neo.emergency + local root = neo.requestAccess("k.root") + if root then + vmEnvironment._MMcomList = root.component.list + else + vmEnvironment._MMcomList = function () + return function () + end + end + end +end + +-- bootstrap + +vmBaseCoroutineWrap = coroutine.wrap(function () + vmBaseCoroutine = coroutine.running() + local eepromAddress = vmComponent.list("eeprom")() + if not eepromAddress then + error("No EEPROM") + end + local code = vmComponent.invoke(eepromAddress, "get") + local res, f = load(code, "=eeprom", "t", vmEnvironment) + if not res then + error(f) + else + res() + end +end) + +while ((not vmBaseCoroutine) or (coroutine.status(vmBaseCoroutine) ~= "dead")) and not vmSelfdestruct do + local details = {vmBaseCoroutineWrap()} + while postVMRList[1] do + table.remove(postVMRList, 1)() + end + if details[1] then + local checkTimer = nil + if details[1] ~= math.huge then + checkTimer = neo.scheduleTimer(details[1]) + --neo.emergency("metamachine timer " .. details[1]) + else + --neo.emergency("metamachine HANG") + end + while true do + local ev = {coroutine.yield()} + if ev[1] == "k.timer" then + if ev[2] == checkTimer then + break + end + elseif ev[1]:sub(1, 2) == "h." then + if passthroughs[ev[2]] then + ev[1] = ev[1]:sub(3) + table.insert(signalStack, ev) + break + end + elseif ev[1] == "x.neo.pub.window" then + local id = ev[2] + if ev[3] == "key" then + if ev[6] then + table.insert(signalStack, {"key_down", screensInt[id][1] .. "-kb", ev[4], ev[5], "neo"}) + else + table.insert(signalStack, {"key_up", screensInt[id][1] .. "-kb", ev[4], ev[5], "neo"}) + end + break + elseif ev[3] == "line" then + screensInt[id][2].line(ev[4]) + elseif ev[3] == "touch" or ev[3] == "drag" or ev[3] == "drop" or ev[3] == "scroll" then + local x = ev[4] + local y = ev[5] + if screensInt[id][2].precise then + x = (x - 1) + ev[6] + y = (y - 1) + ev[7] + end + table.insert(signalStack, {ev[3], screensInt[id][1], x, y, ev[8], "neo"}) + break + elseif ev[3] == "close" then + return + end + end + end + else + error("Yield in root coroutine") + end +end diff --git a/repository/data/app-claw/local.lua b/repository/data/app-claw/local.lua index ce99e31..40f8154 100644 --- a/repository/data/app-claw/local.lua +++ b/repository/data/app-claw/local.lua @@ -24,7 +24,7 @@ return { }, ["neo-docs"] = { desc = "KittenOS NEO system documentation", - v = 2, + v = 3, deps = { "zzz-license-pd" }, @@ -106,6 +106,30 @@ return { "docs/repoauthors/svc-ghostie" }, }, + ["app-metamachine"] = { + desc = "Virtual machine", + v = 0, + deps = { + "neo", + "zzz-license-pd" + }, + dirs = { + "apps", + "libs", + "docs", + "docs/repoauthors", + "data", + "data/app-metamachine" + }, + files = { + "apps/app-metamachine.lua", + "libs/metamachine-vgpu.lua", + "libs/metamachine-vfs.lua", + "docs/repoauthors/app-metamachine", + "data/app-metamachine/confboot.lua", + "data/app-metamachine/lucaboot.lua" + }, + }, ["app-launchbar"] = { desc = "Application launcher bar", v = 0, @@ -115,6 +139,7 @@ return { }, dirs = { "apps", + "docs", "docs/repoauthors" }, files = { @@ -131,6 +156,7 @@ return { }, dirs = { "apps", + "docs", "docs/repoauthors" }, files = { diff --git a/repository/data/app-metamachine/confboot.lua b/repository/data/app-metamachine/confboot.lua new file mode 100644 index 0000000..d992063 --- /dev/null +++ b/repository/data/app-metamachine/confboot.lua @@ -0,0 +1,237 @@ +-- _MMstartVM(name) +-- _MMcomList(...) +-- _MMserial(str) +-- _MMdeserial(str) +local screen = component.proxy(component.list("screen", true)()) +local gpu = component.proxy(component.list("gpu", true)()) +local fs = component.proxy("world") + +screen.turnOn() +gpu.bind(screen.address) +gpu.setResolution(50, 15) +gpu.setForeground(0) +gpu.setBackground(0xFFFFFF) + +local menu +local currentY + +local currentVMId +local currentVM + +local genMainMenu, genFSSelector, genEditor + +function genFSSelector(cb) + local fsName = "" + menu = { + {" - Select VFS -", function () end}, + {"Cancel", cb}, + {"New FS: ", function () + fs.makeDirectory("fs-" .. fsName) + genFSSelector(cb) + end, function (text) + if text then fsName = text end + return fsName + end} + } + currentY = 2 + local fsl = fs.list("") + table.sort(fsl) + for k, v in ipairs(fsl) do + if v:sub(#v) == "/" and v:sub(1, 3) == "fs-" then + local id = v:sub(4, #v - 1) + table.insert(menu, {id, function () + cb(id) + end}) + table.insert(menu, {" Delete", function () + fs.remove("fs-" .. id) + genFSSelector(cb) + end}) + end + end +end + +local function doVMSave() + local f = fs.open("vm-" .. currentVMId, "wb") + if not f then error("VM Save failed...") end + fs.write(f, _MMserial(currentVM)) + fs.close(f) +end + +function genEditor() + menu = { +--01234567890123456789012345678901234567890123456789 + {" - configuring VM: " .. currentVMId, function () end}, + {"Save & Return", function () + doVMSave() + currentVM, currentVMId = nil + genMainMenu() + end}, + {"Save & Launch", function () + doVMSave() + _MMstartVM(currentVMId) + computer.shutdown() + end}, + {"Delete", function () + fs.remove("vm-" .. currentVMId) + currentVM, currentVMId = nil + genMainMenu() + end}, + } + currentY = 3 + for k, v in pairs(currentVM) do + local v1 = tostring(v) + if type(v) ~= "string" then + v1 = "virt. ".. v[1] + end + table.insert(menu, {"Del. " .. v1 .. " " .. k, function () + currentVM[k] = nil + genEditor() + end}) + end + table.insert(menu, {"+ Virtual FS (R/W)...", function () + genFSSelector(function (fsa) + if fsa then + currentVM["fs-" .. fsa] = {"filesystem", "/fs-" .. fsa .. "/", false} + end + genEditor() + end) + end}) + table.insert(menu, {"+ Virtual FS (R/O)...", function () + genFSSelector(function (fsa) + if fsa then + currentVM[fsa .. "-fs"] = {"filesystem", fsa, true} + end + genEditor() + end) + end}) + local tx = { + "+ Screen 50x15:", + "+ Screen 80x24:", + "+ Screen 160x49:" + } + local txw = { + 50, + 80, + 160 + } + local txh = { + 15, + 24, + 49 + } + for i = 1, 3 do + local cName = currentVMId .. "-screen" + local nt = 0 + while currentVM[cName] do + nt = nt + 1 + cName = currentVMId .. "-" .. nt + end + table.insert(menu, {tx[i], function () + currentVM[cName] = {"screen", cName, txw[i], txh[i], 8} + genEditor() + end, function (text) + if text then cName = text end + return cName + end}) + end + for address, ty in _MMcomList("") do + if (not currentVM[address]) and ty ~= "gpu" then + table.insert(menu, {"+ Host " .. ty .. " " .. address, function () + currentVM[address] = ty + genEditor() + end}) + end + end +end + +function genMainMenu() + local vmName = "" + menu = { +--01234567890123456789012345678901234567890123456789 + {" - metamachine configurator -- use keyboard - ", function () end}, + {"Shutdown", computer.shutdown}, + {"New VM: ", function () + local f = fs.open("vm-" .. vmName, "wb") + if not f then return end + fs.write(f, _MMserial({ + [vmName .. "-eeprom"] = {"eeprom", "/lucaboot.lua", "/vd-" .. vmName .. ".bin", "LUCcABOOT VM BIOS", true}, + [vmName .. "-screen"] = {"screen", vmName, 50, 15, 8} + })) + fs.close(f) + genMainMenu() + end, function (text) + if text then vmName = text end + return vmName + end} + } + currentY = 3 + local fsl = fs.list("") + table.sort(fsl) + for k, v in ipairs(fsl) do + if v:sub(#v) == "/" then + elseif v:sub(1, 3) == "vm-" then + local id = v:sub(4) + table.insert(menu, #menu, {id, function () + local f = fs.open("vm-" .. id, "rb") + if not f then return end + local str = "" + while true do + local sb = fs.read(f, 2048) + if not sb then break end + str = str .. sb + end + currentVM = _MMdeserial(str) or {} + fs.close(f) + currentVMId = id + genEditor() + end}) + end + end +end + +---- + +genMainMenu() + +local function draw() + gpu.fill(1, 1, 50, 15, " ") + local camera = math.max(0, math.min(math.floor(currentY - 7), #menu - 15)) + for i = 1, #menu do + local pfx = " " + if currentY == i then + pfx = "> " + end + local pox = "" + if menu[i][3] then + pox = menu[i][3]() + end + gpu.set(1, i - camera, pfx .. menu[i][1] .. pox) + end +end + +-- Final main loop. +draw() +while true do + local t = {computer.pullSignal()} + if t[1] == "key_down" then + if t[4] == 200 then + currentY = math.max(1, currentY - 1) + draw() + elseif t[4] == 208 then + currentY = math.min(currentY + 1, #menu) + draw() + elseif t[3] == 13 then + menu[currentY][2]() + draw() + elseif t[3] == 8 then + local tx = menu[currentY][3]() + menu[currentY][3](unicode.sub(tx, 1, unicode.len(tx) - 1)) + draw() + elseif t[3] >= 32 then + if menu[currentY][3] then + menu[currentY][3](menu[currentY][3]() .. unicode.char(t[3])) + draw() + end + end + end +end diff --git a/repository/data/app-metamachine/lucaboot.lua b/repository/data/app-metamachine/lucaboot.lua new file mode 100644 index 0000000..93edab6 --- /dev/null +++ b/repository/data/app-metamachine/lucaboot.lua @@ -0,0 +1,26 @@ +-- LUCcABOOT v0 +local lr = "(no inits)" +for a in component.list("filesystem", true) do + local dat = component.proxy(a) + local fh = dat.open("/init.lua", "rb") + if fh then + local ttl = "" + while true do + local chk = dat.read(fh, 2048) + if not chk then break end + ttl = ttl .. chk + end + computer.getBootAddress = function () return a end + computer.setBootAddress = function () end + local fn, r = load(ttl, "=init.lua", "t") + if not fn then + lr = r + dat.close(fh) + else + dat.close(fh) + return fn() + end + end +end +error("No available operating systems. " .. lr) + diff --git a/repository/docs/kn-refer b/repository/docs/kn-refer index 9fe2661..1a0bbcb 100644 --- a/repository/docs/kn-refer +++ b/repository/docs/kn-refer @@ -123,28 +123,19 @@ os is extended with: address = computer.address The following are just host functions - (*: wrapped for security - these - functions detect metatable abuse): + (*: wrapped for security): - assert, ipairs, load, next*, + assert, ipairs, load*, next*, pairs, pcall, xpcall, select, type, error, tonumber, tostring, setmetatable, getmetatable*, rawset*, rawget, rawlen, rawequal -(NOTE: Before you consider that load - has no checks: The policy regarding - load is taken from the host system. - Which means bytecode loading is - almost certainly off. If it's not - off, then this is the user's fault, - as doing so is marked as a security - risk for very obvious reasons. - As for trying to use the environment - to bypass a metatable, I tested. - Metatables do apply to environments, - including global creation. - There is no way to win.) +(Apparently load, if not given an + argument, uses the global metatable. + This is of course a security hole. + A very big one. So it ended up + getting wrapped as of R3.) "require" and "neo" are the parts of the environment where a NEO-specific diff --git a/repository/docs/repoauthors/app-metamachine b/repository/docs/repoauthors/app-metamachine new file mode 100644 index 0000000..bfb7d72 --- /dev/null +++ b/repository/docs/repoauthors/app-metamachine @@ -0,0 +1,6 @@ +repository/apps/app-metamachine.lua: 20kdc, Public Domain +repository/libs/metamachine-vgpu.lua: 20kdc, Public Domain +repository/libs/metamachine-vfs.lua: 20kdc, Public Domain +repository/data/app-metamachine/confboot.lua: 20kdc, Public Domain +repository/data/app-metamachine/lucaboot.lua: 20kdc, Public Domain + diff --git a/repository/libs/metamachine-vfs.lua b/repository/libs/metamachine-vfs.lua new file mode 100644 index 0000000..c98e1ff --- /dev/null +++ b/repository/libs/metamachine-vfs.lua @@ -0,0 +1,161 @@ +-- This is released into the public domain. +-- No warranty is provided, implied or otherwise. + +-- metamachine-vgpu.lua : Virtual GPU library +-- Authors: 20kdc + +return function (icecap, address, path, ro) + if path ~= "/" then + icecap.makeDirectory(path:sub(1, #path - 1)) + end + local function resolvePath(p, post) + local pth = {} + local issue = false + string.gsub(p, "[^\\/]+", function (str) + if str == ".." then + if not pth[1] then + issue = true + else + table.remove(pth, #pth) + end + elseif str ~= "." then + table.insert(pth, str) + end + end) + if issue then + return + end + local str = path + if post then + str = str:sub(1, #str - 1) + end + for k, v in ipairs(pth) do + if k > 1 or post then + str = str .. "/" + end + str = str .. v + end + return str + end + local function wrapThing(fn, post, roStop) + -- If we're adding a "/", we get rid of the original "/". + -- + local pofx = "" + if post then + pofx = "/" + end + return function (p) + if ro and roStop then + return false, "read-only filesystem" + end + p = resolvePath(p, post) + if p then + local nt = {pcall(fn, p .. pofx)} + if nt[1] then + return table.unpack(nt, 2) + end + end + return nil, "no such file or directory" + end + end + local function wrapStat(s) + return wrapThing(function (px) + local stat = icecap.stat(px) + if stat then + return stat[s] + end + end, false, false) + end + local handles = {} + local lHandle = 0 + local modeMapping = { + r = false, + rb = false, + w = true, + wb = true, + a = "append", + ab = "append" + } + return { + type = "filesystem", + getLabel = function () + return "VFS" + end, + setLabel = function (label) + end, + isReadOnly = function () + return ro or icecap.isReadOnly() + end, + spaceUsed = function () + return icecap.spaceUsed() + end, + spaceTotal = function () + return icecap.spaceTotal() + end, + list = wrapThing(icecap.list, true, false), + exists = wrapThing(function (px) + if icecap.stat(px) then + return true + end + return false + end, false, false), + isDirectory = wrapStat(1), + size = wrapStat(2), + lastModified = wrapStat(3), + makeDirectory = wrapThing(icecap.makeDirectory, false, true), + rename = function (a, b) + if ro then return false, "read-only filesystem" end + a = resolvePath(a) + b = resolvePath(b) + if not (a and b) then + return nil, a + end + return icecap.rename(a, b) + end, + remove = wrapThing(icecap.remove, false, true), + -- + open = function (p, mode) + checkArg(1, p, "string") + p = resolvePath(p) + if not p then return nil, "failed to open" end + if rawequal(mode, nil) then mode = "r" end + if modeMapping[mode] == nil then + error("unsupported mode " .. tostring(mode)) + end + mode = modeMapping[mode] + if (mode ~= false) and ro then return nil, "read-only filesystem" end + lHandle = lHandle + 1 + handles[lHandle] = icecap.open(p, mode) + if not handles[lHandle] then + return nil, "failed to open" + end + return lHandle + end, + read = function (fh, len) + checkArg(1, fh, "number") + checkArg(2, len, "number") + if not handles[fh] then return nil, "bad file descriptor" end + if not handles[fh].read then return nil, "bad file descriptor" end + return handles[fh].read(len) + end, + write = function (fh, data) + checkArg(1, fh, "number") + if not handles[fh] then return nil, "bad file descriptor" end + if not handles[fh].write then return nil, "bad file descriptor" end + return handles[fh].write(data) + end, + seek = function (fh, whence, point) + checkArg(1, fh, "number") + if not handles[fh] then return nil, "bad file descriptor" end + if not handles[fh].seek then return nil, "bad file descriptor" end + return handles[fh].seek(whence, point) + end, + close = function (fh) + checkArg(1, fh, "number") + if not handles[fh] then return nil, "bad file descriptor" end + handles[fh].close() + handles[fh] = nil + return true + end, + } +end diff --git a/repository/libs/metamachine-vgpu.lua b/repository/libs/metamachine-vgpu.lua new file mode 100644 index 0000000..0c0b9c7 --- /dev/null +++ b/repository/libs/metamachine-vgpu.lua @@ -0,0 +1,408 @@ +-- This is released into the public domain. +-- No warranty is provided, implied or otherwise. + +-- metamachine-vgpu.lua : Virtual GPU library +-- Authors: 20kdc + +return { + -- Creates a new virtual GPU. + -- 'screens' is the component mapping, and thus: + -- [id] = + newGPU = function (screens) + local boundScreen + local backgroundRGB, foregroundRGB = 0, 0xFFFFFF + local function bound() + if not screens[boundScreen] then return end + local gpu, rebound = screens[boundScreen]() + if not gpu then screens[boundScreen] = nil return end + if rebound then + gpu.setBackground(backgroundRGB) + gpu.setForeground(foregroundRGB) + end + return gpu + end + return { + -- Virtual GPU proxy + type = "gpu", + -- == getAspectRatio more or less + getSize = function () + local gpu = bound() + if not gpu then + return 1, 1 + else + return gpu.getSize() + end + end, + bind = function (adr, rst) + boundScreen = adr + local gpu = bound() + if gpu then + if rst then + gpu.setResolution(gpu.maxResolution()) + end + return true + end + boundScreen = nil + -- :( + return false, "No such virtual screen" + end, + getScreen = function () + return boundScreen + end, + maxResolution = function () + local gpu = bound() + if gpu then + return gpu.maxResolution() + end + error("unbound") + end, + getResolution = function () + local gpu = bound() + if gpu then + return gpu.getResolution() + end + error("unbound") + end, + getViewport = function () + -- annoyingly undocumented so we'll pretend it's this, as OCEmu does + local gpu = bound() + if gpu then + return gpu.getResolution() + end + error("unbound") + end, + setResolution = function (...) + local gpu = bound() + if gpu then + return gpu.setResolution(...) + end + error("unbound") + end, + setViewport = function (...) + -- annoyingly undocumented so we'll pretend it's this, as OCEmu does + local gpu = bound() + if gpu then + return gpu.setResolution(...) + end + error("unbound") + end, + maxDepth = function () + local gpu = bound() + if gpu then + return gpu.maxDepth() + end + error("unbound") + end, + getDepth = function () + local gpu = bound() + if gpu then + return gpu.getDepth() + end + error("unbound") + end, + setDepth = function (...) + local gpu = bound() + if gpu then + return gpu.setDepth(...) + end + error("unbound") + end, + get = function (...) + local gpu = bound() + if gpu then + return gpu.get(...) + end + error("unbound") + end, + set = function (...) + local gpu = bound() + if gpu then + return gpu.set(...) + end + error("unbound") + end, + copy = function (...) + local gpu = bound() + if gpu then + return gpu.copy(...) + end + error("unbound") + end, + fill = function (...) + local gpu = bound() + if gpu then + return gpu.fill(...) + end + error("unbound") + end, + getPaletteColor = function () + return 0 + end, + setPaletteColor = function () + -- Fail + end, + setForeground = function (rgb) + checkArg(1, rgb, "number") + local old = foregroundRGB + foregroundRGB = rgb + local gpu = bound() + if gpu then + gpu.setForeground(foregroundRGB) + end + return old + end, + setBackground = function (rgb) + checkArg(1, rgb, "number") + local old = backgroundRGB + backgroundRGB = rgb + local gpu = bound() + if gpu then + gpu.setBackground(backgroundRGB) + end + return old + end, + getForeground = function () + return foregroundRGB + end, + getBackground = function () + return backgroundRGB + end + } + end, + -- 'window' is used for span and setSize. + -- emitResize(w, h) is used for screen_resized events. + -- queueLine(y) is used for line queuing. + newBuffer = function (window, keyboards, maxW, maxH, emitResize, queueLine) + local screenW, screenH + local screenText = "" + local screenFR = "" + local screenFG = "" + local screenFB = "" + local screenBR = "" + local screenBG = "" + local screenBB = "" + -- Gets characters for R, G and B + local function decodeRGB(rgb) + return + string.char(math.floor(rgb / 65536) % 256), + string.char(math.floor(rgb / 256) % 256), + string.char(rgb % 256) + end + -- Returns the width, or nothing if totally out of bounds. + local function put(x, y, ch, fg, bg) + if x < 1 or x > screenW then return end + if y < 1 or y > screenH then return end + local fr, fg, fb = decodeRGB(fg) + local br, bg, bb = decodeRGB(bg) + ch = unicode.safeTextFormat(ch) + local chw = unicode.len(ch) + -- Crop + ch = unicode.sub(ch, 1, (screenW - x) + 1) + chw = unicode.len(ch) + + local index = x + ((y - 1) * screenW) + screenText = unicode.sub(screenText, 1, index - 1) .. ch .. unicode.sub(screenText, index + chw) + screenFR = screenFR:sub(1, index - 1) .. fr:rep(chw) .. screenFR:sub(index + chw) + screenFG = screenFG:sub(1, index - 1) .. fg:rep(chw) .. screenFG:sub(index + chw) + screenFB = screenFB:sub(1, index - 1) .. fb:rep(chw) .. screenFB:sub(index + chw) + screenBR = screenBR:sub(1, index - 1) .. br:rep(chw) .. screenBR:sub(index + chw) + screenBG = screenBG:sub(1, index - 1) .. bg:rep(chw) .. screenBG:sub(index + chw) + screenBB = screenBB:sub(1, index - 1) .. bb:rep(chw) .. screenBB:sub(index + chw) + return chw + end + local function getCh(x, y) + x, y = math.floor(x), math.floor(y) + if x < 1 or x > screenW then return " ", 0, 0 end + if y < 1 or y > screenH then return " ", 0, 0 end + local index = x + ((y - 1) * screenW) + local fg = (screenFR:byte(index) * 65536) + (screenFG:byte(index) * 256) + screenFB:byte(index) + local bg = (screenBR:byte(index) * 65536) + (screenBG:byte(index) * 256) + screenBB:byte(index) + return unicode.sub(screenText, index, index), fg, bg + end + -- + -- Directly exposed to userspace + local function setSize(w, h, first) + w = math.min(math.max(math.floor(w), 1), maxW) + h = math.min(math.max(math.floor(h), 1), maxH) + screenW, screenH = w, h + screenText = (" "):rep(w * h) + screenFR = ("\xFF"):rep(w * h) + screenFG = screenFR + screenFB = screenFG + screenBR = ("\x00"):rep(w * h) + screenBG = screenBR + screenBB = screenBG + if not first then emitResize(w, h) end + window.setSize(w, h) + end + local function rectOOB(x, y, w, h) + if x < 1 or x > screenW - (w - 1) then return true end + if y < 1 or y > screenH - (h - 1) then return true end + end + local function redrawLine(x, y, w) + x, y, w = math.floor(x), math.floor(y), math.floor(w) + w = math.min(w, screenW - (x - 1)) + if w < 1 then return end + if x < 1 or x > screenW then return end + if y < 1 or y > screenH then return end + local index = x + ((y - 1) * screenW) + local currentSegmentI + local currentSegment + local currentSegmentR2 + local function flushSegment() + if not currentSegment then return end + local tx = unicode.undoSafeTextFormat(currentSegment) + local fg = (currentSegmentR2:byte(1) * 65536) + (currentSegmentR2:byte(2) * 256) + currentSegmentR2:byte(3) + local bg = (currentSegmentR2:byte(4) * 65536) + (currentSegmentR2:byte(5) * 256) + currentSegmentR2:byte(6) + -- Span format is bg, fg, not fg, bg + window.span(x + currentSegmentI - 1, y, tx, bg, fg) + currentSegment = nil + currentSegmentI = nil + currentSegmentR2 = nil + end + for i = 1, w do + local idx = index + i - 1 + local p = unicode.sub(screenText, idx, idx) + local s = + screenFR:sub(idx, idx) .. screenFG:sub(idx, idx) .. screenFB:sub(idx, idx) .. + screenBR:sub(idx, idx) .. screenBG:sub(idx, idx) .. screenBB:sub(idx, idx) + if currentSegmentR2 ~= s then + flushSegment() + currentSegmentI = i + currentSegmentR2 = s + currentSegment = p + else + currentSegment = currentSegment .. p + end + end + flushSegment() + end + local function queueRedraw(x, y, w, h) + for i = 1, h do + queueLine(y + i - 1) + end + end + setSize(maxW, maxH, true) + local fgRGB, bgRGB = 0xFFFFFF, 0 + local videoInterfaceChipset = { + getSize = function () + return 1, 1 + end, + maxResolution = function () + return maxW, maxH + end, + getResolution = function () + return screenW, screenH + end, + setResolution = setSize, + maxDepth = function () + return 8 + end, + getDepth = function () + return 8 + end, + setDepth = function (d) + end, + get = getCh, + set = function (x, y, str, v) + x, y = math.floor(x), math.floor(y) + if v then + for i = 1, unicode.len(str) do + put(x, y + i - 1, unicode.sub(str, i, i), fgRGB, bgRGB) + end + return true + end + local chw = put(x, y, str, fgRGB, bgRGB) + if chw then + queueLine(y) + else + return false, "Out of bounds." + end + return true + end, + copy = function (x, y, w, h, ox, oy) + x, y, w, h, ox, oy = math.floor(x), math.floor(y), math.floor(w), math.floor(h), math.floor(ox), math.floor(oy) + if rectOOB(x, y, w, h) then return false, "out of bounds. s" end + if rectOOB(x + ox, y + oy, w, h) then return false, "out of bounds. t" end + local collation = {} + for iy = 1, h do + collation[iy] = {} + for ix = 1, w do + collation[iy][ix] = {getCh(ix + x - 1, iy + y - 1)} + end + end + for iy = 1, h do + for ix = 1, w do + local cc = collation[iy][ix] + if ix + unicode.charWidth(cc[1]) - 1 <= w then + put(ix + ox + x - 1, iy + oy + y - 1, cc[1], cc[2], cc[3]) + end + end + end + queueRedraw(x + ox, y + oy, w, h) + return true + end, + fill = function (x, y, w, h, str) + x, y, w, h = math.floor(x), math.floor(y), math.floor(w), math.floor(h) + if rectOOB(x, y, w, h) then return false, "out of bounds" end + str = unicode.sub(str, 1, 1) + str = str:rep(math.floor(w / unicode.charWidth(str))) + for i = 1, h do + put(x, y + i - 1, str, fgRGB, bgRGB) + end + queueRedraw(x, y, w, h) + return true + end, + setForeground = function (rgb) + fgRGB = rgb + end, + setBackground = function (rgb) + bgRGB = rgb + end, + } + -- Various interfaces + local int = { + -- Internal interface + line = function (y) + redrawLine(1, y, screenW) + end, + precise = false + } + return function () + return videoInterfaceChipset, false + end, int, { + type = "screen", + isOn = function () + return true + end, + turnOn = function () + return true + end, + turnOff = function () + return true + end, + getAspectRatio = function () + return 1, 1 + end, + getKeyboards = function () + local kbs = {} + for k, v in ipairs(keyboards) do + kbs[k] = v + end + return kbs + end, + setPrecise = function (p) + int.precise = p + end, + isPrecise = function () + return int.precise + end, + setTouchModeInverted = function (p) + return false + end, + isTouchModeInverted = function () + return false + end, + } + end +}