add preliminary half-tested half-working implementation of the `io` global
parent
9813dee130
commit
a1e6b75b22
@ -0,0 +1,307 @@
|
||||
local openNull, openPath
|
||||
do
|
||||
local filedata = setmetatable({}, { __mode = 'k' }) --weak-keyed mapping of file object to data/inner (fh of wrapFile)
|
||||
local filemt = { __metatable = '<file>' }
|
||||
filemt.__index = filemt
|
||||
|
||||
function isFile(obj)
|
||||
if filedata[obj] then
|
||||
-- if this is true, it's an object we control, so it's safe to trust attribute access
|
||||
return true, not obj._closed
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
local function wrapFile(mode, fh)
|
||||
local rwa, bin, plus, b2 = match(mode or 'r', '^([rwa])(b?)(%+?)(b?)$') --match all of x, xb, x+, xb+, x+b
|
||||
if not rwa or (bin ~= '' and b2 ~= '') then return nil, 'bad mode' end
|
||||
bin = bin .. b2 --when (bin == '') xor (b2 == ''), this is logical or
|
||||
|
||||
local read = rwa == 'r' or plus
|
||||
local write = rwa ~= 'r' or plus
|
||||
local append = rwa == 'a'
|
||||
local update = plus ~= ''
|
||||
|
||||
local file = setmetatable({}, filemt)
|
||||
filedata[file] = fh
|
||||
if read then
|
||||
fh._file_rbuf = ''
|
||||
end
|
||||
if write then
|
||||
fh._file_wbuf, fh._file_wmode, fh._file_wcap = '', 'full', nil --nil capacity means default, which may vary with e.g. system memory usage
|
||||
end
|
||||
|
||||
return file
|
||||
end
|
||||
|
||||
local function null_close() return true end
|
||||
local function null_read() return nil end --always eof
|
||||
local function null_write() return true end --eats data
|
||||
--null devices can't be seeked
|
||||
|
||||
function openNull(mode)
|
||||
return assert(wrapFile(mode, { close = null_close, read = null_read, write = null_write }))
|
||||
end
|
||||
|
||||
local function path_close(fh) return invoke(fh.address, 'close', fh.handle) end
|
||||
local function path_read(fh, n) return invoke(fh.address, 'read', fh.handle, n) end
|
||||
local function path_write(fh, data) return invoke(fh.address, 'write', fh.handle, data) end
|
||||
local function path_seek(fh, ...) return invoke(fh.address, 'seek', fh.handle, ...) end
|
||||
|
||||
function openPath(mode, addr, path)
|
||||
local fd, err = invoke(addr, 'open', path, mode) --should we validate `mode` first?
|
||||
if not fd then
|
||||
return nil, err
|
||||
end
|
||||
return assert(wrapFile(mode, { address = addr, handle = fd, close = path_close, read = path_read, write = path_write, seek = path_seek }))
|
||||
end
|
||||
|
||||
|
||||
--file method helpers
|
||||
local function assertOpen(file, how)
|
||||
local fh = filedata[file]
|
||||
if fh._file_closed then error('attempt to use a closed file', 3) end
|
||||
if how == 'read' then
|
||||
if not fh._file_rbuf then return nil, 'file not readable' end
|
||||
elseif how == 'write' then
|
||||
if not fh._file_wbuf then return nil, 'file not writable' end
|
||||
elseif how == 'seek' then
|
||||
if not fh.seek then return nil, 'file not seekable' end
|
||||
elseif how then
|
||||
error('bad how: should be read, write, seek, or nil', 2)
|
||||
end
|
||||
return fh
|
||||
end
|
||||
|
||||
local function flush(fh)
|
||||
local ok, err = fh:write(fh._file_wbuf)
|
||||
if ok then
|
||||
fh._file_wbuf = ''
|
||||
return true
|
||||
else
|
||||
return nil, err
|
||||
end
|
||||
end
|
||||
|
||||
local function read1(fh, n)
|
||||
if type(n) == 'number' then
|
||||
while #fh._file_rbuf < n do
|
||||
local blocksz = n - #fh._file_rbuf
|
||||
if blocksz < 512 then blocksz = 512 end --TODO tune default capacity to e.g. free memory
|
||||
local data, err = fh:read(blocksz)
|
||||
if err then
|
||||
return nil, err
|
||||
elseif not data then --eof
|
||||
break
|
||||
end
|
||||
fh._file_rbuf = fh._file_rbuf .. data
|
||||
end
|
||||
|
||||
local data = sub(fh._file_rbuf, 1, n)
|
||||
fh._file_rbuf = sub(fh._file_rbuf, n + 1)
|
||||
return data
|
||||
elseif n == 'a' then
|
||||
local parts = { fh._file_rbuf }
|
||||
local chunk, err
|
||||
repeat
|
||||
chunk, err = fh:read(512)
|
||||
parts[#parts+1] = chunk
|
||||
until not chunk
|
||||
|
||||
local cat = concat(parts)
|
||||
|
||||
if err then
|
||||
fh._file_rbuf = cat
|
||||
return nil, err
|
||||
else
|
||||
fh._file_rbuf = ''
|
||||
return cat
|
||||
end
|
||||
else
|
||||
error('TODO read1 patterns')
|
||||
end
|
||||
end
|
||||
|
||||
local function write1(fh, data)
|
||||
local wbuf = fh._file_wbuf .. data
|
||||
fh._file_wbuf = wbuf
|
||||
|
||||
local mode = fh._file_wmode
|
||||
if mode == 'no' then
|
||||
if wbuf ~= '' then return flush(fh) end
|
||||
elseif mode == 'full' then
|
||||
local cap = fh._file_wcap or 512 --TODO tune default capacity to e.g. free memory
|
||||
if #fh._file_wbuf >= cap then
|
||||
return flush(fh)
|
||||
end
|
||||
else --for line buffering, only flush complete lines
|
||||
local i1, i2 = find(fh._file_wbuf, '.+[\r\n]') --`i2` is the last cr or lf in the buffer
|
||||
if i1 then --if there are any newlines
|
||||
local rest = sub(fh._file_wbuf, i2 + 1)
|
||||
fh._file_wbuf = sub(fh._file_wbuf, 1, i2)
|
||||
local ok, err = flush(fh)
|
||||
if ok then fh._file_wbuf = rest end
|
||||
return ok, err
|
||||
else --no newlines, nothing to flush
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
--file methods
|
||||
function filemt:close()
|
||||
local fh = assertOpen(self)
|
||||
if fh._file_wbuf then
|
||||
local ok, err = flush(fh)
|
||||
if not ok then return nil, err end --XXX should close ever fail? what do we expect the caller to do?
|
||||
end
|
||||
fh:close()
|
||||
fh._file_closed = true
|
||||
end
|
||||
|
||||
filemt.__close = filemt.close --for future lua 5.4 compat
|
||||
|
||||
function filemt:flush()
|
||||
local fh, err = assertOpen(self, 'write')
|
||||
if not fh then return nil, err end
|
||||
|
||||
local ok, err = flush(fh)
|
||||
if not ok then return nil, err end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function filemt:lines(...)
|
||||
local pats = pack(...)
|
||||
for i = 1, pats.n do
|
||||
local pat = pats[i]
|
||||
local t = checkArgEx(i, pat, 'string', 'integer')
|
||||
if t == 'string' and pat ~= 'n' and pat ~= 'a' and pat ~= 'l' and pat ~= 'L' then
|
||||
error(format('bad argument #%d to \'lines\' (invalid format)', i), 2)
|
||||
end
|
||||
end
|
||||
if pats.n == 0 then
|
||||
pats[1], pats.n = 'l', 1
|
||||
end
|
||||
|
||||
local ok, err = assertOpen(self, 'read')
|
||||
if not ok then error(err, 2) end --more informative than the error from `for line in nil, err do end`
|
||||
|
||||
return function()
|
||||
local ret = {}
|
||||
for i = 1, pats.n do
|
||||
local ok, err = read1(fh, pats[i])
|
||||
if not ok then
|
||||
if err then error(err, 2) end
|
||||
break
|
||||
end
|
||||
ret[i] = ok
|
||||
end
|
||||
return unpack(ret, 1, pats.n)
|
||||
end
|
||||
end
|
||||
|
||||
function filemt:read(...)
|
||||
local args = pack(...)
|
||||
for i = 1, args.n do
|
||||
local pat = args[i]
|
||||
local t = checkArgEx(i, pat, 'string', 'integer')
|
||||
if t == 'string' and pat ~= 'n' and pat ~= 'a' and pat ~= 'l' and pat ~= 'L' then
|
||||
error(format('bad argument #%d to \'read\' (invalid format)', i), 2)
|
||||
end
|
||||
end
|
||||
if args.n == 0 then
|
||||
args[1], args.n = 'l', 1
|
||||
end
|
||||
|
||||
local fh, err = assertOpen(self, 'read')
|
||||
if not fh then return nil, err end
|
||||
|
||||
local args = pack(...)
|
||||
local iend = args.n
|
||||
for i = 1, args.n do
|
||||
local ok, err = read1(fh, args[i])
|
||||
if ok then
|
||||
args[i] = ok
|
||||
else
|
||||
iend = i + 1
|
||||
args[i] = nil
|
||||
args[i + 1] = err
|
||||
break
|
||||
end
|
||||
end
|
||||
return unpack(args, 1, iend)
|
||||
end
|
||||
|
||||
function filemt:seek(whence, offset)
|
||||
if type(whence) == 'number' and rawequal(offset, nil) then
|
||||
checkArgEx(1, whence, 'integer') --produce a better error message citing arg 1
|
||||
whence, offset = 'cur', whence
|
||||
end
|
||||
checkArgEx(1, whence, 'string', 'nil')
|
||||
checkArgEx(2, offset, 'integer', 'nil')
|
||||
|
||||
local fh, err = assertOpen(self, 'seek')
|
||||
if not fh then return nil, err end
|
||||
|
||||
--before seeking, flush any buffered writes
|
||||
if fh._file_wbuf then
|
||||
local ok, err = flush(fh)
|
||||
if not ok then return nil, err end
|
||||
end
|
||||
|
||||
local ok, err = fh:seek(whence or 'cur', offset or 0)
|
||||
if not ok then return nil, err end
|
||||
|
||||
|
||||
local tell = (whence == 'cur' or not whence) and (offset == 0 or not offset) --true if we didn't move ie ftell()
|
||||
--after seeking, if we moved, invalidate the read buffer
|
||||
if (whence and whence ~= cur) or (offset and offset ~= 0) then
|
||||
fh._file_rbuf = fh._file_rbuf and ''
|
||||
end
|
||||
|
||||
return ok
|
||||
end
|
||||
|
||||
function filemt:setvbuf(mode, size)
|
||||
checkArg(1, mode, 'string')
|
||||
if mode ~= 'no' and mode ~= 'line' and mode ~= 'full' then
|
||||
error(format('bad argument #1 to \'setvbuf\' (invalid option %q)', mode), 2)
|
||||
end
|
||||
checkArgEx(2, size, 'integer', 'nil')
|
||||
|
||||
local fh, err = assertOpen(self)
|
||||
if not fh then return nil, err end
|
||||
|
||||
if not fh._file_wbuf then
|
||||
return true --on readonly files, setvbuf is a noop
|
||||
end
|
||||
|
||||
fh._file_wmode, fh._file_wcap = mode, size
|
||||
--don't flush now; we don't have a way to propagate errors. the next write (or close) will take care of it.
|
||||
return true
|
||||
end
|
||||
|
||||
function filemt:write(...)
|
||||
local args = pack(...)
|
||||
for i = 1, args.n do
|
||||
local arg = args[i]
|
||||
local t = checkArgEx(i, arg, 'string', 'number')
|
||||
if t ~= 'string' then args[i] = tostring(arg) end
|
||||
end
|
||||
|
||||
local fh, err = assertOpen(self, 'write')
|
||||
if not fh then return nil, err end
|
||||
|
||||
for i = 1, args.n do
|
||||
local ok, err = write1(fh, args[i])
|
||||
if not ok then return nil, err end
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
end
|
||||
|
||||
-- vi: set ts=2:
|
Loading…
Reference in New Issue