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**