# -*- coding: utf-8 -*- """独立校验交付的两张 .face —— 只读交付物 + 固件镜像,不信任任何中间产物。 role=replace -> dist/S5ePAnim.face pay 应等于**改后镜像**的字节,rb 应等于**原厂**前 64 B role=restore -> dist/S5ePAnimRestore.face pay 应等于**原厂**字节, rb 应等于**改后**前 64 B 查的东西(缺一不可): ① .face 头:magic / b4 / b5 / id / 预览块尺寸 326x326 ② 从 .face 里**反解**加载的 Lua 源码:ROLE 正确、占位符已回填、 分区键是 'health'(不是 '/dev/health',那样运行时会拼成 if=nil) ③ pay / rb 表逐条:目标偏移 == 原厂槽位 data_off、长度 == 原槽位长度(等长覆盖的底线) ④ 每条 pay 长度 == 原槽位长度,且能按 LVGL RLE 解出**正好 usize 字节**、解成 464x464 ⑤ 每条 pay 字节 == 另一侧镜像对应位置;每条 rb 指纹 == 另一侧前 64 B ⑥ 改后镜像的 inode 表 / superblock / 分区长度与原厂**逐位一致** ⑦ 负载在 .face 里原样连续存放(偏移 == 文件基址 + 魔数长度 + 相对偏移 的前提) """ import os, re, sys, json, struct, hashlib sys.stdout.reconfigure(encoding='utf-8', errors='replace') sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from paths import (PROJ, INPUTS, WORK, DIST, TARGET_IMAGE, DIRP, FACE_W, FACE_H, WF_ID, PREVIEW, SCREEN) from romfs import Romfs import lvgl_bin as L ROLE = (sys.argv[1] if len(sys.argv) > 1 else 'replace').lower() assert ROLE in ('replace', 'restore'), ROLE NAME = 'S5ePAnim' if ROLE == 'replace' else 'S5ePAnimRestore' FACE = os.path.join(DIST, NAME + '.face') PATCHED = os.path.join(WORK, 'anim', 'vela_health_anim.bin') FP_LEN = 64 ok = fail = 0 def ck(cond, msg): global ok, fail if cond: ok += 1 print(' OK ' + msg) else: fail += 1 print(' FAIL ' + msg) print('=' * 78) print('%s(role=%s) %s' % (NAME, ROLE, FACE)) print('=' * 78) if not os.path.exists(FACE): raise SystemExit('交付物不存在:%s' % FACE) face = open(FACE, 'rb').read() stock_b = open(TARGET_IMAGE, 'rb').read() patch_b = open(PATCHED, 'rb').read() stock = Romfs(TARGET_IMAGE) patched = Romfs(PATCHED) # ---------------------------------------------------------------- ① 头 print('\n[1] .face 头') ck(face[0:4] == b'\x5a\xa5\x34\x12', 'magic 5a a5 34 12') ck(face[4] in (0, 1, 2), 'b4 = %d' % face[4]) ck(face[5] == 0, 'b5 = %d(固件自带表盘 b5 也是 0)' % face[5]) ck(face[40:50] == WF_ID.encode() + b'\x00', 'id = %r(期望 %r)' % (face[40:50], WF_ID)) a32, = struct.unpack(' 0, '找到 Lua 结尾 "return ui\\n"') _start = face.rfind(b'--[[', 0, _end) lua = face[_start:_end + len(b'return ui\n')] ck(lua.startswith(b'--[[--') and b'local UIIMG = {' in lua, 'Lua 提取成功(%d B)' % len(lua)) txt = lua.decode('utf-8') ck(face.count(lua) == 1, 'Lua 原样出现 1 次(偏移 0x%x)' % face.find(lua)) c = re.search(r"local ROLE = '(\w+)'", txt) ck(c is not None and c.group(1) == ROLE, "Lua 里的 ROLE == '%s'" % ROLE) ck('__UIIMG__' not in txt and '__PAY__' not in txt and '__RB__' not in txt and '__ROLE__' not in txt, '占位符已全部回填') ck(re.search(r'local W, H = %d, %d' % (SCREEN, SCREEN), txt) is not None, '界面尺寸 W,H = %d,%d(480x480 圆屏)' % (SCREEN, SCREEN)) # ★ S4e 那版踩过的坑:Lua 表里存 '/dev/app',运行时又拼一次 -> if=nil mdev = re.search(r'local DEVS = \{([^}]*)\}', txt) ck(mdev is not None and '/dev/health' in mdev.group(1), 'DEVS 里有 health -> /dev/health:%s' % (mdev.group(1).strip() if mdev else None)) # ---------------------------------------------------------------- 偏移(自己找,不信 Lua) print('\n[3] 资源偏移(独立在 .face 里搜字节,不信 Lua 表)') ML_PAY = b'\x00S5P-PAYv1\x00\xa5\x5a\xc3\x3c' ML_FP = b'\x00S5P-FPv1\x00\x5a\xa5\x3c\xc3' offs = json.load(open(os.path.join(PROJ, 'blob_' + ROLE, 'offsets.json'), encoding='utf-8')) pay_blob = open(os.path.join(PROJ, 'blob_' + ROLE, 'payload.bin'), 'rb').read() fp_blob = open(os.path.join(PROJ, 'blob_' + ROLE, 'fp.bin'), 'rb').read() ck(face.count(ML_PAY) == 1, 'payload 魔数(%d B)在 .face 里唯一' % len(ML_PAY)) ck(face.count(ML_FP) == 1, 'fp 魔数(%d B)在 .face 里唯一' % len(ML_FP)) pay_base = face.find(ML_PAY) fp_base = face.find(ML_FP) ck(face.find(pay_blob) == pay_base, 'payload.bin 原样连续存放在 0x%x' % pay_base) ck(face.find(fp_blob) == fp_base, 'fp.bin 原样连续存放在 0x%x' % fp_base) recs = offs['records'] ck(len(recs) == 119, '负载记录 %d 条(期望 119)' % len(recs)) # 整个负载文件必须**原样连续**放在 base 处 —— 这是"偏移 == base + 魔数 + rel"成立的前提 ck(face[pay_base:pay_base + len(pay_blob)] == pay_blob, 'payload.bin %d B 在 .face 里逐字节连续' % len(pay_blob)) ck(face[fp_base:fp_base + len(fp_blob)] == fp_blob, 'fp.bin %d B 在 .face 里逐字节连续' % len(fp_blob)) # Lua 表里的偏移必须和独立量出来的一致 pay = [tuple(int(x) for x in r) for r in re.findall(r'\{(\d+),(\d+),(\d+)\}', re.search(r'pay = \{(.*?)\n \},', txt, re.S).group(1))] rb = [tuple(int(x) for x in r) for r in re.findall(r'\{(\d+),(\d+),(\d+)\}', re.search(r'rb = \{(.*?)\n \},', txt, re.S).group(1))] ck(len(pay) == len(recs) and len(rb) == len(recs), 'Lua 表 pay %d 条 / rb %d 条' % (len(pay), len(rb))) bad_off = 0 for i, r in enumerate(recs): if pay[i] != (pay_base + len(ML_PAY) + r['rel'], r['target'], r['length']): bad_off += 1 if rb[i] != (fp_base + len(ML_FP) + r['fprel'], r['target'], r['fplen']): bad_off += 1 ck(bad_off == 0, 'Lua 表里的 %d 条偏移 == 文件基址 + 魔数长度 + 相对偏移' % len(recs)) # ---------------------------------------------------------------- 原厂槽位 slots = {} 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 slots[i.data_off] = (p, i.size, i) ck(len(slots) == 119, '原厂 %s 下 %d 个槽位' % (DIRP, len(slots))) usizes = {struct.unpack(' 100, '%d/%d 条的"原厂前 64 B"与"改后前 64 B"确实不同(指纹有区分度)' % (diff_fp, len(recs))) # ---------------------------------------------------------------- ⑥ 镜像完整性 print('\n[6] 改后镜像的完整性') 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 == patched.sb_size and stock.sb_cksum == patched.sb_cksum, 'superblock 一致(size %d / cksum %08x)' % (patched.sb_size, patched.sb_cksum)) ck(stock.volume == patched.volume == 'health', '卷名 health 不变') ck(len(stock_b) == len(patch_b), '分区长度不变(%d B)' % len(patch_b)) chg = [i for i in range(len(stock_b)) if stock_b[i] != patch_b[i]] last_slot_end = max(o + slots[o][1] for o in slots) ck(chg and chg[0] >= min(slots) and chg[-1] < last_slot_end, '改动只落在负载槽位区间内(首差 0x%x,末差 0x%x,槽位区间 0x%x..0x%x)' % (chg[0], chg[-1], min(slots), last_slot_end)) # 每个槽位都必须真的被改过(不是"看起来写了、其实字节没变") unchanged = [slots[o][0] for o in slots if stock_b[o:o + slots[o][1]] == patch_b[o:o + slots[o][1]]] ck(not unchanged, '119 个槽位全部都真的写入了新字节(未变的槽位:%s)' % (unchanged or '无')) # ---------------------------------------------------------------- ⑦ UI 图 print('\n[7] UI 图片') uipairs = re.findall(r'\{(\d+),(\d+),"([a-z_]+)"\}', txt) ck(len(uipairs) == 8, 'UIIMG 条目 %d 条(期望 8)' % len(uipairs)) badui = 0 for o, l, n in uipairs: blob = open(os.path.join(PROJ, 'ui_images', n + '.bin'), 'rb').read() if int(l) != len(blob) or face[int(o):int(o) + int(l)] != blob: badui += 1 ck(badui == 0, '8 张界面素材都与 ui_images/*.bin 逐字节一致') # ---------------------------------------------------------------- 体积 print('\n[8] 体积') ck(len(face) < 20 * 1048576, '交付物 %.2f MB < 20 MB' % (len(face) / 1048576)) print(' 两张盘同 id:装一份就顶掉另一份,设备上只占一份空间') print('\n' + '=' * 78) print('%s:%d 项通过,%d 项失败' % (NAME, ok, fail)) if fail: sys.exit(1) print('全部通过 —— %s(%d B,sha256 %s)' % (os.path.basename(FACE), len(face), hashlib.sha256(face).hexdigest()[:16]))