Files
HyperionOS/Src/Hyperion-kernel/lib/modules/Hyperion/10_vfs.kmod

630 lines
16 KiB
Plaintext

-- :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")