from __future__ import annotations from pathlib import Path from typing import Optional from factory_private_config import get_optional, get_required, load_private_config from .models import DeviceCheckConfig, FlashPrivateConfig _ALLOWED_FLASH_MODE = {"qio", "qout", "dio", "dout", "keep"} _ALLOWED_FLASH_FREQ = { "keep", "80m", "60m", "48m", "40m", "30m", "26m", "24m", "20m", "16m", "15m", "12m", } _ALLOWED_FLASH_SIZE = { "keep", "detect", "256kb", "512kb", "1mb", "2mb", "2mb-c1", "4mb", "4mb-c1", "8mb", "16mb", "32mb", "64mb", "128mb", } _ALLOWED_FLASH_BEFORE = {"default-reset", "usb-reset", "no-reset", "no-reset-no-sync"} _ALLOWED_FLASH_AFTER = { "default-reset", "hard-reset", "soft-reset", "watchdog-reset", "no-reset", "no-reset-stub", "noreset", "none", "no", } def _coerce_str(value: object, label: str) -> str: if isinstance(value, (int, float)): return str(value) if isinstance(value, str) and value.strip(): return value.strip() raise ValueError(f"私有配置无效: {label}") def _optional_str(value: object) -> Optional[str]: if isinstance(value, str): text = value.strip() return text if text else None return None def _optional_bool_or_none(value: object) -> Optional[bool]: if value is None: return None if isinstance(value, bool): return value if isinstance(value, (int, float)): return bool(int(value)) if isinstance(value, str): text = value.strip().lower() if text in {"1", "true", "yes", "y", "on"}: return True if text in {"0", "false", "no", "n", "off"}: return False return None def _coerce_positive_float(value: Optional[float], fallback: float) -> float: if value is None: return fallback try: parsed = float(value) except (TypeError, ValueError): return fallback return max(1.0, parsed) def _coerce_positive_int(value: Optional[int], fallback: int) -> int: if value is None: return fallback try: parsed = int(value) except (TypeError, ValueError): return fallback return max(1, parsed) def _ui_text(ui_cfg: dict, key: str, fallback: str) -> str: value = ui_cfg.get(key) if isinstance(value, str) and value.strip(): return value.strip() return fallback def _coerce_offset_int(value: str, label: str) -> int: try: return int(value, 0) except ValueError as exc: raise ValueError(f"私有配置无效: {label}") from exc def _validate_allowed( value: str, label: str, allowed: set[str], *, normalize_dash: bool = False, ) -> str: normalized = value.strip().lower() if normalize_dash: normalized = normalized.replace("_", "-") if normalized not in allowed: allowed_text = ", ".join(sorted(allowed)) raise ValueError(f"私有配置无效: {label}={value}(允许: {allowed_text})") return normalized def _validate_app_layout_binding( layout: list[tuple[str, str]], app_bin_name: str, app_partition_offset: str, ) -> None: app_offsets = [offset for offset, name in layout if name == app_bin_name] if not app_offsets: raise ValueError("私有配置无效: flash.app_bin_name 必须出现在 flash.layout") app_layout_offsets = {_coerce_offset_int(offset, "flash.layout.offset") for offset in app_offsets} if len(app_layout_offsets) != 1: raise ValueError("私有配置无效: flash.layout 中 app_bin_name 对应多个偏移地址") app_offset = _coerce_offset_int(app_partition_offset, "flash.app_partition_offset") if app_offset not in app_layout_offsets: layout_offset = app_offsets[0] raise ValueError( "私有配置无效: flash.app_partition_offset 与 flash.layout 中 app_bin_name 的 offset 不一致" f"(app_partition_offset={app_partition_offset}, layout_offset={layout_offset})" ) def _load_flash_private_config( base_dir: Path, raw: Optional[dict] = None ) -> FlashPrivateConfig: if raw is None: raw = load_private_config(base_dir) flash_cfg = get_required(raw, ("flash",), (dict,)) chip = _coerce_str(get_required(flash_cfg, ("chip",), (str, int)), "flash.chip") baud = _coerce_str(get_required(flash_cfg, ("baud",), (str, int)), "flash.baud") bin_dir_raw = _coerce_str(get_required(flash_cfg, ("bin_dir",), (str,)), "flash.bin_dir") bin_dir = Path(bin_dir_raw) bin_enc_raw = get_optional(flash_cfg, ("bin_encryption",), None) if bin_enc_raw is not None: raise ValueError("私有配置无效: flash.bin_encryption 已移除。") layout_raw = get_required(flash_cfg, ("layout",), (list,)) layout: list[tuple[str, str]] = [] for entry in layout_raw: offset: Optional[str] = None name: Optional[str] = None if isinstance(entry, (list, tuple)) and len(entry) == 2: offset = _coerce_str(entry[0], "flash.layout.offset") name = _coerce_str(entry[1], "flash.layout.name") elif isinstance(entry, dict): offset = _coerce_str(entry.get("offset"), "flash.layout.offset") name = _coerce_str(entry.get("name"), "flash.layout.name") else: raise ValueError("私有配置无效: flash.layout") if name.endswith(".enc"): raise ValueError("私有配置无效: flash.layout 不应包含 .enc 后缀。") layout.append((offset, name)) if not layout: raise ValueError("私有配置无效: flash.layout 不能为空") app_bin_name = _coerce_str( get_required(flash_cfg, ("app_bin_name",), (str,)), "flash.app_bin_name" ) if app_bin_name.endswith(".enc"): raise ValueError("私有配置无效: flash.app_bin_name 不应包含 .enc 后缀。") app_partition_offset = _coerce_str( get_required(flash_cfg, ("app_partition_offset",), (str, int)), "flash.app_partition_offset", ) _validate_app_layout_binding(layout, app_bin_name, app_partition_offset) flash_args_raw = get_required(flash_cfg, ("flash_args",), (dict,)) use_stub_value = _optional_bool_or_none(get_optional(flash_args_raw, ("use_stub",), None)) flash_args = { "flash_mode": _validate_allowed( _coerce_str( get_required(flash_args_raw, ("flash_mode",), (str, int)), "flash.flash_args.flash_mode", ), "flash.flash_args.flash_mode", _ALLOWED_FLASH_MODE, ), "flash_freq": _validate_allowed( _coerce_str( get_required(flash_args_raw, ("flash_freq",), (str, int)), "flash.flash_args.flash_freq", ), "flash.flash_args.flash_freq", _ALLOWED_FLASH_FREQ, ), "flash_size": _validate_allowed( _coerce_str( get_required(flash_args_raw, ("flash_size",), (str, int)), "flash.flash_args.flash_size", ), "flash.flash_args.flash_size", _ALLOWED_FLASH_SIZE, ), "before": _validate_allowed( _coerce_str( get_required(flash_args_raw, ("before",), (str, int)), "flash.flash_args.before", ), "flash.flash_args.before", _ALLOWED_FLASH_BEFORE, normalize_dash=True, ), "after": _validate_allowed( _coerce_str( get_required(flash_args_raw, ("after",), (str, int)), "flash.flash_args.after", ), "flash.flash_args.after", _ALLOWED_FLASH_AFTER, normalize_dash=True, ), "use_stub": True if use_stub_value is None else use_stub_value, } check_cfg = get_required(raw, ("device_check",), (dict,)) crypt_cnt_fields_raw = get_required(check_cfg, ("crypt_cnt_fields",), (list,)) crypt_cnt_fields = tuple(_coerce_str(item, "device_check.crypt_cnt_fields") for item in crypt_cnt_fields_raw) crypt_cnt_expect = _optional_str(get_optional(check_cfg, ("crypt_cnt_expect",), None)) if crypt_cnt_expect: expect_norm = crypt_cnt_expect.strip().lower() if expect_norm not in {"odd", "even", "zero", "any"}: raise ValueError("私有配置无效: device_check.crypt_cnt_expect") crypt_cnt_expect = expect_norm secure_boot_expected = _optional_bool_or_none( get_optional(check_cfg, ("secure_boot_expected",), None) ) secure_boot_field = _coerce_str( get_required(check_cfg, ("secure_boot_field",), (str,)), "device_check.secure_boot_field" ) flash_key_purpose_match = _optional_str( get_optional(check_cfg, ("flash_key_purpose_match",), None) ) secure_boot_key_purpose_match = _optional_str( get_optional(check_cfg, ("secure_boot_key_purpose_match",), None) ) key_read_disable_field = _coerce_str( get_required(check_cfg, ("key_read_disable_field",), (str,)), "device_check.key_read_disable_field", ) ui_cfg = get_optional(raw, ("ui",), {}) if not isinstance(ui_cfg, dict): ui_cfg = {} return FlashPrivateConfig( chip=chip, baud=baud, bin_dir=bin_dir, layout=layout, app_bin_name=app_bin_name, app_partition_offset=app_partition_offset, flash_args=flash_args, device_check=DeviceCheckConfig( crypt_cnt_fields=crypt_cnt_fields, secure_boot_field=secure_boot_field, secure_boot_expected=secure_boot_expected, crypt_cnt_expect=crypt_cnt_expect, flash_key_purpose_match=flash_key_purpose_match, secure_boot_key_purpose_match=secure_boot_key_purpose_match, key_read_disable_field=key_read_disable_field, ), ui=ui_cfg, )