初始化奇妙小屏幕控制器项目
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import ipaddress
|
||||
import json
|
||||
import re
|
||||
import struct
|
||||
from typing import Any
|
||||
|
||||
|
||||
PRODUCT_ID = "matrix-screen-controller-walnutpi"
|
||||
CONFIG_FILE_BYTES = 64 * 1024
|
||||
HEADER_BYTES = 4096
|
||||
SLOT_BYTES = (CONFIG_FILE_BYTES - HEADER_BYTES) // 2
|
||||
FILE_MAGIC = b"MSCCFG2\0"
|
||||
SLOT_MAGIC = b"MSCSLOT\0"
|
||||
FORMAT_VERSION = 1
|
||||
_HEADER = struct.Struct("<8sII")
|
||||
_SLOT_HEADER = struct.Struct("<8sQI32s")
|
||||
_USERNAME = re.compile(r"[a-z_][a-z0-9_-]{0,31}", re.ASCII)
|
||||
_VERSION = re.compile(r"(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)", re.ASCII)
|
||||
|
||||
|
||||
class ImageConfigError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def _exact_object(value: Any, fields: set[str], label: str) -> dict[str, Any]:
|
||||
if not isinstance(value, dict) or set(value) != fields:
|
||||
raise ImageConfigError(f"{label}字段无效")
|
||||
return value
|
||||
|
||||
|
||||
def _utf16_units(value: str) -> int:
|
||||
# .NET String.Length counts UTF-16 code units; retain 1.0.1 validation semantics.
|
||||
return len(value.encode("utf-16-le")) // 2
|
||||
|
||||
|
||||
def _validate_secret(value: Any, label: str, minimum: int, maximum: int) -> str:
|
||||
if not isinstance(value, str) or not minimum <= _utf16_units(value) <= maximum:
|
||||
raise ImageConfigError(f"{label}长度或字符无效")
|
||||
if any(character in value for character in ("\0", "\r", "\n")):
|
||||
raise ImageConfigError(f"{label}长度或字符无效")
|
||||
return value
|
||||
|
||||
|
||||
def _validate_text(value: Any, label: str, minimum: int, maximum_bytes: int) -> str:
|
||||
if not isinstance(value, str) or _utf16_units(value) < minimum or len(value.encode("utf-8")) > maximum_bytes:
|
||||
raise ImageConfigError(f"{label}长度或字符无效")
|
||||
if any(character in value for character in ("\0", "\r", "\n")):
|
||||
raise ImageConfigError(f"{label}长度或字符无效")
|
||||
return value
|
||||
|
||||
|
||||
def validate_config(value: Any) -> dict[str, Any]:
|
||||
source = _exact_object(
|
||||
value,
|
||||
{"schema_version", "product", "software_version", "account", "wifi", "ipv4"},
|
||||
"镜像配置",
|
||||
)
|
||||
schema = source["schema_version"]
|
||||
if type(schema) is not int or schema != FORMAT_VERSION or source["product"] != PRODUCT_ID:
|
||||
raise ImageConfigError("镜像产品或配置版本不受支持")
|
||||
software_version = source["software_version"]
|
||||
if not isinstance(software_version, str) or _VERSION.fullmatch(software_version) is None:
|
||||
raise ImageConfigError("软件版本格式无效")
|
||||
|
||||
account = _exact_object(source["account"], {"username", "password"}, "账户配置")
|
||||
username = account["username"]
|
||||
if not isinstance(username, str) or _USERNAME.fullmatch(username) is None:
|
||||
raise ImageConfigError("用户名只能使用小写字母、数字、下划线和连字符,且最长 32 个字符")
|
||||
account_password = _validate_secret(account["password"], "账户密码", 8, 128)
|
||||
|
||||
wifi = _exact_object(source["wifi"], {"ssid", "password"}, "Wi-Fi 配置")
|
||||
ssid = _validate_text(wifi["ssid"], "Wi-Fi 名称", 1, 32)
|
||||
wifi_password = _validate_secret(wifi["password"], "Wi-Fi 密码", 8, 63)
|
||||
|
||||
ipv4 = _exact_object(source["ipv4"], {"mode", "address", "prefix", "gateway", "dns"}, "IPv4 配置")
|
||||
mode = ipv4["mode"]
|
||||
if mode == "dhcp":
|
||||
normalized_ipv4: dict[str, Any] = {"mode": "dhcp", "address": "", "prefix": 0, "gateway": "", "dns": []}
|
||||
elif mode == "static":
|
||||
try:
|
||||
address = ipaddress.IPv4Address(ipv4["address"])
|
||||
gateway = ipaddress.IPv4Address(ipv4["gateway"])
|
||||
except (ipaddress.AddressValueError, TypeError) as exception:
|
||||
raise ImageConfigError("静态 IPv4 地址或网关无效") from exception
|
||||
prefix = ipv4["prefix"]
|
||||
if type(prefix) is not int or not 1 <= prefix <= 32:
|
||||
raise ImageConfigError("IPv4 前缀必须是 1 到 32")
|
||||
if gateway not in ipaddress.IPv4Network(f"{address}/{prefix}", strict=False):
|
||||
raise ImageConfigError("静态 IPv4 网关必须与地址处于同一子网")
|
||||
dns_source = ipv4["dns"]
|
||||
if not isinstance(dns_source, list) or not 1 <= len(dns_source) <= 4:
|
||||
raise ImageConfigError("DNS 必须包含 1 到 4 个 IPv4 地址")
|
||||
try:
|
||||
dns = [str(ipaddress.IPv4Address(item)) for item in dns_source]
|
||||
except (ipaddress.AddressValueError, TypeError) as exception:
|
||||
raise ImageConfigError("DNS 必须包含 1 到 4 个 IPv4 地址") from exception
|
||||
normalized_ipv4 = {
|
||||
"mode": "static",
|
||||
"address": str(ipv4["address"]),
|
||||
"prefix": prefix,
|
||||
"gateway": str(ipv4["gateway"]),
|
||||
"dns": dns,
|
||||
}
|
||||
else:
|
||||
raise ImageConfigError("IPv4 模式无效")
|
||||
|
||||
return {
|
||||
"schema_version": FORMAT_VERSION,
|
||||
"product": PRODUCT_ID,
|
||||
"software_version": software_version,
|
||||
"account": {"username": username, "password": account_password},
|
||||
"wifi": {"ssid": ssid, "password": wifi_password},
|
||||
"ipv4": normalized_ipv4,
|
||||
}
|
||||
|
||||
|
||||
def serialize_config(value: Any) -> str:
|
||||
return json.dumps(validate_config(value), ensure_ascii=False, separators=(",", ":"))
|
||||
|
||||
|
||||
def deserialize_config(text: str) -> dict[str, Any]:
|
||||
try:
|
||||
value = json.loads(text)
|
||||
except (UnicodeError, json.JSONDecodeError) as exception:
|
||||
raise ImageConfigError("配置 JSON 无效") from exception
|
||||
return validate_config(value)
|
||||
|
||||
|
||||
def _slot_offset(index: int) -> int:
|
||||
return HEADER_BYTES + index * SLOT_BYTES
|
||||
|
||||
|
||||
def _encode_slot(config: Any, generation: int) -> bytes:
|
||||
payload = (serialize_config(config) + "\n").encode("utf-8")
|
||||
if len(payload) > SLOT_BYTES - _SLOT_HEADER.size:
|
||||
raise ImageConfigError("配置内容过大")
|
||||
header = _SLOT_HEADER.pack(SLOT_MAGIC, generation, len(payload), hashlib.sha256(payload).digest())
|
||||
return header + payload + bytes(SLOT_BYTES - len(header) - len(payload))
|
||||
|
||||
|
||||
def create_config_file(config: Any) -> bytes:
|
||||
result = bytearray(CONFIG_FILE_BYTES)
|
||||
_HEADER.pack_into(result, 0, FILE_MAGIC, FORMAT_VERSION, CONFIG_FILE_BYTES)
|
||||
result[_slot_offset(0) : _slot_offset(0) + SLOT_BYTES] = _encode_slot(config, 1)
|
||||
return bytes(result)
|
||||
|
||||
|
||||
def read_config_file(data: bytes) -> tuple[dict[str, Any], int, int]:
|
||||
if len(data) != CONFIG_FILE_BYTES:
|
||||
raise ImageConfigError("镜像配置文件大小无效")
|
||||
try:
|
||||
magic, version, declared_size = _HEADER.unpack_from(data, 0)
|
||||
except struct.error as exception:
|
||||
raise ImageConfigError("镜像配置区头部无效") from exception
|
||||
if magic != FILE_MAGIC or version != FORMAT_VERSION or declared_size != CONFIG_FILE_BYTES:
|
||||
raise ImageConfigError("镜像配置区头部无效")
|
||||
|
||||
valid: list[tuple[int, int, dict[str, Any]]] = []
|
||||
for index in range(2):
|
||||
offset = _slot_offset(index)
|
||||
slot_magic, generation, payload_bytes, digest = _SLOT_HEADER.unpack_from(data, offset)
|
||||
if slot_magic != SLOT_MAGIC or payload_bytes <= 0 or payload_bytes > SLOT_BYTES - _SLOT_HEADER.size:
|
||||
continue
|
||||
payload_start = offset + _SLOT_HEADER.size
|
||||
payload = data[payload_start : payload_start + payload_bytes]
|
||||
if hashlib.sha256(payload).digest() != digest:
|
||||
continue
|
||||
try:
|
||||
document = deserialize_config(payload.decode("utf-8"))
|
||||
except (UnicodeError, ImageConfigError):
|
||||
continue
|
||||
valid.append((generation, index, document))
|
||||
if not valid:
|
||||
raise ImageConfigError("镜像配置区没有可恢复的有效副本")
|
||||
generation, index, document = max(valid, key=lambda item: (item[0], item[1]))
|
||||
return document, generation, index
|
||||
|
||||
|
||||
def encode_update(data: bytes, config: Any) -> tuple[bytes, int]:
|
||||
_current, generation, active = read_config_file(data)
|
||||
target = 1 - active
|
||||
return _encode_slot(config, generation + 1), target
|
||||
|
||||
|
||||
def update_config_file(data: bytes, config: Any) -> bytes:
|
||||
slot_data, target = encode_update(data, config)
|
||||
result = bytearray(data)
|
||||
offset = _slot_offset(target)
|
||||
result[offset : offset + SLOT_BYTES] = slot_data
|
||||
return bytes(result)
|
||||
|
||||
|
||||
def slot_offset(index: int) -> int:
|
||||
if index not in (0, 1):
|
||||
raise ValueError("配置槽索引无效")
|
||||
return _slot_offset(index)
|
||||
|
||||
Reference in New Issue
Block a user