#!/usr/bin/env python3 """Fail-closed, profile-aware verifier for S03-H1 runtime configurations.""" import argparse import hashlib import sys from pathlib import Path EXPECTED_PROFILE_HASHES = { "TEST": "d037561b32760fbd0e2aec9f1eefc5efda27b1918eca4742c89092a8a2f7e92e", "DIAGNOSTIC": "cd999ff249452298d650a22c8fe5b0b03bf48c0735f4630befdec8209cbb13a3", } EXPECTED_DISPLAY_CLASSES = { "TEST": "GAMDataSource", "DIAGNOSTIC": "LoggerDataSource", } EXPECTED_GAMDISPLAY_BLOCK_SHA256 = ( "13b14b504dac43df5febf230cd069c5093d951726382b1d5dee8edf43d6814d0" ) EXPECTED_IOGAM_WRITER_BLOCK_SHA256 = ( "b3d54ad2fae08e6640bfa0be39ead9841906b4573a3b407e5ecf21340b5c5717" ) EXPECTED_FILEWRITER_BLOCK_SHA256 = ( "0bb949b9d72fa758bdfd8b2039336bc1fda071ecf038304402032de63f4fc455" ) PLACEHOLDER_LINE = ' Filename = "__MARTE_RUN_DIR__/s03_output.bin"' def digest(text): return hashlib.sha256(text.encode("utf-8")).hexdigest() def extract_block(text, start_marker, end_marker): if text.count(start_marker) != 1: raise ValueError(f"START_MARKER_COUNT:{start_marker!r}:{text.count(start_marker)}") start = text.index(start_marker) try: end = text.index(end_marker, start) except ValueError as exc: raise ValueError(f"END_MARKER_MISSING:{end_marker!r}") from exc return text[start:end] class Verification: def __init__(self): self.checks = {} self.first_error = "NONE" def check(self, name, condition, error_code): self.checks[name] = "PASS" if condition else "FAIL" if not condition and self.first_error == "NONE": self.first_error = error_code def emit(self): for name in sorted(self.checks): print(f"{name}={self.checks[name]}") print(f"PROFILE_ERROR_CODE={self.first_error}") def verify(mode, config_path, expected_filename): report = Verification() try: text = config_path.read_text(encoding="utf-8") except OSError as exc: print("CONFIG_READ_STATUS=FAIL") print(f"CONFIG_READ_ERROR={exc}") print("PROFILE_ERROR_CODE=CONFIG_READ_FAILED") print("CONFIG_PROFILE_VERIFICATION_STATUS=FAIL") return 1 expected_line = f' Filename = "{expected_filename}"' filename_count = sum(1 for line in text.splitlines() if line == expected_line) placeholder_count = text.count("__MARTE_RUN_DIR__") canonical = text if filename_count == 1: canonical = text.replace(expected_line, PLACEHOLDER_LINE, 1) canonical_hash = digest(canonical) expected_profile_hash = EXPECTED_PROFILE_HASHES[mode] expected_display_class = EXPECTED_DISPLAY_CLASSES[mode] print(f"VERIFIED_MODE={mode}") print(f"EXPECTED_FILENAME={expected_filename}") print(f"EXPECTED_FILENAME_COUNT={filename_count}") print(f"PLACEHOLDER_COUNT={placeholder_count}") print(f"CANONICAL_CONFIG_SHA256={canonical_hash}") print(f"EXPECTED_PROFILE_CONFIG_SHA256={expected_profile_hash}") report.check( "RUNTIME_FILENAME_STATUS", filename_count == 1 and placeholder_count == 0, "RUNTIME_FILENAME_MISMATCH", ) report.check( "GAMDISPLAY_OBJECT_STATUS", text.count("+GAMDisplay =") == 1, "GAMDISPLAY_OBJECT_COUNT_MISMATCH", ) report.check( "THREAD_ORDER_STATUS", text.count( "Functions = {GAMTimer SentinelProducer GAMDisplay IOGAM_Writer}" ) == 1, "THREAD_ORDER_MISMATCH", ) try: gamdisplay = extract_block( canonical, " +GAMDisplay = {\n", " +IOGAM_Writer = {\n", ) except ValueError as exc: print(f"GAMDISPLAY_EXTRACTION_ERROR={exc}") gamdisplay = "" display_checks = { "GAMDISPLAY_CLASS_COUNT": gamdisplay.count("Class = IOGAM") == 1, "GAMDISPLAY_COUNTER_COUNT": gamdisplay.count( " Counter = {\n" ) == 2, "GAMDISPLAY_TIME_COUNT": gamdisplay.count(" Time = {\n") == 2, "GAMDISPLAY_CYCLETIME_COUNT": gamdisplay.count( "State1_Thread1_CycleTime =" ) == 2, "GAMDISPLAY_SCALAR_A_COUNT": gamdisplay.count("SentinelScalarA =") == 2, "GAMDISPLAY_SCALAR_B_COUNT": gamdisplay.count("SentinelScalarB =") == 2, "GAMDISPLAY_VECTOR_COUNT": gamdisplay.count("SentinelVector =") == 2, "GAMDISPLAY_VECTOR_DIMENSION_COUNT": gamdisplay.count( "NumberOfDimensions = 1" ) == 2, "GAMDISPLAY_VECTOR_ELEMENTS_COUNT": gamdisplay.count( "NumberOfElements = 3" ) == 2, "GAMDISPLAY_OUTPUT_DATASOURCE_COUNT": gamdisplay.count( "DataSource = Display" ) == 6, } for name, passed in display_checks.items(): report.check(name, passed, f"{name}_MISMATCH") gamdisplay_hash = digest(gamdisplay) print(f"GAMDISPLAY_BLOCK_SHA256={gamdisplay_hash}") print( "EXPECTED_GAMDISPLAY_BLOCK_SHA256=" f"{EXPECTED_GAMDISPLAY_BLOCK_SHA256}" ) report.check( "GAMDISPLAY_BLOCK_STATUS", gamdisplay_hash == EXPECTED_GAMDISPLAY_BLOCK_SHA256, "GAMDISPLAY_BLOCK_HASH_MISMATCH", ) try: writer = extract_block( canonical, " +IOGAM_Writer = {\n", " }\n +Data = {\n", ) except ValueError as exc: print(f"IOGAM_WRITER_EXTRACTION_ERROR={exc}") writer = "" writer_hash = digest(writer) print(f"IOGAM_WRITER_BLOCK_SHA256={writer_hash}") print( "EXPECTED_IOGAM_WRITER_BLOCK_SHA256=" f"{EXPECTED_IOGAM_WRITER_BLOCK_SHA256}" ) report.check( "IOGAM_WRITER_INVARIANT_STATUS", writer_hash == EXPECTED_IOGAM_WRITER_BLOCK_SHA256, "IOGAM_WRITER_BLOCK_HASH_MISMATCH", ) try: filewriter = extract_block( canonical, " +FileWriter = {\n", " }\n +States = {\n", ) except ValueError as exc: print(f"FILEWRITER_EXTRACTION_ERROR={exc}") filewriter = "" filewriter_hash = digest(filewriter) print(f"FILEWRITER_BLOCK_SHA256={filewriter_hash}") print( "EXPECTED_FILEWRITER_BLOCK_SHA256=" f"{EXPECTED_FILEWRITER_BLOCK_SHA256}" ) report.check( "FILEWRITER_INVARIANT_STATUS", filewriter_hash == EXPECTED_FILEWRITER_BLOCK_SHA256, "FILEWRITER_BLOCK_HASH_MISMATCH", ) structural_expectations = { "CONSTANT_GAM_COUNT": (text.count("Class = ConstantGAM"), 1), "SENTINEL_SCALAR_A_DECLARATION_COUNT": ( text.count("SentinelScalarA ="), 6, ), "SENTINEL_SCALAR_B_DECLARATION_COUNT": ( text.count("SentinelScalarB ="), 6, ), "SENTINEL_VECTOR_DECLARATION_COUNT": ( text.count("SentinelVector ="), 6, ), "VECTOR_DIMENSION_COUNT": (text.count("NumberOfDimensions = 1"), 6), "VECTOR_CARDINALITY_COUNT": (text.count("NumberOfElements = 3"), 6), "FILEWRITER_CLASS_COUNT": ( text.count("Class = FileDataSource::FileWriter"), 1, ), "PLAIN_FILEWRITER_CLASS_COUNT": (text.count("Class = FileWriter"), 0), } for name, (observed, expected) in structural_expectations.items(): print(f"{name}={observed}") print(f"EXPECTED_{name}={expected}") report.check( f"{name}_STATUS", observed == expected, f"{name}_MISMATCH", ) expected_display_block = ( " +Display = {\n" f" Class = {expected_display_class}\n" " }\n" ) report.check( "DISPLAY_DATASOURCE_CLASS_STATUS", text.count(expected_display_block) == 1, "DISPLAY_DATASOURCE_CLASS_MISMATCH", ) report.check( "PROFILE_CANONICAL_HASH_STATUS", canonical_hash == expected_profile_hash, "PROFILE_CANONICAL_HASH_MISMATCH", ) report.emit() passed = all(value == "PASS" for value in report.checks.values()) print(f"CONFIG_PROFILE_VERIFICATION_STATUS={'PASS' if passed else 'FAIL'}") return 0 if passed else 1 def main(): parser = argparse.ArgumentParser() parser.add_argument("mode", choices=sorted(EXPECTED_PROFILE_HASHES)) parser.add_argument("configuration", type=Path) parser.add_argument("expected_filename") args = parser.parse_args() return verify(args.mode, args.configuration, args.expected_filename) if __name__ == "__main__": sys.exit(main())