├── LICENSE ├── README.md ├── mod_push_appserver ├── cached_store.lib.lua ├── mod_push_appserver.lua ├── pl │ ├── Date.lua │ ├── List.lua │ ├── Map.lua │ ├── MultiMap.lua │ ├── OrderedMap.lua │ ├── Set.lua │ ├── app.lua │ ├── array2d.lua │ ├── class.lua │ ├── compat.lua │ ├── comprehension.lua │ ├── config.lua │ ├── data.lua │ ├── dir.lua │ ├── file.lua │ ├── func.lua │ ├── import_into.lua │ ├── init.lua │ ├── input.lua │ ├── lapp.lua │ ├── lexer.lua │ ├── luabalanced.lua │ ├── operator.lua │ ├── path.lua │ ├── permute.lua │ ├── pretty.lua │ ├── seq.lua │ ├── sip.lua │ ├── strict.lua │ ├── stringio.lua │ ├── stringx.lua │ ├── tablex.lua │ ├── template.lua │ ├── test.lua │ ├── text.lua │ ├── types.lua │ ├── url.lua │ ├── utils.lua │ └── xml.lua ├── uncached_store.lib.lua └── zthrottle.lib.lua ├── mod_push_appserver_apns └── mod_push_appserver_apns.lua ├── mod_push_appserver_fcm └── mod_push_appserver_fcm.lua ├── old-readme.md └── tools ├── Readme.txt ├── certconvert.sh ├── localhost.pem └── simple_apns_emulator.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2018 Thilo Molitor 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mod\_push\_appserver 2 | 3 | This project is not maintained anymore, use the follow-up project over here: https://github.com/monal-im/fpush 4 | -------------------------------------------------------------------------------- /mod_push_appserver/cached_store.lib.lua: -------------------------------------------------------------------------------- 1 | return function(params) 2 | local store = module:open_store(); 3 | local cache = {}; 4 | local token2node_cache = {}; 5 | local api = {}; 6 | function api:get(node) 7 | if not cache[node] then 8 | local err; 9 | cache[node], err = store:get(node); 10 | if not cache[node] and err then 11 | module:log("error", "Error reading push notification storage for node '%s': %s", node, tostring(err)); 12 | cache[node] = {}; 13 | return cache[node], false; 14 | end 15 | end 16 | if not cache[node] then cache[node] = {} end 17 | -- add entry to token2node cache, too 18 | if cache[node].token and cache[node].node then token2node_cache[cache[node].token] = cache[node].node; end 19 | return cache[node], true; 20 | end 21 | function api:set(node, data) 22 | local settings = api:get(node); -- load node's data 23 | -- fill caches 24 | cache[node] = data; 25 | if settings.token and settings.node then token2node_cache[settings.token] = settings.node; end 26 | local ok, err = store:set(node, cache[node]); 27 | if not ok then 28 | module:log("error", "Error writing push notification storage for node '%s': %s", node, tostring(err)); 29 | return false; 30 | end 31 | return true; 32 | end 33 | function api:list() 34 | return store:users(); 35 | end 36 | function api:token2node(token) 37 | if token2node_cache[token] then return token2node_cache[token]; end 38 | for node in store:users() do 39 | local err; 40 | -- read data directly, we don't want to cache full copies of stale entries as api:get() would do 41 | settings, err = store:get(node); 42 | if not settings and err then 43 | module:log("error", "Error reading push notification storage for node '%s': %s", node, tostring(err)); 44 | settings = {}; 45 | end 46 | if settings.token and settings.node then token2node_cache[settings.token] = settings.node; end 47 | end 48 | if token2node_cache[token] then return token2node_cache[token]; end 49 | return nil; 50 | end 51 | return api; 52 | end; -------------------------------------------------------------------------------- /mod_push_appserver/pl/Map.lua: -------------------------------------------------------------------------------- 1 | --- A Map class. 2 | -- 3 | -- > Map = require 'pl.Map' 4 | -- > m = Map{one=1,two=2} 5 | -- > m:update {three=3,four=4,two=20} 6 | -- > = m == M{one=1,two=20,three=3,four=4} 7 | -- true 8 | -- 9 | -- Dependencies: `pl.utils`, `pl.class`, `pl.tablex`, `pl.pretty` 10 | -- @classmod pl.Map 11 | 12 | local tablex = require 'pl.tablex' 13 | local utils = require 'pl.utils' 14 | local stdmt = utils.stdmt 15 | local deepcompare = tablex.deepcompare 16 | 17 | local pretty_write = require 'pl.pretty' . write 18 | local Map = stdmt.Map 19 | local Set = stdmt.Set 20 | 21 | local class = require 'pl.class' 22 | 23 | -- the Map class --------------------- 24 | class(nil,nil,Map) 25 | 26 | function Map:_init (t) 27 | local mt = getmetatable(t) 28 | if mt == Set or mt == Map then 29 | self:update(t) 30 | else 31 | return t -- otherwise assumed to be a map-like table 32 | end 33 | end 34 | 35 | 36 | local function makelist(t) 37 | return setmetatable(t, require('pl.List')) 38 | end 39 | 40 | --- list of keys. 41 | Map.keys = tablex.keys 42 | 43 | --- list of values. 44 | Map.values = tablex.values 45 | 46 | --- return an iterator over all key-value pairs. 47 | function Map:iter () 48 | return pairs(self) 49 | end 50 | 51 | --- return a List of all key-value pairs, sorted by the keys. 52 | function Map:items() 53 | local ls = makelist(tablex.pairmap (function (k,v) return makelist {k,v} end, self)) 54 | ls:sort(function(t1,t2) return t1[1] < t2[1] end) 55 | return ls 56 | end 57 | 58 | -- Will return the existing value, or if it doesn't exist it will set 59 | -- a default value and return it. 60 | function Map:setdefault(key, defaultval) 61 | return self[key] or self:set(key,defaultval) or defaultval 62 | end 63 | 64 | --- size of map. 65 | -- note: this is a relatively expensive operation! 66 | -- @class function 67 | -- @name Map:len 68 | Map.len = tablex.size 69 | 70 | --- put a value into the map. 71 | -- This will remove the key if the value is `nil` 72 | -- @param key the key 73 | -- @param val the value 74 | function Map:set (key,val) 75 | self[key] = val 76 | end 77 | 78 | --- get a value from the map. 79 | -- @param key the key 80 | -- @return the value, or nil if not found. 81 | function Map:get (key) 82 | return rawget(self,key) 83 | end 84 | 85 | local index_by = tablex.index_by 86 | 87 | --- get a list of values indexed by a list of keys. 88 | -- @param keys a list-like table of keys 89 | -- @return a new list 90 | function Map:getvalues (keys) 91 | return makelist(index_by(self,keys)) 92 | end 93 | 94 | --- update the map using key/value pairs from another table. 95 | -- @tab table 96 | -- @function Map:update 97 | Map.update = tablex.update 98 | 99 | --- equality between maps. 100 | -- @within metamethods 101 | -- @tparam Map m another map. 102 | function Map:__eq (m) 103 | -- note we explicitly ask deepcompare _not_ to use __eq! 104 | return deepcompare(self,m,true) 105 | end 106 | 107 | --- string representation of a map. 108 | -- @within metamethods 109 | function Map:__tostring () 110 | return pretty_write(self,'') 111 | end 112 | 113 | return Map 114 | -------------------------------------------------------------------------------- /mod_push_appserver/pl/MultiMap.lua: -------------------------------------------------------------------------------- 1 | --- MultiMap, a Map which has multiple values per key. 2 | -- 3 | -- Dependencies: `pl.utils`, `pl.class`, `pl.List`, `pl.Map` 4 | -- @classmod pl.MultiMap 5 | 6 | local utils = require 'pl.utils' 7 | local class = require 'pl.class' 8 | local List = require 'pl.List' 9 | local Map = require 'pl.Map' 10 | 11 | -- MultiMap is a standard MT 12 | local MultiMap = utils.stdmt.MultiMap 13 | 14 | class(Map,nil,MultiMap) 15 | MultiMap._name = 'MultiMap' 16 | 17 | function MultiMap:_init (t) 18 | if not t then return end 19 | self:update(t) 20 | end 21 | 22 | --- update a MultiMap using a table. 23 | -- @param t either a Multimap or a map-like table. 24 | -- @return the map 25 | function MultiMap:update (t) 26 | utils.assert_arg(1,t,'table') 27 | if Map:class_of(t) then 28 | for k,v in pairs(t) do 29 | self[k] = List() 30 | self[k]:append(v) 31 | end 32 | else 33 | for k,v in pairs(t) do 34 | self[k] = List(v) 35 | end 36 | end 37 | end 38 | 39 | --- add a new value to a key. Setting a nil value removes the key. 40 | -- @param key the key 41 | -- @param val the value 42 | -- @return the map 43 | function MultiMap:set (key,val) 44 | if val == nil then 45 | self[key] = nil 46 | else 47 | if not self[key] then 48 | self[key] = List() 49 | end 50 | self[key]:append(val) 51 | end 52 | end 53 | 54 | return MultiMap 55 | -------------------------------------------------------------------------------- /mod_push_appserver/pl/OrderedMap.lua: -------------------------------------------------------------------------------- 1 | --- OrderedMap, a map which preserves ordering. 2 | -- 3 | -- Derived from `pl.Map`. 4 | -- 5 | -- Dependencies: `pl.utils`, `pl.tablex`, `pl.class`, `pl.List`, `pl.Map` 6 | -- @classmod pl.OrderedMap 7 | 8 | local tablex = require 'pl.tablex' 9 | local utils = require 'pl.utils' 10 | local List = require 'pl.List' 11 | local index_by,tsort,concat = tablex.index_by,table.sort,table.concat 12 | 13 | local class = require 'pl.class' 14 | local Map = require 'pl.Map' 15 | 16 | local OrderedMap = class(Map) 17 | OrderedMap._name = 'OrderedMap' 18 | 19 | local rawset = rawset 20 | 21 | --- construct an OrderedMap. 22 | -- Will throw an error if the argument is bad. 23 | -- @param t optional initialization table, same as for @{OrderedMap:update} 24 | function OrderedMap:_init (t) 25 | rawset(self,'_keys',List()) 26 | if t then 27 | local map,err = self:update(t) 28 | if not map then error(err,2) end 29 | end 30 | end 31 | 32 | local assert_arg,raise = utils.assert_arg,utils.raise 33 | 34 | --- update an OrderedMap using a table. 35 | -- If the table is itself an OrderedMap, then its entries will be appended. 36 | -- if it s a table of the form `{{key1=val1},{key2=val2},...}` these will be appended. 37 | -- 38 | -- Otherwise, it is assumed to be a map-like table, and order of extra entries is arbitrary. 39 | -- @tab t a table. 40 | -- @return the map, or nil in case of error 41 | -- @return the error message 42 | function OrderedMap:update (t) 43 | assert_arg(1,t,'table') 44 | if OrderedMap:class_of(t) then 45 | for k,v in t:iter() do 46 | self:set(k,v) 47 | end 48 | elseif #t > 0 then -- an array must contain {key=val} tables 49 | if type(t[1]) == 'table' then 50 | for _,pair in ipairs(t) do 51 | local key,value = next(pair) 52 | if not key then return raise 'empty pair initialization table' end 53 | self:set(key,value) 54 | end 55 | else 56 | return raise 'cannot use an array to initialize an OrderedMap' 57 | end 58 | else 59 | for k,v in pairs(t) do 60 | self:set(k,v) 61 | end 62 | end 63 | return self 64 | end 65 | 66 | --- set the key's value. This key will be appended at the end of the map. 67 | -- 68 | -- If the value is nil, then the key is removed. 69 | -- @param key the key 70 | -- @param val the value 71 | -- @return the map 72 | function OrderedMap:set (key,val) 73 | if rawget(self, key) == nil and val ~= nil then -- new key 74 | self._keys:append(key) -- we keep in order 75 | rawset(self,key,val) -- don't want to provoke __newindex! 76 | else -- existing key-value pair 77 | if val == nil then 78 | self._keys:remove_value(key) 79 | rawset(self,key,nil) 80 | else 81 | self[key] = val 82 | end 83 | end 84 | return self 85 | end 86 | 87 | OrderedMap.__newindex = OrderedMap.set 88 | 89 | --- insert a key/value pair before a given position. 90 | -- Note: if the map already contains the key, then this effectively 91 | -- moves the item to the new position by first removing at the old position. 92 | -- Has no effect if the key does not exist and val is nil 93 | -- @int pos a position starting at 1 94 | -- @param key the key 95 | -- @param val the value; if nil use the old value 96 | function OrderedMap:insert (pos,key,val) 97 | local oldval = self[key] 98 | val = val or oldval 99 | if oldval then 100 | self._keys:remove_value(key) 101 | end 102 | if val then 103 | self._keys:insert(pos,key) 104 | rawset(self,key,val) 105 | end 106 | return self 107 | end 108 | 109 | --- return the keys in order. 110 | -- (Not a copy!) 111 | -- @return List 112 | function OrderedMap:keys () 113 | return self._keys 114 | end 115 | 116 | --- return the values in order. 117 | -- this is relatively expensive. 118 | -- @return List 119 | function OrderedMap:values () 120 | return List(index_by(self,self._keys)) 121 | end 122 | 123 | --- sort the keys. 124 | -- @func cmp a comparison function as for @{table.sort} 125 | -- @return the map 126 | function OrderedMap:sort (cmp) 127 | tsort(self._keys,cmp) 128 | return self 129 | end 130 | 131 | --- iterate over key-value pairs in order. 132 | function OrderedMap:iter () 133 | local i = 0 134 | local keys = self._keys 135 | local idx 136 | return function() 137 | i = i + 1 138 | if i > #keys then return nil end 139 | idx = keys[i] 140 | return idx,self[idx] 141 | end 142 | end 143 | 144 | --- iterate over an ordered map (5.2). 145 | -- @within metamethods 146 | -- @function OrderedMap:__pairs 147 | OrderedMap.__pairs = OrderedMap.iter 148 | 149 | --- string representation of an ordered map. 150 | -- @within metamethods 151 | function OrderedMap:__tostring () 152 | local res = {} 153 | for i,v in ipairs(self._keys) do 154 | local val = self[v] 155 | local vs = tostring(val) 156 | if type(val) ~= 'number' then 157 | vs = '"'..vs..'"' 158 | end 159 | res[i] = tostring(v)..'='..vs 160 | end 161 | return '{'..concat(res,',')..'}' 162 | end 163 | 164 | return OrderedMap 165 | 166 | 167 | 168 | -------------------------------------------------------------------------------- /mod_push_appserver/pl/Set.lua: -------------------------------------------------------------------------------- 1 | --- A Set class. 2 | -- 3 | -- > Set = require 'pl.Set' 4 | -- > = Set{'one','two'} == Set{'two','one'} 5 | -- true 6 | -- > fruit = Set{'apple','banana','orange'} 7 | -- > = fruit['banana'] 8 | -- true 9 | -- > = fruit['hazelnut'] 10 | -- nil 11 | -- > colours = Set{'red','orange','green','blue'} 12 | -- > = fruit,colours 13 | -- [apple,orange,banana] [blue,green,orange,red] 14 | -- > = fruit+colours 15 | -- [blue,green,apple,red,orange,banana] 16 | -- [orange] 17 | -- > more_fruits = fruit + 'apricot' 18 | -- > = fruit*colours 19 | -- > = more_fruits, fruit 20 | -- [banana,apricot,apple,orange] [banana,apple,orange] 21 | -- 22 | -- Dependencies: `pl.utils`, `pl.tablex`, `pl.class`, `pl.Map`, (`pl.List` if __tostring is used) 23 | -- @module pl.Set 24 | 25 | local tablex = require 'pl.tablex' 26 | local utils = require 'pl.utils' 27 | local array_tostring, concat = utils.array_tostring, table.concat 28 | local merge,difference = tablex.merge,tablex.difference 29 | local Map = require 'pl.Map' 30 | local class = require 'pl.class' 31 | local stdmt = utils.stdmt 32 | local Set = stdmt.Set 33 | 34 | -- the Set class -------------------- 35 | class(Map,nil,Set) 36 | 37 | -- note: Set has _no_ methods! 38 | Set.__index = nil 39 | 40 | local function makeset (t) 41 | return setmetatable(t,Set) 42 | end 43 | 44 | --- create a set.
45 | -- @param t may be a Set, Map or list-like table. 46 | -- @class function 47 | -- @name Set 48 | function Set:_init (t) 49 | t = t or {} 50 | local mt = getmetatable(t) 51 | if mt == Set or mt == Map then 52 | for k in pairs(t) do self[k] = true end 53 | else 54 | for _,v in ipairs(t) do self[v] = true end 55 | end 56 | end 57 | 58 | --- string representation of a set. 59 | -- @within metamethods 60 | function Set:__tostring () 61 | return '['..concat(array_tostring(Set.values(self)),',')..']' 62 | end 63 | 64 | --- get a list of the values in a set. 65 | -- @param self a Set 66 | -- @function Set.values 67 | -- @return a list 68 | Set.values = Map.keys 69 | 70 | --- map a function over the values of a set. 71 | -- @param self a Set 72 | -- @param fn a function 73 | -- @param ... extra arguments to pass to the function. 74 | -- @return a new set 75 | function Set.map (self,fn,...) 76 | fn = utils.function_arg(1,fn) 77 | local res = {} 78 | for k in pairs(self) do 79 | res[fn(k,...)] = true 80 | end 81 | return makeset(res) 82 | end 83 | 84 | --- union of two sets (also +). 85 | -- @param self a Set 86 | -- @param set another set 87 | -- @return a new set 88 | function Set.union (self,set) 89 | return merge(self,set,true) 90 | end 91 | 92 | --- modifies '+' operator to allow addition of non-Set elements 93 | --- Preserves +/- semantics - does not modify first argument. 94 | local function setadd(self,other) 95 | local mt = getmetatable(other) 96 | if mt == Set or mt == Map then 97 | return Set.union(self,other) 98 | else 99 | local new = Set(self) 100 | new[other] = true 101 | return new 102 | end 103 | end 104 | 105 | --- union of sets. 106 | -- @within metamethods 107 | -- @function Set.__add 108 | 109 | Set.__add = setadd 110 | 111 | --- intersection of two sets (also *). 112 | -- @param self a Set 113 | -- @param set another set 114 | -- @return a new set 115 | -- @usage 116 | -- > s = Set{10,20,30} 117 | -- > t = Set{20,30,40} 118 | -- > = t 119 | -- [20,30,40] 120 | -- > = Set.intersection(s,t) 121 | -- [30,20] 122 | -- > = s*t 123 | -- [30,20] 124 | 125 | function Set.intersection (self,set) 126 | return merge(self,set,false) 127 | end 128 | 129 | --- intersection of sets. 130 | -- @within metamethods 131 | -- @function Set.__mul 132 | Set.__mul = Set.intersection 133 | 134 | --- new set with elements in the set that are not in the other (also -). 135 | -- @param self a Set 136 | -- @param set another set 137 | -- @return a new set 138 | function Set.difference (self,set) 139 | return difference(self,set,false) 140 | end 141 | 142 | --- modifies "-" operator to remove non-Set values from set. 143 | --- Preserves +/- semantics - does not modify first argument. 144 | local function setminus (self,other) 145 | local mt = getmetatable(other) 146 | if mt == Set or mt == Map then 147 | return Set.difference(self,other) 148 | else 149 | local new = Set(self) 150 | new[other] = nil 151 | return new 152 | end 153 | end 154 | 155 | --- difference of sets. 156 | -- @within metamethods 157 | -- @function Set.__sub 158 | Set.__sub = setminus 159 | 160 | -- a new set with elements in _either_ the set _or_ other but not both (also ^). 161 | -- @param self a Set 162 | -- @param set another set 163 | -- @return a new set 164 | function Set.symmetric_difference (self,set) 165 | return difference(self,set,true) 166 | end 167 | 168 | --- symmetric difference of sets. 169 | -- @within metamethods 170 | -- @function Set.__pow 171 | Set.__pow = Set.symmetric_difference 172 | 173 | --- is the first set a subset of the second (also <)?. 174 | -- @param self a Set 175 | -- @param set another set 176 | -- @return true or false 177 | function Set.issubset (self,set) 178 | for k in pairs(self) do 179 | if not set[k] then return false end 180 | end 181 | return true 182 | end 183 | 184 | --- first set subset of second? 185 | -- @within metamethods 186 | -- @function Set.__lt 187 | Set.__lt = Set.issubset 188 | 189 | --- is the set empty?. 190 | -- @param self a Set 191 | -- @return true or false 192 | function Set.isempty (self) 193 | return next(self) == nil 194 | end 195 | 196 | --- are the sets disjoint? (no elements in common). 197 | -- Uses naive definition, i.e. that intersection is empty 198 | -- @param s1 a Set 199 | -- @param s2 another set 200 | -- @return true or false 201 | function Set.isdisjoint (s1,s2) 202 | return Set.isempty(Set.intersection(s1,s2)) 203 | end 204 | 205 | --- size of this set (also # for 5.2). 206 | -- @param s a Set 207 | -- @return size 208 | -- @function Set.len 209 | Set.len = tablex.size 210 | 211 | --- cardinality of set (5.2). 212 | -- @within metamethods 213 | -- @function Set.__len 214 | Set.__len = Set.len 215 | 216 | --- equality between sets. 217 | -- @within metamethods 218 | function Set.__eq (s1,s2) 219 | return Set.issubset(s1,s2) and Set.issubset(s2,s1) 220 | end 221 | 222 | return Set 223 | -------------------------------------------------------------------------------- /mod_push_appserver/pl/app.lua: -------------------------------------------------------------------------------- 1 | --- Application support functions. 2 | -- See @{01-introduction.md.Application_Support|the Guide} 3 | -- 4 | -- Dependencies: `pl.utils`, `pl.path` 5 | -- @module pl.app 6 | 7 | local io,package,require = _G.io, _G.package, _G.require 8 | local utils = require 'pl.utils' 9 | local path = require 'pl.path' 10 | 11 | local app = {} 12 | 13 | --- return the name of the current script running. 14 | -- The name will be the name as passed on the command line 15 | -- @return string filename 16 | function app.script_name() 17 | if _G.arg and _G.arg[0] then 18 | return _G.arg[0] 19 | end 20 | return utils.raise("No script name found") 21 | end 22 | 23 | --- add the current script's path to the Lua module path. 24 | -- Applies to both the source and the binary module paths. It makes it easy for 25 | -- the main file of a multi-file program to access its modules in the same directory. 26 | -- `base` allows these modules to be put in a specified subdirectory, to allow for 27 | -- cleaner deployment and resolve potential conflicts between a script name and its 28 | -- library directory. 29 | -- @string base optional base directory. 30 | -- @treturn string the current script's path with a trailing slash 31 | function app.require_here (base) 32 | local p = path.dirname(app.script_name()) 33 | if not path.isabs(p) then 34 | p = path.join(path.currentdir(),p) 35 | end 36 | if p:sub(-1,-1) ~= path.sep then 37 | p = p..path.sep 38 | end 39 | if base then 40 | base = path.normcase(base) 41 | if path.isabs(base) then 42 | p = base .. path.sep 43 | else 44 | p = p..base..path.sep 45 | end 46 | end 47 | local so_ext = path.is_windows and 'dll' or 'so' 48 | local lsep = package.path:find '^;' and '' or ';' 49 | local csep = package.cpath:find '^;' and '' or ';' 50 | package.path = ('%s?.lua;%s?%sinit.lua%s%s'):format(p,p,path.sep,lsep,package.path) 51 | package.cpath = ('%s?.%s%s%s'):format(p,so_ext,csep,package.cpath) 52 | return p 53 | end 54 | 55 | --- return a suitable path for files private to this application. 56 | -- These will look like '~/.SNAME/file', with '~' as with expanduser and 57 | -- SNAME is the name of the script without .lua extension. 58 | -- If the directory does not exist, it will be created. 59 | -- @string file a filename (w/out path) 60 | -- @return a full pathname, or nil 61 | -- @return cannot create directory error 62 | -- @usage 63 | -- -- when run from a script called 'testapp' (on Windows): 64 | -- local app = require 'pl.app' 65 | -- print(app.appfile 'test.txt') 66 | -- -- C:\Documents and Settings\steve\.testapp\test.txt 67 | function app.appfile(file) 68 | local sfullname, err = app.script_name() 69 | if not sfullname then return utils.raise(err) end 70 | local sname = path.basename(sfullname) 71 | local name = path.splitext(sname) 72 | local dir = path.join(path.expanduser('~'),'.'..name) 73 | if not path.isdir(dir) then 74 | local ret = path.mkdir(dir) 75 | if not ret then return utils.raise('cannot create '..dir) end 76 | end 77 | return path.join(dir,file) 78 | end 79 | 80 | --- return string indicating operating system. 81 | -- @return 'Windows','OSX' or whatever uname returns (e.g. 'Linux') 82 | function app.platform() 83 | if path.is_windows then 84 | return 'Windows' 85 | else 86 | local f = io.popen('uname') 87 | local res = f:read() 88 | if res == 'Darwin' then res = 'OSX' end 89 | f:close() 90 | return res 91 | end 92 | end 93 | 94 | --- return the full command-line used to invoke this script. 95 | -- It will not include the scriptname itself, see `app.script_name`. 96 | -- @return command-line 97 | -- @return name of Lua program used 98 | -- @usage 99 | -- -- execute: lua -lluacov -e 'print(_VERSION)' myscript.lua 100 | -- 101 | -- -- myscript.lua 102 | -- print(require("pl.app").lua())) --> lua -lluacov -e 'print(_VERSION)' 103 | function app.lua() 104 | local args = _G.arg 105 | if not args then 106 | return utils.raise "not in a main program" 107 | end 108 | 109 | local cmd = {} 110 | local i = -1 111 | while true do 112 | table.insert(cmd, 1, args[i]) 113 | if not args[i-1] then 114 | return utils.quote_arg(cmd), args[i] 115 | end 116 | i = i - 1 117 | end 118 | end 119 | 120 | --- parse command-line arguments into flags and parameters. 121 | -- Understands GNU-style command-line flags; short (`-f`) and long (`--flag`). 122 | -- 123 | -- These may be given a value with either '=' or ':' (`-k:2`,`--alpha=3.2`,`-n2`), 124 | -- a number value can be given without a space. If the flag is marked 125 | -- as having a value, then a space-separated value is also accepted (`-i hello`), 126 | -- see the `flags_with_values` argument). 127 | -- 128 | -- Multiple short args can be combined like so: ( `-abcd`). 129 | -- 130 | -- When specifying the `flags_valid` parameter, its contents can also contain 131 | -- aliasses, to convert short/long flags to the same output name. See the 132 | -- example below. 133 | -- 134 | -- Note: if a flag is repeated, the last value wins. 135 | -- @tparam {string} args an array of strings (default is the global `arg`) 136 | -- @tab flags_with_values any flags that take values, either list or hash 137 | -- table e.g. `{ out=true }` or `{ "out" }`. 138 | -- @tab flags_valid (optional) flags that are valid, either list or hashtable. 139 | -- If not given, everything 140 | -- will be accepted(everything in `flags_with_values` will automatically be allowed) 141 | -- @return a table of flags (flag=value pairs) 142 | -- @return an array of parameters 143 | -- @raise if args is nil, then the global `args` must be available! 144 | -- @usage 145 | -- -- Simple form: 146 | -- local flags, params = app.parse_args(nil, 147 | -- { "hello", "world" }, -- list of flags taking values 148 | -- { "l", "a", "b"}) -- list of allowed flags (value ones will be added) 149 | -- 150 | -- -- More complex example using aliasses: 151 | -- local valid = { 152 | -- long = "l", -- if 'l' is specified, it is reported as 'long' 153 | -- new = { "n", "old" }, -- here both 'n' and 'old' will go into 'new' 154 | -- } 155 | -- local values = { 156 | -- "value", -- will automatically be added to the allowed set of flags 157 | -- "new", -- will mark 'n' and 'old' as requiring a value as well 158 | -- } 159 | -- local flags, params = app.parse_args(nil, values, valid) 160 | -- 161 | -- -- command: myapp.lua -l --old:hello --value world param1 param2 162 | -- -- will yield: 163 | -- flags = { 164 | -- long = true, -- input from 'l' 165 | -- new = "hello", -- input from 'old' 166 | -- value = "world", -- allowed because it was in 'values', note: space separated! 167 | -- } 168 | -- params = { 169 | -- [1] = "param1" 170 | -- [2] = "param2" 171 | -- } 172 | function app.parse_args (args,flags_with_values, flags_valid) 173 | if not args then 174 | args = _G.arg 175 | if not args then utils.raise "Not in a main program: 'arg' not found" end 176 | end 177 | 178 | local with_values = {} 179 | for k,v in pairs(flags_with_values or {}) do 180 | if type(k) == "number" then 181 | k = v 182 | end 183 | with_values[k] = true 184 | end 185 | 186 | local valid 187 | if not flags_valid then 188 | -- if no allowed flags provided, we create a table that always returns 189 | -- the keyname, no matter what you look up 190 | valid = setmetatable({},{ __index = function(_, key) return key end }) 191 | else 192 | valid = {} 193 | for k,aliasses in pairs(flags_valid) do 194 | if type(k) == "number" then -- array/list entry 195 | k = aliasses 196 | end 197 | if type(aliasses) == "string" then -- single alias 198 | aliasses = { aliasses } 199 | end 200 | if type(aliasses) == "table" then -- list of aliasses 201 | -- it's the alternate name, so add the proper mappings 202 | for i, alias in ipairs(aliasses) do 203 | valid[alias] = k 204 | end 205 | end 206 | valid[k] = k 207 | end 208 | do 209 | local new_with_values = {} -- needed to prevent "invalid key to 'next'" error 210 | for k,v in pairs(with_values) do 211 | if not valid[k] then 212 | valid[k] = k -- add the with_value entry as a valid one 213 | new_with_values[k] = true 214 | else 215 | new_with_values[valid[k]] = true --set, but by its alias 216 | end 217 | end 218 | with_values = new_with_values 219 | end 220 | end 221 | 222 | -- now check that all flags with values are reported as such under all 223 | -- of their aliasses 224 | for k, main_alias in pairs(valid) do 225 | if with_values[main_alias] then 226 | with_values[k] = true 227 | end 228 | end 229 | 230 | local _args = {} 231 | local flags = {} 232 | local i = 1 233 | while i <= #args do 234 | local a = args[i] 235 | local v = a:match('^-(.+)') 236 | local is_long 237 | if not v then 238 | -- we have a parameter 239 | _args[#_args+1] = a 240 | else 241 | -- it's a flag 242 | if v:find '^-' then 243 | is_long = true 244 | v = v:sub(2) 245 | end 246 | if with_values[v] then 247 | if i == #args or args[i+1]:find '^-' then 248 | return utils.raise ("no value for '"..v.."'") 249 | end 250 | flags[valid[v]] = args[i+1] 251 | i = i + 1 252 | else 253 | -- a value can also be indicated with = or : 254 | local var,val = utils.splitv (v,'[=:]') 255 | var = var or v 256 | val = val or true 257 | if not is_long then 258 | if #var > 1 then 259 | if var:find '.%d+' then -- short flag, number value 260 | val = var:sub(2) 261 | var = var:sub(1,1) 262 | else -- multiple short flags 263 | for i = 1,#var do 264 | local f = var:sub(i,i) 265 | if not valid[f] then 266 | return utils.raise("unknown flag '"..f.."'") 267 | else 268 | f = valid[f] 269 | end 270 | flags[f] = true 271 | end 272 | val = nil -- prevents use of var as a flag below 273 | end 274 | else -- single short flag (can have value, defaults to true) 275 | val = val or true 276 | end 277 | end 278 | if val then 279 | if not valid[var] then 280 | return utils.raise("unknown flag '"..var.."'") 281 | else 282 | var = valid[var] 283 | end 284 | flags[var] = val 285 | end 286 | end 287 | end 288 | i = i + 1 289 | end 290 | return flags,_args 291 | end 292 | 293 | return app 294 | -------------------------------------------------------------------------------- /mod_push_appserver/pl/array2d.lua: -------------------------------------------------------------------------------- 1 | --- Operations on two-dimensional arrays. 2 | -- See @{02-arrays.md.Operations_on_two_dimensional_tables|The Guide} 3 | -- 4 | -- Dependencies: `pl.utils`, `pl.tablex`, `pl.types` 5 | -- @module pl.array2d 6 | 7 | local tonumber,assert,tostring,io,ipairs,string,table = 8 | _G.tonumber,_G.assert,_G.tostring,_G.io,_G.ipairs,_G.string,_G.table 9 | local setmetatable,getmetatable = setmetatable,getmetatable 10 | 11 | local tablex = require 'pl.tablex' 12 | local utils = require 'pl.utils' 13 | local types = require 'pl.types' 14 | local imap,tmap,reduce,keys,tmap2,tset,index_by = tablex.imap,tablex.map,tablex.reduce,tablex.keys,tablex.map2,tablex.set,tablex.index_by 15 | local remove = table.remove 16 | local splitv,fprintf,assert_arg = utils.splitv,utils.fprintf,utils.assert_arg 17 | local byte = string.byte 18 | local stdout = io.stdout 19 | local min = math.min 20 | 21 | 22 | local array2d = {} 23 | 24 | local function obj (int,out) 25 | local mt = getmetatable(int) 26 | if mt then 27 | setmetatable(out,mt) 28 | end 29 | return out 30 | end 31 | 32 | local function makelist (res) 33 | return setmetatable(res, require('pl.List')) 34 | end 35 | 36 | 37 | local function index (t,k) 38 | return t[k] 39 | end 40 | 41 | --- return the row and column size. 42 | -- @array2d t a 2d array 43 | -- @treturn int number of rows 44 | -- @treturn int number of cols 45 | function array2d.size (t) 46 | assert_arg(1,t,'table') 47 | return #t,#t[1] 48 | end 49 | 50 | --- extract a column from the 2D array. 51 | -- @array2d a 2d array 52 | -- @param key an index or key 53 | -- @return 1d array 54 | function array2d.column (a,key) 55 | assert_arg(1,a,'table') 56 | return makelist(imap(index,a,key)) 57 | end 58 | local column = array2d.column 59 | 60 | --- map a function over a 2D array 61 | -- @func f a function of at least one argument 62 | -- @array2d a 2d array 63 | -- @param arg an optional extra argument to be passed to the function. 64 | -- @return 2d array 65 | function array2d.map (f,a,arg) 66 | assert_arg(1,a,'table') 67 | f = utils.function_arg(1,f) 68 | return obj(a,imap(function(row) return imap(f,row,arg) end, a)) 69 | end 70 | 71 | --- reduce the rows using a function. 72 | -- @func f a binary function 73 | -- @array2d a 2d array 74 | -- @return 1d array 75 | -- @see pl.tablex.reduce 76 | function array2d.reduce_rows (f,a) 77 | assert_arg(1,a,'table') 78 | return tmap(function(row) return reduce(f,row) end, a) 79 | end 80 | 81 | --- reduce the columns using a function. 82 | -- @func f a binary function 83 | -- @array2d a 2d array 84 | -- @return 1d array 85 | -- @see pl.tablex.reduce 86 | function array2d.reduce_cols (f,a) 87 | assert_arg(1,a,'table') 88 | return tmap(function(c) return reduce(f,column(a,c)) end, keys(a[1])) 89 | end 90 | 91 | --- reduce a 2D array into a scalar, using two operations. 92 | -- @func opc operation to reduce the final result 93 | -- @func opr operation to reduce the rows 94 | -- @param a 2D array 95 | function array2d.reduce2 (opc,opr,a) 96 | assert_arg(3,a,'table') 97 | local tmp = array2d.reduce_rows(opr,a) 98 | return reduce(opc,tmp) 99 | end 100 | 101 | --- map a function over two arrays. 102 | -- They can be both or either 2D arrays 103 | -- @func f function of at least two arguments 104 | -- @int ad order of first array (1 or 2) 105 | -- @int bd order of second array (1 or 2) 106 | -- @tab a 1d or 2d array 107 | -- @tab b 1d or 2d array 108 | -- @param arg optional extra argument to pass to function 109 | -- @return 2D array, unless both arrays are 1D 110 | function array2d.map2 (f,ad,bd,a,b,arg) 111 | assert_arg(1,a,'table') 112 | assert_arg(2,b,'table') 113 | f = utils.function_arg(1,f) 114 | if ad == 1 and bd == 2 then 115 | return imap(function(row) 116 | return tmap2(f,a,row,arg) 117 | end, b) 118 | elseif ad == 2 and bd == 1 then 119 | return imap(function(row) 120 | return tmap2(f,row,b,arg) 121 | end, a) 122 | elseif ad == 1 and bd == 1 then 123 | return tmap2(f,a,b) 124 | elseif ad == 2 and bd == 2 then 125 | return tmap2(function(rowa,rowb) 126 | return tmap2(f,rowa,rowb,arg) 127 | end, a,b) 128 | end 129 | end 130 | 131 | --- cartesian product of two 1d arrays. 132 | -- @func f a function of 2 arguments 133 | -- @array t1 a 1d table 134 | -- @array t2 a 1d table 135 | -- @return 2d table 136 | -- @usage product('..',{1,2},{'a','b'}) == {{'1a','2a'},{'1b','2b'}} 137 | function array2d.product (f,t1,t2) 138 | f = utils.function_arg(1,f) 139 | assert_arg(2,t1,'table') 140 | assert_arg(3,t2,'table') 141 | local res, map = {}, tablex.map 142 | for i,v in ipairs(t2) do 143 | res[i] = map(f,t1,v) 144 | end 145 | return res 146 | end 147 | 148 | --- flatten a 2D array. 149 | -- (this goes over columns first.) 150 | -- @array2d t 2d table 151 | -- @return a 1d table 152 | -- @usage flatten {{1,2},{3,4},{5,6}} == {1,2,3,4,5,6} 153 | function array2d.flatten (t) 154 | local res = {} 155 | local k = 1 156 | for _,a in ipairs(t) do -- for all rows 157 | for i = 1,#a do 158 | res[k] = a[i] 159 | k = k + 1 160 | end 161 | end 162 | return makelist(res) 163 | end 164 | 165 | --- reshape a 2D array. 166 | -- @array2d t 2d array 167 | -- @int nrows new number of rows 168 | -- @bool co column-order (Fortran-style) (default false) 169 | -- @return a new 2d array 170 | function array2d.reshape (t,nrows,co) 171 | local nr,nc = array2d.size(t) 172 | local ncols = nr*nc / nrows 173 | local res = {} 174 | local ir,ic = 1,1 175 | for i = 1,nrows do 176 | local row = {} 177 | for j = 1,ncols do 178 | row[j] = t[ir][ic] 179 | if not co then 180 | ic = ic + 1 181 | if ic > nc then 182 | ir = ir + 1 183 | ic = 1 184 | end 185 | else 186 | ir = ir + 1 187 | if ir > nr then 188 | ic = ic + 1 189 | ir = 1 190 | end 191 | end 192 | end 193 | res[i] = row 194 | end 195 | return obj(t,res) 196 | end 197 | 198 | --- swap two rows of an array. 199 | -- @array2d t a 2d array 200 | -- @int i1 a row index 201 | -- @int i2 a row index 202 | function array2d.swap_rows (t,i1,i2) 203 | assert_arg(1,t,'table') 204 | t[i1],t[i2] = t[i2],t[i1] 205 | end 206 | 207 | --- swap two columns of an array. 208 | -- @array2d t a 2d array 209 | -- @int j1 a column index 210 | -- @int j2 a column index 211 | function array2d.swap_cols (t,j1,j2) 212 | assert_arg(1,t,'table') 213 | for i = 1,#t do 214 | local row = t[i] 215 | row[j1],row[j2] = row[j2],row[j1] 216 | end 217 | end 218 | 219 | --- extract the specified rows. 220 | -- @array2d t 2d array 221 | -- @tparam {int} ridx a table of row indices 222 | function array2d.extract_rows (t,ridx) 223 | return obj(t,index_by(t,ridx)) 224 | end 225 | 226 | --- extract the specified columns. 227 | -- @array2d t 2d array 228 | -- @tparam {int} cidx a table of column indices 229 | function array2d.extract_cols (t,cidx) 230 | assert_arg(1,t,'table') 231 | local res = {} 232 | for i = 1,#t do 233 | res[i] = index_by(t[i],cidx) 234 | end 235 | return obj(t,res) 236 | end 237 | 238 | --- remove a row from an array. 239 | -- @function array2d.remove_row 240 | -- @array2d t a 2d array 241 | -- @int i a row index 242 | array2d.remove_row = remove 243 | 244 | --- remove a column from an array. 245 | -- @array2d t a 2d array 246 | -- @int j a column index 247 | function array2d.remove_col (t,j) 248 | assert_arg(1,t,'table') 249 | for i = 1,#t do 250 | remove(t[i],j) 251 | end 252 | end 253 | 254 | local function _parse (s) 255 | local c,r 256 | if s:sub(1,1) == 'R' then 257 | r,c = s:match 'R(%d+)C(%d+)' 258 | r,c = tonumber(r),tonumber(c) 259 | else 260 | c,r = s:match '(.)(.)' 261 | c = byte(c) - byte 'A' + 1 262 | r = tonumber(r) 263 | end 264 | assert(c ~= nil and r ~= nil,'bad cell specifier: '..s) 265 | return r,c 266 | end 267 | 268 | --- parse a spreadsheet range. 269 | -- The range can be specified either as 'A1:B2' or 'R1C1:R2C2'; 270 | -- a special case is a single element (e.g 'A1' or 'R1C1') 271 | -- @string s a range. 272 | -- @treturn int start col 273 | -- @treturn int start row 274 | -- @treturn int end col 275 | -- @treturn int end row 276 | function array2d.parse_range (s) 277 | if s:find ':' then 278 | local start,finish = splitv(s,':') 279 | local i1,j1 = _parse(start) 280 | local i2,j2 = _parse(finish) 281 | return i1,j1,i2,j2 282 | else -- single value 283 | local i,j = _parse(s) 284 | return i,j 285 | end 286 | end 287 | 288 | --- get a slice of a 2D array using spreadsheet range notation. @see parse_range 289 | -- @array2d t a 2D array 290 | -- @string rstr range expression 291 | -- @return a slice 292 | -- @see array2d.parse_range 293 | -- @see array2d.slice 294 | function array2d.range (t,rstr) 295 | assert_arg(1,t,'table') 296 | local i1,j1,i2,j2 = array2d.parse_range(rstr) 297 | if i2 then 298 | return array2d.slice(t,i1,j1,i2,j2) 299 | else -- single value 300 | return t[i1][j1] 301 | end 302 | end 303 | 304 | local function default_range (t,i1,j1,i2,j2) 305 | local nr, nc = array2d.size(t) 306 | i1,j1 = i1 or 1, j1 or 1 307 | i2,j2 = i2 or nr, j2 or nc 308 | if i2 < 0 then i2 = nr + i2 + 1 end 309 | if j2 < 0 then j2 = nc + j2 + 1 end 310 | return i1,j1,i2,j2 311 | end 312 | 313 | --- get a slice of a 2D array. Note that if the specified range has 314 | -- a 1D result, the rank of the result will be 1. 315 | -- @array2d t a 2D array 316 | -- @int i1 start row (default 1) 317 | -- @int j1 start col (default 1) 318 | -- @int i2 end row (default N) 319 | -- @int j2 end col (default M) 320 | -- @return an array, 2D in general but 1D in special cases. 321 | function array2d.slice (t,i1,j1,i2,j2) 322 | assert_arg(1,t,'table') 323 | i1,j1,i2,j2 = default_range(t,i1,j1,i2,j2) 324 | local res = {} 325 | for i = i1,i2 do 326 | local val 327 | local row = t[i] 328 | if j1 == j2 then 329 | val = row[j1] 330 | else 331 | val = {} 332 | for j = j1,j2 do 333 | val[#val+1] = row[j] 334 | end 335 | end 336 | res[#res+1] = val 337 | end 338 | if i1 == i2 then res = res[1] end 339 | return obj(t,res) 340 | end 341 | 342 | --- set a specified range of an array to a value. 343 | -- @array2d t a 2D array 344 | -- @param value the value (may be a function) 345 | -- @int i1 start row (default 1) 346 | -- @int j1 start col (default 1) 347 | -- @int i2 end row (default N) 348 | -- @int j2 end col (default M) 349 | -- @see tablex.set 350 | function array2d.set (t,value,i1,j1,i2,j2) 351 | i1,j1,i2,j2 = default_range(t,i1,j1,i2,j2) 352 | for i = i1,i2 do 353 | tset(t[i],value,j1,j2) 354 | end 355 | end 356 | 357 | --- write a 2D array to a file. 358 | -- @array2d t a 2D array 359 | -- @param f a file object (default stdout) 360 | -- @string fmt a format string (default is just to use tostring) 361 | -- @int i1 start row (default 1) 362 | -- @int j1 start col (default 1) 363 | -- @int i2 end row (default N) 364 | -- @int j2 end col (default M) 365 | function array2d.write (t,f,fmt,i1,j1,i2,j2) 366 | assert_arg(1,t,'table') 367 | f = f or stdout 368 | local rowop 369 | if fmt then 370 | rowop = function(row,j) fprintf(f,fmt,row[j]) end 371 | else 372 | rowop = function(row,j) f:write(tostring(row[j]),' ') end 373 | end 374 | local function newline() 375 | f:write '\n' 376 | end 377 | array2d.forall(t,rowop,newline,i1,j1,i2,j2) 378 | end 379 | 380 | --- perform an operation for all values in a 2D array. 381 | -- @array2d t 2D array 382 | -- @func row_op function to call on each value 383 | -- @func end_row_op function to call at end of each row 384 | -- @int i1 start row (default 1) 385 | -- @int j1 start col (default 1) 386 | -- @int i2 end row (default N) 387 | -- @int j2 end col (default M) 388 | function array2d.forall (t,row_op,end_row_op,i1,j1,i2,j2) 389 | assert_arg(1,t,'table') 390 | i1,j1,i2,j2 = default_range(t,i1,j1,i2,j2) 391 | for i = i1,i2 do 392 | local row = t[i] 393 | for j = j1,j2 do 394 | row_op(row,j) 395 | end 396 | if end_row_op then end_row_op(i) end 397 | end 398 | end 399 | 400 | ---- move a block from the destination to the source. 401 | -- @array2d dest a 2D array 402 | -- @int di start row in dest 403 | -- @int dj start col in dest 404 | -- @array2d src a 2D array 405 | -- @int i1 start row (default 1) 406 | -- @int j1 start col (default 1) 407 | -- @int i2 end row (default N) 408 | -- @int j2 end col (default M) 409 | function array2d.move (dest,di,dj,src,i1,j1,i2,j2) 410 | assert_arg(1,dest,'table') 411 | assert_arg(4,src,'table') 412 | i1,j1,i2,j2 = default_range(src,i1,j1,i2,j2) 413 | local nr,nc = array2d.size(dest) 414 | i2, j2 = min(nr,i2), min(nc,j2) 415 | --i1, j1 = max(1,i1), max(1,j1) 416 | dj = dj - 1 417 | for i = i1,i2 do 418 | local drow, srow = dest[i+di-1], src[i] 419 | for j = j1,j2 do 420 | drow[j+dj] = srow[j] 421 | end 422 | end 423 | end 424 | 425 | --- iterate over all elements in a 2D array, with optional indices. 426 | -- @array2d a 2D array 427 | -- @tparam {int} indices with indices (default false) 428 | -- @int i1 start row (default 1) 429 | -- @int j1 start col (default 1) 430 | -- @int i2 end row (default N) 431 | -- @int j2 end col (default M) 432 | -- @return either value or i,j,value depending on indices 433 | function array2d.iter (a,indices,i1,j1,i2,j2) 434 | assert_arg(1,a,'table') 435 | local norowset = not (i2 and j2) 436 | i1,j1,i2,j2 = default_range(a,i1,j1,i2,j2) 437 | local i,j = i1-1,j1-1 438 | local row,nr = nil,0 439 | local onr = j2 - j1 + 1 440 | return function() 441 | j = j + 1 442 | if j > nr then 443 | j = j1 444 | i = i + 1 445 | if i > i2 then return nil end 446 | row = a[i] 447 | nr = norowset and #row or onr 448 | end 449 | if indices then 450 | return i,j,row[j] 451 | else 452 | return row[j] 453 | end 454 | end 455 | end 456 | 457 | --- iterate over all columns. 458 | -- @array2d a a 2D array 459 | -- @return each column in turn 460 | function array2d.columns (a) 461 | assert_arg(1,a,'table') 462 | local n = a[1][1] 463 | local i = 0 464 | return function() 465 | i = i + 1 466 | if i > n then return nil end 467 | return column(a,i) 468 | end 469 | end 470 | 471 | --- new array of specified dimensions 472 | -- @int rows number of rows 473 | -- @int cols number of cols 474 | -- @param val initial value; if it's a function then use `val(i,j)` 475 | -- @return new 2d array 476 | function array2d.new(rows,cols,val) 477 | local res = {} 478 | local fun = types.is_callable(val) 479 | for i = 1,rows do 480 | local row = {} 481 | if fun then 482 | for j = 1,cols do row[j] = val(i,j) end 483 | else 484 | for j = 1,cols do row[j] = val end 485 | end 486 | res[i] = row 487 | end 488 | return res 489 | end 490 | 491 | return array2d 492 | 493 | 494 | -------------------------------------------------------------------------------- /mod_push_appserver/pl/class.lua: -------------------------------------------------------------------------------- 1 | --- Provides a reuseable and convenient framework for creating classes in Lua. 2 | -- Two possible notations: 3 | -- 4 | -- B = class(A) 5 | -- class.B(A) 6 | -- 7 | -- The latter form creates a named class within the current environment. Note 8 | -- that this implicitly brings in `pl.utils` as a dependency. 9 | -- 10 | -- See the Guide for further @{01-introduction.md.Simplifying_Object_Oriented_Programming_in_Lua|discussion} 11 | -- @module pl.class 12 | 13 | local error, getmetatable, io, pairs, rawget, rawset, setmetatable, tostring, type = 14 | _G.error, _G.getmetatable, _G.io, _G.pairs, _G.rawget, _G.rawset, _G.setmetatable, _G.tostring, _G.type 15 | local compat 16 | 17 | -- this trickery is necessary to prevent the inheritance of 'super' and 18 | -- the resulting recursive call problems. 19 | local function call_ctor (c,obj,...) 20 | -- nice alias for the base class ctor 21 | local base = rawget(c,'_base') 22 | if base then 23 | local parent_ctor = rawget(base,'_init') 24 | while not parent_ctor do 25 | base = rawget(base,'_base') 26 | if not base then break end 27 | parent_ctor = rawget(base,'_init') 28 | end 29 | if parent_ctor then 30 | rawset(obj,'super',function(obj,...) 31 | call_ctor(base,obj,...) 32 | end) 33 | end 34 | end 35 | local res = c._init(obj,...) 36 | rawset(obj,'super',nil) 37 | return res 38 | end 39 | 40 | --- initializes an __instance__ upon creation. 41 | -- @function class:_init 42 | -- @param ... parameters passed to the constructor 43 | -- @usage local Cat = class() 44 | -- function Cat:_init(name) 45 | -- --self:super(name) -- call the ancestor initializer if needed 46 | -- self.name = name 47 | -- end 48 | -- 49 | -- local pussycat = Cat("pussycat") 50 | -- print(pussycat.name) --> pussycat 51 | 52 | --- checks whether an __instance__ is derived from some class. 53 | -- Works the other way around as `class_of`. It has two ways of using; 54 | -- 1) call with a class to check against, 2) call without params. 55 | -- @function instance:is_a 56 | -- @param some_class class to check against, or `nil` to return the class 57 | -- @return `true` if `instance` is derived from `some_class`, or if `some_class == nil` then 58 | -- it returns the class table of the instance 59 | -- @usage local pussycat = Lion() -- assuming Lion derives from Cat 60 | -- if pussycat:is_a(Cat) then 61 | -- -- it's true, it is a Lion, but also a Cat 62 | -- end 63 | -- 64 | -- if pussycat:is_a() == Lion then 65 | -- -- It's true 66 | -- end 67 | local function is_a(self,klass) 68 | if klass == nil then 69 | -- no class provided, so return the class this instance is derived from 70 | return getmetatable(self) 71 | end 72 | local m = getmetatable(self) 73 | if not m then return false end --*can't be an object! 74 | while m do 75 | if m == klass then return true end 76 | m = rawget(m,'_base') 77 | end 78 | return false 79 | end 80 | 81 | --- checks whether an __instance__ is derived from some class. 82 | -- Works the other way around as `is_a`. 83 | -- @function some_class:class_of 84 | -- @param some_instance instance to check against 85 | -- @return `true` if `some_instance` is derived from `some_class` 86 | -- @usage local pussycat = Lion() -- assuming Lion derives from Cat 87 | -- if Cat:class_of(pussycat) then 88 | -- -- it's true 89 | -- end 90 | local function class_of(klass,obj) 91 | if type(klass) ~= 'table' or not rawget(klass,'is_a') then return false end 92 | return klass.is_a(obj,klass) 93 | end 94 | 95 | --- cast an object to another class. 96 | -- It is not clever (or safe!) so use carefully. 97 | -- @param some_instance the object to be changed 98 | -- @function some_class:cast 99 | local function cast (klass, obj) 100 | return setmetatable(obj,klass) 101 | end 102 | 103 | 104 | local function _class_tostring (obj) 105 | local mt = obj._class 106 | local name = rawget(mt,'_name') 107 | setmetatable(obj,nil) 108 | local str = tostring(obj) 109 | setmetatable(obj,mt) 110 | if name then str = name ..str:gsub('table','') end 111 | return str 112 | end 113 | 114 | local function tupdate(td,ts,dont_override) 115 | for k,v in pairs(ts) do 116 | if not dont_override or td[k] == nil then 117 | td[k] = v 118 | end 119 | end 120 | end 121 | 122 | local function _class(base,c_arg,c) 123 | -- the class `c` will be the metatable for all its objects, 124 | -- and they will look up their methods in it. 125 | local mt = {} -- a metatable for the class to support __call and _handler 126 | -- can define class by passing it a plain table of methods 127 | local plain = type(base) == 'table' and not getmetatable(base) 128 | if plain then 129 | c = base 130 | base = c._base 131 | else 132 | c = c or {} 133 | end 134 | 135 | if type(base) == 'table' then 136 | -- our new class is a shallow copy of the base class! 137 | -- but be careful not to wipe out any methods we have been given at this point! 138 | tupdate(c,base,plain) 139 | c._base = base 140 | -- inherit the 'not found' handler, if present 141 | if rawget(c,'_handler') then mt.__index = c._handler end 142 | elseif base ~= nil then 143 | error("must derive from a table type",3) 144 | end 145 | 146 | c.__index = c 147 | setmetatable(c,mt) 148 | if not plain then 149 | c._init = nil 150 | end 151 | 152 | if base and rawget(base,'_class_init') then 153 | base._class_init(c,c_arg) 154 | end 155 | 156 | -- expose a ctor which can be called by () 157 | mt.__call = function(class_tbl,...) 158 | local obj 159 | if rawget(c,'_create') then obj = c._create(...) end 160 | if not obj then obj = {} end 161 | setmetatable(obj,c) 162 | 163 | if rawget(c,'_init') then -- explicit constructor 164 | local res = call_ctor(c,obj,...) 165 | if res then -- _if_ a ctor returns a value, it becomes the object... 166 | obj = res 167 | setmetatable(obj,c) 168 | end 169 | elseif base and rawget(base,'_init') then -- default constructor 170 | -- make sure that any stuff from the base class is initialized! 171 | call_ctor(base,obj,...) 172 | end 173 | 174 | if base and rawget(base,'_post_init') then 175 | base._post_init(obj) 176 | end 177 | 178 | return obj 179 | end 180 | -- Call Class.catch to set a handler for methods/properties not found in the class! 181 | c.catch = function(self, handler) 182 | if type(self) == "function" then 183 | -- called using . instead of : 184 | handler = self 185 | end 186 | c._handler = handler 187 | mt.__index = handler 188 | end 189 | c.is_a = is_a 190 | c.class_of = class_of 191 | c.cast = cast 192 | c._class = c 193 | 194 | if not rawget(c,'__tostring') then 195 | c.__tostring = _class_tostring 196 | end 197 | 198 | return c 199 | end 200 | 201 | --- create a new class, derived from a given base class. 202 | -- Supporting two class creation syntaxes: 203 | -- either `Name = class(base)` or `class.Name(base)`. 204 | -- The first form returns the class directly and does not set its `_name`. 205 | -- The second form creates a variable `Name` in the current environment set 206 | -- to the class, and also sets `_name`. 207 | -- @function class 208 | -- @param base optional base class 209 | -- @param c_arg optional parameter to class constructor 210 | -- @param c optional table to be used as class 211 | local class 212 | class = setmetatable({},{ 213 | __call = function(fun,...) 214 | return _class(...) 215 | end, 216 | __index = function(tbl,key) 217 | if key == 'class' then 218 | io.stderr:write('require("pl.class").class is deprecated. Use require("pl.class")\n') 219 | return class 220 | end 221 | compat = compat or require 'pl.compat' 222 | local env = compat.getfenv(2) 223 | return function(...) 224 | local c = _class(...) 225 | c._name = key 226 | rawset(env,key,c) 227 | return c 228 | end 229 | end 230 | }) 231 | 232 | class.properties = class() 233 | 234 | function class.properties._class_init(klass) 235 | klass.__index = function(t,key) 236 | -- normal class lookup! 237 | local v = klass[key] 238 | if v then return v end 239 | -- is it a getter? 240 | v = rawget(klass,'get_'..key) 241 | if v then 242 | return v(t) 243 | end 244 | -- is it a field? 245 | return rawget(t,'_'..key) 246 | end 247 | klass.__newindex = function (t,key,value) 248 | -- if there's a setter, use that, otherwise directly set table 249 | local p = 'set_'..key 250 | local setter = klass[p] 251 | if setter then 252 | setter(t,value) 253 | else 254 | rawset(t,key,value) 255 | end 256 | end 257 | end 258 | 259 | 260 | return class 261 | 262 | -------------------------------------------------------------------------------- /mod_push_appserver/pl/compat.lua: -------------------------------------------------------------------------------- 1 | ---------------- 2 | --- Lua 5.1/5.2/5.3 compatibility. 3 | -- Injects `table.pack`, `table.unpack`, and `package.searchpath` in the global 4 | -- environment, to make sure they are available for Lua 5.1 and LuaJIT. 5 | -- 6 | -- All other functions are exported as usual in the returned module table. 7 | -- 8 | -- NOTE: everything in this module is also available in `pl.utils`. 9 | -- @module pl.compat 10 | local compat = {} 11 | 12 | --- boolean flag this is Lua 5.1 (or LuaJIT). 13 | -- @field lua51 14 | compat.lua51 = _VERSION == 'Lua 5.1' 15 | 16 | --- boolean flag this is LuaJIT. 17 | -- @field jit 18 | compat.jit = (tostring(assert):match('builtin') ~= nil) 19 | 20 | --- boolean flag this is LuaJIT with 5.2 compatibility compiled in. 21 | -- @field jit52 22 | if compat.jit then 23 | -- 'goto' is a keyword when 52 compatibility is enabled in LuaJit 24 | compat.jit52 = not loadstring("local goto = 1") 25 | end 26 | 27 | --- the directory separator character for the current platform. 28 | -- @field dir_separator 29 | compat.dir_separator = _G.package.config:sub(1,1) 30 | 31 | --- boolean flag this is a Windows platform. 32 | -- @field is_windows 33 | compat.is_windows = compat.dir_separator == '\\' 34 | 35 | --- execute a shell command, in a compatible and platform independent way. 36 | -- This is a compatibility function that returns the same for Lua 5.1 and 37 | -- Lua 5.2+. 38 | -- 39 | -- NOTE: Windows systems can use signed 32bit integer exitcodes. Posix systems 40 | -- only use exitcodes 0-255, anything else is undefined. 41 | -- @param cmd a shell command 42 | -- @return true if successful 43 | -- @return actual return code 44 | function compat.execute(cmd) 45 | local res1,res2,res3 = os.execute(cmd) 46 | if res2 == "No error" and res3 == 0 and compat.is_windows then 47 | -- os.execute bug in Lua 5.2+ not reporting -1 properly on Windows 48 | res3 = -1 49 | end 50 | if compat.lua51 and not compat.jit52 then 51 | if compat.is_windows then 52 | return res1==0,res1 53 | else 54 | res1 = res1 > 255 and res1 / 256 or res1 55 | return res1==0,res1 56 | end 57 | else 58 | if compat.is_windows then 59 | return res3==0,res3 60 | else 61 | return not not res1,res3 62 | end 63 | end 64 | end 65 | 66 | ---------------- 67 | -- Load Lua code as a text or binary chunk (in a Lua 5.2 compatible way). 68 | -- @param ld code string or loader 69 | -- @param[opt] source name of chunk for errors 70 | -- @param[opt] mode 'b', 't' or 'bt' 71 | -- @param[opt] env environment to load the chunk in 72 | -- @function compat.load 73 | 74 | --------------- 75 | -- Get environment of a function (in a Lua 5.1 compatible way). 76 | -- Not 100% compatible, so with Lua 5.2 it may return nil for a function with no 77 | -- global references! 78 | -- Based on code by [Sergey Rozhenko](http://lua-users.org/lists/lua-l/2010-06/msg00313.html) 79 | -- @param f a function or a call stack reference 80 | -- @function compat.getfenv 81 | 82 | --------------- 83 | -- Set environment of a function (in a Lua 5.1 compatible way). 84 | -- @param f a function or a call stack reference 85 | -- @param env a table that becomes the new environment of `f` 86 | -- @function compat.setfenv 87 | 88 | if compat.lua51 then -- define Lua 5.2 style load() 89 | if not compat.jit then -- but LuaJIT's load _is_ compatible 90 | local lua51_load = load 91 | function compat.load(str,src,mode,env) 92 | local chunk,err 93 | if type(str) == 'string' then 94 | if str:byte(1) == 27 and not (mode or 'bt'):find 'b' then 95 | return nil,"attempt to load a binary chunk" 96 | end 97 | chunk,err = loadstring(str,src) 98 | else 99 | chunk,err = lua51_load(str,src) 100 | end 101 | if chunk and env then setfenv(chunk,env) end 102 | return chunk,err 103 | end 104 | else 105 | compat.load = load 106 | end 107 | compat.setfenv, compat.getfenv = setfenv, getfenv 108 | else 109 | compat.load = load 110 | -- setfenv/getfenv replacements for Lua 5.2 111 | -- by Sergey Rozhenko 112 | -- http://lua-users.org/lists/lua-l/2010-06/msg00313.html 113 | -- Roberto Ierusalimschy notes that it is possible for getfenv to return nil 114 | -- in the case of a function with no globals: 115 | -- http://lua-users.org/lists/lua-l/2010-06/msg00315.html 116 | function compat.setfenv(f, t) 117 | f = (type(f) == 'function' and f or debug.getinfo(f + 1, 'f').func) 118 | local name 119 | local up = 0 120 | repeat 121 | up = up + 1 122 | name = debug.getupvalue(f, up) 123 | until name == '_ENV' or name == nil 124 | if name then 125 | debug.upvaluejoin(f, up, function() return name end, 1) -- use unique upvalue 126 | debug.setupvalue(f, up, t) 127 | end 128 | if f ~= 0 then return f end 129 | end 130 | 131 | function compat.getfenv(f) 132 | local f = f or 0 133 | f = (type(f) == 'function' and f or debug.getinfo(f + 1, 'f').func) 134 | local name, val 135 | local up = 0 136 | repeat 137 | up = up + 1 138 | name, val = debug.getupvalue(f, up) 139 | until name == '_ENV' or name == nil 140 | return val 141 | end 142 | end 143 | 144 | --- Global exported functions (for Lua 5.1 & LuaJIT) 145 | -- @section lua52 146 | 147 | --- pack an argument list into a table. 148 | -- @param ... any arguments 149 | -- @return a table with field n set to the length 150 | -- @function table.pack 151 | if not table.pack then 152 | function table.pack (...) -- luacheck: ignore 153 | return {n=select('#',...); ...} 154 | end 155 | end 156 | 157 | --- unpack a table and return the elements. 158 | -- 159 | -- NOTE: this version does NOT honor the n field, and hence it is not nil-safe. 160 | -- See `utils.unpack` for a version that is nil-safe. 161 | -- @param t table to unpack 162 | -- @param[opt] i index from which to start unpacking, defaults to 1 163 | -- @param[opt] t index of the last element to unpack, defaults to #t 164 | -- @return multiple return values from the table 165 | -- @function table.unpack 166 | -- @see utils.unpack 167 | if not table.unpack then 168 | table.unpack = unpack -- luacheck: ignore 169 | end 170 | 171 | --- return the full path where a Lua module name would be matched. 172 | -- @param mod module name, possibly dotted 173 | -- @param path a path in the same form as package.path or package.cpath 174 | -- @see path.package_path 175 | -- @function package.searchpath 176 | if not package.searchpath then 177 | local sep = package.config:sub(1,1) 178 | function package.searchpath (mod,path) -- luacheck: ignore 179 | mod = mod:gsub('%.',sep) 180 | for m in path:gmatch('[^;]+') do 181 | local nm = m:gsub('?',mod) 182 | local f = io.open(nm,'r') 183 | if f then f:close(); return nm end 184 | end 185 | end 186 | end 187 | 188 | return compat 189 | -------------------------------------------------------------------------------- /mod_push_appserver/pl/comprehension.lua: -------------------------------------------------------------------------------- 1 | --- List comprehensions implemented in Lua. 2 | -- 3 | -- See the [wiki page](http://lua-users.org/wiki/ListComprehensions) 4 | -- 5 | -- local C= require 'pl.comprehension' . new() 6 | -- 7 | -- C ('x for x=1,10') () 8 | -- ==> {1,2,3,4,5,6,7,8,9,10} 9 | -- C 'x^2 for x=1,4' () 10 | -- ==> {1,4,9,16} 11 | -- C '{x,x^2} for x=1,4' () 12 | -- ==> {{1,1},{2,4},{3,9},{4,16}} 13 | -- C '2*x for x' {1,2,3} 14 | -- ==> {2,4,6} 15 | -- dbl = C '2*x for x' 16 | -- dbl {10,20,30} 17 | -- ==> {20,40,60} 18 | -- C 'x for x if x % 2 == 0' {1,2,3,4,5} 19 | -- ==> {2,4} 20 | -- C '{x,y} for x = 1,2 for y = 1,2' () 21 | -- ==> {{1,1},{1,2},{2,1},{2,2}} 22 | -- C '{x,y} for x for y' ({1,2},{10,20}) 23 | -- ==> {{1,10},{1,20},{2,10},{2,20}} 24 | -- assert(C 'sum(x^2 for x)' {2,3,4} == 2^2+3^2+4^2) 25 | -- 26 | -- (c) 2008 David Manura. Licensed under the same terms as Lua (MIT license). 27 | -- 28 | -- Dependencies: `pl.utils`, `pl.luabalanced` 29 | -- 30 | -- See @{07-functional.md.List_Comprehensions|the Guide} 31 | -- @module pl.comprehension 32 | 33 | local utils = require 'pl.utils' 34 | 35 | local status,lb = pcall(require, "pl.luabalanced") 36 | if not status then 37 | lb = require 'luabalanced' 38 | end 39 | 40 | local math_max = math.max 41 | local table_concat = table.concat 42 | 43 | -- fold operations 44 | -- http://en.wikipedia.org/wiki/Fold_(higher-order_function) 45 | local ops = { 46 | list = {init=' {} ', accum=' __result[#__result+1] = (%s) '}, 47 | table = {init=' {} ', accum=' local __k, __v = %s __result[__k] = __v '}, 48 | sum = {init=' 0 ', accum=' __result = __result + (%s) '}, 49 | min = {init=' nil ', accum=' local __tmp = %s ' .. 50 | ' if __result then if __tmp < __result then ' .. 51 | '__result = __tmp end else __result = __tmp end '}, 52 | max = {init=' nil ', accum=' local __tmp = %s ' .. 53 | ' if __result then if __tmp > __result then ' .. 54 | '__result = __tmp end else __result = __tmp end '}, 55 | } 56 | 57 | 58 | -- Parses comprehension string expr. 59 | -- Returns output expression list string, array of for types 60 | -- ('=', 'in' or nil) , array of input variable name 61 | -- strings , array of input variable value strings 62 | -- , array of predicate expression strings , 63 | -- operation name string , and number of placeholder 64 | -- parameters . 65 | -- 66 | -- The is equivalent to the mathematical set-builder notation: 67 | -- 68 | -- { | in , } 69 | -- 70 | -- @usage "x^2 for x" -- array values 71 | -- @usage "x^2 for x=1,10,2" -- numeric for 72 | -- @usage "k^v for k,v in pairs(_1)" -- iterator for 73 | -- @usage "(x+y)^2 for x for y if x > y" -- nested 74 | -- 75 | local function parse_comprehension(expr) 76 | local pos = 1 77 | 78 | -- extract opname (if exists) 79 | local opname 80 | local tok, post = expr:match('^%s*([%a_][%w_]*)%s*%(()', pos) 81 | local pose = #expr + 1 82 | if tok then 83 | local tok2, posb = lb.match_bracketed(expr, post-1) 84 | assert(tok2, 'syntax error') 85 | if expr:match('^%s*$', posb) then 86 | opname = tok 87 | pose = posb - 1 88 | pos = post 89 | end 90 | end 91 | opname = opname or "list" 92 | 93 | -- extract out expression list 94 | local out; out, pos = lb.match_explist(expr, pos) 95 | assert(out, "syntax error: missing expression list") 96 | out = table_concat(out, ', ') 97 | 98 | -- extract "for" clauses 99 | local fortypes = {} 100 | local invarlists = {} 101 | local invallists = {} 102 | while 1 do 103 | local post = expr:match('^%s*for%s+()', pos) 104 | if not post then break end 105 | pos = post 106 | 107 | -- extract input vars 108 | local iv; iv, pos = lb.match_namelist(expr, pos) 109 | assert(#iv > 0, 'syntax error: zero variables') 110 | for _,ident in ipairs(iv) do 111 | assert(not ident:match'^__', 112 | "identifier " .. ident .. " may not contain __ prefix") 113 | end 114 | invarlists[#invarlists+1] = iv 115 | 116 | -- extract '=' or 'in' (optional) 117 | local fortype, post = expr:match('^(=)%s*()', pos) 118 | if not fortype then fortype, post = expr:match('^(in)%s+()', pos) end 119 | if fortype then 120 | pos = post 121 | -- extract input value range 122 | local il; il, pos = lb.match_explist(expr, pos) 123 | assert(#il > 0, 'syntax error: zero expressions') 124 | assert(fortype ~= '=' or #il == 2 or #il == 3, 125 | 'syntax error: numeric for requires 2 or three expressions') 126 | fortypes[#invarlists] = fortype 127 | invallists[#invarlists] = il 128 | else 129 | fortypes[#invarlists] = false 130 | invallists[#invarlists] = false 131 | end 132 | end 133 | assert(#invarlists > 0, 'syntax error: missing "for" clause') 134 | 135 | -- extract "if" clauses 136 | local preds = {} 137 | while 1 do 138 | local post = expr:match('^%s*if%s+()', pos) 139 | if not post then break end 140 | pos = post 141 | local pred; pred, pos = lb.match_expression(expr, pos) 142 | assert(pred, 'syntax error: predicated expression not found') 143 | preds[#preds+1] = pred 144 | end 145 | 146 | -- extract number of parameter variables (name matching "_%d+") 147 | local stmp = ''; lb.gsub(expr, function(u, sin) -- strip comments/strings 148 | if u == 'e' then stmp = stmp .. ' ' .. sin .. ' ' end 149 | end) 150 | local max_param = 0; stmp:gsub('[%a_][%w_]*', function(s) 151 | local s = s:match('^_(%d+)$') 152 | if s then max_param = math_max(max_param, tonumber(s)) end 153 | end) 154 | 155 | if pos ~= pose then 156 | assert(false, "syntax error: unrecognized " .. expr:sub(pos)) 157 | end 158 | 159 | --DEBUG: 160 | --print('----\n', string.format("%q", expr), string.format("%q", out), opname) 161 | --for k,v in ipairs(invarlists) do print(k,v, invallists[k]) end 162 | --for k,v in ipairs(preds) do print(k,v) end 163 | 164 | return out, fortypes, invarlists, invallists, preds, opname, max_param 165 | end 166 | 167 | 168 | -- Create Lua code string representing comprehension. 169 | -- Arguments are in the form returned by parse_comprehension. 170 | local function code_comprehension( 171 | out, fortypes, invarlists, invallists, preds, opname, max_param 172 | ) 173 | local op = assert(ops[opname]) 174 | local code = op.accum:gsub('%%s', out) 175 | 176 | for i=#preds,1,-1 do local pred = preds[i] 177 | code = ' if ' .. pred .. ' then ' .. code .. ' end ' 178 | end 179 | for i=#invarlists,1,-1 do 180 | if not fortypes[i] then 181 | local arrayname = '__in' .. i 182 | local idx = '__idx' .. i 183 | code = 184 | ' for ' .. idx .. ' = 1, #' .. arrayname .. ' do ' .. 185 | ' local ' .. invarlists[i][1] .. ' = ' .. arrayname .. '['..idx..'] ' .. 186 | code .. ' end ' 187 | else 188 | code = 189 | ' for ' .. 190 | table_concat(invarlists[i], ', ') .. 191 | ' ' .. fortypes[i] .. ' ' .. 192 | table_concat(invallists[i], ', ') .. 193 | ' do ' .. code .. ' end ' 194 | end 195 | end 196 | code = ' local __result = ( ' .. op.init .. ' ) ' .. code 197 | return code 198 | end 199 | 200 | 201 | -- Convert code string represented by code_comprehension 202 | -- into Lua function. Also must pass ninputs = #invarlists, 203 | -- max_param, and invallists (from parse_comprehension). 204 | -- Uses environment env. 205 | local function wrap_comprehension(code, ninputs, max_param, invallists, env) 206 | assert(ninputs > 0) 207 | local ts = {} 208 | for i=1,max_param do 209 | ts[#ts+1] = '_' .. i 210 | end 211 | for i=1,ninputs do 212 | if not invallists[i] then 213 | local name = '__in' .. i 214 | ts[#ts+1] = name 215 | end 216 | end 217 | if #ts > 0 then 218 | code = ' local ' .. table_concat(ts, ', ') .. ' = ... ' .. code 219 | end 220 | code = code .. ' return __result ' 221 | --print('DEBUG:', code) 222 | local f, err = utils.load(code,'tmp','t',env) 223 | if not f then assert(false, err .. ' with generated code ' .. code) end 224 | return f 225 | end 226 | 227 | 228 | -- Build Lua function from comprehension string. 229 | -- Uses environment env. 230 | local function build_comprehension(expr, env) 231 | local out, fortypes, invarlists, invallists, preds, opname, max_param 232 | = parse_comprehension(expr) 233 | local code = code_comprehension( 234 | out, fortypes, invarlists, invallists, preds, opname, max_param) 235 | local f = wrap_comprehension(code, #invarlists, max_param, invallists, env) 236 | return f 237 | end 238 | 239 | 240 | -- Creates new comprehension cache. 241 | -- Any list comprehension function created are set to the environment 242 | -- env (defaults to caller of new). 243 | local function new(env) 244 | -- Note: using a single global comprehension cache would have had 245 | -- security implications (e.g. retrieving cached functions created 246 | -- in other environments). 247 | -- The cache lookup function could have instead been written to retrieve 248 | -- the caller's environment, lookup up the cache private to that 249 | -- environment, and then looked up the function in that cache. 250 | -- That would avoid the need for this 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 | --------------------------------------------------------------------------------