298 lines
9.8 KiB
Python
298 lines
9.8 KiB
Python
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,
|
||
)
|