Files

183 lines
7.7 KiB
Python
Executable File

#!/usr/bin/env python3
"""Validate hardware evidence and its explicit sensor_cfg.ini expectations."""
from __future__ import annotations
import argparse
import configparser
import hashlib
import json
import os
import sys
import tempfile
from pathlib import Path
def sha256_file(path: Path) -> str:
value = hashlib.sha256()
with path.open("rb") as handle:
for block in iter(lambda: handle.read(1024 * 1024), b""):
value.update(block)
return value.hexdigest()
def write_json(path: Path, payload: dict[str, object]) -> None:
path = path.expanduser().resolve(strict=False)
path.parent.mkdir(parents=True, exist_ok=True)
with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=path.parent, delete=False) as handle:
json.dump(payload, handle, ensure_ascii=False, indent=2)
handle.write("\n")
temporary = Path(handle.name)
os.replace(temporary, path)
def normalized(value: object) -> str:
text = str(value).strip()
try:
return str(int(text, 0))
except ValueError:
return text.casefold()
def validate(psp: Path, topology_path: Path) -> dict[str, object]:
errors: list[str] = []
psp = psp.expanduser().resolve(strict=True)
topology_path = topology_path.expanduser().resolve(strict=True)
topology = json.loads(topology_path.read_text(encoding="utf-8"))
if not isinstance(topology, dict) or topology.get("schema_version") != 1:
raise ValueError("hardware-topology.json 必须是 schema_version=1 的对象")
for field in ("project", "chip", "system_mode", "storage"):
if not topology.get(field):
errors.append(f"硬件拓扑缺少字段: {field}")
storage = topology.get("storage")
if not isinstance(storage, dict) or not storage.get("type") or not isinstance(storage.get("capacity_bytes"), int):
errors.append("storage 必须包含 type 和整数 capacity_bytes;型号未知时可省略 model")
sources = topology.get("sources")
source_results: list[dict[str, object]] = []
if not isinstance(sources, list) or not sources:
errors.append("硬件拓扑至少需要一个可校验证据源")
sources = []
for index, item in enumerate(sources):
if not isinstance(item, dict):
errors.append(f"sources[{index}] 必须是对象")
continue
raw_path, expected = item.get("path"), item.get("sha256")
if not isinstance(raw_path, str) or not raw_path:
errors.append(f"sources[{index}].path 不能为空")
continue
source_path = Path(raw_path).expanduser()
if not source_path.is_absolute():
source_path = (topology_path.parent / source_path).resolve(strict=False)
exists = source_path.is_file() and not source_path.is_symlink()
actual = sha256_file(source_path) if exists else None
if not exists:
errors.append(f"硬件证据源不存在或不是普通文件: {source_path}")
if not isinstance(expected, str) or len(expected) != 64 or expected != actual:
errors.append(f"硬件证据源 hash 不匹配: {source_path}")
source_results.append({"path": str(source_path), "expected_sha256": expected, "actual_sha256": actual})
conflicts = topology.get("conflicts")
if not isinstance(conflicts, list):
errors.append("conflicts 必须是数组")
elif conflicts:
errors.append("硬件资料仍有未解决冲突,禁止进入 PSP 适配")
sensor_files = sorted(
path for path in psp.rglob("sensor_cfg.ini")
if path.is_file() and not path.is_symlink() and ".git" not in path.parts
)
if len(sensor_files) != 1:
errors.append(f"必须唯一定位 sensor_cfg.ini,实际 {len(sensor_files)} 个")
parser = None
else:
parser = configparser.ConfigParser(interpolation=None, strict=False)
try:
parser.read(sensor_files[0], encoding="utf-8")
except configparser.Error as exc:
errors.append(f"sensor_cfg.ini 解析失败: {exc}")
parser = None
sensors = topology.get("sensors")
if not isinstance(sensors, list) or not sensors:
errors.append("sensors 必须是非空数组")
sensors = []
required_sensor_fields = (
"section", "model", "enum_name", "i2c_bus", "i2c_address",
"mipi_device", "lane_count", "evidence_sources", "config_expectations",
)
seen_sections: set[str] = set()
checks: list[dict[str, object]] = []
for index, sensor in enumerate(sensors):
if not isinstance(sensor, dict):
errors.append(f"sensors[{index}] 必须是对象")
continue
missing = [field for field in required_sensor_fields if field not in sensor]
if missing:
errors.append(f"sensors[{index}] 缺少字段: {missing}")
continue
section = str(sensor["section"])
if section.casefold() in seen_sections:
errors.append(f"Sensor section 重复: {section}")
seen_sections.add(section.casefold())
evidence_indexes = sensor.get("evidence_sources")
if not isinstance(evidence_indexes, list) or not evidence_indexes or any(
not isinstance(value, int) or value < 0 or value >= len(sources)
for value in evidence_indexes
):
errors.append(f"{section}.evidence_sources 必须引用有效 sources 索引")
expectations = sensor.get("config_expectations")
row_checks: list[dict[str, object]] = []
if not isinstance(expectations, dict) or not expectations:
errors.append(f"{section}.config_expectations 必须明确列出配置字段")
elif parser is None or not parser.has_section(section):
errors.append(f"sensor_cfg.ini 缺少硬件拓扑声明的 section: {section}")
else:
for key, expected in sorted(expectations.items()):
actual = parser.get(section, key, fallback=None)
matched = actual is not None and normalized(actual) == normalized(expected)
row_checks.append({"key": key, "expected": expected, "actual": actual, "matched": matched})
if not matched:
errors.append(f"{section}.{key} 与硬件拓扑不一致:期望 {expected!r},实际 {actual!r}")
checks.append({"section": section, "model": sensor.get("model"), "checks": row_checks})
configured_sections = [] if parser is None else [
name for name in parser.sections() if name.casefold().startswith("sensor")
]
if len(configured_sections) != len(sensors):
errors.append(f"Sensor 数量不一致:拓扑 {len(sensors)},配置 {len(configured_sections)}")
return {
"schema_version": 1,
"passed": not errors,
"topology_path": str(topology_path),
"topology_sha256": sha256_file(topology_path),
"psp": str(psp),
"sensor_cfg": str(sensor_files[0]) if len(sensor_files) == 1 else None,
"sources": source_results,
"sensor_checks": checks,
"errors": errors,
}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--psp", type=Path, required=True)
parser.add_argument("--topology", type=Path, required=True)
parser.add_argument("--output", type=Path)
args = parser.parse_args()
try:
result = validate(args.psp, args.topology)
if args.output:
write_json(args.output, result)
print(json.dumps(result, ensure_ascii=False, indent=2))
return 0 if result["passed"] else 2
except (OSError, UnicodeError, ValueError, json.JSONDecodeError) as exc:
print(json.dumps({"passed": False, "error": str(exc)}, ensure_ascii=False, indent=2))
return 2
if __name__ == "__main__":
raise SystemExit(main())