226 lines
9.1 KiB
Python
226 lines
9.1 KiB
Python
from __future__ import annotations
|
|
|
|
import gzip
|
|
import hashlib
|
|
import io
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import tarfile
|
|
|
|
import pytest
|
|
|
|
from scripts.image_kernel_payload import (
|
|
ImageKernelPayloadError,
|
|
stage_payload,
|
|
verify_staged_payload,
|
|
)
|
|
from scripts.image_bootstrap_payload import (
|
|
APP_RELATIVE,
|
|
TARGET_RELATIVE,
|
|
ImageBootstrapPayloadError,
|
|
stage_payload as stage_bootstrap_payload,
|
|
verify_staged_payload as verify_bootstrap_payload,
|
|
)
|
|
from scripts.kernel_artifact import (
|
|
ARTIFACT_NAME,
|
|
KERNEL_RELEASE,
|
|
REQUIRED_BOOT_FILES,
|
|
SOURCE_ARCHIVE_SHA256,
|
|
SOURCE_COMMIT,
|
|
KernelArtifactError,
|
|
sha256_file,
|
|
verify_kernel_dependency,
|
|
verify_source_dependency,
|
|
)
|
|
|
|
|
|
SOURCE_ROOT = Path(__file__).resolve().parents[1]
|
|
PROJECT_ROOT = SOURCE_ROOT.parent
|
|
|
|
|
|
def _dependency(
|
|
root: Path,
|
|
*,
|
|
release_marker: str = KERNEL_RELEASE,
|
|
source_marker: str = SOURCE_COMMIT,
|
|
extra_member: tarfile.TarInfo | None = None,
|
|
) -> Path:
|
|
root.mkdir()
|
|
files = {
|
|
"boot/Image-matrix-axp313a1": f"prefix Linux version {KERNEL_RELEASE} suffix".encode(),
|
|
"boot/sun50i-h616-walnutpi-1b-matrix-axp313a1.dtb": b"dtb-sd",
|
|
"boot/sun50i-h616-walnutpi-1b-emmc-matrix-axp313a1.dtb": b"dtb-emmc",
|
|
f"boot/System.map-{KERNEL_RELEASE}": b"system-map",
|
|
f"boot/config-{KERNEL_RELEASE}": b"config",
|
|
"KERNEL_RELEASE": f"{release_marker}\n".encode(),
|
|
"SOURCE_COMMIT": f"{source_marker}\n".encode(),
|
|
f"lib/modules/{KERNEL_RELEASE}/kernel/example.ko": b"module",
|
|
}
|
|
sums = "".join(
|
|
f"{hashlib.sha256(body).hexdigest()} {name}\n" for name, body in sorted(files.items())
|
|
).encode()
|
|
files["SHA256SUMS"] = sums
|
|
artifact = root / ARTIFACT_NAME
|
|
with artifact.open("wb") as raw:
|
|
with gzip.GzipFile(filename="", mode="wb", fileobj=raw, mtime=0) as compressed:
|
|
with tarfile.open(fileobj=compressed, mode="w", format=tarfile.PAX_FORMAT) as archive:
|
|
for name, body in sorted(files.items()):
|
|
info = tarfile.TarInfo(name)
|
|
info.size = len(body)
|
|
info.uid = info.gid = 0
|
|
info.mtime = 0
|
|
archive.addfile(info, io.BytesIO(body))
|
|
if extra_member is not None:
|
|
extra_member.uid = extra_member.gid = 0
|
|
extra_member.mtime = 0
|
|
archive.addfile(extra_member)
|
|
digest = sha256_file(artifact)
|
|
(root / "SHA256SUMS").write_text(f"{digest} {ARTIFACT_NAME}\n", encoding="ascii")
|
|
metadata = {
|
|
"schema_version": 1,
|
|
"architecture": "aarch64",
|
|
"kernel_release": KERNEL_RELEASE,
|
|
"source_commit": SOURCE_COMMIT,
|
|
"source_archive_sha256": SOURCE_ARCHIVE_SHA256,
|
|
"artifact": ARTIFACT_NAME,
|
|
"artifact_bytes": artifact.stat().st_size,
|
|
"artifact_sha256": digest,
|
|
"unpacked_file_bytes": sum(len(body) for body in files.values()),
|
|
"regular_file_count": len(files),
|
|
"module_file_count": 1,
|
|
"provenance": "synthetic test payload",
|
|
}
|
|
(root / "METADATA.json").write_text(json.dumps(metadata), encoding="utf-8")
|
|
return root
|
|
|
|
|
|
def test_registered_kernel_and_source_dependencies_are_complete():
|
|
kernel = PROJECT_ROOT / "发布更新相关/其他依赖/aarch64-kernel/6.1.31-matrix-axp313a1"
|
|
source = PROJECT_ROOT / "发布更新相关/其他依赖/kernel-source/walnutpi-linux-6.1.31-30ff3fd5"
|
|
if os.environ.get("MATRIX_SOURCE_ONLY_UPDATE_TESTS") == "1":
|
|
if not kernel.exists() and not source.exists():
|
|
pytest.skip("source-only update payload intentionally omits image build dependencies")
|
|
info = verify_kernel_dependency(kernel)
|
|
assert info.module_file_count == 3048
|
|
assert info.regular_file_count == 3056
|
|
assert info.unpacked_file_bytes == 160051604
|
|
assert verify_source_dependency(source)["archive_bytes"] == 249095239
|
|
registry = json.loads((PROJECT_ROOT / "发布更新相关/其他依赖/DEPENDENCIES.json").read_text(encoding="utf-8"))
|
|
active = {item["id"]: item for item in registry["active"]}
|
|
assert active["axp313a-kernel-runtime"]["path"] == "aarch64-kernel/6.1.31-matrix-axp313a1"
|
|
assert active["walnutpi-linux-kernel-source"]["path"] == (
|
|
"kernel-source/walnutpi-linux-6.1.31-30ff3fd5"
|
|
)
|
|
|
|
|
|
def test_synthetic_kernel_dependency_round_trip(tmp_path: Path):
|
|
info = verify_kernel_dependency(_dependency(tmp_path / "kernel"))
|
|
assert info.kernel_release == KERNEL_RELEASE
|
|
assert info.module_file_count == 1
|
|
assert REQUIRED_BOOT_FILES
|
|
|
|
|
|
def test_kernel_dependency_rejects_outer_digest_damage(tmp_path: Path):
|
|
dependency = _dependency(tmp_path / "kernel")
|
|
artifact = dependency / ARTIFACT_NAME
|
|
body = bytearray(artifact.read_bytes())
|
|
body[-1] ^= 0xFF
|
|
artifact.write_bytes(body)
|
|
with pytest.raises(KernelArtifactError, match="SHA-256 mismatch"):
|
|
verify_kernel_dependency(dependency)
|
|
|
|
|
|
def test_kernel_dependency_rejects_wrong_release_marker(tmp_path: Path):
|
|
dependency = _dependency(tmp_path / "kernel", release_marker="wrong-release")
|
|
with pytest.raises(KernelArtifactError, match="release marker"):
|
|
verify_kernel_dependency(dependency)
|
|
|
|
|
|
def test_kernel_dependency_rejects_wrong_source_marker_and_missing_binary(tmp_path: Path):
|
|
dependency = _dependency(tmp_path / "wrong", source_marker="0" * 40)
|
|
with pytest.raises(KernelArtifactError, match="source commit marker"):
|
|
verify_kernel_dependency(dependency)
|
|
missing = _dependency(tmp_path / "missing")
|
|
(missing / ARTIFACT_NAME).unlink()
|
|
with pytest.raises(KernelArtifactError, match="binary is missing"):
|
|
verify_kernel_dependency(missing)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("name", "kind", "message"),
|
|
(
|
|
("unsafe-link", tarfile.SYMTYPE, "link or special file"),
|
|
("../escape", tarfile.REGTYPE, "unsafe archive path"),
|
|
("secret.txt", tarfile.REGTYPE, "unowned file"),
|
|
),
|
|
)
|
|
def test_kernel_dependency_rejects_links_and_unsafe_paths(
|
|
tmp_path: Path, name: str, kind: bytes, message: str
|
|
):
|
|
extra = tarfile.TarInfo(name)
|
|
extra.type = kind
|
|
if kind == tarfile.SYMTYPE:
|
|
extra.linkname = "boot/Image-matrix-axp313a1"
|
|
dependency = _dependency(tmp_path / "kernel", extra_member=extra)
|
|
with pytest.raises(KernelArtifactError, match=message):
|
|
verify_kernel_dependency(dependency)
|
|
|
|
|
|
def test_image_root_payload_stage_verify_and_corruption(tmp_path: Path):
|
|
dependency = _dependency(tmp_path / "kernel")
|
|
root = tmp_path / "root"
|
|
root.mkdir()
|
|
result = stage_payload(root, dependency, reserve_bytes=0)
|
|
assert result["kernel_release"] == KERNEL_RELEASE
|
|
assert verify_staged_payload(root, dependency) == result
|
|
staged = root / "opt/matrix-image-bootstrap/axp313a" / ARTIFACT_NAME
|
|
staged.write_bytes(staged.read_bytes() + b"damage")
|
|
with pytest.raises(KernelArtifactError, match="SHA-256 mismatch"):
|
|
verify_staged_payload(root, dependency)
|
|
|
|
|
|
def test_image_root_payload_refuses_existing_target_and_insufficient_space(tmp_path: Path):
|
|
dependency = _dependency(tmp_path / "kernel")
|
|
existing_root = tmp_path / "existing"
|
|
existing_root.mkdir()
|
|
stage_payload(existing_root, dependency, reserve_bytes=0)
|
|
with pytest.raises(ImageKernelPayloadError, match="already exists"):
|
|
stage_payload(existing_root, dependency, reserve_bytes=0)
|
|
small_root = tmp_path / "small"
|
|
small_root.mkdir()
|
|
with pytest.raises(ImageKernelPayloadError, match="lacks room"):
|
|
stage_payload(small_root, dependency, reserve_bytes=10**30)
|
|
|
|
|
|
def test_image_bootstrap_stages_application_and_kernel_together(tmp_path: Path):
|
|
dependency = _dependency(tmp_path / "kernel")
|
|
bundle = tmp_path / "MSCBOOT.TGZ"
|
|
bundle.write_bytes(b"application-bundle")
|
|
root = tmp_path / "root"
|
|
root.mkdir()
|
|
result = stage_bootstrap_payload(root, bundle, dependency, reserve_bytes=0)
|
|
assert result["bundle_bytes"] == len(b"application-bundle")
|
|
assert result["bundle_path"] == "opt/matrix-image-bootstrap/app/MSCBOOT.TGZ"
|
|
assert verify_bootstrap_payload(root, bundle, dependency) == result
|
|
assert (root / TARGET_RELATIVE / APP_RELATIVE).read_bytes() == bundle.read_bytes()
|
|
|
|
|
|
def test_image_bootstrap_rejects_damage_existing_target_and_combined_space(tmp_path: Path):
|
|
dependency = _dependency(tmp_path / "kernel")
|
|
bundle = tmp_path / "MSCBOOT.TGZ"
|
|
bundle.write_bytes(b"application-bundle")
|
|
root = tmp_path / "root"
|
|
root.mkdir()
|
|
stage_bootstrap_payload(root, bundle, dependency, reserve_bytes=0)
|
|
with pytest.raises(ImageBootstrapPayloadError, match="already exists"):
|
|
stage_bootstrap_payload(root, bundle, dependency, reserve_bytes=0)
|
|
staged = root / TARGET_RELATIVE / APP_RELATIVE
|
|
staged.write_bytes(b"damaged")
|
|
with pytest.raises(ImageBootstrapPayloadError, match="differs"):
|
|
verify_bootstrap_payload(root, bundle, dependency)
|
|
small_root = tmp_path / "small"
|
|
small_root.mkdir()
|
|
with pytest.raises(ImageBootstrapPayloadError, match="both offline payloads"):
|
|
stage_bootstrap_payload(small_root, bundle, dependency, reserve_bytes=10**30)
|