call to
251 | -- explicitly manage caches; however, that might also have an undue
252 | -- performance penalty.
253 |
254 | if not env then
255 | env = utils.getfenv(2)
256 | end
257 |
258 | local mt = {}
259 | local cache = setmetatable({}, mt)
260 |
261 | -- Index operator builds, caches, and returns Lua function
262 | -- corresponding to comprehension expression string.
263 | --
264 | -- Example: f = comprehension['x^2 for x']
265 | --
266 | function mt:__index(expr)
267 | local f = build_comprehension(expr, env)
268 | self[expr] = f -- cache
269 | return f
270 | end
271 |
272 | -- Convenience syntax.
273 | -- Allows comprehension 'x^2 for x' instead of comprehension['x^2 for x'].
274 | mt.__call = mt.__index
275 |
276 | cache.new = new
277 |
278 | return cache
279 | end
280 |
281 |
282 | local comprehension = {}
283 | comprehension.new = new
284 |
285 | return comprehension
286 |
--------------------------------------------------------------------------------
/mod_push_appserver/pl/config.lua:
--------------------------------------------------------------------------------
1 | --- Reads configuration files into a Lua table.
2 | -- Understands INI files, classic Unix config files, and simple
3 | -- delimited columns of values. See @{06-data.md.Reading_Configuration_Files|the Guide}
4 | --
5 | -- # test.config
6 | -- # Read timeout in seconds
7 | -- read.timeout=10
8 | -- # Write timeout in seconds
9 | -- write.timeout=5
10 | -- #acceptable ports
11 | -- ports = 1002,1003,1004
12 | --
13 | -- -- readconfig.lua
14 | -- local config = require 'config'
15 | -- local t = config.read 'test.config'
16 | -- print(pretty.write(t))
17 | --
18 | -- ### output #####
19 | -- {
20 | -- ports = {
21 | -- 1002,
22 | -- 1003,
23 | -- 1004
24 | -- },
25 | -- write_timeout = 5,
26 | -- read_timeout = 10
27 | -- }
28 | --
29 | -- @module pl.config
30 |
31 | local type,tonumber,ipairs,io, table = _G.type,_G.tonumber,_G.ipairs,_G.io,_G.table
32 |
33 | local function split(s,re)
34 | local res = {}
35 | local t_insert = table.insert
36 | re = '[^'..re..']+'
37 | for k in s:gmatch(re) do t_insert(res,k) end
38 | return res
39 | end
40 |
41 | local function strip(s)
42 | return s:gsub('^%s+',''):gsub('%s+$','')
43 | end
44 |
45 | local function strip_quotes (s)
46 | return s:gsub("['\"](.*)['\"]",'%1')
47 | end
48 |
49 | local config = {}
50 |
51 | --- like io.lines(), but allows for lines to be continued with '\'.
52 | -- @param file a file-like object (anything where read() returns the next line) or a filename.
53 | -- Defaults to stardard input.
54 | -- @return an iterator over the lines, or nil
55 | -- @return error 'not a file-like object' or 'file is nil'
56 | function config.lines(file)
57 | local f,openf,err
58 | local line = ''
59 | if type(file) == 'string' then
60 | f,err = io.open(file,'r')
61 | if not f then return nil,err end
62 | openf = true
63 | else
64 | f = file or io.stdin
65 | if not file.read then return nil, 'not a file-like object' end
66 | end
67 | if not f then return nil, 'file is nil' end
68 | return function()
69 | local l = f:read()
70 | while l do
71 | -- only for non-blank lines that don't begin with either ';' or '#'
72 | if l:match '%S' and not l:match '^%s*[;#]' then
73 | -- does the line end with '\'?
74 | local i = l:find '\\%s*$'
75 | if i then -- if so,
76 | line = line..l:sub(1,i-1)
77 | elseif line == '' then
78 | return l
79 | else
80 | l = line..l
81 | line = ''
82 | return l
83 | end
84 | end
85 | l = f:read()
86 | end
87 | if openf then f:close() end
88 | end
89 | end
90 |
91 | --- read a configuration file into a table
92 | -- @param file either a file-like object or a string, which must be a filename
93 | -- @tab[opt] cnfg a configuration table that may contain these fields:
94 | --
95 | -- * `smart` try to deduce what kind of config file we have (default false)
96 | -- * `variablilize` make names into valid Lua identifiers (default true)
97 | -- * `convert_numbers` try to convert values into numbers (default true)
98 | -- * `trim_space` ensure that there is no starting or trailing whitespace with values (default true)
99 | -- * `trim_quotes` remove quotes from strings (default false)
100 | -- * `list_delim` delimiter to use when separating columns (default ',')
101 | -- * `keysep` separator between key and value pairs (default '=')
102 | --
103 | -- @return a table containing items, or `nil`
104 | -- @return error message (same as @{config.lines}
105 | function config.read(file,cnfg)
106 | local auto
107 |
108 | local iter,err = config.lines(file)
109 | if not iter then return nil,err end
110 | local line = iter()
111 | cnfg = cnfg or {}
112 | if cnfg.smart then
113 | auto = true
114 | if line:match '^[^=]+=' then
115 | cnfg.keysep = '='
116 | elseif line:match '^[^:]+:' then
117 | cnfg.keysep = ':'
118 | cnfg.list_delim = ':'
119 | elseif line:match '^%S+%s+' then
120 | cnfg.keysep = ' '
121 | -- more than two columns assume that it's a space-delimited list
122 | -- cf /etc/fstab with /etc/ssh/ssh_config
123 | if line:match '^%S+%s+%S+%s+%S+' then
124 | cnfg.list_delim = ' '
125 | end
126 | cnfg.variabilize = false
127 | end
128 | end
129 |
130 |
131 | local function check_cnfg (var,def)
132 | local val = cnfg[var]
133 | if val == nil then return def else return val end
134 | end
135 |
136 | local initial_digits = '^[%d%+%-]'
137 | local t = {}
138 | local top_t = t
139 | local variablilize = check_cnfg ('variabilize',true)
140 | local list_delim = check_cnfg('list_delim',',')
141 | local convert_numbers = check_cnfg('convert_numbers',true)
142 | local convert_boolean = check_cnfg('convert_boolean',false)
143 | local trim_space = check_cnfg('trim_space',true)
144 | local trim_quotes = check_cnfg('trim_quotes',false)
145 | local ignore_assign = check_cnfg('ignore_assign',false)
146 | local keysep = check_cnfg('keysep','=')
147 | local keypat = keysep == ' ' and '%s+' or '%s*'..keysep..'%s*'
148 | if list_delim == ' ' then list_delim = '%s+' end
149 |
150 | local function process_name(key)
151 | if variablilize then
152 | key = key:gsub('[^%w]','_')
153 | end
154 | return key
155 | end
156 |
157 | local function process_value(value)
158 | if list_delim and value:find(list_delim) then
159 | value = split(value,list_delim)
160 | for i,v in ipairs(value) do
161 | value[i] = process_value(v)
162 | end
163 | elseif convert_numbers and value:find(initial_digits) then
164 | local val = tonumber(value)
165 | if not val and value:match ' kB$' then
166 | value = value:gsub(' kB','')
167 | val = tonumber(value)
168 | end
169 | if val then value = val end
170 | elseif convert_boolean and value == 'true' then
171 | return true
172 | elseif convert_boolean and value == 'false' then
173 | return false
174 | end
175 | if type(value) == 'string' then
176 | if trim_space then value = strip(value) end
177 | if not trim_quotes and auto and value:match '^"' then
178 | trim_quotes = true
179 | end
180 | if trim_quotes then value = strip_quotes(value) end
181 | end
182 | return value
183 | end
184 |
185 | while line do
186 | if line:find('^%[') then -- section!
187 | local section = process_name(line:match('%[([^%]]+)%]'))
188 | t = top_t
189 | t[section] = {}
190 | t = t[section]
191 | else
192 | line = line:gsub('^%s*','')
193 | local i1,i2 = line:find(keypat)
194 | if i1 and not ignore_assign then -- key,value assignment
195 | local key = process_name(line:sub(1,i1-1))
196 | local value = process_value(line:sub(i2+1))
197 | t[key] = value
198 | else -- a plain list of values...
199 | t[#t+1] = process_value(line)
200 | end
201 | end
202 | line = iter()
203 | end
204 | return top_t
205 | end
206 |
207 | return config
208 |
--------------------------------------------------------------------------------
/mod_push_appserver/pl/file.lua:
--------------------------------------------------------------------------------
1 | --- File manipulation functions: reading, writing, moving and copying.
2 | --
3 | -- This module wraps a number of functions from other modules into a
4 | -- file related module for convenience.
5 | --
6 | -- Dependencies: `pl.utils`, `pl.dir`, `pl.path`
7 | -- @module pl.file
8 | local os = os
9 | local utils = require 'pl.utils'
10 | local dir = require 'pl.dir'
11 | local path = require 'pl.path'
12 |
13 | local file = {}
14 |
15 | --- return the contents of a file as a string.
16 | -- This function is a copy of `utils.readfile`.
17 | -- @function file.read
18 | file.read = utils.readfile
19 |
20 | --- write a string to a file.
21 | -- This function is a copy of `utils.writefile`.
22 | -- @function file.write
23 | file.write = utils.writefile
24 |
25 | --- copy a file.
26 | -- This function is a copy of `dir.copyfile`.
27 | -- @function file.copy
28 | file.copy = dir.copyfile
29 |
30 | --- move a file.
31 | -- This function is a copy of `dir.movefile`.
32 | -- @function file.move
33 | file.move = dir.movefile
34 |
35 | --- Return the time of last access as the number of seconds since the epoch.
36 | -- This function is a copy of `path.getatime`.
37 | -- @function file.access_time
38 | file.access_time = path.getatime
39 |
40 | ---Return when the file was created.
41 | -- This function is a copy of `path.getctime`.
42 | -- @function file.creation_time
43 | file.creation_time = path.getctime
44 |
45 | --- Return the time of last modification.
46 | -- This function is a copy of `path.getmtime`.
47 | -- @function file.modified_time
48 | file.modified_time = path.getmtime
49 |
50 | --- Delete a file.
51 | -- This function is a copy of `os.remove`.
52 | -- @function file.delete
53 | file.delete = os.remove
54 |
55 | return file
56 |
--------------------------------------------------------------------------------
/mod_push_appserver/pl/func.lua:
--------------------------------------------------------------------------------
1 | --- Functional helpers like composition, binding and placeholder expressions.
2 | -- Placeholder expressions are useful for short anonymous functions, and were
3 | -- inspired by the Boost Lambda library.
4 | --
5 | -- > utils.import 'pl.func'
6 | -- > ls = List{10,20,30}
7 | -- > = ls:map(_1+1)
8 | -- {11,21,31}
9 | --
10 | -- They can also be used to _bind_ particular arguments of a function.
11 | --
12 | -- > p = bind(print,'start>',_0)
13 | -- > p(10,20,30)
14 | -- > start> 10 20 30
15 | --
16 | -- See @{07-functional.md.Creating_Functions_from_Functions|the Guide}
17 | --
18 | -- Dependencies: `pl.utils`, `pl.tablex`
19 | -- @module pl.func
20 | local type,setmetatable,getmetatable,rawset = type,setmetatable,getmetatable,rawset
21 | local concat,append = table.concat,table.insert
22 | local tostring = tostring
23 | local utils = require 'pl.utils'
24 | local pairs,rawget,unpack,pack = pairs,rawget,utils.unpack,utils.pack
25 | local tablex = require 'pl.tablex'
26 | local map = tablex.map
27 | local _DEBUG = rawget(_G,'_DEBUG')
28 | local assert_arg = utils.assert_arg
29 |
30 | local func = {}
31 |
32 | -- metatable for Placeholder Expressions (PE)
33 | local _PEMT = {}
34 |
35 | local function P (t)
36 | setmetatable(t,_PEMT)
37 | return t
38 | end
39 |
40 | func.PE = P
41 |
42 | local function isPE (obj)
43 | return getmetatable(obj) == _PEMT
44 | end
45 |
46 | func.isPE = isPE
47 |
48 | -- construct a placeholder variable (e.g _1 and _2)
49 | local function PH (idx)
50 | return P {op='X',repr='_'..idx, index=idx}
51 | end
52 |
53 | -- construct a constant placeholder variable (e.g _C1 and _C2)
54 | local function CPH (idx)
55 | return P {op='X',repr='_C'..idx, index=idx}
56 | end
57 |
58 | func._1,func._2,func._3,func._4,func._5 = PH(1),PH(2),PH(3),PH(4),PH(5)
59 | func._0 = P{op='X',repr='...',index=0}
60 |
61 | function func.Var (name)
62 | local ls = utils.split(name,'[%s,]+')
63 | local res = {}
64 | for i = 1, #ls do
65 | append(res,P{op='X',repr=ls[i],index=0})
66 | end
67 | return unpack(res)
68 | end
69 |
70 | function func._ (value)
71 | return P{op='X',repr=value,index='wrap'}
72 | end
73 |
74 | local repr
75 |
76 | func.Nil = func.Var 'nil'
77 |
78 | function _PEMT.__index(obj,key)
79 | return P{op='[]',obj,key}
80 | end
81 |
82 | function _PEMT.__call(fun,...)
83 | return P{op='()',fun,...}
84 | end
85 |
86 | function _PEMT.__tostring (e)
87 | return repr(e)
88 | end
89 |
90 | function _PEMT.__unm(arg)
91 | return P{op='unm',arg}
92 | end
93 |
94 | function func.Not (arg)
95 | return P{op='not',arg}
96 | end
97 |
98 | function func.Len (arg)
99 | return P{op='#',arg}
100 | end
101 |
102 |
103 | local function binreg(context,t)
104 | for name,op in pairs(t) do
105 | rawset(context,name,function(x,y)
106 | return P{op=op,x,y}
107 | end)
108 | end
109 | end
110 |
111 | local function import_name (name,fun,context)
112 | rawset(context,name,function(...)
113 | return P{op='()',fun,...}
114 | end)
115 | end
116 |
117 | local imported_functions = {}
118 |
119 | local function is_global_table (n)
120 | return type(_G[n]) == 'table'
121 | end
122 |
123 | --- wrap a table of functions. This makes them available for use in
124 | -- placeholder expressions.
125 | -- @string tname a table name
126 | -- @tab context context to put results, defaults to environment of caller
127 | function func.import(tname,context)
128 | assert_arg(1,tname,'string',is_global_table,'arg# 1: not a name of a global table')
129 | local t = _G[tname]
130 | context = context or _G
131 | for name,fun in pairs(t) do
132 | import_name(name,fun,context)
133 | imported_functions[fun] = name
134 | end
135 | end
136 |
137 | --- register a function for use in placeholder expressions.
138 | -- @func fun a function
139 | -- @string[opt] name an optional name
140 | -- @return a placeholder functiond
141 | function func.register (fun,name)
142 | assert_arg(1,fun,'function')
143 | if name then
144 | assert_arg(2,name,'string')
145 | imported_functions[fun] = name
146 | end
147 | return function(...)
148 | return P{op='()',fun,...}
149 | end
150 | end
151 |
152 | function func.lookup_imported_name (fun)
153 | return imported_functions[fun]
154 | end
155 |
156 | local function _arg(...) return ... end
157 |
158 | function func.Args (...)
159 | return P{op='()',_arg,...}
160 | end
161 |
162 | -- binary operators with their precedences (see Lua manual)
163 | -- precedences might be incremented by one before use depending on
164 | -- left- or right-associativity, space them out
165 | local binary_operators = {
166 | ['or'] = 0,
167 | ['and'] = 2,
168 | ['=='] = 4, ['~='] = 4, ['<'] = 4, ['>'] = 4, ['<='] = 4, ['>='] = 4,
169 | ['..'] = 6,
170 | ['+'] = 8, ['-'] = 8,
171 | ['*'] = 10, ['/'] = 10, ['%'] = 10,
172 | ['^'] = 14
173 | }
174 |
175 | -- unary operators with their precedences
176 | local unary_operators = {
177 | ['not'] = 12, ['#'] = 12, ['unm'] = 12
178 | }
179 |
180 | -- comparisons (as prefix functions)
181 | binreg (func,{And='and',Or='or',Eq='==',Lt='<',Gt='>',Le='<=',Ge='>='})
182 |
183 | -- standard binary operators (as metamethods)
184 | binreg (_PEMT,{__add='+',__sub='-',__mul='*',__div='/',__mod='%',__pow='^',__concat='..'})
185 |
186 | binreg (_PEMT,{__eq='=='})
187 |
188 | --- all elements of a table except the first.
189 | -- @tab ls a list-like table.
190 | function func.tail (ls)
191 | assert_arg(1,ls,'table')
192 | local res = {}
193 | for i = 2,#ls do
194 | append(res,ls[i])
195 | end
196 | return res
197 | end
198 |
199 | --- create a string representation of a placeholder expression.
200 | -- @param e a placeholder expression
201 | -- @param lastpred not used
202 | function repr (e,lastpred)
203 | local tail = func.tail
204 | if isPE(e) then
205 | local pred = binary_operators[e.op] or unary_operators[e.op]
206 | if pred then
207 | -- binary or unary operator
208 | local s
209 | if binary_operators[e.op] then
210 | local left_pred = pred
211 | local right_pred = pred
212 | if e.op == '..' or e.op == '^' then
213 | left_pred = left_pred + 1
214 | else
215 | right_pred = right_pred + 1
216 | end
217 | local left_arg = repr(e[1], left_pred)
218 | local right_arg = repr(e[2], right_pred)
219 | s = left_arg..' '..e.op..' '..right_arg
220 | else
221 | local op = e.op == 'unm' and '-' or e.op
222 | s = op..' '..repr(e[1], pred)
223 | end
224 | if lastpred and lastpred > pred then
225 | s = '('..s..')'
226 | end
227 | return s
228 | else -- either postfix, or a placeholder
229 | local ls = map(repr,e)
230 | if e.op == '[]' then
231 | return ls[1]..'['..ls[2]..']'
232 | elseif e.op == '()' then
233 | local fn
234 | if ls[1] ~= nil then -- was _args, undeclared!
235 | fn = ls[1]
236 | else
237 | fn = ''
238 | end
239 | return fn..'('..concat(tail(ls),',')..')'
240 | else
241 | return e.repr
242 | end
243 | end
244 | elseif type(e) == 'string' then
245 | return '"'..e..'"'
246 | elseif type(e) == 'function' then
247 | local name = func.lookup_imported_name(e)
248 | if name then return name else return tostring(e) end
249 | else
250 | return tostring(e) --should not really get here!
251 | end
252 | end
253 | func.repr = repr
254 |
255 | -- collect all the non-PE values in this PE into vlist, and replace each occurence
256 | -- with a constant PH (_C1, etc). Return the maximum placeholder index found.
257 | local collect_values
258 | function collect_values (e,vlist)
259 | if isPE(e) then
260 | if e.op ~= 'X' then
261 | local m = 0
262 | for i = 1,#e do
263 | local subx = e[i]
264 | local pe = isPE(subx)
265 | if pe then
266 | if subx.op == 'X' and subx.index == 'wrap' then
267 | subx = subx.repr
268 | pe = false
269 | else
270 | m = math.max(m,collect_values(subx,vlist))
271 | end
272 | end
273 | if not pe then
274 | append(vlist,subx)
275 | e[i] = CPH(#vlist)
276 | end
277 | end
278 | return m
279 | else -- was a placeholder, it has an index...
280 | return e.index
281 | end
282 | else -- plain value has no placeholder dependence
283 | return 0
284 | end
285 | end
286 | func.collect_values = collect_values
287 |
288 | --- instantiate a PE into an actual function. First we find the largest placeholder used,
289 | -- e.g. _2; from this a list of the formal parameters can be build. Then we collect and replace
290 | -- any non-PE values from the PE, and build up a constant binding list.
291 | -- Finally, the expression can be compiled, and e.__PE_function is set.
292 | -- @param e a placeholder expression
293 | -- @return a function
294 | function func.instantiate (e)
295 | local consts,values,parms = {},{},{}
296 | local rep, err, fun
297 | local n = func.collect_values(e,values)
298 | for i = 1,#values do
299 | append(consts,'_C'..i)
300 | if _DEBUG then print(i,values[i]) end
301 | end
302 | for i =1,n do
303 | append(parms,'_'..i)
304 | end
305 | consts = concat(consts,',')
306 | parms = concat(parms,',')
307 | rep = repr(e)
308 | local fstr = ('return function(%s) return function(%s) return %s end end'):format(consts,parms,rep)
309 | if _DEBUG then print(fstr) end
310 | fun,err = utils.load(fstr,'fun')
311 | if not fun then return nil,err end
312 | fun = fun() -- get wrapper
313 | fun = fun(unpack(values)) -- call wrapper (values could be empty)
314 | e.__PE_function = fun
315 | return fun
316 | end
317 |
318 | --- instantiate a PE unless it has already been done.
319 | -- @param e a placeholder expression
320 | -- @return the function
321 | function func.I(e)
322 | if rawget(e,'__PE_function') then
323 | return e.__PE_function
324 | else return func.instantiate(e)
325 | end
326 | end
327 |
328 | utils.add_function_factory(_PEMT,func.I)
329 |
330 | --- bind the first parameter of the function to a value.
331 | -- @function func.bind1
332 | -- @func fn a function of one or more arguments
333 | -- @param p a value
334 | -- @return a function of one less argument
335 | -- @usage (bind1(math.max,10))(20) == math.max(10,20)
336 | func.bind1 = utils.bind1
337 | func.curry = func.bind1
338 |
339 | --- create a function which chains two functions.
340 | -- @func f a function of at least one argument
341 | -- @func g a function of at least one argument
342 | -- @return a function
343 | -- @usage printf = compose(io.write,string.format)
344 | function func.compose (f,g)
345 | return function(...) return f(g(...)) end
346 | end
347 |
348 | --- bind the arguments of a function to given values.
349 | -- `bind(fn,v,_2)` is equivalent to `bind1(fn,v)`.
350 | -- @func fn a function of at least one argument
351 | -- @param ... values or placeholder variables
352 | -- @return a function
353 | -- @usage (bind(f,_1,a))(b) == f(a,b)
354 | -- @usage (bind(f,_2,_1))(a,b) == f(b,a)
355 | function func.bind(fn,...)
356 | local args = pack(...)
357 | local holders,parms,bvalues,values = {},{},{'fn'},{}
358 | local nv,maxplace,varargs = 1,0,false
359 | for i = 1,args.n do
360 | local a = args[i]
361 | if isPE(a) and a.op == 'X' then
362 | append(holders,a.repr)
363 | maxplace = math.max(maxplace,a.index)
364 | if a.index == 0 then varargs = true end
365 | else
366 | local v = '_v'..nv
367 | append(bvalues,v)
368 | append(holders,v)
369 | append(values,a)
370 | nv = nv + 1
371 | end
372 | end
373 | for np = 1,maxplace do
374 | append(parms,'_'..np)
375 | end
376 | if varargs then append(parms,'...') end
377 | bvalues = concat(bvalues,',')
378 | parms = concat(parms,',')
379 | holders = concat(holders,',')
380 | local fstr = ([[
381 | return function (%s)
382 | return function(%s) return fn(%s) end
383 | end
384 | ]]):format(bvalues,parms,holders)
385 | if _DEBUG then print(fstr) end
386 | local res = utils.load(fstr)
387 | res = res()
388 | return res(fn,unpack(values))
389 | end
390 |
391 | return func
392 |
393 |
394 |
--------------------------------------------------------------------------------
/mod_push_appserver/pl/import_into.lua:
--------------------------------------------------------------------------------
1 | --------------
2 | -- PL loader, for loading all PL libraries, only on demand.
3 | -- Whenever a module is implicitly accesssed, the table will have the module automatically injected.
4 | -- (e.g. `_ENV.tablex`)
5 | -- then that module is dynamically loaded. The submodules are all brought into
6 | -- the table that is provided as the argument, or returned in a new table.
7 | -- If a table is provided, that table's metatable is clobbered, but the values are not.
8 | -- This module returns a single function, which is passed the environment.
9 | -- If this is `true`, then return a 'shadow table' as the module
10 | -- See @{01-introduction.md.To_Inject_or_not_to_Inject_|the Guide}
11 |
12 | -- @module pl.import_into
13 |
14 | return function(env)
15 | local mod
16 | if env == true then
17 | mod = {}
18 | env = {}
19 | end
20 | local env = env or {}
21 |
22 | local modules = {
23 | utils = true,path=true,dir=true,tablex=true,stringio=true,sip=true,
24 | input=true,seq=true,lexer=true,stringx=true,
25 | config=true,pretty=true,data=true,func=true,text=true,
26 | operator=true,lapp=true,array2d=true,
27 | comprehension=true,xml=true,types=true,
28 | test = true, app = true, file = true, class = true,
29 | luabalanced = true, permute = true, template = true,
30 | url = true, compat = true,
31 | -- classes --
32 | List = true, Map = true, Set = true,
33 | OrderedMap = true, MultiMap = true, Date = true,
34 | }
35 | rawset(env,'utils',require 'pl.utils')
36 |
37 | for name,klass in pairs(env.utils.stdmt) do
38 | klass.__index = function(t,key)
39 | return require ('pl.'..name)[key]
40 | end;
41 | end
42 |
43 | -- ensure that we play nice with libraries that also attach a metatable
44 | -- to the global table; always forward to a custom __index if we don't
45 | -- match
46 |
47 | local _hook,_prev_index
48 | local gmt = {}
49 | local prevenvmt = getmetatable(env)
50 | if prevenvmt then
51 | _prev_index = prevenvmt.__index
52 | if prevenvmt.__newindex then
53 | gmt.__index = prevenvmt.__newindex
54 | end
55 | end
56 |
57 | function gmt.hook(handler)
58 | _hook = handler
59 | end
60 |
61 | function gmt.__index(t,name)
62 | local found = modules[name]
63 | -- either true, or the name of the module containing this class.
64 | -- either way, we load the required module and make it globally available.
65 | if found then
66 | -- e..g pretty.dump causes pl.pretty to become available as 'pretty'
67 | rawset(env,name,require('pl.'..name))
68 | return env[name]
69 | else
70 | local res
71 | if _hook then
72 | res = _hook(t,name)
73 | if res then return res end
74 | end
75 | if _prev_index then
76 | return _prev_index(t,name)
77 | end
78 | end
79 | end
80 |
81 | if mod then
82 | function gmt.__newindex(t,name,value)
83 | mod[name] = value
84 | rawset(t,name,value)
85 | end
86 | end
87 |
88 | setmetatable(env,gmt)
89 |
90 | return env,mod or env
91 | end
92 |
--------------------------------------------------------------------------------
/mod_push_appserver/pl/init.lua:
--------------------------------------------------------------------------------
1 | --------------
2 | -- Entry point for loading all PL libraries only on demand, into the global space.
3 | -- Requiring 'pl' means that whenever a module is implicitly accesssed
4 | -- (e.g. `utils.split`)
5 | -- then that module is dynamically loaded. The submodules are all brought into
6 | -- the global space.
7 | --Updated to use @{pl.import_into}
8 | -- @module pl
9 | require'pl.import_into'(_G)
10 |
11 | if rawget(_G,'PENLIGHT_STRICT') then require 'pl.strict' end
12 |
--------------------------------------------------------------------------------
/mod_push_appserver/pl/input.lua:
--------------------------------------------------------------------------------
1 | --- Iterators for extracting words or numbers from an input source.
2 | --
3 | -- require 'pl'
4 | -- local total,n = seq.sum(input.numbers())
5 | -- print('average',total/n)
6 | --
7 | -- _source_ is defined as a string or a file-like object (i.e. has a read() method which returns the next line)
8 | --
9 | -- See @{06-data.md.Reading_Unstructured_Text_Data|here}
10 | --
11 | -- Dependencies: `pl.utils`
12 | -- @module pl.input
13 | local strfind = string.find
14 | local strsub = string.sub
15 | local strmatch = string.match
16 | local utils = require 'pl.utils'
17 | local unpack = utils.unpack
18 | local pairs,type,tonumber = pairs,type,tonumber
19 | local patterns = utils.patterns
20 | local io = io
21 |
22 | local input = {}
23 |
24 | --- create an iterator over all tokens.
25 | -- based on allwords from PiL, 7.1
26 | -- @func getter any function that returns a line of text
27 | -- @string pattern
28 | -- @string[opt] fn Optionally can pass a function to process each token as it's found.
29 | -- @return an iterator
30 | function input.alltokens (getter,pattern,fn)
31 | local line = getter() -- current line
32 | local pos = 1 -- current position in the line
33 | return function () -- iterator function
34 | while line do -- repeat while there are lines
35 | local s, e = strfind(line, pattern, pos)
36 | if s then -- found a word?
37 | pos = e + 1 -- next position is after this token
38 | local res = strsub(line, s, e) -- return the token
39 | if fn then res = fn(res) end
40 | return res
41 | else
42 | line = getter() -- token not found; try next line
43 | pos = 1 -- restart from first position
44 | end
45 | end
46 | return nil -- no more lines: end of traversal
47 | end
48 | end
49 | local alltokens = input.alltokens
50 |
51 | -- question: shd this _split_ a string containing line feeds?
52 |
53 | --- create a function which grabs the next value from a source. If the source is a string, then the getter
54 | -- will return the string and thereafter return nil. If not specified then the source is assumed to be stdin.
55 | -- @param f a string or a file-like object (i.e. has a read() method which returns the next line)
56 | -- @return a getter function
57 | function input.create_getter(f)
58 | if f then
59 | if type(f) == 'string' then
60 | local ls = utils.split(f,'\n')
61 | local i,n = 0,#ls
62 | return function()
63 | i = i + 1
64 | if i > n then return nil end
65 | return ls[i]
66 | end
67 | else
68 | -- anything that supports the read() method!
69 | if not f.read then error('not a file-like object') end
70 | return function() return f:read() end
71 | end
72 | else
73 | return io.read -- i.e. just read from stdin
74 | end
75 | end
76 |
77 | --- generate a sequence of numbers from a source.
78 | -- @param f A source
79 | -- @return An iterator
80 | function input.numbers(f)
81 | return alltokens(input.create_getter(f),
82 | '('..patterns.FLOAT..')',tonumber)
83 | end
84 |
85 | --- generate a sequence of words from a source.
86 | -- @param f A source
87 | -- @return An iterator
88 | function input.words(f)
89 | return alltokens(input.create_getter(f),"%w+")
90 | end
91 |
92 | local function apply_tonumber (no_fail,...)
93 | local args = {...}
94 | for i = 1,#args do
95 | local n = tonumber(args[i])
96 | if n == nil then
97 | if not no_fail then return nil,args[i] end
98 | else
99 | args[i] = n
100 | end
101 | end
102 | return args
103 | end
104 |
105 | --- parse an input source into fields.
106 | -- By default, will fail if it cannot convert a field to a number.
107 | -- @param ids a list of field indices, or a maximum field index
108 | -- @string delim delimiter to parse fields (default space)
109 | -- @param f a source @see create_getter
110 | -- @tab opts option table, `{no_fail=true}`
111 | -- @return an iterator with the field values
112 | -- @usage for x,y in fields {2,3} do print(x,y) end -- 2nd and 3rd fields from stdin
113 | function input.fields (ids,delim,f,opts)
114 | local sep
115 | local s
116 | local getter = input.create_getter(f)
117 | local no_fail = opts and opts.no_fail
118 | local no_convert = opts and opts.no_convert
119 | if not delim or delim == ' ' then
120 | delim = '%s'
121 | sep = '%s+'
122 | s = '%s*'
123 | else
124 | sep = delim
125 | s = ''
126 | end
127 | local max_id = 0
128 | if type(ids) == 'table' then
129 | for i,id in pairs(ids) do
130 | if id > max_id then max_id = id end
131 | end
132 | else
133 | max_id = ids
134 | ids = {}
135 | for i = 1,max_id do ids[#ids+1] = i end
136 | end
137 | local pat = '[^'..delim..']*'
138 | local k = 1
139 | for i = 1,max_id do
140 | if ids[k] == i then
141 | k = k + 1
142 | s = s..'('..pat..')'
143 | else
144 | s = s..pat
145 | end
146 | if i < max_id then
147 | s = s..sep
148 | end
149 | end
150 | local linecount = 1
151 | return function()
152 | local line,results,err
153 | repeat
154 | line = getter()
155 | linecount = linecount + 1
156 | if not line then return nil end
157 | if no_convert then
158 | results = {strmatch(line,s)}
159 | else
160 | results,err = apply_tonumber(no_fail,strmatch(line,s))
161 | if not results then
162 | utils.quit("line "..(linecount-1)..": cannot convert '"..err.."' to number")
163 | end
164 | end
165 | until #results > 0
166 | return unpack(results)
167 | end
168 | end
169 |
170 | return input
171 |
172 |
--------------------------------------------------------------------------------
/mod_push_appserver/pl/luabalanced.lua:
--------------------------------------------------------------------------------
1 | --- Extract delimited Lua sequences from strings.
2 | -- Inspired by Damian Conway's Text::Balanced in Perl.
3 | --
4 | -- - [1] Lua Wiki Page
5 | -- - [2] http://search.cpan.org/dist/Text-Balanced/lib/Text/Balanced.pm
6 | --
7 | --
8 | -- local lb = require "pl.luabalanced"
9 | -- --Extract Lua expression starting at position 4.
10 | -- print(lb.match_expression("if x^2 + x > 5 then print(x) end", 4))
11 | -- --> x^2 + x > 5 16
12 | -- --Extract Lua string starting at (default) position 1.
13 | -- print(lb.match_string([["test\"123" .. "more"]]))
14 | -- --> "test\"123" 12
15 | --
16 | -- (c) 2008, David Manura, Licensed under the same terms as Lua (MIT license).
17 | -- @class module
18 | -- @name pl.luabalanced
19 |
20 | local M = {}
21 |
22 | local assert = assert
23 |
24 | -- map opening brace <-> closing brace.
25 | local ends = { ['('] = ')', ['{'] = '}', ['['] = ']' }
26 | local begins = {}; for k,v in pairs(ends) do begins[v] = k end
27 |
28 |
29 | -- Match Lua string in string starting at position .
30 | -- Returns , , where is the matched
31 | -- string (or nil on no match) and is the character
32 | -- following the match (or on no match).
33 | -- Supports all Lua string syntax: "...", '...', [[...]], [=[...]=], etc.
34 | local function match_string(s, pos)
35 | pos = pos or 1
36 | local posa = pos
37 | local c = s:sub(pos,pos)
38 | if c == '"' or c == "'" then
39 | pos = pos + 1
40 | while 1 do
41 | pos = assert(s:find("[" .. c .. "\\]", pos), 'syntax error')
42 | if s:sub(pos,pos) == c then
43 | local part = s:sub(posa, pos)
44 | return part, pos + 1
45 | else
46 | pos = pos + 2
47 | end
48 | end
49 | else
50 | local sc = s:match("^%[(=*)%[", pos)
51 | if sc then
52 | local _; _, pos = s:find("%]" .. sc .. "%]", pos)
53 | assert(pos)
54 | local part = s:sub(posa, pos)
55 | return part, pos + 1
56 | else
57 | return nil, pos
58 | end
59 | end
60 | end
61 | M.match_string = match_string
62 |
63 |
64 | -- Match bracketed Lua expression, e.g. "(...)", "{...}", "[...]", "[[...]]",
65 | -- [=[...]=], etc.
66 | -- Function interface is similar to match_string.
67 | local function match_bracketed(s, pos)
68 | pos = pos or 1
69 | local posa = pos
70 | local ca = s:sub(pos,pos)
71 | if not ends[ca] then
72 | return nil, pos
73 | end
74 | local stack = {}
75 | while 1 do
76 | pos = s:find('[%(%{%[%)%}%]\"\']', pos)
77 | assert(pos, 'syntax error: unbalanced')
78 | local c = s:sub(pos,pos)
79 | if c == '"' or c == "'" then
80 | local part; part, pos = match_string(s, pos)
81 | assert(part)
82 | elseif ends[c] then -- open
83 | local mid, posb
84 | if c == '[' then mid, posb = s:match('^%[(=*)%[()', pos) end
85 | if mid then
86 | pos = s:match('%]' .. mid .. '%]()', posb)
87 | assert(pos, 'syntax error: long string not terminated')
88 | if #stack == 0 then
89 | local part = s:sub(posa, pos-1)
90 | return part, pos
91 | end
92 | else
93 | stack[#stack+1] = c
94 | pos = pos + 1
95 | end
96 | else -- close
97 | assert(stack[#stack] == assert(begins[c]), 'syntax error: unbalanced')
98 | stack[#stack] = nil
99 | if #stack == 0 then
100 | local part = s:sub(posa, pos)
101 | return part, pos+1
102 | end
103 | pos = pos + 1
104 | end
105 | end
106 | end
107 | M.match_bracketed = match_bracketed
108 |
109 |
110 | -- Match Lua comment, e.g. "--...\n", "--[[...]]", "--[=[...]=]", etc.
111 | -- Function interface is similar to match_string.
112 | local function match_comment(s, pos)
113 | pos = pos or 1
114 | if s:sub(pos, pos+1) ~= '--' then
115 | return nil, pos
116 | end
117 | pos = pos + 2
118 | local partt, post = match_string(s, pos)
119 | if partt then
120 | return '--' .. partt, post
121 | end
122 | local part; part, pos = s:match('^([^\n]*\n?)()', pos)
123 | return '--' .. part, pos
124 | end
125 |
126 |
127 | -- Match Lua expression, e.g. "a + b * c[e]".
128 | -- Function interface is similar to match_string.
129 | local wordop = {['and']=true, ['or']=true, ['not']=true}
130 | local is_compare = {['>']=true, ['<']=true, ['~']=true}
131 | local function match_expression(s, pos)
132 | pos = pos or 1
133 | local _
134 | local posa = pos
135 | local lastident
136 | local poscs, posce
137 | while pos do
138 | local c = s:sub(pos,pos)
139 | if c == '"' or c == "'" or c == '[' and s:find('^[=%[]', pos+1) then
140 | local part; part, pos = match_string(s, pos)
141 | assert(part, 'syntax error')
142 | elseif c == '-' and s:sub(pos+1,pos+1) == '-' then
143 | -- note: handle adjacent comments in loop to properly support
144 | -- backtracing (poscs/posce).
145 | poscs = pos
146 | while s:sub(pos,pos+1) == '--' do
147 | local part; part, pos = match_comment(s, pos)
148 | assert(part)
149 | pos = s:match('^%s*()', pos)
150 | posce = pos
151 | end
152 | elseif c == '(' or c == '{' or c == '[' then
153 | _, pos = match_bracketed(s, pos)
154 | elseif c == '=' and s:sub(pos+1,pos+1) == '=' then
155 | pos = pos + 2 -- skip over two-char op containing '='
156 | elseif c == '=' and is_compare[s:sub(pos-1,pos-1)] then
157 | pos = pos + 1 -- skip over two-char op containing '='
158 | elseif c:match'^[%)%}%];,=]' then
159 | local part = s:sub(posa, pos-1)
160 | return part, pos
161 | elseif c:match'^[%w_]' then
162 | local newident,newpos = s:match('^([%w_]+)()', pos)
163 | if pos ~= posa and not wordop[newident] then -- non-first ident
164 | local pose = ((posce == pos) and poscs or pos) - 1
165 | while s:match('^%s', pose) do pose = pose - 1 end
166 | local ce = s:sub(pose,pose)
167 | if ce:match'[%)%}\'\"%]]' or
168 | ce:match'[%w_]' and not wordop[lastident]
169 | then
170 | local part = s:sub(posa, pos-1)
171 | return part, pos
172 | end
173 | end
174 | lastident, pos = newident, newpos
175 | else
176 | pos = pos + 1
177 | end
178 | pos = s:find('[%(%{%[%)%}%]\"\';,=%w_%-]', pos)
179 | end
180 | local part = s:sub(posa, #s)
181 | return part, #s+1
182 | end
183 | M.match_expression = match_expression
184 |
185 |
186 | -- Match name list (zero or more names). E.g. "a,b,c"
187 | -- Function interface is similar to match_string,
188 | -- but returns array as match.
189 | local function match_namelist(s, pos)
190 | pos = pos or 1
191 | local list = {}
192 | while 1 do
193 | local c = #list == 0 and '^' or '^%s*,%s*'
194 | local item, post = s:match(c .. '([%a_][%w_]*)%s*()', pos)
195 | if item then pos = post else break end
196 | list[#list+1] = item
197 | end
198 | return list, pos
199 | end
200 | M.match_namelist = match_namelist
201 |
202 |
203 | -- Match expression list (zero or more expressions). E.g. "a+b,b*c".
204 | -- Function interface is similar to match_string,
205 | -- but returns array as match.
206 | local function match_explist(s, pos)
207 | pos = pos or 1
208 | local list = {}
209 | while 1 do
210 | if #list ~= 0 then
211 | local post = s:match('^%s*,%s*()', pos)
212 | if post then pos = post else break end
213 | end
214 | local item; item, pos = match_expression(s, pos)
215 | assert(item, 'syntax error')
216 | list[#list+1] = item
217 | end
218 | return list, pos
219 | end
220 | M.match_explist = match_explist
221 |
222 |
223 | -- Replace snippets of code in Lua code string
224 | -- using replacement function f(u,sin) --> sout.
225 | -- is the type of snippet ('c' = comment, 's' = string,
226 | -- 'e' = any other code).
227 | -- Snippet is replaced with (unless is nil or false, in
228 | -- which case the original snippet is kept)
229 | -- This is somewhat analogous to string.gsub .
230 | local function gsub(s, f)
231 | local pos = 1
232 | local posa = 1
233 | local sret = ''
234 | while 1 do
235 | pos = s:find('[%-\'\"%[]', pos)
236 | if not pos then break end
237 | if s:match('^%-%-', pos) then
238 | local exp = s:sub(posa, pos-1)
239 | if #exp > 0 then sret = sret .. (f('e', exp) or exp) end
240 | local comment; comment, pos = match_comment(s, pos)
241 | sret = sret .. (f('c', assert(comment)) or comment)
242 | posa = pos
243 | else
244 | local posb = s:find('^[\'\"%[]', pos)
245 | local str
246 | if posb then str, pos = match_string(s, posb) end
247 | if str then
248 | local exp = s:sub(posa, posb-1)
249 | if #exp > 0 then sret = sret .. (f('e', exp) or exp) end
250 | sret = sret .. (f('s', str) or str)
251 | posa = pos
252 | else
253 | pos = pos + 1
254 | end
255 | end
256 | end
257 | local exp = s:sub(posa)
258 | if #exp > 0 then sret = sret .. (f('e', exp) or exp) end
259 | return sret
260 | end
261 | M.gsub = gsub
262 |
263 |
264 | return M
265 |
--------------------------------------------------------------------------------
/mod_push_appserver/pl/operator.lua:
--------------------------------------------------------------------------------
1 | --- Lua operators available as functions.
2 | --
3 | -- (similar to the Python module of the same name)
4 | --
5 | -- There is a module field `optable` which maps the operator strings
6 | -- onto these functions, e.g. `operator.optable['()']==operator.call`
7 | --
8 | -- Operator strings like '>' and '{}' can be passed to most Penlight functions
9 | -- expecting a function argument.
10 | --
11 | -- @module pl.operator
12 |
13 | local strfind = string.find
14 |
15 | local operator = {}
16 |
17 | --- apply function to some arguments **()**
18 | -- @param fn a function or callable object
19 | -- @param ... arguments
20 | function operator.call(fn,...)
21 | return fn(...)
22 | end
23 |
24 | --- get the indexed value from a table **[]**
25 | -- @param t a table or any indexable object
26 | -- @param k the key
27 | function operator.index(t,k)
28 | return t[k]
29 | end
30 |
31 | --- returns true if arguments are equal **==**
32 | -- @param a value
33 | -- @param b value
34 | function operator.eq(a,b)
35 | return a==b
36 | end
37 |
38 | --- returns true if arguments are not equal **~=**
39 | -- @param a value
40 | -- @param b value
41 | function operator.neq(a,b)
42 | return a~=b
43 | end
44 |
45 | --- returns true if a is less than b **<**
46 | -- @param a value
47 | -- @param b value
48 | function operator.lt(a,b)
49 | return a < b
50 | end
51 |
52 | --- returns true if a is less or equal to b **<=**
53 | -- @param a value
54 | -- @param b value
55 | function operator.le(a,b)
56 | return a <= b
57 | end
58 |
59 | --- returns true if a is greater than b **>**
60 | -- @param a value
61 | -- @param b value
62 | function operator.gt(a,b)
63 | return a > b
64 | end
65 |
66 | --- returns true if a is greater or equal to b **>=**
67 | -- @param a value
68 | -- @param b value
69 | function operator.ge(a,b)
70 | return a >= b
71 | end
72 |
73 | --- returns length of string or table **#**
74 | -- @param a a string or a table
75 | function operator.len(a)
76 | return #a
77 | end
78 |
79 | --- add two values **+**
80 | -- @param a value
81 | -- @param b value
82 | function operator.add(a,b)
83 | return a+b
84 | end
85 |
86 | --- subtract b from a **-**
87 | -- @param a value
88 | -- @param b value
89 | function operator.sub(a,b)
90 | return a-b
91 | end
92 |
93 | --- multiply two values __*__
94 | -- @param a value
95 | -- @param b value
96 | function operator.mul(a,b)
97 | return a*b
98 | end
99 |
100 | --- divide first value by second **/**
101 | -- @param a value
102 | -- @param b value
103 | function operator.div(a,b)
104 | return a/b
105 | end
106 |
107 | --- raise first to the power of second **^**
108 | -- @param a value
109 | -- @param b value
110 | function operator.pow(a,b)
111 | return a^b
112 | end
113 |
114 | --- modulo; remainder of a divided by b **%**
115 | -- @param a value
116 | -- @param b value
117 | function operator.mod(a,b)
118 | return a%b
119 | end
120 |
121 | --- concatenate two values (either strings or `__concat` defined) **..**
122 | -- @param a value
123 | -- @param b value
124 | function operator.concat(a,b)
125 | return a..b
126 | end
127 |
128 | --- return the negative of a value **-**
129 | -- @param a value
130 | function operator.unm(a)
131 | return -a
132 | end
133 |
134 | --- false if value evaluates as true **not**
135 | -- @param a value
136 | function operator.lnot(a)
137 | return not a
138 | end
139 |
140 | --- true if both values evaluate as true **and**
141 | -- @param a value
142 | -- @param b value
143 | function operator.land(a,b)
144 | return a and b
145 | end
146 |
147 | --- true if either value evaluate as true **or**
148 | -- @param a value
149 | -- @param b value
150 | function operator.lor(a,b)
151 | return a or b
152 | end
153 |
154 | --- make a table from the arguments **{}**
155 | -- @param ... non-nil arguments
156 | -- @return a table
157 | function operator.table (...)
158 | return {...}
159 | end
160 |
161 | --- match two strings **~**.
162 | -- uses @{string.find}
163 | function operator.match (a,b)
164 | return strfind(a,b)~=nil
165 | end
166 |
167 | --- the null operation.
168 | -- @param ... arguments
169 | -- @return the arguments
170 | function operator.nop (...)
171 | return ...
172 | end
173 |
174 | ---- Map from operator symbol to function.
175 | -- Most of these map directly from operators;
176 | -- But note these extras
177 | --
178 | -- * __'()'__ `call`
179 | -- * __'[]'__ `index`
180 | -- * __'{}'__ `table`
181 | -- * __'~'__ `match`
182 | --
183 | -- @table optable
184 | -- @field operator
185 | operator.optable = {
186 | ['+']=operator.add,
187 | ['-']=operator.sub,
188 | ['*']=operator.mul,
189 | ['/']=operator.div,
190 | ['%']=operator.mod,
191 | ['^']=operator.pow,
192 | ['..']=operator.concat,
193 | ['()']=operator.call,
194 | ['[]']=operator.index,
195 | ['<']=operator.lt,
196 | ['<=']=operator.le,
197 | ['>']=operator.gt,
198 | ['>=']=operator.ge,
199 | ['==']=operator.eq,
200 | ['~=']=operator.neq,
201 | ['#']=operator.len,
202 | ['and']=operator.land,
203 | ['or']=operator.lor,
204 | ['{}']=operator.table,
205 | ['~']=operator.match,
206 | ['']=operator.nop,
207 | }
208 |
209 | return operator
210 |
--------------------------------------------------------------------------------
/mod_push_appserver/pl/path.lua:
--------------------------------------------------------------------------------
1 | --- Path manipulation and file queries.
2 | --
3 | -- This is modelled after Python's os.path library (10.1); see @{04-paths.md|the Guide}.
4 | --
5 | -- Dependencies: `pl.utils`, `lfs`
6 | -- @module pl.path
7 |
8 | -- imports and locals
9 | local _G = _G
10 | local sub = string.sub
11 | local getenv = os.getenv
12 | local tmpnam = os.tmpname
13 | local attributes, currentdir, link_attrib
14 | local package = package
15 | local append, concat, remove = table.insert, table.concat, table.remove
16 | local utils = require 'pl.utils'
17 | local assert_string,raise = utils.assert_string,utils.raise
18 |
19 | local attrib
20 | local path = {}
21 |
22 | local res,lfs = _G.pcall(_G.require,'lfs')
23 | if res then
24 | attributes = lfs.attributes
25 | currentdir = lfs.currentdir
26 | link_attrib = lfs.symlinkattributes
27 | else
28 | error("pl.path requires LuaFileSystem")
29 | end
30 |
31 | attrib = attributes
32 | path.attrib = attrib
33 | path.link_attrib = link_attrib
34 |
35 | --- Lua iterator over the entries of a given directory.
36 | -- Behaves like `lfs.dir`
37 | path.dir = lfs.dir
38 |
39 | --- Creates a directory.
40 | path.mkdir = lfs.mkdir
41 |
42 | --- Removes a directory.
43 | path.rmdir = lfs.rmdir
44 |
45 | ---- Get the working directory.
46 | path.currentdir = currentdir
47 |
48 | --- Changes the working directory.
49 | path.chdir = lfs.chdir
50 |
51 |
52 | --- is this a directory?
53 | -- @string P A file path
54 | function path.isdir(P)
55 | assert_string(1,P)
56 | if P:match("\\$") then
57 | P = P:sub(1,-2)
58 | end
59 | return attrib(P,'mode') == 'directory'
60 | end
61 |
62 | --- is this a file?.
63 | -- @string P A file path
64 | function path.isfile(P)
65 | assert_string(1,P)
66 | return attrib(P,'mode') == 'file'
67 | end
68 |
69 | -- is this a symbolic link?
70 | -- @string P A file path
71 | function path.islink(P)
72 | assert_string(1,P)
73 | if link_attrib then
74 | return link_attrib(P,'mode')=='link'
75 | else
76 | return false
77 | end
78 | end
79 |
80 | --- return size of a file.
81 | -- @string P A file path
82 | function path.getsize(P)
83 | assert_string(1,P)
84 | return attrib(P,'size')
85 | end
86 |
87 | --- does a path exist?.
88 | -- @string P A file path
89 | -- @return the file path if it exists, nil otherwise
90 | function path.exists(P)
91 | assert_string(1,P)
92 | return attrib(P,'mode') ~= nil and P
93 | end
94 |
95 | --- Return the time of last access as the number of seconds since the epoch.
96 | -- @string P A file path
97 | function path.getatime(P)
98 | assert_string(1,P)
99 | return attrib(P,'access')
100 | end
101 |
102 | --- Return the time of last modification
103 | -- @string P A file path
104 | function path.getmtime(P)
105 | return attrib(P,'modification')
106 | end
107 |
108 | ---Return the system's ctime.
109 | -- @string P A file path
110 | function path.getctime(P)
111 | assert_string(1,P)
112 | return path.attrib(P,'change')
113 | end
114 |
115 |
116 | local function at(s,i)
117 | return sub(s,i,i)
118 | end
119 |
120 | path.is_windows = utils.is_windows
121 |
122 | local other_sep
123 | -- !constant sep is the directory separator for this platform.
124 | if path.is_windows then
125 | path.sep = '\\'; other_sep = '/'
126 | path.dirsep = ';'
127 | else
128 | path.sep = '/'
129 | path.dirsep = ':'
130 | end
131 | local sep = path.sep
132 |
133 | --- are we running Windows?
134 | -- @class field
135 | -- @name path.is_windows
136 |
137 | --- path separator for this platform.
138 | -- @class field
139 | -- @name path.sep
140 |
141 | --- separator for PATH for this platform
142 | -- @class field
143 | -- @name path.dirsep
144 |
145 | --- given a path, return the directory part and a file part.
146 | -- if there's no directory part, the first value will be empty
147 | -- @string P A file path
148 | function path.splitpath(P)
149 | assert_string(1,P)
150 | local i = #P
151 | local ch = at(P,i)
152 | while i > 0 and ch ~= sep and ch ~= other_sep do
153 | i = i - 1
154 | ch = at(P,i)
155 | end
156 | if i == 0 then
157 | return '',P
158 | else
159 | return sub(P,1,i-1), sub(P,i+1)
160 | end
161 | end
162 |
163 | --- return an absolute path.
164 | -- @string P A file path
165 | -- @string[opt] pwd optional start path to use (default is current dir)
166 | function path.abspath(P,pwd)
167 | assert_string(1,P)
168 | if pwd then assert_string(2,pwd) end
169 | local use_pwd = pwd ~= nil
170 | if not use_pwd and not currentdir then return P end
171 | P = P:gsub('[\\/]$','')
172 | pwd = pwd or currentdir()
173 | if not path.isabs(P) then
174 | P = path.join(pwd,P)
175 | elseif path.is_windows and not use_pwd and at(P,2) ~= ':' and at(P,2) ~= '\\' then
176 | P = pwd:sub(1,2)..P -- attach current drive to path like '\\fred.txt'
177 | end
178 | return path.normpath(P)
179 | end
180 |
181 | --- given a path, return the root part and the extension part.
182 | -- if there's no extension part, the second value will be empty
183 | -- @string P A file path
184 | -- @treturn string root part
185 | -- @treturn string extension part (maybe empty)
186 | function path.splitext(P)
187 | assert_string(1,P)
188 | local i = #P
189 | local ch = at(P,i)
190 | while i > 0 and ch ~= '.' do
191 | if ch == sep or ch == other_sep then
192 | return P,''
193 | end
194 | i = i - 1
195 | ch = at(P,i)
196 | end
197 | if i == 0 then
198 | return P,''
199 | else
200 | return sub(P,1,i-1),sub(P,i)
201 | end
202 | end
203 |
204 | --- return the directory part of a path
205 | -- @string P A file path
206 | function path.dirname(P)
207 | assert_string(1,P)
208 | local p1 = path.splitpath(P)
209 | return p1
210 | end
211 |
212 | --- return the file part of a path
213 | -- @string P A file path
214 | function path.basename(P)
215 | assert_string(1,P)
216 | local _,p2 = path.splitpath(P)
217 | return p2
218 | end
219 |
220 | --- get the extension part of a path.
221 | -- @string P A file path
222 | function path.extension(P)
223 | assert_string(1,P)
224 | local _,p2 = path.splitext(P)
225 | return p2
226 | end
227 |
228 | --- is this an absolute path?.
229 | -- @string P A file path
230 | function path.isabs(P)
231 | assert_string(1,P)
232 | if path.is_windows then
233 | return at(P,1) == '/' or at(P,1)=='\\' or at(P,2)==':'
234 | else
235 | return at(P,1) == '/'
236 | end
237 | end
238 |
239 | --- return the path resulting from combining the individual paths.
240 | -- if the second (or later) path is absolute, we return the last absolute path (joined with any non-absolute paths following).
241 | -- empty elements (except the last) will be ignored.
242 | -- @string p1 A file path
243 | -- @string p2 A file path
244 | -- @string ... more file paths
245 | function path.join(p1,p2,...)
246 | assert_string(1,p1)
247 | assert_string(2,p2)
248 | if select('#',...) > 0 then
249 | local p = path.join(p1,p2)
250 | local args = {...}
251 | for i = 1,#args do
252 | assert_string(i,args[i])
253 | p = path.join(p,args[i])
254 | end
255 | return p
256 | end
257 | if path.isabs(p2) then return p2 end
258 | local endc = at(p1,#p1)
259 | if endc ~= path.sep and endc ~= other_sep and endc ~= "" then
260 | p1 = p1..path.sep
261 | end
262 | return p1..p2
263 | end
264 |
265 | --- normalize the case of a pathname. On Unix, this returns the path unchanged;
266 | -- for Windows, it converts the path to lowercase, and it also converts forward slashes
267 | -- to backward slashes.
268 | -- @string P A file path
269 | function path.normcase(P)
270 | assert_string(1,P)
271 | if path.is_windows then
272 | return (P:lower():gsub('/','\\'))
273 | else
274 | return P
275 | end
276 | end
277 |
278 | --- normalize a path name.
279 | -- A//B, A/./B and A/foo/../B all become A/B.
280 | -- @string P a file path
281 | function path.normpath(P)
282 | assert_string(1,P)
283 | -- Split path into anchor and relative path.
284 | local anchor = ''
285 | if path.is_windows then
286 | if P:match '^\\\\' then -- UNC
287 | anchor = '\\\\'
288 | P = P:sub(3)
289 | elseif at(P, 1) == '/' or at(P, 1) == '\\' then
290 | anchor = '\\'
291 | P = P:sub(2)
292 | elseif at(P, 2) == ':' then
293 | anchor = P:sub(1, 2)
294 | P = P:sub(3)
295 | if at(P, 1) == '/' or at(P, 1) == '\\' then
296 | anchor = anchor..'\\'
297 | P = P:sub(2)
298 | end
299 | end
300 | P = P:gsub('/','\\')
301 | else
302 | -- According to POSIX, in path start '//' and '/' are distinct,
303 | -- but '///+' is equivalent to '/'.
304 | if P:match '^//' and at(P, 3) ~= '/' then
305 | anchor = '//'
306 | P = P:sub(3)
307 | elseif at(P, 1) == '/' then
308 | anchor = '/'
309 | P = P:match '^/*(.*)$'
310 | end
311 | end
312 | local parts = {}
313 | for part in P:gmatch('[^'..sep..']+') do
314 | if part == '..' then
315 | if #parts ~= 0 and parts[#parts] ~= '..' then
316 | remove(parts)
317 | else
318 | append(parts, part)
319 | end
320 | elseif part ~= '.' then
321 | append(parts, part)
322 | end
323 | end
324 | P = anchor..concat(parts, sep)
325 | if P == '' then P = '.' end
326 | return P
327 | end
328 |
329 | --- relative path from current directory or optional start point
330 | -- @string P a path
331 | -- @string[opt] start optional start point (default current directory)
332 | function path.relpath (P,start)
333 | assert_string(1,P)
334 | if start then assert_string(2,start) end
335 | local split,normcase,min,append = utils.split, path.normcase, math.min, table.insert
336 | P = normcase(path.abspath(P,start))
337 | start = start or currentdir()
338 | start = normcase(start)
339 | local startl, Pl = split(start,sep), split(P,sep)
340 | local n = min(#startl,#Pl)
341 | if path.is_windows and n > 0 and at(Pl[1],2) == ':' and Pl[1] ~= startl[1] then
342 | return P
343 | end
344 | local k = n+1 -- default value if this loop doesn't bail out!
345 | for i = 1,n do
346 | if startl[i] ~= Pl[i] then
347 | k = i
348 | break
349 | end
350 | end
351 | local rell = {}
352 | for i = 1, #startl-k+1 do rell[i] = '..' end
353 | if k <= #Pl then
354 | for i = k,#Pl do append(rell,Pl[i]) end
355 | end
356 | return table.concat(rell,sep)
357 | end
358 |
359 |
360 | --- Replace a starting '~' with the user's home directory.
361 | -- In windows, if HOME isn't set, then USERPROFILE is used in preference to
362 | -- HOMEDRIVE HOMEPATH. This is guaranteed to be writeable on all versions of Windows.
363 | -- @string P A file path
364 | function path.expanduser(P)
365 | assert_string(1,P)
366 | if at(P,1) == '~' then
367 | local home = getenv('HOME')
368 | if not home then -- has to be Windows
369 | home = getenv 'USERPROFILE' or (getenv 'HOMEDRIVE' .. getenv 'HOMEPATH')
370 | end
371 | return home..sub(P,2)
372 | else
373 | return P
374 | end
375 | end
376 |
377 |
378 | ---Return a suitable full path to a new temporary file name.
379 | -- unlike os.tmpnam(), it always gives you a writeable path (uses TEMP environment variable on Windows)
380 | function path.tmpname ()
381 | local res = tmpnam()
382 | -- On Windows if Lua is compiled using MSVC14 os.tmpname
383 | -- already returns an absolute path within TEMP env variable directory,
384 | -- no need to prepend it.
385 | if path.is_windows and not res:find(':') then
386 | res = getenv('TEMP')..res
387 | end
388 | return res
389 | end
390 |
391 | --- return the largest common prefix path of two paths.
392 | -- @string path1 a file path
393 | -- @string path2 a file path
394 | function path.common_prefix (path1,path2)
395 | assert_string(1,path1)
396 | assert_string(2,path2)
397 | path1, path2 = path.normcase(path1), path.normcase(path2)
398 | -- get them in order!
399 | if #path1 > #path2 then path2,path1 = path1,path2 end
400 | for i = 1,#path1 do
401 | local c1 = at(path1,i)
402 | if c1 ~= at(path2,i) then
403 | local cp = path1:sub(1,i-1)
404 | if at(path1,i-1) ~= sep then
405 | cp = path.dirname(cp)
406 | end
407 | return cp
408 | end
409 | end
410 | if at(path2,#path1+1) ~= sep then path1 = path.dirname(path1) end
411 | return path1
412 | --return ''
413 | end
414 |
415 | --- return the full path where a particular Lua module would be found.
416 | -- Both package.path and package.cpath is searched, so the result may
417 | -- either be a Lua file or a shared library.
418 | -- @string mod name of the module
419 | -- @return on success: path of module, lua or binary
420 | -- @return on error: nil,error string
421 | function path.package_path(mod)
422 | assert_string(1,mod)
423 | local res
424 | mod = mod:gsub('%.',sep)
425 | res = package.searchpath(mod,package.path)
426 | if res then return res,true end
427 | res = package.searchpath(mod,package.cpath)
428 | if res then return res,false end
429 | return raise 'cannot find module on path'
430 | end
431 |
432 |
433 | ---- finis -----
434 | return path
435 |
--------------------------------------------------------------------------------
/mod_push_appserver/pl/permute.lua:
--------------------------------------------------------------------------------
1 | --- Permutation operations.
2 | --
3 | -- Dependencies: `pl.utils`, `pl.tablex`
4 | -- @module pl.permute
5 | local tablex = require 'pl.tablex'
6 | local utils = require 'pl.utils'
7 | local copy = tablex.deepcopy
8 | local append = table.insert
9 | local coroutine = coroutine
10 | local resume = coroutine.resume
11 | local assert_arg = utils.assert_arg
12 |
13 |
14 | local permute = {}
15 |
16 | -- PiL, 9.3
17 |
18 | local permgen
19 | permgen = function (a, n, fn)
20 | if n == 0 then
21 | fn(a)
22 | else
23 | for i=1,n do
24 | -- put i-th element as the last one
25 | a[n], a[i] = a[i], a[n]
26 |
27 | -- generate all permutations of the other elements
28 | permgen(a, n - 1, fn)
29 |
30 | -- restore i-th element
31 | a[n], a[i] = a[i], a[n]
32 |
33 | end
34 | end
35 | end
36 |
37 | --- an iterator over all permutations of the elements of a list.
38 | -- Please note that the same list is returned each time, so do not keep references!
39 | -- @param a list-like table
40 | -- @return an iterator which provides the next permutation as a list
41 | function permute.iter (a)
42 | assert_arg(1,a,'table')
43 | local n = #a
44 | local co = coroutine.create(function () permgen(a, n, coroutine.yield) end)
45 | return function () -- iterator
46 | local _, res = resume(co)
47 | return res
48 | end
49 | end
50 |
51 | --- construct a table containing all the permutations of a list.
52 | -- @param a list-like table
53 | -- @return a table of tables
54 | -- @usage permute.table {1,2,3} --> {{2,3,1},{3,2,1},{3,1,2},{1,3,2},{2,1,3},{1,2,3}}
55 | function permute.table (a)
56 | assert_arg(1,a,'table')
57 | local res = {}
58 | local n = #a
59 | permgen(a,n,function(t) append(res,copy(t)) end)
60 | return res
61 | end
62 |
63 | return permute
64 |
--------------------------------------------------------------------------------
/mod_push_appserver/pl/pretty.lua:
--------------------------------------------------------------------------------
1 | --- Pretty-printing Lua tables.
2 | -- Also provides a sandboxed Lua table reader and
3 | -- a function to present large numbers in human-friendly format.
4 | --
5 | -- Dependencies: `pl.utils`, `pl.lexer`, `pl.stringx`, `debug`
6 | -- @module pl.pretty
7 |
8 | local append = table.insert
9 | local concat = table.concat
10 | local mfloor, mhuge = math.floor, math.huge
11 | local mtype = math.type
12 | local utils = require 'pl.utils'
13 | local lexer = require 'pl.lexer'
14 | local debug = require 'debug'
15 | local quote_string = require'pl.stringx'.quote_string
16 | local assert_arg = utils.assert_arg
17 |
18 | local original_tostring = tostring
19 |
20 | -- Patch tostring to format numbers with better precision
21 | -- and to produce cross-platform results for
22 | -- infinite values and NaN.
23 | local function tostring(value)
24 | if type(value) ~= "number" then
25 | return original_tostring(value)
26 | elseif value ~= value then
27 | return "NaN"
28 | elseif value == mhuge then
29 | return "Inf"
30 | elseif value == -mhuge then
31 | return "-Inf"
32 | elseif (_VERSION ~= "Lua 5.3" or mtype(value) == "integer") and mfloor(value) == value then
33 | return ("%d"):format(value)
34 | else
35 | local res = ("%.14g"):format(value)
36 | if _VERSION == "Lua 5.3" and mtype(value) == "float" and not res:find("%.") then
37 | -- Number is internally a float but looks like an integer.
38 | -- Insert ".0" after first run of digits.
39 | res = res:gsub("%d+", "%0.0", 1)
40 | end
41 | return res
42 | end
43 | end
44 |
45 | local pretty = {}
46 |
47 | local function save_global_env()
48 | local env = {}
49 | env.hook, env.mask, env.count = debug.gethook()
50 |
51 | -- env.hook is "external hook" if is a C hook function
52 | if env.hook~="external hook" then
53 | debug.sethook()
54 | end
55 |
56 | env.string_mt = getmetatable("")
57 | debug.setmetatable("", nil)
58 | return env
59 | end
60 |
61 | local function restore_global_env(env)
62 | if env then
63 | debug.setmetatable("", env.string_mt)
64 | if env.hook~="external hook" then
65 | debug.sethook(env.hook, env.mask, env.count)
66 | end
67 | end
68 | end
69 |
70 | --- Read a string representation of a Lua table.
71 | -- This function loads and runs the string as Lua code, but bails out
72 | -- if it contains a function definition.
73 | -- Loaded string is executed in an empty environment.
74 | -- @string s string to read in `{...}` format, possibly with some whitespace
75 | -- before or after the curly braces. A single line comment may be present
76 | -- at the beginning.
77 | -- @return a table in case of success.
78 | -- If loading the string failed, return `nil` and error message.
79 | -- If executing loaded string failed, return `nil` and the error it raised.
80 | function pretty.read(s)
81 | assert_arg(1,s,'string')
82 | if s:find '^%s*%-%-' then -- may start with a comment..
83 | s = s:gsub('%-%-.-\n','')
84 | end
85 | if not s:find '^%s*{' then return nil,"not a Lua table" end
86 | if s:find '[^\'"%w_]function[^\'"%w_]' then
87 | local tok = lexer.lua(s)
88 | for t,v in tok do
89 | if t == 'keyword' and v == 'function' then
90 | return nil,"cannot have functions in table definition"
91 | end
92 | end
93 | end
94 | s = 'return '..s
95 | local chunk,err = utils.load(s,'tbl','t',{})
96 | if not chunk then return nil,err end
97 | local global_env = save_global_env()
98 | local ok,ret = pcall(chunk)
99 | restore_global_env(global_env)
100 | if ok then return ret
101 | else
102 | return nil,ret
103 | end
104 | end
105 |
106 | --- Read a Lua chunk.
107 | -- @string s Lua code.
108 | -- @tab[opt] env environment used to run the code, empty by default.
109 | -- @bool[opt] paranoid abort loading if any looping constructs a found in the code
110 | -- and disable string methods.
111 | -- @return the environment in case of success or `nil` and syntax or runtime error
112 | -- if something went wrong.
113 | function pretty.load (s, env, paranoid)
114 | env = env or {}
115 | if paranoid then
116 | local tok = lexer.lua(s)
117 | for t,v in tok do
118 | if t == 'keyword'
119 | and (v == 'for' or v == 'repeat' or v == 'function' or v == 'goto')
120 | then
121 | return nil,"looping not allowed"
122 | end
123 | end
124 | end
125 | local chunk,err = utils.load(s,'tbl','t',env)
126 | if not chunk then return nil,err end
127 | local global_env = paranoid and save_global_env()
128 | local ok,err = pcall(chunk)
129 | restore_global_env(global_env)
130 | if not ok then return nil,err end
131 | return env
132 | end
133 |
134 | local function quote_if_necessary (v)
135 | if not v then return ''
136 | else
137 | --AAS
138 | if v:find ' ' then v = quote_string(v) end
139 | end
140 | return v
141 | end
142 |
143 | local keywords
144 |
145 | local function is_identifier (s)
146 | return type(s) == 'string' and s:find('^[%a_][%w_]*$') and not keywords[s]
147 | end
148 |
149 | local function quote (s)
150 | if type(s) == 'table' then
151 | return pretty.write(s,'')
152 | else
153 | --AAS
154 | return quote_string(s)-- ('%q'):format(tostring(s))
155 | end
156 | end
157 |
158 | local function index (numkey,key)
159 | --AAS
160 | if not numkey then
161 | key = quote(key)
162 | key = key:find("^%[") and (" " .. key .. " ") or key
163 | end
164 | return '['..key..']'
165 | end
166 |
167 |
168 | --- Create a string representation of a Lua table.
169 | -- This function never fails, but may complain by returning an
170 | -- extra value. Normally puts out one item per line, using
171 | -- the provided indent; set the second parameter to an empty string
172 | -- if you want output on one line.
173 | -- @tab tbl Table to serialize to a string.
174 | -- @string[opt] space The indent to use.
175 | -- Defaults to two spaces; pass an empty string for no indentation.
176 | -- @bool[opt] not_clever Pass `true` for plain output, e.g `{['key']=1}`.
177 | -- Defaults to `false`.
178 | -- @return a string
179 | -- @return an optional error message
180 | function pretty.write (tbl,space,not_clever)
181 | if type(tbl) ~= 'table' then
182 | local res = tostring(tbl)
183 | if type(tbl) == 'string' then return quote(tbl) end
184 | return res, 'not a table'
185 | end
186 | if not keywords then
187 | keywords = lexer.get_keywords()
188 | end
189 | local set = ' = '
190 | if space == '' then set = '=' end
191 | space = space or ' '
192 | local lines = {}
193 | local line = ''
194 | local tables = {}
195 |
196 |
197 | local function put(s)
198 | if #s > 0 then
199 | line = line..s
200 | end
201 | end
202 |
203 | local function putln (s)
204 | if #line > 0 then
205 | line = line..s
206 | append(lines,line)
207 | line = ''
208 | else
209 | append(lines,s)
210 | end
211 | end
212 |
213 | local function eat_last_comma ()
214 | local n = #lines
215 | local lastch = lines[n]:sub(-1,-1)
216 | if lastch == ',' then
217 | lines[n] = lines[n]:sub(1,-2)
218 | end
219 | end
220 |
221 |
222 | local writeit
223 | writeit = function (t,oldindent,indent)
224 | local tp = type(t)
225 | if tp ~= 'string' and tp ~= 'table' then
226 | putln(quote_if_necessary(tostring(t))..',')
227 | elseif tp == 'string' then
228 | -- if t:find('\n') then
229 | -- putln('[[\n'..t..']],')
230 | -- else
231 | -- putln(quote(t)..',')
232 | -- end
233 | --AAS
234 | putln(quote_string(t) ..",")
235 | elseif tp == 'table' then
236 | if tables[t] then
237 | putln(',')
238 | return
239 | end
240 | tables[t] = true
241 | local newindent = indent..space
242 | putln('{')
243 | local used = {}
244 | if not not_clever then
245 | for i,val in ipairs(t) do
246 | put(indent)
247 | writeit(val,indent,newindent)
248 | used[i] = true
249 | end
250 | end
251 | for key,val in pairs(t) do
252 | local tkey = type(key)
253 | local numkey = tkey == 'number'
254 | if not_clever then
255 | key = tostring(key)
256 | put(indent..index(numkey,key)..set)
257 | writeit(val,indent,newindent)
258 | else
259 | if not numkey or not used[key] then -- non-array indices
260 | if tkey ~= 'string' then
261 | key = tostring(key)
262 | end
263 | if numkey or not is_identifier(key) then
264 | key = index(numkey,key)
265 | end
266 | put(indent..key..set)
267 | writeit(val,indent,newindent)
268 | end
269 | end
270 | end
271 | tables[t] = nil
272 | eat_last_comma()
273 | putln(oldindent..'},')
274 | else
275 | putln(tostring(t)..',')
276 | end
277 | end
278 | writeit(tbl,'',space)
279 | eat_last_comma()
280 | return concat(lines,#space > 0 and '\n' or '')
281 | end
282 |
283 | --- Dump a Lua table out to a file or stdout.
284 | -- @tab t The table to write to a file or stdout.
285 | -- @string[opt] filename File name to write too. Defaults to writing
286 | -- to stdout.
287 | function pretty.dump (t, filename)
288 | if not filename then
289 | print(pretty.write(t))
290 | return true
291 | else
292 | return utils.writefile(filename, pretty.write(t))
293 | end
294 | end
295 |
296 | local memp,nump = {'B','KiB','MiB','GiB'},{'','K','M','B'}
297 |
298 | local function comma (val)
299 | local thou = math.floor(val/1000)
300 | if thou > 0 then return comma(thou)..','.. tostring(val % 1000)
301 | else return tostring(val) end
302 | end
303 |
304 | --- Format large numbers nicely for human consumption.
305 | -- @number num a number.
306 | -- @string[opt] kind one of `'M'` (memory in `KiB`, `MiB`, etc.),
307 | -- `'N'` (postfixes are `'K'`, `'M'` and `'B'`),
308 | -- or `'T'` (use commas as thousands separator), `'N'` by default.
309 | -- @int[opt] prec number of digits to use for `'M'` and `'N'`, `1` by default.
310 | function pretty.number (num,kind,prec)
311 | local fmt = '%.'..(prec or 1)..'f%s'
312 | if kind == 'T' then
313 | return comma(num)
314 | else
315 | local postfixes, fact
316 | if kind == 'M' then
317 | fact = 1024
318 | postfixes = memp
319 | else
320 | fact = 1000
321 | postfixes = nump
322 | end
323 | local div = fact
324 | local k = 1
325 | while num >= div and k <= #postfixes do
326 | div = div * fact
327 | k = k + 1
328 | end
329 | div = div / fact
330 | if k > #postfixes then k = k - 1; div = div/fact end
331 | if k > 1 then
332 | return fmt:format(num/div,postfixes[k] or 'duh')
333 | else
334 | return num..postfixes[1]
335 | end
336 | end
337 | end
338 |
339 | return pretty
340 |
--------------------------------------------------------------------------------
/mod_push_appserver/pl/sip.lua:
--------------------------------------------------------------------------------
1 | --- Simple Input Patterns (SIP).
2 | -- SIP patterns start with '$', then a
3 | -- one-letter type, and then an optional variable in curly braces.
4 | --
5 | -- sip.match('$v=$q','name="dolly"',res)
6 | -- ==> res=={'name','dolly'}
7 | -- sip.match('($q{first},$q{second})','("john","smith")',res)
8 | -- ==> res=={second='smith',first='john'}
9 | --
10 | -- Type names:
11 | --
12 | -- v identifier
13 | -- i integer
14 | -- f floating-point
15 | -- q quoted string
16 | -- ([{< match up to closing bracket
17 | --
18 | -- See @{08-additional.md.Simple_Input_Patterns|the Guide}
19 | --
20 | -- @module pl.sip
21 |
22 | local loadstring = rawget(_G,'loadstring') or load
23 | local unpack = rawget(_G,'unpack') or rawget(table,'unpack')
24 |
25 | local append,concat = table.insert,table.concat
26 | local ipairs,type = ipairs,type
27 | local io,_G = io,_G
28 | local print,rawget = print,rawget
29 |
30 | local patterns = {
31 | FLOAT = '[%+%-%d]%d*%.?%d*[eE]?[%+%-]?%d*',
32 | INTEGER = '[+%-%d]%d*',
33 | IDEN = '[%a_][%w_]*',
34 | OPTION = '[%a_][%w_%-]*',
35 | }
36 |
37 | local function assert_arg(idx,val,tp)
38 | if type(val) ~= tp then
39 | error("argument "..idx.." must be "..tp, 2)
40 | end
41 | end
42 |
43 | local sip = {}
44 |
45 | local brackets = {['<'] = '>', ['('] = ')', ['{'] = '}', ['['] = ']' }
46 | local stdclasses = {a=1,c=0,d=1,l=1,p=0,u=1,w=1,x=1,s=0}
47 |
48 | local function group(s)
49 | return '('..s..')'
50 | end
51 |
52 | -- escape all magic characters except $, which has special meaning
53 | -- Also, un-escape any characters after $, so $( and $[ passes through as is.
54 | local function escape (spec)
55 | return (spec:gsub('[%-%.%+%[%]%(%)%^%%%?%*]','%%%0'):gsub('%$%%(%S)','$%1'))
56 | end
57 |
58 | -- Most spaces within patterns can match zero or more spaces.
59 | -- Spaces between alphanumeric characters or underscores or between
60 | -- patterns that can match these characters, however, must match at least
61 | -- one space. Otherwise '$v $v' would match 'abcd' as {'abc', 'd'}.
62 | -- This function replaces continuous spaces within a pattern with either
63 | -- '%s*' or '%s+' according to this rule. The pattern has already
64 | -- been stripped of pattern names by now.
65 | local function compress_spaces(patt)
66 | return (patt:gsub("()%s+()", function(i1, i2)
67 | local before = patt:sub(i1 - 2, i1 - 1)
68 | if before:match('%$[vifadxlu]') or before:match('^[^%$]?[%w_]$') then
69 | local after = patt:sub(i2, i2 + 1)
70 | if after:match('%$[vifadxlu]') or after:match('^[%w_]') then
71 | return '%s+'
72 | end
73 | end
74 | return '%s*'
75 | end))
76 | end
77 |
78 | local pattern_map = {
79 | v = group(patterns.IDEN),
80 | i = group(patterns.INTEGER),
81 | f = group(patterns.FLOAT),
82 | o = group(patterns.OPTION),
83 | r = '(%S.*)',
84 | p = '([%a]?[:]?[\\/%.%w_]+)'
85 | }
86 |
87 | function sip.custom_pattern(flag,patt)
88 | pattern_map[flag] = patt
89 | end
90 |
91 | --- convert a SIP pattern into the equivalent Lua string pattern.
92 | -- @param spec a SIP pattern
93 | -- @param options a table; only the at_start
field is
94 | -- currently meaningful and ensures that the pattern is anchored
95 | -- at the start of the string.
96 | -- @return a Lua string pattern.
97 | function sip.create_pattern (spec,options)
98 | assert_arg(1,spec,'string')
99 | local fieldnames,fieldtypes = {},{}
100 |
101 | if type(spec) == 'string' then
102 | spec = escape(spec)
103 | else
104 | local res = {}
105 | for i,s in ipairs(spec) do
106 | res[i] = escape(s)
107 | end
108 | spec = concat(res,'.-')
109 | end
110 |
111 | local kount = 1
112 |
113 | local function addfield (name,type)
114 | name = name or kount
115 | append(fieldnames,name)
116 | fieldtypes[name] = type
117 | kount = kount + 1
118 | end
119 |
120 | local named_vars = spec:find('{%a+}')
121 |
122 | if options and options.at_start then
123 | spec = '^'..spec
124 | end
125 | if spec:sub(-1,-1) == '$' then
126 | spec = spec:sub(1,-2)..'$r'
127 | if named_vars then spec = spec..'{rest}' end
128 | end
129 |
130 | local names
131 |
132 | if named_vars then
133 | names = {}
134 | spec = spec:gsub('{(%a+)}',function(name)
135 | append(names,name)
136 | return ''
137 | end)
138 | end
139 | spec = compress_spaces(spec)
140 |
141 | local k = 1
142 | local err
143 | local r = (spec:gsub('%$%S',function(s)
144 | local type,name
145 | type = s:sub(2,2)
146 | if names then name = names[k]; k=k+1 end
147 | -- this kludge is necessary because %q generates two matches, and
148 | -- we want to ignore the first. Not a problem for named captures.
149 | if not names and type == 'q' then
150 | addfield(nil,'Q')
151 | else
152 | addfield(name,type)
153 | end
154 | local res
155 | if pattern_map[type] then
156 | res = pattern_map[type]
157 | elseif type == 'q' then
158 | -- some Lua pattern matching voodoo; we want to match '...' as
159 | -- well as "...", and can use the fact that %n will match a
160 | -- previous capture. Adding the extra field above comes from needing
161 | -- to accommodate the extra spurious match (which is either ' or ")
162 | addfield(name,type)
163 | res = '(["\'])(.-)%'..(kount-2)
164 | else
165 | local endbracket = brackets[type]
166 | if endbracket then
167 | res = '(%b'..type..endbracket..')'
168 | elseif stdclasses[type] or stdclasses[type:lower()] then
169 | res = '(%'..type..'+)'
170 | else
171 | err = "unknown format type or character class"
172 | end
173 | end
174 | return res
175 | end))
176 |
177 | if err then
178 | return nil,err
179 | else
180 | return r,fieldnames,fieldtypes
181 | end
182 | end
183 |
184 |
185 | local function tnumber (s)
186 | return s == 'd' or s == 'i' or s == 'f'
187 | end
188 |
189 | function sip.create_spec_fun(spec,options)
190 | local fieldtypes,fieldnames
191 | local ls = {}
192 | spec,fieldnames,fieldtypes = sip.create_pattern(spec,options)
193 | if not spec then return spec,fieldnames end
194 | local named_vars = type(fieldnames[1]) == 'string'
195 | for i = 1,#fieldnames do
196 | append(ls,'mm'..i)
197 | end
198 | ls[1] = ls[1] or "mm1" -- behave correctly if there are no patterns
199 | local fun = ('return (function(s,res)\n\tlocal %s = s:match(%q)\n'):format(concat(ls,','),spec)
200 | fun = fun..'\tif not mm1 then return false end\n'
201 | local k=1
202 | for i,f in ipairs(fieldnames) do
203 | if f ~= '_' then
204 | local var = 'mm'..i
205 | if tnumber(fieldtypes[f]) then
206 | var = 'tonumber('..var..')'
207 | elseif brackets[fieldtypes[f]] then
208 | var = var..':sub(2,-2)'
209 | end
210 | if named_vars then
211 | fun = ('%s\tres.%s = %s\n'):format(fun,f,var)
212 | else
213 | if fieldtypes[f] ~= 'Q' then -- we skip the string-delim capture
214 | fun = ('%s\tres[%d] = %s\n'):format(fun,k,var)
215 | k = k + 1
216 | end
217 | end
218 | end
219 | end
220 | return fun..'\treturn true\nend)\n', named_vars
221 | end
222 |
223 | --- convert a SIP pattern into a matching function.
224 | -- The returned function takes two arguments, the line and an empty table.
225 | -- If the line matched the pattern, then this function returns true
226 | -- and the table is filled with field-value pairs.
227 | -- @param spec a SIP pattern
228 | -- @param options optional table; {at_start=true} ensures that the pattern
229 | -- is anchored at the start of the string.
230 | -- @return a function if successful, or nil,error
231 | function sip.compile(spec,options)
232 | assert_arg(1,spec,'string')
233 | local fun,names = sip.create_spec_fun(spec,options)
234 | if not fun then return nil,names end
235 | if rawget(_G,'_DEBUG') then print(fun) end
236 | local chunk,err = loadstring(fun,'tmp')
237 | if err then return nil,err end
238 | return chunk(),names
239 | end
240 |
241 | local cache = {}
242 |
243 | --- match a SIP pattern against a string.
244 | -- @param spec a SIP pattern
245 | -- @param line a string
246 | -- @param res a table to receive values
247 | -- @param options (optional) option table
248 | -- @return true or false
249 | function sip.match (spec,line,res,options)
250 | assert_arg(1,spec,'string')
251 | assert_arg(2,line,'string')
252 | assert_arg(3,res,'table')
253 | if not cache[spec] then
254 | cache[spec] = sip.compile(spec,options)
255 | end
256 | return cache[spec](line,res)
257 | end
258 |
259 | --- match a SIP pattern against the start of a string.
260 | -- @param spec a SIP pattern
261 | -- @param line a string
262 | -- @param res a table to receive values
263 | -- @return true or false
264 | function sip.match_at_start (spec,line,res)
265 | return sip.match(spec,line,res,{at_start=true})
266 | end
267 |
268 | --- given a pattern and a file object, return an iterator over the results
269 | -- @param spec a SIP pattern
270 | -- @param f a file-like object.
271 | function sip.fields (spec,f)
272 | assert_arg(1,spec,'string')
273 | if not f then return nil,"no file object" end
274 | local fun,err = sip.compile(spec)
275 | if not fun then return nil,err end
276 | local res = {}
277 | return function()
278 | while true do
279 | local line = f:read()
280 | if not line then return end
281 | if fun(line,res) then
282 | local values = res
283 | res = {}
284 | return unpack(values)
285 | end
286 | end
287 | end
288 | end
289 |
290 | local read_patterns = {}
291 |
292 | --- register a match which will be used in the read function.
293 | -- @string spec a SIP pattern
294 | -- @func fun a function to be called with the results of the match
295 | -- @see read
296 | function sip.pattern (spec,fun)
297 | assert_arg(1,spec,'string')
298 | local pat,named = sip.compile(spec)
299 | append(read_patterns,{pat=pat,named=named,callback=fun})
300 | end
301 |
302 | --- enter a loop which applies all registered matches to the input file.
303 | -- @param f a file-like object
304 | -- @array matches optional list of `{spec,fun}` pairs, as for `pattern` above.
305 | function sip.read (f,matches)
306 | local owned,err
307 | if not f then return nil,"no file object" end
308 | if type(f) == 'string' then
309 | f,err = io.open(f)
310 | if not f then return nil,err end
311 | owned = true
312 | end
313 | if matches then
314 | for _,p in ipairs(matches) do
315 | sip.pattern(p[1],p[2])
316 | end
317 | end
318 | local res = {}
319 | for line in f:lines() do
320 | for _,item in ipairs(read_patterns) do
321 | if item.pat(line,res) then
322 | if item.callback then
323 | if item.named then
324 | item.callback(res)
325 | else
326 | item.callback(unpack(res))
327 | end
328 | end
329 | res = {}
330 | break
331 | end
332 | end
333 | end
334 | if owned then f:close() end
335 | end
336 |
337 | return sip
338 |
--------------------------------------------------------------------------------
/mod_push_appserver/pl/strict.lua:
--------------------------------------------------------------------------------
1 | --- Checks uses of undeclared global variables.
2 | -- All global variables must be 'declared' through a regular assignment
3 | -- (even assigning `nil` will do) in a main chunk before being used
4 | -- anywhere or assigned to inside a function. Existing metatables `__newindex` and `__index`
5 | -- metamethods are respected.
6 | --
7 | -- You can set any table to have strict behaviour using `strict.module`. Creating a new
8 | -- module with `strict.closed_module` makes the module immune to monkey-patching, if
9 | -- you don't wish to encourage monkey business.
10 | --
11 | -- If the global `PENLIGHT_NO_GLOBAL_STRICT` is defined, then this module won't make the
12 | -- global environment strict - if you just want to explicitly set table strictness.
13 | --
14 | -- @module pl.strict
15 |
16 | require 'debug' -- for Lua 5.2
17 | local getinfo, error, rawset, rawget = debug.getinfo, error, rawset, rawget
18 | local strict = {}
19 |
20 | local function what ()
21 | local d = getinfo(3, "S")
22 | return d and d.what or "C"
23 | end
24 |
25 | --- make an existing table strict.
26 | -- @string name name of table (optional)
27 | -- @tab[opt] mod table - if `nil` then we'll return a new table
28 | -- @tab[opt] predeclared - table of variables that are to be considered predeclared.
29 | -- @return the given table, or a new table
30 | function strict.module (name,mod,predeclared)
31 | local mt, old_newindex, old_index, old_index_type, global
32 | if predeclared then
33 | global = predeclared.__global
34 | end
35 | if type(mod) == 'table' then
36 | mt = getmetatable(mod)
37 | if mt and rawget(mt,'__declared') then return end -- already patched...
38 | else
39 | mod = {}
40 | end
41 | if mt == nil then
42 | mt = {}
43 | setmetatable(mod, mt)
44 | else
45 | old_newindex = mt.__newindex
46 | old_index = mt.__index
47 | old_index_type = type(old_index)
48 | end
49 | mt.__declared = predeclared or {}
50 | mt.__newindex = function(t, n, v)
51 | if old_newindex then
52 | old_newindex(t, n, v)
53 | if rawget(t,n)~=nil then return end
54 | end
55 | if not mt.__declared[n] then
56 | if global then
57 | local w = what()
58 | if w ~= "main" and w ~= "C" then
59 | error("assign to undeclared global '"..n.."'", 2)
60 | end
61 | end
62 | mt.__declared[n] = true
63 | end
64 | rawset(t, n, v)
65 | end
66 | mt.__index = function(t,n)
67 | if not mt.__declared[n] and what() ~= "C" then
68 | if old_index then
69 | if old_index_type == "table" then
70 | local fallback = old_index[n]
71 | if fallback ~= nil then
72 | return fallback
73 | end
74 | else
75 | local res = old_index(t, n)
76 | if res ~= nil then
77 | return res
78 | end
79 | end
80 | end
81 | local msg = "variable '"..n.."' is not declared"
82 | if name then
83 | msg = msg .. " in '"..name.."'"
84 | end
85 | error(msg, 2)
86 | end
87 | return rawget(t, n)
88 | end
89 | return mod
90 | end
91 |
92 | --- make all tables in a table strict.
93 | -- So `strict.make_all_strict(_G)` prevents monkey-patching
94 | -- of any global table
95 | -- @tab T
96 | function strict.make_all_strict (T)
97 | for k,v in pairs(T) do
98 | if type(v) == 'table' and v ~= T then
99 | strict.module(k,v)
100 | end
101 | end
102 | end
103 |
104 | --- make a new module table which is closed to further changes.
105 | function strict.closed_module (mod,name)
106 | local M = {}
107 | mod = mod or {}
108 | local mt = getmetatable(mod)
109 | if not mt then
110 | mt = {}
111 | setmetatable(mod,mt)
112 | end
113 | mt.__newindex = function(t,k,v)
114 | M[k] = v
115 | end
116 | return strict.module(name,M)
117 | end
118 |
119 | if not rawget(_G,'PENLIGHT_NO_GLOBAL_STRICT') then
120 | strict.module(nil,_G,{_PROMPT=true,__global=true})
121 | end
122 |
123 | return strict
124 |
125 |
--------------------------------------------------------------------------------
/mod_push_appserver/pl/stringio.lua:
--------------------------------------------------------------------------------
1 | --- Reading and writing strings using file-like objects.
2 | --
3 | -- f = stringio.open(text)
4 | -- l1 = f:read() -- read first line
5 | -- n,m = f:read ('*n','*n') -- read two numbers
6 | -- for line in f:lines() do print(line) end -- iterate over all lines
7 | -- f = stringio.create()
8 | -- f:write('hello')
9 | -- f:write('dolly')
10 | -- assert(f:value(),'hellodolly')
11 | --
12 | -- See @{03-strings.md.File_style_I_O_on_Strings|the Guide}.
13 | -- @module pl.stringio
14 |
15 | local unpack = rawget(_G,'unpack') or rawget(table,'unpack')
16 | local tonumber = tonumber
17 | local concat,append = table.concat,table.insert
18 |
19 | local stringio = {}
20 |
21 | -- Writer class
22 | local SW = {}
23 | SW.__index = SW
24 |
25 | local function xwrite(self,...)
26 | local args = {...} --arguments may not be nil!
27 | for i = 1, #args do
28 | append(self.tbl,args[i])
29 | end
30 | end
31 |
32 | function SW:write(arg1,arg2,...)
33 | if arg2 then
34 | xwrite(self,arg1,arg2,...)
35 | else
36 | append(self.tbl,arg1)
37 | end
38 | end
39 |
40 | function SW:writef(fmt,...)
41 | self:write(fmt:format(...))
42 | end
43 |
44 | function SW:value()
45 | return concat(self.tbl)
46 | end
47 |
48 | function SW:__tostring()
49 | return self:value()
50 | end
51 |
52 | function SW:close() -- for compatibility only
53 | end
54 |
55 | function SW:seek()
56 | end
57 |
58 | -- Reader class
59 | local SR = {}
60 | SR.__index = SR
61 |
62 | function SR:_read(fmt)
63 | local i,str = self.i,self.str
64 | local sz = #str
65 | if i > sz then return nil end
66 | local res
67 | if fmt == '*l' or fmt == '*L' then
68 | local idx = str:find('\n',i) or (sz+1)
69 | res = str:sub(i,fmt == '*l' and idx-1 or idx)
70 | self.i = idx+1
71 | elseif fmt == '*a' then
72 | res = str:sub(i)
73 | self.i = sz
74 | elseif fmt == '*n' then
75 | local _,i2,idx
76 | _,idx = str:find ('%s*%d+',i)
77 | _,i2 = str:find ('^%.%d+',idx+1)
78 | if i2 then idx = i2 end
79 | _,i2 = str:find ('^[eE][%+%-]*%d+',idx+1)
80 | if i2 then idx = i2 end
81 | local val = str:sub(i,idx)
82 | res = tonumber(val)
83 | self.i = idx+1
84 | elseif type(fmt) == 'number' then
85 | res = str:sub(i,i+fmt-1)
86 | self.i = i + fmt
87 | else
88 | error("bad read format",2)
89 | end
90 | return res
91 | end
92 |
93 | function SR:read(...)
94 | if select('#',...) == 0 then
95 | return self:_read('*l')
96 | else
97 | local res, fmts = {},{...}
98 | for i = 1, #fmts do
99 | res[i] = self:_read(fmts[i])
100 | end
101 | return unpack(res)
102 | end
103 | end
104 |
105 | function SR:seek(whence,offset)
106 | local base
107 | whence = whence or 'cur'
108 | offset = offset or 0
109 | if whence == 'set' then
110 | base = 1
111 | elseif whence == 'cur' then
112 | base = self.i
113 | elseif whence == 'end' then
114 | base = #self.str
115 | end
116 | self.i = base + offset
117 | return self.i
118 | end
119 |
120 | function SR:lines(...)
121 | local n, args = select('#',...)
122 | if n > 0 then
123 | args = {...}
124 | end
125 | return function()
126 | if n == 0 then
127 | return self:_read '*l'
128 | else
129 | return self:read(unpack(args))
130 | end
131 | end
132 | end
133 |
134 | function SR:close() -- for compatibility only
135 | end
136 |
137 | --- create a file-like object which can be used to construct a string.
138 | -- The resulting object has an extra `value()` method for
139 | -- retrieving the string value. Implements `file:write`, `file:seek`, `file:lines`,
140 | -- plus an extra `writef` method which works like `utils.printf`.
141 | -- @usage f = create(); f:write('hello, dolly\n'); print(f:value())
142 | function stringio.create()
143 | return setmetatable({tbl={}},SW)
144 | end
145 |
146 | --- create a file-like object for reading from a given string.
147 | -- Implements `file:read`.
148 | -- @string s The input string.
149 | -- @usage fs = open '20 10'; x,y = f:read ('*n','*n'); assert(x == 20 and y == 10)
150 | function stringio.open(s)
151 | return setmetatable({str=s,i=1},SR)
152 | end
153 |
154 | function stringio.lines(s,...)
155 | return stringio.open(s):lines(...)
156 | end
157 |
158 | return stringio
159 |
--------------------------------------------------------------------------------
/mod_push_appserver/pl/template.lua:
--------------------------------------------------------------------------------
1 | --- A template preprocessor.
2 | -- Originally by [Ricki Lake](http://lua-users.org/wiki/SlightlyLessSimpleLuaPreprocessor)
3 | --
4 | -- There are two rules:
5 | --
6 | -- * lines starting with # are Lua
7 | -- * otherwise, `$(expr)` is the result of evaluating `expr`
8 | --
9 | -- Example:
10 | --
11 | -- # for i = 1,3 do
12 | -- $(i) Hello, Word!
13 | -- # end
14 | -- ===>
15 | -- 1 Hello, Word!
16 | -- 2 Hello, Word!
17 | -- 3 Hello, Word!
18 | --
19 | -- Other escape characters can be used, when the defaults conflict
20 | -- with the output language.
21 | --
22 | -- > for _,n in pairs{'one','two','three'} do
23 | -- static int l_${n} (luaState *state);
24 | -- > end
25 | --
26 | -- See @{03-strings.md.Another_Style_of_Template|the Guide}.
27 | --
28 | -- Dependencies: `pl.utils`
29 | -- @module pl.template
30 |
31 | local utils = require 'pl.utils'
32 |
33 | local append,format,strsub,strfind,strgsub = table.insert,string.format,string.sub,string.find,string.gsub
34 |
35 | local APPENDER = "\n__R_size = __R_size + 1; __R_table[__R_size] = "
36 |
37 | local function parseDollarParen(pieces, chunk, exec_pat, newline)
38 | local s = 1
39 | for term, executed, e in chunk:gmatch(exec_pat) do
40 | executed = '('..strsub(executed,2,-2)..')'
41 | append(pieces, APPENDER..format("%q", strsub(chunk,s, term - 1)))
42 | append(pieces, APPENDER..format("__tostring(%s or '')", executed))
43 | s = e
44 | end
45 | local r
46 | if newline then
47 | r = format("%q", strgsub(strsub(chunk,s),"\n",""))
48 | else
49 | r = format("%q", strsub(chunk,s))
50 | end
51 | if r ~= '""' then
52 | append(pieces, APPENDER..r)
53 | end
54 | end
55 |
56 | local function parseHashLines(chunk,inline_escape,brackets,esc,newline)
57 | local exec_pat = "()"..inline_escape.."(%b"..brackets..")()"
58 |
59 | local esc_pat = esc.."+([^\n]*\n?)"
60 | local esc_pat1, esc_pat2 = "^"..esc_pat, "\n"..esc_pat
61 | local pieces, s = {"return function()\nlocal __R_size, __R_table, __tostring = 0, {}, __tostring", n = 1}, 1
62 | while true do
63 | local _, e, lua = strfind(chunk,esc_pat1, s)
64 | if not e then
65 | local ss
66 | ss, e, lua = strfind(chunk,esc_pat2, s)
67 | parseDollarParen(pieces, strsub(chunk,s, ss), exec_pat, newline)
68 | if not e then break end
69 | end
70 | if strsub(lua, -1, -1) == "\n" then lua = strsub(lua, 1, -2) end
71 | append(pieces, "\n"..lua)
72 | s = e + 1
73 | end
74 | append(pieces, "\nreturn __R_table\nend")
75 |
76 | -- let's check for a special case where there is nothing to template, but it's
77 | -- just a single static string
78 | local short = false
79 | if (#pieces == 3) and (pieces[2]:find(APPENDER, 1, true) == 1) then
80 | pieces = { "return " .. pieces[2]:sub(#APPENDER+1,-1) }
81 | short = true
82 | end
83 | -- if short == true, the generated function will not return a table of strings,
84 | -- but a single string
85 | return table.concat(pieces), short
86 | end
87 |
88 | local template = {}
89 |
90 | --- expand the template using the specified environment.
91 | -- This function will compile and render the template. For more performant
92 | -- recurring usage use the two step approach by using `compile` and `ct:render`.
93 | -- There are six special fields in the environment table `env`
94 | --
95 | -- * `_parent`: continue looking up in this table (e.g. `_parent=_G`).
96 | -- * `_brackets`: bracket pair that wraps inline Lua expressions, default is '()'.
97 | -- * `_escape`: character marking Lua lines, default is '#'
98 | -- * `_inline_escape`: character marking inline Lua expression, default is '$'.
99 | -- * `_chunk_name`: chunk name for loaded templates, used if there
100 | -- is an error in Lua code. Default is 'TMP'.
101 | -- * `_debug`: if truthy, the generated code will be printed upon a render error
102 | --
103 | -- @string str the template string
104 | -- @tab[opt] env the environment
105 | -- @return `rendered template + nil + source_code`, or `nil + error + source_code`. The last
106 | -- return value (`source_code`) is only returned if the debug option is used.
107 | function template.substitute(str,env)
108 | env = env or {}
109 | local t, err = template.compile(str, {
110 | chunk_name = rawget(env,"_chunk_name"),
111 | escape = rawget(env,"_escape"),
112 | inline_escape = rawget(env,"_inline_escape"),
113 | inline_brackets = rawget(env,"_brackets"),
114 | newline = nil,
115 | debug = rawget(env,"_debug")
116 | })
117 | if not t then return t, err end
118 |
119 | return t:render(env, rawget(env,"_parent"), rawget(env,"_debug"))
120 | end
121 |
122 | --- executes the previously compiled template and renders it.
123 | -- @function ct:render
124 | -- @tab[opt] env the environment.
125 | -- @tab[opt] parent continue looking up in this table (e.g. `parent=_G`).
126 | -- @bool[opt] db if thruthy, it will print the code upon a render error
127 | -- (provided the template was compiled with the debug option).
128 | -- @return `rendered template + nil + source_code`, or `nil + error + source_code`. The last return value
129 | -- (`source_code`) is only returned if the template was compiled with the debug option.
130 | -- @usage
131 | -- local ct, err = template.compile(my_template)
132 | -- local rendered , err = ct:render(my_env, parent)
133 | local render = function(self, env, parent, db)
134 | env = env or {}
135 | if parent then -- parent is a bit silly, but for backward compatibility retained
136 | setmetatable(env, {__index = parent})
137 | end
138 | setmetatable(self.env, {__index = env})
139 |
140 | local res, out = xpcall(self.fn, debug.traceback)
141 | if not res then
142 | if self.code and db then print(self.code) end
143 | return nil, out, self.code
144 | end
145 | return table.concat(out), nil, self.code
146 | end
147 |
148 | --- compiles the template.
149 | -- Returns an object that can repeatedly be rendered without parsing/compiling
150 | -- the template again.
151 | -- The options passed in the `opts` table support the following options:
152 | --
153 | -- * `chunk_name`: chunk name for loaded templates, used if there
154 | -- is an error in Lua code. Default is 'TMP'.
155 | -- * `escape`: character marking Lua lines, default is '#'
156 | -- * `inline_escape`: character marking inline Lua expression, default is '$'.
157 | -- * `inline_brackets`: bracket pair that wraps inline Lua expressions, default is '()'.
158 | -- * `newline`: string to replace newline characters, default is `nil` (not replacing newlines).
159 | -- * `debug`: if truthy, the generated source code will be retained within the compiled template object, default is `nil`.
160 | --
161 | -- @string str the template string
162 | -- @tab[opt] opts the compilation options to use
163 | -- @return template object, or `nil + error + source_code`
164 | -- @usage
165 | -- local ct, err = template.compile(my_template)
166 | -- local rendered , err = ct:render(my_env, parent)
167 | function template.compile(str, opts)
168 | opts = opts or {}
169 | local chunk_name = opts.chunk_name or 'TMP'
170 | local escape = opts.escape or '#'
171 | local inline_escape = opts.inline_escape or '$'
172 | local inline_brackets = opts.inline_brackets or '()'
173 |
174 | local code, short = parseHashLines(str,inline_escape,inline_brackets,escape,opts.newline)
175 | local env = { __tostring = tostring }
176 | local fn, err = utils.load(code, chunk_name,'t',env)
177 | if not fn then return nil, err, code end
178 |
179 | if short then
180 | -- the template returns a single constant string, let's optimize for that
181 | local constant_string = fn()
182 | return {
183 | fn = fn(),
184 | env = env,
185 | render = function(self) -- additional params can be ignored
186 | -- skip the metatable magic and error handling in the render
187 | -- function above for this special case
188 | return constant_string, nil, self.code
189 | end,
190 | code = opts.debug and code or nil,
191 | }
192 | end
193 |
194 | return {
195 | fn = fn(),
196 | env = env,
197 | render = render,
198 | code = opts.debug and code or nil,
199 | }
200 | end
201 |
202 | return template
203 |
--------------------------------------------------------------------------------
/mod_push_appserver/pl/test.lua:
--------------------------------------------------------------------------------
1 | --- Useful test utilities.
2 | --
3 | -- test.asserteq({1,2},{1,2}) -- can compare tables
4 | -- test.asserteq(1.2,1.19,0.02) -- compare FP numbers within precision
5 | -- T = test.tuple -- used for comparing multiple results
6 | -- test.asserteq(T(string.find(" me","me")),T(2,3))
7 | --
8 | -- Dependencies: `pl.utils`, `pl.tablex`, `pl.pretty`, `pl.path`, `debug`
9 | -- @module pl.test
10 |
11 | local tablex = require 'pl.tablex'
12 | local utils = require 'pl.utils'
13 | local pretty = require 'pl.pretty'
14 | local path = require 'pl.path'
15 | local type,unpack,pack = type,utils.unpack,utils.pack
16 | local clock = os.clock
17 | local debug = require 'debug'
18 | local io = io
19 |
20 | local function dump(x)
21 | if type(x) == 'table' and not (getmetatable(x) and getmetatable(x).__tostring) then
22 | return pretty.write(x,' ',true)
23 | elseif type(x) == 'string' then
24 | return '"'..x..'"'
25 | else
26 | return tostring(x)
27 | end
28 | end
29 |
30 | local test = {}
31 |
32 | ---- error handling for test results.
33 | -- By default, this writes to stderr and exits the program.
34 | -- Re-define this function to raise an error and/or redirect output
35 | function test.error_handler(file,line,got_text, needed_text,msg)
36 | local err = io.stderr
37 | err:write(path.basename(file)..':'..line..': assertion failed\n')
38 | err:write("got:\t",got_text,'\n')
39 | err:write("needed:\t",needed_text,'\n')
40 | utils.quit(1,msg or "these values were not equal")
41 | end
42 |
43 | local function complain (x,y,msg,where)
44 | local i = debug.getinfo(3 + (where or 0))
45 | test.error_handler(i.short_src,i.currentline,dump(x),dump(y),msg)
46 | end
47 |
48 | --- general test complain message.
49 | -- Useful for composing new test functions (see tests/tablex.lua for an example)
50 | -- @param x a value
51 | -- @param y value to compare first value against
52 | -- @param msg message
53 | -- @param where extra level offset for errors
54 | -- @function complain
55 | test.complain = complain
56 |
57 | --- like assert, except takes two arguments that must be equal and can be tables.
58 | -- If they are plain tables, it will use tablex.deepcompare.
59 | -- @param x any value
60 | -- @param y a value equal to x
61 | -- @param eps an optional tolerance for numerical comparisons
62 | -- @param where extra level offset
63 | function test.asserteq (x,y,eps,where)
64 | local res = x == y
65 | if not res then
66 | res = tablex.deepcompare(x,y,true,eps)
67 | end
68 | if not res then
69 | complain(x,y,nil,where)
70 | end
71 | end
72 |
73 | --- assert that the first string matches the second.
74 | -- @param s1 a string
75 | -- @param s2 a string
76 | -- @param where extra level offset
77 | function test.assertmatch (s1,s2,where)
78 | if not s1:match(s2) then
79 | complain (s1,s2,"these strings did not match",where)
80 | end
81 | end
82 |
83 | --- assert that the function raises a particular error.
84 | -- @param fn a function or a table of the form {function,arg1,...}
85 | -- @param e a string to match the error against
86 | -- @param where extra level offset
87 | function test.assertraise(fn,e,where)
88 | local ok, err
89 | if type(fn) == 'table' then
90 | ok, err = pcall(unpack(fn))
91 | else
92 | ok, err = pcall(fn)
93 | end
94 | if ok or err:match(e)==nil then
95 | complain (err,e,"these errors did not match",where)
96 | end
97 | end
98 |
99 | --- a version of asserteq that takes two pairs of values.
100 | -- x1==y1 and x2==y2
must be true. Useful for functions that naturally
101 | -- return two values.
102 | -- @param x1 any value
103 | -- @param x2 any value
104 | -- @param y1 any value
105 | -- @param y2 any value
106 | -- @param where extra level offset
107 | function test.asserteq2 (x1,x2,y1,y2,where)
108 | if x1 ~= y1 then complain(x1,y1,nil,where) end
109 | if x2 ~= y2 then complain(x2,y2,nil,where) end
110 | end
111 |
112 | -- tuple type --
113 |
114 | local tuple_mt = {
115 | unpack = unpack
116 | }
117 | tuple_mt.__index = tuple_mt
118 |
119 | function tuple_mt.__tostring(self)
120 | local ts = {}
121 | for i=1, self.n do
122 | local s = self[i]
123 | ts[i] = type(s) == 'string' and ('%q'):format(s) or tostring(s)
124 | end
125 | return 'tuple(' .. table.concat(ts, ', ') .. ')'
126 | end
127 |
128 | function tuple_mt.__eq(a, b)
129 | if a.n ~= b.n then return false end
130 | for i=1, a.n do
131 | if a[i] ~= b[i] then return false end
132 | end
133 | return true
134 | end
135 |
136 | function tuple_mt.__len(self)
137 | return self.n
138 | end
139 |
140 | --- encode an arbitrary argument list as a tuple.
141 | -- This can be used to compare to other argument lists, which is
142 | -- very useful for testing functions which return a number of values.
143 | -- Unlike regular array-like tables ('sequences') they may contain nils.
144 | -- Tuples understand equality and know how to print themselves out.
145 | -- The # operator is defined to be the size, irrespecive of any nils,
146 | -- and there is an `unpack` method.
147 | -- @usage asserteq(tuple( ('ab'):find 'a'), tuple(1,1))
148 | function test.tuple(...)
149 | return setmetatable(pack(...), tuple_mt)
150 | end
151 |
152 | --- Time a function. Call the function a given number of times, and report the number of seconds taken,
153 | -- together with a message. Any extra arguments will be passed to the function.
154 | -- @string msg a descriptive message
155 | -- @int n number of times to call the function
156 | -- @func fun the function
157 | -- @param ... optional arguments to fun
158 | function test.timer(msg,n,fun,...)
159 | local start = clock()
160 | for i = 1,n do fun(...) end
161 | utils.printf("%s: took %7.2f sec\n",msg,clock()-start)
162 | end
163 |
164 | return test
165 |
--------------------------------------------------------------------------------
/mod_push_appserver/pl/text.lua:
--------------------------------------------------------------------------------
1 | --- Text processing utilities.
2 | --
3 | -- This provides a Template class (modeled after the same from the Python
4 | -- libraries, see string.Template). It also provides similar functions to those
5 | -- found in the textwrap module.
6 | --
7 | -- See @{03-strings.md.String_Templates|the Guide}.
8 | --
9 | -- Calling `text.format_operator()` overloads the % operator for strings to give Python/Ruby style formated output.
10 | -- This is extended to also do template-like substitution for map-like data.
11 | --
12 | -- > require 'pl.text'.format_operator()
13 | -- > = '%s = %5.3f' % {'PI',math.pi}
14 | -- PI = 3.142
15 | -- > = '$name = $value' % {name='dog',value='Pluto'}
16 | -- dog = Pluto
17 | --
18 | -- Dependencies: `pl.utils`, `pl.types`
19 | -- @module pl.text
20 |
21 | local gsub = string.gsub
22 | local concat,append = table.concat,table.insert
23 | local utils = require 'pl.utils'
24 | local bind1,usplit,assert_arg = utils.bind1,utils.split,utils.assert_arg
25 | local is_callable = require 'pl.types'.is_callable
26 | local unpack = utils.unpack
27 |
28 | local text = {}
29 |
30 |
31 | local function makelist(l)
32 | return setmetatable(l, require('pl.List'))
33 | end
34 |
35 | local function lstrip(str) return (str:gsub('^%s+','')) end
36 | local function strip(str) return (lstrip(str):gsub('%s+$','')) end
37 | local function split(s,delim) return makelist(usplit(s,delim)) end
38 |
39 | local function imap(f,t,...)
40 | local res = {}
41 | for i = 1,#t do res[i] = f(t[i],...) end
42 | return res
43 | end
44 |
45 | local function _indent (s,sp)
46 | local sl = split(s,'\n')
47 | return concat(imap(bind1('..',sp),sl),'\n')..'\n'
48 | end
49 |
50 | --- indent a multiline string.
51 | -- @param s the string
52 | -- @param n the size of the indent
53 | -- @param ch the character to use when indenting (default ' ')
54 | -- @return indented string
55 | function text.indent (s,n,ch)
56 | assert_arg(1,s,'string')
57 | assert_arg(2,n,'number')
58 | return _indent(s,string.rep(ch or ' ',n))
59 | end
60 |
61 | --- dedent a multiline string by removing any initial indent.
62 | -- useful when working with [[..]] strings.
63 | -- @param s the string
64 | -- @return a string with initial indent zero.
65 | function text.dedent (s)
66 | assert_arg(1,s,'string')
67 | local sl = split(s,'\n')
68 | local _,i2 = (#sl>0 and sl[1] or ''):find('^%s*')
69 | sl = imap(string.sub,sl,i2+1)
70 | return concat(sl,'\n')..'\n'
71 | end
72 |
73 | --- format a paragraph into lines so that they fit into a line width.
74 | -- It will not break long words, so lines can be over the length
75 | -- to that extent.
76 | -- @param s the string
77 | -- @param width the margin width, default 70
78 | -- @return a list of lines (List object)
79 | -- @see pl.List
80 | function text.wrap (s,width)
81 | assert_arg(1,s,'string')
82 | width = width or 70
83 | s = s:gsub('\n',' ')
84 | local i,nxt = 1
85 | local lines,line = {}
86 | while i < #s do
87 | nxt = i+width
88 | if s:find("[%w']",nxt) then -- inside a word
89 | nxt = s:find('%W',nxt+1) -- so find word boundary
90 | end
91 | line = s:sub(i,nxt)
92 | i = i + #line
93 | append(lines,strip(line))
94 | end
95 | return makelist(lines)
96 | end
97 |
98 | --- format a paragraph so that it fits into a line width.
99 | -- @param s the string
100 | -- @param width the margin width, default 70
101 | -- @return a string
102 | -- @see wrap
103 | function text.fill (s,width)
104 | return concat(text.wrap(s,width),'\n') .. '\n'
105 | end
106 |
107 | local Template = {}
108 | text.Template = Template
109 | Template.__index = Template
110 | setmetatable(Template, {
111 | __call = function(obj,tmpl)
112 | return Template.new(tmpl)
113 | end})
114 |
115 | function Template.new(tmpl)
116 | assert_arg(1,tmpl,'string')
117 | local res = {}
118 | res.tmpl = tmpl
119 | setmetatable(res,Template)
120 | return res
121 | end
122 |
123 | local function _substitute(s,tbl,safe)
124 | local subst
125 | if is_callable(tbl) then
126 | subst = tbl
127 | else
128 | function subst(f)
129 | local s = tbl[f]
130 | if not s then
131 | if safe then
132 | return f
133 | else
134 | error("not present in table "..f)
135 | end
136 | else
137 | return s
138 | end
139 | end
140 | end
141 | local res = gsub(s,'%${([%w_]+)}',subst)
142 | return (gsub(res,'%$([%w_]+)',subst))
143 | end
144 |
145 | --- substitute values into a template, throwing an error.
146 | -- This will throw an error if no name is found.
147 | -- @param tbl a table of name-value pairs.
148 | function Template:substitute(tbl)
149 | assert_arg(1,tbl,'table')
150 | return _substitute(self.tmpl,tbl,false)
151 | end
152 |
153 | --- substitute values into a template.
154 | -- This version just passes unknown names through.
155 | -- @param tbl a table of name-value pairs.
156 | function Template:safe_substitute(tbl)
157 | assert_arg(1,tbl,'table')
158 | return _substitute(self.tmpl,tbl,true)
159 | end
160 |
161 | --- substitute values into a template, preserving indentation.
162 | -- If the value is a multiline string _or_ a template, it will insert
163 | -- the lines at the correct indentation.
164 | -- Furthermore, if a template, then that template will be subsituted
165 | -- using the same table.
166 | -- @param tbl a table of name-value pairs.
167 | function Template:indent_substitute(tbl)
168 | assert_arg(1,tbl,'table')
169 | if not self.strings then
170 | self.strings = split(self.tmpl,'\n')
171 | end
172 | -- the idea is to substitute line by line, grabbing any spaces as
173 | -- well as the $var. If the value to be substituted contains newlines,
174 | -- then we split that into lines and adjust the indent before inserting.
175 | local function subst(line)
176 | return line:gsub('(%s*)%$([%w_]+)',function(sp,f)
177 | local subtmpl
178 | local s = tbl[f]
179 | if not s then error("not present in table "..f) end
180 | if getmetatable(s) == Template then
181 | subtmpl = s
182 | s = s.tmpl
183 | else
184 | s = tostring(s)
185 | end
186 | if s:find '\n' then
187 | s = _indent(s,sp)
188 | end
189 | if subtmpl then return _substitute(s,tbl)
190 | else return s
191 | end
192 | end)
193 | end
194 | local lines = imap(subst,self.strings)
195 | return concat(lines,'\n')..'\n'
196 | end
197 |
198 | ------- Python-style formatting operator ------
199 | -- (see the lua-users wiki) --
200 |
201 | function text.format_operator()
202 |
203 | local format = string.format
204 |
205 | -- a more forgiving version of string.format, which applies
206 | -- tostring() to any value with a %s format.
207 | local function formatx (fmt,...)
208 | local args = {...}
209 | local i = 1
210 | for p in fmt:gmatch('%%.') do
211 | if p == '%s' and type(args[i]) ~= 'string' then
212 | args[i] = tostring(args[i])
213 | end
214 | i = i + 1
215 | end
216 | return format(fmt,unpack(args))
217 | end
218 |
219 | local function basic_subst(s,t)
220 | return (s:gsub('%$([%w_]+)',t))
221 | end
222 |
223 | -- Note this goes further than the original, and will allow these cases:
224 | -- 1. a single value
225 | -- 2. a list of values
226 | -- 3. a map of var=value pairs
227 | -- 4. a function, as in gsub
228 | -- For the second two cases, it uses $-variable substituion.
229 | getmetatable("").__mod = function(a, b)
230 | if b == nil then
231 | return a
232 | elseif type(b) == "table" and getmetatable(b) == nil then
233 | if #b == 0 then -- assume a map-like table
234 | return _substitute(a,b,true)
235 | else
236 | return formatx(a,unpack(b))
237 | end
238 | elseif type(b) == 'function' then
239 | return basic_subst(a,b)
240 | else
241 | return formatx(a,b)
242 | end
243 | end
244 | end
245 |
246 | return text
247 |
--------------------------------------------------------------------------------
/mod_push_appserver/pl/types.lua:
--------------------------------------------------------------------------------
1 | ---- Dealing with Detailed Type Information
2 |
3 | -- Dependencies `pl.utils`
4 | -- @module pl.types
5 |
6 | local utils = require 'pl.utils'
7 | local math_ceil = math.ceil
8 | local assert_arg = utils.assert_arg
9 | local types = {}
10 |
11 | --- is the object either a function or a callable object?.
12 | -- @param obj Object to check.
13 | function types.is_callable (obj)
14 | return type(obj) == 'function' or getmetatable(obj) and getmetatable(obj).__call and true
15 | end
16 |
17 | --- is the object of the specified type?.
18 | -- If the type is a string, then use type, otherwise compare with metatable.
19 | --
20 | -- NOTE: this function is imported from `utils.is_type`.
21 | -- @param obj An object to check
22 | -- @param tp The expected type
23 | -- @function is_type
24 | -- @see utils.is_type
25 | types.is_type = utils.is_type
26 |
27 | local fileMT = getmetatable(io.stdout)
28 |
29 | --- a string representation of a type.
30 | -- For tables and userdata with metatables, we assume that the metatable has a `_name`
31 | -- field. If the field is not present it will return 'unknown table' or
32 | -- 'unknown userdata'.
33 | -- Lua file objects return the type 'file'.
34 | -- @param obj an object
35 | -- @return a string like 'number', 'table', 'file' or 'List'
36 | function types.type (obj)
37 | local t = type(obj)
38 | if t == 'table' or t == 'userdata' then
39 | local mt = getmetatable(obj)
40 | if mt == fileMT then
41 | return 'file'
42 | elseif mt == nil then
43 | return t
44 | else
45 | -- TODO: the "unknown" is weird, it should just return the type
46 | return mt._name or "unknown "..t
47 | end
48 | else
49 | return t
50 | end
51 | end
52 |
53 | --- is this number an integer?
54 | -- @param x a number
55 | -- @raise error if x is not a number
56 | -- @return boolean
57 | function types.is_integer (x)
58 | return math_ceil(x)==x
59 | end
60 |
61 | --- Check if the object is "empty".
62 | -- An object is considered empty if it is:
63 | --
64 | -- - `nil`
65 | -- - a table with out any items (key-value pairs or indexes)
66 | -- - a string with no content ("")
67 | -- - not a nil/table/string
68 | -- @param o The object to check if it is empty.
69 | -- @param ignore_spaces If the object is a string and this is true the string is
70 | -- considered empty if it only contains spaces.
71 | -- @return `true` if the object is empty, otherwise a falsy value.
72 | function types.is_empty(o, ignore_spaces)
73 | if o == nil then
74 | return true
75 | elseif type(o) == "table" then
76 | return next(o) == nil
77 | elseif type(o) == "string" then
78 | return o == "" or (not not ignore_spaces and (not not o:find("^%s+$")))
79 | else
80 | return true
81 | end
82 | end
83 |
84 | local function check_meta (val)
85 | if type(val) == 'table' then return true end
86 | return getmetatable(val)
87 | end
88 |
89 | --- is an object 'array-like'?
90 | -- An object is array like if:
91 | --
92 | -- - it is a table, or
93 | -- - it has a metatable with `__len` and `__index` methods
94 | --
95 | -- NOTE: since `__len` is 5.2+, on 5.1 is usually returns `false` for userdata
96 | -- @param val any value.
97 | -- @return `true` if the object is array-like, otherwise a falsy value.
98 | function types.is_indexable (val)
99 | local mt = check_meta(val)
100 | if mt == true then return true end
101 | return mt and mt.__len and mt.__index and true
102 | end
103 |
104 | --- can an object be iterated over with `pairs`?
105 | -- An object is iterable if:
106 | --
107 | -- - it is a table, or
108 | -- - it has a metatable with a `__pairs` meta method
109 | --
110 | -- NOTE: since `__pairs` is 5.2+, on 5.1 is usually returns `false` for userdata
111 | -- @param val any value.
112 | -- @return `true` if the object is iterable, otherwise a falsy value.
113 | function types.is_iterable (val)
114 | local mt = check_meta(val)
115 | if mt == true then return true end
116 | return mt and mt.__pairs and true
117 | end
118 |
119 | --- can an object accept new key/pair values?
120 | -- An object is iterable if:
121 | --
122 | -- - it is a table, or
123 | -- - it has a metatable with a `__newindex` meta method
124 | --
125 | -- @param val any value.
126 | -- @return `true` if the object is writeable, otherwise a falsy value.
127 | function types.is_writeable (val)
128 | local mt = check_meta(val)
129 | if mt == true then return true end
130 | return mt and mt.__newindex and true
131 | end
132 |
133 | -- Strings that should evaluate to true. -- TODO: add on/off ???
134 | local trues = { yes=true, y=true, ["true"]=true, t=true, ["1"]=true }
135 | -- Conditions types should evaluate to true.
136 | local true_types = {
137 | boolean=function(o, true_strs, check_objs) return o end,
138 | string=function(o, true_strs, check_objs)
139 | o = o:lower()
140 | if trues[o] then
141 | return true
142 | end
143 | -- Check alternative user provided strings.
144 | for _,v in ipairs(true_strs or {}) do
145 | if type(v) == "string" and o == v:lower() then
146 | return true
147 | end
148 | end
149 | return false
150 | end,
151 | number=function(o, true_strs, check_objs) return o ~= 0 end,
152 | table=function(o, true_strs, check_objs) if check_objs and next(o) ~= nil then return true end return false end
153 | }
154 | --- Convert to a boolean value.
155 | -- True values are:
156 | --
157 | -- * boolean: true.
158 | -- * string: 'yes', 'y', 'true', 't', '1' or additional strings specified by `true_strs`.
159 | -- * number: Any non-zero value.
160 | -- * table: Is not empty and `check_objs` is true.
161 | -- * everything else: Is not `nil` and `check_objs` is true.
162 | --
163 | -- @param o The object to evaluate.
164 | -- @param[opt] true_strs optional Additional strings that when matched should evaluate to true. Comparison is case insensitive.
165 | -- This should be a List of strings. E.g. "ja" to support German.
166 | -- @param[opt] check_objs True if objects should be evaluated.
167 | -- @return true if the input evaluates to true, otherwise false.
168 | function types.to_bool(o, true_strs, check_objs)
169 | local true_func
170 | if true_strs then
171 | assert_arg(2, true_strs, "table")
172 | end
173 | true_func = true_types[type(o)]
174 | if true_func then
175 | return true_func(o, true_strs, check_objs)
176 | elseif check_objs and o ~= nil then
177 | return true
178 | end
179 | return false
180 | end
181 |
182 |
183 | return types
184 |
--------------------------------------------------------------------------------
/mod_push_appserver/pl/url.lua:
--------------------------------------------------------------------------------
1 | --- Python-style URL quoting library.
2 | --
3 | -- @module pl.url
4 |
5 | local url = {}
6 |
7 | local function quote_char(c)
8 | return string.format("%%%02X", string.byte(c))
9 | end
10 |
11 | --- Quote the url, replacing special characters using the '%xx' escape.
12 | -- @string s the string
13 | -- @bool quote_plus Also escape slashes and replace spaces by plus signs.
14 | -- @return The quoted string, or if `s` wasn't a string, just plain unaltered `s`.
15 | function url.quote(s, quote_plus)
16 | if type(s) ~= "string" then
17 | return s
18 | end
19 |
20 | s = s:gsub("\n", "\r\n")
21 | s = s:gsub("([^A-Za-z0-9 %-_%./])", quote_char)
22 | if quote_plus then
23 | s = s:gsub(" ", "+")
24 | s = s:gsub("/", quote_char)
25 | else
26 | s = s:gsub(" ", "%%20")
27 | end
28 |
29 | return s
30 | end
31 |
32 | local function unquote_char(h)
33 | return string.char(tonumber(h, 16))
34 | end
35 |
36 | --- Unquote the url, replacing '%xx' escapes and plus signs.
37 | -- @string s the string
38 | -- @return The unquoted string, or if `s` wasn't a string, just plain unaltered `s`.
39 | function url.unquote(s)
40 | if type(s) ~= "string" then
41 | return s
42 | end
43 |
44 | s = s:gsub("+", " ")
45 | s = s:gsub("%%(%x%x)", unquote_char)
46 | s = s:gsub("\r\n", "\n")
47 |
48 | return s
49 | end
50 |
51 | return url
52 |
--------------------------------------------------------------------------------
/mod_push_appserver/uncached_store.lib.lua:
--------------------------------------------------------------------------------
1 | return function(params)
2 | local store = module:open_store();
3 | local api = {};
4 | function api:get(node)
5 | local settings, err = store:get(node);
6 | if not settings and err then
7 | module:log("error", "Error reading push notification storage for node '%s': %s", node, tostring(err));
8 | return nil, false;
9 | end
10 | if not settings then settings = {} end
11 | return settings, true;
12 | end
13 | function api:set(node, data)
14 | local settings = api:get(node); -- load node's data
15 | local ok, err = store:set(node, data);
16 | if not ok then
17 | module:log("error", "Error writing push notification storage for node '%s': %s", node, tostring(err));
18 | return false;
19 | end
20 | return true;
21 | end
22 | function api:list()
23 | return store:users();
24 | end
25 | function api:token2node(token)
26 | for node in store:users() do
27 | -- read data directly, we don't want to cache full copies of stale entries as api:get() would do
28 | local settings, err = store:get(node);
29 | if not settings and err then
30 | module:log("error", "Error reading push notification storage for node '%s': %s", node, tostring(err));
31 | settings = {};
32 | end
33 | if settings.token and settings.node and settings.token == token then return settings.node; end
34 | end
35 | return nil;
36 | end
37 | return api;
38 | end;
--------------------------------------------------------------------------------
/mod_push_appserver/zthrottle.lib.lua:
--------------------------------------------------------------------------------
1 | local time = require "util.time";
2 |
3 | local distance = 0;
4 | local api = {};
5 | local data = {}
6 |
7 | function api:set_distance(d)
8 | distance = d;
9 | end
10 |
11 | function api:incoming(id, callback)
12 | if not data[id] then data[id] = {}; end
13 | -- directly call callback() if the last call for this id was more than `distance` seconds away
14 | if not data[id]["last_call"] or time.now() > data[id]["last_call"] + distance then
15 | data[id]["last_call"] = time.now();
16 | if data[id]["timer"] then data[id]["timer"]:stop(); data[id]["timer"] = nil; end
17 | module:log("info", "Calling callback directly");
18 | callback();
19 | return "allowed";
20 | -- use timer to delay second invocation
21 | elseif not data[id]["timer"] then
22 | data[id]["timer"] = module:add_timer(distance - (time.now() - data[id]["last_call"]), function()
23 | data[id]["timer"] = nil;
24 | data[id]["last_call"] = time.now();
25 | module:log("info", "Calling delayed callback");
26 | callback();
27 | end);
28 | return "delayed";
29 | -- ignore all other invocations until the delayed one fired
30 | else
31 | module:log("debug", "Ignoring incoming call");
32 | return "ignored";
33 | end
34 | end
35 |
36 | return api;
37 |
--------------------------------------------------------------------------------
/mod_push_appserver_apns/mod_push_appserver_apns.lua:
--------------------------------------------------------------------------------
1 | -- mod_push_appserver_apns
2 | --
3 | -- Copyright (C) 2017-2020 Thilo Molitor
4 | --
5 | -- This file is MIT/X11 licensed.
6 | --
7 | -- Submodule implementing APNS communication
8 | --
9 |
10 | -- this is the master module
11 | module:depends("push_appserver");
12 |
13 | -- imports
14 | local appserver_global = module:shared("*/push_appserver/appserver_global");
15 | local cq = require "net.cqueues".cq;
16 | local promise = require "cqueues.promise";
17 | local http_client = require "http.client";
18 | local new_headers = require "http.headers".new;
19 | local ce = require "cqueues.errno";
20 | local openssl_ctx = require "openssl.ssl.context";
21 | local x509 = require "openssl.x509";
22 | local pkey = require "openssl.pkey";
23 | local json = require "util.json";
24 |
25 | -- configuration
26 | local test_environment = false;
27 | local apns_cert = module:get_option_string("push_appserver_apns_cert", nil); --push certificate (no default)
28 | local apns_key = module:get_option_string("push_appserver_apns_key", nil); --push certificate key (no default)
29 | local topic = module:get_option_string("push_appserver_apns_topic", nil); --apns topic: app bundle id (no default)
30 | local capath = module:get_option_string("push_appserver_apns_capath", "/etc/ssl/certs"); --ca path on debian systems
31 | local ciphers = module:get_option_string("push_appserver_apns_ciphers",
32 | "ECDHE-RSA-AES256-GCM-SHA384:"..
33 | "ECDHE-ECDSA-AES256-GCM-SHA384:"..
34 | "ECDHE-RSA-AES128-GCM-SHA256:"..
35 | "ECDHE-ECDSA-AES128-GCM-SHA256"
36 | );
37 | local mutable_content = module:get_option_boolean("push_appserver_apns_mutable_content", true); --flag high prio pushes as mutable content
38 | local push_ttl = module:get_option_number("push_appserver_apns_push_ttl", os.time() + (4*7*24*3600)); --now + 4 weeks
39 | local push_priority = module:get_option_string("push_appserver_apns_push_priority", "auto"); --automatically decide push priority
40 | local sandbox = module:get_option_boolean("push_appserver_apns_sandbox", true); --use APNS sandbox
41 | local collapse_pushes = module:get_option_boolean("push_appserver_apns_collapse_pushes", false); --collapse pushes into one
42 | local push_host = sandbox and "api.sandbox.push.apple.com" or "api.push.apple.com";
43 | local push_port = 443;
44 | if test_environment then push_host = "localhost"; end
45 | local default_tls_options = openssl_ctx.OP_NO_COMPRESSION + openssl_ctx.OP_SINGLE_ECDH_USE + openssl_ctx.OP_NO_SSLv2 + openssl_ctx.OP_NO_SSLv3;
46 |
47 | -- check config
48 | assert(apns_cert ~= nil, "You need to set 'push_appserver_apns_cert'")
49 | assert(apns_key ~= nil, "You need to set 'push_appserver_apns_key'")
50 | assert(topic ~= nil, "You need to set 'push_appserver_apns_topic'")
51 |
52 | -- global state
53 | local connection_promise = nil;
54 | local cleanup_controller = nil
55 | local certstring = "";
56 | local keystring = "";
57 |
58 | -- general utility functions
59 | local function non_final_status(status)
60 | return status:sub(1, 1) == "1" and status ~= "101"
61 | end
62 | local function readAll(file)
63 | local f = assert(io.open(file, "rb"));
64 | local content = f:read("*all");
65 | f:close();
66 | return content;
67 | end
68 | local function unregister_token(token)
69 | -- add unregister token to prosody event queue
70 | module:log("debug", "Adding unregister-push-token to prosody event queue...");
71 | module:add_timer(1e-06, function()
72 | module:log("warn", "Unregistering failing APNS token %s", tostring(token))
73 | module:fire_event("unregister-push-token", {token = tostring(token), type = "apns"})
74 | end);
75 | end
76 | local function close_connection()
77 | -- reset promise to start a new connection
78 | local p = connection_promise;
79 | connection_promise = nil;
80 |
81 | -- ignore errors in here
82 | if p then
83 | local ok, errobj = pcall(function()
84 | local stream, err, errno, ok;
85 | -- this waits for the resolution of the OLD promise and either returns the connection object or throws an error
86 | -- (which will be caught by the wrapping pcall)
87 | local connection = p:get();
88 |
89 | -- close OLD connection
90 | connection.goaway_handler = nil;
91 | connection:close();
92 | end);
93 | end
94 | end
95 |
96 | -- handlers
97 | local function apns_push_priority_handler(event)
98 | -- determine real push priorit to use when sending to apns
99 | local priority = push_priority;
100 | if push_priority == "auto" then
101 | priority = (event.summary and event.summary["last-message-body"] ~= nil) and "high" or "silent";
102 | end
103 | return priority;
104 | end
105 |
106 | local function apns_handler(event)
107 | local settings, async_callback = event.settings, event.async_callback;
108 | -- prepare data to send (using latest binary format, not the legacy binary format or the new http/2 format)
109 | local payload;
110 | local priority = apns_push_priority_handler(event);
111 | if priority == "high" then
112 | payload = '{"aps":{'..(mutable_content and '"mutable-content":"1",' or '')..'"alert":{"title":"New Message", "body":"New Message"}, "sound":"default"}}';
113 | else
114 | payload = '{"aps":{"content-available":1}}';
115 | end
116 |
117 | local function retry()
118 | close_connection();
119 | module:add_timer(1, function()
120 | module:fire_event("incoming-push-to-apns", event);
121 | end);
122 | end
123 |
124 | cq:wrap(function()
125 | -- create new tls context and connection if not already connected
126 | -- this uses a cqueues promise to make sure we're not connecting twice
127 | if connection_promise == nil then
128 | connection_promise = promise.new();
129 | module:log("info", "Creating new connection to APNS server '%s:%s'...", push_host, tostring(push_port));
130 |
131 | -- create new tls context
132 | local ctx = openssl_ctx.new("TLSv1_2", false);
133 | ctx:setCipherList(ciphers);
134 | ctx:setOptions(default_tls_options);
135 | ctx:setEphemeralKey(pkey.new{ type = "EC", curve = "prime256v1" });
136 | local store = ctx:getStore();
137 | store:add(capath);
138 | ctx:setVerify(openssl_ctx.VERIFY_PEER);
139 | if test_environment then ctx:setVerify(openssl_ctx.VERIFY_NONE); end
140 | ctx:setCertificate(x509.new(certstring));
141 | ctx:setPrivateKey(pkey.new(keystring));
142 |
143 | -- create new connection and log possible errors
144 | module:log("debug", "TLS configured, now calling http_client.connect('%s:%s')...", push_host, tostring(push_port));
145 | local connection, err, errno = http_client.connect({
146 | host = push_host;
147 | port = push_port;
148 | tls = true;
149 | version = 2;
150 | ctx = ctx;
151 | }, 8);
152 | module:log("debug", "http_client.connect() returned %s", tostring(connection));
153 | if connection == nil then
154 | module:log("error", "APNS connect error %s: %s", tostring(errno), tostring(err));
155 | connection_promise:set(false, {error = err, errno = errno});
156 | else
157 | -- close connection on GOAWAY frame
158 | module:log("info", "connection established, waiting for GOAWAY frame in extra cqueue function...");
159 | connection.goaway_handler = cq:wrap(function()
160 | while connection and connection.goaway_handler do
161 | if connection.recv_goaway:wait(60.0) then -- make sure we are frequently checking connection.goaway_handler
162 | module:log("info", "received GOAWAY frame, closing connection...");
163 | connection.goaway_handler = nil;
164 | connection:close();
165 | return;
166 | end
167 | end
168 | end);
169 | connection_promise:set(true, connection);
170 | end
171 | end
172 |
173 | -- wait for connection establishment before continuing by waiting for the connection promise which wraps the connection object
174 | local ok, errobj = pcall(function()
175 | local stream, err, errno, ok;
176 | -- this waits for the resolution of the promise and either returns the connection object or throws an error
177 | -- (which will be caught by the wrapping pcall)
178 | local connection = connection_promise:get();
179 |
180 | if connection.recv_goaway_lowest then -- check for goaway (is there any api method for this??)
181 | module:log("error", "reconnecting because we received a GOAWAY frame: %s", tostring(connection.recv_goaway_lowest));
182 | return retry();
183 | end
184 |
185 | -- create new stream for our request
186 | module:log("debug", "Creating new http/2 stream...");
187 | stream, err, errno = connection:new_stream();
188 | if stream == nil then
189 | module:log("warn", "retrying: APNS new_stream error %s: %s", tostring(errno), tostring(err));
190 | return retry();
191 | end
192 | module:log("debug", "New http/2 stream id: %s", stream.id);
193 |
194 | -- write request
195 | module:log("debug", "Writing http/2 request on stream %s...", stream.id);
196 | local req_headers = new_headers();
197 | req_headers:upsert(":method", "POST");
198 | req_headers:upsert(":scheme", "https");
199 | req_headers:upsert(":path", "/3/device/"..settings["token"]);
200 | req_headers:upsert("content-length", string.format("%d", #payload));
201 | module:log("debug", "APNS topic: %s (%s)", tostring(topic), tostring(priority == "voip" and topic..".voip" or topic));
202 | req_headers:upsert("apns-topic", priority == "voip" and topic..".voip" or topic);
203 | req_headers:upsert("apns-expiration", tostring(push_ttl));
204 | local collapse_id = nil;
205 | if priority == "high" then
206 | if collapse_pushes then collapse_id = "xmpp-body-push"; end
207 | module:log("debug", "high: push_type: alert, priority: 10, collapse-id: %s", tostring(collapse_id));
208 | req_headers:upsert("apns-push-type", "alert");
209 | req_headers:upsert("apns-priority", "10");
210 | if collapse_id then req_headers:upsert("apns-collapse-id", collapse_id); end
211 | elseif priority == "voip" then
212 | if collapse_pushes then collapse_id = "xmpp-voip-push"; end
213 | module:log("debug", "voip: push_type: alert, priority: 10, collapse-id: %s", tostring(collapse_id));
214 | req_headers:upsert("apns-push-type", "alert");
215 | req_headers:upsert("apns-priority", "10");
216 | if collapse_id then req_headers:upsert("apns-collapse-id", collapse_id); end
217 | else
218 | if collapse_pushes then collapse_id = "xmpp-nobody-push"; end
219 | module:log("debug", "silent: push_type: background, priority: 5, collapse-id: %s", tostring(collapse_id));
220 | req_headers:upsert("apns-push-type", "background");
221 | req_headers:upsert("apns-priority", "5");
222 | if collapse_id then req_headers:upsert("apns-collapse-id", collapse_id); end
223 | end
224 | ok, err, errno = stream:write_headers(req_headers, false, 2);
225 | if not ok then
226 | stream:shutdown();
227 | module:log("warn", "retrying stream %s: APNS write_headers error %s: %s", stream.id, tostring(errno), tostring(err));
228 | return retry();
229 | end
230 | module:log("debug", "payload: %s", payload);
231 | ok, err, errno = stream:write_body_from_string(payload, 2)
232 | if not ok then
233 | stream:shutdown();
234 | module:log("warn", "retrying stream %s: APNS write_body_from_string error %s: %s", stream.id, tostring(errno), tostring(err));
235 | return retry();
236 | end
237 |
238 | -- read response
239 | module:log("debug", "Reading http/2 response on stream %s:", stream.id);
240 | local headers;
241 | -- Skip through 1xx informational headers.
242 | -- From RFC 7231 Section 6.2: "A user agent MAY ignore unexpected 1xx responses"
243 | repeat
244 | module:log("debug", "Reading http/2 headers on stream %s...", stream.id);
245 | headers, err, errno = stream:get_headers(1);
246 | if headers == nil then
247 | stream:shutdown();
248 | module:log("warn", "retrying stream %s: APNS get_headers error %s: %s", stream.id, tostring(errno or ce.EPIPE), tostring(err or ce.strerror(ce.EPIPE)));
249 | return retry();
250 | end
251 | until not non_final_status(headers:get(":status"))
252 |
253 | -- close stream and check response
254 | module:log("debug", "Reading http/2 body on stream %s...", stream.id);
255 | local body, err, errno = stream:get_body_as_string(1);
256 | module:log("debug", "All done, shutting down http/2 stream %s...", stream.id);
257 | stream:shutdown();
258 | if body == nil then
259 | module:log("warn", "retrying stream %s: APNS get_body_as_string error %s: %s", stream.id, tostring(errno or ce.EPIPE), tostring(err or ce.strerror(ce.EPIPE)));
260 | return retry();
261 | end
262 | local status = headers:get(":status");
263 | local response = json.decode(body);
264 | module:log("debug", "APNS response body(%s): %s", tostring(status), tostring(body));
265 | module:log("debug", "Decoded APNS response body(%s): %s", tostring(status), appserver_global.pretty.write(response));
266 | if status == "200" then
267 | async_callback(false);
268 | return;
269 | end
270 |
271 | -- process returned errors
272 | module:log("info", "APNS error response %s: %s", tostring(status), tostring(response["reason"]));
273 | async_callback(string.format("APNS error response %s: %s", tostring(status), tostring(response["reason"])));
274 | if
275 | (status == "400" and response["reason"] == "BadDeviceToken") or
276 | (status == "400" and response["reason"] == "DeviceTokenNotForTopic") or
277 | (status == "410" and response["reason"] == "Unregistered")
278 | then
279 | unregister_token(settings["token"]);
280 | end
281 |
282 | -- try again on idle timeout
283 | if status == "400" and response["reason"] == "IdleTimeout" then
284 | return retry();
285 | end
286 | end);
287 |
288 | -- handle connection errors (and other backtraces in the push code)
289 | if not ok then
290 | module:log("error", "Catched APNS (connect) error: %s", appserver_global.pretty.write(errobj));
291 | connection_promise = nil; --retry connection next time
292 | async_callback("Error sending APNS request");
293 | end
294 | end);
295 |
296 | return true; -- signal the use of use async iq responses
297 | end
298 |
299 | -- setup
300 | certstring = readAll(apns_cert);
301 | keystring = readAll(apns_key);
302 | module:hook("incoming-push-to-apns", apns_handler);
303 | module:hook("determine-apns-priority", apns_push_priority_handler);
304 | module:log("info", "Appserver APNS submodule loaded");
305 | function module.unload()
306 | if module.unhook then
307 | module:unhook("incoming-push-to-apns", apns_handler);
308 | module:unhook("determine-apns-priority", apns_push_priority_handler);
309 | end
310 | module:log("info", "Appserver APNS submodule unloaded");
311 | end
312 |
--------------------------------------------------------------------------------
/mod_push_appserver_fcm/mod_push_appserver_fcm.lua:
--------------------------------------------------------------------------------
1 | -- mod_push_appserver_fcm
2 | --
3 | -- Copyright (C) 2017-2020 Thilo Molitor
4 | --
5 | -- This file is MIT/X11 licensed.
6 | --
7 | -- Submodule implementing FCM communication
8 | --
9 |
10 | -- this is the master module
11 | module:depends("push_appserver");
12 |
13 | local appserver_global = module:shared("*/push_appserver/appserver_global");
14 | local http = require "net.http";
15 | local json = require "util.json";
16 |
17 | -- configuration
18 | local fcm_key = module:get_option_string("push_appserver_fcm_key", nil); --push api key (no default)
19 | local capath = module:get_option_string("push_appserver_fcm_capath", "/etc/ssl/certs"); --ca path on debian systems
20 | local ciphers = module:get_option_string("push_appserver_fcm_ciphers",
21 | "ECDHE-RSA-AES256-GCM-SHA384:"..
22 | "ECDHE-ECDSA-AES256-GCM-SHA384:"..
23 | "ECDHE-RSA-AES128-GCM-SHA256:"..
24 | "ECDHE-ECDSA-AES128-GCM-SHA256"
25 | ); --supported ciphers
26 | local push_ttl = module:get_option_number("push_appserver_fcm_push_ttl", nil); --no ttl (equals 4 weeks)
27 | local push_priority = module:get_option_string("push_appserver_fcm_push_priority", "high"); --high priority pushes (can be "high" or "normal")
28 | local push_endpoint = "https://fcm.googleapis.com/fcm/send";
29 |
30 | -- check config
31 | assert(fcm_key ~= nil, "You need to set 'push_appserver_fcm_key'")
32 |
33 | -- high level network (https) functions
34 | local function send_request(data, callback)
35 | local x = {
36 | sslctx = {
37 | mode = "client",
38 | protocol = "tlsv1_2",
39 | verify = {"peer", "fail_if_no_peer_cert"},
40 | capath = capath,
41 | ciphers = ciphers,
42 | options = {
43 | "no_sslv2",
44 | "no_sslv3",
45 | "no_ticket",
46 | "no_compression",
47 | "cipher_server_preference",
48 | "single_dh_use",
49 | "single_ecdh_use",
50 | }
51 | },
52 | headers = {
53 | ["Authorization"] = "key="..tostring(fcm_key),
54 | ["Content-Type"] = "application/json",
55 | },
56 | body = data
57 | };
58 | local ok, err = http.request(push_endpoint, x, callback);
59 | if not ok then
60 | callback(nil, err);
61 | end
62 | end
63 |
64 | -- handlers
65 | local function fcm_push_priority_handler(event)
66 | return (push_priority=="high" and "high" or "normal");
67 | end
68 |
69 | local function fcm_handler(event)
70 | local settings, async_callback = event.settings, event.async_callback;
71 | local data = {
72 | ["to"] = tostring(settings["token"]),
73 | ["collapse_key"] = "mod_push_appserver_fcm.collapse",
74 | ["priority"] = fcm_push_priority_handler(event),
75 | ["data"] = {},
76 | };
77 | if push_ttl and push_ttl > 0 then data["time_to_live"] = push_ttl; end -- ttl is optional (google's default: 4 weeks)
78 |
79 | local callback = function(response, status_code)
80 | if not response then
81 | module:log("error", "Could not send FCM request: %s", tostring(status_code));
82 | async_callback(tostring(status_code)); -- return error message
83 | return;
84 | end
85 | module:log("debug", "response status code: %s, raw response body: %s", tostring(status_code), response);
86 |
87 | if status_code ~= 200 then
88 | local fcm_error = "Unknown FCM error.";
89 | if status_code == 400 then fcm_error="Invalid JSON or unknown fields."; end
90 | if status_code == 401 then fcm_error="There was an error authenticating the sender account."; end
91 | if status_code >= 500 and status_code < 600 then fcm_error="Internal server error, please retry again later."; end
92 | module:log("error", "Got FCM error: '%s' (%s)", fcm_error, tostring(status_code));
93 | async_callback(fcm_error);
94 | return;
95 | end
96 | response = json.decode(response);
97 | module:log("debug", "decoded: %s", appserver_global.pretty.write(response));
98 |
99 | -- handle errors
100 | if response.failure > 0 then
101 | module:log("warn", "FCM returned %s failures:", tostring(response.failure));
102 | local fcm_error = true;
103 | for k, result in pairs(response.results) do
104 | if result.error and #tostring(result.error)then
105 | module:log("warn", "Got FCM error: '%s'", tostring(result.error));
106 | fcm_error = tostring(result.error); -- return last error to mod_push_appserver
107 | if result.error == "NotRegistered" then
108 | -- add unregister token to prosody event queue
109 | module:log("debug", "Adding unregister-push-token to prosody event queue...");
110 | module:add_timer(1e-06, function()
111 | module:log("warn", "Unregistering failing FCM token %s", tostring(settings["token"]))
112 | module:fire_event("unregister-push-token", {token = tostring(settings["token"]), type = "fcm"})
113 | end)
114 | end
115 | end
116 | end
117 | async_callback(fcm_error);
118 | return;
119 | end
120 |
121 | -- handle success
122 | for k, result in pairs(response.results) do
123 | if result.message_id then
124 | module:log("debug", "got FCM message id: '%s'", tostring(result.message_id));
125 | end
126 | end
127 |
128 | async_callback(false); -- --> no error occured
129 | return;
130 | end
131 |
132 | data = json.encode(data);
133 | module:log("debug", "sending to %s, json string: %s", push_endpoint, data);
134 | send_request(data, callback);
135 | return true; -- signal the use of use async iq responses
136 | end
137 |
138 | -- setup
139 | module:hook("incoming-push-to-fcm", fcm_handler);
140 | module:hook("determine-fcm-priority", fcm_push_priority_handler);
141 | module:log("info", "Appserver FCM submodule loaded");
142 | function module.unload()
143 | if module.unhook then
144 | module:unhook("incoming-push-to-fcm", fcm_handler);
145 | module:unhook("determine-fcm-priority", fcm_push_priority_handler);
146 | end
147 | module:log("info", "Appserver FCM submodule unloaded");
148 | end
149 |
--------------------------------------------------------------------------------
/tools/Readme.txt:
--------------------------------------------------------------------------------
1 | Use the provided tool to convert p12 files generated by Apple's keychain into a format that prosody can use. You may also require the entrust root certificate authority be installed on your machine. You can get that here:
2 | https://www.entrustdatacard.com/pages/root-certificates-download
3 |
--------------------------------------------------------------------------------
/tools/certconvert.sh:
--------------------------------------------------------------------------------
1 |
2 | echo This is a tool to convert p12 files genreated by OSX with the APNS certificate and key to the format desired by this module. This expects the input file to be named voip.p12.
3 | openssl pkcs12 -in voip.p12 -out voip.crt -nodes -nokeys -clcerts
4 | openssl pkcs12 -in voip.p12 -out voip.key -nodes -nocerts -clcerts
5 |
6 |
--------------------------------------------------------------------------------
/tools/localhost.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIDYDCCAkigAwIBAgIJAOZKaWXFsKibMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
3 | BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
4 | aWRnaXRzIFB0eSBMdGQwHhcNMTkwMjEyMDkxMzQ0WhcNMjAwMjEyMDkxMzQ0WjBF
5 | MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
6 | ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
7 | CgKCAQEA04EqnuAgGd272dlgvE57eXYS1Zh2jZIrQ8uWbIBfgrzJv34mHlakNOTv
8 | wIepQ0VW0PMTYIzHy1knsxdXOf+tsuZdBrIeQPuvu3NE5m1H5RttAHlw5SL9ITzX
9 | 2GvZ4U/47+nnD+Jvj3MEYJOaNLRC+YzsffCmZp99uliRxaFu0qNOB4EE8BWPe4Pc
10 | YENS5SI5c0E8ySnwQkcQ7LJpXITi7am8W+s0uTg/mPBuSJCGiHUVkTwN+EI6GlVW
11 | GDL+ft9/4e/x+jvlBRPGOKrAbtFyXarwDAmuvV5pl/jZk8He86emy0hF/gB42WDd
12 | Du5KUt7sMcJcydhz/Ny8Rmubf6wNwQIDAQABo1MwUTAdBgNVHQ4EFgQU8RxCYQnu
13 | 8fbWLaczXJjzuAg2f3EwHwYDVR0jBBgwFoAU8RxCYQnu8fbWLaczXJjzuAg2f3Ew
14 | DwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAu4IGRWRxxFfjA4WL
15 | vSVpNzejgw2+8hRDXlJohJ6Ab8/kgd7fYzWSI9ijPXo6sXvVryZUijllwgYAoaqL
16 | NM8/ZE/n00Zvu4lfd57IYgQXydOlJ9KRKfJJ63e8sRq4wHuVLktz8NjdOB1gDx9K
17 | MgzHdcZtvJdRRDVZ3anYhD0xe/VvKyvPoR++P/0IP6+Kn0EoBBZ+1KxsXCc9LpAg
18 | CgI3Phz9SwYTk1ruD/HdPmyzSOYLvsxZsguWODBbU0RqAK4ncTvX5c8u1OCJiz/q
19 | UspR1/3W2bjing9j9JqDu/1kxLPjSZdlmNRNoaRRzZHwpzXU80PTlI6G/aRwP3SF
20 | XoRDOA==
21 | -----END CERTIFICATE-----
22 | -----BEGIN PRIVATE KEY-----
23 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDTgSqe4CAZ3bvZ
24 | 2WC8Tnt5dhLVmHaNkitDy5ZsgF+CvMm/fiYeVqQ05O/Ah6lDRVbQ8xNgjMfLWSez
25 | F1c5/62y5l0Gsh5A+6+7c0TmbUflG20AeXDlIv0hPNfYa9nhT/jv6ecP4m+PcwRg
26 | k5o0tEL5jOx98KZmn326WJHFoW7So04HgQTwFY97g9xgQ1LlIjlzQTzJKfBCRxDs
27 | smlchOLtqbxb6zS5OD+Y8G5IkIaIdRWRPA34QjoaVVYYMv5+33/h7/H6O+UFE8Y4
28 | qsBu0XJdqvAMCa69XmmX+NmTwd7zp6bLSEX+AHjZYN0O7kpS3uwxwlzJ2HP83LxG
29 | a5t/rA3BAgMBAAECggEBAJuJmpE5mwHKmTGMdWIliRH6bcFcHJrhyiVSG23xPcvE
30 | D81d9KRM7PblARch7KzG2iBREYfoH6mAB/zhBkllqBQu6mxZInyBWmtMSnf2F2/5
31 | qkA2DnrDL0l5F/ZU4eaazCOKbFy3FXl1iNuI0Fc/1Rz1sJixF33SfkBCj/i5Telh
32 | zkQKcocd9qgCOchDA2t3ETFeYtGGcjtnCCOZeJcxqmJqNtOuuFWoT+uq+xk0gDTw
33 | bpJp6HtXRPeC67P+Bi/l/5lo0Wrw64dzA+Ww03Zjn2rZ06rILl0yGiWBLzu+JOsF
34 | Ql21VhGdpVZf7UdGgHDwYIh6rQGILeSacs05z6hlUQECgYEA78bR4M19xxk4GDdp
35 | KzhktRmQM9QfIPfybhT9LICR2jpywU6eFbZXknDjRWRsbBZbnhXSxTMtldLekMsh
36 | huZ90DwlR1+hYF+bI+fD6onhT90px1coo+itjCFGzv4eN4G+l4wJH4UifJzCnExi
37 | mGDwkY6RMIJfu9V9YTfx1PUgmTECgYEA4dClE+IrWYyOkWc2l4GCunLBL+HYGYXe
38 | YK0ROGo+nKMmGpwRU53p57f5UlJmNLXB2LxdvK/bLs45R9vvQ4pbiav2Mnl9AkR0
39 | Vx57515q2m9UkGeavuslgQvpWUXzSM33q45ktfxBQGm9eNRqZFPNFYLU7ojEmc8U
40 | JPf7gq1XmZECgYBIQSybxrDBX5skyQXbLVpDrJlk1OYwhCc8/vwv/ep1zakpEWzX
41 | 9CO9kGEcVx/JDo+7Oq5SGJMDFBFXpq7KvQhqyMUfFfVGWmkgz8WdFsGb1HSzilNH
42 | 2WT61khFNhSa+3EYr+1L302+KWsHuIj5jDTSWBjuekspCjOHKVmpp9iT8QKBgQDA
43 | U75lovoc+RPsT6Y1f/7h4h8cMxSlGFmAqabDD+pn6qTngQlY2GSCETVuofOe7Tc+
44 | 66BCttzNjqNGytGMCulP5oB4GPUZ20fjc3lAZDBJ/wxdOYCZHxoAQS7r9CHzXkmV
45 | el/YiBLjU6wmn3RGIwHHUnkc+KvJ/I9BtVwnvo+foQKBgFBu1Sn6SqwtElfrM7Wf
46 | 1j7SKK978pyXbdU0XH1kTd2kmj0TtYtEAwaV3tiPd/NLRSBR6ICTqrOy4sJLM1vN
47 | 4YjVCJ9LsWhyGmQ96uXR4Jw3ev1mlS80nYxcxqmUagvvS/VP/6NF8BvG2sFA3n/Y
48 | yn4/vcibmO5aQAZPNgb+HVhH
49 | -----END PRIVATE KEY-----
50 |
--------------------------------------------------------------------------------
/tools/simple_apns_emulator.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | import socket, ssl, time
3 | import argparse
4 | import random
5 |
6 | parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter, description="Simple APNS backend server emulator.\n")
7 | parser.add_argument("-l", "--listen", metavar='HOSTNAME', help="Local hostname or IP to listen on (Default: 0.0.0.0 e.g. any)", default="0.0.0.0")
8 | parser.add_argument("-p", "--port", metavar='PORT', type=int, help="Port to listen on (Default: 2195)", default=2195)
9 | parser.add_argument("--probability", metavar='PROBABILITY', type=float, help="Error Probability (Default: 0.5)", default=0.5)
10 | parser.add_argument("--cert", metavar='CERT', help="Certificate file to use (Default: localhost.pem)", default="localhost.pem")
11 | parser.add_argument("--key", metavar='KEY', help="Key file to use (Default: localhost.pem)", default="localhost.pem")
12 | args = parser.parse_args()
13 |
14 | context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
15 | context.load_cert_chain(certfile=args.cert, keyfile=args.key)
16 |
17 | bindsocket = socket.socket()
18 | bindsocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
19 | bindsocket.bind((args.listen, args.port))
20 | bindsocket.listen(5)
21 |
22 | print("Waiting for connections on %s:%d..." % (args.listen, args.port))
23 | while True:
24 | newsocket, fromaddr = bindsocket.accept()
25 | sslsoc = context.wrap_socket(newsocket, server_side=True)
26 | print("Got new connection from %s..." % str(fromaddr))
27 | while True:
28 | request = sslsoc.read()
29 | if not len(request):
30 | break
31 | print("< %s" % str(request))
32 | if random.random() < args.probability:
33 | # the following simulates an error response of type 8 (invalid token)
34 | time.sleep(0.2)
35 | response = b'\x08\x08'+request[-8:-4]
36 | print("> %s" % str(response))
37 | sslsoc.write(response)
38 | sslsoc.close()
39 | break
40 | print("Connection was closed, waiting for new one...")
41 |
--------------------------------------------------------------------------------