Source code for plaso.lib.objectfilter

#!/usr/bin/env python
#
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Classes to perform filtering of objects based on their data members.

Given a list of objects and a textual filter expression, these classes allow
you to determine which objects match the filter. The system has two main
pieces: A parser for the supported grammar and a filter implementation.

Given any complying user-supplied grammar, it is parsed with a custom lexer
based on GRR's lexer and then compiled into an actual implementation by using
the filter implementation. A filter implementation simply provides actual
implementations for the primitives required to perform filtering. The compiled
result is always a class supporting the Filter interface.

If we define a class called Car such as:


class Car(object):
  def __init__(self, code, color="white", doors=3):
    self.code = code
    self.color = color
    self.doors = 3

And we have two instances:

  ford_ka = Car("FORDKA1", color="grey")
  toyota_corolla = Car("COROLLA1", color="white", doors=5)
  fleet = [ford_ka, toyota_corolla]

We want to find cars that are grey and have 3 or more doors. We could filter
our fleet like this:

  criteria = "(color is grey) and (doors >= 3)"
  parser = ContextFilterParser(criteria).Parse()
  compiled_filter = parser.Compile(LowercaseAttributeFilterImp)

  for car in fleet:
    if compiled_filter.Matches(car):
      print("Car %s matches the supplied filter." % car.code)

The filter expression contains two subexpressions joined by an AND operator:
  "color is grey" and "doors >= 3"

This means we want to search for objects matching these two subexpressions.
Let's analyze the first one in depth "color is grey":

  "color": the left operand specifies a search path to look for the data. This
  tells our filtering system to look for the color property on passed objects.
  "is": the operator. Values retrieved for the "color" property will be checked
  against the right operand to see if they are equal.
  "grey": the right operand. It specifies an explicit value to check for.

So each time an object is passed through the filter, it will expand the value
of the color data member, and compare its value against "grey".

Because data members of objects are often not simple datatypes but other
objects, the system allows you to reference data members within other data
members by separating each by a dot. Let's see an example:

Let's add a more complex Car class with default tyre data:


class CarWithTyres(Car):
  def __init__(self, code, tyres=None, color="white", doors=3):
    super(self, CarWithTyres).__init__(code, color, doors)
    tyres = tyres or Tyre("Pirelli", "PZERO")


class Tyre(object):
  def __init__(self, brand, code):
    self.brand = brand
    self.code = code

And two new instances:
  ford_ka = CarWithTyres("FORDKA", color="grey", tyres=Tyre("AVON", "ZT5"))
  toyota_corolla = Car("COROLLA1", color="white", doors=5)
  fleet = [ford_ka, toyota_corolla]

To filter a car based on the tyre brand, we would use a search path of
"tyres.brand".

Because the filter implementation provides the actual classes that perform
handling of the search paths, operators, etc. customizing the behaviour of the
filter is easy. Three basic filter implementations are given:

  BaseFilterImplementation: search path expansion is done on attribute names
  as provided (case-sensitive).
  LowercaseAttributeFilterImp: search path expansion is done on the lowercased
  attribute name, so that it only accesses attributes, not methods.
  DictFilterImplementation: search path expansion is done on dictionary access
  to the given object. So "a.b" expands the object obj to obj["a"]["b"]
