ComplianceAsCode/content

View on GitHub
ssg/boolean_expression.py

Summary

Maintainability
Test Coverage
"""
Common functions for Boolean Expressions
"""

from ssg.ext.boolean import boolean
from ssg import requirement_specs

# We don't support ~= to avoid confusion with boolean operator NOT (~)
SPEC_SYMBOLS = ['<', '>', '=', '!', ',', '[', ']']
VERSION_SYMBOLS = ['.', '-', '_', ":"]


class Function(boolean.Function):
    """
    Base class for boolean functions.

    This class should be subclassed and passed to the `Algebra` as `function_cls`
    to enrich expression elements with domain-specific methods.
    """
    def is_and(self):
        return isinstance(self, boolean.AND)

    def is_or(self):
        return isinstance(self, boolean.OR)

    def is_not(self):
        return isinstance(self, boolean.NOT)

    def as_id(self):
        """
        Generate a string representation of the boolean expression.

        This method constructs a unique identifier for the boolean expression by recursively
        calling `as_id` on its arguments and combining them with the appropriate boolean operator.

        Returns:
            str: A string representing the boolean expression. If the expression is a negation, it
                 returns 'not_' followed by the identifier of the negated argument. If the
                 expression is a conjunction or disjunction, it returns the identifiers of the
                 arguments joined by '_and_' or '_or_'.
        """
        if self.is_not():
            return 'not_{0}'.format(self.args[0].as_id())
        op = 'unknown_bool_op'
        if self.is_and():
            op = 'and'
        if self.is_or():
            op = 'or'
        return '_{0}_'.format(op).join([arg.as_id() for arg in self.args])


class Symbol(boolean.Symbol):
    """
    Symbol class represents a boolean symbol with domain-specific methods.

    This class should be subclassed and passed to the `Algebra` as `symbol_cls`
    to enrich expression elements with domain-specific methods.

    Attributes:
        requirement (requirement_specs.Requirement): The requirement object associated with the symbol.
        obj (requirement_specs.Requirement): Alias for the requirement attribute.
    """
    def __init__(self, obj):
        super(Symbol, self).__init__(obj)
        self.requirement = requirement_specs.Requirement(obj)
        self.obj = self.requirement

    def __call__(self, **kwargs):
        full_name = self.name
        if self.arg:
            full_name += '[' + self.arg + ']'
        val = kwargs.get(full_name, False)
        if self.requirement.has_version_specs():
            if type(val) is str:
                return val in self.requirement
            return False
        return bool(val)

    def __hash__(self):
        return hash(self.as_id())

    def __eq__(self, other):
        return hash(self) == hash(other)

    def __lt__(self, other):
        return self.as_id() < other.as_id()

    def as_id(self):
        id_str = self.name
        if self.arg:
            id_str += '_' + self.arg
        if self.requirement.has_version_specs():
            id_str += '_' + self.requirement.ver_specs.oval_id
        return id_str

    def as_dict(self):
        res = {
            'id': self.as_id(),
            'name': self.name,
            'arg': self.arg,
            'ver_specs': [],
            'ver_specs_id': '',
            'ver_specs_cpe': '',
            'ver_specs_title': '',
        }

        if self.requirement.has_version_specs():
            for ver_spec in sorted(self.requirement.ver_specs):
                res['ver_specs'].append({
                    'id': ver_spec.oval_id,
                    'op': ver_spec.op,
                    'ver': ver_spec.ver,
                    'evr_op': ver_spec.evr_op,
                    'evr_ver': ver_spec.evr_ver,
                    'ev_ver': ver_spec.ev_ver
                })
            res['ver_specs_id'] = self.requirement.ver_specs.oval_id
            res['ver_specs_cpe'] = self.requirement.ver_specs.cpe_id
            res['ver_specs_title'] = self.requirement.ver_specs.title

        return res

    def has_version_specs(self):
        return self.requirement.has_version_specs()

    @property
    def arg(self):
        return self.requirement.arg or ''

    @property
    def name(self):
        return self.requirement.name

    @staticmethod
    def is_parametrized(name):
        return requirement_specs.Requirement.is_parametrized(name)

    @staticmethod
    def get_base_of_parametrized_name(name):
        return requirement_specs.Requirement.get_base_for_parametrized(name)


class Algebra(boolean.BooleanAlgebra):
    """
    Base class for boolean algebra.

    The Algebra class parses and evaluates boolean expressions, where operators
    can be any combination of "~, &, |, !, *, +, not, and, or" and variable symbols
    can contain version specifiers as described in PEP440 and PEP508.
    - No white space is allowed inside specifier expressions.
    - The ~= specifier operator is not supported.

    Limitations:
    - no white space is allowed inside specifier expressions;
    - ~= specifier operator is not supported.

    Example:
        "(oranges>=2.0.8,<=5 | fried[banana]) and !pie[apple]"

    Attributes:
        symbol_cls (class): The class used for symbols in the boolean expressions.
        function_cls (class): The class used for functions in the boolean expressions.
    """
    def __init__(self, symbol_cls, function_cls):
        not_cls = type('FunctionNOT', (function_cls, boolean.NOT), {})
        and_cls = type('FunctionAND', (function_cls, boolean.AND), {})
        or_cls = type('FunctionOR', (function_cls, boolean.OR), {})
        super(Algebra, self).__init__(allowed_in_token=VERSION_SYMBOLS+SPEC_SYMBOLS,
                                      Symbol_class=symbol_cls,
                                      NOT_class=not_cls, AND_class=and_cls, OR_class=or_cls)