forked from Hyperion/HyperionOS
351 lines
9.8 KiB
Plaintext
351 lines
9.8 KiB
Plaintext
local args = {...}
|
|
local kernel = args[1]
|
|
local fs = {}
|
|
|
|
-- =====================================================================
|
|
-- INTERNAL STATE
|
|
-- =====================================================================
|
|
|
|
local disks = {} -- address → disk object
|
|
local mounts = {["/"] = "$"} -- mountpoint → disk address (root = boot disk)
|
|
local cwd = "/"
|
|
|
|
local SYMLINK_PREFIX = "#!@SYMLINK["
|
|
local SYMLINK_SUFFIX = "]"
|
|
local SYMLINK_MAX_DEPTH = 64
|
|
|
|
|
|
-- =====================================================================
|
|
-- PATH NORMALIZATION
|
|
-- =====================================================================
|
|
|
|
local function splitPath(p)
|
|
local t = {}
|
|
for part in p:gmatch("[^/]+") do t[#t+1] = part end
|
|
return t
|
|
end
|
|
|
|
local function normalizePath(path)
|
|
if not path or path == "" then return "/" end
|
|
|
|
-- cwd
|
|
if path:sub(1,1) ~= "/" then
|
|
path=cwd..path
|
|
end
|
|
|
|
|
|
local parts = splitPath(path)
|
|
local out = {}
|
|
|
|
for _,part in ipairs(parts) do
|
|
if part == ".." then
|
|
if #out > 0 then table.remove(out) end
|
|
elseif part ~= "." and part ~= "" then
|
|
out[#out+1] = part
|
|
end
|
|
end
|
|
|
|
return "/" .. table.concat(out, "/")
|
|
end
|
|
|
|
|
|
-- =====================================================================
|
|
-- DISK & MOUNT RESOLUTION
|
|
-- =====================================================================
|
|
|
|
-- Finds which disk owns an absolute path
|
|
local function resolveMount(abs)
|
|
local best = "/"
|
|
|
|
for mount, addr in pairs(mounts) do
|
|
if abs:sub(1, #mount) == mount then
|
|
if #mount > #best then
|
|
best = mount
|
|
end
|
|
end
|
|
end
|
|
|
|
local disk = disks[mounts[best]]
|
|
if not disk then
|
|
error("No disk registered for mount: " .. best)
|
|
end
|
|
|
|
local sub = abs:sub(#best + 1)
|
|
if sub == "" then sub = "/" end
|
|
return disk, sub
|
|
end
|
|
|
|
|
|
|
|
-- =====================================================================
|
|
-- SYMLINK HANDLING
|
|
-- =====================================================================
|
|
|
|
local function isSymlinkRaw(disk, p)
|
|
if not disk:fileExists(p) then return false end
|
|
local text = disk:readAllText(p)
|
|
return text and text:sub(1, #SYMLINK_PREFIX) == SYMLINK_PREFIX
|
|
end
|
|
|
|
local function readSymlinkRaw(disk, p)
|
|
local text = disk:readAllText(p)
|
|
if not text then return nil end
|
|
if text:sub(1, #SYMLINK_PREFIX) ~= SYMLINK_PREFIX then return nil end
|
|
|
|
local t = text:sub(#SYMLINK_PREFIX + 1)
|
|
if t:sub(-1) == SYMLINK_SUFFIX then
|
|
t = t:sub(1, -2)
|
|
end
|
|
return t
|
|
end
|
|
|
|
|
|
-- =====================================================================
|
|
-- FULL PATH RESOLUTION (FOLLOWS SYMLINKS)
|
|
-- =====================================================================
|
|
|
|
local function resolveSymlink(path)
|
|
local abs = normalizePath(path)
|
|
local parts = splitPath(abs)
|
|
local out = {}
|
|
|
|
local depth = 0
|
|
local idx = 1
|
|
|
|
while idx <= #parts do
|
|
local comp = parts[idx]
|
|
|
|
if comp == "." then
|
|
-- nothing
|
|
elseif comp == ".." then
|
|
if #out > 0 then table.remove(out) end
|
|
else
|
|
local curAbs = "/" .. table.concat(out, "/")
|
|
if curAbs ~= "/" then curAbs = curAbs .. "/" end
|
|
curAbs = curAbs .. comp
|
|
|
|
local disk, dpath = resolveMount(curAbs)
|
|
|
|
if isSymlinkRaw(disk, dpath) then
|
|
depth = depth + 1
|
|
if depth > SYMLINK_MAX_DEPTH then
|
|
error("Too many symlink levels: " .. path)
|
|
end
|
|
|
|
local target = readSymlinkRaw(disk, dpath)
|
|
if target then
|
|
local newAbs
|
|
|
|
if target:sub(1,1) == "/" then
|
|
-- absolute target
|
|
newAbs = normalizePath(target)
|
|
else
|
|
-- relative to current out[]
|
|
local tmp = "/" .. table.concat(out, "/")
|
|
if tmp ~= "/" then tmp = tmp .. "/" end
|
|
tmp = tmp .. target
|
|
newAbs = normalizePath(tmp)
|
|
end
|
|
|
|
-- rebuild remaining parts
|
|
local remaining = {}
|
|
for j = idx + 1, #parts do
|
|
remaining[#remaining+1] = parts[j]
|
|
end
|
|
|
|
-- restart symlink resolution with new path
|
|
abs = newAbs
|
|
parts = splitPath(abs)
|
|
|
|
-- append remaining
|
|
for _,x in ipairs(remaining) do parts[#parts+1] = x end
|
|
|
|
out = {}
|
|
idx = 0
|
|
else
|
|
out[#out+1] = comp
|
|
end
|
|
else
|
|
out[#out+1] = comp
|
|
end
|
|
end
|
|
|
|
idx = idx + 1
|
|
end
|
|
|
|
local finalAbs = "/" .. table.concat(out, "/")
|
|
return resolveMount(finalAbs)
|
|
end
|
|
|
|
|
|
|
|
-- =====================================================================
|
|
-- PUBLIC API
|
|
-- =====================================================================
|
|
|
|
-- MOUNT OPERATIONS -----------------------------------------------------
|
|
|
|
function fs.virtDisk(diskObj)
|
|
if kernel.uid ~= 0 then error("Permission Denied") end
|
|
if disks[diskObj.address] then
|
|
error("Disk exists: " .. diskObj.address)
|
|
end
|
|
disks[diskObj.address] = diskObj
|
|
end
|
|
|
|
function fs.mount(disk, mountPoint)
|
|
if kernel.uid ~= 0 then error("Permission Denied") end
|
|
mountPoint = normalizePath(mountPoint)
|
|
|
|
local drive, path = resolveMount(normalizePath(mountPoint))
|
|
if not drive:directoryExists(path) then error("Must mount on folder") end
|
|
|
|
if mountPoint ~= "/" and mounts[mountPoint] then
|
|
error("Already mounted: " .. mountPoint)
|
|
end
|
|
|
|
mounts[mountPoint] = disk
|
|
end
|
|
|
|
function fs.unmount(mountPoint)
|
|
if kernel.uid ~= 0 then error("Permission Denied") end
|
|
mountPoint = normalizePath(mountPoint)
|
|
|
|
if mountPoint == "/" then error("Cannot unmount root") end
|
|
mounts[mountPoint] = nil
|
|
end
|
|
|
|
function fs.eject(addr)
|
|
if kernel.uid ~= 0 then error("Permission Denied") end
|
|
disks[addr] = nil
|
|
end
|
|
|
|
function fs.isMount(path)
|
|
if mounts[normalizePath(path)] then return true, mounts[normalizePath(path)] end
|
|
end
|
|
|
|
-- SYMLINK API ----------------------------------------------------------
|
|
|
|
function fs.symlink(target, linkPath)
|
|
kernel.log("WARNING: Symlinks are a untested feature if you find any bugs please report them to https://git.astronand.dev/Hyperion/HyperionOS","WARN")
|
|
local disk, p = resolveMount(normalizePath(linkPath))
|
|
return disk:writeAllText(p, SYMLINK_PREFIX .. target .. SYMLINK_SUFFIX)
|
|
end
|
|
|
|
function fs.isLink(path)
|
|
local disk, p = resolveMount(normalizePath(path))
|
|
return isSymlinkRaw(disk, p)
|
|
end
|
|
|
|
function fs.readLink(path)
|
|
local disk, p = resolveMount(normalizePath(path))
|
|
return readSymlinkRaw(disk, p)
|
|
end
|
|
|
|
|
|
-- FILE OPERATIONS ------------------------------------------------------
|
|
|
|
function fs.exists(path, ...)
|
|
local disk, p = resolveSymlink(path)
|
|
return disk:fileExists(p, ...) or disk:directoryExists(p, ...)
|
|
end
|
|
|
|
function fs.isFile(path, ...)
|
|
local disk, p = resolveSymlink(path)
|
|
return disk:fileExists(p, ...)
|
|
end
|
|
|
|
function fs.isDir(path, ...)
|
|
local disk, p = resolveSymlink(path)
|
|
return disk:directoryExists(p, ...)
|
|
end
|
|
|
|
function fs.list(path, ...)
|
|
local disk, p = resolveSymlink(path)
|
|
return disk:list(p, ...)
|
|
end
|
|
|
|
function fs.makeDir(path, ...)
|
|
local disk, p = resolveSymlink(path)
|
|
return disk:makeDirectory(p, ...)
|
|
end
|
|
|
|
-- remove does NOT follow symlinks (UNIX semantics)
|
|
function fs.remove(path, ...)
|
|
if fs.isMount(path) then return "Cannot delete mounted folder" end
|
|
local abs = normalizePath(path)
|
|
local disk, p = resolveMount(abs)
|
|
return disk:remove(p, ...)
|
|
end
|
|
|
|
function fs.readAllText(path, ...)
|
|
local disk, p = resolveSymlink(path)
|
|
return disk:readAllText(p, ...)
|
|
end
|
|
|
|
function fs.writeAllText(path, text, ...)
|
|
local disk, p = resolveSymlink(path)
|
|
return disk:writeAllText(p, text, ...)
|
|
end
|
|
|
|
function fs.appendAllText(path, text, ...)
|
|
local disk, p = resolveSymlink(path)
|
|
return disk:appendAllText(p, text, ...)
|
|
end
|
|
|
|
function fs.load(path, name, ...)
|
|
return load(fs.readAllText(path, ...), name or path, nil, kernel._U)
|
|
end
|
|
|
|
function fs.getSize(path, ...)
|
|
local disk, p = resolveSymlink(path)
|
|
return disk:getSize(p, ...)
|
|
end
|
|
|
|
-- =====================================================================
|
|
-- WD STUFF
|
|
-- =====================================================================
|
|
|
|
function fs.setCwd(path)
|
|
if not path or path == "" then return "/" end
|
|
|
|
-- ensure absolute
|
|
if path:sub(1,1) ~= "/" then
|
|
path = "/" .. path
|
|
end
|
|
|
|
local parts = splitPath(path)
|
|
local out = {}
|
|
|
|
for _,part in ipairs(parts) do
|
|
if part == ".." then
|
|
if #out > 0 then table.remove(out) end
|
|
elseif part ~= "." and part ~= "" then
|
|
out[#out+1] = part
|
|
end
|
|
end
|
|
|
|
cwd="/" .. table.concat(out, "/")
|
|
end
|
|
|
|
function fs.getCwd(path)
|
|
return cwd
|
|
end
|
|
|
|
-- =====================================================================
|
|
-- INIT
|
|
-- =====================================================================
|
|
|
|
kernel.log("Loading disks for vfs")
|
|
local ok,err = xpcall(function()
|
|
for _,v in kernel.initdisks.list() do
|
|
fs.virtDisk(v)
|
|
end
|
|
end, debug.traceback)
|
|
if not ok then kernel.panic(err) end
|
|
|
|
kernel.disks=disks
|
|
kernel.mounts=mounts
|
|
kernel.fs = fs
|
|
kernel.cache.preload.fs = fs
|
|
kernel.cache.preload.filesystem = kernel.cache.preload.fs |