forked from Hyperion/HyperionOS
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ab1e847d1c | |||
| 62a03bfe6b | |||
| e77a8b3636 | |||
| 6bb7f03a3e | |||
| 8798a2f4fe | |||
| a6d2f6dca7 | |||
| b015d5880a | |||
| 6694711423 | |||
| 40c97ca000 | |||
| dd2437d4af | |||
| d026cfbb03 | |||
| aad7efd055 |
@@ -1,24 +1,33 @@
|
|||||||
local args = {...}
|
local args = {...}
|
||||||
local name = syscall.getTask(syscall.getpid()).name
|
local name = syscall.getTask(syscall.getpid()).name
|
||||||
local fs = require("sys.fs")
|
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
|
if not args[1] then
|
||||||
print(name..": Cannot access '"..args[1].."': No such file.")
|
while true do
|
||||||
|
local content = syscall.read(0, 1024)
|
||||||
|
if not content or content == "" then break end
|
||||||
|
printInline(content)
|
||||||
|
end
|
||||||
|
print("")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local file = 0
|
for _, arg in ipairs(args) do
|
||||||
if args[1] then
|
local filePath = arg
|
||||||
file = syscall.open(filePath, "r")
|
if filePath:sub(1,1) ~= "/" then
|
||||||
end
|
filePath = syscall.getcwd().."/"..filePath
|
||||||
local content=""
|
end
|
||||||
while content~=nil or file == 0 do
|
|
||||||
content=syscall.read(file, 1024)
|
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)
|
printInline(content)
|
||||||
|
end
|
||||||
|
syscall.close(fd)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
syscall.close(file)
|
|
||||||
print("")
|
print("")
|
||||||
162
Src/Hyperion-bash/bin/chattr
Normal file
162
Src/Hyperion-bash/bin/chattr
Normal file
@@ -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)
|
||||||
117
Src/Hyperion-bash/bin/chgrp
Normal file
117
Src/Hyperion-bash/bin/chgrp
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
--: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
|
||||||
|
local msg = tostring(err)
|
||||||
|
if msg:find("EPERM") or msg:find("EACCES") then
|
||||||
|
msg = "operation not permitted (must be root)"
|
||||||
|
elseif msg:find("ENOENT") then
|
||||||
|
msg = "no such file or directory"
|
||||||
|
end
|
||||||
|
print(name .. ": cannot change group of '" .. path .. "': " .. msg)
|
||||||
|
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)
|
||||||
268
Src/Hyperion-bash/bin/chmod
Normal file
268
Src/Hyperion-bash/bin/chmod
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
--: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
|
||||||
|
local msg = tostring(cerr)
|
||||||
|
if msg:find("EACCES") or msg:find("EPERM") then
|
||||||
|
msg = "permission denied"
|
||||||
|
elseif msg:find("ENOENT") then
|
||||||
|
msg = "no such file or directory"
|
||||||
|
end
|
||||||
|
print(name .. ": cannot change permissions of '" .. path .. "': " .. msg)
|
||||||
|
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)
|
||||||
150
Src/Hyperion-bash/bin/chown
Normal file
150
Src/Hyperion-bash/bin/chown
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
--: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
|
||||||
|
local msg = tostring(err)
|
||||||
|
if msg:find("EPERM") or msg:find("EACCES") then
|
||||||
|
msg = "operation not permitted (must be root)"
|
||||||
|
elseif msg:find("ENOENT") then
|
||||||
|
msg = "no such file or directory"
|
||||||
|
end
|
||||||
|
print(name .. ": cannot change owner of '" .. path .. "': " .. msg)
|
||||||
|
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)
|
||||||
83
Src/Hyperion-bash/bin/chroot
Normal file
83
Src/Hyperion-bash/bin/chroot
Normal file
@@ -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
|
||||||
@@ -1,174 +1,205 @@
|
|||||||
--:Minify:--
|
--:Minify:--
|
||||||
-- help: display command reference with paged scrolling
|
|
||||||
|
|
||||||
local COMMANDS = {
|
local COMMANDS = {
|
||||||
-- {name, usage, description, {flags...}}
|
{ name="cd", usage="cd [dir]", desc="Change working directory. Use '-' to return to previous directory.", flags={} },
|
||||||
-- flags: {flag, desc}
|
{ 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={
|
||||||
name = "cat",
|
{"-a","Show hidden files (starting with .)"},
|
||||||
usage = "cat [file]",
|
{"-l","Long format: permissions, owner, group, size, mtime, name"},
|
||||||
desc = "Print file contents to stdout. Reads from stdin if no file given.",
|
{"-h","Human-readable file sizes (with -l)"},
|
||||||
flags = {}
|
{"--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 = "cd",
|
{"-name PAT", "Match filename against shell glob (* and ?)"},
|
||||||
usage = "cd [dir]",
|
{"-type f|d|l","Filter by file, directory, or symlink"},
|
||||||
desc = "Change working directory. Use '-' to return to previous directory.",
|
{"-maxdepth N","Descend at most N directory levels"},
|
||||||
flags = {}
|
{"-mindepth N","Skip entries shallower than N levels"},
|
||||||
},
|
{"-empty", "Match empty files or empty directories"},
|
||||||
{
|
}},
|
||||||
name = "clear",
|
{ name="cp", usage="cp [-rRp] SOURCE... DEST", desc="Copy files or directories.", flags={
|
||||||
usage = "clear",
|
{"-r,-R","Recurse into directories"},
|
||||||
desc = "Clear the terminal screen.",
|
{"-p", "Preserve permissions"},
|
||||||
flags = {}
|
{"--help","Display help and exit"},
|
||||||
},
|
}},
|
||||||
{
|
{ name="mv", usage="mv [-f] SOURCE... DEST", desc="Move or rename files and directories.", flags={
|
||||||
name = "echo",
|
{"-f", "Do not prompt before overwriting (default)"},
|
||||||
usage = "echo [text...]",
|
{"--help","Display help and exit"},
|
||||||
desc = "Print arguments to stdout.",
|
}},
|
||||||
flags = {}
|
{ 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"},
|
||||||
name = "hfetch",
|
{"--help","Display help and exit"},
|
||||||
usage = "hfetch",
|
}},
|
||||||
desc = "Display system information in a neofetch-style layout.",
|
{ name="touch", usage="touch FILE...", desc="Create an empty file, or no-op if it already exists.", flags={} },
|
||||||
flags = {}
|
{ name="mkdir", usage="mkdir <dir>", 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)"},
|
||||||
name = "id",
|
{"-f", "Remove existing destination before creating link"},
|
||||||
usage = "id [username]",
|
{"--help","Display help and exit"},
|
||||||
desc = "Print user identity (uid, gid). Defaults to current user.",
|
}},
|
||||||
flags = {}
|
{ 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"},
|
||||||
name = "login",
|
{"--help","Display help and exit"},
|
||||||
usage = "login",
|
}},
|
||||||
desc = "System login prompt. Launched automatically at boot.",
|
{ name="tail", usage="tail [-n N] [file...]", desc="Print the last N lines of each file (default 10).", flags={
|
||||||
flags = {}
|
{"-n N","Number of lines to print"},
|
||||||
},
|
{"--help","Display help and exit"},
|
||||||
{
|
}},
|
||||||
name = "ls",
|
{ name="wc", usage="wc [-lwc] [file...]", desc="Count lines, words, and bytes in files.", flags={
|
||||||
usage = "ls [-alh] [dir]",
|
{"-l","Print line count"},
|
||||||
desc = "List directory contents.",
|
{"-w","Print word count"},
|
||||||
flags = {
|
{"-c","Print byte count"},
|
||||||
{"-a", "Show hidden files (starting with .)"},
|
{"--help","Display help and exit"},
|
||||||
{"-l", "Long format: permissions, owner, size"},
|
}},
|
||||||
{"-h", "Human-readable file sizes"},
|
{ 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"},
|
||||||
name = "lsusers",
|
{"-l","Print only filenames that contain a match"},
|
||||||
usage = "lsusers",
|
{"-c","Print count of matching lines per file"},
|
||||||
desc = "List all user accounts with uid, gid, home, and shell.",
|
{"-r,-R","Recurse into directories"},
|
||||||
flags = {}
|
{"--help","Display help and exit"},
|
||||||
},
|
}},
|
||||||
{
|
{ name="sed", usage="sed 's/PAT/REPL/' [file...]", desc="Stream editor. Applies substitution commands to each line.", flags={} },
|
||||||
name = "lua",
|
{ name="sort", usage="sort [-rnu] [file...]", desc="Sort lines of text.", flags={
|
||||||
usage = "lua",
|
{"-r","Reverse the sort order"},
|
||||||
desc = "Interactive Lua REPL prompt.",
|
{"-n","Numeric sort"},
|
||||||
flags = {}
|
{"-u","Suppress duplicate lines"},
|
||||||
},
|
{"--help","Display help and exit"},
|
||||||
{
|
}},
|
||||||
name = "mkdir",
|
{ name="uniq", usage="uniq [-cdui] [input [output]]", desc="Filter adjacent duplicate lines.", flags={
|
||||||
usage = "mkdir <dir>",
|
{"-c","Prefix each line with its repetition count"},
|
||||||
desc = "Create a directory.",
|
{"-d","Print only lines that appear more than once"},
|
||||||
flags = {}
|
{"-u","Print only lines that appear exactly once"},
|
||||||
},
|
{"-i","Ignore case when comparing"},
|
||||||
{
|
{"--help","Display help and exit"},
|
||||||
name = "passwd",
|
}},
|
||||||
usage = "passwd [username]",
|
{ name="tee", usage="tee [-a] [file...]", desc="Copy stdin to stdout and to each FILE simultaneously.", flags={
|
||||||
desc = "Change a user password. Non-root must verify current password first.",
|
{"-a","Append to files instead of overwriting"},
|
||||||
flags = {}
|
{"--help","Display help and exit"},
|
||||||
},
|
}},
|
||||||
{
|
{ name="basename", usage="basename STRING [SUFFIX]", desc="Strip directory and optional suffix from a path.", flags={} },
|
||||||
name = "ps",
|
{ name="dirname", usage="dirname STRING...", desc="Strip the last component from a path.", flags={} },
|
||||||
usage = "ps",
|
{ name="readlink", usage="readlink [-fenq] file...", desc="Print the target of a symbolic link.", flags={
|
||||||
desc = "List running tasks with pid, user, name, and status.",
|
{"-f","Canonicalize: follow every symlink component"},
|
||||||
flags = {}
|
{"-e","Like -f but all components must exist"},
|
||||||
},
|
{"-n","Do not output trailing newline"},
|
||||||
{
|
{"--help","Display help and exit"},
|
||||||
name = "pwd",
|
}},
|
||||||
usage = "pwd",
|
{ name="stat", usage="stat file...", desc="Display file type, size, owner, group, and permissions.", flags={
|
||||||
desc = "Print current working directory.",
|
{"--help","Display help and exit"},
|
||||||
flags = {}
|
}},
|
||||||
},
|
{ 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"},
|
||||||
name = "su",
|
{"--help","Display help and exit"},
|
||||||
usage = "su [username]",
|
}},
|
||||||
desc = "Switch user. Defaults to root. Root can switch without a password.",
|
{ name="chown", usage="chown [-R] USER[:GROUP] file...", desc="Change file owner and/or group.", flags={
|
||||||
flags = {}
|
{"-R","Recurse into directories"},
|
||||||
},
|
{"--help","Display help and exit"},
|
||||||
{
|
}},
|
||||||
name = "sudo",
|
{ name="chgrp", usage="chgrp [-R] GROUP file...", desc="Change file group ownership.", flags={
|
||||||
usage = "sudo [-u user] <command> [args...]",
|
{"-R","Recurse into directories"},
|
||||||
desc = "Run a command as another user (default root). Authenticates as current user.",
|
{"--help","Display help and exit"},
|
||||||
flags = {
|
}},
|
||||||
{"-u user", "Run as the specified user (name or uid)"},
|
{ 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 = "sysdump",
|
{ name="ps", usage="ps", desc="List running tasks with pid, user, name, and status.", flags={} },
|
||||||
usage = "sysdump",
|
{ name="hostname", usage="hostname [NAME]", desc="Print or set the system hostname.", flags={} },
|
||||||
desc = "List all registered kernel syscalls.",
|
{ name="uname", usage="uname [-asnrm]", desc="Print system information (OS name, hostname, release, machine).", flags={
|
||||||
flags = {}
|
{"-a","Print all fields"},
|
||||||
},
|
{"-s","Kernel name"},
|
||||||
{
|
{"-n","Node hostname"},
|
||||||
name = "useradd",
|
{"-r","Kernel release"},
|
||||||
usage = "useradd [-p pw] [-g gid] [-d home] [-s shell] [-M] <username>",
|
{"-m","Machine hardware name"},
|
||||||
desc = "Create a new user account.",
|
{"--help","Display help and exit"},
|
||||||
flags = {
|
}},
|
||||||
{"-p pw", "Set password (prompted interactively if omitted)"},
|
{ 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] <user>", desc="Create a new user account.", flags={
|
||||||
|
{"-p pw", "Set password"},
|
||||||
{"-g gid", "Set primary group id"},
|
{"-g gid", "Set primary group id"},
|
||||||
{"-d home", "Set home directory (default: /home/username)"},
|
{"-d home", "Set home directory (default /home/username)"},
|
||||||
{"-s shell", "Set login shell (default: /bin/hysh)"},
|
{"-s shell","Set login shell (default /bin/hysh)"},
|
||||||
{"-M", "Do not create home directory"},
|
{"-M", "Do not create home directory"},
|
||||||
}
|
}},
|
||||||
},
|
{ name="userdel", usage="userdel [-r] <user>", desc="Delete a user account.", flags={
|
||||||
{
|
{"-r","Also remove the user's home directory"},
|
||||||
name = "userdel",
|
}},
|
||||||
usage = "userdel [-r] <username>",
|
{ name="usermod", usage="usermod [-l name] [-p pw] [-g gid] [-d home] [-s shell] [-LU] <user>", desc="Modify an existing user account.", flags={
|
||||||
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] <username>",
|
|
||||||
desc = "Modify an existing user account.",
|
|
||||||
flags = {
|
|
||||||
{"-l name", "Rename the user"},
|
{"-l name", "Rename the user"},
|
||||||
{"-p pw", "Set new password"},
|
{"-p pw", "Set new password"},
|
||||||
{"-g gid", "Change primary group id"},
|
{"-g gid", "Change primary group id"},
|
||||||
{"-d home", "Change home directory"},
|
{"-d home", "Change home directory"},
|
||||||
{"-s shell", "Change login shell"},
|
{"-s shell","Change login shell"},
|
||||||
{"-L", "Lock the account (disable login)"},
|
{"-L", "Lock the account"},
|
||||||
{"-U", "Unlock 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 = "whoami",
|
{ name="su", usage="su [username]", desc="Switch user. Defaults to root. Root can switch without a password.", flags={} },
|
||||||
usage = "whoami",
|
{ name="sudo", usage="sudo [-u user] CMD [args...]", desc="Run a command as another user (default root).", flags={
|
||||||
desc = "Print the current username.",
|
{"-u user","Run as the specified user (name or uid)"},
|
||||||
flags = {}
|
}},
|
||||||
},
|
{ 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 = "yes",
|
{ name="help", usage="help [command]", desc="Display this command reference. Pass a command name to filter.", flags={} },
|
||||||
usage = "yes [text]",
|
{ name="lua", usage="lua", desc="Interactive Lua REPL prompt.", flags={} },
|
||||||
desc = "Repeatedly print 'y' (or given text) until interrupted with Ctrl+C.",
|
{ name="micro", usage="micro [file]", desc="Full-screen terminal text editor.", flags={} },
|
||||||
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
|
do
|
||||||
local C_HEAD = 7 -- yellow (section headers)
|
local seen = {}
|
||||||
local C_CMD = 5 -- cyan (command name)
|
local deduped = {}
|
||||||
local C_USAGE = 1 -- white (usage line)
|
for _, cmd in ipairs(COMMANDS) do
|
||||||
local C_DESC = 13 -- grey (description)
|
if not seen[cmd.name] then
|
||||||
local C_FLAG = 3 -- green (flag name)
|
seen[cmd.name] = true
|
||||||
local C_DIM = 12 -- dark grey
|
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 lines = {}
|
||||||
local function push(text, col) lines[#lines+1] = {text, col or 1} end
|
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)
|
push("", 1)
|
||||||
|
|
||||||
local args = {...}
|
local args = {...}
|
||||||
local filter = args[1] -- optional: help <command>
|
local filter = args[1]
|
||||||
|
|
||||||
local function addCmd(cmd)
|
local function addCmd(cmd)
|
||||||
push(cmd.name, C_CMD)
|
push(cmd.name, C_CMD)
|
||||||
@@ -207,11 +238,10 @@ else
|
|||||||
for _, cmd in ipairs(COMMANDS) do addCmd(cmd) end
|
for _, cmd in ipairs(COMMANDS) do addCmd(cmd) end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Pager
|
|
||||||
local sizeStr = syscall.devctl(1, "size")
|
local sizeStr = syscall.devctl(1, "size")
|
||||||
local screenW = tonumber(sizeStr:sub(1, sizeStr:find(";")-1)) or 51
|
local screenW = tonumber(sizeStr:match("^(%d+)")) or 51
|
||||||
local screenH = tonumber(sizeStr:sub(sizeStr:find(";")+1)) or 19
|
local screenH = tonumber(sizeStr:match(";(%d+)")) or 19
|
||||||
local pageSize = screenH - 2 -- reserve bottom row for status bar
|
local pageSize = screenH - 2
|
||||||
|
|
||||||
local scroll = 0
|
local scroll = 0
|
||||||
local totalLines = #lines
|
local totalLines = #lines
|
||||||
@@ -225,14 +255,12 @@ local function render()
|
|||||||
if li <= totalLines then
|
if li <= totalLines then
|
||||||
local text, col = lines[li][1], lines[li][2]
|
local text, col = lines[li][1], lines[li][2]
|
||||||
syscall.devctl(1, "sfgc", col)
|
syscall.devctl(1, "sfgc", col)
|
||||||
-- truncate to screen width
|
|
||||||
if #text > screenW then text = text:sub(1, screenW) end
|
if #text > screenW then text = text:sub(1, screenW) end
|
||||||
syscall.write(1, text .. "\n")
|
syscall.write(1, text .. "\n")
|
||||||
else
|
else
|
||||||
syscall.write(1, "\n")
|
syscall.write(1, "\n")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
-- status bar
|
|
||||||
syscall.devctl(1, "sfgc", 16)
|
syscall.devctl(1, "sfgc", 16)
|
||||||
syscall.devctl(1, "sbgc", 13)
|
syscall.devctl(1, "sbgc", 13)
|
||||||
local pct = math.floor(math.min(100, (scroll + pageSize) / totalLines * 100))
|
local pct = math.floor(math.min(100, (scroll + pageSize) / totalLines * 100))
|
||||||
@@ -246,7 +274,6 @@ local function render()
|
|||||||
dirty = false
|
dirty = false
|
||||||
end
|
end
|
||||||
|
|
||||||
-- If output fits on one screen, just print without pager
|
|
||||||
if totalLines <= pageSize then
|
if totalLines <= pageSize then
|
||||||
syscall.devctl(1, "clear")
|
syscall.devctl(1, "clear")
|
||||||
syscall.devctl(1, "spos", 1, 1)
|
syscall.devctl(1, "spos", 1, 1)
|
||||||
@@ -262,30 +289,20 @@ render()
|
|||||||
while true do
|
while true do
|
||||||
local ch = syscall.read(0)
|
local ch = syscall.read(0)
|
||||||
if not ch or ch == "" then
|
if not ch or ch == "" then
|
||||||
-- idle
|
|
||||||
elseif ch == "q" or ch == "Q" then
|
elseif ch == "q" or ch == "Q" then
|
||||||
break
|
break
|
||||||
elseif ch == "\17" then -- up arrow
|
elseif ch == "\17" then
|
||||||
if scroll > 0 then
|
if scroll > 0 then scroll = scroll - 1; dirty = true end
|
||||||
scroll = scroll - 1
|
elseif ch == "\18" then
|
||||||
dirty = true
|
if scroll + pageSize < totalLines then scroll = scroll + 1; dirty = true end
|
||||||
end
|
elseif ch == "\19" then
|
||||||
elseif ch == "\18" then -- down arrow
|
scroll = math.max(0, scroll - pageSize); dirty = true
|
||||||
if scroll + pageSize < totalLines then
|
elseif ch == "\20" then
|
||||||
scroll = scroll + 1
|
scroll = math.min(totalLines - pageSize, scroll + pageSize); dirty = true
|
||||||
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
|
|
||||||
end
|
end
|
||||||
if dirty then render() end
|
if dirty then render() end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Restore screen
|
|
||||||
syscall.devctl(1, "clear")
|
syscall.devctl(1, "clear")
|
||||||
syscall.devctl(1, "spos", 1, 1)
|
syscall.devctl(1, "spos", 1, 1)
|
||||||
syscall.devctl(1, "sfgc", 1)
|
syscall.devctl(1, "sfgc", 1)
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ local host_str = syscall.getHost() or "Unknown"
|
|||||||
local cc_ver = host_str:match("ComputerCraft ([%d%.]+)") or host_str
|
local cc_ver = host_str:match("ComputerCraft ([%d%.]+)") or host_str
|
||||||
|
|
||||||
local info = {
|
local info = {
|
||||||
-- {label, value} label=nil means print value as-is (userhost / separator)
|
|
||||||
{nil, userhost},
|
{nil, userhost},
|
||||||
{nil, string.rep("-", #userhost)},
|
{nil, string.rep("-", #userhost)},
|
||||||
{"OS", syscall.version() or "Unknown"},
|
{"OS", syscall.version() or "Unknown"},
|
||||||
@@ -42,7 +41,7 @@ local info = {
|
|||||||
{"Uptime", formatUptime(syscall.getUptime() or 0)},
|
{"Uptime", formatUptime(syscall.getUptime() or 0)},
|
||||||
{"Tasks", tostring(#(syscall.getTasks() or {}))},
|
{"Tasks", tostring(#(syscall.getTasks() or {}))},
|
||||||
{"Shell", syscall.getEnviron("SHELL") or "Unknown"},
|
{"Shell", syscall.getEnviron("SHELL") or "Unknown"},
|
||||||
{"Terminal", "TTY1"},
|
{"Terminal", "tty1"},
|
||||||
{"UID", tostring(syscall.getuid())},
|
{"UID", tostring(syscall.getuid())},
|
||||||
{"Packages", "n/a (spm)"},
|
{"Packages", "n/a (spm)"},
|
||||||
}
|
}
|
||||||
@@ -71,15 +70,12 @@ local lines = math.max(#logo, #info)
|
|||||||
for i = 1, lines do
|
for i = 1, lines do
|
||||||
local logo_str = logo[i] or string.rep(" ", 36)
|
local logo_str = logo[i] or string.rep(" ", 36)
|
||||||
|
|
||||||
-- print logo segment in cyan
|
|
||||||
c(C_LOGO)
|
c(C_LOGO)
|
||||||
printInline(logo_str)
|
printInline(logo_str)
|
||||||
|
|
||||||
-- print separator pipe
|
|
||||||
c(C_LABEL)
|
c(C_LABEL)
|
||||||
printInline("| ")
|
printInline("| ")
|
||||||
|
|
||||||
-- print info segment
|
|
||||||
local row = info[i]
|
local row = info[i]
|
||||||
if row then
|
if row then
|
||||||
if row[1] == nil and i == 1 then
|
if row[1] == nil and i == 1 then
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
--:Minify:--
|
--:Minify:--
|
||||||
syscall.open("/dev/tty/TTY1","r")
|
syscall.open("/dev/tty/tty1","r")
|
||||||
syscall.open("/dev/tty/TTY1","w")
|
syscall.open("/dev/tty/tty1","w")
|
||||||
syscall.open("/dev/null","r")
|
syscall.open("/dev/null","r")
|
||||||
syscall.devctl(1,"clear")
|
syscall.devctl(1,"clear")
|
||||||
syscall.devctl(1,"sfgc",1)
|
syscall.devctl(1,"sfgc",1)
|
||||||
@@ -57,8 +57,8 @@ while true do
|
|||||||
printInline("> ")
|
printInline("> ")
|
||||||
end
|
end
|
||||||
proc = syscall.spawn(function(...)
|
proc = syscall.spawn(function(...)
|
||||||
syscall.open("/dev/tty/TTY1","r")
|
syscall.open("/dev/tty/tty1","r")
|
||||||
syscall.open("/dev/tty/TTY1","w")
|
syscall.open("/dev/tty/tty1","w")
|
||||||
syscall.open("/dev/null","w")
|
syscall.open("/dev/null","w")
|
||||||
program(...)
|
program(...)
|
||||||
end, path, nil, {table.unpack(split, 2)})
|
end, path, nil, {table.unpack(split, 2)})
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ local args = {...}
|
|||||||
local uid
|
local uid
|
||||||
|
|
||||||
if args[1] then
|
if args[1] then
|
||||||
uid = syscall.auth_getuid(args[1])
|
uid = syscall.getuid(args[1])
|
||||||
if not uid then
|
if not uid then
|
||||||
print("id: user '" .. args[1] .. "' does not exist")
|
print("id: user '" .. args[1] .. "' does not exist")
|
||||||
syscall.exit(1); return
|
syscall.exit(1); return
|
||||||
@@ -12,7 +12,7 @@ else
|
|||||||
uid = syscall.getuid()
|
uid = syscall.getuid()
|
||||||
end
|
end
|
||||||
|
|
||||||
local pwent = syscall.auth_getpasswd(uid)
|
local pwent = syscall.getpasswd(uid)
|
||||||
local name = (pwent and pwent.username) or tostring(uid)
|
local name = (pwent and pwent.username) or tostring(uid)
|
||||||
local gid = (pwent and pwent.gid) or uid
|
local gid = (pwent and pwent.gid) or uid
|
||||||
|
|
||||||
|
|||||||
96
Src/Hyperion-bash/bin/ln
Normal file
96
Src/Hyperion-bash/bin/ln
Normal file
@@ -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
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
--:Minify:--
|
--:Minify:--
|
||||||
syscall.open("/dev/tty/TTY1", "r") --stdin (fd 0)
|
syscall.open("/dev/tty/tty1", "r") --stdin (fd 0)
|
||||||
syscall.open("/dev/tty/TTY1", "w") --stdout (fd 1)
|
syscall.open("/dev/tty/tty1", "w") --stdout (fd 1)
|
||||||
syscall.open("/dev/null", "w") --stderr (fd 2)
|
syscall.open("/dev/null", "w") --stderr (fd 2)
|
||||||
|
|
||||||
local fs = require("sys.fs")
|
|
||||||
|
|
||||||
local MAX_ATTEMPTS = 3
|
local MAX_ATTEMPTS = 3
|
||||||
|
|
||||||
@@ -12,7 +11,6 @@ local function readLine(mask)
|
|||||||
while true do
|
while true do
|
||||||
local ch = syscall.read(0)
|
local ch = syscall.read(0)
|
||||||
if not ch or ch == "" then
|
if not ch or ch == "" then
|
||||||
-- buffer empty, spin
|
|
||||||
elseif ch == "\n" then
|
elseif ch == "\n" then
|
||||||
syscall.write(1, "\n")
|
syscall.write(1, "\n")
|
||||||
return input
|
return input
|
||||||
@@ -29,7 +27,12 @@ local function readLine(mask)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function firstBoot()
|
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
|
if shadow:match("%S") then return end
|
||||||
|
|
||||||
syscall.devctl(1, "clear")
|
syscall.devctl(1, "clear")
|
||||||
@@ -54,7 +57,7 @@ local function firstBoot()
|
|||||||
syscall.write(1, "Password too short (minimum 6 characters).\n\n")
|
syscall.write(1, "Password too short (minimum 6 characters).\n\n")
|
||||||
syscall.devctl(1, "sfgc", 1)
|
syscall.devctl(1, "sfgc", 1)
|
||||||
else
|
else
|
||||||
local ok, err = syscall.auth_setpassword(0, pw1)
|
local ok, err = syscall.setpassword(0, pw1)
|
||||||
if ok then
|
if ok then
|
||||||
syscall.devctl(1, "sfgc", 3)
|
syscall.devctl(1, "sfgc", 3)
|
||||||
syscall.write(1, "Root password set.\n\n")
|
syscall.write(1, "Root password set.\n\n")
|
||||||
@@ -71,35 +74,39 @@ local function firstBoot()
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function spawnShell(username, uid, shell, homedir)
|
local function spawnShell(username, uid, shell, homedir)
|
||||||
local shellText = fs.readAllText(shell)
|
local existsOk, existsErr = pcall(syscall.exists, shell)
|
||||||
if not shellText then
|
if not existsOk or not existsErr then
|
||||||
syscall.write(1, "login: shell not found: " .. shell .. "\n")
|
syscall.write(1, "login: shell not found: " .. shell .. "\n")
|
||||||
sleep(2)
|
sleep(2)
|
||||||
return false
|
return false
|
||||||
end
|
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("HOME", homedir)
|
||||||
syscall.setEnviron("USER", username)
|
syscall.setEnviron("USER", username)
|
||||||
syscall.setEnviron("SHELL", shell)
|
syscall.setEnviron("SHELL", shell)
|
||||||
syscall.setEnviron("PATH", "/bin/")
|
syscall.setEnviron("PATH", "/bin/")
|
||||||
|
|
||||||
local shellFn, loadErr = load(shellText, "@" .. shell)
|
local setuidOk, setuidErr = pcall(syscall.setuid, uid)
|
||||||
if not shellFn then
|
if not setuidOk then
|
||||||
syscall.log("login: shell load error: " .. tostring(loadErr), "ERROR")
|
syscall.write(1, "login: setuid failed: " .. tostring(setuidErr) .. "\n")
|
||||||
syscall.exit(-1)
|
sleep(2)
|
||||||
return
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
local ok, runErr = xpcall(shellFn, debug.traceback)
|
local chdirOk, chdirErr = pcall(syscall.chdir, homedir)
|
||||||
if not ok then
|
if not chdirOk then
|
||||||
syscall.log("login: shell runtime error: " .. tostring(runErr), "ERROR")
|
pcall(syscall.chdir, "/")
|
||||||
end
|
end
|
||||||
end, username .. ":shell")
|
|
||||||
|
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
|
||||||
|
|
||||||
syscall.exit(0)
|
syscall.exit(0)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -124,10 +131,11 @@ local function doLogin()
|
|||||||
syscall.write(1, "Password: ")
|
syscall.write(1, "Password: ")
|
||||||
local password = readLine("*")
|
local password = readLine("*")
|
||||||
|
|
||||||
local ok, err = syscall.auth_login(username, password)
|
local ok, err = syscall.login(username, password)
|
||||||
if ok then
|
if ok then
|
||||||
local uid = syscall.auth_getuid(username)
|
local uid = syscall.getuid()
|
||||||
local pwent = uid and syscall.auth_getpasswd(uid)
|
local pwent = syscall.getpasswd(uid)
|
||||||
|
|
||||||
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 "/"
|
local homedir = (pwent and pwent.homedir) or "/"
|
||||||
|
|
||||||
|
|||||||
157
Src/Hyperion-bash/bin/loimgcreate
Normal file
157
Src/Hyperion-bash/bin/loimgcreate
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
--:Minify:--
|
||||||
|
-- Usage:
|
||||||
|
-- loimgcreate <srcdir> <image.hfs> create image from directory
|
||||||
|
-- loimgcreate -x <image.hfs> <dest> 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.." <srcdir> <image.hfs>")
|
||||||
|
print(" "..name.." -x <image.hfs> <destdir>")
|
||||||
|
print("")
|
||||||
|
print("Pack a directory into a portable HFS image file, or extract one.")
|
||||||
|
print("")
|
||||||
|
print(" <srcdir> <image.hfs> recursively pack srcdir into image.hfs")
|
||||||
|
print(" -x <image.hfs> <dest> 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 <image.hfs> and <destdir>")
|
||||||
|
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 <srcdir> and <image.hfs>")
|
||||||
|
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)
|
||||||
427
Src/Hyperion-bash/bin/looptest
Normal file
427
Src/Hyperion-bash/bin/looptest
Normal file
@@ -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
|
||||||
129
Src/Hyperion-bash/bin/losetup
Normal file
129
Src/Hyperion-bash/bin/losetup
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
--:Minify:--
|
||||||
|
-- Usage:
|
||||||
|
-- losetup <path> attach directory or .hfs image; print loop id
|
||||||
|
-- losetup -d <id> detach loop device
|
||||||
|
-- losetup -l list attached loop devices
|
||||||
|
-- losetup -i <path> 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.." <path>")
|
||||||
|
print(" "..name.." -i <path>")
|
||||||
|
print(" "..name.." -d <id>")
|
||||||
|
print(" "..name.." -l")
|
||||||
|
print("")
|
||||||
|
print("Manage loop devices.")
|
||||||
|
print("")
|
||||||
|
print(" <path> attach a directory (bind) or .hfs image file")
|
||||||
|
print(" -i <path> force image mode for the given file")
|
||||||
|
print(" -d <id> 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)
|
||||||
@@ -1,44 +1,32 @@
|
|||||||
|
--:Minify:--
|
||||||
local cloptions = {
|
local cloptions = {
|
||||||
a = false,
|
a = false,
|
||||||
h = false,
|
h = false,
|
||||||
l = false,
|
l = false,
|
||||||
help = false,
|
help = false,
|
||||||
}
|
}
|
||||||
|
local inpArgs = { ... }
|
||||||
local inpArgs = {...}
|
|
||||||
local args = {}
|
local args = {}
|
||||||
local name = syscall.getTask(syscall.getpid()).name
|
local name = syscall.getTask(syscall.getpid()).name
|
||||||
|
|
||||||
local optToSet = false
|
|
||||||
for _, v in pairs(inpArgs) do
|
for _, v in pairs(inpArgs) do
|
||||||
if optToSet then
|
if v:sub(1, 2) == "--" then
|
||||||
cloptions[optToSet] = v
|
|
||||||
optToSet = false
|
|
||||||
elseif v:sub(1, 2) == "--" then
|
|
||||||
local opt = v:sub(3)
|
local opt = v:sub(3)
|
||||||
if cloptions[opt] == nil then
|
if cloptions[opt] == nil then
|
||||||
print(name..": unrecognized option '"..v.."'.")
|
print(name .. ": unrecognized option '" .. v .. "'.")
|
||||||
if cloptions.help ~= nil then
|
print("try '" .. name .. " --help' for more information.")
|
||||||
print("try '"..name.." --help' for more information.")
|
|
||||||
end
|
|
||||||
return
|
return
|
||||||
elseif cloptions[opt] == false then
|
|
||||||
cloptions[opt] = true
|
|
||||||
else
|
|
||||||
optToSet = opt
|
|
||||||
end
|
end
|
||||||
|
cloptions[opt] = true
|
||||||
elseif v:sub(1, 1) == "-" then
|
elseif v:sub(1, 1) == "-" then
|
||||||
for i = 2, #v do
|
for i = 2, #v do
|
||||||
local opt = v:sub(i, i)
|
local opt = v:sub(i, i)
|
||||||
if cloptions[opt] == nil then
|
if cloptions[opt] == nil then
|
||||||
print(name..": invalid option '-"..opt.."'.")
|
print(name .. ": invalid option '-" .. opt .. "'.")
|
||||||
if cloptions.help ~= nil then
|
print("try '" .. name .. " --help' for more information.")
|
||||||
print("try '"..name.." --help' for more information.")
|
|
||||||
end
|
|
||||||
return
|
return
|
||||||
else
|
|
||||||
cloptions[opt] = true
|
|
||||||
end
|
end
|
||||||
|
cloptions[opt] = true
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
table.insert(args, v)
|
table.insert(args, v)
|
||||||
@@ -46,100 +34,160 @@ for _, v in pairs(inpArgs) do
|
|||||||
end
|
end
|
||||||
|
|
||||||
if cloptions.help then
|
if cloptions.help then
|
||||||
print("Usage: "..name.." [OPTION]... [DIR]")
|
print("Usage: " .. name .. " [OPTION]... [DIR]")
|
||||||
print("List all entries in the specified DIRectory, or cwd if not specified")
|
print("List all entries in the specified DIRectory, or cwd if not specified.")
|
||||||
print("\nOptions:")
|
print("")
|
||||||
print(" -a Do not ignore entries starting with .")
|
print("Options:")
|
||||||
print(" -h with -l, print sizes in a human readble format")
|
print(" -a do not ignore entries starting with .")
|
||||||
print(" -l Use a long listing format")
|
print(" -h with -l, print sizes in human readable format")
|
||||||
print(" --help Display this help and exit")
|
print(" -l use a long listing format")
|
||||||
|
print(" --help display this help and exit")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local fs = require("sys.fs")
|
local fs = require("sys.fs")
|
||||||
local dir = (args[1] or "")
|
local dir = args[1] or ""
|
||||||
if dir:sub(1, 1) ~= "/" then
|
if dir:sub(1, 1) ~= "/" then
|
||||||
dir = syscall.getcwd().."/"..dir
|
dir = syscall.getcwd() .. "/" .. dir
|
||||||
end
|
|
||||||
|
|
||||||
if dir:sub(#dir, #dir) ~= "/" then
|
|
||||||
dir = dir.."/"
|
|
||||||
end
|
end
|
||||||
|
if dir:sub(-1) ~= "/" then dir = dir .. "/" end
|
||||||
|
|
||||||
if not fs.isDir(dir) then
|
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
|
return
|
||||||
end
|
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 screenSizeStr = syscall.devctl(1, "size")
|
||||||
local sizeX = tonumber(screenSizeStr:sub(1, screenSizeStr:find(";")-1))
|
local sizeX = tonumber(screenSizeStr:match("^(%d+)")) or 80
|
||||||
local sizeY = tonumber(screenSizeStr:sub(screenSizeStr:find(";")+1))
|
|
||||||
|
|
||||||
local list = fs.list(dir)
|
local list = fs.list(dir)
|
||||||
if not cloptions.a then
|
if not cloptions.a then
|
||||||
for i = #list, 1, -1 do
|
for i = #list, 1, -1 do
|
||||||
if list[i]:sub(1, 1) == "." then
|
if list[i]:sub(1, 1) == "." then table.remove(list, i) end
|
||||||
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
|
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
|
end
|
||||||
|
|
||||||
local colWidth = 0
|
local colWidth = 0
|
||||||
local numCols = 1
|
for _, v in ipairs(list) do
|
||||||
if not cloptions.l then
|
if #v + 2 > colWidth then colWidth = #v + 2 end
|
||||||
for _, item in pairs(list) do
|
|
||||||
if #item + 2 > colWidth then
|
|
||||||
colWidth = #item + 2
|
|
||||||
end
|
|
||||||
end
|
|
||||||
numCols = math.floor(sizeX / colWidth)
|
|
||||||
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
|
if isSym then
|
||||||
local fileStats = syscall.stat(dir..v)
|
syscall.devctl(1, "sfgc", 6)
|
||||||
local isDir = fs.isDir(dir..v)
|
elseif isDir then
|
||||||
if cloptions.l then
|
syscall.devctl(1, "sfgc", 4)
|
||||||
if isDir then
|
|
||||||
printInline("d")
|
|
||||||
else
|
else
|
||||||
printInline("-")
|
local isExec = stat and stat.perms and (math.floor(stat.perms / (2^9)) % 2 == 1)
|
||||||
end
|
syscall.devctl(1, "sfgc", isExec and 3 or 1)
|
||||||
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)
|
|
||||||
else
|
|
||||||
syscall.devctl(1,"sfgc",1)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
printInline(v)
|
printInline(v)
|
||||||
|
syscall.devctl(1, "sfgc", 1)
|
||||||
printInline((" "):rep(colWidth - #v))
|
printInline((" "):rep(colWidth - #v))
|
||||||
syscall.devctl(1,"sfgc",1)
|
|
||||||
if i % numCols == 0 then
|
if i % numCols == 0 then print("") end
|
||||||
print("")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if #list % numCols ~= 0 then
|
|
||||||
print("")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if #list % numCols ~= 0 then print("") end
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
--:Minify:--
|
--:Minify:--
|
||||||
local users = syscall.auth_listusers()
|
local users = syscall.listusers()
|
||||||
if not users or #users == 0 then
|
if not users or #users == 0 then
|
||||||
print("No users found.")
|
print("No users found.")
|
||||||
return
|
return
|
||||||
|
|||||||
423
Src/Hyperion-bash/bin/micro
Normal file
423
Src/Hyperion-bash/bin/micro
Normal file
@@ -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 ""))
|
||||||
152
Src/Hyperion-bash/bin/mount
Normal file
152
Src/Hyperion-bash/bin/mount
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
--:Minify:--
|
||||||
|
-- Usage:
|
||||||
|
-- mount list all current mounts
|
||||||
|
-- mount <id> <mountpoint> mount loop device id at mountpoint
|
||||||
|
-- mount -o loop <src> <dest> attach <src> as loop device and mount at <dest>
|
||||||
|
-- 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.." <id> <mountpoint>")
|
||||||
|
print(" "..name.." -o loop <source> <mountpoint>")
|
||||||
|
print("")
|
||||||
|
print("Mount a loop device or filesystem.")
|
||||||
|
print("")
|
||||||
|
print(" (no args) list all active mount points")
|
||||||
|
print(" <id> <mountpoint> mount an already-attached loop device")
|
||||||
|
print(" -o loop <src> <dest> 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 <source> and <mountpoint>")
|
||||||
|
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)
|
||||||
@@ -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(" | ")
|
|
||||||
@@ -9,7 +9,7 @@ local currentUid = syscall.getuid()
|
|||||||
|
|
||||||
local targetUid
|
local targetUid
|
||||||
if targetName then
|
if targetName then
|
||||||
targetUid = syscall.auth_getuid(targetName)
|
targetUid = syscall.getuid(targetName)
|
||||||
if not targetUid then
|
if not targetUid then
|
||||||
print("passwd: user '" .. targetName .. "' does not exist")
|
print("passwd: user '" .. targetName .. "' does not exist")
|
||||||
syscall.exit(1); return
|
syscall.exit(1); return
|
||||||
@@ -36,7 +36,7 @@ if currentUid ~= 0 then
|
|||||||
if #cur > 0 then cur=cur:sub(1,-2); syscall.write(1,"\b \b") end
|
if #cur > 0 then cur=cur:sub(1,-2); syscall.write(1,"\b \b") end
|
||||||
else cur=cur..ch; syscall.write(1,"*") end
|
else cur=cur..ch; syscall.write(1,"*") end
|
||||||
end
|
end
|
||||||
local ok, err = syscall.auth_elevate(targetName, cur)
|
local ok, err = syscall.elevate(targetName, cur)
|
||||||
if not ok then
|
if not ok then
|
||||||
sleep(1)
|
sleep(1)
|
||||||
print("passwd: authentication failure")
|
print("passwd: authentication failure")
|
||||||
@@ -71,7 +71,7 @@ if pw1 ~= pw2 then
|
|||||||
syscall.exit(1); return
|
syscall.exit(1); return
|
||||||
end
|
end
|
||||||
|
|
||||||
local ok, err = syscall.auth_setpassword(targetUid, pw1)
|
local ok, err = syscall.setpassword(targetUid, pw1)
|
||||||
if not ok then
|
if not ok then
|
||||||
print("passwd: " .. tostring(err))
|
print("passwd: " .. tostring(err))
|
||||||
syscall.exit(1); return
|
syscall.exit(1); return
|
||||||
|
|||||||
82
Src/Hyperion-bash/bin/readlink
Normal file
82
Src/Hyperion-bash/bin/readlink
Normal file
@@ -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
|
||||||
429
Src/Hyperion-bash/bin/sed
Normal file
429
Src/Hyperion-bash/bin/sed
Normal file
@@ -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
|
||||||
151
Src/Hyperion-bash/bin/socktest
Normal file
151
Src/Hyperion-bash/bin/socktest
Normal file
@@ -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("<html") ~= nil
|
||||||
|
or body:lower():find("<!doctype") ~= nil
|
||||||
|
or body:find("{") ~= nil
|
||||||
|
or body:find("HTTP") ~= nil
|
||||||
|
check("body looks like HTTP content", hasHtml, "no recognisable content markers")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
head("[ 6 ] UNIX socket loopback IPC")
|
||||||
|
do
|
||||||
|
local sockPath = "/tmp/socktest.sock"
|
||||||
|
pcall(syscall.remove, sockPath)
|
||||||
|
|
||||||
|
local sok, sfd = pcall(syscall.socket, "unix", "stream")
|
||||||
|
check("server socket(unix,stream)", sok, sfd)
|
||||||
|
|
||||||
|
if sok then
|
||||||
|
local bok = pcall(syscall.bind, sfd, sockPath)
|
||||||
|
check("bind(" .. sockPath .. ")", bok, "bind failed")
|
||||||
|
|
||||||
|
local lok = pcall(syscall.listen, sfd, 1)
|
||||||
|
check("listen()", lok, "listen failed")
|
||||||
|
|
||||||
|
local cok, cfd = pcall(syscall.socket, "unix", "stream")
|
||||||
|
check("client socket(unix,stream)", cok, cfd)
|
||||||
|
|
||||||
|
if cok then
|
||||||
|
local connok = pcall(syscall.connect, cfd, sockPath)
|
||||||
|
check("client connect(" .. sockPath .. ")", connok, "connect failed")
|
||||||
|
|
||||||
|
local aok, afd = pcall(syscall.accept, sfd)
|
||||||
|
check("accept() returns client fd", aok, afd)
|
||||||
|
|
||||||
|
if connok and aok then
|
||||||
|
local sendok = pcall(syscall.send, cfd, "hello hyperion")
|
||||||
|
check("send() from client", sendok, "send failed")
|
||||||
|
|
||||||
|
local rok, data = pcall(syscall.recv, afd, 1024)
|
||||||
|
check("recv() on server side", rok and data == "hello hyperion",
|
||||||
|
rok and ("got: " .. tostring(data)) or tostring(data))
|
||||||
|
|
||||||
|
local repok = pcall(syscall.send, afd, "hello back")
|
||||||
|
check("send() reply from server", repok, "send failed")
|
||||||
|
|
||||||
|
local rok2, data2 = pcall(syscall.recv, cfd, 1024)
|
||||||
|
check("recv() reply on client", rok2 and data2 == "hello back",
|
||||||
|
rok2 and ("got: " .. tostring(data2)) or tostring(data2))
|
||||||
|
|
||||||
|
pcall(syscall.close, afd)
|
||||||
|
end
|
||||||
|
|
||||||
|
pcall(syscall.close, cfd)
|
||||||
|
end
|
||||||
|
|
||||||
|
pcall(syscall.close, sfd)
|
||||||
|
pcall(syscall.remove, sockPath)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
@@ -1 +0,0 @@
|
|||||||
syscall.chown("/bin", 0, 0)
|
|
||||||
@@ -1,21 +1,15 @@
|
|||||||
--:Minify:--
|
--:Minify:--
|
||||||
local fs = require("sys.fs")
|
|
||||||
local targetUser = ({ ... })[1] or "root"
|
local targetUser = ({ ... })[1] or "root"
|
||||||
|
|
||||||
local currentUid = syscall.getuid()
|
local currentUid = syscall.getuid()
|
||||||
local currentUser = syscall.getUsername(currentUid) or tostring(currentUid)
|
local targetUid = syscall.getuidbyname(targetUser)
|
||||||
|
|
||||||
local targetUid = syscall.auth_getuid(targetUser)
|
|
||||||
if not targetUid then
|
if not targetUid then
|
||||||
print("su: user '" .. targetUser .. "' does not exist")
|
print("su: user '" .. targetUser .. "' does not exist")
|
||||||
syscall.exit(1)
|
syscall.exit(1)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local ok, err
|
if currentUid ~= 0 then
|
||||||
if currentUid == 0 then
|
|
||||||
ok = true
|
|
||||||
else
|
|
||||||
printInline("Password: ")
|
printInline("Password: ")
|
||||||
local pw = ""
|
local pw = ""
|
||||||
while true do
|
while true do
|
||||||
@@ -25,16 +19,13 @@ else
|
|||||||
syscall.write(1, "\n")
|
syscall.write(1, "\n")
|
||||||
break
|
break
|
||||||
elseif ch == "\b" then
|
elseif ch == "\b" then
|
||||||
if #pw > 0 then
|
if #pw > 0 then pw = pw:sub(1, -2); syscall.write(1, "\b \b") end
|
||||||
pw = pw:sub(1, -2); syscall.write(1, "\b \b")
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
pw = pw .. ch
|
pw = pw .. ch; syscall.write(1, "*")
|
||||||
syscall.write(1, "*")
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
ok, err = syscall.auth_elevate(targetUser, pw)
|
local ok, err = syscall.elevate(targetUser, pw)
|
||||||
if not ok then
|
if not ok then
|
||||||
sleep(1)
|
sleep(1)
|
||||||
print("su: Authentication failure")
|
print("su: Authentication failure")
|
||||||
@@ -43,38 +34,23 @@ else
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if currentUid == 0 then
|
syscall.setuid(targetUid)
|
||||||
syscall.setuid(targetUid)
|
|
||||||
end
|
|
||||||
|
|
||||||
local pwent = syscall.auth_getpasswd(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 "/"
|
local homedir = (pwent and pwent.homedir) or "/"
|
||||||
|
|
||||||
syscall.chdir(homedir)
|
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("HOME", homedir)
|
||||||
syscall.setEnviron("USER", targetUser)
|
syscall.setEnviron("USER", targetUser)
|
||||||
syscall.setEnviron("SHELL", shell)
|
syscall.setEnviron("SHELL", shell)
|
||||||
|
|
||||||
local shellText = fs.readAllText(shell)
|
local ok, err = pcall(syscall.exec, shell)
|
||||||
if not shellText then
|
if not ok then
|
||||||
print("su: shell not found: " .. shell)
|
print("su: cannot exec shell '" .. shell .. "': " .. tostring(err))
|
||||||
syscall.exit(1)
|
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
|
end
|
||||||
@@ -10,7 +10,7 @@ if cmdArgs[i] == "-u" then
|
|||||||
local uarg = cmdArgs[i] or "root"
|
local uarg = cmdArgs[i] or "root"
|
||||||
local numUid = tonumber(uarg)
|
local numUid = tonumber(uarg)
|
||||||
if numUid then
|
if numUid then
|
||||||
local pwent = syscall.auth_getpasswd(numUid)
|
local pwent = syscall.getpasswd(numUid)
|
||||||
targetUser = (pwent and pwent.username) or uarg
|
targetUser = (pwent and pwent.username) or uarg
|
||||||
else
|
else
|
||||||
targetUser = uarg
|
targetUser = uarg
|
||||||
@@ -31,7 +31,7 @@ for j = i + 1, #cmdArgs do restArgs[#restArgs + 1] = cmdArgs[j] end
|
|||||||
local currentUid = syscall.getuid()
|
local currentUid = syscall.getuid()
|
||||||
local currentUser = syscall.getUsername(currentUid) or tostring(currentUid)
|
local currentUser = syscall.getUsername(currentUid) or tostring(currentUid)
|
||||||
|
|
||||||
local targetUid = syscall.auth_getuid(targetUser)
|
local targetUid = syscall.getuidbyname(targetUser)
|
||||||
if not targetUid then
|
if not targetUid then
|
||||||
print("sudo: user '" .. targetUser .. "' does not exist")
|
print("sudo: user '" .. targetUser .. "' does not exist")
|
||||||
syscall.exit(1)
|
syscall.exit(1)
|
||||||
@@ -39,7 +39,7 @@ if not targetUid then
|
|||||||
end
|
end
|
||||||
|
|
||||||
if currentUid ~= 0 then
|
if currentUid ~= 0 then
|
||||||
printInline("[sudo] password for " .. currentUser .. ": ")
|
printInline("[sudo] password for root: ")
|
||||||
local pw = ""
|
local pw = ""
|
||||||
while true do
|
while true do
|
||||||
local ch = syscall.read(0)
|
local ch = syscall.read(0)
|
||||||
@@ -55,7 +55,7 @@ if currentUid ~= 0 then
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local ok, err = syscall.auth_elevate(currentUser, pw)
|
local ok, err = syscall.elevate("root", pw)
|
||||||
if not ok then
|
if not ok then
|
||||||
sleep(1)
|
sleep(1)
|
||||||
print("sudo: Authentication failure")
|
print("sudo: Authentication failure")
|
||||||
@@ -63,7 +63,7 @@ if currentUid ~= 0 then
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
if targetUid ~= 0 then
|
if targetUid ~= currentUid then
|
||||||
syscall.setuid(targetUid)
|
syscall.setuid(targetUid)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
@@ -97,7 +97,7 @@ if not program then
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local pwent = syscall.auth_getpasswd(targetUid)
|
local pwent = syscall.getpasswd(targetUid)
|
||||||
if pwent and pwent.homedir then
|
if pwent and pwent.homedir then
|
||||||
syscall.setEnviron("HOME", pwent.homedir)
|
syscall.setEnviron("HOME", pwent.homedir)
|
||||||
end
|
end
|
||||||
|
|||||||
111
Src/Hyperion-bash/bin/umount
Normal file
111
Src/Hyperion-bash/bin/umount
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
--:Minify:--
|
||||||
|
-- Usage:
|
||||||
|
-- umount <mountpoint> unmount; auto-detach loop device if one is found
|
||||||
|
-- umount -l <id> detach loop device without unmounting (force)
|
||||||
|
-- umount --no-detach <mpt> 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.." <mountpoint>")
|
||||||
|
print(" "..name.." --no-detach <mountpoint>")
|
||||||
|
print(" "..name.." -l <loopid>")
|
||||||
|
print("")
|
||||||
|
print("Unmount a filesystem mounted at <mountpoint>.")
|
||||||
|
print("")
|
||||||
|
print(" <mountpoint> unmount and auto-detach any loop device")
|
||||||
|
print(" --no-detach unmount but keep the loop device attached")
|
||||||
|
print(" -l <loopid> 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
|
||||||
@@ -48,7 +48,7 @@ if not password then
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local uid, err = syscall.auth_newuser(
|
local uid, err = syscall.newuser(
|
||||||
opt.username, password, opt.gid, opt.homedir, opt.shell or "/bin/hysh"
|
opt.username, password, opt.gid, opt.homedir, opt.shell or "/bin/hysh"
|
||||||
)
|
)
|
||||||
if not uid then
|
if not uid then
|
||||||
|
|||||||
@@ -14,15 +14,15 @@ if not username then
|
|||||||
syscall.exit(1); return
|
syscall.exit(1); return
|
||||||
end
|
end
|
||||||
|
|
||||||
local uid = syscall.auth_getuid(username)
|
local uid = syscall.getuid(username)
|
||||||
if not uid then
|
if not uid then
|
||||||
print("userdel: user '" .. username .. "' does not exist")
|
print("userdel: user '" .. username .. "' does not exist")
|
||||||
syscall.exit(1); return
|
syscall.exit(1); return
|
||||||
end
|
end
|
||||||
|
|
||||||
local pwent = syscall.auth_getpasswd(uid)
|
local pwent = syscall.getpasswd(uid)
|
||||||
|
|
||||||
local ok, err = syscall.auth_deleteuser(uid)
|
local ok, err = syscall.deleteuser(uid)
|
||||||
if not ok then
|
if not ok then
|
||||||
print("userdel: " .. tostring(err))
|
print("userdel: " .. tostring(err))
|
||||||
syscall.exit(1); return
|
syscall.exit(1); return
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ if opt.lock and opt.unlock then
|
|||||||
syscall.exit(1); return
|
syscall.exit(1); return
|
||||||
end
|
end
|
||||||
|
|
||||||
local uid = syscall.auth_getuid(opt.username)
|
local uid = syscall.getuid(opt.username)
|
||||||
if not uid then
|
if not uid then
|
||||||
print("usermod: user '" .. opt.username .. "' does not exist")
|
print("usermod: user '" .. opt.username .. "' does not exist")
|
||||||
syscall.exit(1); return
|
syscall.exit(1); return
|
||||||
@@ -38,12 +38,12 @@ local function apply(fn, ...)
|
|||||||
if not ok then print("usermod: " .. tostring(err)); syscall.exit(1) end
|
if not ok then print("usermod: " .. tostring(err)); syscall.exit(1) end
|
||||||
end
|
end
|
||||||
|
|
||||||
if opt.newname then apply(syscall.auth_setusername, uid, opt.newname) end
|
if opt.newname then apply(syscall.setusername, uid, opt.newname) end
|
||||||
if opt.password then apply(syscall.auth_setpassword, uid, opt.password) end
|
if opt.password then apply(syscall.setpassword, uid, opt.password) end
|
||||||
if opt.gid then apply(syscall.auth_setgid, uid, opt.gid) end
|
if opt.gid then apply(syscall.setgid, uid, opt.gid) end
|
||||||
if opt.homedir then apply(syscall.auth_sethomedir, uid, opt.homedir) end
|
if opt.homedir then apply(syscall.sethomedir, uid, opt.homedir) end
|
||||||
if opt.shell then apply(syscall.auth_setshell, uid, opt.shell) end
|
if opt.shell then apply(syscall.setshell, uid, opt.shell) end
|
||||||
if opt.lock then apply(syscall.auth_lockuser, uid) end
|
if opt.lock then apply(syscall.lockuser, uid) end
|
||||||
if opt.unlock then apply(syscall.auth_unlockuser, uid) end
|
if opt.unlock then apply(syscall.unlockuser, uid) end
|
||||||
|
|
||||||
print("usermod: updated user '" .. opt.username .. "'")
|
print("usermod: updated user '" .. opt.username .. "'")
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ for i,v in pairs(kernel.processes) do
|
|||||||
end, i)
|
end, i)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if not fs.exists("/bin/startup") then
|
||||||
|
fs.mkdir("/bin/startup")
|
||||||
|
end
|
||||||
local files = fs.list("/bin/startup")
|
local files = fs.list("/bin/startup")
|
||||||
if not files then error("Failed to list /bin/startup") end
|
if not files then error("Failed to list /bin/startup") end
|
||||||
for i,v in ipairs(files) do
|
for i,v in ipairs(files) do
|
||||||
|
|||||||
@@ -254,7 +254,6 @@ local ok, err = xpcall(function()
|
|||||||
if not ok then displaySuperBadError(err) end
|
if not ok then displaySuperBadError(err) end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
-- time is in milliseconds
|
|
||||||
function coroutine.resumeWithTimeout(co, timeout, ...)
|
function coroutine.resumeWithTimeout(co, timeout, ...)
|
||||||
local startTime = computer.time()
|
local startTime = computer.time()
|
||||||
debug.sethook(co, function()
|
debug.sethook(co, function()
|
||||||
@@ -294,6 +293,14 @@ local ok, err = xpcall(function()
|
|||||||
queueEvent("componentAdded", "disk")
|
queueEvent("componentAdded", "disk")
|
||||||
elseif event[1] == "disk_eject" then
|
elseif event[1] == "disk_eject" then
|
||||||
queueEvent("componentRemoved", "disk")
|
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
|
elseif event[1] == "NoSleep" then
|
||||||
exit = true
|
exit = true
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ function peripheral.isPresent(name)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function peripheral.getType(peripheral)
|
function peripheral.getType(peripheral)
|
||||||
if type(peripheral) == "string" then -- Peripheral name passed
|
if type(peripheral) == "string" then
|
||||||
if native.isPresent(peripheral) then
|
if native.isPresent(peripheral) then
|
||||||
return native.getType(peripheral)
|
return native.getType(peripheral)
|
||||||
end
|
end
|
||||||
@@ -58,7 +58,7 @@ function peripheral.getType(peripheral)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function peripheral.hasType(peripheral, peripheral_type)
|
function peripheral.hasType(peripheral, peripheral_type)
|
||||||
if type(peripheral) == "string" then -- Peripheral name passed
|
if type(peripheral) == "string" then
|
||||||
if native.isPresent(peripheral) then
|
if native.isPresent(peripheral) then
|
||||||
return native.hasType(peripheral, peripheral_type)
|
return native.hasType(peripheral, peripheral_type)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ local computer = args[6]
|
|||||||
local ifs = args[7]
|
local ifs = args[7]
|
||||||
local kernel = {}
|
local kernel = {}
|
||||||
kernel.LOG_Text=""
|
kernel.LOG_Text=""
|
||||||
kernel.version="HyperionOS V1.0.0"
|
kernel.version="HyperionOS V1.2.0"
|
||||||
kernel.process = "Kernel"
|
kernel.process = "Kernel"
|
||||||
kernel.users={[0]="root",[1]="User"}
|
kernel.users={[0]="root",[1]="User"}
|
||||||
kernel.hostname = "hyperion"
|
kernel.hostname = "hyperion"
|
||||||
@@ -184,10 +184,17 @@ end
|
|||||||
|
|
||||||
kernel.log("Gathering modules")
|
kernel.log("Gathering modules")
|
||||||
for _, i in ipairs(ifs.list("/lib/modules")) do
|
for _, i in ipairs(ifs.list("/lib/modules")) do
|
||||||
for _,v in ipairs(ifs.list("/lib/modules/"..i)) do
|
local modlist = ifs.list("/lib/modules/"..i)
|
||||||
|
if not modlist then
|
||||||
|
kernel.log("WARNING: could not list /lib/modules/"..i.." (skipping)", "WARN", 8)
|
||||||
|
else
|
||||||
|
for _,v in ipairs(modlist) do
|
||||||
local prior=tonumber(v:sub(1,2))
|
local prior=tonumber(v:sub(1,2))
|
||||||
|
if prior then
|
||||||
modules[prior+1][#modules[prior+1]+1]="/lib/modules/"..i.."/"..v
|
modules[prior+1][#modules[prior+1]+1]="/lib/modules/"..i.."/"..v
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
kernel.ifs=ifs
|
kernel.ifs=ifs
|
||||||
|
|||||||
@@ -1,629 +0,0 @@
|
|||||||
-- :Minify:--
|
|
||||||
local kernel = ...
|
|
||||||
local vfs = {}
|
|
||||||
kernel.vfs = vfs
|
|
||||||
vfs.mounts = {["$"] = "/"}
|
|
||||||
vfs.disks = kernel.disks
|
|
||||||
|
|
||||||
-- Path normalization
|
|
||||||
local function normalizePath(path)
|
|
||||||
local task = kernel.currentTask
|
|
||||||
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
|
|
||||||
if #parts > 0 then table.remove(parts) end
|
|
||||||
elseif part ~= "." and part ~= "" then
|
|
||||||
table.insert(parts, part)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return "/" .. table.concat(parts, "/")
|
|
||||||
end
|
|
||||||
|
|
||||||
function vfs.splitPath(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
|
|
||||||
|
|
||||||
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
|
|
||||||
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
|
|
||||||
|
|
||||||
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}
|
|
||||||
end
|
|
||||||
|
|
||||||
return ret
|
|
||||||
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 parts = {}
|
|
||||||
for p in fullPath:gmatch("[^/]+") do table.insert(parts, p) end
|
|
||||||
|
|
||||||
-- default fallback
|
|
||||||
local default = {owner = 0, group = 0, perms = 63, cmeta = ""}
|
|
||||||
|
|
||||||
-- 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
|
|
||||||
|
|
||||||
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
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return default
|
|
||||||
end
|
|
||||||
|
|
||||||
local function ensureParentMeta(path)
|
|
||||||
local disk, fullPath = resolvePath(path)
|
|
||||||
fullPath = normalizePath(fullPath)
|
|
||||||
|
|
||||||
-- split parent + name
|
|
||||||
local parent, name = fullPath:match("^(.*)/([^/]+)$")
|
|
||||||
if not parent then
|
|
||||||
parent = "/"
|
|
||||||
name = fullPath:gsub("^/", "")
|
|
||||||
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()
|
|
||||||
end
|
|
||||||
|
|
||||||
return metaPath, name
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Permission checking
|
|
||||||
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 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
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if bit_is_set(bits, modes[mode].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
|
|
||||||
return path
|
|
||||||
end
|
|
||||||
|
|
||||||
local required = {
|
|
||||||
"open",
|
|
||||||
"type",
|
|
||||||
"list",
|
|
||||||
"attributes",
|
|
||||||
"fileExists",
|
|
||||||
"makeDirectory",
|
|
||||||
"remove"
|
|
||||||
}
|
|
||||||
|
|
||||||
local function check(disk)
|
|
||||||
for _, name in ipairs(required) do
|
|
||||||
if type(disk[name]) ~= "function" then
|
|
||||||
error("Invalid disk: missing method '" .. name .. "'")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function vfs.mount(target, diskOrId)
|
|
||||||
if kernel.uid ~= 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
|
|
||||||
|
|
||||||
if type(diskOrId) == "string" then
|
|
||||||
disk = kernel.disks[diskOrId]
|
|
||||||
if not disk then error("ENODEV") end
|
|
||||||
check(disk)
|
|
||||||
id = diskOrId
|
|
||||||
elseif type(diskOrId) == "table" then
|
|
||||||
check(disk)
|
|
||||||
disk = diskOrId
|
|
||||||
id = disk.address
|
|
||||||
vfs.disks[id] = disk
|
|
||||||
else
|
|
||||||
error("EINVAL")
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Prevent shadowing an existing mount
|
|
||||||
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
|
|
||||||
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
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
error("EINVAL")
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Open file
|
|
||||||
function vfs.open(path, mode)
|
|
||||||
checkSystemLimit()
|
|
||||||
local task = kernel.currentTask
|
|
||||||
local fd = allocFD(task)
|
|
||||||
local disk, diskPath = resolvePath(path)
|
|
||||||
if not disk then error("NODISK") end
|
|
||||||
|
|
||||||
local meta = getFileMeta(path)
|
|
||||||
checkperms(meta, mode)
|
|
||||||
|
|
||||||
local handle
|
|
||||||
if disk:type(diskPath)~="directory" then
|
|
||||||
handle = disk:open(diskPath, mode)
|
|
||||||
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
|
|
||||||
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
|
|
||||||
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
|
|
||||||
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
|
|
||||||
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
|
|
||||||
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
|
|
||||||
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
|
|
||||||
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
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Sendfile
|
|
||||||
function vfs.sendfile(outfd, infd, count)
|
|
||||||
local task = kernel.currentTask
|
|
||||||
local inFile = task.fd[infd]
|
|
||||||
local outFile = task.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
|
|
||||||
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)
|
|
||||||
return {
|
|
||||||
size = attrs.size,
|
|
||||||
modified = attrs.modified,
|
|
||||||
created = attrs.created,
|
|
||||||
owner = meta.owner,
|
|
||||||
group = meta.group,
|
|
||||||
xattr = meta.cmeta
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
function vfs.fstat(fd)
|
|
||||||
local task = kernel.currentTask
|
|
||||||
local file = task.fd[fd]
|
|
||||||
if not file then error("EBADF") end
|
|
||||||
local disk, path = resolvePath(file.path)
|
|
||||||
local attrs = disk:attributes(path)
|
|
||||||
return {
|
|
||||||
size = attrs.size,
|
|
||||||
modified = attrs.modified,
|
|
||||||
created = attrs.created,
|
|
||||||
owner = file.meta.owner,
|
|
||||||
group = file.meta.group,
|
|
||||||
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")
|
|
||||||
local list = disk:list(diskPath)
|
|
||||||
if table.indexOf(list, ".meta") ~= -1 then
|
|
||||||
table.remove(list, table.indexOf(list, ".meta"))
|
|
||||||
end
|
|
||||||
return list
|
|
||||||
end
|
|
||||||
|
|
||||||
function vfs.mkdir(path)
|
|
||||||
local disk, diskPath = resolvePath(path)
|
|
||||||
local meta = getFileMeta(path)
|
|
||||||
checkperms(meta, "w")
|
|
||||||
disk:makeDirectory(diskPath)
|
|
||||||
end
|
|
||||||
|
|
||||||
function vfs.remove(path)
|
|
||||||
local disk, diskPath = resolvePath(path)
|
|
||||||
local meta = getFileMeta(path)
|
|
||||||
checkperms(meta, "w")
|
|
||||||
disk:remove(diskPath)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Permission functions
|
|
||||||
function vfs.chmod(path, perms)
|
|
||||||
local disk, diskPath = resolvePath(path)
|
|
||||||
local meta = getFileMeta(path)
|
|
||||||
|
|
||||||
if meta.owner ~= kernel.currentTask.uid then error("EACCES") end
|
|
||||||
meta.perms = perms
|
|
||||||
|
|
||||||
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()
|
|
||||||
end
|
|
||||||
|
|
||||||
function vfs.fchmod(fd, perms)
|
|
||||||
local task = kernel.currentTask
|
|
||||||
local file = task.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()
|
|
||||||
end
|
|
||||||
|
|
||||||
function vfs.fchown(fd, uid, gid)
|
|
||||||
local task = kernel.currentTask
|
|
||||||
local file = task.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")
|
|
||||||
return disk:fileExists(diskPath)
|
|
||||||
end
|
|
||||||
|
|
||||||
function vfs.type(path)
|
|
||||||
local disk, diskPath = resolvePath(path)
|
|
||||||
local meta = getFileMeta(path)
|
|
||||||
checkperms(meta, "r")
|
|
||||||
return disk:type(diskPath)
|
|
||||||
end
|
|
||||||
|
|
||||||
function vfs.getcwd() return kernel.currentTask.cwd end
|
|
||||||
|
|
||||||
function vfs.chdir(path) kernel.currentTask.cwd = path 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
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
checkSystemLimit()
|
|
||||||
|
|
||||||
file.refcount = file.refcount + 1
|
|
||||||
task.fd[newfd] = file
|
|
||||||
total = total + 1
|
|
||||||
|
|
||||||
return newfd
|
|
||||||
end
|
|
||||||
|
|
||||||
function vfs.devctl(fd, method, ...)
|
|
||||||
if not kernel.currentTask.fd[fd] then error("EBADF") end
|
|
||||||
if not kernel.currentTask.fd[fd].handle[method] then error("EINVAL") end
|
|
||||||
return kernel.currentTask.fd[fd].handle[method](...)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Export syscalls
|
|
||||||
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
|
|
||||||
|
|
||||||
kernel.log("VFS module loaded")
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
--:Minify:--
|
|
||||||
local kernel = ...
|
|
||||||
local socket = {}
|
|
||||||
|
|
||||||
function socket.socket()
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
function socket.bind()
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
kernel.socket=socket
|
|
||||||
kernel.log("Loaded socket module")
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
--: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"
|
|
||||||
@@ -1,438 +0,0 @@
|
|||||||
-- :Minify:--
|
|
||||||
local kernel = ...
|
|
||||||
local tasks = {}
|
|
||||||
local sys = {}
|
|
||||||
local nextpid = 2
|
|
||||||
kernel.exitMain = false
|
|
||||||
|
|
||||||
function sys.spawn(func, name, envars, args, tgid)
|
|
||||||
local id = nextpid
|
|
||||||
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
|
|
||||||
end
|
|
||||||
end
|
|
||||||
for v, _ in ipairs(tasks[tostring(id)].fd) do pcall(kernel.vfs.close,v) 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,
|
|
||||||
children = {},
|
|
||||||
parent = kernel.currentTask,
|
|
||||||
siblings = kernel.currentTask.children,
|
|
||||||
syscallReturn = {},
|
|
||||||
cwd = kernel.currentTask.cwd,
|
|
||||||
timeSlice = 0,
|
|
||||||
lastTime = 0,
|
|
||||||
totalTime = 0,
|
|
||||||
numRuns = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
table.insert(kernel.currentTask.children, tasks[tostring(id)])
|
|
||||||
return id
|
|
||||||
end
|
|
||||||
|
|
||||||
function sys.sleep(s)
|
|
||||||
kernel.currentTask.status = "S"
|
|
||||||
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 = {}
|
|
||||||
|
|
||||||
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
|
|
||||||
end
|
|
||||||
|
|
||||||
function sys.collect(pid)
|
|
||||||
local children = {}
|
|
||||||
for i, v in ipairs(kernel.currentTask.children) do children[i] = v.pid end
|
|
||||||
|
|
||||||
if not tasks[tostring(pid)] then
|
|
||||||
return false, "Task does not exist"
|
|
||||||
|
|
||||||
elseif not isEqualToAny(tasks[tostring(pid)].pid, table.unpack(children)) then
|
|
||||||
return false, "You do not own this task"
|
|
||||||
|
|
||||||
elseif tasks[tostring(pid)].status ~= "Z" then
|
|
||||||
return false, "Task must exit to collect status"
|
|
||||||
|
|
||||||
else
|
|
||||||
tasks[tostring(pid)].reapTime = 0
|
|
||||||
return true, tasks[tostring(pid)].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
|
|
||||||
return false, "Task does not exist"
|
|
||||||
|
|
||||||
elseif tasks[tostring(pid)].status == "Z" then
|
|
||||||
return false, "Task is already dead"
|
|
||||||
|
|
||||||
else
|
|
||||||
tasks[tostring(pid)].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
|
|
||||||
return false, "Task does not exist"
|
|
||||||
|
|
||||||
elseif tasks[tostring(pid)].status ~= "R" then
|
|
||||||
return false, "Cannot stop non running task"
|
|
||||||
|
|
||||||
else
|
|
||||||
tasks[tostring(pid)].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
|
|
||||||
return false, "Task does not exist"
|
|
||||||
|
|
||||||
elseif tasks[tostring(pid)].status ~= "T" then
|
|
||||||
return false, "Task is not stopped"
|
|
||||||
|
|
||||||
else
|
|
||||||
tasks[tostring(pid)].status = "R"
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
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
|
|
||||||
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.exit(code)
|
|
||||||
if kernel.config.logTaskExit then
|
|
||||||
if code then
|
|
||||||
kernel.log("Task " .. tostring(kernel.currentTask.pid) .. " exited with code: " .. tostring(code), "INFO")
|
|
||||||
else
|
|
||||||
kernel.log("Task " .. tostring(kernel.currentTask.pid) .. " exited without code", "INFO")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
tasks[tostring(kernel.currentTask.pid)].status = "Z"
|
|
||||||
if type(code) == "number" then
|
|
||||||
tasks[tostring(kernel.currentTask.pid)].exit = code
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function sys.setuid(uid)
|
|
||||||
if kernel.uid ~= 0 then error("EACCES") end
|
|
||||||
kernel.currentTask.uid = uid
|
|
||||||
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.syscallReturn = nil
|
|
||||||
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
|
|
||||||
for _, child in ipairs(task.children) do
|
|
||||||
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 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
|
|
||||||
|
|
||||||
function kernel.main()
|
|
||||||
while not kernel.exitMain do
|
|
||||||
local N = 0
|
|
||||||
local Tmin_hit = 0
|
|
||||||
local Tmax_hit = 0
|
|
||||||
local totalTaskTime = 0
|
|
||||||
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
|
|
||||||
end
|
|
||||||
if task.status == "R" then
|
|
||||||
kernel.currentTask = task
|
|
||||||
kernel.uid = 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
|
|
||||||
local coro = coroutine.create(task.sigh)
|
|
||||||
if kernel.config.preempt then
|
|
||||||
coroutine.resumeWithTimeout(coro, task.timeSlice, table.remove(task.sigq, 1))
|
|
||||||
else
|
|
||||||
coroutine.resume(coro, table.remove(task.sigq, 1))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- check for exit/stop
|
|
||||||
if task.status=="R" then
|
|
||||||
-- measure execution time
|
|
||||||
local startTime = kernel.computer:time()
|
|
||||||
local ret
|
|
||||||
if kernel.config.preempt then
|
|
||||||
ret = {
|
|
||||||
coroutine.resumeWithTimeout(
|
|
||||||
task.coro,
|
|
||||||
task.timeSlice,
|
|
||||||
table.unpack(task.syscallReturn)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else
|
|
||||||
ret = {
|
|
||||||
coroutine.resume(
|
|
||||||
task.coro,
|
|
||||||
table.unpack(task.syscallReturn)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
local elapsed = kernel.computer:time() - startTime
|
|
||||||
task.lastTime = elapsed
|
|
||||||
task.totalTime = (task.totalTime or 0) + elapsed
|
|
||||||
task.numRuns = (task.numRuns or 0) + 1
|
|
||||||
|
|
||||||
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)
|
|
||||||
task.status = "Z"
|
|
||||||
task.exit = "processHandlerException: " .. ret[2]
|
|
||||||
|
|
||||||
elseif ret[1] == "timeout" then
|
|
||||||
task.ivs = task.ivs + 1
|
|
||||||
task.syscallReturn = {}
|
|
||||||
|
|
||||||
elseif ret[1] == "success" or ret[1] == true then
|
|
||||||
task.vs = task.vs + 1
|
|
||||||
|
|
||||||
if ret[2] == "syscall" then
|
|
||||||
if kernel.syscalls[ret[3]] then
|
|
||||||
if kernel.config.debugSyscalls then
|
|
||||||
kernel.log("Task " .. task.pid .. " invoking syscall: " .. ret[3], "DBUG", 5)
|
|
||||||
|
|
||||||
for i = 4, #ret do
|
|
||||||
kernel.log(" inval[" .. tostring(i - 3) .. "] = " .. tostring(ret[i]), "DBUG", 5)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local sysret = {
|
|
||||||
xpcall(kernel.syscalls[ret[3]], 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
|
|
||||||
)
|
|
||||||
|
|
||||||
else
|
|
||||||
kernel.log(
|
|
||||||
"Task " .. task.pid .. " syscall " .. ret[3] .. " completed returning " .. tostring(#sysret - 1) .. " values", "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
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if not sysret[1] then
|
|
||||||
task.syscallReturn = {false, sysret[2]}
|
|
||||||
|
|
||||||
else
|
|
||||||
task.syscallReturn = {
|
|
||||||
true, table.unpack(sysret, 2)
|
|
||||||
}
|
|
||||||
end
|
|
||||||
else
|
|
||||||
task.syscallReturn = {
|
|
||||||
false, "Unknown syscall: " .. tostring(ret[3])
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
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
|
|
||||||
end
|
|
||||||
|
|
||||||
-- clean up dead tasks
|
|
||||||
reapDeadTasks()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
kernel.tasks = tasks
|
|
||||||
kernel.hpv = sys
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
--: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
|
|
||||||
end
|
|
||||||
fn()
|
|
||||||
end
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
--:Minify:--
|
|
||||||
local kernel = ...
|
|
||||||
|
|
||||||
local bit32 = require("bit32")
|
|
||||||
local bor = bit32.bor
|
|
||||||
local lshift = bit32.lshift
|
|
||||||
|
|
||||||
-- 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 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 data = ""
|
|
||||||
for _, e in ipairs(entries) do
|
|
||||||
data = data .. metaEntry(e[1], e[2], e[3], e[4])
|
|
||||||
end
|
|
||||||
|
|
||||||
local ok, err = pcall(function()
|
|
||||||
local f = rootDisk:open(path, "w")
|
|
||||||
f.write(data)
|
|
||||||
f.close()
|
|
||||||
end)
|
|
||||||
if not ok then
|
|
||||||
kernel.log("permissions: failed to write /" .. path .. ": " .. tostring(err), "WARN", 8)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
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},
|
|
||||||
})
|
|
||||||
|
|
||||||
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},
|
|
||||||
})
|
|
||||||
|
|
||||||
writeMeta("/bin/startup", {
|
|
||||||
{"test.lua", 0, 0, RWX_R_R},
|
|
||||||
})
|
|
||||||
|
|
||||||
writeMeta("/etc", {
|
|
||||||
{"passwd", 0, 0, RW_R_R},
|
|
||||||
{"shadow", 0, 0, RW____ },
|
|
||||||
{"pam.d", 0, 0, RWX_R_R},
|
|
||||||
})
|
|
||||||
|
|
||||||
writeMeta("/etc/pam.d", {
|
|
||||||
{"secret", 0, 0, RW____},
|
|
||||||
})
|
|
||||||
|
|
||||||
writeMeta("/sbin", {
|
|
||||||
{"init.lua", 0, 0, RWX_R_R},
|
|
||||||
})
|
|
||||||
|
|
||||||
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},
|
|
||||||
})
|
|
||||||
|
|
||||||
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},
|
|
||||||
})
|
|
||||||
|
|
||||||
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")
|
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
-- :Minify:--
|
-- :Minify:--
|
||||||
|
local kernel = ...
|
||||||
|
kernel.allowGlobalOverwrites = true
|
||||||
|
|
||||||
function string.hasSuffix(str, suffix)
|
function string.hasSuffix(str, suffix)
|
||||||
return string.sub(str, #suffix + 1) == suffix
|
return string.sub(str, #suffix + 1) == suffix
|
||||||
end
|
end
|
||||||
@@ -68,30 +71,24 @@ end
|
|||||||
local function serialize(tbl, seen)
|
local function serialize(tbl, seen)
|
||||||
seen = seen or {}
|
seen = seen or {}
|
||||||
|
|
||||||
-- If we've seen this table before, return a placeholder to prevent infinite loops
|
|
||||||
if seen[tbl] then return '"[Circular Reference]"' end
|
if seen[tbl] then return '"[Circular Reference]"' end
|
||||||
|
|
||||||
-- Mark this table as seen
|
|
||||||
seen[tbl] = true
|
seen[tbl] = true
|
||||||
|
|
||||||
local output = "{"
|
local output = "{"
|
||||||
local first = true
|
local first = true
|
||||||
|
|
||||||
for i, v in pairs(tbl) do
|
for i, v in pairs(tbl) do
|
||||||
-- Handle comma placement more cleanly
|
|
||||||
if not first then output = output .. "," end
|
if not first then output = output .. "," end
|
||||||
first = false
|
first = false
|
||||||
|
|
||||||
-- Serialize Key
|
|
||||||
if type(i) == "string" then
|
if type(i) == "string" then
|
||||||
output = output .. "[\"" .. i .. "\"]="
|
output = output .. "[\"" .. i .. "\"]="
|
||||||
elseif type(i) == "number" then
|
elseif type(i) == "number" then
|
||||||
output = output .. "[" .. tostring(i) .. "]="
|
output = output .. "[" .. tostring(i) .. "]="
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Serialize Value
|
|
||||||
if type(v) == "table" then
|
if type(v) == "table" then
|
||||||
-- Pass the 'seen' table down to the recursive call
|
|
||||||
output = output .. serialize(v, seen)
|
output = output .. serialize(v, seen)
|
||||||
elseif type(v) == "string" then
|
elseif type(v) == "string" then
|
||||||
output = output .. "[=[" .. v .. "]=]"
|
output = output .. "[=[" .. v .. "]=]"
|
||||||
906
Src/Hyperion-kernel/lib/modules/hyperion/10_vfs.kmod
Normal file
906
Src/Hyperion-kernel/lib/modules/hyperion/10_vfs.kmod
Normal file
@@ -0,0 +1,906 @@
|
|||||||
|
-- :Minify:--
|
||||||
|
local kernel = ...
|
||||||
|
local vfs = {}
|
||||||
|
kernel.vfs = vfs
|
||||||
|
vfs.mounts = {["$"] = "/"}
|
||||||
|
vfs.disks = kernel.disks
|
||||||
|
|
||||||
|
-- Metafile format (version 2)
|
||||||
|
-- File header: 1 byte = version (0x02)
|
||||||
|
-- Per-entry:
|
||||||
|
-- 1 byte = name length
|
||||||
|
-- N bytes = name
|
||||||
|
-- 1 byte = entry type (0x00 = regular, 0x01 = symlink)
|
||||||
|
-- 2 bytes = owner uid (little-endian uint16)
|
||||||
|
-- 2 bytes = group gid (little-endian uint16)
|
||||||
|
-- 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 1:
|
||||||
|
-- 1 byte name len, N bytes name, 1 byte etype, 1 byte owner,
|
||||||
|
-- 1 byte group, 2 bytes perms (little-endian), 1 byte cmeta len, N bytes cmeta
|
||||||
|
--
|
||||||
|
-- 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 = 0x02
|
||||||
|
|
||||||
|
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
|
||||||
|
local firstByte = raw:byte(1)
|
||||||
|
if firstByte == 0x02 or firstByte == 0x01 then
|
||||||
|
version = firstByte
|
||||||
|
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 == 0x02 then
|
||||||
|
if p + 6 > #raw then break end
|
||||||
|
etype = raw:byte(p); p = p + 1
|
||||||
|
owner = raw:byte(p) + raw:byte(p+1) * 256; p = p + 2
|
||||||
|
group = raw:byte(p) + raw:byte(p+1) * 256; p = p + 2
|
||||||
|
perms = raw:byte(p) + raw:byte(p+1) * 256; p = p + 2
|
||||||
|
elseif version == 0x01 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
|
||||||
|
local olo = (m.owner or 0) % 256
|
||||||
|
local ohi = math.floor((m.owner or 0) / 256) % 256
|
||||||
|
local glo = (m.group or 0) % 256
|
||||||
|
local ghi = math.floor((m.group or 0) / 256) % 256
|
||||||
|
out = out
|
||||||
|
.. string.char(#name) .. name
|
||||||
|
.. string.char(m.etype or 0x00)
|
||||||
|
.. string.char(olo, ohi, glo, ghi, plo, phi)
|
||||||
|
.. string.char(#m.cmeta) .. m.cmeta
|
||||||
|
end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
local SAFE_COMPONENT_PATTERN = "^[A-Za-z0-9_.+%-%@%(%)%[%]]+$"
|
||||||
|
|
||||||
|
local function normalizePath(path)
|
||||||
|
local task = kernel.currentTask
|
||||||
|
local cwd = task.cwd or "/"
|
||||||
|
|
||||||
|
if path:sub(1, 1) ~= "/" then
|
||||||
|
path = cwd .. "/" .. path
|
||||||
|
end
|
||||||
|
|
||||||
|
local stack = {}
|
||||||
|
local i = 1
|
||||||
|
local len = #path
|
||||||
|
while i <= len do
|
||||||
|
local j = path:find("/", i, true)
|
||||||
|
local comp
|
||||||
|
if j then
|
||||||
|
comp = path:sub(i, j - 1)
|
||||||
|
i = j + 1
|
||||||
|
else
|
||||||
|
comp = path:sub(i)
|
||||||
|
i = len + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
comp = comp:match("^%s*(.-)%s*$")
|
||||||
|
|
||||||
|
if comp == "" or comp == "." then
|
||||||
|
elseif comp == ".." then
|
||||||
|
if #stack > 0 then
|
||||||
|
table.remove(stack)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
comp = comp:lower()
|
||||||
|
if not comp:match(SAFE_COMPONENT_PATTERN) then
|
||||||
|
error("EINVAL: illegal characters in path component: " .. comp, 2)
|
||||||
|
end
|
||||||
|
if comp == ".meta" then
|
||||||
|
error("EINVAL: reserved path component: " .. comp, 2)
|
||||||
|
end
|
||||||
|
table.insert(stack, comp)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local result = "/" .. table.concat(stack, "/")
|
||||||
|
|
||||||
|
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, "/")
|
||||||
|
while table.indexOf(rv, "") ~= -1 do
|
||||||
|
table.remove(rv, table.indexOf(rv, ""))
|
||||||
|
end
|
||||||
|
return rv
|
||||||
|
end
|
||||||
|
|
||||||
|
local function resolveMount(normalPath)
|
||||||
|
local mountPoint, mountId = nil, nil
|
||||||
|
for id, mp in pairs(vfs.mounts) do
|
||||||
|
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 = normalPath:sub(#mountPoint + 1)
|
||||||
|
if diskPath == "" then diskPath = "/" end
|
||||||
|
return vfs.disks[mountId], diskPath
|
||||||
|
end
|
||||||
|
|
||||||
|
vfs._parseMetafile = parseMetafile
|
||||||
|
|
||||||
|
local function readMetaEntry(disk, parentDiskPath, filename)
|
||||||
|
if filename == ".meta" then error("EACCES: Cannot open metafile") end
|
||||||
|
local mp
|
||||||
|
if parentDiskPath == "/" then
|
||||||
|
mp = ".meta"
|
||||||
|
else
|
||||||
|
local p = parentDiskPath:gsub("^/+", "")
|
||||||
|
mp = p .. "/.meta"
|
||||||
|
end
|
||||||
|
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
|
||||||
|
|
||||||
|
if raw and #raw > 0 and raw:byte(1) ~= META_VERSION then
|
||||||
|
local upgraded = makeMetafile(parseMetafile(raw))
|
||||||
|
local wok, wf = pcall(function() return disk:open(mp, "w") end)
|
||||||
|
if wok and wf then wf.write(upgraded); if wf.close then wf.close() end end
|
||||||
|
raw = upgraded
|
||||||
|
end
|
||||||
|
|
||||||
|
local parsed = parseMetafile(raw)
|
||||||
|
return parsed[filename]
|
||||||
|
end
|
||||||
|
|
||||||
|
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 path:gmatch("[^/]+") do table.insert(parts, p) end
|
||||||
|
|
||||||
|
local resolved = ""
|
||||||
|
|
||||||
|
for i, part in ipairs(parts) do
|
||||||
|
local candidate = resolved == "" and ("/" .. part) or (resolved .. "/" .. part)
|
||||||
|
|
||||||
|
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 writeMetaEntry(path, name, entry, noFollow)
|
||||||
|
local real = resolveSymlinks(path, noFollow)
|
||||||
|
local disk, diskPath = resolveMount(real)
|
||||||
|
|
||||||
|
local mp
|
||||||
|
if diskPath == "/" then
|
||||||
|
mp = ".meta"
|
||||||
|
else
|
||||||
|
mp = diskPath:gsub("^/+", "") .. "/.meta"
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
existing[name] = entry
|
||||||
|
|
||||||
|
local f = disk:open(mp, "w")
|
||||||
|
f.write(makeMetafile(existing))
|
||||||
|
if f.close then f.close() end
|
||||||
|
end
|
||||||
|
|
||||||
|
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 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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
local function normalizeMountPoint(path)
|
||||||
|
path = normalizePath(path)
|
||||||
|
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 checkDisk(disk)
|
||||||
|
for _, name in ipairs(required) do
|
||||||
|
if type(disk[name]) ~= "function" then
|
||||||
|
error("Invalid disk: missing method '" .. name .. "'")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
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, id
|
||||||
|
if type(diskOrId) == "string" then
|
||||||
|
disk = kernel.disks[diskOrId]
|
||||||
|
if not disk then error("ENODEV") end
|
||||||
|
checkDisk(disk); id = diskOrId
|
||||||
|
elseif type(diskOrId) == "table" then
|
||||||
|
checkDisk(diskOrId); disk = diskOrId
|
||||||
|
id = disk.address; vfs.disks[id] = disk
|
||||||
|
else error("EINVAL") end
|
||||||
|
|
||||||
|
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)
|
||||||
|
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
|
||||||
|
vfs.mounts[id] = nil; return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
error("EINVAL")
|
||||||
|
end
|
||||||
|
|
||||||
|
function vfs.open(path, mode)
|
||||||
|
checkSystemLimit()
|
||||||
|
local task = kernel.currentTask
|
||||||
|
local fd = allocFD(task)
|
||||||
|
local disk, diskPath = resolvePath(path)
|
||||||
|
if not disk then error("NODISK") end
|
||||||
|
|
||||||
|
local meta = getFileMeta(path)
|
||||||
|
local isNew = (mode == "w" or mode == "a") and not disk:fileExists(diskPath)
|
||||||
|
|
||||||
|
checkperms(meta, mode == "r" and "r" or "w")
|
||||||
|
|
||||||
|
local handle
|
||||||
|
if disk:type(diskPath) ~= "directory" then
|
||||||
|
handle = disk:open(diskPath, mode)
|
||||||
|
if type(handle) ~= "table" then error("ENFILE") end
|
||||||
|
end
|
||||||
|
|
||||||
|
if isNew then
|
||||||
|
local euid = (task and (task.euid or task.uid)) or kernel.uid
|
||||||
|
local egid = (task and task.gid) or 0
|
||||||
|
local norm = normalizePath(path)
|
||||||
|
local parent = norm:match("^(.*)/[^/]+$") or "/"
|
||||||
|
if parent == "" then parent = "/" end
|
||||||
|
local name = norm:match("[^/]+$")
|
||||||
|
if name then
|
||||||
|
local entry = { etype=0x00, owner=euid, group=egid,
|
||||||
|
perms=vfs.PERM.RW_R_R, cmeta="" }
|
||||||
|
pcall(writeMetaEntry, parent, name, entry, false)
|
||||||
|
meta = entry
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
function vfs.read(fd, count)
|
||||||
|
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
|
||||||
|
|
||||||
|
function vfs.write(fd, content)
|
||||||
|
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
|
||||||
|
|
||||||
|
function vfs.pread(fd, count, offset)
|
||||||
|
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 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
|
||||||
|
|
||||||
|
function vfs.lseek(fd, offset, whence)
|
||||||
|
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
|
||||||
|
|
||||||
|
function vfs.fsync(fd)
|
||||||
|
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
|
||||||
|
|
||||||
|
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 and file.handle and file.handle.close then
|
||||||
|
file.handle.close()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function vfs.sendfile(outfd, infd, count)
|
||||||
|
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 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
|
||||||
|
|
||||||
|
function vfs.stat(path)
|
||||||
|
local disk, diskPath = resolvePath(path)
|
||||||
|
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,
|
||||||
|
modified = attrs.modified,
|
||||||
|
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 file = kernel.currentTask.fd[fd]
|
||||||
|
if not file then error("EBADF") end
|
||||||
|
local disk, diskPath = resolvePath(file.path)
|
||||||
|
local attrs = disk:attributes(diskPath)
|
||||||
|
return {
|
||||||
|
size = attrs.size,
|
||||||
|
modified = attrs.modified,
|
||||||
|
created = attrs.created,
|
||||||
|
owner = file.meta.owner,
|
||||||
|
group = file.meta.group,
|
||||||
|
perms = file.meta.perms,
|
||||||
|
etype = file.meta.etype,
|
||||||
|
xattr = file.meta.cmeta,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
function vfs.listdir(path)
|
||||||
|
local disk, diskPath = resolvePath(path)
|
||||||
|
local meta = getFileMeta(path)
|
||||||
|
checkperms(meta, "r")
|
||||||
|
if disk:type(diskPath) ~= "directory" then error("ENOTDIR") end
|
||||||
|
|
||||||
|
local list = disk:list(diskPath)
|
||||||
|
local seen = {}
|
||||||
|
local out = {}
|
||||||
|
for _, v in ipairs(list) do
|
||||||
|
if v ~= ".meta" then
|
||||||
|
seen[v] = true
|
||||||
|
table.insert(out, v)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
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)
|
||||||
|
local norm = normalizePath(path)
|
||||||
|
local parent = norm:match("^(.*)/[^/]+$") or "/"
|
||||||
|
if parent == "" then parent = "/" end
|
||||||
|
local parentMeta = getFileMeta(parent)
|
||||||
|
checkperms(parentMeta, "w")
|
||||||
|
local disk, diskPath = resolvePath(path)
|
||||||
|
disk:makeDirectory(diskPath)
|
||||||
|
local task = kernel.currentTask
|
||||||
|
local euid = (task and (task.euid or task.uid)) or kernel.uid
|
||||||
|
local egid = (task and task.gid) or 0
|
||||||
|
local name = norm:match("[^/]+$")
|
||||||
|
if name then
|
||||||
|
local entry = { etype=0x00, owner=euid, group=egid,
|
||||||
|
perms=vfs.PERM.RWX_RX, cmeta="" }
|
||||||
|
pcall(writeMetaEntry, parent, name, entry, false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function vfs.remove(path)
|
||||||
|
local norm = resolveSymlinks(path, true)
|
||||||
|
local parent = norm:match("^(.*)/[^/]+$") or "/"
|
||||||
|
if parent == "" then parent = "/" end
|
||||||
|
local parentMeta = getFileMeta(parent)
|
||||||
|
checkperms(parentMeta, "w")
|
||||||
|
|
||||||
|
local meta = getFileMeta(path, true)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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 disk, parentDisk = resolveMount(parent)
|
||||||
|
local mp
|
||||||
|
if parentDisk == "/" then
|
||||||
|
mp = ".meta"
|
||||||
|
else
|
||||||
|
mp = parentDisk:gsub("^/+", "") .. "/.meta"
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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 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 _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 file = kernel.currentTask.fd[fd]
|
||||||
|
if not file then error("EBADF") end
|
||||||
|
vfs.chown(file.path, uid, gid)
|
||||||
|
end
|
||||||
|
|
||||||
|
function vfs.exists(path)
|
||||||
|
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 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.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
|
||||||
|
|
||||||
|
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
|
||||||
|
checkSystemLimit()
|
||||||
|
file.refcount = file.refcount + 1
|
||||||
|
task.fd[newfd] = file
|
||||||
|
total = total + 1
|
||||||
|
return newfd
|
||||||
|
end
|
||||||
|
|
||||||
|
function vfs.devctl(fd, method, ...)
|
||||||
|
if not kernel.currentTask.fd[fd] then error("EBADF") end
|
||||||
|
if not kernel.currentTask.fd[fd].handle[method] then error("EINVAL") end
|
||||||
|
return kernel.currentTask.fd[fd].handle[method](...)
|
||||||
|
end
|
||||||
|
|
||||||
|
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["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")
|
||||||
@@ -7,11 +7,9 @@ proxy.address = "tmpfs0000"
|
|||||||
proxy.isvirt = true
|
proxy.isvirt = true
|
||||||
proxy.isReadOnly = function() return false end
|
proxy.isReadOnly = function() return false end
|
||||||
|
|
||||||
-- Space functions (just placeholders)
|
|
||||||
proxy.spaceUsed = function() return 0 end
|
proxy.spaceUsed = function() return 0 end
|
||||||
proxy.spaceTotal = function() return 0 end
|
proxy.spaceTotal = function() return 0 end
|
||||||
|
|
||||||
-- Writable operations
|
|
||||||
proxy.makeDirectory = function(_, path)
|
proxy.makeDirectory = function(_, path)
|
||||||
local steps = kernel.vfs.splitPath(path)
|
local steps = kernel.vfs.splitPath(path)
|
||||||
local step = data
|
local step = data
|
||||||
@@ -52,7 +50,6 @@ proxy.attributes = function(_, path)
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Open files
|
|
||||||
function proxy:open(path, mode)
|
function proxy:open(path, mode)
|
||||||
local steps = kernel.vfs.splitPath(path)
|
local steps = kernel.vfs.splitPath(path)
|
||||||
local step = data
|
local step = data
|
||||||
@@ -76,21 +73,27 @@ function proxy:open(path, mode)
|
|||||||
local chunk = content:sub(pos, pos+amount-1)
|
local chunk = content:sub(pos, pos+amount-1)
|
||||||
pos = pos + #chunk
|
pos = pos + #chunk
|
||||||
return chunk
|
return chunk
|
||||||
end
|
end,
|
||||||
|
close = function() end,
|
||||||
}
|
}
|
||||||
elseif mode == "w" then
|
elseif mode == "w" then
|
||||||
step[filename] = ""
|
step[filename] = ""
|
||||||
|
local buf = {}
|
||||||
return {
|
return {
|
||||||
write = function(str)
|
write = function(str)
|
||||||
step[filename] = str
|
buf[#buf + 1] = str
|
||||||
end
|
end,
|
||||||
|
close = function()
|
||||||
|
step[filename] = table.concat(buf)
|
||||||
|
end,
|
||||||
}
|
}
|
||||||
elseif mode == "a" then
|
elseif mode == "a" then
|
||||||
if type(step[filename]) ~= "string" then step[filename] = "" end
|
if type(step[filename]) ~= "string" then step[filename] = "" end
|
||||||
return {
|
return {
|
||||||
write = function(str)
|
write = function(str)
|
||||||
step[filename] = step[filename] .. str
|
step[filename] = step[filename] .. str
|
||||||
end
|
end,
|
||||||
|
close = function() end,
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
error("EACCES")
|
error("EACCES")
|
||||||
@@ -123,7 +126,8 @@ function proxy:list(path)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function proxy:fileExists(path)
|
function proxy:fileExists(path)
|
||||||
return pcall(function() return self:type(path) end)
|
local t = self:type(path)
|
||||||
|
return t == "file" or t == "directory"
|
||||||
end
|
end
|
||||||
|
|
||||||
kernel.disks["tmpfs0000"] = proxy
|
kernel.disks["tmpfs0000"] = proxy
|
||||||
540
Src/Hyperion-kernel/lib/modules/hyperion/13_loopdev.kmod
Normal file
540
Src/Hyperion-kernel/lib/modules/hyperion/13_loopdev.kmod
Normal file
@@ -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)")
|
||||||
556
Src/Hyperion-kernel/lib/modules/hyperion/20_socket.kmod
Normal file
556
Src/Hyperion-kernel/lib/modules/hyperion/20_socket.kmod
Normal file
@@ -0,0 +1,556 @@
|
|||||||
|
-- :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 sockets = {}
|
||||||
|
local unixSocks = {}
|
||||||
|
local nextSockId = 1
|
||||||
|
|
||||||
|
local function allocSockId()
|
||||||
|
local id = nextSockId
|
||||||
|
nextSockId = nextSockId + 1
|
||||||
|
return id
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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")
|
||||||
395
Src/Hyperion-kernel/lib/modules/hyperion/26_tty.kmod
Normal file
395
Src/Hyperion-kernel/lib/modules/hyperion/26_tty.kmod
Normal file
@@ -0,0 +1,395 @@
|
|||||||
|
-- :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
|
||||||
@@ -3,6 +3,7 @@ local args = {...}
|
|||||||
local kernel = args[1]
|
local kernel = args[1]
|
||||||
kernel._G = _G
|
kernel._G = _G
|
||||||
|
|
||||||
|
|
||||||
local function readonly(tbl)
|
local function readonly(tbl)
|
||||||
return setmetatable({}, {
|
return setmetatable({}, {
|
||||||
__index = function(_, key)
|
__index = function(_, key)
|
||||||
@@ -20,7 +21,7 @@ local function readonly(tbl)
|
|||||||
error("Attempt to modify global variable '" .. k .. "'", 2)
|
error("Attempt to modify global variable '" .. k .. "'", 2)
|
||||||
end,
|
end,
|
||||||
|
|
||||||
__pairs = function()
|
__pairs = function(self)
|
||||||
local function iter(_, key)
|
local function iter(_, key)
|
||||||
local nextKey, value = next(tbl, key)
|
local nextKey, value = next(tbl, key)
|
||||||
if type(value) == "table" then
|
if type(value) == "table" then
|
||||||
@@ -28,7 +29,7 @@ local function readonly(tbl)
|
|||||||
end
|
end
|
||||||
return nextKey, value
|
return nextKey, value
|
||||||
end
|
end
|
||||||
return iter, tbl, nil
|
return iter, self, nil
|
||||||
end,
|
end,
|
||||||
|
|
||||||
__ipairs = function()
|
__ipairs = function()
|
||||||
@@ -49,8 +50,8 @@ local function readonly(tbl)
|
|||||||
__metatable = false
|
__metatable = false
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
local origLoad = load
|
||||||
|
|
||||||
kernel._U = readonly(kernel._G)
|
kernel._U = readonly(kernel._G)
|
||||||
kernel.allowGlobalOverwrites = true
|
|
||||||
kernel._U._G = kernel._U
|
kernel._U._G = kernel._U
|
||||||
kernel.allowGlobalOverwrites = false
|
kernel._U.load = function(a,b,c,d) return origLoad(a,b,c,d or kernel._U) end
|
||||||
@@ -264,11 +264,13 @@ function auth.login(username, password)
|
|||||||
end
|
end
|
||||||
|
|
||||||
kernel.currentUID = uid
|
kernel.currentUID = uid
|
||||||
if kernel.currentProcess then
|
|
||||||
kernel.currentProcess.uid = uid
|
local _task = kernel.currentTask
|
||||||
kernel.currentProcess.euid = uid
|
if _task then
|
||||||
kernel.currentProcess.gid = tonumber(entry[2]) or uid
|
_task.uid = uid
|
||||||
kernel.currentProcess.egid = tonumber(entry[2]) or uid
|
_task.euid = uid
|
||||||
|
_task.gid = tonumber(entry[2]) or uid
|
||||||
|
_task.egid = tonumber(entry[2]) or uid
|
||||||
end
|
end
|
||||||
|
|
||||||
kernel.log("AUTH: login uid=" .. tostring(uid) .. " (" .. username .. ")")
|
kernel.log("AUTH: login uid=" .. tostring(uid) .. " (" .. username .. ")")
|
||||||
@@ -372,7 +374,7 @@ function auth.newUser(username, password, gid, homedir, shell)
|
|||||||
local uid = nextUID()
|
local uid = nextUID()
|
||||||
gid = tonumber(gid) or uid
|
gid = tonumber(gid) or uid
|
||||||
homedir = homedir or ("/home/" .. username)
|
homedir = homedir or ("/home/" .. username)
|
||||||
shell = shell or "/bin/sh"
|
shell = shell or "/bin/hysh"
|
||||||
|
|
||||||
passwd[#passwd + 1] = {
|
passwd[#passwd + 1] = {
|
||||||
tostring(uid),
|
tostring(uid),
|
||||||
@@ -392,6 +394,8 @@ function auth.newUser(username, password, gid, homedir, shell)
|
|||||||
|
|
||||||
if kernel.vfs.mkdir and not kernel.vfs.exists(homedir) then
|
if kernel.vfs.mkdir and not kernel.vfs.exists(homedir) then
|
||||||
kernel.vfs.mkdir(homedir)
|
kernel.vfs.mkdir(homedir)
|
||||||
|
-- Homedir must be owned by the new user, not root
|
||||||
|
pcall(kernel.vfs.chown, homedir, uid, uid)
|
||||||
end
|
end
|
||||||
|
|
||||||
kernel.log("AUTH: new user '" .. username .. "' uid=" .. tostring(uid))
|
kernel.log("AUTH: new user '" .. username .. "' uid=" .. tostring(uid))
|
||||||
@@ -436,11 +440,9 @@ function auth.deleteUser(uid)
|
|||||||
if not entry then return nil, "No such user" end
|
if not entry then return nil, "No such user" end
|
||||||
local username = entry[3]
|
local username = entry[3]
|
||||||
|
|
||||||
-- Remove from passwd
|
|
||||||
for i, v in ipairs(passwd) do
|
for i, v in ipairs(passwd) do
|
||||||
if tonumber(v[1]) == uid then table.remove(passwd, i); break end
|
if tonumber(v[1]) == uid then table.remove(passwd, i); break end
|
||||||
end
|
end
|
||||||
-- Remove from shadow
|
|
||||||
for i, v in ipairs(shadow) do
|
for i, v in ipairs(shadow) do
|
||||||
if tonumber(v[1]) == uid then table.remove(shadow, i); break end
|
if tonumber(v[1]) == uid then table.remove(shadow, i); break end
|
||||||
end
|
end
|
||||||
@@ -463,7 +465,6 @@ function auth.lockUser(uid)
|
|||||||
local sEntry = getShadowByUID(uid)
|
local sEntry = getShadowByUID(uid)
|
||||||
if not sEntry then return nil, "No shadow entry for uid" end
|
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
|
if sEntry[3]:sub(1,1) ~= "!" then
|
||||||
sEntry[3] = "!" .. sEntry[3]
|
sEntry[3] = "!" .. sEntry[3]
|
||||||
end
|
end
|
||||||
@@ -567,9 +568,6 @@ function auth.setGID(uid, gid)
|
|||||||
return true
|
return true
|
||||||
end
|
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)
|
function auth.elevate(targetUsername, password)
|
||||||
if type(targetUsername) ~= "string" or type(password) ~= "string" then
|
if type(targetUsername) ~= "string" or type(password) ~= "string" then
|
||||||
return nil, "Authentication failure"
|
return nil, "Authentication failure"
|
||||||
@@ -593,33 +591,32 @@ function auth.elevate(targetUsername, password)
|
|||||||
return nil, "Authentication failure"
|
return nil, "Authentication failure"
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Directly set the calling task's uid — trusted kernel path
|
|
||||||
local task = kernel.currentTask
|
local task = kernel.currentTask
|
||||||
local prevUid = task.uid
|
local prevUid = task.uid
|
||||||
task.uid = uid
|
task.uid = 0
|
||||||
task.euid = uid
|
task.euid = 0
|
||||||
task.gid = tonumber(entry[2]) or uid
|
task.gid = 0
|
||||||
task.egid = tonumber(entry[2]) or uid
|
task.egid = 0
|
||||||
kernel.uid = uid
|
kernel.uid = 0
|
||||||
|
|
||||||
kernel.log("AUTH: elevate uid=" .. tostring(prevUid) .. " -> " .. tostring(uid) .. " (" .. targetUsername .. ")")
|
kernel.log("AUTH: elevate uid=" .. tostring(prevUid) .. " -> 0 (via " .. targetUsername .. ")")
|
||||||
return true, uid
|
return true, uid
|
||||||
end
|
end
|
||||||
|
|
||||||
if kernel.syscalls then
|
if kernel.syscalls then
|
||||||
kernel.syscalls["auth_login"] = auth.login
|
kernel.syscalls["login"] = auth.login
|
||||||
kernel.syscalls["auth_setpassword"] = auth.setPassword
|
kernel.syscalls["setpassword"] = auth.setPassword
|
||||||
kernel.syscalls["auth_setusername"] = auth.setUsername
|
kernel.syscalls["setusername"] = auth.setUsername
|
||||||
kernel.syscalls["auth_newuser"] = auth.newUser
|
kernel.syscalls["newuser"] = auth.newUser
|
||||||
kernel.syscalls["auth_whoami"] = auth.whoami
|
kernel.syscalls["whoami"] = auth.whoami
|
||||||
kernel.syscalls["auth_getuid"] = auth.getUID
|
kernel.syscalls["getuidbyname"]= auth.getUID
|
||||||
kernel.syscalls["auth_getpasswd"] = auth.getPasswd
|
kernel.syscalls["getpasswd"] = auth.getPasswd
|
||||||
kernel.syscalls["auth_elevate"] = auth.elevate
|
kernel.syscalls["elevate"] = auth.elevate
|
||||||
kernel.syscalls["auth_deleteuser"] = auth.deleteUser
|
kernel.syscalls["deleteuser"] = auth.deleteUser
|
||||||
kernel.syscalls["auth_lockuser"] = auth.lockUser
|
kernel.syscalls["lockuser"] = auth.lockUser
|
||||||
kernel.syscalls["auth_unlockuser"] = auth.unlockUser
|
kernel.syscalls["unlockuser"] = auth.unlockUser
|
||||||
kernel.syscalls["auth_listusers"] = auth.listUsers
|
kernel.syscalls["listusers"] = auth.listUsers
|
||||||
kernel.syscalls["auth_setshell"] = auth.setShell
|
kernel.syscalls["setshell"] = auth.setShell
|
||||||
kernel.syscalls["auth_sethomedir"] = auth.setHomedir
|
kernel.syscalls["sethomedir"] = auth.setHomedir
|
||||||
kernel.syscalls["auth_setgid"] = auth.setGID
|
kernel.syscalls["setgid"] = auth.setGID
|
||||||
end
|
end
|
||||||
478
Src/Hyperion-kernel/lib/modules/hyperion/45_hypervisor.kmod
Normal file
478
Src/Hyperion-kernel/lib/modules/hyperion/45_hypervisor.kmod
Normal file
@@ -0,0 +1,478 @@
|
|||||||
|
--:Minify:--
|
||||||
|
local kernel = ...
|
||||||
|
local tasks = {}
|
||||||
|
local sys = {}
|
||||||
|
local nextpid = 2
|
||||||
|
kernel.exitMain = false
|
||||||
|
|
||||||
|
local resumeWithTimeout = coroutine.resumeWithTimeout
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
tasks[tostring(id)] = {
|
||||||
|
coro = coroutine.create(function()
|
||||||
|
local ok, err = xpcall(func, debug.traceback, table.unpack(args or {}))
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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 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 or kernel.kernelTask,
|
||||||
|
siblings = (kernel.currentTask and kernel.currentTask.children) or kernel.kernelTask.children,
|
||||||
|
syscallReturn = {},
|
||||||
|
cwd = (kernel.currentTask and kernel.currentTask.cwd) or "/",
|
||||||
|
timeSlice = 0,
|
||||||
|
lastTime = 0,
|
||||||
|
totalTime = 0,
|
||||||
|
numRuns = 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
coroutine.yield()
|
||||||
|
end
|
||||||
|
|
||||||
|
function sys.getTask(pid)
|
||||||
|
local task = tasks[tostring(pid)]
|
||||||
|
if not task then return nil 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,
|
||||||
|
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 _, v in ipairs(kernel.currentTask.children) do children[#children+1] = v.pid end
|
||||||
|
|
||||||
|
local task = tasks[tostring(pid)]
|
||||||
|
if not task then
|
||||||
|
return false, "Task does not exist"
|
||||||
|
elseif not isEqualToAny(task.pid, table.unpack(children)) then
|
||||||
|
return false, "You do not own this task"
|
||||||
|
elseif task.status ~= "Z" then
|
||||||
|
return false, "Task must exit to collect status"
|
||||||
|
else
|
||||||
|
task.reapTime = 0
|
||||||
|
return true, task.exit
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function sys.kill(pid)
|
||||||
|
local task = tasks[tostring(pid)]
|
||||||
|
if not task then
|
||||||
|
return false, "Task does not exist"
|
||||||
|
elseif task.status == "Z" then
|
||||||
|
return false, "Task is already dead"
|
||||||
|
end
|
||||||
|
local caller = kernel.currentTask
|
||||||
|
local ceuid = caller and (caller.euid or caller.uid) or kernel.uid
|
||||||
|
if ceuid ~= 0 and task.uid ~= (caller and caller.uid or kernel.uid) then
|
||||||
|
return false, "EPERM"
|
||||||
|
end
|
||||||
|
task.status = "Z"
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function sys.stop(pid)
|
||||||
|
local task = tasks[tostring(pid)]
|
||||||
|
if not task then
|
||||||
|
return false, "Task does not exist"
|
||||||
|
elseif task.status ~= "R" then
|
||||||
|
return false, "Cannot stop non-running task"
|
||||||
|
else
|
||||||
|
task.status = "T"
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function sys.continue(pid)
|
||||||
|
local task = tasks[tostring(pid)]
|
||||||
|
if not task then
|
||||||
|
return false, "Task does not exist"
|
||||||
|
elseif task.status ~= "T" then
|
||||||
|
return false, "Task is not stopped"
|
||||||
|
else
|
||||||
|
task.status = "R"
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function sys.getpid() return kernel.currentTask.pid end
|
||||||
|
function sys.getppid() return kernel.currentTask.parent.pid end
|
||||||
|
|
||||||
|
function sys.getTasks()
|
||||||
|
local ret = {}
|
||||||
|
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, 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(task.pid) .. " exited with code: " .. tostring(code), "INFO")
|
||||||
|
else
|
||||||
|
kernel.log("Task " .. tostring(task.pid) .. " exited without code", "INFO")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
tasks[tostring(task.pid)].status = "Z"
|
||||||
|
if type(code) == "number" then
|
||||||
|
tasks[tostring(task.pid)].exit = code
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function sys.setuid(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 function reapDeadTasks()
|
||||||
|
for pid, task in pairs(tasks) do
|
||||||
|
if task.status == "Z" and not task.reapTime then
|
||||||
|
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
|
||||||
|
|
||||||
|
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.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 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
|
||||||
|
|
||||||
|
function kernel.main()
|
||||||
|
while not kernel.exitMain do
|
||||||
|
local N = 0
|
||||||
|
local Tmin_hit = 0
|
||||||
|
local Tmax_hit = 0
|
||||||
|
local totalTaskTime = 0
|
||||||
|
local taskTimes = {}
|
||||||
|
|
||||||
|
for pid, task in pairs(tasks) do
|
||||||
|
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.euid or task.uid
|
||||||
|
kernel.process = task.name
|
||||||
|
N = N + 1
|
||||||
|
|
||||||
|
task.timeSlice = math.min(Tmax, math.max(Tmin, B / (N ^ alpha)))
|
||||||
|
|
||||||
|
if task.sigq and #task.sigq ~= 0 and task.sigh then
|
||||||
|
local coro = coroutine.create(task.sigh)
|
||||||
|
if kernel.config.preempt then
|
||||||
|
resumeWithTimeout(coro, task.timeSlice, table.remove(task.sigq, 1))
|
||||||
|
else
|
||||||
|
coroutine.resume(coro, table.remove(task.sigq, 1))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if task.status == "R" then
|
||||||
|
local startTime = kernel.computer:time()
|
||||||
|
local ret
|
||||||
|
|
||||||
|
if kernel.config.preempt then
|
||||||
|
ret = { resumeWithTimeout(task.coro, task.timeSlice, table.unpack(task.syscallReturn)) }
|
||||||
|
else
|
||||||
|
ret = { coroutine.resume(task.coro, table.unpack(task.syscallReturn)) }
|
||||||
|
end
|
||||||
|
|
||||||
|
local elapsed = kernel.computer:time() - startTime
|
||||||
|
task.lastTime = elapsed
|
||||||
|
task.totalTime = (task.totalTime or 0) + elapsed
|
||||||
|
task.numRuns = (task.numRuns or 0) + 1
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
if ret[1] == "error" or ret[1] == false then
|
||||||
|
kernel.log("processHandlerException: " .. tostring(ret[2]), "ERROR", 2)
|
||||||
|
task.status = "Z"
|
||||||
|
task.exit = "processHandlerException: " .. tostring(ret[2])
|
||||||
|
|
||||||
|
elseif ret[1] == "timeout" then
|
||||||
|
task.ivs = task.ivs + 1
|
||||||
|
task.syscallReturn = {}
|
||||||
|
|
||||||
|
elseif ret[1] == "success" or ret[1] == true then
|
||||||
|
task.vs = task.vs + 1
|
||||||
|
|
||||||
|
if ret[2] == "syscall" then
|
||||||
|
local scname = ret[3]
|
||||||
|
if kernel.syscalls[scname] then
|
||||||
|
if kernel.config.debugSyscalls then
|
||||||
|
kernel.log("Task " .. task.pid .. " syscall: " .. scname, "DBUG", 5)
|
||||||
|
for i = 4, #ret do
|
||||||
|
kernel.log(" inval[" .. (i-3) .. "] = " .. tostring(ret[i]), "DBUG", 5)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
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 " .. scname .. " failed: " .. tostring(sysret[2]), "ERROR", 2)
|
||||||
|
else
|
||||||
|
kernel.log("Task " .. task.pid .. " syscall " .. scname .. " ok, " .. (#sysret-1) .. " retvals", "DBUG", 5)
|
||||||
|
for i = 2, #sysret do
|
||||||
|
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] }
|
||||||
|
else
|
||||||
|
task.syscallReturn = { true, table.unpack(sysret, 2) }
|
||||||
|
end
|
||||||
|
else
|
||||||
|
task.syscallReturn = { false, "Unknown syscall: " .. tostring(scname) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
@@ -3,6 +3,11 @@ local kernel = ...
|
|||||||
kernel.log("Loading init system...")
|
kernel.log("Loading init system...")
|
||||||
kernel.log("InitPath: " .. kernel.config.initPath)
|
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 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)
|
kernel.vfs.close(handle)
|
||||||
9
Src/Hyperion-kernel/lib/modules/hyperion/91_login.kmod
Normal file
9
Src/Hyperion-kernel/lib/modules/hyperion/91_login.kmod
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
-- :Minify:--
|
||||||
|
local kernel = ...
|
||||||
|
|
||||||
|
kernel.processes.login = function()
|
||||||
|
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
|
||||||
|
end
|
||||||
233
Src/Hyperion-kernel/lib/modules/hyperion/92_permissions.kmod
Normal file
233
Src/Hyperion-kernel/lib/modules/hyperion/92_permissions.kmod
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
-- :Minify:--
|
||||||
|
local kernel = ...
|
||||||
|
|
||||||
|
local P = kernel.vfs.P
|
||||||
|
local PERM = kernel.vfs.PERM
|
||||||
|
|
||||||
|
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 = 0x02
|
||||||
|
local rootDisk = kernel.disks["$"]
|
||||||
|
|
||||||
|
local function makeEntry(name, etype, owner, group, perms, cmeta)
|
||||||
|
cmeta = cmeta or ""
|
||||||
|
local plo = perms % 256
|
||||||
|
local phi = math.floor(perms / 256) % 256
|
||||||
|
local olo = (owner or 0) % 256
|
||||||
|
local ohi = math.floor((owner or 0) / 256) % 256
|
||||||
|
local glo = (group or 0) % 256
|
||||||
|
local ghi = math.floor((group or 0) / 256) % 256
|
||||||
|
return string.char(#name) .. name
|
||||||
|
.. string.char(etype, olo, ohi, glo, ghi, plo, phi)
|
||||||
|
.. string.char(#cmeta) .. cmeta
|
||||||
|
end
|
||||||
|
|
||||||
|
local REG = 0x00
|
||||||
|
|
||||||
|
local function mergeMeta(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 existing = {}
|
||||||
|
local rok, rf = pcall(function() return rootDisk:open(metaPath, "r") end)
|
||||||
|
if rok and rf then
|
||||||
|
local raw = rf.read(65535)
|
||||||
|
if rf.close then rf.close() end
|
||||||
|
existing = (kernel.vfs._parseMetafile and kernel.vfs._parseMetafile(raw)) or {}
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, e in ipairs(entries) do
|
||||||
|
local name = e[1]
|
||||||
|
local etype = e[2] or REG
|
||||||
|
local owner = e[3] or 0
|
||||||
|
local group = e[4] or 0
|
||||||
|
local perms = e[5] or RWX_RX_RX
|
||||||
|
local cmeta = e[6] or ""
|
||||||
|
existing[name] = {
|
||||||
|
etype = etype,
|
||||||
|
owner = owner,
|
||||||
|
group = group,
|
||||||
|
perms = perms,
|
||||||
|
cmeta = cmeta,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local data = string.char(META_VERSION)
|
||||||
|
for name, m in pairs(existing) do
|
||||||
|
data = data .. makeEntry(
|
||||||
|
name,
|
||||||
|
m.etype or REG,
|
||||||
|
m.owner or 0,
|
||||||
|
m.group or 0,
|
||||||
|
m.perms or RWX_RX_RX,
|
||||||
|
m.cmeta or ""
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
local ok, err = pcall(function()
|
||||||
|
local f = rootDisk:open(metaPath, "w")
|
||||||
|
f.write(data)
|
||||||
|
f.close()
|
||||||
|
end)
|
||||||
|
if not ok then
|
||||||
|
kernel.log("permissions: failed to write " .. metaPath .. ": " .. tostring(err), "WARN", 8)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
kernel.log("Seeding filesystem permissions...", "INFO")
|
||||||
|
|
||||||
|
mergeMeta("/", {
|
||||||
|
{"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},
|
||||||
|
})
|
||||||
|
|
||||||
|
mergeMeta("/boot", {
|
||||||
|
{"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},
|
||||||
|
})
|
||||||
|
|
||||||
|
mergeMeta("/boot/cct", {
|
||||||
|
{"boot.lua", REG, 0, 0, RW_R_R},
|
||||||
|
{"initdisks", REG, 0, 0, RW_R_R},
|
||||||
|
{"eeprom", REG, 0, 0, RW_R_R},
|
||||||
|
})
|
||||||
|
|
||||||
|
mergeMeta("/boot/oc", {
|
||||||
|
{"boot.lua", REG, 0, 0, RW_R_R},
|
||||||
|
{"initfs.lua",REG, 0, 0, RW_R_R},
|
||||||
|
{"eeprom", REG, 0, 0, RW_R_R},
|
||||||
|
})
|
||||||
|
|
||||||
|
mergeMeta("/sbin", {
|
||||||
|
{"init.lua", REG, 0, 0, RWX_RX_RX},
|
||||||
|
})
|
||||||
|
|
||||||
|
mergeMeta("/bin", {
|
||||||
|
{"cat", REG, 0, 0, RWX_RX_RX},
|
||||||
|
{"chattr", REG, 0, 0, RWX_RX_RX},
|
||||||
|
{"chgrp", REG, 0, 0, RWX_RX_RX},
|
||||||
|
{"chmod", REG, 0, 0, RWX_RX_RX},
|
||||||
|
{"chown", REG, 0, 0, RWX_RX_RX},
|
||||||
|
{"chroot", 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},
|
||||||
|
{"help", REG, 0, 0, RWX_RX_RX},
|
||||||
|
{"hysh", REG, 0, 0, RWX_RX_RX},
|
||||||
|
{"hyshex", REG, 0, 0, RWX_RX_RX},
|
||||||
|
{"id", REG, 0, 0, RWX_RX_RX},
|
||||||
|
{"install", REG, 0, 0, RWX_RX_RX},
|
||||||
|
{"ln", REG, 0, 0, RWX_RX_RX},
|
||||||
|
{"login", REG, 0, 0, SUID_755 },
|
||||||
|
{"loimgcreate", REG, 0, 0, RWX_RX_RX},
|
||||||
|
{"looptest", REG, 0, 0, RWX_RX_RX},
|
||||||
|
{"losetup", REG, 0, 0, RWX_RX_RX},
|
||||||
|
{"ls", REG, 0, 0, RWX_RX_RX},
|
||||||
|
{"lsusers", REG, 0, 0, RWX_RX_RX},
|
||||||
|
{"lua", REG, 0, 0, RWX_RX_RX},
|
||||||
|
{"luaold", REG, 0, 0, RWX_RX_RX},
|
||||||
|
{"micro", REG, 0, 0, RWX_RX_RX},
|
||||||
|
{"mkdir", REG, 0, 0, RWX_RX_RX},
|
||||||
|
{"mount", REG, 0, 0, RWX_RX_RX},
|
||||||
|
{"passwd", REG, 0, 0, RWX_RX_RX},
|
||||||
|
{"ps", REG, 0, 0, RWX_RX_RX},
|
||||||
|
{"pwd", REG, 0, 0, RWX_RX_RX},
|
||||||
|
{"readlink", REG, 0, 0, RWX_RX_RX},
|
||||||
|
{"sed", REG, 0, 0, RWX_RX_RX},
|
||||||
|
{"socktest", REG, 0, 0, RWX_RX_RX},
|
||||||
|
{"spm", REG, 0, 0, RWX_RX_RX},
|
||||||
|
{"startup", 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},
|
||||||
|
{"umount", REG, 0, 0, RWX_RX_RX},
|
||||||
|
{"useradd", REG, 0, 0, RWX_RX_RX},
|
||||||
|
{"userdel", REG, 0, 0, RWX_RX_RX},
|
||||||
|
{"usermod", REG, 0, 0, RWX_RX_RX},
|
||||||
|
{"whoami", REG, 0, 0, RWX_RX_RX},
|
||||||
|
{"yes", REG, 0, 0, RWX_RX_RX},
|
||||||
|
})
|
||||||
|
|
||||||
|
mergeMeta("/bin/startup", {
|
||||||
|
{"test.lua", REG, 0, 0, RWX_RX_RX},
|
||||||
|
})
|
||||||
|
|
||||||
|
mergeMeta("/lib", {
|
||||||
|
{"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 },
|
||||||
|
})
|
||||||
|
|
||||||
|
mergeMeta("/lib/sys", {
|
||||||
|
{"fs", REG, 0, 0, RW_R_R},
|
||||||
|
{"hpv", REG, 0, 0, RW_R_R},
|
||||||
|
{"ipc", REG, 0, 0, RW_R_R},
|
||||||
|
{"term", REG, 0, 0, RW_R_R},
|
||||||
|
{"init", REG, 0, 0, RW_R_R},
|
||||||
|
})
|
||||||
|
|
||||||
|
mergeMeta("/lib/modules", {
|
||||||
|
{"hyperion", REG, 0, 0, RWX_RX_RX},
|
||||||
|
})
|
||||||
|
|
||||||
|
mergeMeta("/lib/modules/hyperion", {
|
||||||
|
{"01_stdlib.kmod", REG, 0, 0, RW_R_R},
|
||||||
|
{"10_vfs.kmod", REG, 0, 0, RW_R_R},
|
||||||
|
{"11_require.kmod", REG, 0, 0, RW_R_R},
|
||||||
|
{"12_devfs.kmod", REG, 0, 0, RW_R_R},
|
||||||
|
{"12_tmpfs.kmod", REG, 0, 0, RW_R_R},
|
||||||
|
{"13_loopdev.kmod", REG, 0, 0, RW_R_R},
|
||||||
|
{"14_keventd.kmod", REG, 0, 0, RW_R_R},
|
||||||
|
{"19_fstab.kmod", REG, 0, 0, RW_R_R},
|
||||||
|
{"20_signals.kmod", REG, 0, 0, RW_R_R},
|
||||||
|
{"20_socket.kmod", REG, 0, 0, RW_R_R},
|
||||||
|
{"26_tty.kmod", REG, 0, 0, RW_R_R},
|
||||||
|
{"30_userspace.kmod", REG, 0, 0, RW_R_R},
|
||||||
|
{"40_auth.kmod", REG, 0, 0, RW_R_R},
|
||||||
|
{"45_hypervisor.kmod", REG, 0, 0, RW_R_R},
|
||||||
|
{"47_dbg.kmod", REG, 0, 0, RW_R_R},
|
||||||
|
{"50_gpio.kmod", REG, 0, 0, RW_R_R},
|
||||||
|
{"70_stdlibadv.kmod", REG, 0, 0, RW_R_R},
|
||||||
|
{"90_init.kmod", REG, 0, 0, RW_R_R},
|
||||||
|
{"91_login.kmod", REG, 0, 0, RW_R_R},
|
||||||
|
{"92_permissions.kmod", REG, 0, 0, RW_R_R},
|
||||||
|
{"99_final.kmod", REG, 0, 0, RW_R_R},
|
||||||
|
})
|
||||||
|
|
||||||
|
mergeMeta("/etc", {
|
||||||
|
{"passwd", REG, 0, 0, RW_R_R },
|
||||||
|
{"shadow", REG, 0, 0, RW____ },
|
||||||
|
{"pam.d", REG, 0, 0, RWX_RX_RX},
|
||||||
|
})
|
||||||
|
|
||||||
|
mergeMeta("/etc/pam.d", {
|
||||||
|
{"secret", REG, 0, 0, RW____},
|
||||||
|
})
|
||||||
|
|
||||||
|
kernel.log("Filesystem permissions seeded.", "INFO")
|
||||||
2
Src/Hyperion-kernel/lib/modules/hyperion/99_final.kmod
Normal file
2
Src/Hyperion-kernel/lib/modules/hyperion/99_final.kmod
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
local kernel = ...
|
||||||
|
kernel.allowGlobalOverwrites = false
|
||||||
73
build.py
73
build.py
@@ -23,7 +23,11 @@ import sys
|
|||||||
import shutil
|
import shutil
|
||||||
import argparse
|
import argparse
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import hashlib
|
||||||
|
import random
|
||||||
|
import string
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
PROJECT_ROOT = Path(__file__).resolve().parent
|
PROJECT_ROOT = Path(__file__).resolve().parent
|
||||||
SRC_ROOT = PROJECT_ROOT / "Src"
|
SRC_ROOT = PROJECT_ROOT / "Src"
|
||||||
@@ -111,7 +115,7 @@ def has_minify_header(path: Path) -> bool:
|
|||||||
return False
|
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()
|
clean()
|
||||||
BUILD_ROOT.mkdir()
|
BUILD_ROOT.mkdir()
|
||||||
|
|
||||||
@@ -136,9 +140,21 @@ def main():
|
|||||||
help="Release build: eeprom placed as startup.lua (default)")
|
help="Release build: eeprom placed as startup.lua (default)")
|
||||||
parser.add_argument("--dev", dest="release", action="store_false",
|
parser.add_argument("--dev", dest="release", action="store_false",
|
||||||
help="Dev build: boot.lua and eeprom copied unchanged")
|
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()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.makeuser and args.release:
|
||||||
|
parser.error("--makeuser is only allowed with --dev builds")
|
||||||
|
|
||||||
if args.target == "clean":
|
if args.target == "clean":
|
||||||
clean()
|
clean()
|
||||||
return
|
return
|
||||||
@@ -147,8 +163,63 @@ def main():
|
|||||||
include_test = "test" in args.target
|
include_test = "test" in args.target
|
||||||
|
|
||||||
run_build(minify=minify, include_test=include_test, arch=args.arch, release=args.release)
|
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.")
|
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__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
13
building.md
13
building.md
@@ -53,13 +53,24 @@ Optional arguments:
|
|||||||
* **`--dev`**
|
* **`--dev`**
|
||||||
|
|
||||||
* Development mode
|
* Development mode
|
||||||
* Bootloader does not start automatically
|
* Bootloader does not start automatically. You must run `eeprom` in CraftOS to start Hyperion.
|
||||||
|
|
||||||
* **`--release`** (default)
|
* **`--release`** (default)
|
||||||
|
|
||||||
* Release mode
|
* Release mode
|
||||||
* Bootloader starts automatically
|
* 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**
|
**Examples**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -1 +1,9 @@
|
|||||||
# Contributors
|
# Contributors
|
||||||
|
- Astronand (dc astronand; web astronand.dev)
|
||||||
|
- Lead developer, creator of Hyperion
|
||||||
|
- GHXX (dc ghxx)
|
||||||
|
- Pentesting, AC development
|
||||||
|
- Ryan (dc ryan.s.t)
|
||||||
|
- UI design and implementation, shell design
|
||||||
|
- SpSf/SpartanSoftware (dc swoshswosh_01578)
|
||||||
|
- Optimization, systems development
|
||||||
Reference in New Issue
Block a user