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)
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
def fixture_aggregate_in_relation_package(pyrflx_: pyrflx.PyRFLX) -> pyrflx.Package: return pyrflx_.package("Aggregate_In_Relation")
def fixture_low_order_package(pyrflx_: pyrflx.PyRFLX) -> pyrflx.Package: return pyrflx_.package("Low_Order")
def fixture_endianness_package(pyrflx_: pyrflx.PyRFLX) -> pyrflx.Package: return pyrflx_.package("Endianness")
def fixture_parameterized_package(pyrflx_: pyrflx.PyRFLX) -> pyrflx.Package: return pyrflx_.package("Parameterized")
def fixture_always_valid_aspect_package(pyrflx_: pyrflx.PyRFLX) -> pyrflx.Package: return pyrflx_.package("Always_Valid_Aspect")
def fixture_message_type_size(pyrflx_: pyrflx.PyRFLX) -> pyrflx.Package: return pyrflx_.package("Message_Type_Size_Condition")
def fixture_sequence_message_package(pyrflx_: pyrflx.PyRFLX) -> pyrflx.Package: return pyrflx_.package("Sequence_Message")
def fixture_tlv_package(pyrflx_: pyrflx.PyRFLX) -> pyrflx.Package: return pyrflx_.package("TLV")
def fixture_udp_package(pyrflx_: pyrflx.PyRFLX) -> pyrflx.Package: return pyrflx_.package("UDP")
def fixture_tls_alert_package(pyrflx_: pyrflx.PyRFLX) -> pyrflx.Package: return pyrflx_.package("TLS_Alert")
def fixture_tls_record_package(pyrflx_: pyrflx.PyRFLX) -> pyrflx.Package: return pyrflx_.package("TLS_Record")
def fixture_ipv4_package(pyrflx_: pyrflx.PyRFLX) -> pyrflx.Package: return pyrflx_.package("IPv4")
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, )
def fixture_ethernet_package(pyrflx_: pyrflx.PyRFLX) -> pyrflx.Package: return pyrflx_.package("Ethernet")
def fixture_icmp_package(pyrflx_: pyrflx.PyRFLX) -> pyrflx.Package: return pyrflx_.package("ICMP")
def fixture_message_size_package(pyrflx_: pyrflx.PyRFLX) -> pyrflx.Package: return pyrflx_.package("Message_Size")