"""

from __future__ import unicode_literals

import abc
import binascii
import codecs
import logging
import re

from plaso.lib import errors
from plaso.lib import lexer
from plaso.lib import py2to3


# pylint: disable=attribute-defined-outside-init
# pylint: disable=missing-docstring,missing-param-doc
# pylint: disable=missing-type-doc,missing-yield-type-doc

[docs]def GetUnicodeString(value): """Attempts to convert the argument to a Unicode string. Args: value (list|int|bytes|str): value to convert. Returns: str: string representation of the argument. """ if isinstance(value, list): value = [GetUnicodeString(item) for item in value] return ''.join(value) if isinstance(value, py2to3.INTEGER_TYPES): value = '{0:d}'.format(value) if not isinstance(value, py2to3.UNICODE_TYPE): return codecs.decode(value, 'utf8', 'ignore')
return value
[docs]class InvalidNumberOfOperands(errors.Error):
"""The number of operands provided to this operator is wrong."""
[docs]class Filter(object): """Base class for every filter.""" def __init__(self, arguments=None, value_expander=None): """Constructor. Args: arguments: Arguments to the filter. value_expander: A callable that will be used to expand values for the objects passed to this filter. Implementations expanders are provided by subclassing ValueExpander. Raises: ValueError: If the given value_expander is not a subclass of ValueExpander """ self.value_expander = None self.value_expander_cls = value_expander if self.value_expander_cls: if not issubclass(self.value_expander_cls, ValueExpander): raise ValueError('{0:s} is not a valid value expander'.format( self.value_expander_cls)) self.value_expander = self.value_expander_cls() self.args = arguments or [] logging.debug('Adding {0!s}'.format(arguments))
[docs] @abc.abstractmethod def Matches(self, obj):
"""Whether object obj matches this filter."""
[docs] def Filter(self, objects): """Returns a list of objects that pass the filter."""
return filter(self.Matches, objects) def __str__(self): return '{0:s}({1:s})'.format(
self.__class__.__name__, ', '.join([str(arg) for arg in self.args]))
[docs]class AndFilter(Filter): """Performs a boolean AND of the given Filter instances as arguments. Note that if no conditions are passed, all objects will pass. """
[docs] def Matches(self, obj): for child_filter in self.args: if not child_filter.Matches(obj): return False
return True
[docs]class OrFilter(Filter): """Performs a boolean OR of the given Filter instances as arguments. Note that if no conditions are passed, all objects will pass. """
[docs] def Matches(self, obj): if not self.args: return True for child_filter in self.args: if child_filter.Matches(obj): return True
return False # pylint: disable=abstract-method
[docs]class Operator(Filter):
"""Base class for all operators."""
[docs]class IdentityFilter(Operator):
[docs] def Matches(self, _):
return True
[docs]class UnaryOperator(Operator): """Base class for unary operators.""" def __init__(self, operand, **kwargs): """Constructor.""" super(UnaryOperator, self).__init__(arguments=[operand], **kwargs) if len(self.args) != 1: raise InvalidNumberOfOperands( 'Only one operand is accepted by {0:s}. Received {1:d}.'.format(
self.__class__.__name__, len(self.args)))
[docs]class BinaryOperator(Operator): """Base class for binary operators. The left operand is always a path into the object which will be expanded for values. The right operand is a value defined at initialization and is stored at self.right_operand. """ def __init__(self, arguments=None, **kwargs): super(BinaryOperator, self).__init__(arguments=arguments, **kwargs) if len(self.args) != 2: raise InvalidNumberOfOperands( 'Only two operands are accepted by {0:s}. Received {1:s}.'.format( self.__class__.__name__, len(self.args))) self.left_operand = self.args[0]
self.right_operand = self.args[1]
[docs]class GenericBinaryOperator(BinaryOperator): """Allows easy implementations of operators.""" def __init__(self, **kwargs): super(GenericBinaryOperator, self).__init__(**kwargs) self.bool_value = True
[docs] def FlipBool(self): logging.debug('Negative matching.')
self.bool_value = not self.bool_value
[docs] def Operation(self, x, y):
"""Performs the operation between two values."""
[docs] def Operate(self, values): """Takes a list of values and if at least one matches, returns True.""" for val in values: try: if self.Operation(val, self.right_operand): return True else: continue except (ValueError, TypeError): continue
return False
[docs] def Matches(self, obj): key = self.left_operand values = self.value_expander.Expand(obj, key) values = list(values) if values and self.Operate(values): return self.bool_value
return not self.bool_value
[docs]class Equals(GenericBinaryOperator): """Matches objects when the right operand equals the expanded value."""
[docs] def Operation(self, x, y):
return x == y
[docs]class NotEquals(Equals): """Matches when the right operand isn't equal to the expanded value.""" def __init__(self, **kwargs): super(NotEquals, self).__init__(**kwargs)
self.bool_value = False
[docs]class Less(GenericBinaryOperator): """Whether the expanded value >= right_operand."""
[docs] def Operation(self, x, y):
return x < y
[docs]class LessEqual(GenericBinaryOperator): """Whether the expanded value <= right_operand."""
[docs] def Operation(self, x, y):
return x <= y
[docs]class Greater(GenericBinaryOperator): """Whether the expanded value > right_operand."""
[docs] def Operation(self, x, y):
return x > y
[docs]class GreaterEqual(GenericBinaryOperator): """Whether the expanded value >= right_operand."""
[docs] def Operation(self, x, y):
return x >= y
[docs]class Contains(GenericBinaryOperator): """Whether the right operand is contained in the value."""
[docs] def Operation(self, x, y): if isinstance(x, py2to3.STRING_TYPES): return y.lower() in x.lower()
return y in x
[docs]class InSet(GenericBinaryOperator): # TODO(user): Change to an N-ary Operator? """Whether all values are contained within the right operand."""
[docs] def Operation(self, x, y): """Whether x is fully contained in y.""" if x in y: return True # x might be an iterable # first we need to skip strings or we'll do silly things # pylint: disable=consider-merging-isinstance if isinstance(x, py2to3.STRING_TYPES) or isinstance(x, bytes): return False try: for value in x: if value not in y: return False return True except TypeError:
return False
[docs]class Regexp(GenericBinaryOperator): """Whether the value matches the regexp in the right operand.""" def __init__(self, *children, **kwargs): super(Regexp, self).__init__(*children, **kwargs) # Note that right_operand is not necessarily a string. logging.debug('Compiled: {0!s}'.format(self.right_operand)) try: self.compiled_re = re.compile( GetUnicodeString(self.right_operand), re.DOTALL) except re.error: raise ValueError('Regular expression "{0!s}" is malformed.'.format( self.right_operand))
[docs] def Operation(self, x, unused_y): try: if self.compiled_re.search(GetUnicodeString(x)): return True except TypeError: pass
return False
[docs]class RegexpInsensitive(Regexp): """Whether the value matches the regexp in the right operand.""" def __init__(self, *children, **kwargs): super(RegexpInsensitive, self).__init__(*children, **kwargs) # Note that right_operand is not necessarily a string. logging.debug('Compiled: {0!s}'.format(self.right_operand)) try: self.compiled_re = re.compile(GetUnicodeString(self.right_operand), re.I | re.DOTALL) except re.error: raise ValueError('Regular expression "{0!s}" is malformed.'.format(
self.right_operand))
[docs]class Context(Operator): """Restricts the child operators to a specific context within the object. Solves the context problem. The context problem is the following: Suppose you store a list of loaded DLLs within a process. Suppose that for each of these DLLs you store the number of imported functions and each of the imported functions name. Imagine that a malicious DLL is injected into processes and its indicators are that it only imports one function and that it is RegQueryValueEx. Yo'd write your indicator like this: AndOperator( Equal("ImportedDLLs.ImpFunctions.Name", "RegQueryValueEx"), Equal("ImportedDLLs.NumImpFunctions", "1") ) Now imagine you have these two processes on a given system. Process1 * __ImportedDlls * __Name: "notevil.dll" * __ImpFunctions * __Name: "CreateFileA" * __NumImpFunctions: 1 * __Name: "alsonotevil.dll" * __ImpFunctions * __Name: "RegQueryValueEx" * __Name: "CreateFileA" * __NumImpFunctions: 2 Process2 * __ImportedDlls * __Name: "evil.dll" * __ImpFunctions * __Name: "RegQueryValueEx" * __NumImpFunctions: 1 Both Process1 and Process2 match your query, as each of the indicators are evaluated separately. While you wanted to express "find me processes that have a DLL that has both one imported function and ReqQueryValueEx is in the list of imported functions", your indicator actually means "find processes that have at least a DLL with 1 imported functions and at least one DLL that imports the ReqQueryValueEx function". To write such an indicator you need to specify a context of ImportedDLLs for these two clauses. Such that you convert your indicator to:: Context("ImportedDLLs", AndOperator( Equal("ImpFunctions.Name", "RegQueryValueEx"), Equal("NumImpFunctions", "1") )) Context will execute the filter specified as the second parameter for each of the objects under "ImportedDLLs", thus applying the condition per DLL, not per object and returning the right result. """ def __init__(self, arguments=None, **kwargs): if len(arguments) != 2: raise InvalidNumberOfOperands('Context accepts only 2 operands.') super(Context, self).__init__(arguments=arguments, **kwargs) self.context, self.condition = self.args
[docs] def Matches(self, obj): for object_list in self.value_expander.Expand(obj, self.context): for sub_object in object_list: if self.condition.Matches(sub_object): return True
return False OP2FN = { 'equals': Equals, 'is': Equals, '==': Equals, '!=': NotEquals, 'contains': Contains, '>': Greater, '>=': GreaterEqual, '<': Less, '<=': LessEqual, 'inset': InSet, 'regexp': Regexp, 'iregexp': RegexpInsensitive}
[docs]class ValueExpander(object): """Encapsulates the logic to expand values available in an object. Once instantiated and called, this class returns all the values that follow a given field path. """ FIELD_SEPARATOR = '.' def _GetAttributeName(self, path): """Returns the attribute name to fetch given a path.""" return path[0] def _GetValue(self, unused_obj, unused_attr_name): """Returns the value of tha attribute attr_name.""" raise NotImplementedError() def _AtLeaf(self, attr_value): """Called when at a leaf value. Should yield a value.""" yield attr_value def _AtNonLeaf(self, attr_value, path): """Called when at a non-leaf value. Should recurse and yield values.""" try: # Check first for iterables # If it's a dictionary, we yield it if isinstance(attr_value, dict): yield attr_value else: # If it's an iterable, we recurse on each value. for sub_obj in attr_value: for value in self.Expand(sub_obj, path[1:]): yield value except TypeError: # This is then not iterable, we recurse with the value for value in self.Expand(attr_value, path[1:]): yield value
[docs] def Expand(self, obj, path): """Returns a list of all the values for the given path in the object obj. Given a path such as ["sub1", "sub2"] it returns all the values available in obj.sub1.sub2 as a list. sub1 and sub2 must be data attributes or properties. If sub1 returns a list of objects, or a generator, Expand aggregates the values for the remaining path for each of the objects, thus returning a list of all the values under the given path for the input object. Args: obj: An object that will be traversed for the given path path: A list of strings Yields: The values once the object is traversed. """ if isinstance(path, py2to3.STRING_TYPES): path = path.split(self.FIELD_SEPARATOR) attr_name = self._GetAttributeName(path) attr_value = self._GetValue(obj, attr_name) if attr_value is None: return if len(path) == 1: for value in self._AtLeaf(attr_value): yield value else: for value in self._AtNonLeaf(attr_value, path):
yield value
[docs]class AttributeValueExpander(ValueExpander): """An expander that gives values based on object attribute names.""" def _GetValue(self, obj, attr_name):
return getattr(obj, attr_name, None)
[docs]class LowercaseAttributeValueExpander(AttributeValueExpander): """An expander that lowercases all attribute names before access.""" def _GetAttributeName(self, path):
return path[0].lower()
[docs]class DictValueExpander(ValueExpander): """An expander that gets values from dictionary access to the object.""" def _GetValue(self, obj, attr_name):
return obj.get(attr_name, None)
[docs]class BasicExpression(lexer.Expression): """Basic Expression.""" def __init__(self): super(BasicExpression, self).__init__() self.bool_value = True
[docs] def FlipBool(self):
self.bool_value = not self.bool_value
[docs] def Compile(self, filter_implementation): arguments = [self.attribute] op_str = self.operator.lower() operator = filter_implementation.OPS.get(op_str, None) if not operator: raise errors.ParseError('Unknown operator {0:s} provided.'.format( self.operator)) arguments.extend(self.args) expander = filter_implementation.FILTERS['ValueExpander'] ops = operator(arguments=arguments, value_expander=expander) if not self.bool_value: if hasattr(ops, 'FlipBool'): ops.FlipBool()
return ops
[docs]class ContextExpression(lexer.Expression): """Represents the context operator.""" def __init__(self, attribute="", part=None): self.attribute = attribute self.args = [] if part: self.args.append(part) super(ContextExpression, self).__init__() def __str__(self): return 'Context({0:s} {1:s})'.format( self.attribute, [str(x) for x in self.args])
[docs] def SetExpression(self, expression): """Set the expression.""" if isinstance(expression, lexer.Expression): self.args = [expression] else: raise errors.ParseError(
'Expected expression, got {0:s}.'.format(expression))
[docs] def Compile(self, filter_implementation): """Compile the expression.""" arguments = [self.attribute] for arg in self.args: arguments.append(arg.Compile(filter_implementation)) expander = filter_implementation.FILTERS['ValueExpander'] context_cls = filter_implementation.FILTERS['Context'] return context_cls(arguments=arguments,
value_expander=expander)
[docs]class BinaryExpression(lexer.BinaryExpression):
[docs] def Compile(self, filter_implementation): """Compile the binary expression into a filter object.""" operator = self.operator.lower() if operator == 'and' or operator == '&&': method = 'AndFilter' elif operator == 'or' or operator == '||': method = 'OrFilter' else: raise errors.ParseError( 'Invalid binary operator {0:s}.'.format(operator)) args = [x.Compile(filter_implementation) for x in self.args]
return filter_implementation.FILTERS[method](arguments=args)
[docs]class Parser(lexer.SearchParser): """Parses and generates an AST for a query written in the described language. Examples of valid syntax: size is 40 (name contains "Program Files" AND hash.md5 is "123abc") @imported_modules (num_symbols = 14 AND symbol.name is "FindWindow") """ expression_cls = BasicExpression binary_expression_cls = BinaryExpression context_cls = ContextExpression tokens = [ # Operators and related tokens lexer.Token('INITIAL', r'\@[\w._0-9]+', 'ContextOperator,PushState', 'CONTEXTOPEN'), lexer.Token('INITIAL', r'[^\s\(\)]', 'PushState,PushBack', 'ATTRIBUTE'), lexer.Token('INITIAL', r'\(', 'PushState,BracketOpen', None), lexer.Token('INITIAL', r'\)', 'BracketClose', 'BINARY'), # Context lexer.Token('CONTEXTOPEN', r'\(', 'BracketOpen', 'INITIAL'), # Double quoted string lexer.Token('STRING', '"', 'PopState,StringFinish', None), lexer.Token('STRING', r'\\x(..)', 'HexEscape', None), lexer.Token('STRING', r'\\(.)', 'StringEscape', None), lexer.Token('STRING', r'[^\\"]+', 'StringInsert', None), # Single quoted string lexer.Token('SQ_STRING', '\'', 'PopState,StringFinish', None), lexer.Token('SQ_STRING', r'\\x(..)', 'HexEscape', None), lexer.Token('SQ_STRING', r'\\(.)', 'StringEscape', None), lexer.Token('SQ_STRING', r'[^\\\']+', 'StringInsert', None), # Basic expression lexer.Token('ATTRIBUTE', r'[\w._0-9]+', 'StoreAttribute', 'OPERATOR'), lexer.Token('OPERATOR', r'not ', 'FlipLogic', None), lexer.Token('OPERATOR', r'(\w+|[<>!=]=?)', 'StoreOperator', 'CHECKNOT'), lexer.Token('CHECKNOT', r'not', 'FlipLogic', 'ARG'), lexer.Token('CHECKNOT', r'\s+', None, None), lexer.Token('CHECKNOT', r'([^not])', 'PushBack', 'ARG'), lexer.Token('ARG', r'(\d+\.\d+)', 'InsertFloatArg', 'ARG'), lexer.Token('ARG', r'(0x\d+)', 'InsertInt16Arg', 'ARG'), lexer.Token('ARG', r'(\d+)', 'InsertIntArg', 'ARG'), lexer.Token('ARG', '"', 'PushState,StringStart', 'STRING'), lexer.Token('ARG', '\'', 'PushState,StringStart', 'SQ_STRING'), # When the last parameter from arg_list has been pushed # State where binary operators are supported (AND, OR) lexer.Token('BINARY', r'(?i)(and|or|\&\&|\|\|)', 'BinaryOperator', 'INITIAL'), # - We can also skip spaces lexer.Token('BINARY', r'\s+', None, None), # - But if it's not "and" or just spaces we have to go back lexer.Token('BINARY', '.', 'PushBack,PopState', None), # Skip whitespace. lexer.Token('.', r'\s+', None, None), ]
[docs] def StoreAttribute(self, string='', **kwargs): self.flipped = False
super(Parser, self).StoreAttribute(string, **kwargs)
[docs] def FlipAllowed(self): """Raise an error if the not keyword is used where it is not allowed.""" if not hasattr(self, 'flipped'): raise errors.ParseError('Not defined.') if not self.flipped: return if self.current_expression.operator: if not self.current_expression.operator.lower() in ( 'is', 'contains', 'inset', 'equals'): raise errors.ParseError( 'Keyword \'not\' does not work against operator: {0:s}'.format(
self.current_expression.operator))
[docs] def FlipLogic(self, **unused_kwargs): """Flip the boolean logic of the expression. If an expression is configured to return True when the condition is met this logic will flip that to False, and vice versa. """ if hasattr(self, 'flipped') and self.flipped: raise errors.ParseError( 'The operator \'not\' can only be expressed once.') if self.current_expression.args: raise errors.ParseError( 'Unable to place the keyword \'not\' after an argument.') self.flipped = True # Check if this flip operation should be allowed. self.FlipAllowed() if hasattr(self.current_expression, 'FlipBool'): self.current_expression.FlipBool() logging.debug('Negative matching [flipping boolean logic].') else: logging.warning(
'Unable to perform a negative match, issuing a positive one.')
[docs] def InsertArg(self, string='', **unused_kwargs): """Insert an arg to the current expression.""" # Note that "string" is not necessarily of type string. logging.debug('Storing argument: {0!s}'.format(string)) # Check if this flip operation should be allowed. self.FlipAllowed() # This expression is complete if self.current_expression.AddArg(string): self.stack.append(self.current_expression) self.current_expression = self.expression_cls() # We go to the BINARY state, to find if there's an AND or OR operator return 'BINARY'
return None
[docs] def InsertFloatArg(self, string='', **unused_kwargs): """Inserts a Float argument.""" try: float_value = float(string) except (TypeError, ValueError): raise errors.ParseError('{0:s} is not a valid float.'.format(string))
return self.InsertArg(float_value)
[docs] def InsertIntArg(self, string='', **unused_kwargs): """Inserts an Integer argument.""" try: int_value = int(string) except (TypeError, ValueError): raise errors.ParseError('{0:s} is not a valid integer.'.format(string))
return self.InsertArg(int_value)
[docs] def InsertInt16Arg(self, string='', **unused_kwargs): """Inserts an Integer in base16 argument.""" try: int_value = int(string, 16) except (TypeError, ValueError): raise errors.ParseError( '{0:s} is not a valid base16 integer.'.format(string))
return self.InsertArg(int_value)
[docs] def StringFinish(self, **unused_kwargs): if self.state == 'ATTRIBUTE': return self.StoreAttribute(string=self.string) elif self.state == 'ARG': return self.InsertArg(string=self.string)
return None
[docs] def StringEscape(self, string, match, **unused_kwargs): """Escape backslashes found inside a string quote. Backslashes followed by anything other than [\'"rnbt.ws] will raise an Error. Args: string: The string that matched. match: the match object (instance of re.MatchObject). Where match.group(1) contains the escaped code. Raises: ParseError: When the escaped string is not one of [\'"rnbt] """ if match.group(1) in '\\\'"rnbt\\.ws': self.string += codecs.decode(string, 'unicode_escape') else:
raise errors.ParseError('Invalid escape character {0:s}.'.format(string))
[docs] def HexEscape(self, string, match, **unused_kwargs): """Converts a hex escaped string.""" logging.debug('HexEscape matched {0:s}.'.format(string)) hex_string = match.group(1) try: hex_string = binascii.unhexlify(hex_string) hex_string = codecs.decode(hex_string, 'utf-8') self.string += hex_string except (TypeError, binascii.Error):
raise errors.ParseError('Invalid hex escape {0!s}.'.format(hex_string))
[docs] def ContextOperator(self, string='', **unused_kwargs):
self.stack.append(self.context_cls(string[1:]))
[docs] def Reduce(self): """Reduce the token stack into an AST.""" # Check for sanity if self.state != 'INITIAL' and self.state != 'BINARY': self.Error('Premature end of expression') length = len(self.stack) while length > 1: # Precedence order self._CombineParenthesis() self._CombineBinaryExpressions('and') self._CombineBinaryExpressions('or') self._CombineContext() # No change if len(self.stack) == length: break length = len(self.stack) if length != 1: self.Error('Illegal query expression')
return self.stack[0]
[docs] def Error(self, message=None, _=None): # Note that none of the values necessarily are strings. raise errors.ParseError( '{0!s} in position {1!s}: {2!s} <----> {3!s} )'.format( message, len(self.processed_buffer), self.processed_buffer,
self.buffer)) def _CombineBinaryExpressions(self, operator): for i in range(1, len(self.stack)-1): item = self.stack[i] if (isinstance(item, lexer.BinaryExpression) and item.operator.lower() == operator.lower() and isinstance(self.stack[i-1], lexer.Expression) and isinstance(self.stack[i+1], lexer.Expression)): lhs = self.stack[i-1] rhs = self.stack[i+1] self.stack[i].AddOperands(lhs, rhs) self.stack[i-1] = None self.stack[i+1] = None self.stack = list(filter(None, self.stack)) def _CombineContext(self): # Context can merge from item 0 for i in range(len(self.stack)-1, 0, -1): item = self.stack[i-1] if (isinstance(item, ContextExpression) and isinstance(self.stack[i], lexer.Expression)): expression = self.stack[i] self.stack[i-1].SetExpression(expression) self.stack[i] = None
self.stack = list(filter(None, self.stack)) ### FILTER IMPLEMENTATIONS
[docs]class BaseFilterImplementation(object): """Defines the base implementation of an object filter by its attributes. Inherit from this class, switch any of the needed operators and pass it to the Compile method of a parsed string to obtain an executable filter. """ OPS = OP2FN FILTERS = { 'ValueExpander': AttributeValueExpander, 'AndFilter': AndFilter, 'OrFilter': OrFilter, 'IdentityFilter': IdentityFilter,
'Context': Context}