# -*- coding: utf-8 -*- """S4 图标表盘交付物的独立校验(不信任构建脚本的账本)。 从 dist/S5Icons.face 里把 Lua 源码抠出来 -> 解析 UIIMG / PAYTBL 两张表 -> 每一条都拿去和 **固件镜像**(inputs/target 原厂 + .work/s4_from_s5 改后)以及 ui_images/ 目录逐字节比对;另外单独验证 voice_aivs 那条的字节就是 assets/override/voice_aivs.png 按槽位格式重编码的结果。 用法: python tools/s4e_verify.py """ import os, re, sys, json, hashlib sys.stdout.reconfigure(encoding='utf-8', errors='replace') sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from romfs import Romfs from lvgl_bin import decode_lvgl_bin import iconpack as ip from PIL import Image, ImageFilter from paths import PROJ, INPUTS, WORK, DIST FACE = os.path.join(DIST, 'S5Icons.face') STOCK = os.path.join(INPUTS, 'target', 'vela_app.bin') PATCHED = os.path.join(WORK, 's4_from_s5', 'vela_app_s5icons.bin') MANIFEST = os.path.join(WORK, 's4_from_s5', 'manifest.json') WID = '362150101' fails = [] def ck(cond, msg): print((' OK ' if cond else ' FAIL ') + msg) if not cond: fails.append(msg) face = open(FACE, 'rb').read() _end = face.find(b'return ui\n') if _end < 0: raise SystemExit('face 里找不到 Lua 结尾') _start = face.rfind(b'--[[', 0, _end) lua = face[_start:_end + len(b'return ui\n')] if not lua.startswith(b'--[[--') or b'local UIIMG = {' not in lua: raise SystemExit('Lua 提取失败:%r ... %r' % (lua[:24], lua[-24:])) txt = lua.decode('utf-8') print('== 头部 ==') ck(face[0:4] == b'\x5a\xa5\x34\x12', 'magic 5a a5 34 12') ck(face[40:50] == (WID + '\x00').encode(), 'id = %r' % face[40:50]) ck(face[5] == 0, 'b5 = %d(固件自带表盘也是 0)' % face[5]) ck(face.count(lua) == 1, 'Lua 源码原样出现 %d 次(%d B)' % (face.count(lua), len(lua))) print(' 文件 %d B' % len(face)) print('== UI 图片 ==') ui = re.findall(r'\{(\d+),(\d+),"([a-z_]+)"\}', txt) files = {f[:-4]: open(os.path.join(PROJ, 'ui_images', f), 'rb').read() for f in os.listdir(os.path.join(PROJ, 'ui_images')) if f.endswith('.bin')} ck(len(ui) == len(files), 'UIIMG %d 条(ui_images 里 %d 个)' % (len(ui), len(files))) for o, l, n in ui: o, l = int(o), int(l) ck(n in files and face[o:o + l] == files[n], '%-12s @0x%06x len=%-6d 与 ui_images/%s.bin 一致' % (n, o, l, n)) print('== 负载表结构 ==') _pay_at = txt.index('pay = {') _rb_at = txt.index('rb = {', _pay_at) pay = [(int(a), int(b), int(c)) for a, b, c in re.findall(r'\{(\d+),(\d+),(\d+)\}', txt[_pay_at:_rb_at])] rb = [(int(a), int(b), int(c)) for a, b, c in re.findall(r'\{(\d+),(\d+),(\d+)\}', txt[_rb_at:])] exp = json.load(open(MANIFEST, encoding='utf-8'))['records'] # 计划替换的 81 条 pj = json.load(open(os.path.join(PROJ, 'payload.json'), encoding='utf-8')) emit = pj['records'] # 实际下发的(含 canary) dropped = pj['dropped_identical'] ck(len(emit) - 1 == len(exp) - len(dropped), '下发 %d 条 = 计划 %d 条 - 与源逐字节相同的 %d 条 %s + canary' % (len(emit), len(exp), len(dropped), dropped)) ck(len(pay) == len(rb) == len(emit), 'pay %d 条 / rb %d 条(payload.json %d 条)' % (len(pay), len(rb), len(emit))) ck(len(pay) > 0 and len(pay) == len(rb), '两张表条数相同') print('== 逐条字节比对(负载 vs 改后镜像、回滚 vs 原厂镜像)==') stock = open(STOCK, 'rb').read() patched = open(PATCHED, 'rb').read() ck(len(stock) == len(patched) and stock[:8] == b'-rom1fs-', '两个整分区镜像等长且是 ROMFS') bad, misalign, manifest_bad = 0, 0, 0 for i, ((po, pt, pl), (ro, rt, rl)) in enumerate(zip(pay, rb)): if not (po and ro) or (pt, pl) != (rt, rl): manifest_bad += 1 continue if pt % 4 or pt + pl > len(stock): # RLE 素材的长度可以是任意值,只有偏移必须对齐 misalign += 1 if (face[po:po + pl] != patched[pt:pt + pl] or len(face[po:po + pl]) != pl or face[ro:ro + pl] != stock[pt:pt + pl] or len(face[ro:ro + pl]) != pl): bad += 1 print(' FAIL #%d %s off=0x%x len=%d' % (i, emit[i]['name'] if i < len(emit) else '?', pt, pl)) ck(bad == 0, '%d 条:负载 == 改后镜像,回滚 == 原厂镜像' % len(pay)) ck(misalign == 0, '全部 4 对齐且不越界') ck(manifest_bad == 0, 'pay/rb 的目标偏移与长度一一对应') names_in_lua = [e['name'] for e in emit] plan_only = [r['name'] for r in exp if r['name'] not in dropped] ck(names_in_lua[1:] == plan_only, '下发名单(去掉 canary)== 计划名单(去掉 %s)' % dropped) print('== 负载名单 ==') names = [r['name'] for r in exp] ck('voice_aivs' in names, 'voice_aivs 在负载里') ck(sorted(names) == sorted(set(names)), '没有重名记录') print('== voice_aivs 的字节 == 用户 PNG 重编码 ==') rec = next(r for r in exp if r['name'] == 'voice_aivs') r = Romfs(STOCK) sheet = {p: i for p, i in r.entries() if p == 'voice/aivs/launcher.bin'} ino = sheet['voice/aivs/launcher.bin'] ck(ino.data_off == rec['offset'] and ino.size == rec['length'], 'manifest 偏移/长度 == ROMFS inode(0x%08x / %d)' % (ino.data_off, ino.size)) h, stock_img = decode_lvgl_bin(r.read(ino)) def ink_bbox(img, thr=16): a = img.convert('RGBA').getchannel('A'); w, hh = a.size; px = a.load() x0, y0, x1, y1 = w, hh, -1, -1 for y in range(hh): for x in range(w): if px[x, y] > thr: x0 = min(x0, x); y0 = min(y0, y); x1 = max(x1, x); y1 = max(y1, y) return None if x1 < 0 else (x0, y0, x1, y1) sp = os.path.join(PROJ, 'assets', 'override', 'voice_aivs.png') src = Image.open(sp).convert('RGBA') sb = ink_bbox(stock_img) target = max(sb[2] - sb[0] + 1, sb[3] - sb[1] + 1) nb = ink_bbox(src) art = src.crop((nb[0], nb[1], nb[2] + 1, nb[3] + 1)).resize((target, target), Image.LANCZOS) cv = Image.new('RGBA', (h['w'], h['h']), (0, 0, 0, 0)) cv.paste(art, ((h['w'] - target) // 2, (h['h'] - target) // 2)) want = ip.encode(cv, h['cf'], h['w'], h['h'], h['flags']) ck(len(want) == ino.size, '按槽位格式重编码 %d B == 槽位 %d B' % (len(want), ino.size)) got = [(o, t, l) for (o, t, l) in pay if t == rec['offset']] ck(bool(got) and face[got[0][0]:got[0][0] + rec['length']] == want, '交付物里 voice_aivs 的负载字节 == 用户 PNG 重编码结果') ck(patched[ino.data_off:ino.data_off + ino.size] == want, '改后整分区镜像里也是同一份字节') ck(stock[ino.data_off:ino.data_off + ino.size] != want, '与 S4 原厂字节确实不同') ck(os.path.basename(sp) == 'voice_aivs.png' and hashlib.sha256(open(sp, 'rb').read()).hexdigest()[:16] == '362166b54ac03f2b', 'override 源图 sha256 362166b54ac03f2b(= 用户给的 smallai.png)') print('== ROMFS 结构未被破坏 ==') a = [(p, i.off, i.next, i.spec, i.size, i.cksum) for p, i in Romfs(STOCK).entries()] b = [(p, i.off, i.next, i.spec, i.size, i.cksum) for p, i in Romfs(PATCHED).entries()] ck(a == b, 'inode 表逐位不变(%d 条)' % len(a)) ck(Romfs(STOCK).sb_size == Romfs(PATCHED).sb_size and Romfs(STOCK).sb_cksum == Romfs(PATCHED).sb_cksum, 'superblock 不变') r2 = Romfs(PATCHED) i1 = {p: i for p, i in Romfs(STOCK).entries()} i2 = {p: i for p, i in r2.entries()} changed = sorted(p for p in i1 if Romfs(STOCK).read(i1[p]) != r2.read(i2[p])) ck(len(changed) == len(pay) - 1, '真正变化的文件 %d 个(负载 %d 条,其中 canary 是分区头)' % (len(changed), len(pay))) print('== 负载/回滚文件在 .face 里连续存放 ==') pfiles = [(f, open(os.path.join(PROJ, 'payload', f), 'rb').read()) for f in sorted(os.listdir(os.path.join(PROJ, 'payload'))) if f.endswith('.bin')] rbfiles = [(f, open(os.path.join(PROJ, 'payload_rollback', f), 'rb').read()) for f in sorted(os.listdir(os.path.join(PROJ, 'payload_rollback'))) if f.endswith('.bin')] ck(len(pfiles) == len(pay), 'payload/ 里 %d 个文件 == %d 条' % (len(pfiles), len(pay))) hit_pay = sum(1 for (o, t, l) in pay if any(face[o:o + l] == d for _, d in pfiles)) hit_rb = sum(1 for (o, t, l) in rb if any(face[o:o + l] == d for _, d in rbfiles)) ck(hit_pay == len(pay), '每条负载都能在 payload/ 里找到同名同字节的文件(%d/%d)' % (hit_pay, len(pay))) ck(hit_rb == len(rb), '每条回滚都能在 payload_rollback/ 里找到(%d/%d)' % (hit_rb, len(rb))) print() if fails: print('!! %d 项未通过' % len(fails)) sys.exit(1) print('全部通过 —— %s (%d B, sha256 %s)' % (os.path.basename(FACE), len(face), hashlib.sha256(face).hexdigest()[:16]))