From 40c97ca00038b11bc1789e67ce9c8fef9e60ba3a Mon Sep 17 00:00:00 2001 From: spsf Date: Sun, 22 Feb 2026 21:53:02 -0600 Subject: [PATCH] Hyperion v1.2.0 --- Src/Hyperion-bash/bin/cat | 41 +- Src/Hyperion-bash/bin/chattr | 162 +++ Src/Hyperion-bash/bin/chgrp | 111 ++ Src/Hyperion-bash/bin/chmod | 262 +++++ Src/Hyperion-bash/bin/chown | 144 +++ Src/Hyperion-bash/bin/chroot | 83 ++ Src/Hyperion-bash/bin/help | 399 +++---- Src/Hyperion-bash/bin/hysh | 997 +++++++++++++++--- Src/Hyperion-bash/bin/ln | 96 ++ Src/Hyperion-bash/bin/login | 59 +- Src/Hyperion-bash/bin/loimgcreate | 157 +++ Src/Hyperion-bash/bin/looptest | 427 ++++++++ Src/Hyperion-bash/bin/losetup | 129 +++ Src/Hyperion-bash/bin/ls | 238 +++-- Src/Hyperion-bash/bin/micro | 423 ++++++++ Src/Hyperion-bash/bin/mount | 152 +++ Src/Hyperion-bash/bin/neofetch | 18 - Src/Hyperion-bash/bin/readlink | 82 ++ Src/Hyperion-bash/bin/sed | 429 ++++++++ Src/Hyperion-bash/bin/socktest | 151 +++ Src/Hyperion-bash/bin/startup/test.lua | 1 - Src/Hyperion-bash/bin/su | 58 +- Src/Hyperion-bash/bin/umount | 111 ++ Src/Hyperion-firmware-cct/boot/cct/boot.lua | 9 +- Src/Hyperion-kernel/boot/kernel.lua | 2 +- .../lib/modules/Hyperion/10_vfs.kmod | 925 +++++++++------- .../lib/modules/Hyperion/12_tmpfs.kmod | 22 +- .../lib/modules/Hyperion/13_loopdev.kmod | 540 ++++++++++ .../lib/modules/Hyperion/20_socket.kmod | 558 +++++++++- .../lib/modules/Hyperion/26_tty.kmod | 401 ++++++- .../lib/modules/Hyperion/40_auth.kmod | 23 +- .../lib/modules/Hyperion/45_hypervisor.kmod | 516 ++++----- .../lib/modules/Hyperion/90_init.kmod | 33 +- .../lib/modules/Hyperion/91_login.kmod | 15 +- .../lib/modules/Hyperion/92_permissions.kmod | 205 ++-- build.py | 73 +- building.md | 13 +- 37 files changed, 6736 insertions(+), 1329 deletions(-) create mode 100644 Src/Hyperion-bash/bin/chattr create mode 100644 Src/Hyperion-bash/bin/chgrp create mode 100644 Src/Hyperion-bash/bin/chmod create mode 100644 Src/Hyperion-bash/bin/chown create mode 100644 Src/Hyperion-bash/bin/chroot create mode 100644 Src/Hyperion-bash/bin/ln create mode 100644 Src/Hyperion-bash/bin/loimgcreate create mode 100644 Src/Hyperion-bash/bin/looptest create mode 100644 Src/Hyperion-bash/bin/losetup create mode 100644 Src/Hyperion-bash/bin/micro create mode 100644 Src/Hyperion-bash/bin/mount delete mode 100644 Src/Hyperion-bash/bin/neofetch create mode 100644 Src/Hyperion-bash/bin/readlink create mode 100644 Src/Hyperion-bash/bin/sed create mode 100644 Src/Hyperion-bash/bin/socktest delete mode 100644 Src/Hyperion-bash/bin/startup/test.lua create mode 100644 Src/Hyperion-bash/bin/umount create mode 100644 Src/Hyperion-kernel/lib/modules/Hyperion/13_loopdev.kmod diff --git a/Src/Hyperion-bash/bin/cat b/Src/Hyperion-bash/bin/cat index b9dbe6a..1156880 100644 --- a/Src/Hyperion-bash/bin/cat +++ b/Src/Hyperion-bash/bin/cat @@ -1,24 +1,33 @@ local args = {...} local name = syscall.getTask(syscall.getpid()).name local fs = require("sys.fs") -local filePath = (args[1] or "") -if filePath:sub(1, 1) ~= "/" then - filePath = syscall.getcwd().."/"..filePath -end -if not fs.exists(filePath) and args[1] then - print(name..": Cannot access '"..args[1].."': No such file.") +if not args[1] then + while true do + local content = syscall.read(0, 1024) + if not content or content == "" then break end + printInline(content) + end + print("") return end -local file = 0 -if args[1] then - file = syscall.open(filePath, "r") +for _, arg in ipairs(args) do + local filePath = arg + if filePath:sub(1,1) ~= "/" then + filePath = syscall.getcwd().."/"..filePath + end + + if not fs.exists(filePath) then + print(name..": Cannot access '"..arg.."': No such file.") + else + local fd = syscall.open(filePath, "r") + while true do + local content = syscall.read(fd, 1024) + if not content or content == "" then break end + printInline(content) + end + syscall.close(fd) + end end -local content="" -while content~=nil or file == 0 do - content=syscall.read(file, 1024) - printInline(content) -end -syscall.close(file) -print("") \ No newline at end of file +print("") diff --git a/Src/Hyperion-bash/bin/chattr b/Src/Hyperion-bash/bin/chattr new file mode 100644 index 0000000..3a35e6c --- /dev/null +++ b/Src/Hyperion-bash/bin/chattr @@ -0,0 +1,162 @@ +--:Minify:-- +-- supports +i/-i (immutable) stored in the file's cmeta/xattr field +local name = syscall.getTask(syscall.getpid()).name +local cloptions = { R = false, help = false } +local args = {} +local modeStr = nil + +for _, v in ipairs({ ... }) do + if v:sub(1, 2) == "--" then + local opt = v:sub(3) + if cloptions[opt] == nil then + print(name .. ": unrecognized option '" .. v .. "'") + print("try '" .. name .. " --help' for more information.") + syscall.exit(1); return + end + cloptions[opt] = true + elseif v:sub(1, 1) == "-" and not v:match("^%-[%+%-]") then + local isFlag = true + for i = 2, #v do + local c = v:sub(i,i) + if cloptions[c] ~= nil then + cloptions[c] = true + else + isFlag = false; break + end + end + if not isFlag then + modeStr = v + end + elseif v:sub(1,1) == "+" or (v:sub(1,1) == "-" and v:match("^%-[a-zA-Z]")) then + modeStr = v + else + table.insert(args, v) + end +end + +if cloptions.help then + print("Usage: " .. name .. " [OPTION]... +-= ATTRS FILE...") + print("Change file attributes on a filesystem.") + print("") + print("Attributes:") + print(" i immutable: file cannot be modified, renamed, or deleted") + print(" a append-only: file can only be appended to") + print("") + print("Operators: +attr add, -attr remove") + print("Example: " .. name .. " +i /etc/passwd") + print("") + print("Options:") + print(" -R operate on files and directories recursively") + print(" --help display this help and exit") + return +end + +if not modeStr or #args < 1 then + print(name .. ": missing operand") + print("try '" .. name .. " --help' for more information.") + syscall.exit(1); return +end + +local op = modeStr:sub(1, 1) +local attrs = modeStr:sub(2) + +if op ~= "+" and op ~= "-" then + print(name .. ": invalid operator '" .. op .. "' (use + or -)") + syscall.exit(1); return +end + +local function getXattr(path) + local stat = pcall(function() return syscall.stat(path) end) and syscall.stat(path) + if stat then return stat.xattr or "" end + return "" +end + +local IMMUTABLE_TAG = "|i" +local APPENDONLY_TAG = "|a" + +local function attrTag(c) + if c == "i" then return IMMUTABLE_TAG + elseif c == "a" then return APPENDONLY_TAG + else return nil end +end + +local function chattrPath(path) + local stat = syscall.stat(path) + if not stat then + print(name .. ": cannot stat '" .. path .. "': No such file or directory") + return false + end + if stat.etype == 0x01 then + return true + end + + if not syscall.setxattr then + print(name .. ": kernel does not expose setxattr syscall; cannot modify attributes") + syscall.exit(1); return false + end + + local xattr = stat.xattr or "" + + for i = 1, #attrs do + local c = attrs:sub(i, i) + local tag = attrTag(c) + if not tag then + print(name .. ": unsupported attribute '" .. c .. "'") + syscall.exit(1); return false + end + local hasTag = xattr:find(tag, 1, true) + if op == "+" and not hasTag then + xattr = xattr .. tag + elseif op == "-" and hasTag then + xattr = xattr:gsub(tag:gsub("|", "%%|"), "") + end + end + + local ok, err = pcall(syscall.setxattr, path, xattr) + if not ok then + print(name .. ": cannot set attributes on '" .. path .. "': " .. tostring(err)) + return false + end + return true +end + +local function chattrRecursive(path) + if not chattrPath(path) then return end + if syscall.type(path) == "directory" then + local ok, list = pcall(syscall.listdir, path) + if ok then + for _, entry in ipairs(list) do + local child = path + if child:sub(-1) ~= "/" then child = child .. "/" end + chattrRecursive(child .. entry) + end + end + end +end + +local cwd = syscall.getcwd() +local function absPath(p) + if p:sub(1,1) ~= "/" then p = cwd .. "/" .. p end + return p +end + +if not syscall.setxattr then + print(name .. ": kernel does not expose setxattr; attributes cannot be persisted") + print(name .. ": add sys[\"setxattr\"] = vfs.setxattr to 10_vfs.kmod to enable this") + syscall.exit(1); return +end + +local exitCode = 0 +for i = 1, #args do + local path = absPath(args[i]) + if not syscall.exists(path) then + print(name .. ": cannot access '" .. args[i] .. "': No such file or directory") + exitCode = 1 + elseif cloptions.R then + chattrRecursive(path) + else + if not chattrPath(path) then exitCode = 1 end + end +end + +syscall.exit(exitCode) diff --git a/Src/Hyperion-bash/bin/chgrp b/Src/Hyperion-bash/bin/chgrp new file mode 100644 index 0000000..51508f8 --- /dev/null +++ b/Src/Hyperion-bash/bin/chgrp @@ -0,0 +1,111 @@ +--:Minify:-- +-- chgrp: change group ownership +local name = syscall.getTask(syscall.getpid()).name +local cloptions = { R = false, help = false } +local args = {} + +for _, v in ipairs({ ... }) do + if v:sub(1, 2) == "--" then + local opt = v:sub(3) + if cloptions[opt] == nil then + print(name .. ": unrecognized option '" .. v .. "'") + print("try '" .. name .. " --help' for more information.") + syscall.exit(1); return + end + cloptions[opt] = true + elseif v:sub(1, 1) == "-" then + for i = 2, #v do + local opt = v:sub(i, i) + if cloptions[opt] == nil then + print(name .. ": invalid option '-" .. opt .. "'") + print("try '" .. name .. " --help' for more information.") + syscall.exit(1); return + end + cloptions[opt] = true + end + else + table.insert(args, v) + end +end + +if cloptions.help then + print("Usage: " .. name .. " [OPTION]... GROUP FILE...") + print("Change the group of each FILE to GROUP.") + print("GROUP may be a group name or numeric ID.") + print("") + print("Options:") + print(" -R operate on files and directories recursively") + print(" --help display this help and exit") + return +end + +if #args < 2 then + print(name .. ": missing operand") + print("try '" .. name .. " --help' for more information.") + syscall.exit(1); return +end + +local groupStr = args[1] + +local function resolveGid(s) + local n = tonumber(s) + if n then return n end + local uid = syscall.getuidbyname and syscall.getuidbyname(s) + if uid then + local pwent = syscall.getpasswd(uid) + if pwent then return pwent.gid end + end + print(name .. ": invalid group: '" .. s .. "'") + syscall.exit(1) +end + +local newGid = resolveGid(groupStr) + +local function chgrpPath(path) + local stat = syscall.stat(path) + if not stat then + print(name .. ": cannot stat '" .. path .. "': No such file or directory") + return false + end + local ok, err = pcall(syscall.chown, path, stat.owner, newGid) + if not ok then + print(name .. ": cannot change group of '" .. path .. "': " .. tostring(err)) + return false + end + return true +end + +local function chgrpRecursive(path) + if not chgrpPath(path) then return end + if syscall.type(path) == "directory" then + local ok, list = pcall(syscall.listdir, path) + if ok then + for _, entry in ipairs(list) do + local child = path + if child:sub(-1) ~= "/" then child = child .. "/" end + chgrpRecursive(child .. entry) + end + end + end +end + +local cwd = syscall.getcwd() +local function absPath(p) + if p:sub(1,1) ~= "/" then p = cwd .. "/" .. p end + return p +end + +local exitCode = 0 +for i = 2, #args do + local path = absPath(args[i]) + if not syscall.exists(path) then + print(name .. ": cannot access '" .. args[i] .. "': No such file or directory") + exitCode = 1 + elseif cloptions.R then + chgrpRecursive(path) + else + if not chgrpPath(path) then exitCode = 1 end + end +end + +syscall.exit(exitCode) diff --git a/Src/Hyperion-bash/bin/chmod b/Src/Hyperion-bash/bin/chmod new file mode 100644 index 0000000..6e97737 --- /dev/null +++ b/Src/Hyperion-bash/bin/chmod @@ -0,0 +1,262 @@ +--:Minify:-- +local name = syscall.getTask(syscall.getpid()).name +local cloptions = { R = false, help = false } +local args = {} + +for _, v in ipairs({ ... }) do + if v:sub(1, 2) == "--" then + local opt = v:sub(3) + if cloptions[opt] == nil then + print(name .. ": unrecognized option '" .. v .. "'") + print("try '" .. name .. " --help' for more information.") + syscall.exit(1); return + end + cloptions[opt] = true + elseif v:sub(1, 1) == "-" then + for i = 2, #v do + local opt = v:sub(i, i) + if cloptions[opt] == nil then + print(name .. ": invalid option '-" .. opt .. "'") + print("try '" .. name .. " --help' for more information.") + syscall.exit(1); return + end + cloptions[opt] = true + end + else + table.insert(args, v) + end +end + +if cloptions.help then + print("Usage: " .. name .. " [OPTION]... MODE FILE...") + print("Change the file mode bits of each FILE to MODE.") + print("") + print("MODE may be octal (e.g. 755) or symbolic (e.g. u+x, go-w, a=r).") + print("") + print("Octal bit layout (Hyperion):") + print(" owner: r=32 w=16 x=512 group: r=8 w=4 x=256") + print(" world: r=2 w=1 x=128 suid=64") + print(" Common: 644=rw-r--r-- 755=rwxr-xr-x 700=rwx------") + print("") + print("Symbolic: [ugoa][+-=][rwxs] (comma-separated list)") + print("") + print("Options:") + print(" -R change files and directories recursively") + print(" --help display this help and exit") + return +end + +if #args < 2 then + print(name .. ": missing operand") + print("try '" .. name .. " --help' for more information.") + syscall.exit(1); return +end + +local modeArg = args[1] + +local P = { + OWNER_R = 32, OWNER_W = 16, OWNER_X = 512, + GROUP_R = 8, GROUP_W = 4, GROUP_X = 256, + WORLD_R = 2, WORLD_W = 1, WORLD_X = 128, + SUID = 64, +} + +local function bit_is_set(num, bit) + return math.floor(num / (2 ^ bit)) % 2 == 1 +end + +local function parseOctal(s) + local n = tonumber(s, 8) + if not n then return nil end + + local result = 0 + if bit_is_set(n, 8) then result = result + P.OWNER_R end -- 0400 + if bit_is_set(n, 7) then result = result + P.OWNER_W end -- 0200 + if bit_is_set(n, 6) then result = result + P.OWNER_X end -- 0100 + + if bit_is_set(n, 5) then result = result + P.GROUP_R end -- 040 + if bit_is_set(n, 4) then result = result + P.GROUP_W end -- 020 + if bit_is_set(n, 3) then result = result + P.GROUP_X end -- 010 + + if bit_is_set(n, 2) then result = result + P.WORLD_R end -- 004 + if bit_is_set(n, 1) then result = result + P.WORLD_W end -- 002 + if bit_is_set(n, 0) then result = result + P.WORLD_X end -- 001 + + if bit_is_set(n, 11) then result = result + P.SUID end + + return result +end + +local function applySymbolic(modeStr, existingPerms) + local perms = existingPerms + + for clause in (modeStr .. ","):gmatch("([^,]+),") do + local who_str, rest = clause:match("^([ugoa]*)([+%-=].+)$") + if not who_str then + print(name .. ": invalid mode: '" .. clause .. "'") + syscall.exit(1); return nil + end + if who_str == "" or who_str == "a" then who_str = "ugo" end + + local op = rest:sub(1, 1) + local bits_str = rest:sub(2) + + local mask = 0 + for i = 1, #bits_str do + local c = bits_str:sub(i, i) + for j = 1, #who_str do + local w = who_str:sub(j, j) + if c == "r" then + if w == "u" then mask = mask + P.OWNER_R + elseif w == "g" then mask = mask + P.GROUP_R + elseif w == "o" then mask = mask + P.WORLD_R end + elseif c == "w" then + if w == "u" then mask = mask + P.OWNER_W + elseif w == "g" then mask = mask + P.GROUP_W + elseif w == "o" then mask = mask + P.WORLD_W end + elseif c == "x" then + if w == "u" then mask = mask + P.OWNER_X + elseif w == "g" then mask = mask + P.GROUP_X + elseif w == "o" then mask = mask + P.WORLD_X end + elseif c == "s" then + if w == "u" then mask = mask + P.SUID end + end + end + end + + if op == "+" then + perms = perms + (mask - (perms % (mask + 1) - perms % mask > 0 and 0 or 0)) + + perms = perms - (perms % 1) + local function bor(a, b) + local result, bit = 0, 1 + while a > 0 or b > 0 do + if (a % 2 == 1) or (b % 2 == 1) then result = result + bit end + a = math.floor(a / 2); b = math.floor(b / 2); bit = bit * 2 + end + return result + end + perms = bor(perms, mask) + elseif op == "-" then + local function band(a, b) + local result, bit = 0, 1 + while a > 0 and b > 0 do + if (a % 2 == 1) and (b % 2 == 1) then result = result + bit end + a = math.floor(a / 2); b = math.floor(b / 2); bit = bit * 2 + end + return result + end + local function bxor(a, b) + local result, bit = 0, 1 + while a > 0 or b > 0 do + if (a % 2 == 1) ~= (b % 2 == 1) then result = result + bit end + a = math.floor(a / 2); b = math.floor(b / 2); bit = bit * 2 + end + return result + end + perms = bxor(perms, band(perms, mask)) + elseif op == "=" then + local clearMask = 0 + for j = 1, #who_str do + local w = who_str:sub(j, j) + if w == "u" then clearMask = clearMask + P.OWNER_R + P.OWNER_W + P.OWNER_X + P.SUID + elseif w == "g" then clearMask = clearMask + P.GROUP_R + P.GROUP_W + P.GROUP_X + elseif w == "o" then clearMask = clearMask + P.WORLD_R + P.WORLD_W + P.WORLD_X end + end + local function bxor(a, b) + local result, bit = 0, 1 + while a > 0 or b > 0 do + if (a % 2 == 1) ~= (b % 2 == 1) then result = result + bit end + a = math.floor(a / 2); b = math.floor(b / 2); bit = bit * 2 + end + return result + end + local function band(a, b) + local result, bit = 0, 1 + while a > 0 and b > 0 do + if (a % 2 == 1) and (b % 2 == 1) then result = result + bit end + a = math.floor(a / 2); b = math.floor(b / 2); bit = bit * 2 + end + return result + end + local function bor(a, b) + local result, bit = 0, 1 + while a > 0 or b > 0 do + if (a % 2 == 1) or (b % 2 == 1) then result = result + bit end + a = math.floor(a / 2); b = math.floor(b / 2); bit = bit * 2 + end + return result + end + perms = bxor(perms, band(perms, clearMask)) + perms = bor(perms, mask) + else + print(name .. ": invalid operator in mode: '" .. clause .. "'") + syscall.exit(1); return nil + end + end + + return perms +end + +local function resolveMode(modeStr, existingPerms) + if modeStr:match("^[0-7]+$") then + local p = parseOctal(modeStr) + if p then return p end + end + + return applySymbolic(modeStr, existingPerms) +end + +local function chmodPath(path) + local stat, err = pcall(syscall.stat, path) + local existingPerms = 0 + if stat then + local s = syscall.stat(path) + existingPerms = s and s.perms or 0 + end + + local newPerms = resolveMode(modeArg, existingPerms) + if newPerms == nil then return false end + + local ok, cerr = pcall(syscall.chmod, path, newPerms) + if not ok then + print(name .. ": cannot change permissions of '" .. path .. "': " .. tostring(cerr)) + return false + end + return true +end + +local function chmodRecursive(path) + if not chmodPath(path) then return end + if syscall.type(path) == "directory" then + local ok, list = pcall(syscall.listdir, path) + if ok then + for _, entry in ipairs(list) do + local child = path + if child:sub(-1) ~= "/" then child = child .. "/" end + chmodRecursive(child .. entry) + end + end + end +end + +local cwd = syscall.getcwd() +local function absPath(p) + if p:sub(1,1) ~= "/" then p = cwd .. "/" .. p end + return p +end + +local exitCode = 0 +for i = 2, #args do + local path = absPath(args[i]) + if not syscall.exists(path) then + print(name .. ": cannot access '" .. args[i] .. "': No such file or directory") + exitCode = 1 + elseif cloptions.R then + chmodRecursive(path) + else + if not chmodPath(path) then exitCode = 1 end + end +end + +syscall.exit(exitCode) diff --git a/Src/Hyperion-bash/bin/chown b/Src/Hyperion-bash/bin/chown new file mode 100644 index 0000000..4cb32ad --- /dev/null +++ b/Src/Hyperion-bash/bin/chown @@ -0,0 +1,144 @@ +--:Minify:-- +local name = syscall.getTask(syscall.getpid()).name +local cloptions = { R = false, help = false } +local args = {} + +for _, v in ipairs({ ... }) do + if v:sub(1, 2) == "--" then + local opt = v:sub(3) + if cloptions[opt] == nil then + print(name .. ": unrecognized option '" .. v .. "'") + print("try '" .. name .. " --help' for more information.") + syscall.exit(1); return + end + cloptions[opt] = true + elseif v:sub(1, 1) == "-" then + for i = 2, #v do + local opt = v:sub(i, i) + if cloptions[opt] == nil then + print(name .. ": invalid option '-" .. opt .. "'") + print("try '" .. name .. " --help' for more information.") + syscall.exit(1); return + end + cloptions[opt] = true + end + else + table.insert(args, v) + end +end + +if cloptions.help then + print("Usage: " .. name .. " [OPTION]... OWNER[:GROUP] FILE...") + print(" " .. name .. " [OPTION]... :GROUP FILE...") + print("Change the owner and/or group of each FILE.") + print("OWNER and GROUP may be names or numeric IDs.") + print("") + print("Options:") + print(" -R operate on files and directories recursively") + print(" --help display this help and exit") + return +end + +if #args < 2 then + print(name .. ": missing operand") + print("try '" .. name .. " --help' for more information.") + syscall.exit(1); return +end + +local spec = args[1] +local ownerStr, groupStr + +if spec:sub(1,1) == ":" then + groupStr = spec:sub(2) +else + local colon = spec:find(":", 1, true) + if colon then + ownerStr = spec:sub(1, colon - 1) + groupStr = spec:sub(colon + 1) + if groupStr == "" then groupStr = nil end + else + ownerStr = spec + end +end + +local function resolveUid(s) + if not s or s == "" then return nil end + local n = tonumber(s) + if n then return n end + local uid = syscall.getuidbyname and syscall.getuidbyname(s) + if uid then return uid end + print(name .. ": invalid user: '" .. s .. "'") + syscall.exit(1) +end + +local function resolveGid(s) + if not s or s == "" then return nil end + local n = tonumber(s) + if n then return n end + local uid = syscall.getuidbyname and syscall.getuidbyname(s) + if uid then + local pwent = syscall.getpasswd(uid) + if pwent then return pwent.gid end + end + print(name .. ": invalid group: '" .. s .. "'") + syscall.exit(1) +end + +local newUid = resolveUid(ownerStr) +local newGid = resolveGid(groupStr) + +if newUid == nil and newGid == nil then + print(name .. ": no owner or group specified") + syscall.exit(1); return +end + +local function chownPath(path) + local stat = syscall.stat(path) + if not stat then + print(name .. ": cannot stat '" .. path .. "': No such file or directory") + return false + end + local uid = newUid ~= nil and newUid or stat.owner + local gid = newGid ~= nil and newGid or stat.group + local ok, err = pcall(syscall.chown, path, uid, gid) + if not ok then + print(name .. ": cannot change owner of '" .. path .. "': " .. tostring(err)) + return false + end + return true +end + +local function chownRecursive(path) + if not chownPath(path) then return end + if syscall.type(path) == "directory" then + local ok, list = pcall(syscall.listdir, path) + if ok then + for _, entry in ipairs(list) do + local child = path + if child:sub(-1) ~= "/" then child = child .. "/" end + chownRecursive(child .. entry) + end + end + end +end + +local cwd = syscall.getcwd() +local function absPath(p) + if p:sub(1,1) ~= "/" then p = cwd .. "/" .. p end + return p +end + +local exitCode = 0 +for i = 2, #args do + local path = absPath(args[i]) + if not syscall.exists(path) then + print(name .. ": cannot access '" .. args[i] .. "': No such file or directory") + exitCode = 1 + elseif cloptions.R then + chownRecursive(path) + else + if not chownPath(path) then exitCode = 1 end + end +end + +syscall.exit(exitCode) diff --git a/Src/Hyperion-bash/bin/chroot b/Src/Hyperion-bash/bin/chroot new file mode 100644 index 0000000..d55970b --- /dev/null +++ b/Src/Hyperion-bash/bin/chroot @@ -0,0 +1,83 @@ +--:Minify:-- +local name = syscall.getTask(syscall.getpid()).name +local args = {} +local cloptions = { help = false } + +for _, v in ipairs({ ... }) do + if v:sub(1, 2) == "--" then + local opt = v:sub(3) + if opt == "help" then + cloptions.help = true + else + print(name .. ": unrecognized option '" .. v .. "'") + print("try '" .. name .. " --help' for more information.") + syscall.exit(1); return + end + elseif v:sub(1, 1) == "-" then + print(name .. ": invalid option '" .. v .. "'") + print("try '" .. name .. " --help' for more information.") + syscall.exit(1); return + else + table.insert(args, v) + end +end + +if cloptions.help then + print("Usage: " .. name .. " NEWROOT [COMMAND [ARG]...]") + print("Run COMMAND with root directory set to NEWROOT.") + print("If COMMAND is omitted, runs the current user's shell.") + print("") + print("Requires root (uid 0).") + return +end + +if #args < 1 then + print(name .. ": missing operand") + print("try '" .. name .. " --help' for more information.") + syscall.exit(1); return +end + +local euid = syscall.geteuid and syscall.geteuid() or syscall.getuid() +if euid ~= 0 then + print(name .. ": cannot change root directory: Permission denied") + syscall.exit(1); return +end + +local newRoot = args[1] +if newRoot:sub(1,1) ~= "/" then + newRoot = syscall.getcwd() .. "/" .. newRoot +end + +if not syscall.exists(newRoot) then + print(name .. ": cannot change root directory to '" .. args[1] .. "': No such file or directory") + syscall.exit(1); return +end + +if syscall.type(newRoot) ~= "directory" then + print(name .. ": '" .. args[1] .. "': Not a directory") + syscall.exit(1); return +end + +local ok, err = pcall(syscall.chroot, newRoot) +if not ok then + print(name .. ": cannot change root directory to '" .. args[1] .. "': " .. tostring(err)) + syscall.exit(1); return +end + +local shell +if #args >= 2 then + shell = args[2] +else + local uid = syscall.getuid() + local pwent = syscall.getpasswd(uid) + shell = (pwent and pwent.shell) or "/bin/hysh" +end + +local execArgs = {} +for i = 3, #args do table.insert(execArgs, args[i]) end + +local execOk, execErr = pcall(syscall.exec, shell, execArgs) +if not execOk then + print(name .. ": failed to run command '" .. shell .. "': " .. tostring(execErr)) + syscall.exit(127) +end diff --git a/Src/Hyperion-bash/bin/help b/Src/Hyperion-bash/bin/help index df9a33f..ccd0dfd 100644 --- a/Src/Hyperion-bash/bin/help +++ b/Src/Hyperion-bash/bin/help @@ -1,174 +1,205 @@ --:Minify:-- --- help: display command reference with paged scrolling local COMMANDS = { - -- {name, usage, description, {flags...}} - -- flags: {flag, desc} - { - name = "cat", - usage = "cat [file]", - desc = "Print file contents to stdout. Reads from stdin if no file given.", - flags = {} - }, - { - name = "cd", - usage = "cd [dir]", - desc = "Change working directory. Use '-' to return to previous directory.", - flags = {} - }, - { - name = "clear", - usage = "clear", - desc = "Clear the terminal screen.", - flags = {} - }, - { - name = "echo", - usage = "echo [text...]", - desc = "Print arguments to stdout.", - flags = {} - }, - { - name = "hfetch", - usage = "hfetch", - desc = "Display system information in a neofetch-style layout.", - flags = {} - }, - { - name = "id", - usage = "id [username]", - desc = "Print user identity (uid, gid). Defaults to current user.", - flags = {} - }, - { - name = "login", - usage = "login", - desc = "System login prompt. Launched automatically at boot.", - flags = {} - }, - { - name = "ls", - usage = "ls [-alh] [dir]", - desc = "List directory contents.", - flags = { - {"-a", "Show hidden files (starting with .)"}, - {"-l", "Long format: permissions, owner, size"}, - {"-h", "Human-readable file sizes"}, - } - }, - { - name = "lsusers", - usage = "lsusers", - desc = "List all user accounts with uid, gid, home, and shell.", - flags = {} - }, - { - name = "lua", - usage = "lua", - desc = "Interactive Lua REPL prompt.", - flags = {} - }, - { - name = "mkdir", - usage = "mkdir ", - desc = "Create a directory.", - flags = {} - }, - { - name = "passwd", - usage = "passwd [username]", - desc = "Change a user password. Non-root must verify current password first.", - flags = {} - }, - { - name = "ps", - usage = "ps", - desc = "List running tasks with pid, user, name, and status.", - flags = {} - }, - { - name = "pwd", - usage = "pwd", - desc = "Print current working directory.", - flags = {} - }, - { - name = "su", - usage = "su [username]", - desc = "Switch user. Defaults to root. Root can switch without a password.", - flags = {} - }, - { - name = "sudo", - usage = "sudo [-u user] [args...]", - desc = "Run a command as another user (default root). Authenticates as current user.", - flags = { - {"-u user", "Run as the specified user (name or uid)"}, - } - }, - { - name = "sysdump", - usage = "sysdump", - desc = "List all registered kernel syscalls.", - flags = {} - }, - { - name = "useradd", - usage = "useradd [-p pw] [-g gid] [-d home] [-s shell] [-M] ", - desc = "Create a new user account.", - flags = { - {"-p pw", "Set password (prompted interactively if omitted)"}, - {"-g gid", "Set primary group id"}, - {"-d home", "Set home directory (default: /home/username)"}, - {"-s shell", "Set login shell (default: /bin/hysh)"}, - {"-M", "Do not create home directory"}, - } - }, - { - name = "userdel", - usage = "userdel [-r] ", - desc = "Delete a user account.", - flags = { - {"-r", "Also recursively remove the user's home directory"}, - } - }, - { - name = "usermod", - usage = "usermod [-l name] [-p pw] [-g gid] [-d home] [-s shell] [-L] [-U] ", - desc = "Modify an existing user account.", - flags = { - {"-l name", "Rename the user"}, - {"-p pw", "Set new password"}, - {"-g gid", "Change primary group id"}, - {"-d home", "Change home directory"}, - {"-s shell", "Change login shell"}, - {"-L", "Lock the account (disable login)"}, - {"-U", "Unlock the account"}, - } - }, - { - name = "whoami", - usage = "whoami", - desc = "Print the current username.", - flags = {} - }, - { - name = "yes", - usage = "yes [text]", - desc = "Repeatedly print 'y' (or given text) until interrupted with Ctrl+C.", - flags = {} - }, + { name="cd", usage="cd [dir]", desc="Change working directory. Use '-' to return to previous directory.", flags={} }, + { name="pwd", usage="pwd", desc="Print current working directory.", flags={} }, + { name="ls", usage="ls [-alh] [dir]", desc="List directory contents. Coloured by type: dirs=blue, symlinks=cyan, executables=green.", flags={ + {"-a","Show hidden files (starting with .)"}, + {"-l","Long format: permissions, owner, group, size, mtime, name"}, + {"-h","Human-readable file sizes (with -l)"}, + {"--help","Display help and exit"}, + }}, + { name="find", usage="find [path] [-name PAT] [-type f|d|l] [-maxdepth N]", desc="Walk the filesystem tree and print matching paths.", flags={ + {"-name PAT", "Match filename against shell glob (* and ?)"}, + {"-type f|d|l","Filter by file, directory, or symlink"}, + {"-maxdepth N","Descend at most N directory levels"}, + {"-mindepth N","Skip entries shallower than N levels"}, + {"-empty", "Match empty files or empty directories"}, + }}, + { name="cp", usage="cp [-rRp] SOURCE... DEST", desc="Copy files or directories.", flags={ + {"-r,-R","Recurse into directories"}, + {"-p", "Preserve permissions"}, + {"--help","Display help and exit"}, + }}, + { name="mv", usage="mv [-f] SOURCE... DEST", desc="Move or rename files and directories.", flags={ + {"-f", "Do not prompt before overwriting (default)"}, + {"--help","Display help and exit"}, + }}, + { name="rm", usage="rm [-rRf] FILE...", desc="Remove files or directories.", flags={ + {"-r,-R","Recursively remove directories and their contents"}, + {"-f", "Ignore nonexistent files, never prompt"}, + {"--help","Display help and exit"}, + }}, + { name="touch", usage="touch FILE...", desc="Create an empty file, or no-op if it already exists.", flags={} }, + { name="mkdir", usage="mkdir ", desc="Create a directory.", flags={} }, + { name="ln", usage="ln -s [-f] TARGET LINK", desc="Create a symbolic link. Multiple targets can be linked into a directory.", flags={ + {"-s", "Create a symbolic link (required; hard links not supported)"}, + {"-f", "Remove existing destination before creating link"}, + {"--help","Display help and exit"}, + }}, + { name="cat", usage="cat [file...]", desc="Print file(s) to stdout. Reads stdin if no file given.", flags={} }, + { name="head", usage="head [-n N] [file...]", desc="Print the first N lines of each file (default 10).", flags={ + {"-n N","Number of lines to print"}, + {"--help","Display help and exit"}, + }}, + { name="tail", usage="tail [-n N] [file...]", desc="Print the last N lines of each file (default 10).", flags={ + {"-n N","Number of lines to print"}, + {"--help","Display help and exit"}, + }}, + { name="wc", usage="wc [-lwc] [file...]", desc="Count lines, words, and bytes in files.", flags={ + {"-l","Print line count"}, + {"-w","Print word count"}, + {"-c","Print byte count"}, + {"--help","Display help and exit"}, + }}, + { name="grep", usage="grep [-ivnlcrR] PATTERN [file...]", desc="Search for lines matching a Lua pattern.", flags={ + {"-i","Ignore case"}, + {"-v","Invert: select non-matching lines"}, + {"-n","Prefix output with line numbers"}, + {"-l","Print only filenames that contain a match"}, + {"-c","Print count of matching lines per file"}, + {"-r,-R","Recurse into directories"}, + {"--help","Display help and exit"}, + }}, + { name="sed", usage="sed 's/PAT/REPL/' [file...]", desc="Stream editor. Applies substitution commands to each line.", flags={} }, + { name="sort", usage="sort [-rnu] [file...]", desc="Sort lines of text.", flags={ + {"-r","Reverse the sort order"}, + {"-n","Numeric sort"}, + {"-u","Suppress duplicate lines"}, + {"--help","Display help and exit"}, + }}, + { name="uniq", usage="uniq [-cdui] [input [output]]", desc="Filter adjacent duplicate lines.", flags={ + {"-c","Prefix each line with its repetition count"}, + {"-d","Print only lines that appear more than once"}, + {"-u","Print only lines that appear exactly once"}, + {"-i","Ignore case when comparing"}, + {"--help","Display help and exit"}, + }}, + { name="tee", usage="tee [-a] [file...]", desc="Copy stdin to stdout and to each FILE simultaneously.", flags={ + {"-a","Append to files instead of overwriting"}, + {"--help","Display help and exit"}, + }}, + { name="basename", usage="basename STRING [SUFFIX]", desc="Strip directory and optional suffix from a path.", flags={} }, + { name="dirname", usage="dirname STRING...", desc="Strip the last component from a path.", flags={} }, + { name="readlink", usage="readlink [-fenq] file...", desc="Print the target of a symbolic link.", flags={ + {"-f","Canonicalize: follow every symlink component"}, + {"-e","Like -f but all components must exist"}, + {"-n","Do not output trailing newline"}, + {"--help","Display help and exit"}, + }}, + { name="stat", usage="stat file...", desc="Display file type, size, owner, group, and permissions.", flags={ + {"--help","Display help and exit"}, + }}, + { name="chmod", usage="chmod [-R] MODE file...", desc="Change file permissions. MODE may be octal (755) or symbolic (u+x).", flags={ + {"-R", "Recurse into directories"}, + {"--help","Display help and exit"}, + }}, + { name="chown", usage="chown [-R] USER[:GROUP] file...", desc="Change file owner and/or group.", flags={ + {"-R","Recurse into directories"}, + {"--help","Display help and exit"}, + }}, + { name="chgrp", usage="chgrp [-R] GROUP file...", desc="Change file group ownership.", flags={ + {"-R","Recurse into directories"}, + {"--help","Display help and exit"}, + }}, + { name="chattr", usage="chattr [+-=][attrs] file...", desc="Change file attributes.", flags={} }, + { name="echo", usage="echo [text...]", desc="Print arguments to stdout.", flags={} }, + { name="whoami", usage="whoami", desc="Print the current username.", flags={} }, + { name="id", usage="id [username]", desc="Print user identity (uid, gid).", flags={} }, + { name="ps", usage="ps", desc="List running tasks with pid, user, name, and status.", flags={} }, + { name="hostname", usage="hostname [NAME]", desc="Print or set the system hostname.", flags={} }, + { name="uname", usage="uname [-asnrm]", desc="Print system information (OS name, hostname, release, machine).", flags={ + {"-a","Print all fields"}, + {"-s","Kernel name"}, + {"-n","Node hostname"}, + {"-r","Kernel release"}, + {"-m","Machine hardware name"}, + {"--help","Display help and exit"}, + }}, + { name="df", usage="df [-h] [path...]", desc="Report filesystem disk space usage.", flags={ + {"-h","Human-readable sizes (K, M, G)"}, + {"--help","Display help and exit"}, + }}, + { name="stat", usage="stat file...", desc="Display file status: type, size, permissions, owner.", flags={} }, + { name="env", usage="env [KEY=VAL]... [CMD]", desc="Print the environment, or run a command with modified environment.", flags={} }, + { name="printenv", usage="printenv [NAME...]", desc="Print environment variable values (all if no names given).", flags={} }, + { name="sleep", usage="sleep N[smhd]", desc="Pause for N seconds (or minutes/hours/days with m/h/d suffix).", flags={} }, + { name="true", usage="true", desc="Do nothing, exit successfully (status 0).", flags={} }, + { name="false", usage="false", desc="Do nothing, exit unsuccessfully (status 1).", flags={} }, + { name="yes", usage="yes [text]", desc="Repeatedly print 'y' (or given text) until interrupted.", flags={} }, + { name="mount", usage="mount [-o loop] [SRC DEST | ID MNT]", desc="Mount a loop device or show all current mounts.", flags={ + {"-o loop","Attach SRC as a loop device and mount at DEST in one step"}, + {"--help","Display help and exit"}, + }}, + { name="umount", usage="umount [--no-detach] MOUNTPOINT | -l LOOPID", desc="Unmount a filesystem and auto-detach its loop device.", flags={ + {"--no-detach","Unmount but keep loop device attached"}, + {"-l LOOPID","Force-detach a loop device without unmounting"}, + {"--help","Display help and exit"}, + }}, + { name="losetup", usage="losetup [-dil] [path]", desc="Attach a directory or .hfs image as a loop device.", flags={ + {"-d ID","Detach loop device"}, + {"-i path","Force image mode (even without .hfs extension)"}, + {"-l","List all attached loop devices"}, + {"--help","Display help and exit"}, + }}, + { name="loimgcreate", usage="loimgcreate [-x] SRC DEST", desc="Pack a directory into a portable HFS image, or extract one.", flags={ + {"-x","Extract image to destination directory"}, + {"--help","Display help and exit"}, + }}, + { name="useradd", usage="useradd [-p pw] [-g gid] [-d home] [-s shell] [-M] ", desc="Create a new user account.", flags={ + {"-p pw", "Set password"}, + {"-g gid", "Set primary group id"}, + {"-d home", "Set home directory (default /home/username)"}, + {"-s shell","Set login shell (default /bin/hysh)"}, + {"-M", "Do not create home directory"}, + }}, + { name="userdel", usage="userdel [-r] ", desc="Delete a user account.", flags={ + {"-r","Also remove the user's home directory"}, + }}, + { name="usermod", usage="usermod [-l name] [-p pw] [-g gid] [-d home] [-s shell] [-LU] ", desc="Modify an existing user account.", flags={ + {"-l name", "Rename the user"}, + {"-p pw", "Set new password"}, + {"-g gid", "Change primary group id"}, + {"-d home", "Change home directory"}, + {"-s shell","Change login shell"}, + {"-L", "Lock the account"}, + {"-U", "Unlock the account"}, + }}, + { name="passwd", usage="passwd [username]", desc="Change a user password.", flags={} }, + { name="lsusers", usage="lsusers", desc="List all user accounts with uid, gid, home, and shell.", flags={} }, + { name="su", usage="su [username]", desc="Switch user. Defaults to root. Root can switch without a password.", flags={} }, + { name="sudo", usage="sudo [-u user] CMD [args...]", desc="Run a command as another user (default root).", flags={ + {"-u user","Run as the specified user (name or uid)"}, + }}, + { name="exit", usage="exit [N]", desc="Exit the shell with optional status code N.", flags={} }, + { name="clear", usage="clear", desc="Clear the terminal screen.", flags={} }, + { name="help", usage="help [command]", desc="Display this command reference. Pass a command name to filter.", flags={} }, + { name="lua", usage="lua", desc="Interactive Lua REPL prompt.", flags={} }, + { name="micro", usage="micro [file]", desc="Full-screen terminal text editor.", flags={} }, + { name="hfetch", usage="hfetch", desc="Display system information in a neofetch-style layout.", flags={} }, + { name="sysdump", usage="sysdump", desc="List all registered kernel syscalls.", flags={} }, + { name="chroot", usage="chroot DIR [CMD]", desc="Run a command with a different root directory.", flags={} }, } --- Build lines to display -local C_HEAD = 7 -- yellow (section headers) -local C_CMD = 5 -- cyan (command name) -local C_USAGE = 1 -- white (usage line) -local C_DESC = 13 -- grey (description) -local C_FLAG = 3 -- green (flag name) -local C_DIM = 12 -- dark grey +do + local seen = {} + local deduped = {} + for _, cmd in ipairs(COMMANDS) do + if not seen[cmd.name] then + seen[cmd.name] = true + table.insert(deduped, cmd) + end + end + COMMANDS = deduped +end + +local C_HEAD = 7 +local C_CMD = 5 +local C_USAGE = 1 +local C_DESC = 13 +local C_FLAG = 3 +local C_DIM = 12 --- Each entry is {text, color} local lines = {} local function push(text, col) lines[#lines+1] = {text, col or 1} end @@ -177,7 +208,7 @@ push(string.rep("=", 50), C_DIM) push("", 1) local args = {...} -local filter = args[1] -- optional: help +local filter = args[1] local function addCmd(cmd) push(cmd.name, C_CMD) @@ -207,11 +238,10 @@ else for _, cmd in ipairs(COMMANDS) do addCmd(cmd) end end --- Pager local sizeStr = syscall.devctl(1, "size") -local screenW = tonumber(sizeStr:sub(1, sizeStr:find(";")-1)) or 51 -local screenH = tonumber(sizeStr:sub(sizeStr:find(";")+1)) or 19 -local pageSize = screenH - 2 -- reserve bottom row for status bar +local screenW = tonumber(sizeStr:match("^(%d+)")) or 51 +local screenH = tonumber(sizeStr:match(";(%d+)")) or 19 +local pageSize = screenH - 2 local scroll = 0 local totalLines = #lines @@ -225,14 +255,12 @@ local function render() if li <= totalLines then local text, col = lines[li][1], lines[li][2] syscall.devctl(1, "sfgc", col) - -- truncate to screen width if #text > screenW then text = text:sub(1, screenW) end syscall.write(1, text .. "\n") else syscall.write(1, "\n") end end - -- status bar syscall.devctl(1, "sfgc", 16) syscall.devctl(1, "sbgc", 13) local pct = math.floor(math.min(100, (scroll + pageSize) / totalLines * 100)) @@ -246,7 +274,6 @@ local function render() dirty = false end --- If output fits on one screen, just print without pager if totalLines <= pageSize then syscall.devctl(1, "clear") syscall.devctl(1, "spos", 1, 1) @@ -262,30 +289,20 @@ render() while true do local ch = syscall.read(0) if not ch or ch == "" then - -- idle elseif ch == "q" or ch == "Q" then break - elseif ch == "\17" then -- up arrow - if scroll > 0 then - scroll = scroll - 1 - dirty = true - end - elseif ch == "\18" then -- down arrow - if scroll + pageSize < totalLines then - scroll = scroll + 1 - dirty = true - end - elseif ch == "\19" then -- left / page up (reuse left arrow) - scroll = math.max(0, scroll - pageSize) - dirty = true - elseif ch == "\20" then -- right / page down - scroll = math.min(totalLines - pageSize, scroll + pageSize) - dirty = true + elseif ch == "\17" then + if scroll > 0 then scroll = scroll - 1; dirty = true end + elseif ch == "\18" then + if scroll + pageSize < totalLines then scroll = scroll + 1; dirty = true end + elseif ch == "\19" then + scroll = math.max(0, scroll - pageSize); dirty = true + elseif ch == "\20" then + scroll = math.min(totalLines - pageSize, scroll + pageSize); dirty = true end if dirty then render() end end --- Restore screen syscall.devctl(1, "clear") syscall.devctl(1, "spos", 1, 1) syscall.devctl(1, "sfgc", 1) diff --git a/Src/Hyperion-bash/bin/hysh b/Src/Hyperion-bash/bin/hysh index f9c6fdf..5e45fdb 100644 --- a/Src/Hyperion-bash/bin/hysh +++ b/Src/Hyperion-bash/bin/hysh @@ -5,8 +5,6 @@ syscall.open("/dev/null","w") --stderr (device 2) local success, errorMsg = xpcall(function() - - local fs = require("sys.fs") syscall.devctl(1,"clear") @@ -23,53 +21,788 @@ syscall.chdir("/") local oldWD = "" for i = 1, 16 do - syscall.devctl(1,"sbgc",i) - printInline(" "); + syscall.devctl(1,"sbgc",i); printInline(" ") end print("\n") syscall.sigcatch(function(sig) - if sig == 1 then - terminate = true - end + if sig == 1 then terminate = true end end) +local function abspath(p) + if not p or p == "" then return syscall.getcwd() end + if p:sub(1,1) ~= "/" then p = syscall.getcwd().."/"..p end + local parts = {} + for seg in p:gmatch("[^/]+") do + if seg == ".." then if #parts > 0 then table.remove(parts) end + elseif seg ~= "." then table.insert(parts, seg) end + end + return "/"..table.concat(parts, "/") +end + +local function basename(p) + p = p:gsub("/$","") + return p:match("([^/]+)$") or p +end + +local function dirname(p) + p = p:gsub("/$","") + local d = p:match("^(.*)/[^/]+$") + if not d then return "." end + if d == "" then return "/" end + return d +end + +local function readall(fd) + local t = {} + while true do + local ok, chunk = pcall(syscall.read, fd, 65536) + if not ok or not chunk or chunk == "" then break end + t[#t+1] = chunk + end + return table.concat(t) +end + +local function readfile(path) + local ok, fd = pcall(syscall.open, path, "r") + if not ok then return nil, fd end + local data = readall(fd) + pcall(syscall.close, fd) + return data +end + +local function writefile(path, data) + local ok, fd = pcall(syscall.open, path, "w") + if not ok then return false, fd end + pcall(syscall.write, fd, data) + pcall(syscall.close, fd) + return true +end + +local function parseargs(rawargs, flagspec, longflags) + local opts = {} + local args = {} + longflags = longflags or {} + local i = 1 + while i <= #rawargs do + local v = rawargs[i] + if v == "--" then + for j = i+1, #rawargs do args[#args+1] = rawargs[j] end + break + elseif v:sub(1,2) == "--" then + local lf = v:sub(3) + if longflags[lf] ~= nil then opts[lf] = true + else opts["_unknown"] = v end + elseif v:sub(1,1) == "-" and #v > 1 then + for k = 2, #v do + local c = v:sub(k,k) + if flagspec:find(c, 1, true) then opts[c] = true + else opts["_unknown"] = "-"..c end + end + else + args[#args+1] = v + end + i = i + 1 + end + return opts, args +end + +local function eachline(text, fn) + local pos = 1 + while pos <= #text do + local nl = text:find("\n", pos, true) + if nl then fn(text:sub(pos, nl-1)); pos = nl+1 + else fn(text:sub(pos)); break end + end +end + +local function splitlines(text) + local lines = {} + eachline(text, function(l) lines[#lines+1] = l end) + return lines +end + +local function copyfile(src, dst) + local data, err = readfile(src) + if not data then return false, err end + local ok, err2 = writefile(dst, data) + if not ok then return false, err2 end + local ok2, stat = pcall(syscall.stat, src) + if ok2 and stat and stat.perms then pcall(syscall.chmod, dst, stat.perms) end + return true +end + +local function rmtree(path) + local t = syscall.type(path) + if t == "directory" then + local ok, list = pcall(syscall.listdir, path) + if ok then + for _, e in ipairs(list) do + rmtree(path:gsub("/$","").."/"..e) + end + end + end + if t then pcall(syscall.remove, path) end +end + +local function copytree(src, dst) + pcall(syscall.mkdir, dst) + local ok, list = pcall(syscall.listdir, src) + if not ok then return end + for _, e in ipairs(list) do + local s = src:gsub("/$","").."/"..e + local d = dst:gsub("/$","").."/"..e + local t = syscall.type(s) + if t == "directory" then copytree(s, d) + elseif t == "file" then copyfile(s, d) + elseif t == "symlink" then + local ok2, tgt = pcall(syscall.readlink, s) + if ok2 then pcall(syscall.remove, d); pcall(syscall.symlink, tgt, d) end + end + end +end + local builtinCmds = {} builtinCmds.cd = function(path) local cwd = syscall.getcwd() - local dirIn = (path or "") + local dirIn = path or "" if dirIn == "-" then - if oldWD == "" then - print("hysh-cd: No previous working directory set.") - else - print(oldWD) - syscall.chdir(oldWD) - oldWD = cwd - end - return + if oldWD == "" then print("hysh: cd: no previous directory"); return end + print(oldWD); local tmp = oldWD; oldWD = cwd; syscall.chdir(tmp); return end - local dirInMod = dirIn - if dirIn:sub(1, 1) ~= "/" then dirInMod = cwd .. "/" .. dirIn end - local parts = {} - for part in dirInMod:gmatch("[^/]+") do - if part == ".." then - if #parts > 0 then table.remove(parts) end - elseif part ~= "." and part ~= "" then - table.insert(parts, part) - end + local target = abspath(dirIn) + if target:sub(-1) ~= "/" then target = target.."/" end + if not fs.isDir(target) then + print("hysh: cd: "..dirIn..": No such directory"); return end - local normDir = "/" .. table.concat(parts, "/") - if normDir:sub(#normDir, #normDir) ~= "/" then normDir = normDir .. "/" end - - if not fs.isDir(normDir) then - print("hysh-cd: "..dirIn..": No such directory.") - return - end - oldWD = cwd - syscall.chdir(normDir) + oldWD = cwd; syscall.chdir(target) end +builtinCmds.exit = function(code) + syscall.exit(tonumber(code) or 0) +end + +builtinCmds.echo = function(...) + local args = {...} + local n = false + local start = 1 + if args[1] == "-n" then n = true; start = 2 end + local out = table.concat(args, " ", start) + if n then printInline(out) else print(out) end +end + +builtinCmds.pwd = function() + print(syscall.getcwd()) +end + +builtinCmds["true"] = function() end +builtinCmds["false"] = function() end + +builtinCmds.sleep = function(...) + local args = {...} + if #args == 0 then print("sleep: missing operand"); return end + local total = 0 + for _, a in ipairs(args) do + local n, u = a:match("^([%d%.]+)([smhd]?)$") + if not n then print("sleep: invalid time '"..a.."'"); return end + n = tonumber(n) + if u == "m" then n = n * 60 + elseif u == "h" then n = n * 3600 + elseif u == "d" then n = n * 86400 end + total = total + n + end + sleep(total) +end + +builtinCmds.clear = function() + syscall.devctl(1,"clear") + syscall.devctl(1,"sfgc",1) + syscall.devctl(1,"spos",1,1) +end + +builtinCmds.whoami = function() + print(syscall.getUsername() or "unknown") +end + +builtinCmds.hostname = function(...) + local args = {...} + if #args == 0 then + print(syscall.getHostname() or "unknown") + else + local ok, err = pcall(syscall.setHostname, args[1]) + if not ok then print("hostname: "..tostring(err)) end + end +end + +builtinCmds.uname = function(...) + local opts, _ = parseargs({...}, "asnrm") + local all = opts.a + local parts = {} + local results = {} + for word in string.gmatch(syscall.version(), "%S+") do + table.insert(results, word) + end + if all or opts.s or (not opts.s and not opts.n and not opts.r and not opts.m) then + parts[#parts+1] = results[1] + end + if all or opts.n then parts[#parts+1] = (syscall.getHostname() or "hyperion") end + if all or opts.r then parts[#parts+1] = results[2] end + if all or opts.m then parts[#parts+1] = "virtual" end + print(table.concat(parts, " ")) +end + +builtinCmds.printenv = function(...) + local args = {...} + local vars = {"PATH","HOME","USER","SHELL","TERM","HOSTNAME","PWD","OLDPWD","LANG","TZ","EDITOR"} + if #args == 0 then + for _, k in ipairs(vars) do + local ok, v = pcall(syscall.getEnviron, k) + if ok and v then print(k.."="..v) end + end + else + for _, k in ipairs(args) do + local ok, v = pcall(syscall.getEnviron, k) + if ok and v then print(v) end + end + end +end + +builtinCmds.env = function(...) + local args = {...} + local i = 1 + while i <= #args and args[i]:match("^[%w_]+=") do + local k, v = args[i]:match("^([^=]+)=(.*)") + pcall(syscall.setEnviron, k, v) + i = i + 1 + end + if i > #args then + builtinCmds.printenv() + end +end + +builtinCmds.touch = function(...) + local _, args = parseargs({...}, "amc") + if #args == 0 then print("touch: missing operand"); return end + for _, a in ipairs(args) do + local path = abspath(a) + if not syscall.exists(path) then + local ok, fd = pcall(syscall.open, path, "w") + if ok then pcall(syscall.close, fd) + else print("touch: cannot create '"..a.."': "..tostring(fd)) end + end + end +end + +builtinCmds.mkdir = function(...) + local opts, args = parseargs({...}, "p") + if #args == 0 then print("mkdir: missing operand"); return end + for _, a in ipairs(args) do + local path = abspath(a) + if path:sub(-1) ~= "/" then path = path.."/" end + if fs.isDir(path) then + print("mkdir: cannot create '"..a.."': Directory exists"); return + end + local ok, err = pcall(syscall.mkdir, path) + if not ok then print("mkdir: cannot create '"..a.."': "..tostring(err)) end + end +end + +builtinCmds.rm = function(...) + local opts, args = parseargs({...}, "rRf") + if #args == 0 then print("rm: missing operand"); return end + local recursive = opts.r or opts.R + for _, a in ipairs(args) do + local path = abspath(a) + local t = syscall.type(path) + if not t then + if not opts.f then print("rm: cannot remove '"..a.."': No such file or directory") end + elseif t == "directory" then + if not recursive then print("rm: cannot remove '"..a.."': Is a directory") + else rmtree(path) end + else + local ok, err = pcall(syscall.remove, path) + if not ok then print("rm: cannot remove '"..a.."': "..tostring(err)) end + end + end +end + +builtinCmds.cp = function(...) + local opts, args = parseargs({...}, "rRp") + if #args < 2 then print("cp: missing operand"); return end + local recursive = opts.r or opts.R + local dst = abspath(args[#args]) + local dstIsDir = syscall.type(dst) == "directory" + if #args > 2 and not dstIsDir then + print("cp: target '"..args[#args].."' is not a directory"); return + end + for i = 1, #args-1 do + local src = abspath(args[i]) + local t = syscall.type(src) + if not t then + print("cp: '"..args[i].."': No such file or directory") + elseif t == "directory" then + if not recursive then print("cp: omitting directory '"..args[i].."'") + else copytree(src, dstIsDir and (dst:gsub("/$","").."/"..basename(args[i])) or dst) end + else + local d = dstIsDir and (dst:gsub("/$","").."/"..basename(args[i])) or dst + local ok, err = copyfile(src, d) + if not ok then print("cp: '"..args[i].."': "..tostring(err)) end + end + end +end + +builtinCmds.mv = function(...) + local opts, args = parseargs({...}, "f") + if #args < 2 then print("mv: missing operand"); return end + local dst = abspath(args[#args]) + local dstIsDir = syscall.type(dst) == "directory" + if #args > 2 and not dstIsDir then + print("mv: target '"..args[#args].."' is not a directory"); return + end + for i = 1, #args-1 do + local src = abspath(args[i]) + local t = syscall.type(src) + if not t then + print("mv: '"..args[i].."': No such file or directory") + else + local d = dstIsDir and (dst:gsub("/$","").."/"..basename(args[i])) or dst + if t == "directory" then + copytree(src, d); rmtree(src) + else + local ok, err = copyfile(src, d) + if ok then pcall(syscall.remove, src) + else print("mv: '"..args[i].."': "..tostring(err)) end + end + end + end +end + +builtinCmds.cat = function(...) + local args = {...} + if #args == 0 then + while true do + local ok, chunk = pcall(syscall.read, 0, 4096) + if not ok or not chunk or chunk == "" then break end + printInline(chunk) + end + print("") + return + end + for _, a in ipairs(args) do + local path = abspath(a) + if not syscall.exists(path) then + print("cat: "..a..": No such file or directory") + else + local ok, fd = pcall(syscall.open, path, "r") + if not ok then print("cat: "..a..": "..tostring(fd)) + else + while true do + local ok2, chunk = pcall(syscall.read, fd, 65536) + if not ok2 or not chunk or chunk == "" then break end + printInline(chunk) + end + pcall(syscall.close, fd) + end + end + end +end + +builtinCmds.head = function(...) + local raw = {...} + local n = 10 + local files = {} + local i = 1 + while i <= #raw do + local v = raw[i] + if v == "-n" then i=i+1; n=tonumber(raw[i]) or 10 + elseif v:match("^%-n%d+$") then n=tonumber(v:sub(3)) + elseif v:match("^%-%d+$") then n=tonumber(v:sub(2)) + elseif v:sub(1,1) ~= "-" or v == "-" then files[#files+1] = v + end + i=i+1 + end + local multi = #files > 1 + local function dohead(text, label) + if multi then + syscall.devctl(1,"sfgc",4); print("==> "..label.." <=="); syscall.devctl(1,"sfgc",1) + end + local count = 0 + for line in (text.."\n"):gmatch("([^\n]*)\n") do + if count >= n then break end + print(line); count = count+1 + end + end + if #files == 0 then + dohead(readall(0), "stdin") + else + for _, a in ipairs(files) do + if a == "-" then dohead(readall(0), "stdin") + else + local data, err = readfile(abspath(a)) + if not data then print("head: "..a..": "..tostring(err)) + else dohead(data, a) end + end + end + end +end + +builtinCmds.tail = function(...) + local raw = {...} + local n = 10 + local files = {} + local i = 1 + while i <= #raw do + local v = raw[i] + if v == "-n" then i=i+1; n=tonumber(raw[i]) or 10 + elseif v:match("^%-n%d+$") then n=tonumber(v:sub(3)) + elseif v:match("^%-%d+$") then n=tonumber(v:sub(2)) + elseif v:sub(1,1) ~= "-" or v == "-" then files[#files+1] = v + end + i=i+1 + end + local multi = #files > 1 + local function dotail(text, label) + if multi then + syscall.devctl(1,"sfgc",4); print("==> "..label.." <=="); syscall.devctl(1,"sfgc",1) + end + local lines = splitlines(text) + local start = math.max(1, #lines - n + 1) + for j = start, #lines do print(lines[j]) end + end + if #files == 0 then dotail(readall(0), "stdin") + else + for _, a in ipairs(files) do + if a == "-" then dotail(readall(0), "stdin") + else + local data, err = readfile(abspath(a)) + if not data then print("tail: "..a..": "..tostring(err)) + else dotail(data, a) end + end + end + end +end + +builtinCmds.wc = function(...) + local opts, args = parseargs({...}, "lwc") + local showAll = not opts.l and not opts.w and not opts.c + if showAll then opts.l=true; opts.w=true; opts.c=true end + local function count(text) + local l,w,b = 0,0,#text + for _ in text:gmatch("\n") do l=l+1 end + for _ in text:gmatch("%S+") do w=w+1 end + return l,w,b + end + local function fmt(l,w,c,lbl) + local p={} + if opts.l then p[#p+1]=string.format("%7d",l) end + if opts.w then p[#p+1]=string.format("%7d",w) end + if opts.c then p[#p+1]=string.format("%7d",c) end + if lbl then p[#p+1]=" "..lbl end + print(table.concat(p)) + end + local tl,tw,tc = 0,0,0 + if #args == 0 then + local l,w,c = count(readall(0)); fmt(l,w,c) + else + for _, a in ipairs(args) do + local data, err = readfile(abspath(a)) + if not data then print("wc: "..a..": "..tostring(err)) + else + local l,w,c = count(data); fmt(l,w,c,a) + tl=tl+l; tw=tw+w; tc=tc+c + end + end + if #args > 1 then fmt(tl,tw,tc,"total") end + end +end + +builtinCmds.grep = function(...) + local opts, args = parseargs({...}, "ivnlcrR", {["ignore-case"]=true,["invert-match"]=true}) + if #args == 0 then print("grep: missing pattern"); return end + local pat = args[1] + if opts.i or opts["ignore-case"] then pat = pat:lower() end + local recursive = opts.r or opts.R + local found = false + + local function greptext(text, label, showlabel) + local count = 0 + local linenum = 0 + local hasMatch = false + eachline(text, function(line) + linenum = linenum+1 + local test = (opts.i or opts["ignore-case"]) and line:lower() or line + local m = (test:find(pat) ~= nil) + if opts.v or opts["invert-match"] then m = not m end + if m then + count=count+1; hasMatch=true; found=true + if not opts.l and not opts.c then + local out = "" + if showlabel then out=out..label..":" end + if opts.n then out=out..linenum..":" end + print(out..line) + end + end + end) + if opts.l and hasMatch then print(label) end + if opts.c then + print((showlabel and label..":" or "")..count) + end + end + + local function grepfile(path, label, showlabel) + local data, err = readfile(path) + if not data then print("grep: "..label..": "..tostring(err)); return end + greptext(data, label, showlabel) + end + + local function grepdir(dir, prefix) + local ok, list = pcall(syscall.listdir, dir) + if not ok then return end + for _, e in ipairs(list) do + local fp = dir:gsub("/$","").."/"..e + local lbl = (prefix ~= "" and prefix.."/" or "")..e + local t = syscall.type(fp) + if t == "directory" then grepdir(fp, lbl) + elseif t == "file" then grepfile(fp, lbl, true) end + end + end + + if #args == 1 then + greptext(readall(0), "(stdin)", false) + else + local showlabel = #args > 2 or recursive + for i = 2, #args do + local path = abspath(args[i]) + local t = syscall.type(path) + if t == "directory" then + if recursive then grepdir(path, args[i]) + else print("grep: "..args[i]..": Is a directory") end + elseif t then + grepfile(path, args[i], showlabel) + else + print("grep: "..args[i]..": No such file or directory") + end + end + end +end + +builtinCmds.sort = function(...) + local opts, args = parseargs({...}, "rnu") + local lines = {} + local function addlines(text) + local ls = splitlines(text) + for _, l in ipairs(ls) do lines[#lines+1] = l end + end + if #args == 0 then addlines(readall(0)) + else + for _, a in ipairs(args) do + local data, err = readfile(abspath(a)) + if not data then print("sort: "..a..": "..tostring(err)) + else addlines(data) end + end + end + table.sort(lines, function(a,b) + if opts.n then + local na = tonumber(a:match("^%-?%d+%.?%d*")) or 0 + local nb = tonumber(b:match("^%-?%d+%.?%d*")) or 0 + return opts.r and na > nb or na < nb + end + return opts.r and a > b or a < b + end) + local prev = nil + for _, l in ipairs(lines) do + if not opts.u or l ~= prev then print(l); prev = l end + end +end + +builtinCmds.uniq = function(...) + local opts, args = parseargs({...}, "cdui") + local text + if args[1] then + local data, err = readfile(abspath(args[1])) + if not data then print("uniq: "..args[1]..": "..tostring(err)); return end + text = data + else text = readall(0) end + + local prev, prevCount = nil, 0 + local out = {} + local function emit(line, count) + if opts.d and count == 1 then return end + if opts.u and count > 1 then return end + out[#out+1] = opts.c and string.format("%7d %s", count, line) or line + end + eachline(text, function(line) + local cmp = opts.i and line:lower() or line + local cprev = opts.i and (prev and prev:lower()) or prev + if cmp == cprev then prevCount = prevCount+1 + else + if prev ~= nil then emit(prev, prevCount) end + prev = line; prevCount = 1 + end + end) + if prev ~= nil then emit(prev, prevCount) end + + if args[2] then + local ok, err = writefile(abspath(args[2]), table.concat(out, "\n").."\n") + if not ok then print("uniq: "..args[2]..": "..tostring(err)) end + else + for _, l in ipairs(out) do print(l) end + end +end + +builtinCmds.tee = function(...) + local opts, args = parseargs({...}, "a") + local mode = opts.a and "a" or "w" + local fds = {} + for _, a in ipairs(args) do + local ok, fd = pcall(syscall.open, abspath(a), mode) + if not ok then print("tee: "..a..": "..tostring(fd)) + else fds[#fds+1] = fd end + end + while true do + local ok, chunk = pcall(syscall.read, 0, 4096) + if not ok or not chunk or chunk == "" then break end + pcall(syscall.write, 1, chunk) + for _, fd in ipairs(fds) do pcall(syscall.write, fd, chunk) end + end + for _, fd in ipairs(fds) do pcall(syscall.close, fd) end +end + +builtinCmds.find = function(...) + local raw = {...} + local roots, filters = {}, {} + local maxdepth, mindepth = nil, 0 + local i = 1 + while i <= #raw and raw[i]:sub(1,1) ~= "-" do + roots[#roots+1] = raw[i]; i=i+1 + end + if #roots == 0 then roots = {"."} end + while i <= #raw do + local tok = raw[i] + if tok == "-name" then + i=i+1 + local pat = "^"..(raw[i] or ""):gsub("([%.%+%-%^%$%(%)%[%]%%])","%%%1"):gsub("%*",".*"):gsub("%?",".").. "$" + filters[#filters+1] = function(_, e, _2, _3) return e:match(pat) ~= nil end + elseif tok == "-type" then + i=i+1; local ft=raw[i] or "" + filters[#filters+1] = function(_, _, t, _2) + if ft=="f" then return t=="file" + elseif ft=="d" then return t=="directory" + elseif ft=="l" then return t=="symlink" end; return true + end + elseif tok == "-maxdepth" then i=i+1; maxdepth=tonumber(raw[i]) or 0 + elseif tok == "-mindepth" then i=i+1; mindepth=tonumber(raw[i]) or 0 + elseif tok == "-empty" then + filters[#filters+1] = function(path, _, t, _2) + if t=="file" then local ok,s=pcall(syscall.stat,path); return ok and s and (s.size or 0)==0 + elseif t=="directory" then local ok,l=pcall(syscall.listdir,path); return ok and l and #l==0 end + return false + end + end + i=i+1 + end + local function match(path, e, t, depth) + for _, f in ipairs(filters) do if not f(path,e,t,depth) then return false end end + return true + end + local function walk(path, disp, depth) + local t = syscall.type(path) + if not t then return end + local e = path:match("([^/]+)/?$") or path + if depth >= mindepth and match(path,e,t,depth) then print(disp) end + if t=="directory" and (maxdepth==nil or depth 1 and p:sub(-1)=="/" do p=p:sub(1,-2) end + local b = p:match("([^/]+)$") or p + if args[2] and b:sub(-#args[2]) == args[2] then b = b:sub(1, #b-#args[2]) end + print(b) +end + +builtinCmds.dirname = function(...) + local args = {...} + if #args == 0 then print("dirname: missing operand"); return end + for _, p in ipairs(args) do + while #p > 1 and p:sub(-1)=="/" do p=p:sub(1,-2) end + local d = p:match("^(.*)/[^/]+$") + if not d then print(".") + elseif d == "" then print("/") + else print(d) end + end +end + +builtinCmds.df = function(...) + local opts, _ = parseargs({...}, "h") + local function hfmt(n) + local u={"K","M","G","T"}; local i=0 + while n>=1024 and i<#u do n=n/1024; i=i+1 end + if i==0 then return tostring(math.floor(n)) end + if n<10 then return string.format("%.1f%s",n,u[i]) end + return math.floor(n)..u[i] + end + local function fmt(n) return opts.h and hfmt(n) or tostring(n) end + print(string.format("%-20s %10s %10s %10s %6s %-s","Filesystem","Size","Used","Avail","Use%","Mounted on")) + local mounts = {{"$","/"}, {"devfs0000","/dev/"}, {"tmpfs0000","/tmp/"}} + for _, m in ipairs(mounts) do + local id, mp = m[1], m[2] + print(string.format("%-20s %10s %10s %10s %6s %-s", id, "?", "?", "?", "?%", mp)) + end +end local function getUserInput() syscall.devctl(1,"sfgc",3) @@ -88,188 +821,156 @@ local function getUserInput() local blinkState = false local cursorPos = 1 local history = 0 - local dirty = true -- redraw on first iteration + local dirty = true local function redraw() syscall.devctl(1,"spos",curOffsetX,curOffsetY) - -- text before cursor syscall.write(1, string.sub(input, 1, cursorPos-1)) - -- cursor character (inverted if blinking on) if blinkState then - syscall.devctl(1,"sfgc",16) - syscall.devctl(1,"sbgc",1) + syscall.devctl(1,"sfgc",16); syscall.devctl(1,"sbgc",1) end if cursorPos > #input then syscall.write(1, " ") else syscall.write(1, string.sub(input, cursorPos, cursorPos)) end - syscall.devctl(1,"sfgc",1) - syscall.devctl(1,"sbgc",16) - -- text after cursor + trailing space to erase old chars + syscall.devctl(1,"sfgc",1); syscall.devctl(1,"sbgc",16) syscall.write(1, string.sub(input, cursorPos+1) .. " ") end while true do local key = syscall.read(0) if key and key ~= "" then - if key == "\19" then -- left arrow - if cursorPos > 1 then - cursorPos = cursorPos - 1 - dirty = true + if key=="\19" then if cursorPos>1 then cursorPos=cursorPos-1;dirty=true end + elseif key=="\20" then if cursorPos<=#input then cursorPos=cursorPos+1;dirty=true end + elseif key=="\17" then + if history<#commandHistory then + history=history+1 + input=commandHistory[#commandHistory-history+1] + cursorPos=#input+1;dirty=true end - elseif key == "\20" then -- right arrow - if cursorPos <= #input then - cursorPos = cursorPos + 1 - dirty = true + elseif key=="\18" then + if history>1 then + history=history-1 + input=commandHistory[#commandHistory-history+1] + cursorPos=#input+1;dirty=true + elseif history==1 then + history=0;input="";cursorPos=1;dirty=true end - elseif key == "\17" then -- up arrow - if history < #commandHistory then - history = history + 1 - input = commandHistory[#commandHistory - history + 1] - cursorPos = #input + 1 - dirty = true + elseif key=="\b" then + if cursorPos>1 then + input=string.sub(input,1,cursorPos-2)..string.sub(input,cursorPos) + cursorPos=cursorPos-1;dirty=true end - elseif key == "\18" then -- down arrow - if history > 1 then - history = history - 1 - input = commandHistory[#commandHistory - history + 1] - cursorPos = #input + 1 - dirty = true - elseif history == 1 then - history = 0 - input = "" - cursorPos = 1 - dirty = true - end - elseif key == "\b" then - if cursorPos > 1 then - input = string.sub(input, 1, cursorPos-2) .. string.sub(input, cursorPos) - cursorPos = cursorPos - 1 - dirty = true - end - elseif key == "\n" then - -- redraw cleanly with no cursor highlight before committing - syscall.devctl(1,"sfgc",1) - syscall.devctl(1,"sbgc",16) + elseif key=="\n" then + syscall.devctl(1,"sfgc",1);syscall.devctl(1,"sbgc",16) syscall.devctl(1,"spos",curOffsetX,curOffsetY) - syscall.write(1, input .. " \n") + syscall.write(1, input.." \n") return input else - input = string.sub(input, 1, cursorPos-1) .. key .. string.sub(input, cursorPos) - cursorPos = cursorPos + 1 - dirty = true + input=string.sub(input,1,cursorPos-1)..key..string.sub(input,cursorPos) + cursorPos=cursorPos+1;dirty=true end end - - -- cursor blink - local curBlink = ((math.floor(syscall.getUptime() / 500) % 2) == 0) - if curBlink ~= blinkState then - blinkState = curBlink - dirty = true - end - - if dirty then - redraw() - dirty = false - end + local curBlink = ((math.floor(syscall.getUptime()/500)%2)==0) + if curBlink~=blinkState then blinkState=curBlink;dirty=true end + if dirty then redraw();dirty=false end end end +local function printError(progName, msg) + syscall.devctl(1,"sfgc",2) + local s = tostring(msg) + local line, rest = s:match("%]:(%d+): (.+)$") + if not line then line, rest = s:match(":(%d+): (.+)$") end + if line then + printInline(progName..": error on line "..line..": "); print(rest) + else + print(progName..": "..s) + end + syscall.devctl(1,"sfgc",1) +end + local function runCommand(command) do - local func = load("return " .. command, "@equation", "t", {}) + local func = load("return "..command, "@equation", "t", {}) if func then - local success, result = pcall(func) - if success and type(result) == "number" then - print(result) - return - end + local ok, result = pcall(func) + if ok and type(result)=="number" then print(result); return end end end terminate = false local args = string.split(command, " ") + for i = #args, 1, -1 do + if args[i] == "" then table.remove(args, i) end + end + if #args == 0 then return end + if builtinCmds[args[1]] then - local success, msg = pcall(builtinCmds[args[1]], table.unpack(args, 2)) - if not success then - local errSL = string.sub(msg, string.find(msg, "]") + 2) - syscall.devctl(1,"sfgc",2) - printInline(args[1]..": Program runtime error on line ") - print(string.sub(errSL, 1, string.find(errSL, ":") - 1)) - syscall.devctl(1,"sfgc",1) - print(string.sub(errSL, string.find(errSL, ":") + 1)) - end + local ok, err = pcall(builtinCmds[args[1]], table.unpack(args, 2)) + if not ok then printError(args[1], err) end return end local cmdPath = "" if string.find(args[1], "/") then - if fs.exists(args[1]) then - cmdPath = args[1] - end + local candidate = args[1] + if candidate:sub(1,1) ~= "/" then candidate = syscall.getcwd().."/"..candidate end + if fs.exists(candidate) then cmdPath = candidate end else local paths = string.split(syscall.getEnviron("PATH"), ":") - for _, path in pairs(paths) do - if fs.exists(path..args[1]) then - cmdPath = path..args[1] - break - end + for _, p in pairs(paths) do + if fs.exists(p..args[1]) then cmdPath = p..args[1]; break end + end + if cmdPath == "" then + local cwd = syscall.getcwd() + local candidate = cwd..(cwd:sub(-1)=="/" and "" or "/")..args[1] + if fs.exists(candidate) then cmdPath = candidate end end end - if cmdPath == "" then - print(args[1]..": Command not found") + if cmdPath == "" then print(args[1]..": Command not found"); return end + + local progName = cmdPath:match("([^/]+)$") or args[1] + + local xok, xerr = pcall(syscall.access, cmdPath, "x") + if not xok then + syscall.devctl(1,"sfgc",2); print(progName..": Permission denied"); syscall.devctl(1,"sfgc",1) return end - local progName = string.sub(cmdPath, #cmdPath - string.find(string.reverse(cmdPath), "/") + 2) - local text = fs.readAllText(cmdPath) local program, err = load(text, progName) if not program then - local errSL = string.sub(err, string.find(err, ":") + 1) syscall.devctl(1,"sfgc",2) - printInline(progName..": Program load error on line ") - print(string.sub(errSL, 1, string.find(errSL, ":") - 1)) - syscall.devctl(1,"sfgc",1) - print(string.sub(errSL, string.find(errSL, ":") + 1)) - return + local line, rest = tostring(err):match(":(%d+): (.+)$") + if line then printInline(progName..": load error on line "..line..": "); print(rest) + else print(progName..": load error: "..tostring(err)) end + syscall.devctl(1,"sfgc",1); return end local proc = syscall.spawn(function(...) syscall.open("/dev/tty/TTY1","r") syscall.open("/dev/tty/TTY1","w") syscall.open("/dev/null","w") - local success, msg = pcall(program, ...) - if not success then - local errSL = string.sub(msg, string.find(msg, ":") + 1) - syscall.devctl(1,"sfgc",2) - printInline(progName..": Program runtime error on line ") - print(string.sub(errSL, 1, string.find(errSL, ":") - 1)) - syscall.devctl(1,"sfgc",1) - print(string.sub(errSL, string.find(errSL, ":") + 1)) - end + local ok2, msg = pcall(program, ...) + if not ok2 then printError(progName, msg) end end, progName, nil, {table.unpack(args, 2)}) while true do local exited, code = syscall.collect(proc) if exited then - if code then - print("\nTask exited with code:\n"..tostring(code)) - end + if code then print("\nTask exited with code:\n"..tostring(code)) end return end if terminate then - local success, err = syscall.kill(proc) - if success then - syscall.devctl(1,"sbgc",16) - syscall.devctl(1,"sfgc",2) - print("\nProgram Terminated.") - syscall.devctl(1,"sfgc",1) + local ok2 = syscall.kill(proc) + if ok2 then + syscall.devctl(1,"sbgc",16); syscall.devctl(1,"sfgc",2) + print("\nProgram Terminated."); syscall.devctl(1,"sfgc",1) end - terminate = false - break + terminate = false; break end sleep(0.05) end @@ -285,8 +986,6 @@ while true do end end - ---ERROR HANDLING end, debug.traceback) if not success then diff --git a/Src/Hyperion-bash/bin/ln b/Src/Hyperion-bash/bin/ln new file mode 100644 index 0000000..27668da --- /dev/null +++ b/Src/Hyperion-bash/bin/ln @@ -0,0 +1,96 @@ +--:Minify:-- +local name = syscall.getTask(syscall.getpid()).name +local cloptions = { s = false, f = false, help = false } +local args = {} + +for _, v in ipairs({ ... }) do + if v:sub(1, 2) == "--" then + local opt = v:sub(3) + if cloptions[opt] == nil then + print(name .. ": unrecognized option '" .. v .. "'") + print("try '" .. name .. " --help' for more information.") + syscall.exit(1); return + end + cloptions[opt] = true + elseif v:sub(1, 1) == "-" then + for i = 2, #v do + local opt = v:sub(i, i) + if cloptions[opt] == nil then + print(name .. ": invalid option '-" .. opt .. "'") + print("try '" .. name .. " --help' for more information.") + syscall.exit(1); return + end + cloptions[opt] = true + end + else + table.insert(args, v) + end +end + +if cloptions.help then + print("Usage: " .. name .. " [OPTION]... TARGET LINK_NAME") + print(" " .. name .. " [OPTION]... TARGET... DIRECTORY") + print("Create links between files.") + print("") + print("Options:") + print(" -s make symbolic links instead of hard links") + print(" -f remove existing destination files") + print(" --help display this help and exit") + print("") + print("With no -s, hard links are not supported (filesystem limitation).") + print("Use -s for symbolic links.") + return +end + +if #args < 2 then + print(name .. ": missing operand") + print("try '" .. name .. " --help' for more information.") + syscall.exit(1); return +end + +if not cloptions.s then + print(name .. ": hard links are not supported; use -s for symbolic links") + syscall.exit(1); return +end + +local dest = args[#args] +local destDir = syscall.type(dest) == "directory" + +local function cwd() + local d = syscall.getcwd() + if d:sub(-1) ~= "/" then d = d .. "/" end + return d +end + +local function absPath(p) + if p:sub(1,1) ~= "/" then p = cwd() .. p end + return p +end + +for i = 1, #args - 1 do + local target = args[i] + local linkPath + + if destDir then + local basename = target:match("[^/]+$") or target + linkPath = absPath(dest) + if linkPath:sub(-1) ~= "/" then linkPath = linkPath .. "/" end + linkPath = linkPath .. basename + else + linkPath = absPath(dest) + end + + if cloptions.f and syscall.exists(linkPath) then + local ok, err = pcall(syscall.remove, linkPath) + if not ok then + print(name .. ": cannot remove '" .. linkPath .. "': " .. tostring(err)) + syscall.exit(1); return + end + end + + local ok, err = pcall(syscall.symlink, target, linkPath) + if not ok then + print(name .. ": failed to create symlink '" .. linkPath .. "' -> '" .. target .. "': " .. tostring(err)) + syscall.exit(1); return + end +end diff --git a/Src/Hyperion-bash/bin/login b/Src/Hyperion-bash/bin/login index be63256..912e952 100644 --- a/Src/Hyperion-bash/bin/login +++ b/Src/Hyperion-bash/bin/login @@ -3,7 +3,6 @@ syscall.open("/dev/tty/TTY1", "r") --stdin (fd 0) syscall.open("/dev/tty/TTY1", "w") --stdout (fd 1) syscall.open("/dev/null", "w") --stderr (fd 2) -local fs = require("sys.fs") local MAX_ATTEMPTS = 3 @@ -12,7 +11,6 @@ local function readLine(mask) while true do local ch = syscall.read(0) if not ch or ch == "" then - -- buffer empty, spin elseif ch == "\n" then syscall.write(1, "\n") return input @@ -29,7 +27,12 @@ local function readLine(mask) end local function firstBoot() - local shadow = fs.readAllText("/etc/shadow") or "" + local shadow = "" + local _fd, _fderr = pcall(function() + local fd = syscall.open("/etc/shadow", "r") + shadow = syscall.read(fd, 65535) or "" + syscall.close(fd) + end) if shadow:match("%S") then return end syscall.devctl(1, "clear") @@ -71,35 +74,36 @@ local function firstBoot() end local function spawnShell(username, uid, shell, homedir) - local shellText = fs.readAllText(shell) - if not shellText then + local existsOk, existsErr = pcall(syscall.exists, shell) + if not existsOk or not existsErr then syscall.write(1, "login: shell not found: " .. shell .. "\n") sleep(2) return false end - local errFifo = {} + local accessOk, accessErr = pcall(syscall.access, shell, "rx") - local proc = syscall.spawn(function() - syscall.setuid(uid) - syscall.chdir(homedir) - syscall.setEnviron("HOME", homedir) - syscall.setEnviron("USER", username) - syscall.setEnviron("SHELL", shell) - syscall.setEnviron("PATH", "/bin/") + syscall.setEnviron("HOME", homedir) + syscall.setEnviron("USER", username) + syscall.setEnviron("SHELL", shell) + syscall.setEnviron("PATH", "/bin/") - local shellFn, loadErr = load(shellText, "@" .. shell) - if not shellFn then - syscall.log("login: shell load error: " .. tostring(loadErr), "ERROR") - syscall.exit(-1) - return - end + local setuidOk, setuidErr = pcall(syscall.setuid, uid) + if not setuidOk then + syscall.write(1, "login: setuid failed: " .. tostring(setuidErr) .. "\n") + sleep(2) + return false + end + + local chdirOk, chdirErr = pcall(syscall.chdir, homedir) + + local ok, err = pcall(syscall.execspawn, shell, username .. ":shell") + if not ok then + syscall.write(1, "login: failed to launch shell: " .. tostring(err) .. "\n") + sleep(2) + return false + end - local ok, runErr = xpcall(shellFn, debug.traceback) - if not ok then - syscall.log("login: shell runtime error: " .. tostring(runErr), "ERROR") - end - end, username .. ":shell") syscall.exit(0) end @@ -126,9 +130,10 @@ local function doLogin() local ok, err = syscall.login(username, password) if ok then - local uid = syscall.getuid(username) - local pwent = uid and syscall.getpasswd(uid) - local shell = (pwent and pwent.shell) or "/bin/hysh" + local uid = syscall.getuid() + local pwent = syscall.getpasswd(uid) + + local shell = (pwent and pwent.shell) or "/bin/hysh" local homedir = (pwent and pwent.homedir) or "/" syscall.devctl(1, "sfgc", 3) diff --git a/Src/Hyperion-bash/bin/loimgcreate b/Src/Hyperion-bash/bin/loimgcreate new file mode 100644 index 0000000..76d9699 --- /dev/null +++ b/Src/Hyperion-bash/bin/loimgcreate @@ -0,0 +1,157 @@ +--:Minify:-- +-- Usage: +-- loimgcreate create image from directory +-- loimgcreate -x extract image back to a directory +-- loimgcreate --help + +local name = syscall.getTask(syscall.getpid()).name +local args, opts = {}, { x=false, help=false } + +for _, v in ipairs({...}) do + if v:sub(1,2) == "--" then + local o = v:sub(3) + if o == "help" then opts.help = true + else print(name..": unrecognised option '"..v.."'") + print("try '"..name.." --help' for more information.") + syscall.exit(1); return end + elseif v:sub(1,1) == "-" then + for i = 2, #v do + local c = v:sub(i,i) + if opts[c] ~= nil then opts[c] = true + else print(name..": invalid option '-"..c.."'") + print("try '"..name.." --help' for more information.") + syscall.exit(1); return end + end + else + table.insert(args, v) + end +end + +if opts.help then + print("Usage: "..name.." ") + print(" "..name.." -x ") + print("") + print("Pack a directory into a portable HFS image file, or extract one.") + print("") + print(" recursively pack srcdir into image.hfs") + print(" -x extract image.hfs into dest (created if needed)") + print("") + print("HFS images can be mounted with:") + print(" mount -o loop /path/to/image.hfs /mnt/point") + print("") + print("Requires root.") + return +end + +local fs = require("sys.fs") + +if opts.x then + if #args < 2 then + print(name..": -x requires and ") + print("try '"..name.." --help' for more information.") + syscall.exit(1); return + end + + local imgPath = args[1] + local destPath = args[2] + if imgPath:sub(1,1) ~= "/" then imgPath = syscall.getcwd().."/"..imgPath end + if destPath:sub(1,1) ~= "/" then destPath = syscall.getcwd().."/"..destPath end + + local tmpMnt = "/tmp/._loimgcreate_"..tostring(math.random(100000,999999)) + + local ok1, loopId = pcall(syscall.losetup, imgPath, true) + if not ok1 then + print(name..": losetup: "..tostring(loopId)); syscall.exit(1); return + end + + local ok2, merr = pcall(syscall.mount, tmpMnt, loopId) + if not ok2 then + pcall(syscall.lodetach, loopId) + print(name..": mount: "..tostring(merr)); syscall.exit(1); return + end + + if not fs.isDir(destPath) then + local ok3, derr = pcall(syscall.mkdir, destPath) + if not ok3 then + pcall(syscall.umount, tmpMnt); pcall(syscall.lodetach, loopId) + print(name..": mkdir '"..args[2].."': "..tostring(derr)) + syscall.exit(1); return + end + end + + local count = 0 + local function copyTree(src, dst) + local entries = fs.list(src) + if not entries then return end + for _, ent in ipairs(entries) do + local srcFull = src:gsub("/$","").."/"..ent + local dstFull = dst:gsub("/$","").."/"..ent + if fs.isDir(srcFull) then + pcall(syscall.mkdir, dstFull) + copyTree(srcFull, dstFull) + else + local ok, rfd = pcall(syscall.open, srcFull, "r") + if ok then + local ok2, wfd = pcall(syscall.open, dstFull, "w") + if ok2 then + local ok3, data = pcall(syscall.read, rfd, 65536*16) + if ok3 and data then pcall(syscall.write, wfd, data) end + pcall(syscall.close, wfd) + count = count + 1 + end + pcall(syscall.close, rfd) + end + end + end + end + + copyTree(tmpMnt, destPath) + + pcall(syscall.umount, tmpMnt) + pcall(syscall.lodetach, loopId) + + syscall.devctl(1, "sfgc", 10) + print(name..": extracted "..count.." file(s) to "..destPath) + syscall.devctl(1, "sfgc", 1) + return +end + +if #args < 2 then + print(name..": missing operands — need and ") + print("try '"..name.." --help' for more information.") + syscall.exit(1); return +end + +local srcPath = args[1] +local imgPath = args[2] +if srcPath:sub(1,1) ~= "/" then srcPath = syscall.getcwd().."/"..srcPath end +if imgPath:sub(1,1) ~= "/" then imgPath = syscall.getcwd().."/"..imgPath end + +if not fs.isDir(srcPath) then + print(name..": '"..args[1].."': not a directory") + syscall.exit(1); return +end + +local ok, imgStr = pcall(syscall.loimgcreate, srcPath) +if not ok then + local msg = tostring(imgStr) + if msg:find("EPERM") then msg = "Permission denied" + elseif msg:find("ENOTDIR") then msg = "'"..args[1].."': not a directory" end + print(name..": "..msg); syscall.exit(1); return +end + +local ok2, werr = pcall(syscall.loimgwrite, imgStr, imgPath) +if not ok2 then + print(name..": write '"..args[2].."': "..tostring(werr)) + syscall.exit(1); return +end + +local lineCount = 0 +for _ in imgStr:gmatch("\n") do lineCount = lineCount + 1 end +local byteCount = #imgStr + +syscall.devctl(1, "sfgc", 10) +print(name..": image written to "..imgPath) +syscall.devctl(1, "sfgc", 14) +print(string.format(" %d records, %d bytes", lineCount - 1, byteCount)) +syscall.devctl(1, "sfgc", 1) diff --git a/Src/Hyperion-bash/bin/looptest b/Src/Hyperion-bash/bin/looptest new file mode 100644 index 0000000..58e9aa4 --- /dev/null +++ b/Src/Hyperion-bash/bin/looptest @@ -0,0 +1,427 @@ +--:Minify:-- + +local passed, failed = 0, 0 + +local function pass(msg) syscall.devctl(1,"sfgc",10); print(" PASS "..msg); syscall.devctl(1,"sfgc",1) end +local function fail(msg) syscall.devctl(1,"sfgc",2); print(" FAIL "..msg); syscall.devctl(1,"sfgc",1) end +local function info(msg) syscall.devctl(1,"sfgc",14); print(" .... "..msg); syscall.devctl(1,"sfgc",1) end +local function head(msg) syscall.devctl(1,"sfgc",4); print("\n"..msg); syscall.devctl(1,"sfgc",1) end + +local function check(label, ok, err) + if ok then passed = passed + 1; pass(label) + else failed = failed + 1; fail(label.." - "..tostring(err)) end +end + +local function writeFile(path, data) + local ok, fd = pcall(syscall.open, path, "w") + if not ok then return false, fd end + local ok2, err = pcall(syscall.write, fd, data) + pcall(syscall.close, fd) + return ok2, err +end + +local function readFile(path) + local ok, fd = pcall(syscall.open, path, "r") + if not ok then return false, fd end + local ok2, data = pcall(syscall.read, fd, 65536) + pcall(syscall.close, fd) + return ok2, data +end + +local function rmrf(path) + local t = syscall.type(path) + if t == "directory" then + local ok, entries = pcall(syscall.listdir, path) + if ok then + for _, name in ipairs(entries) do + rmrf(path:gsub("/$","").."/"..name) + end + end + pcall(syscall.remove, path) + elseif t == "file" then + pcall(syscall.remove, path) + end +end + +local SCRATCH = "/tmp/looptest_scratch" +local SRC_DIR = SCRATCH.."/src" +local BIND_MNT = SCRATCH.."/bind_mnt" +local IMG_PATH = SCRATCH.."/test.hfs" +local IMG_MNT = SCRATCH.."/img_mnt" +local BIND_LOOP = nil +local IMG_LOOP = nil + +rmrf(SCRATCH) +pcall(syscall.mkdir, SCRATCH) +pcall(syscall.mkdir, SRC_DIR) +pcall(syscall.mkdir, BIND_MNT) +pcall(syscall.mkdir, IMG_MNT) +pcall(syscall.mkdir, SRC_DIR.."/subdir") + +writeFile(SRC_DIR.."/hello.txt", "hello from hyperion\n") +writeFile(SRC_DIR.."/data.txt", "line1\nline2\nline3\n") +writeFile(SRC_DIR.."/subdir/deep.txt", "deep file\n") + +head("[ 1 ] bind mode - losetup on a directory") +do + local ok, id = pcall(syscall.losetup, SRC_DIR) + check("losetup(dir) returns a loop id", ok and type(id) == "string" and id:sub(1,4) == "loop", id) + if ok then + BIND_LOOP = id + info("attached as "..id) + end +end + +head("[ 2 ] bind mode - mount and read files") +do + if BIND_LOOP then + local mok = pcall(syscall.mount, BIND_MNT, BIND_LOOP) + check("mount(bind_mnt, "..BIND_LOOP..")", mok, "mount failed") + + if mok then + local lok, entries = pcall(syscall.listdir, BIND_MNT) + check("listdir through bind mount", lok and type(entries) == "table", entries) + + local rok, data = readFile(BIND_MNT.."/hello.txt") + check("read hello.txt through bind", rok and data == "hello from hyperion\n", + rok and ("got: "..tostring(data):sub(1,40)) or tostring(data)) + + local rok2, data2 = readFile(BIND_MNT.."/subdir/deep.txt") + check("read subdir/deep.txt through bind", rok2 and data2 == "deep file\n", + rok2 and ("got: "..tostring(data2)) or tostring(data2)) + end + else + check("mount (skipped - no loop id)", false, "losetup failed in [1]") + end +end + +head("[ 3 ] bind mode - write through loop mount, verify on host") +do + if BIND_LOOP then + local wok, werr = writeFile(BIND_MNT.."/written.txt", "written via loop\n") + check("write new file through bind mount", wok, werr) + + local rok, data = readFile(BIND_MNT.."/written.txt") + check("read back through bind mount", rok and data == "written via loop\n", + rok and ("got: "..tostring(data)) or tostring(data)) + + local rok2, data2 = readFile(SRC_DIR.."/written.txt") + check("file visible on host path (bind is transparent)", rok2 and data2 == "written via loop\n", + rok2 and ("got: "..tostring(data2)) or tostring(data2)) + else + check("write (skipped)", false, "bind mount not set up") + end +end + +head("[ 4 ] bind mode - lodetach while mounted returns EBUSY") +do + if BIND_LOOP then + local ok = pcall(syscall.lodetach, BIND_LOOP) + check("lodetach while mounted is refused (EBUSY)", not ok, "should have errored") + else + check("lodetach busy check (skipped)", false, "no bind loop") + end +end + +head("[ 5 ] bind mode - umount then lodetach") +do + if BIND_LOOP then + local uok = pcall(syscall.umount, BIND_MNT) + check("umount(bind_mnt)", uok, "umount failed") + + local dok = pcall(syscall.lodetach, BIND_LOOP) + check("lodetach after umount", dok, "lodetach failed") + + if dok then BIND_LOOP = nil end + else + check("umount+lodetach (skipped)", false, "no bind loop") + end +end + +head("[ 6 ] loimgcreate - serialise directory to HFS image") +do + local ok, imgStr = pcall(syscall.loimgcreate, SRC_DIR) + check("loimgcreate(srcdir) returns a string", ok and type(imgStr) == "string" and #imgStr > 0, imgStr) + + if ok then + info("image size: "..#imgStr.." bytes") + + local isBHFS = imgStr:sub(1, 4) == "BHFS" + check("image has BHFS magic header", isBHFS, + "got: "..imgStr:sub(1,4):gsub(".", function(c) return string.format("%02X ", c:byte()) end)) + check("image has correct version byte (0x01)", imgStr:byte(5) == 1, + "version byte: "..tostring(imgStr:byte(5))) + check("image contains FILE record (type=0x01)", imgStr:find("\001", 9, true) ~= nil, "no FILE type byte found") + check("image contains DIR record (type=0x02)", imgStr:find("\002", 9, true) ~= nil, "no DIR type byte found") + check("image ends with END record (type=0xFF)", imgStr:byte(#imgStr) == 0xFF, + "last byte: 0x"..string.format("%02X", imgStr:byte(#imgStr))) + + local wok, werr = pcall(syscall.loimgwrite, imgStr, IMG_PATH) + check("loimgwrite writes image file", wok, werr) + check("image file exists on disk", syscall.type(IMG_PATH) == "file", "file not found") + end +end + +head("[ 7 ] HFS image - losetup attaches image file") +do + if syscall.type(IMG_PATH) == "file" then + local ok, id = pcall(syscall.losetup, IMG_PATH) + check("losetup(image.hfs) returns loop id", ok and type(id) == "string", id) + if ok then + IMG_LOOP = id + info("image attached as "..id) + + local lok, devs = pcall(syscall.lolist) + local found = false + if lok then + for lid, info_entry in pairs(devs) do + if lid == id then found = true end + end + end + check("lolist() contains new image device", found, "not found in lolist") + end + else + check("losetup image (skipped - no image file)", false, "image not created in [6]") + end +end + +head("[ 8 ] HFS image - mount and read files") +do + if IMG_LOOP then + local mok = pcall(syscall.mount, IMG_MNT, IMG_LOOP) + check("mount(img_mnt, "..IMG_LOOP..")", mok, "mount failed") + + if mok then + local lok, entries = pcall(syscall.listdir, IMG_MNT) + check("listdir through image mount returns table", lok and type(entries) == "table", entries) + if lok then + local hasHello = false + local hasSubdir = false + for _, e in ipairs(entries) do + if e == "hello.txt" then hasHello = true end + if e == "subdir" then hasSubdir = true end + end + check("hello.txt visible in image root", hasHello, "not listed") + check("subdir/ visible in image root", hasSubdir, "not listed") + end + + local rok, data = readFile(IMG_MNT.."/hello.txt") + check("read hello.txt from image", rok and data == "hello from hyperion\n", + rok and ("got: "..tostring(data):sub(1,40)) or tostring(data)) + + local rok2, data2 = readFile(IMG_MNT.."/data.txt") + check("read data.txt from image", rok2 and data2 == "line1\nline2\nline3\n", + rok2 and ("got: "..tostring(data2)) or tostring(data2)) + end + else + check("image mount read (skipped)", false, "no image loop") + end +end + +head("[ 9 ] HFS image - write new files into image mount") +do + if IMG_LOOP then + local wok, werr = writeFile(IMG_MNT.."/newfile.txt", "created inside image\n") + check("write new file into image mount", wok, werr) + + local rok, data = readFile(IMG_MNT.."/newfile.txt") + check("read back newly written file", rok and data == "created inside image\n", + rok and ("got: "..tostring(data)) or tostring(data)) + + local wok2, werr2 = writeFile(IMG_MNT.."/hello.txt", "overwritten\n") + check("overwrite existing file in image", wok2, werr2) + + local rok2, data2 = readFile(IMG_MNT.."/hello.txt") + check("overwritten content reads back correctly", rok2 and data2 == "overwritten\n", + rok2 and ("got: "..tostring(data2)) or tostring(data2)) + + local rok3, orig = readFile(IMG_PATH) + check("disk image file is unchanged after in-memory write", + rok3 and orig and orig:find("/hello%.txt") ~= nil, + rok3 and "filename record missing from image" or tostring(orig)) + else + check("image write test (skipped)", false, "image not mounted") + end +end + +head("[ 10 ] HFS image - sub-directory traversal") +do + if IMG_LOOP then + local t = syscall.type(IMG_MNT.."/subdir") + check("type(subdir) == 'directory'", t == "directory", "got: "..tostring(t)) + + local lok, entries = pcall(syscall.listdir, IMG_MNT.."/subdir") + check("listdir(subdir) works", lok and type(entries) == "table", entries) + + local rok, data = readFile(IMG_MNT.."/subdir/deep.txt") + check("read subdir/deep.txt from image", rok and data == "deep file\n", + rok and ("got: "..tostring(data)) or tostring(data)) + + local mok = pcall(syscall.mkdir, IMG_MNT.."/subdir/newdir") + check("mkdir inside image mount", mok, "mkdir failed") + check("new dir has type 'directory'", + syscall.type(IMG_MNT.."/subdir/newdir") == "directory", + "wrong type") + + local wok, werr = writeFile(IMG_MNT.."/subdir/newdir/x.txt", "x\n") + check("write file in newly created subdir", wok, werr) + else + check("subdir traversal (skipped)", false, "image not mounted") + end +end + +head("[ 11 ] HFS image - lodetach while mounted returns EBUSY") +do + if IMG_LOOP then + local ok = pcall(syscall.lodetach, IMG_LOOP) + check("lodetach image while mounted is refused", not ok, "should have errored EBUSY") + else + check("lodetach busy (skipped)", false, "no image loop") + end +end + +head("[ 12 ] HFS image - umount then lodetach") +do + if IMG_LOOP then + local uok = pcall(syscall.umount, IMG_MNT) + check("umount(img_mnt)", uok, "umount failed") + + local dok = pcall(syscall.lodetach, IMG_LOOP) + check("lodetach after umount", dok, "lodetach failed") + + if dok then + local lok, devs = pcall(syscall.lolist) + local found = false + if lok then + for lid in pairs(devs) do + if lid == IMG_LOOP then found = true end + end + end + check("lolist no longer shows detached device", not found, "still present in lolist") + IMG_LOOP = nil + end + else + check("image umount+lodetach (skipped)", false, "no image loop") + end +end + +head("[ 13 ] lolist - reflects attached device count") +do + local ok1, id1 = pcall(syscall.losetup, SRC_DIR) + local ok2, id2 = pcall(syscall.losetup, SRC_DIR) + check("attach first device for lolist test", ok1, id1) + check("attach second device for lolist test", ok2, id2) + + if ok1 and ok2 then + local lok, devs = pcall(syscall.lolist) + check("lolist() succeeds", lok, devs) + if lok then + local found1, found2 = false, false + for lid in pairs(devs) do + if lid == id1 then found1 = true end + if lid == id2 then found2 = true end + end + check("lolist contains first device", found1, "missing "..id1) + check("lolist contains second device", found2, "missing "..id2) + local count = 0 + for _ in pairs(devs) do count = count + 1 end + info("lolist shows "..count.." device(s)") + end + end + + if ok1 then pcall(syscall.lodetach, id1) end + if ok2 then pcall(syscall.lodetach, id2) end +end + +head("[ 14 ] losetup - non-existent path returns error") +do + local ok = pcall(syscall.losetup, "/tmp/does_not_exist_xyz_looptest") + check("losetup on missing path errors", not ok, "should have errored") +end + +head("[ 15 ] mount - same loop device cannot be mounted twice") +do + local ok, id = pcall(syscall.losetup, SRC_DIR) + check("losetup for double-mount test", ok, id) + if ok then + local m1ok = pcall(syscall.mount, BIND_MNT, id) + check("first mount succeeds", m1ok, "mount failed") + + if m1ok then + local m2ok = pcall(syscall.mount, IMG_MNT, id) + check("second mount of same device is refused", not m2ok, "should have errored EBUSY") + pcall(syscall.umount, BIND_MNT) + end + pcall(syscall.lodetach, id) + end +end + +head("[ 16 ] loimgcreate - on a regular file returns ENOTDIR") +do + local ok = pcall(syscall.loimgcreate, IMG_PATH) + check("loimgcreate on a file errors (ENOTDIR)", not ok, "should have errored") +end + +head("[ 17 ] HFS image - binary round-trip (all byte values)") +do + local bytes = {} + for i = 0, 255 do bytes[i+1] = string.char(i) end + local binData = table.concat(bytes) + + local binSrc = SCRATCH.."/binsrc" + pcall(syscall.mkdir, binSrc) + writeFile(binSrc.."/binary.bin", binData) + + local ok1, imgStr = pcall(syscall.loimgcreate, binSrc) + check("loimgcreate handles binary content", ok1, imgStr) + + if ok1 then + local binImg = SCRATCH.."/binary.hfs" + pcall(syscall.loimgwrite, imgStr, binImg) + + local ok2, lid = pcall(syscall.losetup, binImg) + check("losetup on binary image", ok2, lid) + + if ok2 then + local mnt = SCRATCH.."/binmnt" + pcall(syscall.mkdir, mnt) + local mok = pcall(syscall.mount, mnt, lid) + check("mount binary image", mok, "mount failed") + + if mok then + local rok, readBack = readFile(mnt.."/binary.bin") + check("binary file readable from image", rok, readBack) + check("binary data round-trips without corruption", + rok and readBack == binData, + rok and string.format("length in=%d out=%d", #binData, #(readBack or "")) or tostring(readBack)) + pcall(syscall.umount, mnt) + end + pcall(syscall.lodetach, lid) + end + end +end + +head("[ 18 ] second-run safety - lolist is empty after full cleanup") +do + local lok, devs = pcall(syscall.lolist) + check("lolist() call succeeds", lok, devs) + if lok then + local count = 0 + for _ in pairs(devs) do count = count + 1 end + check("no leftover loop devices after all tests", count == 0, + count.." device(s) still attached: ".. + (function() + local ids = {} + for id in pairs(devs) do ids[#ids+1] = id end + return table.concat(ids, ", ") + end)()) + end +end + +rmrf(SCRATCH) + +print("") +syscall.devctl(1, "sfgc", failed == 0 and 10 or 2) +print(string.format("Results: %d passed, %d failed", passed, failed)) +syscall.devctl(1, "sfgc", 1) +if failed > 0 then syscall.exit(1) end diff --git a/Src/Hyperion-bash/bin/losetup b/Src/Hyperion-bash/bin/losetup new file mode 100644 index 0000000..bbba40d --- /dev/null +++ b/Src/Hyperion-bash/bin/losetup @@ -0,0 +1,129 @@ +--:Minify:-- +-- Usage: +-- losetup attach directory or .hfs image; print loop id +-- losetup -d detach loop device +-- losetup -l list attached loop devices +-- losetup -i force image mode (even without .hfs extension) +-- losetup --help + +local name = syscall.getTask(syscall.getpid()).name +local args, opts = {}, { d=false, l=false, i=false, help=false } + +for _, v in ipairs({...}) do + if v:sub(1,2) == "--" then + local o = v:sub(3) + if o == "help" then opts.help = true + else print(name..": unrecognised option '"..v.."'") + print("try '"..name.." --help' for more information.") + syscall.exit(1); return end + elseif v:sub(1,1) == "-" then + for i = 2, #v do + local c = v:sub(i,i) + if opts[c] ~= nil then opts[c] = true + else print(name..": invalid option '-"..c.."'") + print("try '"..name.." --help' for more information.") + syscall.exit(1); return end + end + else + table.insert(args, v) + end +end + +if opts.help then + print("Usage: "..name.." ") + print(" "..name.." -i ") + print(" "..name.." -d ") + print(" "..name.." -l") + print("") + print("Manage loop devices.") + print("") + print(" attach a directory (bind) or .hfs image file") + print(" -i force image mode for the given file") + print(" -d detach loop device by id (must be unmounted first)") + print(" -l list all currently attached loop devices") + print("") + print("Requires root. Loop device ids look like loop0, loop1, …") + return +end + +if opts.l then + local ok, devs = pcall(syscall.lolist) + if not ok then + print(name..": "..tostring(devs)); syscall.exit(1); return + end + local any = false + local ids = {} + for id in pairs(devs) do ids[#ids+1] = id end + table.sort(ids) + for _, id in ipairs(ids) do + any = true + local info = devs[id] + local mode = (type(info) == "table" and info.mode) or "bind" + local path = (type(info) == "table" and info.path) or tostring(info) + local colour = mode == "image" and 5 or 4 + syscall.devctl(1, "sfgc", 3) + printInline(string.format("%-10s", id)) + syscall.devctl(1, "sfgc", colour) + printInline(string.format("%-7s", "["..mode.."]")) + syscall.devctl(1, "sfgc", 1) + print(" "..path) + end + if not any then + syscall.devctl(1, "sfgc", 14) + print(name..": no loop devices attached") + syscall.devctl(1, "sfgc", 1) + end + return +end + +if opts.d then + if #args < 1 then + print(name..": -d requires a loop device id") + print("try '"..name.." --help' for more information.") + syscall.exit(1); return + end + local id = args[1] + local ok, err = pcall(syscall.lodetach, id) + if not ok then + local msg = tostring(err) + if msg:find("EPERM") then msg = "Permission denied" + elseif msg:find("ENXIO") then msg = "no such loop device '"..id.."'" + elseif msg:find("EBUSY") then msg = "device '"..id.."' is still mounted, unmount first" + end + print(name..": "..msg); syscall.exit(1); return + end + syscall.devctl(1, "sfgc", 10) + print(name..": detached "..id) + syscall.devctl(1, "sfgc", 1) + return +end + +if #args < 1 then + print(name..": missing path operand") + print("try '"..name.." --help' for more information.") + syscall.exit(1); return +end + +local path = args[1] +if path:sub(1,1) ~= "/" then + path = syscall.getcwd().."/"..path +end + +local ftype = syscall.type and syscall.type(path) +if not (ftype == "file" or ftype == "directory") then + print(name..": '"..args[1].."': no such file or directory") + syscall.exit(1); return +end + +local ok, result = pcall(syscall.losetup, path, opts.i or nil) +if not ok then + local msg = tostring(result) + if msg:find("EPERM") then msg = "Permission denied" + elseif msg:find("ENOENT") then msg = "'"..args[1].."': no such file" + elseif msg:find("EINVAL") then msg = "'"..args[1].."': not a directory or .hfs image" + elseif msg:find("EIO") then msg = "'"..args[1].."': I/O error reading image" + end + print(name..": "..msg); syscall.exit(1); return +end + +print(result) diff --git a/Src/Hyperion-bash/bin/ls b/Src/Hyperion-bash/bin/ls index a6de4a9..97c7126 100644 --- a/Src/Hyperion-bash/bin/ls +++ b/Src/Hyperion-bash/bin/ls @@ -1,44 +1,32 @@ +--:Minify:-- local cloptions = { - a = false, - h = false, - l = false, + a = false, + h = false, + l = false, help = false, } +local inpArgs = { ... } +local args = {} +local name = syscall.getTask(syscall.getpid()).name -local inpArgs = {...} -local args = {} -local name = syscall.getTask(syscall.getpid()).name - -local optToSet = false for _, v in pairs(inpArgs) do - if optToSet then - cloptions[optToSet] = v - optToSet = false - elseif v:sub(1, 2) == "--" then + if v:sub(1, 2) == "--" then local opt = v:sub(3) if cloptions[opt] == nil then - print(name..": unrecognized option '"..v.."'.") - if cloptions.help ~= nil then - print("try '"..name.." --help' for more information.") - end + print(name .. ": unrecognized option '" .. v .. "'.") + print("try '" .. name .. " --help' for more information.") return - elseif cloptions[opt] == false then - cloptions[opt] = true - else - optToSet = opt end + cloptions[opt] = true elseif v:sub(1, 1) == "-" then for i = 2, #v do local opt = v:sub(i, i) if cloptions[opt] == nil then - print(name..": invalid option '-"..opt.."'.") - if cloptions.help ~= nil then - print("try '"..name.." --help' for more information.") - end + print(name .. ": invalid option '-" .. opt .. "'.") + print("try '" .. name .. " --help' for more information.") return - else - cloptions[opt] = true end + cloptions[opt] = true end else table.insert(args, v) @@ -46,100 +34,160 @@ for _, v in pairs(inpArgs) do end if cloptions.help then - print("Usage: "..name.." [OPTION]... [DIR]") - print("List all entries in the specified DIRectory, or cwd if not specified") - print("\nOptions:") - print(" -a Do not ignore entries starting with .") - print(" -h with -l, print sizes in a human readble format") - print(" -l Use a long listing format") - print(" --help Display this help and exit") + print("Usage: " .. name .. " [OPTION]... [DIR]") + print("List all entries in the specified DIRectory, or cwd if not specified.") + print("") + print("Options:") + print(" -a do not ignore entries starting with .") + print(" -h with -l, print sizes in human readable format") + print(" -l use a long listing format") + print(" --help display this help and exit") return end -local fs = require("sys.fs") -local dir = (args[1] or "") +local fs = require("sys.fs") +local dir = args[1] or "" if dir:sub(1, 1) ~= "/" then - dir = syscall.getcwd().."/"..dir -end - -if dir:sub(#dir, #dir) ~= "/" then - dir = dir.."/" + dir = syscall.getcwd() .. "/" .. dir end +if dir:sub(-1) ~= "/" then dir = dir .. "/" end if not fs.isDir(dir) then - print(name..": Cannot access '"..args[1].."': No such directory.") + print(name .. ": cannot access '" .. (args[1] or dir) .. "': no such directory") return end +local function permStr(perms, etype) + local function b(n) return math.floor(perms / (2^n)) % 2 == 1 end + local t + if etype == 0x01 then t = "l" + elseif etype == nil then t = "-" + else t = "-" end + + local ur = b(5) and "r" or "-" + local uw = b(4) and "w" or "-" + local ux = b(9) and (b(6) and "s" or "x") or (b(6) and "S" or "-") + local gr = b(3) and "r" or "-" + local gw = b(2) and "w" or "-" + local gx = b(8) and "x" or "-" + local wr = b(1) and "r" or "-" + local ww = b(0) and "w" or "-" + local wx = b(7) and "x" or "-" + + return t .. ur .. uw .. ux .. gr .. gw .. gx .. wr .. ww .. wx +end + +local sizePrefixes = { "K", "M", "G", "T" } +local function humanSize(size) + local scale = 0 + while size >= 1024 and scale < #sizePrefixes do + size = size / 1024 + scale = scale + 1 + end + if scale == 0 then return tostring(size) end + if size < 10 then + return string.format("%.1f%s", size, sizePrefixes[scale]) + end + return math.floor(size) .. sizePrefixes[scale] +end local screenSizeStr = syscall.devctl(1, "size") -local sizeX = tonumber(screenSizeStr:sub(1, screenSizeStr:find(";")-1)) -local sizeY = tonumber(screenSizeStr:sub(screenSizeStr:find(";")+1)) +local sizeX = tonumber(screenSizeStr:match("^(%d+)")) or 80 local list = fs.list(dir) if not cloptions.a then for i = #list, 1, -1 do - if list[i]:sub(1, 1) == "." then - table.remove(list, i) - end + if list[i]:sub(1, 1) == "." then table.remove(list, i) end end end +table.sort(list) + +if #list == 0 then return end + +if cloptions.l then + for _, v in ipairs(list) do + local fullPath = dir .. v + local stat = syscall.lstat and syscall.lstat(fullPath) or syscall.stat(fullPath) + local isDir = fs.isDir(fullPath) + local isSym = stat and stat.etype == 0x01 + + local typeChar + if isSym then typeChar = "l" + elseif isDir then typeChar = "d" + else typeChar = "-" end + + local pstr + if stat and stat.perms then + pstr = permStr(stat.perms, stat.etype) + else + pstr = typeChar .. "---------" + end + + local size = (stat and stat.size) or 0 + local sizeStr = cloptions.h and humanSize(size) or tostring(size) + + local mtime = (stat and stat.modified) and math.floor(stat.modified / 1000) or 0 + + local owner = (stat and tostring(stat.owner)) or "0" + local group = (stat and tostring(stat.group)) or "0" + + printInline(pstr .. " " .. owner .. " " .. group .. " ") + printInline(string.format("%6s", sizeStr) .. " ") + printInline(tostring(mtime) .. " ") + + if isSym then + syscall.devctl(1, "sfgc", 6) + printInline(v) + syscall.devctl(1, "sfgc", 1) + local ok, target = pcall(syscall.readlink, fullPath) + if ok then + printInline(" -> ") + local targetExists = pcall(syscall.stat, fullPath) + syscall.devctl(1, "sfgc", targetExists and 6 or 2) + printInline(target) + syscall.devctl(1, "sfgc", 1) + end + elseif isDir then + syscall.devctl(1, "sfgc", 4) + printInline(v) + syscall.devctl(1, "sfgc", 1) + else + local isExec = stat and stat.perms and (math.floor(stat.perms / (2^9)) % 2 == 1) + syscall.devctl(1, "sfgc", isExec and 3 or 1) + printInline(v) + syscall.devctl(1, "sfgc", 1) + end + print("") + end + return +end local colWidth = 0 -local numCols = 1 -if not cloptions.l then - for _, item in pairs(list) do - if #item + 2 > colWidth then - colWidth = #item + 2 - end - end - numCols = math.floor(sizeX / colWidth) +for _, v in ipairs(list) do + if #v + 2 > colWidth then colWidth = #v + 2 end end +local numCols = math.max(1, math.floor(sizeX / colWidth)) -local sizePrefixes = {"K", "M", "G"} +for i, v in ipairs(list) do + local fullPath = dir .. v + local isDir = fs.isDir(fullPath) + local stat = syscall.lstat and syscall.lstat(fullPath) or syscall.stat(fullPath) + local isSym = stat and stat.etype == 0x01 -for i,v in ipairs(list) do - local fileStats = syscall.stat(dir..v) - local isDir = fs.isDir(dir..v) - if cloptions.l then - if isDir then - printInline("d") - else - printInline("-") - end - printInline("------ ") - printInline(fileStats.owner.." ") - printInline(fileStats.group.." ") - local size = fileStats.size - if cloptions.h then - local scale = 0 - while size > 1024 do - size = size / 1024 - scale = scale + 1 - end - if scale > 0 then - if size < 10 then - size = math.floor(size).."."..math.floor((size * 10) % 10)..sizePrefixes[scale] - else - size = math.floor(size)..sizePrefixes[scale] - end - end - end - printInline(size.." ") - printInline(math.floor(fileStats.modified / 1000).." ") - end - if isDir then - syscall.devctl(1,"sfgc",4) + if isSym then + syscall.devctl(1, "sfgc", 6) + elseif isDir then + syscall.devctl(1, "sfgc", 4) else - syscall.devctl(1,"sfgc",1) + local isExec = stat and stat.perms and (math.floor(stat.perms / (2^9)) % 2 == 1) + syscall.devctl(1, "sfgc", isExec and 3 or 1) end + printInline(v) + syscall.devctl(1, "sfgc", 1) printInline((" "):rep(colWidth - #v)) - syscall.devctl(1,"sfgc",1) - if i % numCols == 0 then - print("") - end + + if i % numCols == 0 then print("") end end -if #list % numCols ~= 0 then - print("") -end \ No newline at end of file + +if #list % numCols ~= 0 then print("") end diff --git a/Src/Hyperion-bash/bin/micro b/Src/Hyperion-bash/bin/micro new file mode 100644 index 0000000..3a21d9d --- /dev/null +++ b/Src/Hyperion-bash/bin/micro @@ -0,0 +1,423 @@ +--:Minify:-- +-- Arrows move cursor Home/End line start/end +-- PgUp/PgDn page up/down Backspace delete left +-- Ctrl-D/Delete delete right Tab 4 spaces +-- Ctrl-W save Ctrl-X save + quit +-- Ctrl-P quit Ctrl-K cut line +-- Ctrl-U paste Ctrl-F find +-- Ctrl-N find next Ctrl-G go to line +-- Ctrl-A line start Ctrl-E line end +-- Ctrl-B page up Ctrl-L page down + +local args = { ... } + +local function termSize() + local s = syscall.devctl(1, "size") + return tonumber(s:match("^(%d+)")) or 80, + tonumber(s:match(";(%d+)$")) or 24 +end +local function tpos(x,y) syscall.devctl(1,"spos",x,y) end +local function tfg(c) syscall.devctl(1,"sfgc",c) end +local function tbg(c) syscall.devctl(1,"sbgc",c) end +local function twrite(s) if s and s~="" then syscall.write(1,s) end end +local function tclear() syscall.devctl(1,"clear") end + +local W, H = termSize() +local ROWS = H - 2 + +local lines = {""} +local cx = 1 +local cy = 1 +local scrollY = 0 +local dirty = true +local fname = nil +local msg = "" +local msgErr = false +local clip = nil +local sPat = "" +local sLine = 0 +local blinkState = false + +local function absPath(p) + if p:sub(1,1) == "/" then return p end + local cwd = syscall.getcwd() + cwd = cwd:gsub("/+$", "") + return cwd .. "/" .. p +end + +local function loadFile(path) + if not syscall.exists(path) then + lines = {""}; msg = "[new file]"; return + end + local fd = syscall.open(path, "r") + local buf = "" + while true do + local c = syscall.read(fd, 4096) + if not c or c == "" then break end + buf = buf .. c + end + syscall.close(fd) + lines = {} + for ln in (buf.."\n"):gmatch("([^\n]*)\n") do + table.insert(lines, ln) + end + if #lines > 1 and lines[#lines] == "" and buf:sub(-1) == "\n" then + table.remove(lines) + end + if #lines == 0 then lines = {""} end +end + +local function saveFile(path) + local ok, err = pcall(function() + local fd = syscall.open(path, "w") + for i, ln in ipairs(lines) do + syscall.write(fd, ln) + if i < #lines then syscall.write(fd, "\n") end + end + syscall.write(fd, "\n") + syscall.close(fd) + end) + if ok then + msg = "Saved: "..path; msgErr = false + else + msg = "Save failed: "..tostring(err); msgErr = true + end +end + +local function wrappedRows(lineStr) + return math.max(1, math.ceil(#lineStr / W)) +end + +local function logicalToScreen(li, col) + return math.floor((col - 1) / W) +end + +local function buildScreenMap() + local map = {} + local sr = 0 + for li = 1, #lines do + local len = #lines[li] + local nrows = wrappedRows(lines[li]) + for r = 0, nrows - 1 do + sr = sr + 1 + map[sr] = {li, r * W + 1} + end + end + return map, sr +end + +local function cursorScreenRow(map) + local offset = logicalToScreen(cy, cx) + for sr, entry in ipairs(map) do + if entry[1] == cy and math.floor((entry[2]-1)/W) == offset then + return sr + end + end + return 1 +end + +local function clampCx() + local m = #lines[cy] + 1 + if cx > m then cx = m end + if cx < 1 then cx = 1 end +end + +local function clampScroll(map) + local csr = cursorScreenRow(map) + if csr - 1 < scrollY then scrollY = csr - 1 end + if csr - 1 >= scrollY + ROWS then scrollY = csr - ROWS end + if scrollY < 0 then scrollY = 0 end +end + +local function pad(s, w) + if #s >= w then return s:sub(1, w) end + return s .. string.rep(" ", w - #s) +end + +local function drawTop() + tpos(1,1); tbg(4); tfg(16) + local left = " edit" .. (fname and (" - "..fname) or "") + if dirty then left = left .. " [+]" end + local right = tostring(cy)..","..tostring(cx).." " + twrite(pad(left..string.rep(" ", math.max(1, W-#left-#right))..right, W)) + tbg(16); tfg(1) +end + +local function drawBottom() + tpos(1, H); tbg(4); tfg(16) + if msg ~= "" then + if msgErr then tbg(2) end + twrite(pad(" "..msg, W)) + msg = ""; msgErr = false + else + twrite(pad(" ^W Save ^X Quit+Save ^P Quit ^K Cut ^U Paste ^F Find ^G Go", W)) + end + tbg(16); tfg(1) +end + +local function drawLines(map) + local curSR = cursorScreenRow(map) + local curRowOffset = logicalToScreen(cy, cx) + local curColInRow = cx - curRowOffset * W + + for row = 1, ROWS do + local sr = scrollY + row + tpos(1, row + 1) + local entry = map[sr] + if entry then + local li = entry[1] + local startCol = entry[2] + local seg = lines[li]:sub(startCol, startCol + W - 1) + local isCursorRow = (sr == curSR) + + if isCursorRow then + local ci = curColInRow + ci = math.min(ci, #seg + 1) + local before = seg:sub(1, ci-1) + local curCh = ci > #seg and " " or seg:sub(ci, ci) + local after = seg:sub(ci+1) + + tfg(1); tbg(16); twrite(before) + if blinkState then tfg(16); tbg(1) else tfg(1); tbg(16) end + twrite(curCh) + tfg(1); tbg(16); twrite(after) + local drawn = #before + 1 + #after + if drawn < W then twrite(string.rep(" ", W - drawn)) end + else + if li == sLine and sPat ~= "" and entry[2] == 1 then + local s, e = seg:find(sPat) + if s then + tfg(1); tbg(16); twrite(seg:sub(1,s-1)) + tfg(16); tbg(3); twrite(seg:sub(s,e)) + tfg(1); tbg(16); twrite(seg:sub(e+1)) + twrite(string.rep(" ", W - #seg)) + else + tfg(1); tbg(16); twrite(pad(seg, W)) + end + else + tfg(1); tbg(16); twrite(pad(seg, W)) + end + end + else + tfg(13); tbg(16); twrite(pad("~", W)); tfg(1) + end + end +end + +local function redraw() + W, H = termSize(); ROWS = H - 2 + local map = buildScreenMap() + clampScroll(map) + drawTop() + drawLines(map) + drawBottom() + tpos(1, H) + tbg(16); tfg(1) +end + +local function prompt(label, default) + local inp = default or "" + while true do + tpos(1, H); tbg(3); tfg(16) + twrite(pad(" "..label..inp.." ", W)) + tbg(16); tfg(1) + local key = syscall.read(0) + if not key or key == "" then sleep(0.02) + elseif key == "\27" then return nil + elseif key == "\n" then return inp + elseif key == "\b" then if #inp > 0 then inp = inp:sub(1,-2) end + else + local b = key:byte(1) + if b >= 32 and b < 127 then inp = inp..key:sub(1,1) end + end + end +end + +local function insChar(c) + local ln = lines[cy] + lines[cy] = ln:sub(1,cx-1)..c..ln:sub(cx) + cx = cx+1; dirty = true +end + +local function delLeft() + if cx > 1 then + local ln = lines[cy] + lines[cy] = ln:sub(1,cx-2)..ln:sub(cx) + cx = cx-1; dirty = true + elseif cy > 1 then + local above = lines[cy-1] + cx = #above+1 + lines[cy-1] = above..lines[cy] + table.remove(lines, cy) + cy = cy-1; dirty = true + end +end + +local function delRight() + local ln = lines[cy] + if cx <= #ln then + lines[cy] = ln:sub(1,cx-1)..ln:sub(cx+1); dirty = true + elseif cy < #lines then + lines[cy] = ln..lines[cy+1] + table.remove(lines, cy+1); dirty = true + end +end + +local function newline() + local ln = lines[cy] + local pre = ln:sub(1,cx-1) + local post = ln:sub(cx) + local ind = pre:match("^(%s*)") or "" + lines[cy] = pre + table.insert(lines, cy+1, ind..post) + cy = cy+1; cx = #ind+1; dirty = true +end + +local function cutLine() + clip = lines[cy] + table.remove(lines, cy) + if #lines == 0 then lines = {""} end + if cy > #lines then cy = #lines end + cx = 1; dirty = true; msg = "Cut" +end + +local function pasteLine() + if not clip then msg = "Nothing to paste"; return end + table.insert(lines, cy, clip) + cy = cy+1; cx = 1; dirty = true; msg = "Pasted" +end + +local function findNext() + if sPat == "" then + local p = prompt("Find: ", "") + if not p or p == "" then dirty = true; return end + sPat = p; sLine = 0 + end + local start = sLine > 0 and sLine or cy + for i = 1, #lines do + local idx = (start-1+i) % #lines + 1 + if lines[idx]:find(sPat) then + cy = idx; sLine = idx + cx = lines[idx]:find(sPat) or 1 + msg = "Found: line "..idx; dirty = true; return + end + end + msg = "Not found: "..sPat; msgErr = true; dirty = true +end + +local function goToLine() + local p = prompt("Go to line: ", "") + if not p then dirty = true; return end + local n = tonumber(p) + if not n then msg = "Not a number"; msgErr = true; dirty = true; return end + cy = math.max(1, math.min(#lines, math.floor(n))) + cx = 1; msg = "Line "..cy; dirty = true +end + +local function doSave() + if not fname then + local p = prompt("Save as: ", "") + dirty = true + if not p or p == "" then return false end + fname = absPath(p) + end + saveFile(fname); dirty = true; return not msgErr +end + +local function moveCursorUp(map) + local csr = cursorScreenRow(map) + if csr <= 1 then return end + local prev = map[csr - 1] + if not prev then return end + local newLi = prev[1] + local newCol = prev[2] + (cx - 1) % W + cx = math.min(newCol, #lines[newLi] + 1) + cy = newLi +end + +local function moveCursorDown(map) + local csr = cursorScreenRow(map) + local next = map[csr + 1] + if not next then return end + local newLi = next[1] + local newCol = next[2] + (cx - 1) % W + cx = math.min(newCol, #lines[newLi] + 1) + cy = newLi +end + +if args[1] then + fname = absPath(args[1]) + loadFile(fname) +end + +tclear() + +local running = true +while running do + local map = buildScreenMap() + + local key = syscall.read(0) + if key and key ~= "" then + local b = key:byte(1) + if key == "\17" then moveCursorUp(map); dirty=true + elseif key == "\18" then moveCursorDown(map); dirty=true + elseif key == "\19" then + if cx > 1 then cx=cx-1 + elseif cy > 1 then cy=cy-1; cx=#lines[cy]+1 end + dirty=true + elseif key == "\20" then + if cx <= #lines[cy] then cx=cx+1 + elseif cy < #lines then cy=cy+1; cx=1 end + dirty=true + elseif key == "\n" then newline() + elseif key == "\b" then delLeft() + elseif key == "\t" then for _=1,4 do insChar(" ") end + elseif b == 1 then cx=1; dirty=true + elseif b == 2 then + for _=1,ROWS do moveCursorUp(map) end; dirty=true + elseif b == 4 then delRight() + elseif b == 5 then cx=#lines[cy]+1; dirty=true + elseif b == 6 then + local p=prompt("Find: ",sPat); dirty=true + if p then sPat=p; sLine=0; findNext() end + elseif b == 7 then goToLine() + elseif b == 11 then cutLine() + elseif b == 12 then + for _=1,ROWS do moveCursorDown(map) end; dirty=true + elseif b == 14 then + if sPat=="" then + local p=prompt("Find: ",""); dirty=true + if p then sPat=p; sLine=0 end + end + findNext() + elseif b == 16 then + if dirty then + local p=prompt("Unsaved changes. Quit? [y/N] ","") + dirty=true + if p and p:lower()=="y" then running=false end + else running=false end + elseif b == 21 then pasteLine() + elseif b == 23 then doSave() + elseif b == 24 then doSave(); running=false + else + if b >= 32 and b < 127 then insChar(key:sub(1,1)) end + end + end + + local curBlink = (math.floor(syscall.getUptime() / 500) % 2) == 0 + if curBlink ~= blinkState then + blinkState = curBlink + dirty = true + end + + if dirty then + clampCx() + redraw() + dirty = false + end + + sleep(0.05) +end + +tclear(); tfg(1); tbg(16); tpos(1,1) +print("edit: exited"..(fname and (" - "..fname) or "")) diff --git a/Src/Hyperion-bash/bin/mount b/Src/Hyperion-bash/bin/mount new file mode 100644 index 0000000..ab580e2 --- /dev/null +++ b/Src/Hyperion-bash/bin/mount @@ -0,0 +1,152 @@ +--:Minify:-- +-- Usage: +-- mount list all current mounts +-- mount mount loop device id at mountpoint +-- mount -o loop attach as loop device and mount at +-- mount --help + +local name = syscall.getTask(syscall.getpid()).name +local args, opts = {}, { help=false, o=nil } + +local i = 1 +local rawArgs = {...} +while i <= #rawArgs do + local v = rawArgs[i] + if v:sub(1,2) == "--" then + local o = v:sub(3) + if o == "help" then opts.help = true + else print(name..": unrecognised option '"..v.."'") + print("try '"..name.." --help' for more information.") + syscall.exit(1); return end + elseif v == "-o" then + i = i + 1 + opts.o = rawArgs[i] + elseif v:sub(1,1) == "-" then + local rest = v:sub(2) + if rest:sub(1,1) == "o" then + if #rest > 1 then opts.o = rest:sub(2) + else i = i + 1; opts.o = rawArgs[i] end + else + print(name..": invalid option '"..v.."'") + print("try '"..name.." --help' for more information.") + syscall.exit(1); return + end + else + table.insert(args, v) + end + i = i + 1 +end + +if opts.help then + print("Usage: "..name) + print(" "..name.." ") + print(" "..name.." -o loop ") + print("") + print("Mount a loop device or filesystem.") + print("") + print(" (no args) list all active mount points") + print(" mount an already-attached loop device") + print(" -o loop attach src as loop device and mount at dest") + print(" src can be a directory (bind) or .hfs image") + print("") + print("Requires root for all operations except listing.") + return +end + +if #args == 0 and not opts.o then + local ok, mounts = pcall(syscall.mounts or function() + error("ENOSYS") + end) + + local loDevs = {} + local lok, ld = pcall(syscall.lolist) + if lok then + for id, info in pairs(ld) do + local path = (type(info)=="table" and info.path) or tostring(info) + local mode = (type(info)=="table" and info.mode) or "bind" + loDevs[id] = { path=path, mode=mode } + end + end + + if next(loDevs) == nil then + syscall.devctl(1, "sfgc", 14) + print("(no loop devices attached)") + syscall.devctl(1, "sfgc", 1) + return + end + + for id, info in pairs(loDevs) do + local colour = info.mode == "image" and 5 or 4 + syscall.devctl(1, "sfgc", colour) + printInline(info.mode.." "..id) + syscall.devctl(1, "sfgc", 1) + print(" on "..info.path) + end + return +end + +if opts.o and opts.o:lower() == "loop" then + if #args < 2 then + print(name..": -o loop requires and ") + print("try '"..name.." --help' for more information.") + syscall.exit(1); return + end + + local src = args[1] + local dest = args[2] + + if src:sub(1,1) ~= "/" then src = syscall.getcwd().."/"..src end + if dest:sub(1,1) ~= "/" then dest = syscall.getcwd().."/"..dest end + + local ok, loopId = pcall(syscall.losetup, src) + if not ok then + local msg = tostring(loopId) + if msg:find("EPERM") then msg = "Permission denied" + elseif msg:find("EINVAL") then msg = "'"..args[1].."': not a directory or .hfs image" + elseif msg:find("EIO") then msg = "'"..args[1].."': I/O error reading image" + end + print(name..": losetup: "..msg); syscall.exit(1); return + end + + local ok2, merr = pcall(syscall.mount, dest, loopId) + if not ok2 then + pcall(syscall.lodetach, loopId) + local msg = tostring(merr) + if msg:find("EPERM") then msg = "Permission denied" + elseif msg:find("EBUSY") then msg = "'"..dest.."' is already a mount point" + elseif msg:find("ENODEV") then msg = "loop device not found (internal error)" + end + print(name..": mount: "..msg); syscall.exit(1); return + end + + syscall.devctl(1, "sfgc", 10) + print(name..": "..loopId.." mounted at "..dest) + syscall.devctl(1, "sfgc", 1) + return +end + +if #args == 2 then + local loopId = args[1] + local dest = args[2] + if dest:sub(1,1) ~= "/" then dest = syscall.getcwd().."/"..dest end + + local ok, err = pcall(syscall.mount, dest, loopId) + if not ok then + local msg = tostring(err) + if msg:find("EPERM") then msg = "Permission denied" + elseif msg:find("ENODEV") then msg = "'"..loopId.."': no such device - use losetup first" + elseif msg:find("EBUSY") then msg = "'"..dest.."' is already a mount point" + elseif msg:find("EINVAL") then msg = "invalid arguments" + end + print(name..": "..msg); syscall.exit(1); return + end + + syscall.devctl(1, "sfgc", 10) + print(name..": "..loopId.." mounted at "..dest) + syscall.devctl(1, "sfgc", 1) + return +end + +print(name..": wrong number of arguments") +print("try '"..name.." --help' for more information.") +syscall.exit(1) diff --git a/Src/Hyperion-bash/bin/neofetch b/Src/Hyperion-bash/bin/neofetch deleted file mode 100644 index 64a78f2..0000000 --- a/Src/Hyperion-bash/bin/neofetch +++ /dev/null @@ -1,18 +0,0 @@ -local userhost = (syscall.getUsername() or "Unknown").."@"..(syscall.getHostname() or "Unknown") -print(".. *. .. | "..userhost) -print(" *= +@* +* | "..string.rep("-",#userhost)) -print(" .@#. -@@@= :#@. | OS: "..(syscall.version() or "Unknown")) -print(" =@@+ *@@@# +@@= | Host: "..(syscall.getHost() or "Unknown")) -print(" %@@%: *@@@# -%@@% | Uptime: "..(syscall.getUptime() or "Unknown")) -print(" :@@@@+ *@@@# .*@@@@: | Tasks: "..tostring((#syscall.getTasks() or "Unknown"))) -print(" :*@@@%- *@@@# -@@@@*: | Packages: ".."Unknown") -print(" =%@@#. *@@@# .#@@%= | Shell: "..(syscall.getEnviron("SHELL") or "Unknown")) -print(" :=. :*@@= *@@@# =@@+: .=: | ") -print(" %@#=..*# +@@@# #*..=#@# | ") -print(" .@@@@+=# .%@%: #=+@@@@. | ") -print(" .....=# -@= *+...:. | ") -print(" -*%*-@= - =@-*%*- | ") -print(" -@*. -@%. :%@- :*@- | ") -print(" .#@#@* | ") -print(" -#- | ") -print(" | ") \ No newline at end of file diff --git a/Src/Hyperion-bash/bin/readlink b/Src/Hyperion-bash/bin/readlink new file mode 100644 index 0000000..1d93acc --- /dev/null +++ b/Src/Hyperion-bash/bin/readlink @@ -0,0 +1,82 @@ +--:Minify:-- +local name = syscall.getTask(syscall.getpid()).name +local cloptions = { n = false, f = false, e = false, help = false } +local args = {} + +for _, v in ipairs({ ... }) do + if v:sub(1, 2) == "--" then + local opt = v:sub(3) + if cloptions[opt] == nil then + print(name .. ": unrecognized option '" .. v .. "'") + syscall.exit(1); return + end + cloptions[opt] = true + elseif v:sub(1, 1) == "-" then + for i = 2, #v do + local opt = v:sub(i, i) + if cloptions[opt] == nil then + print(name .. ": invalid option '-" .. opt .. "'") + syscall.exit(1); return + end + cloptions[opt] = true + end + else + table.insert(args, v) + end +end + +if cloptions.help then + print("Usage: " .. name .. " [OPTION]... FILE...") + print("Print the resolved target of symbolic links.") + print("") + print("Options:") + print(" -f canonicalize: follow every symlink; last component need not exist") + print(" -e like -f but all components must exist") + print(" -n do not output trailing newline") + print(" --help display this help and exit") + return +end + +if #args == 0 then + print(name .. ": missing operand") + print("try '" .. name .. " --help' for more information.") + syscall.exit(1); return +end + +local function absPath(p) + if p:sub(1,1) ~= "/" then + local d = syscall.getcwd() + if d:sub(-1) ~= "/" then d = d .. "/" end + p = d .. p + end + return p +end + +local anyErr = false +for _, path in ipairs(args) do + path = absPath(path) + + if cloptions.f or cloptions.e then + local ok, stat = pcall(syscall.stat, path) + if not ok then + if cloptions.e then + print(name .. ": " .. path .. ": " .. tostring(stat)) + anyErr = true + else + if not cloptions.n then print(path) else printInline(path) end + end + else + if not cloptions.n then print(path) else printInline(path) end + end + else + local ok, target = pcall(syscall.readlink, path) + if not ok then + print(name .. ": " .. path .. ": " .. tostring(target)) + anyErr = true + else + if not cloptions.n then print(target) else printInline(target) end + end + end +end + +if anyErr then syscall.exit(1) end diff --git a/Src/Hyperion-bash/bin/sed b/Src/Hyperion-bash/bin/sed new file mode 100644 index 0000000..aa0192d --- /dev/null +++ b/Src/Hyperion-bash/bin/sed @@ -0,0 +1,429 @@ +--:Minify:-- +-- Supports: s/pat/repl/[gip], d, p, q, =, addr1[,addr2]cmd +-- Addressing: line numbers, $, /regex/ +-- Flags: -n (silent), -e script, -i (in-place) + +local name = syscall.getTask(syscall.getpid()).name + +local scripts = {} +local files = {} +local silent = false +local inplace = false +local args = { ... } +local i = 1 + +while i <= #args do + local a = args[i] + if a == "-n" then + silent = true + elseif a == "-i" then + inplace = true + elseif a == "-e" then + i = i + 1 + if not args[i] then + print(name .. ": option -e requires an argument"); syscall.exit(1); return + end + table.insert(scripts, args[i]) + elseif a:sub(1,2) == "-e" then + table.insert(scripts, a:sub(3)) + elseif a == "--help" then + print("Usage: " .. name .. " [OPTION]... SCRIPT [FILE...]") + print(" " .. name .. " [OPTION]... -e SCRIPT... [FILE...]") + print("Stream editor. Reads FILE(s) (or stdin) line by line,") + print("applies SCRIPT, and writes results to stdout.") + print("") + print("Commands:") + print(" s/REGEX/REPL/[flags] substitute (flags: g global, i ignore-case, p print)") + print(" d delete line (skip to next)") + print(" p print current line") + print(" q quit") + print(" = print current line number") + print(" y/src/dst/ transliterate characters") + print("") + print("Addressing (prefix any command):") + print(" N line number N") + print(" $ last line") + print(" /REGEX/ lines matching regex") + print(" N,M line range") + print(" N,/REGEX/ from line N until regex match") + print("") + print("Options:") + print(" -n suppress default output") + print(" -e SCRIPT add script expression") + print(" -i edit file in-place") + print(" --help display this help and exit") + return + elseif a:sub(1,1) == "-" then + print(name .. ": unknown option: " .. a) + syscall.exit(1); return + else + if #scripts == 0 then + table.insert(scripts, a) + else + table.insert(files, a) + end + end + i = i + 1 +end + +if #scripts == 0 then + print(name .. ": no script specified"); syscall.exit(1); return +end + +local script = table.concat(scripts, "\n") + +local function patEscape(s) + return s:gsub("([%(%)%.%%%+%-%*%?%[%^%$])", "%%%1") +end + +local function sedPatToLua(pat, icase) + pat = pat:gsub("\\%(", "("):gsub("\\%)", ")") + pat = pat:gsub("\\1", "%%1"):gsub("\\2", "%%2") + return pat +end + +local function parseDelim(s, pos, delim) + local out = {} + while pos <= #s do + local c = s:sub(pos, pos) + if c == "\\" and pos < #s then + pos = pos + 1 + local nc = s:sub(pos, pos) + if nc == delim then + table.insert(out, delim) + elseif nc == "n" then + table.insert(out, "\n") + else + table.insert(out, "\\" .. nc) + end + elseif c == delim then + return table.concat(out), pos + 1 + else + table.insert(out, c) + end + pos = pos + 1 + end + return table.concat(out), pos +end + +local function parseAddr(s, pos) + local c = s:sub(pos, pos) + if c == "" then return nil, pos end + if c:match("%d") then + local numstr = s:match("^(%d+)", pos) + return { type="line", n=tonumber(numstr) }, pos + #numstr + elseif c == "$" then + return { type="last" }, pos + 1 + elseif c == "/" then + local pat, npos = parseDelim(s, pos + 1, "/") + return { type="regex", pat=pat }, npos + end + return nil, pos +end + +local function parseCommands(src) + local cmds = {} + local pos = 1 + local len = #src + + local function skip() + while pos <= len and (src:sub(pos,pos) == " " or src:sub(pos,pos) == "\t") do + pos = pos + 1 + end + end + + while pos <= len do + skip() + if pos > len then break end + + local c = src:sub(pos, pos) + if c == "\n" or c == ";" then + pos = pos + 1 + goto continue + end + if c == "#" then + while pos <= len and src:sub(pos,pos) ~= "\n" do pos = pos + 1 end + goto continue + end + + local addr1, addr2 + addr1, pos = parseAddr(src, pos) + skip() + if addr1 and pos <= len and src:sub(pos,pos) == "," then + pos = pos + 1 + skip() + addr2, pos = parseAddr(src, pos) + end + skip() + + if pos > len then break end + local cmd = src:sub(pos, pos) + pos = pos + 1 + + if cmd == "s" then + local delim = src:sub(pos, pos); pos = pos + 1 + local pat, p1 = parseDelim(src, pos, delim); pos = p1 + local repl, p2 = parseDelim(src, pos, delim); pos = p2 + local flags = "" + while pos <= len and src:sub(pos,pos):match("[giIp]") do + flags = flags .. src:sub(pos,pos); pos = pos + 1 + end + table.insert(cmds, { addr1=addr1, addr2=addr2, cmd="s", + pat=pat, repl=repl, flags=flags }) + + elseif cmd == "y" then + local delim = src:sub(pos, pos); pos = pos + 1 + local srcch, p1 = parseDelim(src, pos, delim); pos = p1 + local dstch, p2 = parseDelim(src, pos, delim); pos = p2 + table.insert(cmds, { addr1=addr1, addr2=addr2, cmd="y", + src=srcch, dst=dstch }) + + elseif cmd == "d" or cmd == "p" or cmd == "q" or cmd == "=" then + table.insert(cmds, { addr1=addr1, addr2=addr2, cmd=cmd }) + + elseif cmd == "{" then + local depth = 1 + local start = pos + while pos <= len and depth > 0 do + local ch = src:sub(pos,pos) + if ch == "{" then depth = depth + 1 + elseif ch == "}" then depth = depth - 1 end + pos = pos + 1 + end + local inner = src:sub(start, pos - 2) + local innerCmds = parseCommands(inner) + for _, ic in ipairs(innerCmds) do + ic.addr1 = ic.addr1 or addr1 + ic.addr2 = ic.addr2 or addr2 + end + for _, ic in ipairs(innerCmds) do + table.insert(cmds, ic) + end + + elseif cmd == "\n" or cmd == ";" then + else + end + + ::continue:: + end + + return cmds +end + +local cmds = parseCommands(script) + +local inRange = {} + +local function addrMatch(cmd, lineNum, line, isLast, ci) + local a1 = cmd.addr1 + local a2 = cmd.addr2 + + if not a1 then return true end + + local function matchOne(addr, ln, l) + if addr.type == "line" then return ln == addr.n + elseif addr.type == "last" then return isLast + elseif addr.type == "regex" then return l:find(sedPatToLua(addr.pat)) ~= nil + end + return false + end + + if not a2 then + return matchOne(a1, lineNum, line) + end + + if inRange[ci] then + local endMatch + if a2.type == "line" then endMatch = (lineNum >= a2.n) + elseif a2.type == "last" then endMatch = isLast + elseif a2.type == "regex" then endMatch = (line:find(sedPatToLua(a2.pat)) ~= nil) + end + if endMatch then inRange[ci] = false end + return true + else + if matchOne(a1, lineNum, line) then + if a2.type == "line" and a2.n <= lineNum then + else + inRange[ci] = true + end + return true + end + return false + end +end + +local function doSubst(line, pat, repl, flags) + local global = flags:find("g") ~= nil + local icase = flags:find("[iI]") ~= nil + local luaPat = sedPatToLua(pat, icase) + + local function buildRepl(whole, ...) + local caps = { ... } + local out = {} + local rp = repl + local ri = 1 + while ri <= #rp do + local rc = rp:sub(ri, ri) + if rc == "&" then + table.insert(out, whole) + elseif rc == "\\" and ri < #rp then + ri = ri + 1 + local nc = rp:sub(ri, ri) + if nc:match("%d") then + local idx = tonumber(nc) + table.insert(out, caps[idx] or "") + elseif nc == "n" then + table.insert(out, "\n") + else + table.insert(out, nc) + end + else + table.insert(out, rc) + end + ri = ri + 1 + end + return table.concat(out) + end + + local result + local changed = false + if global then + result = line:gsub(luaPat, buildRepl) + changed = (result ~= line) + else + local s, e, whole + local parts = { line:find(luaPat) } + if parts[1] then + s = parts[1]; e = parts[2] + local caps = {} + for ci = 3, #parts do caps[#caps+1] = parts[ci] end + local wmatch = line:sub(s, e) + local replStr = buildRepl(wmatch, table.unpack(caps)) + result = line:sub(1, s-1) .. replStr .. line:sub(e+1) + changed = true + else + result = line + end + end + return result, changed +end + +local function doTranslit(line, src, dst) + local out = {} + for ci = 1, #line do + local c = line:sub(ci, ci) + local idx = src:find(c, 1, true) + if idx and idx <= #dst then + table.insert(out, dst:sub(idx, idx)) + else + table.insert(out, c) + end + end + return table.concat(out) +end + +local function processLines(lines, outputLines) + local total = #lines + for lineNum, line in ipairs(lines) do + local isLast = (lineNum == total) + local deleted = false + local printed = false + local quit = false + + local bare = line:gsub("\n$", "") + + for ci, cmd in ipairs(cmds) do + if addrMatch(cmd, lineNum, bare, isLast, ci) then + if cmd.cmd == "d" then + deleted = true; break + + elseif cmd.cmd == "p" then + table.insert(outputLines, bare) + + elseif cmd.cmd == "=" then + table.insert(outputLines, tostring(lineNum)) + + elseif cmd.cmd == "q" then + if not silent then table.insert(outputLines, bare) end + quit = true; break + + elseif cmd.cmd == "s" then + local newLine, changed = doSubst(bare, cmd.pat, cmd.repl, cmd.flags) + bare = newLine + if changed and cmd.flags:find("p") then + table.insert(outputLines, bare) + end + + elseif cmd.cmd == "y" then + bare = doTranslit(bare, cmd.src, cmd.dst) + end + end + end + + if quit then break end + if not deleted and not silent then + table.insert(outputLines, bare) + end + end +end + +local function readLines(fd) + local lines = {} + local buf = "" + while true do + local chunk = syscall.read(fd, 1024) + if not chunk or chunk == "" then break end + buf = buf .. chunk + end + for line in (buf .. "\n"):gmatch("([^\n]*)\n") do + table.insert(lines, line) + end + if buf ~= "" and buf:sub(-1) ~= "\n" and lines[#lines] == "" then + table.remove(lines) + end + return lines +end + +local function runOnFile(path) + local fd + if path then + local ok, err = pcall(function() fd = syscall.open(path, "r") end) + if not ok then + print(name .. ": " .. path .. ": " .. tostring(err)) + return false + end + else + fd = 0 + end + + local lines = readLines(fd) + if path then syscall.close(fd) end + + inRange = {} + + local outputLines = {} + processLines(lines, outputLines) + + if inplace and path then + local wfd = syscall.open(path, "w") + for _, ol in ipairs(outputLines) do + syscall.write(wfd, ol .. "\n") + end + syscall.close(wfd) + else + for _, ol in ipairs(outputLines) do + print(ol) + end + end + return true +end + +if #files == 0 then + runOnFile(nil) +else + for _, f in ipairs(files) do + local absf = f + if absf:sub(1,1) ~= "/" then absf = syscall.getcwd() .. "/" .. f end + runOnFile(absf) + end +end diff --git a/Src/Hyperion-bash/bin/socktest b/Src/Hyperion-bash/bin/socktest new file mode 100644 index 0000000..026f457 --- /dev/null +++ b/Src/Hyperion-bash/bin/socktest @@ -0,0 +1,151 @@ +--:Minify:-- +local args = { ... } +local target = args[1] or "http://example.com" + +local function pass(msg) syscall.devctl(1,"sfgc",10); print(" PASS " .. msg); syscall.devctl(1,"sfgc",1) end +local function fail(msg) syscall.devctl(1,"sfgc",2); print(" FAIL " .. msg); syscall.devctl(1,"sfgc",1) end +local function info(msg) syscall.devctl(1,"sfgc",14); print(" .... " .. msg); syscall.devctl(1,"sfgc",1) end +local function head(msg) syscall.devctl(1,"sfgc",4); print("\n" .. msg); syscall.devctl(1,"sfgc",1) end + +local passed, failed = 0, 0 +local function check(name, ok, err) + if ok then passed = passed + 1; pass(name) + else failed = failed + 1; fail(name .. " - " .. tostring(err)) end +end + +head("[ 1 ] socket() creation") +do + local ok, fd = pcall(syscall.socket, "inet", "stream") + check("socket(inet, stream) returns fd", ok and type(fd) == "number", fd) + + if ok then + local cok = pcall(syscall.close, fd) + check("close() on socket fd", cok, "close failed") + end + + local ok2, fd2 = pcall(syscall.socket, "unix", "stream") + check("socket(unix, stream) returns fd", ok2 and type(fd2) == "number", fd2) + if ok2 then pcall(syscall.close, fd2) end + + local ok3 = pcall(syscall.socket, "ax25", "stream") + check("socket(ax25) returns EAFNOSUPPORT", not ok3, "should have errored") +end + +head("[ 2 ] connect() to " .. target) +local sockfd +do + local ok, fd = pcall(syscall.socket, "inet", "stream") + check("socket() before connect", ok, fd) + + if ok then + sockfd = fd + local cok, cerr = pcall(syscall.connect, fd, target) + check("connect(" .. target .. ")", cok, cerr) + end +end + +head("[ 3 ] send() HTTP GET via socket") +do + if sockfd then + local sok, serr = pcall(syscall.send, sockfd, "") + check("send() does not error", sok, serr) + else + check("send() skipped (no socket)", false, "socket creation failed") + end +end + +head("[ 4 ] recv() reads HTTP response") +do + if sockfd then + info("waiting for response (recv blocks up to 10s)...") + local ok, body = pcall(syscall.recv, sockfd, 65536) + check("recv() returns non-empty body", ok and body and #body > 0, + ok and "empty response" or tostring(body)) + if ok and body and #body > 0 then + info("received " .. #body .. " bytes") + local preview = body:sub(1, 120):gsub("\r", ""):gsub("\n", " ") + info("preview: " .. preview) + end + pcall(syscall.close, sockfd) + sockfd = nil + else + check("recv() skipped (no socket)", false, "socket creation failed") + end +end + +head("[ 5 ] httpget() convenience wrapper") +do + info("GET " .. target .. " ...") + local ok, body = pcall(syscall.httpget, target) + check("httpget() succeeds", ok, body) + if ok then + check("httpget() returns non-empty string", type(body) == "string" and #body > 0, "empty") + if type(body) == "string" and #body > 0 then + info("received " .. #body .. " bytes") + local preview = body:sub(1, 120):gsub("\r", ""):gsub("\n", " ") + info("preview: " .. preview) + local hasHtml = body:lower():find(" 0 then syscall.exit(1) end diff --git a/Src/Hyperion-bash/bin/startup/test.lua b/Src/Hyperion-bash/bin/startup/test.lua deleted file mode 100644 index 1025215..0000000 --- a/Src/Hyperion-bash/bin/startup/test.lua +++ /dev/null @@ -1 +0,0 @@ -syscall.chown("/bin", 0, 0) \ No newline at end of file diff --git a/Src/Hyperion-bash/bin/su b/Src/Hyperion-bash/bin/su index cf98d75..5405b39 100644 --- a/Src/Hyperion-bash/bin/su +++ b/Src/Hyperion-bash/bin/su @@ -1,21 +1,15 @@ --:Minify:-- -local fs = require("sys.fs") local targetUser = ({ ... })[1] or "root" - local currentUid = syscall.getuid() -local currentUser = syscall.getUsername(currentUid) or tostring(currentUid) +local targetUid = syscall.getuidbyname(targetUser) -local targetUid = syscall.getuid(targetUser) if not targetUid then print("su: user '" .. targetUser .. "' does not exist") syscall.exit(1) return end -local ok, err -if currentUid == 0 then - ok = true -else +if currentUid ~= 0 then printInline("Password: ") local pw = "" while true do @@ -25,16 +19,13 @@ else syscall.write(1, "\n") break elseif ch == "\b" then - if #pw > 0 then - pw = pw:sub(1, -2); syscall.write(1, "\b \b") - end + if #pw > 0 then pw = pw:sub(1, -2); syscall.write(1, "\b \b") end else - pw = pw .. ch - syscall.write(1, "*") + pw = pw .. ch; syscall.write(1, "*") end end - ok, err = syscall.elevate(targetUser, pw) + local ok, err = syscall.elevate(targetUser, pw) if not ok then sleep(1) print("su: Authentication failure") @@ -43,38 +34,23 @@ else end end -if currentUid == 0 then - syscall.setuid(targetUid) -end +syscall.setuid(targetUid) local pwent = syscall.getpasswd(targetUid) -local shell = (pwent and pwent.shell) or "/bin/hysh" +local shell = (pwent and pwent.shell) or "/bin/hysh" local homedir = (pwent and pwent.homedir) or "/" -syscall.chdir(homedir) -syscall.setEnviron("HOME", homedir) -syscall.setEnviron("USER", targetUser) +local ok_cd, err_cd = pcall(syscall.chdir, homedir) +if not ok_cd then + homedir = "/" + syscall.chdir(homedir) +end +syscall.setEnviron("HOME", homedir) +syscall.setEnviron("USER", targetUser) syscall.setEnviron("SHELL", shell) -local shellText = fs.readAllText(shell) -if not shellText then - print("su: shell not found: " .. shell) +local ok, err = pcall(syscall.exec, shell) +if not ok then + print("su: cannot exec shell '" .. shell .. "': " .. tostring(err)) syscall.exit(1) - return end - -local shellFn, loadErr = load(shellText, "@" .. shell) - -if not shellFn then - print("su: cannot load shell: " .. tostring(loadErr)) - syscall.exit(1) - return -end - -local success, err = syscall.kill(syscall.getppid()) -if success then - syscall.spawn(shellFn, targetUser .. ":" .. shell, syscall.getEnviron()) - syscall.exit(0) -else - print("su: "..err) -end \ No newline at end of file diff --git a/Src/Hyperion-bash/bin/umount b/Src/Hyperion-bash/bin/umount new file mode 100644 index 0000000..d806892 --- /dev/null +++ b/Src/Hyperion-bash/bin/umount @@ -0,0 +1,111 @@ +--:Minify:-- +-- Usage: +-- umount unmount; auto-detach loop device if one is found +-- umount -l detach loop device without unmounting (force) +-- umount --no-detach unmount but leave loop device attached +-- umount --help + +local name = syscall.getTask(syscall.getpid()).name +local args, opts = {}, { l=false, ["no-detach"]=false, help=false } + +for _, v in ipairs({...}) do + if v:sub(1,2) == "--" then + local o = v:sub(3) + if opts[o] ~= nil then opts[o] = true + else print(name..": unrecognised option '"..v.."'") + print("try '"..name.." --help' for more information.") + syscall.exit(1); return end + elseif v:sub(1,1) == "-" then + for i = 2, #v do + local c = v:sub(i,i) + if opts[c] ~= nil then opts[c] = true + else print(name..": invalid option '-"..c.."'") + print("try '"..name.." --help' for more information.") + syscall.exit(1); return end + end + else + table.insert(args, v) + end +end + +if opts.help then + print("Usage: "..name.." ") + print(" "..name.." --no-detach ") + print(" "..name.." -l ") + print("") + print("Unmount a filesystem mounted at .") + print("") + print(" unmount and auto-detach any loop device") + print(" --no-detach unmount but keep the loop device attached") + print(" -l forcibly detach a loop device (no unmount)") + print("") + print("Requires root.") + return +end + +if opts.l then + if #args < 1 then + print(name..": -l requires a loop device id") + print("try '"..name.." --help' for more information.") + syscall.exit(1); return + end + local id = args[1] + local ok, err = pcall(syscall.lodetach, id) + if not ok then + local msg = tostring(err) + if msg:find("EPERM") then msg = "Permission denied" + elseif msg:find("ENXIO") then msg = "no such loop device '"..id.."'" + elseif msg:find("EBUSY") then msg = "'"..id.."' is still mounted - unmount first or omit -l" + end + print(name..": "..msg); syscall.exit(1); return + end + syscall.devctl(1, "sfgc", 10) + print(name..": detached "..id) + syscall.devctl(1, "sfgc", 1) + return +end + +if #args < 1 then + print(name..": missing mount point operand") + print("try '"..name.." --help' for more information.") + syscall.exit(1); return +end + +local mpt = args[1] +if mpt:sub(1,1) ~= "/" then mpt = syscall.getcwd().."/"..mpt end + +local loopIdToDetach = nil +if not opts["no-detach"] then + local lok, devs = pcall(syscall.lolist) + if lok then + loopIdToDetach = {} + for id in pairs(devs) do + loopIdToDetach[#loopIdToDetach + 1] = id + end + end +end + +local ok, err = pcall(syscall.umount, mpt) +if not ok then + local msg = tostring(err) + if msg:find("EPERM") then msg = "Permission denied" + elseif msg:find("EINVAL") then msg = "'"..args[1].."' is not a mount point" + elseif msg:find("EBUSY") then msg = "'"..args[1].."' is busy - close open files first" + end + print(name..": "..msg); syscall.exit(1); return +end + +syscall.devctl(1, "sfgc", 10) +print(name..": unmounted "..mpt) +syscall.devctl(1, "sfgc", 1) + +if loopIdToDetach then + for _, id in ipairs(loopIdToDetach) do + local dok = pcall(syscall.lodetach, id) + if dok then + syscall.devctl(1, "sfgc", 14) + print(name..": auto-detached "..id) + syscall.devctl(1, "sfgc", 1) + end + end +end diff --git a/Src/Hyperion-firmware-cct/boot/cct/boot.lua b/Src/Hyperion-firmware-cct/boot/cct/boot.lua index dc48723..7bdae02 100644 --- a/Src/Hyperion-firmware-cct/boot/cct/boot.lua +++ b/Src/Hyperion-firmware-cct/boot/cct/boot.lua @@ -254,7 +254,6 @@ local ok, err = xpcall(function() if not ok then displaySuperBadError(err) end end) - -- time is in milliseconds function coroutine.resumeWithTimeout(co, timeout, ...) local startTime = computer.time() debug.sethook(co, function() @@ -294,6 +293,14 @@ local ok, err = xpcall(function() queueEvent("componentAdded", "disk") elseif event[1] == "disk_eject" then queueEvent("componentRemoved", "disk") + elseif event[1] == "modem_message" then + queueEvent("modem_message", table.unpack(event, 2)) + elseif event[1] == "rednet_message" then + queueEvent("rednet_message", table.unpack(event, 2)) + elseif event[1] == "http_success" then + queueEvent("http_success", table.unpack(event, 2)) + elseif event[1] == "http_failure" then + queueEvent("http_failure", table.unpack(event, 2)) elseif event[1] == "NoSleep" then exit = true end diff --git a/Src/Hyperion-kernel/boot/kernel.lua b/Src/Hyperion-kernel/boot/kernel.lua index 498b95a..4136dca 100644 --- a/Src/Hyperion-kernel/boot/kernel.lua +++ b/Src/Hyperion-kernel/boot/kernel.lua @@ -8,7 +8,7 @@ local computer = args[6] local ifs = args[7] local kernel = {} kernel.LOG_Text="" -kernel.version="HyperionOS V1.0.0" +kernel.version="HyperionOS V1.2.0" kernel.process = "Kernel" kernel.users={[0]="root",[1]="User"} kernel.hostname = "hyperion" diff --git a/Src/Hyperion-kernel/lib/modules/Hyperion/10_vfs.kmod b/Src/Hyperion-kernel/lib/modules/Hyperion/10_vfs.kmod index e641638..d984820 100644 --- a/Src/Hyperion-kernel/lib/modules/Hyperion/10_vfs.kmod +++ b/Src/Hyperion-kernel/lib/modules/Hyperion/10_vfs.kmod @@ -5,11 +5,101 @@ kernel.vfs = vfs vfs.mounts = {["$"] = "/"} vfs.disks = kernel.disks --- Path normalization +-- Metafile format (version 1) +-- File header: 1 byte = version (0x01) +-- Per-entry: +-- 1 byte = name length +-- N bytes = name +-- 1 byte = entry type (0x00 = regular, 0x01 = symlink) +-- 1 byte = owner uid +-- 1 byte = group gid +-- 2 bytes = perms (little-endian uint16) +-- bit 0 = world-write bit 1 = world-read +-- bit 2 = group-write bit 3 = group-read +-- bit 4 = owner-write bit 5 = owner-read +-- bit 6 = suid +-- bit 7 = world-exec +-- bit 8 = group-exec +-- bit 9 = owner-exec +-- 1 byte = cmeta length +-- N bytes = cmeta (for symlinks: the link target path) +-- +-- Version 0: +-- No file header. Per-entry: +-- 1 byte name len, N bytes name, 1 byte owner, 1 byte group, +-- 1 byte perms (low 7 bits only), 1 byte cmeta len, N bytes cmeta + +local META_VERSION = 0x01 + +local function bit_is_set(num, bit) + return math.floor(num / (2 ^ bit)) % 2 == 1 +end + +local function parseMetafile(raw) + if not raw or raw == "" then return {} end + local ret = {} + local p = 1 + + local version = 0 + if raw:byte(1) == META_VERSION then + version = META_VERSION + p = 2 + end + + while p <= #raw do + if p > #raw then break end + local namelen = raw:byte(p); p = p + 1 + if namelen == 0 or p + namelen - 1 > #raw then break end + local name = raw:sub(p, p + namelen - 1); p = p + namelen + + local etype, owner, group, perms, cmeta + + if version == META_VERSION then + if p + 4 > #raw then break end + etype = raw:byte(p); p = p + 1 + owner = raw:byte(p); p = p + 1 + group = raw:byte(p); p = p + 1 + perms = raw:byte(p) + raw:byte(p+1) * 256; p = p + 2 + else + if p + 2 > #raw then break end + etype = 0x00 + owner = raw:byte(p); p = p + 1 + group = raw:byte(p); p = p + 1 + perms = raw:byte(p); p = p + 1 + end + + if p > #raw then break end + local cmetalen = raw:byte(p); p = p + 1 + cmeta = "" + if cmetalen > 0 then + cmeta = raw:sub(p, p + cmetalen - 1); p = p + cmetalen + end + + ret[name] = { etype = etype, owner = owner, group = group, + perms = perms, cmeta = cmeta } + end + + return ret +end + +local function makeMetafile(meta) + local out = string.char(META_VERSION) + for name, m in pairs(meta) do + local plo = m.perms % 256 + local phi = math.floor(m.perms / 256) % 256 + out = out + .. string.char(#name) .. name + .. string.char(m.etype or 0x00) + .. string.char(m.owner, m.group, plo, phi) + .. string.char(#m.cmeta) .. m.cmeta + end + return out +end + local function normalizePath(path) local task = kernel.currentTask - local cwd = task.cwd or "/" - if path:sub(1, 1) ~= "/" then path = cwd .. "/" .. path end + local cwd = task.cwd or "/" + if path:sub(1,1) ~= "/" then path = cwd .. "/" .. path end local parts = {} for part in path:gmatch("[^/]+") do if part == ".." then @@ -18,236 +108,224 @@ local function normalizePath(path) table.insert(parts, part) end end - return "/" .. table.concat(parts, "/") + local result = "/" .. table.concat(parts, "/") + local root = task and task.root + if root and root ~= "/" then + if result ~= root and result:sub(1, #root + 1) ~= root .. "/" then + result = root + end + end + return result end function vfs.splitPath(path) - local rv=string.split(path,"/") + local rv = string.split(path, "/") while table.indexOf(rv, "") ~= -1 do table.remove(rv, table.indexOf(rv, "")) end return rv end --- Resolve mount and disk path -local function resolvePath(path) - path = normalizePath(path) - - local mountPoint = nil - local mountId = nil - +local function resolveMount(normalPath) + local mountPoint, mountId = nil, nil for id, mp in pairs(vfs.mounts) do - if path == mp or (mp == "/" and path:sub(1, 1) == "/") or path:sub(1, #mp + 1) == mp .. "/" then - if not mountPoint or #mp > #mountPoint then - mountPoint = mp - mountId = id + local mpNorm = (mp ~= "/" and mp:sub(-1) == "/") and mp:sub(1,-2) or mp + if normalPath == mpNorm + or (mpNorm == "/" and normalPath:sub(1,1) == "/") + or normalPath:sub(1, #mpNorm + 1) == mpNorm .. "/" + then + if not mountPoint or #mpNorm > #mountPoint then + mountPoint = mpNorm + mountId = id end end end - - if not mountId then - error("ENODEV") - end - - local diskPath = path:sub(#mountPoint + 1) - if diskPath == "" then - diskPath = "/" - end - - if kernel.config.logPathResolution then - kernel.log("Path '"..path.."' resolved to disk '"..mountId.."' and path '"..diskPath.."'") - end - + if not mountId then error("ENODEV") end + local diskPath = normalPath:sub(#mountPoint + 1) + if diskPath == "" then diskPath = "/" end return vfs.disks[mountId], diskPath end --- Allocate file descriptor for current task -local function allocFD(task) - local fd = 0 - while task.fd[fd] do fd = fd + 1 end - if fd >= kernel.config.maxFilesPerTask then error("ENFILE") end - return fd -end - --- System-wide open file limit -local total = 0 -local function checkSystemLimit() - if total >= kernel.config.maxOpenFiles - 16 then error("ENFILE") end -end - --- File object constructor -local function newFileObj(handle, mode, path, meta, type) - return { - handle = handle, - mode = mode, - path = path, - meta = meta, - type = type, - refcount = 1 - } -end - -function vfs.newfd(fdobj) - checkSystemLimit() - total=total+1 - local fd = allocFD(kernel.currentTask) - kernel.currentTask.fd[fd]=fdobj -end - --- Parse metafile -local function parseMetafile(file) - if not file or file == "" then return {} end - - local ret = {} - local pointer = 1 - - while pointer <= #file do - local namelen = file:byte(pointer) - pointer = pointer + 1 - - local name = file:sub(pointer, pointer + namelen - 1) - pointer = pointer + namelen - - local owner = file:byte(pointer) - local group = file:byte(pointer + 1) - local perms = file:byte(pointer + 2) - pointer = pointer + 3 - - local cmetalen = file:byte(pointer) - pointer = pointer + 1 - - local cmeta = "" - if cmetalen > 0 then - cmeta = file:sub(pointer, pointer + cmetalen - 1) - pointer = pointer + cmetalen - end - - ret[name] = {owner = owner, group = group, perms = perms, cmeta = cmeta} +local function readMetaEntry(disk, parentDiskPath, filename) + if filename == ".meta" then error("Cannot open metafile") end + local mp + if parentDiskPath == "/" then + mp = ".meta" + else + local p = parentDiskPath:gsub("^/+", "") + mp = p .. "/.meta" end - - return ret + local ok, f = pcall(function() return disk:open(mp, "r") end) + if not ok or not f then return nil end + local raw = f.read(65535) + if f.close then f.close() end + local parsed = parseMetafile(raw) + return parsed[filename] end --- Build metafile -local function makeMetafile(meta) - local file = "" - for name, m in pairs(meta) do - local entry = "" - entry = entry .. string.char(#name) .. name - entry = entry .. string.char(m.owner, m.group, m.perms) - entry = entry .. string.char(#m.cmeta) .. m.cmeta - file = file .. entry - end - return file -end - --- Get file metadata object -local function getFileMeta(path) - local disk, fullPath = resolvePath(path) - fullPath = normalizePath(fullPath) +local MAX_SYMLINK = 16 +local function resolveSymlinks(path, noFollow, _depth) + _depth = _depth or 0 + if _depth > MAX_SYMLINK then error("ELOOP") end + path = normalizePath(path) local parts = {} - for p in fullPath:gmatch("[^/]+") do table.insert(parts, p) end + for p in path:gmatch("[^/]+") do table.insert(parts, p) end - -- default fallback - local default = {owner = 0, group = 0, perms = 63, cmeta = ""} + local resolved = "" - -- walk from deepest parent upward - for i = #parts, 1, -1 do - local parent = "/" .. table.concat(parts, "/", 1, i - 1) - if parent ~= "/" then parent = parent .. "/" end + for i, part in ipairs(parts) do + local candidate = resolved == "" and ("/" .. part) or (resolved .. "/" .. part) - local target = parts[i] - if target == ".meta" then error("Cannot open metafile") end - local metaPath = parent .. ".meta" - - if disk:fileExists(metaPath) then - local f = disk:open(metaPath, "r") - local text = f.read(65535) - f.close() - - local parsed = parseMetafile(text) - if parsed[target] then return parsed[target] end + if noFollow and i == #parts then + resolved = candidate + break end + + local disk, parentDisk = resolveMount(resolved == "" and "/" or resolved) + local entry = readMetaEntry(disk, parentDisk, part) + + if entry and entry.etype == 0x01 then + local target = entry.cmeta + if target:sub(1,1) ~= "/" then + target = (resolved == "" and "/" or resolved) .. "/" .. target + end + if i < #parts then + target = target .. "/" .. table.concat(parts, "/", i+1, #parts) + end + return resolveSymlinks(normalizePath(target), noFollow, _depth + 1) + end + + resolved = candidate end + if resolved == "" then resolved = "/" end + return resolved +end + +local function resolvePath(path, noFollow) + local real = resolveSymlinks(path, noFollow) + local disk, diskPath = resolveMount(real) + if kernel.config.logPathResolution then + kernel.log("resolvePath '"..path.."' -> '"..real.."' diskPath '"..diskPath.."'") + end + return disk, diskPath, real +end + +local function getFileMeta(path, noFollow) + local real = resolveSymlinks(path, noFollow) + + local parts = {} + for p in real:gmatch("[^/]+") do table.insert(parts, p) end + + local default = { etype = 0x00, owner = 0, group = 0, perms = 63, cmeta = "" } + if #parts == 0 then return default end + + local parentNorm = "/" .. table.concat(parts, "/", 1, #parts - 1) + if parentNorm == "" then parentNorm = "/" end + local disk, parentDiskPath = resolveMount(parentNorm) + local entry = readMetaEntry(disk, parentDiskPath, parts[#parts]) + if entry then return entry end return default end -local function ensureParentMeta(path) - local disk, fullPath = resolvePath(path) - fullPath = normalizePath(fullPath) +local function writeMetaEntry(path, name, entry, noFollow) + local real = resolveSymlinks(path, noFollow) + local disk, diskPath = resolveMount(real) - -- split parent + name - local parent, name = fullPath:match("^(.*)/([^/]+)$") - if not parent then - parent = "/" - name = fullPath:gsub("^/", "") + local mp + if diskPath == "/" then + mp = ".meta" + else + mp = diskPath:gsub("^/+", "") .. "/.meta" end - if name == ".meta" then error("Cannot open metafile") end - - if parent ~= "/" and parent:sub(-1) ~= "/" then parent = parent .. "/" end - - local metaPath = parent .. ".meta" - - if not disk:fileExists(metaPath) then - local f = disk:open(metaPath, "w") - f.write("") - f.close() + local existing = {} + local rok, rf = pcall(function() return disk:open(mp, "r") end) + if rok and rf then + local raw = rf.read(65535) + if rf.close then rf.close() end + existing = parseMetafile(raw) end - return metaPath, name + existing[name] = entry + + local f = disk:open(mp, "w") + f.write(makeMetafile(existing)) + if f.close then f.close() end end --- Permission checking +vfs.P = { + OWNER_R = 1 * (2^5), -- 32 + OWNER_W = 1 * (2^4), -- 16 + OWNER_X = 1 * (2^9), -- 512 + GROUP_R = 1 * (2^3), -- 8 + GROUP_W = 1 * (2^2), -- 4 + GROUP_X = 1 * (2^8), -- 256 + WORLD_R = 1 * (2^1), -- 2 + WORLD_W = 1 * (2^0), -- 1 + WORLD_X = 1 * (2^7), -- 128 + SUID = 1 * (2^6), -- 64 +} + +local P = vfs.P + +vfs.PERM = { + RW_R_R = P.OWNER_R + P.OWNER_W + P.GROUP_R + P.WORLD_R, -- 644 + RWX_RX = P.OWNER_R + P.OWNER_W + P.OWNER_X + P.GROUP_R + P.GROUP_X + P.WORLD_R + P.WORLD_X, -- 755 + RW_R__ = P.OWNER_R + P.OWNER_W + P.GROUP_R, -- 640 + RW____ = P.OWNER_R + P.OWNER_W, -- 600 + RWXR__ = P.OWNER_R + P.OWNER_W + P.OWNER_X + P.GROUP_R + P.WORLD_R, -- 744 + SUID_755 = P.SUID + P.OWNER_R + P.OWNER_W + P.OWNER_X + P.GROUP_R + P.GROUP_X + P.WORLD_R + P.WORLD_X, + RWXRWXRWX = P.OWNER_R+P.OWNER_W+P.OWNER_X+P.GROUP_R+P.GROUP_W+P.GROUP_X+P.WORLD_R+P.WORLD_W+P.WORLD_X, +} + local function checkperms(meta, mode) - local modes = { - r = {owner = 5, group = 3, everyone = 1}, - w = {owner = 4, group = 2, everyone = 0}, - a = {owner = 4, group = 2, everyone = 0} - } + local task = kernel.currentTask + local euid = (task and task.euid) or (task and task.uid) or kernel.uid + local groups = (task and task.groups) or kernel.groups or {} + + if euid == 0 then return true end local bits = meta.perms - local function bit_is_set(num, bit) - return math.floor(num / (2 ^ bit)) % 2 == 1 - end - if kernel.uid == 0 then return true end - if kernel.uid == meta.owner and bit_is_set(bits, modes[mode].owner) then - return true - end - - if meta.group and kernel.groups then - for _, gid in ipairs(kernel.groups) do - if gid == meta.group and bit_is_set(bits, modes[mode].group) then - return true + if mode == "x" then + if euid == meta.owner and bit_is_set(bits, 9) then return true end + if meta.group then + for _, gid in ipairs(groups) do + if gid == meta.group and bit_is_set(bits, 8) then return true end end end + if bit_is_set(bits, 7) then return true end + error("EACCES") end - if bit_is_set(bits, modes[mode].everyone) then return true end + local bitmap = { + r = {owner = 5, group = 3, everyone = 1}, + w = {owner = 4, group = 2, everyone = 0}, + a = {owner = 4, group = 2, everyone = 0}, + } + local m = bitmap[mode] + if not m then error("EINVAL") end + + if euid == meta.owner and bit_is_set(bits, m.owner) then return true end + if meta.group then + for _, gid in ipairs(groups) do + if gid == meta.group and bit_is_set(bits, m.group) then return true end + end + end + if bit_is_set(bits, m.everyone) then return true end error("EACCES") end --- mounts local function normalizeMountPoint(path) path = normalizePath(path) - if path ~= "/" and path:sub(-1) == "/" then path = path:sub(1, -2) end + if path ~= "/" and path:sub(-1) == "/" then path = path:sub(1,-2) end return path end -local required = { - "open", - "type", - "list", - "attributes", - "fileExists", - "makeDirectory", - "remove" -} - -local function check(disk) +local required = {"open","type","list","attributes","fileExists","makeDirectory","remove"} +local function checkDisk(disk) for _, name in ipairs(required) do if type(disk[name]) ~= "function" then error("Invalid disk: missing method '" .. name .. "'") @@ -255,209 +333,244 @@ local function check(disk) end end -function vfs.mount(target, diskOrId) - if kernel.uid ~= 0 then error("EPERM") end - if not target then error("EINVAL") end +local total = 0 +local function allocFD(task) + local fd = 0 + while task.fd[fd] do fd = fd + 1 end + if fd >= kernel.config.maxFilesPerTask then error("ENFILE") end + return fd +end +local function checkSystemLimit() + if total >= kernel.config.maxOpenFiles - 16 then error("ENFILE") end +end +local function newFileObj(handle, mode, path, meta, ftype) + return { handle=handle, mode=mode, path=path, meta=meta, type=ftype, refcount=1 } +end +function vfs.newfd(fdobj) + checkSystemLimit(); total = total + 1 + local fd = allocFD(kernel.currentTask) + kernel.currentTask.fd[fd] = fdobj + return fd +end + +function vfs.mount(target, diskOrId) + local _euid = (kernel.currentTask and (kernel.currentTask.euid or kernel.currentTask.uid)) or kernel.uid + if _euid ~= 0 then error("EPERM") end + if not target then error("EINVAL") end target = normalizeMountPoint(target) if not vfs.exists(target) then vfs.mkdir(target) end if vfs.type(target) ~= "directory" then error("EINVAL") end - local disk - local id - + local disk, id if type(diskOrId) == "string" then disk = kernel.disks[diskOrId] if not disk then error("ENODEV") end - check(disk) - id = diskOrId + checkDisk(disk); id = diskOrId elseif type(diskOrId) == "table" then - check(disk) - disk = diskOrId - id = disk.address - vfs.disks[id] = disk - else - error("EINVAL") - end + checkDisk(diskOrId); disk = diskOrId + id = disk.address; vfs.disks[id] = disk + else error("EINVAL") end - -- Prevent shadowing an existing mount + if vfs.mounts[id] then error("EBUSY") end for _, mp in pairs(vfs.mounts) do if mp == target then error("EBUSY") end end - vfs.mounts[id] = target return true end function vfs.umount(target) - if kernel.uid ~= 0 then error("EPERM") end + local _euid = (kernel.currentTask and (kernel.currentTask.euid or kernel.currentTask.uid)) or kernel.uid + if _euid ~= 0 then error("EPERM") end if not target then error("EINVAL") end - target = normalizeMountPoint(target) - for id, mp in pairs(vfs.mounts) do if mp == target then - if id == "$" then error("EBUSY") end -- root fs - vfs.mounts[id] = nil - return true + if id == "$" then error("EBUSY") end + vfs.mounts[id] = nil; return true end end - error("EINVAL") end --- Open file function vfs.open(path, mode) checkSystemLimit() local task = kernel.currentTask - local fd = allocFD(task) + local fd = allocFD(task) local disk, diskPath = resolvePath(path) if not disk then error("NODISK") end local meta = getFileMeta(path) - checkperms(meta, mode) + checkperms(meta, mode == "r" and "r" or "w") local handle - if disk:type(diskPath)~="directory" then + if disk:type(diskPath) ~= "directory" then handle = disk:open(diskPath, mode) - if type(handle)~="table" then error("ENFILE") end + if type(handle) ~= "table" then error("ENFILE") end end - task.fd[fd] = newFileObj(handle, mode, path, meta, disk:type(diskPath)) - if not disk.isvirt then - total = total + 1 + local fobj = newFileObj(handle, mode, path, meta, disk:type(diskPath)) + if mode == "r" and bit_is_set(meta.perms, 6) then + fobj.suid_owner = meta.owner end + task.fd[fd] = fobj + if not disk.isvirt then total = total + 1 end return fd end --- Read function vfs.read(fd, count) - local task = kernel.currentTask - local file = task.fd[fd] - if not file then error("EBADF") end - if not file.handle.read then error("EBADF") end + local file = kernel.currentTask.fd[fd] + if not file or not file.handle or not file.handle.read then error("EBADF") end return file.handle.read(count or 1) or "" end --- Write function vfs.write(fd, content) - local task = kernel.currentTask - local file = task.fd[fd] - if not file then error("EBADF") end - if not file.handle.write then error("EBADF") end + local file = kernel.currentTask.fd[fd] + if not file or not file.handle or not file.handle.write then error("EBADF") end return file.handle.write(content) end --- Pread / Pwrite function vfs.pread(fd, count, offset) - local task = kernel.currentTask - local file = task.fd[fd] - if not file then error("EBADF") end - if not file.handle.read then error("EBADF") end - if not file.handle.seek then error("EBADF") end + local file = kernel.currentTask.fd[fd] + if not file or not file.handle or not file.handle.read or not file.handle.seek then error("EBADF") end file.handle.seek("set", offset) return file.handle.read(count or 1) or "" end function vfs.pwrite(fd, content, offset) - local task = kernel.currentTask - local file = task.fd[fd] - if not file then error("EBADF") end - if not file.handle.write then error("EBADF") end - if not file.handle.seek then error("EBADF") end + local file = kernel.currentTask.fd[fd] + if not file or not file.handle or not file.handle.write or not file.handle.seek then error("EBADF") end file.handle.seek("set", offset) return file.handle.write(content) end --- Seek function vfs.lseek(fd, offset, whence) - local task = kernel.currentTask - local file = task.fd[fd] - if not file then error("EBADF") end - if not file.handle.seek then error("EBADF") end + local file = kernel.currentTask.fd[fd] + if not file or not file.handle or not file.handle.seek then error("EBADF") end return file.handle.seek(whence or "set", offset) end --- Fsync function vfs.fsync(fd) - local task = kernel.currentTask - local file = task.fd[fd] - if not file then error("EBADF") end - if not file.handle.flush then error("EBADF") end + local file = kernel.currentTask.fd[fd] + if not file or not file.handle or not file.handle.flush then error("EBADF") end if file.mode ~= "w" and file.mode ~= "a" then error("EBADF") end file.handle.flush() end --- Close function vfs.close(fd) local task = kernel.currentTask local file = task.fd[fd] if not file then error("EBADF") end - task.fd[fd] = nil total = total - 1 - file.refcount = file.refcount - 1 - if file.refcount <= 0 then - if file.handle.close then - file.handle.close() - end + if file.refcount <= 0 and file.handle and file.handle.close then + file.handle.close() end end --- Sendfile function vfs.sendfile(outfd, infd, count) - local task = kernel.currentTask - local inFile = task.fd[infd] - local outFile = task.fd[outfd] + local inFile = kernel.currentTask.fd[infd] + local outFile = kernel.currentTask.fd[outfd] if not inFile or not outFile then error("EBADF") end - if not inFile.handle.read then error("EBADF") end - if not outFile.handle.write then error("EBADF") end + if not inFile.handle.read or not outFile.handle.write then error("EBADF") end local data = inFile.handle.read(count or 1024) if not data or data == "" then return end return outFile.handle.write(data) end --- Stat / Fstat function vfs.stat(path) local disk, diskPath = resolvePath(path) - local meta = getFileMeta(path) - local attrs = disk:attributes(diskPath) + local meta = getFileMeta(path) + local ok, attrs = pcall(disk.attributes, disk, diskPath) + if not ok then attrs = { size=0, modified=0, created=0 } end return { - size = attrs.size, + size = attrs.size, modified = attrs.modified, - created = attrs.created, - owner = meta.owner, - group = meta.group, - xattr = meta.cmeta + created = attrs.created, + owner = meta.owner, + group = meta.group, + perms = meta.perms, + etype = meta.etype, + xattr = meta.cmeta, + } +end + +function vfs.lstat(path) + local meta = getFileMeta(path, true) + + local attrs + if meta.etype == 0x01 then + attrs = { size=0, modified=0, created=0 } + else + local disk, diskPath = resolvePath(path, true) + local ok, a = pcall(disk.attributes, disk, diskPath) + attrs = ok and a or { size=0, modified=0, created=0 } + end + return { + size = attrs.size, + modified = attrs.modified, + created = attrs.created, + owner = meta.owner, + group = meta.group, + perms = meta.perms, + etype = meta.etype, + xattr = (meta.etype == 0x01) and "" or meta.cmeta, + symlink_target = (meta.etype == 0x01) and meta.cmeta or nil, } end function vfs.fstat(fd) - local task = kernel.currentTask - local file = task.fd[fd] + local file = kernel.currentTask.fd[fd] if not file then error("EBADF") end - local disk, path = resolvePath(file.path) - local attrs = disk:attributes(path) + local disk, diskPath = resolvePath(file.path) + local attrs = disk:attributes(diskPath) return { - size = attrs.size, + size = attrs.size, modified = attrs.modified, created = attrs.created, - owner = file.meta.owner, - group = file.meta.group, - xattr = file.meta.cmeta + owner = file.meta.owner, + group = file.meta.group, + perms = file.meta.perms, + etype = file.meta.etype, + xattr = file.meta.cmeta, } end --- Directory operations function vfs.listdir(path) local disk, diskPath = resolvePath(path) - if disk:type(diskPath) ~= "directory" then error("ENOENT") end local meta = getFileMeta(path) checkperms(meta, "r") + if disk:type(diskPath) ~= "directory" then error("ENOTDIR") end + local list = disk:list(diskPath) - if table.indexOf(list, ".meta") ~= -1 then - table.remove(list, table.indexOf(list, ".meta")) + local seen = {} + local out = {} + for _, v in ipairs(list) do + if v ~= ".meta" then + seen[v] = true + table.insert(out, v) + end end - return list + + local mp + if diskPath == "/" then + mp = ".meta" + else + mp = diskPath:gsub("^/+", "") .. "/.meta" + end + local lok, lf = pcall(function() return disk:open(mp, "r") end) + if lok and lf then + local raw = lf.read(65535) + if lf.close then lf.close() end + local parsed = parseMetafile(raw) + for name, entry in pairs(parsed) do + if entry.etype == 0x01 and not seen[name] then + table.insert(out, name) + end + end + end + + return out end function vfs.mkdir(path) @@ -468,100 +581,173 @@ function vfs.mkdir(path) end function vfs.remove(path) - local disk, diskPath = resolvePath(path) - local meta = getFileMeta(path) + local meta = getFileMeta(path, true) checkperms(meta, "w") - disk:remove(diskPath) + + if kernel.unixSockets and kernel.unixSockets[path] then + kernel.unixSockets[path] = nil + end + + if meta.etype == 0x01 then + local norm = resolveSymlinks(path, true) + local parent = norm:match("^(.*)/[^/]+$") or "/" + if parent == "" then parent = "/" end + local name = norm:match("[^/]+$") + local disk, parentDiskPath = resolveMount(parent) + local mp + if parentDiskPath == "/" then mp = ".meta" + else mp = parentDiskPath:gsub("^/+", "") .. "/.meta" end + local rok, rf = pcall(function() return disk:open(mp, "r") end) + local parsed = {} + if rok and rf then + local raw = rf.read(65535) + if rf.close then rf.close() end + parsed = parseMetafile(raw) + end + parsed[name] = nil + local f2 = disk:open(mp, "w") + f2.write(makeMetafile(parsed)) + if f2.close then f2.close() end + else + local disk, diskPath = resolvePath(path) + disk:remove(diskPath) + end end --- Permission functions -function vfs.chmod(path, perms) - local disk, diskPath = resolvePath(path) +function vfs.symlink(target, linkPath) + if type(target) ~= "string" or type(linkPath) ~= "string" then error("EINVAL") end + local norm = normalizePath(linkPath) + local parent = norm:match("^(.*)/[^/]+$") or "/" + if parent == "" then parent = "/" end + local name = norm:match("[^/]+$") + if not name then error("EINVAL") end + + + local parentMeta = getFileMeta(parent) + checkperms(parentMeta, "w") + + local task = kernel.currentTask + local euid = (task and (task.euid or task.uid)) or kernel.uid + local egid = (task and task.gid) or kernel.gid or 0 + local entry = { + etype = 0x01, + owner = euid, + group = egid, + perms = vfs.PERM.RWXRWXRWX, + cmeta = target, + } + local ok, err = pcall(writeMetaEntry, parent, name, entry, false) + if not ok then error(err) end +end + +function vfs.readlink(path) + local meta = getFileMeta(path, true) + if meta.etype ~= 0x01 then error("EINVAL") end + return meta.cmeta +end + +function vfs.access(path, mode) local meta = getFileMeta(path) + for i = 1, #mode do + checkperms(meta, mode:sub(i,i)) + end + return true +end - if meta.owner ~= kernel.currentTask.uid then error("EACCES") end - meta.perms = perms +local function updateMeta(path, fn, noFollow) + local real = resolveSymlinks(path, noFollow) + local norm = real + local parent = norm:match("^(.*)/[^/]+$") or "/" + if parent == "" then parent = "/" end + local name = norm:match("[^/]+$") + if not name then error("EINVAL") end - local mpath, target = ensureParentMeta(path) + local disk, parentDisk = resolveMount(parent) + local mp + if parentDisk == "/" then + mp = ".meta" + else + mp = parentDisk:gsub("^/+", "") .. "/.meta" + end - local mf = disk:open(mpath, "r") - local text = mf.read(65535) - mf.close() + local existing = {} + local uok, uf = pcall(function() return disk:open(mp, "r") end) + if uok and uf then + local raw = uf.read(65535) + if uf.close then uf.close() end + existing = parseMetafile(raw) + end + local entry = existing[name] or { etype=0, owner=0, group=0, perms=63, cmeta="" } + fn(entry) + existing[name] = entry + local f = disk:open(mp, "w"); f.write(makeMetafile(existing)); if f.close then f.close() end +end - local parsed = parseMetafile(text) - parsed[target] = meta - - local f = disk:open(mpath, "w") - f.write(makeMetafile(parsed)) - f.close() +function vfs.chmod(path, perms) + local meta = getFileMeta(path) + local euid = (kernel.currentTask and (kernel.currentTask.euid or kernel.currentTask.uid)) or kernel.uid + if euid ~= 0 and euid ~= meta.owner then error("EACCES") end + updateMeta(path, function(e) e.perms = perms end) end function vfs.fchmod(fd, perms) - local task = kernel.currentTask - local file = task.fd[fd] + local file = kernel.currentTask.fd[fd] if not file then error("EBADF") end vfs.chmod(file.path, perms) end function vfs.chown(path, uid, gid) - local disk, diskPath = resolvePath(path) - local meta = getFileMeta(path) - - if meta.owner ~= kernel.currentTask.uid then error("EACCES") end - meta.owner = uid - meta.group = gid - - local mpath, target = ensureParentMeta(path) - - local mf = disk:open(mpath, "r") - local text = mf.read(65535) - mf.close() - - local parsed = parseMetafile(text) - parsed[target] = meta - - local f = disk:open(mpath, "w") - f.write(makeMetafile(parsed)) - f.close() + local _euid = (kernel.currentTask and (kernel.currentTask.euid or kernel.currentTask.uid)) or kernel.uid + if _euid ~= 0 then error("EPERM") end + updateMeta(path, function(e) e.owner = uid; e.group = gid end) end function vfs.fchown(fd, uid, gid) - local task = kernel.currentTask - local file = task.fd[fd] + local file = kernel.currentTask.fd[fd] if not file then error("EBADF") end vfs.chown(file.path, uid, gid) end function vfs.exists(path) - local disk, diskPath = resolvePath(path) - local meta = getFileMeta(path) - checkperms(meta, "r") + local meta = getFileMeta(path, true) + if meta.etype == 0x01 then return true end + local ok, disk, diskPath = pcall(resolvePath, path) + if not ok then return false end return disk:fileExists(diskPath) end function vfs.type(path) - local disk, diskPath = resolvePath(path) - local meta = getFileMeta(path) - checkperms(meta, "r") + local meta = getFileMeta(path, true) + if meta.etype == 0x01 then return "symlink" end + local ok, disk, diskPath = pcall(resolvePath, path) + if not ok then return nil end return disk:type(diskPath) end function vfs.getcwd() return kernel.currentTask.cwd end +function vfs.chdir(path) + if vfs.type(path) ~= "directory" then error("ENOTDIR") end + kernel.currentTask.cwd = normalizePath(path) +end -function vfs.chdir(path) kernel.currentTask.cwd = path end +function vfs.chroot(path) + local euid = (kernel.currentTask and (kernel.currentTask.euid or kernel.currentTask.uid)) or kernel.uid + if euid ~= 0 then error("EPERM") end + if vfs.type(path) ~= "directory" then error("ENOTDIR") end + local norm = normalizePath(path) + kernel.currentTask.root = norm + kernel.currentTask.cwd = norm +end function vfs.dup(oldfd) local task = kernel.currentTask local file = task.fd[oldfd] if not file then error("EBADF") end - checkSystemLimit() - local newfd = allocFD(task) file.refcount = file.refcount + 1 task.fd[newfd] = file total = total + 1 - return newfd end @@ -569,24 +755,13 @@ function vfs.dup2(oldfd, newfd) local task = kernel.currentTask local file = task.fd[oldfd] if not file then error("EBADF") end - if newfd < 0 or newfd >= kernel.config.maxFilesPerTask then - error("EBADF") - end - - if oldfd == newfd then - return newfd - end - - if task.fd[newfd] then - vfs.close(newfd) - end - + if newfd < 0 or newfd >= kernel.config.maxFilesPerTask then error("EBADF") end + if oldfd == newfd then return newfd end + if task.fd[newfd] then vfs.close(newfd) end checkSystemLimit() - file.refcount = file.refcount + 1 task.fd[newfd] = file total = total + 1 - return newfd end @@ -596,34 +771,44 @@ function vfs.devctl(fd, method, ...) return kernel.currentTask.fd[fd].handle[method](...) end --- Export syscalls +vfs.resolveMount = resolveMount + local sys = kernel.syscalls -sys["open"] = vfs.open -sys["close"] = vfs.close -sys["read"] = vfs.read -sys["write"] = vfs.write -sys["pread"] = vfs.pread -sys["pwrite"] = vfs.pwrite -sys["lseek"] = vfs.lseek -sys["fsync"] = vfs.fsync -sys["sendfile"] = vfs.sendfile -sys["stat"] = vfs.stat -sys["fstat"] = vfs.fstat -sys["mkdir"] = vfs.mkdir -sys["remove"] = vfs.remove -sys["listdir"] = vfs.listdir -sys["chmod"] = vfs.chmod -sys["fchmod"] = vfs.fchmod -sys["chown"] = vfs.chown -sys["fchown"] = vfs.fchown -sys["exists"] = vfs.exists -sys["type"] = vfs.type -sys["mount"] = vfs.mount -sys["umount"] = vfs.umount -sys["getcwd"] = vfs.getcwd -sys["chdir"] = vfs.chdir -sys["dup"] = vfs.dup -sys["dup2"] = vfs.dup2 -sys["devctl"] = vfs.devctl +sys["open"] = vfs.open +sys["close"] = vfs.close +sys["read"] = vfs.read +sys["write"] = vfs.write +sys["pread"] = vfs.pread +sys["pwrite"] = vfs.pwrite +sys["lseek"] = vfs.lseek +sys["fsync"] = vfs.fsync +sys["sendfile"] = vfs.sendfile +sys["stat"] = vfs.stat +sys["lstat"] = vfs.lstat +sys["fstat"] = vfs.fstat +sys["mkdir"] = vfs.mkdir +sys["remove"] = vfs.remove +sys["listdir"] = vfs.listdir +sys["chmod"] = vfs.chmod +sys["fchmod"] = vfs.fchmod +sys["chown"] = vfs.chown +sys["fchown"] = vfs.fchown +sys["exists"] = vfs.exists +sys["type"] = vfs.type +sys["mount"] = vfs.mount +sys["umount"] = vfs.umount +sys["getcwd"] = vfs.getcwd +sys["chdir"] = vfs.chdir +sys["chroot"] = vfs.chroot +sys["dup"] = vfs.dup +sys["dup2"] = vfs.dup2 +sys["devctl"] = vfs.devctl +sys["symlink"] = vfs.symlink +sys["readlink"] = vfs.readlink +sys["access"] = vfs.access +sys["fget_suid"] = function(fd) + local fobj = kernel.currentTask and kernel.currentTask.fd[fd] + return fobj and fobj.suid_owner or nil +end kernel.log("VFS module loaded") diff --git a/Src/Hyperion-kernel/lib/modules/Hyperion/12_tmpfs.kmod b/Src/Hyperion-kernel/lib/modules/Hyperion/12_tmpfs.kmod index ea26a6f..371409d 100644 --- a/Src/Hyperion-kernel/lib/modules/Hyperion/12_tmpfs.kmod +++ b/Src/Hyperion-kernel/lib/modules/Hyperion/12_tmpfs.kmod @@ -7,11 +7,9 @@ proxy.address = "tmpfs0000" proxy.isvirt = true proxy.isReadOnly = function() return false end --- Space functions (just placeholders) proxy.spaceUsed = function() return 0 end proxy.spaceTotal = function() return 0 end --- Writable operations proxy.makeDirectory = function(_, path) local steps = kernel.vfs.splitPath(path) local step = data @@ -52,7 +50,6 @@ proxy.attributes = function(_, path) } end --- Open files function proxy:open(path, mode) local steps = kernel.vfs.splitPath(path) local step = data @@ -71,26 +68,32 @@ function proxy:open(path, mode) local content = step[filename] local pos = 1 return { - read = function(amount) + read = function(amount) amount = amount or #content local chunk = content:sub(pos, pos+amount-1) pos = pos + #chunk return chunk - end + end, + close = function() end, } elseif mode == "w" then step[filename] = "" + local buf = {} return { write = function(str) - step[filename] = str - end + buf[#buf + 1] = str + end, + close = function() + step[filename] = table.concat(buf) + end, } elseif mode == "a" then if type(step[filename]) ~= "string" then step[filename] = "" end return { write = function(str) step[filename] = step[filename] .. str - end + end, + close = function() end, } else error("EACCES") @@ -123,7 +126,8 @@ function proxy:list(path) end function proxy:fileExists(path) - return pcall(function() return self:type(path) end) + local t = self:type(path) + return t == "file" or t == "directory" end kernel.disks["tmpfs0000"] = proxy \ No newline at end of file diff --git a/Src/Hyperion-kernel/lib/modules/Hyperion/13_loopdev.kmod b/Src/Hyperion-kernel/lib/modules/Hyperion/13_loopdev.kmod new file mode 100644 index 0000000..f1dcf2c --- /dev/null +++ b/Src/Hyperion-kernel/lib/modules/Hyperion/13_loopdev.kmod @@ -0,0 +1,540 @@ +-- :Minify:-- +-- Loop device driver: +-- +-- BIND (directory) - re-routes VFS calls into a host directory subtree. +-- Identical to the original behaviour. +-- +-- IMAGE (*.hfs file) - mounts a Hyperion Filesystem Image. The image is +-- loaded entirely into memory; reads and writes operate +-- on the in-memory tree, so the image file is only +-- touched on attach/detach. +-- +-- BHFS v1 - Binary Hyperion Filesystem Image format: +-- +-- File header (8 bytes): +-- [0-3] magic: 0x42 0x48 0x46 0x53 ("BHFS") +-- [4] version: 0x01 +-- [5] flags: bit0 = per-file deflate compression enabled +-- [6-7] reserved: 0x00 0x00 +-- +-- Records (repeated until END record): +-- [0] type: 0x01=file 0x02=dir 0x03=symlink 0xFF=end +-- [1-4] path_len (uint32 LE) - byte length of the path string +-- [5-8] raw_size (uint32 LE) - original uncompressed data size (0 for dirs) +-- [9-12] stored_size (uint32 LE) - bytes that follow in stream +-- (< raw_size means deflate-compressed; +-- = raw_size means stored as-is) +-- [13 .. 13+path_len-1] path bytes (no null terminator) +-- [.. +stored_size] data bytes +-- +-- Dirs have raw_size=0, stored_size=0, zero data bytes. +-- Symlinks store the target path as data; stored_size == raw_size (no compression). +-- +-- Syscalls: +-- id = syscall.losetup(path) attach dir OR .hfs image +-- id = syscall.losetup(path, true) force image mode +-- syscall.lodetach(id) detach (must be unmounted first) +-- tbl = syscall.lolist() {id -> {path,mode}, ...} +-- str = syscall.loimgcreate(srcdir) serialise VFS dir -> BHFS binary string +-- syscall.loimgwrite(str, dest) write BHFS string to a file (binary) + +local kernel = ... + +local _deflate = nil +local function getDeflate() + if _deflate == nil then + local ok, lib = pcall(require, "store.deflate") + _deflate = ok and lib or false + end + return _deflate or nil +end + +local function pack32(n) + n = math.floor(n) % 4294967296 + return string.char( + n % 256, + math.floor(n / 256) % 256, + math.floor(n / 65536) % 256, + math.floor(n / 16777216) % 256 + ) +end + +local function unpack32(s, i) + local a, b, c, d = s:byte(i, i + 3) + return (a or 0) + + (b or 0) * 256 + + (c or 0) * 65536 + + (d or 0) * 16777216 +end + +local BHFS_MAGIC = "BHFS" +local BHFS_VERSION = "\001" +local BHFS_FLAG_COMPRESS = 1 + +local TYPE_FILE = "\001" +local TYPE_DIR = "\002" +local TYPE_LINK = "\003" +local TYPE_END = "\255" + +local B64D = {} +do + local a = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + for i = 1, #a do B64D[a:sub(i, i)] = i - 1 end +end + +local function b64dec(s) + s = s:gsub("[^A-Za-z0-9+/=]", "") + local t, i = {}, 1 + while i <= #s do + local c1 = B64D[s:sub(i, i )] or 0 + local c2 = B64D[s:sub(i+1, i+1)] or 0 + local c3 = B64D[s:sub(i+2, i+2)] or 0 + local c4 = B64D[s:sub(i+3, i+3)] or 0 + local n = c1*262144 + c2*4096 + c3*64 + c4 + t[#t+1] = string.char(math.floor(n/65536) % 256) + if s:sub(i+2, i+2) ~= "=" then t[#t+1] = string.char(math.floor(n/256) % 256) end + if s:sub(i+3, i+3) ~= "=" then t[#t+1] = string.char(n % 256) end + i = i + 4 + end + return table.concat(t) +end + +local loopDevs = {} +local nextLoop = 0 + +local function makeBindDisk(id, dirPath) + local disk = { address = id, isvirt = false } + disk.isReadOnly = function() return false end + disk.spaceUsed = function() return 0 end + disk.spaceTotal = function() return 0 end + disk.setLabel = function() end + disk.getLabel = function() return id end + + local function resolveBase() + local mp, mid = "/", "$" + for id2, m in pairs(kernel.vfs.mounts) do + if dirPath == m or (m == "/" and dirPath:sub(1,1) == "/") + or dirPath:sub(1, #m+1) == m.."/" then + if #m > #mp then mp = m; mid = id2 end + end + end + return kernel.vfs.disks[mid], dirPath:sub(#mp+1) + end + + local function dp(path) + local hd, base = resolveBase() + local b = (base == "" or base == "/") and "" or base:gsub("^/+","") + local p = path:gsub("^/+","") + local c = ((b=="") and "/"..p or "/"..b.."/"..p):gsub("//+","/") + local r = c:sub(2); if r == "" then r = "/" end + return hd, r + end + + function disk:open(path,mode) local h,r=dp(path); return h:open(r,mode) end + function disk:type(path) local h,r=dp(path); return h:type(r) end + function disk:list(path) local h,r=dp(path); return h:list(r) end + function disk:fileExists(path) local h,r=dp(path); return h:fileExists(r) end + function disk:attributes(path) local h,r=dp(path); return h:attributes(r) end + function disk:makeDirectory(path) local h,r=dp(path); return h:makeDirectory(r) end + function disk:remove(path) local h,r=dp(path); return h:remove(r) end + return disk +end + +local function makeImageDisk(id, imageStr) + local root = { kind="dir", children={} } + + local function getNode(path, create) + local parts = {} + for p in path:gmatch("[^/]+") do parts[#parts+1] = p end + local node = root + for i = 1, #parts do + local name = parts[i] + if not node.children then + if not create then return nil end + node.children = {} + end + if not node.children[name] then + if not create then return nil end + node.children[name] = { kind="dir", children={} } + end + node = node.children[name] + end + return node + end + + local function ensureParent(path) + local par = path:match("^(.*)/[^/]+$") or "" + if par ~= "" then + local n = getNode(par, true) + if not n.children then n.children = {} end + end + end + + if imageStr:sub(1, 4) == BHFS_MAGIC then + local pos = 9 + + while pos <= #imageStr do + local rtype = imageStr:sub(pos, pos) + pos = pos + 1 + + if rtype == TYPE_END then break end + + local path_len = unpack32(imageStr, pos); pos = pos + 4 + local raw_size = unpack32(imageStr, pos); pos = pos + 4 + local stored_size = unpack32(imageStr, pos); pos = pos + 4 + + local path = imageStr:sub(pos, pos + path_len - 1) + pos = pos + path_len + + local stored_data = imageStr:sub(pos, pos + stored_size - 1) + pos = pos + stored_size + local data = stored_data + if stored_size < raw_size then + local deflate = getDeflate() + if deflate then + data = deflate.decompress(stored_data) or stored_data + end + end + + if rtype == TYPE_DIR then + if path ~= "" and path ~= "/" then + ensureParent(path) + local n = getNode(path, true) + n.kind = "dir"; n.children = n.children or {} + end + elseif rtype == TYPE_FILE then + ensureParent(path) + local n = getNode(path, true) + n.kind="file"; n.data=data; n.size=#data; n.children=nil + elseif rtype == TYPE_LINK then + ensureParent(path) + local n = getNode(path, true) + n.kind="link"; n.target=data; n.children=nil + end + end + else + for line in (imageStr.."\n"):gmatch("([^\n]*)\n") do + if line == "END" then + break + elseif line:sub(1,4) == "DIR " then + local p = line:sub(5):match("^%s*(.-)%s*$") + if p and p ~= "" and p ~= "/" then + ensureParent(p) + local n = getNode(p, true) + n.kind = "dir"; n.children = n.children or {} + end + elseif line:sub(1,5) == "FILE " then + local p, sz, body = line:sub(6):match("^(%S+)%s+(%d+)%s*(.-)%s*$") + if p then + ensureParent(p) + local data = (tonumber(sz) or 0) > 0 and b64dec(body) or "" + local n = getNode(p, true) + n.kind="file"; n.data=data; n.size=#data; n.children=nil + end + elseif line:sub(1,5) == "LINK " then + local p, tgt = line:sub(6):match("^(%S+)%s+(.+)$") + if p then + ensureParent(p) + local n = getNode(p, true) + n.kind="link"; n.target=tgt; n.children=nil + end + end + end + end + + local disk = { address=id, isvirt=false } + disk.isReadOnly = function() return false end + disk.spaceTotal = function() return 1024*1024*64 end + disk.spaceUsed = function() + local tot = 0 + local function w(n) + if n.kind=="file" then tot = tot + (n.size or 0) + elseif n.kind=="dir" then for _,c in pairs(n.children or {}) do w(c) end end + end + w(root); return tot + end + disk.setLabel = function() end + disk.getLabel = function() return id end + + local function norm(path) + return path:gsub("^/+",""):gsub("/+$","") + end + + function disk:type(path) + local p = norm(path) + if p == "" then return "directory" end + local n = getNode(p) + if not n then return nil end + if n.kind == "dir" then return "directory" end + return "file" + end + + function disk:fileExists(path) + local p = norm(path) + if p == "" then return true end + return getNode(p) ~= nil + end + + function disk:list(path) + local p = norm(path) + local node = (p=="") and root or getNode(p) + if not node or node.kind ~= "dir" then return {} end + local out = {} + for name in pairs(node.children or {}) do out[#out+1] = name end + return out + end + + function disk:attributes(path) + local p = norm(path) + local node = (p=="") and root or getNode(p) + if not node then return nil end + return { + size = node.kind=="file" and (node.size or 0) or 0, + isDir = node.kind=="dir", + isReadOnly = false, + created = 0, + modified = 0, + } + end + + function disk:open(path, mode) + local p = norm(path) + local node = getNode(p) + + if mode == "r" then + if not node or node.kind ~= "file" then error("ENOENT: "..path) end + local data, pos = node.data or "", 1 + return { + read = function(n) + if pos > #data then return nil end + local chunk = data:sub(pos, pos + (n or 1) - 1) + pos = pos + #chunk; return chunk + end, + readAll = function() + local all = data:sub(pos); pos = #data + 1; return all + end, + readLine = function() + if pos > #data then return nil end + local nl = data:find("\n", pos, true) + local line + if nl then line=data:sub(pos, nl-1); pos=nl+1 + else line=data:sub(pos); pos=#data+1 end + return line + end, + seek = function(w, o) + o = o or 0 + if w == "set" then pos = o + 1 + elseif w == "cur" then pos = pos + o + elseif w == "end" then pos = #data + 1 + o end + return pos - 1 + end, + close = function() end, + } + + elseif mode == "w" or mode == "a" then + local buf = (mode=="a" and node and node.kind=="file") + and {node.data or ""} or {} + local done = false + local function commit() + if done then return end; done = true + local data = table.concat(buf) + if not node then ensureParent(p); node = getNode(p, true) end + node.kind="file"; node.data=data; node.size=#data; node.children=nil + end + return { + write = function(s) buf[#buf+1] = tostring(s) end, + writeLine = function(s) buf[#buf+1] = tostring(s).."\n" end, + flush = function() end, + close = commit, + } + else + error("EINVAL: unknown mode: "..tostring(mode)) + end + end + + function disk:makeDirectory(path) + local p = norm(path) + if p == "" then return end + ensureParent(p) + local n = getNode(p, true) + n.kind="dir"; n.children=n.children or {}; n.data=nil; n.size=nil + end + + function disk:remove(path) + local p = norm(path) + if p == "" then error("EBUSY: cannot remove root") end + local par = p:match("^(.*)/[^/]+$") or "" + local name = p:match("([^/]+)$") + local pn = (par=="") and root or getNode(par) + if pn and pn.children then pn.children[name] = nil end + end + + disk._root = root + return disk +end + +local function serializeDir(srcPath) + local deflate = getDeflate() + local useCompress = deflate ~= nil + + local flags = useCompress and BHFS_FLAG_COMPRESS or 0 + local parts = { + BHFS_MAGIC, + BHFS_VERSION, + string.char(flags), + "\0\0", + } + + srcPath = srcPath:gsub("/$", "") + + local MIN_COMPRESS = 64 + + local function walk(vpath) + local ftype = kernel.vfs.type(vpath) + + if ftype == "directory" then + if vpath ~= srcPath then + local relPath = vpath:sub(#srcPath + 1) + parts[#parts+1] = TYPE_DIR + parts[#parts+1] = pack32(#relPath) + parts[#parts+1] = pack32(0) + parts[#parts+1] = pack32(0) + parts[#parts+1] = relPath + end + local ok, entries = pcall(kernel.vfs.listdir, vpath) + if ok and entries then + table.sort(entries) + for _, name in ipairs(entries) do + walk(vpath:gsub("/$","").."/"..name) + end + end + + elseif ftype == "file" then + local relPath = vpath:sub(#srcPath + 1) + local ok, fd = pcall(kernel.vfs.open, vpath, "r") + if ok then + local rawData = "" + local ok2, content = pcall(kernel.vfs.read, fd, 1024*1024) + if ok2 then rawData = content or "" end + pcall(kernel.vfs.close, fd) + + local storedData = rawData + if useCompress and #rawData >= MIN_COMPRESS then + local compressed = deflate.compress(rawData) + if compressed and #compressed < #rawData then + storedData = compressed + end + end + + parts[#parts+1] = TYPE_FILE + parts[#parts+1] = pack32(#relPath) + parts[#parts+1] = pack32(#rawData) + parts[#parts+1] = pack32(#storedData) + parts[#parts+1] = relPath + parts[#parts+1] = storedData + end + end + end + + walk(srcPath) + + parts[#parts+1] = TYPE_END + + return table.concat(parts) +end + +kernel.syscalls["losetup"] = function(filePath, forceImage) + if not filePath then error("EINVAL") end + local task = kernel.currentTask + local euid = (task and (task.euid or task.uid)) or kernel.uid + if euid ~= 0 then error("EPERM") end + + filePath = filePath:gsub("/$", "") + local id = "loop" .. tostring(nextLoop) + nextLoop = nextLoop + 1 + + local ftype = kernel.vfs.type(filePath) + local disk, mode + + if not forceImage and ftype == "directory" then + disk = makeBindDisk(id, filePath) + mode = "bind" + elseif ftype == "file" or forceImage then + if ftype ~= "file" then error("ENOENT: not a file: "..filePath) end + + local img + local ok, fd = pcall(kernel.vfs.open, filePath, "rb") + if ok then + local ok2, data = pcall(kernel.vfs.read, fd, 1024*1024*16) + pcall(kernel.vfs.close, fd) + if ok2 and data then img = data end + end + if not img then + local ok2, fd2 = pcall(kernel.vfs.open, filePath, "r") + if not ok2 then error("EIO: cannot open image: "..filePath) end + local ok3, data = pcall(kernel.vfs.read, fd2, 1024*1024*16) + pcall(kernel.vfs.close, fd2) + if not ok3 or not data then error("EIO: cannot read image: "..filePath) end + img = data + end + + disk = makeImageDisk(id, img) + mode = "image" + else + error("EINVAL: path must be a directory or .hfs image file") + end + + kernel.vfs.disks[id] = disk + loopDevs[id] = { path=filePath, disk=disk, mode=mode } + kernel.log("losetup: attached "..id.." ("..mode..") -> "..filePath, "INFO") + return id +end + +kernel.syscalls["lodetach"] = function(id) + local task = kernel.currentTask + local euid = (task and (task.euid or task.uid)) or kernel.uid + if euid ~= 0 then error("EPERM") end + + if not loopDevs[id] then error("ENXIO") end + for mid in pairs(kernel.vfs.mounts) do + if mid == id then error("EBUSY: loop device is still mounted") end + end + kernel.vfs.disks[id] = nil + loopDevs[id] = nil + kernel.log("lodetach: detached "..id, "INFO") +end + +kernel.syscalls["lolist"] = function() + local rv = {} + for id, info in pairs(loopDevs) do + rv[id] = { path=info.path, mode=info.mode } + end + return rv +end + +kernel.syscalls["loimgcreate"] = function(srcPath) + local task = kernel.currentTask + local euid = (task and (task.euid or task.uid)) or kernel.uid + if euid ~= 0 then error("EPERM") end + if not srcPath then error("EINVAL") end + if kernel.vfs.type(srcPath) ~= "directory" then error("ENOTDIR: "..srcPath) end + return serializeDir(srcPath) +end + +kernel.syscalls["loimgwrite"] = function(imgStr, destPath) + local task = kernel.currentTask + local euid = (task and (task.euid or task.uid)) or kernel.uid + if euid ~= 0 then error("EPERM") end + if not imgStr or not destPath then error("EINVAL") end + + local ok, fd = pcall(kernel.vfs.open, destPath, "wb") + if not ok then + ok, fd = pcall(kernel.vfs.open, destPath, "w") + if not ok then error("EIO: cannot write: "..tostring(destPath)) end + end + local ok2, werr = pcall(kernel.vfs.write, fd, imgStr) + pcall(kernel.vfs.close, fd) + if not ok2 then error("EIO: write failed: "..tostring(werr)) end +end + +kernel.log("Loop device driver loaded (bind + BHFS binary image + legacy HFS compat)") diff --git a/Src/Hyperion-kernel/lib/modules/Hyperion/20_socket.kmod b/Src/Hyperion-kernel/lib/modules/Hyperion/20_socket.kmod index ce8ccf2..c95e70b 100644 --- a/Src/Hyperion-kernel/lib/modules/Hyperion/20_socket.kmod +++ b/Src/Hyperion-kernel/lib/modules/Hyperion/20_socket.kmod @@ -1,14 +1,556 @@ ---:Minify:-- +-- :Minify:-- +-- Supports: +-- AF_UNIX - local IPC via /var/run/*.sock paths +-- AF_INET - network sockets with three backends: +-- rednet://0.0.B.C or rednet+PROTO://0.0.B.C -> CC rednet (computer B*256+C) +-- modem://0.0.B.C -> raw CC modem frames +-- http://host/path or https://... -> HTTP via CC http API +-- A.B.C.D (dotted quad, non-zero A) -> HTTP +-- +-- Socket lifecycle: +-- fd = syscall.socket(domain, socktype) -- "unix"/"inet", "stream"/"dgram" +-- syscall.bind(fd, address) -- server: claim address +-- syscall.listen(fd, backlog) -- server: mark as listening +-- cfd = syscall.accept(fd) -- server: get connected client fd (blocking poll) +-- syscall.connect(fd, address) -- client: connect to server +-- syscall.send(fd, data) -- send bytes +-- syscall.recv(fd, len) -- receive bytes (blocking poll, returns "" on nothing) +-- syscall.sockshutdown(fd) -- half-close send side +-- -- normal vfs.close(fd) closes the socket + local kernel = ... -local socket = {} -function socket.socket() - +local sockets = {} +local unixSocks = {} +local nextSockId = 1 + +local function allocSockId() + local id = nextSockId + nextSockId = nextSockId + 1 + return id end -function socket.bind() - +local function parseAddress(addr) + if not addr then error("EINVAL") end + + if addr:sub(1,1) == "/" or addr:sub(1,5) == "unix:" then + local path = addr:sub(1,5) == "unix:" and addr:sub(6) or addr + return { backend="unix", path=path } + end + + local rproto, raddr = addr:match("^rednet%+?([^:/]*)://(.+)$") + if raddr then + local a,b,c,d = raddr:match("^(%d+)%.(%d+)%.(%d+)%.(%d+)$") + if not a then error("EINVAL: bad rednet address " .. raddr) end + local compId = tonumber(c)*256 + tonumber(d) + return { backend="rednet", compId=compId, + protocol=(rproto ~= "" and rproto or "hyperion") } + end + + local maddr = addr:match("^modem://(.+)$") + if maddr then + local a,b,c,d = maddr:match("^(%d+)%.(%d+)%.(%d+)%.(%d+)$") + if not a then error("EINVAL: bad modem address " .. maddr) end + local compId = tonumber(c)*256 + tonumber(d) + local port = tonumber(maddr:match(":(%d+)$")) or 0 + return { backend="modem", compId=compId, port=port } + end + + local scheme, rest = addr:match("^(https?)://(.+)$") + if scheme then + return { backend=scheme, url=addr } + end + + local a,b,c,d = addr:match("^(%d+)%.(%d+)%.(%d+)%.(%d+)") + if a and tonumber(a) ~= 0 then + return { backend="http", url="http://" .. addr } + end + + error("EINVAL: unrecognised address format: " .. tostring(addr)) end -kernel.socket=socket -kernel.log("Loaded socket module") \ No newline at end of file +local rednetOpen = false +local function ensureRednet() + if rednetOpen then return end + local rn = kernel.apis and kernel.apis.rednet + if not rn then error("ENODEV: no rednet API available") end + local peripheral = kernel.apis.peripheral + if peripheral then + for _, name in ipairs(peripheral.getNames and peripheral.getNames() or {}) do + if peripheral.getType(name) == "modem" then + pcall(rn.open, name) + end + end + end + rednetOpen = true +end + +local function getModem() + local peripheral = kernel.apis and kernel.apis.peripheral + if not peripheral then error("ENODEV") end + for _, name in ipairs(peripheral.getNames and peripheral.getNames() or {}) do + if peripheral.getType(name) == "modem" then + local m = peripheral.wrap(name) + if m then return m, name end + end + end + error("ENODEV: no modem peripheral found") +end + +local function pumpEvents() + local ev = kernel.computer:getMachineEvent() + while ev do + if ev == "rednet_message" then + for _, sock in pairs(sockets) do + if sock.backend == "rednet" and sock.bound then + if sock.address.protocol == tostring(select(4, table.unpack({ev}))) or + sock.address.protocol == "hyperion" then + end + end + end + end + ev = kernel.computer:getMachineEvent() + end +end + +local function pollEvent() + local results = table.pack(kernel.computer:getMachineEvent()) + if results.n == 0 or results[1] == nil then return nil end + return results +end + +local function dispatchEvent(ev) + if not ev then return end + local evtype = ev[1] + + if evtype == "rednet_message" then + local senderId = ev[2] + local message = ev[3] + local protocol = ev[4] or "hyperion" + for _, sock in pairs(sockets) do + if sock.backend == "rednet" and (sock.listening or sock.connected) then + if sock.address and sock.address.protocol == protocol then + table.insert(sock.rxbuf, { from=senderId, data=message }) + end + end + end + + elseif evtype == "modem_message" then + local channel = ev[3] + local msg = ev[5] + local fromCh = ev[4] + for _, sock in pairs(sockets) do + if sock.backend == "modem" and sock.modemChannel == channel then + table.insert(sock.rxbuf, { from=fromCh, data=msg }) + end + end + + elseif evtype == "http_success" then + local url = ev[2] + local handle = ev[3] + for _, sock in pairs(sockets) do + if sock.backend == "http" or sock.backend == "https" then + if sock.pendingUrl == url then + local body = handle.readAll and handle.readAll() or "" + handle.close() + table.insert(sock.rxbuf, { data=body, done=true }) + sock.pendingUrl = nil + sock.connected = true + end + end + end + + elseif evtype == "http_failure" then + local url = ev[2] + local err = ev[3] + for _, sock in pairs(sockets) do + if (sock.backend == "http" or sock.backend == "https") and + sock.pendingUrl == url then + sock.error = err + sock.pendingUrl = nil + end + end + end +end + +local function pumpAll() + local ev = pollEvent() + while ev do + dispatchEvent(ev) + ev = pollEvent() + end +end + +local function newSocket(domain, socktype) + local sock = { + id = allocSockId(), + domain = domain, -- "unix" | "inet" + socktype = socktype, -- "stream" | "dgram" + backend = nil, + state = "idle", -- idle | bound | listening | connected | closed + rxbuf = {}, + txbuf = {}, + backlog = {}, + address = nil, + peer = nil, + modemChannel = nil, + modem = nil, + pendingUrl = nil, + bound = false, + listening = false, + connected = false, + error = nil, + } + sockets[sock.id] = sock + return sock +end + +local sockSend, sockClose + +local function socketToFd(sock) + return { + isSocket = true, + sockId = sock.id, + mode = "rw", + meta = { etype=0, owner=0, group=0, perms=0x1FF, cmeta="" }, + type = "socket", + refcount = 1, + handle = { + read = function(count) + pumpAll() + if #sock.rxbuf == 0 then return "" end + local item = table.remove(sock.rxbuf, 1) + local data = type(item) == "table" and (item.data or "") or tostring(item) + if count and #data > count then + table.insert(sock.rxbuf, 1, { data=data:sub(count+1), from=item.from }) + data = data:sub(1, count) + end + return data + end, + write = function(data) + if sock.state == "closed" then error("EBADF") end + return sockSend(sock, data) + end, + close = function() + sockClose(sock) + end, + } + } +end + +sockSend = function(sock, data) + if sock.backend == "unix" then + local peer = sock.peer + if not peer then error("ENOTCONN") end + table.insert(peer.rxbuf, { data=data }) + return #data + + elseif sock.backend == "rednet" then + ensureRednet() + local rn = kernel.apis.rednet + rn.send(sock.address.compId, data, sock.address.protocol) + return #data + + elseif sock.backend == "modem" then + local modem = sock.modem + if not modem then error("ENOTCONN") end + modem.transmit(sock.address.port, sock.modemChannel or 0, data) + return #data + + elseif sock.backend == "http" or sock.backend == "https" then + local http = kernel.apis and kernel.apis.http + if not http then error("ENODEV: no http API") end + local url = sock.address.url + local ok, err = pcall(http.request, url, data, { + ["Content-Type"] = "application/octet-stream" + }) + if not ok then error("ENETDOWN: " .. tostring(err)) end + sock.pendingUrl = url + return #data + end + error("EPROTONOSUPPORT") +end + +sockClose = function(sock) + if sock.state == "closed" then return end + sock.state = "closed" + + if sock.backend == "unix" then + if sock.peer then + sock.peer.peer = nil + sock.peer.state = "closed" + end + if sock.bound and sock.address and sock.address.path then + unixSocks[sock.address.path] = nil + end + + elseif sock.backend == "modem" and sock.modem and sock.modemChannel then + pcall(sock.modem.close, sock.modemChannel) + + elseif sock.backend == "rednet" then + end + + sockets[sock.id] = nil +end + +kernel.syscalls["socket"] = function(domain, socktype) + domain = domain or "inet" + socktype = socktype or "stream" + if domain ~= "unix" and domain ~= "inet" then error("EAFNOSUPPORT") end + if socktype ~= "stream" and socktype ~= "dgram" then error("EPROTOTYPE") end + + local sock = newSocket(domain, socktype) + local fdobj = socketToFd(sock) + local fd = kernel.vfs.newfd(fdobj) + return fd +end + +kernel.syscalls["bind"] = function(fd, address) + local task = kernel.currentTask + local fdobj = task.fd[fd] + if not fdobj or not fdobj.isSocket then error("ENOTSOCK") end + local sock = sockets[fdobj.sockId] + if not sock then error("EBADF") end + if sock.bound then error("EINVAL") end + + local parsed = parseAddress(address) + + if parsed.backend == "unix" then + local existing = unixSocks[parsed.path] + if existing then + if existing.state == "closed" then + unixSocks[parsed.path] = nil + else + error("EADDRINUSE") + end + end + sock.backend = "unix" + sock.address = parsed + sock.bound = true + sock.state = "bound" + unixSocks[parsed.path] = sock + + elseif parsed.backend == "rednet" then + ensureRednet() + sock.backend = "rednet" + sock.address = parsed + sock.bound = true + sock.state = "bound" + + elseif parsed.backend == "modem" then + local modem, side = getModem() + sock.backend = "modem" + sock.address = parsed + sock.modem = modem + sock.modemChannel = parsed.port + sock.bound = true + sock.state = "bound" + modem.open(parsed.port) + + else + error("EOPNOTSUPP: cannot bind to " .. parsed.backend .. " address") + end +end + +kernel.syscalls["listen"] = function(fd, backlog) + local task = kernel.currentTask + local fdobj = task.fd[fd] + if not fdobj or not fdobj.isSocket then error("ENOTSOCK") end + local sock = sockets[fdobj.sockId] + if not sock then error("EBADF") end + if not sock.bound then error("EDESTADDRREQ") end + sock.listening = true + sock.state = "listening" + sock.maxBacklog = backlog or 5 +end + +kernel.syscalls["accept"] = function(fd) + local task = kernel.currentTask + local fdobj = task.fd[fd] + if not fdobj or not fdobj.isSocket then error("ENOTSOCK") end + local sock = sockets[fdobj.sockId] + if not sock then error("EBADF") end + if not sock.listening then error("EINVAL") end + + local deadline = kernel.computer:time() + 30000 + while #sock.backlog == 0 do + pumpAll() + if kernel.computer:time() > deadline then error("ETIMEDOUT") end + coroutine.yield() + end + + local clientSock = table.remove(sock.backlog, 1) + local cfdobj = socketToFd(clientSock) + local newfd = kernel.vfs.newfd(cfdobj) + return newfd +end + +kernel.syscalls["connect"] = function(fd, address) + local task = kernel.currentTask + local fdobj = task.fd[fd] + if not fdobj or not fdobj.isSocket then error("ENOTSOCK") end + local sock = sockets[fdobj.sockId] + if not sock then error("EBADF") end + if sock.connected then error("EISCONN") end + + local parsed = parseAddress(address) + sock.address = parsed + sock.backend = parsed.backend + + if parsed.backend == "unix" then + local server = unixSocks[parsed.path] + if not server then error("ECONNREFUSED") end + if not server.listening then error("ECONNREFUSED") end + if #server.backlog >= (server.maxBacklog or 5) then error("ECONNREFUSED") end + + local serverPeer = newSocket("unix", sock.socktype) + serverPeer.backend = "unix" + serverPeer.connected = true + serverPeer.state = "connected" + serverPeer.peer = sock + + sock.peer = serverPeer + sock.connected = true + sock.state = "connected" + + table.insert(server.backlog, serverPeer) + + elseif parsed.backend == "rednet" then + ensureRednet() + sock.connected = true + sock.state = "connected" + + elseif parsed.backend == "modem" then + local modem, side = getModem() + local replyChannel = math.random(1024, 65534) + sock.modem = modem + sock.modemChannel = replyChannel + sock.connected = true + sock.state = "connected" + modem.open(replyChannel) + + elseif parsed.backend == "http" or parsed.backend == "https" then + sock.connected = true + sock.state = "connected" + + else + error("EAFNOSUPPORT") + end +end + +kernel.syscalls["send"] = function(fd, data) + local task = kernel.currentTask + local fdobj = task.fd[fd] + if not fdobj or not fdobj.isSocket then error("ENOTSOCK") end + local sock = sockets[fdobj.sockId] + if not sock then error("EBADF") end + return sockSend(sock, data) +end + +kernel.syscalls["recv"] = function(fd, maxlen, timeout_ms) + local task = kernel.currentTask + local fdobj = task.fd[fd] + if not fdobj or not fdobj.isSocket then error("ENOTSOCK") end + local sock = sockets[fdobj.sockId] + if not sock then error("EBADF") end + + local deadline = kernel.computer:time() + (timeout_ms or 10000) + while #sock.rxbuf == 0 do + pumpAll() + if #sock.rxbuf > 0 then break end + if sock.state == "closed" or sock.error then + if sock.error then error("ECONNRESET: " .. tostring(sock.error)) end + return "" + end + if kernel.computer:time() > deadline then return "" end + coroutine.yield() + end + + local item = table.remove(sock.rxbuf, 1) + local data = type(item) == "table" and (item.data or "") or tostring(item) + if maxlen and #data > maxlen then + table.insert(sock.rxbuf, 1, { data=data:sub(maxlen+1), from=item and item.from }) + data = data:sub(1, maxlen) + end + return data +end + +kernel.syscalls["sockshutdown"] = function(fd) + local task = kernel.currentTask + local fdobj = task.fd[fd] + if not fdobj or not fdobj.isSocket then error("ENOTSOCK") end + local sock = sockets[fdobj.sockId] + if sock then sockClose(sock) end +end + +kernel.syscalls["getpeername"] = function(fd) + local task = kernel.currentTask + local fdobj = task.fd[fd] + if not fdobj or not fdobj.isSocket then error("ENOTSOCK") end + local sock = sockets[fdobj.sockId] + if not sock or not sock.connected then error("ENOTCONN") end + if sock.address then return sock.address end + return nil +end + +kernel.syscalls["getsockname"] = function(fd) + local task = kernel.currentTask + local fdobj = task.fd[fd] + if not fdobj or not fdobj.isSocket then error("ENOTSOCK") end + local sock = sockets[fdobj.sockId] + if not sock then error("EBADF") end + return sock.address +end + +kernel.syscalls["httpget"] = function(url, headers) + local http = kernel.apis and kernel.apis.http + if not http then error("ENODEV: no http API") end + + local ok, err = pcall(http.request, url, nil, headers) + if not ok then error("ENETDOWN: " .. tostring(err)) end + + local deadline = kernel.computer:time() + 15000 + while true do + local ev = pollEvent() + if ev then + if ev[1] == "http_success" and ev[2] == url then + local handle = ev[3] + local body = handle.readAll and handle.readAll() or "" + handle.close() + return body + elseif ev[1] == "http_failure" and ev[2] == url then + error("ECONNREFUSED: " .. tostring(ev[3])) + else + dispatchEvent(ev) + end + end + if kernel.computer:time() > deadline then error("ETIMEDOUT") end + coroutine.yield() + end +end + +kernel.syscalls["resolve"] = function(hostname) + if hostname:match("^%d+%.%d+%.%d+%.%d+$") then return hostname end + + local a,b,c,d = hostname:match("^(%d+)%.(%d+)%.(%d+)%.(%d+)$") + if a and tonumber(a) == 0 and tonumber(b) == 0 then + return hostname + end + + local http = kernel.apis and kernel.apis.http + if not http then error("ENODEV: no http API for DNS") end + + local url = "https://cloudflare-dns.com/dns-query?name=" .. hostname .. "&type=A" + local body = kernel.syscalls["httpget"](url, { + ["Accept"] = "application/dns-json" + }) + + local ip = body:match('"type":1[^}]*"data":"([%d%.]+)"') + if not ip then error("ENOENT: could not resolve " .. hostname) end + return ip +end + +kernel.sockets = sockets +kernel.unixSockets = unixSocks + +kernel.log("Loaded socket module") diff --git a/Src/Hyperion-kernel/lib/modules/Hyperion/26_tty.kmod b/Src/Hyperion-kernel/lib/modules/Hyperion/26_tty.kmod index 3d1bcb1..ba120a2 100644 --- a/Src/Hyperion-kernel/lib/modules/Hyperion/26_tty.kmod +++ b/Src/Hyperion-kernel/lib/modules/Hyperion/26_tty.kmod @@ -1,6 +1,395 @@ ---:Minify:-- -local kernel=... -kernel.vfs.open("/dev/null", "r") -kernel.vfs.open("/dev/tty/TTY1", "w") -kernel.vfs.open("/dev/null", "w") -kernel.status="term" \ No newline at end of file +-- :Minify:-- +local kernel = ... +local apis = kernel.apis +local native = apis.peripheral +local sides = {"top", "bottom", "left", "right", "front", "back"} +local peripheral={} + +function peripheral.getNames() + local results = {} + for n = 1, #sides do + local side = sides[n] + if native.isPresent(side) then + table.insert(results, side) + if native.hasType(side, "peripheral_hub") then + local remote = native.call(side, "getNamesRemote") + for _, name in ipairs(remote) do + table.insert(results, name) + end + end + end + end + return results +end + +function peripheral.isPresent(name) + if native.isPresent(name) then + return true + end + + for n = 1, #sides do + local side = sides[n] + if native.hasType(side, "peripheral_hub") and native.call(side, "isPresentRemote", name) then + return true + end + end + return false +end + +function peripheral.getType(peripheral) + if type(peripheral) == "string" then + if native.isPresent(peripheral) then + return native.getType(peripheral) + end + for n = 1, #sides do + local side = sides[n] + if native.hasType(side, "peripheral_hub") and native.call(side, "isPresentRemote", peripheral) then + return native.call(side, "getTypeRemote", peripheral) + end + end + return nil + else + local mt = getmetatable(peripheral) + if not mt or mt.__name ~= "peripheral" or type(mt.types) ~= "table" then + error("bad argument #1 (table is not a peripheral)", 2) + end + return table.unpack(mt.types) + end +end + +function peripheral.hasType(peripheral, peripheral_type) + if type(peripheral) == "string" then + if native.isPresent(peripheral) then + return native.hasType(peripheral, peripheral_type) + end + for n = 1, #sides do + local side = sides[n] + if native.hasType(side, "peripheral_hub") and native.call(side, "isPresentRemote", peripheral) then + return native.call(side, "hasTypeRemote", peripheral, peripheral_type) + end + end + return nil + else + local mt = getmetatable(peripheral) + if not mt or mt.__name ~= "peripheral" or type(mt.types) ~= "table" then + error("bad argument #1 (table is not a peripheral)", 2) + end + return mt.types[peripheral_type] ~= nil + end +end + +function peripheral.getMethods(name) + if native.isPresent(name) then + return native.getMethods(name) + end + for n = 1, #sides do + local side = sides[n] + if native.hasType(side, "peripheral_hub") and native.call(side, "isPresentRemote", name) then + return native.call(side, "getMethodsRemote", name) + end + end + return nil +end + +function peripheral.getName(peripheral) + local mt = getmetatable(peripheral) + if not mt or mt.__name ~= "peripheral" or type(mt.name) ~= "string" then + error("bad argument #1 (table is not a peripheral)", 2) + end + return mt.name +end + +function peripheral.call(name, method, ...) + if native.isPresent(name) then + return native.call(name, method, ...) + end + + for n = 1, #sides do + local side = sides[n] + if native.hasType(side, "peripheral_hub") and native.call(side, "isPresentRemote", name) then + return native.call(side, "callRemote", name, method, ...) + end + end + return nil +end + +function peripheral.wrap(name) + local methods = peripheral.getMethods(name) + if not methods then + return nil + end + + local types = { peripheral.getType(name) } + for i = 1, #types do types[types[i]] = true end + local result = setmetatable({}, { + __name = "peripheral", + name = name, + type = types[1], + types = types, + }) + for _, method in ipairs(methods) do + result[method] = function(...) + return peripheral.call(name, method, ...) + end + end + return result +end + +function peripheral.find(ty, filter) + local results = {} + for _, name in ipairs(peripheral.getNames()) do + if peripheral.hasType(name, ty) then + local wrapped = peripheral.wrap(name) + if filter == nil or filter(name, wrapped) then + table.insert(results, wrapped) + end + end + end + return table.unpack(results) +end + +local icolors = { + [0x1] = 1, -- #000000 + [0x2] = 2, -- #FFFFFF + [0x4] = 3, -- #FF0000 + [0x8] = 4, -- #00FF00 + [0x10] = 5, -- #0000FF + [0x20] = 6, -- #00FFFF + [0x40] = 7, -- #FF00FF + [0x80] = 8, -- #FFFF00 + [0x100] = 9, -- #FF6D00 + [0x200] = 10, -- #6DFF55 + [0x400] = 11, -- #24FFFF + [0x800] = 12, -- #924900 + [0x1000] = 13, -- #6D6D55 + [0x2000] = 14, -- #DBDBAA + [0x4000] = 15, -- #6D00FF + [0x8000] = 16 -- #B6FF00 +} + +local colors = { + 0x0001, -- #000000 + 0x0002, -- #FFFFFF + 0x0004, -- #FF0000 + 0x0008, -- #00FF00 + 0x0010, -- #0000FF + 0x0020, -- #00FFFF + 0x0040, -- #FF00FF + 0x0080, -- #FFFF00 + 0x0100, -- #FF6D00 + 0x0200, -- #6DFF55 + 0x0400, -- #24FFFF + 0x0800, -- #924900 + 0x1000, -- #6D6D55 + 0x2000, -- #DBDBAA + 0x4000, -- #6D00FF + 0x8000 -- #B6FF00 +} + +local function write(text, term) + local x, y = term.getCursorPos() + local w, h = term.getSize() + + for i = 1, #text do + local c = text:sub(i, i) + + if c == "\n" then + y = y + 1 + x = 1 + elseif c == "\t" then + local tabSize = 4 + local spaces = tabSize - ((x - 1) % tabSize) + term.write(string.rep(" ", spaces)) + x = x + spaces + elseif c == "\b" then + if x > 1 then + x = x - 1 + term.setCursorPos(x, y) + term.write(" ") + term.setCursorPos(x, y) + end + else + if x <= w and y <= h then + term.setCursorPos(x, y) + term.write(c) + x = x + 1 + end + end + + if x > w then + x = 1 + y = y + 1 + end + + if y - 1 >= h then + term.scroll(1) + y = h + term.setCursorPos(x, y) + end + end + + term.setCursorPos(x, y) +end + +kernel.devfs.data.tty={} +local ctrl,alt = false, false + +local function serializeBool(bool) + if bool then + return "T" + else + return "F" + end +end + +local function newtty(obj, id, ev) + kernel.devfs.data["tty"][id] = function(op, mode) + if op=="type" then + return "character device" + elseif op=="open" then + local h = { + read=function(amount) + local rv="" + for i=1, amount or 1 do + local event = {ev()} + if event[1] then + rv=rv..event[1] + end + end + if rv=="" then rv=nil end + return rv + end, + write=function(content) + write(content, obj) + end, + size=function() + local s={obj.getSize()} + return table.concat(s,";") + end, + clear=function() + obj.clear() + obj.setCursorPos(1,1) + end, + gpos=function() + local s={obj.getCursorPos()} + return table.concat(s,";") + end, + spos=function(x,y) + return obj.setCursorPos(x,y) + end, + sfgc=function(c) + return obj.setTextColor(colors[c]) + end, + sbgc=function(c) + return obj.setBackgroundColor(colors[c]) + end, + gfgc=function() + return icolors[obj.getTextColor()] + end, + gbgc=function() + return icolors[obj.getBackgroundColor()] + end, + gctrl=function() + return serializeBool(ctrl)..";"..serializeBool(alt) + end + } + if mode=="rw" then + return h + elseif mode=="r" then + h["write"]=nil + return h + elseif mode=="w" then + h["read"]=nil + return h + end + end + end +end + +local fifo = kernel.newFifo() + +local ctrlLetterKeys = nil +local specialKeys = nil + +local function buildKeyMaps() + if ctrlLetterKeys then return end + local k = apis.keys + ctrlLetterKeys = {} + local letters = { + {k.a,1},{k.b,2},{k.c,3},{k.d,4},{k.e,5},{k.f,6},{k.g,7}, + {k.h,8}, {k.j,10},{k.k,11},{k.l,12},{k.m,13}, + {k.n,14},{k.o,15},{k.p,16}, + {k.u,21},{k.v,22},{k.w,23},{k.x,24},{k.y,25},{k.z,26}, + } + for _, pair in ipairs(letters) do + ctrlLetterKeys[pair[1]] = string.char(pair[2]) + end + specialKeys = { + [k.home] = "\1", + [k.delete] = "\4", + [k["end"]] = "\5", + [k.pageUp] = "\2", + [k.pageDown]= "\12", + } +end + +kernel.processes.cctmond = function() + local timeout = false + while true do + local event = {kernel.computer:getMachineEvent()} + + if event[1] then + local eventType = event[1] + local charOrKey = event[3] + + buildKeyMaps() + + if eventType == "keyPressed" then + if charOrKey == apis.keys.leftCtrl or charOrKey == apis.keys.rightCtrl then + ctrl = true + elseif charOrKey == apis.keys.leftAlt or charOrKey == apis.keys.rightAlt then + alt = true + end + + if ctrl and charOrKey == apis.keys.c then + for _, task in ipairs(syscall.getTasks()) do + syscall.sigsend(task, 1) + end + end + + if ctrl and ctrlLetterKeys[charOrKey] then + fifo.push(ctrlLetterKeys[charOrKey]) + end + + if specialKeys[charOrKey] then + fifo.push(specialKeys[charOrKey]) + end + elseif eventType == "keyReleased" then + if charOrKey == apis.keys.leftCtrl or charOrKey == apis.keys.rightCtrl then + ctrl = false + elseif charOrKey == apis.keys.leftAlt or charOrKey == apis.keys.rightAlt then + alt = false + end + elseif eventType == "keyTyped" then + if charOrKey then fifo.push(charOrKey) end + end + + timeout = false + else + timeout = true + end + + if timeout then + sleep(0.05) + end + end +end + +newtty(apis.term, "TTY1", fifo.pop) + + +for i,v in ipairs({peripheral.find("monitor")}) do + v.setTextScale(.5) + v.write("Initializing...") + newtty(v,"TTY"..tostring(i+1),function () end) +end \ No newline at end of file diff --git a/Src/Hyperion-kernel/lib/modules/Hyperion/40_auth.kmod b/Src/Hyperion-kernel/lib/modules/Hyperion/40_auth.kmod index f8b017b..111c79d 100644 --- a/Src/Hyperion-kernel/lib/modules/Hyperion/40_auth.kmod +++ b/Src/Hyperion-kernel/lib/modules/Hyperion/40_auth.kmod @@ -264,11 +264,13 @@ function auth.login(username, password) end kernel.currentUID = uid - if kernel.currentProcess then - kernel.currentProcess.uid = uid - kernel.currentProcess.euid = uid - kernel.currentProcess.gid = tonumber(entry[2]) or uid - kernel.currentProcess.egid = tonumber(entry[2]) or uid + + local _task = kernel.currentTask + if _task then + _task.uid = uid + _task.euid = uid + _task.gid = tonumber(entry[2]) or uid + _task.egid = tonumber(entry[2]) or uid end kernel.log("AUTH: login uid=" .. tostring(uid) .. " (" .. username .. ")") @@ -372,7 +374,7 @@ function auth.newUser(username, password, gid, homedir, shell) local uid = nextUID() gid = tonumber(gid) or uid homedir = homedir or ("/home/" .. username) - shell = shell or "/bin/sh" + shell = shell or "/bin/hysh" passwd[#passwd + 1] = { tostring(uid), @@ -436,11 +438,9 @@ function auth.deleteUser(uid) if not entry then return nil, "No such user" end local username = entry[3] - -- Remove from passwd for i, v in ipairs(passwd) do if tonumber(v[1]) == uid then table.remove(passwd, i); break end end - -- Remove from shadow for i, v in ipairs(shadow) do if tonumber(v[1]) == uid then table.remove(shadow, i); break end end @@ -463,7 +463,6 @@ function auth.lockUser(uid) local sEntry = getShadowByUID(uid) if not sEntry then return nil, "No shadow entry for uid" end - -- Prefix hash with ! to lock (standard Linux convention) if sEntry[3]:sub(1,1) ~= "!" then sEntry[3] = "!" .. sEntry[3] end @@ -567,9 +566,6 @@ function auth.setGID(uid, gid) return true end --- Elevate the calling task to targetUid after verifying targetUsername's password. --- This is the kernel-side primitive for su/sudo — it bypasses the kernel.uid==0 --- check in sys.setuid because the auth module itself is trusted kernel code. function auth.elevate(targetUsername, password) if type(targetUsername) ~= "string" or type(password) ~= "string" then return nil, "Authentication failure" @@ -593,7 +589,6 @@ function auth.elevate(targetUsername, password) return nil, "Authentication failure" end - -- Directly set the calling task's uid — trusted kernel path local task = kernel.currentTask local prevUid = task.uid task.uid = uid @@ -612,7 +607,7 @@ if kernel.syscalls then kernel.syscalls["setusername"] = auth.setUsername kernel.syscalls["newuser"] = auth.newUser kernel.syscalls["whoami"] = auth.whoami - kernel.syscalls["getuid"] = auth.getUID + kernel.syscalls["getuidbyname"]= auth.getUID kernel.syscalls["getpasswd"] = auth.getPasswd kernel.syscalls["elevate"] = auth.elevate kernel.syscalls["deleteuser"] = auth.deleteUser diff --git a/Src/Hyperion-kernel/lib/modules/Hyperion/45_hypervisor.kmod b/Src/Hyperion-kernel/lib/modules/Hyperion/45_hypervisor.kmod index e78f8ff..f59d3a1 100644 --- a/Src/Hyperion-kernel/lib/modules/Hyperion/45_hypervisor.kmod +++ b/Src/Hyperion-kernel/lib/modules/Hyperion/45_hypervisor.kmod @@ -1,305 +1,355 @@ --- :Minify:-- +--:Minify:-- local kernel = ... -local tasks = {} -local sys = {} +local tasks = {} +local sys = {} local nextpid = 2 kernel.exitMain = false -function sys.spawn(func, name, envars, args, tgid) +local function bit_is_set(num, bit) + return math.floor(num / (2 ^ bit)) % 2 == 1 +end + +local function loadExecutable(path, env) + kernel.vfs.access(path, "rx") + + local fd = kernel.vfs.open(path, "r") + local data = kernel.vfs.read(fd, 1024 * 1024 * 4) + kernel.vfs.close(fd) + + local func, err = load(data, "@" .. path, "t", env or kernel._U) + if not func then error("ENOEXEC: " .. tostring(err)) end + + local meta = kernel.vfs.lstat(path) + local suid_set = bit_is_set(meta.perms, 6) + local caller_uid = kernel.currentTask and kernel.currentTask.uid or kernel.uid + local euid = suid_set and meta.owner or caller_uid + + return func, euid, suid_set +end + +local function createTask(func, name, envars, args, tgid, real_uid, eff_uid) local id = nextpid - nextpid = nextpid + 1 + nextpid = nextpid + 1 tasks[tostring(id)] = { coro = coroutine.create(function() local ok, err = xpcall(func, debug.traceback, table.unpack(args or {})) - if not ok then - if kernel.config.logTaskExit then - kernel.log( - "Task " .. tostring(id) .. " exited with err: " .. - tostring(err), "ERROR", 2) - end - if type(err) == "number" then - tasks[tostring(id)].exit = err - end - else - if kernel.config.logTaskExit then - if err then - kernel.log("Task " .. tostring(id) .. - " exited with code: " .. tostring(err), - "INFO") - else - kernel.log("Task " .. tostring(id) .. - " exited without code", "INFO") - end - end - - if type(err) == "number" then - tasks[tostring(id)].exit = err + if kernel.config.logTaskExit then + if not ok then + kernel.log("Task " .. tostring(id) .. " exited with err: " .. tostring(err), "ERROR", 2) + elseif err then + kernel.log("Task " .. tostring(id) .. " exited with code: " .. tostring(err), "INFO") + else + kernel.log("Task " .. tostring(id) .. " exited without code", "INFO") end end - for v, _ in ipairs(tasks[tostring(id)].fd) do pcall(kernel.vfs.close,v) end - tasks[tostring(id)].status = "Z" + if type(err) == "number" then + tasks[tostring(id)].exit = err + end + + if tasks[tostring(id)].fd then + for fd, _ in pairs(tasks[tostring(id)].fd) do + pcall(kernel.vfs.close, fd) + end + end + tasks[tostring(id)].status = "Z" end), - name = name or ("task" .. tostring(id)), - envars = envars or kernel.currentTask.envars, - args = args or {}, - status = "R", - pid = id, - tgid = tgid or kernel.currentTask.tgid, - uid = kernel.uid, - fd = {}, - sleep = 0, - ivs = 0, - vs = 0, + + name = name or ("task" .. tostring(id)), + envars = envars or (kernel.currentTask and kernel.currentTask.envars or {}), + args = args or {}, + status = "R", + pid = id, + tgid = tgid or (kernel.currentTask and kernel.currentTask.tgid or id), + uid = real_uid, + euid = eff_uid, + gid = (kernel.currentTask and kernel.currentTask.gid) or 0, + groups = (kernel.currentTask and kernel.currentTask.groups) or {}, + fd = {}, + sleep = 0, + ivs = 0, + vs = 0, children = {}, - parent = kernel.currentTask, - siblings = kernel.currentTask.children, + parent = kernel.currentTask or kernel.kernelTask, + siblings = (kernel.currentTask and kernel.currentTask.children) or kernel.kernelTask.children, syscallReturn = {}, - cwd = kernel.currentTask.cwd, + cwd = (kernel.currentTask and kernel.currentTask.cwd) or "/", timeSlice = 0, - lastTime = 0, + lastTime = 0, totalTime = 0, - numRuns = 0 + numRuns = 0, } - table.insert(kernel.currentTask.children, tasks[tostring(id)]) + table.insert( + (kernel.currentTask and kernel.currentTask.children) or kernel.kernelTask.children, + tasks[tostring(id)] + ) return id end +function sys.spawn(func, name, envars, args, tgid) + local caller = kernel.currentTask + local real_uid = caller and caller.uid or kernel.uid + local eff_uid = caller and caller.euid or real_uid + return createTask(func, name, envars, args, tgid, real_uid, eff_uid) +end + +function sys.execspawn(path, name, envars, args, tgid) + local func, euid, suid_active = loadExecutable(path, kernel._U) + + local caller = kernel.currentTask + local real_uid = caller and caller.uid or kernel.uid + + if suid_active then + kernel.log( + "execspawn: suid exec '" .. path .. + "' caller_uid=" .. tostring(real_uid) .. + " -> euid=" .. tostring(euid), "INFO" + ) + end + + return createTask(func, name or path, envars, args, tgid, real_uid, euid) +end + +function sys.exec(path, args, envars) + local task = kernel.currentTask + local func, euid, _ = loadExecutable(path, kernel._U) + + if task.fd then + for fd, _ in pairs(task.fd) do + if fd > 2 then pcall(kernel.vfs.close, fd) end + end + end + + task.euid = euid + task.args = args or {} + task.envars = envars or task.envars + task.name = path + + task.coro = coroutine.create(function() + local ok, err = xpcall(func, debug.traceback, table.unpack(task.args)) + if kernel.config.logTaskExit then + if not ok then + kernel.log("Task " .. tostring(task.pid) .. " exec '" .. path .. "' err: " .. tostring(err), "ERROR", 2) + else + kernel.log("Task " .. tostring(task.pid) .. " exec '" .. path .. "' exited: " .. tostring(err), "INFO") + end + end + if type(err) == "number" then tasks[tostring(task.pid)].exit = err end + if tasks[tostring(task.pid)].fd then + for fd, _ in pairs(tasks[tostring(task.pid)].fd) do + pcall(kernel.vfs.close, fd) + end + end + tasks[tostring(task.pid)].status = "Z" + end) + task.syscallReturn = {} + coroutine.yield() +end + function sys.sleep(s) kernel.currentTask.status = "S" - kernel.currentTask.sleep = kernel.computer:time() + s * 1000 + kernel.currentTask.sleep = kernel.computer:time() + s * 1000 coroutine.yield() end function sys.getTask(pid) - if tasks[tostring(pid)] then - local task = tasks[tostring(pid)] - local children = {} - local siblings = {} + local task = tasks[tostring(pid)] + if not task then return nil end - for i, v in ipairs(task.children) do children[i] = v.pid end - for i, v in ipairs(task.siblings) do siblings[i] = v.pid end + local children, siblings = {}, {} + for i, v in ipairs(task.children) do children[i] = v.pid end + for i, v in ipairs(task.siblings) do siblings[i] = v.pid end - return { - name = task.name, - status = task.status, - pid = task.pid, - tgid = task.tgid, - username = kernel.users[task.uid], - uid = task.uid, - exit = task.exit, - sleep = task.sleep, - ivs = task.ivs, - vs = task.vs, - children = children, - siblings = siblings, - parent = task.parent.pid, - cwd = task.cwd, - term = task.term - } - end + return { + name = task.name, + status = task.status, + pid = task.pid, + tgid = task.tgid, + username = kernel.users[task.uid], + uid = task.uid, + euid = task.euid, + exit = task.exit, + sleep = task.sleep, + ivs = task.ivs, + vs = task.vs, + children = children, + siblings = siblings, + parent = task.parent.pid, + cwd = task.cwd, + term = task.term, + } end function sys.collect(pid) local children = {} - for i, v in ipairs(kernel.currentTask.children) do children[i] = v.pid end + for _, v in ipairs(kernel.currentTask.children) do children[#children+1] = v.pid end - if not tasks[tostring(pid)] then + local task = tasks[tostring(pid)] + if not task then return false, "Task does not exist" - - elseif not isEqualToAny(tasks[tostring(pid)].pid, table.unpack(children)) then + elseif not isEqualToAny(task.pid, table.unpack(children)) then return false, "You do not own this task" - - elseif tasks[tostring(pid)].status ~= "Z" then + elseif task.status ~= "Z" then return false, "Task must exit to collect status" - else - tasks[tostring(pid)].reapTime = 0 - return true, tasks[tostring(pid)].exit + task.reapTime = 0 + return true, task.exit end end function sys.kill(pid) - local children = {} - for i, v in ipairs(kernel.currentTask.children) do children[i] = v.pid end - - if not tasks[tostring(pid)] then + local task = tasks[tostring(pid)] + if not task then return false, "Task does not exist" - - elseif tasks[tostring(pid)].status == "Z" then + elseif task.status == "Z" then return false, "Task is already dead" - else - tasks[tostring(pid)].status = "Z" + task.status = "Z" return true end end function sys.stop(pid) - local children = {} - for i, v in ipairs(kernel.currentTask.children) do children[i] = v.pid end - - if not tasks[tostring(pid)] then + local task = tasks[tostring(pid)] + if not task then return false, "Task does not exist" - - elseif tasks[tostring(pid)].status ~= "R" then - return false, "Cannot stop non running task" - + elseif task.status ~= "R" then + return false, "Cannot stop non-running task" else - tasks[tostring(pid)].status = "T" + task.status = "T" return true end end function sys.continue(pid) - local children = {} - for i, v in ipairs(kernel.currentTask.children) do children[i] = v.pid end - if not tasks[tostring(pid)] then + local task = tasks[tostring(pid)] + if not task then return false, "Task does not exist" - - elseif tasks[tostring(pid)].status ~= "T" then + elseif task.status ~= "T" then return false, "Task is not stopped" - else - tasks[tostring(pid)].status = "R" + task.status = "R" return true end end -function sys.getpid() return kernel.currentTask.pid end - +function sys.getpid() return kernel.currentTask.pid end function sys.getppid() return kernel.currentTask.parent.pid end function sys.getTasks() local ret = {} - for i, v in pairs(tasks) do ret[#ret + 1] = v.pid end + for _, v in pairs(tasks) do ret[#ret+1] = v.pid end return ret end -function sys.getEnviron(key) return kernel.currentTask.envars[key] end - -function sys.setEnviron(key, value) kernel.currentTask.envars[key] = value end +function sys.getEnviron(key) return kernel.currentTask.envars[key] end +function sys.setEnviron(key, val) kernel.currentTask.envars[key] = val end function sys.exit(code) + local task = kernel.currentTask if kernel.config.logTaskExit then if code then - kernel.log("Task " .. tostring(kernel.currentTask.pid) .. " exited with code: " .. tostring(code), "INFO") + kernel.log("Task " .. tostring(task.pid) .. " exited with code: " .. tostring(code), "INFO") else - kernel.log("Task " .. tostring(kernel.currentTask.pid) .. " exited without code", "INFO") + kernel.log("Task " .. tostring(task.pid) .. " exited without code", "INFO") end end - - tasks[tostring(kernel.currentTask.pid)].status = "Z" + tasks[tostring(task.pid)].status = "Z" if type(code) == "number" then - tasks[tostring(kernel.currentTask.pid)].exit = code + tasks[tostring(task.pid)].exit = code end end function sys.setuid(uid) - if kernel.uid ~= 0 then error("EACCES") end - kernel.currentTask.uid = uid + local task = kernel.currentTask + if task.euid ~= 0 and task.uid ~= uid then + error("EPERM") + end + task.uid = uid + task.euid = uid + kernel.uid = uid +end + +function sys.geteuid() + return kernel.currentTask.euid end function sys.getuid() return kernel.currentTask.uid end -local sysc = kernel.syscalls -sysc["spawn"] = sys.spawn -sysc["sleep"] = sys.sleep -sysc["getTask"] = sys.getTask -sysc["collect"] = sys.collect -sysc["kill"] = sys.kill -sysc["stop"] = sys.stop -sysc["continue"] = sys.continue -sysc["getpid"] = sys.getpid -sysc["getppid"] = sys.getppid -sysc["getTasks"] = sys.getTasks -sysc["setEnviron"] = sys.setEnviron -sysc["getEnviron"] = sys.getEnviron -sysc["exit"] = sys.exit -sysc["setuid"] = sys.setuid -sysc["getuid"] = sys.getuid -kernel._G.sleep = function(...) coroutine.yield("syscall", "sleep", ...) end - local function reapDeadTasks() for pid, task in pairs(tasks) do if task.status == "Z" and not task.reapTime then - kernel.currentTask = task - kernel.uid = task.uid - kernel.process = task.name - task.coro = nil - task.ivs = nil - task.vs = nil - task.args = nil - task.envars = nil - task.cwd = nil - task.numRuns = nil - task.totalTime = nil - task.lastTime = nil - task.timeSlice = nil + task.coro = nil + task.ivs = nil + task.vs = nil + task.args = nil + task.envars = nil + task.cwd = nil + task.numRuns = nil + task.totalTime = nil + task.lastTime = nil + task.timeSlice = nil task.syscallReturn = nil - task.sleep = nil - task.fd = nil - task.reapTime = kernel.computer:time() + 30000 + task.sleep = nil + task.fd = nil + task.reapTime = kernel.computer:time() + 30000 - elseif task.reapTime and kernel.computer:time() > task.reapTime and - task.status == "Z" then + elseif task.reapTime and kernel.computer:time() > task.reapTime + and task.status == "Z" then for _, child in ipairs(task.children) do - child.parent = tasks["1"] + child.parent = tasks["1"] child.siblings = tasks["1"].children table.insert(tasks["1"].children, child) end - for i, sibling in ipairs(task.siblings) do if sibling.pid == task.pid then table.remove(task.siblings, i) break end end - tasks[pid] = nil end end end -local alpha = 0.85 -local C_target = 0.01 -local Tmin = 0.0005 -local Tmax = 0.5 +local alpha = 0.85 +local C_target = 0.01 +local Tmin = 0.0005 +local Tmax = 0.5 local lambda_budget = 0.08 -local lambda_clamp = 0.03 -local lambda_var = 0.02 -local k_min = 0.5 -local k_max = 0.5 -local B = 0.01 +local lambda_clamp = 0.03 +local lambda_var = 0.02 +local k_min = 0.5 +local k_max = 0.5 +local B = 0.01 function kernel.main() while not kernel.exitMain do - local N = 0 - local Tmin_hit = 0 - local Tmax_hit = 0 + local N = 0 + local Tmin_hit = 0 + local Tmax_hit = 0 local totalTaskTime = 0 - local taskTimes = {} + local taskTimes = {} for pid, task in pairs(tasks) do - if task.status == "S" then - if kernel.computer:time() >= task.sleep then - task.status = "R" - task.sleep = 0 - end + if task.status == "S" and kernel.computer:time() >= task.sleep then + task.status = "R" + task.sleep = 0 end + if task.status == "R" then kernel.currentTask = task - kernel.uid = task.uid + + kernel.uid = task.euid or task.uid kernel.process = task.name N = N + 1 - -- assign adaptive time slice task.timeSlice = math.min(Tmax, math.max(Tmin, B / (N ^ alpha))) - if task.sigq and #task.sigq~=0 and task.sigh then + if task.sigq and #task.sigq ~= 0 and task.sigh then local coro = coroutine.create(task.sigh) if kernel.config.preempt then coroutine.resumeWithTimeout(coro, task.timeSlice, table.remove(task.sigq, 1)) @@ -308,44 +358,31 @@ function kernel.main() end end - -- check for exit/stop - if task.status=="R" then - -- measure execution time + if task.status == "R" then local startTime = kernel.computer:time() local ret + if kernel.config.preempt then - ret = { - coroutine.resumeWithTimeout( - task.coro, - task.timeSlice, - table.unpack(task.syscallReturn) - ) - } + ret = { coroutine.resumeWithTimeout(task.coro, task.timeSlice, table.unpack(task.syscallReturn)) } else - ret = { - coroutine.resume( - task.coro, - table.unpack(task.syscallReturn) - ) - } + ret = { coroutine.resume(task.coro, table.unpack(task.syscallReturn)) } end local elapsed = kernel.computer:time() - startTime - task.lastTime = elapsed + task.lastTime = elapsed task.totalTime = (task.totalTime or 0) + elapsed - task.numRuns = (task.numRuns or 0) + 1 + task.numRuns = (task.numRuns or 0) + 1 - taskTimes[#taskTimes + 1] = elapsed + taskTimes[#taskTimes+1] = elapsed totalTaskTime = totalTaskTime + elapsed if elapsed <= Tmin then Tmin_hit = Tmin_hit + 1 end if elapsed >= Tmax then Tmax_hit = Tmax_hit + 1 end - -- handle task results if ret[1] == "error" or ret[1] == false then - kernel.log("processHandlerException: " .. ret[2], "ERROR", 2) + kernel.log("processHandlerException: " .. tostring(ret[2]), "ERROR", 2) task.status = "Z" - task.exit = "processHandlerException: " .. ret[2] + task.exit = "processHandlerException: " .. tostring(ret[2]) elseif ret[1] == "timeout" then task.ivs = task.ivs + 1 @@ -355,57 +392,36 @@ function kernel.main() task.vs = task.vs + 1 if ret[2] == "syscall" then - if kernel.syscalls[ret[3]] then + local scname = ret[3] + if kernel.syscalls[scname] then if kernel.config.debugSyscalls then - kernel.log("Task " .. task.pid .. " invoking syscall: " .. ret[3], "DBUG", 5) - + kernel.log("Task " .. task.pid .. " syscall: " .. scname, "DBUG", 5) for i = 4, #ret do - kernel.log(" inval[" .. tostring(i - 3) .. "] = " .. tostring(ret[i]), "DBUG", 5) + kernel.log(" inval[" .. (i-3) .. "] = " .. tostring(ret[i]), "DBUG", 5) end end - local sysret = { - xpcall(kernel.syscalls[ret[3]], debug.traceback, table.unpack(ret, 4)) - } + local sysret = { xpcall(kernel.syscalls[scname], debug.traceback, table.unpack(ret, 4)) } if kernel.config.debugSyscalls then if not sysret[1] then - kernel.log( - "Task " .. task.pid .. " syscall " .. ret[3] .. " failed: " .. tostring(sysret[2]), "ERROR", 2 - ) - + kernel.log("Task " .. task.pid .. " syscall " .. scname .. " failed: " .. tostring(sysret[2]), "ERROR", 2) else - kernel.log( - "Task " .. task.pid .. " syscall " .. ret[3] .. " completed returning " .. tostring(#sysret - 1) .. " values", "DBUG", 5 - ) - + kernel.log("Task " .. task.pid .. " syscall " .. scname .. " ok, " .. (#sysret-1) .. " retvals", "DBUG", 5) for i = 2, #sysret do - if type(sysret[i]) == "table" then - kernel.log( - " retval[" .. tostring(i - 1) .. "] = " .. table.serialize(sysret[i]),"DBUG", 5 - ) - - else - kernel.log( - " retval[" .. tostring(i - 1) .. "] = " .. tostring(sysret[i]), "DBUG", 5 - ) - end + local v = type(sysret[i]) == "table" and table.serialize(sysret[i]) or tostring(sysret[i]) + kernel.log(" retval[" .. (i-1) .. "] = " .. v, "DBUG", 5) end end end if not sysret[1] then - task.syscallReturn = {false, sysret[2]} - + task.syscallReturn = { false, sysret[2] } else - task.syscallReturn = { - true, table.unpack(sysret, 2) - } + task.syscallReturn = { true, table.unpack(sysret, 2) } end else - task.syscallReturn = { - false, "Unknown syscall: " .. tostring(ret[3]) - } + task.syscallReturn = { false, "Unknown syscall: " .. tostring(scname) } end end end @@ -415,24 +431,42 @@ function kernel.main() local T_prev_avg = (N > 0) and (totalTaskTime / N) or 0 local T_prev_var = 0 - for _, t in ipairs(taskTimes) do T_prev_var = T_prev_var + (t - T_prev_avg) ^ 2 end if N > 0 then T_prev_var = T_prev_var / N end if N > 0 then - local f_clamp = k_min * (Tmin_hit / N) - k_max * (Tmax_hit / N) - local B_budget = (C_target * (N ^ (alpha - 1))) / - math.max(T_prev_avg, 1e-8) - B = B + lambda_budget * (B_budget - B) + lambda_clamp * f_clamp - - lambda_var * T_prev_var + local f_clamp = k_min * (Tmin_hit / N) - k_max * (Tmax_hit / N) + local B_budget = (C_target * (N ^ (alpha - 1))) / math.max(T_prev_avg, 1e-8) + B = B + lambda_budget * (B_budget - B) + lambda_clamp * f_clamp - lambda_var * T_prev_var end - -- clean up dead tasks reapDeadTasks() end end +local sysc = kernel.syscalls +sysc["spawn"] = sys.spawn +sysc["execspawn"] = sys.execspawn +sysc["exec"] = sys.exec +sysc["sleep"] = sys.sleep +sysc["getTask"] = sys.getTask +sysc["collect"] = sys.collect +sysc["kill"] = sys.kill +sysc["stop"] = sys.stop +sysc["continue"] = sys.continue +sysc["getpid"] = sys.getpid +sysc["getppid"] = sys.getppid +sysc["getTasks"] = sys.getTasks +sysc["setEnviron"] = sys.setEnviron +sysc["getEnviron"] = sys.getEnviron +sysc["exit"] = sys.exit +sysc["setuid"] = sys.setuid +sysc["getuid"] = sys.getuid +sysc["geteuid"] = sys.geteuid + +kernel._G.sleep = function(...) coroutine.yield("syscall", "sleep", ...) end + kernel.tasks = tasks -kernel.hpv = sys +kernel.hpv = sys diff --git a/Src/Hyperion-kernel/lib/modules/Hyperion/90_init.kmod b/Src/Hyperion-kernel/lib/modules/Hyperion/90_init.kmod index f4d592d..2cfc4ef 100644 --- a/Src/Hyperion-kernel/lib/modules/Hyperion/90_init.kmod +++ b/Src/Hyperion-kernel/lib/modules/Hyperion/90_init.kmod @@ -3,8 +3,13 @@ local kernel = ... kernel.log("Loading init system...") kernel.log("InitPath: " .. kernel.config.initPath) +local initOk, initErr = pcall(kernel.vfs.access, kernel.config.initPath, "rx") +if not initOk then + kernel.PANIC("Init binary not executable: " .. kernel.config.initPath .. " (" .. tostring(initErr) .. ")") +end + local handle = kernel.vfs.open(kernel.config.initPath, "r") -local data = kernel.vfs.read(handle, 1024 * 1024 * 4) +local data = kernel.vfs.read(handle, 1024 * 1024 * 4) kernel.vfs.close(handle) local initFunc, err = load(data, "@sysinit", "t", kernel._U) @@ -20,27 +25,27 @@ kernel.tasks["1"] = { end end), - name = "sysinit", + name = "sysinit", status = "R", - pid = 1, - tgid = 1, - uid = 0, - fd = {}, + pid = 1, + tgid = 1, + uid = 0, + fd = {}, envars = {}, - args = {}, - exit = "", - sleep = 0, - ivs = 0, - vs = 0, + args = {}, + exit = "", + sleep = 0, + ivs = 0, + vs = 0, parent = kernel.kernelTask, siblings = kernel.kernelTask.children, children = {}, syscallReturn = {}, - cwd = "/", + cwd = "/", timeSlice = 0, - lastTime = 0, + lastTime = 0, totalTime = 0, - numRuns = 0 + numRuns = 0 } kernel.log("created init task with PID 1") diff --git a/Src/Hyperion-kernel/lib/modules/Hyperion/91_login.kmod b/Src/Hyperion-kernel/lib/modules/Hyperion/91_login.kmod index 7835c95..621f94b 100644 --- a/Src/Hyperion-kernel/lib/modules/Hyperion/91_login.kmod +++ b/Src/Hyperion-kernel/lib/modules/Hyperion/91_login.kmod @@ -1,16 +1,9 @@ ---:Minify:-- +-- :Minify:-- local kernel = ... --- It runs at uid 0 so it can call setuid() to drop privileges to the logged in user kernel.processes.login = function() - local handle = kernel.vfs.open("/bin/login", "r") - local text = kernel.vfs.read(handle, 1024 * 1024) - kernel.vfs.close(handle) - - local fn, err = load(text, "@/bin/login", "t", kernel._U) - if not fn then - kernel.log("Failed to load /bin/login: " .. tostring(err), "ERROR", 2) - return + local ok, err = pcall(syscall.execspawn, "/bin/login", "login") + if not ok then + kernel.log("Failed to exec /bin/login: " .. tostring(err), "ERROR", 2) end - fn() end diff --git a/Src/Hyperion-kernel/lib/modules/Hyperion/92_permissions.kmod b/Src/Hyperion-kernel/lib/modules/Hyperion/92_permissions.kmod index 2418746..9f1a90e 100644 --- a/Src/Hyperion-kernel/lib/modules/Hyperion/92_permissions.kmod +++ b/Src/Hyperion-kernel/lib/modules/Hyperion/92_permissions.kmod @@ -1,165 +1,142 @@ ---:Minify:-- +-- :Minify:-- local kernel = ... -local bit32 = require("bit32") -local bor = bit32.bor -local lshift = bit32.lshift +local P = kernel.vfs.P +local PERM = kernel.vfs.PERM --- bit 0 = everyone-write, bit 1 = everyone-read --- bit 2 = group-write, bit 3 = group-read --- bit 4 = owner-write, bit 5 = owner-read --- bit 6 = suid -local P_OWNER_R = lshift(1, 5) -local P_OWNER_W = lshift(1, 4) -local P_GROUP_R = lshift(1, 3) -local P_GROUP_W = lshift(1, 2) -local P_WORLD_R = lshift(1, 1) -local P_WORLD_W = lshift(1, 0) -local P_SUID = lshift(1, 6) - -local RW_R_R = bor(P_OWNER_R, P_OWNER_W, P_GROUP_R, P_WORLD_R) -- 644 / rw-r--r-- -local RWX_R_R = bor(P_OWNER_R, P_OWNER_W, P_GROUP_R, P_WORLD_R) -- 755 / rwxr--r-- -local RW_R__ = bor(P_OWNER_R, P_OWNER_W, P_GROUP_R) -- 640 / rw-r----- -local RW____ = bor(P_OWNER_R, P_OWNER_W) -- 600 / rw------- -local SUID_755 = bor(P_SUID, P_OWNER_R, P_OWNER_W, P_GROUP_R, P_WORLD_R) -- 4755 - -local function metaEntry(name, owner, group, perms) - return string.char(#name) .. name - .. string.char(owner, group, perms) - .. string.char(0) -end +local RW_R_R = P.OWNER_R + P.OWNER_W + P.GROUP_R + P.WORLD_R +local RWX_RX_RX = P.OWNER_R + P.OWNER_W + P.OWNER_X + + P.GROUP_R + P.GROUP_X + + P.WORLD_R + P.WORLD_X +local RW_R__ = P.OWNER_R + P.OWNER_W + P.GROUP_R +local RW____ = P.OWNER_R + P.OWNER_W +local RWXRWXRWX = PERM.RWXRWXRWX +local SUID_755 = PERM.SUID_755 +local META_VERSION = 0x01 local rootDisk = kernel.disks["$"] -local function writeMeta(dir, entries) - local diskDir = dir == "/" and "/" or dir - local path = (diskDir:sub(-1) == "/" and diskDir or diskDir .. "/") .. ".meta" - if path:sub(1,1) == "/" then path = path:sub(2) end - if path == "" then path = ".meta" end +local function makeEntry(name, etype, owner, group, perms, cmeta) + cmeta = cmeta or "" + local plo = perms % 256 + local phi = math.floor(perms / 256) % 256 + return string.char(#name) .. name + .. string.char(etype, owner, group, plo, phi) + .. string.char(#cmeta) .. cmeta +end - local data = "" +local function writeMeta(dir, entries) + local diskDir = dir + if diskDir:sub(1,1) == "/" then diskDir = diskDir:sub(2) end + local metaPath = (diskDir == "" and ".meta" or diskDir .. "/.meta") + + local data = string.char(META_VERSION) for _, e in ipairs(entries) do - data = data .. metaEntry(e[1], e[2], e[3], e[4]) + data = data .. makeEntry(e[1], e[2] or 0x00, e[3], e[4], e[5], e[6]) end local ok, err = pcall(function() - local f = rootDisk:open(path, "w") + local f = rootDisk:open(metaPath, "w") f.write(data) f.close() end) if not ok then - kernel.log("permissions: failed to write /" .. path .. ": " .. tostring(err), "WARN", 8) + kernel.log("permissions: failed to write " .. metaPath .. ": " .. tostring(err), "WARN", 8) end end +local REG = 0x00 + if rootDisk:fileExists(".meta") then kernel.log("Permissions already seeded, skipping.", "INFO") else kernel.log("Seeding filesystem permissions...", "INFO") + -- / writeMeta("/", { - {"bin", 0, 0, RWX_R_R}, - {"boot", 0, 0, RWX_R_R}, - {"dev", 0, 0, RWX_R_R}, - {"etc", 0, 0, RWX_R_R}, - {"home", 0, 0, RWX_R_R}, - {"lib", 0, 0, RWX_R_R}, - {"root", 0, 0, RW____ }, - {"sbin", 0, 0, RWX_R_R}, - {"tmp", 0, 0, bor(P_OWNER_R, P_OWNER_W, P_GROUP_R, P_GROUP_W, P_WORLD_R, P_WORLD_W)}, - {"usr", 0, 0, RWX_R_R}, - {"var", 0, 0, RWX_R_R}, + {"bin", REG, 0, 0, RWX_RX_RX}, + {"boot", REG, 0, 0, RWX_RX_RX}, + {"dev", REG, 0, 0, RWX_RX_RX}, + {"etc", REG, 0, 0, RWX_RX_RX}, + {"home", REG, 0, 0, RWX_RX_RX}, + {"lib", REG, 0, 0, RWX_RX_RX}, + {"root", REG, 0, 0, RW____ }, + {"sbin", REG, 0, 0, RWX_RX_RX}, + {"tmp", REG, 0, 0, RWXRWXRWX}, + {"usr", REG, 0, 0, RWX_RX_RX}, + {"var", REG, 0, 0, RWX_RX_RX}, }) + -- /bin writeMeta("/bin", { - {"cat", 0, 0, RWX_R_R}, - {"clear", 0, 0, RWX_R_R}, - {"echo", 0, 0, RWX_R_R}, - {"hfetch", 0, 0, RWX_R_R}, - {"hysh", 0, 0, RWX_R_R}, - {"hyshex", 0, 0, RWX_R_R}, - {"install", 0, 0, RWX_R_R}, - {"login", 0, 0, SUID_755}, - {"ls", 0, 0, RWX_R_R}, - {"lua", 0, 0, RWX_R_R}, - {"luaold", 0, 0, RWX_R_R}, - {"mkdir", 0, 0, RWX_R_R}, - {"ps", 0, 0, RWX_R_R}, - {"pwd", 0, 0, RWX_R_R}, - {"spm", 0, 0, RWX_R_R}, - {"su", 0, 0, SUID_755}, - {"sudo", 0, 0, SUID_755}, - {"sysdump", 0, 0, RWX_R_R}, - {"whoami", 0, 0, RWX_R_R}, - {"yes", 0, 0, RWX_R_R}, - {"startup", 0, 0, RWX_R_R}, + {"cat", REG, 0, 0, RWX_RX_RX}, + {"clear", REG, 0, 0, RWX_RX_RX}, + {"echo", REG, 0, 0, RWX_RX_RX}, + {"hfetch", REG, 0, 0, RWX_RX_RX}, + {"hysh", REG, 0, 0, RWX_RX_RX}, + {"hyshex", REG, 0, 0, RWX_RX_RX}, + {"install", REG, 0, 0, RWX_RX_RX}, + {"login", REG, 0, 0, SUID_755 }, + {"ls", REG, 0, 0, RWX_RX_RX}, + {"lua", REG, 0, 0, RWX_RX_RX}, + {"luaold", REG, 0, 0, RWX_RX_RX}, + {"mkdir", REG, 0, 0, RWX_RX_RX}, + {"ps", REG, 0, 0, RWX_RX_RX}, + {"pwd", REG, 0, 0, RWX_RX_RX}, + {"spm", REG, 0, 0, RWX_RX_RX}, + {"su", REG, 0, 0, SUID_755 }, + {"sudo", REG, 0, 0, SUID_755 }, + {"sysdump", REG, 0, 0, RWX_RX_RX}, + {"whoami", REG, 0, 0, RWX_RX_RX}, + {"yes", REG, 0, 0, RWX_RX_RX}, + {"startup", REG, 0, 0, RWX_RX_RX}, + {"ln", REG, 0, 0, RWX_RX_RX}, + {"readlink", REG, 0, 0, RWX_RX_RX}, }) writeMeta("/bin/startup", { - {"test.lua", 0, 0, RWX_R_R}, + {"test.lua", REG, 0, 0, RWX_RX_RX}, }) + -- /etc writeMeta("/etc", { - {"passwd", 0, 0, RW_R_R}, - {"shadow", 0, 0, RW____ }, - {"pam.d", 0, 0, RWX_R_R}, + {"passwd", REG, 0, 0, RW_R_R}, + {"shadow", REG, 0, 0, RW____}, + {"pam.d", REG, 0, 0, RWX_RX_RX}, }) writeMeta("/etc/pam.d", { - {"secret", 0, 0, RW____}, + {"secret", REG, 0, 0, RW____}, }) + -- /sbin writeMeta("/sbin", { - {"init.lua", 0, 0, RWX_R_R}, + {"init.lua", REG, 0, 0, RWX_RX_RX}, }) + -- /boot writeMeta("/boot", { - {"kernel.lua", 0, 0, RW_R_R}, - {"boot.cfg", 0, 0, RW_R_R}, - {"safeboot.cfg", 0, 0, RW_R_R}, - {"fstab", 0, 0, RW_R_R}, - {"initfs", 0, 0, RW_R_R}, - {"cct", 0, 0, RWX_R_R}, - {"oc", 0, 0, RWX_R_R}, + {"kernel.lua", REG, 0, 0, RW_R_R }, + {"boot.cfg", REG, 0, 0, RW_R_R }, + {"safeboot.cfg", REG, 0, 0, RW_R_R }, + {"fstab", REG, 0, 0, RW_R_R }, + {"initfs", REG, 0, 0, RW_R_R }, + {"cct", REG, 0, 0, RWX_RX_RX}, + {"oc", REG, 0, 0, RWX_RX_RX}, }) + -- /lib writeMeta("/lib", { - {"sys", 0, 0, RWX_R_R}, - {"modules", 0, 0, RWX_R_R}, - {"crypto", 0, 0, RWX_R_R}, - {"store", 0, 0, RWX_R_R}, - {"snip", 0, 0, RW_R_R}, - {"io", 0, 0, RW_R_R}, - {"bit32", 0, 0, RW_R_R}, + {"sys", REG, 0, 0, RWX_RX_RX}, + {"modules", REG, 0, 0, RWX_RX_RX}, + {"crypto", REG, 0, 0, RWX_RX_RX}, + {"store", REG, 0, 0, RWX_RX_RX}, + {"snip", REG, 0, 0, RW_R_R }, + {"io", REG, 0, 0, RW_R_R }, + {"bit32", REG, 0, 0, RW_R_R }, }) kernel.log("Filesystem permissions seeded.", "INFO") end --- TODO: move this to vfs.kmod -local _orig_open = kernel.vfs.open -kernel.vfs.open = function(path, mode) - local fd = _orig_open(path, mode) - if mode == "r" then - local task = kernel.currentTask - local fobj = task.fd[fd] - if fobj and fobj.meta then - local suid_set = bit32.extract(fobj.meta.perms, 6) == 1 - if suid_set then - fobj.suid_owner = fobj.meta.owner - end - end - end - return fd -end - -kernel.syscalls["fget_suid"] = function(fd) - local task = kernel.currentTask - local fobj = task and task.fd[fd] - if fobj and fobj.suid_owner then - return fobj.suid_owner - end - return nil -end - kernel.log("Permission module loaded.", "INFO") diff --git a/build.py b/build.py index e865e70..d8fceeb 100644 --- a/build.py +++ b/build.py @@ -23,7 +23,11 @@ import sys import shutil import argparse import subprocess +import hashlib +import random +import string from pathlib import Path +from typing import Union PROJECT_ROOT = Path(__file__).resolve().parent SRC_ROOT = PROJECT_ROOT / "Src" @@ -111,7 +115,7 @@ def has_minify_header(path: Path) -> bool: return False -def run_build(minify: bool, include_test: bool, arch: str | None, release: bool): +def run_build(minify: bool, include_test: bool, arch: Union[str, None], release: bool): clean() BUILD_ROOT.mkdir() @@ -136,9 +140,21 @@ def main(): help="Release build: eeprom placed as startup.lua (default)") parser.add_argument("--dev", dest="release", action="store_false", help="Dev build: boot.lua and eeprom copied unchanged") + parser.add_argument( + "--makeuser", metavar=("USERNAME", "PASSWORD"), nargs=2, action="append", + default=[], + help=( + "Pre-create a user on first boot (dev builds only). " + "May be specified multiple times. " + "Example: --makeuser root secretpass --makeuser alice alicepass" + ), + ) args = parser.parse_args() + if args.makeuser and args.release: + parser.error("--makeuser is only allowed with --dev builds") + if args.target == "clean": clean() return @@ -147,8 +163,63 @@ def main(): include_test = "test" in args.target run_build(minify=minify, include_test=include_test, arch=args.arch, release=args.release) + + if args.makeuser: + print("Injecting first-boot user setup ...") + inject_makeusers(args.makeuser, args.arch) + print() + print("Build complete.") +def _make_firstboot_kmod(users): + lines = [] + lines.append("local kernel = ...") + lines.append("local auth = kernel.auth") + lines.append("") + + for username, password in users: + u = username.replace("\\", "\\\\").replace("'", "\\'") + p = password.replace("\\", "\\\\").replace("'", "\\'") + + if username == "root": + lines.append("do") + lines.append(" local ok, err = auth.setPassword(0, '" + p + "')") + lines.append(" if ok then") + lines.append(" kernel.log('FIRSTBOOT: root password set')") + lines.append(" else") + lines.append(" kernel.log('FIRSTBOOT: root password error: ' .. tostring(err), 'ERROR')") + lines.append(" end") + lines.append("end") + else: + lines.append("do") + lines.append(" local uid, err = auth.newUser('" + u + "', '" + p + "')") + lines.append(" if uid then") + lines.append(" kernel.log('FIRSTBOOT: created user " + u + " uid=' .. tostring(uid))") + lines.append(" else") + lines.append(" kernel.log('FIRSTBOOT: failed to create user " + u + ": ' .. tostring(err), 'ERROR')") + lines.append(" end") + lines.append("end") + lines.append("") + + lines.append("do") + lines.append(" local ok, err = pcall(function()") + lines.append(" kernel.vfs.remove('/lib/modules/Hyperion/50_firstboot_users.kmod')") + lines.append(" end)") + lines.append(" if not ok then") + lines.append(" kernel.log('FIRSTBOOT: could not self-delete: ' .. tostring(err), 'WARN')") + lines.append(" end") + lines.append("end") + + return "\n".join(lines) + "\n" + + +def inject_makeusers(users, arch): + base = BUILD_ROOT / "$" if arch else BUILD_ROOT + kmod_path = base / "lib" / "modules" / "Hyperion" / "50_firstboot_users.kmod" + kmod_path.parent.mkdir(parents=True, exist_ok=True) + kmod_path.write_text(_make_firstboot_kmod(users), encoding="utf-8") + print(" Wrote first-boot user setup -> " + str(kmod_path.relative_to(BUILD_ROOT))) + if __name__ == "__main__": main() diff --git a/building.md b/building.md index 7219892..585f45e 100644 --- a/building.md +++ b/building.md @@ -53,12 +53,23 @@ Optional arguments: * **`--dev`** * Development mode - * Bootloader does not start automatically + * Bootloader does not start automatically. You must run `eeprom` in CraftOS to start Hyperion. * **`--release`** (default) * Release mode * Bootloader starts automatically + +* **`--makeuser username password`** + Makes a username upon startup. Only works for `--dev` builds. + + * `--makeuser root rootpass` + + Makes the root account already exist on first boot with rootpass as password + + * `--makeuser root rootpass --makeuser alice alicepass` + + Makes the root account and alice account already exist on first boot with defined passwords **Examples**