Last active
June 19, 2018 14:22
-
-
Save sorcerykid/9751e0b532337eb36a49d18b04b21166 to your computer and use it in GitHub Desktop.
Filter Mod
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-------------------------------------------------------- | |
-- Minetest :: Auth Redux Mod v2.1 (auth_rx) | |
-- | |
-- See README.txt for licensing and release notes. | |
-- Copyright (c) 2017-2018, Leslie E. Krause | |
-- | |
-- ./games/minetest_game/mods/auth_rx/filter.lua | |
-------------------------------------------------------- | |
FILTER_TYPE_STRING = 11 | |
FILTER_TYPE_BOOLEAN = 12 | |
FILTER_TYPE_NUMBER = 13 | |
FILTER_TYPE_PATTERN = 14 | |
FILTER_TYPE_SERIES = 15 | |
FILTER_MODE_FAIL = 20 | |
FILTER_MODE_PASS = 21 | |
FILTER_BOOL_AND = 30 | |
FILTER_BOOL_OR = 31 | |
FILTER_BOOL_XOR = 32 | |
FILTER_BOOL_NOW = 33 | |
FILTER_COND_FALSE = 40 | |
FILTER_COND_TRUE = 41 | |
FILTER_COMP_EQ = 50 | |
FILTER_COMP_GT = 51 | |
FILTER_COMP_LT = 52 | |
FILTER_COMP_IS = 53 | |
---------------------------- | |
-- AuthFilter class | |
---------------------------- | |
function AuthFilter( path, name ) | |
local src = { } | |
local opt = { is_debug = false, is_strict = true } | |
local self = { } | |
local file = io.open( path .. "/" .. name, "rb" ) | |
if not file then | |
error( "The specified ruleset file does not exist." ) | |
end | |
for line in file:lines( ) do | |
-- encode string and pattern literals to make parsing easier | |
line = string.gsub( line, "\"(.-)\"", function ( str ) | |
return "\"" .. minetest.encode_base64( str ) | |
end ) | |
line = string.gsub( line, "'(.-)'", function ( str ) | |
return "'" .. minetest.encode_base64( str ) | |
end ) | |
line = string.gsub( line, "/(.-)/", function ( str ) | |
return "/" .. minetest.encode_base64( str ) | |
end ) | |
table.insert( src, line ) | |
end | |
file:close( file ) | |
---------------------------- | |
-- private methods | |
---------------------------- | |
local throw = function ( msg, num ) | |
-- minetest.log( "error", msg .. " (line " .. num .. ")" ) | |
error( msg .. " (line " .. num .. ")" ) | |
end | |
local get_operand = function ( token, vars ) | |
local t, v | |
if string.find( token, "^\$[a-zA-Z_]+$" ) then | |
local var = string.sub( token, 2 ) | |
if not vars[ var ] then | |
return nil | |
else | |
t = vars[ var ].type | |
v = vars[ var ].value | |
end | |
elseif string.find( token, "^@[a-zA-Z._-]*$" ) then | |
t = FILTER_TYPE_SERIES | |
v = { } | |
local file = io.open( path .. "/" .. string.sub( token, 2 ), "rb" ) | |
if not file then | |
return nil | |
end | |
for line in file:lines( ) do | |
table.insert( v, line ) | |
end | |
elseif string.find( token, "^/.*$" ) then | |
-- sanitize search phrase and convert to regexp pattern | |
local sanitizer = | |
{ | |
["["] = ""; | |
["]"] = ""; | |
["^"] = "%^"; | |
["$"] = "%$"; | |
["("] = "%("; | |
[")"] = "%)"; | |
["%"] = "%%"; | |
["."] = "%."; | |
["-"] = "%-"; | |
["*"] = "[a-zA-Z0-9_-]*"; | |
["+"] = "[a-zA-Z0-9_-]+"; | |
["?"] = "[a-zA-Z0-9_-]"; | |
["#"] = "%d"; | |
["~"] = "%a"; | |
} | |
t = FILTER_TYPE_PATTERN | |
v = minetest.decode_base64( string.sub( token, 2 ) ) | |
v = "^" .. string.gsub( string.upper( v ), ".", sanitizer ) .. "$" | |
elseif string.find( token, "^'.*$" ) then | |
t = FILTER_TYPE_STRING | |
v = minetest.decode_base64( string.sub( token, 2 ) ) | |
elseif string.find( token, "^\".*$" ) then | |
t = FILTER_TYPE_STRING | |
v = minetest.decode_base64( string.sub( token, 2 ) ) | |
v = string.gsub( v, "\$([a-zA-Z_]+)", function ( var ) | |
return vars[ var ] and tostring( vars[ var ].value ) or "?" | |
end ) | |
elseif string.find( token, "^%d+$" ) then | |
t = FILTER_TYPE_NUMBER | |
v = tonumber( token ) | |
else | |
return nil | |
end | |
return { type = t, value = v } | |
end | |
local evaluate = function ( rule ) | |
-- short circuit binary logic to simplify evaluation | |
local res = ( rule.bool == FILTER_BOOL_AND ) | |
local xor = 0 | |
for i, v in ipairs( rule.expr ) do | |
if rule.bool == FILTER_BOOL_AND and not v then | |
return false | |
elseif rule.bool == FILTER_BOOL_OR and v then | |
return true | |
elseif rule.bool == FILTER_BOOL_XOR and v then | |
xor = xor + 1 | |
end | |
end | |
if xor == 1 then return true end | |
return res | |
end | |
---------------------------- | |
-- public methods | |
---------------------------- | |
self.process = function( vars ) | |
local rule | |
local note = "Access denied." | |
vars[ "true" ] = { type = FILTER_TYPE_BOOLEAN, value = true } | |
vars[ "false" ] = { type = FILTER_TYPE_BOOLEAN, value = false } | |
vars[ "time" ] = { type = FILTER_TYPE_NUMBER, value = os.time( ) } | |
for num, line in ipairs( src ) do | |
-- FIXME: ignore extraneous whitespace, even at beginning of line | |
local stmt = string.split( line, " ", false ) | |
if string.byte( line ) == 35 or #stmt == 0 then | |
-- skip comments (lines beginning with hash character) and empty lines | |
-- TODO: these should be stripped on file import | |
elseif stmt[ 1 ] == "continue" then | |
if #stmt ~= 1 then throw( "Invalid 'continue' statement in ruleset", num ) end | |
if rule == nil then | |
throw( "No ruleset declared", num ) | |
end | |
print( " eval:", dump( rule.expr ) ) | |
if evaluate( rule ) then | |
return ( rule.mode == FILTER_MODE_FAIL and note or nil ) | |
end | |
rule = nil | |
elseif stmt[ 1 ] == "try" then | |
if rule then throw( "Missing 'continue' statement in ruleset", num ) end | |
if #stmt ~= 2 then throw( "Invalid 'try' statement in ruleset", num ) end | |
local oper = get_operand( stmt[ 2 ], vars ) | |
if not oper then | |
throw( "Unrecognized operand in ruleset", num ) | |
end | |
note = oper.value | |
elseif stmt[ 1 ] == "pass" or stmt[ 1 ] == "fail" then | |
if rule then throw( "Missing continue statement in ruleset", num ) end | |
if #stmt ~= 2 then throw( "Invalid 'pass' or 'fail' statement in ruleset", num ) end | |
rule = { } | |
local mode = ( { ["pass"] = FILTER_MODE_PASS, ["fail"] = FILTER_MODE_FAIL } )[ stmt[ 1 ] ] | |
local bool = ( { ["all"] = FILTER_BOOL_AND, ["any"] = FILTER_BOOL_OR, ["one"] = FILTER_BOOL_XOR, ["now"] = FILTER_BOOL_NOW } )[ stmt[ 2 ] ] | |
if not mode or not bool then | |
throw( "Unrecognized keywords in ruleset", num ) | |
end | |
if bool == FILTER_BOOL_NOW then | |
return ( mode == FILTER_MODE_FAIL and note or nil ) | |
end | |
rule.mode = mode | |
rule.bool = bool | |
rule.expr = { } | |
elseif stmt[ 1 ] == "when" or stmt[ 1 ] == "until" then | |
if #stmt ~= 4 then throw( "Invalid 'when' or 'until' statement in ruleset", num ) end | |
local cond = ( { ["when"] = FILTER_COND_TRUE, ["until"] = FILTER_COND_FALSE } )[ stmt[ 1 ] ] | |
local comp = ( { ["eq"] = FILTER_COMP_EQ, ["is"] = FILTER_COMP_IS } )[ stmt[ 3 ] ] | |
if not cond or not comp then | |
throw( "Unrecognized keywords in ruleset", num ) | |
end | |
local oper1 = get_operand( stmt[ 2 ], vars ) | |
local oper2 = get_operand( stmt[ 4 ], vars ) | |
--print( " oper1=", dump( oper1 ) ) | |
print( " oper2=", dump( oper2 ) ) | |
if not oper1 or not oper2 then | |
throw( "Unrecognized operands in ruleset", num ) | |
elseif oper1.type ~= FILTER_TYPE_SERIES then | |
throw( "Mismatched operands in ruleset", num ) | |
end | |
-- cache second operand value for efficiency | |
-- TODO: might want to move the redundant operand type checks out of loop? | |
local value2 = ( comp == FILTER_COMP_IS and oper2.type == FILTER_TYPE_STRING ) and string.upper( oper2.value ) or oper2.value | |
local expr = false | |
for i, value1 in ipairs( oper1.value ) do | |
--print( "?", value1, value2 ) | |
if comp == FILTER_COMP_EQ and oper2.type == FILTER_TYPE_STRING then | |
expr = ( value1 == value2 ) | |
elseif comp == FILTER_COMP_IS and oper2.type == FILTER_TYPE_STRING then | |
expr = ( string.upper( value1 ) == value2 ) | |
elseif comp == FILTER_COMP_IS and oper2.type == FILTER_TYPE_PATTERN then | |
expr = ( string.find( string.upper( value1 ), value2 ) == 1 ) | |
else | |
throw( "Mismatched operands in ruleset", num ) | |
end | |
if expr then break end | |
end | |
if cond == FILTER_COND_FALSE then expr = not expr end | |
table.insert( rule.expr, expr ) | |
elseif stmt[ 1 ] == "if" or stmt[ 1 ] == "unless" then | |
if #stmt ~= 4 then throw( "Invalid 'if' or 'unless' statement in ruleset", num ) end | |
local cond = ( { ["if"] = FILTER_COND_TRUE, ["unless"] = FILTER_COND_FALSE } )[ stmt[ 1 ] ] | |
local comp = ( { ["eq"] = FILTER_COMP_EQ, ["gt"] = FILTER_COMP_GT, ["lt"] = FILTER_COMP_LT, ["is"] = FILTER_COMP_IS } )[ stmt[ 3 ] ] | |
if not cond or not comp then | |
throw( "Unrecognized keywords in ruleset", num ) | |
end | |
local oper1 = get_operand( stmt[ 2 ], vars ) | |
local oper2 = get_operand( stmt[ 4 ], vars ) | |
--print( " oper1=", dump( oper1 ) ) | |
--print( " oper2=", dump( oper2 ) ) | |
if not oper1 or not oper2 then | |
throw( "Unrecognized operands in ruleset", num ) | |
end | |
-- FIXME: don't allow equality comparison of patterns or series | |
local expr | |
if comp == FILTER_COMP_EQ and oper1.type == oper2.type and oper1.type ~= FILTER_TYPE_SERIES and oper1.type ~= FILTER_TYPE_PATTERN then | |
expr = ( oper1.value == oper2.value ) | |
elseif comp == FILTER_COMP_IS and oper1.type == FILTER_TYPE_STRING and oper2.type == FILTER_TYPE_STRING then | |
expr = ( string.upper( oper1.value ) == string.upper( oper2.value ) ) | |
elseif comp == FILTER_COMP_IS and oper1.type == FILTER_TYPE_STRING and oper2.type == FILTER_TYPE_PATTERN then | |
expr = ( string.find( string.upper( oper1.value ), oper2.value ) == 1 ) | |
elseif comp == FILTER_COMP_GT and oper1.type == FILTER_TYPE_NUMBER and oper2.type == FILTER_TYPE_NUMBER then | |
expr = ( oper1.value > oper2.value ) | |
elseif comp == FILTER_COMP_LT and oper1.type == FILTER_TYPE_NUMBER and oper2.type == FILTER_TYPE_NUMBER then | |
expr = ( oper1.value < oper2.value ) | |
else | |
throw( "Mismatched operands in ruleset", num ) | |
end | |
if cond == FILTER_COND_FALSE then expr = not expr end | |
table.insert( rule.expr, expr ) | |
-- TODO: immediately evaluating each expression (thus avoiding a list) would be optimal, | |
-- but probably requires state table; efficiency vs complexity scenario | |
else | |
throw( "Invalid statement in ruleset", num ) | |
end | |
end | |
throw( "Unexpected end-of-file in ruleset", num ) | |
end | |
return self | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment