diff --git a/docs/tool-verification/EXTENSION-rasc.md b/docs/tool-verification/EXTENSION-rasc.md index 0112503..0aa4bd5 100644 --- a/docs/tool-verification/EXTENSION-rasc.md +++ b/docs/tool-verification/EXTENSION-rasc.md @@ -11,7 +11,7 @@ that produced it. | It is faster than the Python `droidasc` on the same archives | **observed** | 4.0–5.6× for `classes`, up to 16.2× for `findrefs` (tables below) | | It returns the same answers | **observed** | **identical class-definition sets**, 30,768 / 30,768 with **0 differences in either direction**; `findrefs string` identical on one sample, `type` a superset (390 vs 384) | | Its `getclass` decompiles ordinary classes correctly | **observed** | 16 of 17 JADX-judged classes matched literal-for-literal, 0 reorderings | -| **It silently drops the bodies of enum constants that override a method** | **observed** | `org.bouncycastle.crypto.PasswordConverter`: JADX emits three anonymous subclasses with `convert`/`getType`; `rasc` emits a bare constant list, **no warning** | +| **Its outer-class view of an enum does not inline the constant bodies, and says nothing** | **observed — and the first write-up of this row over-claimed; see §The blind spot** | JADX inlines the three anonymous subclasses; `rasc` prints a bare constant list. Both tools decompile `PasswordConverter$1/2/3` **fully** (404–407 B, containing `convert`/`getType`), so what is missing is an inlining step, not the code | | Its own benchmark's 8.0× geometric mean reproduces here | **not reproduced, and explained** | that figure is from a 343 MiB / 567k-class archive; on a 34.8 MB / 30,768-class app the ratio is 4×. Both are real; the small-archive figure is what a normal task hits | ## Environment @@ -102,12 +102,28 @@ missing : ['ASCII', 'PKCS12', 'UTF8'] VERDICT: form difference (enum constants printed as a list, literals not re-emitted) ``` -**The automated verdict understates it, and that is the finding.** The harness counts literals; the -human check shows the literals are gone *because the whole anonymous subclass body is gone* — JADX -prints `ASCII { ... convert() ... getType() ... }` and `rasc` prints `ASCII,`. `rasc` emits no -comment, no TODO, no warning. Output that is clean, plausible and incomplete is the failure shape this -repository's `pitfalls.md` exists to catalogue, so the consequence is a rule, not a caveat: confirm an -enum with no bodies using a second reader. +**The automated verdict understates it, and the hand check then overstated it. Both corrections are +kept.** The harness counts literals, and the literals in JADX's output belong to the anonymous +subclasses it inlines; `rasc`'s outer-class view prints a bare constant list. The first write-up here +said "the method bodies are gone". **That was wrong, and the way it was wrong is worth recording: the +subclasses were never queried.** They exist as their own classes and both tools decompile them fully: + +``` +$ rasc getclass 'Lorg/bouncycastle/crypto/PasswordConverter$1;' -> 404 B, has convert(/getType( +$ droidasc getclass 'Lorg/bouncycastle/crypto/PasswordConverter$1;' -> 460 B, has both +$ ... $2 and $3 the same shape, 406-407 B vs 462-463 B +``` + +So the accurate finding is narrower than the first version and narrower than the harness's phrasing: +**`rasc` does not inline enum-constant bodies into the outer class view, and it does not say so** — +the bodies are one `getclass` away, on the subclass JADX itself names in a comment +(`// from class: ...PasswordConverter.1`). Neither does the Python tool inline; `droidasc`'s outer +listing is just richer (2,170 B vs 314 B, with `$VALUES`, `$values()` and the constructors explicit). + +The general lesson this left behind, and the reason the episode stays in the record: a harness that +counts one artefact (literals) can point at a real difference while misdescribing it, and a hand +check that stops at the first comparison can convert that into an overstated claim. The corrective was +to query the classes the tool we were comparing against had named — which cost one command. ## Not established diff --git a/skills/apk-reverse/references/rasc-and-droidsaw.md b/skills/apk-reverse/references/rasc-and-droidsaw.md index 74b3a63..24fda23 100644 --- a/skills/apk-reverse/references/rasc-and-droidsaw.md +++ b/skills/apk-reverse/references/rasc-and-droidsaw.md @@ -54,15 +54,16 @@ the ones a normal task hits. `python skills/apk-reverse/scripts/rasc_build.py --verify ` reproduces the class-set comparison and is the check to run after any rebuild. -## The blind spot: abstract enum constants lose their bodies +## The blind spot: `getclass` on the outer enum does not inline the constant bodies `bench/jadx_parity.py` from the `rasc` repository compares **string literals** between JADX and `rasc` per class, counting a literal JADX has and `rasc` does not as a lost-code signal. Run here on a fair sample — app-like classes, no obfuscated identifiers, since the harness itself refuses to judge a class JADX cannot decompile — **20 classes picked, 17 judged, 1 with a difference, 0 reorderings**. -The one difference is not a formatting artefact. `org.bouncycastle.crypto.PasswordConverter` is an -enum whose constants each override an abstract method: +The one difference is `org.bouncycastle.crypto.PasswordConverter`, an enum whose constants each +override an abstract method. JADX inlines each constant's anonymous subclass; `rasc` prints the +constant list: ```java // JADX @@ -75,31 +76,45 @@ public enum PasswordConverter implements CharToByteConverter { ``` ```java -// rasc +// rasc -- getclass on the OUTER class public enum PasswordConverter implements org.bouncycastle.crypto.CharToByteConverter { - ASCII, - UTF8, - PKCS12; + ASCII, UTF8, PKCS12; public PasswordConverter() { } public PasswordConverter(PasswordConverter$1 v3) { } } ``` -**Both the per-constant method bodies and the string literals are gone, and `rasc` emits no warning -that anything was dropped.** The output is clean, plausible, and incomplete — the same failure shape -`pitfalls.md` is a catalogue of. So the rule for this layer is a rule, not a preference: +**What is actually lost, measured directly on the constants.** The bodies live in their own classes, +so the question is whether they survive as their own classes. They do: + +| class | `rasc getclass` | `droidasc getclass` | +|---|---|---| +| `PasswordConverter$1` | 404 B, contains `convert(`/`getType(` | 460 B, contains both | +| `PasswordConverter$2` | 407 B, contains both | 463 B, contains both | +| `PasswordConverter$3` | 406 B, contains both | 462 B, contains both | + +So the correct severity is **a missing inlining step in the outer-class view, not unrecoverable code**: +the source is one `getclass` away, on the anonymous subclass JADX names in its own comment +(`// from class: ...PasswordConverter.1`). This reference said "the method bodies are gone" when this +row was first written, and that was wrong — the subclasses were never queried. The two Python-visible +symptoms are the same in both tools (neither inlines), so the difference here is JADX's convenience, +not a `rasc` regression against the Python implementation. + +`droidasc`'s outer output is nevertheless richer than `rasc`'s for the same class — 2,170 B against +314 B, carrying the synthetic `$VALUES`, `$values()` and the constructors explicitly — which matters +when the enum's *shape* is the thing you are reading. + +The rule, now proportional to what was measured: - **Use `rasc getclass` for locating and reading ordinary classes.** That is what it was measured on. -- **When the class is an `enum` with constant-specific bodies, confirm with a second reader** - (`droidasc getclass`, or JADX). A collapsed enum is visible on sight: the constants are printed as a - bare list and no method bodies follow. Treat "enum with no bodies" as *unverified output*, not as - "this enum has no logic". -- **Never treat a single decompiler's output as the definition of what exists.** This is the same rule - the VMP section states about opcode tables, applied to a class body. +- **When the class is an `enum` with constant-specific bodies, read the constants.** A collapsed enum + is visible on sight (bare constant list, no bodies). Either query the anonymous subclasses directly + (`Lpkg/Enum$1;`) or use `droidasc`, whose outer-class listing is more complete. +- **Never treat a single decompiler's output as the definition of what exists.** The same rule the VMP + section states about opcode tables, applied to a class body. -Frequency matters for calibration: 1 of 17 judged classes here, and the other 16 agreed literal for -literal. So the correct posture is **adopt, with the cross-check kept**, not "reject" and not -"drop the Python tool from the kit". +Frequency for calibration: 1 of 17 judged classes here, and the other 16 agreed literal for literal. +Adopt it, and keep the cross-check — but do not describe this as lost code. ## What it does not change diff --git a/skills/apk-reverse/references/toolchain.md b/skills/apk-reverse/references/toolchain.md index 1c4d086..73ee17c 100644 --- a/skills/apk-reverse/references/toolchain.md +++ b/skills/apk-reverse/references/toolchain.md @@ -56,12 +56,15 @@ python scripts/rasc_build.py --build # needs git + rustup; ~2 min python scripts/rasc_build.py --verify app.apk # compares class sets against droidasc, fails on a difference ``` -**The one thing to know before trusting its `getclass`:** an `enum` whose constants override an -abstract method is printed as a bare constant list, and **the per-constant bodies are dropped with no -warning**. Sampling 20 app-like classes from a real app, 17 were judged by the JADX-parity harness and -16 agreed literal for literal; the one that differed was exactly this shape. So: use it to *locate* -and to read ordinary classes, and confirm any enum that shows no bodies with a second reader. The -measurements, the failing class and the build traps are in `references/rasc-and-droidsaw.md`. +**The one thing to know before trusting its `getclass`:** on an `enum` whose constants override an +abstract method, the outer class is printed as a bare constant list and the per-constant bodies are +**not inlined** — no warning, and none in the Python tool either, because both leave those bodies in +their own subclasses. They are one query away (`Lpkg/Enum$1;`, and both tools decompile those fully); +`droidasc`'s outer-class listing is simply richer (2,170 B vs 314 B on the measured class, carrying +`$VALUES`, `$values()` and the constructors). Sampling 20 app-like classes from a real app, 17 were +judged by the JADX-parity harness and 16 agreed literal for literal; the one that differed was exactly +this shape. Use it to *locate* and to read ordinary classes, and read the constants when an enum shows +no bodies. The measurements and the failing class are in `references/rasc-and-droidsaw.md`. ### droidasc (ASC) — ask an APK "who references this?", in one query