|
4 | 4 | import asn1tools |
5 | 5 | import glob |
6 | 6 |
|
7 | | -def get_schema_files(full = False): |
| 7 | + |
| 8 | +def get_schema_files(full=False): |
| 9 | + """Get the list of schema files for compilation. |
| 10 | + |
| 11 | + Args: |
| 12 | + full: If True, use full-const.asn, otherwise use tiny-const.asn |
| 13 | + |
| 14 | + Returns: |
| 15 | + List of absolute paths to schema files |
| 16 | + """ |
8 | 17 | script_dir = os.path.dirname(os.path.abspath(__file__)) |
9 | | - schema_files = [ os.path.join(script_dir, "jam-types.asn") ] |
| 18 | + schema_files = [os.path.join(script_dir, "jam-types.asn")] |
| 19 | + |
10 | 20 | if full: |
11 | | - schema_files += [ os.path.join(script_dir, "full-const.asn") ] |
| 21 | + schema_files.append(os.path.join(script_dir, "full-const.asn")) |
12 | 22 | else: |
13 | | - schema_files += [ os.path.join(script_dir, "tiny-const.asn") ] |
14 | | - return schema_files |
| 23 | + schema_files.append(os.path.join(script_dir, "tiny-const.asn")) |
15 | 24 |
|
| 25 | + return schema_files |
| 26 | + |
16 | 27 |
|
17 | | -# Tweaks: |
18 | | -# - Support for user defined tweak callback. This is called first. |
19 | | -# - JSON uses snake case, ASN.1 requires kebab case. |
20 | | -# - JSON prefix octet strings with '0x', ASN doesn't like it. |
21 | | -def make_asn1_parsable(json_str, json_tweaks_callback): |
| 28 | +def make_asn1_parsable(json_str, json_tweaks_callback=None): |
| 29 | + """Transform JSON to be parsable by ASN.1 schema. |
| 30 | + |
| 31 | + Transformations: |
| 32 | + - Apply user-defined tweak callback if provided |
| 33 | + - Convert snake_case to kebab-case (JSON uses snake case, ASN.1 requires kebab case) |
| 34 | + - Remove '0x' prefix from hex strings (ASN.1 doesn't like it) |
| 35 | + |
| 36 | + Args: |
| 37 | + json_str: Original JSON string |
| 38 | + json_tweaks_callback: Optional callback to modify JSON object |
| 39 | + |
| 40 | + Returns: |
| 41 | + Modified JSON string compatible with ASN.1 |
| 42 | + """ |
22 | 43 | if json_tweaks_callback is not None: |
23 | 44 | json_obj = json.loads(json_str) |
24 | 45 | json_obj = json_tweaks_callback(json_obj) |
25 | 46 | json_str = json.dumps(json_obj, indent=4) |
| 47 | + |
| 48 | + # Convert snake_case to kebab-case and remove hex prefixes |
26 | 49 | json_str = json_str.replace('_', '-').replace('0x', '') |
27 | 50 | return json_str |
28 | 51 |
|
29 | 52 |
|
30 | | -def path_to_root_type(path): |
31 | | - # Strip the directory path and extension |
32 | | - name = os.path.splitext(os.path.basename(path))[0] |
33 | | - # Remove "_X" if it ends with a number |
34 | | - name = re.sub(r'_\d+$', '', name) |
| 53 | +def path_to_type_name(path): |
| 54 | + """Convert file path to ASN.1 type name. |
| 55 | + |
| 56 | + Converts filename to PascalCase for ASN.1 type detection. |
| 57 | + |
| 58 | + Args: |
| 59 | + path: File path |
| 60 | + |
| 61 | + Returns: |
| 62 | + PascalCase type name |
| 63 | + """ |
| 64 | + # Strip directory path and extension |
| 65 | + name = os.path.splitext(os.path.basename(path))[0] |
| 66 | + # Remove "_X" suffix if it ends with a number |
| 67 | + name = re.sub(r'_\d+$', '', name) |
35 | 68 | # Convert kebab-case or snake_case to PascalCase |
36 | | - name = re.sub(r'[-_](\w)', lambda m: m.group(1).upper(), name) |
| 69 | + name = re.sub(r'[-_](\w)', lambda m: m.group(1).upper(), name) |
37 | 70 | # Capitalize the first character |
38 | | - name = name[0].upper() + name[1:] |
39 | | - return name |
| 71 | + name = name[0].upper() + name[1:] |
| 72 | + return name |
40 | 73 |
|
41 | 74 |
|
42 | | -def validate(schema, json_file, json_tweaks_callback = None): |
43 | | - print("* Validating: ", json_file) |
| 75 | +def validate(schema, json_file, json_tweaks_callback=None): |
| 76 | + """Validate a JSON file against an ASN.1 schema. |
| 77 | + |
| 78 | + The root type for decoding is determined as follows: |
| 79 | + - If the schema contains "TestCase", use that type |
| 80 | + - Otherwise, derive the type from the filename (e.g., "my_type.json" → "MyType") |
44 | 81 |
|
| 82 | + Args: |
| 83 | + schema: Compiled ASN.1 schema |
| 84 | + json_file: Path to JSON file to validate |
| 85 | + json_tweaks_callback: Optional callback to modify JSON before validation |
| 86 | + """ |
| 87 | + # Determine root type used for decoding |
45 | 88 | if "TestCase" in schema.types: |
46 | | - root_type = "TestCase" |
| 89 | + root_type = "TestCase" |
47 | 90 | else: |
48 | | - # Auto-detect root type from schema |
49 | | - root_type = path_to_root_type(json_file) |
50 | | - |
51 | | - # Decode from json using the schema |
52 | | - json_bytes = open(json_file, "rb").read() |
| 91 | + # Auto-detect root type from filename |
| 92 | + root_type = path_to_type_name(json_file) |
| 93 | + |
| 94 | + # Read and prepare JSON |
| 95 | + with open(json_file, "rb") as f: |
| 96 | + json_bytes = f.read() |
| 97 | + |
53 | 98 | json_str_org = json_bytes.decode('utf-8') |
54 | 99 | json_str_org = make_asn1_parsable(json_str_org, json_tweaks_callback) |
55 | | - |
| 100 | + |
| 101 | + # Validate by round-trip encoding/decoding |
56 | 102 | json_bytes = json_str_org.encode('utf-8') |
57 | 103 | decoded = schema.decode(root_type, json_bytes, check_constraints=True) |
58 | | - |
59 | | - # Encode to json using the schema |
60 | 104 | encoded = schema.encode(root_type, decoded, check_constraints=True) |
61 | | - # Original json uses snake case, asn1 requires kebab case |
| 105 | + |
| 106 | + # Normalize for comparison |
62 | 107 | json_str = encoded.decode('utf-8') |
63 | 108 | json_obj = json.loads(json_str) |
64 | | - # Strings are converted to arrays of characters, |
65 | | - # map back to single string. |
66 | | - json_str = json.dumps(json_obj, indent = 4) |
| 109 | + json_str = json.dumps(json_obj, indent=4) |
67 | 110 |
|
68 | | - assert (json_str.rstrip().lower() == json_str_org.rstrip().lower()) |
| 111 | + # Verify round-trip consistency |
| 112 | + assert json_str.rstrip().lower() == json_str_org.rstrip().lower() |
69 | 113 |
|
70 | | -def validate_group(group_name, group_schema, spec_name, json_tweaks_callback = None): |
| 114 | +def validate_group(group_name, group_schema, spec_name, json_tweaks_callback=None): |
| 115 | + """Validate a group of JSON files against an ASN.1 schema. |
| 116 | + |
| 117 | + Args: |
| 118 | + group_name: Name of the validation group (for display) |
| 119 | + group_schema: ASN.1 schema file name (or None for base schema only) |
| 120 | + spec_name: Specification name ("tiny", "full", or "data") |
| 121 | + json_tweaks_callback: Optional callback to modify JSON before validation |
| 122 | + """ |
71 | 123 | print(f"\n[Validating {group_name} ({spec_name})]") |
| 124 | + |
| 125 | + # Build schema file list |
72 | 126 | schema_files = get_schema_files(spec_name == "full") |
73 | 127 | if group_schema is not None: |
74 | | - schema_files += [group_schema] |
| 128 | + schema_files.append(group_schema) |
| 129 | + |
| 130 | + # Compile schema and validate all JSON files |
75 | 131 | schema = asn1tools.compile_files(schema_files, codec="jer") |
76 | 132 | for json_file in glob.glob(f"{spec_name}/*.json"): |
| 133 | + print("* Validating:", json_file) |
77 | 134 | validate(schema, json_file, json_tweaks_callback) |
0 commit comments