1
0
forked from redo/BlockLua
This commit is contained in:
2025-10-05 19:50:29 -04:00
parent f447c039c7
commit 01f216f31e
16 changed files with 2824 additions and 2596 deletions

2
.gitignore vendored
View File

@@ -1,3 +1 @@
.*
!.gitignore
build/ build/

View File

@@ -27,43 +27,50 @@ _VERSION = "LTN12 1.0.1"
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
-- returns a high level filter that cycles a low-level filter -- returns a high level filter that cycles a low-level filter
function filter.cycle(low, ctx, extra) function filter.cycle(low, ctx, extra)
base.assert(low) base.assert(low)
return function(chunk) return function(chunk)
local ret local ret
ret, ctx = low(ctx, chunk, extra) ret, ctx = low(ctx, chunk, extra)
return ret return ret
end end
end end
-- chains a bunch of filters together -- chains a bunch of filters together
-- (thanks to Wim Couwenberg) -- (thanks to Wim Couwenberg)
function filter.chain(...) function filter.chain(...)
local n = #arg local n = #arg
local top, index = 1, 1 local top, index = 1, 1
local retry = "" local retry = ""
return function(chunk) return function(chunk)
retry = chunk and retry retry = chunk and retry
while true do while true do
if index == top then if index == top then
chunk = arg[index](chunk) chunk = arg[index](chunk)
if chunk == "" or top == n then return chunk if chunk == "" or top == n then
elseif chunk then index = index + 1 return chunk
else elseif chunk then
top = top+1 index = index + 1
index = top else
end top = top + 1
else index = top
chunk = arg[index](chunk or "")
if chunk == "" then
index = index - 1
chunk = retry
elseif chunk then
if index == n then return chunk
else index = index + 1 end
else base.error("filter returned inappropriate nil") end
end
end end
else
chunk = arg[index](chunk or "")
if chunk == "" then
index = index - 1
chunk = retry
elseif chunk then
if index == n then
return chunk
else
index = index + 1
end
else
base.error("filter returned inappropriate nil")
end
end
end end
end
end end
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
@@ -71,130 +78,143 @@ end
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
-- create an empty source -- create an empty source
local function empty() local function empty()
return nil return nil
end end
function source.empty() function source.empty()
return empty return empty
end end
-- returns a source that just outputs an error -- returns a source that just outputs an error
function source.error(err) function source.error(err)
return function() return function()
return nil, err return nil, err
end end
end end
-- creates a file source -- creates a file source
function source.file(handle, io_err) function source.file(handle, io_err)
if handle then if handle then
return function() return function()
local chunk = handle:read(BLOCKSIZE) local chunk = handle:read(BLOCKSIZE)
if not chunk then handle:close() end if not chunk then handle:close() end
return chunk return chunk
end end
else return source.error(io_err or "unable to open file") end else
return source.error(io_err or "unable to open file")
end
end end
-- turns a fancy source into a simple source -- turns a fancy source into a simple source
function source.simplify(src) function source.simplify(src)
base.assert(src) base.assert(src)
return function() return function()
local chunk, err_or_new = src() local chunk, err_or_new = src()
src = err_or_new or src src = err_or_new or src
if not chunk then return nil, err_or_new if not chunk then
else return chunk end return nil, err_or_new
else
return chunk
end end
end
end end
-- creates string source -- creates string source
function source.string(s) function source.string(s)
if s then if s then
local i = 1 local i = 1
return function() return function()
local chunk = string.sub(s, i, i+BLOCKSIZE-1) local chunk = string.sub(s, i, i + BLOCKSIZE - 1)
i = i + BLOCKSIZE i = i + BLOCKSIZE
if chunk ~= "" then return chunk if chunk ~= "" then
else return nil end return chunk
end else
else return source.empty() end return nil
end
end
else
return source.empty()
end
end end
-- creates rewindable source -- creates rewindable source
function source.rewind(src) function source.rewind(src)
base.assert(src) base.assert(src)
local t = {} local t = {}
return function(chunk) return function(chunk)
if not chunk then if not chunk then
chunk = table.remove(t) chunk = table.remove(t)
if not chunk then return src() if not chunk then
else return chunk end return src()
else else
table.insert(t, chunk) return chunk
end end
else
table.insert(t, chunk)
end end
end
end end
function source.chain(src, f) function source.chain(src, f)
base.assert(src and f) base.assert(src and f)
local last_in, last_out = "", "" local last_in, last_out = "", ""
local state = "feeding" local state = "feeding"
local err local err
return function() return function()
if not last_out then if not last_out then
base.error('source is empty!', 2) base.error('source is empty!', 2)
end
while true do
if state == "feeding" then
last_in, err = src()
if err then return nil, err end
last_out = f(last_in)
if not last_out then
if last_in then
base.error('filter returned inappropriate nil')
else
return nil
end
elseif last_out ~= "" then
state = "eating"
if last_in then last_in = "" end
return last_out
end
else
last_out = f(last_in)
if last_out == "" then
if last_in == "" then
state = "feeding"
else
base.error('filter returned ""')
end
elseif not last_out then
if last_in then
base.error('filter returned inappropriate nil')
else
return nil
end
else
return last_out
end
end
end
end end
while true do
if state == "feeding" then
last_in, err = src()
if err then return nil, err end
last_out = f(last_in)
if not last_out then
if last_in then
base.error('filter returned inappropriate nil')
else
return nil
end
elseif last_out ~= "" then
state = "eating"
if last_in then last_in = "" end
return last_out
end
else
last_out = f(last_in)
if last_out == "" then
if last_in == "" then
state = "feeding"
else
base.error('filter returned ""')
end
elseif not last_out then
if last_in then
base.error('filter returned inappropriate nil')
else
return nil
end
else
return last_out
end
end
end
end
end end
-- creates a source that produces contents of several sources, one after the -- creates a source that produces contents of several sources, one after the
-- other, as if they were concatenated -- other, as if they were concatenated
-- (thanks to Wim Couwenberg) -- (thanks to Wim Couwenberg)
function source.cat(...) function source.cat(...)
local src = table.remove(arg, 1) local src = table.remove(arg, 1)
return function() return function()
while src do while src do
local chunk, err = src() local chunk, err = src()
if chunk then return chunk end if chunk then return chunk end
if err then return nil, err end if err then return nil, err end
src = table.remove(arg, 1) src = table.remove(arg, 1)
end
end end
end
end end
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
@@ -202,68 +222,74 @@ end
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
-- creates a sink that stores into a table -- creates a sink that stores into a table
function sink.table(t) function sink.table(t)
t = t or {} t = t or {}
local f = function(chunk, err) local f = function(chunk, err)
if chunk then table.insert(t, chunk) end if chunk then table.insert(t, chunk) end
return 1 return 1
end end
return f, t return f, t
end end
-- turns a fancy sink into a simple sink -- turns a fancy sink into a simple sink
function sink.simplify(snk) function sink.simplify(snk)
base.assert(snk) base.assert(snk)
return function(chunk, err) return function(chunk, err)
local ret, err_or_new = snk(chunk, err) local ret, err_or_new = snk(chunk, err)
if not ret then return nil, err_or_new end if not ret then return nil, err_or_new end
snk = err_or_new or snk snk = err_or_new or snk
return 1 return 1
end end
end end
-- creates a file sink -- creates a file sink
function sink.file(handle, io_err) function sink.file(handle, io_err)
if handle then if handle then
return function(chunk, err) return function(chunk, err)
if not chunk then if not chunk then
handle:close() handle:close()
return 1 return 1
else return handle:write(chunk) end else
end return handle:write(chunk)
else return sink.error(io_err or "unable to open file") end end
end
else
return sink.error(io_err or "unable to open file")
end
end end
-- creates a sink that discards data -- creates a sink that discards data
local function null() local function null()
return 1 return 1
end end
function sink.null() function sink.null()
return null return null
end end
-- creates a sink that just returns an error -- creates a sink that just returns an error
function sink.error(err) function sink.error(err)
return function() return function()
return nil, err return nil, err
end end
end end
-- chains a sink with a filter -- chains a sink with a filter
function sink.chain(f, snk) function sink.chain(f, snk)
base.assert(f and snk) base.assert(f and snk)
return function(chunk, err) return function(chunk, err)
if chunk ~= "" then if chunk ~= "" then
local filtered = f(chunk) local filtered = f(chunk)
local done = chunk and "" local done = chunk and ""
while true do while true do
local ret, snkerr = snk(filtered, err) local ret, snkerr = snk(filtered, err)
if not ret then return nil, snkerr end if not ret then return nil, snkerr end
if filtered == done then return 1 end if filtered == done then return 1 end
filtered = f(done) filtered = f(done)
end end
else return 1 end else
return 1
end end
end
end end
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
@@ -271,22 +297,27 @@ end
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
-- pumps one chunk from the source to the sink -- pumps one chunk from the source to the sink
function pump.step(src, snk) function pump.step(src, snk)
local chunk, src_err = src() local chunk, src_err = src()
local ret, snk_err = snk(chunk, src_err) local ret, snk_err = snk(chunk, src_err)
if chunk and ret then return 1 if chunk and ret then
else return nil, src_err or snk_err end return 1
else
return nil, src_err or snk_err
end
end end
-- pumps all data from a source to a sink, using a step function -- pumps all data from a source to a sink, using a step function
function pump.all(src, snk, step) function pump.all(src, snk, step)
base.assert(src and snk) base.assert(src and snk)
step = step or pump.step step = step or pump.step
while true do while true do
local ret, err = step(src, snk) local ret, err = step(src, snk)
if not ret then if not ret then
if err then return nil, err if err then
else return 1 end return nil, err
end else
return 1
end
end end
end
end end

View File

@@ -22,53 +22,60 @@ wrapt = {}
-- creates a function that chooses a filter by name from a given table -- creates a function that chooses a filter by name from a given table
local function choose(table) local function choose(table)
return function(name, opt1, opt2) return function(name, opt1, opt2)
if base.type(name) ~= "string" then if base.type(name) ~= "string" then
name, opt1, opt2 = "default", name, opt1 name, opt1, opt2 = "default", name, opt1
end
local f = table[name or "nil"]
if not f then
base.error("unknown key (" .. base.tostring(name) .. ")", 3)
else return f(opt1, opt2) end
end end
local f = table[name or "nil"]
if not f then
base.error("unknown key (" .. base.tostring(name) .. ")", 3)
else
return f(opt1, opt2)
end
end
end end
-- define the encoding filters -- define the encoding filters
encodet['base64'] = function() encodet['base64'] = function()
return ltn12.filter.cycle(b64, "") return ltn12.filter.cycle(b64, "")
end end
encodet['quoted-printable'] = function(mode) encodet['quoted-printable'] = function(mode)
return ltn12.filter.cycle(qp, "", return ltn12.filter.cycle(qp, "",
(mode == "binary") and "=0D=0A" or "\r\n") (mode == "binary") and "=0D=0A" or "\r\n")
end end
-- define the decoding filters -- define the decoding filters
decodet['base64'] = function() decodet['base64'] = function()
return ltn12.filter.cycle(unb64, "") return ltn12.filter.cycle(unb64, "")
end end
decodet['quoted-printable'] = function() decodet['quoted-printable'] = function()
return ltn12.filter.cycle(unqp, "") return ltn12.filter.cycle(unqp, "")
end end
local function format(chunk) local function format(chunk)
if chunk then if chunk then
if chunk == "" then return "''" if chunk == "" then
else return string.len(chunk) end return "''"
else return "nil" end else
return string.len(chunk)
end
else
return "nil"
end
end end
-- define the line-wrap filters -- define the line-wrap filters
wrapt['text'] = function(length) wrapt['text'] = function(length)
length = length or 76 length = length or 76
return ltn12.filter.cycle(wrp, length, length) return ltn12.filter.cycle(wrp, length, length)
end end
wrapt['base64'] = wrapt['text'] wrapt['base64'] = wrapt['text']
wrapt['default'] = wrapt['text'] wrapt['default'] = wrapt['text']
wrapt['quoted-printable'] = function() wrapt['quoted-printable'] = function()
return ltn12.filter.cycle(qpwrp, 76, 76) return ltn12.filter.cycle(qpwrp, 76, 76)
end end
-- function that choose the encoding, decoding or wrap algorithm -- function that choose the encoding, decoding or wrap algorithm
@@ -78,10 +85,10 @@ wrap = choose(wrapt)
-- define the end-of-line normalization filter -- define the end-of-line normalization filter
function normalize(marker) function normalize(marker)
return ltn12.filter.cycle(eol, 0, marker) return ltn12.filter.cycle(eol, 0, marker)
end end
-- high level stuffing filter -- high level stuffing filter
function stuff() function stuff()
return ltn12.filter.cycle(dot, 2) return ltn12.filter.cycle(dot, 2)
end end

View File

@@ -17,39 +17,42 @@ module("socket")
-- Exported auxiliar functions -- Exported auxiliar functions
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
function connect(address, port, laddress, lport) function connect(address, port, laddress, lport)
local sock, err = socket.tcp() local sock, err = socket.tcp()
if not sock then return nil, err end if not sock then return nil, err end
if laddress then if laddress then
local res, err = sock:bind(laddress, lport, -1) local res, err = sock:bind(laddress, lport, -1)
if not res then return nil, err end
end
local res, err = sock:connect(address, port)
if not res then return nil, err end if not res then return nil, err end
return sock end
local res, err = sock:connect(address, port)
if not res then return nil, err end
return sock
end end
function bind(host, port, backlog) function bind(host, port, backlog)
local sock, err = socket.tcp() local sock, err = socket.tcp()
if not sock then return nil, err end if not sock then return nil, err end
sock:setoption("reuseaddr", true) sock:setoption("reuseaddr", true)
local res, err = sock:bind(host, port) local res, err = sock:bind(host, port)
if not res then return nil, err end if not res then return nil, err end
res, err = sock:listen(backlog) res, err = sock:listen(backlog)
if not res then return nil, err end if not res then return nil, err end
return sock return sock
end end
try = newtry() try = newtry()
function choose(table) function choose(table)
return function(name, opt1, opt2) return function(name, opt1, opt2)
if base.type(name) ~= "string" then if base.type(name) ~= "string" then
name, opt1, opt2 = "default", name, opt1 name, opt1, opt2 = "default", name, opt1
end
local f = table[name or "nil"]
if not f then base.error("unknown key (".. base.tostring(name) ..")", 3)
else return f(opt1, opt2) end
end end
local f = table[name or "nil"]
if not f then
base.error("unknown key (" .. base.tostring(name) .. ")", 3)
else
return f(opt1, opt2)
end
end
end end
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
@@ -62,29 +65,34 @@ sinkt = {}
BLOCKSIZE = 2048 BLOCKSIZE = 2048
sinkt["close-when-done"] = function(sock) sinkt["close-when-done"] = function(sock)
return base.setmetatable({ return base.setmetatable({
getfd = function() return sock:getfd() end, getfd = function() return sock:getfd() end,
dirty = function() return sock:dirty() end dirty = function() return sock:dirty() end
}, { }, {
__call = function(self, chunk, err) __call = function(self, chunk, err)
if not chunk then if not chunk then
sock:close() sock:close()
return 1 return 1
else return sock:send(chunk) end else
end return sock:send(chunk)
}) end
end
})
end end
sinkt["keep-open"] = function(sock) sinkt["keep-open"] = function(sock)
return base.setmetatable({ return base.setmetatable({
getfd = function() return sock:getfd() end, getfd = function() return sock:getfd() end,
dirty = function() return sock:dirty() end dirty = function() return sock:dirty() end
}, { }, {
__call = function(self, chunk, err) __call = function(self, chunk, err)
if chunk then return sock:send(chunk) if chunk then
else return 1 end return sock:send(chunk)
end else
}) return 1
end
end
})
end end
sinkt["default"] = sinkt["keep-open"] sinkt["default"] = sinkt["keep-open"]
@@ -92,42 +100,44 @@ sinkt["default"] = sinkt["keep-open"]
sink = choose(sinkt) sink = choose(sinkt)
sourcet["by-length"] = function(sock, length) sourcet["by-length"] = function(sock, length)
return base.setmetatable({ return base.setmetatable({
getfd = function() return sock:getfd() end, getfd = function() return sock:getfd() end,
dirty = function() return sock:dirty() end dirty = function() return sock:dirty() end
}, { }, {
__call = function() __call = function()
if length <= 0 then return nil end if length <= 0 then return nil end
local size = math.min(socket.BLOCKSIZE, length) local size = math.min(socket.BLOCKSIZE, length)
local chunk, err = sock:receive(size) local chunk, err = sock:receive(size)
if err then return nil, err end if err then return nil, err end
length = length - string.len(chunk) length = length - string.len(chunk)
return chunk return chunk
end end
}) })
end end
sourcet["until-closed"] = function(sock) sourcet["until-closed"] = function(sock)
local done local done
return base.setmetatable({ return base.setmetatable({
getfd = function() return sock:getfd() end, getfd = function() return sock:getfd() end,
dirty = function() return sock:dirty() end dirty = function() return sock:dirty() end
}, { }, {
__call = function() __call = function()
if done then return nil end if done then return nil end
local chunk, err, partial = sock:receive(socket.BLOCKSIZE) local chunk, err, partial = sock:receive(socket.BLOCKSIZE)
if not err then return chunk if not err then
elseif err == "closed" then return chunk
sock:close() elseif err == "closed" then
done = 1 sock:close()
return partial done = 1
else return nil, err end return partial
end else
}) return nil, err
end
end
})
end end
sourcet["default"] = sourcet["until-closed"] sourcet["default"] = sourcet["until-closed"]
source = choose(sourcet) source = choose(sourcet)

View File

@@ -36,246 +36,253 @@ PASSWORD = "anonymous@anonymous.org"
local metat = { __index = {} } local metat = { __index = {} }
function open(server, port, create) function open(server, port, create)
local tp = socket.try(tp.connect(server, port or PORT, TIMEOUT, create)) local tp = socket.try(tp.connect(server, port or PORT, TIMEOUT, create))
local f = base.setmetatable({ tp = tp }, metat) local f = base.setmetatable({ tp = tp }, metat)
-- make sure everything gets closed in an exception -- make sure everything gets closed in an exception
f.try = socket.newtry(function() f:close() end) f.try = socket.newtry(function() f:close() end)
return f return f
end end
function metat.__index:portconnect() function metat.__index:portconnect()
self.try(self.server:settimeout(TIMEOUT)) self.try(self.server:settimeout(TIMEOUT))
self.data = self.try(self.server:accept()) self.data = self.try(self.server:accept())
self.try(self.data:settimeout(TIMEOUT)) self.try(self.data:settimeout(TIMEOUT))
end end
function metat.__index:pasvconnect() function metat.__index:pasvconnect()
self.data = self.try(socket.tcp()) self.data = self.try(socket.tcp())
self.try(self.data:settimeout(TIMEOUT)) self.try(self.data:settimeout(TIMEOUT))
self.try(self.data:connect(self.pasvt.ip, self.pasvt.port)) self.try(self.data:connect(self.pasvt.ip, self.pasvt.port))
end end
function metat.__index:login(user, password) function metat.__index:login(user, password)
self.try(self.tp:command("user", user or USER)) self.try(self.tp:command("user", user or USER))
local code, reply = self.try(self.tp:check{"2..", 331}) local code, reply = self.try(self.tp:check { "2..", 331 })
if code == 331 then if code == 331 then
self.try(self.tp:command("pass", password or PASSWORD)) self.try(self.tp:command("pass", password or PASSWORD))
self.try(self.tp:check("2..")) self.try(self.tp:check("2.."))
end end
return 1 return 1
end end
function metat.__index:pasv() function metat.__index:pasv()
self.try(self.tp:command("pasv")) self.try(self.tp:command("pasv"))
local code, reply = self.try(self.tp:check("2..")) local code, reply = self.try(self.tp:check("2.."))
local pattern = "(%d+)%D(%d+)%D(%d+)%D(%d+)%D(%d+)%D(%d+)" 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)) 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.try(a and b and c and d and p1 and p2, reply)
self.pasvt = { self.pasvt = {
ip = string.format("%d.%d.%d.%d", a, b, c, d), ip = string.format("%d.%d.%d.%d", a, b, c, d),
port = p1*256 + p2 port = p1 * 256 + p2
} }
if self.server then if self.server then
self.server:close() self.server:close()
self.server = nil self.server = nil
end end
return self.pasvt.ip, self.pasvt.port return self.pasvt.ip, self.pasvt.port
end end
function metat.__index:port(ip, port) function metat.__index:port(ip, port)
self.pasvt = nil self.pasvt = nil
if not ip then if not ip then
ip, port = self.try(self.tp:getcontrol():getsockname()) ip, port = self.try(self.tp:getcontrol():getsockname())
self.server = self.try(socket.bind(ip, 0)) self.server = self.try(socket.bind(ip, 0))
ip, port = self.try(self.server:getsockname()) ip, port = self.try(self.server:getsockname())
self.try(self.server:settimeout(TIMEOUT)) self.try(self.server:settimeout(TIMEOUT))
end end
local pl = math.mod(port, 256) local pl = math.mod(port, 256)
local ph = (port - pl)/256 local ph = (port - pl) / 256
local arg = string.gsub(string.format("%s,%d,%d", ip, ph, pl), "%.", ",") local arg = string.gsub(string.format("%s,%d,%d", ip, ph, pl), "%.", ",")
self.try(self.tp:command("port", arg)) self.try(self.tp:command("port", arg))
self.try(self.tp:check("2..")) self.try(self.tp:check("2.."))
return 1 return 1
end end
function metat.__index:send(sendt) function metat.__index:send(sendt)
self.try(self.pasvt or self.server, "need port or pasv first") self.try(self.pasvt or self.server, "need port or pasv first")
-- if there is a pasvt table, we already sent a PASV command -- if there is a pasvt table, we already sent a PASV command
-- we just get the data connection into self.data -- we just get the data connection into self.data
if self.pasvt then self:pasvconnect() end if self.pasvt then self:pasvconnect() end
-- get the transfer argument and command -- get the transfer argument and command
local argument = sendt.argument or local argument = sendt.argument or
url.unescape(string.gsub(sendt.path or "", "^[/\\]", "")) url.unescape(string.gsub(sendt.path or "", "^[/\\]", ""))
if argument == "" then argument = nil end if argument == "" then argument = nil end
local command = sendt.command or "stor" local command = sendt.command or "stor"
-- send the transfer command and check the reply -- send the transfer command and check the reply
self.try(self.tp:command(command, argument)) self.try(self.tp:command(command, argument))
local code, reply = self.try(self.tp:check{"2..", "1.."}) local code, reply = self.try(self.tp:check { "2..", "1.." })
-- if there is not a a pasvt table, then there is a server -- if there is not a a pasvt table, then there is a server
-- and we already sent a PORT command -- and we already sent a PORT command
if not self.pasvt then self:portconnect() end if not self.pasvt then self:portconnect() end
-- get the sink, source and step for the transfer -- get the sink, source and step for the transfer
local step = sendt.step or ltn12.pump.step local step = sendt.step or ltn12.pump.step
local readt = {self.tp.c} local readt = { self.tp.c }
local checkstep = function(src, snk) local checkstep = function(src, snk)
-- check status in control connection while downloading -- check status in control connection while downloading
local readyt = socket.select(readt, nil, 0) local readyt = socket.select(readt, nil, 0)
if readyt[tp] then code = self.try(self.tp:check("2..")) end if readyt[tp] then code = self.try(self.tp:check("2..")) end
return step(src, snk) return step(src, snk)
end end
local sink = socket.sink("close-when-done", self.data) local sink = socket.sink("close-when-done", self.data)
-- transfer all data and check error -- transfer all data and check error
self.try(ltn12.pump.all(sendt.source, sink, checkstep)) self.try(ltn12.pump.all(sendt.source, sink, checkstep))
if string.find(code, "1..") then self.try(self.tp:check("2..")) end if string.find(code, "1..") then self.try(self.tp:check("2..")) end
-- done with data connection -- done with data connection
self.data:close() self.data:close()
-- find out how many bytes were sent -- find out how many bytes were sent
local sent = socket.skip(1, self.data:getstats()) local sent = socket.skip(1, self.data:getstats())
self.data = nil self.data = nil
return sent return sent
end end
function metat.__index:receive(recvt) function metat.__index:receive(recvt)
self.try(self.pasvt or self.server, "need port or pasv first") self.try(self.pasvt or self.server, "need port or pasv first")
if self.pasvt then self:pasvconnect() end if self.pasvt then self:pasvconnect() end
local argument = recvt.argument or local argument = recvt.argument or
url.unescape(string.gsub(recvt.path or "", "^[/\\]", "")) url.unescape(string.gsub(recvt.path or "", "^[/\\]", ""))
if argument == "" then argument = nil end if argument == "" then argument = nil end
local command = recvt.command or "retr" local command = recvt.command or "retr"
self.try(self.tp:command(command, argument)) self.try(self.tp:command(command, argument))
local code = self.try(self.tp:check{"1..", "2.."}) local code = self.try(self.tp:check { "1..", "2.." })
if not self.pasvt then self:portconnect() end if not self.pasvt then self:portconnect() end
local source = socket.source("until-closed", self.data) local source = socket.source("until-closed", self.data)
local step = recvt.step or ltn12.pump.step local step = recvt.step or ltn12.pump.step
self.try(ltn12.pump.all(source, recvt.sink, step)) self.try(ltn12.pump.all(source, recvt.sink, step))
if string.find(code, "1..") then self.try(self.tp:check("2..")) end if string.find(code, "1..") then self.try(self.tp:check("2..")) end
self.data:close() self.data:close()
self.data = nil self.data = nil
return 1 return 1
end end
function metat.__index:cwd(dir) function metat.__index:cwd(dir)
self.try(self.tp:command("cwd", dir)) self.try(self.tp:command("cwd", dir))
self.try(self.tp:check(250)) self.try(self.tp:check(250))
return 1 return 1
end end
function metat.__index:type(type) function metat.__index:type(type)
self.try(self.tp:command("type", type)) self.try(self.tp:command("type", type))
self.try(self.tp:check(200)) self.try(self.tp:check(200))
return 1 return 1
end end
function metat.__index:greet() function metat.__index:greet()
local code = self.try(self.tp:check{"1..", "2.."}) local code = self.try(self.tp:check { "1..", "2.." })
if string.find(code, "1..") then self.try(self.tp:check("2..")) end if string.find(code, "1..") then self.try(self.tp:check("2..")) end
return 1 return 1
end end
function metat.__index:quit() function metat.__index:quit()
self.try(self.tp:command("quit")) self.try(self.tp:command("quit"))
self.try(self.tp:check("2..")) self.try(self.tp:check("2.."))
return 1 return 1
end end
function metat.__index:close() function metat.__index:close()
if self.data then self.data:close() end if self.data then self.data:close() end
if self.server then self.server:close() end if self.server then self.server:close() end
return self.tp:close() return self.tp:close()
end end
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
-- High level FTP API -- High level FTP API
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
local function override(t) local function override(t)
if t.url then if t.url then
local u = url.parse(t.url) local u = url.parse(t.url)
for i,v in base.pairs(t) do for i, v in base.pairs(t) do
u[i] = v u[i] = v
end end
return u return u
else return t end else
return t
end
end end
local function tput(putt) local function tput(putt)
putt = override(putt) putt = override(putt)
socket.try(putt.host, "missing hostname") socket.try(putt.host, "missing hostname")
local f = open(putt.host, putt.port, putt.create) local f = open(putt.host, putt.port, putt.create)
f:greet() f:greet()
f:login(putt.user, putt.password) f:login(putt.user, putt.password)
if putt.type then f:type(putt.type) end if putt.type then f:type(putt.type) end
f:pasv() f:pasv()
local sent = f:send(putt) local sent = f:send(putt)
f:quit() f:quit()
f:close() f:close()
return sent return sent
end end
local default = { local default = {
path = "/", path = "/",
scheme = "ftp" scheme = "ftp"
} }
local function parse(u) local function parse(u)
local t = socket.try(url.parse(u, default)) local t = socket.try(url.parse(u, default))
socket.try(t.scheme == "ftp", "wrong scheme '" .. t.scheme .. "'") socket.try(t.scheme == "ftp", "wrong scheme '" .. t.scheme .. "'")
socket.try(t.host, "missing hostname") socket.try(t.host, "missing hostname")
local pat = "^type=(.)$" local pat = "^type=(.)$"
if t.params then if t.params then
t.type = socket.skip(2, string.find(t.params, pat)) t.type = socket.skip(2, string.find(t.params, pat))
socket.try(t.type == "a" or t.type == "i", socket.try(t.type == "a" or t.type == "i",
"invalid type '" .. t.type .. "'") "invalid type '" .. t.type .. "'")
end end
return t return t
end end
local function sput(u, body) local function sput(u, body)
local putt = parse(u) local putt = parse(u)
putt.source = ltn12.source.string(body) putt.source = ltn12.source.string(body)
return tput(putt) return tput(putt)
end end
put = socket.protect(function(putt, body) put = socket.protect(function(putt, body)
if base.type(putt) == "string" then return sput(putt, body) if base.type(putt) == "string" then
else return tput(putt) end return sput(putt, body)
else
return tput(putt)
end
end) end)
local function tget(gett) local function tget(gett)
gett = override(gett) gett = override(gett)
socket.try(gett.host, "missing hostname") socket.try(gett.host, "missing hostname")
local f = open(gett.host, gett.port, gett.create) local f = open(gett.host, gett.port, gett.create)
f:greet() f:greet()
f:login(gett.user, gett.password) f:login(gett.user, gett.password)
if gett.type then f:type(gett.type) end if gett.type then f:type(gett.type) end
f:pasv() f:pasv()
f:receive(gett) f:receive(gett)
f:quit() f:quit()
return f:close() return f:close()
end end
local function sget(u) local function sget(u)
local gett = parse(u) local gett = parse(u)
local t = {} local t = {}
gett.sink = ltn12.sink.table(t) gett.sink = ltn12.sink.table(t)
tget(gett) tget(gett)
return table.concat(t) return table.concat(t)
end end
command = socket.protect(function(cmdt) command = socket.protect(function(cmdt)
cmdt = override(cmdt) cmdt = override(cmdt)
socket.try(cmdt.host, "missing hostname") socket.try(cmdt.host, "missing hostname")
socket.try(cmdt.command, "missing command") socket.try(cmdt.command, "missing command")
local f = open(cmdt.host, cmdt.port, cmdt.create) local f = open(cmdt.host, cmdt.port, cmdt.create)
f:greet() f:greet()
f:login(cmdt.user, cmdt.password) f:login(cmdt.user, cmdt.password)
f.try(f.tp:command(cmdt.command, cmdt.argument)) f.try(f.tp:command(cmdt.command, cmdt.argument))
if cmdt.check then f.try(f.tp:check(cmdt.check)) end if cmdt.check then f.try(f.tp:check(cmdt.check)) end
f:quit() f:quit()
return f:close() return f:close()
end) end)
get = socket.protect(function(gett) get = socket.protect(function(gett)
if base.type(gett) == "string" then return sget(gett) if base.type(gett) == "string" then
else return tget(gett) end return sget(gett)
else
return tget(gett)
end
end) end)

View File

@@ -31,73 +31,76 @@ USERAGENT = socket._VERSION
-- Reads MIME headers from a connection, unfolding where needed -- Reads MIME headers from a connection, unfolding where needed
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
local function receiveheaders(sock, headers) local function receiveheaders(sock, headers)
local line, name, value, err local line, name, value, err
headers = headers or {} headers = headers or {}
-- get first line -- 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() line, err = sock:receive()
if err then return nil, err end if err then return nil, err end
-- headers go until a blank line is found -- unfold any folded values
while line ~= "" do while string.find(line, "^%s") do
-- get field-name and value value = value .. line
name, value = socket.skip(2, string.find(line, "^(.-):%s*(.*)")) line = sock:receive()
if not (name and value) then return nil, "malformed reponse headers" end if err then return nil, err 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 end
return headers -- save pair in table
if headers[name] then
headers[name] = headers[name] .. ", " .. value
else
headers[name] = value
end
end
return headers
end end
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
-- Extra sources and sinks -- Extra sources and sinks
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
socket.sourcet["http-chunked"] = function(sock, headers) socket.sourcet["http-chunked"] = function(sock, headers)
return base.setmetatable({ return base.setmetatable({
getfd = function() return sock:getfd() end, getfd = function() return sock:getfd() end,
dirty = function() return sock:dirty() end dirty = function() return sock:dirty() end
}, { }, {
__call = function() __call = function()
-- get chunk size, skip extention -- get chunk size, skip extention
local line, err = sock:receive() local line, err = sock:receive()
if err then return nil, err end if err then return nil, err end
local size = base.tonumber(string.gsub(line, ";.*", ""), 16) local size = base.tonumber(string.gsub(line, ";.*", ""), 16)
if not size then return nil, "invalid chunk size" end if not size then return nil, "invalid chunk size" end
-- was it the last chunk? -- was it the last chunk?
if size > 0 then if size > 0 then
-- if not, get chunk and skip terminating CRLF -- if not, get chunk and skip terminating CRLF
local chunk, err, part = sock:receive(size) local chunk, err, part = sock:receive(size)
if chunk then sock:receive() end if chunk then sock:receive() end
return chunk, err return chunk, err
else else
-- if it was, read trailers into headers table -- if it was, read trailers into headers table
headers, err = receiveheaders(sock, headers) headers, err = receiveheaders(sock, headers)
if not headers then return nil, err end if not headers then return nil, err end
end end
end end
}) })
end end
socket.sinkt["http-chunked"] = function(sock) socket.sinkt["http-chunked"] = function(sock)
return base.setmetatable({ return base.setmetatable({
getfd = function() return sock:getfd() end, getfd = function() return sock:getfd() end,
dirty = function() return sock:dirty() end dirty = function() return sock:dirty() end
}, { }, {
__call = function(self, chunk, err) __call = function(self, chunk, err)
if not chunk then return sock:send("0\r\n\r\n") end if not chunk then return sock:send("0\r\n\r\n") end
local size = string.format("%X\r\n", string.len(chunk)) local size = string.format("%X\r\n", string.len(chunk))
return sock:send(size .. chunk .. "\r\n") return sock:send(size .. chunk .. "\r\n")
end end
}) })
end end
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
@@ -106,245 +109,251 @@ end
local metat = { __index = {} } local metat = { __index = {} }
function open(host, port, create) function open(host, port, create)
-- create socket with user connect function, or with default -- create socket with user connect function, or with default
local c = socket.try((create or socket.tcp)()) local c = socket.try((create or socket.tcp)())
local h = base.setmetatable({ c = c }, metat) local h = base.setmetatable({ c = c }, metat)
-- create finalized try -- create finalized try
h.try = socket.newtry(function() h:close() end) h.try = socket.newtry(function() h:close() end)
-- set timeout before connecting -- set timeout before connecting
h.try(c:settimeout(TIMEOUT)) h.try(c:settimeout(TIMEOUT))
h.try(c:connect(host, port or PORT)) h.try(c:connect(host, port or PORT))
-- here everything worked -- here everything worked
return h return h
end end
function metat.__index:sendrequestline(method, uri) function metat.__index:sendrequestline(method, uri)
local reqline = string.format("%s %s HTTP/1.1\r\n", method or "GET", uri) local reqline = string.format("%s %s HTTP/1.1\r\n", method or "GET", uri)
return self.try(self.c:send(reqline)) return self.try(self.c:send(reqline))
end end
function metat.__index:sendheaders(headers) function metat.__index:sendheaders(headers)
local h = "\r\n" local h = "\r\n"
for i, v in base.pairs(headers) do for i, v in base.pairs(headers) do
h = i .. ": " .. v .. "\r\n" .. h h = i .. ": " .. v .. "\r\n" .. h
end end
self.try(self.c:send(h)) self.try(self.c:send(h))
return 1 return 1
end end
function metat.__index:sendbody(headers, source, step) function metat.__index:sendbody(headers, source, step)
source = source or ltn12.source.empty() source = source or ltn12.source.empty()
step = step or ltn12.pump.step step = step or ltn12.pump.step
-- if we don't know the size in advance, send chunked and hope for the best -- if we don't know the size in advance, send chunked and hope for the best
local mode = "http-chunked" local mode = "http-chunked"
if headers["content-length"] then mode = "keep-open" end if headers["content-length"] then mode = "keep-open" end
return self.try(ltn12.pump.all(source, socket.sink(mode, self.c), step)) return self.try(ltn12.pump.all(source, socket.sink(mode, self.c), step))
end end
function metat.__index:receivestatusline() function metat.__index:receivestatusline()
local status = self.try(self.c:receive(5)) local status = self.try(self.c:receive(5))
-- identify HTTP/0.9 responses, which do not contain a status line -- identify HTTP/0.9 responses, which do not contain a status line
-- this is just a heuristic, but is what the RFC recommends -- this is just a heuristic, but is what the RFC recommends
if status ~= "HTTP/" then return nil, status end if status ~= "HTTP/" then return nil, status end
-- otherwise proceed reading a status line -- otherwise proceed reading a status line
status = self.try(self.c:receive("*l", status)) status = self.try(self.c:receive("*l", status))
local code = socket.skip(2, string.find(status, "HTTP/%d*%.%d* (%d%d%d)")) local code = socket.skip(2, string.find(status, "HTTP/%d*%.%d* (%d%d%d)"))
return self.try(base.tonumber(code), status) return self.try(base.tonumber(code), status)
end end
function metat.__index:receiveheaders() function metat.__index:receiveheaders()
return self.try(receiveheaders(self.c)) return self.try(receiveheaders(self.c))
end end
function metat.__index:receivebody(headers, sink, step) function metat.__index:receivebody(headers, sink, step)
sink = sink or ltn12.sink.null() sink = sink or ltn12.sink.null()
step = step or ltn12.pump.step step = step or ltn12.pump.step
local length = base.tonumber(headers["content-length"]) local length = base.tonumber(headers["content-length"])
local t = headers["transfer-encoding"] -- shortcut local t = headers["transfer-encoding"] -- shortcut
local mode = "default" -- connection close local mode = "default" -- connection close
if t and t ~= "identity" then mode = "http-chunked" if t and t ~= "identity" then
elseif base.tonumber(headers["content-length"]) then mode = "by-length" end mode = "http-chunked"
return self.try(ltn12.pump.all(socket.source(mode, self.c, length), elseif base.tonumber(headers["content-length"]) then
sink, step)) mode = "by-length"
end
return self.try(ltn12.pump.all(socket.source(mode, self.c, length),
sink, step))
end end
function metat.__index:receive09body(status, sink, step) function metat.__index:receive09body(status, sink, step)
local source = ltn12.source.rewind(socket.source("until-closed", self.c)) local source = ltn12.source.rewind(socket.source("until-closed", self.c))
source(status) source(status)
return self.try(ltn12.pump.all(source, sink, step)) return self.try(ltn12.pump.all(source, sink, step))
end end
function metat.__index:close() function metat.__index:close()
return self.c:close() return self.c:close()
end end
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
-- High level HTTP API -- High level HTTP API
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
local function adjusturi(reqt) local function adjusturi(reqt)
local u = reqt local u = reqt
-- if there is a proxy, we need the full url. otherwise, just a part. -- if there is a proxy, we need the full url. otherwise, just a part.
if not reqt.proxy and not PROXY then if not reqt.proxy and not PROXY then
u = { u = {
path = socket.try(reqt.path, "invalid path 'nil'"), path = socket.try(reqt.path, "invalid path 'nil'"),
params = reqt.params, params = reqt.params,
query = reqt.query, query = reqt.query,
fragment = reqt.fragment fragment = reqt.fragment
} }
end end
return url.build(u) return url.build(u)
end end
local function adjustproxy(reqt) local function adjustproxy(reqt)
local proxy = reqt.proxy or PROXY local proxy = reqt.proxy or PROXY
if proxy then if proxy then
proxy = url.parse(proxy) proxy = url.parse(proxy)
return proxy.host, proxy.port or 3128 return proxy.host, proxy.port or 3128
else else
return reqt.host, reqt.port return reqt.host, reqt.port
end end
end end
local function adjustheaders(reqt) local function adjustheaders(reqt)
-- default headers -- default headers
local lower = { local lower = {
["user-agent"] = USERAGENT, ["user-agent"] = USERAGENT,
["host"] = reqt.host, ["host"] = reqt.host,
["connection"] = "close, TE", ["connection"] = "close, TE",
["te"] = "trailers" ["te"] = "trailers"
} }
-- if we have authentication information, pass it along -- if we have authentication information, pass it along
if reqt.user and reqt.password then if reqt.user and reqt.password then
lower["authorization"] = lower["authorization"] =
"Basic " .. (mime.b64(reqt.user .. ":" .. reqt.password)) "Basic " .. (mime.b64(reqt.user .. ":" .. reqt.password))
end end
-- override with user headers -- override with user headers
for i,v in base.pairs(reqt.headers or lower) do for i, v in base.pairs(reqt.headers or lower) do
lower[string.lower(i)] = v lower[string.lower(i)] = v
end end
return lower return lower
end end
-- default url parts -- default url parts
local default = { local default = {
host = "", host = "",
port = PORT, port = PORT,
path ="/", path = "/",
scheme = "http" scheme = "http"
} }
local function adjustrequest(reqt) local function adjustrequest(reqt)
-- parse url if provided -- parse url if provided
local nreqt = reqt.url and url.parse(reqt.url, default) or {} local nreqt = reqt.url and url.parse(reqt.url, default) or {}
-- explicit components override url -- explicit components override url
for i,v in base.pairs(reqt) do nreqt[i] = v end for i, v in base.pairs(reqt) do nreqt[i] = v end
if nreqt.port == "" then nreqt.port = 80 end if nreqt.port == "" then nreqt.port = 80 end
socket.try(nreqt.host and nreqt.host ~= "", socket.try(nreqt.host and nreqt.host ~= "",
"invalid host '" .. base.tostring(nreqt.host) .. "'") "invalid host '" .. base.tostring(nreqt.host) .. "'")
-- compute uri if user hasn't overriden -- compute uri if user hasn't overriden
nreqt.uri = reqt.uri or adjusturi(nreqt) nreqt.uri = reqt.uri or adjusturi(nreqt)
-- ajust host and port if there is a proxy -- ajust host and port if there is a proxy
nreqt.host, nreqt.port = adjustproxy(nreqt) nreqt.host, nreqt.port = adjustproxy(nreqt)
-- adjust headers in request -- adjust headers in request
nreqt.headers = adjustheaders(nreqt) nreqt.headers = adjustheaders(nreqt)
return nreqt return nreqt
end end
local function shouldredirect(reqt, code, headers) local function shouldredirect(reqt, code, headers)
return headers.location and return headers.location and
string.gsub(headers.location, "%s", "") ~= "" and string.gsub(headers.location, "%s", "") ~= "" and
(reqt.redirect ~= false) and (reqt.redirect ~= false) and
(code == 301 or code == 302) and (code == 301 or code == 302) and
(not reqt.method or reqt.method == "GET" or reqt.method == "HEAD") (not reqt.method or reqt.method == "GET" or reqt.method == "HEAD")
and (not reqt.nredirects or reqt.nredirects < 5) and (not reqt.nredirects or reqt.nredirects < 5)
end end
local function shouldreceivebody(reqt, code) local function shouldreceivebody(reqt, code)
if reqt.method == "HEAD" then return nil end if reqt.method == "HEAD" then return nil end
if code == 204 or code == 304 then return nil end if code == 204 or code == 304 then return nil end
if code >= 100 and code < 200 then return nil end if code >= 100 and code < 200 then return nil end
return 1 return 1
end end
-- forward declarations -- forward declarations
local trequest, tredirect local trequest, tredirect
function tredirect(reqt, location) function tredirect(reqt, location)
local result, code, headers, status = trequest { local result, code, headers, status = trequest {
-- the RFC says the redirect URL has to be absolute, but some -- the RFC says the redirect URL has to be absolute, but some
-- servers do not respect that -- servers do not respect that
url = url.absolute(reqt.url, location), url = url.absolute(reqt.url, location),
source = reqt.source, source = reqt.source,
sink = reqt.sink, sink = reqt.sink,
headers = reqt.headers, headers = reqt.headers,
proxy = reqt.proxy, proxy = reqt.proxy,
nredirects = (reqt.nredirects or 0) + 1, nredirects = (reqt.nredirects or 0) + 1,
create = reqt.create create = reqt.create
} }
-- pass location header back as a hint we redirected -- pass location header back as a hint we redirected
headers = headers or {} headers = headers or {}
headers.location = headers.location or location headers.location = headers.location or location
return result, code, headers, status return result, code, headers, status
end end
function trequest(reqt) function trequest(reqt)
-- we loop until we get what we want, or -- we loop until we get what we want, or
-- until we are sure there is no way to get it -- until we are sure there is no way to get it
local nreqt = adjustrequest(reqt) local nreqt = adjustrequest(reqt)
local h = open(nreqt.host, nreqt.port, nreqt.create) local h = open(nreqt.host, nreqt.port, nreqt.create)
-- send request line and headers -- send request line and headers
h:sendrequestline(nreqt.method, nreqt.uri) h:sendrequestline(nreqt.method, nreqt.uri)
h:sendheaders(nreqt.headers) h:sendheaders(nreqt.headers)
-- if there is a body, send it -- if there is a body, send it
if nreqt.source then if nreqt.source then
h:sendbody(nreqt.headers, nreqt.source, nreqt.step) h:sendbody(nreqt.headers, nreqt.source, nreqt.step)
end end
local code, status = h:receivestatusline() local code, status = h:receivestatusline()
-- if it is an HTTP/0.9 server, simply get the body and we are done -- if it is an HTTP/0.9 server, simply get the body and we are done
if not code then if not code then
h:receive09body(status, nreqt.sink, nreqt.step) h:receive09body(status, nreqt.sink, nreqt.step)
return 1, 200 return 1, 200
end end
local headers local headers
-- ignore any 100-continue messages -- ignore any 100-continue messages
while code == 100 do while code == 100 do
headers = h:receiveheaders()
code, status = h:receivestatusline()
end
headers = h:receiveheaders() headers = h:receiveheaders()
-- at this point we should have a honest reply from the server code, status = h:receivestatusline()
-- we can't redirect if we already used the source, so we report the error end
if shouldredirect(nreqt, code, headers) and not nreqt.source then headers = h:receiveheaders()
h:close() -- at this point we should have a honest reply from the server
return tredirect(reqt, headers.location) -- we can't redirect if we already used the source, so we report the error
end if shouldredirect(nreqt, code, headers) and not nreqt.source then
-- here we are finally done
if shouldreceivebody(nreqt, code) then
h:receivebody(headers, nreqt.sink, nreqt.step)
end
h:close() h:close()
return 1, code, headers, status 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 end
local function srequest(u, b) local function srequest(u, b)
local t = {} local t = {}
local reqt = { local reqt = {
url = u, url = u,
sink = ltn12.sink.table(t) 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"
} }
if b then reqt.method = "POST"
reqt.source = ltn12.source.string(b) end
reqt.headers = { local code, headers, status = socket.skip(1, trequest(reqt))
["content-length"] = string.len(b), return table.concat(t), code, headers, status
["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 end
request = socket.protect(function(reqt, body) request = socket.protect(function(reqt, body)
if base.type(reqt) == "string" then return srequest(reqt, body) if base.type(reqt) == "string" then
else return trequest(reqt) end return srequest(reqt, body)
else
return trequest(reqt)
end
end) end)

View File

@@ -40,95 +40,95 @@ ZONE = "-0000"
local metat = { __index = {} } local metat = { __index = {} }
function metat.__index:greet(domain) function metat.__index:greet(domain)
self.try(self.tp:check("2..")) self.try(self.tp:check("2.."))
self.try(self.tp:command("EHLO", domain or DOMAIN)) self.try(self.tp:command("EHLO", domain or DOMAIN))
return socket.skip(1, self.try(self.tp:check("2.."))) return socket.skip(1, self.try(self.tp:check("2..")))
end end
function metat.__index:mail(from) function metat.__index:mail(from)
self.try(self.tp:command("MAIL", "FROM:" .. from)) self.try(self.tp:command("MAIL", "FROM:" .. from))
return self.try(self.tp:check("2..")) return self.try(self.tp:check("2.."))
end end
function metat.__index:rcpt(to) function metat.__index:rcpt(to)
self.try(self.tp:command("RCPT", "TO:" .. to)) self.try(self.tp:command("RCPT", "TO:" .. to))
return self.try(self.tp:check("2..")) return self.try(self.tp:check("2.."))
end end
function metat.__index:data(src, step) function metat.__index:data(src, step)
self.try(self.tp:command("DATA")) self.try(self.tp:command("DATA"))
self.try(self.tp:check("3..")) self.try(self.tp:check("3.."))
self.try(self.tp:source(src, step)) self.try(self.tp:source(src, step))
self.try(self.tp:send("\r\n.\r\n")) self.try(self.tp:send("\r\n.\r\n"))
return self.try(self.tp:check("2..")) return self.try(self.tp:check("2.."))
end end
function metat.__index:quit() function metat.__index:quit()
self.try(self.tp:command("QUIT")) self.try(self.tp:command("QUIT"))
return self.try(self.tp:check("2..")) return self.try(self.tp:check("2.."))
end end
function metat.__index:close() function metat.__index:close()
return self.tp:close() return self.tp:close()
end end
function metat.__index:login(user, password) function metat.__index:login(user, password)
self.try(self.tp:command("AUTH", "LOGIN")) self.try(self.tp:command("AUTH", "LOGIN"))
self.try(self.tp:check("3..")) self.try(self.tp:check("3.."))
self.try(self.tp:command(mime.b64(user))) self.try(self.tp:command(mime.b64(user)))
self.try(self.tp:check("3..")) self.try(self.tp:check("3.."))
self.try(self.tp:command(mime.b64(password))) self.try(self.tp:command(mime.b64(password)))
return self.try(self.tp:check("2..")) return self.try(self.tp:check("2.."))
end end
function metat.__index:plain(user, password) function metat.__index:plain(user, password)
local auth = "PLAIN " .. mime.b64("\0" .. user .. "\0" .. password) local auth = "PLAIN " .. mime.b64("\0" .. user .. "\0" .. password)
self.try(self.tp:command("AUTH", auth)) self.try(self.tp:command("AUTH", auth))
return self.try(self.tp:check("2..")) return self.try(self.tp:check("2.."))
end end
function metat.__index:auth(user, password, ext) function metat.__index:auth(user, password, ext)
if not user or not password then return 1 end if not user or not password then return 1 end
if string.find(ext, "AUTH[^\n]+LOGIN") then if string.find(ext, "AUTH[^\n]+LOGIN") then
return self:login(user, password) return self:login(user, password)
elseif string.find(ext, "AUTH[^\n]+PLAIN") then elseif string.find(ext, "AUTH[^\n]+PLAIN") then
return self:plain(user, password) return self:plain(user, password)
else else
self.try(nil, "authentication not supported") self.try(nil, "authentication not supported")
end end
end end
-- send message or throw an exception -- send message or throw an exception
function metat.__index:send(mailt) function metat.__index:send(mailt)
self:mail(mailt.from) self:mail(mailt.from)
if base.type(mailt.rcpt) == "table" then if base.type(mailt.rcpt) == "table" then
for i,v in base.ipairs(mailt.rcpt) do for i, v in base.ipairs(mailt.rcpt) do
self:rcpt(v) self:rcpt(v)
end
else
self:rcpt(mailt.rcpt)
end end
self:data(ltn12.source.chain(mailt.source, mime.stuff()), mailt.step) else
self:rcpt(mailt.rcpt)
end
self:data(ltn12.source.chain(mailt.source, mime.stuff()), mailt.step)
end end
function open(server, port, create) function open(server, port, create)
local tp = socket.try(tp.connect(server or SERVER, port or PORT, local tp = socket.try(tp.connect(server or SERVER, port or PORT,
TIMEOUT, create)) TIMEOUT, create))
local s = base.setmetatable({tp = tp}, metat) local s = base.setmetatable({ tp = tp }, metat)
-- make sure tp is closed if we get an exception -- make sure tp is closed if we get an exception
s.try = socket.newtry(function() s.try = socket.newtry(function()
s:close() s:close()
end) end)
return s return s
end end
-- convert headers to lowercase -- convert headers to lowercase
local function lower_headers(headers) local function lower_headers(headers)
local lower = {} local lower = {}
for i,v in base.pairs(headers or lower) do for i, v in base.pairs(headers or lower) do
lower[string.lower(i)] = v lower[string.lower(i)] = v
end end
return lower return lower
end end
--------------------------------------------------------------------------- ---------------------------------------------------------------------------
@@ -137,9 +137,9 @@ end
-- returns a hopefully unique mime boundary -- returns a hopefully unique mime boundary
local seqno = 0 local seqno = 0
local function newboundary() local function newboundary()
seqno = seqno + 1 seqno = seqno + 1
return string.format('%s%05d==%05u', os.date('%d%m%Y%H%M%S'), return string.format('%s%05d==%05u', os.date('%d%m%Y%H%M%S'),
math.random(0, 99999), seqno) math.random(0, 99999), seqno)
end end
-- send_message forward declaration -- send_message forward declaration
@@ -147,105 +147,116 @@ local send_message
-- yield the headers all at once, it's faster -- yield the headers all at once, it's faster
local function send_headers(headers) local function send_headers(headers)
local h = "\r\n" local h = "\r\n"
for i,v in base.pairs(headers) do for i, v in base.pairs(headers) do
h = i .. ': ' .. v .. "\r\n" .. h h = i .. ': ' .. v .. "\r\n" .. h
end end
coroutine.yield(h) coroutine.yield(h)
end end
-- yield multipart message body from a multipart message table -- yield multipart message body from a multipart message table
local function send_multipart(mesgt) local function send_multipart(mesgt)
-- make sure we have our boundary and send headers -- make sure we have our boundary and send headers
local bd = newboundary() local bd = newboundary()
local headers = lower_headers(mesgt.headers or {}) local headers = lower_headers(mesgt.headers or {})
headers['content-type'] = headers['content-type'] or 'multipart/mixed' headers['content-type'] = headers['content-type'] or 'multipart/mixed'
headers['content-type'] = headers['content-type'] .. headers['content-type'] = headers['content-type'] ..
'; boundary="' .. bd .. '"' '; boundary="' .. bd .. '"'
send_headers(headers) send_headers(headers)
-- send preamble -- send preamble
if mesgt.body.preamble then if mesgt.body.preamble then
coroutine.yield(mesgt.body.preamble) coroutine.yield(mesgt.body.preamble)
coroutine.yield("\r\n") coroutine.yield("\r\n")
end end
-- send each part separated by a boundary -- send each part separated by a boundary
for i, m in base.ipairs(mesgt.body) do for i, m in base.ipairs(mesgt.body) do
coroutine.yield("\r\n--" .. bd .. "\r\n") coroutine.yield("\r\n--" .. bd .. "\r\n")
send_message(m) send_message(m)
end end
-- send last boundary -- send last boundary
coroutine.yield("\r\n--" .. bd .. "--\r\n\r\n") coroutine.yield("\r\n--" .. bd .. "--\r\n\r\n")
-- send epilogue -- send epilogue
if mesgt.body.epilogue then if mesgt.body.epilogue then
coroutine.yield(mesgt.body.epilogue) coroutine.yield(mesgt.body.epilogue)
coroutine.yield("\r\n") coroutine.yield("\r\n")
end end
end end
-- yield message body from a source -- yield message body from a source
local function send_source(mesgt) local function send_source(mesgt)
-- make sure we have a content-type -- make sure we have a content-type
local headers = lower_headers(mesgt.headers or {}) local headers = lower_headers(mesgt.headers or {})
headers['content-type'] = headers['content-type'] or headers['content-type'] = headers['content-type'] or
'text/plain; charset="iso-8859-1"' 'text/plain; charset="iso-8859-1"'
send_headers(headers) send_headers(headers)
-- send body from source -- send body from source
while true do while true do
local chunk, err = mesgt.body() local chunk, err = mesgt.body()
if err then coroutine.yield(nil, err) if err then
elseif chunk then coroutine.yield(chunk) coroutine.yield(nil, err)
else break end elseif chunk then
coroutine.yield(chunk)
else
break
end end
end
end end
-- yield message body from a string -- yield message body from a string
local function send_string(mesgt) local function send_string(mesgt)
-- make sure we have a content-type -- make sure we have a content-type
local headers = lower_headers(mesgt.headers or {}) local headers = lower_headers(mesgt.headers or {})
headers['content-type'] = headers['content-type'] or headers['content-type'] = headers['content-type'] or
'text/plain; charset="iso-8859-1"' 'text/plain; charset="iso-8859-1"'
send_headers(headers) send_headers(headers)
-- send body from string -- send body from string
coroutine.yield(mesgt.body) coroutine.yield(mesgt.body)
end end
-- message source -- message source
function send_message(mesgt) function send_message(mesgt)
if base.type(mesgt.body) == "table" then send_multipart(mesgt) if base.type(mesgt.body) == "table" then
elseif base.type(mesgt.body) == "function" then send_source(mesgt) send_multipart(mesgt)
else send_string(mesgt) end elseif base.type(mesgt.body) == "function" then
send_source(mesgt)
else
send_string(mesgt)
end
end end
-- set defaul headers -- set defaul headers
local function adjust_headers(mesgt) local function adjust_headers(mesgt)
local lower = lower_headers(mesgt.headers) local lower = lower_headers(mesgt.headers)
lower["date"] = lower["date"] or lower["date"] = lower["date"] or
os.date("!%a, %d %b %Y %H:%M:%S ") .. (mesgt.zone or ZONE) os.date("!%a, %d %b %Y %H:%M:%S ") .. (mesgt.zone or ZONE)
lower["x-mailer"] = lower["x-mailer"] or socket._VERSION lower["x-mailer"] = lower["x-mailer"] or socket._VERSION
-- this can't be overriden -- this can't be overriden
lower["mime-version"] = "1.0" lower["mime-version"] = "1.0"
return lower return lower
end end
function message(mesgt) function message(mesgt)
mesgt.headers = adjust_headers(mesgt) mesgt.headers = adjust_headers(mesgt)
-- create and return message source -- create and return message source
local co = coroutine.create(function() send_message(mesgt) end) local co = coroutine.create(function() send_message(mesgt) end)
return function() return function()
local ret, a, b = coroutine.resume(co) local ret, a, b = coroutine.resume(co)
if ret then return a, b if ret then
else return nil, a end return a, b
else
return nil, a
end end
end
end end
--------------------------------------------------------------------------- ---------------------------------------------------------------------------
-- High level SMTP API -- High level SMTP API
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
send = socket.protect(function(mailt) send = socket.protect(function(mailt)
local s = open(mailt.server, mailt.port, mailt.create) local s = open(mailt.server, mailt.port, mailt.create)
local ext = s:greet(mailt.domain) local ext = s:greet(mailt.domain)
s:auth(mailt.user, mailt.password, ext) s:auth(mailt.user, mailt.password, ext)
s:send(mailt) s:send(mailt)
s:quit() s:quit()
return s:close() return s:close()
end) end)

View File

@@ -24,100 +24,104 @@ TIMEOUT = 60
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
-- gets server reply (works for SMTP and FTP) -- gets server reply (works for SMTP and FTP)
local function get_reply(c) local function get_reply(c)
local code, current, sep local code, current, sep
local line, err = c:receive() local line, err = c:receive()
local reply = line local reply = line
if err then return nil, err end if err then return nil, err end
code, sep = socket.skip(2, string.find(line, "^(%d%d%d)(.?)")) code, sep = socket.skip(2, string.find(line, "^(%d%d%d)(.?)"))
if not code then return nil, "invalid server reply" end if not code then return nil, "invalid server reply" end
if sep == "-" then -- reply is multiline if sep == "-" then -- reply is multiline
repeat repeat
line, err = c:receive() line, err = c:receive()
if err then return nil, err end if err then return nil, err end
current, sep = socket.skip(2, string.find(line, "^(%d%d%d)(.?)")) current, sep = socket.skip(2, string.find(line, "^(%d%d%d)(.?)"))
reply = reply .. "\n" .. line reply = reply .. "\n" .. line
-- reply ends with same code -- reply ends with same code
until code == current and sep == " " until code == current and sep == " "
end end
return code, reply return code, reply
end end
-- metatable for sock object -- metatable for sock object
local metat = { __index = {} } local metat = { __index = {} }
function metat.__index:check(ok) function metat.__index:check(ok)
local code, reply = get_reply(self.c) local code, reply = get_reply(self.c)
if not code then return nil, reply end if not code then return nil, reply end
if base.type(ok) ~= "function" then if base.type(ok) ~= "function" then
if base.type(ok) == "table" then if base.type(ok) == "table" then
for i, v in base.ipairs(ok) do for i, v in base.ipairs(ok) do
if string.find(code, v) then if string.find(code, v) then
return base.tonumber(code), reply 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 end
else return ok(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 end
function metat.__index:command(cmd, arg) function metat.__index:command(cmd, arg)
if arg then if arg then
return self.c:send(cmd .. " " .. arg.. "\r\n") return self.c:send(cmd .. " " .. arg .. "\r\n")
else else
return self.c:send(cmd .. "\r\n") return self.c:send(cmd .. "\r\n")
end end
end end
function metat.__index:sink(snk, pat) function metat.__index:sink(snk, pat)
local chunk, err = c:receive(pat) local chunk, err = c:receive(pat)
return snk(chunk, err) return snk(chunk, err)
end end
function metat.__index:send(data) function metat.__index:send(data)
return self.c:send(data) return self.c:send(data)
end end
function metat.__index:receive(pat) function metat.__index:receive(pat)
return self.c:receive(pat) return self.c:receive(pat)
end end
function metat.__index:getfd() function metat.__index:getfd()
return self.c:getfd() return self.c:getfd()
end end
function metat.__index:dirty() function metat.__index:dirty()
return self.c:dirty() return self.c:dirty()
end end
function metat.__index:getcontrol() function metat.__index:getcontrol()
return self.c return self.c
end end
function metat.__index:source(source, step) function metat.__index:source(source, step)
local sink = socket.sink("keep-open", self.c) local sink = socket.sink("keep-open", self.c)
local ret, err = ltn12.pump.all(source, sink, step or ltn12.pump.step) local ret, err = ltn12.pump.all(source, sink, step or ltn12.pump.step)
return ret, err return ret, err
end end
-- closes the underlying c -- closes the underlying c
function metat.__index:close() function metat.__index:close()
self.c:close() self.c:close()
return 1 return 1
end end
-- connect with server and return c object -- connect with server and return c object
function connect(host, port, timeout, create) function connect(host, port, timeout, create)
local c, e = (create or socket.tcp)() local c, e = (create or socket.tcp)()
if not c then return nil, e end if not c then return nil, e end
c:settimeout(timeout or TIMEOUT) c:settimeout(timeout or TIMEOUT)
local r, e = c:connect(host, port) local r, e = c:connect(host, port)
if not r then if not r then
c:close() c:close()
return nil, e return nil, e
end end
return base.setmetatable({c = c}, metat) return base.setmetatable({ c = c }, metat)
end end

View File

@@ -26,9 +26,9 @@ _VERSION = "URL 1.0.1"
-- escaped representation of string binary -- escaped representation of string binary
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
function escape(s) function escape(s)
return string.gsub(s, "([^A-Za-z0-9_])", function(c) return string.gsub(s, "([^A-Za-z0-9_])", function(c)
return string.format("%%%02x", string.byte(c)) return string.format("%%%02x", string.byte(c))
end) end)
end end
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
@@ -40,25 +40,28 @@ end
-- escaped representation of string binary -- escaped representation of string binary
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
local function make_set(t) local function make_set(t)
local s = {} local s = {}
for i,v in base.ipairs(t) do for i, v in base.ipairs(t) do
s[t[i]] = 1 s[t[i]] = 1
end end
return s return s
end end
-- these are allowed withing a path segment, along with alphanum -- these are allowed withing a path segment, along with alphanum
-- other characters must be escaped -- other characters must be escaped
local segment_set = make_set { local segment_set = make_set {
"-", "_", ".", "!", "~", "*", "'", "(", "-", "_", ".", "!", "~", "*", "'", "(",
")", ":", "@", "&", "=", "+", "$", ",", ")", ":", "@", "&", "=", "+", "$", ",",
} }
local function protect_segment(s) local function protect_segment(s)
return string.gsub(s, "([^A-Za-z0-9_])", function (c) return string.gsub(s, "([^A-Za-z0-9_])", function(c)
if segment_set[c] then return c if segment_set[c] then
else return string.format("%%%02x", string.byte(c)) end return c
end) else
return string.format("%%%02x", string.byte(c))
end
end)
end end
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
@@ -69,9 +72,9 @@ end
-- escaped representation of string binary -- escaped representation of string binary
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
function unescape(s) function unescape(s)
return string.gsub(s, "%%(%x%x)", function(hex) return string.gsub(s, "%%(%x%x)", function(hex)
return string.char(base.tonumber(hex, 16)) return string.char(base.tonumber(hex, 16))
end) end)
end end
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
@@ -83,24 +86,24 @@ end
-- corresponding absolute path -- corresponding absolute path
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
local function absolute_path(base_path, relative_path) local function absolute_path(base_path, relative_path)
if string.sub(relative_path, 1, 1) == "/" then return relative_path end if string.sub(relative_path, 1, 1) == "/" then return relative_path end
local path = string.gsub(base_path, "[^/]*$", "") local path = string.gsub(base_path, "[^/]*$", "")
path = path .. relative_path path = path .. relative_path
path = string.gsub(path, "([^/]*%./)", function (s) path = string.gsub(path, "([^/]*%./)", function(s)
if s ~= "./" then return s else return "" end 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(path, "/%.$", "/") end
local reduced path = string.gsub(reduced, "([^/]*/%.%.)$", function(s)
while reduced ~= path do if s ~= "../.." then return "" else return s end
reduced = path end)
path = string.gsub(reduced, "([^/]*/%.%./)", function (s) return path
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 end
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
@@ -122,51 +125,59 @@ end
-- the leading '/' in {/<path>} is considered part of <path> -- the leading '/' in {/<path>} is considered part of <path>
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
function parse(url, default) function parse(url, default)
-- initialize default parameters -- initialize default parameters
local parsed = {} local parsed = {}
for i,v in base.pairs(default or parsed) do parsed[i] = v end for i, v in base.pairs(default or parsed) do parsed[i] = v end
-- empty url is parsed to nil -- empty url is parsed to nil
if not url or url == "" then return nil, "invalid url" end if not url or url == "" then return nil, "invalid url" end
-- remove whitespace -- remove whitespace
-- url = string.gsub(url, "%s", "") -- url = string.gsub(url, "%s", "")
-- get fragment -- get fragment
url = string.gsub(url, "#(.*)$", function(f) url = string.gsub(url, "#(.*)$", function(f)
parsed.fragment = f parsed.fragment = f
return "" return ""
end)
-- get scheme
url = string.gsub(url, "^([%w][%w%+%-%.]*)%:",
function(s)
parsed.scheme = s; return ""
end) end)
-- get scheme -- get authority
url = string.gsub(url, "^([%w][%w%+%-%.]*)%:", url = string.gsub(url, "^//([^/]*)", function(n)
function(s) parsed.scheme = s; return "" end) parsed.authority = n
-- get authority return ""
url = string.gsub(url, "^//([^/]*)", function(n) end)
parsed.authority = n -- get query stringing
return "" 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) end)
-- get query stringing authority = string.gsub(authority, ":([^:]*)$",
url = string.gsub(url, "%?(.*)", function(q) function(p)
parsed.query = q parsed.port = p; return ""
return ""
end) end)
-- get params if authority ~= "" then parsed.host = authority end
url = string.gsub(url, "%;(.*)", function(p) local userinfo = parsed.userinfo
parsed.params = p if not userinfo then return parsed end
return "" userinfo = string.gsub(userinfo, ":([^:]*)$",
function(p)
parsed.password = p; return ""
end) end)
-- path is whatever was left parsed.user = userinfo
if url ~= "" then parsed.path = url end return parsed
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 end
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
@@ -178,28 +189,28 @@ end
-- a stringing with the corresponding URL -- a stringing with the corresponding URL
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
function build(parsed) function build(parsed)
local ppath = parse_path(parsed.path or "") local ppath = parse_path(parsed.path or "")
local url = build_path(ppath) local url = build_path(ppath)
if parsed.params then url = url .. ";" .. parsed.params end if parsed.params then url = url .. ";" .. parsed.params end
if parsed.query then url = url .. "?" .. parsed.query end if parsed.query then url = url .. "?" .. parsed.query end
local authority = parsed.authority local authority = parsed.authority
if parsed.host then if parsed.host then
authority = parsed.host authority = parsed.host
if parsed.port then authority = authority .. ":" .. parsed.port end if parsed.port then authority = authority .. ":" .. parsed.port end
local userinfo = parsed.userinfo local userinfo = parsed.userinfo
if parsed.user then if parsed.user then
userinfo = parsed.user userinfo = parsed.user
if parsed.password then if parsed.password then
userinfo = userinfo .. ":" .. parsed.password userinfo = userinfo .. ":" .. parsed.password
end end
end end
if userinfo then authority = userinfo .. "@" .. authority end if userinfo then authority = userinfo .. "@" .. authority end
end end
if authority then url = "//" .. authority .. url end if authority then url = "//" .. authority .. url end
if parsed.scheme then url = parsed.scheme .. ":" .. url end if parsed.scheme then url = parsed.scheme .. ":" .. url end
if parsed.fragment then url = url .. "#" .. parsed.fragment end if parsed.fragment then url = url .. "#" .. parsed.fragment end
-- url = string.gsub(url, "%s", "") -- url = string.gsub(url, "%s", "")
return url return url
end end
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
@@ -211,35 +222,38 @@ end
-- corresponding absolute url -- corresponding absolute url
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
function absolute(base_url, relative_url) function absolute(base_url, relative_url)
if base.type(base_url) == "table" then if base.type(base_url) == "table" then
base_parsed = base_url base_parsed = base_url
base_url = build(base_parsed) base_url = build(base_parsed)
else else
base_parsed = parse(base_url) base_parsed = parse(base_url)
end end
local relative_parsed = parse(relative_url) local relative_parsed = parse(relative_url)
if not base_parsed then return relative_url if not base_parsed then
elseif not relative_parsed then return base_url return relative_url
elseif relative_parsed.scheme then return relative_url elseif not relative_parsed then
else return base_url
relative_parsed.scheme = base_parsed.scheme elseif relative_parsed.scheme then
if not relative_parsed.authority then return relative_url
relative_parsed.authority = base_parsed.authority else
if not relative_parsed.path then relative_parsed.scheme = base_parsed.scheme
relative_parsed.path = base_parsed.path if not relative_parsed.authority then
if not relative_parsed.params then relative_parsed.authority = base_parsed.authority
relative_parsed.params = base_parsed.params if not relative_parsed.path then
if not relative_parsed.query then relative_parsed.path = base_parsed.path
relative_parsed.query = base_parsed.query if not relative_parsed.params then
end relative_parsed.params = base_parsed.params
end if not relative_parsed.query then
else relative_parsed.query = base_parsed.query
relative_parsed.path = absolute_path(base_parsed.path or "", end
relative_parsed.path)
end
end end
return build(relative_parsed) else
relative_parsed.path = absolute_path(base_parsed.path or "",
relative_parsed.path)
end
end end
return build(relative_parsed)
end
end end
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
@@ -250,16 +264,16 @@ end
-- segment: a table with one entry per segment -- segment: a table with one entry per segment
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
function parse_path(path) function parse_path(path)
local parsed = {} local parsed = {}
path = path or "" path = path or ""
--path = string.gsub(path, "%s", "") --path = string.gsub(path, "%s", "")
string.gsub(path, "([^/]+)", function (s) table.insert(parsed, s) end) string.gsub(path, "([^/]+)", function(s) table.insert(parsed, s) end)
for i = 1, #parsed do for i = 1, #parsed do
parsed[i] = unescape(parsed[i]) parsed[i] = unescape(parsed[i])
end end
if string.sub(path, 1, 1) == "/" then parsed.is_absolute = 1 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 if string.sub(path, -1, -1) == "/" then parsed.is_directory = 1 end
return parsed return parsed
end end
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
@@ -271,27 +285,27 @@ end
-- path: corresponding path stringing -- path: corresponding path stringing
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
function build_path(parsed, unsafe) function build_path(parsed, unsafe)
local path = "" local path = ""
local n = #parsed local n = #parsed
if unsafe then if unsafe then
for i = 1, n-1 do for i = 1, n - 1 do
path = path .. parsed[i] path = path .. parsed[i]
path = path .. "/" path = path .. "/"
end end
if n > 0 then if n > 0 then
path = path .. parsed[n] path = path .. parsed[n]
if parsed.is_directory then path = path .. "/" end if parsed.is_directory then path = path .. "/" end
end end
else else
for i = 1, n-1 do for i = 1, n - 1 do
path = path .. protect_segment(parsed[i]) path = path .. protect_segment(parsed[i])
path = path .. "/" path = path .. "/"
end end
if n > 0 then if n > 0 then
path = path .. protect_segment(parsed[n]) path = path .. protect_segment(parsed[n])
if parsed.is_directory then path = path .. "/" end if parsed.is_directory then path = path .. "/" end
end end
end end
if parsed.is_absolute then path = "/" .. path end if parsed.is_absolute then path = "/" .. path end
return path return path
end end

View File

@@ -1,10 +1,12 @@
-- Sanitize the Lua environment to: -- Sanitize the Lua environment to:
-- Prevent scripts from accessing files outside the game directory -- Prevent scripts from accessing files outside the game directory
-- Prevent usage of libraries other than -- Prevent usage of libraries other than
-- Utility: Convert a list of strings into a map of string->true -- Utility: Convert a list of strings into a map of string->true
local function tmap(t) local u = {}; for _, n in ipairs(t) do u[n] = true end; return u; end local function tmap(t)
local u = {}; for _, n in ipairs(t) do u[n] = true end; return u;
end
-- Save banned global variables for wrapping with safe functions -- Save banned global variables for wrapping with safe functions
local old_io = io local old_io = io
@@ -15,98 +17,99 @@ local old_package = package
-- Remove all global variables except a whitelist -- Remove all global variables except a whitelist
local ok_names = tmap { local ok_names = tmap {
'_G', '_bllua_ts', '_bllua_on_unload', '_bllua_on_error', '_G', '_bllua_ts', '_bllua_on_unload', '_bllua_on_error',
'string', 'table', 'math', 'coroutine', 'bit', 'string', 'table', 'math', 'coroutine', 'bit',
'pairs', 'ipairs', 'next', 'unpack', 'select', 'pairs', 'ipairs', 'next', 'unpack', 'select',
'error', 'assert', 'pcall', 'xpcall', 'error', 'assert', 'pcall', 'xpcall',
'type', 'tostring', 'tonumber', 'type', 'tostring', 'tonumber',
'loadstring', 'loadstring',
'getmetatable', 'setmetatable', 'getmetatable', 'setmetatable',
'rawget', 'rawset', 'rawequal', 'rawlen', 'rawget', 'rawset', 'rawequal', 'rawlen',
'module', '_VERSION', 'module', '_VERSION',
} }
local not_ok_names = {} local not_ok_names = {}
for n, _ in pairs(_G) do for n, _ in pairs(_G) do
if not ok_names[n] then if not ok_names[n] then
table.insert(not_ok_names, n) table.insert(not_ok_names, n)
end end
end end
for _, n in ipairs(not_ok_names) do for _, n in ipairs(not_ok_names) do
_G[n] = nil _G[n] = nil
end end
-- Sanitize file paths to point only to allowed files within the game directory -- Sanitize file paths to point only to allowed files within the game directory
-- List of allowed directories for reading/writing -- List of allowed directories for reading/writing
local allowed_dirs = tmap { local allowed_dirs = tmap {
'add-ons', 'base', 'config', 'saves', 'screenshots', 'shaders' 'add-ons', 'base', 'config', 'saves', 'screenshots', 'shaders'
} }
-- List of allowed directories for reading only -- List of allowed directories for reading only
local allowed_dirs_readonly = tmap { local allowed_dirs_readonly = tmap {
'lualib' 'lualib'
} }
-- List of disallowed file extensions - basically executable file extensions -- List of disallowed file extensions - basically executable file extensions
-- Note that even without this protection, exploiting would still require somehow -- Note that even without this protection, exploiting would still require somehow
-- getting a file within the allowed directories to autorun, -- getting a file within the allowed directories to autorun,
-- so this is just a precaution. -- so this is just a precaution.
local disallowed_exts = tmap { local disallowed_exts = tmap {
-- windows -- windows
'bat','bin','cab','cmd','com','cpl','ex_','exe','gadget','inf','ins','inx','isu', 'bat', 'bin', 'cab', 'cmd', 'com', 'cpl', 'ex_', 'exe', 'gadget', 'inf', 'ins', 'inx', 'isu',
'job','jse','lnk','msc','msi','msp','mst','paf','pif','ps1','reg','rgs','scr', 'job', 'jse', 'lnk', 'msc', 'msi', 'msp', 'mst', 'paf', 'pif', 'ps1', 'reg', 'rgs', 'scr',
'sct','shb','shs','u3p','vb','vbe','vbs','vbscript','ws','wsf','wsh', 'sct', 'shb', 'shs', 'u3p', 'vb', 'vbe', 'vbs', 'vbscript', 'ws', 'wsf', 'wsh',
-- linux -- linux
'csh','ksh','out','run','sh', 'csh', 'ksh', 'out', 'run', 'sh',
-- mac/other -- mac/other
'action','apk','app','command','ipa','osx','prg','workflow', 'action', 'apk', 'app', 'command', 'ipa', 'osx', 'prg', 'workflow',
} }
-- Arguments: file name (relative to game directory), boolean true if only reading -- Arguments: file name (relative to game directory), boolean true if only reading
-- Return: clean file path if allowed (or nil if disallowed), -- Return: clean file path if allowed (or nil if disallowed),
-- error string (or nil if allowed) -- error string (or nil if allowed)
local function safe_path(fn, readonly) local function safe_path(fn, readonly)
fn = fn:gsub('\\', '/') fn = fn:gsub('\\', '/')
fn = fn:gsub('^ +', '') fn = fn:gsub('^ +', '')
fn = fn:gsub(' +$', '') fn = fn:gsub(' +$', '')
-- whitelist characters -- whitelist characters
local ic = fn:find('[^a-zA-Z0-9_%-/ %.]') local ic = fn:find('[^a-zA-Z0-9_%-/ %.]')
if ic then if ic then
return nil, 'Filename \''..fn..'\' contains invalid character \''.. return nil, 'Filename \'' .. fn .. '\' contains invalid character \'' ..
fn:sub(ic, ic)..'\' at position '..ic fn:sub(ic, ic) .. '\' at position ' .. ic
end end
-- disallow up-dirs, absolute paths, and relative paths -- disallow up-dirs, absolute paths, and relative paths
-- './' and '../' are possible in scripts, because they're processed into -- './' and '../' are possible in scripts, because they're processed into
-- absolute paths in util.lua before reaching here -- absolute paths in util.lua before reaching here
if fn:find('^%.') or fn:find('%.%.') or fn:find(':') or fn:find('^/') then if fn:find('^%.') or fn:find('%.%.') or fn:find(':') or fn:find('^/') then
return nil, 'Filename \''..fn..'\' contains invalid sequence' return nil, 'Filename \'' .. fn .. '\' contains invalid sequence'
end end
-- allow only whitelisted dirs -- allow only whitelisted dirs
local dir = fn:match('^([^/]+)/') local dir = fn:match('^([^/]+)/')
if (not dir) or ( if (not dir) or (
(not allowed_dirs[dir:lower()]) and (not allowed_dirs[dir:lower()]) and
((not readonly) or (not allowed_dirs_readonly[dir:lower()])) ) then ((not readonly) or (not allowed_dirs_readonly[dir:lower()]))) then
return nil, 'filename is in disallowed directory '..(dir or 'nil') return nil, 'filename is in disallowed directory ' .. (dir or 'nil')
end end
-- disallow blacklisted extensions or no extension -- disallow blacklisted extensions or no extension
local ext = fn:match('%.([^/%.]+)$') local ext = fn:match('%.([^/%.]+)$')
if (not ext) or (disallowed_exts[ext:lower()]) then if (not ext) or (disallowed_exts[ext:lower()]) then
return nil, 'Filename \''..fn..'\' has disallowed extension \''.. return nil, 'Filename \'' .. fn .. '\' has disallowed extension \'' ..
(ext or '')..'\'' (ext or '') .. '\''
end end
return fn, nil return fn, nil
end end
-- Wrap io.open with path sanitization -- Wrap io.open with path sanitization
function _bllua_io_open(fn, md) function _bllua_io_open(fn, md)
md = md or 'r' md = md or 'r'
local readonly = md=='r' or md=='rb' local readonly = md == 'r' or md == 'rb'
local fns, err = safe_path(fn, readonly) local fns, err = safe_path(fn, readonly)
if fns then if fns then
return old_io.open(fns, md) return old_io.open(fns, md)
else else
return nil, err return nil, err
end end
end end
-- Allow io.type (works on file handles returned by io.open) -- Allow io.type (works on file handles returned by io.open)
function _bllua_io_type(f) function _bllua_io_type(f)
return old_io.type(f) return old_io.type(f)
end end
-- Wrap require with a blacklist for unsafe built-in modules -- Wrap require with a blacklist for unsafe built-in modules
@@ -114,29 +117,30 @@ end
-- Note that util.lua wraps this and provides 'require', -- Note that util.lua wraps this and provides 'require',
-- only falling back here if the package is not found in user files -- only falling back here if the package is not found in user files
local disallowed_packages = tmap { local disallowed_packages = tmap {
'ffi', 'debug', 'package', 'io', 'os', 'ffi', 'debug', 'package', 'io', 'os',
'_bllua_ts', '_bllua_ts',
} }
function _bllua_requiresecure(name) function _bllua_requiresecure(name)
if name:find('[^a-zA-Z0-9_%-%.]') or name:find('%.%.') or if name:find('[^a-zA-Z0-9_%-%.]') or name:find('%.%.') or
name:find('^%.') or name:find('%.$') then name:find('^%.') or name:find('%.$') then
error('require: package name contains invalid character', 3) error('require: package name contains invalid character', 3)
elseif disallowed_packages[name] then elseif disallowed_packages[name] then
error('require: attempt to require disallowed module \''..name..'\'', 3) error('require: attempt to require disallowed module \'' .. name .. '\'', 3)
else else
-- todo: reimplement require to not use package.* stuff? -- todo: reimplement require to not use package.* stuff?
return old_require(name) return old_require(name)
end end
end end
package = { package = {
seeall = old_package.seeall, seeall = old_package.seeall,
} }
-- Provide limited debug -- Provide limited debug
debug = { debug = {
traceback = old_debug.traceback, traceback = old_debug.traceback,
getinfo = old_debug.getinfo, getinfo = old_debug.getinfo,
getfilename = old_debug.getfilename, -- defined in lua.env.lua getfilename = old_debug.getfilename, -- defined in lua.env.lua
} }
_bllua_ts.echo(' Executed bllua-env-safe.lua') _bllua_ts.echo(' Executed bllua-env-safe.lua')

View File

@@ -9,32 +9,32 @@ _bllua_on_unload = {}
-- Utility for getting the current filename -- Utility for getting the current filename
function debug.getfilename(level) function debug.getfilename(level)
if type(level) == 'number' then level = level+1 end if type(level) == 'number' then level = level + 1 end
local info = debug.getinfo(level) local info = debug.getinfo(level)
if not info then return nil end if not info then return nil end
local filename = info.source:match('^%-%-%[%[([^%]]+)%]%]') local filename = info.source:match('^%-%-%[%[([^%]]+)%]%]')
return filename return filename
end end
-- Called when pcall fails on a ts->lua call, used to print detailed error info -- Called when pcall fails on a ts->lua call, used to print detailed error info
function _bllua_on_error(err) function _bllua_on_error(err)
err = err:match(': (.+)$') or err err = err:match(': (.+)$') or err
local tracelines = {err} local tracelines = { err }
local level = 2 local level = 2
while true do while true do
local info = debug.getinfo(level) local info = debug.getinfo(level)
if not info then break end if not info then break end
local filename = debug.getfilename(level) or info.short_src local filename = debug.getfilename(level) or info.short_src
local funcname = info.name local funcname = info.name
if funcname=='dofile' then break end if funcname == 'dofile' then break end
table.insert(tracelines, string.format('%s:%s in function \'%s\'', table.insert(tracelines, string.format('%s:%s in function \'%s\'',
filename, filename,
info.currentline==-1 and '' or info.currentline..':', info.currentline == -1 and '' or info.currentline .. ':',
funcname funcname
)) ))
level = level+1 level = level + 1
end end
return table.concat(tracelines, '\n') return table.concat(tracelines, '\n')
end end
_bllua_ts.echo(' Executed bllua-env.lua') _bllua_ts.echo(' Executed bllua-env.lua')

View File

@@ -4,129 +4,129 @@
-- Class hierarchy, adapted from https://notabug.org/Queuenard/blockland-DLL-tools/src/master/class_hierarchy -- Class hierarchy, adapted from https://notabug.org/Queuenard/blockland-DLL-tools/src/master/class_hierarchy
bl.class('SimObject') bl.class('SimObject')
bl.class('ScriptObject', 'SimObject') bl.class('ScriptObject', 'SimObject')
bl.class('SimSet', 'SimObject') bl.class('SimSet', 'SimObject')
bl.class('SimGroup', 'SimSet') bl.class('SimGroup', 'SimSet')
bl.class('GuiControl', 'SimGroup') bl.class('GuiControl', 'SimGroup')
bl.class('GuiTextCtrl' , 'GuiControl') bl.class('GuiTextCtrl', 'GuiControl')
bl.class('GuiSwatchCtrl' , 'GuiControl') bl.class('GuiSwatchCtrl', 'GuiControl')
bl.class('GuiButtonBaseCtrl' , 'GuiControl') bl.class('GuiButtonBaseCtrl', 'GuiControl')
bl.class('GuiArrayCtrl' , 'GuiControl') bl.class('GuiArrayCtrl', 'GuiControl')
bl.class('GuiScrollCtrl' , 'GuiControl') bl.class('GuiScrollCtrl', 'GuiControl')
bl.class('GuiMouseEventCtrl' , 'GuiControl') bl.class('GuiMouseEventCtrl', 'GuiControl')
bl.class('GuiProgressCtrl' , 'GuiControl') bl.class('GuiProgressCtrl', 'GuiControl')
bl.class('GuiSliderCtrl' , 'GuiControl') bl.class('GuiSliderCtrl', 'GuiControl')
bl.class('GuiConsoleTextCtrl' , 'GuiControl') bl.class('GuiConsoleTextCtrl', 'GuiControl')
bl.class('GuiTSCtrl' , 'GuiControl') bl.class('GuiTSCtrl', 'GuiControl')
bl.class('GuiObjectView', 'GuiTSCtrl') bl.class('GuiObjectView', 'GuiTSCtrl')
bl.class('GameTSCtrl' , 'GuiTSCtrl') bl.class('GameTSCtrl', 'GuiTSCtrl')
bl.class('EditTSCtrl' , 'GuiTSCtrl') bl.class('EditTSCtrl', 'GuiTSCtrl')
bl.class('GuiPlayerView', 'GuiTSCtrl') bl.class('GuiPlayerView', 'GuiTSCtrl')
bl.class('GuiShapeNameHud' , 'GuiControl') bl.class('GuiShapeNameHud', 'GuiControl')
bl.class('GuiHealthBarHud' , 'GuiControl') bl.class('GuiHealthBarHud', 'GuiControl')
bl.class('GuiGraphCtrl' , 'GuiControl') bl.class('GuiGraphCtrl', 'GuiControl')
bl.class('GuiInspector' , 'GuiControl') bl.class('GuiInspector', 'GuiControl')
bl.class('GuiChunkedBitmapCtrl', 'GuiControl') bl.class('GuiChunkedBitmapCtrl', 'GuiControl')
bl.class('GuiInputCtrl' , 'GuiControl') bl.class('GuiInputCtrl', 'GuiControl')
bl.class('GuiNoMouseCtrl' , 'GuiControl') bl.class('GuiNoMouseCtrl', 'GuiControl')
bl.class('GuiBitmapBorderCtrl' , 'GuiControl') bl.class('GuiBitmapBorderCtrl', 'GuiControl')
bl.class('GuiBackgroundCtrl' , 'GuiControl') bl.class('GuiBackgroundCtrl', 'GuiControl')
bl.class('GuiEditorRuler' , 'GuiControl') bl.class('GuiEditorRuler', 'GuiControl')
bl.class('GuiClockHud' , 'GuiControl') bl.class('GuiClockHud', 'GuiControl')
bl.class('GuiEditCtrl' , 'GuiControl') bl.class('GuiEditCtrl', 'GuiControl')
bl.class('GuiFilterCtrl' , 'GuiControl') bl.class('GuiFilterCtrl', 'GuiControl')
bl.class('GuiFrameSetCtrl' , 'GuiControl') bl.class('GuiFrameSetCtrl', 'GuiControl')
bl.class('GuiMenuBar' , 'GuiControl') bl.class('GuiMenuBar', 'GuiControl')
bl.class('GuiMessageVectorCtrl', 'GuiControl') bl.class('GuiMessageVectorCtrl', 'GuiControl')
bl.class('GuiBitmapCtrl' , 'GuiControl') bl.class('GuiBitmapCtrl', 'GuiControl')
bl.class('GuiCrossHairHud', 'GuiBitmapCtrl') bl.class('GuiCrossHairHud', 'GuiBitmapCtrl')
bl.class('ScriptGroup', 'SimGroup') bl.class('ScriptGroup', 'SimGroup')
bl.class('NetConnection', 'SimGroup') bl.class('NetConnection', 'SimGroup')
bl.class('GameConnection', 'NetConnection') bl.class('GameConnection', 'NetConnection')
bl.class('Path', 'SimGroup') bl.class('Path', 'SimGroup')
bl.class('TCPObject', 'SimObject') bl.class('TCPObject', 'SimObject')
bl.class('SOCKObject', 'TCPObject') bl.class('SOCKObject', 'TCPObject')
bl.class('HTTPObject', 'TCPObject') bl.class('HTTPObject', 'TCPObject')
bl.class('SimDataBlock', 'SimObject') bl.class('SimDataBlock', 'SimObject')
bl.class('AudioEnvironment' , 'SimDataBlock') bl.class('AudioEnvironment', 'SimDataBlock')
bl.class('AudioSampleEnvironment', 'SimDataBlock') bl.class('AudioSampleEnvironment', 'SimDataBlock')
bl.class('AudioDescription' , 'SimDataBlock') bl.class('AudioDescription', 'SimDataBlock')
bl.class('GameBaseData' , 'SimDataBlock') bl.class('GameBaseData', 'SimDataBlock')
bl.class('ShapeBaseData' , 'GameBaseData') bl.class('ShapeBaseData', 'GameBaseData')
bl.class('CameraData' , 'ShapeBaseData') bl.class('CameraData', 'ShapeBaseData')
bl.class('ItemData' , 'ShapeBaseData') bl.class('ItemData', 'ShapeBaseData')
bl.class('MissionMarkerData', 'ShapeBaseData') bl.class('MissionMarkerData', 'ShapeBaseData')
bl.class('PathCameraData' , 'ShapeBaseData') bl.class('PathCameraData', 'ShapeBaseData')
bl.class('PlayerData' , 'ShapeBaseData') bl.class('PlayerData', 'ShapeBaseData')
bl.class('StaticShapeData' , 'ShapeBaseData') bl.class('StaticShapeData', 'ShapeBaseData')
bl.class('VehicleData' , 'ShapeBaseData') bl.class('VehicleData', 'ShapeBaseData')
bl.class('FlyingVehicleData' , 'VehicleData') bl.class('FlyingVehicleData', 'VehicleData')
bl.class('WheeledVehicleData', 'VehicleData') bl.class('WheeledVehicleData', 'VehicleData')
bl.class('DebrisData' , 'GameBaseData') bl.class('DebrisData', 'GameBaseData')
bl.class('ProjectileData' , 'GameBaseData') bl.class('ProjectileData', 'GameBaseData')
bl.class('ShapeBaseImageData' , 'GameBaseData') bl.class('ShapeBaseImageData', 'GameBaseData')
bl.class('TriggerData' , 'GameBaseData') bl.class('TriggerData', 'GameBaseData')
bl.class('ExplosionData' , 'GameBaseData') bl.class('ExplosionData', 'GameBaseData')
bl.class('fxLightData' , 'GameBaseData') bl.class('fxLightData', 'GameBaseData')
bl.class('LightningData' , 'GameBaseData') bl.class('LightningData', 'GameBaseData')
bl.class('ParticleEmitterNodeData', 'GameBaseData') bl.class('ParticleEmitterNodeData', 'GameBaseData')
bl.class('SplashData' , 'GameBaseData') bl.class('SplashData', 'GameBaseData')
bl.class('fxDTSBrickData' , 'GameBaseData') bl.class('fxDTSBrickData', 'GameBaseData')
bl.class('ParticleEmitterData' , 'GameBaseData') bl.class('ParticleEmitterData', 'GameBaseData')
bl.class('WheeledVehicleTire' , 'SimDataBlock') bl.class('WheeledVehicleTire', 'SimDataBlock')
bl.class('WheeledVehicleSpring' , 'SimDataBlock') bl.class('WheeledVehicleSpring', 'SimDataBlock')
bl.class('TSShapeConstructor' , 'SimDataBlock') bl.class('TSShapeConstructor', 'SimDataBlock')
bl.class('AudioProfile' , 'SimDataBlock') bl.class('AudioProfile', 'SimDataBlock')
bl.class('ParticleData' , 'SimDataBlock') bl.class('ParticleData', 'SimDataBlock')
bl.class('MaterialPropertyMap', 'SimObject') bl.class('MaterialPropertyMap', 'SimObject')
bl.class('NetObject', 'SimObject') bl.class('NetObject', 'SimObject')
bl.class('SceneObject', 'NetObject') bl.class('SceneObject', 'NetObject')
bl.class('GameBase', 'SceneObject') bl.class('GameBase', 'SceneObject')
bl.class('ShapeBase', 'GameBase') bl.class('ShapeBase', 'GameBase')
bl.class('MissionMarker', 'ShapeBase') bl.class('MissionMarker', 'ShapeBase')
bl.class('SpawnSphere' , 'MissionMarker') bl.class('SpawnSphere', 'MissionMarker')
bl.class('VehicleSpawnMarker', 'MissionMarker') bl.class('VehicleSpawnMarker', 'MissionMarker')
bl.class('Waypoint' , 'MissionMarker') bl.class('Waypoint', 'MissionMarker')
bl.class('StaticShape' , 'ShapeBase') bl.class('StaticShape', 'ShapeBase')
bl.class('ScopeAlwaysShape', 'StaticShape') bl.class('ScopeAlwaysShape', 'StaticShape')
bl.class('Player' , 'ShapeBase') bl.class('Player', 'ShapeBase')
bl.class('AIPlayer', 'Player') bl.class('AIPlayer', 'Player')
bl.class('Camera' , 'ShapeBase') bl.class('Camera', 'ShapeBase')
bl.class('Item' , 'ShapeBase') bl.class('Item', 'ShapeBase')
bl.class('PathCamera' , 'ShapeBase') bl.class('PathCamera', 'ShapeBase')
bl.class('Vehicle' , 'ShapeBase') bl.class('Vehicle', 'ShapeBase')
bl.class('FlyingVehicle' , 'Vehicle') bl.class('FlyingVehicle', 'Vehicle')
bl.class('WheeledVehicle', 'Vehicle') bl.class('WheeledVehicle', 'Vehicle')
bl.class('Explosion' , 'GameBase') bl.class('Explosion', 'GameBase')
bl.class('Splash' , 'GameBase') bl.class('Splash', 'GameBase')
bl.class('Debris' , 'GameBase') bl.class('Debris', 'GameBase')
bl.class('Projectile' , 'GameBase') bl.class('Projectile', 'GameBase')
bl.class('Trigger' , 'GameBase') bl.class('Trigger', 'GameBase')
bl.class('fxLight' , 'GameBase') bl.class('fxLight', 'GameBase')
bl.class('Lightning' , 'GameBase') bl.class('Lightning', 'GameBase')
bl.class('ParticleEmitterNode', 'GameBase') bl.class('ParticleEmitterNode', 'GameBase')
bl.class('ParticleEmitter' , 'GameBase') bl.class('ParticleEmitter', 'GameBase')
bl.class('Precipitation' , 'GameBase') bl.class('Precipitation', 'GameBase')
bl.class('TSStatic' , 'SceneObject') bl.class('TSStatic', 'SceneObject')
bl.class('VehicleBlocker', 'SceneObject') bl.class('VehicleBlocker', 'SceneObject')
bl.class('Marker' , 'SceneObject') bl.class('Marker', 'SceneObject')
bl.class('AudioEmitter' , 'SceneObject') bl.class('AudioEmitter', 'SceneObject')
bl.class('PhysicalZone' , 'SceneObject') bl.class('PhysicalZone', 'SceneObject')
bl.class('fxDayCycle' , 'SceneObject') bl.class('fxDayCycle', 'SceneObject')
bl.class('fxDTSBrick' , 'SceneObject') bl.class('fxDTSBrick', 'SceneObject')
bl.class('fxPlane' , 'SceneObject') bl.class('fxPlane', 'SceneObject')
bl.class('fxSunLight' , 'SceneObject') bl.class('fxSunLight', 'SceneObject')
bl.class('Sky' , 'SceneObject') bl.class('Sky', 'SceneObject')
bl.class('SceneRoot' , 'SceneObject') bl.class('SceneRoot', 'SceneObject')
bl.class('Sun', 'NetObject') bl.class('Sun', 'NetObject')
bl.class('GuiCursor', 'SimObject') bl.class('GuiCursor', 'SimObject')
bl.class('ConsoleLogger' , 'SimObject') bl.class('ConsoleLogger', 'SimObject')
bl.class('QuotaObject' , 'SimObject') bl.class('QuotaObject', 'SimObject')
bl.class('FileObject' , 'SimObject') bl.class('FileObject', 'SimObject')
bl.class('BanList' , 'SimObject') bl.class('BanList', 'SimObject')
bl.class('GuiControlProfile', 'SimObject') bl.class('GuiControlProfile', 'SimObject')
bl.class('MessageVector' , 'SimObject') bl.class('MessageVector', 'SimObject')
bl.class('ActionMap' , 'SimObject') bl.class('ActionMap', 'SimObject')
-- Auto-generated from game scripts -- Auto-generated from game scripts
bl.type('ActionMap::blockBind:1', 'object') bl.type('ActionMap::blockBind:1', 'object')

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,3 @@
-- This Lua code provides some built-in utilities for writing Lua add-ons -- This Lua code provides some built-in utilities for writing Lua add-ons
-- It is eval'd automatically once BLLua3 has loaded the TS API and environment -- It is eval'd automatically once BLLua3 has loaded the TS API and environment
-- It only has access to the sandboxed lua environment, just like user code. -- It only has access to the sandboxed lua environment, just like user code.
@@ -8,9 +7,10 @@ ts = _bllua_ts
-- Provide limited OS functions -- Provide limited OS functions
os = os or {} os = os or {}
---@diagnostic disable-next-line: duplicate-set-field ---@diagnostic disable-next-line: duplicate-set-field
function os.time() return math.floor(tonumber(_bllua_ts.call('getSimTime'))/1000) end function os.time() return math.floor(tonumber(_bllua_ts.call('getSimTime')) / 1000) end
---@diagnostic disable-next-line: duplicate-set-field ---@diagnostic disable-next-line: duplicate-set-field
function os.clock() return tonumber(_bllua_ts.call('getSimTime'))/1000 end function os.clock() return tonumber(_bllua_ts.call('getSimTime')) / 1000 end
-- Virtual file class, emulating a file object as returned by io.open -- Virtual file class, emulating a file object as returned by io.open
-- Used to wrap io.open to allow reading from zips (using TS) -- Used to wrap io.open to allow reading from zips (using TS)
@@ -18,145 +18,150 @@ function os.clock() return tonumber(_bllua_ts.call('getSimTime'))/1000 end
-- Can't read nulls, can't distinguish between CRLF and LF. -- Can't read nulls, can't distinguish between CRLF and LF.
-- Todo someday: actually read the zip in lua? -- Todo someday: actually read the zip in lua?
local file_meta = { local file_meta = {
read = function(file, mode) read = function(file, mode)
file:_init() file:_init()
if not file or type(file)~='table' or not file._is_file then error('File:read: Not a file', 2) end if not file or type(file) ~= 'table' or not file._is_file then error('File:read: Not a file', 2) end
if file._is_open ~= true then error('File:read: File is closed', 2) end if file._is_open ~= true then error('File:read: File is closed', 2) end
if mode=='*n' then if mode == '*n' then
local ws, n = file.data:match('^([ \t\r\n]*)([0-9%.%-e]+)', file.pos) local ws, n = file.data:match('^([ \t\r\n]*)([0-9%.%-e]+)', file.pos)
if n then if n then
file.pos = file.pos + #ws + #n file.pos = file.pos + #ws + #n
return n return n
else else
return nil return nil
end end
elseif mode=='*a' then elseif mode == '*a' then
local d = file.data:sub(file.pos, #file.data) local d = file.data:sub(file.pos, #file.data)
file.pos = #file.data + 1 file.pos = #file.data + 1
return d return d
elseif mode=='*l' then elseif mode == '*l' then
local l, ws = file.data:match('^([^\r\n]*)(\r?\n)', file.pos) local l, ws = file.data:match('^([^\r\n]*)(\r?\n)', file.pos)
if not l then if not l then
l = file.data:match('^([^\r\n]*)$', file.pos); ws = ''; l = file.data:match('^([^\r\n]*)$', file.pos); ws = '';
if l=='' then return nil end if l == '' then return nil end
end end
if l then if l then
file.pos = file.pos + #l + #ws file.pos = file.pos + #l + #ws
return l return l
else else
return nil return nil
end end
elseif type(mode)=='number' then elseif type(mode) == 'number' then
local d = file.data:sub(file.pos, file.pos+mode) local d = file.data:sub(file.pos, file.pos + mode)
file.pos = file.pos + #d file.pos = file.pos + #d
return d return d
else else
error('File:read: Invalid mode \''..mode..'\'', 2) error('File:read: Invalid mode \'' .. mode .. '\'', 2)
end end
end, end,
lines = function(file) lines = function(file)
file:_init() file:_init()
return function() return function()
return file:read('*l') return file:read('*l')
end end
end, end,
close = function(file) close = function(file)
if not file._is_open then error('File:close: File is not open', 2) end if not file._is_open then error('File:close: File is not open', 2) end
file._is_open = false file._is_open = false
end, end,
__index = function(f, k) return rawget(f, k) or getmetatable(f)[k] end, __index = function(f, k) return rawget(f, k) or getmetatable(f)[k] end,
_init = function(f) _init = function(f)
if not f.data then if not f.data then
f.data = _bllua_ts.call('_bllua_ReadEntireFile', f.filename) f.data = _bllua_ts.call('_bllua_ReadEntireFile', f.filename)
end end
end, end,
} }
local function new_file_obj(fn) local function new_file_obj(fn)
local file = { local file = {
_is_file = true, _is_file = true,
_is_open = true, _is_open = true,
pos = 1, pos = 1,
__index = file_meta.__index, __index = file_meta.__index,
filename = fn, filename = fn,
data = nil, data = nil,
} }
setmetatable(file, file_meta) setmetatable(file, file_meta)
return file return file
end end
local function tflip(t) local u = {}; for _, n in ipairs(t) do u[n] = true end; return u; end local function tflip(t)
local allowed_zip_dirs = tflip{ local u = {}; for _, n in ipairs(t) do u[n] = true end; return u;
'add-ons', 'base', 'config', 'saves', 'screenshots', 'shaders' end
local allowed_zip_dirs = tflip {
'add-ons', 'base', 'config', 'saves', 'screenshots', 'shaders'
} }
local function io_open_absolute(fn, mode) local function io_open_absolute(fn, mode)
-- if file exists, use original mode -- if file exists, use original mode
local res, err = _bllua_io_open(fn, mode) local res, err = _bllua_io_open(fn, mode)
if res then return res end if res then return res end
-- otherwise, if TS sees file but Lua doesn't, it must be in a zip, so use TS reader -- otherwise, if TS sees file but Lua doesn't, it must be in a zip, so use TS reader
local dir = fn:match('^[^/]+') local dir = fn:match('^[^/]+')
if not allowed_zip_dirs[dir:lower()] then return nil, 'File is not in one of the allowed directories' end if not allowed_zip_dirs[dir:lower()] then return nil, 'File is not in one of the allowed directories' end
local exist = _bllua_ts.call('isFile', fn) == '1' local exist = _bllua_ts.call('isFile', fn) == '1'
if not exist then return nil, err end if not exist then return nil, err end
if mode~=nil and mode~='r' and mode~='rb' then if mode ~= nil and mode ~= 'r' and mode ~= 'rb' then
return nil, 'Files in zips can only be opened in read mode' end return nil, 'Files in zips can only be opened in read mode'
end
-- return a temp lua file object with the data
local fi = new_file_obj(fn) -- return a temp lua file object with the data
return fi local fi = new_file_obj(fn)
return fi
end end
io = io or {} io = io or {}
---@diagnostic disable-next-line: duplicate-set-field ---@diagnostic disable-next-line: duplicate-set-field
function io.open(fn, mode, errn) function io.open(fn, mode, errn)
errn = errn or 1 errn = errn or 1
-- try to open the file with relative path, otherwise use absolute path -- try to open the file with relative path, otherwise use absolute path
local curfn = debug.getfilename(errn + 1) or _bllua_ts.getvar('Con::File') local curfn = debug.getfilename(errn + 1) or _bllua_ts.getvar('Con::File')
if curfn == '' then curfn = nil end if curfn == '' then curfn = nil end
if fn:find('^%.') then if fn:find('^%.') then
local relfn = curfn and fn:find('^%./') and local relfn = curfn and fn:find('^%./') and
curfn:gsub('[^/]+$', '')..fn:gsub('^%./', '') curfn:gsub('[^/]+$', '') .. fn:gsub('^%./', '')
if relfn then if relfn then
local fi, err = io_open_absolute(relfn, mode) local fi, err = io_open_absolute(relfn, mode)
return fi, err, relfn return fi, err, relfn
else else
return nil, 'Invalid path', fn return nil, 'Invalid path', fn
end end
else else
local fi, err = io_open_absolute(fn, mode) local fi, err = io_open_absolute(fn, mode)
return fi, err, fn return fi, err, fn
end end
end end
---@diagnostic disable-next-line: duplicate-set-field ---@diagnostic disable-next-line: duplicate-set-field
function io.lines(fn) function io.lines(fn)
local fi, err, fn2 = io.open(fn, nil, 2) local fi, err, fn2 = io.open(fn, nil, 2)
if not fi then error('Error opening file \''..fn2..'\': '..err, 2) end if not fi then error('Error opening file \'' .. fn2 .. '\': ' .. err, 2) end
return fi:lines() return fi:lines()
end end
---@diagnostic disable-next-line: duplicate-set-field ---@diagnostic disable-next-line: duplicate-set-field
function io.type(f) function io.type(f)
---@diagnostic disable-next-line: undefined-field ---@diagnostic disable-next-line: undefined-field
if type(f)=='table' and f._is_file then if type(f) == 'table' and f._is_file then
---@diagnostic disable-next-line: undefined-field ---@diagnostic disable-next-line: undefined-field
return f._is_open and 'file' or 'closed file' return f._is_open and 'file' or 'closed file'
else else
return _bllua_io_type(f) return _bllua_io_type(f)
end end
end end
-- provide dofile -- provide dofile
function dofile(fn, errn) function dofile(fn, errn)
errn = errn or 1 errn = errn or 1
local fi, err, fn2 = io.open(fn, 'r', errn+1) local fi, err, fn2 = io.open(fn, 'r', errn + 1)
if not fi then error('Error executing file \''..fn2..'\': '..err, errn+1) end if not fi then error('Error executing file \'' .. fn2 .. '\': ' .. err, errn + 1) end
print('Executing '..fn2) print('Executing ' .. fn2)
local text = fi:read('*a') local text = fi:read('*a')
fi:close() fi:close()
return assert(loadstring('--[['..fn2..']]'..text))() return assert(loadstring('--[[' .. fn2 .. ']]' .. text))()
end end
-- provide require (just a wrapper for dofile) -- provide require (just a wrapper for dofile)
@@ -165,63 +170,67 @@ end
-- blockland directory -- blockland directory
-- current add-on -- current add-on
local function file_exists(fn, errn) local function file_exists(fn, errn)
local fi, err, fn2 = io.open(fn, 'r', errn+1) local fi, err, fn2 = io.open(fn, 'r', errn + 1)
if fi then if fi then
fi:close() fi:close()
return fn2 return fn2
else else
return nil return nil
end end
end end
local require_memo = {} local require_memo = {}
function require(mod) function require(mod)
if require_memo[mod] then return unpack(require_memo[mod]) end if require_memo[mod] then return unpack(require_memo[mod]) end
local fp = mod:gsub('%.', '/') local fp = mod:gsub('%.', '/')
local fns = { local fns = {
'./'..fp..'.lua', -- local file './' .. fp .. '.lua', -- local file
'./'..fp..'/init.lua', -- local library './' .. fp .. '/init.lua', -- local library
fp..'.lua', -- global file fp .. '.lua', -- global file
fp..'/init.lua', -- global library fp .. '/init.lua', -- global library
} }
if fp:lower():find('^add-ons/') then if fp:lower():find('^add-ons/') then
local addonpath = fp:lower():match('^add-ons/[^/]+')..'/' local addonpath = fp:lower():match('^add-ons/[^/]+') .. '/'
table.insert(fns, addonpath..fp..'.lua') -- add-on file table.insert(fns, addonpath .. fp .. '.lua') -- add-on file
table.insert(fns, addonpath..fp..'/init.lua') -- add-on library table.insert(fns, addonpath .. fp .. '/init.lua') -- add-on library
end end
for _,fn in ipairs(fns) do for _, fn in ipairs(fns) do
local fne = file_exists(fn, 2) local fne = file_exists(fn, 2)
if fne then if fne then
local res = {dofile(fne, 2)} local res = { dofile(fne, 2) }
require_memo[mod] = res require_memo[mod] = res
return unpack(res) return unpack(res)
end end
end end
return _bllua_requiresecure(mod) return _bllua_requiresecure(mod)
end end
-- Exposure to TS -- Exposure to TS
function _bllua_getvar(name) return _G[name] end function _bllua_getvar(name) return _G[name] end
function _bllua_setvar(name, val) _G[name] = val end function _bllua_setvar(name, val) _G[name] = val end
function _bllua_eval(code) return loadstring(code)() end function _bllua_eval(code) return loadstring(code)() end
function _bllua_exec(fn) return dofile(fn, 2) end function _bllua_exec(fn) return dofile(fn, 2) end
local function isValidCode(code) local function isValidCode(code)
local f,e = loadstring(code) local f, e = loadstring(code)
return f~=nil return f ~= nil
end end
function _bllua_smarteval(code) function _bllua_smarteval(code)
if (not code:find('^print%(')) and isValidCode('print('..code..')') then if (not code:find('^print%(')) and isValidCode('print(' .. code .. ')') then
code = 'print('..code..')' end code = 'print(' .. code .. ')'
local f,e = loadstring(code) end
if f then local f, e = loadstring(code)
return f() if f then
else return f()
print(e) else
end print(e)
end
end end
function ts.setvar(name, val) function ts.setvar(name, val)
_bllua_ts.call('_bllua_set_var', name, val) _bllua_ts.call('_bllua_set_var', name, val)
end end
_bllua_ts.call('echo', ' Executed libts-lua.lua') _bllua_ts.call('echo', ' Executed libts-lua.lua')

View File

@@ -1,347 +1,374 @@
-- Basic functionality that should be standard in Lua -- Basic functionality that should be standard in Lua
-- Table / List -- Table / List
-- Whether a table contains no keys -- Whether a table contains no keys
function table.empty(t) function table.empty(t)
return next(t)~=nil return next(t) ~= nil
end end
-- Apply a function to each key in a table -- Apply a function to each key in a table
function table.map(f, ...) function table.map(f, ...)
local ts = {...} local ts = { ... }
local u = {} local u = {}
for k,_ in pairs(ts[1]) do for k, _ in pairs(ts[1]) do
local args = {} local args = {}
for j=1,#ts do args[j] = ts[j][i] end for j = 1, #ts do args[j] = ts[j][i] end
u[i] = f(unpack(args)) u[i] = f(unpack(args))
end end
return u return u
end end
function table.map_list(f, ...) function table.map_list(f, ...)
local ts = {...} local ts = { ... }
local u = {} local u = {}
for i=1,#ts[1] do for i = 1, #ts[1] do
local args = {} local args = {}
for j=1,#ts do args[j] = ts[j][i] end for j = 1, #ts do args[j] = ts[j][i] end
u[i] = f(unpack(args)) u[i] = f(unpack(args))
end end
return u return u
end end
-- Swap keys/values -- Swap keys/values
function table.swap(t) function table.swap(t)
local u = {} local u = {}
for k,v in pairs(t) do u[v] = k end for k, v in pairs(t) do u[v] = k end
return u return u
end end
-- Reverse a list -- Reverse a list
function table.reverse(l) function table.reverse(l)
local m = {} local m = {}
for i=1,#l do m[#l-i+1] = l[i] end for i = 1, #l do m[#l - i + 1] = l[i] end
return m return m
end end
-- Whether a table is a list/array (has only monotonic integer keys) -- Whether a table is a list/array (has only monotonic integer keys)
function table.islist(t) function table.islist(t)
local n = 0 local n = 0
for i,_ in pairs(t) do for i, _ in pairs(t) do
if type(i)~='number' or i%1~=0 then return false end if type(i) ~= 'number' or i % 1 ~= 0 then return false end
n = n+1 n = n + 1
end end
return n==#t return n == #t
end end
-- Append contents of other tables to first table -- Append contents of other tables to first table
function table.append(t, ...) function table.append(t, ...)
local a = {...} local a = { ... }
for _,u in ipairs(a) do for _, u in ipairs(a) do
for _,v in ipairs(u) do table.insert(t,v) end for _, v in ipairs(u) do table.insert(t, v) end
end end
return t return t
end end
-- Create a new table containing all keys from any number of tables -- Create a new table containing all keys from any number of tables
-- latter tables in the arg list override prior ones -- latter tables in the arg list override prior ones
-- overlaps, NOT appends, integer keys -- overlaps, NOT appends, integer keys
function table.join(...) function table.join(...)
local ts = {...} local ts = { ... }
local w = {} local w = {}
for _,t in ipairs(ts) do for _, t in ipairs(ts) do
for k,v in pairs(t) do w[k] = v end for k, v in pairs(t) do w[k] = v end
end end
return w return w
end end
-- Whether a table contains a certain value in any key -- Whether a table contains a certain value in any key
function table.contains(t,s) function table.contains(t, s)
for _,v in pairs(t) do for _, v in pairs(t) do
if v==s then return true end if v == s then return true end
end end
return false return false
end end
function table.contains_list(t,s)
for _,v in ipairs(t) do function table.contains_list(t, s)
if v==s then return true end for _, v in ipairs(t) do
end if v == s then return true end
return false end
return false
end end
-- Copy a table to another table -- Copy a table to another table
function table.copy(t) function table.copy(t)
local u = {} local u = {}
for k,v in pairs(t) do u[k] = v end for k, v in pairs(t) do u[k] = v end
return u return u
end end
function table.copy_list(l) function table.copy_list(l)
local m = {} local m = {}
for i,v in ipairs(l) do m[i] = v end for i, v in ipairs(l) do m[i] = v end
return m return m
end end
-- Sort a table in a new copy -- Sort a table in a new copy
function table.sortcopy(t, f) function table.sortcopy(t, f)
local u = table.copy_list(t) local u = table.copy_list(t)
table.sort(u, f) table.sort(u, f)
return u return u
end end
-- Remove a value from a table -- Remove a value from a table
function table.removevalue(t, r) function table.removevalue(t, r)
local rem = {} local rem = {}
for k,v in pairs(t) do for k, v in pairs(t) do
if v==r then table.insert(rem, k) end if v == r then table.insert(rem, k) end
end end
for _,k in ipairs(rem) do t[k] = nil end for _, k in ipairs(rem) do t[k] = nil end
end end
function table.removevalue_list(t, r) function table.removevalue_list(t, r)
for i = #t, 1, -1 do for i = #t, 1, -1 do
if t[i]==r then if t[i] == r then
table.remove(t, i) table.remove(t, i)
end end
end end
end end
-- Export tables into formatted executable strings -- Export tables into formatted executable strings
local function tabs(tabLevel) local function tabs(tabLevel)
return (' '):rep(tabLevel) return (' '):rep(tabLevel)
end end
local valueToString local valueToString
local function tableToString(t, tabLevel, seen) local function tableToString(t, tabLevel, seen)
if type(t)~='table' or (getmetatable(t) and getmetatable(t).__tostring) then if type(t) ~= 'table' or (getmetatable(t) and getmetatable(t).__tostring) then
return tostring(t) return tostring(t)
elseif table.islist(t) then elseif table.islist(t) then
if #t==0 then if #t == 0 then
return '{}' return '{}'
else else
local strs = {} local strs = {}
local containsTables = false local containsTables = false
for _,v in ipairs(t) do for _, v in ipairs(t) do
if type(v)=='table' then containsTables = true end if type(v) == 'table' then containsTables = true end
table.insert(strs, valueToString(v, tabLevel+1, seen)..',') table.insert(strs, valueToString(v, tabLevel + 1, seen) .. ',')
end end
if containsTables or #t>3 then if containsTables or #t > 3 then
return '{\n'..tabs(tabLevel+1) return '{\n' .. tabs(tabLevel + 1)
..table.concat(strs, '\n'..tabs(tabLevel+1)) .. table.concat(strs, '\n' .. tabs(tabLevel + 1))
..'\n'..tabs(tabLevel)..'}' .. '\n' .. tabs(tabLevel) .. '}'
else else
return '{ '..table.concat(strs, ' ')..' }' return '{ ' .. table.concat(strs, ' ') .. ' }'
end end
end end
else else
local containsNonStringKeys = false local containsNonStringKeys = false
for k,v in pairs(t) do for k, v in pairs(t) do
if type(k)~='string' or k:find('[^a-zA-Z0-9_]') then if type(k) ~= 'string' or k:find('[^a-zA-Z0-9_]') then
containsNonStringKeys = true containsNonStringKeys = true
elseif type(k)=='table' then elseif type(k) == 'table' then
error('table.tostring: table contains a table as key, cannot serialize') error('table.tostring: table contains a table as key, cannot serialize')
end end
end end
local strs = {} local strs = {}
if containsNonStringKeys then if containsNonStringKeys then
for k,v in pairs(t) do for k, v in pairs(t) do
table.insert(strs, '\n'..tabs(tabLevel+1) table.insert(strs, '\n' .. tabs(tabLevel + 1)
..'['..valueToString(k, tabLevel+1, seen)..'] = ' .. '[' .. valueToString(k, tabLevel + 1, seen) .. '] = '
..valueToString(v, tabLevel+1, seen)..',') .. valueToString(v, tabLevel + 1, seen) .. ',')
end end
else else
for k,v in pairs(t) do for k, v in pairs(t) do
table.insert(strs, '\n'..tabs(tabLevel+1) table.insert(strs, '\n' .. tabs(tabLevel + 1)
..k..' = '..valueToString(v, tabLevel+1, seen)..',') .. k .. ' = ' .. valueToString(v, tabLevel + 1, seen) .. ',')
end end
end end
return '{'..table.concat(strs)..'\n'..tabs(tabLevel)..'}' return '{' .. table.concat(strs) .. '\n' .. tabs(tabLevel) .. '}'
end end
end end
valueToString = function(v, tabLevel, seen) valueToString = function(v, tabLevel, seen)
local t = type(v) local t = type(v)
if t=='table' then if t == 'table' then
if seen[v] then if seen[v] then
return 'nil --[[ already seen: '..tostring(v)..' ]]' return 'nil --[[ already seen: ' .. tostring(v) .. ' ]]'
else else
seen[v] = true seen[v] = true
return tableToString(v, tabLevel, seen) return tableToString(v, tabLevel, seen)
end end
elseif t=='string' then elseif t == 'string' then
return '\''..string.escape(v)..'\'' return '\'' .. string.escape(v) .. '\''
elseif t=='number' or t=='boolean' then elseif t == 'number' or t == 'boolean' then
return tostring(v) return tostring(v)
else else
--error('table.tostring: table contains a '..t..' value, cannot serialize') --error('table.tostring: table contains a '..t..' value, cannot serialize')
return 'nil --[[ cannot serialize '..t..': '..tostring(v)..' ]]' return 'nil --[[ cannot serialize ' .. t .. ': ' .. tostring(v) .. ' ]]'
end end
end end
function table.tostring(t) function table.tostring(t)
return tableToString(t, 0, {}) return tableToString(t, 0, {})
end end
-- String -- String
-- Split string into table by separator -- Split string into table by separator
-- or by chars if no separator given -- or by chars if no separator given
-- if regex is not true, sep is treated as a regex pattern -- if regex is not true, sep is treated as a regex pattern
function string.split(str, sep, noregex) function string.split(str, sep, noregex)
if type(str)~='string' then if type(str) ~= 'string' then
error('string.split: argument #1: expected string, got '..type(str), 2) end error('string.split: argument #1: expected string, got ' .. type(str), 2)
if sep==nil or sep=='' then end
local t = {} if sep == nil or sep == '' then
local ns = #str local t = {}
for x = 1, ns do local ns = #str
table.insert(t, str:sub(x, x)) for x = 1, ns do
end table.insert(t, str:sub(x, x))
return t end
elseif type(sep)=='string' then return t
local t = {} elseif type(sep) == 'string' then
if #str>0 then local t = {}
local first = 1 if #str > 0 then
while true do local first = 1
local last, newfirst = str:find(sep, first, noregex) while true do
if not last then break end local last, newfirst = str:find(sep, first, noregex)
table.insert(t, str:sub(first, last-1)) if not last then break end
first = newfirst+1 table.insert(t, str:sub(first, last - 1))
end first = newfirst + 1
table.insert(t, str:sub(first, #str)) end
end table.insert(t, str:sub(first, #str))
return t end
else return t
error( else
'string.split: argument #2: expected string or nil, got '..type(sep), 2) error(
end 'string.split: argument #2: expected string or nil, got ' .. type(sep), 2)
end end
-- Split string to a list of char bytes
function string.bytes(s)
local b = {}
for i=1,#s do
local c = s:sub(i,i)
table.insert(b, c:byte())
end
return b
end
-- Trim leading and trailing whitespace
function string.trim(s, ws)
ws = ws or ' \t\r\n'
return s:gsub('^['..ws..']+', ''):gsub('['..ws..']+$', '')..''
end
-- String slicing and searching using [] operator
local str_meta = getmetatable('')
local str_meta_index_old= str_meta.__index
function str_meta.__index(s,k)
if type(k)=='string' then
return str_meta_index_old[k]
elseif type(k)=='number' then
if k<0 then k = #s+k+1 end
return string.sub(s,k,k)
elseif type(k)=='table' then
local a = k[1]<0 and (#s+k[1]+1) or k[1]
local b = k[2]<0 and (#s+k[2]+1) or k[2]
return string.sub(s,a,b)
end
end
-- String iterator
function string.chars(s)
local i = 0
return function()
i = i+1
if i<=#s then return s:sub(i,i)
else return nil end
end
end
-- Escape sequences
local defaultEscapes = {
['\\'] = '\\\\',
['\''] = '\\\'',
['\"'] = '\\\"',
['\t'] = '\\t',
['\r'] = '\\r',
['\n'] = '\\n',
['\0'] = '\\0',
}
function string.escape(s, escapes)
escapes = escapes or defaultEscapes
local t = {}
for i=1,#s do
local c = s:sub(i,i)
table.insert(t, escapes[c] or c)
end
return table.concat(t)
end
local defaultEscapeChar = '\\'
local defaultUnescapes = {
['\\'] = '\\',
['\''] = '\'',
['\"'] = '\"',
['t'] = '\t',
['r'] = '\r',
['n'] = '\n',
['0'] = '\0',
}
function string.unescape(s, escapeChar, unescapes)
escapeChar = escapeChar or defaultEscapeChar
unescapes = unescapes or defaultUnescapes
local t = {}
local inEscape = false
for i=1,#s do
local c = s:sub(i,i)
if inEscape then
table.insert(t, unescapes[c]
or error('string.unescape: invalid escape sequence: \''
..escapeChar..c..'\''))
elseif c==escapeChar then
inEscape = true
else
table.insert(t, c)
end
end
return table.concat(t)
end end
-- Split string to a list of char bytes
function string.bytes(s)
local b = {}
for i = 1, #s do
local c = s:sub(i, i)
table.insert(b, c:byte())
end
return b
end
-- Trim leading and trailing whitespace
function string.trim(s, ws)
ws = ws or ' \t\r\n'
return s:gsub('^[' .. ws .. ']+', ''):gsub('[' .. ws .. ']+$', '') .. ''
end
-- String slicing and searching using [] operator
local str_meta = getmetatable('')
local str_meta_index_old = str_meta.__index
function str_meta.__index(s, k)
if type(k) == 'string' then
return str_meta_index_old[k]
elseif type(k) == 'number' then
if k < 0 then k = #s + k + 1 end
return string.sub(s, k, k)
elseif type(k) == 'table' then
local a = k[1] < 0 and (#s + k[1] + 1) or k[1]
local b = k[2] < 0 and (#s + k[2] + 1) or k[2]
return string.sub(s, a, b)
end
end
-- String iterator
function string.chars(s)
local i = 0
return function()
i = i + 1
if i <= #s then
return s:sub(i, i)
else
return nil
end
end
end
-- Escape sequences
local defaultEscapes = {
['\\'] = '\\\\',
['\''] = '\\\'',
['\"'] = '\\\"',
['\t'] = '\\t',
['\r'] = '\\r',
['\n'] = '\\n',
['\0'] = '\\0',
}
function string.escape(s, escapes)
escapes = escapes or defaultEscapes
local t = {}
for i = 1, #s do
local c = s:sub(i, i)
table.insert(t, escapes[c] or c)
end
return table.concat(t)
end
local defaultEscapeChar = '\\'
local defaultUnescapes = {
['\\'] = '\\',
['\''] = '\'',
['\"'] = '\"',
['t'] = '\t',
['r'] = '\r',
['n'] = '\n',
['0'] = '\0',
}
function string.unescape(s, escapeChar, unescapes)
escapeChar = escapeChar or defaultEscapeChar
unescapes = unescapes or defaultUnescapes
local t = {}
local inEscape = false
for i = 1, #s do
local c = s:sub(i, i)
if inEscape then
table.insert(t, unescapes[c]
or error('string.unescape: invalid escape sequence: \''
.. escapeChar .. c .. '\''))
elseif c == escapeChar then
inEscape = true
else
table.insert(t, c)
end
end
return table.concat(t)
end
-- IO -- IO
io = io or {} io = io or {}
-- Read entire file at once, return nil,err if access failed -- Read entire file at once, return nil,err if access failed
function io.readall(filename) function io.readall(filename)
local fi,err = io.open(filename, 'rb') local fi, err = io.open(filename, 'rb')
if not fi then return nil,err end if not fi then return nil, err end
local s = fi:read("*a") local s = fi:read("*a")
fi:close() fi:close()
return s return s
end
-- Write data to file all at once, return true if success / false,err if failure
function io.writeall(filename, data)
local fi,err = io.open(filename, 'wb')
if not fi then return false,err end
fi:write(data)
fi:close()
return true,nil
end end
-- Write data to file all at once, return true if success / false,err if failure
function io.writeall(filename, data)
local fi, err = io.open(filename, 'wb')
if not fi then return false, err end
fi:write(data)
fi:close()
return true, nil
end
-- Math -- Math
-- Round -- Round
function math.round(x) function math.round(x)
return math.floor(x+0.5) return math.floor(x + 0.5)
end end
-- Mod that accounts for floating point inaccuracy -- Mod that accounts for floating point inaccuracy
function math.mod(a,b) function math.mod(a, b)
local m = a%b local m = a % b
if m==0 or math.abs(m)<1e-15 or math.abs(m-b)<1e-15 then return 0 if m == 0 or math.abs(m) < 1e-15 or math.abs(m - b) < 1e-15 then
else return m end return 0
else
return m
end
end end
-- Clamp value between min and max -- Clamp value between min and max
function math.clamp(v, n, x) function math.clamp(v, n, x)
return math.min(x, math.max(v, n)) return math.min(x, math.max(v, n))
end end

View File

@@ -1,218 +1,237 @@
-- Vector math class with operators -- Vector math class with operators
local vector_meta local vector_meta
local vector_new local vector_new
local function vector_check(v, n, name, argn) local function vector_check(v, n, name, argn)
if not v.__is_vector then if not v.__is_vector then
error('vector '..name..': argument #'..(argn or 1) error('vector ' .. name .. ': argument #' .. (argn or 1)
..': expected vector, got '..type(v), n+1) end .. ': expected vector, got ' .. type(v), n + 1)
end
end end
local function vector_checksamelen(v1, v2, name) local function vector_checksamelen(v1, v2, name)
vector_check(v1, 3, name, 1) vector_check(v1, 3, name, 1)
vector_check(v2, 3, name, 2) vector_check(v2, 3, name, 2)
if #v1~=#v2 then if #v1 ~= #v2 then
error('vector '..name..': vector lengths do not match (lengths are ' error('vector ' .. name .. ': vector lengths do not match (lengths are '
..#v1..' and '..#v2..')', 3) end .. #v1 .. ' and ' .. #v2 .. ')', 3)
return #v1 end
return #v1
end end
local function vector_checklen(v1, v2, name, len) local function vector_checklen(v1, v2, name, len)
vector_check(v1, 3, name, 1) vector_check(v1, 3, name, 1)
vector_check(v2, 3, name, 2) vector_check(v2, 3, name, 2)
if #v1~=len or #v2~=len then if #v1 ~= len or #v2 ~= len then
error('vector '..name..': vector lengths are not '..len..' (lengths are ' error('vector ' .. name .. ': vector lengths are not ' .. len .. ' (lengths are '
..#v1..' and '..#v2..')', 3) end .. #v1 .. ' and ' .. #v2 .. ')', 3)
end
end end
local function vector_opnnn(name, op) local function vector_opnnn(name, op)
return function(v1, v2) return function(v1, v2)
local len = vector_checksamelen(v1, v2, name) local len = vector_checksamelen(v1, v2, name)
local v3 = {} local v3 = {}
for i = 1, len do for i = 1, len do
v3[i] = op(v1[i], v2[i]) v3[i] = op(v1[i], v2[i])
end end
return vector_new(v3) return vector_new(v3)
end end
end end
local function vector_opnxn(name, op) local function vector_opnxn(name, op)
return function(v1, v2) return function(v1, v2)
local v1v = type(v1)=='table' and v1.__is_vector local v1v = type(v1) == 'table' and v1.__is_vector
local v2v = type(v2)=='table' and v2.__is_vector local v2v = type(v2) == 'table' and v2.__is_vector
if v1v and v2v then if v1v and v2v then
local len = vector_checksamelen(v1, v2, name) local len = vector_checksamelen(v1, v2, name)
local v3 = {} local v3 = {}
for i = 1, len do for i = 1, len do
v3[i] = op(v1[i], v2[i]) v3[i] = op(v1[i], v2[i])
end end
return vector_new(v3) return vector_new(v3)
else else
if v2v then v1,v2 = v2,v1 end if v2v then v1, v2 = v2, v1 end
local len = #v1 local len = #v1
local v3 = {} local v3 = {}
for i = 1, len do for i = 1, len do
v3[i] = op(v1[i], v2) v3[i] = op(v1[i], v2)
end end
return vector_new(v3) return vector_new(v3)
end end
end end
end end
local function vector_opn0n(name, op) local function vector_opn0n(name, op)
return function(v1) return function(v1)
--vector_check(v1, 1, name) --vector_check(v1, 1, name)
local len = #v1 local len = #v1
local v2 = {} local v2 = {}
for i = 1, len do for i = 1, len do
v2[i] = op(v1[i]) v2[i] = op(v1[i])
end end
return vector_new(v2) return vector_new(v2)
end end
end end
local vector_indices = {x = 1, y = 2, z = 3, w = 4, r = 1, g = 2, b = 3, a = 4} local vector_indices = { x = 1, y = 2, z = 3, w = 4, r = 1, g = 2, b = 3, a = 4 }
local vector_meta = { local vector_meta = {
__is_vector = true, __is_vector = true,
__index = function(t, k) __index = function(t, k)
if tonumber(k) then return rawget(t, k) if tonumber(k) then
elseif vector_indices[k] then return rawget(t, vector_indices[k]) return rawget(t, k)
else return getmetatable(t)[k] elseif vector_indices[k] then
end return rawget(t, vector_indices[k])
end, else
__newindex = function(t, k, v) return getmetatable(t)[k]
if tonumber(k) then rawset(t, k, v) end
elseif vector_indices[k] then rawset(t, vector_indices[k], v) end,
else return __newindex = function(t, k, v)
end if tonumber(k) then
end, rawset(t, k, v)
__add = vector_opnnn('add', function(x1, x2) return x1+x2 end), elseif vector_indices[k] then
__sub = vector_opnnn('sub', function(x1, x2) return x1-x2 end), rawset(t, vector_indices[k], v)
__mul = vector_opnxn('mul', function(x1, x2) return x1*x2 end), else
__div = vector_opnxn('div', function(x1, x2) return x1/x2 end), return
__pow = vector_opnxn('pow', function(x1, x2) return x1^x2 end), end
__unm = vector_opn0n('inv', function(x1) return -x1 end), end,
__concat = nil, __add = vector_opnnn('add', function(x1, x2) return x1 + x2 end),
--__len = function(v1) return #v1 end, __sub = vector_opnnn('sub', function(x1, x2) return x1 - x2 end),
__len = nil, __mul = vector_opnxn('mul', function(x1, x2) return x1 * x2 end),
__eq = function(v1, v2) __div = vector_opnxn('div', function(x1, x2) return x1 / x2 end),
local len = vector_checksamelen(v1, v2, 'equals') __pow = vector_opnxn('pow', function(x1, x2) return x1 ^ x2 end),
for i = 1, len do __unm = vector_opn0n('inv', function(x1) return -x1 end),
if v1[i]~=v2[i] then return false end __concat = nil,
end --__len = function(v1) return #v1 end,
return true __len = nil,
end, __eq = function(v1, v2)
__lt = nil, local len = vector_checksamelen(v1, v2, 'equals')
__le = nil, for i = 1, len do
__call = nil, if v1[i] ~= v2[i] then return false end
abs = vector_opn0n('abs', math.abs), end
length = function(v1) return true
--vector_check(v1, 2, 'length') end,
local len = #v1 __lt = nil,
local l = 0 __le = nil,
for i = 1, len do __call = nil,
l = l + v1[i]^2 abs = vector_opn0n('abs', math.abs),
end length = function(v1)
return math.sqrt(l) --vector_check(v1, 2, 'length')
end, local len = #v1
normalize = function(v1) local l = 0
--vector_check(v1, 2, 'normal') for i = 1, len do
local length = v1:length() l = l + v1[i] ^ 2
local len = #v1 end
local v3 = {} return math.sqrt(l)
for i = 1, len do end,
if length==0 then v3[i] = 0 normalize = function(v1)
else v3[i] = v1[i]/length end --vector_check(v1, 2, 'normal')
end local length = v1:length()
return vector_new(v3) local len = #v1
end, local v3 = {}
__tostring = function(v1) for i = 1, len do
--vector_check(v1, 2, 'tostring') if length == 0 then
local st = {} v3[i] = 0
local len = #v1 else
for i = 1, len do v3[i] = v1[i] / length
table.insert(st, tostring(v1[i])) end
end end
return 'vector{ '..table.concat(st, ', ')..' }' return vector_new(v3)
end, end,
unpack = function(v1) return unpack(v1) end, __tostring = function(v1)
floor = vector_opn0n('floor', function(x1) return math.floor(x1) end), --vector_check(v1, 2, 'tostring')
ceil = vector_opn0n('ceil' , function(x1) return math.ceil (x1) end), local st = {}
round = vector_opn0n('round', function(x1) return math.floor(x1+0.5) end), local len = #v1
dot = function(v1, v2) for i = 1, len do
local len = vector_checksamelen(v1, v2, 'dot') table.insert(st, tostring(v1[i]))
local x = 0 end
for i = 1, len do return 'vector{ ' .. table.concat(st, ', ') .. ' }'
x = x + v1[i]*v2[i] end,
end unpack = function(v1) return unpack(v1) end,
return x floor = vector_opn0n('floor', function(x1) return math.floor(x1) end),
end, ceil = vector_opn0n('ceil', function(x1) return math.ceil(x1) end),
cross = function(v1, v2) round = vector_opn0n('round', function(x1) return math.floor(x1 + 0.5) end),
vector_checklen(v1, v2, 'cross', 3) dot = function(v1, v2)
return vector_new{ local len = vector_checksamelen(v1, v2, 'dot')
v1[2]*v2[3] - v1[3]*v2[2], local x = 0
v1[3]*v2[1] - v1[1]*v2[3], for i = 1, len do
v1[1]*v2[2] - v1[2]*v2[1], x = x + v1[i] * v2[i]
} end
end, return x
rotateByAngleId = function(v1, r) end,
--vector_check(v1, 2, 'rotate') cross = function(v1, v2)
if type(r)~='number' or r%1~=0 then vector_checklen(v1, v2, 'cross', 3)
error('vector rotateByAngleId: invalid rotation '..tostring(r), 2) end return vector_new {
r = r%4 v1[2] * v2[3] - v1[3] * v2[2],
local v2 v1[3] * v2[1] - v1[1] * v2[3],
if r==0 then v2 = vector_new{ v1[1], v1[2], v1[3] } v1[1] * v2[2] - v1[2] * v2[1],
elseif r==1 then v2 = vector_new{ v1[2], -v1[1], v1[3] } }
elseif r==2 then v2 = vector_new{ -v1[1], -v1[2], v1[3] } end,
elseif r==3 then v2 = vector_new{ -v1[2], v1[1], v1[3] } rotateByAngleId = function(v1, r)
else error('vector rotateByAngleId: invalid rotation '..r, 2) end --vector_check(v1, 2, 'rotate')
return v2 if type(r) ~= 'number' or r % 1 ~= 0 then
end, error('vector rotateByAngleId: invalid rotation ' .. tostring(r), 2)
rotateZ = function(v, r) end
--vector_check(v, 2, 'rotate2d') r = r % 4
if type(r)~='number' then local v2
error('vector rotateZ: invalid rotation '..tostring(r), 2) end if r == 0 then
local len = math.sqrt(v[1]^2 + v[2]^2) v2 = vector_new { v1[1], v1[2], v1[3] }
local ang = math.atan2(v[2], v[1]) + r elseif r == 1 then
local v2 = vector_new{ math.cos(ang)*len, math.sin(ang)*len } v2 = vector_new { v1[2], -v1[1], v1[3] }
return v2 elseif r == 2 then
end, v2 = vector_new { -v1[1], -v1[2], v1[3] }
tsString = function(v) elseif r == 3 then
--vector_check(v, 2, 'tsString') v2 = vector_new { -v1[2], v1[1], v1[3] }
return table.concat(v, ' ') else
end, error('vector rotateByAngleId: invalid rotation ' .. r, 2)
distance = function(v1, v2) end
local len = vector_checksamelen(v1, v2, 'distance') return v2
local sum = 0 end,
for i=1,len do rotateZ = function(v, r)
sum = sum + (v1[i] - v2[i])^2 --vector_check(v, 2, 'rotate2d')
end if type(r) ~= 'number' then
return math.sqrt(sum) error('vector rotateZ: invalid rotation ' .. tostring(r), 2)
end, end
copy = function(v) local len = math.sqrt(v[1] ^ 2 + v[2] ^ 2)
--vector_check(v, 2, 'copy') local ang = math.atan2(v[2], v[1]) + r
return vector_new(v) local v2 = vector_new { math.cos(ang) * len, math.sin(ang) * len }
end, return v2
end,
tsString = function(v)
--vector_check(v, 2, 'tsString')
return table.concat(v, ' ')
end,
distance = function(v1, v2)
local len = vector_checksamelen(v1, v2, 'distance')
local sum = 0
for i = 1, len do
sum = sum + (v1[i] - v2[i]) ^ 2
end
return math.sqrt(sum)
end,
copy = function(v)
--vector_check(v, 2, 'copy')
return vector_new(v)
end,
} }
vector_new = function(vi) vector_new = function(vi)
if vi then if vi then
if type(vi)=='string' then if type(vi) == 'string' then
local vi2 = {} local vi2 = {}
for val in vi:gmatch('[0-9%.%-e]+') do for val in vi:gmatch('[0-9%.%-e]+') do
table.insert(vi2, tonumber(val)) table.insert(vi2, tonumber(val))
end end
vi = vi2 vi = vi2
elseif type(vi)~='table' then elseif type(vi) ~= 'table' then
error('vector: argument #1: expected input table, got '..type(vi), 2) error('vector: argument #1: expected input table, got ' .. type(vi), 2)
end end
local v = {} local v = {}
if #vi>0 then if #vi > 0 then
for i = 1, #vi do v[i] = vi[i] end for i = 1, #vi do v[i] = vi[i] end
else else
for n, i in pairs(vector_indices) do v[i] = vi[n] end for n, i in pairs(vector_indices) do v[i] = vi[n] end
if #v==0 then if #v == 0 then
error('vector: argument #1: table contains no values', 2) error('vector: argument #1: table contains no values', 2)
end end
end end
setmetatable(v, vector_meta) setmetatable(v, vector_meta)
return v return v
else else
error('vector: argument #1: expected input table, got nil', 2) error('vector: argument #1: expected input table, got nil', 2)
end end
end end
vector = vector_new vector = vector_new