Beispiel #1
0
def test_tlv_message_with_not_operator_exhausting() -> None:
    message = Message(
        "TLV::Message_With_Not_Operator_Exhausting",
        [
            Link(INITIAL, Field("Tag")),
            Link(
                Field("Tag"),
                Field("Length"),
                Not(Not(Not(NotEqual(Variable("Tag"), Variable("Msg_Data"))))),
            ),
            Link(
                Field("Tag"),
                FINAL,
                reduce(
                    lambda acc, f: f(acc),
                    [Not, Not] * 16,
                    Not(
                        Or(
                            Not(
                                Not(
                                    Equal(Variable("Tag"),
                                          Variable("Msg_Data")))),
                            Not(Equal(Variable("Tag"), Variable("Msg_Error"))),
                        )),
                ),
            ),
            Link(Field("Length"),
                 Field("Value"),
                 size=Mul(Variable("Length"), Number(8))),
            Link(Field("Value"), FINAL),
        ],
        {
            Field("Tag"): TLV_TAG,
            Field("Length"): TLV_LENGTH,
            Field("Value"): OPAQUE
        },
    )

    with pytest.raises(
            FatalError,
            match=re.escape(
                "failed to simplify complex expression `not (not (not (not "
                "(not (not (not (not (not (not (not (not (not (not (not (not "
                "(not (not (not (not (not (not (not (not (not (not (not (not "
                "(not (not (not (not (not (not (not (Tag = TLV::Msg_Data))\n"
                "                                 "
                "or not (Tag = TLV::Msg_Error))))))))))))))))))))))))))))))))))` "
                "after `16` iterations, best effort: "
                "`not (not (not (not (not (not (not (not (not (not (not (not (not "
                "(not (not (not (not (Tag = TLV::Msg_Data\n"
                "                 or Tag /= TLV::Msg_Error)))))))))))))))))`"),
    ):
        model = PyRFLX(model=Model([TLV_TAG, TLV_LENGTH, message]))
        pkg = model.package("TLV")
        msg = pkg.new_message("Message_With_Not_Operator_Exhausting")
        test_bytes = b"\x01\x00\x04\x00\x00\x00\x00"
        msg.parse(test_bytes)
Beispiel #2
0
def test_tlv_message_with_not_operator() -> None:
    message = Message(
        "TLV::Message_With_Not_Operator",
        [
            Link(INITIAL, Field("Tag")),
            Link(
                Field("Tag"),
                Field("Length"),
                Not(Not(Not(NotEqual(Variable("Tag"), Variable("Msg_Data"))))),
            ),
            Link(
                Field("Tag"),
                FINAL,
                Not(
                    Not(
                        Not(
                            Or(
                                Not(
                                    Not(
                                        Equal(Variable("Tag"),
                                              Variable("Msg_Data")))),
                                Not(
                                    Equal(Variable("Tag"),
                                          Variable("Msg_Error"))),
                            )))),
            ),
            Link(Field("Length"),
                 Field("Value"),
                 size=Mul(Variable("Length"), Number(8))),
            Link(Field("Value"), FINAL),
        ],
        {
            Field("Tag"): TLV_TAG,
            Field("Length"): TLV_LENGTH,
            Field("Value"): OPAQUE
        },
    )

    model = PyRFLX(model=Model([TLV_TAG, TLV_LENGTH, message]))
    pkg = model.package("TLV")
    msg = pkg.new_message("Message_With_Not_Operator")
    test_bytes = b"\x01\x00\x04\x00\x00\x00\x00"
    msg.parse(test_bytes)
    assert msg.valid_message
    assert msg.bytestring == test_bytes
Beispiel #3
0
def fixture_aggregate_in_relation_package(pyrflx_: pyrflx.PyRFLX) -> pyrflx.Package:
    return pyrflx_.package("Aggregate_In_Relation")
Beispiel #4
0
def fixture_low_order_package(pyrflx_: pyrflx.PyRFLX) -> pyrflx.Package:
    return pyrflx_.package("Low_Order")
Beispiel #5
0
def fixture_endianness_package(pyrflx_: pyrflx.PyRFLX) -> pyrflx.Package:
    return pyrflx_.package("Endianness")
Beispiel #6
0
def fixture_parameterized_package(pyrflx_: pyrflx.PyRFLX) -> pyrflx.Package:
    return pyrflx_.package("Parameterized")
Beispiel #7
0
def fixture_always_valid_aspect_package(pyrflx_: pyrflx.PyRFLX) -> pyrflx.Package:
    return pyrflx_.package("Always_Valid_Aspect")
Beispiel #8
0
def fixture_message_type_size(pyrflx_: pyrflx.PyRFLX) -> pyrflx.Package:
    return pyrflx_.package("Message_Type_Size_Condition")
Beispiel #9
0
def fixture_sequence_message_package(pyrflx_: pyrflx.PyRFLX) -> pyrflx.Package:
    return pyrflx_.package("Sequence_Message")
