From be11e41043e363420d15c894299c90ed22203d39 Mon Sep 17 00:00:00 2001 From: Zachery Aaron Shores-Chmielewski Date: Thu, 20 Aug 2026 21:22:54 +0400 Subject: [PATCH] Diff engine: region-aligned byte-equality + plausible string set diffs; pilot reports mt7981 wm/wa across 2022/2024/2026 builds --- .../reports/mt7981_wa-20221208-vs-20260515.md | Bin 0 -> 832 bytes .../reports/mt7981_wm-20221208-vs-20240823.md | Bin 0 -> 3868 bytes .../reports/mt7981_wm-20240823-vs-20260515.md | Bin 0 -> 3079 bytes tools/fw_diff.py | 143 ++++++++++++++++++ 4 files changed, 143 insertions(+) create mode 100644 docs/reports/mt7981_wa-20221208-vs-20260515.md create mode 100644 docs/reports/mt7981_wm-20221208-vs-20240823.md create mode 100644 docs/reports/mt7981_wm-20240823-vs-20260515.md create mode 100755 tools/fw_diff.py diff --git a/docs/reports/mt7981_wa-20221208-vs-20260515.md b/docs/reports/mt7981_wa-20221208-vs-20260515.md new file mode 100644 index 0000000000000000000000000000000000000000..9e94732cc4aa9675677e9de2b491fd927b6e612b GIT binary patch literal 832 zcmah_?QWYe6zy-G;%ZT)R4S3}03nMr$%Isjnl;&4b)8xX9Pm<93W1O|k@&p_=p*(? zW_#&(rh(8o*Y}+3%W1%E)#ugkXZ))-1{3Zuz$3ARF7NV;Cl|&k&yEnFyg- zl{`sB2~TE;$a41%!RnHB+wl&h+}kPF6EVTA-PLl7*h+3R8t`4rlRR4*Mh!fUOQ=C^ zR6{BN}awJ+~xukleZI)Np64d&(qlqQ= zmjta5lA{&K%Em3UujXoZ`@P$>Iz-8cYsvS6yKS}XC%4ev*LU04QI;*dy|>eCS<0zW zIjzbhn=e%)B36lb(8S~rBEDG20|&|QObC6N6^k#SqQi@`VDQxsMt<;Za_X})7O=r_ zFrAD?gP;FbfR^wd^n_o~zxMXc$yWciV%746ODe)z&inyw-?@P;h9ndWH{ZJ-bP ztCz;{?(_4}KMd!S_l?(?l;SqOdrqbEAr!`jG2Z@9kB)gXZR%Bck6t|&v2N(|Vnb<> X(Q%RMvW)xVV$s)pEge)VKZV90J4@%} literal 0 HcmV?d00001 diff --git a/docs/reports/mt7981_wm-20221208-vs-20240823.md b/docs/reports/mt7981_wm-20221208-vs-20240823.md new file mode 100644 index 0000000000000000000000000000000000000000..fb72b6b64b72c017ffb8e1050fbe2543d0bfc98c GIT binary patch literal 3868 zcmbVP>vHS15$9#6NkzN7fp0e4*ce+@p4B>AJKc_5Ir7rd$n#N zNxhZSMEAy{^4xpk5IyV;8{o0uZ-V<5JxLh8Cl0}*@teojsFwesp|!^cz4H3xnviy* z(rF_u)@$fSv@$qwnleBz7uyqu=!;SJqP85-Lo~dh8zZ^gZBHDc4}t~Z<4Rd&I%egN zU)Z=ekhtp)!GrFIYvxJdFd8PCoM!+Dcmhn}ba>GQ-^(&&3?eNa2gLYwb`$^Xu`c1f8BuXcTL4U>o{N;>Dq_F&e?_#+})fNJpN4R#2D3 z*_T%E*yfwplUhXipCS`DJXLeX~$-zRyF(=+%h zydy6JYH0!HvS*bUe_F@K|3`=KC6J^H)?pF=I)%LeNf>?E8Njr(Eku2@bI7%MwtnipAlsK^yNJJD*??IQo%Hzj*L&mHEOh5M2b!NL3H7d3%a z&<29D{ZuULWL0b?;1C9j+h22>JAS&7i$^mF2Cy%tv!ppQ@CZ;DV?3cL5HVJIvr!=VVvu1C?oMDJu&ukSmQ@1U*8YW0DrEDOBAEzZ{mrLT&?3Jub-26}eVT z#1X&y{4}`xycP5clciOF8nN9^K=Dq#R++OZ!YDZR^X!~Imd?FA^yRK}p3(FN;-~Sntk5w{`$k$&sSbfN_<9tI43(y{pj4se;k)MF6nxCf$d(qUeYvPW*+mm z-7@_BMppQ70CWcvEH@aVX|dUY>F9UM`Hk^OoFc&24k?I;5y99jsX+;`eq=hy{Tzc0 zZ|D&&FGcwmB4`4}EtZ9Yia!DM#)VUi>^7K~a)N~8<1+Go4>PmvPqvV6NLFoVj@hI# zMo-URrdZ+u#~abZBYgFuf|PV^(Qh&L25&Zh2qV!HwAq#KgB~DjH2(MjLW?ki~gWnBHzd60C+|uehMaRlGLx*v76EvORO2f9FME zaO2MteX47Fz8TT74(~a4ky+?r?yVxytkArJur1cd4W=e!9%UiJ$F6{7v6_{#j;qcR z%nhrorx`W#P07e7;6+jDcN+zwieo7Jsi|RxnYxx1B1thrn{Nq9Vhs5DHKDxsnwY?0 zf4-E2X{~(X{1|5ZK_L2=V=aD<4;|1V;!$vHtfEHbZGe!G}9bQ;0meA-o5ie#GyyuD}>m(DP|YKAos_ zi+MVkzL9!G&h7iZ6Yb}Jy#DiF0Toit-0@^SPa^i(T6Z$a!M-W2aIm4_5BiP+mUWh{~e zT)Eic9Wm?9ArV$ExGACt?()*<{1?`5I*)zObl#J2?fF$B?%v%6@BFQGIaqq_s`NU3 q&Ql59goPjsV9MV?*hj@_c=`KKMF&2bJWSLTxyu6dWZ5~Nj(!2|V~$e* literal 0 HcmV?d00001 diff --git a/docs/reports/mt7981_wm-20240823-vs-20260515.md b/docs/reports/mt7981_wm-20240823-vs-20260515.md new file mode 100644 index 0000000000000000000000000000000000000000..9086532d1a01ad14f9c512bc0271eef3544b9351 GIT binary patch literal 3079 zcmbVO+iv4F5bblmf<+Ku7qAW8tk?#!sAbEE(Kr!gWs_pDuw{vMgq27wMa4>y=YBvx zv0u_5b%~Ts_o0Rb4rh4g42PVNPEo?o$0K!fOg5UL8Oli02fK)%r>tu+m7zKmmXnh{ znoiJ`_X$pUPH;c*BhnXc)L~y85B2`alP*_|rmE^@xpMloYt>XW#n_?`ocU3V8EP4H z#rUNMilH&oM%G85vHk~*j8g?-ZQP})4a0`U90{6}(^KT)Lqth-a`J-wFl6WjK?Yt> zj0u`nqXpw0`y5UG`Ri{n!QXQ~M!RQ@GxT!u((m`*eZZwsD2kz~rlJ6BNBJWRsy33- zdK7EGu^5C^Q3lGHa9e?ma&-+hblnuos_|!FO;h|XSTo1x5?u>Qp_Sa4Z;GbkSpTpx0=uDEdxmqxJEq+;<~%(Zg42 z0FMKs4K~hoMF!poUGQi$Fl*{P(25@fT4#Kym&d2pf>Mmiq>aQf-58v=(Auz*G;IiA zv8)lg=yP*0YN1UCyu}R#Q+jKJF1mABAoOuXR)s#+;l=~^ST{{EcEQ8(U@WLb6|6an z$RQIYfJ0FM7W!bG_fX)cDJV<@ilvXXXW0a~(~UEAr>o7C2W`VpHr90Z`?h8MVZrRy zerOh?%$;8OB#d$YQ?6P^Ls@fwM)(Ash0UT|U6K$NLf1nbgjON$W1lR;;!CyiYZp$w z5qczurW|S;@#hqSJ(upMi}VydWR1M2Wa}*4*kKU(6KS$w*}OKb;b*B9Jn%>BwW@~j za1Y!yN!mxbuJS=xJTe~*dO<*6=?Tix6XpGzkbb!_gJp|<$X3{nX$~v04 zGe=J9Ge4Q2^*vhA5JN7|oT3?Y9+377f0xkbr;Fdfn^lTi*h^50 zi&}i$;k5%un>1lwr_&(tSmehydD3WC5%G%IBM!ca^QyNbwJmHl_jfUdHIme^y6)XL z@lw8oJK*9Bb1zD&6jTNhafvIZcVt;ByhqFuNPvs`A`cM8#h0`2YY%xiNu`Le^Y|<- z2B@Wv`%k+Ye<4Mm&63bf`3*g~KF3S4r-NMeidB#`yZO&Ga01Pu{o#_*Q26-l$5GhQ zU(=&QkC?gG51-%BuQBH_h8xe@-fGlay-el%kka(Vk5tNIn^V#zkTF@5X8dhnua>vu zVtaS#kKPU&$eH8WPjH6xcAtrxAk!NOwR2>W2NKiLr@Y>x$DSo6%$xMYi!EH$@_r^c zD;!1xoMoRM`DGNY4hdgC9bn0kvQ^GQ~)>n9GPf(f%OB*VwfT9Aek1z=lG|vKM(VrO6JzCnED|>V6Y&>)!xFYf- zil9W6JVjC+q2Q8cA1_|E8Nx6 zLpQf``$8$sl+i)^;}p>t3vyQ}Gopq!}EeAS-%st@qJE$`UC`tlkrpSiXu`c?q^B F{{o;{m=FK} literal 0 HcmV?d00001 diff --git a/tools/fw_diff.py b/tools/fw_diff.py new file mode 100755 index 0000000..4f53b29 --- /dev/null +++ b/tools/fw_diff.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +"""Diff two Connac2 firmware container versions (same file, different builds). + +Compares: metadata (hidden build strings, sizes), region tables aligned by +address, per-region byte-equality ratio, and printable-string set changes +(added/removed — the human-readable changelog signal). + +Usage: fw_diff.py A.bin B.bin [--md out.md] +""" +import argparse +import re +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from mtk_fw_extract import REGION, TRAILER # noqa: E402 + + +def parse(d: bytes): + t = TRAILER.unpack_from(d, len(d) - TRAILER.size) + n = t[2] + table = len(d) - TRAILER.size - n * REGION.size + off, regions = 0, {} + for i in range(n): + r = REGION.unpack_from(d, table + i * REGION.size) + addr, ln, feat = r[4], r[5], r[6] + regions[addr] = {'len': ln, 'feat': feat, 'data': d[off:off + ln]} + off += ln + hidden = None + gap = table - off + if gap > 0: + runs = [x.strip(b'#') for x in re.findall(rb'[\x20-\x7e]{8,}', d[off:table])] + runs = [x for x in runs if len(x) >= 8] + hidden = runs[0].decode() if runs else None + return {'trailer_date': t[7].decode(errors='replace'), + 'n_region': n, 'hidden': hidden, 'regions': regions, + 'size': len(d)} + + +def equal_ratio(a: bytes, b: bytes) -> float: + if not a and not b: + return 1.0 + n = min(len(a), len(b)) + eq = sum(x == y for x, y in zip(a[:n], b[:n])) + return eq / max(len(a), len(b)) + + +JUNK = re.compile(r'^[A-Za-z0-9 _%:.\-(){}\[\],;+*/=<>!\'"?&|$@#~`^\\]*$') + + +def strings(data: bytes, minlen=6): + """Human-plausible strings only: leading identifier character, a real + alphabetic run, and letter-density floor. Filters random binary that + happens to decode printable.""" + out = set() + for m in re.findall(rb'[\x20-\x7e]{%d,}' % minlen, data): + s = m.strip(b'#').decode(errors='replace') + if len(s) < minlen: + continue + if not re.match(r'^[A-Za-z%_]', s): + continue + if not re.search(r'[A-Za-z]{3,}', s): + continue + letters = sum(c.isalpha() or c == '_' for c in s) + if letters / len(s) < 0.5: + continue + out.add(s) + return out + + +def diff(a_path: Path, b_path: Path) -> dict: + A, B = parse(a_path.read_bytes()), parse(b_path.read_bytes()) + addrs = sorted(set(A['regions']) | set(B['regions'])) + regions = [] + for a in addrs: + ra, rb = A['regions'].get(a), B['regions'].get(a) + if ra and rb: + regions.append({'addr': f'0x{a:08x}', 'in': 'both', + 'len_a': ra['len'], 'len_b': rb['len'], + 'feat_a': f"0x{ra['feat']:02x}", + 'feat_b': f"0x{rb['feat']:02x}", + 'equal': round(equal_ratio(ra['data'], rb['data']), 4)}) + else: + r = ra or rb + regions.append({'addr': f'0x{a:08x}', + 'in': 'a' if ra else 'b', + 'len': r['len'], 'feat': f"0x{r['feat']:02x}"}) + sa = set().union(*[strings(r['data']) for r in A['regions'].values()]) + sb = set().union(*[strings(r['data']) for r in B['regions'].values()]) + return { + 'a': {'file': str(a_path), 'build': A['hidden'], + 'trailer_date': A['trailer_date'], 'size': A['size']}, + 'b': {'file': str(b_path), 'build': B['hidden'], + 'trailer_date': B['trailer_date'], 'size': B['size']}, + 'regions': regions, + 'strings_added': sorted(sb - sa), + 'strings_removed': sorted(sa - sb), + } + + +def to_markdown(r: dict, cap=60) -> str: + L = [f"# {Path(r['a']['file']).name} vs {Path(r['b']['file']).name}", + '', + f"- A: `{r['a']['build']}` (trailer {r['a']['trailer_date']}, {r['a']['size']}B)", + f"- B: `{r['b']['build']}` (trailer {r['b']['trailer_date']}, {r['b']['size']}B)", + '', '## Regions', '', + '| addr | in | len A | len B | feat A→B | equal bytes |', + '|---|---|---|---|---|---|'] + for g in r['regions']: + if g['in'] == 'both': + L.append(f"| {g['addr']} | both | {g['len_a']} | {g['len_b']} " + f"| {g['feat_a']}→{g['feat_b']} | {g['equal']:.1%} |") + else: + L.append(f"| {g['addr']} | {g['in']} only | {g.get('len','')} | " + f" | {g.get('feat','')} | — |") + L += ['', f"## Strings added in B ({len(r['strings_added'])}, capped {cap})", ''] + L += [f'- `{s}`' for s in r['strings_added'][:cap]] + L += ['', f"## Strings removed in B ({len(r['strings_removed'])}, capped {cap})", ''] + L += [f'- `{s}`' for s in r['strings_removed'][:cap]] + return '\n'.join(L) + '\n' + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument('a') + ap.add_argument('b') + ap.add_argument('--md') + ap.add_argument('--json') + args = ap.parse_args() + r = diff(Path(args.a), Path(args.b)) + md = to_markdown(r) + if args.md: + Path(args.md).parent.mkdir(parents=True, exist_ok=True) + Path(args.md).write_text(md) + if args.json: + Path(args.json).write_text(__import__('json').dumps(r, indent=2)) + print(md[:2500]) + print(f"... [{len(r['strings_added'])} added / " + f"{len(r['strings_removed'])} removed strings]") + + +if __name__ == '__main__': + main()