7 changed files with 532 additions and 10 deletions
@ -0,0 +1,448 @@ |
|||
local unicode = { |
|||
len = utf8.len, |
|||
sub = function(s, i, j) |
|||
checkArg(1, s, "string") |
|||
i = i or 1 |
|||
j = j or math.maxinteger |
|||
if i<1 or j<1 then |
|||
local n = utf8.len(s) |
|||
if not n then return nil end |
|||
if i<0 then i = n+1+i end |
|||
if j<0 then j = n+1+j end |
|||
if i<0 then i = 1 elseif i>n then i = n end |
|||
if j<0 then j = 1 elseif j>n then j = n end |
|||
end |
|||
if j<i then return "" end |
|||
i = utf8.offset(s,i) or math.maxinteger |
|||
j = utf8.offset(s,j+1) or math.maxinteger |
|||
if i and j then return s:sub(i,j-1) |
|||
elseif i then return s:sub(i) |
|||
else return "" |
|||
end |
|||
end, |
|||
} |
|||
|
|||
local buffer = {} |
|||
|
|||
function buffer.new(mode, stream) |
|||
local result = { |
|||
mode = {}, |
|||
stream = stream, |
|||
bufferRead = "", |
|||
bufferWrite = "", |
|||
bufferSize = math.max(512, math.min(8 * 1024, native.freeMemory() / 8)), |
|||
bufferMode = "full", |
|||
readTimeout = math.huge |
|||
} |
|||
mode = mode or "r" |
|||
for i = 1, unicode.len(mode) do |
|||
result.mode[unicode.sub(mode, i, i)] = true |
|||
end |
|||
local metatable = { |
|||
__index = buffer, |
|||
__metatable = "file" |
|||
} |
|||
return setmetatable(result, metatable) |
|||
end |
|||
|
|||
function buffer:close() |
|||
if self.mode.w or self.mode.a then |
|||
self:flush() |
|||
end |
|||
self.closed = true |
|||
return self.stream:close() |
|||
end |
|||
|
|||
function buffer:flush() |
|||
if #self.bufferWrite > 0 then |
|||
local result, reason = self.stream:write(self.bufferWrite) |
|||
if result then |
|||
self.bufferWrite = "" |
|||
else |
|||
if reason then |
|||
return nil, reason |
|||
else |
|||
return nil, "bad file descriptor" |
|||
end |
|||
end |
|||
end |
|||
|
|||
return self |
|||
end |
|||
|
|||
function buffer: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 buffer:read(...) |
|||
local timeout = (native.uptime() / 1000) + self.readTimeout |
|||
|
|||
local function readChunk() |
|||
if (native.uptime() / 1000) > timeout then |
|||
error("timeout") |
|||
end |
|||
local result, reason = self.stream:read(self.bufferSize) |
|||
if result then |
|||
self.bufferRead = self.bufferRead .. result |
|||
return self |
|||
else -- error or eof |
|||
return nil, reason |
|||
end |
|||
end |
|||
|
|||
local function readBytesOrChars(n) |
|||
n = math.max(n, 0) |
|||
local len, sub |
|||
if self.mode.b then |
|||
len = rawlen |
|||
sub = string.sub |
|||
else |
|||
len = unicode.len |
|||
sub = unicode.sub |
|||
end |
|||
local buffer = "" |
|||
repeat |
|||
if len(self.bufferRead) == 0 then |
|||
local result, reason = readChunk() |
|||
if not result then |
|||
if reason then |
|||
return nil, reason |
|||
else -- eof |
|||
return #buffer > 0 and buffer or nil |
|||
end |
|||
end |
|||
end |
|||
local left = n - len(buffer) |
|||
buffer = buffer .. sub(self.bufferRead, 1, left) |
|||
self.bufferRead = sub(self.bufferRead, left + 1) |
|||
until len(buffer) == n |
|||
return buffer |
|||
end |
|||
|
|||
local function readNumber() |
|||
local len, sub |
|||
if self.mode.b then |
|||
len = rawlen |
|||
sub = string.sub |
|||
else |
|||
len = unicode.len |
|||
sub = unicode.sub |
|||
end |
|||
local buffer = "" |
|||
local first = true |
|||
local decimal = false |
|||
local last = false |
|||
local hex = false |
|||
local pat = "^[0-9]+" |
|||
local minbuf = 3 -- "+0x" (sign + hexadecimal tag) |
|||
-- this function is used to read trailing numbers (1e2, 0x1p2, etc) |
|||
local function readnum(checksign) |
|||
local _buffer = "" |
|||
local sign = "" |
|||
while true do |
|||
if len(self.bufferRead) == 0 then |
|||
local result, reason = readChunk() |
|||
if not result then |
|||
if reason then |
|||
return nil, reason |
|||
else -- eof |
|||
return #_buffer > 0 and (sign .. _buffer) or nil |
|||
end |
|||
end |
|||
end |
|||
if checksign then |
|||
local _sign = sub(self.bufferRead, 1, 1) |
|||
if _sign == "+" or _sign == "-" then |
|||
-- "eat" the sign (Rio Lua behaviour) |
|||
sign = sub(self.bufferRead, 1, 1) |
|||
self.bufferRead = sub(self.bufferRead, 2) |
|||
end |
|||
checksign = false |
|||
else |
|||
local x,y = string.find(self.bufferRead, pat) |
|||
if not x then |
|||
break |
|||
else |
|||
_buffer = _buffer .. sub(self.bufferRead, 1, y) |
|||
self.bufferRead = sub(self.bufferRead, y + 1) |
|||
end |
|||
end |
|||
end |
|||
return #_buffer > 0 and (sign .. _buffer) or nil |
|||
end |
|||
while true do |
|||
if len(self.bufferRead) == 0 or len(self.bufferRead) < minbuf then |
|||
local result, reason = readChunk() |
|||
if not result then |
|||
if reason then |
|||
return nil, reason |
|||
else -- eof |
|||
return #buffer > 0 and tonumber(buffer) or nil |
|||
end |
|||
end |
|||
end |
|||
-- these ifs are here so we run the buffer check above |
|||
if first then |
|||
local sign = sub(self.bufferRead, 1, 1) |
|||
if sign == "+" or sign == "-" then |
|||
-- "eat" the sign (Rio Lua behaviour) |
|||
buffer = buffer .. sub(self.bufferRead, 1, 1) |
|||
self.bufferRead = sub(self.bufferRead, 2) |
|||
end |
|||
local hextag = sub(self.bufferRead, 1, 2) |
|||
if hextag == "0x" or hextag == "0X" then |
|||
pat = "^[0-9A-Fa-f]+" |
|||
-- "eat" the 0x, see https://gist.github.com/SoniEx2/570a363d81b743353151 |
|||
buffer = buffer .. sub(self.bufferRead, 1, 2) |
|||
self.bufferRead = sub(self.bufferRead, 3) |
|||
hex = true |
|||
end |
|||
minbuf = 0 |
|||
first = false |
|||
elseif decimal then |
|||
local sep = sub(self.bufferRead, 1, 1) |
|||
if sep == "." then |
|||
buffer = buffer .. sep |
|||
self.bufferRead = sub(self.bufferRead, 2) |
|||
local temp = readnum(false) -- no sign |
|||
if temp then |
|||
buffer = buffer .. temp |
|||
end |
|||
end |
|||
if not tonumber(buffer) then break end |
|||
decimal = false |
|||
last = true |
|||
minbuf = 1 |
|||
elseif last then |
|||
local tag = sub(self.bufferRead, 1, 1) |
|||
if hex and (tag == "p" or tag == "P") then |
|||
local temp = sub(self.bufferRead, 1, 1) |
|||
self.bufferRead = sub(self.bufferRead, 2) |
|||
local temp2 = readnum(true) -- this eats the next sign if any |
|||
if temp2 then |
|||
buffer = buffer .. temp .. temp2 |
|||
end |
|||
elseif tag == "e" or tag == "E" then |
|||
local temp = sub(self.bufferRead, 1, 1) |
|||
self.bufferRead = sub(self.bufferRead, 2) |
|||
local temp2 = readnum(true) -- this eats the next sign if any |
|||
if temp2 then |
|||
buffer = buffer .. temp .. temp2 |
|||
end |
|||
end |
|||
break |
|||
else |
|||
local x,y = string.find(self.bufferRead, pat) |
|||
if not x then |
|||
minbuf = 1 |
|||
decimal = true |
|||
else |
|||
buffer = buffer .. sub(self.bufferRead, 1, y) |
|||
self.bufferRead = sub(self.bufferRead, y + 1) |
|||
end |
|||
end |
|||
end |
|||
return tonumber(buffer) |
|||
end |
|||
|
|||
local function readLine(chop) |
|||
local start = 1 |
|||
while true do |
|||
local l = self.bufferRead:find("\n", start, true) |
|||
if l then |
|||
local result = self.bufferRead:sub(1, l + (chop and -1 or 0)) |
|||
self.bufferRead = self.bufferRead:sub(l + 1) |
|||
return result |
|||
else |
|||
start = #self.bufferRead |
|||
local result, reason = readChunk() |
|||
if not result then |
|||
if reason then |
|||
return nil, reason |
|||
else -- eof |
|||
local result = #self.bufferRead > 0 and self.bufferRead or nil |
|||
self.bufferRead = "" |
|||
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.bufferRead |
|||
self.bufferRead = "" |
|||
return result |
|||
end |
|||
|
|||
local function read(n, format) |
|||
if type(format) == "number" then |
|||
return readBytesOrChars(format) |
|||
else |
|||
if type(format) ~= "string" or unicode.sub(format, 1, 1) ~= "*" then |
|||
error("bad argument #" .. n .. " (invalid option)") |
|||
end |
|||
format = unicode.sub(format, 2, 2) |
|||
if format == "n" then |
|||
return readNumber() |
|||
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 |
|||
|
|||
if self.mode.w or self.mode.a then |
|||
self:flush() |
|||
end |
|||
|
|||
local results = {} |
|||
local formats = table.pack(...) |
|||
if formats.n == 0 then |
|||
return readLine(true) |
|||
end |
|||
for i = 1, formats.n do |
|||
local result, reason = read(i, formats[i]) |
|||
if result then |
|||
results[i] = result |
|||
elseif reason then |
|||
return nil, reason |
|||
end |
|||
end |
|||
return table.unpack(results, 1, formats.n) |
|||
end |
|||
|
|||
function buffer:seek(whence, offset) |
|||
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 self.mode.w or self.mode.a then |
|||
self:flush() |
|||
elseif whence == "cur" then |
|||
offset = offset - #self.bufferRead |
|||
end |
|||
local result, reason = self.stream:seek(whence, offset) |
|||
if result then |
|||
self.bufferRead = "" |
|||
return result |
|||
else |
|||
return nil, reason |
|||
end |
|||
end |
|||
|
|||
function buffer:setvbuf(mode, size) |
|||
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.bufferMode = mode |
|||
self.bufferSize = size |
|||
|
|||
return self.bufferMode, self.bufferSize |
|||
end |
|||
|
|||
function buffer:getTimeout() |
|||
return self.readTimeout |
|||
end |
|||
|
|||
function buffer:setTimeout(value) |
|||
self.readTimeout = tonumber(value) |
|||
end |
|||
|
|||
function buffer:write(...) |
|||
if self.closed then |
|||
return nil, "bad file descriptor" |
|||
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" then |
|||
if self.bufferSize - #self.bufferWrite < #arg then |
|||
result, reason = self:flush() |
|||
if not result then |
|||
return nil, reason |
|||
end |
|||
end |
|||
if #arg > self.bufferSize then |
|||
result, reason = self.stream:write(arg) |
|||
else |
|||
self.bufferWrite = self.bufferWrite .. arg |
|||
result = self |
|||
end |
|||
|
|||
elseif self.bufferMode == "line" then |
|||
local l |
|||
repeat |
|||
local idx = arg:find("\n", (l or 0) + 1, true) |
|||
if idx then |
|||
l = idx |
|||
end |
|||
until not idx |
|||
if l or #arg > self.bufferSize then |
|||
result, reason = self:flush() |
|||
if not result then |
|||
return nil, reason |
|||
end |
|||
end |
|||
if l then |
|||
result, reason = self.stream:write(arg:sub(1, l)) |
|||
if not result then |
|||
return nil, reason |
|||
end |
|||
arg = arg:sub(l + 1) |
|||
end |
|||
if #arg > self.bufferSize then |
|||
result, reason = self.stream:write(arg) |
|||
else |
|||
self.bufferWrite = self.bufferWrite .. arg |
|||
result = self |
|||
end |
|||
|
|||
else -- self.bufferMode == "no" |
|||
result, reason = self.stream:write(arg) |
|||
end |
|||
|
|||
if not result then |
|||
return nil, reason |
|||
end |
|||
end |
|||
|
|||
return self |
|||
end |
|||
|
|||
return buffer |
Loading…
issues.context.reference_issue