Beispiel #10
0
def fixture_tlv_package(pyrflx_: pyrflx.PyRFLX) -> pyrflx.Package:
    return pyrflx_.package("TLV")
Beispiel #11
0
def fixture_udp_package(pyrflx_: pyrflx.PyRFLX) -> pyrflx.Package:
    return pyrflx_.package("UDP")
Beispiel #12
0
def fixture_tls_alert_package(pyrflx_: pyrflx.PyRFLX) -> pyrflx.Package:
    return pyrflx_.package("TLS_Alert")
Beispiel #13
0
def fixture_tls_record_package(pyrflx_: pyrflx.PyRFLX) -> pyrflx.Package:
    return pyrflx_.package("TLS_Record")
Beispiel #14
0
def fixture_ipv4_package(pyrflx_: pyrflx.PyRFLX) -> pyrflx.Package:
    return pyrflx_.package("IPv4")
Beispiel #15
0
class Validator:
    def __init__(
        self,
        files: Iterable[Union[str, Path]],
        checksum_module: str = None,
        skip_model_verification: bool = False,
        skip_message_verification: bool = False,
        split_disjunctions: bool = False,
    ):
        model = self._create_model([Path(f) for f in files],
                                   skip_model_verification, split_disjunctions)
        checksum_functions = self._parse_checksum_module(checksum_module)
        missing_checksum_definitions = {
            (str(message.identifier), str(field_identifier))
            for message in model.messages
            for field_identifier in message.checksums
        } - {(str(message_name), field_name)
             for message_name, checksum_mapping in checksum_functions.items()
             for field_name in checksum_mapping}

        if len(missing_checksum_definitions) != 0:
            raise ValidationError(
                "missing checksum definition for " + ", ".join([
                    f'field "{field_name}" of "{message_name}"' for
                    message_name, field_name in missing_checksum_definitions
                ]))

        try:
            self._pyrflx = PyRFLX(model, checksum_functions,
                                  skip_message_verification)
        except PyRFLXError as e:
            raise ValidationError(f"invalid checksum definition: {e}") from e

    def validate(
        self,
        message_identifier: ID,
        directory_invalid: Path = None,
        directory_valid: Path = None,
        json_output: Path = None,
        abort_on_error: bool = False,
        coverage: bool = False,
        target_coverage: float = 0.00,
    ) -> None:
        # pylint: disable = too-many-arguments, too-many-locals

        if target_coverage < 0 or target_coverage > 100:
            raise ValidationError(
                f"target coverage must be between 0 and 100, got {target_coverage}"
            )

        try:
            message_value = self._pyrflx.package(
                message_identifier.parent).new_message(message_identifier.name)
        except KeyError as e:
            raise ValidationError(
                f'message "{message_identifier.name}" could not be found '
                f'in package "{message_identifier.parent}"') from e

        incorrectly_classified = 0
        coverage_info = CoverageInformation(list(self._pyrflx), coverage)

        with OutputWriter(json_output) as output_writer:
            for directory_path, is_valid_directory in [
                (directory_valid, True),
                (directory_invalid, False),
            ]:
                directory = (sorted(directory_path.glob("*.raw"))
                             if directory_path is not None else [])
                for path in directory:
                    validation_result = self._validate_message(
                        path, is_valid_directory, message_value)
                    coverage_info.update(validation_result.parsed_message)
                    validation_result.print_console_output()
                    output_writer.write_result(validation_result)
                    if not validation_result.validation_success:
                        incorrectly_classified += 1
                        if abort_on_error:
                            raise ValidationError(
                                f"aborted: message {path} was classified incorrectly"
                            )
            coverage_info.print_coverage()

        error_msgs = []
        if incorrectly_classified != 0:
            error_msgs.append(
                f"{incorrectly_classified} messages were classified incorrectly"
            )
        if (coverage and coverage_info.total_covered_links /
                coverage_info.total_links < target_coverage / 100):
            error_msgs.append(
                f"missed target coverage of {target_coverage/100:.2%}, "
                f"reached {coverage_info.total_covered_links / coverage_info.total_links:.2%}"
            )
        if len(error_msgs) > 0:
            raise ValidationError("\n".join(e for e in error_msgs))

    def _create_model(self, files: List[Path], skip_model_verification: bool,
                      split_disjunctions: bool) -> Model:
        for f in files:
            if not f.is_file():
                raise ValidationError(f'specification file not found: "{f}"')
        parser = Parser(skip_model_verification)
        parser.parse(*files)
        model = parser.create_model()
        if split_disjunctions:
            messages: Dict[ID, Message] = {}
            for t in model.types:
                if isinstance(t, Message):
                    messages[t.identifier] = self._expand_message_links(
                        t, messages)
            model = Model(
                [self._replace_messages(t, messages) for t in model.types])
        return model

    def _expand_message_links(self, message: Message,
                              messages: Dict[ID, Message]) -> Message:
        """Split disjunctions in link conditions."""
        structure = []
        for link in message.structure:
            conditions = self._expand_expression(link.condition.simplified())

            if len(conditions) == 1:
                structure.append(link)
                continue

            for condition in conditions:
                structure.append(
                    Link(
                        link.source,
                        link.target,
                        condition,
                        link.size,
                        link.first,
                        condition.location,
                    ))

        types = {
            f: self._replace_messages(t, messages)
            for f, t in message.types.items()
        }

        return message.copy(structure=structure, types=types)

    @staticmethod
    def _replace_messages(type_: mty.Type,
                          messages: Dict[ID, Message]) -> mty.Type:
        """Recursively replace messages."""
        if isinstance(type_, Message):
            return messages[type_.identifier]
        if isinstance(type_, Refinement):
            return Refinement(
                type_.package,
                messages[type_.pdu.identifier],
                type_.field,
                messages[type_.sdu.identifier],
                type_.condition,
                type_.location,
            )
        if isinstance(type_, mty.Sequence) and isinstance(
                type_.element_type, Message):
            return mty.Sequence(
                type_.identifier,
                messages[type_.element_type.identifier],
                type_.location,
            )
        return type_

    @staticmethod
    def _expand_expression(expression: expr.Expr) -> List[expr.Expr]:
        """Create disjunction by expanding the expression and return it as a list."""
        if isinstance(expression, expr.Or):
            return expression.terms

        if not isinstance(expression, expr.And):
            return [expression]

        atoms = []
        disjunctions = []

        for e in expression.terms:
            if isinstance(e, expr.Or):
                disjunctions.append(e.terms)
            else:
                atoms.append(e)

        disjunctions.append([expr.And(*atoms)])

        result: List[expr.Expr] = []
        for value in (expr.And(*dict.fromkeys(p)).simplified()
                      for p in product(*disjunctions)):
            for seen in result:
                if expr.Not(expr.Equal(
                        value, seen)).check().result == expr.ProofResult.UNSAT:
                    break
            else:
                result.append(value)
        return result

    @staticmethod
    def _parse_checksum_module(
            name: Optional[str]) -> Dict[StrID, Dict[str, ChecksumFunction]]:
        if name is None:
            return {}

        checksum_functions = {}

        try:
            checksum_module = importlib.import_module(name)
        except ImportError as e:
            raise ValidationError(
                f'provided module "{name}" cannot be '
                f"imported, make sure module name is provided as "
                f'"package.module" and not as file system path: {e}') from e

        try:
            checksum_functions = checksum_module.checksum_functions
        except AttributeError as e:
            raise ValidationError(
                f'missing attribute "checksum_function" in checksum module "{name}"'
            ) from e

        if not isinstance(checksum_functions, dict):
            raise ValidationError(
                f'attribute "checksum_function" of "{name}" is not a dict')

        for message_id, checksum_field_mapping in checksum_functions.items():
            if not isinstance(checksum_field_mapping, dict):
                raise ValidationError(
                    f'value at key "{message_id}" is not a dict')
            for field_name, checksum_func_callable in checksum_field_mapping.items(
            ):
                if not callable(checksum_func_callable):
                    raise ValidationError(
                        f'value at key "{field_name}" is not a callable checksum function'
                    )

        return checksum_functions

    @staticmethod
    def _validate_message(message_path: Path, valid_original_message: bool,
                          message_value: MessageValue) -> "ValidationResult":
        if not message_path.is_file():
            raise ValidationError(f"{message_path} is not a regular file")

        parameters_path = message_path.with_suffix(".yaml")
        message_parameters: Dict[str, Union[bool, int, str]] = {}

        if parameters_path.is_file():
            yaml = YAML()
            message_parameters = yaml.load(parameters_path)

        original_message = message_path.read_bytes()
        parsed_message = message_value.clone()
        parser_error = None

        try:
            parsed_message.add_parameters(message_parameters)
        except PyRFLXError as e:
            raise ValidationError(f"{message_path}: {e}") from e

        try:
            parsed_message.parse(original_message)
            valid_parser_result = parsed_message.bytestring == original_message
            if not valid_parser_result:
                assert parsed_message.valid_message
                assert len(parsed_message.bytestring) <= len(original_message)
                assert original_message.startswith(parsed_message.bytestring)
                parser_error = "message parsed by PyRFLX is shorter than the original message"
        except PyRFLXError as e:
            parser_error = str(e)
            valid_parser_result = False

        return ValidationResult(
            valid_original_message == valid_parser_result,
            parsed_message,
            parser_error,
            message_path,
            original_message,
            valid_original_message,
            valid_parser_result,
        )
Beispiel #16
0
def fixture_ethernet_package(pyrflx_: pyrflx.PyRFLX) -> pyrflx.Package:
    return pyrflx_.package("Ethernet")
Beispiel #17
0
def fixture_icmp_package(pyrflx_: pyrflx.PyRFLX) -> pyrflx.Package:
    return pyrflx_.package("ICMP")
Beispiel #18
0
def fixture_message_size_package(pyrflx_: pyrflx.PyRFLX) -> pyrflx.Package:
    return pyrflx_.package("Message_Size")