forked from Hyperion/HyperionOS
530 lines
13 KiB
Plaintext
530 lines
13 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, "/") .. (path:sub(-1) == "/" and "/" or "")
|
|
end
|
|
|
|
-- Resolve mount and disk path
|
|
local function resolvePath(path)
|
|
path = normalizePath(path)
|
|
|
|
local mountPoint = "/"
|
|
local mountId = "$"
|
|
for k,v in pairs(vfs.mounts) do
|
|
if path:sub(1,#v) == v and #v > #mountPoint then
|
|
mountPoint = v
|
|
mountId = k
|
|
end
|
|
end
|
|
|
|
local diskPath = path:sub(#mountPoint)
|
|
if diskPath == "" then diskPath = "/" end
|
|
|
|
return vfs.disks[mountId], diskPath
|
|
end
|
|
|
|
-- Allocate file descriptor for current task
|
|
local function allocFD(task)
|
|
local fd = 0
|
|
while task.fd[fd] do fd = fd + 1 end
|
|
if fd >= kernel.config.maxFilesPerTask then error("ENFILE") end
|
|
return fd
|
|
end
|
|
|
|
-- System-wide open file limit
|
|
local total = 0
|
|
local function checkSystemLimit()
|
|
if total >= kernel.config.maxOpenFiles - 16 then error("ENFILE") end
|
|
end
|
|
|
|
-- File object constructor
|
|
local function newFileObj(handle, mode, path, meta)
|
|
return {
|
|
handle = handle,
|
|
mode = mode,
|
|
path = path,
|
|
meta = meta
|
|
}
|
|
end
|
|
|
|
-- Validate mode
|
|
local function ismode(mode)
|
|
if not (mode == "r" or mode == "w" or mode == "a") then error("EINVAL") end
|
|
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=62, 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]
|
|
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
|
|
|
|
function vfs.mount(target, diskOrId)
|
|
if kernel.uid ~= 0 then error("EPERM") end
|
|
if not target then error("EINVAL") end
|
|
|
|
target = normalizeMountPoint(target)
|
|
|
|
local disk
|
|
local id
|
|
|
|
if type(diskOrId) == "string" then
|
|
disk = kernel.disks[diskOrId]
|
|
id = diskOrId
|
|
if not disk then error("ENODEV") end
|
|
elseif type(diskOrId) == "table" then
|
|
disk = diskOrId
|
|
id = tostring(disk)
|
|
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)
|
|
ismode(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 = disk:open(diskPath, mode)
|
|
task.fd[fd] = newFileObj(handle, mode, path, meta)
|
|
total = total + 1
|
|
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 file.mode ~= "r" then error("EBADF") end
|
|
return file.handle.read(count or 1)
|
|
end
|
|
|
|
-- Write
|
|
function vfs.write(fd, content)
|
|
local task = kernel.currentTask
|
|
local file = task.fd[fd]
|
|
if not file then error("EBADF") end
|
|
if file.mode ~= "w" and file.mode ~= "a" 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 file.mode ~= "r" then error("EBADF") end
|
|
file.handle.seek("set", offset)
|
|
return file.handle.read(count or 1)
|
|
end
|
|
|
|
function vfs.pwrite(fd, content, offset)
|
|
local task = kernel.currentTask
|
|
local file = task.fd[fd]
|
|
if not file then error("EBADF") end
|
|
if file.mode ~= "w" and file.mode ~= "a" 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
|
|
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 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
|
|
file.handle.close()
|
|
task.fd[fd] = nil
|
|
total = total - 1
|
|
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 inFile.mode ~= "r" then error("EBADF") end
|
|
if outFile.mode ~= "w" and outFile.mode ~= "a" then error("EBADF") end
|
|
local data = inFile.handle.read(count or 1024)
|
|
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("ENOTDIR") end
|
|
local meta = getFileMeta(path)
|
|
checkperms(meta, "r")
|
|
return disk:list(diskPath)
|
|
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)
|
|
checkperms(meta, "w")
|
|
|
|
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)
|
|
checkperms(meta, "w")
|
|
|
|
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
|
|
|
|
-- 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
|
|
|
|
kernel.log("VFS module loaded") |