diff --git a/include/luares.h b/include/luares.h index d321d97..fcf54ee 100644 --- a/include/luares.h +++ b/include/luares.h @@ -9,6 +9,7 @@ extern char lua_init[]; extern char lua_internet[]; extern char lua_sandbox[]; extern char lua_textgpu[]; +extern char lua_util_buffer[]; extern char lua_util_color[]; extern char lua_util_random[]; #endif diff --git a/src/c/internet.c b/src/c/internet.c index a3d4649..1b77a78 100644 --- a/src/c/internet.c +++ b/src/c/internet.c @@ -18,7 +18,7 @@ static int l_open(lua_State *L) { //TODO: Any mem leaks? const char* hostaddr = lua_tostring(L, 1); - const char* port = lua_tostring(L, 2); + int port = lua_tonumber(L, 2); struct addrinfo hints, *servinfo, *p; int status; @@ -27,7 +27,7 @@ static int l_open(lua_State *L) { //TODO: Any mem leaks? hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; - if ((status = getaddrinfo(hostaddr, port, &hints, &servinfo)) != 0) { + if ((status = getaddrinfo(hostaddr, NULL, &hints, &servinfo)) != 0) { lua_pushnil(L); lua_pushstring(L, gai_strerror(status)); return 2; @@ -35,6 +35,12 @@ static int l_open(lua_State *L) { //TODO: Any mem leaks? int sockfd; for(p = servinfo; p != NULL; p = p->ai_next) { + if(p->ai_family == AF_INET) { + ((struct sockaddr_in*)p->ai_addr)->sin_port = htons(port); + } else { + ((struct sockaddr_in6*)p->ai_addr)->sin6_port = htons(port); + } + if ((sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1) { continue; } diff --git a/src/c/modules.c b/src/c/modules.c index 51a2202..92645e0 100644 --- a/src/c/modules.c +++ b/src/c/modules.c @@ -19,7 +19,7 @@ void setup_modules(lua_State *L) { pushstuple(L, "sandbox", lua_sandbox); pushstuple(L, "textgpu", lua_textgpu); pushstuple(L, "random", lua_util_random); - + pushstuple(L, "buffer", lua_util_buffer); pushstuple(L, "eepromDefault", res_eepromDefault); diff --git a/src/lua/core/filesystem.lua b/src/lua/core/filesystem.lua index 41ad5ff..6ee750d 100644 --- a/src/lua/core/filesystem.lua +++ b/src/lua/core/filesystem.lua @@ -73,8 +73,9 @@ function filesystem.register(basePath) checkArg(1, path, "string") checkArg(2, path, "string", "nil") local m = "r" - if mode == "a" then m = "a" - elseif mode == "w" then m = "w" end + mode = mode or "" + if mode:match("a") then m = "a" + elseif mode:match("w") then m = "w" end local fd = native.fs_open(realpath(path), m) if not fd then return nil, path diff --git a/src/lua/core/init.lua b/src/lua/core/init.lua index bd7c97f..631e14a 100644 --- a/src/lua/core/init.lua +++ b/src/lua/core/init.lua @@ -43,6 +43,7 @@ function main() --Utils loadModule("random") loadModule("color") + loadModule("buffer") modules.address = modules.random.uuid() --TODO: PREALPHA: Make constant diff --git a/src/lua/core/internet.lua b/src/lua/core/internet.lua index 31f8317..b6e8def 100644 --- a/src/lua/core/internet.lua +++ b/src/lua/core/internet.lua @@ -17,7 +17,7 @@ function internet.start() checkArg(1, address, "string") checkArg(2, port, "number") - local sfd, reason = net.open(address, tostring(port)) + local sfd, reason = net.open(address, port) return { finishConnect = function() if not sfd then @@ -35,18 +35,83 @@ function internet.start() return net.write(sfd, data) end, close = function() - native.close(sfd) + native.fs_close(sfd) end } end function component.request(url, post) local host = url:match("http://([^/]+)") - local con = component.connect(host, 80) - if con:finishConnect() then - con:write("GET " .. url .. " HTTP/1.1\r\nHost: " .. host .. "\r\n\r\n") + local socket = component.connect(host, 80) + if socket.finishConnect() then + socket.write("GET " .. url .. " HTTP/1.1\r\nHost: " .. host .. "\r\nConnection: close\r\n\r\n") end + local stream = {} + + function stream:seek() + return nil, "bad file descriptor" + end + + function stream:write() + return nil, "bad file descriptor" + end + + function stream:read(n) + if not socket then + return nil, "connection is closed" + end + return socket.read(n) + end + + function stream:close() + if socket then + socket.close() + socket = nil + end + end + + local connection = modules.buffer.new("rb", stream) + connection.readTimeout = 10 + local header = nil + + --TODO: GC close + --TODO: Chunked support + + local finishConnect = function() --Read header + header = {} + header.status = connection:read("*l"):match("HTTP/.%.. (%d+) (.+)\r") + while true do + local line = connection:read("*l") + if not line or line == "" or line == "\r" then + break + end + io.stderr:write("\nline: " .. line .. "\n") + local k, v = line:match("([^:]+): (.+)\r") + header[k:lower()] = v + end + header["content-length"] = tonumber(header["content-length"]) + end + + return { + finishConnect = finishConnect, + read = function(n) + if not header then + finishConnect() + end + if header["content-length"] < 1 then + return nil + end + checkArg(1, n, "number", "nil") + n = n or math.min(8192, header["content-length"]) + local res = connection:read(n) + header["content-length"] = header["content-length"] - #res + return res + end, + close = function() + connection:close() + end + } end modules.component.api.register(nil, "internet", component) diff --git a/src/lua/core/util/buffer.lua b/src/lua/core/util/buffer.lua new file mode 100644 index 0000000..275161a --- /dev/null +++ b/src/lua/core/util/buffer.lua @@ -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 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