# -*- coding: utf-8 -*- """独立总校验 —— 只读两张交付物 + 固件镜像,**不读任何构建中间产物**。 与 tools/s5e_verify.py 的分工 ----------------------------- s5e_verify.py 会读 `blob_/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(' /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(' 像素区之后**正好** 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())