初始化奇妙小屏幕控制器项目

This commit is contained in:
2026-09-08 22:56:52 +08:00
commit 8d368de3b5
491 changed files with 67678 additions and 0 deletions
@@ -0,0 +1,63 @@
from __future__ import annotations
from pathlib import Path
import struct
SECTOR_BYTES = 512
PARTITION_LBA = 1
PARTITION_SECTORS = 8192
RESERVED_SECTORS = 1
FAT_COUNT = 2
FAT_SECTORS = 32
ROOT_ENTRIES = 512
ROOT_SECTORS = ROOT_ENTRIES * 32 // SECTOR_BYTES
DATA_RELATIVE_SECTOR = RESERVED_SECTORS + FAT_COUNT * FAT_SECTORS + ROOT_SECTORS
def build_fixture(path: Path, config_file: bytes) -> Path:
if len(config_file) != 64 * 1024:
raise ValueError("配置文件必须为 64 KiB")
image = bytearray((PARTITION_LBA + PARTITION_SECTORS) * SECTOR_BYTES)
image[510:512] = b"\x55\xaa"
partition_entry = 446
image[partition_entry + 4] = 0x06
struct.pack_into("<II", image, partition_entry + 8, PARTITION_LBA, PARTITION_SECTORS)
boot = PARTITION_LBA * SECTOR_BYTES
image[boot : boot + 3] = b"\xeb\x3c\x90"
image[boot + 3 : boot + 11] = b"MSDOS5.0"
struct.pack_into("<H", image, boot + 11, SECTOR_BYTES)
image[boot + 13] = 1
struct.pack_into("<H", image, boot + 14, RESERVED_SECTORS)
image[boot + 16] = FAT_COUNT
struct.pack_into("<H", image, boot + 17, ROOT_ENTRIES)
struct.pack_into("<H", image, boot + 19, PARTITION_SECTORS)
image[boot + 21] = 0xF8
struct.pack_into("<H", image, boot + 22, FAT_SECTORS)
image[boot + 510 : boot + 512] = b"\x55\xaa"
fat = bytearray(FAT_SECTORS * SECTOR_BYTES)
struct.pack_into("<HH", fat, 0, 0xFFF8, 0xFFFF)
clusters = len(config_file) // SECTOR_BYTES
for index in range(clusters):
cluster = 2 + index
struct.pack_into("<H", fat, cluster * 2, 0xFFFF if index == clusters - 1 else cluster + 1)
first_fat = boot + RESERVED_SECTORS * SECTOR_BYTES
image[first_fat : first_fat + len(fat)] = fat
image[first_fat + len(fat) : first_fat + 2 * len(fat)] = fat
root = boot + (RESERVED_SECTORS + FAT_COUNT * FAT_SECTORS) * SECTOR_BYTES
image[root : root + 11] = b"MSCCFG BIN"
image[root + 11] = 0x20
struct.pack_into("<H", image, root + 26, 2)
struct.pack_into("<I", image, root + 28, len(config_file))
data = boot + DATA_RELATIVE_SECTOR * SECTOR_BYTES
image[data : data + len(config_file)] = config_file
path.write_bytes(image)
return path
def first_fat_offset() -> int:
return (PARTITION_LBA + RESERVED_SECTORS) * SECTOR_BYTES
@@ -0,0 +1,106 @@
from __future__ import annotations
import copy
import importlib.util
from pathlib import Path
import sys
import unittest
SOURCE_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(SOURCE_ROOT / "src"))
from matrix_image_editor.config import ( # noqa: E402
HEADER_BYTES,
SLOT_BYTES,
ImageConfigError,
create_config_file,
read_config_file,
update_config_file,
validate_config,
)
def sample_config() -> dict:
return {
"schema_version": 1,
"product": "matrix-screen-controller-walnutpi",
"software_version": "1.0.3",
"account": {"username": "matrix", "password": "password123"},
"wifi": {"ssid": "测试 WiFi", "password": "wifi-pass-123"},
"ipv4": {"mode": "dhcp", "address": "", "prefix": 0, "gateway": "", "dns": []},
}
class ConfigTests(unittest.TestCase):
def test_round_trip_and_update_inactive_slot(self) -> None:
original = create_config_file(sample_config())
config, generation, slot = read_config_file(original)
self.assertEqual((generation, slot), (1, 0))
config["wifi"]["ssid"] = "新网络"
updated = update_config_file(original, config)
verified, generation, slot = read_config_file(updated)
self.assertEqual((generation, slot, verified["wifi"]["ssid"]), (2, 1, "新网络"))
self.assertEqual(updated[HEADER_BYTES : HEADER_BYTES + SLOT_BYTES], original[HEADER_BYTES : HEADER_BYTES + SLOT_BYTES])
def test_corrupt_active_slot_falls_back_and_both_corrupt_fail(self) -> None:
original = create_config_file(sample_config())
updated = bytearray(update_config_file(original, sample_config()))
updated[HEADER_BYTES + SLOT_BYTES + 60] ^= 0x01
_config, generation, slot = read_config_file(bytes(updated))
self.assertEqual((generation, slot), (1, 0))
updated[HEADER_BYTES + 60] ^= 0x01
with self.assertRaises(ImageConfigError):
read_config_file(bytes(updated))
def test_rejects_unknown_product_schema_and_fields(self) -> None:
for field, value in (("product", "wrong"), ("schema_version", 2)):
config = sample_config()
config[field] = value
with self.assertRaises(ImageConfigError):
validate_config(config)
config = sample_config()
config["unexpected"] = True
with self.assertRaises(ImageConfigError):
validate_config(config)
def test_utf8_ssid_and_utf16_secret_lengths_match_contract(self) -> None:
config = sample_config()
config["wifi"]["ssid"] = "中" * 10
validate_config(config)
config["wifi"]["ssid"] = "中" * 11
with self.assertRaises(ImageConfigError):
validate_config(config)
config = sample_config()
config["account"]["password"] = "😀" * 4
validate_config(config)
def test_static_ipv4_requires_same_subnet_and_valid_dns(self) -> None:
config = sample_config()
config["ipv4"] = {
"mode": "static",
"address": "192.168.10.20",
"prefix": 24,
"gateway": "192.168.10.1",
"dns": ["1.1.1.1", "8.8.8.8"],
}
validate_config(config)
config["ipv4"]["gateway"] = "192.168.11.1"
with self.assertRaises(ImageConfigError):
validate_config(config)
def test_project_codec_cross_compatibility_when_repository_is_present(self) -> None:
project_codec = SOURCE_ROOT.parents[2] / "核桃派软件源代码" / "scripts" / "image_config.py"
if not project_codec.is_file():
self.skipTest("独立源码副本中没有主工程交叉校验器")
spec = importlib.util.spec_from_file_location("project_image_config", project_codec)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
ours = create_config_file(copy.deepcopy(sample_config()))
theirs = module.create_config_file(copy.deepcopy(sample_config()))
self.assertEqual(module.read_config_file(ours)[0], read_config_file(theirs)[0])
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,48 @@
from __future__ import annotations
import os
from pathlib import Path
import sys
import unittest
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
SOURCE_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(SOURCE_ROOT / "src"))
sys.path.insert(0, str(Path(__file__).resolve().parent))
from PySide6.QtWidgets import QApplication, QLineEdit # noqa: E402
from matrix_image_editor.gui import MainWindow # noqa: E402
from test_config import sample_config # noqa: E402
class GuiTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.app = QApplication.instance() or QApplication([])
def setUp(self) -> None:
self.window = MainWindow()
def tearDown(self) -> None:
self.window.close()
def test_passwords_are_hidden_and_dhcp_disables_static_fields(self) -> None:
self.assertEqual(self.window.account_password.echoMode(), QLineEdit.EchoMode.Password)
self.assertEqual(self.window.wifi_password.echoMode(), QLineEdit.EchoMode.Password)
for widget in (self.window.address, self.window.prefix, self.window.gateway, self.window.dns):
self.assertFalse(widget.isEnabled())
self.window.mode.setCurrentIndex(1)
for widget in (self.window.address, self.window.prefix, self.window.gateway, self.window.dns):
self.assertTrue(widget.isEnabled())
def test_loaded_values_collect_without_changing_image_version(self) -> None:
config = sample_config()
self.window._apply_loaded(Path("sample.img"), config)
self.window.username.setText("new_user")
collected = self.window._collect()
self.assertEqual(collected["software_version"], config["software_version"])
self.assertEqual(collected["account"]["username"], "new_user")
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,139 @@
from __future__ import annotations
import hashlib
import json
import os
from pathlib import Path
import struct
import sys
import tempfile
import unittest
SOURCE_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(SOURCE_ROOT / "src"))
sys.path.insert(0, str(Path(__file__).resolve().parent))
from fixture_image import build_fixture, first_fat_offset # noqa: E402
from test_config import sample_config # noqa: E402
from matrix_image_editor.config import create_config_file # noqa: E402
from matrix_image_editor.cli import run_command # noqa: E402
from matrix_image_editor.fat16 import Fat16Disk, Fat16Error # noqa: E402
from matrix_image_editor.operations import ( # noqa: E402
ImageOperationError,
copy_write_and_verify,
read_config,
write_and_verify,
)
from matrix_image_editor.paths import ImagePathError, cleanup_new_destination, validate_new_destination # noqa: E402
class ImageOperationTests(unittest.TestCase):
def setUp(self) -> None:
self.temporary = tempfile.TemporaryDirectory(prefix="镜像编辑器-")
self.root = Path(self.temporary.name)
self.image = build_fixture(self.root / "原 镜像.img", create_config_file(sample_config()))
def tearDown(self) -> None:
self.temporary.cleanup()
def test_read_write_sidecar_and_reopen(self) -> None:
config = read_config(self.image)
config["wifi"]["ssid"] = "中文 新 WiFi"
progress: list[int] = []
verified = write_and_verify(self.image, config, progress.append)
self.assertEqual(verified["wifi"]["ssid"], "中文 新 WiFi")
sidecar = Path(str(self.image) + ".sha256")
raw = sidecar.read_bytes()
self.assertFalse(raw.startswith(b"\xef\xbb\xbf"))
self.assertEqual(raw, f"{hashlib.sha256(self.image.read_bytes()).hexdigest()} {self.image.name}\n".encode("utf-8"))
self.assertEqual(progress[-1], 100)
def test_rejects_bad_sidecar_forms_and_content(self) -> None:
sidecar = Path(str(self.image) + ".sha256")
digest = hashlib.sha256(self.image.read_bytes()).hexdigest()
invalid = [
b"\xef\xbb\xbf" + f"{digest} {self.image.name}\n".encode("utf-8"),
f"{digest.upper()} {self.image.name}\n".encode("utf-8"),
f"{digest} {self.image.name}\n".encode("utf-8"),
f"{digest} wrong.img\n".encode("utf-8"),
f"{'0' * 64} {self.image.name}\n".encode("utf-8"),
f"{digest} {self.image.name}\r\n".encode("utf-8"),
]
for payload in invalid:
with self.subTest(payload=payload[:12]):
sidecar.write_bytes(payload)
with self.assertRaises(ImageOperationError):
read_config(self.image)
def test_copy_apply_preserves_source_and_cleans_failure(self) -> None:
original = self.image.read_bytes()
config = sample_config()
config["account"]["username"] = "new_user"
target, verified = copy_write_and_verify(self.image, self.root / "新 镜像.img", config)
self.assertEqual(verified["account"]["username"], "new_user")
self.assertEqual(self.image.read_bytes(), original)
self.assertTrue(Path(str(target) + ".sha256").is_file())
failed = self.root / "失败.img"
wrong_version = sample_config()
wrong_version["software_version"] = "9.9.9"
with self.assertRaises(ImageOperationError):
copy_write_and_verify(self.image, failed, wrong_version)
self.assertFalse(failed.exists())
self.assertFalse(Path(str(failed) + ".sha256").exists())
def test_path_rejections(self) -> None:
with self.assertRaises(ImagePathError):
validate_new_destination(self.image, self.image)
existing = self.root / "existing.img"
existing.write_bytes(b"")
with self.assertRaises(ImagePathError):
validate_new_destination(self.image, existing)
orphan = self.root / "orphan.img"
Path(str(orphan) + ".sha256").write_text("orphan", encoding="utf-8")
with self.assertRaises(ImagePathError):
validate_new_destination(self.image, orphan)
with self.assertRaises(ImagePathError):
validate_new_destination(self.image, self.root / "wrong.txt")
if os.name == "nt":
with self.assertRaises(ImagePathError):
validate_new_destination(self.image, self.root / "CON.img")
def test_fat_cycle_and_partition_bounds_are_rejected(self) -> None:
cyclic = self.root / "cycle.img"
cyclic.write_bytes(self.image.read_bytes())
with cyclic.open("r+b") as stream:
stream.seek(first_fat_offset() + 2 * 2)
stream.write(struct.pack("<H", 2))
with Fat16Disk(cyclic, writable=False) as disk:
with self.assertRaises(Fat16Error):
disk.read_file("MSCCFG.BIN")
truncated = self.root / "truncated.img"
truncated.write_bytes(self.image.read_bytes()[:-512])
with self.assertRaises(Fat16Error):
Fat16Disk(truncated, writable=False)
def test_cli_contract_and_apply(self) -> None:
import contextlib
import io
output = io.StringIO()
with contextlib.redirect_stdout(output):
self.assertEqual(run_command(["--validate", str(self.image)]), 0)
document = json.loads(output.getvalue())
self.assertEqual(
set(document),
{"valid", "SoftwareVersion", "Username", "Ssid", "Mode", "Address"},
)
config_path = self.root / "config.json"
config_path.write_text(json.dumps(sample_config(), ensure_ascii=False), encoding="utf-8-sig")
target = self.root / "cli-new.img"
with contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(run_command(["--apply", str(self.image), str(config_path), str(target)]), 0)
self.assertTrue(target.is_file())
self.assertTrue(Path(str(target) + ".sha256").is_file())
if __name__ == "__main__":
unittest.main()