diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..bc59634 --- /dev/null +++ b/.flake8 @@ -0,0 +1,34 @@ +# flake8 configuration for this repository. +# +# Three settings, each with the measurement behind it rather than a preference. +# +# max-line-length = 160 +# 120 was tried first and two lines still exceeded it: the secret-scanner rule table in +# scan_leaks.py holds raw regex source of 148 and 156 characters. Splitting a regex inside a raw +# string either changes the pattern or forces concatenation that is harder to read than the long +# line, so the limit is set from what a real string in this repository needs (156) with a little +# room, and a line over 160 is a genuine defect rather than a style disagreement. +# +# max-complexity is not set, and C901 is excluded below with the measurement: this repository's +# parsers and dispatchers reach 21-41 (check_commands.command_findings is 41, a flat if/elif chain +# over finding kinds). Setting a threshold above the maximum would be a number chosen to pass rather +# than a standard; splitting them into one function per case would move the shape of the format away +# from the code that reads it, and looking like the specification is this repository's argument. +# The numbers are recorded here so a future pass can decide to refactor with them in hand. +# +# extend-ignore: +# C901 see above -- reported here as a measurement, not enforced +# E203 slice whitespace (`data[start : end]`); flake8 and every autoformatter disagree, and the +# repository has no autoformatter +# +# Run from the repository root: +# flake8 *.py skills/apk-reverse/scripts/*.py +# +# Enforced in CI: the `lint` job in .github/workflows/ci.yml runs this exact command alongside mypy, +# on a bare interpreter, so the configuration is exercised in the poorest environment it must keep +# working in. + +[flake8] +max-line-length = 160 +extend-ignore = C901,E203 +exclude = .git,__pycache__,tools,build,dist diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 10c3426..e2e48bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,13 +18,19 @@ # rather than failing. Run it locally after downloading the sample. # # Coverage is therefore: static structure, documentation anchors, budget, command -# references, pure parsers, CLI contracts and no-device flows. Nothing more is -# claimed. +# references, routing coherence, pure parsers, CLI contracts, typing/style lint and +# no-device flows. Nothing more is claimed. name: ci on: push: pull_request: +# +# NOTE ON TRIGGERS: no `paths:` filter, deliberately. A filter that watches only +# `*.py` and the lint configuration would skip the structural gates on exactly the +# changes they exist to catch -- a reference index or a documented command edited +# without touching a script. check_refs.py and check_routing.py are documentation +# gates, so they have to run on documentation commits. permissions: contents: read @@ -49,7 +55,7 @@ jobs: PYTHONUTF8: "1" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - uses: actions/setup-python@v5 with: @@ -88,3 +94,37 @@ jobs: - name: Summarise skipped and expected-failure coverage if: always() run: python -m pytest -q -ra --no-header -p no:cacheprovider 2>&1 | tail -n 40 + + lint: + # Static checks over the same tracked surface the local commands cover, so what passes locally + # cannot drift from what CI enforces. Both tools read their configuration (mypy.ini, .flake8) + # from the repository root automatically. + # + # The environment is deliberately bare -- only mypy and flake8, none of PyYAML, capstone, tiktoken + # or frida. That is the poorest environment the configuration must keep working in, which is what + # mypy.ini's ignore_missing_imports overrides exist for: CI exercises the fallbacks instead of a + # machine-local install hiding a regression. + # + # One interpreter, not the matrix: the checks are static, and the matrix's job is runtime + # behaviour, which lint does not observe. `check_routing.py` -- a gate the test job also runs -- + # is deliberately not repeated here. + # + # Verified locally before being added: on a venv with only mypy and flake8, `flake8` reports 0 and + # `mypy` reports "Success: no issues found in 55 source files". + name: lint (mypy + flake8) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install lint dependencies + run: python -m pip install --upgrade mypy flake8 + + - name: mypy + run: mypy *.py skills/apk-reverse/scripts/*.py + + - name: flake8 + run: flake8 *.py skills/apk-reverse/scripts/*.py diff --git a/.gitignore b/.gitignore index 073eddd..c45b555 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,8 @@ __pycache__/ .vscode/ .idea/ .DS_Store + +# local input material -- the same rule as 存在问题和例子/: the reports these passes are based on +# are the user's material, not content of this repository +新的建议和思路/ +朋友全面审查和建议/ diff --git a/check_budget.py b/check_budget.py index d65fd50..eb6ae32 100644 --- a/check_budget.py +++ b/check_budget.py @@ -239,7 +239,6 @@ def check_hedging(): if ln.strip() and not ln.startswith('|') and not ln.startswith('#') and not ln.startswith('---')] hedged = [(i + 1, ln.strip()) for i, ln in enumerate(lines) if HEDGE_RE.search(ln)] - imperative = [ln for ln in body if re.match(r'^\s*[-*]? *(\*\*)?[A-Z]', ln)] ratio = (len(hedged) / len(body)) if body else 0.0 notes = [] if hedged: @@ -267,7 +266,7 @@ def check_restated(): for label, pats in RESTATED.items(): hits = [p for p, t in corpus.items() if any(re.search(x, t, re.I) for x in pats)] # files whose job is to state results do not count as drift - drift = [h for h in hits if not any(h.startswith(l) for l in LEGITIMATE_HOMES)] + drift = [h for h in hits if not any(h.startswith(p) for p in LEGITIMATE_HOMES)] if len(drift) > 3: notes.append('restated in %d non-record files: %s -- %s' % (len(drift), label, ', '.join(sorted(d.split('/')[-1] for d in drift)))) @@ -291,7 +290,7 @@ def check_discoverable(): name = os.path.basename(f) if name not in body: out.append(('fail', 'references/%s is named neither in SKILL.md nor in ' - 'references/routing.md -- nothing will load it' % name)) + 'references/routing.md -- nothing will load it' % name)) return out @@ -349,23 +348,23 @@ def main(): if args.json: print(json.dumps({ - 'findings': [{'level': l, 'message': m} for l, m in findings], + 'findings': [{'level': ln, 'message': m} for ln, m in findings], 'notes': notes, 'index_lines': n_idx, 'total_lines': content, 'token_estimate': toks, }, indent=2)) else: - for l, m in findings: - print('%s %s' % ({'ok': ' ok ', 'warn': ' WARN ', 'fail': ' FAIL '}[l], m)) + for ln, m in findings: + print('%s %s' % ({'ok': ' ok ', 'warn': ' WARN ', 'fail': ' FAIL '}[ln], m)) for m in notes: print(' note %s' % m) - n_fail = sum(1 for l, _ in findings if l == 'fail') - n_warn = sum(1 for l, _ in findings if l == 'warn') + n_fail = sum(1 for ln, _ in findings if ln == 'fail') + n_warn = sum(1 for ln, _ in findings if ln == 'warn') print() print('== budget: %d failure(s), %d warning(s), %d note(s) ==' % (n_fail, n_warn, len(notes))) - n_bad = sum(1 for l, _ in findings if l == 'fail' or (args.strict and l == 'warn')) + n_bad = sum(1 for ln, _ in findings if ln == 'fail' or (args.strict and ln == 'warn')) return 1 if n_bad else 0 diff --git a/check_commands.py b/check_commands.py index a210451..9d178c0 100644 --- a/check_commands.py +++ b/check_commands.py @@ -47,7 +47,6 @@ Usage Exit codes: 0 = every documented command matches its script, 1 = at least one inconsistency, 2 = usage error, 4 = internal error. The last line is `RESULT=`. """ -from __future__ import annotations import argparse import ast @@ -116,17 +115,20 @@ EXTERNAL_SCRIPTS = { # normalised command text), not a line number, so it survives edits above it. Each entry # needs a reason, and the report prints them: an exclusion a reader can audit is not a # hole, whereas a silent one would be indistinguishable from a missed drift. +_FINDINGS_QUOTE_REASON = ( + "FINDINGS.md quotes this command as the historical defect it records. It is " + "evidence about the drift, not an instruction a reader runs; the page's own text " + "says the script has no --pkg.") +_RECONSTRUCTION_QUOTE_REASON = ( + "the same command appears in this pass's evidence file as the `--fix-report` " + "output that proves the checker catches it -- a reproduction of a defect, printed " + "with the fix beside it.") + QUOTED_COMMANDS = { ('docs/tool-verification/FINDINGS.md', - 'python scripts/sig_probe.py --live --pkg '): - "FINDINGS.md quotes this command as the historical defect it records. It is " - "evidence about the drift, not an instruction a reader runs; the page's own text " - "says the script has no --pkg.", + 'python scripts/sig_probe.py --live --pkg '): _FINDINGS_QUOTE_REASON, ('docs/tool-verification/EXTENSION-reconstruction.md', - 'python scripts/sig_probe.py --live --pkg '): - "the same command appears in this pass's evidence file as the `--fix-report` " - "output that proves the checker catches it -- a reproduction of a defect, printed " - "with the fix beside it.", + 'python scripts/sig_probe.py --live --pkg '): _RECONSTRUCTION_QUOTE_REASON, } @@ -543,7 +545,7 @@ def find_script_token(tokens): return None, 'no script argument' -_WORKBENCH_INDEX = {} +_WORKBENCH_INDEX: dict[str, set[str]] = {} def _workbench_names(): @@ -768,7 +770,7 @@ def command_findings(doc_path, text, spec_cache, include_workbench): known = spec['flags'] known_long = [f for f in known if f.startswith('--')] - unknown, positions, warnings = [], [], [] + unknown, positions = [], [] before = len(findings) expect_value = False after_ddash = False diff --git a/check_refs.py b/check_refs.py index 54eb85e..91e7a06 100644 --- a/check_refs.py +++ b/check_refs.py @@ -186,7 +186,9 @@ def main(): target_name = m.group('file') if target_name is None: - target_text, target_list = text, [path] + # `target_list` is kept as part of the pair this branch assigns so both arms + # of the branch below produce the same two names; only `target_text` is read. + target_text, target_list = text, [path] # noqa: F841 target_label = where else: candidates = [c for c in by_name.get(target_name, []) diff --git a/check_repo.py b/check_repo.py index 3d719a8..67c1209 100644 --- a/check_repo.py +++ b/check_repo.py @@ -25,7 +25,6 @@ On the root README: import ast import os import re -import re import subprocess import sys @@ -46,7 +45,7 @@ NAMED_PATH_RE = re.compile( BARE_PATH_RE = re.compile(r'(?references|scripts)/' r'(?P[A-Za-z0-9_\-]+\.(?:md|py|js))') -fail = [] +fail: list[str] = [] def read(path): diff --git a/check_routing.py b/check_routing.py index 9543d8d..cee861b 100644 --- a/check_routing.py +++ b/check_routing.py @@ -24,7 +24,6 @@ import argparse import glob import json import os -import re import sys ROOT = os.path.dirname(os.path.abspath(__file__)) @@ -38,10 +37,13 @@ ROUTE = os.path.join(REFS, 'routing.md') # the row's *cells*, not the whole line -- every data row also starts and ends with `|`, so any # whole-line pattern loose enough to accept a separator also accepts every data row, and the reverse # mistake drops every data row and reports an intact inventory as broken. + + def is_separator(row): cells = [c for c in row.strip().strip('|').split('|')] return bool(cells) and all(c.strip() and set(c.strip()) <= set('-:') for c in cells) + # Headings are matched by prefix: they contain an em dash, and comparing against an ASCII hyphen # silently reports "table not found" instead of failing. PREFIXES = { diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..a3ccc7c --- /dev/null +++ b/mypy.ini @@ -0,0 +1,49 @@ +# mypy configuration for this repository. +# +# The repository is designed to check out and run its gates with only the standard library plus +# pytest (see pytest.ini and .github/workflows/ci.yml): frida, capstone, tiktoken and PyYAML are +# optional runtime dependencies that scripts import lazily or behind try/except, with documented +# fallbacks. Do not require them -- or their stubs -- to be installed for +# `mypy *.py skills/apk-reverse/scripts/*.py` to pass. When they *are* installed and typed +# (e.g. types-PyYAML), mypy still uses the real stubs; these overrides only apply where the module is +# missing or untyped. +# +# When installed, capstone ships no py.typed marker; frida and tiktoken are host-side extras. +# Everything else in the tree is analyzed normally, including every gate script -- which is the point: +# check_repo/check_refs/check_budget/check_routing/check_commands carry the behaviour of the whole +# repository, so they are worth type-checking rather than excluding. +# +# Enforced in CI: the `lint` job in .github/workflows/ci.yml runs mypy with exactly this +# configuration, on a bare interpreter (no frida/capstone/tiktoken/PyYAML installed), so the +# overrides below are exercised rather than skipped. +# +# Verified locally on 2026-09-22: `mypy` over 55 files reports no issues with the overrides in place, +# on a venv containing only mypy and flake8. + +[mypy] +python_version = 3.9 +warn_unused_ignores = False + +[mypy-frida] +ignore_missing_imports = True + +[mypy-frida.*] +ignore_missing_imports = True + +[mypy-capstone] +ignore_missing_imports = True + +[mypy-capstone.*] +ignore_missing_imports = True + +[mypy-tiktoken] +ignore_missing_imports = True + +[mypy-tiktoken.*] +ignore_missing_imports = True + +[mypy-yaml] +ignore_missing_imports = True + +[mypy-yaml.*] +ignore_missing_imports = True diff --git a/skills/apk-reverse/scripts/blob_decode.py b/skills/apk-reverse/scripts/blob_decode.py index 1ec82ea..b5baa30 100644 --- a/skills/apk-reverse/scripts/blob_decode.py +++ b/skills/apk-reverse/scripts/blob_decode.py @@ -36,12 +36,10 @@ Notes: * Output is written to --out; nothing on device is touched. * The winning parameters are printed as a copy-pasteable line for --encode. """ -from __future__ import annotations import argparse import base64 import binascii -import json import re import sys import time diff --git a/skills/apk-reverse/scripts/capabilities.py b/skills/apk-reverse/scripts/capabilities.py index 4e62f59..8e3d80a 100644 --- a/skills/apk-reverse/scripts/capabilities.py +++ b/skills/apk-reverse/scripts/capabilities.py @@ -60,7 +60,6 @@ Usage Exit codes: 0 = every capability ok, 3 = at least one blocked, 2 = usage error, 4 = internal error. The last line is `RESULT=`. """ -from __future__ import annotations import argparse import ast @@ -700,8 +699,8 @@ class Probe: rc, v = _run(['adb', '-s', serial, 'shell', 'su -c id'], timeout=15) out['root'] = rc == 0 and 'uid=0' in v rc, ps = _run(['adb', '-s', serial, 'shell', 'ps -A'], timeout=20) - hits = [l for l in ps.splitlines() - if 'frida' in l.lower() and 'grep' not in l.lower()] + hits = [row for row in ps.splitlines() + if 'frida' in row.lower() and 'grep' not in row.lower()] if rc == 0: out['frida_server'] = bool(hits) self._device = out diff --git a/skills/apk-reverse/scripts/coldstart.py b/skills/apk-reverse/scripts/coldstart.py index 019bc5a..cc0ea3c 100644 --- a/skills/apk-reverse/scripts/coldstart.py +++ b/skills/apk-reverse/scripts/coldstart.py @@ -90,7 +90,10 @@ class Device(object): base = ['adb'] try: with open(path, 'wb') as fh: - r = subprocess.run(base + ['exec-out', 'screencap', '-p'], + # The PNG is validated from the file that comes back, so the exit status is + # deliberately discarded: a non-zero screencap that still wrote a valid PNG is + # a success, and the header check below is the authority. + r = subprocess.run(base + ['exec-out', 'screencap', '-p'], # noqa: F841 stdout=fh, timeout=timeout) size = os.path.getsize(path) if size < 8: @@ -161,14 +164,14 @@ def main(argv): formatter_class=argparse.RawDescriptionHelpFormatter, epilog=__doc__.split('Usage')[-1][:1200]) ap.add_argument('--serial', help='adb serial (REQUIRED when several devices are ' - 'online, so the wrong one is not picked at random)') + 'online, so the wrong one is not picked at random)') ap.add_argument('--pkg', required=True, help='package name') ap.add_argument('--activity', help='component to start (default: monkey/launcher)') ap.add_argument('--out', default='coldstart', help='output directory') ap.add_argument('--duration', type=float, default=14.0, help='seconds to capture') ap.add_argument('--interval', type=float, default=0.8, help='seconds between frames') ap.add_argument('--expect-activity', help='warn if the foreground activity is not ' - 'this (and not the package at all)') + 'this (and not the package at all)') ap.add_argument('--no-su', action='store_true', help='never use device root') ap.add_argument('--keep-old', action='store_true', help='do not clear existing screenshots in the output dir') @@ -180,8 +183,8 @@ def main(argv): if not args.serial: r = run(['adb', 'devices']) - online = [l.split()[0] for l in (r.stdout or '').splitlines()[1:] - if len(l.split()) >= 2 and l.split()[1] == 'device'] + online = [row.split()[0] for row in (r.stdout or '').splitlines()[1:] + if len(row.split()) >= 2 and row.split()[1] == 'device'] if len(online) > 1: print('error: %d devices online (%s) and no --serial given. adb would ' 'pick one at random and you would debug the wrong target.' @@ -275,8 +278,8 @@ def main(argv): for name in SIGNAL_PATTERNS: print(' %-32s %s' % (name, counts.get(name, 0))) - m = re.findall(r'Displayed ([^:]+): \+([0-9]+)ms', open(log_path, encoding='utf-8', - errors='replace').read()) + m = re.findall(r'Displayed ([^:]+): \+([0-9]+)ms', + open(log_path, encoding='utf-8', errors='replace').read()) if m: print('\n== launch timing (logcat Displayed)') for comp, ms in m: diff --git a/skills/apk-reverse/scripts/device_shell.py b/skills/apk-reverse/scripts/device_shell.py index 87a07e0..2300d36 100644 --- a/skills/apk-reverse/scripts/device_shell.py +++ b/skills/apk-reverse/scripts/device_shell.py @@ -36,7 +36,6 @@ Usage as a tool: Exit codes: 0 success / 1 the value was refused / 2 usage error / 4 internal error. """ -from __future__ import annotations import argparse import re diff --git a/skills/apk-reverse/scripts/dex_find_insn.py b/skills/apk-reverse/scripts/dex_find_insn.py index c9b9df5..7bee05d 100644 --- a/skills/apk-reverse/scripts/dex_find_insn.py +++ b/skills/apk-reverse/scripts/dex_find_insn.py @@ -52,9 +52,7 @@ import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from dexutil import ( # noqa: E402 - Dex, IF_TEST, IF_TESTZ, INVOKE_OPS, MOVE_RESULT_OPS, load_dex, -) +from dexutil import IF_TEST, IF_TESTZ, INVOKE_OPS, MOVE_RESULT_OPS, load_dex # noqa: E402 _KIND_MAP = { "if-test": IF_TEST, @@ -107,7 +105,7 @@ class Filter(object): def _operand_text(dex, insn): """All names this instruction mentions (class, method, field, string).""" parts = [] - op, pos = insn["op"], insn["off"] + op = insn["op"] if op in INVOKE_OPS: cls, nm, ds = dex.method(int.from_bytes(insn["raw"][2:4], "little")) parts += [cls, nm, ds] diff --git a/skills/apk-reverse/scripts/dex_patch_bytes.py b/skills/apk-reverse/scripts/dex_patch_bytes.py index bd82779..8d6da58 100644 --- a/skills/apk-reverse/scripts/dex_patch_bytes.py +++ b/skills/apk-reverse/scripts/dex_patch_bytes.py @@ -53,7 +53,6 @@ import json import os import struct import sys -import zipfile sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) diff --git a/skills/apk-reverse/scripts/dexutil.py b/skills/apk-reverse/scripts/dexutil.py index 940e4ec..2a8a348 100644 --- a/skills/apk-reverse/scripts/dexutil.py +++ b/skills/apk-reverse/scripts/dexutil.py @@ -35,7 +35,6 @@ Pure standard library. Import it, or run it for a per-method dump: """ import hashlib -import struct import sys import zipfile diff --git a/skills/apk-reverse/scripts/doctor.py b/skills/apk-reverse/scripts/doctor.py index 20d1ffa..90512c6 100644 --- a/skills/apk-reverse/scripts/doctor.py +++ b/skills/apk-reverse/scripts/doctor.py @@ -59,7 +59,6 @@ Usage Exit codes: 0 = nothing blocked, 3 = at least one capability blocked, 2 = usage error, 4 = internal error. The last line is `RESULT=`. """ -from __future__ import annotations import argparse import ast @@ -299,7 +298,7 @@ def static_js_deps(path): except OSError as exc: return [], 'OSError: %s' % exc # strip line comments cheaply; a require() inside a string is not worth the parser - body = '\n'.join(l.split('//')[0] for l in src.splitlines()) + body = '\n'.join(row.split('//')[0] for row in src.splitlines()) return sorted(set(re.findall(r"""require\(\s*['"]([^'"]+)['"]\s*\)""", body))), None @@ -429,15 +428,15 @@ def device_report(serial=None): # leftover forwards and proxies rc, fwd = run(['adb', '-s', tgt, 'forward', '--list'], timeout=15) - out['forwards'] = [l for l in fwd.splitlines() if l.strip()][:10] + out['forwards'] = [row for row in fwd.splitlines() if row.strip()][:10] rc, prox = run(['adb', '-s', tgt, 'shell', 'settings get global http_proxy'], timeout=15) out['http_proxy'] = prox.strip()[:120] # is a frida server already running on device? (a common source of "the app # suddenly detects instrumentation" while you believe nothing is attached) rc, ps = run(['adb', '-s', tgt, 'shell', 'ps -A'], timeout=20) - hits = [l for l in ps.splitlines() - if 'frida' in l.lower() and 'grep' not in l.lower()] + hits = [row for row in ps.splitlines() + if 'frida' in row.lower() and 'grep' not in row.lower()] out['device_frida_processes'] = [h.strip()[:140] for h in hits[:6]] return out diff --git a/skills/apk-reverse/scripts/elf_plt.py b/skills/apk-reverse/scripts/elf_plt.py index a65f63f..ce9cdec 100644 --- a/skills/apk-reverse/scripts/elf_plt.py +++ b/skills/apk-reverse/scripts/elf_plt.py @@ -37,7 +37,6 @@ Usage import argparse import hashlib -import os import struct import sys diff --git a/skills/apk-reverse/scripts/install_test.py b/skills/apk-reverse/scripts/install_test.py index 0809467..6d4867a 100644 --- a/skills/apk-reverse/scripts/install_test.py +++ b/skills/apk-reverse/scripts/install_test.py @@ -29,7 +29,6 @@ Useful flags Exit code is 0 only when the app launched AND both pid samples agree. """ import argparse -import os import re import subprocess import sys @@ -73,22 +72,28 @@ def main(): a = ap.parse_args() s = a.serial - print('[0] devices:'); print(adb(['devices', '-l'])) + print('[0] devices:') + print(adb(['devices', '-l'])) if a.uninstall_first: - print('[1] uninstall'); print(su('am force-stop %s; pm uninstall %s' % (a.pkg, a.pkg), s)) + print('[1] uninstall') + print(su('am force-stop %s; pm uninstall %s' % (a.pkg, a.pkg), s)) stage = '/data/local/tmp/_skill_stage.apk' - print('[2] push'); print(adb(['push', a.apk, stage], s)) - print('[3] install'); print(su('pm install -r -t -d %s' % stage, s)) + print('[2] push') + print(adb(['push', a.apk, stage], s)) + print('[3] install') + print(su('pm install -r -t -d %s' % stage, s)) for perm in a.grant: p = perm if perm.startswith('android.permission.') else 'android.permission.' + perm su('pm grant %s %s' % (a.pkg, p), s) - print('[4] clear logcat'); adb(['logcat', '-c'], s) + print('[4] clear logcat') + adb(['logcat', '-c'], s) comp = '%s/%s' % (a.pkg, a.activity if a.activity.startswith('.') or '.' in a.activity else a.activity) - print('[5] launch'); print(sh('am start -n %s' % comp, s)) + print('[5] launch') + print(sh('am start -n %s' % comp, s)) time.sleep(6) p1 = sh('pidof %s' % a.pkg, s).strip() diff --git a/skills/apk-reverse/scripts/java2c_probe.py b/skills/apk-reverse/scripts/java2c_probe.py index 1f1a9c9..dea12bc 100644 --- a/skills/apk-reverse/scripts/java2c_probe.py +++ b/skills/apk-reverse/scripts/java2c_probe.py @@ -135,13 +135,16 @@ class Dex(object): return try: file_size = self._u32(0x20) - map_off = self._u32(0x34) + # Read for completeness against the dex header layout: this function walks the + # header field by field and a reader comparing it to the format spec should not + # have to wonder whether a field was skipped. Not every field feeds the verdict. + map_off = self._u32(0x34) # noqa: F841 string_ids_size = self._u32(0x38) string_ids_off = self._u32(0x3C) type_ids_size = self._u32(0x40) type_ids_off = self._u32(0x44) - proto_ids_size = self._u32(0x48) - proto_ids_off = self._u32(0x4C) + proto_ids_size = self._u32(0x48) # noqa: F841 (see map_off above) + proto_ids_off = self._u32(0x4C) # noqa: F841 (see map_off above) method_ids_size = self._u32(0x58) method_ids_off = self._u32(0x5C) class_defs_size = self._u32(0x60) diff --git a/skills/apk-reverse/scripts/kernelsu_syscall_mask.py b/skills/apk-reverse/scripts/kernelsu_syscall_mask.py index a8514bc..3bc376f 100644 --- a/skills/apk-reverse/scripts/kernelsu_syscall_mask.py +++ b/skills/apk-reverse/scripts/kernelsu_syscall_mask.py @@ -45,7 +45,6 @@ import argparse import json import os import re -import subprocess import sys # --------------------------------------------------------------------------- diff --git a/skills/apk-reverse/scripts/lib_map.py b/skills/apk-reverse/scripts/lib_map.py index a1ac64e..0e5e67c 100644 --- a/skills/apk-reverse/scripts/lib_map.py +++ b/skills/apk-reverse/scripts/lib_map.py @@ -33,7 +33,6 @@ Reading it: * If the library you patched does not appear here at all, your patch cannot matter. That is a plan problem, not a patch problem. """ -from __future__ import annotations import argparse import json @@ -41,7 +40,6 @@ import os import re import subprocess import sys -from collections import defaultdict E_MACHINE = { 0x03: "x86", 0x3E: "x86_64", 0x28: "arm", 0xB7: "aarch64", diff --git a/skills/apk-reverse/scripts/mt_mcp_probe.py b/skills/apk-reverse/scripts/mt_mcp_probe.py index 2294b7c..f35ccb0 100644 --- a/skills/apk-reverse/scripts/mt_mcp_probe.py +++ b/skills/apk-reverse/scripts/mt_mcp_probe.py @@ -92,7 +92,6 @@ def parse_body(raw, ctype): def print_waiting_help(url): - host = "127.0.0.1" print("[waiting] MCP server is not answering at %s" % url) print() print("The MT APK MCP must be started by hand in the MT UI (adb cannot start it):") diff --git a/skills/apk-reverse/scripts/native_crash.py b/skills/apk-reverse/scripts/native_crash.py index 820bd40..c1f43ac 100644 --- a/skills/apk-reverse/scripts/native_crash.py +++ b/skills/apk-reverse/scripts/native_crash.py @@ -69,44 +69,44 @@ def parse(text): "frames": [], "regs": {}, "raw": []} # walk up a little to pick up pid/cmdline/uptime printed just before for j in range(max(0, i - 25), i): - l = lines[j] - mm = PID_RE.search(l) + ln = lines[j] + mm = PID_RE.search(ln) if mm and "pid" not in block: block["pid"], block["tid"], block["tname"], block["proc"] = mm.groups() - mm = PROC_RE.search(l) + mm = PROC_RE.search(ln) if mm and "proc" not in block: block["proc"] = mm.group(1) - mm = UPTIME_RE.search(l) + mm = UPTIME_RE.search(ln) if mm: block["uptime"] = mm.group(1) - if "Cause:" in l and "cause" not in block: - block["cause"] = l.split("Cause:", 1)[1].strip() + if "Cause:" in ln and "cause" not in block: + block["cause"] = ln.split("Cause:", 1)[1].strip() # walk down through the dump j = i while j < len(lines) and j < i + 120: - l = lines[j] - if j > i + 3 and SIGNAL_RE.search(l) and j > i + 3: + ln = lines[j] + if j > i + 3 and SIGNAL_RE.search(ln) and j > i + 3: break - block["raw"].append(l.strip()) - mm = PID_RE.search(l) + block["raw"].append(ln.strip()) + mm = PID_RE.search(ln) if mm and "pid" not in block: block["pid"], block["tid"], block["tname"], block["proc"] = mm.groups() - mm = PROC_RE.search(l) + mm = PROC_RE.search(ln) if mm and "proc" not in block: block["proc"] = mm.group(1) - mm = UPTIME_RE.search(l) + mm = UPTIME_RE.search(ln) if mm: block["uptime"] = mm.group(1) - if "Cause:" in l and "cause" not in block: - block["cause"] = l.split("Cause:", 1)[1].strip() - mm = FRAME_RE.search(l) + if "Cause:" in ln and "cause" not in block: + block["cause"] = ln.split("Cause:", 1)[1].strip() + mm = FRAME_RE.search(ln) if mm: # Backtrace lines look like either of: # #00 pc 00000000000128cc /data/app/.../lib/arm64/libfoo.so # #02 pc 00000000000cccd0 /apex/.../libc.so (__pthread_start+256) # The path is group(3); anything after it is a symbol in parens. path = mm.group(3) - rest = l.split(path, 1)[1].strip() if path in l else "" + rest = ln.split(path, 1)[1].strip() if path in ln else "" sym = "" m2 = re.search(r"\(([^)]+)\)", rest) if m2: @@ -118,9 +118,9 @@ def parse(text): # Register dumps put several registers on one line, so collect all of # them rather than only the leading one. for k, v in re.findall(r"\b([xX]\d{1,2}|sp|lr|pc|pst)\s+([0-9a-fA-F]{8,16})\b", - l.replace("F DEBUG :", " ")): + ln.replace("F DEBUG :", " ")): block["regs"][k] = int(v, 16) - if "backtrace:" in l: + if "backtrace:" in ln: pass j += 1 i = j if j > i else i + 1 diff --git a/skills/apk-reverse/scripts/patch_smali.py b/skills/apk-reverse/scripts/patch_smali.py index b086395..efb1ed2 100644 --- a/skills/apk-reverse/scripts/patch_smali.py +++ b/skills/apk-reverse/scripts/patch_smali.py @@ -28,7 +28,6 @@ Usage: """ import json import os -import re import sys diff --git a/skills/apk-reverse/scripts/preflight.py b/skills/apk-reverse/scripts/preflight.py index aff032f..a02c5e9 100644 --- a/skills/apk-reverse/scripts/preflight.py +++ b/skills/apk-reverse/scripts/preflight.py @@ -18,7 +18,6 @@ Usage: Exit code is 0 when no BLOCKER was found, 1 otherwise. WARN never fails the run. """ -from __future__ import annotations import argparse import json @@ -76,7 +75,8 @@ def main() -> int: ("python", "all scripts"), ): found = shutil.which(tool) - r.add(OK if found else WARN, "host:" + tool, found or "not on PATH", "" if found else "install it if you need " + why) + r.add(OK if found else WARN, "host:" + tool, found or "not on PATH", + "" if found else "install it if you need " + why) for tool in ("apksigner", "zipalign", "aapt", "aapt2"): found = shutil.which(tool) r.add(OK if found else INFO, "host:" + tool, found or "not on PATH", @@ -207,7 +207,8 @@ def main() -> int: "this is the ABI the package manager chose for THIS device") maps = su("cat /proc/$(pidof %s | awk '{print $1}')/maps 2>/dev/null | head -1" % a.pkg) if maps.strip(): - r.add(INFO, "target:running", "process is running", "prefer a clean cold start before measuring anything") + r.add(INFO, "target:running", "process is running", + "prefer a clean cold start before measuring anything") else: r.add(OK, "target:running", "not running", "") @@ -230,7 +231,7 @@ def main() -> int: def finish(r: Report, as_json: bool) -> int: if as_json: print(json.dumps({"worst": r.worst(), - "rows": [dict(level=l, area=ar, detail=d, fix=f) for l, ar, d, f in r.rows]}, + "rows": [dict(level=lv, area=ar, detail=d, fix=f) for lv, ar, d, f in r.rows]}, indent=2, ensure_ascii=False)) return 1 if r.worst() == BLOCKER else 0 diff --git a/skills/apk-reverse/scripts/protobuf_decode_raw.py b/skills/apk-reverse/scripts/protobuf_decode_raw.py index 14a2cc5..4ca6fbe 100644 --- a/skills/apk-reverse/scripts/protobuf_decode_raw.py +++ b/skills/apk-reverse/scripts/protobuf_decode_raw.py @@ -470,13 +470,12 @@ def analyse_payload(chunk, depth, ctx, path, offset): return cands, 'empty' # --- nested message ------------------------------------------------- - nested_ok = False + # (the value this line used to bind is not read; the expression itself was already dropped) if depth < ctx.max_depth: sub = Ctx(ctx.max_depth, ctx.expand, silent=True) sub_fields, nxt, status = walk(chunk, 0, n, depth + 1, sub, path, analyse=False) if status == 'end_of_buffer' and nxt == n and not sub.errors and sub_fields: - nested_ok = True cands.append({ 'kind': 'nested_message', 'confidence': 0.85, @@ -771,7 +770,6 @@ def load_input(args): if args.input is None or args.input == '-': data = sys.stdin.buffer.read() - source = 'stdin' try: text = data.decode('utf-8') except UnicodeDecodeError: @@ -1059,8 +1057,8 @@ def _fixtures(): group = tag(8, 3) + tag(1, 0) + encode_varint(5) + tag(8, 4) non_canonical = tag(1, 0) + b'\x96\x81\x00' overflow = tag(1, 0) + bytes([0xFF] * 9) + bytes([0x7F]) - two_msgs = tag(1, 0) + encode_varint(1) + ld(2, b'ab') + \ - tag(1, 0) + encode_varint(2) + ld(2, b'cd') + two_msgs = (tag(1, 0) + encode_varint(1) + ld(2, b'ab') + + tag(1, 0) + encode_varint(2) + ld(2, b'cd')) fixed_len = encode_varint(len(main)) + main # (name, payload, expectations, round-trip expectation) # round-trip is 'match' for every fixture that re-encodes canonically, and @@ -1126,7 +1124,7 @@ def selftest(args): checks += 1 frames = _fixture_frames(name, payload) tree = {'frames': [{'fields': walk(payload, s, e, 0, Ctx(6), 'f', - analyse=True)[0]} + analyse=True)[0]} for s, e, _p in frames]} report = {'fields': 0, 'identical': 0, 'mismatched': []} out = reencode_tree(tree, report) diff --git a/skills/apk-reverse/scripts/rasc_build.py b/skills/apk-reverse/scripts/rasc_build.py index 415b431..494ecbf 100644 --- a/skills/apk-reverse/scripts/rasc_build.py +++ b/skills/apk-reverse/scripts/rasc_build.py @@ -23,7 +23,6 @@ Requires, to build: git, and a Rust toolchain (rustup). On Windows the GNU host 64-bit MinGW-w64 gcc on PATH -- the 32-bit MinGW that ships with some setups cannot link a 64-bit binary and fails with "64-bit mode not compiled in". """ -from __future__ import annotations import argparse import json @@ -233,13 +232,17 @@ def main(argv=None): print('cannot build: missing %s' % ', '.join(missing), file=sys.stderr) print('RESULT=capability_missing') return EXIT_CAPABILITY - target = payload['toolchain'].get('gcc_target') or '' + gcc_target = payload['toolchain'].get('gcc_target') or '' built = build(args.work, args.jobs) if not built: payload['result'] = 'build_failed' - payload['next_action'] = ('read the cargo output above; on Windows the GNU host ' - 'toolchain needs a 64-bit MinGW-w64 gcc, and a 32-bit MinGW ' - 'fails with "64-bit mode not compiled into"') + # The hint is only useful when the linker target is the 32-bit one, which is the + # failure this actually hit: name the target rather than printing a generic list. + payload['next_action'] = ( + 'read the cargo output above; the GNU host toolchain needs a 64-bit MinGW-w64 ' + 'gcc and this machine reports %r' % (gcc_target or 'no gcc on PATH') if + 'mingw32' in gcc_target or not gcc_target else + 'read the cargo output above') if args.json: print(json.dumps(payload, indent=2)) print('RESULT=build_failed') diff --git a/skills/apk-reverse/scripts/repack.py b/skills/apk-reverse/scripts/repack.py index dd3b507..4edcba3 100644 --- a/skills/apk-reverse/scripts/repack.py +++ b/skills/apk-reverse/scripts/repack.py @@ -216,7 +216,7 @@ def axml_string_pool(blob): a hand-rolled element walk is where such parsers usually go wrong. The pool is also what makes equal-length hex patching of a manifest possible later. """ - import struct + import struct # noqa: F401 -- used inside the pool walker below if len(blob) < 36 or struct.unpack_from(' nothing is changing. Stop waiting and go find out why (this is the stall detector). """ -from __future__ import annotations import argparse import hashlib diff --git a/skills/apk-reverse/scripts/so_constpatch.py b/skills/apk-reverse/scripts/so_constpatch.py index 39d411f..772b757 100644 --- a/skills/apk-reverse/scripts/so_constpatch.py +++ b/skills/apk-reverse/scripts/so_constpatch.py @@ -100,7 +100,6 @@ Exit codes (see references/long-task-discipline.md and the kit-wide convention): ``evidence``, ``warnings``, ``next_action``) followed by the same final ``RESULT=`` line every other script in this kit ends with. """ -from __future__ import annotations import argparse import hashlib diff --git a/skills/apk-reverse/scripts/vmp_diff_harness.py b/skills/apk-reverse/scripts/vmp_diff_harness.py index a2bb278..0d5f050 100644 --- a/skills/apk-reverse/scripts/vmp_diff_harness.py +++ b/skills/apk-reverse/scripts/vmp_diff_harness.py @@ -49,7 +49,10 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) try: import dexutil except ImportError: # pragma: no cover - dexutil = None + # The optional-dependency idiom this repository uses: the name is rebound to None so the caller + # can test for it. mypy objects because `dexutil` is now "may be None", which is exactly the + # truth the `if dexutil is None` guards below rely on -- say so rather than narrowing the type. + dexutil = None # type: ignore[assignment] # --------------------------------------------------------------------------- @@ -1327,8 +1330,8 @@ def cmd_compare(args): "orig": op, "orig_hex": "0x%02x" % op, "name": dexutil.OP_NAMES[op], "reason": UNREACHABLE.get(op) or - "the original never emits this opcode, so there is no " - "known-plaintext instance to read a substitution from", + ("the original never emits this opcode, so there is no " + "known-plaintext instance to read a substitution from"), }) # reverse direction: a private byte claimed by two different originals