Auth Redux is a drop-in replacement for the builtin authentication handler of Minetest.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

filter.lua 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552
  1. --------------------------------------------------------
  2. -- Minetest :: Auth Redux Mod v2.10 (auth_rx)
  3. --
  4. -- See README.txt for licensing and release notes.
  5. -- Copyright (c) 2017-2018, Leslie E. Krause
  6. --------------------------------------------------------
  7. FILTER_TYPE_STRING = 10
  8. FILTER_TYPE_NUMBER = 11
  9. FILTER_TYPE_ADDRESS = 12
  10. FILTER_TYPE_BOOLEAN = 13
  11. FILTER_TYPE_PATTERN = 14
  12. FILTER_TYPE_SERIES = 15
  13. FILTER_TYPE_PERIOD = 16
  14. FILTER_TYPE_MOMENT = 17
  15. FILTER_TYPE_DATESPEC = 18
  16. FILTER_TYPE_TIMESPEC = 19
  17. FILTER_MODE_FAIL = 20
  18. FILTER_MODE_PASS = 21
  19. FILTER_BOOL_AND = 30
  20. FILTER_BOOL_OR = 31
  21. FILTER_BOOL_XOR = 32
  22. FILTER_BOOL_NOW = 33
  23. FILTER_COND_FALSE = 40
  24. FILTER_COND_TRUE = 41
  25. FILTER_COMP_EQ = 50
  26. FILTER_COMP_GT = 51
  27. FILTER_COMP_GTE = 52
  28. FILTER_COMP_LT = 53
  29. FILTER_COMP_LTE = 54
  30. FILTER_COMP_IS = 55
  31. local decode_base64 = minetest.decode_base64
  32. local encode_base64 = minetest.encode_base64
  33. local trim = function ( str )
  34. return string.sub( str, 2, -2 )
  35. end
  36. local localtime = function ( str )
  37. -- daylight saving time is factored in automatically
  38. local x = { string.match( str, "^(%d+)%-(%d+)%-(%d+)T(%d+):(%d+):(%d+)Z$" ) }
  39. return #x > 0 and os.time( { year = x[ 1 ], month = x[ 2 ], day = x[ 3 ], hour = x[ 4 ], min = x[ 5 ], sec = x[ 6 ] } ) or nil
  40. end
  41. local redate = function ( ts )
  42. -- convert to standard time (for timespec and datespec comparisons)
  43. local x = os.date( "*t", ts )
  44. x.isdst = false
  45. return os.time( x )
  46. end
  47. ----------------------------
  48. -- StringPattern class
  49. ----------------------------
  50. function StringPattern( phrase, is_mode, tokens )
  51. local glob = "^" .. string.gsub( phrase, ".", tokens ) .. "$"
  52. return { compare = function ( value, type )
  53. if not is_mode[ type ] then return end
  54. return string.find( value, glob ) == 1
  55. end }
  56. end
  57. ----------------------------
  58. -- NumberPattern class
  59. ----------------------------
  60. function NumberPattern( phrase, is_mode, tokens, parser )
  61. local glob = { }
  62. local ref
  63. local find_token = function ( str, pat )
  64. ref = { string.match( str, pat ) }
  65. return #ref > 0
  66. end
  67. if #phrase ~= #tokens then
  68. return nil
  69. end
  70. for i, v in ipairs( phrase ) do
  71. local eval, args
  72. local t = tokens[ i ]
  73. if find_token( v, "^(" .. t .. ")$" ) then
  74. eval = function ( a, b ) return a == b end
  75. args = { tonumber( ref[ 1 ] ) }
  76. elseif find_token( v, "^(" .. t .. ")%^(" .. t .. ")$" ) then
  77. eval = function ( a, b, c ) return a >= b and a <= c end
  78. args = { tonumber( ref[ 1 ] ), tonumber( ref[ 2 ] ) }
  79. elseif find_token( v, "^(" .. t .. ")([<>])$" ) then
  80. eval = ref[ 2 ] == "<" and
  81. ( function ( a, b ) return a <= b end ) or
  82. ( function ( a, b ) return a >= b end )
  83. args = { tonumber( ref[ 1 ] ) }
  84. elseif v == "?" then
  85. eval = function ( ) return true end
  86. args = { }
  87. else
  88. return nil
  89. end
  90. table.insert( glob, { eval = eval, args = args } )
  91. end
  92. return { compare = function ( value, type )
  93. if not is_mode[ type ] then return end
  94. local fields = parser( value, type )
  95. for i, v in ipairs( glob ) do
  96. if not v.eval( fields[ i ], unpack( v.args ) ) then return false end
  97. end
  98. return true
  99. end }
  100. end
  101. ----------------------------
  102. -- AuthFilter class
  103. ----------------------------
  104. function AuthFilter( path, name, debug )
  105. local src
  106. local is_active = true
  107. local self = { }
  108. local funcs = {
  109. ["add"] = { type = FILTER_TYPE_NUMBER, args = { FILTER_TYPE_NUMBER, FILTER_TYPE_NUMBER }, def = function ( v, a, b ) return a + b end },
  110. ["sub"] = { type = FILTER_TYPE_NUMBER, args = { FILTER_TYPE_NUMBER, FILTER_TYPE_NUMBER }, def = function ( v, a, b ) return a - b end },
  111. ["mul"] = { type = FILTER_TYPE_NUMBER, args = { FILTER_TYPE_NUMBER, FILTER_TYPE_NUMBER }, def = function ( v, a, b ) return a * b end },
  112. ["div"] = { type = FILTER_TYPE_NUMBER, args = { FILTER_TYPE_NUMBER, FILTER_TYPE_NUMBER }, def = function ( v, a, b ) return a / b end },
  113. ["neg"] = { type = FILTER_TYPE_NUMBER, args = { FILTER_TYPE_NUMBER }, def = function ( v, a ) return -a end },
  114. ["abs"] = { type = FILTER_TYPE_NUMBER, args = { FILTER_TYPE_NUMBER }, def = function ( v, a ) return math.abs( a ) end },
  115. ["max"] = { type = FILTER_TYPE_NUMBER, args = { FILTER_TYPE_NUMBER, FILTER_TYPE_NUMBER }, def = function ( v, a, b ) return math.max( a, b ) end },
  116. ["min"] = { type = FILTER_TYPE_NUMBER, args = { FILTER_TYPE_NUMBER, FILTER_TYPE_NUMBER }, def = function ( v, a, b ) return math.min( a, b ) end },
  117. ["int"] = { type = FILTER_TYPE_NUMBER, args = { FILTER_TYPE_NUMBER }, def = function ( v, a ) return a < 0 and math.ceil( a ) or math.floor( a ) end },
  118. ["num"] = { type = FILTER_TYPE_NUMBER, args = { FILTER_TYPE_STRING }, def = function ( v, a ) return tonumber( a ) or 0 end },
  119. ["len"] = { type = FILTER_TYPE_NUMBER, args = { FILTER_TYPE_STRING }, def = function ( v, a ) return string.len( a ) end },
  120. ["lc"] = { type = FILTER_TYPE_STRING, args = { FILTER_TYPE_STRING }, def = function ( v, a ) return string.lower( a ) end },
  121. ["uc"] = { type = FILTER_TYPE_STRING, args = { FILTER_TYPE_STRING }, def = function ( v, a ) return string.upper( a ) end },
  122. ["trim"] = { type = FILTER_TYPE_STRING, args = { FILTER_TYPE_STRING, FILTER_TYPE_NUMBER }, def = function ( v, a, b ) return b > 0 and string.sub( a, 1, -b - 1 ) or string.sub( a, -b + 1 ) end },
  123. ["crop"] = { type = FILTER_TYPE_STRING, args = { FILTER_TYPE_STRING, FILTER_TYPE_NUMBER }, def = function ( v, a, b ) return b > 0 and string.sub( a, 1, b ) or string.sub( a, b, -1 ) end },
  124. ["size"] = { type = FILTER_TYPE_NUMBER, args = { FILTER_TYPE_SERIES }, def = function ( v, a ) return #a end },
  125. ["elem"] = { type = FILTER_TYPE_STRING, args = { FILTER_TYPE_SERIES, FILTER_TYPE_NUMBER }, def = function ( v, a, b ) return a[ b > 0 and b or #a + b + 1 ] or "" end },
  126. ["split"] = { type = FILTER_TYPE_SERIES, args = { FILTER_TYPE_STRING, FILTER_TYPE_STRING }, def = function ( v, a, b ) return string.split( a, b, true ) end },
  127. ["time"] = { type = FILTER_TYPE_TIMESPEC, args = { FILTER_TYPE_MOMENT }, def = function ( v, a ) return redate( a - v.epoch.value ) % 86400 end },
  128. ["date"] = { type = FILTER_TYPE_DATESPEC, args = { FILTER_TYPE_MOMENT }, def = function ( v, a ) return math.floor( redate( a - v.epoch.value ) / 86400 ) end },
  129. ["age"] = { type = FILTER_TYPE_PERIOD, args = { FILTER_TYPE_MOMENT }, def = function ( v, a ) return v.clock.value - a end },
  130. ["before"] = { type = FILTER_TYPE_MOMENT, args = { FILTER_TYPE_MOMENT, FILTER_TYPE_PERIOD }, def = function ( v, a, b ) return a - b end },
  131. ["after"] = { type = FILTER_TYPE_MOMENT, args = { FILTER_TYPE_MOMENT, FILTER_TYPE_PERIOD }, def = function ( v, a, b ) return a + b end },
  132. ["day"] = { type = FILTER_TYPE_STRING, args = { FILTER_TYPE_MOMENT }, def = function ( v, a ) return os.date( "%a", a ) end },
  133. ["at"] = { type = FILTER_TYPE_MOMENT, args = { FILTER_TYPE_STRING }, def = function ( v, a ) return localtime( a ) or 0 end },
  134. ["ip"] = { type = FILTER_TYPE_STRING, args = { FILTER_TYPE_ADDRESS }, def = function ( v, a ) return table.concat( unpack_address( a ), "." ) end },
  135. ["count"] = { type = FILTER_TYPE_NUMBER, args = { FILTER_TYPE_SERIES, FILTER_TYPE_STRING }, def = function ( v, a, b ) local t = 0; for i, v in ipairs( a ) do if v == b then t = t + 1; end; end; return t end },
  136. ["clip"] = { type = FILTER_TYPE_SERIES, args = { FILTER_TYPE_SERIES, FILTER_TYPE_NUMBER }, def = function ( v, a, b ) local x = { }; local s = b < 0 and #a + b + 1 or 0; for i = 0, math.abs( b ) do table.insert( x, a[ s + i ] ); end; return x; end },
  137. }
  138. ----------------------------
  139. -- private methods
  140. ----------------------------
  141. local trace, get_operand, evaluate, tokenize
  142. trace = debug or function ( msg, num )
  143. minetest.log( "error", string.format( "%s (%s/%s, line %d)", msg, path, name, num ) )
  144. return num, "The server encountered an internal error."
  145. end
  146. function get_operand( token, vars )
  147. local t, v, ref
  148. local find_token = function ( pat )
  149. -- use back-references for easier conditional branching
  150. ref = { string.match( token, pat ) }
  151. return #ref > 0 and #ref
  152. end
  153. if find_token( "^(.-)([a-zA-Z0-9_]+)&([A-Za-z0-9+/]*);$" ) then
  154. local name = ref[ 2 ]
  155. local suffix = decode_base64( ref[ 3 ] )
  156. local prefix = ref[ 1 ]
  157. suffix = string.gsub( suffix, "%b()", function( str )
  158. -- encode nested function arguments
  159. return "&" .. encode_base64( trim( str ) ) .. ";"
  160. end )
  161. local args = string.split( suffix, ",", false )
  162. if string.match( prefix, "->$" ) then
  163. -- insert prefixed arguments
  164. table.insert( args, 1, string.sub( prefix, 1, -3 ) )
  165. elseif prefix ~= "" then
  166. return nil
  167. end
  168. if not funcs[ name ] or #funcs[ name ].args ~= #args then
  169. return nil
  170. end
  171. local params = { }
  172. for i, a in ipairs( args ) do
  173. local oper = get_operand( a, vars )
  174. if not oper or oper.type ~= funcs[ name ].args[ i ] then
  175. return nil
  176. end
  177. table.insert( params, oper.value )
  178. end
  179. t = funcs[ name ].type
  180. v = funcs[ name ].def( vars, unpack( params ) )
  181. elseif find_token( "^&([A-Za-z0-9+/]*);$" ) then
  182. t = FILTER_TYPE_SERIES
  183. v = { }
  184. local suffix = decode_base64( ref[ 1 ] )
  185. suffix = string.gsub( suffix, "%b()", function( str )
  186. -- encode nested function arguments
  187. return "&" .. encode_base64( trim( str ) ) .. ";"
  188. end )
  189. local elems = string.split( suffix, ",", false )
  190. for i, e in ipairs( elems ) do
  191. local oper = get_operand( e, vars )
  192. if not oper or oper.type ~= FILTER_TYPE_STRING then
  193. return nil
  194. end
  195. table.insert( v, oper.value )
  196. end
  197. elseif find_token( "^%$([a-zA-Z0-9_]+)$" ) then
  198. local name = ref[ 1 ]
  199. if not vars[ name ] or vars[ name ].value == nil then
  200. return nil
  201. end
  202. t = vars[ name ].type
  203. v = vars[ name ].value
  204. elseif find_token( "^@([a-zA-Z0-9_]+%.txt)$" ) then
  205. t = FILTER_TYPE_SERIES
  206. v = { }
  207. local file = io.open( path .. "/filters/" .. ref[ 1 ], "rb" )
  208. if not file then
  209. return nil
  210. end
  211. for line in file:lines( ) do
  212. table.insert( v, line )
  213. end
  214. elseif find_token( "^/([a-zA-Z0-9+/]*),([stda]);$" ) then
  215. t = FILTER_TYPE_PATTERN
  216. local phrase = minetest.decode_base64( ref[ 1 ] )
  217. if ref[ 2 ] == "s" then
  218. v = StringPattern( phrase, { [FILTER_TYPE_STRING] = true }, {
  219. ["["] = "",
  220. ["]"] = "",
  221. ["^"] = "%^",
  222. ["$"] = "%$",
  223. ["("] = "%(",
  224. [")"] = "%)",
  225. ["%"] = "%%",
  226. ["-"] = "%-",
  227. [","] = "[a-z]",
  228. [";"] = "[A-Z]",
  229. ["="] = "[-_]",
  230. ["!"] = "[a-zA-Z0-9]",
  231. ["*"] = "[a-zA-Z0-9_-]*",
  232. ["+"] = "[a-zA-Z0-9_-]+",
  233. ["?"] = "[a-zA-Z0-9_-]",
  234. ["#"] = "%d",
  235. ["&"] = "%a",
  236. } )
  237. elseif ref[ 2 ] == "t" then
  238. phrase = string.split( phrase, ":", false )
  239. v = NumberPattern( phrase, { [FILTER_TYPE_MOMENT] = true }, { "%d?%d", "%d%d", "%d%d" }, function ( value )
  240. -- direct translation (accounts for daylight saving time and time-zone offset)
  241. local timespec = os.date( "*t", value )
  242. return { timespec.hour, timespec.min, timespec.sec }
  243. end )
  244. elseif ref[ 2 ] == "d" then
  245. phrase = string.split( phrase, "-", false )
  246. v = NumberPattern( phrase, { [FILTER_TYPE_MOMENT] = true }, { "%d%d", "%d%d", "%d%d%d%d" }, function ( value )
  247. -- direct translation (accounts for daylight saving time and time-zone offset)
  248. local datespec = os.date( "*t", value )
  249. return { datespec.day, datespec.month, datespec.year }
  250. end )
  251. elseif ref[ 2 ] == "a" then
  252. phrase = string.split( phrase, ".", false )
  253. v = NumberPattern( phrase, { [FILTER_TYPE_ADDRESS] = true }, { "%d?%d?%d", "%d?%d?%d", "%d?%d?%d", "%d?%d?%d" }, function ( value )
  254. return unpack_address( value )
  255. end )
  256. end
  257. if not v then
  258. return nil
  259. end
  260. elseif find_token( "^(%d+)([ywdhms])$" ) then
  261. local factor = { y = 31536000, w = 604800, d = 86400, h = 3600, m = 60, s = 1 }
  262. t = FILTER_TYPE_PERIOD
  263. v = tonumber( ref[ 1 ] ) * factor[ ref[ 2 ] ]
  264. elseif find_token( "^([-+]%d+)([ywdhms])$" ) then
  265. local factor = { y = 31536000, w = 604800, d = 86400, h = 3600, m = 60, s = 1 }
  266. local origin = string.byte( ref[ 1 ] ) == 45 and vars.clock.value or vars.epoch.value
  267. t = FILTER_TYPE_MOMENT
  268. v = origin + tonumber( ref[ 1 ] ) * factor[ ref[ 2 ] ]
  269. elseif find_token( "^(%d?%d):(%d%d):(%d%d)$" ) or find_token( "^(%d?%d):(%d%d)$" ) then
  270. local timespec = {
  271. isdst = false, day = 1, month = 1, year = 1970, hour = tonumber( ref[ 1 ] ), min = tonumber( ref[ 2 ] ), sec = ref[ 3 ] and tonumber( ref[ 3 ] ) or 0,
  272. }
  273. t = FILTER_TYPE_TIMESPEC
  274. v = ( os.time( timespec ) - vars.epoch.value ) % 86400 -- strip date component and time-zone offset (standardize time and account for overflow too)
  275. elseif find_token( "^(%d%d)%-(%d%d)%-(%d%d%d%d)$" ) then
  276. local datespec = {
  277. isdst = false, day = tonumber( ref[ 1 ] ), month = tonumber( ref[ 2 ] ), year = tonumber( ref[ 3 ] ), hour = 0,
  278. }
  279. t = FILTER_TYPE_DATESPEC
  280. v = math.floor( ( os.time( datespec ) - vars.epoch.value ) / 86400 ) -- strip time component and time-zone offset (standardize time too)
  281. elseif find_token( "^'([a-zA-Z0-9+/]*);$" ) then
  282. t = FILTER_TYPE_STRING
  283. v = decode_base64( ref[ 1 ] )
  284. elseif find_token( "^\"([a-zA-Z0-9+/]*);$" ) then
  285. t = FILTER_TYPE_STRING
  286. v = decode_base64( ref[ 1 ] )
  287. v = string.gsub( v, "%$([a-zA-Z_]+)", function ( var )
  288. return vars[ var ] and tostring( vars[ var ].value ) or "?"
  289. end )
  290. elseif find_token( "^-?%d+$" ) or find_token( "^-?%d*%.%d+$" ) then
  291. t = FILTER_TYPE_NUMBER
  292. v = tonumber( ref[ 1 ] )
  293. elseif find_token( "^(%d+)%.(%d+)%.(%d+)%.(%d+)$" ) then
  294. t = FILTER_TYPE_ADDRESS
  295. v = tonumber( ref[ 1 ] ) * 16777216 + tonumber( ref[ 2 ] ) * 65536 + tonumber( ref[ 3 ] ) * 256 + tonumber( ref[ 4 ] )
  296. else
  297. return nil
  298. end
  299. return { type = t, value = v }
  300. end
  301. function evaluate( rule )
  302. -- short circuit binary logic to simplify evaluation
  303. local res = ( rule.bool == FILTER_BOOL_AND )
  304. local xor = 0
  305. for i, v in ipairs( rule.expr ) do
  306. if rule.bool == FILTER_BOOL_AND and not v then
  307. return false
  308. elseif rule.bool == FILTER_BOOL_OR and v then
  309. return true
  310. elseif rule.bool == FILTER_BOOL_XOR and v then
  311. xor = xor + 1
  312. end
  313. end
  314. if xor == 1 then return true end
  315. return res
  316. end
  317. function tokenize( line )
  318. -- encode string and pattern literals and function arguments to simplify parsing (order IS significant)
  319. line = string.gsub( line, "\"(.-)\"", function ( str )
  320. return "\"" .. encode_base64( str ) .. ";"
  321. end )
  322. line = string.gsub( line, "'(.-)'", function ( str )
  323. return "'" .. encode_base64( str ) .. ";"
  324. end )
  325. line = string.gsub( line, "/(.-)/([stda]?)", function ( a, b )
  326. return "/" .. encode_base64( a ) .. "," .. ( b == "" and "s" or b ) .. ";"
  327. end )
  328. line = string.gsub( line, "%b()", function ( str )
  329. return "&" .. encode_base64( trim( str ) ) .. ";"
  330. end )
  331. return line
  332. end
  333. ----------------------------
  334. -- public methods
  335. ----------------------------
  336. self.translate = function ( field, vars )
  337. return get_operand( tokenize( field ), vars )
  338. end
  339. self.refresh = function ( )
  340. local file = io.open( path .. "/" .. name, "r" )
  341. if not file then
  342. error( "The specified ruleset file does not exist." )
  343. end
  344. src = { }
  345. for line in file:lines( ) do
  346. -- skip comments (lines beginning with hash character) and blank lines
  347. -- TODO: remove extraneous white space at beginning of lines
  348. table.insert( src, string.byte( line ) ~= 35 and tokenize( line ) or "" )
  349. end
  350. file:close( file )
  351. end
  352. self.add_preset_vars = function ( vars )
  353. vars[ "clock" ] = { type = FILTER_TYPE_MOMENT, value = os.time( ) }
  354. vars[ "epoch" ] = { type = FILTER_TYPE_MOMENT, value = os.time( { year = 1970, month = 1, day = 1, hour = 0 } ) }
  355. vars[ "true" ] = { type = FILTER_TYPE_BOOLEAN, value = true }
  356. vars[ "false" ] = { type = FILTER_TYPE_BOOLEAN, value = false }
  357. end
  358. self.process = function( vars )
  359. local rule
  360. local note = "Access denied."
  361. if not is_active then return end
  362. if not debug then
  363. -- allow overriding preset vars when debugger is active
  364. self.add_preset_vars( vars )
  365. end
  366. for num, line in ipairs( src ) do
  367. local stmt = string.split( line, " ", false )
  368. if #stmt == 0 then
  369. -- skip no-op statements
  370. elseif stmt[ 1 ] == "continue" then
  371. if not rule then return trace( "Unexpected 'continue' statement in ruleset", num ) end
  372. if #stmt ~= 1 then return trace( "Invalid 'continue' statement in ruleset", num ) end
  373. if evaluate( rule ) then
  374. return num, ( rule.mode == FILTER_MODE_FAIL and note or nil )
  375. end
  376. rule = nil
  377. elseif stmt[ 1 ] == "try" then
  378. if rule then return trace( "Missing 'continue' statement in ruleset", num ) end
  379. if #stmt ~= 2 then return trace( "Invalid 'try' statement in ruleset", num ) end
  380. local oper = get_operand( stmt[ 2 ], vars )
  381. if not oper or oper.type ~= FILTER_TYPE_STRING then
  382. return trace( "Unrecognized operand in ruleset", num )
  383. end
  384. note = oper.value
  385. elseif stmt[ 1 ] == "pass" or stmt[ 1 ] == "fail" then
  386. if rule then return trace( "Missing 'continue' statement in ruleset", num ) end
  387. if #stmt ~= 2 then return trace( "Invalid 'pass' or 'fail' statement in ruleset", num ) end
  388. rule = { }
  389. local mode = ( { ["pass"] = FILTER_MODE_PASS, ["fail"] = FILTER_MODE_FAIL } )[ stmt[ 1 ] ]
  390. local bool = ( { ["all"] = FILTER_BOOL_AND, ["any"] = FILTER_BOOL_OR, ["one"] = FILTER_BOOL_XOR, ["now"] = FILTER_BOOL_NOW } )[ stmt[ 2 ] ]
  391. if not mode or not bool then
  392. return trace( "Unrecognized keywords in ruleset", num )
  393. end
  394. if bool == FILTER_BOOL_NOW then
  395. return num, ( mode == FILTER_MODE_FAIL and note or nil )
  396. end
  397. rule.mode = mode
  398. rule.bool = bool
  399. rule.expr = { }
  400. elseif stmt[ 1 ] == "when" or stmt[ 1 ] == "until" then
  401. if not rule then return trace( "Unexpected 'when' or 'until' statement in ruleset", num ) end
  402. if #stmt ~= 4 then return trace( "Invalid 'when' or 'until' statement in ruleset", num ) end
  403. local cond = ( { ["when"] = FILTER_COND_TRUE, ["until"] = FILTER_COND_FALSE } )[ stmt[ 1 ] ]
  404. local comp = ( { ["eq"] = FILTER_COMP_EQ, ["is"] = FILTER_COMP_IS } )[ stmt[ 3 ] ]
  405. if not cond or not comp then
  406. return trace( "Unrecognized keywords in ruleset", num )
  407. end
  408. local oper1 = get_operand( stmt[ 2 ], vars )
  409. local oper2 = get_operand( stmt[ 4 ], vars )
  410. if not oper1 or not oper2 then
  411. return trace( "Unrecognized operands in ruleset", num )
  412. elseif oper1.type ~= FILTER_TYPE_SERIES then
  413. return trace( "Mismatched operands in ruleset", num )
  414. end
  415. -- cache second operand value for efficiency
  416. -- TODO: might want to move the redundant operand type checks out of loop?
  417. local value2 = ( comp == FILTER_COMP_IS and oper2.type == FILTER_TYPE_STRING ) and string.upper( oper2.value ) or oper2.value
  418. local type2 = oper2.type
  419. local expr = false
  420. for i, value1 in ipairs( oper1.value ) do
  421. if comp == FILTER_COMP_EQ and type2 == FILTER_TYPE_STRING then
  422. expr = ( value1 == value2 )
  423. elseif comp == FILTER_COMP_IS and type2 == FILTER_TYPE_STRING then
  424. expr = ( string.upper( value1 ) == value2 )
  425. elseif comp == FILTER_COMP_IS and type2 == FILTER_TYPE_PATTERN then
  426. expr = value2.compare( value1, FILTER_TYPE_STRING )
  427. if expr == nil then return trace( "Ambiguous pattern mode in ruleset", num ) end
  428. else
  429. return trace( "Mismatched operands in ruleset", num )
  430. end
  431. if expr then break end
  432. end
  433. if cond == FILTER_COND_FALSE then expr = not expr end
  434. table.insert( rule.expr, expr )
  435. elseif stmt[ 1 ] == "if" or stmt[ 1 ] == "unless" then
  436. if not rule then return trace( "Unexpected 'if' or 'unless' statement in ruleset", num ) end
  437. if #stmt ~= 4 then return trace( "Invalid 'if' or 'unless' statement in ruleset", num ) end
  438. local cond = ( { ["if"] = FILTER_COND_TRUE, ["unless"] = FILTER_COND_FALSE } )[ stmt[ 1 ] ]
  439. local comp = ( { ["eq"] = FILTER_COMP_EQ, ["gt"] = FILTER_COMP_GT, ["lt"] = FILTER_COMP_LT, ["gte"] = FILTER_COMP_GTE, ["lte"] = FILTER_COMP_LTE, ["is"] = FILTER_COMP_IS } )[ stmt[ 3 ] ]
  440. if not cond or not comp then
  441. return trace( "Unrecognized keywords in ruleset", num )
  442. end
  443. local oper1 = get_operand( stmt[ 2 ], vars )
  444. local oper2 = get_operand( stmt[ 4 ], vars )
  445. if not oper1 or not oper2 then
  446. return trace( "Unrecognized operands in ruleset", num )
  447. end
  448. -- only allow comparisons of appropriate and equivalent datatypes
  449. local do_math = { [FILTER_TYPE_NUMBER] = true, [FILTER_TYPE_PERIOD] = true, [FILTER_TYPE_MOMENT] = true, [FILTER_TYPE_DATESPEC] = true, [FILTER_TYPE_TIMESPEC] = true }
  450. local expr
  451. if comp == FILTER_COMP_EQ and oper1.type == oper2.type and oper1.type ~= FILTER_TYPE_SERIES and oper1.type ~= FILTER_TYPE_PATTERN then
  452. expr = ( oper1.value == oper2.value )
  453. elseif comp == FILTER_COMP_GT and oper1.type == oper2.type and do_math[ oper2.type ] then
  454. expr = ( oper1.value > oper2.value )
  455. elseif comp == FILTER_COMP_GTE and oper1.type == oper2.type and do_math[ oper2.type ] then
  456. expr = ( oper1.value >= oper2.value )
  457. elseif comp == FILTER_COMP_LT and oper1.type == oper2.type and do_math[ oper2.type ] then
  458. expr = ( oper1.value < oper2.value )
  459. elseif comp == FILTER_COMP_LTE and oper1.type == oper2.type and do_math[ oper2.type ] then
  460. expr = ( oper1.value <= oper2.value )
  461. elseif comp == FILTER_COMP_IS and oper1.type == FILTER_TYPE_STRING and oper2.type == FILTER_TYPE_STRING then
  462. expr = ( string.upper( oper1.value ) == string.upper( oper2.value ) )
  463. elseif comp == FILTER_COMP_IS and oper2.type == FILTER_TYPE_PATTERN then
  464. expr = oper2.value.compare( oper1.value, oper1.type )
  465. if expr == nil then return trace( "Ambiguous pattern mode in ruleset", num ) end
  466. else
  467. return trace( "Mismatched operands in ruleset", num )
  468. end
  469. if cond == FILTER_COND_FALSE then expr = not expr end
  470. table.insert( rule.expr, expr )
  471. else
  472. return trace( "Invalid statement in ruleset", num )
  473. end
  474. end
  475. return trace( "Unexpected end-of-file in ruleset", 0 )
  476. end
  477. self.enable = function ( )
  478. is_active = true
  479. end
  480. self.disable = function ( )
  481. is_active = false
  482. end
  483. self.is_active = function ( )
  484. return is_active
  485. end
  486. self.refresh( )
  487. return self
  488. end