improvements to guid, devfs, cat, cp, and system error clarity

renaming guid to uuid and removing useless method toHex
adding text read and writer, compare to c++ stdlib stringstream. needed for advanced devfs
fix globbing with relative paths and . and .. in path
moving install confirmation before calling .install
fix cp: cp -r . /another/path (should copy contents of current dir)
fix cat when stdin is missing or closed
improved devfs supporting custom dir handlers for dynamic file listing

other non functional changes to clean up code and improved error messages
This commit is contained in:
payonel 2016-07-16 01:08:31 -07:00
parent 6837c7e02f
commit e2331b67ff
19 changed files with 491 additions and 162 deletions

View File

@ -13,9 +13,14 @@ for i = 1, #args do
io.stderr:write(string.format('cat %s: Is a directory\n', arg))
ec = 1
else
local file, reason = args[i] == "-" and io.stdin or io.open(shell.resolve(args[i]))
local file, reason
if args[i] == "-" then
file, reason = io.stdin, "missing stdin"
else
file, reason = io.open(shell.resolve(args[i]))
end
if not file then
io.stderr:write(string.format("cat: %s: %s\n",args[i],tostring(reason)))
io.stderr:write(string.format("cat: %s: %s\n", args[i], tostring(reason)))
ec = 1
else
repeat

View File

