1048 lines
33 KiB
Plaintext
1048 lines
33 KiB
Plaintext
--: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 tokenizePath(path)
|
|
local isAbsolute = (path:sub(1,1) == "/")
|
|
local tokens = {}
|
|
for comp in (path .. "/"):gmatch("([^/]*)/") do
|
|
table.insert(tokens, comp)
|
|
end
|
|
return isAbsolute, tokens
|
|
end
|
|
|
|
local function validateComponent(comp)
|
|
local lower = comp:lower()
|
|
if not lower:match(SAFE_COMPONENT_PATTERN) then
|
|
error("EINVAL: illegal characters in path component: " .. comp, 3)
|
|
end
|
|
if lower == ".meta" then
|
|
error("EINVAL: reserved path component: .meta", 3)
|
|
end
|
|
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 namei(path, noFollow, symDepth)
|
|
symDepth = symDepth or 0
|
|
if symDepth > MAX_SYMLINK then error("ELOOP") end
|
|
|
|
local task = kernel.currentTask
|
|
local euid = (task and (task.euid or task.uid)) or kernel.uid
|
|
local groups = (task and task.groups) or kernel.groups or {}
|
|
local root = (task and task.root) or "/"
|
|
local cwd = (task and task.cwd) or "/"
|
|
|
|
if root ~= "/" and root:sub(-1) == "/" then root = root:sub(1,-2) end
|
|
|
|
local function canTraverse(entry)
|
|
if euid == 0 then return true end
|
|
if not entry then return true end
|
|
local bits = entry.perms
|
|
if euid == entry.owner and bit_is_set(bits, 9) then return true end
|
|
if entry.group then
|
|
for _, gid in ipairs(groups) do
|
|
if gid == entry.group and bit_is_set(bits, 8) then return true end
|
|
end
|
|
end
|
|
return bit_is_set(bits, 7)
|
|
end
|
|
|
|
local isAbsolute, tokens = tokenizePath(path)
|
|
|
|
local stack = {}
|
|
|
|
if isAbsolute then
|
|
stack = {}
|
|
else
|
|
for seg in cwd:gmatch("[^/]+") do table.insert(stack, seg) end
|
|
end
|
|
|
|
local i = 1
|
|
while i <= #tokens do
|
|
local comp = tokens[i]
|
|
i = i + 1
|
|
|
|
comp = comp:match("^%s*(.-)%s*$")
|
|
|
|
if comp == "" or comp == "." then
|
|
elseif comp == ".." then
|
|
local currentPath = "/" .. table.concat(stack, "/")
|
|
|
|
local jailStack = {}
|
|
if root ~= "/" then
|
|
for seg in root:gmatch("[^/]+") do table.insert(jailStack, seg) end
|
|
end
|
|
|
|
if #stack <= #jailStack then
|
|
stack = {}
|
|
for _, seg in ipairs(jailStack) do table.insert(stack, seg) end
|
|
else
|
|
local exitName = stack[#stack]
|
|
local parentPath = "/" .. table.concat(stack, "/", 1, #stack - 1)
|
|
if parentPath == "/" then parentPath = "/" end
|
|
|
|
local okM, diskM, dpM = pcall(resolveMount, parentPath == "" and "/" or parentPath)
|
|
if okM and diskM then
|
|
local entry = readMetaEntry(diskM, dpM, exitName)
|
|
if entry then
|
|
if entry.etype ~= 0x00 then
|
|
error("ENOTDIR: not a directory: " .. currentPath)
|
|
end
|
|
if not canTraverse(entry) then
|
|
error("EACCES: permission denied traversing " .. currentPath)
|
|
end
|
|
else
|
|
local okD, diskD, dpD = pcall(resolveMount, currentPath)
|
|
if okD and diskD then
|
|
local dtype = diskD:type(dpD)
|
|
if dtype ~= nil and dtype ~= "directory" then
|
|
error("ENOTDIR: not a directory: " .. currentPath)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
table.remove(stack)
|
|
end
|
|
|
|
else
|
|
validateComponent(comp)
|
|
local lname = comp:lower()
|
|
|
|
local curPath = "/" .. table.concat(stack, "/")
|
|
|
|
local okM, diskM, dpM = pcall(resolveMount, curPath == "/" and "/" or curPath)
|
|
local entry = nil
|
|
if okM and diskM then
|
|
entry = readMetaEntry(diskM, dpM, lname)
|
|
end
|
|
|
|
local isFinal = (i > #tokens)
|
|
|
|
if entry and entry.etype == 0x01 then
|
|
if isFinal and noFollow then
|
|
table.insert(stack, lname)
|
|
else
|
|
symDepth = symDepth + 1
|
|
if symDepth > MAX_SYMLINK then error("ELOOP") end
|
|
|
|
local target = entry.cmeta
|
|
if not target or target == "" then
|
|
error("ENOENT: empty symlink target")
|
|
end
|
|
|
|
local symIsAbs, symTokens = tokenizePath(target)
|
|
|
|
if symIsAbs then
|
|
stack = {}
|
|
if root ~= "/" then
|
|
for seg in root:gmatch("[^/]+") do table.insert(stack, seg) end
|
|
end
|
|
end
|
|
|
|
local fresh = {}
|
|
for j = 1, i - 2 do table.insert(fresh, tokens[j]) end
|
|
local insertAt = #fresh + 1
|
|
for _, t in ipairs(symTokens) do table.insert(fresh, t) end
|
|
for j = i, #tokens do table.insert(fresh, tokens[j]) end
|
|
tokens = fresh
|
|
i = insertAt
|
|
end
|
|
else
|
|
table.insert(stack, lname)
|
|
|
|
if not isFinal then
|
|
local newPath = "/" .. table.concat(stack, "/")
|
|
local okD, diskD, dpD = pcall(resolveMount, newPath)
|
|
if okD and diskD then
|
|
local dtype = diskD:type(dpD)
|
|
if dtype ~= nil and dtype ~= "directory" then
|
|
error("ENOTDIR: not a directory: " .. newPath)
|
|
end
|
|
end
|
|
if not canTraverse(entry) then
|
|
error("EACCES: permission denied traversing " .. newPath)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local result = "/" .. table.concat(stack, "/")
|
|
|
|
if root ~= "/" then
|
|
if result ~= root and result:sub(1, #root + 1) ~= root .. "/" then
|
|
result = root
|
|
end
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
local function normalizePath(path)
|
|
local task = kernel.currentTask
|
|
local cwd = (task and task.cwd) or "/"
|
|
local root = (task and task.root) or "/"
|
|
if root ~= "/" and root:sub(-1) == "/" then root = root:sub(1,-2) end
|
|
|
|
local isAbsolute, tokens = tokenizePath(path)
|
|
local stack = {}
|
|
|
|
if not isAbsolute then
|
|
for seg in cwd:gmatch("[^/]+") do table.insert(stack, seg) end
|
|
end
|
|
|
|
local jailStack = {}
|
|
if root ~= "/" then
|
|
for seg in root:gmatch("[^/]+") do table.insert(jailStack, seg) end
|
|
end
|
|
|
|
for _, comp in ipairs(tokens) do
|
|
comp = comp:match("^%s*(.-)%s*$")
|
|
if comp == "" or comp == "." then
|
|
elseif comp == ".." then
|
|
if #stack > #jailStack then
|
|
table.remove(stack)
|
|
end
|
|
else
|
|
table.insert(stack, comp:lower())
|
|
end
|
|
end
|
|
|
|
local result = "/" .. table.concat(stack, "/")
|
|
if root ~= "/" then
|
|
if result ~= root and result:sub(1, #root + 1) ~= root .. "/" then
|
|
result = root
|
|
end
|
|
end
|
|
return result
|
|
end
|
|
|
|
local function resolvePath(path, noFollow)
|
|
local real = namei(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 = namei(path, noFollow)
|
|
|
|
if real == "/" then
|
|
return { etype = 0x00, owner = 0, group = 0, perms = 62, cmeta = "" }
|
|
end
|
|
|
|
local cur = real
|
|
|
|
-- FML i hated implementing this - Astronand
|
|
while true do
|
|
local parent, name = cur:match("^(.*)/([^/]+)$")
|
|
if not parent or parent == "" then parent = "/" end
|
|
|
|
local disk, parentDiskPath = resolveMount(parent)
|
|
local entry = readMetaEntry(disk, parentDiskPath, name)
|
|
|
|
if entry then
|
|
return entry
|
|
end
|
|
|
|
if parent == "/" or cur == "/" then
|
|
break
|
|
end
|
|
|
|
cur = parent
|
|
end
|
|
|
|
return { etype = 0x00, owner = 0, group = 0, perms = 63, cmeta = "" }
|
|
end
|
|
|
|
local function writeMetaEntry(path, name, entry, noFollow)
|
|
local real = namei(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
|
|
if disk.isvirt then fobj.isvirt=true 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
|
|
if not task.fd[fd].isvirt then
|
|
total = total - 1
|
|
end
|
|
task.fd[fd] = nil
|
|
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 = namei(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 = namei(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 = namei(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")
|