Skip to content

Commit f0808ea

Browse files
committed
Add xml_validator script to check attribute types
Signed-off-by: Nathaniel Mitchell <[email protected]>
1 parent 892ad61 commit f0808ea

File tree

2 files changed

+228
-0
lines changed

2 files changed

+228
-0
lines changed

.github/workflows/tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,7 @@ jobs:
302302
- name: Run xml cfg checker
303303
run: |
304304
python tests/cfg_checker.py
305+
python tests/xml_validator.py
305306
306307
- name: pylint pilot for modules folder
307308
run: |

tests/xml_validator.py

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
# CHIPSEC: Platform Security Assessment Framework
2+
# Copyright (c) 2025, Intel Corporation
3+
#
4+
# This program is free software; you can redistribute it and/or
5+
# modify it under the terms of the GNU General Public License
6+
# as published by the Free Software Foundation; Version 2.
7+
#
8+
# This program is distributed in the hope that it will be useful,
9+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
# GNU General Public License for more details.
12+
13+
import xml.etree.ElementTree as ET
14+
from typing import Dict, List, Tuple
15+
import os.path as op
16+
import sys
17+
from os import listdir
18+
19+
sys.path.append(op.abspath(op.join(__file__, "..", "..")))
20+
from chipsec.library.file import get_main_dir
21+
from chipsec.library.defines import is_hex
22+
23+
CFG_PATH = op.join(get_main_dir(), 'chipsec', 'cfg') # Where to look for config files
24+
25+
"""
26+
Usage: python xml_validator.py [config_file1] [config_file2] ...
27+
Run with no arguments to vlidatate all XML config files in the default cfg directory.
28+
"""
29+
30+
class ConfigAttributeValidator:
31+
"""Validates XML configuration attributes against expected data types"""
32+
33+
def __init__(self):
34+
# Define expected attribute types based on _config_convert_data function
35+
self.integer_attrs = {'dev', 'fun', 'vid', 'did', 'rid', 'offset',
36+
'bit', 'size', 'port', 'msr', 'value', 'address',
37+
'fixed_address', 'base_align', 'align_bits', 'mask',
38+
'reg_align', 'limit_align', 'regh_align', 'width', 'reg'}
39+
40+
self.boolean_attrs = {'req_pch'}
41+
self.int_list_attrs = {'bus'}
42+
self.str_list_attrs = {'config'}
43+
self.range_list_attrs = {'detection_value'}
44+
self.validation_errors = []
45+
self.passed_file_count = 0
46+
self.file_count = 0
47+
48+
49+
def validate_integer_value(self, value: str) -> bool:
50+
"""Validate if a string can be converted to integer (base 10 or 16)"""
51+
try:
52+
int(value, 0)
53+
return True
54+
except ValueError:
55+
return False
56+
57+
def validate_boolean_value(self, value: str) -> bool:
58+
"""Validate if a string represents a boolean value"""
59+
return value.lower() in ('true', 'false')
60+
61+
def validate_integer_list(self, value: str) -> bool:
62+
"""Validate comma-separated integer values"""
63+
try:
64+
for item in value.split(','):
65+
int(item.strip(), 0)
66+
return True
67+
except ValueError:
68+
return False
69+
70+
def validate_range_format(self, value: str) -> bool:
71+
"""Validate range format (wildcards, ranges, single values)"""
72+
try:
73+
for item in value.split(','):
74+
item = item.strip()
75+
if item.upper().endswith('*'):
76+
int(item.replace('*', '0'), 0)
77+
elif '-' in item:
78+
parts = item.split('-', 1)
79+
if len(parts) != 2:
80+
return False
81+
int(parts[0], 0)
82+
int(parts[1], 0)
83+
else:
84+
int(item, 0)
85+
return True
86+
except ValueError:
87+
return False
88+
89+
def validate_attribute(self, attr_name: str, attr_value: str, element_tag: str, did_is_range: bool = False) -> bool:
90+
"""Validate a single attribute based on its expected type"""
91+
# Handle special case where 'did' can be range format
92+
if did_is_range and attr_name == 'did':
93+
if not self.validate_range_format(attr_value):
94+
self.validation_errors.append(
95+
f"Element '{element_tag}': Attribute '{attr_name}' has invalid range format: '{attr_value}'"
96+
)
97+
return False
98+
elif attr_name in self.integer_attrs:
99+
if not self.validate_integer_value(attr_value):
100+
self.validation_errors.append(
101+
f"Element '{element_tag}': Attribute '{attr_name}' must be integer: '{attr_value}'"
102+
)
103+
return False
104+
elif attr_name in self.boolean_attrs:
105+
if not self.validate_boolean_value(attr_value):
106+
self.validation_errors.append(
107+
f"Element '{element_tag}': Attribute '{attr_name}' must be boolean (true/false): '{attr_value}'"
108+
)
109+
return False
110+
elif attr_name in self.int_list_attrs:
111+
if not self.validate_integer_list(attr_value):
112+
self.validation_errors.append(
113+
f"Element '{element_tag}': Attribute '{attr_name}' must be comma-separated integers: '{attr_value}'"
114+
)
115+
return False
116+
elif attr_name in self.range_list_attrs:
117+
if not self.validate_range_format(attr_value):
118+
self.validation_errors.append(
119+
f"Element '{element_tag}': Attribute '{attr_name}' has invalid range format: '{attr_value}'"
120+
)
121+
return False
122+
123+
return True
124+
125+
def validate_xml_element(self, element: ET.Element, did_is_range: bool = False) -> bool:
126+
"""Validate all attributes of an XML element"""
127+
element_valid = True
128+
129+
for attr_name, attr_value in element.attrib.items():
130+
if not self.validate_attribute(attr_name, attr_value, element.tag, did_is_range):
131+
element_valid = False
132+
133+
return element_valid
134+
135+
def validate_xml_file(self, file_path: str) -> Tuple[bool, List[str]]:
136+
"""Validate an entire XML configuration file"""
137+
self.validation_errors = []
138+
139+
try:
140+
tree = ET.parse(file_path)
141+
root = tree.getroot()
142+
except ET.ParseError as e:
143+
self.validation_errors.append(f"XML parsing error in '{file_path}': {e}")
144+
return False, self.validation_errors
145+
except FileNotFoundError:
146+
self.validation_errors.append(f"File not found: '{file_path}'")
147+
return False, self.validation_errors
148+
149+
file_valid = True
150+
151+
# Validate different element types with appropriate settings
152+
for element in root.iter():
153+
if element.tag in ('device', 'sku'):
154+
# These elements can have 'did' as range
155+
if not self.validate_xml_element(element, did_is_range=True):
156+
file_valid = False
157+
else:
158+
if not self.validate_xml_element(element):
159+
file_valid = False
160+
161+
return file_valid, self.validation_errors
162+
163+
def print_validation_report(self, file_path: str):
164+
"""Print a validation report for a configuration file"""
165+
is_valid, errors = self.validate_xml_file(file_path)
166+
self.file_count += 1
167+
message = "\t"
168+
if is_valid:
169+
message += f"✓ Configuration file is valid for: {file_path}"
170+
self.passed_file_count += 1
171+
else:
172+
message += f"✗ Found {len(errors)} validation errors in {file_path}:"
173+
for error in errors:
174+
message += f"\n\t- {error}"
175+
print(message)
176+
177+
def print_summary(self):
178+
"""Print a summary of validation results"""
179+
print(f"\nValidation Summary: {self.passed_file_count}/{self.file_count} files passed validation.")
180+
181+
182+
def validate_configuration_files(file_paths: List[str]) -> Dict[str, bool]:
183+
"""Validate multiple configuration files and return results"""
184+
validator = ConfigAttributeValidator()
185+
results = {}
186+
print("Validation Report:\n")
187+
for file_path in file_paths:
188+
is_valid, _ = validator.validate_xml_file(file_path)
189+
results[file_path] = is_valid
190+
validator.print_validation_report(file_path)
191+
validator.print_summary()
192+
return results
193+
194+
195+
def find_xml_files(paths: List[str]) -> List[str]:
196+
files = []
197+
dirs = []
198+
for path in paths:
199+
for cfg_file in listdir(path):
200+
filepath = op.join(path, cfg_file)
201+
if op.isdir(filepath):
202+
dirs.append(filepath)
203+
if filepath.endswith('.xml'):
204+
files.append(filepath)
205+
if dirs:
206+
files.extend(find_xml_files(dirs))
207+
return files
208+
209+
210+
def find_base_config_dirs() -> List[str]:
211+
vid_list = [f for f in listdir(CFG_PATH) if op.isdir(op.join(CFG_PATH, f)) and is_hex(f)]
212+
base_dirs = [op.join(CFG_PATH, vid) for vid in vid_list]
213+
return base_dirs
214+
215+
216+
if __name__ == "__main__":
217+
import sys
218+
219+
if len(sys.argv) < 2:
220+
results = validate_configuration_files(find_xml_files(find_base_config_dirs()))
221+
else:
222+
results = validate_configuration_files(sys.argv[1:])
223+
224+
if all(results.values()):
225+
sys.exit(0)
226+
else:
227+
sys.exit(1)

0 commit comments

Comments
 (0)