From 1be853338461e920dc537ef6f061ae34fe75e55b Mon Sep 17 00:00:00 2001 From: Elliott Carlson Date: Wed, 7 Aug 2024 12:11:03 -0400 Subject: [PATCH 1/2] (feat): Add luasocket as a submodule --- .gitmodules | 3 +++ libs/luasocket | 1 + 2 files changed, 4 insertions(+) create mode 160000 libs/luasocket diff --git a/.gitmodules b/.gitmodules index faf5656b8..f979151aa 100644 --- a/.gitmodules +++ b/.gitmodules @@ -38,3 +38,6 @@ [submodule "libs/imgui"] path = libs/imgui url = https://github.com/TeamREPENTOGON/imgui +[submodule "libs/luasocket"] + path = libs/luasocket + url = https://github.com/lunarmodules/luasocket.git diff --git a/libs/luasocket b/libs/luasocket new file mode 160000 index 000000000..1fad16269 --- /dev/null +++ b/libs/luasocket @@ -0,0 +1 @@ +Subproject commit 1fad1626900a128be724cba9e9c19a6b2fe2bf6b From cdd6c10cf2a7d85450fb47af0d5bf9886211f4cf Mon Sep 17 00:00:00 2001 From: Elliott Carlson Date: Wed, 7 Aug 2024 12:28:01 -0400 Subject: [PATCH 2/2] (feat): Build repentogon version of luasocket for Lua 5.4 --- CMakeLists.txt | 54 ++- libs/luasocket_patch.diff | 13 + repentogon/resources/scripts/main_ex.lua | 4 + .../scripts/repentogon_socket/ftp.lua | 281 ++++++++++++++ .../scripts/repentogon_socket/http.lua | 350 ++++++++++++++++++ .../scripts/repentogon_socket/smtp.lua | 251 +++++++++++++ .../scripts/repentogon_socket/tp.lua | 123 ++++++ .../scripts/repentogon_socket/url.lua | 297 +++++++++++++++ 8 files changed, 1372 insertions(+), 1 deletion(-) create mode 100644 libs/luasocket_patch.diff create mode 100644 repentogon/resources/scripts/repentogon_socket/ftp.lua create mode 100644 repentogon/resources/scripts/repentogon_socket/http.lua create mode 100644 repentogon/resources/scripts/repentogon_socket/smtp.lua create mode 100644 repentogon/resources/scripts/repentogon_socket/tp.lua create mode 100644 repentogon/resources/scripts/repentogon_socket/url.lua diff --git a/CMakeLists.txt b/CMakeLists.txt index 897718caf..9c28f40df 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,7 +14,8 @@ include_directories(${CMAKE_SOURCE_DIR}/include "${CMAKE_SOURCE_DIR}/libs/lua" "${CMAKE_SOURCE_DIR}/libs/LuaBridge/Source/LuaBridge" "${CMAKE_SOURCE_DIR}/libzhl" "${CMAKE_SOURCE_DIR}/libs/imgui" "${CMAKE_SOURCE_DIR}/libs/imgui/misc/freetype" "${CMAKE_SOURCE_DIR}/libs/imgui/backends" - "${CMAKE_SOURCE_DIR}/libs/NaturalSort" "${CMAKE_SOURCE_DIR}/libs/glad/include" "${CMAKE_SOURCE_DIR}/libs/mINI/src/mini") + "${CMAKE_SOURCE_DIR}/libs/NaturalSort" "${CMAKE_SOURCE_DIR}/libs/glad/include" "${CMAKE_SOURCE_DIR}/libs/mINI/src/mini" + "${CMAKE_SOURCE_DIR}/libs/luasocket") # Zydis option(ZYDIS_BUILD_TOOLS "" OFF) @@ -77,6 +78,55 @@ list(REMOVE_ITEM LUA_SRC "${CMAKE_SOURCE_DIR}/libs/lua/onelua.c") add_compile_definitions(LUA_BUILD_AS_DLL) add_library(Lua5.4 SHARED ${LUA_SRC}) +# LuaSocket +set(LUASOCKET_SRC + ${CMAKE_SOURCE_DIR}/libs/luasocket/src/luasocket.c + ${CMAKE_SOURCE_DIR}/libs/luasocket/src/timeout.c + ${CMAKE_SOURCE_DIR}/libs/luasocket/src/buffer.c + ${CMAKE_SOURCE_DIR}/libs/luasocket/src/io.c + ${CMAKE_SOURCE_DIR}/libs/luasocket/src/auxiliar.c + ${CMAKE_SOURCE_DIR}/libs/luasocket/src/compat.c + ${CMAKE_SOURCE_DIR}/libs/luasocket/src/options.c + ${CMAKE_SOURCE_DIR}/libs/luasocket/src/inet.c + ${CMAKE_SOURCE_DIR}/libs/luasocket/src/except.c + ${CMAKE_SOURCE_DIR}/libs/luasocket/src/select.c + ${CMAKE_SOURCE_DIR}/libs/luasocket/src/tcp.c + ${CMAKE_SOURCE_DIR}/libs/luasocket/src/udp.c + ${CMAKE_SOURCE_DIR}/libs/luasocket/src/mime.c + ${CMAKE_SOURCE_DIR}/libs/luasocket/src/wsocket.c +) + +set(LUASOCKET_HEADERS + ${CMAKE_SOURCE_DIR}/libs/luasocket/src/luasocket.h + ${CMAKE_SOURCE_DIR}/libs/luasocket/src/timeout.h + ${CMAKE_SOURCE_DIR}/libs/luasocket/src/buffer.h + ${CMAKE_SOURCE_DIR}/libs/luasocket/src/io.h + ${CMAKE_SOURCE_DIR}/libs/luasocket/src/auxiliar.h + ${CMAKE_SOURCE_DIR}/libs/luasocket/src/compat.h + ${CMAKE_SOURCE_DIR}/libs/luasocket/src/options.h + ${CMAKE_SOURCE_DIR}/libs/luasocket/src/inet.h + ${CMAKE_SOURCE_DIR}/libs/luasocket/src/except.h + ${CMAKE_SOURCE_DIR}/libs/luasocket/src/select.h + ${CMAKE_SOURCE_DIR}/libs/luasocket/src/tcp.h + ${CMAKE_SOURCE_DIR}/libs/luasocket/src/udp.h + ${CMAKE_SOURCE_DIR}/libs/luasocket/src/mime.h + ${CMAKE_SOURCE_DIR}/libs/luasocket/src/wsocket.h +) +execute_process( + COMMAND ${GIT_EXECUTABLE} apply ${CMAKE_SOURCE_DIR}/libs/luasocket_patch.diff + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/libs/luasocket + RESULT_VARIABLE result +) + +if(result) + message(FATAL_ERROR "Failed to apply luasocket_patch.diff: ${result}") +endif() + +add_compile_definitions(_WIN32) +add_library(LuaSocket SHARED ${LUASOCKET_SRC} ${LUASOCKET_HEADERS}) +target_link_libraries(LuaSocket ws2_32 Lua5.4) +# target_include_directories(LuaSocket PUBLIC ${CMAKE_SOURCE_DIR}/libs/luasocket/src) + # IsaacRepentance.h/.cpp FILE(GLOB ZHL_FILES ${CMAKE_SOURCE_DIR}/libzhl/functions/*.zhl) LIST(APPEND ZHL_FILES ${CMAKE_SOURCE_DIR}/libzhl/functions/ExtraTypes) @@ -207,6 +257,7 @@ target_compile_options(zhlDelirium PUBLIC "/MD") add_custom_command(TARGET zhlREPENTOGON POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory "${PROJECT_SOURCE_DIR}/repentogon/resources" "$/resources") add_custom_command(TARGET zhlREPENTOGON POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory "${PROJECT_SOURCE_DIR}/repentogon/resources-repentogon" "$/resources-repentogon") +add_custom_command(TARGET zhlREPENTOGON POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy "$/LuaSocket.dll" "$/resources/scripts/repentogon_socket/core.dll") # add_custom_command(TARGET zhlDelirium POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory "${PROJECT_SOURCE_DIR}/delirium/resources-delirium" "$/resources-delirium") if(NOT ISAAC_DIRECTORY STREQUAL "") @@ -218,6 +269,7 @@ if(NOT ISAAC_DIRECTORY STREQUAL "") add_custom_command(TARGET zhlREPENTOGON POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy "$/zhlREPENTOGON.dll" "${ISAAC_DIRECTORY}") add_custom_command(TARGET zhlREPENTOGON POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory "${PROJECT_SOURCE_DIR}/repentogon/resources" "${ISAAC_DIRECTORY}/resources") add_custom_command(TARGET zhlREPENTOGON POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory "${PROJECT_SOURCE_DIR}/repentogon/resources-repentogon" "${ISAAC_DIRECTORY}/resources-repentogon") + add_custom_command(TARGET zhlREPENTOGON POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy "$/LuaSocket.dll" "${ISAAC_DIRECTORY}/resources/scripts/repentogon_socket/core.dll") add_custom_command(TARGET zhlDelirium POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy "$/zhlDelirium.dll" "${ISAAC_DIRECTORY}") # add_custom_command(TARGET zhlDelirium POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory "${PROJECT_SOURCE_DIR}/delirium/resources-delirium" "${ISAAC_DIRECTORY}/resources-delirium") endif() diff --git a/libs/luasocket_patch.diff b/libs/luasocket_patch.diff new file mode 100644 index 000000000..4ebe9b092 --- /dev/null +++ b/libs/luasocket_patch.diff @@ -0,0 +1,13 @@ +diff --git a/src/luasocket.c b/src/luasocket.c +index 0fd99f7..683fa08 100644 +--- a/src/luasocket.c ++++ b/src/luasocket.c +@@ -96,7 +96,7 @@ static int base_open(lua_State *L) { + /*-------------------------------------------------------------------------*\ + * Initializes all library modules. + \*-------------------------------------------------------------------------*/ +-LUASOCKET_API int luaopen_socket_core(lua_State *L) { ++LUASOCKET_API int luaopen_repentogon_socket_core(lua_State *L) { + int i; + base_open(L); + for (i = 0; mod[i].name; i++) mod[i].func(L); diff --git a/repentogon/resources/scripts/main_ex.lua b/repentogon/resources/scripts/main_ex.lua index 0735d13ae..bba8634e9 100644 --- a/repentogon/resources/scripts/main_ex.lua +++ b/repentogon/resources/scripts/main_ex.lua @@ -49,6 +49,10 @@ end local oldrequire = require function require(modname) + if modname == "socket.core" then + return oldrequire("repentogon_socket.core") + end + local ret = oldrequire(modname) return ret end diff --git a/repentogon/resources/scripts/repentogon_socket/ftp.lua b/repentogon/resources/scripts/repentogon_socket/ftp.lua new file mode 100644 index 000000000..624e58205 --- /dev/null +++ b/repentogon/resources/scripts/repentogon_socket/ftp.lua @@ -0,0 +1,281 @@ +----------------------------------------------------------------------------- +-- FTP support for the Lua language +-- LuaSocket toolkit. +-- Author: Diego Nehab +-- RCS ID: $Id: ftp.lua,v 1.45 2007/07/11 19:25:47 diego Exp $ +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +-- Declare module and import dependencies +----------------------------------------------------------------------------- +local base = _G +local table = require("table") +local string = require("string") +local math = require("math") +local socket = require("socket") +local url = require("socket.url") +local tp = require("socket.tp") +local ltn12 = require("ltn12") +module("socket.ftp") + +----------------------------------------------------------------------------- +-- Program constants +----------------------------------------------------------------------------- +-- timeout in seconds before the program gives up on a connection +TIMEOUT = 60 +-- default port for ftp service +PORT = 21 +-- this is the default anonymous password. used when no password is +-- provided in url. should be changed to your e-mail. +USER = "ftp" +PASSWORD = "anonymous@anonymous.org" + +----------------------------------------------------------------------------- +-- Low level FTP API +----------------------------------------------------------------------------- +local metat = { __index = {} } + +function open(server, port, create) + local tp = socket.try(tp.connect(server, port or PORT, TIMEOUT, create)) + local f = base.setmetatable({ tp = tp }, metat) + -- make sure everything gets closed in an exception + f.try = socket.newtry(function() f:close() end) + return f +end + +function metat.__index:portconnect() + self.try(self.server:settimeout(TIMEOUT)) + self.data = self.try(self.server:accept()) + self.try(self.data:settimeout(TIMEOUT)) +end + +function metat.__index:pasvconnect() + self.data = self.try(socket.tcp()) + self.try(self.data:settimeout(TIMEOUT)) + self.try(self.data:connect(self.pasvt.ip, self.pasvt.port)) +end + +function metat.__index:login(user, password) + self.try(self.tp:command("user", user or USER)) + local code, reply = self.try(self.tp:check{"2..", 331}) + if code == 331 then + self.try(self.tp:command("pass", password or PASSWORD)) + self.try(self.tp:check("2..")) + end + return 1 +end + +function metat.__index:pasv() + self.try(self.tp:command("pasv")) + local code, reply = self.try(self.tp:check("2..")) + local pattern = "(%d+)%D(%d+)%D(%d+)%D(%d+)%D(%d+)%D(%d+)" + local a, b, c, d, p1, p2 = socket.skip(2, string.find(reply, pattern)) + self.try(a and b and c and d and p1 and p2, reply) + self.pasvt = { + ip = string.format("%d.%d.%d.%d", a, b, c, d), + port = p1*256 + p2 + } + if self.server then + self.server:close() + self.server = nil + end + return self.pasvt.ip, self.pasvt.port +end + +function metat.__index:port(ip, port) + self.pasvt = nil + if not ip then + ip, port = self.try(self.tp:getcontrol():getsockname()) + self.server = self.try(socket.bind(ip, 0)) + ip, port = self.try(self.server:getsockname()) + self.try(self.server:settimeout(TIMEOUT)) + end + local pl = math.mod(port, 256) + local ph = (port - pl)/256 + local arg = string.gsub(string.format("%s,%d,%d", ip, ph, pl), "%.", ",") + self.try(self.tp:command("port", arg)) + self.try(self.tp:check("2..")) + return 1 +end + +function metat.__index:send(sendt) + self.try(self.pasvt or self.server, "need port or pasv first") + -- if there is a pasvt table, we already sent a PASV command + -- we just get the data connection into self.data + if self.pasvt then self:pasvconnect() end + -- get the transfer argument and command + local argument = sendt.argument or + url.unescape(string.gsub(sendt.path or "", "^[/\\]", "")) + if argument == "" then argument = nil end + local command = sendt.command or "stor" + -- send the transfer command and check the reply + self.try(self.tp:command(command, argument)) + local code, reply = self.try(self.tp:check{"2..", "1.."}) + -- if there is not a a pasvt table, then there is a server + -- and we already sent a PORT command + if not self.pasvt then self:portconnect() end + -- get the sink, source and step for the transfer + local step = sendt.step or ltn12.pump.step + local readt = {self.tp.c} + local checkstep = function(src, snk) + -- check status in control connection while downloading + local readyt = socket.select(readt, nil, 0) + if readyt[tp] then code = self.try(self.tp:check("2..")) end + return step(src, snk) + end + local sink = socket.sink("close-when-done", self.data) + -- transfer all data and check error + self.try(ltn12.pump.all(sendt.source, sink, checkstep)) + if string.find(code, "1..") then self.try(self.tp:check("2..")) end + -- done with data connection + self.data:close() + -- find out how many bytes were sent + local sent = socket.skip(1, self.data:getstats()) + self.data = nil + return sent +end + +function metat.__index:receive(recvt) + self.try(self.pasvt or self.server, "need port or pasv first") + if self.pasvt then self:pasvconnect() end + local argument = recvt.argument or + url.unescape(string.gsub(recvt.path or "", "^[/\\]", "")) + if argument == "" then argument = nil end + local command = recvt.command or "retr" + self.try(self.tp:command(command, argument)) + local code = self.try(self.tp:check{"1..", "2.."}) + if not self.pasvt then self:portconnect() end + local source = socket.source("until-closed", self.data) + local step = recvt.step or ltn12.pump.step + self.try(ltn12.pump.all(source, recvt.sink, step)) + if string.find(code, "1..") then self.try(self.tp:check("2..")) end + self.data:close() + self.data = nil + return 1 +end + +function metat.__index:cwd(dir) + self.try(self.tp:command("cwd", dir)) + self.try(self.tp:check(250)) + return 1 +end + +function metat.__index:type(type) + self.try(self.tp:command("type", type)) + self.try(self.tp:check(200)) + return 1 +end + +function metat.__index:greet() + local code = self.try(self.tp:check{"1..", "2.."}) + if string.find(code, "1..") then self.try(self.tp:check("2..")) end + return 1 +end + +function metat.__index:quit() + self.try(self.tp:command("quit")) + self.try(self.tp:check("2..")) + return 1 +end + +function metat.__index:close() + if self.data then self.data:close() end + if self.server then self.server:close() end + return self.tp:close() +end + +----------------------------------------------------------------------------- +-- High level FTP API +----------------------------------------------------------------------------- +local function override(t) + if t.url then + local u = url.parse(t.url) + for i,v in base.pairs(t) do + u[i] = v + end + return u + else return t end +end + +local function tput(putt) + putt = override(putt) + socket.try(putt.host, "missing hostname") + local f = open(putt.host, putt.port, putt.create) + f:greet() + f:login(putt.user, putt.password) + if putt.type then f:type(putt.type) end + f:pasv() + local sent = f:send(putt) + f:quit() + f:close() + return sent +end + +local default = { + path = "/", + scheme = "ftp" +} + +local function parse(u) + local t = socket.try(url.parse(u, default)) + socket.try(t.scheme == "ftp", "wrong scheme '" .. t.scheme .. "'") + socket.try(t.host, "missing hostname") + local pat = "^type=(.)$" + if t.params then + t.type = socket.skip(2, string.find(t.params, pat)) + socket.try(t.type == "a" or t.type == "i", + "invalid type '" .. t.type .. "'") + end + return t +end + +local function sput(u, body) + local putt = parse(u) + putt.source = ltn12.source.string(body) + return tput(putt) +end + +put = socket.protect(function(putt, body) + if base.type(putt) == "string" then return sput(putt, body) + else return tput(putt) end +end) + +local function tget(gett) + gett = override(gett) + socket.try(gett.host, "missing hostname") + local f = open(gett.host, gett.port, gett.create) + f:greet() + f:login(gett.user, gett.password) + if gett.type then f:type(gett.type) end + f:pasv() + f:receive(gett) + f:quit() + return f:close() +end + +local function sget(u) + local gett = parse(u) + local t = {} + gett.sink = ltn12.sink.table(t) + tget(gett) + return table.concat(t) +end + +command = socket.protect(function(cmdt) + cmdt = override(cmdt) + socket.try(cmdt.host, "missing hostname") + socket.try(cmdt.command, "missing command") + local f = open(cmdt.host, cmdt.port, cmdt.create) + f:greet() + f:login(cmdt.user, cmdt.password) + f.try(f.tp:command(cmdt.command, cmdt.argument)) + if cmdt.check then f.try(f.tp:check(cmdt.check)) end + f:quit() + return f:close() +end) + +get = socket.protect(function(gett) + if base.type(gett) == "string" then return sget(gett) + else return tget(gett) end +end) + diff --git a/repentogon/resources/scripts/repentogon_socket/http.lua b/repentogon/resources/scripts/repentogon_socket/http.lua new file mode 100644 index 000000000..976af895e --- /dev/null +++ b/repentogon/resources/scripts/repentogon_socket/http.lua @@ -0,0 +1,350 @@ +----------------------------------------------------------------------------- +-- HTTP/1.1 client support for the Lua language. +-- LuaSocket toolkit. +-- Author: Diego Nehab +-- RCS ID: $Id: http.lua,v 1.70 2007/03/12 04:08:40 diego Exp $ +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +-- Declare module and import dependencies +------------------------------------------------------------------------------- +local socket = require("socket") +local url = require("socket.url") +local ltn12 = require("ltn12") +local mime = require("mime") +local string = require("string") +local base = _G +local table = require("table") +module("socket.http") + +----------------------------------------------------------------------------- +-- Program constants +----------------------------------------------------------------------------- +-- connection timeout in seconds +TIMEOUT = 60 +-- default port for document retrieval +PORT = 80 +-- user agent field sent in request +USERAGENT = socket._VERSION + +----------------------------------------------------------------------------- +-- Reads MIME headers from a connection, unfolding where needed +----------------------------------------------------------------------------- +local function receiveheaders(sock, headers) + local line, name, value, err + headers = headers or {} + -- get first line + line, err = sock:receive() + if err then return nil, err end + -- headers go until a blank line is found + while line ~= "" do + -- get field-name and value + name, value = socket.skip(2, string.find(line, "^(.-):%s*(.*)")) + if not (name and value) then return nil, "malformed reponse headers" end + name = string.lower(name) + -- get next line (value might be folded) + line, err = sock:receive() + if err then return nil, err end + -- unfold any folded values + while string.find(line, "^%s") do + value = value .. line + line = sock:receive() + if err then return nil, err end + end + -- save pair in table + if headers[name] then headers[name] = headers[name] .. ", " .. value + else headers[name] = value end + end + return headers +end + +----------------------------------------------------------------------------- +-- Extra sources and sinks +----------------------------------------------------------------------------- +socket.sourcet["http-chunked"] = function(sock, headers) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function() + -- get chunk size, skip extention + local line, err = sock:receive() + if err then return nil, err end + local size = base.tonumber(string.gsub(line, ";.*", ""), 16) + if not size then return nil, "invalid chunk size" end + -- was it the last chunk? + if size > 0 then + -- if not, get chunk and skip terminating CRLF + local chunk, err, part = sock:receive(size) + if chunk then sock:receive() end + return chunk, err + else + -- if it was, read trailers into headers table + headers, err = receiveheaders(sock, headers) + if not headers then return nil, err end + end + end + }) +end + +socket.sinkt["http-chunked"] = function(sock) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function(self, chunk, err) + if not chunk then return sock:send("0\r\n\r\n") end + local size = string.format("%X\r\n", string.len(chunk)) + return sock:send(size .. chunk .. "\r\n") + end + }) +end + +----------------------------------------------------------------------------- +-- Low level HTTP API +----------------------------------------------------------------------------- +local metat = { __index = {} } + +function open(host, port, create) + -- create socket with user connect function, or with default + local c = socket.try((create or socket.tcp)()) + local h = base.setmetatable({ c = c }, metat) + -- create finalized try + h.try = socket.newtry(function() h:close() end) + -- set timeout before connecting + h.try(c:settimeout(TIMEOUT)) + h.try(c:connect(host, port or PORT)) + -- here everything worked + return h +end + +function metat.__index:sendrequestline(method, uri) + local reqline = string.format("%s %s HTTP/1.1\r\n", method or "GET", uri) + return self.try(self.c:send(reqline)) +end + +function metat.__index:sendheaders(headers) + local h = "\r\n" + for i, v in base.pairs(headers) do + h = i .. ": " .. v .. "\r\n" .. h + end + self.try(self.c:send(h)) + return 1 +end + +function metat.__index:sendbody(headers, source, step) + source = source or ltn12.source.empty() + step = step or ltn12.pump.step + -- if we don't know the size in advance, send chunked and hope for the best + local mode = "http-chunked" + if headers["content-length"] then mode = "keep-open" end + return self.try(ltn12.pump.all(source, socket.sink(mode, self.c), step)) +end + +function metat.__index:receivestatusline() + local status = self.try(self.c:receive(5)) + -- identify HTTP/0.9 responses, which do not contain a status line + -- this is just a heuristic, but is what the RFC recommends + if status ~= "HTTP/" then return nil, status end + -- otherwise proceed reading a status line + status = self.try(self.c:receive("*l", status)) + local code = socket.skip(2, string.find(status, "HTTP/%d*%.%d* (%d%d%d)")) + return self.try(base.tonumber(code), status) +end + +function metat.__index:receiveheaders() + return self.try(receiveheaders(self.c)) +end + +function metat.__index:receivebody(headers, sink, step) + sink = sink or ltn12.sink.null() + step = step or ltn12.pump.step + local length = base.tonumber(headers["content-length"]) + local t = headers["transfer-encoding"] -- shortcut + local mode = "default" -- connection close + if t and t ~= "identity" then mode = "http-chunked" + elseif base.tonumber(headers["content-length"]) then mode = "by-length" end + return self.try(ltn12.pump.all(socket.source(mode, self.c, length), + sink, step)) +end + +function metat.__index:receive09body(status, sink, step) + local source = ltn12.source.rewind(socket.source("until-closed", self.c)) + source(status) + return self.try(ltn12.pump.all(source, sink, step)) +end + +function metat.__index:close() + return self.c:close() +end + +----------------------------------------------------------------------------- +-- High level HTTP API +----------------------------------------------------------------------------- +local function adjusturi(reqt) + local u = reqt + -- if there is a proxy, we need the full url. otherwise, just a part. + if not reqt.proxy and not PROXY then + u = { + path = socket.try(reqt.path, "invalid path 'nil'"), + params = reqt.params, + query = reqt.query, + fragment = reqt.fragment + } + end + return url.build(u) +end + +local function adjustproxy(reqt) + local proxy = reqt.proxy or PROXY + if proxy then + proxy = url.parse(proxy) + return proxy.host, proxy.port or 3128 + else + return reqt.host, reqt.port + end +end + +local function adjustheaders(reqt) + -- default headers + local lower = { + ["user-agent"] = USERAGENT, + ["host"] = reqt.host, + ["connection"] = "close, TE", + ["te"] = "trailers" + } + -- if we have authentication information, pass it along + if reqt.user and reqt.password then + lower["authorization"] = + "Basic " .. (mime.b64(reqt.user .. ":" .. reqt.password)) + end + -- override with user headers + for i,v in base.pairs(reqt.headers or lower) do + lower[string.lower(i)] = v + end + return lower +end + +-- default url parts +local default = { + host = "", + port = PORT, + path ="/", + scheme = "http" +} + +local function adjustrequest(reqt) + -- parse url if provided + local nreqt = reqt.url and url.parse(reqt.url, default) or {} + -- explicit components override url + for i,v in base.pairs(reqt) do nreqt[i] = v end + if nreqt.port == "" then nreqt.port = 80 end + socket.try(nreqt.host and nreqt.host ~= "", + "invalid host '" .. base.tostring(nreqt.host) .. "'") + -- compute uri if user hasn't overriden + nreqt.uri = reqt.uri or adjusturi(nreqt) + -- ajust host and port if there is a proxy + nreqt.host, nreqt.port = adjustproxy(nreqt) + -- adjust headers in request + nreqt.headers = adjustheaders(nreqt) + return nreqt +end + +local function shouldredirect(reqt, code, headers) + return headers.location and + string.gsub(headers.location, "%s", "") ~= "" and + (reqt.redirect ~= false) and + (code == 301 or code == 302) and + (not reqt.method or reqt.method == "GET" or reqt.method == "HEAD") + and (not reqt.nredirects or reqt.nredirects < 5) +end + +local function shouldreceivebody(reqt, code) + if reqt.method == "HEAD" then return nil end + if code == 204 or code == 304 then return nil end + if code >= 100 and code < 200 then return nil end + return 1 +end + +-- forward declarations +local trequest, tredirect + +function tredirect(reqt, location) + local result, code, headers, status = trequest { + -- the RFC says the redirect URL has to be absolute, but some + -- servers do not respect that + url = url.absolute(reqt.url, location), + source = reqt.source, + sink = reqt.sink, + headers = reqt.headers, + proxy = reqt.proxy, + nredirects = (reqt.nredirects or 0) + 1, + create = reqt.create + } + -- pass location header back as a hint we redirected + headers = headers or {} + headers.location = headers.location or location + return result, code, headers, status +end + +function trequest(reqt) + -- we loop until we get what we want, or + -- until we are sure there is no way to get it + local nreqt = adjustrequest(reqt) + local h = open(nreqt.host, nreqt.port, nreqt.create) + -- send request line and headers + h:sendrequestline(nreqt.method, nreqt.uri) + h:sendheaders(nreqt.headers) + -- if there is a body, send it + if nreqt.source then + h:sendbody(nreqt.headers, nreqt.source, nreqt.step) + end + local code, status = h:receivestatusline() + -- if it is an HTTP/0.9 server, simply get the body and we are done + if not code then + h:receive09body(status, nreqt.sink, nreqt.step) + return 1, 200 + end + local headers + -- ignore any 100-continue messages + while code == 100 do + headers = h:receiveheaders() + code, status = h:receivestatusline() + end + headers = h:receiveheaders() + -- at this point we should have a honest reply from the server + -- we can't redirect if we already used the source, so we report the error + if shouldredirect(nreqt, code, headers) and not nreqt.source then + h:close() + return tredirect(reqt, headers.location) + end + -- here we are finally done + if shouldreceivebody(nreqt, code) then + h:receivebody(headers, nreqt.sink, nreqt.step) + end + h:close() + return 1, code, headers, status +end + +local function srequest(u, b) + local t = {} + local reqt = { + url = u, + sink = ltn12.sink.table(t) + } + if b then + reqt.source = ltn12.source.string(b) + reqt.headers = { + ["content-length"] = string.len(b), + ["content-type"] = "application/x-www-form-urlencoded" + } + reqt.method = "POST" + end + local code, headers, status = socket.skip(1, trequest(reqt)) + return table.concat(t), code, headers, status +end + +request = socket.protect(function(reqt, body) + if base.type(reqt) == "string" then return srequest(reqt, body) + else return trequest(reqt) end +end) diff --git a/repentogon/resources/scripts/repentogon_socket/smtp.lua b/repentogon/resources/scripts/repentogon_socket/smtp.lua new file mode 100644 index 000000000..4335e61b5 --- /dev/null +++ b/repentogon/resources/scripts/repentogon_socket/smtp.lua @@ -0,0 +1,251 @@ +----------------------------------------------------------------------------- +-- SMTP client support for the Lua language. +-- LuaSocket toolkit. +-- Author: Diego Nehab +-- RCS ID: $Id: smtp.lua,v 1.46 2007/03/12 04:08:40 diego Exp $ +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +-- Declare module and import dependencies +----------------------------------------------------------------------------- +local base = _G +local coroutine = require("coroutine") +local string = require("string") +local math = require("math") +local os = require("os") +local socket = require("socket") +local tp = require("socket.tp") +local ltn12 = require("ltn12") +local mime = require("mime") +module("socket.smtp") + +----------------------------------------------------------------------------- +-- Program constants +----------------------------------------------------------------------------- +-- timeout for connection +TIMEOUT = 60 +-- default server used to send e-mails +SERVER = "localhost" +-- default port +PORT = 25 +-- domain used in HELO command and default sendmail +-- If we are under a CGI, try to get from environment +DOMAIN = os.getenv("SERVER_NAME") or "localhost" +-- default time zone (means we don't know) +ZONE = "-0000" + +--------------------------------------------------------------------------- +-- Low level SMTP API +----------------------------------------------------------------------------- +local metat = { __index = {} } + +function metat.__index:greet(domain) + self.try(self.tp:check("2..")) + self.try(self.tp:command("EHLO", domain or DOMAIN)) + return socket.skip(1, self.try(self.tp:check("2.."))) +end + +function metat.__index:mail(from) + self.try(self.tp:command("MAIL", "FROM:" .. from)) + return self.try(self.tp:check("2..")) +end + +function metat.__index:rcpt(to) + self.try(self.tp:command("RCPT", "TO:" .. to)) + return self.try(self.tp:check("2..")) +end + +function metat.__index:data(src, step) + self.try(self.tp:command("DATA")) + self.try(self.tp:check("3..")) + self.try(self.tp:source(src, step)) + self.try(self.tp:send("\r\n.\r\n")) + return self.try(self.tp:check("2..")) +end + +function metat.__index:quit() + self.try(self.tp:command("QUIT")) + return self.try(self.tp:check("2..")) +end + +function metat.__index:close() + return self.tp:close() +end + +function metat.__index:login(user, password) + self.try(self.tp:command("AUTH", "LOGIN")) + self.try(self.tp:check("3..")) + self.try(self.tp:command(mime.b64(user))) + self.try(self.tp:check("3..")) + self.try(self.tp:command(mime.b64(password))) + return self.try(self.tp:check("2..")) +end + +function metat.__index:plain(user, password) + local auth = "PLAIN " .. mime.b64("\0" .. user .. "\0" .. password) + self.try(self.tp:command("AUTH", auth)) + return self.try(self.tp:check("2..")) +end + +function metat.__index:auth(user, password, ext) + if not user or not password then return 1 end + if string.find(ext, "AUTH[^\n]+LOGIN") then + return self:login(user, password) + elseif string.find(ext, "AUTH[^\n]+PLAIN") then + return self:plain(user, password) + else + self.try(nil, "authentication not supported") + end +end + +-- send message or throw an exception +function metat.__index:send(mailt) + self:mail(mailt.from) + if base.type(mailt.rcpt) == "table" then + for i,v in base.ipairs(mailt.rcpt) do + self:rcpt(v) + end + else + self:rcpt(mailt.rcpt) + end + self:data(ltn12.source.chain(mailt.source, mime.stuff()), mailt.step) +end + +function open(server, port, create) + local tp = socket.try(tp.connect(server or SERVER, port or PORT, + TIMEOUT, create)) + local s = base.setmetatable({tp = tp}, metat) + -- make sure tp is closed if we get an exception + s.try = socket.newtry(function() + s:close() + end) + return s +end + +-- convert headers to lowercase +local function lower_headers(headers) + local lower = {} + for i,v in base.pairs(headers or lower) do + lower[string.lower(i)] = v + end + return lower +end + +--------------------------------------------------------------------------- +-- Multipart message source +----------------------------------------------------------------------------- +-- returns a hopefully unique mime boundary +local seqno = 0 +local function newboundary() + seqno = seqno + 1 + return string.format('%s%05d==%05u', os.date('%d%m%Y%H%M%S'), + math.random(0, 99999), seqno) +end + +-- send_message forward declaration +local send_message + +-- yield the headers all at once, it's faster +local function send_headers(headers) + local h = "\r\n" + for i,v in base.pairs(headers) do + h = i .. ': ' .. v .. "\r\n" .. h + end + coroutine.yield(h) +end + +-- yield multipart message body from a multipart message table +local function send_multipart(mesgt) + -- make sure we have our boundary and send headers + local bd = newboundary() + local headers = lower_headers(mesgt.headers or {}) + headers['content-type'] = headers['content-type'] or 'multipart/mixed' + headers['content-type'] = headers['content-type'] .. + '; boundary="' .. bd .. '"' + send_headers(headers) + -- send preamble + if mesgt.body.preamble then + coroutine.yield(mesgt.body.preamble) + coroutine.yield("\r\n") + end + -- send each part separated by a boundary + for i, m in base.ipairs(mesgt.body) do + coroutine.yield("\r\n--" .. bd .. "\r\n") + send_message(m) + end + -- send last boundary + coroutine.yield("\r\n--" .. bd .. "--\r\n\r\n") + -- send epilogue + if mesgt.body.epilogue then + coroutine.yield(mesgt.body.epilogue) + coroutine.yield("\r\n") + end +end + +-- yield message body from a source +local function send_source(mesgt) + -- make sure we have a content-type + local headers = lower_headers(mesgt.headers or {}) + headers['content-type'] = headers['content-type'] or + 'text/plain; charset="iso-8859-1"' + send_headers(headers) + -- send body from source + while true do + local chunk, err = mesgt.body() + if err then coroutine.yield(nil, err) + elseif chunk then coroutine.yield(chunk) + else break end + end +end + +-- yield message body from a string +local function send_string(mesgt) + -- make sure we have a content-type + local headers = lower_headers(mesgt.headers or {}) + headers['content-type'] = headers['content-type'] or + 'text/plain; charset="iso-8859-1"' + send_headers(headers) + -- send body from string + coroutine.yield(mesgt.body) +end + +-- message source +function send_message(mesgt) + if base.type(mesgt.body) == "table" then send_multipart(mesgt) + elseif base.type(mesgt.body) == "function" then send_source(mesgt) + else send_string(mesgt) end +end + +-- set defaul headers +local function adjust_headers(mesgt) + local lower = lower_headers(mesgt.headers) + lower["date"] = lower["date"] or + os.date("!%a, %d %b %Y %H:%M:%S ") .. (mesgt.zone or ZONE) + lower["x-mailer"] = lower["x-mailer"] or socket._VERSION + -- this can't be overriden + lower["mime-version"] = "1.0" + return lower +end + +function message(mesgt) + mesgt.headers = adjust_headers(mesgt) + -- create and return message source + local co = coroutine.create(function() send_message(mesgt) end) + return function() + local ret, a, b = coroutine.resume(co) + if ret then return a, b + else return nil, a end + end +end + +--------------------------------------------------------------------------- +-- High level SMTP API +----------------------------------------------------------------------------- +send = socket.protect(function(mailt) + local s = open(mailt.server, mailt.port, mailt.create) + local ext = s:greet(mailt.domain) + s:auth(mailt.user, mailt.password, ext) + s:send(mailt) + s:quit() + return s:close() +end) diff --git a/repentogon/resources/scripts/repentogon_socket/tp.lua b/repentogon/resources/scripts/repentogon_socket/tp.lua new file mode 100644 index 000000000..5c968cd2c --- /dev/null +++ b/repentogon/resources/scripts/repentogon_socket/tp.lua @@ -0,0 +1,123 @@ +----------------------------------------------------------------------------- +-- Unified SMTP/FTP subsystem +-- LuaSocket toolkit. +-- Author: Diego Nehab +-- RCS ID: $Id: tp.lua,v 1.22 2006/03/14 09:04:15 diego Exp $ +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +-- Declare module and import dependencies +----------------------------------------------------------------------------- +local base = _G +local string = require("string") +local socket = require("socket") +local ltn12 = require("ltn12") +module("socket.tp") + +----------------------------------------------------------------------------- +-- Program constants +----------------------------------------------------------------------------- +TIMEOUT = 60 + +----------------------------------------------------------------------------- +-- Implementation +----------------------------------------------------------------------------- +-- gets server reply (works for SMTP and FTP) +local function get_reply(c) + local code, current, sep + local line, err = c:receive() + local reply = line + if err then return nil, err end + code, sep = socket.skip(2, string.find(line, "^(%d%d%d)(.?)")) + if not code then return nil, "invalid server reply" end + if sep == "-" then -- reply is multiline + repeat + line, err = c:receive() + if err then return nil, err end + current, sep = socket.skip(2, string.find(line, "^(%d%d%d)(.?)")) + reply = reply .. "\n" .. line + -- reply ends with same code + until code == current and sep == " " + end + return code, reply +end + +-- metatable for sock object +local metat = { __index = {} } + +function metat.__index:check(ok) + local code, reply = get_reply(self.c) + if not code then return nil, reply end + if base.type(ok) ~= "function" then + if base.type(ok) == "table" then + for i, v in base.ipairs(ok) do + if string.find(code, v) then + return base.tonumber(code), reply + end + end + return nil, reply + else + if string.find(code, ok) then return base.tonumber(code), reply + else return nil, reply end + end + else return ok(base.tonumber(code), reply) end +end + +function metat.__index:command(cmd, arg) + if arg then + return self.c:send(cmd .. " " .. arg.. "\r\n") + else + return self.c:send(cmd .. "\r\n") + end +end + +function metat.__index:sink(snk, pat) + local chunk, err = c:receive(pat) + return snk(chunk, err) +end + +function metat.__index:send(data) + return self.c:send(data) +end + +function metat.__index:receive(pat) + return self.c:receive(pat) +end + +function metat.__index:getfd() + return self.c:getfd() +end + +function metat.__index:dirty() + return self.c:dirty() +end + +function metat.__index:getcontrol() + return self.c +end + +function metat.__index:source(source, step) + local sink = socket.sink("keep-open", self.c) + local ret, err = ltn12.pump.all(source, sink, step or ltn12.pump.step) + return ret, err +end + +-- closes the underlying c +function metat.__index:close() + self.c:close() + return 1 +end + +-- connect with server and return c object +function connect(host, port, timeout, create) + local c, e = (create or socket.tcp)() + if not c then return nil, e end + c:settimeout(timeout or TIMEOUT) + local r, e = c:connect(host, port) + if not r then + c:close() + return nil, e + end + return base.setmetatable({c = c}, metat) +end + diff --git a/repentogon/resources/scripts/repentogon_socket/url.lua b/repentogon/resources/scripts/repentogon_socket/url.lua new file mode 100644 index 000000000..f4a213cad --- /dev/null +++ b/repentogon/resources/scripts/repentogon_socket/url.lua @@ -0,0 +1,297 @@ +----------------------------------------------------------------------------- +-- URI parsing, composition and relative URL resolution +-- LuaSocket toolkit. +-- Author: Diego Nehab +-- RCS ID: $Id: url.lua,v 1.38 2006/04/03 04:45:42 diego Exp $ +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +-- Declare module +----------------------------------------------------------------------------- +local string = require("string") +local base = _G +local table = require("table") +module("socket.url") + +----------------------------------------------------------------------------- +-- Module version +----------------------------------------------------------------------------- +_VERSION = "URL 1.0.1" + +----------------------------------------------------------------------------- +-- Encodes a string into its escaped hexadecimal representation +-- Input +-- s: binary string to be encoded +-- Returns +-- escaped representation of string binary +----------------------------------------------------------------------------- +function escape(s) + return string.gsub(s, "([^A-Za-z0-9_])", function(c) + return string.format("%%%02x", string.byte(c)) + end) +end + +----------------------------------------------------------------------------- +-- Protects a path segment, to prevent it from interfering with the +-- url parsing. +-- Input +-- s: binary string to be encoded +-- Returns +-- escaped representation of string binary +----------------------------------------------------------------------------- +local function make_set(t) + local s = {} + for i,v in base.ipairs(t) do + s[t[i]] = 1 + end + return s +end + +-- these are allowed withing a path segment, along with alphanum +-- other characters must be escaped +local segment_set = make_set { + "-", "_", ".", "!", "~", "*", "'", "(", + ")", ":", "@", "&", "=", "+", "$", ",", +} + +local function protect_segment(s) + return string.gsub(s, "([^A-Za-z0-9_])", function (c) + if segment_set[c] then return c + else return string.format("%%%02x", string.byte(c)) end + end) +end + +----------------------------------------------------------------------------- +-- Encodes a string into its escaped hexadecimal representation +-- Input +-- s: binary string to be encoded +-- Returns +-- escaped representation of string binary +----------------------------------------------------------------------------- +function unescape(s) + return string.gsub(s, "%%(%x%x)", function(hex) + return string.char(base.tonumber(hex, 16)) + end) +end + +----------------------------------------------------------------------------- +-- Builds a path from a base path and a relative path +-- Input +-- base_path +-- relative_path +-- Returns +-- corresponding absolute path +----------------------------------------------------------------------------- +local function absolute_path(base_path, relative_path) + if string.sub(relative_path, 1, 1) == "/" then return relative_path end + local path = string.gsub(base_path, "[^/]*$", "") + path = path .. relative_path + path = string.gsub(path, "([^/]*%./)", function (s) + if s ~= "./" then return s else return "" end + end) + path = string.gsub(path, "/%.$", "/") + local reduced + while reduced ~= path do + reduced = path + path = string.gsub(reduced, "([^/]*/%.%./)", function (s) + if s ~= "../../" then return "" else return s end + end) + end + path = string.gsub(reduced, "([^/]*/%.%.)$", function (s) + if s ~= "../.." then return "" else return s end + end) + return path +end + +----------------------------------------------------------------------------- +-- Parses a url and returns a table with all its parts according to RFC 2396 +-- The following grammar describes the names given to the URL parts +-- ::= :///;?# +-- ::= @: +-- ::= [:] +-- :: = {/} +-- Input +-- url: uniform resource locator of request +-- default: table with default values for each field +-- Returns +-- table with the following fields, where RFC naming conventions have +-- been preserved: +-- scheme, authority, userinfo, user, password, host, port, +-- path, params, query, fragment +-- Obs: +-- the leading '/' in {/} is considered part of +----------------------------------------------------------------------------- +function parse(url, default) + -- initialize default parameters + local parsed = {} + for i,v in base.pairs(default or parsed) do parsed[i] = v end + -- empty url is parsed to nil + if not url or url == "" then return nil, "invalid url" end + -- remove whitespace + -- url = string.gsub(url, "%s", "") + -- get fragment + url = string.gsub(url, "#(.*)$", function(f) + parsed.fragment = f + return "" + end) + -- get scheme + url = string.gsub(url, "^([%w][%w%+%-%.]*)%:", + function(s) parsed.scheme = s; return "" end) + -- get authority + url = string.gsub(url, "^//([^/]*)", function(n) + parsed.authority = n + return "" + end) + -- get query stringing + url = string.gsub(url, "%?(.*)", function(q) + parsed.query = q + return "" + end) + -- get params + url = string.gsub(url, "%;(.*)", function(p) + parsed.params = p + return "" + end) + -- path is whatever was left + if url ~= "" then parsed.path = url end + local authority = parsed.authority + if not authority then return parsed end + authority = string.gsub(authority,"^([^@]*)@", + function(u) parsed.userinfo = u; return "" end) + authority = string.gsub(authority, ":([^:]*)$", + function(p) parsed.port = p; return "" end) + if authority ~= "" then parsed.host = authority end + local userinfo = parsed.userinfo + if not userinfo then return parsed end + userinfo = string.gsub(userinfo, ":([^:]*)$", + function(p) parsed.password = p; return "" end) + parsed.user = userinfo + return parsed +end + +----------------------------------------------------------------------------- +-- Rebuilds a parsed URL from its components. +-- Components are protected if any reserved or unallowed characters are found +-- Input +-- parsed: parsed URL, as returned by parse +-- Returns +-- a stringing with the corresponding URL +----------------------------------------------------------------------------- +function build(parsed) + local ppath = parse_path(parsed.path or "") + local url = build_path(ppath) + if parsed.params then url = url .. ";" .. parsed.params end + if parsed.query then url = url .. "?" .. parsed.query end + local authority = parsed.authority + if parsed.host then + authority = parsed.host + if parsed.port then authority = authority .. ":" .. parsed.port end + local userinfo = parsed.userinfo + if parsed.user then + userinfo = parsed.user + if parsed.password then + userinfo = userinfo .. ":" .. parsed.password + end + end + if userinfo then authority = userinfo .. "@" .. authority end + end + if authority then url = "//" .. authority .. url end + if parsed.scheme then url = parsed.scheme .. ":" .. url end + if parsed.fragment then url = url .. "#" .. parsed.fragment end + -- url = string.gsub(url, "%s", "") + return url +end + +----------------------------------------------------------------------------- +-- Builds a absolute URL from a base and a relative URL according to RFC 2396 +-- Input +-- base_url +-- relative_url +-- Returns +-- corresponding absolute url +----------------------------------------------------------------------------- +function absolute(base_url, relative_url) + if base.type(base_url) == "table" then + base_parsed = base_url + base_url = build(base_parsed) + else + base_parsed = parse(base_url) + end + local relative_parsed = parse(relative_url) + if not base_parsed then return relative_url + elseif not relative_parsed then return base_url + elseif relative_parsed.scheme then return relative_url + else + relative_parsed.scheme = base_parsed.scheme + if not relative_parsed.authority then + relative_parsed.authority = base_parsed.authority + if not relative_parsed.path then + relative_parsed.path = base_parsed.path + if not relative_parsed.params then + relative_parsed.params = base_parsed.params + if not relative_parsed.query then + relative_parsed.query = base_parsed.query + end + end + else + relative_parsed.path = absolute_path(base_parsed.path or "", + relative_parsed.path) + end + end + return build(relative_parsed) + end +end + +----------------------------------------------------------------------------- +-- Breaks a path into its segments, unescaping the segments +-- Input +-- path +-- Returns +-- segment: a table with one entry per segment +----------------------------------------------------------------------------- +function parse_path(path) + local parsed = {} + path = path or "" + --path = string.gsub(path, "%s", "") + string.gsub(path, "([^/]+)", function (s) table.insert(parsed, s) end) + for i = 1, table.getn(parsed) do + parsed[i] = unescape(parsed[i]) + end + if string.sub(path, 1, 1) == "/" then parsed.is_absolute = 1 end + if string.sub(path, -1, -1) == "/" then parsed.is_directory = 1 end + return parsed +end + +----------------------------------------------------------------------------- +-- Builds a path component from its segments, escaping protected characters. +-- Input +-- parsed: path segments +-- unsafe: if true, segments are not protected before path is built +-- Returns +-- path: corresponding path stringing +----------------------------------------------------------------------------- +function build_path(parsed, unsafe) + local path = "" + local n = table.getn(parsed) + if unsafe then + for i = 1, n-1 do + path = path .. parsed[i] + path = path .. "/" + end + if n > 0 then + path = path .. parsed[n] + if parsed.is_directory then path = path .. "/" end + end + else + for i = 1, n-1 do + path = path .. protect_segment(parsed[i]) + path = path .. "/" + end + if n > 0 then + path = path .. protect_segment(parsed[n]) + if parsed.is_directory then path = path .. "/" end + end + end + if parsed.is_absolute then path = "/" .. path end + return path +end