Skip to content

feat: batch decompile many classes in one APK pass - #57

Open
void0x14 wants to merge 1 commit into
MG1937:dev-0.1.0from
void0x14:batch-decompile
Open

void0x14 wants to merge 1 commit into
MG1937:dev-0.1.0from
void0x14:batch-decompile

Conversation

@void0x14

Copy link
Copy Markdown

What this adds

getclass is fine for one class. For a whole package you end up looping it
from the shell, and each run opens the APK again and re-inflates the DEX
entries until it finds the class. On a 64-DEX APK that repeated inflate is most
of the work. Decompiling the class is a small part of it.

batch inflates every DEX once, indexes the classes they define, then
decompiles from those buffers. One process, no worker pool, no IPC, no cache on
disk.

Benchmark

Real APK: 358 MB, 64 DEX, 34,655 classes under com.kubi.

Command Time
getclass in a loop over 2 hours, did not finish
droidasc batch --prefix com.kubi -o out/ 8m 1s

Same 100 classes, each approach timed end to end including interpreter startup:

Command Time
100 × getclass 23.86 s
batch --from-file classes.txt 1.67 s

The batch output was byte-identical to getclass for all 100 classes, inner
classes included. A test checks this by diffing the two outputs.

Throughput is about 72 classes/s wall clock. The decompiler is GIL-bound, so
threads would not help. The win comes from reading the APK once.

Usage

droidasc batch app.apk --prefix com.poc -o out/
droidasc batch app.apk --from-file classes.txt -o out/
droidasc batch app.apk --from-file classes.txt -o out/ --resume

--prefix takes com.poc, com/poc or Lcom/poc, same as listclass.
--from-file takes one class per line, dotted or Dalvik, and ignores lines
starting with #. It reads what listclass -o writes. -o is required, and
files mirror the package tree: Lcom/poc/Main$a; becomes
out/com/poc/Main$a.java. --resume skips classes whose file already exists.
--limit caps the run.

A class listed but missing from the APK prints a warning and is skipped. The
run fails only when nothing matches, or when every selected class fails to
decompile. One bad class doesn't stop the rest.

Tests

tests/test_batch.py has 10 cases: name parsing, prefix selection, output
parity with getclass, empty prefix, missing class, empty list, resume,
summary, limit. tests/run_tests.py picks it up, and the unit suite stays green
(75 tests).

Files

  • droidasc/asc_client/batch.py (new): inflate-once index and decompile helpers
  • droidasc/cli.py: batch subcommand
  • tests/test_batch.py (new)
  • README.md: command list and batch section

01101sam pushed a commit to 01101sam/asc that referenced this pull request Nov 12, 2026
Adds `droidasc batch`, which inflates each DEX once and decompiles every
selected class (--prefix or --from-file) into a package tree, with
--resume and --limit.

Fixes on top of upstream MG1937#57:
- sanitize class names before using them as paths; a hostile descriptor
  such as L../../x; could previously write outside --out-dir
- give classes that differ only by case distinct output files (~N suffix);
  on macOS/Windows they overwrote each other and --resume then skipped them
- decompile a class listed twice in --from-file only once
- reject a negative --limit instead of silently slicing from the end

(cherry picked from commit b4effb9)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ra8caPaYoi8jheXnkE5dPe
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant