forked from redo/BlockLua
		
	format
This commit is contained in:
		
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,3 +1 @@ | ||||
| .* | ||||
| !.gitignore | ||||
| build/ | ||||
|   | ||||
							
								
								
									
										369
									
								
								lualib/ltn12.lua
									
									
									
									
									
								
							
							
						
						
									
										369
									
								
								lualib/ltn12.lua
									
									
									
									
									
								
							| @@ -27,43 +27,50 @@ _VERSION = "LTN12 1.0.1" | ||||
| ----------------------------------------------------------------------------- | ||||
| -- returns a high level filter that cycles a low-level filter | ||||
| function filter.cycle(low, ctx, extra) | ||||
|     base.assert(low) | ||||
|     return function(chunk) | ||||
|         local ret | ||||
|         ret, ctx = low(ctx, chunk, extra) | ||||
|         return ret | ||||
|     end | ||||
|   base.assert(low) | ||||
|   return function(chunk) | ||||
|     local ret | ||||
|     ret, ctx = low(ctx, chunk, extra) | ||||
|     return ret | ||||
|   end | ||||
| end | ||||
|  | ||||
| -- chains a bunch of filters together | ||||
| -- (thanks to Wim Couwenberg) | ||||
| function filter.chain(...) | ||||
|     local n = #arg | ||||
|     local top, index = 1, 1 | ||||
|     local retry = "" | ||||
|     return function(chunk) | ||||
|         retry = chunk and retry | ||||
|         while true do | ||||
|             if index == top then | ||||
|                 chunk = arg[index](chunk) | ||||
|                 if chunk == "" or top == n then return chunk | ||||
|                 elseif chunk then index = index + 1 | ||||
|                 else | ||||
|                     top = top+1 | ||||
|                     index = top | ||||
|                 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 | ||||
|   local n = #arg | ||||
|   local top, index = 1, 1 | ||||
|   local retry = "" | ||||
|   return function(chunk) | ||||
|     retry = chunk and retry | ||||
|     while true do | ||||
|       if index == top then | ||||
|         chunk = arg[index](chunk) | ||||
|         if chunk == "" or top == n then | ||||
|           return chunk | ||||
|         elseif chunk then | ||||
|           index = index + 1 | ||||
|         else | ||||
|           top = top + 1 | ||||
|           index = top | ||||
|         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 | ||||
|  | ||||
| ----------------------------------------------------------------------------- | ||||
| @@ -71,130 +78,143 @@ end | ||||
| ----------------------------------------------------------------------------- | ||||
| -- create an empty source | ||||
| local function empty() | ||||
|     return nil | ||||
|   return nil | ||||
| end | ||||
|  | ||||
| function source.empty() | ||||
|     return empty | ||||
|   return empty | ||||
| end | ||||
|  | ||||
| -- returns a source that just outputs an error | ||||
| function source.error(err) | ||||
|     return function() | ||||
|         return nil, err | ||||
|     end | ||||
|   return function() | ||||
|     return nil, err | ||||
|   end | ||||
| end | ||||
|  | ||||
| -- creates a file source | ||||
| function source.file(handle, io_err) | ||||
|     if handle then | ||||
|         return function() | ||||
|             local chunk = handle:read(BLOCKSIZE) | ||||
|             if not chunk then handle:close() end | ||||
|             return chunk | ||||
|         end | ||||
|     else return source.error(io_err or "unable to open file") end | ||||
|   if handle then | ||||
|     return function() | ||||
|       local chunk = handle:read(BLOCKSIZE) | ||||
|       if not chunk then handle:close() end | ||||
|       return chunk | ||||
|     end | ||||
|   else | ||||
|     return source.error(io_err or "unable to open file") | ||||
|   end | ||||
| end | ||||
|  | ||||
| -- turns a fancy source into a simple source | ||||
| function source.simplify(src) | ||||
|     base.assert(src) | ||||
|     return function() | ||||
|         local chunk, err_or_new = src() | ||||
|         src = err_or_new or src | ||||
|         if not chunk then return nil, err_or_new | ||||
|         else return chunk end | ||||
|   base.assert(src) | ||||
|   return function() | ||||
|     local chunk, err_or_new = src() | ||||
|     src = err_or_new or src | ||||
|     if not chunk then | ||||
|       return nil, err_or_new | ||||
|     else | ||||
|       return chunk | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| -- creates string source | ||||
| function source.string(s) | ||||
|     if s then | ||||
|         local i = 1 | ||||
|         return function() | ||||
|             local chunk = string.sub(s, i, i+BLOCKSIZE-1) | ||||
|             i = i + BLOCKSIZE | ||||
|             if chunk ~= "" then return chunk | ||||
|             else return nil end | ||||
|         end | ||||
|     else return source.empty() end | ||||
|   if s then | ||||
|     local i = 1 | ||||
|     return function() | ||||
|       local chunk = string.sub(s, i, i + BLOCKSIZE - 1) | ||||
|       i = i + BLOCKSIZE | ||||
|       if chunk ~= "" then | ||||
|         return chunk | ||||
|       else | ||||
|         return nil | ||||
|       end | ||||
|     end | ||||
|   else | ||||
|     return source.empty() | ||||
|   end | ||||
| end | ||||
|  | ||||
| -- creates rewindable source | ||||
| function source.rewind(src) | ||||
|     base.assert(src) | ||||
|     local t = {} | ||||
|     return function(chunk) | ||||
|         if not chunk then | ||||
|             chunk = table.remove(t) | ||||
|             if not chunk then return src() | ||||
|             else return chunk end | ||||
|         else | ||||
|             table.insert(t, chunk) | ||||
|         end | ||||
|   base.assert(src) | ||||
|   local t = {} | ||||
|   return function(chunk) | ||||
|     if not chunk then | ||||
|       chunk = table.remove(t) | ||||
|       if not chunk then | ||||
|         return src() | ||||
|       else | ||||
|         return chunk | ||||
|       end | ||||
|     else | ||||
|       table.insert(t, chunk) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function source.chain(src, f) | ||||
|     base.assert(src and f) | ||||
|     local last_in, last_out = "", "" | ||||
|     local state = "feeding" | ||||
|     local err | ||||
|     return function() | ||||
|         if not last_out then | ||||
|             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 | ||||
|   base.assert(src and f) | ||||
|   local last_in, last_out = "", "" | ||||
|   local state = "feeding" | ||||
|   local err | ||||
|   return function() | ||||
|     if not last_out then | ||||
|       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 | ||||
|  | ||||
| -- creates a source that produces contents of several sources, one after the | ||||
| -- other, as if they were concatenated | ||||
| -- (thanks to Wim Couwenberg) | ||||
| function source.cat(...) | ||||
|     local src = table.remove(arg, 1) | ||||
|     return function() | ||||
|         while src do | ||||
|             local chunk, err = src() | ||||
|             if chunk then return chunk end | ||||
|             if err then return nil, err end | ||||
|             src = table.remove(arg, 1) | ||||
|         end | ||||
|   local src = table.remove(arg, 1) | ||||
|   return function() | ||||
|     while src do | ||||
|       local chunk, err = src() | ||||
|       if chunk then return chunk end | ||||
|       if err then return nil, err end | ||||
|       src = table.remove(arg, 1) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| ----------------------------------------------------------------------------- | ||||
| @@ -202,68 +222,74 @@ end | ||||
| ----------------------------------------------------------------------------- | ||||
| -- creates a sink that stores into a table | ||||
| function sink.table(t) | ||||
|     t = t or {} | ||||
|     local f = function(chunk, err) | ||||
|         if chunk then table.insert(t, chunk) end | ||||
|         return 1 | ||||
|     end | ||||
|     return f, t | ||||
|   t = t or {} | ||||
|   local f = function(chunk, err) | ||||
|     if chunk then table.insert(t, chunk) end | ||||
|     return 1 | ||||
|   end | ||||
|   return f, t | ||||
| end | ||||
|  | ||||
| -- turns a fancy sink into a simple sink | ||||
| function sink.simplify(snk) | ||||
|     base.assert(snk) | ||||
|     return function(chunk, err) | ||||
|         local ret, err_or_new = snk(chunk, err) | ||||
|         if not ret then return nil, err_or_new end | ||||
|         snk = err_or_new or snk | ||||
|         return 1 | ||||
|     end | ||||
|   base.assert(snk) | ||||
|   return function(chunk, err) | ||||
|     local ret, err_or_new = snk(chunk, err) | ||||
|     if not ret then return nil, err_or_new end | ||||
|     snk = err_or_new or snk | ||||
|     return 1 | ||||
|   end | ||||
| end | ||||
|  | ||||
| -- creates a file sink | ||||
| function sink.file(handle, io_err) | ||||
|     if handle then | ||||
|         return function(chunk, err) | ||||
|             if not chunk then | ||||
|                 handle:close() | ||||
|                 return 1 | ||||
|             else return handle:write(chunk) end | ||||
|         end | ||||
|     else return sink.error(io_err or "unable to open file") end | ||||
|   if handle then | ||||
|     return function(chunk, err) | ||||
|       if not chunk then | ||||
|         handle:close() | ||||
|         return 1 | ||||
|       else | ||||
|         return handle:write(chunk) | ||||
|       end | ||||
|     end | ||||
|   else | ||||
|     return sink.error(io_err or "unable to open file") | ||||
|   end | ||||
| end | ||||
|  | ||||
| -- creates a sink that discards data | ||||
| local function null() | ||||
|     return 1 | ||||
|   return 1 | ||||
| end | ||||
|  | ||||
| function sink.null() | ||||
|     return null | ||||
|   return null | ||||
| end | ||||
|  | ||||
| -- creates a sink that just returns an error | ||||
| function sink.error(err) | ||||
|     return function() | ||||
|         return nil, err | ||||
|     end | ||||
|   return function() | ||||
|     return nil, err | ||||
|   end | ||||
| end | ||||
|  | ||||
| -- chains a sink with a filter | ||||
| function sink.chain(f, snk) | ||||
|     base.assert(f and snk) | ||||
|     return function(chunk, err) | ||||
|         if chunk ~= "" then | ||||
|             local filtered = f(chunk) | ||||
|             local done = chunk and "" | ||||
|             while true do | ||||
|                 local ret, snkerr = snk(filtered, err) | ||||
|                 if not ret then return nil, snkerr end | ||||
|                 if filtered == done then return 1 end | ||||
|                 filtered = f(done) | ||||
|             end | ||||
|         else return 1 end | ||||
|   base.assert(f and snk) | ||||
|   return function(chunk, err) | ||||
|     if chunk ~= "" then | ||||
|       local filtered = f(chunk) | ||||
|       local done = chunk and "" | ||||
|       while true do | ||||
|         local ret, snkerr = snk(filtered, err) | ||||
|         if not ret then return nil, snkerr end | ||||
|         if filtered == done then return 1 end | ||||
|         filtered = f(done) | ||||
|       end | ||||
|     else | ||||
|       return 1 | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| ----------------------------------------------------------------------------- | ||||
| @@ -271,22 +297,27 @@ end | ||||
| ----------------------------------------------------------------------------- | ||||
| -- pumps one chunk from the source to the sink | ||||
| function pump.step(src, snk) | ||||
|     local chunk, src_err = src() | ||||
|     local ret, snk_err = snk(chunk, src_err) | ||||
|     if chunk and ret then return 1 | ||||
|     else return nil, src_err or snk_err end | ||||
|   local chunk, src_err = src() | ||||
|   local ret, snk_err = snk(chunk, src_err) | ||||
|   if chunk and ret then | ||||
|     return 1 | ||||
|   else | ||||
|     return nil, src_err or snk_err | ||||
|   end | ||||
| end | ||||
|  | ||||
| -- pumps all data from a source to a sink, using a step function | ||||
| function pump.all(src, snk, step) | ||||
|     base.assert(src and snk) | ||||
|     step = step or pump.step | ||||
|     while true do | ||||
|         local ret, err = step(src, snk) | ||||
|         if not ret then | ||||
|             if err then return nil, err | ||||
|             else return 1 end | ||||
|         end | ||||
|   base.assert(src and snk) | ||||
|   step = step or pump.step | ||||
|   while true do | ||||
|     local ret, err = step(src, snk) | ||||
|     if not ret then | ||||
|       if err then | ||||
|         return nil, err | ||||
|       else | ||||
|         return 1 | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
|   | ||||
| @@ -22,53 +22,60 @@ wrapt = {} | ||||
|  | ||||
| -- creates a function that chooses a filter by name from a given table | ||||
| local function choose(table) | ||||
|     return function(name, opt1, opt2) | ||||
|         if base.type(name) ~= "string" then | ||||
|             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 | ||||
|   return function(name, opt1, opt2) | ||||
|     if base.type(name) ~= "string" then | ||||
|       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 | ||||
|  | ||||
| -- define the encoding filters | ||||
| encodet['base64'] = function() | ||||
|     return ltn12.filter.cycle(b64, "") | ||||
|   return ltn12.filter.cycle(b64, "") | ||||
| end | ||||
|  | ||||
| encodet['quoted-printable'] = function(mode) | ||||
|     return ltn12.filter.cycle(qp, "", | ||||
|         (mode == "binary") and "=0D=0A" or "\r\n") | ||||
|   return ltn12.filter.cycle(qp, "", | ||||
|     (mode == "binary") and "=0D=0A" or "\r\n") | ||||
| end | ||||
|  | ||||
| -- define the decoding filters | ||||
| decodet['base64'] = function() | ||||
|     return ltn12.filter.cycle(unb64, "") | ||||
|   return ltn12.filter.cycle(unb64, "") | ||||
| end | ||||
|  | ||||
| decodet['quoted-printable'] = function() | ||||
|     return ltn12.filter.cycle(unqp, "") | ||||
|   return ltn12.filter.cycle(unqp, "") | ||||
| end | ||||
|  | ||||
| local function format(chunk) | ||||
|     if chunk then | ||||
|         if chunk == "" then return "''" | ||||
|         else return string.len(chunk) end | ||||
|     else return "nil" end | ||||
|   if chunk then | ||||
|     if chunk == "" then | ||||
|       return "''" | ||||
|     else | ||||
|       return string.len(chunk) | ||||
|     end | ||||
|   else | ||||
|     return "nil" | ||||
|   end | ||||
| end | ||||
|  | ||||
| -- define the line-wrap filters | ||||
| wrapt['text'] = function(length) | ||||
|     length = length or 76 | ||||
|     return ltn12.filter.cycle(wrp, length, length) | ||||
|   length = length or 76 | ||||
|   return ltn12.filter.cycle(wrp, length, length) | ||||
| end | ||||
| wrapt['base64'] = wrapt['text'] | ||||
| wrapt['default'] = wrapt['text'] | ||||
|  | ||||
| wrapt['quoted-printable'] = function() | ||||
|     return ltn12.filter.cycle(qpwrp, 76, 76) | ||||
|   return ltn12.filter.cycle(qpwrp, 76, 76) | ||||
| end | ||||
|  | ||||
| -- function that choose the encoding, decoding or wrap algorithm | ||||
| @@ -78,10 +85,10 @@ wrap = choose(wrapt) | ||||
|  | ||||
| -- define the end-of-line normalization filter | ||||
| function normalize(marker) | ||||
|     return ltn12.filter.cycle(eol, 0, marker) | ||||
|   return ltn12.filter.cycle(eol, 0, marker) | ||||
| end | ||||
|  | ||||
| -- high level stuffing filter | ||||
| function stuff() | ||||
|     return ltn12.filter.cycle(dot, 2) | ||||
|   return ltn12.filter.cycle(dot, 2) | ||||
| end | ||||
|   | ||||
| @@ -17,39 +17,42 @@ module("socket") | ||||
| -- Exported auxiliar functions | ||||
| ----------------------------------------------------------------------------- | ||||
| function connect(address, port, laddress, lport) | ||||
|     local sock, err = socket.tcp() | ||||
|     if not sock then return nil, err end | ||||
|     if laddress then | ||||
|         local res, err = sock:bind(laddress, lport, -1) | ||||
|         if not res then return nil, err end | ||||
|     end | ||||
|     local res, err = sock:connect(address, port) | ||||
|   local sock, err = socket.tcp() | ||||
|   if not sock then return nil, err end | ||||
|   if laddress then | ||||
|     local res, err = sock:bind(laddress, lport, -1) | ||||
|     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 | ||||
|  | ||||
| function bind(host, port, backlog) | ||||
|     local sock, err = socket.tcp() | ||||
|     if not sock then return nil, err end | ||||
|     sock:setoption("reuseaddr", true) | ||||
|     local res, err = sock:bind(host, port) | ||||
|     if not res then return nil, err end | ||||
|     res, err = sock:listen(backlog) | ||||
|     if not res then return nil, err end | ||||
|     return sock | ||||
|   local sock, err = socket.tcp() | ||||
|   if not sock then return nil, err end | ||||
|   sock:setoption("reuseaddr", true) | ||||
|   local res, err = sock:bind(host, port) | ||||
|   if not res then return nil, err end | ||||
|   res, err = sock:listen(backlog) | ||||
|   if not res then return nil, err end | ||||
|   return sock | ||||
| end | ||||
|  | ||||
| try = newtry() | ||||
|  | ||||
| function choose(table) | ||||
|     return function(name, opt1, opt2) | ||||
|         if base.type(name) ~= "string" then | ||||
|             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 | ||||
|   return function(name, opt1, opt2) | ||||
|     if base.type(name) ~= "string" then | ||||
|       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 | ||||
|  | ||||
| ----------------------------------------------------------------------------- | ||||
| @@ -62,29 +65,34 @@ sinkt = {} | ||||
| BLOCKSIZE = 2048 | ||||
|  | ||||
| sinkt["close-when-done"] = function(sock) | ||||
|     return base.setmetatable({ | ||||
|         getfd = function() return sock:getfd() end, | ||||
|         dirty = function() return sock:dirty() end | ||||
|     }, { | ||||
|         __call = function(self, chunk, err) | ||||
|             if not chunk then | ||||
|                 sock:close() | ||||
|                 return 1 | ||||
|             else return sock:send(chunk) end | ||||
|         end | ||||
|     }) | ||||
|   return base.setmetatable({ | ||||
|     getfd = function() return sock:getfd() end, | ||||
|     dirty = function() return sock:dirty() end | ||||
|   }, { | ||||
|     __call = function(self, chunk, err) | ||||
|       if not chunk then | ||||
|         sock:close() | ||||
|         return 1 | ||||
|       else | ||||
|         return sock:send(chunk) | ||||
|       end | ||||
|     end | ||||
|   }) | ||||
| end | ||||
|  | ||||
| sinkt["keep-open"] = function(sock) | ||||
|     return base.setmetatable({ | ||||
|         getfd = function() return sock:getfd() end, | ||||
|         dirty = function() return sock:dirty() end | ||||
|     }, { | ||||
|         __call = function(self, chunk, err) | ||||
|             if chunk then return sock:send(chunk) | ||||
|             else return 1 end | ||||
|         end | ||||
|     }) | ||||
|   return base.setmetatable({ | ||||
|     getfd = function() return sock:getfd() end, | ||||
|     dirty = function() return sock:dirty() end | ||||
|   }, { | ||||
|     __call = function(self, chunk, err) | ||||
|       if chunk then | ||||
|         return sock:send(chunk) | ||||
|       else | ||||
|         return 1 | ||||
|       end | ||||
|     end | ||||
|   }) | ||||
| end | ||||
|  | ||||
| sinkt["default"] = sinkt["keep-open"] | ||||
| @@ -92,42 +100,44 @@ sinkt["default"] = sinkt["keep-open"] | ||||
| sink = choose(sinkt) | ||||
|  | ||||
| sourcet["by-length"] = function(sock, length) | ||||
|     return base.setmetatable({ | ||||
|         getfd = function() return sock:getfd() end, | ||||
|         dirty = function() return sock:dirty() end | ||||
|     }, { | ||||
|         __call = function() | ||||
|             if length <= 0 then return nil end | ||||
|             local size = math.min(socket.BLOCKSIZE, length) | ||||
|             local chunk, err = sock:receive(size) | ||||
|             if err then return nil, err end | ||||
|             length = length - string.len(chunk) | ||||
|             return chunk | ||||
|         end | ||||
|     }) | ||||
|   return base.setmetatable({ | ||||
|     getfd = function() return sock:getfd() end, | ||||
|     dirty = function() return sock:dirty() end | ||||
|   }, { | ||||
|     __call = function() | ||||
|       if length <= 0 then return nil end | ||||
|       local size = math.min(socket.BLOCKSIZE, length) | ||||
|       local chunk, err = sock:receive(size) | ||||
|       if err then return nil, err end | ||||
|       length = length - string.len(chunk) | ||||
|       return chunk | ||||
|     end | ||||
|   }) | ||||
| end | ||||
|  | ||||
| sourcet["until-closed"] = function(sock) | ||||
|     local done | ||||
|     return base.setmetatable({ | ||||
|         getfd = function() return sock:getfd() end, | ||||
|         dirty = function() return sock:dirty() end | ||||
|     }, { | ||||
|         __call = function() | ||||
|             if done then return nil end | ||||
|             local chunk, err, partial = sock:receive(socket.BLOCKSIZE) | ||||
|             if not err then return chunk | ||||
|             elseif err == "closed" then | ||||
|                 sock:close() | ||||
|                 done = 1 | ||||
|                 return partial | ||||
|             else return nil, err end | ||||
|         end | ||||
|     }) | ||||
|   local done | ||||
|   return base.setmetatable({ | ||||
|     getfd = function() return sock:getfd() end, | ||||
|     dirty = function() return sock:dirty() end | ||||
|   }, { | ||||
|     __call = function() | ||||
|       if done then return nil end | ||||
|       local chunk, err, partial = sock:receive(socket.BLOCKSIZE) | ||||
|       if not err then | ||||
|         return chunk | ||||
|       elseif err == "closed" then | ||||
|         sock:close() | ||||
|         done = 1 | ||||
|         return partial | ||||
|       else | ||||
|         return nil, err | ||||
|       end | ||||
|     end | ||||
|   }) | ||||
| end | ||||
|  | ||||
|  | ||||
| sourcet["default"] = sourcet["until-closed"] | ||||
|  | ||||
| source = choose(sourcet) | ||||
|  | ||||
|   | ||||
| @@ -36,246 +36,253 @@ PASSWORD = "anonymous@anonymous.org" | ||||
| local metat = { __index = {} } | ||||
|  | ||||
| function open(server, port, create) | ||||
|     local tp = socket.try(tp.connect(server, port or PORT, TIMEOUT, create)) | ||||
|     local f = base.setmetatable({ tp = tp }, metat) | ||||
|     -- make sure everything gets closed in an exception | ||||
|     f.try = socket.newtry(function() f:close() end) | ||||
|     return f | ||||
|   local tp = socket.try(tp.connect(server, port or PORT, TIMEOUT, create)) | ||||
|   local f = base.setmetatable({ tp = tp }, metat) | ||||
|   -- make sure everything gets closed in an exception | ||||
|   f.try = socket.newtry(function() f:close() end) | ||||
|   return f | ||||
| end | ||||
|  | ||||
| function metat.__index:portconnect() | ||||
|     self.try(self.server:settimeout(TIMEOUT)) | ||||
|     self.data = self.try(self.server:accept()) | ||||
|     self.try(self.data:settimeout(TIMEOUT)) | ||||
|   self.try(self.server:settimeout(TIMEOUT)) | ||||
|   self.data = self.try(self.server:accept()) | ||||
|   self.try(self.data:settimeout(TIMEOUT)) | ||||
| end | ||||
|  | ||||
| function metat.__index:pasvconnect() | ||||
|     self.data = self.try(socket.tcp()) | ||||
|     self.try(self.data:settimeout(TIMEOUT)) | ||||
|     self.try(self.data:connect(self.pasvt.ip, self.pasvt.port)) | ||||
|   self.data = self.try(socket.tcp()) | ||||
|   self.try(self.data:settimeout(TIMEOUT)) | ||||
|   self.try(self.data:connect(self.pasvt.ip, self.pasvt.port)) | ||||
| end | ||||
|  | ||||
| function metat.__index:login(user, password) | ||||
|     self.try(self.tp:command("user", user or USER)) | ||||
|     local code, reply = self.try(self.tp:check{"2..", 331}) | ||||
|     if code == 331 then | ||||
|         self.try(self.tp:command("pass", password or PASSWORD)) | ||||
|         self.try(self.tp:check("2..")) | ||||
|     end | ||||
|     return 1 | ||||
|   self.try(self.tp:command("user", user or USER)) | ||||
|   local code, reply = self.try(self.tp:check { "2..", 331 }) | ||||
|   if code == 331 then | ||||
|     self.try(self.tp:command("pass", password or PASSWORD)) | ||||
|     self.try(self.tp:check("2..")) | ||||
|   end | ||||
|   return 1 | ||||
| end | ||||
|  | ||||
| function metat.__index:pasv() | ||||
|     self.try(self.tp:command("pasv")) | ||||
|     local code, reply = self.try(self.tp:check("2..")) | ||||
|     local pattern = "(%d+)%D(%d+)%D(%d+)%D(%d+)%D(%d+)%D(%d+)" | ||||
|     local a, b, c, d, p1, p2 = socket.skip(2, string.find(reply, pattern)) | ||||
|     self.try(a and b and c and d and p1 and p2, reply) | ||||
|     self.pasvt = { | ||||
|         ip = string.format("%d.%d.%d.%d", a, b, c, d), | ||||
|         port = p1*256 + p2 | ||||
|     } | ||||
|     if self.server then | ||||
|         self.server:close() | ||||
|         self.server = nil | ||||
|     end | ||||
|     return self.pasvt.ip, self.pasvt.port | ||||
|   self.try(self.tp:command("pasv")) | ||||
|   local code, reply = self.try(self.tp:check("2..")) | ||||
|   local pattern = "(%d+)%D(%d+)%D(%d+)%D(%d+)%D(%d+)%D(%d+)" | ||||
|   local a, b, c, d, p1, p2 = socket.skip(2, string.find(reply, pattern)) | ||||
|   self.try(a and b and c and d and p1 and p2, reply) | ||||
|   self.pasvt = { | ||||
|     ip = string.format("%d.%d.%d.%d", a, b, c, d), | ||||
|     port = p1 * 256 + p2 | ||||
|   } | ||||
|   if self.server then | ||||
|     self.server:close() | ||||
|     self.server = nil | ||||
|   end | ||||
|   return self.pasvt.ip, self.pasvt.port | ||||
| end | ||||
|  | ||||
| function metat.__index:port(ip, port) | ||||
|     self.pasvt = nil | ||||
|     if not ip then | ||||
|         ip, port = self.try(self.tp:getcontrol():getsockname()) | ||||
|         self.server = self.try(socket.bind(ip, 0)) | ||||
|         ip, port = self.try(self.server:getsockname()) | ||||
|         self.try(self.server:settimeout(TIMEOUT)) | ||||
|     end | ||||
|     local pl = math.mod(port, 256) | ||||
|     local ph = (port - pl)/256 | ||||
|     local arg = string.gsub(string.format("%s,%d,%d", ip, ph, pl), "%.", ",") | ||||
|     self.try(self.tp:command("port", arg)) | ||||
|     self.try(self.tp:check("2..")) | ||||
|     return 1 | ||||
|   self.pasvt = nil | ||||
|   if not ip then | ||||
|     ip, port = self.try(self.tp:getcontrol():getsockname()) | ||||
|     self.server = self.try(socket.bind(ip, 0)) | ||||
|     ip, port = self.try(self.server:getsockname()) | ||||
|     self.try(self.server:settimeout(TIMEOUT)) | ||||
|   end | ||||
|   local pl = math.mod(port, 256) | ||||
|   local ph = (port - pl) / 256 | ||||
|   local arg = string.gsub(string.format("%s,%d,%d", ip, ph, pl), "%.", ",") | ||||
|   self.try(self.tp:command("port", arg)) | ||||
|   self.try(self.tp:check("2..")) | ||||
|   return 1 | ||||
| end | ||||
|  | ||||
| function metat.__index:send(sendt) | ||||
|     self.try(self.pasvt or self.server, "need port or pasv first") | ||||
|     -- if there is a pasvt table, we already sent a PASV command | ||||
|     -- we just get the data connection into self.data | ||||
|     if self.pasvt then self:pasvconnect() end | ||||
|     -- get the transfer argument and command | ||||
|     local argument = sendt.argument or | ||||
|         url.unescape(string.gsub(sendt.path or "", "^[/\\]", "")) | ||||
|     if argument == "" then argument = nil end | ||||
|     local command = sendt.command or "stor" | ||||
|     -- send the transfer command and check the reply | ||||
|     self.try(self.tp:command(command, argument)) | ||||
|     local code, reply = self.try(self.tp:check{"2..", "1.."}) | ||||
|     -- if there is not a a pasvt table, then there is a server | ||||
|     -- and we already sent a PORT command | ||||
|     if not self.pasvt then self:portconnect() end | ||||
|     -- get the sink, source and step for the transfer | ||||
|     local step = sendt.step or ltn12.pump.step | ||||
|     local readt = {self.tp.c} | ||||
|     local checkstep = function(src, snk) | ||||
|         -- check status in control connection while downloading | ||||
|         local readyt = socket.select(readt, nil, 0) | ||||
|         if readyt[tp] then code = self.try(self.tp:check("2..")) end | ||||
|         return step(src, snk) | ||||
|     end | ||||
|     local sink = socket.sink("close-when-done", self.data) | ||||
|     -- transfer all data and check error | ||||
|     self.try(ltn12.pump.all(sendt.source, sink, checkstep)) | ||||
|     if string.find(code, "1..") then self.try(self.tp:check("2..")) end | ||||
|     -- done with data connection | ||||
|     self.data:close() | ||||
|     -- find out how many bytes were sent | ||||
|     local sent = socket.skip(1, self.data:getstats()) | ||||
|     self.data = nil | ||||
|     return sent | ||||
|   self.try(self.pasvt or self.server, "need port or pasv first") | ||||
|   -- if there is a pasvt table, we already sent a PASV command | ||||
|   -- we just get the data connection into self.data | ||||
|   if self.pasvt then self:pasvconnect() end | ||||
|   -- get the transfer argument and command | ||||
|   local argument = sendt.argument or | ||||
|       url.unescape(string.gsub(sendt.path or "", "^[/\\]", "")) | ||||
|   if argument == "" then argument = nil end | ||||
|   local command = sendt.command or "stor" | ||||
|   -- send the transfer command and check the reply | ||||
|   self.try(self.tp:command(command, argument)) | ||||
|   local code, reply = self.try(self.tp:check { "2..", "1.." }) | ||||
|   -- if there is not a a pasvt table, then there is a server | ||||
|   -- and we already sent a PORT command | ||||
|   if not self.pasvt then self:portconnect() end | ||||
|   -- get the sink, source and step for the transfer | ||||
|   local step = sendt.step or ltn12.pump.step | ||||
|   local readt = { self.tp.c } | ||||
|   local checkstep = function(src, snk) | ||||
|     -- check status in control connection while downloading | ||||
|     local readyt = socket.select(readt, nil, 0) | ||||
|     if readyt[tp] then code = self.try(self.tp:check("2..")) end | ||||
|     return step(src, snk) | ||||
|   end | ||||
|   local sink = socket.sink("close-when-done", self.data) | ||||
|   -- transfer all data and check error | ||||
|   self.try(ltn12.pump.all(sendt.source, sink, checkstep)) | ||||
|   if string.find(code, "1..") then self.try(self.tp:check("2..")) end | ||||
|   -- done with data connection | ||||
|   self.data:close() | ||||
|   -- find out how many bytes were sent | ||||
|   local sent = socket.skip(1, self.data:getstats()) | ||||
|   self.data = nil | ||||
|   return sent | ||||
| end | ||||
|  | ||||
| function metat.__index:receive(recvt) | ||||
|     self.try(self.pasvt or self.server, "need port or pasv first") | ||||
|     if self.pasvt then self:pasvconnect() end | ||||
|     local argument = recvt.argument or | ||||
|         url.unescape(string.gsub(recvt.path or "", "^[/\\]", "")) | ||||
|     if argument == "" then argument = nil end | ||||
|     local command = recvt.command or "retr" | ||||
|     self.try(self.tp:command(command, argument)) | ||||
|     local code = self.try(self.tp:check{"1..", "2.."}) | ||||
|     if not self.pasvt then self:portconnect() end | ||||
|     local source = socket.source("until-closed", self.data) | ||||
|     local step = recvt.step or ltn12.pump.step | ||||
|     self.try(ltn12.pump.all(source, recvt.sink, step)) | ||||
|     if string.find(code, "1..") then self.try(self.tp:check("2..")) end | ||||
|     self.data:close() | ||||
|     self.data = nil | ||||
|     return 1 | ||||
|   self.try(self.pasvt or self.server, "need port or pasv first") | ||||
|   if self.pasvt then self:pasvconnect() end | ||||
|   local argument = recvt.argument or | ||||
|       url.unescape(string.gsub(recvt.path or "", "^[/\\]", "")) | ||||
|   if argument == "" then argument = nil end | ||||
|   local command = recvt.command or "retr" | ||||
|   self.try(self.tp:command(command, argument)) | ||||
|   local code = self.try(self.tp:check { "1..", "2.." }) | ||||
|   if not self.pasvt then self:portconnect() end | ||||
|   local source = socket.source("until-closed", self.data) | ||||
|   local step = recvt.step or ltn12.pump.step | ||||
|   self.try(ltn12.pump.all(source, recvt.sink, step)) | ||||
|   if string.find(code, "1..") then self.try(self.tp:check("2..")) end | ||||
|   self.data:close() | ||||
|   self.data = nil | ||||
|   return 1 | ||||
| end | ||||
|  | ||||
| function metat.__index:cwd(dir) | ||||
|     self.try(self.tp:command("cwd", dir)) | ||||
|     self.try(self.tp:check(250)) | ||||
|     return 1 | ||||
|   self.try(self.tp:command("cwd", dir)) | ||||
|   self.try(self.tp:check(250)) | ||||
|   return 1 | ||||
| end | ||||
|  | ||||
| function metat.__index:type(type) | ||||
|     self.try(self.tp:command("type", type)) | ||||
|     self.try(self.tp:check(200)) | ||||
|     return 1 | ||||
|   self.try(self.tp:command("type", type)) | ||||
|   self.try(self.tp:check(200)) | ||||
|   return 1 | ||||
| end | ||||
|  | ||||
| function metat.__index:greet() | ||||
|     local code = self.try(self.tp:check{"1..", "2.."}) | ||||
|     if string.find(code, "1..") then self.try(self.tp:check("2..")) end | ||||
|     return 1 | ||||
|   local code = self.try(self.tp:check { "1..", "2.." }) | ||||
|   if string.find(code, "1..") then self.try(self.tp:check("2..")) end | ||||
|   return 1 | ||||
| end | ||||
|  | ||||
| function metat.__index:quit() | ||||
|     self.try(self.tp:command("quit")) | ||||
|     self.try(self.tp:check("2..")) | ||||
|     return 1 | ||||
|   self.try(self.tp:command("quit")) | ||||
|   self.try(self.tp:check("2..")) | ||||
|   return 1 | ||||
| end | ||||
|  | ||||
| function metat.__index:close() | ||||
|     if self.data then self.data:close() end | ||||
|     if self.server then self.server:close() end | ||||
|     return self.tp:close() | ||||
|   if self.data then self.data:close() end | ||||
|   if self.server then self.server:close() end | ||||
|   return self.tp:close() | ||||
| end | ||||
|  | ||||
| ----------------------------------------------------------------------------- | ||||
| -- High level FTP API | ||||
| ----------------------------------------------------------------------------- | ||||
| local function override(t) | ||||
|     if t.url then | ||||
|         local u = url.parse(t.url) | ||||
|         for i,v in base.pairs(t) do | ||||
|             u[i] = v | ||||
|         end | ||||
|         return u | ||||
|     else return t end | ||||
|   if t.url then | ||||
|     local u = url.parse(t.url) | ||||
|     for i, v in base.pairs(t) do | ||||
|       u[i] = v | ||||
|     end | ||||
|     return u | ||||
|   else | ||||
|     return t | ||||
|   end | ||||
| end | ||||
|  | ||||
| local function tput(putt) | ||||
|     putt = override(putt) | ||||
|     socket.try(putt.host, "missing hostname") | ||||
|     local f = open(putt.host, putt.port, putt.create) | ||||
|     f:greet() | ||||
|     f:login(putt.user, putt.password) | ||||
|     if putt.type then f:type(putt.type) end | ||||
|     f:pasv() | ||||
|     local sent = f:send(putt) | ||||
|     f:quit() | ||||
|     f:close() | ||||
|     return sent | ||||
|   putt = override(putt) | ||||
|   socket.try(putt.host, "missing hostname") | ||||
|   local f = open(putt.host, putt.port, putt.create) | ||||
|   f:greet() | ||||
|   f:login(putt.user, putt.password) | ||||
|   if putt.type then f:type(putt.type) end | ||||
|   f:pasv() | ||||
|   local sent = f:send(putt) | ||||
|   f:quit() | ||||
|   f:close() | ||||
|   return sent | ||||
| end | ||||
|  | ||||
| local default = { | ||||
| 	path = "/", | ||||
| 	scheme = "ftp" | ||||
|   path = "/", | ||||
|   scheme = "ftp" | ||||
| } | ||||
|  | ||||
| local function parse(u) | ||||
|     local t = socket.try(url.parse(u, default)) | ||||
|     socket.try(t.scheme == "ftp", "wrong scheme '" .. t.scheme .. "'") | ||||
|     socket.try(t.host, "missing hostname") | ||||
|     local pat = "^type=(.)$" | ||||
|     if t.params then | ||||
|         t.type = socket.skip(2, string.find(t.params, pat)) | ||||
|         socket.try(t.type == "a" or t.type == "i", | ||||
|             "invalid type '" .. t.type .. "'") | ||||
|     end | ||||
|     return t | ||||
|   local t = socket.try(url.parse(u, default)) | ||||
|   socket.try(t.scheme == "ftp", "wrong scheme '" .. t.scheme .. "'") | ||||
|   socket.try(t.host, "missing hostname") | ||||
|   local pat = "^type=(.)$" | ||||
|   if t.params then | ||||
|     t.type = socket.skip(2, string.find(t.params, pat)) | ||||
|     socket.try(t.type == "a" or t.type == "i", | ||||
|       "invalid type '" .. t.type .. "'") | ||||
|   end | ||||
|   return t | ||||
| end | ||||
|  | ||||
| local function sput(u, body) | ||||
|     local putt = parse(u) | ||||
|     putt.source = ltn12.source.string(body) | ||||
|     return tput(putt) | ||||
|   local putt = parse(u) | ||||
|   putt.source = ltn12.source.string(body) | ||||
|   return tput(putt) | ||||
| end | ||||
|  | ||||
| put = socket.protect(function(putt, body) | ||||
|     if base.type(putt) == "string" then return sput(putt, body) | ||||
|     else return tput(putt) end | ||||
|   if base.type(putt) == "string" then | ||||
|     return sput(putt, body) | ||||
|   else | ||||
|     return tput(putt) | ||||
|   end | ||||
| end) | ||||
|  | ||||
| local function tget(gett) | ||||
|     gett = override(gett) | ||||
|     socket.try(gett.host, "missing hostname") | ||||
|     local f = open(gett.host, gett.port, gett.create) | ||||
|     f:greet() | ||||
|     f:login(gett.user, gett.password) | ||||
|     if gett.type then f:type(gett.type) end | ||||
|     f:pasv() | ||||
|     f:receive(gett) | ||||
|     f:quit() | ||||
|     return f:close() | ||||
|   gett = override(gett) | ||||
|   socket.try(gett.host, "missing hostname") | ||||
|   local f = open(gett.host, gett.port, gett.create) | ||||
|   f:greet() | ||||
|   f:login(gett.user, gett.password) | ||||
|   if gett.type then f:type(gett.type) end | ||||
|   f:pasv() | ||||
|   f:receive(gett) | ||||
|   f:quit() | ||||
|   return f:close() | ||||
| end | ||||
|  | ||||
| local function sget(u) | ||||
|     local gett = parse(u) | ||||
|     local t = {} | ||||
|     gett.sink = ltn12.sink.table(t) | ||||
|     tget(gett) | ||||
|     return table.concat(t) | ||||
|   local gett = parse(u) | ||||
|   local t = {} | ||||
|   gett.sink = ltn12.sink.table(t) | ||||
|   tget(gett) | ||||
|   return table.concat(t) | ||||
| end | ||||
|  | ||||
| command = socket.protect(function(cmdt) | ||||
|     cmdt = override(cmdt) | ||||
|     socket.try(cmdt.host, "missing hostname") | ||||
|     socket.try(cmdt.command, "missing command") | ||||
|     local f = open(cmdt.host, cmdt.port, cmdt.create) | ||||
|     f:greet() | ||||
|     f:login(cmdt.user, cmdt.password) | ||||
|     f.try(f.tp:command(cmdt.command, cmdt.argument)) | ||||
|     if cmdt.check then f.try(f.tp:check(cmdt.check)) end | ||||
|     f:quit() | ||||
|     return f:close() | ||||
|   cmdt = override(cmdt) | ||||
|   socket.try(cmdt.host, "missing hostname") | ||||
|   socket.try(cmdt.command, "missing command") | ||||
|   local f = open(cmdt.host, cmdt.port, cmdt.create) | ||||
|   f:greet() | ||||
|   f:login(cmdt.user, cmdt.password) | ||||
|   f.try(f.tp:command(cmdt.command, cmdt.argument)) | ||||
|   if cmdt.check then f.try(f.tp:check(cmdt.check)) end | ||||
|   f:quit() | ||||
|   return f:close() | ||||
| end) | ||||
|  | ||||
| get = socket.protect(function(gett) | ||||
|     if base.type(gett) == "string" then return sget(gett) | ||||
|     else return tget(gett) end | ||||
|   if base.type(gett) == "string" then | ||||
|     return sget(gett) | ||||
|   else | ||||
|     return tget(gett) | ||||
|   end | ||||
| end) | ||||
|  | ||||
|   | ||||
| @@ -31,73 +31,76 @@ USERAGENT = socket._VERSION | ||||
| -- Reads MIME headers from a connection, unfolding where needed | ||||
| ----------------------------------------------------------------------------- | ||||
| local function receiveheaders(sock, headers) | ||||
|     local line, name, value, err | ||||
|     headers = headers or {} | ||||
|     -- get first line | ||||
|   local line, name, value, err | ||||
|   headers = headers or {} | ||||
|   -- get first line | ||||
|   line, err = sock:receive() | ||||
|   if err then return nil, err end | ||||
|   -- headers go until a blank line is found | ||||
|   while line ~= "" do | ||||
|     -- get field-name and value | ||||
|     name, value = socket.skip(2, string.find(line, "^(.-):%s*(.*)")) | ||||
|     if not (name and value) then return nil, "malformed reponse headers" end | ||||
|     name      = string.lower(name) | ||||
|     -- get next line (value might be folded) | ||||
|     line, err = sock:receive() | ||||
|     if err then return nil, err end | ||||
|     -- headers go until a blank line is found | ||||
|     while line ~= "" do | ||||
|         -- get field-name and value | ||||
|         name, value = socket.skip(2, string.find(line, "^(.-):%s*(.*)")) | ||||
|         if not (name and value) then return nil, "malformed reponse headers" end | ||||
|         name = string.lower(name) | ||||
|         -- get next line (value might be folded) | ||||
|         line, err  = sock:receive() | ||||
|         if err then return nil, err end | ||||
|         -- unfold any folded values | ||||
|         while string.find(line, "^%s") do | ||||
|             value = value .. line | ||||
|             line = sock:receive() | ||||
|             if err then return nil, err end | ||||
|         end | ||||
|         -- save pair in table | ||||
|         if headers[name] then headers[name] = headers[name] .. ", " .. value | ||||
|         else headers[name] = value end | ||||
|     -- unfold any folded values | ||||
|     while string.find(line, "^%s") do | ||||
|       value = value .. line | ||||
|       line = sock:receive() | ||||
|       if err then return nil, err 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 | ||||
|  | ||||
| ----------------------------------------------------------------------------- | ||||
| -- Extra sources and sinks | ||||
| ----------------------------------------------------------------------------- | ||||
| socket.sourcet["http-chunked"] = function(sock, headers) | ||||
|     return base.setmetatable({ | ||||
|         getfd = function() return sock:getfd() end, | ||||
|         dirty = function() return sock:dirty() end | ||||
|     }, { | ||||
|         __call = function() | ||||
|             -- get chunk size, skip extention | ||||
|             local line, err = sock:receive() | ||||
|             if err then return nil, err end | ||||
|             local size = base.tonumber(string.gsub(line, ";.*", ""), 16) | ||||
|             if not size then return nil, "invalid chunk size" end | ||||
|             -- was it the last chunk? | ||||
|             if size > 0 then | ||||
|                 -- if not, get chunk and skip terminating CRLF | ||||
|                 local chunk, err, part = sock:receive(size) | ||||
|                 if chunk then sock:receive() end | ||||
|                 return chunk, err | ||||
|             else | ||||
|                 -- if it was, read trailers into headers table | ||||
|                 headers, err = receiveheaders(sock, headers) | ||||
|                 if not headers then return nil, err end | ||||
|             end | ||||
|         end | ||||
|     }) | ||||
|   return base.setmetatable({ | ||||
|     getfd = function() return sock:getfd() end, | ||||
|     dirty = function() return sock:dirty() end | ||||
|   }, { | ||||
|     __call = function() | ||||
|       -- get chunk size, skip extention | ||||
|       local line, err = sock:receive() | ||||
|       if err then return nil, err end | ||||
|       local size = base.tonumber(string.gsub(line, ";.*", ""), 16) | ||||
|       if not size then return nil, "invalid chunk size" end | ||||
|       -- was it the last chunk? | ||||
|       if size > 0 then | ||||
|         -- if not, get chunk and skip terminating CRLF | ||||
|         local chunk, err, part = sock:receive(size) | ||||
|         if chunk then sock:receive() end | ||||
|         return chunk, err | ||||
|       else | ||||
|         -- if it was, read trailers into headers table | ||||
|         headers, err = receiveheaders(sock, headers) | ||||
|         if not headers then return nil, err end | ||||
|       end | ||||
|     end | ||||
|   }) | ||||
| end | ||||
|  | ||||
| socket.sinkt["http-chunked"] = function(sock) | ||||
|     return base.setmetatable({ | ||||
|         getfd = function() return sock:getfd() end, | ||||
|         dirty = function() return sock:dirty() end | ||||
|     }, { | ||||
|         __call = function(self, chunk, err) | ||||
|             if not chunk then return sock:send("0\r\n\r\n") end | ||||
|             local size = string.format("%X\r\n", string.len(chunk)) | ||||
|             return sock:send(size ..  chunk .. "\r\n") | ||||
|         end | ||||
|     }) | ||||
|   return base.setmetatable({ | ||||
|     getfd = function() return sock:getfd() end, | ||||
|     dirty = function() return sock:dirty() end | ||||
|   }, { | ||||
|     __call = function(self, chunk, err) | ||||
|       if not chunk then return sock:send("0\r\n\r\n") end | ||||
|       local size = string.format("%X\r\n", string.len(chunk)) | ||||
|       return sock:send(size .. chunk .. "\r\n") | ||||
|     end | ||||
|   }) | ||||
| end | ||||
|  | ||||
| ----------------------------------------------------------------------------- | ||||
| @@ -106,245 +109,251 @@ end | ||||
| local metat = { __index = {} } | ||||
|  | ||||
| function open(host, port, create) | ||||
|     -- create socket with user connect function, or with default | ||||
|     local c = socket.try((create or socket.tcp)()) | ||||
|     local h = base.setmetatable({ c = c }, metat) | ||||
|     -- create finalized try | ||||
|     h.try = socket.newtry(function() h:close() end) | ||||
|     -- set timeout before connecting | ||||
|     h.try(c:settimeout(TIMEOUT)) | ||||
|     h.try(c:connect(host, port or PORT)) | ||||
|     -- here everything worked | ||||
|     return h | ||||
|   -- create socket with user connect function, or with default | ||||
|   local c = socket.try((create or socket.tcp)()) | ||||
|   local h = base.setmetatable({ c = c }, metat) | ||||
|   -- create finalized try | ||||
|   h.try = socket.newtry(function() h:close() end) | ||||
|   -- set timeout before connecting | ||||
|   h.try(c:settimeout(TIMEOUT)) | ||||
|   h.try(c:connect(host, port or PORT)) | ||||
|   -- here everything worked | ||||
|   return h | ||||
| end | ||||
|  | ||||
| function metat.__index:sendrequestline(method, uri) | ||||
|     local reqline = string.format("%s %s HTTP/1.1\r\n", method or "GET", uri) | ||||
|     return self.try(self.c:send(reqline)) | ||||
|   local reqline = string.format("%s %s HTTP/1.1\r\n", method or "GET", uri) | ||||
|   return self.try(self.c:send(reqline)) | ||||
| end | ||||
|  | ||||
| function metat.__index:sendheaders(headers) | ||||
|     local h = "\r\n" | ||||
|     for i, v in base.pairs(headers) do | ||||
|         h = i .. ": " .. v .. "\r\n" .. h | ||||
|     end | ||||
|     self.try(self.c:send(h)) | ||||
|     return 1 | ||||
|   local h = "\r\n" | ||||
|   for i, v in base.pairs(headers) do | ||||
|     h = i .. ": " .. v .. "\r\n" .. h | ||||
|   end | ||||
|   self.try(self.c:send(h)) | ||||
|   return 1 | ||||
| end | ||||
|  | ||||
| function metat.__index:sendbody(headers, source, step) | ||||
|     source = source or ltn12.source.empty() | ||||
|     step = step or ltn12.pump.step | ||||
|     -- if we don't know the size in advance, send chunked and hope for the best | ||||
|     local mode = "http-chunked" | ||||
|     if headers["content-length"] then mode = "keep-open" end | ||||
|     return self.try(ltn12.pump.all(source, socket.sink(mode, self.c), step)) | ||||
|   source = source or ltn12.source.empty() | ||||
|   step = step or ltn12.pump.step | ||||
|   -- if we don't know the size in advance, send chunked and hope for the best | ||||
|   local mode = "http-chunked" | ||||
|   if headers["content-length"] then mode = "keep-open" end | ||||
|   return self.try(ltn12.pump.all(source, socket.sink(mode, self.c), step)) | ||||
| end | ||||
|  | ||||
| function metat.__index:receivestatusline() | ||||
|     local status = self.try(self.c:receive(5)) | ||||
|     -- identify HTTP/0.9 responses, which do not contain a status line | ||||
|     -- this is just a heuristic, but is what the RFC recommends | ||||
|     if status ~= "HTTP/" then return nil, status end | ||||
|     -- otherwise proceed reading a status line | ||||
|     status = self.try(self.c:receive("*l", status)) | ||||
|     local code = socket.skip(2, string.find(status, "HTTP/%d*%.%d* (%d%d%d)")) | ||||
|     return self.try(base.tonumber(code), status) | ||||
|   local status = self.try(self.c:receive(5)) | ||||
|   -- identify HTTP/0.9 responses, which do not contain a status line | ||||
|   -- this is just a heuristic, but is what the RFC recommends | ||||
|   if status ~= "HTTP/" then return nil, status end | ||||
|   -- otherwise proceed reading a status line | ||||
|   status = self.try(self.c:receive("*l", status)) | ||||
|   local code = socket.skip(2, string.find(status, "HTTP/%d*%.%d* (%d%d%d)")) | ||||
|   return self.try(base.tonumber(code), status) | ||||
| end | ||||
|  | ||||
| function metat.__index:receiveheaders() | ||||
|     return self.try(receiveheaders(self.c)) | ||||
|   return self.try(receiveheaders(self.c)) | ||||
| end | ||||
|  | ||||
| function metat.__index:receivebody(headers, sink, step) | ||||
|     sink = sink or ltn12.sink.null() | ||||
|     step = step or ltn12.pump.step | ||||
|     local length = base.tonumber(headers["content-length"]) | ||||
|     local t = headers["transfer-encoding"] -- shortcut | ||||
|     local mode = "default" -- connection close | ||||
|     if t and t ~= "identity" then mode = "http-chunked" | ||||
|     elseif base.tonumber(headers["content-length"]) then mode = "by-length" end | ||||
|     return self.try(ltn12.pump.all(socket.source(mode, self.c, length), | ||||
|         sink, step)) | ||||
|   sink = sink or ltn12.sink.null() | ||||
|   step = step or ltn12.pump.step | ||||
|   local length = base.tonumber(headers["content-length"]) | ||||
|   local t = headers["transfer-encoding"]   -- shortcut | ||||
|   local mode = "default"                   -- connection close | ||||
|   if t and t ~= "identity" then | ||||
|     mode = "http-chunked" | ||||
|   elseif base.tonumber(headers["content-length"]) then | ||||
|     mode = "by-length" | ||||
|   end | ||||
|   return self.try(ltn12.pump.all(socket.source(mode, self.c, length), | ||||
|     sink, step)) | ||||
| end | ||||
|  | ||||
| function metat.__index:receive09body(status, sink, step) | ||||
|     local source = ltn12.source.rewind(socket.source("until-closed", self.c)) | ||||
|     source(status) | ||||
|     return self.try(ltn12.pump.all(source, sink, step)) | ||||
|   local source = ltn12.source.rewind(socket.source("until-closed", self.c)) | ||||
|   source(status) | ||||
|   return self.try(ltn12.pump.all(source, sink, step)) | ||||
| end | ||||
|  | ||||
| function metat.__index:close() | ||||
|     return self.c:close() | ||||
|   return self.c:close() | ||||
| end | ||||
|  | ||||
| ----------------------------------------------------------------------------- | ||||
| -- High level HTTP API | ||||
| ----------------------------------------------------------------------------- | ||||
| local function adjusturi(reqt) | ||||
|     local u = reqt | ||||
|     -- if there is a proxy, we need the full url. otherwise, just a part. | ||||
|     if not reqt.proxy and not PROXY then | ||||
|         u = { | ||||
|            path = socket.try(reqt.path, "invalid path 'nil'"), | ||||
|            params = reqt.params, | ||||
|            query = reqt.query, | ||||
|            fragment = reqt.fragment | ||||
|         } | ||||
|     end | ||||
|     return url.build(u) | ||||
|   local u = reqt | ||||
|   -- if there is a proxy, we need the full url. otherwise, just a part. | ||||
|   if not reqt.proxy and not PROXY then | ||||
|     u = { | ||||
|       path = socket.try(reqt.path, "invalid path 'nil'"), | ||||
|       params = reqt.params, | ||||
|       query = reqt.query, | ||||
|       fragment = reqt.fragment | ||||
|     } | ||||
|   end | ||||
|   return url.build(u) | ||||
| end | ||||
|  | ||||
| local function adjustproxy(reqt) | ||||
|     local proxy = reqt.proxy or PROXY | ||||
|     if proxy then | ||||
|         proxy = url.parse(proxy) | ||||
|         return proxy.host, proxy.port or 3128 | ||||
|     else | ||||
|         return reqt.host, reqt.port | ||||
|     end | ||||
|   local proxy = reqt.proxy or PROXY | ||||
|   if proxy then | ||||
|     proxy = url.parse(proxy) | ||||
|     return proxy.host, proxy.port or 3128 | ||||
|   else | ||||
|     return reqt.host, reqt.port | ||||
|   end | ||||
| end | ||||
|  | ||||
| local function adjustheaders(reqt) | ||||
|     -- default headers | ||||
|     local lower = { | ||||
|         ["user-agent"] = USERAGENT, | ||||
|         ["host"] = reqt.host, | ||||
|         ["connection"] = "close, TE", | ||||
|         ["te"] = "trailers" | ||||
|     } | ||||
|     -- if we have authentication information, pass it along | ||||
|     if reqt.user and reqt.password then | ||||
|         lower["authorization"] =  | ||||
|             "Basic " ..  (mime.b64(reqt.user .. ":" .. reqt.password)) | ||||
|     end | ||||
|     -- override with user headers | ||||
|     for i,v in base.pairs(reqt.headers or lower) do | ||||
|         lower[string.lower(i)] = v | ||||
|     end | ||||
|     return lower | ||||
|   -- default headers | ||||
|   local lower = { | ||||
|     ["user-agent"] = USERAGENT, | ||||
|     ["host"] = reqt.host, | ||||
|     ["connection"] = "close, TE", | ||||
|     ["te"] = "trailers" | ||||
|   } | ||||
|   -- if we have authentication information, pass it along | ||||
|   if reqt.user and reqt.password then | ||||
|     lower["authorization"] = | ||||
|         "Basic " .. (mime.b64(reqt.user .. ":" .. reqt.password)) | ||||
|   end | ||||
|   -- override with user headers | ||||
|   for i, v in base.pairs(reqt.headers or lower) do | ||||
|     lower[string.lower(i)] = v | ||||
|   end | ||||
|   return lower | ||||
| end | ||||
|  | ||||
| -- default url parts | ||||
| local default = { | ||||
|     host = "", | ||||
|     port = PORT, | ||||
|     path ="/", | ||||
|     scheme = "http" | ||||
|   host = "", | ||||
|   port = PORT, | ||||
|   path = "/", | ||||
|   scheme = "http" | ||||
| } | ||||
|  | ||||
| local function adjustrequest(reqt) | ||||
|     -- parse url if provided | ||||
|     local nreqt = reqt.url and url.parse(reqt.url, default) or {} | ||||
|     -- explicit components override url | ||||
|     for i,v in base.pairs(reqt) do nreqt[i] = v end | ||||
|     if nreqt.port == "" then nreqt.port = 80 end | ||||
|     socket.try(nreqt.host and nreqt.host ~= "",  | ||||
|         "invalid host '" .. base.tostring(nreqt.host) .. "'") | ||||
|     -- compute uri if user hasn't overriden | ||||
|     nreqt.uri = reqt.uri or adjusturi(nreqt) | ||||
|     -- ajust host and port if there is a proxy | ||||
|     nreqt.host, nreqt.port = adjustproxy(nreqt) | ||||
|     -- adjust headers in request | ||||
|     nreqt.headers = adjustheaders(nreqt) | ||||
|     return nreqt | ||||
|   -- parse url if provided | ||||
|   local nreqt = reqt.url and url.parse(reqt.url, default) or {} | ||||
|   -- explicit components override url | ||||
|   for i, v in base.pairs(reqt) do nreqt[i] = v end | ||||
|   if nreqt.port == "" then nreqt.port = 80 end | ||||
|   socket.try(nreqt.host and nreqt.host ~= "", | ||||
|     "invalid host '" .. base.tostring(nreqt.host) .. "'") | ||||
|   -- compute uri if user hasn't overriden | ||||
|   nreqt.uri = reqt.uri or adjusturi(nreqt) | ||||
|   -- ajust host and port if there is a proxy | ||||
|   nreqt.host, nreqt.port = adjustproxy(nreqt) | ||||
|   -- adjust headers in request | ||||
|   nreqt.headers = adjustheaders(nreqt) | ||||
|   return nreqt | ||||
| end | ||||
|  | ||||
| local function shouldredirect(reqt, code, headers) | ||||
|     return headers.location and | ||||
|            string.gsub(headers.location, "%s", "") ~= "" and | ||||
|            (reqt.redirect ~= false) and | ||||
|            (code == 301 or code == 302) and | ||||
|            (not reqt.method or reqt.method == "GET" or reqt.method == "HEAD") | ||||
|            and (not reqt.nredirects or reqt.nredirects < 5) | ||||
|   return headers.location and | ||||
|       string.gsub(headers.location, "%s", "") ~= "" and | ||||
|       (reqt.redirect ~= false) and | ||||
|       (code == 301 or code == 302) and | ||||
|       (not reqt.method or reqt.method == "GET" or reqt.method == "HEAD") | ||||
|       and (not reqt.nredirects or reqt.nredirects < 5) | ||||
| end | ||||
|  | ||||
| local function shouldreceivebody(reqt, code) | ||||
|     if reqt.method == "HEAD" then return nil end | ||||
|     if code == 204 or code == 304 then return nil end | ||||
|     if code >= 100 and code < 200 then return nil end | ||||
|     return 1 | ||||
|   if reqt.method == "HEAD" then return nil end | ||||
|   if code == 204 or code == 304 then return nil end | ||||
|   if code >= 100 and code < 200 then return nil end | ||||
|   return 1 | ||||
| end | ||||
|  | ||||
| -- forward declarations | ||||
| local trequest, tredirect | ||||
|  | ||||
| function tredirect(reqt, location) | ||||
|     local result, code, headers, status = trequest { | ||||
|         -- the RFC says the redirect URL has to be absolute, but some | ||||
|         -- servers do not respect that | ||||
|         url = url.absolute(reqt.url, location), | ||||
|         source = reqt.source, | ||||
|         sink = reqt.sink, | ||||
|         headers = reqt.headers, | ||||
|         proxy = reqt.proxy,  | ||||
|         nredirects = (reqt.nredirects or 0) + 1, | ||||
|         create = reqt.create | ||||
|     }    | ||||
|     -- pass location header back as a hint we redirected | ||||
|     headers = headers or {} | ||||
|     headers.location = headers.location or location | ||||
|     return result, code, headers, status | ||||
|   local result, code, headers, status = trequest { | ||||
|     -- the RFC says the redirect URL has to be absolute, but some | ||||
|     -- servers do not respect that | ||||
|     url = url.absolute(reqt.url, location), | ||||
|     source = reqt.source, | ||||
|     sink = reqt.sink, | ||||
|     headers = reqt.headers, | ||||
|     proxy = reqt.proxy, | ||||
|     nredirects = (reqt.nredirects or 0) + 1, | ||||
|     create = reqt.create | ||||
|   } | ||||
|   -- pass location header back as a hint we redirected | ||||
|   headers = headers or {} | ||||
|   headers.location = headers.location or location | ||||
|   return result, code, headers, status | ||||
| end | ||||
|  | ||||
| function trequest(reqt) | ||||
|     -- we loop until we get what we want, or | ||||
|     -- until we are sure there is no way to get it | ||||
|     local nreqt = adjustrequest(reqt) | ||||
|     local h = open(nreqt.host, nreqt.port, nreqt.create) | ||||
|     -- send request line and headers | ||||
|     h:sendrequestline(nreqt.method, nreqt.uri) | ||||
|     h:sendheaders(nreqt.headers) | ||||
|     -- if there is a body, send it | ||||
|     if nreqt.source then | ||||
|         h:sendbody(nreqt.headers, nreqt.source, nreqt.step)  | ||||
|     end | ||||
|     local code, status = h:receivestatusline() | ||||
|     -- if it is an HTTP/0.9 server, simply get the body and we are done | ||||
|     if not code then | ||||
|         h:receive09body(status, nreqt.sink, nreqt.step) | ||||
|         return 1, 200 | ||||
|     end | ||||
|     local headers | ||||
|     -- ignore any 100-continue messages | ||||
|     while code == 100 do  | ||||
|         headers = h:receiveheaders() | ||||
|         code, status = h:receivestatusline() | ||||
|     end | ||||
|   -- we loop until we get what we want, or | ||||
|   -- until we are sure there is no way to get it | ||||
|   local nreqt = adjustrequest(reqt) | ||||
|   local h = open(nreqt.host, nreqt.port, nreqt.create) | ||||
|   -- send request line and headers | ||||
|   h:sendrequestline(nreqt.method, nreqt.uri) | ||||
|   h:sendheaders(nreqt.headers) | ||||
|   -- if there is a body, send it | ||||
|   if nreqt.source then | ||||
|     h:sendbody(nreqt.headers, nreqt.source, nreqt.step) | ||||
|   end | ||||
|   local code, status = h:receivestatusline() | ||||
|   -- if it is an HTTP/0.9 server, simply get the body and we are done | ||||
|   if not code then | ||||
|     h:receive09body(status, nreqt.sink, nreqt.step) | ||||
|     return 1, 200 | ||||
|   end | ||||
|   local headers | ||||
|   -- ignore any 100-continue messages | ||||
|   while code == 100 do | ||||
|     headers = h:receiveheaders() | ||||
|     -- at this point we should have a honest reply from the server | ||||
|     -- we can't redirect if we already used the source, so we report the error  | ||||
|     if shouldredirect(nreqt, code, headers) and not nreqt.source then | ||||
|         h:close() | ||||
|         return tredirect(reqt, headers.location) | ||||
|     end | ||||
|     -- here we are finally done | ||||
|     if shouldreceivebody(nreqt, code) then | ||||
|         h:receivebody(headers, nreqt.sink, nreqt.step) | ||||
|     end | ||||
|     code, status = h:receivestatusline() | ||||
|   end | ||||
|   headers = h:receiveheaders() | ||||
|   -- at this point we should have a honest reply from the server | ||||
|   -- we can't redirect if we already used the source, so we report the error | ||||
|   if shouldredirect(nreqt, code, headers) and not nreqt.source then | ||||
|     h:close() | ||||
|     return 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 | ||||
|  | ||||
| local function srequest(u, b) | ||||
|     local t = {} | ||||
|     local reqt = { | ||||
|         url = u, | ||||
|         sink = ltn12.sink.table(t) | ||||
|   local t = {} | ||||
|   local reqt = { | ||||
|     url = u, | ||||
|     sink = ltn12.sink.table(t) | ||||
|   } | ||||
|   if b then | ||||
|     reqt.source = ltn12.source.string(b) | ||||
|     reqt.headers = { | ||||
|       ["content-length"] = string.len(b), | ||||
|       ["content-type"] = "application/x-www-form-urlencoded" | ||||
|     } | ||||
|     if b then | ||||
|         reqt.source = ltn12.source.string(b) | ||||
|         reqt.headers = { | ||||
|             ["content-length"] = string.len(b), | ||||
|             ["content-type"] = "application/x-www-form-urlencoded" | ||||
|         } | ||||
|         reqt.method = "POST" | ||||
|     end | ||||
|     local code, headers, status = socket.skip(1, trequest(reqt)) | ||||
|     return table.concat(t), code, headers, status | ||||
|     reqt.method = "POST" | ||||
|   end | ||||
|   local code, headers, status = socket.skip(1, trequest(reqt)) | ||||
|   return table.concat(t), code, headers, status | ||||
| end | ||||
|  | ||||
| request = socket.protect(function(reqt, body) | ||||
|     if base.type(reqt) == "string" then return srequest(reqt, body) | ||||
|     else return trequest(reqt) end | ||||
|   if base.type(reqt) == "string" then | ||||
|     return srequest(reqt, body) | ||||
|   else | ||||
|     return trequest(reqt) | ||||
|   end | ||||
| end) | ||||
|   | ||||
| @@ -40,95 +40,95 @@ ZONE = "-0000" | ||||
| local metat = { __index = {} } | ||||
|  | ||||
| function metat.__index:greet(domain) | ||||
|     self.try(self.tp:check("2..")) | ||||
|     self.try(self.tp:command("EHLO", domain or DOMAIN)) | ||||
|     return socket.skip(1, self.try(self.tp:check("2.."))) | ||||
|   self.try(self.tp:check("2..")) | ||||
|   self.try(self.tp:command("EHLO", domain or DOMAIN)) | ||||
|   return socket.skip(1, self.try(self.tp:check("2.."))) | ||||
| end | ||||
|  | ||||
| function metat.__index:mail(from) | ||||
|     self.try(self.tp:command("MAIL", "FROM:" .. from)) | ||||
|     return self.try(self.tp:check("2..")) | ||||
|   self.try(self.tp:command("MAIL", "FROM:" .. from)) | ||||
|   return self.try(self.tp:check("2..")) | ||||
| end | ||||
|  | ||||
| function metat.__index:rcpt(to) | ||||
|     self.try(self.tp:command("RCPT", "TO:" .. to)) | ||||
|     return self.try(self.tp:check("2..")) | ||||
|   self.try(self.tp:command("RCPT", "TO:" .. to)) | ||||
|   return self.try(self.tp:check("2..")) | ||||
| end | ||||
|  | ||||
| function metat.__index:data(src, step) | ||||
|     self.try(self.tp:command("DATA")) | ||||
|     self.try(self.tp:check("3..")) | ||||
|     self.try(self.tp:source(src, step)) | ||||
|     self.try(self.tp:send("\r\n.\r\n")) | ||||
|     return self.try(self.tp:check("2..")) | ||||
|   self.try(self.tp:command("DATA")) | ||||
|   self.try(self.tp:check("3..")) | ||||
|   self.try(self.tp:source(src, step)) | ||||
|   self.try(self.tp:send("\r\n.\r\n")) | ||||
|   return self.try(self.tp:check("2..")) | ||||
| end | ||||
|  | ||||
| function metat.__index:quit() | ||||
|     self.try(self.tp:command("QUIT")) | ||||
|     return self.try(self.tp:check("2..")) | ||||
|   self.try(self.tp:command("QUIT")) | ||||
|   return self.try(self.tp:check("2..")) | ||||
| end | ||||
|  | ||||
| function metat.__index:close() | ||||
|     return self.tp:close() | ||||
|   return self.tp:close() | ||||
| end | ||||
|  | ||||
| function metat.__index:login(user, password) | ||||
|     self.try(self.tp:command("AUTH", "LOGIN")) | ||||
|     self.try(self.tp:check("3..")) | ||||
|     self.try(self.tp:command(mime.b64(user))) | ||||
|     self.try(self.tp:check("3..")) | ||||
|     self.try(self.tp:command(mime.b64(password))) | ||||
|     return self.try(self.tp:check("2..")) | ||||
|   self.try(self.tp:command("AUTH", "LOGIN")) | ||||
|   self.try(self.tp:check("3..")) | ||||
|   self.try(self.tp:command(mime.b64(user))) | ||||
|   self.try(self.tp:check("3..")) | ||||
|   self.try(self.tp:command(mime.b64(password))) | ||||
|   return self.try(self.tp:check("2..")) | ||||
| end | ||||
|  | ||||
| function metat.__index:plain(user, password) | ||||
|     local auth = "PLAIN " .. mime.b64("\0" .. user .. "\0" .. password) | ||||
|     self.try(self.tp:command("AUTH", auth)) | ||||
|     return self.try(self.tp:check("2..")) | ||||
|   local auth = "PLAIN " .. mime.b64("\0" .. user .. "\0" .. password) | ||||
|   self.try(self.tp:command("AUTH", auth)) | ||||
|   return self.try(self.tp:check("2..")) | ||||
| end | ||||
|  | ||||
| function metat.__index:auth(user, password, ext) | ||||
|     if not user or not password then return 1 end | ||||
|     if string.find(ext, "AUTH[^\n]+LOGIN") then | ||||
|         return self:login(user, password) | ||||
|     elseif string.find(ext, "AUTH[^\n]+PLAIN") then | ||||
|         return self:plain(user, password) | ||||
|     else | ||||
|         self.try(nil, "authentication not supported") | ||||
|     end | ||||
|   if not user or not password then return 1 end | ||||
|   if string.find(ext, "AUTH[^\n]+LOGIN") then | ||||
|     return self:login(user, password) | ||||
|   elseif string.find(ext, "AUTH[^\n]+PLAIN") then | ||||
|     return self:plain(user, password) | ||||
|   else | ||||
|     self.try(nil, "authentication not supported") | ||||
|   end | ||||
| end | ||||
|  | ||||
| -- send message or throw an exception | ||||
| function metat.__index:send(mailt) | ||||
|     self:mail(mailt.from) | ||||
|     if base.type(mailt.rcpt) == "table" then | ||||
|         for i,v in base.ipairs(mailt.rcpt) do | ||||
|             self:rcpt(v) | ||||
|         end | ||||
|     else | ||||
|         self:rcpt(mailt.rcpt) | ||||
|   self:mail(mailt.from) | ||||
|   if base.type(mailt.rcpt) == "table" then | ||||
|     for i, v in base.ipairs(mailt.rcpt) do | ||||
|       self:rcpt(v) | ||||
|     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 | ||||
|  | ||||
| function open(server, port, create) | ||||
|     local tp = socket.try(tp.connect(server or SERVER, port or PORT, | ||||
|         TIMEOUT, create)) | ||||
|     local s = base.setmetatable({tp = tp}, metat) | ||||
|     -- make sure tp is closed if we get an exception | ||||
|     s.try = socket.newtry(function() | ||||
|         s:close() | ||||
|     end) | ||||
|     return s | ||||
|   local tp = socket.try(tp.connect(server or SERVER, port or PORT, | ||||
|     TIMEOUT, create)) | ||||
|   local s = base.setmetatable({ tp = tp }, metat) | ||||
|   -- make sure tp is closed if we get an exception | ||||
|   s.try = socket.newtry(function() | ||||
|     s:close() | ||||
|   end) | ||||
|   return s | ||||
| end | ||||
|  | ||||
| -- convert headers to lowercase | ||||
| local function lower_headers(headers) | ||||
|     local lower = {} | ||||
|     for i,v in base.pairs(headers or lower) do | ||||
|         lower[string.lower(i)] = v | ||||
|     end | ||||
|     return lower | ||||
|   local lower = {} | ||||
|   for i, v in base.pairs(headers or lower) do | ||||
|     lower[string.lower(i)] = v | ||||
|   end | ||||
|   return lower | ||||
| end | ||||
|  | ||||
| --------------------------------------------------------------------------- | ||||
| @@ -137,9 +137,9 @@ end | ||||
| -- returns a hopefully unique mime boundary | ||||
| local seqno = 0 | ||||
| local function newboundary() | ||||
|     seqno = seqno + 1 | ||||
|     return string.format('%s%05d==%05u', os.date('%d%m%Y%H%M%S'), | ||||
|         math.random(0, 99999), seqno) | ||||
|   seqno = seqno + 1 | ||||
|   return string.format('%s%05d==%05u', os.date('%d%m%Y%H%M%S'), | ||||
|     math.random(0, 99999), seqno) | ||||
| end | ||||
|  | ||||
| -- send_message forward declaration | ||||
| @@ -147,105 +147,116 @@ local send_message | ||||
|  | ||||
| -- yield the headers all at once, it's faster | ||||
| local function send_headers(headers) | ||||
|     local h = "\r\n" | ||||
|     for i,v in base.pairs(headers) do | ||||
|         h = i .. ': ' .. v .. "\r\n" .. h | ||||
|     end | ||||
|     coroutine.yield(h) | ||||
|   local h = "\r\n" | ||||
|   for i, v in base.pairs(headers) do | ||||
|     h = i .. ': ' .. v .. "\r\n" .. h | ||||
|   end | ||||
|   coroutine.yield(h) | ||||
| end | ||||
|  | ||||
| -- yield multipart message body from a multipart message table | ||||
| local function send_multipart(mesgt) | ||||
|     -- make sure we have our boundary and send headers | ||||
|     local bd = newboundary() | ||||
|     local headers = lower_headers(mesgt.headers or {}) | ||||
|     headers['content-type'] = headers['content-type'] or 'multipart/mixed' | ||||
|     headers['content-type'] = headers['content-type'] .. | ||||
|         '; boundary="' ..  bd .. '"' | ||||
|     send_headers(headers) | ||||
|     -- send preamble | ||||
|     if mesgt.body.preamble then | ||||
|         coroutine.yield(mesgt.body.preamble) | ||||
|         coroutine.yield("\r\n") | ||||
|     end | ||||
|     -- send each part separated by a boundary | ||||
|     for i, m in base.ipairs(mesgt.body) do | ||||
|         coroutine.yield("\r\n--" .. bd .. "\r\n") | ||||
|         send_message(m) | ||||
|     end | ||||
|     -- send last boundary | ||||
|     coroutine.yield("\r\n--" .. bd .. "--\r\n\r\n") | ||||
|     -- send epilogue | ||||
|     if mesgt.body.epilogue then | ||||
|         coroutine.yield(mesgt.body.epilogue) | ||||
|         coroutine.yield("\r\n") | ||||
|     end | ||||
|   -- make sure we have our boundary and send headers | ||||
|   local bd = newboundary() | ||||
|   local headers = lower_headers(mesgt.headers or {}) | ||||
|   headers['content-type'] = headers['content-type'] or 'multipart/mixed' | ||||
|   headers['content-type'] = headers['content-type'] .. | ||||
|       '; boundary="' .. bd .. '"' | ||||
|   send_headers(headers) | ||||
|   -- send preamble | ||||
|   if mesgt.body.preamble then | ||||
|     coroutine.yield(mesgt.body.preamble) | ||||
|     coroutine.yield("\r\n") | ||||
|   end | ||||
|   -- send each part separated by a boundary | ||||
|   for i, m in base.ipairs(mesgt.body) do | ||||
|     coroutine.yield("\r\n--" .. bd .. "\r\n") | ||||
|     send_message(m) | ||||
|   end | ||||
|   -- send last boundary | ||||
|   coroutine.yield("\r\n--" .. bd .. "--\r\n\r\n") | ||||
|   -- send epilogue | ||||
|   if mesgt.body.epilogue then | ||||
|     coroutine.yield(mesgt.body.epilogue) | ||||
|     coroutine.yield("\r\n") | ||||
|   end | ||||
| end | ||||
|  | ||||
| -- yield message body from a source | ||||
| local function send_source(mesgt) | ||||
|     -- make sure we have a content-type | ||||
|     local headers = lower_headers(mesgt.headers or {}) | ||||
|     headers['content-type'] = headers['content-type'] or | ||||
|         'text/plain; charset="iso-8859-1"' | ||||
|     send_headers(headers) | ||||
|     -- send body from source | ||||
|     while true do | ||||
|         local chunk, err = mesgt.body() | ||||
|         if err then coroutine.yield(nil, err) | ||||
|         elseif chunk then coroutine.yield(chunk) | ||||
|         else break end | ||||
|   -- make sure we have a content-type | ||||
|   local headers = lower_headers(mesgt.headers or {}) | ||||
|   headers['content-type'] = headers['content-type'] or | ||||
|       'text/plain; charset="iso-8859-1"' | ||||
|   send_headers(headers) | ||||
|   -- send body from source | ||||
|   while true do | ||||
|     local chunk, err = mesgt.body() | ||||
|     if err then | ||||
|       coroutine.yield(nil, err) | ||||
|     elseif chunk then | ||||
|       coroutine.yield(chunk) | ||||
|     else | ||||
|       break | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| -- yield message body from a string | ||||
| local function send_string(mesgt) | ||||
|     -- make sure we have a content-type | ||||
|     local headers = lower_headers(mesgt.headers or {}) | ||||
|     headers['content-type'] = headers['content-type'] or | ||||
|         'text/plain; charset="iso-8859-1"' | ||||
|     send_headers(headers) | ||||
|     -- send body from string | ||||
|     coroutine.yield(mesgt.body) | ||||
|   -- make sure we have a content-type | ||||
|   local headers = lower_headers(mesgt.headers or {}) | ||||
|   headers['content-type'] = headers['content-type'] or | ||||
|       'text/plain; charset="iso-8859-1"' | ||||
|   send_headers(headers) | ||||
|   -- send body from string | ||||
|   coroutine.yield(mesgt.body) | ||||
| end | ||||
|  | ||||
| -- message source | ||||
| function send_message(mesgt) | ||||
|     if base.type(mesgt.body) == "table" then send_multipart(mesgt) | ||||
|     elseif base.type(mesgt.body) == "function" then send_source(mesgt) | ||||
|     else send_string(mesgt) end | ||||
|   if base.type(mesgt.body) == "table" then | ||||
|     send_multipart(mesgt) | ||||
|   elseif base.type(mesgt.body) == "function" then | ||||
|     send_source(mesgt) | ||||
|   else | ||||
|     send_string(mesgt) | ||||
|   end | ||||
| end | ||||
|  | ||||
| -- set defaul headers | ||||
| local function adjust_headers(mesgt) | ||||
|     local lower = lower_headers(mesgt.headers) | ||||
|     lower["date"] = lower["date"] or | ||||
|         os.date("!%a, %d %b %Y %H:%M:%S ") .. (mesgt.zone or ZONE) | ||||
|     lower["x-mailer"] = lower["x-mailer"] or socket._VERSION | ||||
|     -- this can't be overriden | ||||
|     lower["mime-version"] = "1.0" | ||||
|     return lower | ||||
|   local lower = lower_headers(mesgt.headers) | ||||
|   lower["date"] = lower["date"] or | ||||
|       os.date("!%a, %d %b %Y %H:%M:%S ") .. (mesgt.zone or ZONE) | ||||
|   lower["x-mailer"] = lower["x-mailer"] or socket._VERSION | ||||
|   -- this can't be overriden | ||||
|   lower["mime-version"] = "1.0" | ||||
|   return lower | ||||
| end | ||||
|  | ||||
| function message(mesgt) | ||||
|     mesgt.headers = adjust_headers(mesgt) | ||||
|     -- create and return message source | ||||
|     local co = coroutine.create(function() send_message(mesgt) end) | ||||
|     return function() | ||||
|         local ret, a, b = coroutine.resume(co) | ||||
|         if ret then return a, b | ||||
|         else return nil, a end | ||||
|   mesgt.headers = adjust_headers(mesgt) | ||||
|   -- create and return message source | ||||
|   local co = coroutine.create(function() send_message(mesgt) end) | ||||
|   return function() | ||||
|     local ret, a, b = coroutine.resume(co) | ||||
|     if ret then | ||||
|       return a, b | ||||
|     else | ||||
|       return nil, a | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| --------------------------------------------------------------------------- | ||||
| -- High level SMTP API | ||||
| ----------------------------------------------------------------------------- | ||||
| send = socket.protect(function(mailt) | ||||
|     local s = open(mailt.server, mailt.port, mailt.create) | ||||
|     local ext = s:greet(mailt.domain) | ||||
|     s:auth(mailt.user, mailt.password, ext) | ||||
|     s:send(mailt) | ||||
|     s:quit() | ||||
|     return s:close() | ||||
|   local s = open(mailt.server, mailt.port, mailt.create) | ||||
|   local ext = s:greet(mailt.domain) | ||||
|   s:auth(mailt.user, mailt.password, ext) | ||||
|   s:send(mailt) | ||||
|   s:quit() | ||||
|   return s:close() | ||||
| end) | ||||
|   | ||||
| @@ -24,100 +24,104 @@ TIMEOUT = 60 | ||||
| ----------------------------------------------------------------------------- | ||||
| -- gets server reply (works for SMTP and FTP) | ||||
| local function get_reply(c) | ||||
|     local code, current, sep | ||||
|     local line, err = c:receive() | ||||
|     local reply = line | ||||
|     if err then return nil, err end | ||||
|     code, sep = socket.skip(2, string.find(line, "^(%d%d%d)(.?)")) | ||||
|     if not code then return nil, "invalid server reply" end | ||||
|     if sep == "-" then -- reply is multiline | ||||
|         repeat | ||||
|             line, err = c:receive() | ||||
|             if err then return nil, err end | ||||
|             current, sep = socket.skip(2, string.find(line, "^(%d%d%d)(.?)")) | ||||
|             reply = reply .. "\n" .. line | ||||
|         -- reply ends with same code | ||||
|         until code == current and sep == " " | ||||
|     end | ||||
|     return code, reply | ||||
|   local code, current, sep | ||||
|   local line, err = c:receive() | ||||
|   local reply = line | ||||
|   if err then return nil, err end | ||||
|   code, sep = socket.skip(2, string.find(line, "^(%d%d%d)(.?)")) | ||||
|   if not code then return nil, "invalid server reply" end | ||||
|   if sep == "-" then   -- reply is multiline | ||||
|     repeat | ||||
|       line, err = c:receive() | ||||
|       if err then return nil, err end | ||||
|       current, sep = socket.skip(2, string.find(line, "^(%d%d%d)(.?)")) | ||||
|       reply = reply .. "\n" .. line | ||||
|       -- reply ends with same code | ||||
|     until code == current and sep == " " | ||||
|   end | ||||
|   return code, reply | ||||
| end | ||||
|  | ||||
| -- metatable for sock object | ||||
| local metat = { __index = {} } | ||||
|  | ||||
| function metat.__index:check(ok) | ||||
|     local code, reply = get_reply(self.c) | ||||
|     if not code then return nil, reply end | ||||
|     if base.type(ok) ~= "function" then | ||||
|         if base.type(ok) == "table" then | ||||
|             for i, v in base.ipairs(ok) do | ||||
|                 if string.find(code, v) then | ||||
|                     return base.tonumber(code), reply | ||||
|                 end | ||||
|             end | ||||
|             return nil, reply | ||||
|         else | ||||
|             if string.find(code, ok) then return base.tonumber(code), reply | ||||
|             else return nil, reply end | ||||
|   local code, reply = get_reply(self.c) | ||||
|   if not code then return nil, reply end | ||||
|   if base.type(ok) ~= "function" then | ||||
|     if base.type(ok) == "table" then | ||||
|       for i, v in base.ipairs(ok) do | ||||
|         if string.find(code, v) then | ||||
|           return base.tonumber(code), reply | ||||
|         end | ||||
|     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 | ||||
|  | ||||
| function metat.__index:command(cmd, arg) | ||||
|     if arg then | ||||
|         return self.c:send(cmd .. " " .. arg.. "\r\n") | ||||
|     else | ||||
|         return self.c:send(cmd .. "\r\n") | ||||
|     end | ||||
|   if arg then | ||||
|     return self.c:send(cmd .. " " .. arg .. "\r\n") | ||||
|   else | ||||
|     return self.c:send(cmd .. "\r\n") | ||||
|   end | ||||
| end | ||||
|  | ||||
| function metat.__index:sink(snk, pat) | ||||
|     local chunk, err = c:receive(pat) | ||||
|     return snk(chunk, err) | ||||
|   local chunk, err = c:receive(pat) | ||||
|   return snk(chunk, err) | ||||
| end | ||||
|  | ||||
| function metat.__index:send(data) | ||||
|     return self.c:send(data) | ||||
|   return self.c:send(data) | ||||
| end | ||||
|  | ||||
| function metat.__index:receive(pat) | ||||
|     return self.c:receive(pat) | ||||
|   return self.c:receive(pat) | ||||
| end | ||||
|  | ||||
| function metat.__index:getfd() | ||||
|     return self.c:getfd() | ||||
|   return self.c:getfd() | ||||
| end | ||||
|  | ||||
| function metat.__index:dirty() | ||||
|     return self.c:dirty() | ||||
|   return self.c:dirty() | ||||
| end | ||||
|  | ||||
| function metat.__index:getcontrol() | ||||
|     return self.c | ||||
|   return self.c | ||||
| end | ||||
|  | ||||
| function metat.__index:source(source, step) | ||||
|     local sink = socket.sink("keep-open", self.c) | ||||
|     local ret, err = ltn12.pump.all(source, sink, step or ltn12.pump.step) | ||||
|     return ret, err | ||||
|   local sink = socket.sink("keep-open", self.c) | ||||
|   local ret, err = ltn12.pump.all(source, sink, step or ltn12.pump.step) | ||||
|   return ret, err | ||||
| end | ||||
|  | ||||
| -- closes the underlying c | ||||
| function metat.__index:close() | ||||
|     self.c:close() | ||||
| 	return 1 | ||||
|   self.c:close() | ||||
|   return 1 | ||||
| end | ||||
|  | ||||
| -- connect with server and return c object | ||||
| function connect(host, port, timeout, create) | ||||
|     local c, e = (create or socket.tcp)() | ||||
|     if not c then return nil, e end | ||||
|     c:settimeout(timeout or TIMEOUT) | ||||
|     local r, e = c:connect(host, port) | ||||
|     if not r then | ||||
|         c:close() | ||||
|         return nil, e | ||||
|     end | ||||
|     return base.setmetatable({c = c}, metat) | ||||
|   local c, e = (create or socket.tcp)() | ||||
|   if not c then return nil, e end | ||||
|   c:settimeout(timeout or TIMEOUT) | ||||
|   local r, e = c:connect(host, port) | ||||
|   if not r then | ||||
|     c:close() | ||||
|     return nil, e | ||||
|   end | ||||
|   return base.setmetatable({ c = c }, metat) | ||||
| end | ||||
|  | ||||
|   | ||||
| @@ -26,9 +26,9 @@ _VERSION = "URL 1.0.1" | ||||
| --   escaped representation of string binary | ||||
| ----------------------------------------------------------------------------- | ||||
| function escape(s) | ||||
|     return string.gsub(s, "([^A-Za-z0-9_])", function(c) | ||||
|         return string.format("%%%02x", string.byte(c)) | ||||
|     end) | ||||
|   return string.gsub(s, "([^A-Za-z0-9_])", function(c) | ||||
|     return string.format("%%%02x", string.byte(c)) | ||||
|   end) | ||||
| end | ||||
|  | ||||
| ----------------------------------------------------------------------------- | ||||
| @@ -40,25 +40,28 @@ end | ||||
| --   escaped representation of string binary | ||||
| ----------------------------------------------------------------------------- | ||||
| local function make_set(t) | ||||
| 	local s = {} | ||||
| 	for i,v in base.ipairs(t) do | ||||
| 		s[t[i]] = 1 | ||||
| 	end | ||||
| 	return s | ||||
|   local s = {} | ||||
|   for i, v in base.ipairs(t) do | ||||
|     s[t[i]] = 1 | ||||
|   end | ||||
|   return s | ||||
| end | ||||
|  | ||||
| -- these are allowed withing a path segment, along with alphanum | ||||
| -- other characters must be escaped | ||||
| local segment_set = make_set { | ||||
|     "-", "_", ".", "!", "~", "*", "'", "(", | ||||
| 	")", ":", "@", "&", "=", "+", "$", ",", | ||||
|   "-", "_", ".", "!", "~", "*", "'", "(", | ||||
|   ")", ":", "@", "&", "=", "+", "$", ",", | ||||
| } | ||||
|  | ||||
| local function protect_segment(s) | ||||
| 	return string.gsub(s, "([^A-Za-z0-9_])", function (c) | ||||
| 		if segment_set[c] then return c | ||||
| 		else return string.format("%%%02x", string.byte(c)) end | ||||
| 	end) | ||||
|   return string.gsub(s, "([^A-Za-z0-9_])", function(c) | ||||
|     if segment_set[c] then | ||||
|       return c | ||||
|     else | ||||
|       return string.format("%%%02x", string.byte(c)) | ||||
|     end | ||||
|   end) | ||||
| end | ||||
|  | ||||
| ----------------------------------------------------------------------------- | ||||
| @@ -69,9 +72,9 @@ end | ||||
| --   escaped representation of string binary | ||||
| ----------------------------------------------------------------------------- | ||||
| function unescape(s) | ||||
|     return string.gsub(s, "%%(%x%x)", function(hex) | ||||
|         return string.char(base.tonumber(hex, 16)) | ||||
|     end) | ||||
|   return string.gsub(s, "%%(%x%x)", function(hex) | ||||
|     return string.char(base.tonumber(hex, 16)) | ||||
|   end) | ||||
| end | ||||
|  | ||||
| ----------------------------------------------------------------------------- | ||||
| @@ -83,24 +86,24 @@ end | ||||
| --   corresponding absolute path | ||||
| ----------------------------------------------------------------------------- | ||||
| local function absolute_path(base_path, relative_path) | ||||
|     if string.sub(relative_path, 1, 1) == "/" then return relative_path end | ||||
|     local path = string.gsub(base_path, "[^/]*$", "") | ||||
|     path = path .. relative_path | ||||
|     path = string.gsub(path, "([^/]*%./)", function (s) | ||||
|         if s ~= "./" then return s else return "" end | ||||
|   if string.sub(relative_path, 1, 1) == "/" then return relative_path end | ||||
|   local path = string.gsub(base_path, "[^/]*$", "") | ||||
|   path = path .. relative_path | ||||
|   path = string.gsub(path, "([^/]*%./)", function(s) | ||||
|     if s ~= "./" then return s else return "" end | ||||
|   end) | ||||
|   path = string.gsub(path, "/%.$", "/") | ||||
|   local reduced | ||||
|   while reduced ~= path do | ||||
|     reduced = path | ||||
|     path = string.gsub(reduced, "([^/]*/%.%./)", function(s) | ||||
|       if s ~= "../../" then return "" else return s end | ||||
|     end) | ||||
|     path = string.gsub(path, "/%.$", "/") | ||||
|     local reduced | ||||
|     while reduced ~= path do | ||||
|         reduced = path | ||||
|         path = string.gsub(reduced, "([^/]*/%.%./)", function (s) | ||||
|             if s ~= "../../" then return "" else return s end | ||||
|         end) | ||||
|     end | ||||
|     path = string.gsub(reduced, "([^/]*/%.%.)$", function (s) | ||||
|         if s ~= "../.." then return "" else return s end | ||||
|     end) | ||||
|     return path | ||||
|   end | ||||
|   path = string.gsub(reduced, "([^/]*/%.%.)$", function(s) | ||||
|     if s ~= "../.." then return "" else return s end | ||||
|   end) | ||||
|   return path | ||||
| end | ||||
|  | ||||
| ----------------------------------------------------------------------------- | ||||
| @@ -122,51 +125,59 @@ end | ||||
| --   the leading '/' in {/<path>} is considered part of <path> | ||||
| ----------------------------------------------------------------------------- | ||||
| function parse(url, default) | ||||
|     -- initialize default parameters | ||||
|     local parsed = {} | ||||
|     for i,v in base.pairs(default or parsed) do parsed[i] = v end | ||||
|     -- empty url is parsed to nil | ||||
|     if not url or url == "" then return nil, "invalid url" end | ||||
|     -- remove whitespace | ||||
|     -- url = string.gsub(url, "%s", "") | ||||
|     -- get fragment | ||||
|     url = string.gsub(url, "#(.*)$", function(f) | ||||
|         parsed.fragment = f | ||||
|         return "" | ||||
|   -- initialize default parameters | ||||
|   local parsed = {} | ||||
|   for i, v in base.pairs(default or parsed) do parsed[i] = v end | ||||
|   -- empty url is parsed to nil | ||||
|   if not url or url == "" then return nil, "invalid url" end | ||||
|   -- remove whitespace | ||||
|   -- url = string.gsub(url, "%s", "") | ||||
|   -- get fragment | ||||
|   url = string.gsub(url, "#(.*)$", function(f) | ||||
|     parsed.fragment = f | ||||
|     return "" | ||||
|   end) | ||||
|   -- get scheme | ||||
|   url = string.gsub(url, "^([%w][%w%+%-%.]*)%:", | ||||
|     function(s) | ||||
|       parsed.scheme = s; return "" | ||||
|     end) | ||||
|     -- get scheme | ||||
|     url = string.gsub(url, "^([%w][%w%+%-%.]*)%:", | ||||
|         function(s) parsed.scheme = s; return "" end) | ||||
|     -- get authority | ||||
|     url = string.gsub(url, "^//([^/]*)", function(n) | ||||
|         parsed.authority = n | ||||
|         return "" | ||||
|   -- get authority | ||||
|   url = string.gsub(url, "^//([^/]*)", function(n) | ||||
|     parsed.authority = n | ||||
|     return "" | ||||
|   end) | ||||
|   -- get query stringing | ||||
|   url = string.gsub(url, "%?(.*)", function(q) | ||||
|     parsed.query = q | ||||
|     return "" | ||||
|   end) | ||||
|   -- get params | ||||
|   url = string.gsub(url, "%;(.*)", function(p) | ||||
|     parsed.params = p | ||||
|     return "" | ||||
|   end) | ||||
|   -- path is whatever was left | ||||
|   if url ~= "" then parsed.path = url end | ||||
|   local authority = parsed.authority | ||||
|   if not authority then return parsed end | ||||
|   authority = string.gsub(authority, "^([^@]*)@", | ||||
|     function(u) | ||||
|       parsed.userinfo = u; return "" | ||||
|     end) | ||||
|     -- get query stringing | ||||
|     url = string.gsub(url, "%?(.*)", function(q) | ||||
|         parsed.query = q | ||||
|         return "" | ||||
|   authority = string.gsub(authority, ":([^:]*)$", | ||||
|     function(p) | ||||
|       parsed.port = p; return "" | ||||
|     end) | ||||
|     -- get params | ||||
|     url = string.gsub(url, "%;(.*)", function(p) | ||||
|         parsed.params = p | ||||
|         return "" | ||||
|   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) | ||||
|     -- path is whatever was left | ||||
|     if url ~= "" then parsed.path = url end | ||||
|     local authority = parsed.authority | ||||
|     if not authority then return parsed end | ||||
|     authority = string.gsub(authority,"^([^@]*)@", | ||||
|         function(u) parsed.userinfo = u; return "" end) | ||||
|     authority = string.gsub(authority, ":([^:]*)$", | ||||
|         function(p) parsed.port = p; return "" end) | ||||
|     if authority ~= "" then parsed.host = authority end | ||||
|     local userinfo = parsed.userinfo | ||||
|     if not userinfo then return parsed end | ||||
|     userinfo = string.gsub(userinfo, ":([^:]*)$", | ||||
|         function(p) parsed.password = p; return "" end) | ||||
|     parsed.user = userinfo | ||||
|     return parsed | ||||
|   parsed.user = userinfo | ||||
|   return parsed | ||||
| end | ||||
|  | ||||
| ----------------------------------------------------------------------------- | ||||
| @@ -178,28 +189,28 @@ end | ||||
| --   a stringing with the corresponding URL | ||||
| ----------------------------------------------------------------------------- | ||||
| function build(parsed) | ||||
|     local ppath = parse_path(parsed.path or "") | ||||
|     local url = build_path(ppath) | ||||
|     if parsed.params then url = url .. ";" .. parsed.params end | ||||
|     if parsed.query then url = url .. "?" .. parsed.query end | ||||
| 	local authority = parsed.authority | ||||
| 	if parsed.host then | ||||
| 		authority = parsed.host | ||||
| 		if parsed.port then authority = authority .. ":" .. parsed.port end | ||||
| 		local userinfo = parsed.userinfo | ||||
| 		if parsed.user then | ||||
| 			userinfo = parsed.user | ||||
| 			if parsed.password then | ||||
| 				userinfo = userinfo .. ":" .. parsed.password | ||||
| 			end | ||||
| 		end | ||||
| 		if userinfo then authority = userinfo .. "@" .. authority end | ||||
| 	end | ||||
|     if authority then url = "//" .. authority .. url end | ||||
|     if parsed.scheme then url = parsed.scheme .. ":" .. url end | ||||
|     if parsed.fragment then url = url .. "#" .. parsed.fragment end | ||||
|     -- url = string.gsub(url, "%s", "") | ||||
|     return url | ||||
|   local ppath = parse_path(parsed.path or "") | ||||
|   local url = build_path(ppath) | ||||
|   if parsed.params then url = url .. ";" .. parsed.params end | ||||
|   if parsed.query then url = url .. "?" .. parsed.query end | ||||
|   local authority = parsed.authority | ||||
|   if parsed.host then | ||||
|     authority = parsed.host | ||||
|     if parsed.port then authority = authority .. ":" .. parsed.port end | ||||
|     local userinfo = parsed.userinfo | ||||
|     if parsed.user then | ||||
|       userinfo = parsed.user | ||||
|       if parsed.password then | ||||
|         userinfo = userinfo .. ":" .. parsed.password | ||||
|       end | ||||
|     end | ||||
|     if userinfo then authority = userinfo .. "@" .. authority end | ||||
|   end | ||||
|   if authority then url = "//" .. authority .. url end | ||||
|   if parsed.scheme then url = parsed.scheme .. ":" .. url end | ||||
|   if parsed.fragment then url = url .. "#" .. parsed.fragment end | ||||
|   -- url = string.gsub(url, "%s", "") | ||||
|   return url | ||||
| end | ||||
|  | ||||
| ----------------------------------------------------------------------------- | ||||
| @@ -211,35 +222,38 @@ end | ||||
| --   corresponding absolute url | ||||
| ----------------------------------------------------------------------------- | ||||
| function absolute(base_url, relative_url) | ||||
|     if base.type(base_url) == "table" then | ||||
|         base_parsed = base_url | ||||
|         base_url = build(base_parsed) | ||||
|     else | ||||
|         base_parsed = parse(base_url) | ||||
|     end | ||||
|     local relative_parsed = parse(relative_url) | ||||
|     if not base_parsed then return relative_url | ||||
|     elseif not relative_parsed then return base_url | ||||
|     elseif relative_parsed.scheme then return relative_url | ||||
|     else | ||||
|         relative_parsed.scheme = base_parsed.scheme | ||||
|         if not relative_parsed.authority then | ||||
|             relative_parsed.authority = base_parsed.authority | ||||
|             if not relative_parsed.path then | ||||
|                 relative_parsed.path = base_parsed.path | ||||
|                 if not relative_parsed.params then | ||||
|                     relative_parsed.params = base_parsed.params | ||||
|                     if not relative_parsed.query then | ||||
|                         relative_parsed.query = base_parsed.query | ||||
|                     end | ||||
|                 end | ||||
|             else     | ||||
|                 relative_parsed.path = absolute_path(base_parsed.path or "", | ||||
|                     relative_parsed.path) | ||||
|             end | ||||
|   if base.type(base_url) == "table" then | ||||
|     base_parsed = base_url | ||||
|     base_url = build(base_parsed) | ||||
|   else | ||||
|     base_parsed = parse(base_url) | ||||
|   end | ||||
|   local relative_parsed = parse(relative_url) | ||||
|   if not base_parsed then | ||||
|     return relative_url | ||||
|   elseif not relative_parsed then | ||||
|     return base_url | ||||
|   elseif relative_parsed.scheme then | ||||
|     return relative_url | ||||
|   else | ||||
|     relative_parsed.scheme = base_parsed.scheme | ||||
|     if not relative_parsed.authority then | ||||
|       relative_parsed.authority = base_parsed.authority | ||||
|       if not relative_parsed.path then | ||||
|         relative_parsed.path = base_parsed.path | ||||
|         if not relative_parsed.params then | ||||
|           relative_parsed.params = base_parsed.params | ||||
|           if not relative_parsed.query then | ||||
|             relative_parsed.query = base_parsed.query | ||||
|           end | ||||
|         end | ||||
|         return build(relative_parsed) | ||||
|       else | ||||
|         relative_parsed.path = absolute_path(base_parsed.path or "", | ||||
|           relative_parsed.path) | ||||
|       end | ||||
|     end | ||||
|     return build(relative_parsed) | ||||
|   end | ||||
| end | ||||
|  | ||||
| ----------------------------------------------------------------------------- | ||||
| @@ -250,16 +264,16 @@ end | ||||
| --   segment: a table with one entry per segment | ||||
| ----------------------------------------------------------------------------- | ||||
| function parse_path(path) | ||||
| 	local parsed = {} | ||||
| 	path = path or "" | ||||
| 	--path = string.gsub(path, "%s", "") | ||||
| 	string.gsub(path, "([^/]+)", function (s) table.insert(parsed, s) end) | ||||
| 	for i = 1, #parsed do | ||||
| 		parsed[i] = unescape(parsed[i]) | ||||
| 	end | ||||
| 	if string.sub(path, 1, 1) == "/" then parsed.is_absolute = 1 end | ||||
| 	if string.sub(path, -1, -1) == "/" then parsed.is_directory = 1 end | ||||
| 	return parsed | ||||
|   local parsed = {} | ||||
|   path = path or "" | ||||
|   --path = string.gsub(path, "%s", "") | ||||
|   string.gsub(path, "([^/]+)", function(s) table.insert(parsed, s) end) | ||||
|   for i = 1, #parsed do | ||||
|     parsed[i] = unescape(parsed[i]) | ||||
|   end | ||||
|   if string.sub(path, 1, 1) == "/" then parsed.is_absolute = 1 end | ||||
|   if string.sub(path, -1, -1) == "/" then parsed.is_directory = 1 end | ||||
|   return parsed | ||||
| end | ||||
|  | ||||
| ----------------------------------------------------------------------------- | ||||
| @@ -271,27 +285,27 @@ end | ||||
| --   path: corresponding path stringing | ||||
| ----------------------------------------------------------------------------- | ||||
| function build_path(parsed, unsafe) | ||||
| 	local path = "" | ||||
| 	local n = #parsed | ||||
| 	if unsafe then | ||||
| 		for i = 1, n-1 do | ||||
| 			path = path .. parsed[i] | ||||
| 			path = path .. "/" | ||||
| 		end | ||||
| 		if n > 0 then | ||||
| 			path = path .. parsed[n] | ||||
| 			if parsed.is_directory then path = path .. "/" end | ||||
| 		end | ||||
| 	else | ||||
| 		for i = 1, n-1 do | ||||
| 			path = path .. protect_segment(parsed[i]) | ||||
| 			path = path .. "/" | ||||
| 		end | ||||
| 		if n > 0 then | ||||
| 			path = path .. protect_segment(parsed[n]) | ||||
| 			if parsed.is_directory then path = path .. "/" end | ||||
| 		end | ||||
| 	end | ||||
| 	if parsed.is_absolute then path = "/" .. path end | ||||
| 	return path | ||||
|   local path = "" | ||||
|   local n = #parsed | ||||
|   if unsafe then | ||||
|     for i = 1, n - 1 do | ||||
|       path = path .. parsed[i] | ||||
|       path = path .. "/" | ||||
|     end | ||||
|     if n > 0 then | ||||
|       path = path .. parsed[n] | ||||
|       if parsed.is_directory then path = path .. "/" end | ||||
|     end | ||||
|   else | ||||
|     for i = 1, n - 1 do | ||||
|       path = path .. protect_segment(parsed[i]) | ||||
|       path = path .. "/" | ||||
|     end | ||||
|     if n > 0 then | ||||
|       path = path .. protect_segment(parsed[n]) | ||||
|       if parsed.is_directory then path = path .. "/" end | ||||
|     end | ||||
|   end | ||||
|   if parsed.is_absolute then path = "/" .. path end | ||||
|   return path | ||||
| end | ||||
|   | ||||
| @@ -1,10 +1,12 @@ | ||||
| -- Sanitize the Lua environment to: | ||||
| --   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 | ||||
| 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 | ||||
| local old_io = io | ||||
| @@ -15,98 +17,99 @@ local old_package = package | ||||
|  | ||||
| -- Remove all global variables except a whitelist | ||||
| local ok_names = tmap { | ||||
| 	'_G', '_bllua_ts', '_bllua_on_unload', '_bllua_on_error', | ||||
| 	'string', 'table', 'math', 'coroutine', 'bit', | ||||
| 	'pairs', 'ipairs', 'next', 'unpack', 'select', | ||||
| 	'error', 'assert', 'pcall', 'xpcall', | ||||
| 	'type', 'tostring', 'tonumber', | ||||
| 	'loadstring', | ||||
| 	'getmetatable', 'setmetatable', | ||||
| 	'rawget', 'rawset', 'rawequal', 'rawlen', | ||||
| 	'module', '_VERSION', | ||||
|   '_G', '_bllua_ts', '_bllua_on_unload', '_bllua_on_error', | ||||
|   'string', 'table', 'math', 'coroutine', 'bit', | ||||
|   'pairs', 'ipairs', 'next', 'unpack', 'select', | ||||
|   'error', 'assert', 'pcall', 'xpcall', | ||||
|   'type', 'tostring', 'tonumber', | ||||
|   'loadstring', | ||||
|   'getmetatable', 'setmetatable', | ||||
|   'rawget', 'rawset', 'rawequal', 'rawlen', | ||||
|   'module', '_VERSION', | ||||
| } | ||||
| local not_ok_names = {} | ||||
| for n, _ in pairs(_G) do | ||||
| 	if not ok_names[n] then | ||||
| 		table.insert(not_ok_names, n) | ||||
| 	end | ||||
|   if not ok_names[n] then | ||||
|     table.insert(not_ok_names, n) | ||||
|   end | ||||
| end | ||||
| for _, n in ipairs(not_ok_names) do | ||||
| 	_G[n] = nil | ||||
|   _G[n] = nil | ||||
| end | ||||
|  | ||||
| -- Sanitize file paths to point only to allowed files within the game directory | ||||
| -- List of allowed directories for reading/writing | ||||
| 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 | ||||
| local allowed_dirs_readonly = tmap { | ||||
| 	'lualib' | ||||
|   'lualib' | ||||
| } | ||||
| -- List of disallowed file extensions - basically executable file extensions | ||||
| -- Note that even without this protection, exploiting would still require somehow | ||||
| --   getting a file within the allowed directories to autorun, | ||||
| --   so this is just a precaution. | ||||
| local disallowed_exts = tmap { | ||||
| 	-- windows | ||||
| 	'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', | ||||
| 	'sct','shb','shs','u3p','vb','vbe','vbs','vbscript','ws','wsf','wsh', | ||||
| 	-- linux | ||||
| 	'csh','ksh','out','run','sh', | ||||
| 	-- mac/other | ||||
| 	'action','apk','app','command','ipa','osx','prg','workflow', | ||||
|   -- windows | ||||
|   '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', | ||||
|   'sct', 'shb', 'shs', 'u3p', 'vb', 'vbe', 'vbs', 'vbscript', 'ws', 'wsf', 'wsh', | ||||
|   -- linux | ||||
|   'csh', 'ksh', 'out', 'run', 'sh', | ||||
|   -- mac/other | ||||
|   'action', 'apk', 'app', 'command', 'ipa', 'osx', 'prg', 'workflow', | ||||
| } | ||||
| -- Arguments: file name (relative to game directory), boolean true if only reading | ||||
| -- Return: clean file path if allowed (or nil if disallowed), | ||||
| --         error string (or nil if allowed) | ||||
| local function safe_path(fn, readonly) | ||||
| 	fn = fn:gsub('\\', '/') | ||||
| 	fn = fn:gsub('^ +', '') | ||||
| 	fn = fn:gsub(' +$', '') | ||||
| 	-- whitelist characters | ||||
| 	local ic = fn:find('[^a-zA-Z0-9_%-/ %.]') | ||||
| 	if ic then | ||||
| 		return nil, 'Filename \''..fn..'\' contains invalid character \''.. | ||||
| 			fn:sub(ic, ic)..'\' at position '..ic | ||||
| 	end | ||||
| 	-- disallow up-dirs, absolute paths, and relative paths | ||||
| 	-- './' and '../' are possible in scripts, because they're processed into | ||||
| 	-- absolute paths in util.lua before reaching here | ||||
| 	if fn:find('^%.') or fn:find('%.%.') or fn:find(':') or fn:find('^/') then | ||||
| 		return nil, 'Filename \''..fn..'\' contains invalid sequence' | ||||
| 	end | ||||
| 	-- allow only whitelisted dirs | ||||
| 	local dir = fn:match('^([^/]+)/') | ||||
| 	if (not dir) or ( | ||||
| 		(not allowed_dirs[dir:lower()]) and | ||||
| 		((not readonly) or (not allowed_dirs_readonly[dir:lower()])) ) then | ||||
| 		return nil, 'filename is in disallowed directory '..(dir or 'nil') | ||||
| 	end | ||||
| 	-- disallow blacklisted extensions or no extension | ||||
| 	local ext = fn:match('%.([^/%.]+)$') | ||||
| 	if (not ext) or (disallowed_exts[ext:lower()]) then | ||||
| 		return nil, 'Filename \''..fn..'\' has disallowed extension \''.. | ||||
| 			(ext or '')..'\'' | ||||
| 	end | ||||
| 	return fn, nil | ||||
|   fn = fn:gsub('\\', '/') | ||||
|   fn = fn:gsub('^ +', '') | ||||
|   fn = fn:gsub(' +$', '') | ||||
|   -- whitelist characters | ||||
|   local ic = fn:find('[^a-zA-Z0-9_%-/ %.]') | ||||
|   if ic then | ||||
|     return nil, 'Filename \'' .. fn .. '\' contains invalid character \'' .. | ||||
|         fn:sub(ic, ic) .. '\' at position ' .. ic | ||||
|   end | ||||
|   -- disallow up-dirs, absolute paths, and relative paths | ||||
|   -- './' and '../' are possible in scripts, because they're processed into | ||||
|   -- absolute paths in util.lua before reaching here | ||||
|   if fn:find('^%.') or fn:find('%.%.') or fn:find(':') or fn:find('^/') then | ||||
|     return nil, 'Filename \'' .. fn .. '\' contains invalid sequence' | ||||
|   end | ||||
|   -- allow only whitelisted dirs | ||||
|   local dir = fn:match('^([^/]+)/') | ||||
|   if (not dir) or ( | ||||
|         (not allowed_dirs[dir:lower()]) and | ||||
|         ((not readonly) or (not allowed_dirs_readonly[dir:lower()]))) then | ||||
|     return nil, 'filename is in disallowed directory ' .. (dir or 'nil') | ||||
|   end | ||||
|   -- disallow blacklisted extensions or no extension | ||||
|   local ext = fn:match('%.([^/%.]+)$') | ||||
|   if (not ext) or (disallowed_exts[ext:lower()]) then | ||||
|     return nil, 'Filename \'' .. fn .. '\' has disallowed extension \'' .. | ||||
|         (ext or '') .. '\'' | ||||
|   end | ||||
|   return fn, nil | ||||
| end | ||||
|  | ||||
| -- Wrap io.open with path sanitization | ||||
| function _bllua_io_open(fn, md) | ||||
| 	md = md or 'r' | ||||
| 	local readonly = md=='r' or md=='rb' | ||||
| 	local fns, err = safe_path(fn, readonly) | ||||
| 	if fns then | ||||
| 		return old_io.open(fns, md) | ||||
| 	else | ||||
| 		return nil, err | ||||
| 	end | ||||
|   md = md or 'r' | ||||
|   local readonly = md == 'r' or md == 'rb' | ||||
|   local fns, err = safe_path(fn, readonly) | ||||
|   if fns then | ||||
|     return old_io.open(fns, md) | ||||
|   else | ||||
|     return nil, err | ||||
|   end | ||||
| end | ||||
|  | ||||
| -- Allow io.type (works on file handles returned by io.open) | ||||
| function _bllua_io_type(f) | ||||
| 	return old_io.type(f) | ||||
|   return old_io.type(f) | ||||
| end | ||||
|  | ||||
| -- Wrap require with a blacklist for unsafe built-in modules | ||||
| @@ -114,29 +117,30 @@ end | ||||
| -- Note that util.lua wraps this and provides 'require', | ||||
| --   only falling back here if the package is not found in user files | ||||
| local disallowed_packages = tmap { | ||||
| 	'ffi', 'debug', 'package', 'io', 'os', | ||||
| 	'_bllua_ts', | ||||
|   'ffi', 'debug', 'package', 'io', 'os', | ||||
|   '_bllua_ts', | ||||
| } | ||||
| function _bllua_requiresecure(name) | ||||
| 	if name:find('[^a-zA-Z0-9_%-%.]') or name:find('%.%.') or | ||||
| 		name:find('^%.') or name:find('%.$') then | ||||
| 		error('require: package name contains invalid character', 3) | ||||
| 	elseif disallowed_packages[name] then | ||||
| 		error('require: attempt to require disallowed module \''..name..'\'', 3) | ||||
| 	else | ||||
| 		-- todo: reimplement require to not use package.* stuff? | ||||
| 		return old_require(name) | ||||
| 	end | ||||
|   if name:find('[^a-zA-Z0-9_%-%.]') or name:find('%.%.') or | ||||
|       name:find('^%.') or name:find('%.$') then | ||||
|     error('require: package name contains invalid character', 3) | ||||
|   elseif disallowed_packages[name] then | ||||
|     error('require: attempt to require disallowed module \'' .. name .. '\'', 3) | ||||
|   else | ||||
|     -- todo: reimplement require to not use package.* stuff? | ||||
|     return old_require(name) | ||||
|   end | ||||
| end | ||||
|  | ||||
| package = { | ||||
| 	seeall = old_package.seeall, | ||||
|   seeall = old_package.seeall, | ||||
| } | ||||
|  | ||||
| -- Provide limited debug | ||||
| debug = { | ||||
| 	traceback = old_debug.traceback, | ||||
| 	getinfo = old_debug.getinfo, | ||||
| 	getfilename = old_debug.getfilename, -- defined in lua.env.lua | ||||
|   traceback = old_debug.traceback, | ||||
|   getinfo = old_debug.getinfo, | ||||
|   getfilename = old_debug.getfilename, -- defined in lua.env.lua | ||||
| } | ||||
|  | ||||
| _bllua_ts.echo('  Executed bllua-env-safe.lua') | ||||
|   | ||||
| @@ -9,32 +9,32 @@ _bllua_on_unload = {} | ||||
|  | ||||
| -- Utility for getting the current filename | ||||
| function debug.getfilename(level) | ||||
| 	if type(level) == 'number' then level = level+1 end | ||||
| 	local info = debug.getinfo(level) | ||||
| 	if not info then return nil end | ||||
| 	local filename = info.source:match('^%-%-%[%[([^%]]+)%]%]') | ||||
| 	return filename | ||||
|   if type(level) == 'number' then level = level + 1 end | ||||
|   local info = debug.getinfo(level) | ||||
|   if not info then return nil end | ||||
|   local filename = info.source:match('^%-%-%[%[([^%]]+)%]%]') | ||||
|   return filename | ||||
| end | ||||
|  | ||||
| -- Called when pcall fails on a ts->lua call, used to print detailed error info | ||||
| function _bllua_on_error(err) | ||||
| 	err = err:match(': (.+)$') or err | ||||
| 	local tracelines = {err} | ||||
| 	local level = 2 | ||||
| 	while true do | ||||
| 		local info = debug.getinfo(level) | ||||
| 		if not info then break end | ||||
| 		local filename = debug.getfilename(level) or info.short_src | ||||
| 		local funcname = info.name | ||||
| 		if funcname=='dofile' then break end | ||||
| 		table.insert(tracelines, string.format('%s:%s in function \'%s\'', | ||||
| 			filename, | ||||
| 			info.currentline==-1 and '' or info.currentline..':', | ||||
| 			funcname | ||||
| 		)) | ||||
| 		level = level+1 | ||||
| 	end | ||||
| 	return table.concat(tracelines, '\n') | ||||
|   err = err:match(': (.+)$') or err | ||||
|   local tracelines = { err } | ||||
|   local level = 2 | ||||
|   while true do | ||||
|     local info = debug.getinfo(level) | ||||
|     if not info then break end | ||||
|     local filename = debug.getfilename(level) or info.short_src | ||||
|     local funcname = info.name | ||||
|     if funcname == 'dofile' then break end | ||||
|     table.insert(tracelines, string.format('%s:%s in function \'%s\'', | ||||
|       filename, | ||||
|       info.currentline == -1 and '' or info.currentline .. ':', | ||||
|       funcname | ||||
|     )) | ||||
|     level = level + 1 | ||||
|   end | ||||
|   return table.concat(tracelines, '\n') | ||||
| end | ||||
|  | ||||
| _bllua_ts.echo('  Executed bllua-env.lua') | ||||
|   | ||||
| @@ -4,129 +4,129 @@ | ||||
|  | ||||
| -- Class hierarchy, adapted from https://notabug.org/Queuenard/blockland-DLL-tools/src/master/class_hierarchy | ||||
| bl.class('SimObject') | ||||
| 	bl.class('ScriptObject', 'SimObject') | ||||
| 	bl.class('SimSet', 'SimObject') | ||||
| 		bl.class('SimGroup', 'SimSet') | ||||
| 			bl.class('GuiControl', 'SimGroup') | ||||
| 				bl.class('GuiTextCtrl'         , 'GuiControl') | ||||
| 				bl.class('GuiSwatchCtrl'       , 'GuiControl') | ||||
| 				bl.class('GuiButtonBaseCtrl'   , 'GuiControl') | ||||
| 				bl.class('GuiArrayCtrl'        , 'GuiControl') | ||||
| 				bl.class('GuiScrollCtrl'       , 'GuiControl') | ||||
| 				bl.class('GuiMouseEventCtrl'   , 'GuiControl') | ||||
| 				bl.class('GuiProgressCtrl'     , 'GuiControl') | ||||
| 				bl.class('GuiSliderCtrl'       , 'GuiControl') | ||||
| 				bl.class('GuiConsoleTextCtrl'  , 'GuiControl') | ||||
| 				bl.class('GuiTSCtrl'           , 'GuiControl') | ||||
| 					bl.class('GuiObjectView', 'GuiTSCtrl') | ||||
| 					bl.class('GameTSCtrl'   , 'GuiTSCtrl') | ||||
| 					bl.class('EditTSCtrl'   , 'GuiTSCtrl') | ||||
| 					bl.class('GuiPlayerView', 'GuiTSCtrl') | ||||
| 				bl.class('GuiShapeNameHud'     , 'GuiControl') | ||||
| 				bl.class('GuiHealthBarHud'     , 'GuiControl') | ||||
| 				bl.class('GuiGraphCtrl'        , 'GuiControl') | ||||
| 				bl.class('GuiInspector'        , 'GuiControl') | ||||
| 				bl.class('GuiChunkedBitmapCtrl', 'GuiControl') | ||||
| 				bl.class('GuiInputCtrl'        , 'GuiControl') | ||||
| 				bl.class('GuiNoMouseCtrl'      , 'GuiControl') | ||||
| 				bl.class('GuiBitmapBorderCtrl' , 'GuiControl') | ||||
| 				bl.class('GuiBackgroundCtrl'   , 'GuiControl') | ||||
| 				bl.class('GuiEditorRuler'      , 'GuiControl') | ||||
| 				bl.class('GuiClockHud'         , 'GuiControl') | ||||
| 				bl.class('GuiEditCtrl'         , 'GuiControl') | ||||
| 				bl.class('GuiFilterCtrl'       , 'GuiControl') | ||||
| 				bl.class('GuiFrameSetCtrl'     , 'GuiControl') | ||||
| 				bl.class('GuiMenuBar'          , 'GuiControl') | ||||
| 				bl.class('GuiMessageVectorCtrl', 'GuiControl') | ||||
| 				bl.class('GuiBitmapCtrl'       , 'GuiControl') | ||||
| 					bl.class('GuiCrossHairHud', 'GuiBitmapCtrl') | ||||
| 			bl.class('ScriptGroup', 'SimGroup') | ||||
| 			bl.class('NetConnection', 'SimGroup') | ||||
| 				bl.class('GameConnection', 'NetConnection') | ||||
| 			bl.class('Path', 'SimGroup') | ||||
| 	bl.class('TCPObject', 'SimObject') | ||||
| 		bl.class('SOCKObject', 'TCPObject') | ||||
| 		bl.class('HTTPObject', 'TCPObject') | ||||
| 	bl.class('SimDataBlock', 'SimObject') | ||||
| 		bl.class('AudioEnvironment'      , 'SimDataBlock') | ||||
| 		bl.class('AudioSampleEnvironment', 'SimDataBlock') | ||||
| 		bl.class('AudioDescription'      , 'SimDataBlock') | ||||
| 		bl.class('GameBaseData'          , 'SimDataBlock') | ||||
| 			bl.class('ShapeBaseData'          , 'GameBaseData') | ||||
| 				bl.class('CameraData'       , 'ShapeBaseData') | ||||
| 				bl.class('ItemData'         , 'ShapeBaseData') | ||||
| 				bl.class('MissionMarkerData', 'ShapeBaseData') | ||||
| 				bl.class('PathCameraData'   , 'ShapeBaseData') | ||||
| 				bl.class('PlayerData'       , 'ShapeBaseData') | ||||
| 				bl.class('StaticShapeData'  , 'ShapeBaseData') | ||||
| 				bl.class('VehicleData'      , 'ShapeBaseData') | ||||
| 					bl.class('FlyingVehicleData' , 'VehicleData') | ||||
| 					bl.class('WheeledVehicleData', 'VehicleData') | ||||
| 			bl.class('DebrisData'             , 'GameBaseData') | ||||
| 			bl.class('ProjectileData'         , 'GameBaseData') | ||||
| 			bl.class('ShapeBaseImageData'     , 'GameBaseData') | ||||
| 			bl.class('TriggerData'            , 'GameBaseData') | ||||
| 			bl.class('ExplosionData'          , 'GameBaseData') | ||||
| 			bl.class('fxLightData'            , 'GameBaseData') | ||||
| 			bl.class('LightningData'          , 'GameBaseData') | ||||
| 			bl.class('ParticleEmitterNodeData', 'GameBaseData') | ||||
| 			bl.class('SplashData'             , 'GameBaseData') | ||||
| 			bl.class('fxDTSBrickData'         , 'GameBaseData') | ||||
| 			bl.class('ParticleEmitterData'    , 'GameBaseData') | ||||
| 		bl.class('WheeledVehicleTire'    , 'SimDataBlock') | ||||
| 		bl.class('WheeledVehicleSpring'  , 'SimDataBlock') | ||||
| 		bl.class('TSShapeConstructor'    , 'SimDataBlock') | ||||
| 		bl.class('AudioProfile'          , 'SimDataBlock') | ||||
| 		bl.class('ParticleData'          , 'SimDataBlock') | ||||
| 	bl.class('MaterialPropertyMap', 'SimObject') | ||||
| 	bl.class('NetObject', 'SimObject') | ||||
| 		bl.class('SceneObject', 'NetObject') | ||||
| 			bl.class('GameBase', 'SceneObject') | ||||
| 				bl.class('ShapeBase', 'GameBase') | ||||
| 					bl.class('MissionMarker', 'ShapeBase') | ||||
| 						bl.class('SpawnSphere'       , 'MissionMarker') | ||||
| 						bl.class('VehicleSpawnMarker', 'MissionMarker') | ||||
| 						bl.class('Waypoint'          , 'MissionMarker') | ||||
| 					bl.class('StaticShape'  , 'ShapeBase') | ||||
| 						bl.class('ScopeAlwaysShape', 'StaticShape') | ||||
| 					bl.class('Player'       , 'ShapeBase') | ||||
| 						bl.class('AIPlayer', 'Player') | ||||
| 					bl.class('Camera'       , 'ShapeBase') | ||||
| 					bl.class('Item'         , 'ShapeBase') | ||||
| 					bl.class('PathCamera'   , 'ShapeBase') | ||||
| 					bl.class('Vehicle'      , 'ShapeBase') | ||||
| 						bl.class('FlyingVehicle' , 'Vehicle') | ||||
| 						bl.class('WheeledVehicle', 'Vehicle') | ||||
| 				bl.class('Explosion'          , 'GameBase') | ||||
| 				bl.class('Splash'             , 'GameBase') | ||||
| 				bl.class('Debris'             , 'GameBase') | ||||
| 				bl.class('Projectile'         , 'GameBase') | ||||
| 				bl.class('Trigger'            , 'GameBase') | ||||
| 				bl.class('fxLight'            , 'GameBase') | ||||
| 				bl.class('Lightning'          , 'GameBase') | ||||
| 				bl.class('ParticleEmitterNode', 'GameBase') | ||||
| 				bl.class('ParticleEmitter'    , 'GameBase') | ||||
| 				bl.class('Precipitation'      , 'GameBase') | ||||
| 			bl.class('TSStatic'      , 'SceneObject') | ||||
| 			bl.class('VehicleBlocker', 'SceneObject') | ||||
| 			bl.class('Marker'        , 'SceneObject') | ||||
| 			bl.class('AudioEmitter'  , 'SceneObject') | ||||
| 			bl.class('PhysicalZone'  , 'SceneObject') | ||||
| 			bl.class('fxDayCycle'    , 'SceneObject') | ||||
| 			bl.class('fxDTSBrick'    , 'SceneObject') | ||||
| 			bl.class('fxPlane'       , 'SceneObject') | ||||
| 			bl.class('fxSunLight'    , 'SceneObject') | ||||
| 			bl.class('Sky'           , 'SceneObject') | ||||
| 			bl.class('SceneRoot'     , 'SceneObject') | ||||
| 		bl.class('Sun', 'NetObject') | ||||
| 	bl.class('GuiCursor', 'SimObject') | ||||
| 	bl.class('ConsoleLogger'    , 'SimObject') | ||||
| 	bl.class('QuotaObject'      , 'SimObject') | ||||
| 	bl.class('FileObject'       , 'SimObject') | ||||
| 	bl.class('BanList'          , 'SimObject') | ||||
| 	bl.class('GuiControlProfile', 'SimObject') | ||||
| 	bl.class('MessageVector'    , 'SimObject') | ||||
| 	bl.class('ActionMap'        , 'SimObject') | ||||
| bl.class('ScriptObject', 'SimObject') | ||||
| bl.class('SimSet', 'SimObject') | ||||
| bl.class('SimGroup', 'SimSet') | ||||
| bl.class('GuiControl', 'SimGroup') | ||||
| bl.class('GuiTextCtrl', 'GuiControl') | ||||
| bl.class('GuiSwatchCtrl', 'GuiControl') | ||||
| bl.class('GuiButtonBaseCtrl', 'GuiControl') | ||||
| bl.class('GuiArrayCtrl', 'GuiControl') | ||||
| bl.class('GuiScrollCtrl', 'GuiControl') | ||||
| bl.class('GuiMouseEventCtrl', 'GuiControl') | ||||
| bl.class('GuiProgressCtrl', 'GuiControl') | ||||
| bl.class('GuiSliderCtrl', 'GuiControl') | ||||
| bl.class('GuiConsoleTextCtrl', 'GuiControl') | ||||
| bl.class('GuiTSCtrl', 'GuiControl') | ||||
| bl.class('GuiObjectView', 'GuiTSCtrl') | ||||
| bl.class('GameTSCtrl', 'GuiTSCtrl') | ||||
| bl.class('EditTSCtrl', 'GuiTSCtrl') | ||||
| bl.class('GuiPlayerView', 'GuiTSCtrl') | ||||
| bl.class('GuiShapeNameHud', 'GuiControl') | ||||
| bl.class('GuiHealthBarHud', 'GuiControl') | ||||
| bl.class('GuiGraphCtrl', 'GuiControl') | ||||
| bl.class('GuiInspector', 'GuiControl') | ||||
| bl.class('GuiChunkedBitmapCtrl', 'GuiControl') | ||||
| bl.class('GuiInputCtrl', 'GuiControl') | ||||
| bl.class('GuiNoMouseCtrl', 'GuiControl') | ||||
| bl.class('GuiBitmapBorderCtrl', 'GuiControl') | ||||
| bl.class('GuiBackgroundCtrl', 'GuiControl') | ||||
| bl.class('GuiEditorRuler', 'GuiControl') | ||||
| bl.class('GuiClockHud', 'GuiControl') | ||||
| bl.class('GuiEditCtrl', 'GuiControl') | ||||
| bl.class('GuiFilterCtrl', 'GuiControl') | ||||
| bl.class('GuiFrameSetCtrl', 'GuiControl') | ||||
| bl.class('GuiMenuBar', 'GuiControl') | ||||
| bl.class('GuiMessageVectorCtrl', 'GuiControl') | ||||
| bl.class('GuiBitmapCtrl', 'GuiControl') | ||||
| bl.class('GuiCrossHairHud', 'GuiBitmapCtrl') | ||||
| bl.class('ScriptGroup', 'SimGroup') | ||||
| bl.class('NetConnection', 'SimGroup') | ||||
| bl.class('GameConnection', 'NetConnection') | ||||
| bl.class('Path', 'SimGroup') | ||||
| bl.class('TCPObject', 'SimObject') | ||||
| bl.class('SOCKObject', 'TCPObject') | ||||
| bl.class('HTTPObject', 'TCPObject') | ||||
| bl.class('SimDataBlock', 'SimObject') | ||||
| bl.class('AudioEnvironment', 'SimDataBlock') | ||||
| bl.class('AudioSampleEnvironment', 'SimDataBlock') | ||||
| bl.class('AudioDescription', 'SimDataBlock') | ||||
| bl.class('GameBaseData', 'SimDataBlock') | ||||
| bl.class('ShapeBaseData', 'GameBaseData') | ||||
| bl.class('CameraData', 'ShapeBaseData') | ||||
| bl.class('ItemData', 'ShapeBaseData') | ||||
| bl.class('MissionMarkerData', 'ShapeBaseData') | ||||
| bl.class('PathCameraData', 'ShapeBaseData') | ||||
| bl.class('PlayerData', 'ShapeBaseData') | ||||
| bl.class('StaticShapeData', 'ShapeBaseData') | ||||
| bl.class('VehicleData', 'ShapeBaseData') | ||||
| bl.class('FlyingVehicleData', 'VehicleData') | ||||
| bl.class('WheeledVehicleData', 'VehicleData') | ||||
| bl.class('DebrisData', 'GameBaseData') | ||||
| bl.class('ProjectileData', 'GameBaseData') | ||||
| bl.class('ShapeBaseImageData', 'GameBaseData') | ||||
| bl.class('TriggerData', 'GameBaseData') | ||||
| bl.class('ExplosionData', 'GameBaseData') | ||||
| bl.class('fxLightData', 'GameBaseData') | ||||
| bl.class('LightningData', 'GameBaseData') | ||||
| bl.class('ParticleEmitterNodeData', 'GameBaseData') | ||||
| bl.class('SplashData', 'GameBaseData') | ||||
| bl.class('fxDTSBrickData', 'GameBaseData') | ||||
| bl.class('ParticleEmitterData', 'GameBaseData') | ||||
| bl.class('WheeledVehicleTire', 'SimDataBlock') | ||||
| bl.class('WheeledVehicleSpring', 'SimDataBlock') | ||||
| bl.class('TSShapeConstructor', 'SimDataBlock') | ||||
| bl.class('AudioProfile', 'SimDataBlock') | ||||
| bl.class('ParticleData', 'SimDataBlock') | ||||
| bl.class('MaterialPropertyMap', 'SimObject') | ||||
| bl.class('NetObject', 'SimObject') | ||||
| bl.class('SceneObject', 'NetObject') | ||||
| bl.class('GameBase', 'SceneObject') | ||||
| bl.class('ShapeBase', 'GameBase') | ||||
| bl.class('MissionMarker', 'ShapeBase') | ||||
| bl.class('SpawnSphere', 'MissionMarker') | ||||
| bl.class('VehicleSpawnMarker', 'MissionMarker') | ||||
| bl.class('Waypoint', 'MissionMarker') | ||||
| bl.class('StaticShape', 'ShapeBase') | ||||
| bl.class('ScopeAlwaysShape', 'StaticShape') | ||||
| bl.class('Player', 'ShapeBase') | ||||
| bl.class('AIPlayer', 'Player') | ||||
| bl.class('Camera', 'ShapeBase') | ||||
| bl.class('Item', 'ShapeBase') | ||||
| bl.class('PathCamera', 'ShapeBase') | ||||
| bl.class('Vehicle', 'ShapeBase') | ||||
| bl.class('FlyingVehicle', 'Vehicle') | ||||
| bl.class('WheeledVehicle', 'Vehicle') | ||||
| bl.class('Explosion', 'GameBase') | ||||
| bl.class('Splash', 'GameBase') | ||||
| bl.class('Debris', 'GameBase') | ||||
| bl.class('Projectile', 'GameBase') | ||||
| bl.class('Trigger', 'GameBase') | ||||
| bl.class('fxLight', 'GameBase') | ||||
| bl.class('Lightning', 'GameBase') | ||||
| bl.class('ParticleEmitterNode', 'GameBase') | ||||
| bl.class('ParticleEmitter', 'GameBase') | ||||
| bl.class('Precipitation', 'GameBase') | ||||
| bl.class('TSStatic', 'SceneObject') | ||||
| bl.class('VehicleBlocker', 'SceneObject') | ||||
| bl.class('Marker', 'SceneObject') | ||||
| bl.class('AudioEmitter', 'SceneObject') | ||||
| bl.class('PhysicalZone', 'SceneObject') | ||||
| bl.class('fxDayCycle', 'SceneObject') | ||||
| bl.class('fxDTSBrick', 'SceneObject') | ||||
| bl.class('fxPlane', 'SceneObject') | ||||
| bl.class('fxSunLight', 'SceneObject') | ||||
| bl.class('Sky', 'SceneObject') | ||||
| bl.class('SceneRoot', 'SceneObject') | ||||
| bl.class('Sun', 'NetObject') | ||||
| bl.class('GuiCursor', 'SimObject') | ||||
| bl.class('ConsoleLogger', 'SimObject') | ||||
| bl.class('QuotaObject', 'SimObject') | ||||
| bl.class('FileObject', 'SimObject') | ||||
| bl.class('BanList', 'SimObject') | ||||
| bl.class('GuiControlProfile', 'SimObject') | ||||
| bl.class('MessageVector', 'SimObject') | ||||
| bl.class('ActionMap', 'SimObject') | ||||
|  | ||||
| -- Auto-generated from game scripts | ||||
| bl.type('ActionMap::blockBind:1', 'object') | ||||
|   | ||||
							
								
								
									
										1564
									
								
								src/util/libbl.lua
									
									
									
									
									
								
							
							
						
						
									
										1564
									
								
								src/util/libbl.lua
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,4 +1,3 @@ | ||||
|  | ||||
| -- 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 only has access to the sandboxed lua environment, just like user code. | ||||
| @@ -8,9 +7,10 @@ ts = _bllua_ts | ||||
| -- Provide limited OS functions | ||||
| os = os or {} | ||||
| ---@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 | ||||
| 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 | ||||
| -- 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. | ||||
| -- Todo someday: actually read the zip in lua? | ||||
| local file_meta = { | ||||
| 	read = function(file, mode) | ||||
| 		file:_init() | ||||
| 		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 mode=='*n' then | ||||
| 			local ws, n = file.data:match('^([ \t\r\n]*)([0-9%.%-e]+)', file.pos) | ||||
| 			if n then | ||||
| 				file.pos = file.pos + #ws + #n | ||||
| 				return n | ||||
| 			else | ||||
| 				return nil | ||||
| 			end | ||||
| 		elseif mode=='*a' then | ||||
| 			local d = file.data:sub(file.pos, #file.data) | ||||
| 			file.pos = #file.data + 1 | ||||
| 			return d | ||||
| 		elseif mode=='*l' then | ||||
| 			local l, ws = file.data:match('^([^\r\n]*)(\r?\n)', file.pos) | ||||
| 			if not l then | ||||
| 				l = file.data:match('^([^\r\n]*)$', file.pos); ws = ''; | ||||
| 				if l=='' then return nil end | ||||
| 			end | ||||
| 			if l then | ||||
| 				file.pos = file.pos + #l + #ws | ||||
| 				return l | ||||
| 			else | ||||
| 				return nil | ||||
| 			end | ||||
| 		elseif type(mode)=='number' then | ||||
| 			local d = file.data:sub(file.pos, file.pos+mode) | ||||
| 			file.pos = file.pos + #d | ||||
| 			return d | ||||
| 		else | ||||
| 			error('File:read: Invalid mode \''..mode..'\'', 2) | ||||
| 		end | ||||
| 	end, | ||||
| 	lines = function(file) | ||||
| 		file:_init() | ||||
| 		return function() | ||||
| 			return file:read('*l') | ||||
| 		end | ||||
| 	end, | ||||
| 	close = function(file) | ||||
| 		if not file._is_open then error('File:close: File is not open', 2) end | ||||
| 		file._is_open = false | ||||
| 	end, | ||||
| 	__index = function(f, k) return rawget(f, k) or getmetatable(f)[k] end, | ||||
| 	_init = function(f) | ||||
| 		if not f.data then | ||||
| 			f.data = _bllua_ts.call('_bllua_ReadEntireFile', f.filename) | ||||
| 		end | ||||
| 	end, | ||||
|   read = function(file, mode) | ||||
|     file:_init() | ||||
|     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 mode == '*n' then | ||||
|       local ws, n = file.data:match('^([ \t\r\n]*)([0-9%.%-e]+)', file.pos) | ||||
|       if n then | ||||
|         file.pos = file.pos + #ws + #n | ||||
|         return n | ||||
|       else | ||||
|         return nil | ||||
|       end | ||||
|     elseif mode == '*a' then | ||||
|       local d = file.data:sub(file.pos, #file.data) | ||||
|       file.pos = #file.data + 1 | ||||
|       return d | ||||
|     elseif mode == '*l' then | ||||
|       local l, ws = file.data:match('^([^\r\n]*)(\r?\n)', file.pos) | ||||
|       if not l then | ||||
|         l = file.data:match('^([^\r\n]*)$', file.pos); ws = ''; | ||||
|         if l == '' then return nil end | ||||
|       end | ||||
|       if l then | ||||
|         file.pos = file.pos + #l + #ws | ||||
|         return l | ||||
|       else | ||||
|         return nil | ||||
|       end | ||||
|     elseif type(mode) == 'number' then | ||||
|       local d = file.data:sub(file.pos, file.pos + mode) | ||||
|       file.pos = file.pos + #d | ||||
|       return d | ||||
|     else | ||||
|       error('File:read: Invalid mode \'' .. mode .. '\'', 2) | ||||
|     end | ||||
|   end, | ||||
|   lines = function(file) | ||||
|     file:_init() | ||||
|     return function() | ||||
|       return file:read('*l') | ||||
|     end | ||||
|   end, | ||||
|   close = function(file) | ||||
|     if not file._is_open then error('File:close: File is not open', 2) end | ||||
|     file._is_open = false | ||||
|   end, | ||||
|   __index = function(f, k) return rawget(f, k) or getmetatable(f)[k] end, | ||||
|   _init = function(f) | ||||
|     if not f.data then | ||||
|       f.data = _bllua_ts.call('_bllua_ReadEntireFile', f.filename) | ||||
|     end | ||||
|   end, | ||||
| } | ||||
| local function new_file_obj(fn) | ||||
| 	local file = { | ||||
| 		_is_file = true, | ||||
| 		_is_open = true, | ||||
| 		pos = 1, | ||||
| 		__index = file_meta.__index, | ||||
| 		filename = fn, | ||||
| 		data = nil, | ||||
| 	} | ||||
| 	setmetatable(file, file_meta) | ||||
| 	return file | ||||
|   local file = { | ||||
|     _is_file = true, | ||||
|     _is_open = true, | ||||
|     pos = 1, | ||||
|     __index = file_meta.__index, | ||||
|     filename = fn, | ||||
|     data = nil, | ||||
|   } | ||||
|   setmetatable(file, file_meta) | ||||
|   return file | ||||
| end | ||||
|  | ||||
| local function tflip(t) local u = {}; for _, n in ipairs(t) do u[n] = true end; return u; end | ||||
| local allowed_zip_dirs = tflip{ | ||||
| 	'add-ons', 'base', 'config', 'saves', 'screenshots', 'shaders' | ||||
| local function tflip(t) | ||||
|   local u = {}; for _, n in ipairs(t) do u[n] = true end; return u; | ||||
| end | ||||
| local allowed_zip_dirs = tflip { | ||||
|   'add-ons', 'base', 'config', 'saves', 'screenshots', 'shaders' | ||||
| } | ||||
| local function io_open_absolute(fn, mode) | ||||
| 	-- if file exists, use original mode | ||||
| 	local res, err = _bllua_io_open(fn, mode) | ||||
| 	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 | ||||
| 	local dir = fn:match('^[^/]+') | ||||
| 	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' | ||||
| 	if not exist then return nil, err end | ||||
| 	 | ||||
| 	if mode~=nil and mode~='r' and mode~='rb' then | ||||
| 		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 fi | ||||
|   -- if file exists, use original mode | ||||
|   local res, err = _bllua_io_open(fn, mode) | ||||
|   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 | ||||
|   local dir = fn:match('^[^/]+') | ||||
|   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' | ||||
|   if not exist then return nil, err end | ||||
|  | ||||
|   if mode ~= nil and mode ~= 'r' and mode ~= 'rb' then | ||||
|     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 fi | ||||
| end | ||||
|  | ||||
| io = io or {} | ||||
| ---@diagnostic disable-next-line: duplicate-set-field | ||||
| function io.open(fn, mode, errn) | ||||
| 	errn = errn or 1 | ||||
| 	 | ||||
| 	-- try to open the file with relative path, otherwise use absolute path | ||||
| 	local curfn = debug.getfilename(errn + 1) or _bllua_ts.getvar('Con::File') | ||||
| 	if curfn == '' then curfn = nil end | ||||
| 	if fn:find('^%.') then | ||||
| 		local relfn = curfn and fn:find('^%./') and | ||||
| 			curfn:gsub('[^/]+$', '')..fn:gsub('^%./', '') | ||||
| 		if relfn then | ||||
| 			local fi, err = io_open_absolute(relfn, mode) | ||||
| 			return fi, err, relfn | ||||
| 		else | ||||
| 			return nil, 'Invalid path', fn | ||||
| 		end | ||||
| 	else | ||||
| 		local fi, err = io_open_absolute(fn, mode) | ||||
| 		return fi, err, fn | ||||
| 	end | ||||
|   errn = errn or 1 | ||||
|  | ||||
|   -- try to open the file with relative path, otherwise use absolute path | ||||
|   local curfn = debug.getfilename(errn + 1) or _bllua_ts.getvar('Con::File') | ||||
|   if curfn == '' then curfn = nil end | ||||
|   if fn:find('^%.') then | ||||
|     local relfn = curfn and fn:find('^%./') and | ||||
|         curfn:gsub('[^/]+$', '') .. fn:gsub('^%./', '') | ||||
|     if relfn then | ||||
|       local fi, err = io_open_absolute(relfn, mode) | ||||
|       return fi, err, relfn | ||||
|     else | ||||
|       return nil, 'Invalid path', fn | ||||
|     end | ||||
|   else | ||||
|     local fi, err = io_open_absolute(fn, mode) | ||||
|     return fi, err, fn | ||||
|   end | ||||
| end | ||||
|  | ||||
| ---@diagnostic disable-next-line: duplicate-set-field | ||||
| function io.lines(fn) | ||||
| 	local fi, err, fn2 = io.open(fn, nil, 2) | ||||
| 	if not fi then error('Error opening file \''..fn2..'\': '..err, 2) end | ||||
| 	return fi:lines() | ||||
|   local fi, err, fn2 = io.open(fn, nil, 2) | ||||
|   if not fi then error('Error opening file \'' .. fn2 .. '\': ' .. err, 2) end | ||||
|   return fi:lines() | ||||
| end | ||||
|  | ||||
| ---@diagnostic disable-next-line: duplicate-set-field | ||||
| function io.type(f) | ||||
| ---@diagnostic disable-next-line: undefined-field | ||||
| 	if type(f)=='table' and f._is_file then | ||||
| ---@diagnostic disable-next-line: undefined-field | ||||
| 		return f._is_open and 'file' or 'closed file' | ||||
| 	else | ||||
| 		return _bllua_io_type(f) | ||||
| 	end | ||||
|   ---@diagnostic disable-next-line: undefined-field | ||||
|   if type(f) == 'table' and f._is_file then | ||||
|     ---@diagnostic disable-next-line: undefined-field | ||||
|     return f._is_open and 'file' or 'closed file' | ||||
|   else | ||||
|     return _bllua_io_type(f) | ||||
|   end | ||||
| end | ||||
|  | ||||
| -- provide dofile | ||||
| function dofile(fn, errn) | ||||
| 	errn = errn or 1 | ||||
| 	 | ||||
| 	local fi, err, fn2 = io.open(fn, 'r', errn+1) | ||||
| 	if not fi then error('Error executing file \''..fn2..'\': '..err, errn+1) end | ||||
| 	 | ||||
| 	print('Executing '..fn2) | ||||
| 	local text = fi:read('*a') | ||||
| 	fi:close() | ||||
| 	return assert(loadstring('--[['..fn2..']]'..text))() | ||||
|   errn = errn or 1 | ||||
|  | ||||
|   local fi, err, fn2 = io.open(fn, 'r', errn + 1) | ||||
|   if not fi then error('Error executing file \'' .. fn2 .. '\': ' .. err, errn + 1) end | ||||
|  | ||||
|   print('Executing ' .. fn2) | ||||
|   local text = fi:read('*a') | ||||
|   fi:close() | ||||
|   return assert(loadstring('--[[' .. fn2 .. ']]' .. text))() | ||||
| end | ||||
|  | ||||
| -- provide require (just a wrapper for dofile) | ||||
| @@ -165,63 +170,67 @@ end | ||||
| --   blockland directory | ||||
| --   current add-on | ||||
| local function file_exists(fn, errn) | ||||
| 	local fi, err, fn2 = io.open(fn, 'r', errn+1) | ||||
| 	if fi then | ||||
| 		fi:close() | ||||
| 		return fn2 | ||||
| 	else | ||||
| 		return nil | ||||
| 	end | ||||
|   local fi, err, fn2 = io.open(fn, 'r', errn + 1) | ||||
|   if fi then | ||||
|     fi:close() | ||||
|     return fn2 | ||||
|   else | ||||
|     return nil | ||||
|   end | ||||
| end | ||||
| local require_memo = {} | ||||
| function require(mod) | ||||
| 	if require_memo[mod] then return unpack(require_memo[mod]) end | ||||
| 	local fp = mod:gsub('%.', '/') | ||||
| 	local fns = { | ||||
| 		'./'..fp..'.lua',      -- local file | ||||
| 		'./'..fp..'/init.lua', -- local library | ||||
| 		fp..'.lua',            -- global file | ||||
| 		fp..'/init.lua',       -- global library | ||||
| 	} | ||||
| 	if fp:lower():find('^add-ons/') then | ||||
| 		local addonpath = fp:lower():match('^add-ons/[^/]+')..'/' | ||||
| 		table.insert(fns, addonpath..fp..'.lua')      -- add-on file | ||||
| 		table.insert(fns, addonpath..fp..'/init.lua') -- add-on library | ||||
| 	end | ||||
| 	for _,fn in ipairs(fns) do | ||||
| 		local fne = file_exists(fn, 2) | ||||
| 		if fne then | ||||
| 			local res = {dofile(fne, 2)} | ||||
| 			require_memo[mod] = res | ||||
| 			return unpack(res) | ||||
| 		end | ||||
| 	end | ||||
| 	return _bllua_requiresecure(mod) | ||||
|   if require_memo[mod] then return unpack(require_memo[mod]) end | ||||
|   local fp = mod:gsub('%.', '/') | ||||
|   local fns = { | ||||
|     './' .. fp .. '.lua', -- local file | ||||
|     './' .. fp .. '/init.lua', -- local library | ||||
|     fp .. '.lua',        -- global file | ||||
|     fp .. '/init.lua',   -- global library | ||||
|   } | ||||
|   if fp:lower():find('^add-ons/') then | ||||
|     local addonpath = fp:lower():match('^add-ons/[^/]+') .. '/' | ||||
|     table.insert(fns, addonpath .. fp .. '.lua') -- add-on file | ||||
|     table.insert(fns, addonpath .. fp .. '/init.lua') -- add-on library | ||||
|   end | ||||
|   for _, fn in ipairs(fns) do | ||||
|     local fne = file_exists(fn, 2) | ||||
|     if fne then | ||||
|       local res = { dofile(fne, 2) } | ||||
|       require_memo[mod] = res | ||||
|       return unpack(res) | ||||
|     end | ||||
|   end | ||||
|   return _bllua_requiresecure(mod) | ||||
| end | ||||
|  | ||||
| -- Exposure to TS | ||||
| function _bllua_getvar(name) return _G[name] end | ||||
|  | ||||
| function _bllua_setvar(name, val) _G[name] = val end | ||||
|  | ||||
| function _bllua_eval(code) return loadstring(code)() end | ||||
|  | ||||
| function _bllua_exec(fn) return dofile(fn, 2) end | ||||
|  | ||||
| local function isValidCode(code) | ||||
| 	local f,e = loadstring(code) | ||||
| 	return f~=nil | ||||
|   local f, e = loadstring(code) | ||||
|   return f ~= nil | ||||
| end | ||||
| function _bllua_smarteval(code) | ||||
| 	if (not code:find('^print%(')) and isValidCode('print('..code..')') then | ||||
| 		code = 'print('..code..')' end | ||||
| 	local f,e = loadstring(code) | ||||
| 	if f then | ||||
| 		return f() | ||||
| 	else | ||||
| 		print(e) | ||||
| 	end | ||||
|   if (not code:find('^print%(')) and isValidCode('print(' .. code .. ')') then | ||||
|     code = 'print(' .. code .. ')' | ||||
|   end | ||||
|   local f, e = loadstring(code) | ||||
|   if f then | ||||
|     return f() | ||||
|   else | ||||
|     print(e) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function ts.setvar(name, val) | ||||
| 	_bllua_ts.call('_bllua_set_var', name, val) | ||||
|   _bllua_ts.call('_bllua_set_var', name, val) | ||||
| end | ||||
|  | ||||
| _bllua_ts.call('echo', '  Executed libts-lua.lua') | ||||
|   | ||||
							
								
								
									
										555
									
								
								src/util/std.lua
									
									
									
									
									
								
							
							
						
						
									
										555
									
								
								src/util/std.lua
									
									
									
									
									
								
							| @@ -1,347 +1,374 @@ | ||||
|  | ||||
| -- Basic functionality that should be standard in Lua | ||||
|  | ||||
|  | ||||
| -- Table / List | ||||
| -- Whether a table contains no keys | ||||
| function table.empty(t) | ||||
| 	return next(t)~=nil | ||||
|   return next(t) ~= nil | ||||
| end | ||||
|  | ||||
| -- Apply a function to each key in a table | ||||
| function table.map(f, ...) | ||||
| 	local ts = {...} | ||||
| 	local u = {} | ||||
| 	for k,_ in pairs(ts[1]) do | ||||
| 		local args = {} | ||||
| 		for j=1,#ts do args[j] = ts[j][i] end | ||||
| 		u[i] = f(unpack(args)) | ||||
| 	end | ||||
| 	return u | ||||
|   local ts = { ... } | ||||
|   local u = {} | ||||
|   for k, _ in pairs(ts[1]) do | ||||
|     local args = {} | ||||
|     for j = 1, #ts do args[j] = ts[j][i] end | ||||
|     u[i] = f(unpack(args)) | ||||
|   end | ||||
|   return u | ||||
| end | ||||
|  | ||||
| function table.map_list(f, ...) | ||||
| 	local ts = {...} | ||||
| 	local u = {} | ||||
| 	for i=1,#ts[1] do | ||||
| 		local args = {} | ||||
| 		for j=1,#ts do args[j] = ts[j][i] end | ||||
| 		u[i] = f(unpack(args)) | ||||
| 	end | ||||
| 	return u | ||||
|   local ts = { ... } | ||||
|   local u = {} | ||||
|   for i = 1, #ts[1] do | ||||
|     local args = {} | ||||
|     for j = 1, #ts do args[j] = ts[j][i] end | ||||
|     u[i] = f(unpack(args)) | ||||
|   end | ||||
|   return u | ||||
| end | ||||
|  | ||||
| -- Swap keys/values | ||||
| function table.swap(t) | ||||
| 	local u = {} | ||||
| 	for k,v in pairs(t) do u[v] = k end | ||||
| 	return u | ||||
|   local u = {} | ||||
|   for k, v in pairs(t) do u[v] = k end | ||||
|   return u | ||||
| end | ||||
|  | ||||
| -- Reverse a list | ||||
| function table.reverse(l) | ||||
| 	local m = {} | ||||
| 	for i=1,#l do m[#l-i+1] = l[i] end | ||||
| 	return m | ||||
|   local m = {} | ||||
|   for i = 1, #l do m[#l - i + 1] = l[i] end | ||||
|   return m | ||||
| end | ||||
|  | ||||
| -- Whether a table is a list/array (has only monotonic integer keys) | ||||
| function table.islist(t) | ||||
| 	local n = 0 | ||||
| 	for i,_ in pairs(t) do | ||||
| 		if type(i)~='number' or i%1~=0 then return false end | ||||
| 		n = n+1 | ||||
| 	end | ||||
| 	return n==#t | ||||
|   local n = 0 | ||||
|   for i, _ in pairs(t) do | ||||
|     if type(i) ~= 'number' or i % 1 ~= 0 then return false end | ||||
|     n = n + 1 | ||||
|   end | ||||
|   return n == #t | ||||
| end | ||||
|  | ||||
| -- Append contents of other tables to first table | ||||
| function table.append(t, ...) | ||||
| 	local a = {...} | ||||
| 	for _,u in ipairs(a) do | ||||
| 		for _,v in ipairs(u) do table.insert(t,v) end | ||||
| 	end | ||||
| 	return t | ||||
|   local a = { ... } | ||||
|   for _, u in ipairs(a) do | ||||
|     for _, v in ipairs(u) do table.insert(t, v) end | ||||
|   end | ||||
|   return t | ||||
| end | ||||
|  | ||||
| -- Create a new table containing all keys from any number of tables | ||||
| -- latter tables in the arg list override prior ones | ||||
| -- overlaps, NOT appends, integer keys | ||||
| function table.join(...) | ||||
| 	local ts = {...} | ||||
| 	local w = {} | ||||
| 	for _,t in ipairs(ts) do | ||||
| 		for k,v in pairs(t) do w[k] = v end | ||||
| 	end | ||||
| 	return w | ||||
|   local ts = { ... } | ||||
|   local w = {} | ||||
|   for _, t in ipairs(ts) do | ||||
|     for k, v in pairs(t) do w[k] = v end | ||||
|   end | ||||
|   return w | ||||
| end | ||||
|  | ||||
| -- Whether a table contains a certain value in any key | ||||
| function table.contains(t,s) | ||||
| 	for _,v in pairs(t) do | ||||
| 		if v==s then return true end | ||||
| 	end | ||||
| 	return false | ||||
| function table.contains(t, s) | ||||
|   for _, v in pairs(t) do | ||||
|     if v == s then return true end | ||||
|   end | ||||
|   return false | ||||
| end | ||||
| function table.contains_list(t,s) | ||||
| 	for _,v in ipairs(t) do | ||||
| 		if v==s then return true end | ||||
| 	end | ||||
| 	return false | ||||
|  | ||||
| function table.contains_list(t, s) | ||||
|   for _, v in ipairs(t) do | ||||
|     if v == s then return true end | ||||
|   end | ||||
|   return false | ||||
| end | ||||
|  | ||||
| -- Copy a table to another table | ||||
| function table.copy(t) | ||||
| 	local u = {} | ||||
| 	for k,v in pairs(t) do u[k] = v end | ||||
| 	return u | ||||
|   local u = {} | ||||
|   for k, v in pairs(t) do u[k] = v end | ||||
|   return u | ||||
| end | ||||
|  | ||||
| function table.copy_list(l) | ||||
| 	local m = {} | ||||
| 	for i,v in ipairs(l) do m[i] = v end | ||||
| 	return m | ||||
|   local m = {} | ||||
|   for i, v in ipairs(l) do m[i] = v end | ||||
|   return m | ||||
| end | ||||
|  | ||||
| -- Sort a table in a new copy | ||||
| function table.sortcopy(t, f) | ||||
| 	local u = table.copy_list(t) | ||||
| 	table.sort(u, f) | ||||
| 	return u | ||||
|   local u = table.copy_list(t) | ||||
|   table.sort(u, f) | ||||
|   return u | ||||
| end | ||||
|  | ||||
| -- Remove a value from a table | ||||
| function table.removevalue(t, r) | ||||
| 	local rem = {} | ||||
| 	for k,v in pairs(t) do | ||||
| 		if v==r then table.insert(rem, k) end | ||||
| 	end | ||||
| 	for _,k in ipairs(rem) do t[k] = nil end | ||||
|   local rem = {} | ||||
|   for k, v in pairs(t) do | ||||
|     if v == r then table.insert(rem, k) end | ||||
|   end | ||||
|   for _, k in ipairs(rem) do t[k] = nil end | ||||
| end | ||||
|  | ||||
| function table.removevalue_list(t, r) | ||||
| 	for i = #t, 1, -1 do | ||||
| 		if t[i]==r then | ||||
| 			table.remove(t, i) | ||||
| 		end | ||||
| 	end | ||||
|   for i = #t, 1, -1 do | ||||
|     if t[i] == r then | ||||
|       table.remove(t, i) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| -- Export tables into formatted executable strings | ||||
| local function tabs(tabLevel) | ||||
| 	return ('  '):rep(tabLevel) | ||||
|   return ('  '):rep(tabLevel) | ||||
| end | ||||
| local valueToString | ||||
| local function tableToString(t, tabLevel, seen) | ||||
| 	if type(t)~='table' or (getmetatable(t) and getmetatable(t).__tostring) then | ||||
| 		return tostring(t) | ||||
| 	elseif table.islist(t) then | ||||
| 		if #t==0 then | ||||
| 			return '{}' | ||||
| 		else | ||||
| 			local strs = {} | ||||
| 			local containsTables = false | ||||
| 			for _,v in ipairs(t) do | ||||
| 				if type(v)=='table' then containsTables = true end | ||||
| 				table.insert(strs, valueToString(v, tabLevel+1, seen)..',') | ||||
| 			end | ||||
| 			if containsTables or #t>3 then | ||||
| 				return '{\n'..tabs(tabLevel+1) | ||||
| 					..table.concat(strs, '\n'..tabs(tabLevel+1)) | ||||
| 					..'\n'..tabs(tabLevel)..'}' | ||||
| 			else | ||||
| 				return '{ '..table.concat(strs, ' ')..' }' | ||||
| 			end | ||||
| 		end | ||||
| 	else | ||||
| 		local containsNonStringKeys = false | ||||
| 		for k,v in pairs(t) do | ||||
| 			if type(k)~='string' or k:find('[^a-zA-Z0-9_]') then | ||||
| 				containsNonStringKeys = true | ||||
| 			elseif type(k)=='table' then | ||||
| 				error('table.tostring: table contains a table as key, cannot serialize') | ||||
| 			end | ||||
| 		end | ||||
| 		local strs = {} | ||||
| 		if containsNonStringKeys then | ||||
| 			for k,v in pairs(t) do | ||||
| 				table.insert(strs, '\n'..tabs(tabLevel+1) | ||||
| 					..'['..valueToString(k, tabLevel+1, seen)..'] = ' | ||||
| 					..valueToString(v, tabLevel+1, seen)..',') | ||||
| 			end | ||||
| 		else | ||||
| 			for k,v in pairs(t) do | ||||
| 				table.insert(strs, '\n'..tabs(tabLevel+1) | ||||
| 					..k..' = '..valueToString(v, tabLevel+1, seen)..',') | ||||
| 			end | ||||
| 		end | ||||
| 		return '{'..table.concat(strs)..'\n'..tabs(tabLevel)..'}' | ||||
| 	end | ||||
|   if type(t) ~= 'table' or (getmetatable(t) and getmetatable(t).__tostring) then | ||||
|     return tostring(t) | ||||
|   elseif table.islist(t) then | ||||
|     if #t == 0 then | ||||
|       return '{}' | ||||
|     else | ||||
|       local strs = {} | ||||
|       local containsTables = false | ||||
|       for _, v in ipairs(t) do | ||||
|         if type(v) == 'table' then containsTables = true end | ||||
|         table.insert(strs, valueToString(v, tabLevel + 1, seen) .. ',') | ||||
|       end | ||||
|       if containsTables or #t > 3 then | ||||
|         return '{\n' .. tabs(tabLevel + 1) | ||||
|             .. table.concat(strs, '\n' .. tabs(tabLevel + 1)) | ||||
|             .. '\n' .. tabs(tabLevel) .. '}' | ||||
|       else | ||||
|         return '{ ' .. table.concat(strs, ' ') .. ' }' | ||||
|       end | ||||
|     end | ||||
|   else | ||||
|     local containsNonStringKeys = false | ||||
|     for k, v in pairs(t) do | ||||
|       if type(k) ~= 'string' or k:find('[^a-zA-Z0-9_]') then | ||||
|         containsNonStringKeys = true | ||||
|       elseif type(k) == 'table' then | ||||
|         error('table.tostring: table contains a table as key, cannot serialize') | ||||
|       end | ||||
|     end | ||||
|     local strs = {} | ||||
|     if containsNonStringKeys then | ||||
|       for k, v in pairs(t) do | ||||
|         table.insert(strs, '\n' .. tabs(tabLevel + 1) | ||||
|           .. '[' .. valueToString(k, tabLevel + 1, seen) .. '] = ' | ||||
|           .. valueToString(v, tabLevel + 1, seen) .. ',') | ||||
|       end | ||||
|     else | ||||
|       for k, v in pairs(t) do | ||||
|         table.insert(strs, '\n' .. tabs(tabLevel + 1) | ||||
|           .. k .. ' = ' .. valueToString(v, tabLevel + 1, seen) .. ',') | ||||
|       end | ||||
|     end | ||||
|     return '{' .. table.concat(strs) .. '\n' .. tabs(tabLevel) .. '}' | ||||
|   end | ||||
| end | ||||
| valueToString = function(v, tabLevel, seen) | ||||
| 	local t = type(v) | ||||
| 	if t=='table' then | ||||
| 		if seen[v] then | ||||
| 			return 'nil --[[ already seen: '..tostring(v)..' ]]' | ||||
| 		else | ||||
| 			seen[v] = true | ||||
| 			return tableToString(v, tabLevel, seen) | ||||
| 		end | ||||
| 	elseif t=='string' then | ||||
| 		return '\''..string.escape(v)..'\'' | ||||
| 	elseif t=='number' or t=='boolean' then | ||||
| 		return tostring(v) | ||||
| 	else | ||||
| 		--error('table.tostring: table contains a '..t..' value, cannot serialize') | ||||
| 		return 'nil --[[ cannot serialize '..t..': '..tostring(v)..' ]]' | ||||
| 	end | ||||
|   local t = type(v) | ||||
|   if t == 'table' then | ||||
|     if seen[v] then | ||||
|       return 'nil --[[ already seen: ' .. tostring(v) .. ' ]]' | ||||
|     else | ||||
|       seen[v] = true | ||||
|       return tableToString(v, tabLevel, seen) | ||||
|     end | ||||
|   elseif t == 'string' then | ||||
|     return '\'' .. string.escape(v) .. '\'' | ||||
|   elseif t == 'number' or t == 'boolean' then | ||||
|     return tostring(v) | ||||
|   else | ||||
|     --error('table.tostring: table contains a '..t..' value, cannot serialize') | ||||
|     return 'nil --[[ cannot serialize ' .. t .. ': ' .. tostring(v) .. ' ]]' | ||||
|   end | ||||
| end | ||||
| function table.tostring(t) | ||||
| 	return tableToString(t, 0, {}) | ||||
|   return tableToString(t, 0, {}) | ||||
| end | ||||
|  | ||||
|  | ||||
| -- String | ||||
|  | ||||
| -- Split string into table by separator | ||||
| -- or by chars if no separator given | ||||
| -- if regex is not true, sep is treated as a regex pattern | ||||
| function string.split(str, sep, noregex) | ||||
| 	if type(str)~='string' then | ||||
| 		error('string.split: argument #1: expected string, got '..type(str), 2) end | ||||
| 	if sep==nil or sep=='' then | ||||
| 		local t = {} | ||||
| 		local ns = #str | ||||
| 		for x = 1, ns do | ||||
| 			table.insert(t, str:sub(x, x)) | ||||
| 		end | ||||
| 		return t | ||||
| 	elseif type(sep)=='string' then | ||||
| 		local t = {} | ||||
| 		if #str>0 then | ||||
| 			local first = 1 | ||||
| 			while true do | ||||
| 				local last, newfirst = str:find(sep, first, noregex) | ||||
| 				if not last then break end | ||||
| 				table.insert(t, str:sub(first, last-1)) | ||||
| 				first = newfirst+1 | ||||
| 			end | ||||
| 			table.insert(t, str:sub(first, #str)) | ||||
| 		end | ||||
| 		return t | ||||
| 	else | ||||
| 		error( | ||||
| 			'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) | ||||
|   if type(str) ~= 'string' then | ||||
|     error('string.split: argument #1: expected string, got ' .. type(str), 2) | ||||
|   end | ||||
|   if sep == nil or sep == '' then | ||||
|     local t = {} | ||||
|     local ns = #str | ||||
|     for x = 1, ns do | ||||
|       table.insert(t, str:sub(x, x)) | ||||
|     end | ||||
|     return t | ||||
|   elseif type(sep) == 'string' then | ||||
|     local t = {} | ||||
|     if #str > 0 then | ||||
|       local first = 1 | ||||
|       while true do | ||||
|         local last, newfirst = str:find(sep, first, noregex) | ||||
|         if not last then break end | ||||
|         table.insert(t, str:sub(first, last - 1)) | ||||
|         first = newfirst + 1 | ||||
|       end | ||||
|       table.insert(t, str:sub(first, #str)) | ||||
|     end | ||||
|     return t | ||||
|   else | ||||
|     error( | ||||
|       '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 | ||||
|  | ||||
| -- IO | ||||
|  | ||||
| io = io or {} | ||||
| -- Read entire file at once, return nil,err if access failed | ||||
| function io.readall(filename) | ||||
| 	local fi,err = io.open(filename, 'rb') | ||||
| 	if not fi then return nil,err end | ||||
| 	local s = fi:read("*a") | ||||
| 	fi:close() | ||||
| 	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 | ||||
|   local fi, err = io.open(filename, 'rb') | ||||
|   if not fi then return nil, err end | ||||
|   local s = fi:read("*a") | ||||
|   fi:close() | ||||
|   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 | ||||
|  | ||||
| -- Math | ||||
|  | ||||
| -- Round | ||||
| function math.round(x) | ||||
| 	return math.floor(x+0.5) | ||||
|   return math.floor(x + 0.5) | ||||
| end | ||||
|  | ||||
| -- Mod that accounts for floating point inaccuracy | ||||
| function math.mod(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 | ||||
| 	else return m end | ||||
| function math.mod(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 | ||||
|   else | ||||
|     return m | ||||
|   end | ||||
| end | ||||
|  | ||||
| -- Clamp value between min and max | ||||
| function math.clamp(v, n, x) | ||||
| 	return math.min(x, math.max(v, n)) | ||||
|   return math.min(x, math.max(v, n)) | ||||
| end | ||||
|   | ||||
| @@ -1,218 +1,237 @@ | ||||
|  | ||||
| -- Vector math class with operators | ||||
| local vector_meta | ||||
| local vector_new | ||||
| local function vector_check(v, n, name, argn) | ||||
| 	if not v.__is_vector then | ||||
| 		error('vector '..name..': argument #'..(argn or 1) | ||||
| 			..': expected vector, got '..type(v), n+1) end | ||||
|   if not v.__is_vector then | ||||
|     error('vector ' .. name .. ': argument #' .. (argn or 1) | ||||
|       .. ': expected vector, got ' .. type(v), n + 1) | ||||
|   end | ||||
| end | ||||
| local function vector_checksamelen(v1, v2, name) | ||||
| 	vector_check(v1, 3, name, 1) | ||||
| 	vector_check(v2, 3, name, 2) | ||||
| 	if #v1~=#v2 then | ||||
| 		error('vector '..name..': vector lengths do not match (lengths are ' | ||||
| 			..#v1..' and '..#v2..')', 3) end | ||||
| 	return #v1 | ||||
|   vector_check(v1, 3, name, 1) | ||||
|   vector_check(v2, 3, name, 2) | ||||
|   if #v1 ~= #v2 then | ||||
|     error('vector ' .. name .. ': vector lengths do not match (lengths are ' | ||||
|       .. #v1 .. ' and ' .. #v2 .. ')', 3) | ||||
|   end | ||||
|   return #v1 | ||||
| end | ||||
| local function vector_checklen(v1, v2, name, len) | ||||
| 	vector_check(v1, 3, name, 1) | ||||
| 	vector_check(v2, 3, name, 2) | ||||
| 	if #v1~=len or #v2~=len then | ||||
| 		error('vector '..name..': vector lengths are not '..len..' (lengths are ' | ||||
| 			..#v1..' and '..#v2..')', 3) end | ||||
|   vector_check(v1, 3, name, 1) | ||||
|   vector_check(v2, 3, name, 2) | ||||
|   if #v1 ~= len or #v2 ~= len then | ||||
|     error('vector ' .. name .. ': vector lengths are not ' .. len .. ' (lengths are ' | ||||
|       .. #v1 .. ' and ' .. #v2 .. ')', 3) | ||||
|   end | ||||
| end | ||||
| local function vector_opnnn(name, op) | ||||
| 	return function(v1, v2) | ||||
| 		local len = vector_checksamelen(v1, v2, name) | ||||
| 		local v3 = {} | ||||
| 		for i = 1, len do | ||||
| 			v3[i] = op(v1[i], v2[i]) | ||||
| 		end | ||||
| 		return vector_new(v3) | ||||
| 	end | ||||
|   return function(v1, v2) | ||||
|     local len = vector_checksamelen(v1, v2, name) | ||||
|     local v3 = {} | ||||
|     for i = 1, len do | ||||
|       v3[i] = op(v1[i], v2[i]) | ||||
|     end | ||||
|     return vector_new(v3) | ||||
|   end | ||||
| end | ||||
| local function vector_opnxn(name, op) | ||||
| 	return function(v1, v2) | ||||
| 		local v1v = type(v1)=='table' and v1.__is_vector | ||||
| 		local v2v = type(v2)=='table' and v2.__is_vector | ||||
| 		if v1v and v2v then | ||||
| 			local len = vector_checksamelen(v1, v2, name) | ||||
| 			local v3 = {} | ||||
| 			for i = 1, len do | ||||
| 				v3[i] = op(v1[i], v2[i]) | ||||
| 			end | ||||
| 			return vector_new(v3) | ||||
| 		else | ||||
| 			if v2v then v1,v2 = v2,v1 end | ||||
| 			local len = #v1 | ||||
| 			local v3 = {} | ||||
| 			for i = 1, len do | ||||
| 				v3[i] = op(v1[i], v2) | ||||
| 			end | ||||
| 			return vector_new(v3) | ||||
| 		end | ||||
| 	end | ||||
|   return function(v1, v2) | ||||
|     local v1v = type(v1) == 'table' and v1.__is_vector | ||||
|     local v2v = type(v2) == 'table' and v2.__is_vector | ||||
|     if v1v and v2v then | ||||
|       local len = vector_checksamelen(v1, v2, name) | ||||
|       local v3 = {} | ||||
|       for i = 1, len do | ||||
|         v3[i] = op(v1[i], v2[i]) | ||||
|       end | ||||
|       return vector_new(v3) | ||||
|     else | ||||
|       if v2v then v1, v2 = v2, v1 end | ||||
|       local len = #v1 | ||||
|       local v3 = {} | ||||
|       for i = 1, len do | ||||
|         v3[i] = op(v1[i], v2) | ||||
|       end | ||||
|       return vector_new(v3) | ||||
|     end | ||||
|   end | ||||
| end | ||||
| local function vector_opn0n(name, op) | ||||
| 	return function(v1) | ||||
| 		--vector_check(v1, 1, name) | ||||
| 		local len = #v1 | ||||
| 		local v2 = {} | ||||
| 		for i = 1, len do | ||||
| 			v2[i] = op(v1[i]) | ||||
| 		end | ||||
| 		return vector_new(v2) | ||||
| 	end | ||||
|   return function(v1) | ||||
|     --vector_check(v1, 1, name) | ||||
|     local len = #v1 | ||||
|     local v2 = {} | ||||
|     for i = 1, len do | ||||
|       v2[i] = op(v1[i]) | ||||
|     end | ||||
|     return vector_new(v2) | ||||
|   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 = { | ||||
| 	__is_vector = true, | ||||
| 	__index = function(t, k) | ||||
| 		if tonumber(k) then return rawget(t, k) | ||||
| 		elseif vector_indices[k] then return rawget(t, vector_indices[k]) | ||||
| 		else return getmetatable(t)[k] | ||||
| 		end | ||||
| 	end, | ||||
| 	__newindex = function(t, k, v) | ||||
| 		if tonumber(k) then rawset(t, k, v) | ||||
| 		elseif vector_indices[k] then rawset(t, vector_indices[k], v) | ||||
| 		else return | ||||
| 		end | ||||
| 	end, | ||||
| 	__add = vector_opnnn('add', function(x1, x2) return x1+x2 end), | ||||
| 	__sub = vector_opnnn('sub', function(x1, x2) return x1-x2 end), | ||||
| 	__mul = vector_opnxn('mul', function(x1, x2) return x1*x2 end), | ||||
| 	__div = vector_opnxn('div', function(x1, x2) return x1/x2 end), | ||||
| 	__pow = vector_opnxn('pow', function(x1, x2) return x1^x2 end), | ||||
| 	__unm = vector_opn0n('inv', function(x1) return -x1 end), | ||||
| 	__concat = nil, | ||||
| 	--__len = function(v1) return #v1 end, | ||||
| 	__len = nil, | ||||
| 	__eq = function(v1, v2) | ||||
| 		local len = vector_checksamelen(v1, v2, 'equals') | ||||
| 		for i = 1, len do | ||||
| 			if v1[i]~=v2[i] then return false end | ||||
| 		end | ||||
| 		return true | ||||
| 	end, | ||||
| 	__lt = nil, | ||||
| 	__le = nil, | ||||
| 	__call = nil, | ||||
| 	abs = vector_opn0n('abs', math.abs), | ||||
| 	length = function(v1) | ||||
| 		--vector_check(v1, 2, 'length') | ||||
| 		local len = #v1 | ||||
| 		local l = 0 | ||||
| 		for i = 1, len do | ||||
| 			l = l + v1[i]^2 | ||||
| 		end | ||||
| 		return math.sqrt(l) | ||||
| 	end, | ||||
| 	normalize = function(v1) | ||||
| 		--vector_check(v1, 2, 'normal') | ||||
| 		local length = v1:length() | ||||
| 		local len = #v1 | ||||
| 		local v3 = {} | ||||
| 		for i = 1, len do | ||||
| 			if length==0 then v3[i] = 0 | ||||
| 			else v3[i] = v1[i]/length end | ||||
| 		end | ||||
| 		return vector_new(v3) | ||||
| 	end, | ||||
| 	__tostring = function(v1) | ||||
| 		--vector_check(v1, 2, 'tostring') | ||||
| 		local st = {} | ||||
| 		local len = #v1 | ||||
| 		for i = 1, len do | ||||
| 			table.insert(st, tostring(v1[i])) | ||||
| 		end | ||||
| 		return 'vector{ '..table.concat(st, ', ')..' }' | ||||
| 	end, | ||||
| 	unpack = function(v1) return unpack(v1) end, | ||||
| 	floor = vector_opn0n('floor', function(x1) return math.floor(x1) end), | ||||
| 	ceil  = vector_opn0n('ceil' , function(x1) return math.ceil (x1) end), | ||||
| 	round = vector_opn0n('round', function(x1) return math.floor(x1+0.5) end), | ||||
| 	dot = function(v1, v2) | ||||
| 		local len = vector_checksamelen(v1, v2, 'dot') | ||||
| 		local x = 0 | ||||
| 		for i = 1, len do | ||||
| 			x = x + v1[i]*v2[i] | ||||
| 		end | ||||
| 		return x | ||||
| 	end, | ||||
| 	cross = function(v1, v2) | ||||
| 		vector_checklen(v1, v2, 'cross', 3) | ||||
| 		return vector_new{ | ||||
| 			v1[2]*v2[3] - v1[3]*v2[2], | ||||
| 			v1[3]*v2[1] - v1[1]*v2[3], | ||||
| 			v1[1]*v2[2] - v1[2]*v2[1], | ||||
| 		} | ||||
| 	end, | ||||
| 	rotateByAngleId = function(v1, r) | ||||
| 		--vector_check(v1, 2, 'rotate') | ||||
| 		if type(r)~='number' or r%1~=0 then | ||||
| 			error('vector rotateByAngleId: invalid rotation '..tostring(r), 2) end | ||||
| 		r = r%4 | ||||
| 		local v2 | ||||
| 		if     r==0 then v2 = vector_new{  v1[1],  v1[2], v1[3] } | ||||
| 		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] } | ||||
| 		elseif r==3 then v2 = vector_new{ -v1[2],  v1[1], v1[3] } | ||||
| 		else error('vector rotateByAngleId: invalid rotation '..r, 2) end | ||||
| 		return v2 | ||||
| 	end, | ||||
| 	rotateZ = function(v, r) | ||||
| 		--vector_check(v, 2, 'rotate2d') | ||||
| 		if type(r)~='number' then | ||||
| 			error('vector rotateZ: invalid rotation '..tostring(r), 2) end | ||||
| 		local len = math.sqrt(v[1]^2 + v[2]^2) | ||||
| 		local ang = math.atan2(v[2], v[1]) + r | ||||
| 		local v2 = vector_new{ math.cos(ang)*len, math.sin(ang)*len } | ||||
| 		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, | ||||
|   __is_vector     = true, | ||||
|   __index         = function(t, k) | ||||
|     if tonumber(k) then | ||||
|       return rawget(t, k) | ||||
|     elseif vector_indices[k] then | ||||
|       return rawget(t, vector_indices[k]) | ||||
|     else | ||||
|       return getmetatable(t)[k] | ||||
|     end | ||||
|   end, | ||||
|   __newindex      = function(t, k, v) | ||||
|     if tonumber(k) then | ||||
|       rawset(t, k, v) | ||||
|     elseif vector_indices[k] then | ||||
|       rawset(t, vector_indices[k], v) | ||||
|     else | ||||
|       return | ||||
|     end | ||||
|   end, | ||||
|   __add           = vector_opnnn('add', function(x1, x2) return x1 + x2 end), | ||||
|   __sub           = vector_opnnn('sub', function(x1, x2) return x1 - x2 end), | ||||
|   __mul           = vector_opnxn('mul', function(x1, x2) return x1 * x2 end), | ||||
|   __div           = vector_opnxn('div', function(x1, x2) return x1 / x2 end), | ||||
|   __pow           = vector_opnxn('pow', function(x1, x2) return x1 ^ x2 end), | ||||
|   __unm           = vector_opn0n('inv', function(x1) return -x1 end), | ||||
|   __concat        = nil, | ||||
|   --__len = function(v1) return #v1 end, | ||||
|   __len           = nil, | ||||
|   __eq            = function(v1, v2) | ||||
|     local len = vector_checksamelen(v1, v2, 'equals') | ||||
|     for i = 1, len do | ||||
|       if v1[i] ~= v2[i] then return false end | ||||
|     end | ||||
|     return true | ||||
|   end, | ||||
|   __lt            = nil, | ||||
|   __le            = nil, | ||||
|   __call          = nil, | ||||
|   abs             = vector_opn0n('abs', math.abs), | ||||
|   length          = function(v1) | ||||
|     --vector_check(v1, 2, 'length') | ||||
|     local len = #v1 | ||||
|     local l = 0 | ||||
|     for i = 1, len do | ||||
|       l = l + v1[i] ^ 2 | ||||
|     end | ||||
|     return math.sqrt(l) | ||||
|   end, | ||||
|   normalize       = function(v1) | ||||
|     --vector_check(v1, 2, 'normal') | ||||
|     local length = v1:length() | ||||
|     local len = #v1 | ||||
|     local v3 = {} | ||||
|     for i = 1, len do | ||||
|       if length == 0 then | ||||
|         v3[i] = 0 | ||||
|       else | ||||
|         v3[i] = v1[i] / length | ||||
|       end | ||||
|     end | ||||
|     return vector_new(v3) | ||||
|   end, | ||||
|   __tostring      = function(v1) | ||||
|     --vector_check(v1, 2, 'tostring') | ||||
|     local st = {} | ||||
|     local len = #v1 | ||||
|     for i = 1, len do | ||||
|       table.insert(st, tostring(v1[i])) | ||||
|     end | ||||
|     return 'vector{ ' .. table.concat(st, ', ') .. ' }' | ||||
|   end, | ||||
|   unpack          = function(v1) return unpack(v1) end, | ||||
|   floor           = vector_opn0n('floor', function(x1) return math.floor(x1) end), | ||||
|   ceil            = vector_opn0n('ceil', function(x1) return math.ceil(x1) end), | ||||
|   round           = vector_opn0n('round', function(x1) return math.floor(x1 + 0.5) end), | ||||
|   dot             = function(v1, v2) | ||||
|     local len = vector_checksamelen(v1, v2, 'dot') | ||||
|     local x = 0 | ||||
|     for i = 1, len do | ||||
|       x = x + v1[i] * v2[i] | ||||
|     end | ||||
|     return x | ||||
|   end, | ||||
|   cross           = function(v1, v2) | ||||
|     vector_checklen(v1, v2, 'cross', 3) | ||||
|     return vector_new { | ||||
|       v1[2] * v2[3] - v1[3] * v2[2], | ||||
|       v1[3] * v2[1] - v1[1] * v2[3], | ||||
|       v1[1] * v2[2] - v1[2] * v2[1], | ||||
|     } | ||||
|   end, | ||||
|   rotateByAngleId = function(v1, r) | ||||
|     --vector_check(v1, 2, 'rotate') | ||||
|     if type(r) ~= 'number' or r % 1 ~= 0 then | ||||
|       error('vector rotateByAngleId: invalid rotation ' .. tostring(r), 2) | ||||
|     end | ||||
|     r = r % 4 | ||||
|     local v2 | ||||
|     if r == 0 then | ||||
|       v2 = vector_new { v1[1], v1[2], v1[3] } | ||||
|     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] } | ||||
|     elseif r == 3 then | ||||
|       v2 = vector_new { -v1[2], v1[1], v1[3] } | ||||
|     else | ||||
|       error('vector rotateByAngleId: invalid rotation ' .. r, 2) | ||||
|     end | ||||
|     return v2 | ||||
|   end, | ||||
|   rotateZ         = function(v, r) | ||||
|     --vector_check(v, 2, 'rotate2d') | ||||
|     if type(r) ~= 'number' then | ||||
|       error('vector rotateZ: invalid rotation ' .. tostring(r), 2) | ||||
|     end | ||||
|     local len = math.sqrt(v[1] ^ 2 + v[2] ^ 2) | ||||
|     local ang = math.atan2(v[2], v[1]) + r | ||||
|     local v2 = vector_new { math.cos(ang) * len, math.sin(ang) * len } | ||||
|     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) | ||||
| 	if vi then | ||||
| 		if type(vi)=='string' then | ||||
| 			local vi2 = {} | ||||
| 			for val in vi:gmatch('[0-9%.%-e]+') do | ||||
| 				table.insert(vi2, tonumber(val)) | ||||
| 			end | ||||
| 			vi = vi2 | ||||
| 		elseif type(vi)~='table' then | ||||
| 			error('vector: argument #1: expected input table, got '..type(vi), 2) | ||||
| 		end | ||||
| 		local v = {} | ||||
| 		if #vi>0 then | ||||
| 			for i = 1, #vi do v[i] = vi[i] end | ||||
| 		else | ||||
| 			for n, i in pairs(vector_indices) do v[i] = vi[n] end | ||||
| 			if #v==0 then | ||||
| 				error('vector: argument #1: table contains no values', 2) | ||||
| 			end | ||||
| 		end | ||||
| 		setmetatable(v, vector_meta) | ||||
| 		return v | ||||
| 	else | ||||
| 		error('vector: argument #1: expected input table, got nil', 2) | ||||
| 	end | ||||
|   if vi then | ||||
|     if type(vi) == 'string' then | ||||
|       local vi2 = {} | ||||
|       for val in vi:gmatch('[0-9%.%-e]+') do | ||||
|         table.insert(vi2, tonumber(val)) | ||||
|       end | ||||
|       vi = vi2 | ||||
|     elseif type(vi) ~= 'table' then | ||||
|       error('vector: argument #1: expected input table, got ' .. type(vi), 2) | ||||
|     end | ||||
|     local v = {} | ||||
|     if #vi > 0 then | ||||
|       for i = 1, #vi do v[i] = vi[i] end | ||||
|     else | ||||
|       for n, i in pairs(vector_indices) do v[i] = vi[n] end | ||||
|       if #v == 0 then | ||||
|         error('vector: argument #1: table contains no values', 2) | ||||
|       end | ||||
|     end | ||||
|     setmetatable(v, vector_meta) | ||||
|     return v | ||||
|   else | ||||
|     error('vector: argument #1: expected input table, got nil', 2) | ||||
|   end | ||||
| end | ||||
|  | ||||
| vector = vector_new | ||||
|   | ||||
		Reference in New Issue
	
	Block a user