541 lines
19 KiB
Plaintext
541 lines
19 KiB
Plaintext
--:Minify:--
|
|
-- Loop device driver:
|
|
--
|
|
-- BIND (directory) - re-routes VFS calls into a host directory subtree.
|
|
-- Identical to the original behaviour.
|
|
--
|
|
-- IMAGE (*.hfs file) - mounts a Hyperion Filesystem Image. The image is
|
|
-- loaded entirely into memory; reads and writes operate
|
|
-- on the in-memory tree, so the image file is only
|
|
-- touched on attach/detach.
|
|
--
|
|
-- BHFS v1 - Binary Hyperion Filesystem Image format:
|
|
--
|
|
-- File header (8 bytes):
|
|
-- [0-3] magic: 0x42 0x48 0x46 0x53 ("BHFS")
|
|
-- [4] version: 0x01
|
|
-- [5] flags: bit0 = per-file deflate compression enabled
|
|
-- [6-7] reserved: 0x00 0x00
|
|
--
|
|
-- Records (repeated until END record):
|
|
-- [0] type: 0x01=file 0x02=dir 0x03=symlink 0xFF=end
|
|
-- [1-4] path_len (uint32 LE) - byte length of the path string
|
|
-- [5-8] raw_size (uint32 LE) - original uncompressed data size (0 for dirs)
|
|
-- [9-12] stored_size (uint32 LE) - bytes that follow in stream
|
|
-- (< raw_size means deflate-compressed;
|
|
-- = raw_size means stored as-is)
|
|
-- [13 .. 13+path_len-1] path bytes (no null terminator)
|
|
-- [.. +stored_size] data bytes
|
|
--
|
|
-- Dirs have raw_size=0, stored_size=0, zero data bytes.
|
|
-- Symlinks store the target path as data; stored_size == raw_size (no compression).
|
|
--
|
|
-- Syscalls:
|
|
-- id = syscall.losetup(path) attach dir OR .hfs image
|
|
-- id = syscall.losetup(path, true) force image mode
|
|
-- syscall.lodetach(id) detach (must be unmounted first)
|
|
-- tbl = syscall.lolist() {id -> {path,mode}, ...}
|
|
-- str = syscall.loimgcreate(srcdir) serialise VFS dir -> BHFS binary string
|
|
-- syscall.loimgwrite(str, dest) write BHFS string to a file (binary)
|
|
|
|
local kernel = ...
|
|
|
|
local _deflate = nil
|
|
local function getDeflate()
|
|
if _deflate == nil then
|
|
local ok, lib = pcall(require, "store.deflate")
|
|
_deflate = ok and lib or false
|
|
end
|
|
return _deflate or nil
|
|
end
|
|
|
|
local function pack32(n)
|
|
n = math.floor(n) % 4294967296
|
|
return string.char(
|
|
n % 256,
|
|
math.floor(n / 256) % 256,
|
|
math.floor(n / 65536) % 256,
|
|
math.floor(n / 16777216) % 256
|
|
)
|
|
end
|
|
|
|
local function unpack32(s, i)
|
|
local a, b, c, d = s:byte(i, i + 3)
|
|
return (a or 0)
|
|
+ (b or 0) * 256
|
|
+ (c or 0) * 65536
|
|
+ (d or 0) * 16777216
|
|
end
|
|
|
|
local BHFS_MAGIC = "BHFS"
|
|
local BHFS_VERSION = "\001"
|
|
local BHFS_FLAG_COMPRESS = 1
|
|
|
|
local TYPE_FILE = "\001"
|
|
local TYPE_DIR = "\002"
|
|
local TYPE_LINK = "\003"
|
|
local TYPE_END = "\255"
|
|
|
|
local B64D = {}
|
|
do
|
|
local a = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
|
for i = 1, #a do B64D[a:sub(i, i)] = i - 1 end
|
|
end
|
|
|
|
local function b64dec(s)
|
|
s = s:gsub("[^A-Za-z0-9+/=]", "")
|
|
local t, i = {}, 1
|
|
while i <= #s do
|
|
local c1 = B64D[s:sub(i, i )] or 0
|
|
local c2 = B64D[s:sub(i+1, i+1)] or 0
|
|
local c3 = B64D[s:sub(i+2, i+2)] or 0
|
|
local c4 = B64D[s:sub(i+3, i+3)] or 0
|
|
local n = c1*262144 + c2*4096 + c3*64 + c4
|
|
t[#t+1] = string.char(math.floor(n/65536) % 256)
|
|
if s:sub(i+2, i+2) ~= "=" then t[#t+1] = string.char(math.floor(n/256) % 256) end
|
|
if s:sub(i+3, i+3) ~= "=" then t[#t+1] = string.char(n % 256) end
|
|
i = i + 4
|
|
end
|
|
return table.concat(t)
|
|
end
|
|
|
|
local loopDevs = {}
|
|
local nextLoop = 0
|
|
|
|
local function makeBindDisk(id, dirPath)
|
|
local disk = { address = id, isvirt = false }
|
|
disk.isReadOnly = function() return false end
|
|
disk.spaceUsed = function() return 0 end
|
|
disk.spaceTotal = function() return 0 end
|
|
disk.setLabel = function() end
|
|
disk.getLabel = function() return id end
|
|
|
|
local function resolveBase()
|
|
local mp, mid = "/", "$"
|
|
for id2, m in pairs(kernel.vfs.mounts) do
|
|
if dirPath == m or (m == "/" and dirPath:sub(1,1) == "/")
|
|
or dirPath:sub(1, #m+1) == m.."/" then
|
|
if #m > #mp then mp = m; mid = id2 end
|
|
end
|
|
end
|
|
return kernel.vfs.disks[mid], dirPath:sub(#mp+1)
|
|
end
|
|
|
|
local function dp(path)
|
|
local hd, base = resolveBase()
|
|
local b = (base == "" or base == "/") and "" or base:gsub("^/+","")
|
|
local p = path:gsub("^/+","")
|
|
local c = ((b=="") and "/"..p or "/"..b.."/"..p):gsub("//+","/")
|
|
local r = c:sub(2); if r == "" then r = "/" end
|
|
return hd, r
|
|
end
|
|
|
|
function disk:open(path,mode) local h,r=dp(path); return h:open(r,mode) end
|
|
function disk:type(path) local h,r=dp(path); return h:type(r) end
|
|
function disk:list(path) local h,r=dp(path); return h:list(r) end
|
|
function disk:fileExists(path) local h,r=dp(path); return h:fileExists(r) end
|
|
function disk:attributes(path) local h,r=dp(path); return h:attributes(r) end
|
|
function disk:makeDirectory(path) local h,r=dp(path); return h:makeDirectory(r) end
|
|
function disk:remove(path) local h,r=dp(path); return h:remove(r) end
|
|
return disk
|
|
end
|
|
|
|
local function makeImageDisk(id, imageStr)
|
|
local root = { kind="dir", children={} }
|
|
|
|
local function getNode(path, create)
|
|
local parts = {}
|
|
for p in path:gmatch("[^/]+") do parts[#parts+1] = p end
|
|
local node = root
|
|
for i = 1, #parts do
|
|
local name = parts[i]
|
|
if not node.children then
|
|
if not create then return nil end
|
|
node.children = {}
|
|
end
|
|
if not node.children[name] then
|
|
if not create then return nil end
|
|
node.children[name] = { kind="dir", children={} }
|
|
end
|
|
node = node.children[name]
|
|
end
|
|
return node
|
|
end
|
|
|
|
local function ensureParent(path)
|
|
local par = path:match("^(.*)/[^/]+$") or ""
|
|
if par ~= "" then
|
|
local n = getNode(par, true)
|
|
if not n.children then n.children = {} end
|
|
end
|
|
end
|
|
|
|
if imageStr:sub(1, 4) == BHFS_MAGIC then
|
|
local pos = 9
|
|
|
|
while pos <= #imageStr do
|
|
local rtype = imageStr:sub(pos, pos)
|
|
pos = pos + 1
|
|
|
|
if rtype == TYPE_END then break end
|
|
|
|
local path_len = unpack32(imageStr, pos); pos = pos + 4
|
|
local raw_size = unpack32(imageStr, pos); pos = pos + 4
|
|
local stored_size = unpack32(imageStr, pos); pos = pos + 4
|
|
|
|
local path = imageStr:sub(pos, pos + path_len - 1)
|
|
pos = pos + path_len
|
|
|
|
local stored_data = imageStr:sub(pos, pos + stored_size - 1)
|
|
pos = pos + stored_size
|
|
local data = stored_data
|
|
if stored_size < raw_size then
|
|
local deflate = getDeflate()
|
|
if deflate then
|
|
data = deflate.decompress(stored_data) or stored_data
|
|
end
|
|
end
|
|
|
|
if rtype == TYPE_DIR then
|
|
if path ~= "" and path ~= "/" then
|
|
ensureParent(path)
|
|
local n = getNode(path, true)
|
|
n.kind = "dir"; n.children = n.children or {}
|
|
end
|
|
elseif rtype == TYPE_FILE then
|
|
ensureParent(path)
|
|
local n = getNode(path, true)
|
|
n.kind="file"; n.data=data; n.size=#data; n.children=nil
|
|
elseif rtype == TYPE_LINK then
|
|
ensureParent(path)
|
|
local n = getNode(path, true)
|
|
n.kind="link"; n.target=data; n.children=nil
|
|
end
|
|
end
|
|
else
|
|
for line in (imageStr.."\n"):gmatch("([^\n]*)\n") do
|
|
if line == "END" then
|
|
break
|
|
elseif line:sub(1,4) == "DIR " then
|
|
local p = line:sub(5):match("^%s*(.-)%s*$")
|
|
if p and p ~= "" and p ~= "/" then
|
|
ensureParent(p)
|
|
local n = getNode(p, true)
|
|
n.kind = "dir"; n.children = n.children or {}
|
|
end
|
|
elseif line:sub(1,5) == "FILE " then
|
|
local p, sz, body = line:sub(6):match("^(%S+)%s+(%d+)%s*(.-)%s*$")
|
|
if p then
|
|
ensureParent(p)
|
|
local data = (tonumber(sz) or 0) > 0 and b64dec(body) or ""
|
|
local n = getNode(p, true)
|
|
n.kind="file"; n.data=data; n.size=#data; n.children=nil
|
|
end
|
|
elseif line:sub(1,5) == "LINK " then
|
|
local p, tgt = line:sub(6):match("^(%S+)%s+(.+)$")
|
|
if p then
|
|
ensureParent(p)
|
|
local n = getNode(p, true)
|
|
n.kind="link"; n.target=tgt; n.children=nil
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local disk = { address=id, isvirt=false }
|
|
disk.isReadOnly = function() return false end
|
|
disk.spaceTotal = function() return 1024*1024*64 end
|
|
disk.spaceUsed = function()
|
|
local tot = 0
|
|
local function w(n)
|
|
if n.kind=="file" then tot = tot + (n.size or 0)
|
|
elseif n.kind=="dir" then for _,c in pairs(n.children or {}) do w(c) end end
|
|
end
|
|
w(root); return tot
|
|
end
|
|
disk.setLabel = function() end
|
|
disk.getLabel = function() return id end
|
|
|
|
local function norm(path)
|
|
return path:gsub("^/+",""):gsub("/+$","")
|
|
end
|
|
|
|
function disk:type(path)
|
|
local p = norm(path)
|
|
if p == "" then return "directory" end
|
|
local n = getNode(p)
|
|
if not n then return nil end
|
|
if n.kind == "dir" then return "directory" end
|
|
return "file"
|
|
end
|
|
|
|
function disk:fileExists(path)
|
|
local p = norm(path)
|
|
if p == "" then return true end
|
|
return getNode(p) ~= nil
|
|
end
|
|
|
|
function disk:list(path)
|
|
local p = norm(path)
|
|
local node = (p=="") and root or getNode(p)
|
|
if not node or node.kind ~= "dir" then return {} end
|
|
local out = {}
|
|
for name in pairs(node.children or {}) do out[#out+1] = name end
|
|
return out
|
|
end
|
|
|
|
function disk:attributes(path)
|
|
local p = norm(path)
|
|
local node = (p=="") and root or getNode(p)
|
|
if not node then return nil end
|
|
return {
|
|
size = node.kind=="file" and (node.size or 0) or 0,
|
|
isDir = node.kind=="dir",
|
|
isReadOnly = false,
|
|
created = 0,
|
|
modified = 0,
|
|
}
|
|
end
|
|
|
|
function disk:open(path, mode)
|
|
local p = norm(path)
|
|
local node = getNode(p)
|
|
|
|
if mode == "r" then
|
|
if not node or node.kind ~= "file" then error("ENOENT: "..path) end
|
|
local data, pos = node.data or "", 1
|
|
return {
|
|
read = function(n)
|
|
if pos > #data then return nil end
|
|
local chunk = data:sub(pos, pos + (n or 1) - 1)
|
|
pos = pos + #chunk; return chunk
|
|
end,
|
|
readAll = function()
|
|
local all = data:sub(pos); pos = #data + 1; return all
|
|
end,
|
|
readLine = function()
|
|
if pos > #data then return nil end
|
|
local nl = data:find("\n", pos, true)
|
|
local line
|
|
if nl then line=data:sub(pos, nl-1); pos=nl+1
|
|
else line=data:sub(pos); pos=#data+1 end
|
|
return line
|
|
end,
|
|
seek = function(w, o)
|
|
o = o or 0
|
|
if w == "set" then pos = o + 1
|
|
elseif w == "cur" then pos = pos + o
|
|
elseif w == "end" then pos = #data + 1 + o end
|
|
return pos - 1
|
|
end,
|
|
close = function() end,
|
|
}
|
|
|
|
elseif mode == "w" or mode == "a" then
|
|
local buf = (mode=="a" and node and node.kind=="file")
|
|
and {node.data or ""} or {}
|
|
local done = false
|
|
local function commit()
|
|
if done then return end; done = true
|
|
local data = table.concat(buf)
|
|
if not node then ensureParent(p); node = getNode(p, true) end
|
|
node.kind="file"; node.data=data; node.size=#data; node.children=nil
|
|
end
|
|
return {
|
|
write = function(s) buf[#buf+1] = tostring(s) end,
|
|
writeLine = function(s) buf[#buf+1] = tostring(s).."\n" end,
|
|
flush = function() end,
|
|
close = commit,
|
|
}
|
|
else
|
|
error("EINVAL: unknown mode: "..tostring(mode))
|
|
end
|
|
end
|
|
|
|
function disk:makeDirectory(path)
|
|
local p = norm(path)
|
|
if p == "" then return end
|
|
ensureParent(p)
|
|
local n = getNode(p, true)
|
|
n.kind="dir"; n.children=n.children or {}; n.data=nil; n.size=nil
|
|
end
|
|
|
|
function disk:remove(path)
|
|
local p = norm(path)
|
|
if p == "" then error("EBUSY: cannot remove root") end
|
|
local par = p:match("^(.*)/[^/]+$") or ""
|
|
local name = p:match("([^/]+)$")
|
|
local pn = (par=="") and root or getNode(par)
|
|
if pn and pn.children then pn.children[name] = nil end
|
|
end
|
|
|
|
disk._root = root
|
|
return disk
|
|
end
|
|
|
|
local function serializeDir(srcPath)
|
|
local deflate = getDeflate()
|
|
local useCompress = deflate ~= nil
|
|
|
|
local flags = useCompress and BHFS_FLAG_COMPRESS or 0
|
|
local parts = {
|
|
BHFS_MAGIC,
|
|
BHFS_VERSION,
|
|
string.char(flags),
|
|
"\0\0",
|
|
}
|
|
|
|
srcPath = srcPath:gsub("/$", "")
|
|
|
|
local MIN_COMPRESS = 64
|
|
|
|
local function walk(vpath)
|
|
local ftype = kernel.vfs.type(vpath)
|
|
|
|
if ftype == "directory" then
|
|
if vpath ~= srcPath then
|
|
local relPath = vpath:sub(#srcPath + 1)
|
|
parts[#parts+1] = TYPE_DIR
|
|
parts[#parts+1] = pack32(#relPath)
|
|
parts[#parts+1] = pack32(0)
|
|
parts[#parts+1] = pack32(0)
|
|
parts[#parts+1] = relPath
|
|
end
|
|
local ok, entries = pcall(kernel.vfs.listdir, vpath)
|
|
if ok and entries then
|
|
table.sort(entries)
|
|
for _, name in ipairs(entries) do
|
|
walk(vpath:gsub("/$","").."/"..name)
|
|
end
|
|
end
|
|
|
|
elseif ftype == "file" then
|
|
local relPath = vpath:sub(#srcPath + 1)
|
|
local ok, fd = pcall(kernel.vfs.open, vpath, "r")
|
|
if ok then
|
|
local rawData = ""
|
|
local ok2, content = pcall(kernel.vfs.read, fd, 1024*1024)
|
|
if ok2 then rawData = content or "" end
|
|
pcall(kernel.vfs.close, fd)
|
|
|
|
local storedData = rawData
|
|
if useCompress and #rawData >= MIN_COMPRESS then
|
|
local compressed = deflate.compress(rawData)
|
|
if compressed and #compressed < #rawData then
|
|
storedData = compressed
|
|
end
|
|
end
|
|
|
|
parts[#parts+1] = TYPE_FILE
|
|
parts[#parts+1] = pack32(#relPath)
|
|
parts[#parts+1] = pack32(#rawData)
|
|
parts[#parts+1] = pack32(#storedData)
|
|
parts[#parts+1] = relPath
|
|
parts[#parts+1] = storedData
|
|
end
|
|
end
|
|
end
|
|
|
|
walk(srcPath)
|
|
|
|
parts[#parts+1] = TYPE_END
|
|
|
|
return table.concat(parts)
|
|
end
|
|
|
|
kernel.syscalls["losetup"] = function(filePath, forceImage)
|
|
if not filePath then error("EINVAL") end
|
|
local task = kernel.currentTask
|
|
local euid = (task and (task.euid or task.uid)) or kernel.uid
|
|
if euid ~= 0 then error("EPERM") end
|
|
|
|
filePath = filePath:gsub("/$", "")
|
|
local id = "loop" .. tostring(nextLoop)
|
|
nextLoop = nextLoop + 1
|
|
|
|
local ftype = kernel.vfs.type(filePath)
|
|
local disk, mode
|
|
|
|
if not forceImage and ftype == "directory" then
|
|
disk = makeBindDisk(id, filePath)
|
|
mode = "bind"
|
|
elseif ftype == "file" or forceImage then
|
|
if ftype ~= "file" then error("ENOENT: not a file: "..filePath) end
|
|
|
|
local img
|
|
local ok, fd = pcall(kernel.vfs.open, filePath, "rb")
|
|
if ok then
|
|
local ok2, data = pcall(kernel.vfs.read, fd, 1024*1024*16)
|
|
pcall(kernel.vfs.close, fd)
|
|
if ok2 and data then img = data end
|
|
end
|
|
if not img then
|
|
local ok2, fd2 = pcall(kernel.vfs.open, filePath, "r")
|
|
if not ok2 then error("EIO: cannot open image: "..filePath) end
|
|
local ok3, data = pcall(kernel.vfs.read, fd2, 1024*1024*16)
|
|
pcall(kernel.vfs.close, fd2)
|
|
if not ok3 or not data then error("EIO: cannot read image: "..filePath) end
|
|
img = data
|
|
end
|
|
|
|
disk = makeImageDisk(id, img)
|
|
mode = "image"
|
|
else
|
|
error("EINVAL: path must be a directory or .hfs image file")
|
|
end
|
|
|
|
kernel.vfs.disks[id] = disk
|
|
loopDevs[id] = { path=filePath, disk=disk, mode=mode }
|
|
kernel.log("losetup: attached "..id.." ("..mode..") -> "..filePath, "INFO")
|
|
return id
|
|
end
|
|
|
|
kernel.syscalls["lodetach"] = function(id)
|
|
local task = kernel.currentTask
|
|
local euid = (task and (task.euid or task.uid)) or kernel.uid
|
|
if euid ~= 0 then error("EPERM") end
|
|
|
|
if not loopDevs[id] then error("ENXIO") end
|
|
for mid in pairs(kernel.vfs.mounts) do
|
|
if mid == id then error("EBUSY: loop device is still mounted") end
|
|
end
|
|
kernel.vfs.disks[id] = nil
|
|
loopDevs[id] = nil
|
|
kernel.log("lodetach: detached "..id, "INFO")
|
|
end
|
|
|
|
kernel.syscalls["lolist"] = function()
|
|
local rv = {}
|
|
for id, info in pairs(loopDevs) do
|
|
rv[id] = { path=info.path, mode=info.mode }
|
|
end
|
|
return rv
|
|
end
|
|
|
|
kernel.syscalls["loimgcreate"] = function(srcPath)
|
|
local task = kernel.currentTask
|
|
local euid = (task and (task.euid or task.uid)) or kernel.uid
|
|
if euid ~= 0 then error("EPERM") end
|
|
if not srcPath then error("EINVAL") end
|
|
if kernel.vfs.type(srcPath) ~= "directory" then error("ENOTDIR: "..srcPath) end
|
|
return serializeDir(srcPath)
|
|
end
|
|
|
|
kernel.syscalls["loimgwrite"] = function(imgStr, destPath)
|
|
local task = kernel.currentTask
|
|
local euid = (task and (task.euid or task.uid)) or kernel.uid
|
|
if euid ~= 0 then error("EPERM") end
|
|
if not imgStr or not destPath then error("EINVAL") end
|
|
|
|
local ok, fd = pcall(kernel.vfs.open, destPath, "wb")
|
|
if not ok then
|
|
ok, fd = pcall(kernel.vfs.open, destPath, "w")
|
|
if not ok then error("EIO: cannot write: "..tostring(destPath)) end
|
|
end
|
|
local ok2, werr = pcall(kernel.vfs.write, fd, imgStr)
|
|
pcall(kernel.vfs.close, fd)
|
|
if not ok2 then error("EIO: write failed: "..tostring(werr)) end
|
|
end
|
|
|
|
kernel.log("Loop device driver loaded (bind + BHFS binary image + legacy HFS compat)")
|