@ -148,12 +148,14 @@ end
local to = shell.resolve(args[#args])
for i = 1, #args - 1 do
local fromPath, cuts = args[i]:gsub("(/%.%.?)$", "%1")
fromPath = shell.resolve(fromPath)
local arg = args[i]
local fromPath = shell.resolve(arg)
-- a "contents of" copy is where src path ends in . or ..
-- a source path ending with . is not sufficient - could be the source filename
local contents_of = arg:match("%.$") and not fromPath:match("%.$")
local toPath = to
-- fromPath ending with /. indicates copying the contents of fromPath
-- in which case (cuts>0) we do not append fromPath name to toPath
if cuts == 0 and fs.isDirectory(toPath) then
-- we do not append fromPath name to toPath in case of contents_of copy
if not contents_of and fs.isDirectory(toPath) then
toPath = fs.concat(toPath, fs.name(fromPath))
end
result, reason = recurse(fromPath, toPath)

View File

@ -19,7 +19,7 @@ for i = 1, #args do
reason = "unknown reason"
end
end
io.stderr:write(path .. ": " .. reason .. "\n")
io.stderr:write("mkdir: cannot create directory '" .. tostring(args[i]) .. "': " .. reason .. "\n")
ec = 1
end
end

View File

@ -1,12 +1,12 @@
local fs = require("filesystem")
local guid = require("guid")
local uuid = require("uuid")
local shell = require("shell")
local sh = require("sh")
local touch = loadfile(shell.resolve("touch", "lua"))
local mkdir = loadfile(shell.resolve("mkdir", "lua"))
if not guid or not touch then
if not uuid or not touch then
local errorMessage = "missing tools for mktmp"
io.stderr:write(errorMessage .. '\n')
return false, errorMessage
@ -58,7 +58,7 @@ if not fs.exists(prefix) then
end
while true do
local tmp = prefix .. guid.next()
local tmp = prefix .. uuid.next()
if not fs.exists(tmp) then
local ok, reason

View File

@ -1,6 +1,7 @@
require("filesystem").mount(
setmetatable({
isReadOnly = function()return false end
isReadOnly = function()return false end,
address = require("uuid").next()
},
{
__index=function(tbl,key)return require("devfs")[key]end

View File

@ -1,111 +1,292 @@
local fs = require("filesystem")
local comp = require("component")
local proxy = {points={},address=require("guid").next()}
local function new_node(parent, name, is_dir, proxy)
local node = {parent=parent, name=name, is_dir=is_dir, proxy=proxy}
if not proxy then
node.children = {}
end
return node
end
local nop = function()end
local function new_devfs_dir(name)
local sys = {}
sys.mtab = new_node(nil, name or "/", true)
function proxy.getLabel()
-- returns: dir, point or path
-- node (table): the handler responsible for the path
-- this is essentially the device filesystem that is registered for the given path
-- point (string): the point name (like a file name)
function sys.findNode(path, create)
checkArg(1, path, "string")
local segments = fs.segments(path)
local node = sys.mtab
while #segments > 0 do
local name = table.remove(segments, 1)
local prev_path = path
path = table.concat(segments, "/")
if not node.children[name] then
if not create then
path = prev_path
break
end
node.children[name] = new_node(node, name, true, false)
end
node = node.children[name]
if node.proxy then -- if there is a proxy handler we stop searching here
break
end
end
-- only dirs can have trailing path
-- trailing path on a dev point (file) not allowed
if path == "" or node.is_dir and node.proxy then
return node, path
end
end
function sys.invoke(method, path, ...)
local node, rest = sys.findNode(path)
if not node or -- not found
rest == "" and node.is_dir or -- path is dir
not node.proxy[method] then -- optional method
return 0
end
-- proxy could be a file, which doesn't take an argument, but it can be ignored if passed
return node.proxy[method](rest)
end
function sys.size(path)
return sys.invoke("size", path)
end
function sys.lastModified(path)
return sys.invoke("lastModified", path)
end
function sys.isDirectory(path)
local node, rest = sys.findNode(path)
if not node then
return
end
if rest == "" then
return node.is_dir
elseif node.proxy then
return node.proxy.isDirectory(rest)
end
end
function sys.open(path, mode)
checkArg(1, path, "string")
checkArg(2, mode, "string", "nil")
if not sys.exists(path) then
return nil, path.." file not found"
elseif sys.isDirectory(path) then
return nil, path.." is a directory"
end
mode = mode or "r"
-- everything at this level should be a binary open
mode = mode:gsub("b", "")
if not ({a=true,w=true,r=true})[mode] then
return nil, "invalid mode"
end
local node, rest = sys.findNode(path)
-- there must be a node, else exists would have failed
local args = {}
if rest ~= "" then
-- having more rest means we expect the proxy fs to open the point
args[1] = rest
end
args[#args+1] = mode
return node.proxy.open(table.unpack(args))
end
function sys.list(path)
local node, rest = sys.findNode(path)
if not node or (rest ~= "" and not node.is_dir) then-- not found
return {}
elseif rest == "" and not node.is_dir then -- path is file
return {path}
elseif node.proxy then
-- proxy could be a file, which doesn't take an argument, but it can be ignored if passed
return node.proxy.list(rest)
end
-- rest == "" and node.is_dir
local keys = {}
for k in pairs(node.children) do
table.insert(keys, k)
end
return keys
end
function sys.remove(path)
checkArg(1, path, "string")
if path == "" then
return nil, "no such file or directory"
end
if not sys.exists(path) then
return nil, path.." file not found"
end
local node, rest = sys.findNode(path)
if rest ~= "" then -- if rest is not resolved, this isn't our path
return node.proxy.remove(rest)
end
node.parent.children[node.name] = nil
end
function sys.exists(path)
checkArg(1, path, "string")
local node, rest = sys.findNode(path)
if not node then
return false
elseif rest == "" then
return true
else
return node.proxy.exists(rest)
end
end
function sys.create(path, handler)
if sys.exists(path) then
return nil, "path already exists"
end
local segments = fs.segments(path)
local target = table.remove(segments)
path = table.concat(segments, "/")
if not target or target == "" then
return nil, "missing argument"
end
local node, rest = sys.findNode(path, true)
if rest ~= "" then
return node.proxy.create(rest, handler)
end
node.children[target] = new_node(node, target, not not handler.list, handler)
return true
end
return sys
end
local devfs = new_devfs_dir()
local bfd = "bad file descriptor"
function devfs.getLabel()
return "devfs"
end
function proxy.setLabel(value)
function devfs.setLabel(value)
error("drive does not support labeling")
end
function proxy.spaceTotal()
function devfs.spaceTotal()
return 0
end
function proxy.spaceUsed()
function devfs.spaceUsed()
return 0
end
function proxy.exists(path)
return not not proxy.points[path]
function devfs.makeDirectory(path)
return false, "to create dirs in devfs use devfs.create"
end
function proxy.size(path)
return 0
end
function proxy.isDirectory(path)
return false
end
function proxy.lastModified(path)
return fs.lastModified("/dev/")
end
function proxy.list()
local keys = {}
for k,v in pairs(proxy.points) do
table.insert(keys, k)
end
return keys
end
function proxy.makeDirectory(path)
return false
end
function proxy.remove(path)
if not proxy.exists(path) then return false end
proxy.points[path] = nil
return true
end
function proxy.rename(from, to)
return false
end
proxy.close = nop
function proxy.open(path, mode)
checkArg(1, path, "string")
local handle = proxy.points[path]
if not handle then return nil, "device point [" .. path .. "] does not exist" end
local msg = "device point [" .. path .. "] cannot be opened for "
if mode == "r" then
if not handle.read then
return nil, msg .. "read"
end
else
if not handle.write then
return nil, msg .. "write"
end
end
return handle
end
function proxy.read(h,...)
function devfs.read(h,...)
if not h.read then return nil, bfd end
return h:read(...)
end
function proxy.seek(h,...)
function devfs.seek(h,...)
if not h.seek then return nil, bfd end
return h:seek(...)
end
function proxy.write(h,...)
function devfs.write(h,...)
if not h.write then return nil, bfd end
return h:write(...)
end
function proxy.create(path, handle)
handle.close = handle.close or nop
proxy.points[path] = handle
return true
function devfs.close(h, ...)
if not h.close then return nil, bfd end
return h:close(...)
end
proxy.create("null", {write = nop})
proxy.create("random", {read = function(_,n)
local chars = {}
for i=1,n do
table.insert(chars,string.char(math.random(0,255)))
end
return table.concat(chars)
end})
-- devfs.create creates a new dev point at path
-- devfs is mounted to /sys by default, and /dev is a symlink to /sys/dev. If you want a devfs point to show up in /dev, specify a path here as "/dev/your_path")
-- the handler can be a single file dev file (called a point), or a devfs dir [which allows it to list its own dynamic list of points and dirs]
-- note: devfs dirs that list their own child dirs will have to handle directory queries on their own, such as list() and open(path, mode)
return proxy
-- A handler represents a directory IF it defines list(), which returns a string array of the point names
-- a directory handler acts like simplified filesystem of its own.
-- note: that when creating new devfs points or dirs, devfs.create will not traverse into dynamic directory children of dev mount points
-- Meaning, if you create a devfs dir, which returns dirs children of its own, devfs.create does not support creating dev points
-- on those children
-- see new_devfs_dir() -- it might work for you, /dev uses it
-- Also note, your own devfs dirs may implement open() however they like -- devfs points' open() is called by the devfs library but dynamic
-- dir act as their own library for their own points
-- ### devfs point methods ###
-- Required
-- open(mode: string []) file: returns new file handle for point (see "devfs point handle methods")
-- Optional
-- size(path) number
-- ### devfs point handle instance methods ###
-- Required
-- + technicaly, one of the following is not required when the mode is for the other (e.g. no read when in write mode)
-- write(self, value, ...) boolean: writes each value (params list) and returns success
-- read(self, n: number) string: return string of n bytes, nil when no more bytes available
-- Optional
-- seek(self, whence [string], offset [number]) number: move file handle from whence by offset, return offset result
-- close(self) boolean: close the file handle. Note that if your open method allocated resources, you'll need to release them in close
-- ### devfs dir methods ###
-- Required
-- list() string[]: return list of child point names
-- if you use new_devfs_dir, set metatable on .points with __pairs and __index if you want a dynamic list of files
-- open(path, mode) file (table): return a file handle to path (path is relative)
-- it would be nice to make open() optional, but devfs doesn't know where you might store your point handlers, if you even have any
-- Optional
-- size(path) number
-- lastModified(path) number
-- isDirectory(path) boolean -- default returns false. Having dynamic dirs is considered advanced
-- remove(path) boolean
-- rename(path) boolean
-- exists(path) boolean -- default checks path against list() results
-- /dev is a special handler
local function devfs_load(key)
return require("tools/devfs/" .. key)
end
devfs.create("null", devfs_load("null"))
devfs.create("random", devfs_load("random"))
if comp.isAvailable("eeprom") then
devfs.create("eeprom", devfs_load("eeprom"))
devfs.create("eeprom-data", devfs_load("eeprom-data"))
end
return devfs

View File

@ -112,9 +112,7 @@ function filesystem.setAutorunEnabled(value)
saveConfig()
end
function filesystem.segments(path)
return segments(path)
end
filesystem.segments = segments
function filesystem.canonical(path)
local result = table.concat(segments(path), "/")
@ -367,7 +365,11 @@ function filesystem.makeDirectory(path)
end
local node, rest = findNode(path)
if node.fs and rest then
return node.fs.makeDirectory(rest)
local success, reason = node.fs.makeDirectory(rest)
if not success and not reason and node.fs.isReadOnly() then
reason = "filesystem is readonly"
end
return success, reason
end
if node.fs then
return nil, "virtual directory with that name already exists"
@ -502,11 +504,12 @@ function filesystem.open(path, mode)
checkArg(1, path, "string")
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
if not node.fs or not rest or (({r=true,rb=true})[mode] and not node.fs.exists(rest)) then
return nil, "file not found"
end

View File

@ -1,47 +0,0 @@
local guid = {}
function guid.toHex(n)
if type(n) ~= 'number' then
return nil, string.format("toHex only converts numbers to strings, %s is not a string, but a %s", tostring(n), type(n))
end
if n == 0 then
return '0'
end
local hexchars = "0123456789abcdef"
local result = ""
local prefix = "" -- maybe later allow for arg to request 0x prefix
if n < 0 then
prefix = "-"
n = -n
end
while n > 0 do
local next = math.floor(n % 16) + 1 -- lua has 1 based array indices
n = math.floor(n / 16)
result = hexchars:sub(next, next) .. result
end
return prefix .. result
end
function guid.next()
-- e.g. 3c44c8a9-0613-46a2-ad33-97b6ba2e9d9a
-- 8-4-4-4-12
local sets = {8, 4, 4, 4, 12}
local result = ""
local i
for _,set in ipairs(sets) do
if result:len() > 0 then
result = result .. "-"
end
for i = 1,set do
result = result .. guid.toHex(math.random(0, 15))
end
end
return result
end
return guid

View File

@ -454,7 +454,7 @@ function --[[@delayloaded-start@]] sh.internal.glob(glob_pattern)
local function magical(s)
for _,glob_rule in ipairs(sh.internal.globbers) do
if s:match("[^%%]-"..text.escapeMagic(glob_rule[2])) then
if (" "..s):match("[^%%]"..text.escapeMagic(glob_rule[2])) then
return true
end
end
@ -464,7 +464,6 @@ function --[[@delayloaded-start@]] sh.internal.glob(glob_pattern)
local root = is_abs and '' or shell.getWorkingDirectory():gsub("([^/])$","%1/")
local paths = {is_abs and "/" or ''}
local relative_separator = ''
for i,segment in ipairs(segments) do
local enclosed_pattern = string.format("^(%s)/?$", segment)
local next_paths = {}

View File

@ -243,14 +243,17 @@ function term.readKeyboard(ops)
end
while true do
local killed, name, address, char, code = term.internal.pull(input)
local ok, name, address, char, code = term.internal.pull(input)
if not term.isAvailable() then
return
end
-- we have to keep checking what kb is active in case it is switching during use
-- we could have multiple screens, each with keyboards active
local main_kb = term.keyboard(w)
local main_sc = term.screen(w)
local c = nil
local c
local backup_cache = hints.cache
if name == "interrupted" or name == "term_unavailable" then
if name == "interrupted" then
draw("^C\n",true)
return ""
elseif address == main_kb or address == main_sc then

View File

@ -288,4 +288,93 @@ function --[[@delayloaded-start@]] text.internal.normalize(words, omitQuotes)
return norms
end --[[@delayloaded-end@]]
function --[[@delayloaded-start@]] text.internal.seeker(handle, whence, to)
if not handle.txt then
return nil, "bad file descriptor"
end
to = to or 0
if whence == "cur" then
handle.offset = handle.offset + to
elseif whence == "set" then
handle.offset = to
elseif whence == "end" then
handle.offset = handle.len + to
end
handle.offset = math.max(0, math.min(handle.offset, handle.len))
return handle.offset
end --[[@delayloaded-end@]]
function --[[@delayloaded-start@]] text.internal.reader(txt)
checkArg(1, txt, "string")
local reader =
{
txt = txt,
len = unicode.len(txt),
offset = 0,
read = function(_, n)
checkArg(1, n, "number")
if not _.txt then
return nil, "bad file descriptor"
end
if _.offset >= _.len then
return nil
end
local last_offset = _.offset
_:seek("cur", n)
local next = unicode.sub(_.txt, last_offset + 1, _.offset)
return next
end,
seek = text.internal.seeker,
close = function(_)
if not _.txt then
return nil, "bad file descriptor"
end
_.txt = nil
return true
end,
}
return reader
end --[[@delayloaded-end@]]
function --[[@delayloaded-start@]] text.internal.writer(ostream, append_txt)
if type(ostream) == "table" then
local mt = getmetatable(ostream) or {}
checkArg(1, mt.__call, "function")
end
checkArg(1, ostream, "function", "table")
checkArg(2, append_txt, "string", "nil")
local writer =
{
txt = "",
offset = 0,
len = 0,
write = function(_, ...)
if not _.txt then
return nil, "bad file descriptor"
end
local pre, vs, pos = unicode.sub(_.txt, 1, _.offset), {}, unicode.sub(_.txt, _.offset + 1)
for i,v in ipairs({...}) do
table.insert(vs, v)
end
vs = table.concat(vs)
_:seek("cur", unicode.len(vs))
_.txt = pre .. vs .. pos
_.len = unicode.len(_.txt)
return true
end,
seek = text.internal.seeker,
close = function(_)
if not _.txt then
return nil, "bad file descriptor"
end
ostream((append_txt or "") .. _.txt)
_.txt = nil
return true
end,
}
return writer
end --[[@delayloaded-end@]]
return text, local_env

View File

@ -0,0 +1,19 @@
local comp = require("component")
local text = require("text")
if not comp.isAvailable("eeprom") then
return nil
end
return
{
open = function(mode)
if ({r=true, rb=true})[mode] then
return text.internal.reader(comp.eeprom.getData())
end
return text.internal.writer(comp.eeprom.setData, ({a=true,ab=true})[mode] and comp.eeprom.getData())
end,
size = function()
return string.len(comp.eeprom.getData())
end
}

View File

@ -0,0 +1,19 @@
local comp = require("component")
local text = require("text")
if not comp.isAvailable("eeprom") then
return nil
end
return
{
open = function(mode)
if ({r=true, rb=true})[mode] then
return text.internal.reader(comp.eeprom.get())
end
return text.internal.writer(comp.eeprom.set, ({a=true,ab=true})[mode] and comp.eeprom.get())
end,
size = function()
return string.len(comp.eeprom.get())
end
}

View File

@ -0,0 +1,12 @@
return
{
open = function(mode)
if not mode or not mode:match("[wa]") then
return nil, "write only"
end
return
{
write = function() end
}
end
}

View File

@ -0,0 +1,21 @@
return
{
open = function(mode)
if mode and not mode:match("r") then
return nil, "read only"
end
return
{
read = function(self, n)
local chars = {}
for i=1,n do
table.insert(chars,string.char(math.random(0,255)))
end
return table.concat(chars)
end
}
end,
size = function()
return math.huge
end
}

View File

@ -171,16 +171,11 @@ options.setboot = source.prop.setboot and not options.nosetboot
options.reboot = source.prop.reboot and not options.noreboot
options.source_dir = fs.canonical(source.prop.fromDir or options.fromDir or "") .. '/.'
local installer_path = options.source_root .. "/.install"
if fs.exists(installer_path) then
os.exit(loadfile("/lib/tools/install_utils.lua", "bt", _G)('install', options))
end
local cp_args =
{
"-vrx" .. (options.update and "ui" or ""),
options.source_root .. options.source_dir,
options.target_root .. options.target_dir
options.target_root:gsub("//","/") .. options.target_dir
}
local source_display = (source.prop or {}).label or source.dev.getLabel() or source.path
@ -194,6 +189,11 @@ if not ((io.read() or "n").."y"):match("^%s*[Yy]") then
os.exit()
end
local installer_path = options.source_root .. "/.install"
if fs.exists(installer_path) then
os.exit(loadfile("/lib/tools/install_utils.lua", "bt", _G)('install', options))
end
return
{
setlabel = options.setlabel,

View File

@ -41,18 +41,18 @@ end
if cmd == 'select' then
if #options.sources == 0 then
if options.source_label then
io.stderr:write("No install source matched given label: " .. options.source_label .. '\n')
io.stderr:write("Nothing to install labeled: " .. options.source_label .. '\n')
elseif options.from then
io.stderr:write("No install source found: " .. options.from .. '\n')
io.stderr:write("Nothing to install from: " .. options.from .. '\n')
else
io.stderr:write("Could not find any available installations\n")
io.stderr:write("Nothing to install\n")
end
os.exit(1)
end
if #options.targets == 0 then
if options.to then
io.stderr:write("No such filesystem to install to: " .. options.to .. '\n')
io.stderr:write("No such target to install to: " .. options.to .. '\n')
else
io.stderr:write("No writable disks found, aborting\n")
end

View File

@ -0,0 +1,22 @@
local uuid = {}
function uuid.next()
-- e.g. 3c44c8a9-0613-46a2-ad33-97b6ba2e9d9a
-- 8-4-4-4-12 (halved sizes because bytes make hex pairs)
local sets = {4, 2, 2, 2, 6}
local result = ""
local i
for _,set in ipairs(sets) do
if result:len() > 0 then
result = result .. "-"
end
for i = 1,set do
result = result .. string.format("%02x", math.random(0, 255))
end
end
return result
end
return uuid

View File

@ -9,10 +9,10 @@ DESCRIPTION
OPTIONS
--from=ADDR
Specifies the source filesystem or its root path. ADDR can be the device guid or a directory path. If this is a directory path, it represents a root path to install from. This option can also be used to specify source paths that would otherwise be ignored, those being devfs, tmpfs, and the rootfs. e.g. --from=/tmp . Note that if both --from and a [name] is given, install expects the source path to have a .prop defining the same name. See .prop for more details.
Specifies the source filesystem or its root path. ADDR can be the device uuid or a directory path. If this is a directory path, it represents a root path to install from. This option can also be used to specify source paths that would otherwise be ignored, those being devfs, tmpfs, and the rootfs. e.g. --from=/tmp . Note that if both --from and a [name] is given, install expects the source path to have a .prop defining the same name. See .prop for more details.
--to=ADDR
Same as --from but specifies the target filesystem by guid or its root path. This option can also be used to specify filesystems that are otherwise ignored including tmpfs. i.e. --to=ADDR where ADDR matches the tmpfs device address or its mount point path. e.g. --to=/tmp
Same as --from but specifies the target filesystem by uuid or its root path. This option can also be used to specify filesystems that are otherwise ignored including tmpfs. i.e. --to=ADDR where ADDR matches the tmpfs device address or its mount point path. e.g. --to=/tmp
--fromDir=PATH
Install PATH from source. PATH is relative to the root of the source filesystem or path given by --from. The default is .