blob: 6d00cacd5b47b081b5e4599c70431da2ba774fac [file] [log] [blame] [raw]
local mtab = {children={}}
local function segments(path)
path = path:gsub("\\", "/")
repeat local n; path, n = path:gsub("//", "/") until n == 0
local parts = {}
for part in path:gmatch("[^/]+") do
table.insert(parts, part)
end
local i = 1
while i <= #parts do
if parts[i] == "." then
table.remove(parts, i)
elseif parts[i] == ".." then
table.remove(parts, i)
i = i - 1
if i > 0 then
table.remove(parts, i)
else
i = 1
end
else
i = i + 1
end
end
return parts
end
local function findNode(path, create)
checkArg(1, path, "string")
local parts = segments(path)
local node = mtab
for i = 1, #parts do
if not node.children[parts[i]] then
if create then
node.children[parts[i]] = {children={}, parent=node}
else
return node, table.concat(parts, "/", i)
end
end
node = node.children[parts[i]]
end
return node
end
local function removeEmptyNodes(node)
while node and node.parent and not node.fs and not next(node.children) do
for k, c in pairs(node.parent.children) do
if c == node then
node.parent.children[k] = nil
break
end
end
node = node.parent
end
end
-------------------------------------------------------------------------------
driver.filesystem = {}
function driver.filesystem.canonical(path)
return table.concat(segments(path), "/")
end
function driver.filesystem.concat(pathA, pathB)
return driver.filesystem.canonical(pathA .. "/" .. pathB)
end
function driver.filesystem.mount(fs, path)
if fs and path then
checkArg(1, fs, "string")
local node = findNode(path, true)
if node.fs then
return nil, "another filesystem is already mounted here"
end
node.fs = fs
else
local function path(node)
local result = "/"
while node and node.parent do
for name, child in pairs(node.parent.children) do
if child == node then
result = "/" .. name .. result
break
end
end
node = node.parent
end
return result
end
local queue = {mtab}
return function()
if #queue == 0 then
return nil
else
while true do
local node = table.remove(queue)
for _, child in pairs(node.children) do
table.insert(queue, child)
end
if node.fs then
return node.fs, path(node)
end
end
end
end
end
end
function driver.filesystem.umount(fsOrPath)
local node, rest = findNode(fsOrPath)
if not rest and node.fs then
node.fs = nil
removeEmptyNodes(node)
return true
else
local queue = {mtab}
for fs, path in driver.filesystem.mount() do
if fs == fsOrPath then
local node = findNode(path)
node.fs = nil
removeEmptyNodes(node)
return true
end
end
end
end
-------------------------------------------------------------------------------
function driver.filesystem.spaceTotal(path)
local node, rest = findNode(path)
if node.fs then
return send(node.fs, "fs.spaceTotal")
else
return nil, "no such device"
end
end
function driver.filesystem.spaceUsed(path)
local node, rest = findNode(path)
if node.fs then
return send(node.fs, "fs.spaceUsed")
else
return nil, "no such device"
end
end
-------------------------------------------------------------------------------
function driver.filesystem.exists(path)
local node, rest = findNode(path)
if not rest then -- virtual directory
return true
end
if node.fs then
return send(node.fs, "fs.exists", rest)
end
end
function driver.filesystem.size(path)
local node, rest = findNode(path)
if node.fs and rest then
return send(node.fs, "fs.size", rest)
end
return 0 -- no such file or directory or it's a virtual directory
end
function driver.filesystem.isDirectory(path)
local node, rest = findNode(path)
if node.fs and rest then
return send(node.fs, "fs.isDirectory", rest)
else
return not rest or rest:len() == 0
end
end
function driver.filesystem.dir(path)
local node, rest = findNode(path)
if not node.fs and rest then
return nil, "no such file or directory"
end
local result
if node.fs then
result = table.pack(send(node.fs, "fs.list", rest or ""))
if not result[1] then
return nil, result[2]
end
else
result = {}
end
if not rest then
for k, _ in pairs(node.children) do
table.insert(result, k .. "/")
end
end
table.sort(result)
return table.unpack(result)
end
-------------------------------------------------------------------------------
function driver.filesystem.remove(path)
local node, rest = findNode(path)
if node.fs and rest then
return send(node.fs, "fs.remove", rest)
end
end
function driver.filesystem.rename(oldPath, newPath)
local oldNode, oldRest = findNode(oldPath)
local newNode, newRest = findNode(newPath)
if oldNode.fs and oldRest and newNode.fs and newRest then
if oldNode.fs == newNode.fs then
return send(oldNode.fs, "fs.rename", oldRest, newRest)
else
local result, reason = driver.filesystem.copy(oldPath, newPath)
if result then
return driver.filesystem.remove(oldPath)
else
return nil, reason
end
end
end
end
function driver.filesystem.copy(fromPath, toPath)
--[[ TODO ]]
return nil, "not implemented"
end
-------------------------------------------------------------------------------
local file = {}
function file:close()
if self.handle then
self:flush()
return self.stream:close()
end
end
function file:flush()
if not self.handle then
return nil, "file is closed"
end
if #self.buffer > 0 then
local result, reason = self.stream:write(self.buffer)
if result then
self.buffer = ""
else
if reason then
return nil, reason
else
return nil, "bad file descriptor"
end
end
end
return self
end
function file:lines(...)
local args = table.pack(...)
return function()
local result = table.pack(self:read(table.unpack(args, 1, args.n)))
if not result[1] and result[2] then
error(result[2])
end
return table.unpack(result, 1, result.n)
end
end
function file:read(...)
if not self.handle then
return nil, "file is closed"
end
local function readChunk()
local result, reason = self.stream:read(self.bufferSize)
if result then
self.buffer = self.buffer .. result
return self
else -- error or eof
return nil, reason
end
end
local function readBytesOrChars(n)
local len, sub
if self.mode == "r" then
len = string.len
sub = string.sub
else
assert(self.mode == "rb")
len = rawlen
sub = string.bsub
end
local result = ""
repeat
if len(self.buffer) == 0 then
local result, reason = readChunk()
if not result then
if reason then
return nil, reason
else -- eof
return nil
end
end
end
local left = n - len(result)
result = result .. sub(self.buffer, 1, left)
self.buffer = sub(self.buffer, left + 1)
until len(result) == n
return result
end
local function readLine(chop)
local start = 1
while true do
local l = self.buffer:find("\n", start, true)
if l then
local result = self.buffer:bsub(1, l + (chop and -1 or 0))
self.buffer = self.buffer:bsub(l + 1)
return result
else
start = #self.buffer
local result, reason = readChunk()
if not result then
if reason then
return nil, reason
else -- eof
local result = #self.buffer > 0 and self.buffer or nil
self.buffer = ""
return result
end
end
end
end
end
local function readAll()
repeat
local result, reason = readChunk()
if not result and reason then
return nil, reason
end
until not result -- eof
local result = self.buffer
self.buffer = ""
return result
end
local function read(n, format)
if type(format) == "number" then
return readBytesOrChars(format)
else
if type(format) ~= "string" or format:sub(1, 1) ~= "*" then
error("bad argument #" .. n .. " (invalid option)")
end
format = format:sub(2, 2)
if format == "n" then
--[[ TODO ]]
error("not implemented")
elseif format == "l" then
return readLine(true)
elseif format == "L" then
return readLine(false)
elseif format == "a" then
return readAll()
else
error("bad argument #" .. n .. " (invalid format)")
end
end
end
local result = {}
local formats = table.pack(...)
if formats.n == 0 then
return readLine(true)
end
for i = 1, formats.n do
table.insert(result, read(i, formats[i]))
end
return table.unpack(result)
end
function file:seek(whence, offset)
if not self.handle then
return nil, "file is closed"
end
whence = tostring(whence or "cur")
assert(whence == "set" or whence == "cur" or whence == "end",
"bad argument #1 (set, cur or end expected, got " .. whence .. ")")
offset = offset or 0
checkArg(2, offset, "number")
assert(math.floor(offset) == offset, "bad argument #2 (not an integer)")
if whence == "cur" and offset ~= 0 then
offset = offset - #(self.buffer or "")
end
local result, reason = self.stream:seek(whence, offset)
if result then
if offset ~= 0 then
self.buffer = ""
elseif whence == "cur" then
result = result - #self.buffer
end
end
return result, reason
end
function file:setvbuf(mode, size)
if not self.handle then
return nil, "file is closed"
end
mode = mode or self.bufferMode
size = size or self.bufferSize
assert(mode == "no" or mode == "full" or mode == "line",
"bad argument #1 (no, full or line expected, got " .. tostring(mode) .. ")")
assert(mode == "no" or type(size) == "number",
"bad argument #2 (number expected, got " .. type(size) .. ")")
self:flush()
self.bufferMode = mode
self.bufferSize = mode == "no" and 0 or size
return self.bufferMode, self.bufferSize
end
function file:write(...)
if not self.handle then
return nil, "file is closed"
end
local args = table.pack(...)
for i = 1, args.n do
if type(args[i]) == "number" then
args[i] = tostring(args[i])
end
checkArg(i, args[i], "string")
end
for i = 1, args.n do
local arg = args[i]
local result, reason
if (self.bufferMode == "full" or self.bufferMode == "line") and
self.bufferSize - #self.buffer < #arg
then
result, reason = self:flush()
if not result then
return nil, reason
end
end
if self.bufferMode == "full" then
if #arg > self.bufferSize then
result, reason = self.stream:write(arg)
else
self.buffer = self.buffer .. arg
result = self
end
elseif self.bufferMode == "line" then
local l
repeat
local idx = self.buffer:find("\n", l or 1, true)
if idx then
l = idx
end
until not idx
if l then
result, reason = self:flush()
if not result then
return nil, reason
end
result, reason = self.stream:write(arg:bsub(1, l))
if not result then
return nil, reason
end
arg = arg:bsub(l + 1)
end
if #arg > self.bufferSize then
result, reason = self.stream:write(arg)
else
self.buffer = arg
result = self
end
else -- no
result, reason = self.stream:write(arg)
end
if not result then
return nil, reason
end
end
return self
end
-------------------------------------------------------------------------------
function file.new(fs, handle, mode, stream, nogc)
local result = {
fs = fs,
handle = handle,
mode = mode,
buffer = "",
bufferSize = math.min(8 * 1024, os.totalMemory() / 16),
bufferMode = "full"
}
result.stream = setmetatable(stream, {__index = {file = result}})
local metatable = {
__index = file,
__metatable = "file"
}
if not nogc then
metatable.__gc = function(self)
-- file.close does a syscall, which yields, and that's not possible in
-- the __gc metamethod. So we start a timer to do the yield/cleanup.
if type(event) == "table" and type(event.timer) == "function" then
event.timer(0, function()
self:close()
end)
end
end
end
return setmetatable(result, metatable)
end
-------------------------------------------------------------------------------
local fileStream = {}
function fileStream:close()
send(self.file.fs, "fs.close", self.file.handle)
self.file.handle = nil
end
function fileStream:read(n)
return send(self.file.fs, "fs.read", self.file.handle, n)
end
function fileStream:seek(whence, offset)
return send(self.file.fs, "fs.seek", self.file.handle, whence, offset)
end
function fileStream:write(str)
return send(self.file.fs, "fs.write", self.file.handle, str)
end
-------------------------------------------------------------------------------
function driver.filesystem.open(path, mode)
mode = tostring(mode or "r")
checkArg(2, mode, "string")
assert(({r=true, rb=true, w=true, wb=true, a=true, ab=true})[mode],
"bad argument #2 (r[b], w[b] or a[b] expected, got " .. mode .. ")")
local node, rest = findNode(path)
if not node.fs or not rest then
return nil, "file not found"
end
local handle, reason = send(node.fs, "fs.open", rest, mode)
if not handle then
return nil, reason
end
return file.new(node.fs, handle, mode, fileStream)
end
function driver.filesystem.type(object)
if type(object) == "table" then
if getmetatable(object) == "file" then
if object.handle then
return "file"
else
return "closed file"
end
end
end
return nil
end
-------------------------------------------------------------------------------
function loadfile(filename, env)
local file, reason = driver.filesystem.open(filename)
if not file then
return nil, reason
end
local source, reason = file:read("*a")
file:close()
if not source then
return nil, reason
end
return load(source, "=" .. filename, env)
end
function dofile(filename)
local program, reason = loadfile(filename)
if not program then
return error(reason, 0)
end
return program()
end
-------------------------------------------------------------------------------
io = {}
-------------------------------------------------------------------------------
local stdinStream = {}
local stdinHistory = {}
function stdinStream:close()
return nil, "cannot close standard file"
end
function stdinStream:read(n)
local result = term.read(stdinHistory)
while #stdinHistory > 10 do
table.remove(stdinHistory, 1)
end
return result
end
function stdinStream:seek(whence, offset)
return nil, "bad file descriptor"
end
function stdinStream:write(str)
return nil, "bad file descriptor"
end
local stdoutStream = {}
function stdoutStream:close()
return nil, "cannot close standard file"
end
function stdoutStream:read(n)
return nil, "bad file descriptor"
end
function stdoutStream:seek(whence, offset)
return nil, "bad file descriptor"
end
function stdoutStream:write(str)
term.write(str, true)
return self
end
io.stdin = file.new(nil, "stdin", "r", stdinStream, true)
io.stdout = file.new(nil, "stdout", "w", stdoutStream, true)
io.stderr = io.stdout
io.stdout:setvbuf("no")
-------------------------------------------------------------------------------
local input, output = io.stdin, io.stdout
-------------------------------------------------------------------------------
function io.close(file)
return (file or io.output()):close()
end
function io.flush()
return io.output():flush()
end
function io.input(file)
if file then
if type(file) == "string" then
local result, reason = io.open(file)
if not result then
error(reason)
end
input = result
elseif io.type(file) then
input = file
else
error("bad argument #1 (string or file expected, got " .. type(file) .. ")")
end
end
return input
end
function io.lines(filename, ...)
if filename then
local result, reason = io.open(filename)
if not result then
error(reason)
end
local args = table.pack(...)
return function()
local result = table.pack(file:read(table.unpack(args, 1, args.n)))
if not result[1] then
if result[2] then
error(result[2])
else -- eof
file:close()
return nil
end
end
return table.unpack(result, 1, result.n)
end
else
return io.input():lines()
end
end
io.open = driver.filesystem.open
function io.output(file)
if file then
if type(file) == "string" then
local result, reason = io.open(file, "w")
if not result then
error(reason)
end
output = result
elseif io.type(file) then
output = file
else
error("bad argument #1 (string or file expected, got " .. type(file) .. ")")
end
end
return output
end
-- TODO io.popen = function(prog, mode) end
function io.read(...)
return io.input():read(...)
end
-- TODO io.tmpfile = function() end
io.type = driver.filesystem.type
function io.write(...)
return io.output():write(...)
end
function print(...)
local args = table.pack(...)
for i = 1, args.n do
local arg = tostring(args[i])
if i > 1 then
arg = "\t" .. arg
end
io.stdout:write(arg)
end
io.stdout:write("\n")
end
-------------------------------------------------------------------------------
os.remove = driver.filesystem.remove
os.rename = driver.filesystem.rename
-- TODO os.tmpname = function() end