2026-08-20 17:11:06 +00:00
|
|
|
"""Golden-manifest tests for the Connac2 container extractor.
|
|
|
|
|
|
|
|
|
|
Runs against whatever MediaTek blobs exist on this host (skip otherwise).
|
|
|
|
|
Golden values observed from linux-firmware snapshot 2026-04-20; when a value
|
|
|
|
|
here fails against a newer snapshot, the blob changed — update the golden
|
|
|
|
|
value *and* note it in the changelog, don't silently absorb it.
|
|
|
|
|
"""
|
2026-08-20 17:13:54 +00:00
|
|
|
import os
|
2026-08-20 17:11:06 +00:00
|
|
|
import subprocess
|
|
|
|
|
import sys
|
|
|
|
|
import unittest
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
REPO = Path(__file__).resolve().parent.parent
|
2026-08-20 17:13:54 +00:00
|
|
|
FW = Path(os.environ.get('MTK_FW_DIR', '/lib/firmware/mediatek'))
|
2026-08-20 17:11:06 +00:00
|
|
|
|
2026-08-20 17:26:37 +00:00
|
|
|
# Golden source of truth: dataset/history.jsonl (one record per
|
|
|
|
|
# linux-firmware revision, sha256-keyed). Any tested blob must be a known
|
|
|
|
|
# revision and its parse must match the recorded metadata. A new upstream
|
|
|
|
|
# build therefore fails here until the dataset is refreshed (by design):
|
|
|
|
|
#
|
|
|
|
|
# python3 tools/fw_history.py firmware/linux-firmware -o dataset/history.jsonl
|
|
|
|
|
import json
|
|
|
|
|
|
|
|
|
|
HISTORY = json.loads(
|
|
|
|
|
'[' + ','.join(l for l in
|
|
|
|
|
(REPO / 'dataset' / 'history.jsonl').read_text().splitlines()
|
|
|
|
|
if l) + ']')
|
|
|
|
|
BY_SHA = {r['sha256']: r for r in HISTORY}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _expected(path: Path, sha: str):
|
|
|
|
|
rec = BY_SHA.get(sha)
|
|
|
|
|
if rec is None:
|
|
|
|
|
raise AssertionError(
|
|
|
|
|
f'{path.name}: sha {sha} is not a known linux-firmware revision '
|
|
|
|
|
f'in dataset/history.jsonl — refresh the dataset (see comment '
|
|
|
|
|
f'above) or verify the blob provenance')
|
|
|
|
|
return rec
|
2026-08-20 17:11:06 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse(path: Path, out: Path) -> dict:
|
|
|
|
|
out.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
sys.path.insert(0, str(REPO / 'tools'))
|
|
|
|
|
import mtk_fw_extract as x
|
|
|
|
|
return (x.carve_patch if 'patch' in path.name else x.carve_ram)(path, out)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class LayoutClosure(unittest.TestCase):
|
|
|
|
|
"""sum(region.len) + hidden trailer + table + trailer == file size."""
|
|
|
|
|
|
|
|
|
|
def test_closure(self):
|
|
|
|
|
blobs = sorted(FW.glob('mt79*_wm.bin')) + sorted(FW.glob('mt79*_wa.bin'))
|
|
|
|
|
if not blobs:
|
|
|
|
|
self.skipTest('no mediatek blobs on this host')
|
|
|
|
|
import struct
|
|
|
|
|
T = struct.Struct('<5B2s10s15sI')
|
|
|
|
|
R = struct.Struct('<III4sIIBB14s')
|
|
|
|
|
for p in blobs:
|
|
|
|
|
with self.subTest(blob=p.name):
|
|
|
|
|
d = p.read_bytes()
|
|
|
|
|
n = d[len(d) - T.size + 2]
|
|
|
|
|
tbl = len(d) - T.size - n * R.size
|
|
|
|
|
data_end = sum(R.unpack_from(d, tbl + i * R.size)[5]
|
|
|
|
|
for i in range(n))
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
data_end + (tbl - data_end) + n * R.size + T.size, len(d),
|
|
|
|
|
'layout does not close')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class GoldenRAM(unittest.TestCase):
|
2026-08-20 17:26:37 +00:00
|
|
|
"""Parsed metadata must match the dataset record for the blob's sha."""
|
2026-08-20 17:11:06 +00:00
|
|
|
|
2026-08-20 17:26:37 +00:00
|
|
|
def test_ram(self):
|
|
|
|
|
import hashlib
|
|
|
|
|
import sys as _sys
|
|
|
|
|
_sys.path.insert(0, str(REPO / 'tools'))
|
|
|
|
|
blobs = sorted(FW.glob('mt79*_wm.bin')) + sorted(FW.glob('mt79*_wa.bin'))
|
|
|
|
|
if not blobs:
|
|
|
|
|
self.skipTest('no mediatek blobs on this host')
|
|
|
|
|
for p in blobs:
|
|
|
|
|
with self.subTest(blob=p.name):
|
|
|
|
|
d = p.read_bytes()
|
|
|
|
|
rec = _expected(p, hashlib.sha256(d).hexdigest()[:16])
|
|
|
|
|
info = parse(p, REPO / 'extracted' / 'test_ram')
|
|
|
|
|
self.assertEqual(info['chip_id'], rec.get('chip_id'), p.name)
|
|
|
|
|
self.assertEqual(info['eco'], rec.get('eco'), p.name)
|
|
|
|
|
self.assertEqual(info['n_region'], rec.get('n_region'), p.name)
|
|
|
|
|
self.assertEqual(info.get('hidden_trailer', {}).get('string'),
|
|
|
|
|
rec.get('hidden'), p.name)
|
|
|
|
|
self.assertEqual(info['build_date'], rec.get('trailer_date'),
|
|
|
|
|
p.name)
|
2026-08-20 17:11:06 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class GoldenPatch(unittest.TestCase):
|
|
|
|
|
|
|
|
|
|
def test_patch(self):
|
2026-08-20 17:26:37 +00:00
|
|
|
import hashlib
|
|
|
|
|
blobs = sorted(FW.glob('mt79*_rom_patch.bin'))
|
|
|
|
|
if not blobs:
|
|
|
|
|
self.skipTest('no mediatek blobs on this host')
|
|
|
|
|
for p in blobs:
|
2026-08-20 17:11:06 +00:00
|
|
|
with self.subTest(blob=p.name):
|
2026-08-20 17:26:37 +00:00
|
|
|
d = p.read_bytes()
|
|
|
|
|
rec = _expected(p, hashlib.sha256(d).hexdigest()[:16])
|
2026-08-20 17:11:06 +00:00
|
|
|
info = parse(p, REPO / 'extracted' / 'test_patch')
|
2026-08-20 17:26:37 +00:00
|
|
|
self.assertEqual(info['platform'], rec.get('platform'), p.name)
|
|
|
|
|
self.assertEqual(info['n_region'], rec.get('n_section'), p.name)
|
2026-08-20 17:11:06 +00:00
|
|
|
for sec in info['sections']:
|
|
|
|
|
self.assertEqual(sec['type'], '0x30002')
|
|
|
|
|
self.assertEqual(sec['enc_type'], 0) # plaintext (F4)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
|
unittest.main(verbosity=2)
|