#!/usr/bin/env python3 """ MeshCore Spec Corpus Validator Validates all test vector JSON files against the schema and checks internal consistency of test vectors (hex encoding, field relationships, etc.). Usage: python tools/validate.py [--verbose] [path...] If no paths given, validates all JSON files under corpus/. """ import argparse import json import re import struct import sys from pathlib import Path # Schema validation is optional (requires jsonschema) try: import jsonschema HAS_JSONSCHEMA = True except ImportError: HAS_JSONSCHEMA = False REPO_ROOT = Path(__file__).resolve().parent.parent CORPUS_DIR = REPO_ROOT / "corpus" SCHEMA_PATH = CORPUS_DIR / "schema" / "test-vector.schema.json" # Maps payload type names to their numeric values PAYLOAD_TYPE_MAP = { "request": 0x00, "response": 0x01, "txt_msg": 0x02, "ack": 0x03, "advert": 0x04, "grp_txt": 0x05, "grp_data": 0x06, "anon_req": 0x07, "path": 0x08, "trace": 0x09, "multipart": 0x0A, "control": 0x0B, "raw_custom": 0x0F, } ROUTE_TYPE_MAP = { "transport_flood": 0x00, "flood": 0x01, "direct": 0x02, "transport_direct": 0x03, } # Wire format sizes (bytes) HEADER_SIZE = 1 TRANSPORT_CODES_SIZE = 4 PATH_LEN_SIZE = 1 def strip_hex(s): """Remove whitespace from hex string and return uppercase.""" return re.sub(r'\s+', '', s).upper() def hex_to_bytes(s): """Convert hex string (with optional whitespace) to bytes.""" return bytes.fromhex(strip_hex(s)) def compute_header(version, payload_type, route_type): """Compute the header byte from structured fields.""" pt = PAYLOAD_TYPE_MAP.get(payload_type) rt = ROUTE_TYPE_MAP.get(route_type) if pt is None or rt is None: return None return (version << 6) | (pt << 2) | rt def compute_path_len_byte(hash_size, hash_count): """Compute the path_len byte from hash_size and hash_count.""" return ((hash_size - 1) << 6) | (hash_count & 0x3F) class ValidationError: """A single validation error with file location and optional vector context.""" def __init__(self, file_path, vector_id, message): self.file_path = file_path self.vector_id = vector_id self.message = message def __str__(self): loc = str(self.file_path) if self.vector_id: loc += f" [{self.vector_id}]" return f" ERROR: {loc}: {self.message}" def validate_json_syntax(file_path): """Validate that the file is valid JSON.""" try: with open(file_path, encoding="utf-8") as f: return json.load(f), [] except FileNotFoundError: return None, [ValidationError(file_path, None, f"File not found: {file_path}")] except json.JSONDecodeError as e: return None, [ValidationError(file_path, None, f"Invalid JSON: {e}")] def validate_schema(data, file_path, schema): """Validate against JSON Schema.""" errors = [] try: jsonschema.validate(data, schema) except jsonschema.ValidationError as e: errors.append(ValidationError(file_path, None, f"Schema: {e.message}")) return errors def validate_hex_string(hex_str, field_name, file_path, vector_id): """Validate a hex string is well-formed.""" errors = [] stripped = strip_hex(hex_str) if len(stripped) % 2 != 0: errors.append(ValidationError(file_path, vector_id, f"Odd-length hex in {field_name}: '{stripped}'")) if not re.match(r'^[0-9A-F]*$', stripped): errors.append(ValidationError(file_path, vector_id, f"Invalid or lowercase hex chars in {field_name} (must be uppercase)")) return errors def validate_vector_consistency(vector, file_path): """Validate internal consistency of a single test vector.""" errors = [] vid = vector.get("id", "?") vtype = vector.get("type") # Validate binary hex if "binary" in vector: errors.extend(validate_hex_string(vector["binary"], "binary", file_path, vid)) # For encode_decode vectors, verify header byte matches structured form if vtype in ("encode_decode", "decode_only") and "structured" in vector and "binary" in vector: structured = vector["structured"] binary = hex_to_bytes(vector["binary"]) if len(binary) < 1: errors.append(ValidationError(file_path, vid, "Binary too short for header")) return errors # Check header byte hdr = structured.get("header", {}) expected_header = compute_header( hdr.get("version", 0), hdr.get("payload_type", ""), hdr.get("route_type", "") ) if expected_header is not None and binary[0] != expected_header: errors.append(ValidationError(file_path, vid, f"Header mismatch: binary[0]=0x{binary[0]:02X}, " f"expected=0x{expected_header:02X}")) # Check transport codes presence and values rt = hdr.get("route_type", "") has_tc = rt in ("transport_flood", "transport_direct") offset = HEADER_SIZE if has_tc: if "transport_codes" not in structured: errors.append(ValidationError(file_path, vid, "Route type requires transport_codes but none in structured")) elif len(binary) >= HEADER_SIZE + TRANSPORT_CODES_SIZE: # Verify transport code LE values tc = structured["transport_codes"] tc0 = struct.unpack_from('