Files
o62lte-customising-plan/s5e-pressure-anim/tools/s5e_verify_indep.py
T
2026-10-03 00:01:12 +08:00

290 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""独立总校验 —— 只读两张交付物 + 固件镜像,**不读任何构建中间产物**。
与 tools/s5e_verify.py 的分工
-----------------------------
s5e_verify.py 会读 `blob_<role>/offsets.json` 来核对 Lua 表里的偏移。本脚本刻意
**不读** offsets.json / .work/overlay / .work/build,改为:
* 自己从 `.face` 里反解 Lua(占位符、ROLE、pay/rb/UIIMG 三张表)
* 自己从**固件镜像**里量出 119 个槽位(名字 / 偏移 / 长度)
* 自己证明两张表「自洽 + 连续」:pay[0] 必须紧跟在魔数之后、pay[i+1] == pay[i]+len
* 自己证明**第 k 条负载落在 Measuring{k}.bin 上**(名字对得上,不只是偏移对得上)
* 逐条把负载解成像素:RLE 解出**恰好 usize**,且多出来的那 1 字节正好在像素区之后
(这是"少 1 像素整帧不画"的那个坑)
* 两张交付物**互相印证**(replace 的 pay 前 64 B == restore 的 rb,反之亦然)——
这一条完全不碰 .work,也不碰固件镜像
* 镜像级:inode 表 / superblock / 卷名 / 长度逐位不变;除 119 个槽位外,
**其余每一个文件的每一个字节都逐字节不变**(不是只做区间统计)
用法:python tools/s5e_verify_indep.py
"""
import os, re, sys, hashlib, struct
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from paths import PROJ, DIST, TARGET_IMAGE, DIRP, FACE_W, FACE_H, WF_ID, PREVIEW, SCREEN, WORK
from romfs import Romfs
import lvgl_bin as L
MAGIC_PAY = b'\x00S5P-PAYv1\x00\xa5\x5a\xc3\x3c'
MAGIC_FP = b'\x00S5P-FPv1\x00\x5a\xa5\x3c\xc3'
FACES = {'replace': 'S5ePAnim.face', 'restore': 'S5ePAnimRestore.face'}
PATCHED = os.path.join(WORK, 'anim', 'vela_health_anim.bin')
UI_NAMES = ['back', 'ic_help', 'ic_replace', 'ic_about', 'ic_check', 'ic_rollback',
'ic_qq', 'logo']
ok = fail = 0
def ck(cond, msg):
global ok, fail
if cond:
ok += 1
print(' OK ' + msg)
else:
fail += 1
print(' FAIL ' + msg)
def parse_face(path):
"""把一张 .face 反解成:头 + Lua 文本 + 三张表 + 魔数位置。"""
d = open(path, 'rb').read()
end = d.find(b'return ui\n')
start = d.rfind(b'--[[', 0, end)
lua = d[start:end + len(b'return ui\n')]
txt = lua.decode('utf-8')
out = dict(data=d, lua=lua, txt=txt, lua_off=start)
m = re.search(r"local ROLE = '(\w+)'", txt)
out['role'] = m.group(1) if m else None
for key, pat in (('pay', r'pay = \{(.*?)\n \},'), ('rb', r'rb = \{(.*?)\n \},')):
blk = re.search(pat, txt, re.S)
out[key] = ([tuple(int(x) for x in t)
for t in re.findall(r'\{(\d+),(\d+),(\d+)\}', blk.group(1))]
if blk else [])
out['ui'] = re.findall(r'\{(\d+),(\d+),"([a-z_]+)"\}', txt)
out['pay_base'] = d.find(MAGIC_PAY)
out['fp_base'] = d.find(MAGIC_FP)
out['pay_magic_n'] = d.count(MAGIC_PAY)
out['fp_magic_n'] = d.count(MAGIC_FP)
out['lua_n'] = d.count(lua)
return out
def slots_of(stock):
"""固件镜像里的 119 个槽位:名字 -> (偏移, 长度),并给出名字顺序的列表。"""
sl = {}
for p, i in stock.entries():
if i.type == 1 or not p.startswith(DIRP) or not p.endswith('.bin') or i.size == 0:
continue
sl[os.path.basename(p)] = (i.data_off, i.size)
return sl
def main():
stock_b = open(TARGET_IMAGE, 'rb').read()
patch_b = open(PATCHED, 'rb').read()
stock = Romfs(TARGET_IMAGE)
patched = Romfs(PATCHED)
sl = slots_of(stock)
exp_name = ['Measuring%d.bin' % k for k in range(1, len(sl) + 1)]
print('=' * 78)
print('独立总校验(不读 blob_*/offsets.json、不读 .work/overlay、不读 .work/build)')
print(' 交付物 : dist/%s + dist/%s' % (FACES['replace'], FACES['restore']))
print(' 固件 : inputs/target/vela_health.bin %d B(卷名 %s)'
% (len(stock_b), stock.volume))
print(' 改后 : .work/anim/vela_health_anim.bin %d B' % len(patch_b))
print('=' * 78)
ck(len(sl) == 119, '固件里 %s 下 %d 个槽位(期望 119)' % (DIRP, len(sl)))
ck(sorted(sl.keys()) == sorted(exp_name), '槽位名字就是 Measuring1..119.bin')
faces = {}
for role, fn in FACES.items():
f = parse_face(os.path.join(DIST, fn))
faces[role] = f
other = patch_b if role == 'replace' else stock_b
print('\n' + '-' * 78)
print('[%s] %s %d B sha256 %s'
% (role, fn, len(f['data']), hashlib.sha256(f['data']).hexdigest()))
print('-' * 78)
# ---- ① 头 + Lua ----
d = f['data']
ck(d[0:4] == b'\x5a\xa5\x34\x12', 'magic')
ck(d[5] == 0, 'b5 = %d(固件自带表盘是 0)' % d[5])
ck(d[4] == 0, 'b4 = %d' % d[4])
ck(d[40:50] == WF_ID.encode() + b'\x00', 'id = %r' % d[40:50])
a32, = struct.unpack('<I', d[32:36])
pal, pw, ph, psz = struct.unpack('<IHHI', d[a32:a32 + 12])
ck((pw, ph) == (PREVIEW, PREVIEW), 'preview %dx%d' % (pw, ph))
ck(f['lua_n'] == 1, 'Lua 在文件里只出现 1 次(偏移 0x%x)' % f['lua_off'])
ck(f['role'] == role, "Lua 里的 ROLE == '%s'" % role)
for ph_ in ('__UIIMG__', '__PAY__', '__RB__', '__ROLE__'):
ck(ph_ not in f['txt'], '占位符 %s 已回填' % ph_)
ck(re.search(r'local W, H = %d, %d' % (SCREEN, SCREEN), f['txt']) is not None,
'界面尺寸 W,H = %d,%d' % (SCREEN, SCREEN))
m = re.search(r'local DEVS = \{([^}]*)\}', f['txt'])
ck(m is not None and '/dev/health' in m.group(1),
'DEVS 有 health -> /dev/health')
ck(len(f['ui']) == 8, 'UIIMG %d 条(期望 8)' % len(f['ui']))
# ---- ② 表自洽 + 在文件里连续(完全不看 offsets.json)----
pay, rb = f['pay'], f['rb']
ck(len(pay) == 119 and len(rb) == 119, 'pay %d 条 / rb %d 条(期望 119/119)'
% (len(pay), len(rb)))
ck(f['pay_magic_n'] == 1 and f['fp_magic_n'] == 1,
'两个魔数各命中 1 次(payload 0x%x / fp 0x%x)' % (f['pay_base'], f['fp_base']))
cont = 0
for i, (o, _t, ln) in enumerate(pay):
if i == 0:
if o != f['pay_base'] + len(MAGIC_PAY):
cont += 1
elif o != pay[i - 1][0] + pay[i - 1][2]:
cont += 1
for i, (o, _t, ln) in enumerate(rb):
if i == 0:
if o != f['fp_base'] + len(MAGIC_FP):
cont += 1
elif o != rb[i - 1][0] + rb[i - 1][2]:
cont += 1
ck(cont == 0, '两张表的偏移自洽:首条紧跟魔数,之后逐条紧邻(%d 处不连续)' % cont)
ck(all(rb[i][2] == 64 for i in range(len(rb))), 'rb 每条都是 64 B')
# ---- ③ 第 k 条落在 Measuring{k}.bin 上 ----
bad_name = bad_len = 0
for i, (o, t, ln) in enumerate(pay):
if t != sl[exp_name[i]][0]:
bad_name += 1
if ln != sl[exp_name[i]][1] or rb[i][1] != t:
bad_len += 1
ck(bad_name == 0, '第 k 条负载的目标偏移 == Measuring{k}.bin 的偏移(%d 处不符)' % bad_name)
ck(bad_len == 0, '每条长度 == 该槽位原厂长度,且 rb 的 target 与 pay 一致(%d 处不符)'
% bad_len)
ck(all(t % 512 == 0 for _o, t, _l in pay), '119 个目标偏移全部 512 对齐')
odd = sum(1 for _o, _t, l in pay if l % 4 != 0)
print(' (%d/119 条长度不是 4 的倍数 -> Lua 侧必须走 bs=512+4+1 分段写,'
'否则退化成 bs=1)' % odd)
# ---- ④ 负载字节 == 对应镜像;⑤ 解码 ----
bad_bytes = bad_dec = bad_hdr = 0
big_e = 0
for i, (o, t, ln) in enumerate(pay):
blob = d[o:o + ln]
if blob != other[t:t + ln]:
bad_bytes += 1
if blob == (stock_b if role == 'replace' else patch_b)[t:t + ln]:
big_e += 1
h = L.parse_header(blob)
if (h is None or h['cf'] != 0x0a or not (h['flags'] & L.FLAG_COMPRESSED)
or (h['w'], h['h']) != (FACE_W, FACE_H) or h['stride'] != FACE_W):
bad_hdr += 1
continue
_m, cs, us = struct.unpack('<III', blob[12:24])
try:
raw = L.rle_decompress(blob[24:24 + cs], 1, None)
except Exception:
bad_dec += 1
continue
# ① 解出来必须**恰好** usize(少一字节设备端整帧不画)
# ② usize = 1024 + w*h + 1 -> 像素区之后**正好** 1 字节尾巴(usize_tail)
if (len(raw) != us or us != 1024 + FACE_W * FACE_H + 1
or len(raw) - 1024 - FACE_W * FACE_H != 1):
bad_dec += 1
continue
try:
img = L.decode_lvgl_bin(blob)[1]
if img is None or img.size != (FACE_W, FACE_H):
bad_dec += 1
except Exception:
bad_dec += 1
ck(bad_bytes == 0, '每条负载字节 == %s镜像对应位置(%d 处不符)'
% ('改后' if role == 'replace' else '原厂', bad_bytes))
ck(big_e == 0, '每条负载都真的与"另一侧"不同(%d 条与另一侧相同 —— 应当是 0)' % big_e)
ck(bad_hdr == 0, '每条都是 I8(0x0a) + 压缩 + 464x464 + stride 464(%d 条不符)' % bad_hdr)
ck(bad_dec == 0, '每条 RLE 解出**恰好** usize=216321 且 1024+464*464 之后正好 1 字节尾巴'
'(%d 条不符)' % bad_dec)
# ---- ⑥ 指纹有区分度 ----
diff_fp = sum(1 for i in range(len(pay))
if stock_b[pay[i][1]:pay[i][1] + 64] != patch_b[pay[i][1]:pay[i][1] + 64])
ck(diff_fp == 119, '%d/119 条的"原厂前 64 B"与"改后前 64 B"确实不同' % diff_fp)
ck(all(rb[i] == (rb[i][0], sl[exp_name[i]][0], 64) for i in range(len(rb))),
'rb 的 target 也指向对应槽位')
# ---- ⑦ UI 素材 ----
badui = 0
for o, l, n in f['ui']:
blob = open(os.path.join(PROJ, 'ui_images', n + '.bin'), 'rb').read()
if int(l) != len(blob) or d[int(o):int(o) + int(l)] != blob:
badui += 1
ck(badui == 0, '8 张界面素材与 ui_images/*.bin 逐字节一致(%d 处不符)' % badui)
# ================================================================ 两张盘互证
print('\n' + '=' * 78)
print('[互证] 两张交付物互相印证(完全不碰 .work / 固件镜像)')
print('=' * 78)
rp, rr = faces['replace'], faces['restore']
ck(len(rp['data']) == len(rr['data']), '两张盘字节数相同(%d)' % len(rp['data']))
ck(rp['data'][:50] == rr['data'][:50],
'头 50 B(magic / b4 / b5 / id)逐字节相同')
cross = 0
for i in range(119):
if rp['pay'][i][1] != rr['pay'][i][1]:
cross += 1
continue
o1, t, l1 = rp['pay'][i]
o2, _t2, l2 = rr['pay'][i]
if rp['data'][o1:o1 + 64] != rr['data'][rr['rb'][i][0]:rr['rb'][i][0] + 64]:
cross += 1
if rr['data'][o2:o2 + 64] != rp['data'][rp['rb'][i][0]:rp['rb'][i][0] + 64]:
cross += 1
ck(cross == 0, 'replace 的负载前 64 B == restore 的指纹;restore 的负载前 64 B == '
'replace 的指纹(%d 处不符)' % cross)
same_ui = all(rp['data'][int(o):int(o) + int(l)] == rr['data'][int(o):int(o) + int(l)]
for o, l, _n in rp['ui'])
ck(same_ui, '两张盘的 8 张界面素材逐字节相同(同一套界面)')
ck(rp['pay_base'] == rr['pay_base'] and rp['fp_base'] == rr['fp_base'],
'两张盘的资源基址相同(0x%x / 0x%x)' % (rp['pay_base'], rr['fp_base']))
ndiff = sum(1 for i in range(119)
if rp['data'][rp['pay'][i][0]:rp['pay'][i][0] + rp['pay'][i][2]] !=
rr['data'][rr['pay'][i][0]:rr['pay'][i][0] + rr['pay'][i][2]])
ck(ndiff == 119, '119/119 条负载在两张盘里都不一样(%d)' % ndiff)
# ================================================================ 镜像完整性
print('\n' + '=' * 78)
print('[镜像] 改后镜像 vs 原厂镜像')
print('=' * 78)
a = [(p, i.off, i.next, i.spec, i.size, i.cksum) for p, i in stock.entries()]
b = [(p, i.off, i.next, i.spec, i.size, i.cksum) for p, i in patched.entries()]
ck(a == b, 'inode 表(路径/偏移/next/spec/size/cksum)%d 条逐位不变' % len(a))
ck((stock.sb_size, stock.sb_cksum, stock.volume) ==
(patched.sb_size, patched.sb_cksum, patched.volume),
'superblock 与卷名不变(size %d / cksum %08x / %r)'
% (patched.sb_size, patched.sb_cksum, patched.volume))
ck(len(stock_b) == len(patch_b), '分区长度不变(%d B)' % len(patch_b))
slot_offs = {o for o, _l in sl.values()}
bad_file = 0
checked = 0
for _p, i in stock.entries():
if i.type == 1 or i.data_off in slot_offs:
continue # 目录项 / 119 个槽位本身:槽位在下面单独查
checked += 1
if stock_b[i.data_off:i.data_off + i.size] != patch_b[i.data_off:i.data_off + i.size]:
bad_file += 1
ck(bad_file == 0, '除 119 个槽位外,其余 %d 个文件的字节**逐字节**不变(%d 处不符)'
% (checked, bad_file))
chg = [i for i in range(len(stock_b)) if stock_b[i] != patch_b[i]]
lo, hi = min(slot_offs), max(o + sl[n][1] for n, (o, _s) in sl.items())
ck(chg and chg[0] >= lo and chg[-1] < hi,
'变化区间 0x%x..0x%x 完全落在槽位区间 0x%x..0x%x 内'
% (chg[0], chg[-1], lo, hi))
ident = [n for n, (o, _s) in sl.items() if stock_b[o:o + sl[n][1]] == patch_b[o:o + sl[n][1]]]
ck(not ident, '119 个槽位全部被改写(未变的:%s)' % (ident or '无'))
print('\n' + '=' * 78)
print('独立总校验:%d 项通过,%d 项失败' % (ok, fail))
print('=' * 78)
return 1 if fail else 0
if __name__ == '__main__':
sys.exit(main())