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