diff --git a/jadx-with-jadxecute/.github/ISSUE_TEMPLATE/decompilation-error.md b/jadx-with-jadxecute/.github/ISSUE_TEMPLATE/decompilation-error.md new file mode 100644 index 0000000..ad5b3a3 --- /dev/null +++ b/jadx-with-jadxecute/.github/ISSUE_TEMPLATE/decompilation-error.md @@ -0,0 +1,19 @@ +--- +name: Decompilation error +about: Create a report to help us improve jadx decompiler +title: "[core]" +labels: Core, bug +assignees: '' + +--- + +**Checks before report** +- check [Troubleshooting Q&A](https://github.com/skylot/jadx/wiki/Troubleshooting-Q&A) section on wiki +- search existing issues by exception message + +**Describe error** +- full name of method or class with error +- full java stacktrace (no need to copy method fallback code (commented pseudocode)) +- **IMPORTANT!** attach or provide link to apk file (double check apk version) + + **Note**: GitHub don't allow attach files with `.apk` extension, but you can change extension by adding `.zip` at the end :) diff --git a/jadx-with-jadxecute/.github/ISSUE_TEMPLATE/feature-request.md b/jadx-with-jadxecute/.github/ISSUE_TEMPLATE/feature-request.md new file mode 100644 index 0000000..7c8d3f0 --- /dev/null +++ b/jadx-with-jadxecute/.github/ISSUE_TEMPLATE/feature-request.md @@ -0,0 +1,10 @@ +--- +name: Feature Request +about: Suggest an idea for jadx +title: "[feature]" +labels: new feature +assignees: '' + +--- + +*Describe your idea:* diff --git a/jadx-with-jadxecute/.github/dependabot.yml b/jadx-with-jadxecute/.github/dependabot.yml new file mode 100644 index 0000000..e261d24 --- /dev/null +++ b/jadx-with-jadxecute/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: + # Set update schedule for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/jadx-with-jadxecute/.github/pull_request_template.md b/jadx-with-jadxecute/.github/pull_request_template.md new file mode 100644 index 0000000..12ce91e --- /dev/null +++ b/jadx-with-jadxecute/.github/pull_request_template.md @@ -0,0 +1,5 @@ +:exclamation: Please review the [guidelines for contributing](https://github.com/skylot/jadx/blob/master/CONTRIBUTING.md#Pull-Request-Process) + +### Description +Please describe your pull request. +Reference issue it fixes. diff --git a/jadx-with-jadxecute/.github/workflows/build-artifacts.yml b/jadx-with-jadxecute/.github/workflows/build-artifacts.yml new file mode 100644 index 0000000..c1323ab --- /dev/null +++ b/jadx-with-jadxecute/.github/workflows/build-artifacts.yml @@ -0,0 +1,88 @@ +name: Build Artifacts + +on: + push: + branches: [ master, build-test ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Set up JDK + uses: actions/setup-java@v3 + with: + distribution: 'adopt' + java-version: 8 + + - name: Set jadx version + run: | + JADX_LAST_TAG=$(git describe --abbrev=0 --tags) + JADX_VERSION="${JADX_LAST_TAG:1}.$GITHUB_RUN_NUMBER-${GITHUB_SHA:0:8}" + echo "JADX_VERSION=$JADX_VERSION" >> $GITHUB_ENV + + - uses: burrunan/gradle-cache-action@v1 + name: Build with Gradle + env: + TERM: dumb + with: + arguments: clean dist copyExe + + - name: Save bundle artifact + uses: actions/upload-artifact@v3 + with: + name: ${{ format('jadx-{0}', env.JADX_VERSION) }} + # Waiting fix for https://github.com/actions/upload-artifact/issues/39 to upload zip file + # Upload unpacked files for now + path: build/jadx/**/* + if-no-files-found: error + retention-days: 30 + + - name: Save exe artifact + uses: actions/upload-artifact@v3 + with: + name: ${{ format('jadx-gui-{0}-no-jre-win.exe', env.JADX_VERSION) }} + path: build/*.exe + if-no-files-found: error + retention-days: 30 + + build-win-bundle: + runs-on: windows-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Set up JDK + uses: oracle-actions/setup-java@v1 + with: + release: 17 + + - name: Print Java version + shell: bash + run: java -version + + - name: Set jadx version + shell: bash + run: | + JADX_LAST_TAG=$(git describe --abbrev=0 --tags) + JADX_VERSION="${JADX_LAST_TAG:1}.$GITHUB_RUN_NUMBER-${GITHUB_SHA:0:8}" + echo "JADX_VERSION=$JADX_VERSION" >> $GITHUB_ENV + + - uses: gradle/gradle-build-action@v2 + name: Build with Gradle + env: + TERM: dumb + with: + arguments: clean dist -PbundleJRE=true + + - name: Save exe bundle artifact + uses: actions/upload-artifact@v3 + with: + name: ${{ format('jadx-gui-{0}-with-jre-win', env.JADX_VERSION) }} + path: jadx-gui/build/*-with-jre-win/* + if-no-files-found: error + retention-days: 30 diff --git a/jadx-with-jadxecute/.github/workflows/build-test.yml b/jadx-with-jadxecute/.github/workflows/build-test.yml new file mode 100644 index 0000000..bb3ad56 --- /dev/null +++ b/jadx-with-jadxecute/.github/workflows/build-test.yml @@ -0,0 +1,28 @@ +name: Build Test + +on: + push: + branches: [ master, build-test ] + pull_request: + branches: [ master ] + +jobs: + tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Set up JDK + uses: actions/setup-java@v3 + with: + distribution: 'adopt' + java-version: 8 + + - uses: burrunan/gradle-cache-action@v1 + name: Build with Gradle + env: + TERM: dumb + with: + arguments: clean build dist copyExe --warning-mode=all diff --git a/jadx-with-jadxecute/.github/workflows/codeql-analysis.yml b/jadx-with-jadxecute/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..ed161fa --- /dev/null +++ b/jadx-with-jadxecute/.github/workflows/codeql-analysis.yml @@ -0,0 +1,41 @@ +name: "CodeQL" + +on: + push: + branches: [master] + pull_request: + # The branches below must be a subset of the branches above + branches: [master] + schedule: + - cron: '0 9 * * 5' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: ['java'] + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + queries: +security-extended + languages: ${{ matrix.language }} + + # Don't build tests in jadx-core also skip tests execution and checkstyle tasks + - run: | + ./gradlew clean build -x checkstyleTest -x checkstyleMain -x test -x ':jadx-core:testClasses' + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/jadx-with-jadxecute/.github/workflows/gradle-wrapper-validation.yml b/jadx-with-jadxecute/.github/workflows/gradle-wrapper-validation.yml new file mode 100644 index 0000000..c6df11b --- /dev/null +++ b/jadx-with-jadxecute/.github/workflows/gradle-wrapper-validation.yml @@ -0,0 +1,10 @@ +name: "Validate Gradle Wrapper" +on: [push] + +jobs: + validation: + name: "Validation" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: gradle/wrapper-validation-action@v1 diff --git a/jadx-with-jadxecute/README.md b/jadx-with-jadxecute/README.md new file mode 100644 index 0000000..a6878d7 --- /dev/null +++ b/jadx-with-jadxecute/README.md @@ -0,0 +1,169 @@ + + +## JADX + +[![Build status](https://github.com/skylot/jadx/workflows/Build/badge.svg)](https://github.com/skylot/jadx/actions?query=workflow%3ABuild) +![GitHub contributors](https://img.shields.io/github/contributors/skylot/jadx) +![GitHub all releases](https://img.shields.io/github/downloads/skylot/jadx/total) +![GitHub release (latest by SemVer)](https://img.shields.io/github/downloads/skylot/jadx/latest/total) +![Latest release](https://img.shields.io/github/release/skylot/jadx.svg) +[![Maven Central](https://img.shields.io/maven-central/v/io.github.skylot/jadx-core)](https://search.maven.org/search?q=g:io.github.skylot%20AND%20jadx) +[![License](http://img.shields.io/:license-apache-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0.html) + +**jadx** - Dex to Java decompiler + +Command line and GUI tools for producing Java source code from Android Dex and Apk files + +:exclamation::exclamation::exclamation: Please note that in most cases **jadx** can't decompile all 100% of the code, so errors will occur. Check [Troubleshooting guide](https://github.com/skylot/jadx/wiki/Troubleshooting-Q&A#decompilation-issues) for workarounds + +**Main features:** +- decompile Dalvik bytecode to java classes from APK, dex, aar, aab and zip files +- decode `AndroidManifest.xml` and other resources from `resources.arsc` +- deobfuscator included + +**jadx-gui features:** +- view decompiled code with highlighted syntax +- jump to declaration +- find usage +- full text search +- smali debugger, check [wiki page](https://github.com/skylot/jadx/wiki/Smali-debugger) for setup and usage + +Jadx-gui key bindings can be found [here](https://github.com/skylot/jadx/wiki/JADX-GUI-Key-bindings) + +See these features in action here: [jadx-gui features overview](https://github.com/skylot/jadx/wiki/jadx-gui-features-overview) + + + +### Download +- release + from [github: ![Latest release](https://img.shields.io/github/release/skylot/jadx.svg)](https://github.com/skylot/jadx/releases/latest) +- latest [unstable build ![GitHub commits since tagged version (branch)](https://img.shields.io/github/commits-since/skylot/jadx/latest/master)](https://nightly.link/skylot/jadx/workflows/build-artifacts/master) + +After download unpack zip file go to `bin` directory and run: +- `jadx` - command line version +- `jadx-gui` - UI version + +On Windows run `.bat` files with double-click\ +**Note:** ensure you have installed Java 11 or later 64-bit version. +For Windows, you can download it from [oracle.com](https://www.oracle.com/java/technologies/downloads/#jdk17-windows) (select x64 Installer). + +### Install +1. Arch linux ![Arch Linux package](https://img.shields.io/archlinux/v/community/any/jadx?label=) + ```bash + sudo pacman -S jadx + ``` +2. macOS ![homebrew version](https://img.shields.io/homebrew/v/jadx?label=) + ```bash + brew install jadx + ``` +3. [Flathub ![Flathub](https://img.shields.io/flathub/v/com.github.skylot.jadx?label=)](https://flathub.org/apps/details/com.github.skylot.jadx) + ```bash + flatpak install flathub com.github.skylot.jadx + ``` + +### Use jadx as a library +You can use jadx in your java projects, check details on [wiki page](https://github.com/skylot/jadx/wiki/Use-jadx-as-a-library) + +### Build from source +JDK 8 or higher must be installed: +``` +git clone https://github.com/skylot/jadx.git +cd jadx +./gradlew dist +``` + +(on Windows, use `gradlew.bat` instead of `./gradlew`) + +Scripts for run jadx will be placed in `build/jadx/bin` +and also packed to `build/jadx-.zip` + +### Usage +``` +jadx[-gui] [options] (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab) +options: + -d, --output-dir - output directory + -ds, --output-dir-src - output directory for sources + -dr, --output-dir-res - output directory for resources + -r, --no-res - do not decode resources + -s, --no-src - do not decompile source code + --single-class - decompile a single class, full name, raw or alias + --single-class-output - file or dir for write if decompile a single class + --output-format - can be 'java' or 'json', default: java + -e, --export-gradle - save as android gradle project + -j, --threads-count - processing threads count, default: 4 + -m, --decompilation-mode - code output mode: + 'auto' - trying best options (default) + 'restructure' - restore code structure (normal java code) + 'simple' - simplified instructions (linear, with goto's) + 'fallback' - raw instructions without modifications + --show-bad-code - show inconsistent code (incorrectly decompiled) + --no-imports - disable use of imports, always write entire package name + --no-debug-info - disable debug info + --add-debug-lines - add comments with debug line numbers if available + --no-inline-anonymous - disable anonymous classes inline + --no-inline-methods - disable methods inline + --no-finally - don't extract finally block + --no-replace-consts - don't replace constant value with matching constant field + --escape-unicode - escape non latin characters in strings (with \u) + --respect-bytecode-access-modifiers - don't change original access modifiers + --deobf - activate deobfuscation + --deobf-min - min length of name, renamed if shorter, default: 3 + --deobf-max - max length of name, renamed if longer, default: 64 + --deobf-cfg-file - deobfuscation map file, default: same dir and name as input file with '.jobf' extension + --deobf-cfg-file-mode - set mode for handle deobfuscation map file: + 'read' - read if found, don't save (default) + 'read-or-save' - read if found, save otherwise (don't overwrite) + 'overwrite' - don't read, always save + 'ignore' - don't read and don't save + --deobf-use-sourcename - use source file name as class name alias + --deobf-parse-kotlin-metadata - parse kotlin metadata to class and package names + --deobf-res-name-source - better name source for resources: + 'auto' - automatically select best name (default) + 'resources' - use resources names + 'code' - use R class fields names + --use-kotlin-methods-for-var-names - use kotlin intrinsic methods to rename variables, values: disable, apply, apply-and-hide, default: apply + --rename-flags - fix options (comma-separated list of): + 'case' - fix case sensitivity issues (according to --fs-case-sensitive option), + 'valid' - rename java identifiers to make them valid, + 'printable' - remove non-printable chars from identifiers, + or single 'none' - to disable all renames + or single 'all' - to enable all (default) + --fs-case-sensitive - treat filesystem as case sensitive, false by default + --cfg - save methods control flow graph to dot file + --raw-cfg - save methods control flow graph (use raw instructions) + -f, --fallback - set '--decompilation-mode' to 'fallback' (deprecated) + --use-dx - use dx/d8 to convert java bytecode + --comments-level - set code comments level, values: error, warn, info, debug, user-only, none, default: info + --log-level - set log level, values: quiet, progress, error, warn, info, debug, default: progress + -v, --verbose - verbose output (set --log-level to DEBUG) + -q, --quiet - turn off output (set --log-level to QUIET) + --version - print jadx version + -h, --help - print this help + +Plugin options (-P=): + 1) dex-input: Load .dex and .apk files + - dex-input.verify-checksum - verify dex file checksum before load, values: [yes, no], default: yes + 2) java-convert: Convert .class, .jar and .aar files to dex + - java-convert.mode - convert mode, values: [dx, d8, both], default: both + - java-convert.d8-desugar - use desugar in d8, values: [yes, no], default: no + +Examples: + jadx -d out classes.dex + jadx --rename-flags "none" classes.dex + jadx --rename-flags "valid, printable" classes.dex + jadx --log-level ERROR app.apk + jadx -Pdex-input.verify-checksum=no app.apk +``` +These options also worked on jadx-gui running from command line and override options from preferences dialog + +### Troubleshooting +Please check wiki page [Troubleshooting Q&A](https://github.com/skylot/jadx/wiki/Troubleshooting-Q&A) + +### Contributing +To support this project you can: + - Post thoughts about new features/optimizations that important to you + - Submit decompilation issues, please read before proceed: [Open issue](CONTRIBUTING.md#Open-Issue) + - Open pull request, please follow these rules: [Pull Request Process](CONTRIBUTING.md#Pull-Request-Process) + +--------------------------------------- +*Licensed under the Apache 2.0 License* diff --git a/jadx-with-jadxecute/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java b/jadx-with-jadxecute/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java index 67f1056..686482e 100644 --- a/jadx-with-jadxecute/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java +++ b/jadx-with-jadxecute/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java @@ -7,6 +7,8 @@ import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.function.Function; +import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -89,6 +91,9 @@ public class JadxCLIArgs { @Parameter(names = { "--no-inline-methods" }, description = "disable methods inline") protected boolean inlineMethods = true; + @Parameter(names = { "--no-inline-kotlin-lambda" }, description = "disable inline for Kotlin lambdas") + protected boolean allowInlineKotlinLambda = true; + @Parameter(names = "--no-finally", description = "don't extract finally block") protected boolean extractFinally = true; @@ -187,7 +192,7 @@ public class JadxCLIArgs { @Parameter( names = { "--log-level" }, description = "set log level, values: quiet, progress, error, warn, info, debug", - converter = LogHelper.LogLevelConverter.class + converter = LogLevelConverter.class ) protected LogHelper.LogLevelEnum logLevel = LogHelper.LogLevelEnum.PROGRESS; @@ -285,6 +290,7 @@ public JadxArgs toJadxArgs() { args.setInsertDebugLines(addDebugLines); args.setInlineAnonymousClasses(inlineAnonymousClasses); args.setInlineMethods(inlineMethods); + args.setAllowInlineKotlinLambda(allowInlineKotlinLambda); args.setExtractFinally(extractFinally); args.setRenameFlags(renameFlags); args.setFsCaseSensitive(fsCaseSensitive); @@ -366,6 +372,10 @@ public boolean isInlineMethods() { return inlineMethods; } + public boolean isAllowInlineKotlinLambda() { + return allowInlineKotlinLambda; + } + public boolean isExtractFinally() { return extractFinally; } @@ -487,67 +497,58 @@ public Set convert(String value) { } } - public static class CommentsLevelConverter implements IStringConverter { - @Override - public CommentsLevel convert(String value) { - try { - return CommentsLevel.valueOf(value.toUpperCase()); - } catch (Exception e) { - throw new IllegalArgumentException( - '\'' + value + "' is unknown comments level, possible values are: " - + JadxCLIArgs.enumValuesString(CommentsLevel.values())); - } + public static class CommentsLevelConverter extends BaseEnumConverter { + public CommentsLevelConverter() { + super(CommentsLevel::valueOf, CommentsLevel::values); } } - public static class UseKotlinMethodsForVarNamesConverter implements IStringConverter { - @Override - public UseKotlinMethodsForVarNames convert(String value) { - try { - return UseKotlinMethodsForVarNames.valueOf(value.replace('-', '_').toUpperCase()); - } catch (Exception e) { - throw new IllegalArgumentException( - '\'' + value + "' is unknown, possible values are: " - + JadxCLIArgs.enumValuesString(CommentsLevel.values())); - } + public static class UseKotlinMethodsForVarNamesConverter extends BaseEnumConverter { + public UseKotlinMethodsForVarNamesConverter() { + super(UseKotlinMethodsForVarNames::valueOf, UseKotlinMethodsForVarNames::values); } } - public static class DeobfuscationMapFileModeConverter implements IStringConverter { - @Override - public DeobfuscationMapFileMode convert(String value) { - try { - return DeobfuscationMapFileMode.valueOf(value.toUpperCase()); - } catch (Exception e) { - throw new IllegalArgumentException( - '\'' + value + "' is unknown, possible values are: " - + JadxCLIArgs.enumValuesString(DeobfuscationMapFileMode.values())); - } + public static class DeobfuscationMapFileModeConverter extends BaseEnumConverter { + public DeobfuscationMapFileModeConverter() { + super(DeobfuscationMapFileMode::valueOf, DeobfuscationMapFileMode::values); } } - public static class ResourceNameSourceConverter implements IStringConverter { - @Override - public ResourceNameSource convert(String value) { - try { - return ResourceNameSource.valueOf(value.toUpperCase()); - } catch (Exception e) { - throw new IllegalArgumentException( - '\'' + value + "' is unknown, possible values are: " - + JadxCLIArgs.enumValuesString(ResourceNameSource.values())); - } + public static class ResourceNameSourceConverter extends BaseEnumConverter { + public ResourceNameSourceConverter() { + super(ResourceNameSource::valueOf, ResourceNameSource::values); + } + } + + public static class DecompilationModeConverter extends BaseEnumConverter { + public DecompilationModeConverter() { + super(DecompilationMode::valueOf, DecompilationMode::values); } } - public static class DecompilationModeConverter implements IStringConverter { + public static class LogLevelConverter extends BaseEnumConverter { + public LogLevelConverter() { + super(LogHelper.LogLevelEnum::valueOf, LogHelper.LogLevelEnum::values); + } + } + + public abstract static class BaseEnumConverter> implements IStringConverter { + private final Function parse; + private final Supplier values; + + public BaseEnumConverter(Function parse, Supplier values) { + this.parse = parse; + this.values = values; + } + @Override - public DecompilationMode convert(String value) { + public E convert(String value) { try { - return DecompilationMode.valueOf(value.toUpperCase()); + return parse.apply(stringAsEnumName(value)); } catch (Exception e) { throw new IllegalArgumentException( - '\'' + value + "' is unknown, possible values are: " - + JadxCLIArgs.enumValuesString(DecompilationMode.values())); + '\'' + value + "' is unknown, possible values are: " + enumValuesString(values.get())); } } } @@ -557,4 +558,9 @@ public static String enumValuesString(Enum[] values) { .map(v -> v.name().replace('_', '-').toLowerCase(Locale.ROOT)) .collect(Collectors.joining(", ")); } + + private static String stringAsEnumName(String value) { + // inverse of enumValuesString conversion + return value.replace('-', '_').toUpperCase(Locale.ROOT); + } } diff --git a/jadx-with-jadxecute/jadx-cli/src/main/java/jadx/cli/LogHelper.java b/jadx-with-jadxecute/jadx-cli/src/main/java/jadx/cli/LogHelper.java index 6ab9b14..44afa17 100644 --- a/jadx-with-jadxecute/jadx-cli/src/main/java/jadx/cli/LogHelper.java +++ b/jadx-with-jadxecute/jadx-cli/src/main/java/jadx/cli/LogHelper.java @@ -4,8 +4,6 @@ import org.jetbrains.annotations.Nullable; import org.slf4j.LoggerFactory; -import com.beust.jcommander.IStringConverter; - import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; @@ -119,18 +117,4 @@ private static boolean isCustomLogConfig() { } return false; } - - public static class LogLevelConverter implements IStringConverter { - - @Override - public LogLevelEnum convert(String value) { - try { - return LogLevelEnum.valueOf(value.toUpperCase()); - } catch (Exception e) { - throw new IllegalArgumentException( - '\'' + value + "' is unknown log level, possible values are " - + JadxCLIArgs.enumValuesString(LogLevelEnum.values())); - } - } - } } diff --git a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/api/JadxArgs.java b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/api/JadxArgs.java index aa94c97..8f32e8a 100644 --- a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/api/JadxArgs.java +++ b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/api/JadxArgs.java @@ -53,6 +53,7 @@ public class JadxArgs { private boolean extractFinally = true; private boolean inlineAnonymousClasses = true; private boolean inlineMethods = true; + private boolean allowInlineKotlinLambda = true; private boolean skipResources = false; private boolean skipSources = false; @@ -263,6 +264,14 @@ public void setInlineMethods(boolean inlineMethods) { this.inlineMethods = inlineMethods; } + public boolean isAllowInlineKotlinLambda() { + return allowInlineKotlinLambda; + } + + public void setAllowInlineKotlinLambda(boolean allowInlineKotlinLambda) { + this.allowInlineKotlinLambda = allowInlineKotlinLambda; + } + public boolean isExtractFinally() { return extractFinally; } diff --git a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/api/impl/AnnotatedCodeInfo.java b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/api/impl/AnnotatedCodeInfo.java index bdd6359..1b65191 100644 --- a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/api/impl/AnnotatedCodeInfo.java +++ b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/api/impl/AnnotatedCodeInfo.java @@ -9,7 +9,7 @@ public class AnnotatedCodeInfo implements ICodeInfo { - private String code; + private final String code; private final ICodeMetadata metadata; public AnnotatedCodeInfo(String code, Map lineMapping, Map annotations) { diff --git a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/api/impl/SimpleCodeInfo.java b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/api/impl/SimpleCodeInfo.java index 8d4558a..4422cf8 100644 --- a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/api/impl/SimpleCodeInfo.java +++ b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/api/impl/SimpleCodeInfo.java @@ -5,7 +5,7 @@ public class SimpleCodeInfo implements ICodeInfo { - private String code; + private final String code; public SimpleCodeInfo(String code) { this.code = code; diff --git a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java index e5250e8..80c0ecd 100644 --- a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java +++ b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java @@ -17,6 +17,7 @@ import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; +import jadx.core.dex.attributes.FieldInitInsnAttr; import jadx.core.dex.attributes.nodes.FieldReplaceAttr; import jadx.core.dex.attributes.nodes.GenericInfoAttr; import jadx.core.dex.attributes.nodes.LoopLabelAttr; @@ -210,7 +211,31 @@ private void instanceField(ICodeWriter code, FieldInfo field, InsnArg arg) throw } } + protected void staticField(ICodeWriter code, FieldInfo field) throws CodegenException { + FieldNode fieldNode = root.resolveField(field); + if (fieldNode != null + && fieldNode.contains(AFlag.INLINE_INSTANCE_FIELD) + && fieldNode.getParentClass().contains(AType.ANONYMOUS_CLASS)) { + FieldInitInsnAttr initInsnAttr = fieldNode.get(AType.FIELD_INIT_INSN); + if (initInsnAttr != null) { + InsnNode insn = initInsnAttr.getInsn(); + if (insn instanceof ConstructorInsn) { + fieldNode.add(AFlag.DONT_GENERATE); + inlineAnonymousConstructor(code, fieldNode.getParentClass(), (ConstructorInsn) insn); + return; + } + } + } + makeStaticFieldAccess(code, field, fieldNode, mgen.getClassGen()); + } + public static void makeStaticFieldAccess(ICodeWriter code, FieldInfo field, ClassGen clsGen) { + FieldNode fieldNode = clsGen.getClassNode().root().resolveField(field); + makeStaticFieldAccess(code, field, fieldNode, clsGen); + } + + private static void makeStaticFieldAccess(ICodeWriter code, + FieldInfo field, @Nullable FieldNode fieldNode, ClassGen clsGen) { ClassInfo declClass = field.getDeclClass(); // TODO boolean fieldFromThisClass = clsGen.getClassNode().getClassInfo().equals(declClass); @@ -221,7 +246,6 @@ public static void makeStaticFieldAccess(ICodeWriter code, FieldInfo field, Clas } code.add('.'); } - FieldNode fieldNode = clsGen.getClassNode().root().resolveField(field); if (fieldNode != null) { code.attachAnnotation(fieldNode); } @@ -232,10 +256,6 @@ public static void makeStaticFieldAccess(ICodeWriter code, FieldInfo field, Clas } } - protected void staticField(ICodeWriter code, FieldInfo field) { - makeStaticFieldAccess(code, field, mgen.getClassGen()); - } - public void useClass(ICodeWriter code, ArgType type) { mgen.getClassGen().useClass(code, type); } @@ -695,9 +715,7 @@ private void filledNewArray(FilledNewArrayNode insn, ICodeWriter code) throws Co private void makeConstructor(ConstructorInsn insn, ICodeWriter code) throws CodegenException { ClassNode cls = mth.root().resolveClass(insn.getClassType()); if (cls != null && cls.isAnonymous() && !fallback) { - cls.ensureProcessed(); inlineAnonymousConstructor(code, cls, insn); - mth.getParentClass().addInlinedClass(cls); return; } if (insn.isSelf()) { @@ -748,6 +766,7 @@ private void makeConstructor(ConstructorInsn insn, ICodeWriter code) throws Code } private void inlineAnonymousConstructor(ICodeWriter code, ClassNode cls, ConstructorInsn insn) throws CodegenException { + cls.ensureProcessed(); if (this.mth.getParentClass() == cls) { cls.remove(AType.ANONYMOUS_CLASS); cls.remove(AFlag.DONT_GENERATE); @@ -786,6 +805,8 @@ private void inlineAnonymousConstructor(ICodeWriter code, ClassNode cls, Constru ClassGen classGen = new ClassGen(cls, mgen.getClassGen().getParentGen()); classGen.setOuterNameGen(mgen.getNameGen()); classGen.addClassBody(code, true); + + mth.getParentClass().addInlinedClass(cls); } private void makeInvoke(InvokeNode insn, ICodeWriter code) throws CodegenException { @@ -993,9 +1014,10 @@ private void makeInlinedLambdaMethod(ICodeWriter code, InvokeCustomNode customNo // force set external arg names into call method args int extArgsCount = customNode.getArgsCount(); int startArg = customNode.getHandleType() == MethodHandleType.INVOKE_STATIC ? 0 : 1; // skip 'this' arg + int callArg = 0; for (int i = startArg; i < extArgsCount; i++) { RegisterArg extArg = (RegisterArg) customNode.getArg(i); - RegisterArg callRegArg = callArgs.get(i); + RegisterArg callRegArg = callArgs.get(callArg++); callRegArg.getSVar().setCodeVar(extArg.getSVar().getCodeVar()); } code.add(" -> {"); diff --git a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/codegen/RegionGen.java b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/codegen/RegionGen.java index 03bd5e7..6cb28ea 100644 --- a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/codegen/RegionGen.java +++ b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/codegen/RegionGen.java @@ -270,7 +270,7 @@ public void makeSwitch(SwitchRegion sw, ICodeWriter code) throws CodegenExceptio code.startLine('}'); } - private void addCaseKey(ICodeWriter code, InsnArg arg, Object k) { + private void addCaseKey(ICodeWriter code, InsnArg arg, Object k) throws CodegenException { if (k instanceof FieldNode) { FieldNode fn = (FieldNode) k; if (fn.getParentClass().isEnum()) { diff --git a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/attributes/AFlag.java b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/attributes/AFlag.java index 113888f..1815774 100644 --- a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/attributes/AFlag.java +++ b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/attributes/AFlag.java @@ -37,7 +37,9 @@ public enum AFlag { SKIP_FIRST_ARG, SKIP_ARG, // skip argument in invoke call NO_SKIP_ARGS, + ANONYMOUS_CONSTRUCTOR, + INLINE_INSTANCE_FIELD, THIS, SUPER, diff --git a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/AnonymousClassAttr.java b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/AnonymousClassAttr.java index 0f34bf1..ad4a08a 100644 --- a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/AnonymousClassAttr.java +++ b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/AnonymousClassAttr.java @@ -7,12 +7,19 @@ public class AnonymousClassAttr extends PinnedAttribute { + public enum InlineType { + CONSTRUCTOR, + INSTANCE_FIELD, + } + private final ClassNode outerCls; private final ArgType baseType; + private final InlineType inlineType; - public AnonymousClassAttr(ClassNode outerCls, ArgType baseType) { + public AnonymousClassAttr(ClassNode outerCls, ArgType baseType, InlineType inlineType) { this.outerCls = outerCls; this.baseType = baseType; + this.inlineType = inlineType; } public ClassNode getOuterCls() { @@ -23,6 +30,10 @@ public ArgType getBaseType() { return baseType; } + public InlineType getInlineType() { + return inlineType; + } + @Override public AType getAttrType() { return AType.ANONYMOUS_CLASS; @@ -30,6 +41,6 @@ public AType getAttrType() { @Override public String toString() { - return "AnonymousClass{" + outerCls + ", base: " + baseType + '}'; + return "AnonymousClass{" + outerCls + ", base: " + baseType + ", inline type: " + inlineType + '}'; } } diff --git a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/info/AccessInfo.java b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/info/AccessInfo.java index 4a7eb8b..864d95b 100644 --- a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/info/AccessInfo.java +++ b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/info/AccessInfo.java @@ -1,5 +1,7 @@ package jadx.core.dex.info; +import org.intellij.lang.annotations.MagicConstant; + import jadx.api.plugins.input.data.AccessFlags; import jadx.core.Consts; import jadx.core.utils.exceptions.JadxRuntimeException; @@ -20,10 +22,21 @@ public AccessInfo(int accessFlags, AFType type) { this.type = type; } + @MagicConstant(valuesFromClass = AccessFlags.class) public boolean containsFlag(int flag) { return (accFlags & flag) != 0; } + @MagicConstant(valuesFromClass = AccessFlags.class) + public boolean containsFlags(int... flags) { + for (int flag : flags) { + if ((accFlags & flag) == 0) { + return false; + } + } + return true; + } + public AccessInfo remove(int flag) { if (containsFlag(flag)) { return new AccessInfo(accFlags & ~flag, type); diff --git a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/info/ConstStorage.java b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/info/ConstStorage.java index 7968bf9..ed6b224 100644 --- a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/info/ConstStorage.java +++ b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/info/ConstStorage.java @@ -82,14 +82,22 @@ public void processConstFields(ClassNode cls, List staticFields) { return; } for (FieldNode f : staticFields) { - AccessInfo accFlags = f.getAccessFlags(); - if (accFlags.isStatic() && accFlags.isFinal()) { - EncodedValue constVal = f.get(JadxAttrType.CONSTANT_VALUE); - if (constVal != null && constVal.getValue() != null) { - addConstField(cls, f, constVal.getValue(), accFlags.isPublic()); - } + Object value = getFieldConstValue(f); + if (value != null) { + addConstField(cls, f, value, f.getAccessFlags().isPublic()); + } + } + } + + public static @Nullable Object getFieldConstValue(FieldNode fld) { + AccessInfo accFlags = fld.getAccessFlags(); + if (accFlags.isStatic() && accFlags.isFinal()) { + EncodedValue constVal = fld.get(JadxAttrType.CONSTANT_VALUE); + if (constVal != null) { + return constVal.getValue(); } } + return null; } public void removeForClass(ClassNode cls) { diff --git a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/instructions/args/InsnArg.java b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/instructions/args/InsnArg.java index ccb6e83..8b18416 100644 --- a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/instructions/args/InsnArg.java +++ b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/instructions/args/InsnArg.java @@ -264,6 +264,13 @@ public boolean isSameConst(InsnArg other) { return false; } + public boolean isSameVar(RegisterArg arg) { + if (isRegister()) { + return ((RegisterArg) this).sameRegAndSVar(arg); + } + return false; + } + protected final T copyCommonParams(T copy) { copy.copyAttributesFrom(this); copy.setParentInsn(parentInsn); diff --git a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java index 6a981dd..5d41304 100644 --- a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java +++ b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java @@ -13,8 +13,6 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import jadx.api.DecompilationMode; import jadx.api.ICodeCache; @@ -56,8 +54,6 @@ import static jadx.core.dex.nodes.ProcessState.NOT_LOADED; public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeNode, Comparable { - private static final Logger LOG = LoggerFactory.getLogger(ClassNode.class); - private final RootNode root; private final IClassData clsData; @@ -174,10 +170,10 @@ private ArgType checkSuperType(IClassData cls) { return ArgType.object(superType); } - public void updateGenericClsData(ArgType superClass, List interfaces, List generics) { + public void updateGenericClsData(List generics, ArgType superClass, List interfaces) { + this.generics = generics; this.superClass = superClass; this.interfaces = interfaces; - this.generics = generics; } private static void processAttributes(ClassNode cls) { diff --git a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java index c641ea9..c176bf7 100644 --- a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java +++ b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java @@ -43,6 +43,7 @@ import jadx.core.utils.android.AndroidResourcesUtils; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.xmlgen.IResParser; +import jadx.core.xmlgen.ManifestAttributes; import jadx.core.xmlgen.ResDecoder; import jadx.core.xmlgen.ResourceStorage; import jadx.core.xmlgen.entry.ResourceEntry; @@ -174,12 +175,18 @@ public void loadResources(List resources) { if (parser != null) { processResources(parser.getResStorage()); updateObfuscatedFiles(parser, resources); + updateManifestAttribMap(parser); } } catch (Exception e) { LOG.error("Failed to parse '.arsc' file", e); } } + private void updateManifestAttribMap(IResParser parser) { + ManifestAttributes manifestAttributes = ManifestAttributes.getInstance(); + manifestAttributes.updateAttributes(parser); + } + private @Nullable ResourceFile getResourceFile(List resources) { for (ResourceFile rf : resources) { if (rf.getType() == ResourceType.ARSC) { diff --git a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/visitors/ConstInlineVisitor.java b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/visitors/ConstInlineVisitor.java index ab1d3a5..564584d 100644 --- a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/visitors/ConstInlineVisitor.java +++ b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/visitors/ConstInlineVisitor.java @@ -125,17 +125,30 @@ private static boolean forbidNullInlines(SSAVar sVar) { int k = 0; for (RegisterArg useArg : useList) { InsnNode insn = useArg.getParentInsn(); - if (insn == null) { - continue; - } - if (!canUseNull(insn, useArg)) { - useArg.add(AFlag.DONT_INLINE_CONST); + if (insn != null && forbidNullArgInline(insn, useArg)) { k++; } } return k == useList.size(); } + private static boolean forbidNullArgInline(InsnNode insn, RegisterArg useArg) { + switch (insn.getType()) { + case MOVE: + case CAST: + case CHECK_CAST: + // result is null, chain checks + return forbidNullInlines(insn.getResult().getSVar()); + + default: + if (!canUseNull(insn, useArg)) { + useArg.add(AFlag.DONT_INLINE_CONST); + return true; + } + return false; + } + } + private static boolean canUseNull(InsnNode insn, RegisterArg useArg) { switch (insn.getType()) { case INVOKE: diff --git a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/visitors/ExtractFieldInit.java b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/visitors/ExtractFieldInit.java index e83fd44..6ba3fbb 100644 --- a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/visitors/ExtractFieldInit.java +++ b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/visitors/ExtractFieldInit.java @@ -384,8 +384,14 @@ private static List getConstructorsList(ClassNode cls) { return list; } - private static void addFieldInitAttr(MethodNode mth, FieldNode field, InsnNode insn) { - InsnNode assignInsn = InsnNode.wrapArg(insn.getArg(0)); + private static void addFieldInitAttr(MethodNode mth, FieldNode field, IndexInsnNode putInsn) { + InsnNode assignInsn; + InsnArg fldArg = putInsn.getArg(0); + if (fldArg.isInsnWrap()) { + assignInsn = ((InsnWrapArg) fldArg).getWrapInsn(); + } else { + assignInsn = InsnNode.wrapArg(fldArg); + } field.addAttr(new FieldInitInsnAttr(mth, assignInsn)); } } diff --git a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/visitors/MarkMethodsForInline.java b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/visitors/MarkMethodsForInline.java index 46a012e..6df5e76 100644 --- a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/visitors/MarkMethodsForInline.java +++ b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/visitors/MarkMethodsForInline.java @@ -80,13 +80,47 @@ private static MethodInlineAttr inlineMth(MethodNode mth) { return addInlineAttr(mth, insn); } if (insnsCount == 2 && insns.get(1).getType() == InsnType.RETURN) { - // synthetic field setter - return addInlineAttr(mth, insns.get(0)); + InsnNode firstInsn = insns.get(0); + InsnNode retInsn = insns.get(1); + if (retInsn.getArgsCount() == 0 + || isSyntheticAccessPattern(mth, firstInsn, retInsn)) { + return addInlineAttr(mth, firstInsn); + } } // TODO: inline field arithmetics. Disabled tests: TestAnonymousClass3a and TestAnonymousClass5 return null; } + private static boolean isSyntheticAccessPattern(MethodNode mth, InsnNode firstInsn, InsnNode retInsn) { + List mthRegs = mth.getArgRegs(); + switch (firstInsn.getType()) { + case IGET: + return mthRegs.size() == 1 + && retInsn.getArg(0).isSameVar(firstInsn.getResult()) + && firstInsn.getArg(0).isSameVar(mthRegs.get(0)); + case SGET: + return mthRegs.size() == 0 + && retInsn.getArg(0).isSameVar(firstInsn.getResult()); + + case IPUT: + return mthRegs.size() == 2 + && retInsn.getArg(0).isSameVar(mthRegs.get(1)) + && firstInsn.getArg(0).isSameVar(mthRegs.get(1)) + && firstInsn.getArg(1).isSameVar(mthRegs.get(0)); + case SPUT: + return mthRegs.size() == 1 + && retInsn.getArg(0).isSameVar(mthRegs.get(0)) + && firstInsn.getArg(0).isSameVar(mthRegs.get(0)); + + case INVOKE: + return mthRegs.size() >= 1 + && firstInsn.getArg(0).isSameVar(mthRegs.get(0)) + && retInsn.getArg(0).isSameVar(firstInsn.getResult()); + default: + return false; + } + } + private static MethodInlineAttr addInlineAttr(MethodNode mth, InsnNode insn) { if (!fixVisibilityOfInlineCode(mth, insn)) { return null; diff --git a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/visitors/ProcessAnonymous.java b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/visitors/ProcessAnonymous.java index 1b10ac1..cb5350f 100644 --- a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/visitors/ProcessAnonymous.java +++ b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/visitors/ProcessAnonymous.java @@ -10,9 +10,11 @@ import org.jetbrains.annotations.Nullable; +import jadx.api.plugins.input.data.AccessFlags; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.AnonymousClassAttr; +import jadx.core.dex.attributes.nodes.AnonymousClassAttr.InlineType; import jadx.core.dex.info.AccessInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.ClassNode; @@ -30,6 +32,7 @@ UsageInfoVisitor.class } ) +@SuppressWarnings("BooleanMethodIsAlwaysInverted") public class ProcessAnonymous extends AbstractVisitor { private boolean inlineAnonymousClasses; @@ -64,17 +67,26 @@ private static void markAnonymousClass(ClassNode cls) { if (!canBeAnonymous(cls)) { return; } - MethodNode anonymousConstructor = checkUsage(cls); + MethodNode anonymousConstructor = ListUtils.filterOnlyOne(cls.getMethods(), MethodNode::isConstructor); if (anonymousConstructor == null) { return; } + InlineType inlineType = checkUsage(cls, anonymousConstructor); + if (inlineType == null) { + return; + } ArgType baseType = getBaseType(cls); if (baseType == null) { return; } - ClassNode outerCls = anonymousConstructor.getUseIn().get(0).getParentClass(); + ClassNode outerCls; + if (inlineType == InlineType.INSTANCE_FIELD) { + outerCls = cls.getUseInMth().get(0).getParentClass(); + } else { + outerCls = anonymousConstructor.getUseIn().get(0).getParentClass(); + } outerCls.addInlinedClass(cls); - cls.addAttr(new AnonymousClassAttr(outerCls, baseType)); + cls.addAttr(new AnonymousClassAttr(outerCls, baseType, inlineType)); cls.add(AFlag.DONT_GENERATE); anonymousConstructor.add(AFlag.ANONYMOUS_CONSTRUCTOR); @@ -202,14 +214,11 @@ private static boolean canBeAnonymous(ClassNode cls) { * Checks: * - class have only one constructor which used only once (allow common code for field init) * - methods or fields not used outside (allow only nested inner classes with synthetic usage) + * - if constructor used only in class init check if possible inline by instance field * - * @return anonymous constructor method + * @return decided inline type */ - private static MethodNode checkUsage(ClassNode cls) { - MethodNode ctr = ListUtils.filterOnlyOne(cls.getMethods(), MethodNode::isConstructor); - if (ctr == null) { - return null; - } + private static InlineType checkUsage(ClassNode cls, MethodNode ctr) { if (ctr.getUseIn().size() != 1) { // check if used in common field init in all constructors if (!checkForCommonFieldInit(ctr)) { @@ -219,6 +228,9 @@ private static MethodNode checkUsage(ClassNode cls) { MethodNode ctrUseMth = ctr.getUseIn().get(0); ClassNode ctrUseCls = ctrUseMth.getParentClass(); if (ctrUseCls.equals(cls)) { + if (checkForInstanceFieldUsage(cls, ctr)) { + return InlineType.INSTANCE_FIELD; + } // exclude self usage return null; } @@ -226,6 +238,20 @@ private static MethodNode checkUsage(ClassNode cls) { // exclude usage inside inner classes return null; } + if (!checkMethodsUsage(cls, ctr, ctrUseMth)) { + return null; + } + for (FieldNode field : cls.getFields()) { + for (MethodNode useMth : field.getUseIn()) { + if (badMethodUsage(cls, useMth, field.getAccessFlags())) { + return null; + } + } + } + return InlineType.CONSTRUCTOR; + } + + private static boolean checkMethodsUsage(ClassNode cls, MethodNode ctr, MethodNode ctrUseMth) { for (MethodNode mth : cls.getMethods()) { if (mth == ctr) { continue; @@ -235,18 +261,46 @@ private static MethodNode checkUsage(ClassNode cls) { continue; } if (badMethodUsage(cls, useMth, mth.getAccessFlags())) { - return null; + return false; } } } + return true; + } + + private static boolean checkForInstanceFieldUsage(ClassNode cls, MethodNode ctr) { + MethodNode ctrUseMth = ctr.getUseIn().get(0); + if (!ctrUseMth.getMethodInfo().isClassInit()) { + return false; + } + FieldNode instFld = ListUtils.filterOnlyOne(cls.getFields(), + f -> f.getAccessFlags().containsFlags(AccessFlags.PUBLIC, AccessFlags.STATIC, AccessFlags.FINAL) + && f.getFieldInfo().getType().equals(cls.getClassInfo().getType())); + if (instFld == null) { + return false; + } + List instFldUseIn = instFld.getUseIn(); + if (instFldUseIn.size() != 2 + || !instFldUseIn.contains(ctrUseMth) // initialized in class init + || !instFldUseIn.containsAll(cls.getUseInMth()) // class used only with this field + ) { + return false; + } + if (!checkMethodsUsage(cls, ctr, ctrUseMth)) { + return false; + } for (FieldNode field : cls.getFields()) { + if (field == instFld) { + continue; + } for (MethodNode useMth : field.getUseIn()) { if (badMethodUsage(cls, useMth, field.getAccessFlags())) { - return null; + return false; } } } - return ctr; + instFld.add(AFlag.INLINE_INSTANCE_FIELD); + return true; } private static boolean badMethodUsage(ClassNode cls, MethodNode useMth, AccessInfo accessFlags) { @@ -297,6 +351,13 @@ private static ArgType getBaseType(ClassNode cls) { if (cls.root().getClsp().isImplements(superCls.getObject(), interfaceType.getObject())) { return superCls; } + if (cls.root().getArgs().isAllowInlineKotlinLambda()) { + if (superCls.getObject().equals("kotlin.jvm.internal.Lambda")) { + // Inline such class with have different semantic: missing 'arity' property. + // For now, it is unclear how it may affect code execution. + return interfaceType; + } + } return null; } } diff --git a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/visitors/SignatureProcessor.java b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/visitors/SignatureProcessor.java index 6dd492d..3944ee8 100644 --- a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/visitors/SignatureProcessor.java +++ b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/visitors/SignatureProcessor.java @@ -2,8 +2,12 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Objects; +import java.util.Set; + +import org.jetbrains.annotations.Nullable; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.args.ArgType; @@ -18,7 +22,6 @@ import jadx.core.utils.exceptions.JadxException; public class SignatureProcessor extends AbstractVisitor { - private RootNode root; @Override @@ -55,12 +58,54 @@ private void parseClassSignature(ClassNode cls) { break; } } - cls.updateGenericClsData(superClass, interfaces, generics); + generics = fixTypeParamDeclarations(cls, generics, superClass, interfaces); + cls.updateGenericClsData(generics, superClass, interfaces); } catch (Exception e) { cls.addWarnComment("Failed to parse class signature: " + sp.getSignature(), e); } } + /** + * Add missing type parameters from super type and interfaces to make code compilable + */ + private static List fixTypeParamDeclarations(ClassNode cls, + List generics, ArgType superClass, List interfaces) { + if (interfaces.isEmpty() && superClass.equals(ArgType.OBJECT)) { + return generics; + } + Set typeParams = new HashSet<>(); + superClass.visitTypes(t -> addGenericType(typeParams, t)); + interfaces.forEach(i -> i.visitTypes(t -> addGenericType(typeParams, t))); + if (typeParams.isEmpty()) { + return generics; + } + List knownTypeParams; + if (cls.isInner()) { + knownTypeParams = new ArrayList<>(generics); + cls.visitParentClasses(p -> knownTypeParams.addAll(p.getGenericTypeParameters())); + } else { + knownTypeParams = generics; + } + for (ArgType declTypeParam : knownTypeParams) { + typeParams.remove(declTypeParam.getObject()); + } + if (typeParams.isEmpty()) { + return generics; + } + cls.addInfoComment("Add missing generic type declarations: " + typeParams); + List fixedGenerics = new ArrayList<>(generics.size() + typeParams.size()); + fixedGenerics.addAll(generics); + typeParams.stream().sorted().map(ArgType::genericType).forEach(fixedGenerics::add); + return fixedGenerics; + } + + private static @Nullable Object addGenericType(Set usedTypeParameters, ArgType t) { + if (t.isGenericType()) { + usedTypeParameters.add(t.getObject()); + } + return null; + } + private ArgType validateClsType(ClassNode cls, ArgType candidateType, ArgType currentType) { if (!candidateType.isObject()) { cls.addWarnComment("Incorrect class signature, class is not object: " + SignatureParser.getSignature(cls)); diff --git a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/visitors/shrink/CodeShrinkVisitor.java b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/visitors/shrink/CodeShrinkVisitor.java index 367a13e..496abee 100644 --- a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/visitors/shrink/CodeShrinkVisitor.java +++ b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/visitors/shrink/CodeShrinkVisitor.java @@ -9,6 +9,8 @@ import jadx.core.dex.attributes.AFlag; import jadx.core.dex.instructions.InsnType; +import jadx.core.dex.instructions.InvokeCustomNode; +import jadx.core.dex.instructions.InvokeNode; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.InsnWrapArg; import jadx.core.dex.instructions.args.Named; @@ -123,6 +125,9 @@ private static void checkInline(MethodNode mth, BlockNode block, InsnList insnLi return; } } + if (!checkLambdaInline(arg, assignInsn)) { + return; + } int assignPos = insnList.getIndex(assignInsn); if (assignPos != -1) { @@ -145,6 +150,26 @@ && canMoveBetweenBlocks(mth, assignInsn, assignBlock, block, argsInfo.getInsn()) } } + /** + * Forbid inline lambda into invoke as an instance arg, i.e. this will not compile: + * {@code () -> { ... }.apply(); } + */ + private static boolean checkLambdaInline(RegisterArg arg, InsnNode assignInsn) { + if (assignInsn.getType() == InsnType.INVOKE && assignInsn instanceof InvokeCustomNode) { + for (RegisterArg useArg : arg.getSVar().getUseList()) { + InsnNode parentInsn = useArg.getParentInsn(); + if (parentInsn != null && parentInsn.getType() == InsnType.INVOKE) { + InvokeNode invokeNode = (InvokeNode) parentInsn; + InsnArg instArg = invokeNode.getInstanceArg(); + if (instArg != null && instArg == useArg) { + return false; + } + } + } + } + return true; + } + private static boolean varWithSameNameExists(MethodNode mth, SSAVar inlineVar) { for (SSAVar ssaVar : mth.getSVars()) { if (ssaVar == inlineVar || ssaVar.getCodeVar() == inlineVar.getCodeVar()) { diff --git a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeInferenceVisitor.java b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeInferenceVisitor.java index 37fc056..afbe2bb 100644 --- a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeInferenceVisitor.java +++ b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeInferenceVisitor.java @@ -325,7 +325,7 @@ private ArgType replaceAnonymousType(ConstructorInsn ctr) { ClassNode ctrCls = root.resolveClass(ctr.getClassType()); if (ctrCls != null && ctrCls.contains(AFlag.DONT_GENERATE)) { AnonymousClassAttr baseTypeAttr = ctrCls.get(AType.ANONYMOUS_CLASS); - if (baseTypeAttr != null) { + if (baseTypeAttr != null && baseTypeAttr.getInlineType() == AnonymousClassAttr.InlineType.CONSTRUCTOR) { return baseTypeAttr.getBaseType(); } } diff --git a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/export/ExportGradleProject.java b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/export/ExportGradleProject.java index 3850d34..d5bb197 100644 --- a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/export/ExportGradleProject.java +++ b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/export/ExportGradleProject.java @@ -6,6 +6,7 @@ import java.util.Arrays; import java.util.HashSet; import java.util.Set; +import java.util.regex.Pattern; import javax.xml.parsers.DocumentBuilder; @@ -29,6 +30,8 @@ public class ExportGradleProject { private static final Logger LOG = LoggerFactory.getLogger(ExportGradleProject.class); + private static final Pattern ILLEGAL_GRADLE_CHARS = Pattern.compile("[/\\\\:>\"?*|]"); + private static final Set IGNORE_CLS_NAMES = new HashSet<>(Arrays.asList( "R", "BuildConfig")); @@ -72,7 +75,7 @@ private void saveProjectBuildGradle() throws IOException { private void saveSettingsGradle() throws IOException { TemplateFile tmpl = TemplateFile.fromResources("/export/settings.gradle.tmpl"); - tmpl.add("applicationName", applicationParams.getApplicationName()); + tmpl.add("applicationName", ILLEGAL_GRADLE_CHARS.matcher(applicationParams.getApplicationName()).replaceAll("")); tmpl.save(new File(projectDir, "settings.gradle")); } diff --git a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/utils/DecompilerScheduler.java b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/utils/DecompilerScheduler.java index 26e076c..522888c 100644 --- a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/utils/DecompilerScheduler.java +++ b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/utils/DecompilerScheduler.java @@ -139,11 +139,13 @@ private static List> buildFallback(List classes) { } private void dumpBatchesStats(List classes, List> result, List deps) { + int clsInBatches = result.stream().mapToInt(List::size).sum(); double avg = result.stream().mapToInt(List::size).average().orElse(-1); int maxSingleDeps = classes.stream().mapToInt(JavaClass::getTotalDepsCount).max().orElse(-1); int maxSubDeps = deps.stream().mapToInt(DepInfo::getDepsCount).max().orElse(-1); LOG.info("Batches stats:" + "\n input classes: " + classes.size() + + ",\n classes in batches: " + clsInBatches + ",\n batches: " + result.size() + ",\n average batch size: " + String.format("%.2f", avg) + ",\n max single deps count: " + maxSingleDeps diff --git a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/xmlgen/CommonBinaryParser.java b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/xmlgen/CommonBinaryParser.java index 3377f61..41d9ad1 100644 --- a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/xmlgen/CommonBinaryParser.java +++ b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/xmlgen/CommonBinaryParser.java @@ -26,7 +26,7 @@ protected String[] parseStringPoolNoType() throws IOException { int[] stringsOffset = is.readInt32Array(stringCount); int[] stylesOffset = is.readInt32Array(styleCount); - is.checkPos(start + stringsStart, "Expected strings start"); + is.skipToPos(start + stringsStart, "Expected strings start"); String[] strings = new String[stringCount]; byte[] strData = is.readInt8Array((int) (chunkEnd - is.getPos())); if ((flags & UTF8_FLAG) != 0) { diff --git a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/xmlgen/ManifestAttributes.java b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/xmlgen/ManifestAttributes.java index 283d1a0..ff03141 100644 --- a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/xmlgen/ManifestAttributes.java +++ b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/xmlgen/ManifestAttributes.java @@ -17,6 +17,9 @@ import org.w3c.dom.NodeList; import jadx.core.utils.exceptions.JadxRuntimeException; +import jadx.core.xmlgen.entry.RawNamedValue; +import jadx.core.xmlgen.entry.ResourceEntry; +import jadx.core.xmlgen.entry.ValuesParser; public class ManifestAttributes { private static final Logger LOG = LoggerFactory.getLogger(ManifestAttributes.class); @@ -52,6 +55,8 @@ public String toString() { private final Map attrMap = new HashMap<>(); + private final Map appAttrMap = new HashMap<>(); + private static ManifestAttributes instance; public static ManifestAttributes getInstance() { @@ -168,7 +173,10 @@ private void parseValues(String name, NodeList nodeList) { public String decode(String attrName, long value) { MAttr attr = attrMap.get(attrName); if (attr == null) { - return null; + attr = appAttrMap.get(attrName); + if (attr == null) { + return null; + } } if (attr.getType() == MAttrType.ENUM) { return attr.getValues().get(value); @@ -190,4 +198,32 @@ public String decode(String attrName, long value) { } return null; } + + public void updateAttributes(IResParser parser) { + appAttrMap.clear(); + + ResourceStorage resStorage = parser.getResStorage(); + ValuesParser vp = new ValuesParser(parser.getStrings(), resStorage.getResourcesNames()); + + for (ResourceEntry ri : resStorage.getResources()) { + if (ri.getTypeName().equals("attr") && ri.getNamedValues().size() > 1) { + RawNamedValue first = ri.getNamedValues().get(0); + MAttrType attrTyp; + if (first.getRawValue().getData() == ValuesParser.ATTR_TYPE_FLAGS) { + attrTyp = MAttrType.FLAG; + } else if (first.getRawValue().getData() == ValuesParser.ATTR_TYPE_ENUM || first.getRawValue().getData() == 65600) { + attrTyp = MAttrType.ENUM; + } else { + continue; + } + MAttr attr = new MAttr(attrTyp); + for (int i = 1; i < ri.getNamedValues().size(); i++) { + RawNamedValue rv = ri.getNamedValues().get(i); + String value = vp.decodeNameRef(rv.getNameRef()); + attr.getValues().put((long) rv.getRawValue().getData(), value.startsWith("id.") ? value.substring(3) : value); + } + appAttrMap.put(ri.getKeyName(), attr); + } + } + } } diff --git a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/xmlgen/ParserConstants.java b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/xmlgen/ParserConstants.java index 49fe38c..6480208 100644 --- a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/xmlgen/ParserConstants.java +++ b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/xmlgen/ParserConstants.java @@ -33,7 +33,8 @@ protected ParserConstants() { protected static final int RES_TABLE_TYPE_SPEC_TYPE = 0x0202; // 514 protected static final int RES_TABLE_TYPE_LIBRARY = 0x0203; // 515 protected static final int RES_TABLE_TYPE_OVERLAY = 0x0204; // 516 - protected static final int RES_TABLE_TYPE_STAGED_ALIAS = 0x0206; // 517 + protected static final int RES_TABLE_TYPE_OVERLAY_POLICY = 0x0205; // 517 + protected static final int RES_TABLE_TYPE_STAGED_ALIAS = 0x0206; // 518 /** * Type constants diff --git a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/xmlgen/ParserStream.java b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/xmlgen/ParserStream.java index a50d45d..88d0d0d 100644 --- a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/xmlgen/ParserStream.java +++ b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/xmlgen/ParserStream.java @@ -127,6 +127,10 @@ public void checkPos(long expectedOffset, String error) throws IOException { public void skipToPos(long expectedOffset, String error) throws IOException { long pos = getPos(); + if (pos > expectedOffset) { + throw new IOException(error + ", expected offset not reachable: 0x" + Long.toHexString(expectedOffset) + + ", actual: 0x" + Long.toHexString(getPos())); + } if (pos < expectedOffset) { skip(expectedOffset - pos); } diff --git a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/xmlgen/ResTableParser.java b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/xmlgen/ResTableParser.java index 9dcae36..751ae65 100644 --- a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/xmlgen/ResTableParser.java +++ b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/xmlgen/ResTableParser.java @@ -4,7 +4,9 @@ import java.io.InputStream; import java.util.ArrayList; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.regex.Matcher; @@ -168,11 +170,14 @@ private PackageChunk parsePackage() throws IOException { parseLibraryTypeChunk(chunkStart); break; case RES_TABLE_TYPE_OVERLAY: // 0x0204 + parseOverlayTypeChunk(chunkStart); + break; + case RES_TABLE_TYPE_OVERLAY_POLICY: // 0x0205 throw new IOException( - String.format("Encountered unsupported chunk type TYPE_OVERLAY at offset 0x%x ", chunkStart)); + String.format("Encountered unsupported chunk type RES_TABLE_TYPE_OVERLAY_POLICY at offset 0x%x ", chunkStart)); case RES_TABLE_TYPE_STAGED_ALIAS: // 0x0206 - throw new IOException( - String.format("Encountered unsupported chunk type TYPE_STAGED_ALIAS at offset 0x%x ", chunkStart)); + parseStagedAliasChunk(chunkStart); + break; default: LOG.warn("Unknown chunk type {} encountered at offset {}", type, chunkStart); } @@ -242,6 +247,13 @@ private void parseLibraryTypeChunk(long chunkStart) throws IOException { } } + /** + * Parse an ResTable_type (except for the 2 bytes uint16_t + * from ResChunk_header). + * + * @see ResourceTypes.h + */ private void parseTypeChunk(long start, PackageChunk pkg) throws IOException { /* int headerSize = */ is.readInt16(); @@ -249,9 +261,13 @@ private void parseTypeChunk(long start, PackageChunk pkg) throws IOException { long chunkSize = is.readUInt32(); long chunkEnd = start + chunkSize; + // The type identifier this chunk is holding. Type IDs start at 1 (corresponding + // to the value of the type bits in a resource identifier). 0 is invalid. int id = is.readInt8(); - is.checkInt8(0, "type chunk, res0"); - is.checkInt16(0, "type chunk, res1"); + int flags = is.readInt8(); // 0 or 1 + boolean flagSparse = (flags == 1); + + is.checkInt16(0, "type chunk, reserved"); int entryCount = is.readInt32(); long entriesStart = start + is.readInt32(); @@ -262,21 +278,30 @@ private void parseTypeChunk(long start, PackageChunk pkg) throws IOException { LOG.warn("Invalid config flags detected: {}{}", typeName, config.getQualifiers()); } - int[] entryIndexes = new int[entryCount]; - for (int i = 0; i < entryCount; i++) { - entryIndexes[i] = is.readInt32(); + Map entryOffsetMap = new LinkedHashMap<>(entryCount); + if (flagSparse) { + for (int i = 0; i < entryCount; i++) { + entryOffsetMap.put(is.readInt16(), is.readInt16()); + } + } else { + for (int i = 0; i < entryCount; i++) { + entryOffsetMap.put(i, is.readInt32()); + } } is.checkPos(entriesStart, "Expected entry start"); - for (int i = 0; i < entryCount; i++) { - if (entryIndexes[i] != NO_ENTRY) { + int processed = 0; + for (int index : entryOffsetMap.keySet()) { + int offset = entryOffsetMap.get(index); + if (offset != NO_ENTRY) { if (is.getPos() >= chunkEnd) { // Certain resource obfuscated apps like com.facebook.orca have more entries defined // than actually fit into the chunk size -> ignore the remaining entries - LOG.warn("End of chunk reached - ignoring remaining {} entries", entryCount - i); + LOG.warn("End of chunk reached - ignoring remaining {} entries", entryCount - processed); break; } - parseEntry(pkg, id, i, config.getQualifiers()); + parseEntry(pkg, id, index, config.getQualifiers()); } + processed++; } if (chunkEnd > is.getPos()) { // Skip remaining unknown data in this chunk (e.g. type 8 entries") @@ -287,6 +312,36 @@ private void parseTypeChunk(long start, PackageChunk pkg) throws IOException { } } + private void parseOverlayTypeChunk(long chunkStart) throws IOException { + LOG.trace("parsing overlay type chunk starting at offset {}", chunkStart); + // read ResTable_overlayable_header + /* headerSize = */ is.readInt16(); // usually 1032 bytes + int chunkSize = is.readInt32(); // e.g. 1056 bytes + long expectedEndPos = chunkStart + chunkSize; + String name = is.readString16Fixed(256); // 512 bytes + String actor = is.readString16Fixed(256); // 512 bytes + LOG.trace("Overlay header data: name={} actor={}", name, actor); + // skip: ResTable_overlayable_policy_header + ResTable_ref * x + is.skipToPos(expectedEndPos, "overlay chunk end"); + } + + private void parseStagedAliasChunk(long chunkStart) throws IOException { + // read ResTable_staged_alias_header + LOG.trace("parsing staged alias chunk starting at offset {}", chunkStart); + /* headerSize = */ is.readInt16(); + int chunkSize = is.readInt32(); + long expectedEndPos = chunkStart + chunkSize; + int count = is.readInt32(); + + for (int i = 0; i < count; i++) { + // read ResTable_staged_alias_entry + int stagedResId = is.readInt32(); + int finalizedResId = is.readInt32(); + LOG.debug("Staged alias: stagedResId {} finalizedResId {}", stagedResId, finalizedResId); + } + is.skipToPos(expectedEndPos, "staged alias chunk end"); + } + private void parseEntry(PackageChunk pkg, int typeId, int entryId, String config) throws IOException { int size = is.readInt16(); int flags = is.readInt16(); diff --git a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/xmlgen/ResXmlGen.java b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/xmlgen/ResXmlGen.java index 924c34e..5d8556f 100644 --- a/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/xmlgen/ResXmlGen.java +++ b/jadx-with-jadxecute/jadx-core/src/main/java/jadx/core/xmlgen/ResXmlGen.java @@ -109,10 +109,10 @@ private void addValue(ICodeWriter cw, ResourceEntry ri) { addSimpleValue(cw, ri.getTypeName(), ri.getTypeName(), "name", ri.getKeyName(), valueStr); } else { cw.startLine(); - cw.add('<').add(ri.getTypeName()).add(' '); + cw.add('<').add(ri.getTypeName()).add(" name=\""); String itemTag = "item"; if (ri.getTypeName().equals("attr") && !ri.getNamedValues().isEmpty()) { - cw.add("name=\"").add(ri.getKeyName()); + cw.add(ri.getKeyName()); int type = ri.getNamedValues().get(0).getRawValue().getData(); if ((type & ValuesParser.ATTR_TYPE_ENUM) != 0) { itemTag = "enum"; @@ -123,15 +123,17 @@ private void addValue(ICodeWriter cw, ResourceEntry ri) { if (formatValue != null) { cw.add("\" format=\"").add(formatValue); } - cw.add("\""); } else { - cw.add("name=\"").add(ri.getKeyName()).add('\"'); + cw.add(ri.getKeyName()); } - if (ri.getParentRef() != 0) { - String parent = vp.decodeValue(TYPE_REFERENCE, ri.getParentRef()); - cw.add(" parent=\"").add(parent).add('\"'); + if (ri.getTypeName().equals("style") || ri.getParentRef() != 0) { + cw.add("\" parent=\""); + if (ri.getParentRef() != 0) { + String parent = vp.decodeValue(TYPE_REFERENCE, ri.getParentRef()); + cw.add(parent); + } } - cw.add(">"); + cw.add("\">"); cw.incIndent(); for (RawNamedValue value : ri.getNamedValues()) { @@ -177,7 +179,18 @@ private void addItem(ICodeWriter cw, String itemTag, String typeName, RawNamedVa if (dataType == ParserConstants.TYPE_INT_DEC && nameStr != null) { try { int intVal = Integer.parseInt(valueStr); - String newVal = ManifestAttributes.getInstance().decode(nameStr.replace("android:attr.", ""), intVal); + String newVal = ManifestAttributes.getInstance().decode(nameStr.replace("android:", "").replace("attr.", ""), intVal); + if (newVal != null) { + valueStr = newVal; + } + } catch (NumberFormatException e) { + // ignore + } + } + if (dataType == ParserConstants.TYPE_INT_HEX && nameStr != null) { + try { + int intVal = Integer.decode(valueStr); + String newVal = ManifestAttributes.getInstance().decode(nameStr.replace("android:", "").replace("attr.", ""), intVal); if (newVal != null) { valueStr = newVal; } diff --git a/jadx-with-jadxecute/jadx-core/src/main/resources/export/build.gradle.tmpl b/jadx-with-jadxecute/jadx-core/src/main/resources/export/build.gradle.tmpl index c95e445..134898a 100644 --- a/jadx-with-jadxecute/jadx-core/src/main/resources/export/build.gradle.tmpl +++ b/jadx-with-jadxecute/jadx-core/src/main/resources/export/build.gradle.tmpl @@ -4,7 +4,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:4.0.0' + classpath 'com.android.tools.build:gradle:4.2.2' } } diff --git a/jadx-with-jadxecute/jadx-core/src/test/java/jadx/tests/api/ExportGradleTest.java b/jadx-with-jadxecute/jadx-core/src/test/java/jadx/tests/api/ExportGradleTest.java index 71ca7a2..6fa37e1 100644 --- a/jadx-with-jadxecute/jadx-core/src/test/java/jadx/tests/api/ExportGradleTest.java +++ b/jadx-with-jadxecute/jadx-core/src/test/java/jadx/tests/api/ExportGradleTest.java @@ -67,4 +67,10 @@ protected String getAppGradleBuild() { assertThat(appBuildGradle.exists()); return loadFileContent(appBuildGradle); } + + protected String getSettingsGradle() { + File settingsGradle = new File(exportDir, "settings.gradle"); + assertThat(settingsGradle.exists()); + return loadFileContent(settingsGradle); + } } diff --git a/jadx-with-jadxecute/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java b/jadx-with-jadxecute/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java index f78a61b..2f3e876 100644 --- a/jadx-with-jadxecute/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java +++ b/jadx-with-jadxecute/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java @@ -224,6 +224,11 @@ public List decompileFiles(List files) { return sortedClsNodes; } + @NotNull + public ClassNode searchTestCls(List list, String shortClsName) { + return searchCls(list, getTestPkg() + '.' + shortClsName); + } + @NotNull public ClassNode searchCls(List list, String clsName) { for (ClassNode cls : list) { diff --git a/jadx-with-jadxecute/jadx-core/src/test/java/jadx/tests/export/IllegalCharsForGradleWrapper.java b/jadx-with-jadxecute/jadx-core/src/test/java/jadx/tests/export/IllegalCharsForGradleWrapper.java new file mode 100644 index 0000000..b73e442 --- /dev/null +++ b/jadx-with-jadxecute/jadx-core/src/test/java/jadx/tests/export/IllegalCharsForGradleWrapper.java @@ -0,0 +1,17 @@ +package jadx.tests.export; + +import org.junit.jupiter.api.Test; + +import jadx.tests.api.ExportGradleTest; + +import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; + +class IllegalCharsForGradleWrapper extends ExportGradleTest { + + @Test + void test() { + exportGradle("IllegalCharsForGradleWrapper.xml", "strings.xml"); + + assertThat(getSettingsGradle()).contains("'JadxTestApp'"); + } +} diff --git a/jadx-with-jadxecute/jadx-core/src/test/java/jadx/tests/integration/inline/TestInstanceLambda.java b/jadx-with-jadxecute/jadx-core/src/test/java/jadx/tests/integration/inline/TestInstanceLambda.java new file mode 100644 index 0000000..43e232e --- /dev/null +++ b/jadx-with-jadxecute/jadx-core/src/test/java/jadx/tests/integration/inline/TestInstanceLambda.java @@ -0,0 +1,77 @@ +package jadx.tests.integration.inline; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import org.junit.jupiter.api.Test; + +import jadx.core.dex.attributes.AFlag; +import jadx.core.dex.nodes.ClassNode; +import jadx.core.dex.visitors.ProcessAnonymous; +import jadx.core.utils.ListUtils; +import jadx.tests.api.SmaliTest; + +import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; + +public class TestInstanceLambda extends SmaliTest { + + @SuppressWarnings({ "unchecked", "rawtypes", "SameParameterValue" }) + public static class TestCls { + + public Map test(List list) { + return toMap(list, Lambda$1.INSTANCE); + } + + /** + * Smali test missing 'T' definition in 'Lambda' + * Note: use '$1' so class looks like generated by compiler and pass check in + * {@link ProcessAnonymous#canBeAnonymous(ClassNode)} + */ + @SuppressWarnings({ "CheckStyle", "checkstyle:TypeName" }) + private static class Lambda$1 implements Function { + public static final Lambda$1 INSTANCE = new Lambda$1(); + + @Override + public T apply(T t) { + return t; + } + } + + private static Map toMap(List list, Function valueMap) { + return null; + } + } + + @Test + public void test() { + useJavaInput(); + noDebugInfo(); + assertThat(getClassNode(TestCls.class)) + .code(); + } + + @Test + public void testSmaliDisableInline() { + args.setInlineAnonymousClasses(false); + List classNodes = loadFromSmaliFiles(); + assertThat(searchTestCls(classNodes, "Lambda$1")) + .code() + .containsOne("class Lambda$1 implements Function {"); + assertThat(searchTestCls(classNodes, "TestCls")) + .code() + .containsOne("Lambda$1.INSTANCE"); + } + + @Test + public void testSmali() { + List classNodes = loadFromSmaliFiles(); + assertThat(ListUtils.filter(classNodes, c -> !c.contains(AFlag.DONT_GENERATE))) + .describedAs("Expect lambda to be inlined") + .hasSize(1); + assertThat(searchTestCls(classNodes, "TestCls")) + .code() + .doesNotContain("Lambda$1.INSTANCE") + .containsOne("toMap(list, new Function() {"); + } +} diff --git a/jadx-with-jadxecute/jadx-core/src/test/java/jadx/tests/integration/java8/TestLambdaInstance2.java b/jadx-with-jadxecute/jadx-core/src/test/java/jadx/tests/integration/java8/TestLambdaInstance2.java new file mode 100644 index 0000000..c1cefa3 --- /dev/null +++ b/jadx-with-jadxecute/jadx-core/src/test/java/jadx/tests/integration/java8/TestLambdaInstance2.java @@ -0,0 +1,36 @@ +package jadx.tests.integration.java8; + +import org.junit.jupiter.api.Test; + +import jadx.tests.api.IntegrationTest; + +import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; + +public class TestLambdaInstance2 extends IntegrationTest { + + public static class TestCls { + private String field; + + public Runnable test(String str, int i) { + return () -> call(str, i); + } + + public void call(String str, int i) { + field = str + '=' + i; + } + + public void check() throws Exception { + field = ""; + test("num", 7).run(); + assertThat(field).isEqualTo("num=7"); + } + } + + @Test + public void test() { + assertThat(getClassNode(TestCls.class)) + .code() + .doesNotContain("lambda$") + .containsOne("call(str, i)"); + } +} diff --git a/jadx-with-jadxecute/jadx-core/src/test/java/jadx/tests/integration/java8/TestLambdaReturn.java b/jadx-with-jadxecute/jadx-core/src/test/java/jadx/tests/integration/java8/TestLambdaReturn.java new file mode 100644 index 0000000..3a5ec82 --- /dev/null +++ b/jadx-with-jadxecute/jadx-core/src/test/java/jadx/tests/integration/java8/TestLambdaReturn.java @@ -0,0 +1,61 @@ +package jadx.tests.integration.java8; + +import org.junit.jupiter.api.Test; + +import jadx.tests.api.IntegrationTest; +import jadx.tests.api.extensions.profiles.TestProfile; +import jadx.tests.api.extensions.profiles.TestWithProfiles; + +import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; + +public class TestLambdaReturn extends IntegrationTest { + + @SuppressWarnings("unused") + public static class TestCls { + interface Function0 { + R apply(); + } + + public static class T2 { + public long l; + + public T2(long l) { + this.l = l; + } + + public void w() { + } + } + + public Byte test(Byte b1) { + Function0 f1 = () -> { + new T2(94L).w(); + return null; + }; + f1.apply(); + return null; + } + } + + @TestWithProfiles(TestProfile.DX_J8) + public void test() { + assertThat(getClassNode(TestCls.class)) + .code() + .containsLines(2, + "Function0 f1 = () -> {", + indent() + "new T2(94L).w();", + indent() + "return null;", + "};"); + } + + @TestWithProfiles(TestProfile.D8_J11_DESUGAR) + public void testLambda() { + getClassNode(TestCls.class); + } + + @Test + public void testNoDebug() { + noDebugInfo(); + getClassNode(TestCls.class); + } +} diff --git a/jadx-with-jadxecute/jadx-core/src/test/java/jadx/tests/integration/others/TestNullInline.java b/jadx-with-jadxecute/jadx-core/src/test/java/jadx/tests/integration/others/TestNullInline.java new file mode 100644 index 0000000..92979c6 --- /dev/null +++ b/jadx-with-jadxecute/jadx-core/src/test/java/jadx/tests/integration/others/TestNullInline.java @@ -0,0 +1,43 @@ +package jadx.tests.integration.others; + +import org.junit.jupiter.api.Test; + +import jadx.tests.api.IntegrationTest; + +import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; + +public class TestNullInline extends IntegrationTest { + + @SuppressWarnings({ "RedundantCast", "DataFlowIssue", "unused" }) + public static class TestCls { + public static Long test(Double d1) { + T1 t1 = (T1) null; + return t1.t2.l; + } + + static class T2 { + public long l; + } + + static class T1 { + public T2 t2; + + public T1(T2 t2) { + this.t2 = t2; + } + } + } + + @Test + public void test() { + assertThat(getClassNode(TestCls.class)) + .code() + .containsOne("Long.valueOf(t1.t2.l);"); + } + + @Test + public void testNoDebug() { + noDebugInfo(); + getClassNode(TestCls.class); + } +} diff --git a/jadx-with-jadxecute/jadx-core/src/test/manifest/IllegalCharsForGradleWrapper.xml b/jadx-with-jadxecute/jadx-core/src/test/manifest/IllegalCharsForGradleWrapper.xml new file mode 100644 index 0000000..2e1a1cd --- /dev/null +++ b/jadx-with-jadxecute/jadx-core/src/test/manifest/IllegalCharsForGradleWrapper.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/jadx-with-jadxecute/jadx-core/src/test/smali/inline/TestInstanceLambda/Lambda.smali b/jadx-with-jadxecute/jadx-core/src/test/smali/inline/TestInstanceLambda/Lambda.smali new file mode 100644 index 0000000..79c83b1 --- /dev/null +++ b/jadx-with-jadxecute/jadx-core/src/test/smali/inline/TestInstanceLambda/Lambda.smali @@ -0,0 +1,40 @@ +.class public Linline/Lambda$1; +.super Ljava/lang/Object; + +.implements Ljava/util/function/Function; + + +.annotation system Ldalvik/annotation/Signature; + value = { + "Ljava/lang/Object;", + "Ljava/util/function/Function", + ";" + } +.end annotation + +.field public static final INSTANCE:Linline/Lambda$1; + +.method static constructor ()V + .registers 1 + new-instance v0, Linline/Lambda$1; + invoke-direct {v0}, Linline/Lambda$1;->()V + sput-object v0, Linline/Lambda$1;->INSTANCE:Linline/Lambda$1; + return-void +.end method + +.method private constructor ()V + .registers 1 + invoke-direct {p0}, Ljava/lang/Object;->()V + return-void +.end method + +.method public final apply(Ljava/lang/Object;)Ljava/lang/Object; + .registers 2 + .annotation system Ldalvik/annotation/Signature; + value = { + "(TT;)TT;" + } + .end annotation + + return-object p1 +.end method diff --git a/jadx-with-jadxecute/jadx-core/src/test/smali/inline/TestInstanceLambda/TestCls.smali b/jadx-with-jadxecute/jadx-core/src/test/smali/inline/TestInstanceLambda/TestCls.smali new file mode 100644 index 0000000..325ab0c --- /dev/null +++ b/jadx-with-jadxecute/jadx-core/src/test/smali/inline/TestInstanceLambda/TestCls.smali @@ -0,0 +1,42 @@ +.class public Linline/TestCls; +.super Ljava/lang/Object; + +.method public test(Ljava/util/List;)Ljava/util/Map; + .registers 3 + .annotation system Ldalvik/annotation/Signature; + value = { + "(", + "Ljava/util/List", + "<+TT;>;)", + "Ljava/util/Map", + ";" + } + .end annotation + + sget-object v0, Linline/Lambda$1;->INSTANCE:Linline/Lambda$1; + invoke-static {p1, v0}, Linline/TestCls;->toMap(Ljava/util/List;Ljava/util/function/Function;)Ljava/util/Map; + move-result-object v0 + return-object v0 +.end method + +.method private static toMap(Ljava/util/List;Ljava/util/function/Function;)Ljava/util/Map; + .registers 4 + .annotation system Ldalvik/annotation/Signature; + value = { + "(", + "Ljava/util/List", + "<+TT;>;", + "Ljava/util/function/Function", + ";)", + "Ljava/util/Map", + ";" + } + .end annotation + + const/4 v0, 0x0 + return-object v0 +.end method diff --git a/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/device/debugger/DebugController.java b/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/device/debugger/DebugController.java index 86a206d..129b4a9 100644 --- a/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/device/debugger/DebugController.java +++ b/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/device/debugger/DebugController.java @@ -14,12 +14,10 @@ import javax.swing.JOptionPane; import javax.swing.tree.DefaultMutableTreeNode; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.reactivex.annotations.NonNull; -import io.reactivex.annotations.Nullable; - import jadx.core.dex.info.FieldInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.ClassNode; @@ -42,6 +40,7 @@ import jadx.gui.ui.panel.JDebuggerPanel.IListElement; import jadx.gui.ui.panel.JDebuggerPanel.ValueTreeNode; import jadx.gui.utils.NLS; +import jadx.gui.utils.UiUtils; public final class DebugController implements SmaliDebugger.SuspendListener, IDebugController { @@ -78,7 +77,7 @@ public boolean startDebugger(JDebuggerPanel debuggerPanel, String adbHost, int a initTypeMap(); } this.debuggerPanel = debuggerPanel; - debuggerPanel.resetUI(); + UiUtils.uiRunAndWait(debuggerPanel::resetUI); try { debugger = SmaliDebugger.attach(adbHost, adbPort, this); } catch (SmaliDebuggerException e) { @@ -251,7 +250,6 @@ private RuntimeType castType(ArgType type) { throw new JadxRuntimeException("Unexpected type: " + type); } - @NonNull protected static RuntimeType castType(String type) { RuntimeType rt = null; if (!StringUtils.isEmpty(type)) { @@ -663,22 +661,22 @@ private void updateAllFieldValues(long thisID, FrameNode frame) { } private void updateAllRegisters(FrameNode frame) { - if (buildRegTreeNodes(frame).size() > 0) { - fetchAllRegisters(frame); - } + UiUtils.uiRun(() -> { + if (!buildRegTreeNodes(frame).isEmpty()) { + fetchAllRegisters(frame); + } + }); } private void fetchAllRegisters(FrameNode frame) { List regs = cur.regAdapter.getInitializedList(frame.getCodeOffset()); for (SmaliRegister reg : regs) { - lazyQueue.execute(() -> { - Entry info = cur.regAdapter.getInfo(reg.getRuntimeRegNum(), frame.getCodeOffset()); - RegTreeNode regNode = frame.getRegNodes().get(reg.getRegNum()); - if (info != null) { - applyDbgInfo(regNode, info); - } - updateRegister(regNode, null, true); - }); + Entry info = cur.regAdapter.getInfo(reg.getRuntimeRegNum(), frame.getCodeOffset()); + RegTreeNode regNode = frame.getRegNodes().get(reg.getRegNum()); + if (info != null) { + applyDbgInfo(regNode, info); + } + updateRegister(regNode, null, true); } } diff --git a/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundExecutor.java b/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundExecutor.java index 8c1e0b7..114da9b 100644 --- a/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundExecutor.java +++ b/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundExecutor.java @@ -20,7 +20,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.gui.settings.JadxSettings; import jadx.gui.ui.MainWindow; import jadx.gui.ui.panel.ProgressPanel; @@ -60,14 +59,6 @@ public synchronized Future execute(IBackgroundTask task) { return taskWorker; } - public TaskStatus executeAndWait(IBackgroundTask task) { - try { - return execute(task).get(); - } catch (Exception e) { - throw new JadxRuntimeException("Task execution error", e); - } - } - public synchronized void cancelAll() { try { taskRunning.values().forEach(Cancelable::cancel); diff --git a/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/jobs/DecompileTask.java b/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/jobs/DecompileTask.java index b288df2..01bd04d 100644 --- a/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/jobs/DecompileTask.java +++ b/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/jobs/DecompileTask.java @@ -5,12 +5,15 @@ import java.util.List; import java.util.concurrent.atomic.AtomicInteger; +import javax.swing.JOptionPane; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.ICodeCache; import jadx.api.JavaClass; import jadx.gui.JadxWrapper; +import jadx.gui.ui.MainWindow; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; @@ -23,14 +26,16 @@ public static int calcDecompileTimeLimit(int classCount) { return classCount * CLS_LIMIT + 5000; } + private final MainWindow mainWindow; private final JadxWrapper wrapper; private final AtomicInteger complete = new AtomicInteger(0); private int expectedCompleteCount; private ProcessResult result; - public DecompileTask(JadxWrapper wrapper) { - this.wrapper = wrapper; + public DecompileTask(MainWindow mainWindow) { + this.mainWindow = mainWindow; + this.wrapper = mainWindow.getWrapper(); } @Override @@ -40,6 +45,10 @@ public String getTitle() { @Override public List scheduleJobs() { + if (mainWindow.getCacheObject().isFullDecompilationFinished()) { + return Collections.emptyList(); + } + List classes = wrapper.getIncludedClasses(); expectedCompleteCount = classes.size(); complete.set(0); @@ -87,7 +96,41 @@ public void onDone(ITaskInfo taskInfo) { + ", time limit:{ total: " + timeLimit + "ms, per cls: " + CLS_LIMIT + "ms }" + ", status: " + taskInfo.getStatus()); } - this.result = new ProcessResult(skippedCls, taskInfo.getStatus(), timeLimit); + result = new ProcessResult(skippedCls, taskInfo.getStatus(), timeLimit); + + wrapper.unloadClasses(); + processDecompilationResults(); + System.gc(); + + mainWindow.getCacheObject().setFullDecompilationFinished(skippedCls == 0); + } + + private void processDecompilationResults() { + int skippedCls = result.getSkipped(); + if (skippedCls == 0) { + return; + } + TaskStatus status = result.getStatus(); + LOG.warn("Decompile and indexing of some classes skipped: {}, status: {}", skippedCls, status); + switch (status) { + case CANCEL_BY_USER: { + String reason = NLS.str("message.userCancelTask"); + String message = NLS.str("message.indexIncomplete", reason, skippedCls); + JOptionPane.showMessageDialog(mainWindow, message); + break; + } + case CANCEL_BY_TIMEOUT: { + String reason = NLS.str("message.taskTimeout", result.getTimeLimit()); + String message = NLS.str("message.indexIncomplete", reason, skippedCls); + JOptionPane.showMessageDialog(mainWindow, message); + break; + } + case CANCEL_BY_MEMORY: { + mainWindow.showHeapUsageBar(); + JOptionPane.showMessageDialog(mainWindow, NLS.str("message.indexingClassesSkipped", skippedCls)); + break; + } + } } @Override diff --git a/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/plugins/jadxecute/AddCommentHelper.java b/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/plugins/jadxecute/AddCommentHelper.java deleted file mode 100644 index 1c70726..0000000 --- a/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/plugins/jadxecute/AddCommentHelper.java +++ /dev/null @@ -1,260 +0,0 @@ -package jadx.gui.plugins.jadxecute; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.function.Consumer; -import java.util.stream.Collectors; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import jadx.api.JavaNode; -import jadx.api.data.ICodeComment; -import jadx.api.data.impl.JadxCodeComment; -import jadx.api.data.impl.JadxCodeData; -import jadx.api.data.impl.JadxCodeRef; -import jadx.api.data.impl.JadxNodeRef; -import jadx.gui.treemodel.JClass; -import jadx.gui.ui.MainWindow; -import jadx.gui.ui.codearea.CodeArea; -import jadx.gui.settings.JadxProject; - -import jadx.api.JavaVariable; -import jadx.core.utils.exceptions.JadxRuntimeException; -import jadx.gui.jobs.TaskStatus; -import jadx.gui.treemodel.JMethod; -import jadx.gui.treemodel.JNode; -import jadx.gui.treemodel.JPackage; -import jadx.gui.ui.TabbedPane; -import jadx.gui.ui.codearea.ClassCodeContentPanel; -import jadx.gui.ui.panel.ContentPanel; -import jadx.gui.utils.CacheObject; -import jadx.gui.utils.JNodeCache; - -public class AddCommentHelper { - private static final Logger LOG = LoggerFactory.getLogger(AddCommentHelper.class); - - private MainWindow mainWindow; - private CacheObject cache; - private String newCommentString; - private JavaNode node; - private boolean updateComment; - - public AddCommentHelper(MainWindow mainWindow, JavaNode node) { - this.mainWindow = mainWindow; - this.cache = mainWindow.getCacheObject(); - this.node = node; - } - - // To be implemented - /* - public String addVariableComment(String newCommentString, JavaVariable javaVariable) { - this.newCommentString = newCommentString; - - ICodeComment blankComment = new JadxCodeComment(JadxNodeRef.forJavaNode(node), JadxCodeRef.forVar(javaVariable), ""); - if (blankComment == null) { - return "Failed to add comment"; - } - - ICodeComment existComment = searchForExistComment(blankComment); - if (existComment != null) { - this.updateComment = true; - apply(existComment, javaVariable); - } else { - this.updateComment = false; - apply(blankComment, javaVariable); - } - - return "Added comment in " + node.getFullName() + " for variable " + javaVariable.getName(); - } - */ - - public String addInstructionComment(String newCommentString, int commentSmaliOffset) { - this.newCommentString = newCommentString; - - ICodeComment blankComment = new JadxCodeComment(JadxNodeRef.forJavaNode(node), JadxCodeRef.forInsn(commentSmaliOffset), ""); - if (blankComment == null) { - return "Failed to add comment"; - } - - ICodeComment existComment = searchForExistComment(blankComment); - if (existComment != null) { - this.updateComment = true; - apply(existComment, commentSmaliOffset); - } else { - this.updateComment = false; - apply(blankComment, commentSmaliOffset); - } - - return "Added comment in " + node.getFullName() + " at offset " + commentSmaliOffset; - } - - private void apply(ICodeComment comment, int commentOffset) { - if (newCommentString.isEmpty()) { - if (updateComment) { - updateCommentsData(list -> list.removeIf(c -> c == comment)); - } - return; - } - - ICodeComment newComment = new JadxCodeComment(JadxNodeRef.forJavaNode(node), JadxCodeRef.forInsn(commentOffset), newCommentString); - if (updateComment) { - updateCommentsData(list -> { - list.remove(comment); - list.add(newComment); - }); - } else { - updateCommentsData(list -> list.add(newComment)); - } - } - - private void apply(ICodeComment comment, JavaVariable javaVariable) { - if (newCommentString.isEmpty()) { - if (updateComment) { - updateCommentsData(list -> list.removeIf(c -> c == comment)); - } - return; - } - - ICodeComment newComment = new JadxCodeComment(JadxNodeRef.forJavaNode(node), JadxCodeRef.forVar(javaVariable), newCommentString); - if (updateComment) { - updateCommentsData(list -> { - list.remove(comment); - list.add(newComment); - }); - } else { - updateCommentsData(list -> list.add(newComment)); - } - } - - private ICodeComment searchForExistComment(ICodeComment blankComment) { - try { - JadxProject project = mainWindow.getProject(); - JadxCodeData codeData = project.getCodeData(); - if (codeData == null || codeData.getComments().isEmpty()) { - return null; - } - for (ICodeComment comment : codeData.getComments()) { - if (Objects.equals(comment.getNodeRef(), blankComment.getNodeRef()) - && Objects.equals(comment.getCodeRef(), blankComment.getCodeRef())) { - return comment; - } - } - } catch (Exception e) { - LOG.error("Error searching for exists comment", e); - } - return null; - } - - private String updateCommentsData(Consumer> updater) { - JadxProject project = mainWindow.getProject(); - JadxCodeData codeData = project.getCodeData(); - - try { - if (codeData == null) { - codeData = new JadxCodeData(); - } - List list = new ArrayList<>(codeData.getComments()); - - updater.accept(list); - Collections.sort(list); - codeData.setComments(list); - project.setCodeData(codeData); - mainWindow.getWrapper().reloadCodeData(); - } catch (Exception e) { - LOG.error("Comment action failed", e); - } - try { - refreshState(); - } catch (Exception e) { - LOG.error("Failed to reload code", e); - } - - String retval = ""; - - for (ICodeComment comment : codeData.getComments()) { - retval += comment.getComment() + "\n"; - } - return retval; - } - - private void refreshState() { - mainWindow.getWrapper().reInitRenameVisitor(); - - JNodeCache nodeCache = cache.getNodeCache(); - JavaNode javaNode = node; - - List toUpdate = new ArrayList<>(); - if (javaNode != null) { - toUpdate.add(javaNode); - if (node instanceof JMethod) { - toUpdate.addAll(((JMethod) node).getJavaMethod().getOverrideRelatedMethods()); - } - } else { - throw new JadxRuntimeException("Unexpected node type: " + node); - } - Set updatedTopClasses = toUpdate - .stream() - .map(JavaNode::getTopParentClass) - .map(nodeCache::makeFrom) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); - - LOG.debug("Classes to update: {}", updatedTopClasses); - - refreshTabs(mainWindow.getTabbedPane(), updatedTopClasses); - - if (!updatedTopClasses.isEmpty()) { - mainWindow.getBackgroundExecutor().execute("Refreshing", - () -> refreshClasses(updatedTopClasses), - (status) -> { - if (status == TaskStatus.CANCEL_BY_MEMORY) { - mainWindow.showHeapUsageBar(); - } - if (node instanceof JPackage) { - mainWindow.getTreeRoot().update(); - } - mainWindow.reloadTree(); - }); - } - } - - private void refreshClasses(Set updatedTopClasses) { - if (updatedTopClasses.size() < 10) { - // small batch => reload - LOG.debug("Classes to reload: {}", updatedTopClasses.size()); - for (JClass cls : updatedTopClasses) { - try { - cls.reload(cache); - } catch (Exception e) { - LOG.error("Failed to reload class: {}", cls.getFullName(), e); - } - } - } else { - // big batch => unload - LOG.debug("Classes to unload: {}", updatedTopClasses.size()); - for (JClass cls : updatedTopClasses) { - try { - cls.unload(cache); - } catch (Exception e) { - LOG.error("Failed to unload class: {}", cls.getFullName(), e); - } - } - } - } - - private void refreshTabs(TabbedPane tabbedPane, Set updatedClasses) { - for (Map.Entry entry : tabbedPane.getOpenTabs().entrySet()) { - JClass rootClass = entry.getKey().getRootClass(); - if (updatedClasses.remove(rootClass)) { - ClassCodeContentPanel contentPanel = (ClassCodeContentPanel) entry.getValue(); - CodeArea codeArea = (CodeArea) contentPanel.getJavaCodePanel().getCodeArea(); - codeArea.refreshClass(); - } - } - } -} diff --git a/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/plugins/jadxecute/ErrorHighlightingLinter.java b/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/plugins/jadxecute/ErrorHighlightingLinter.java deleted file mode 100644 index d32047c..0000000 --- a/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/plugins/jadxecute/ErrorHighlightingLinter.java +++ /dev/null @@ -1,5 +0,0 @@ -package jadx.gui.plugins.jadxecute; - -public class ErrorHighlightingLinter { - -} diff --git a/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/plugins/jadxecute/JadxecuteDialog.java b/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/plugins/jadxecute/JadxecuteDialog.java deleted file mode 100644 index 6819c2d..0000000 --- a/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/plugins/jadxecute/JadxecuteDialog.java +++ /dev/null @@ -1,350 +0,0 @@ -package jadx.gui.plugins.jadxecute; - -import java.awt.Container; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.HashMap; -import java.util.Map; - -import javax.swing.JButton; -import javax.swing.JDialog; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.WindowConstants; -import javax.swing.JTextArea; -import javax.swing.JTextField; -import javax.swing.BoxLayout; -import javax.swing.border.EmptyBorder; - -import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; -import org.fife.ui.rsyntaxtextarea.SyntaxConstants; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.awt.BorderLayout; -import java.awt.Color; -import java.awt.Component; -import java.awt.Dimension; -import java.awt.Font; - -import javax.swing.JFileChooser; -import javax.swing.JList; -import javax.swing.JScrollPane; -import javax.swing.ListSelectionModel; - -import jadx.gui.jobs.BackgroundExecutor; -import jadx.gui.settings.JadxSettings; -import jadx.gui.ui.MainWindow; -import jadx.gui.utils.UiUtils; - -public class JadxecuteDialog extends JDialog { - private static final Logger LOG = LoggerFactory.getLogger(JadxecuteDialog.class); - - private final transient JadxSettings settings; - private final transient MainWindow mainWindow; - - private static final int DEFAULT_FONT_SIZE = 12; - - public JadxecuteDialog(MainWindow mainWindow) { - super(mainWindow, "JADXexecute", true); - this.mainWindow = mainWindow; - this.settings = mainWindow.getSettings(); - - initUI(); - } - - private void initUI() { - JPanel mainPanel = new JPanel(); - mainPanel.setLayout(new BorderLayout()); - mainPanel.setBorder(new EmptyBorder(10, 10, 10, 10)); - - // Input and output code areas - JLabel codeInputDescription = new JLabel("Java Input"); - codeInputDescription.setFont(new Font(null, Font.BOLD, DEFAULT_FONT_SIZE)); - codeInputDescription.setPreferredSize(new Dimension(100, 16)); - RSyntaxTextArea codeInputArea = new RSyntaxTextArea(getDefaultCodeInputText()); - codeInputArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVA); - JScrollPane codeInputScrollPanel = new JScrollPane(codeInputArea); - codeInputScrollPanel.setPreferredSize(new Dimension(600, 200)); - - JLabel consoleOutputDescription = new JLabel("Console Output"); - consoleOutputDescription.setFont(new Font(null, Font.BOLD, DEFAULT_FONT_SIZE)); - consoleOutputDescription.setPreferredSize(new Dimension(80, 16)); - consoleOutputDescription.setBorder(new EmptyBorder(10, 0, 10, 0)); - JTextArea consoleOutputArea = new JTextArea(" "); - consoleOutputArea.setEditable(false); - consoleOutputArea.setBackground(Color.BLACK); - consoleOutputArea.setForeground(Color.WHITE); - - JScrollPane consoleOutputScrollPanel = new JScrollPane(consoleOutputArea); - consoleOutputScrollPanel.setPreferredSize(new Dimension(550, 100)); - - // Input and output code areas - JPanel codePanel = initCodePanel(codeInputDescription, codeInputScrollPanel, consoleOutputDescription, consoleOutputScrollPanel); - JPanel filePanel = initFilePanel(codeInputArea); - - // Buttons for running and closing the jadxecute dialog - JPanel bottomPan = new JPanel(); - bottomPan.setLayout(new BorderLayout()); - JPanel buttonPane = new JPanel(); - JLabel statusLabel = new JLabel("Status: Ready"); - statusLabel.setPreferredSize(new Dimension(100, 16)); - JButton run = new JButton("Run"); - JButton close = new JButton("Close"); - close.addActionListener(event -> close()); - - // run code - run.addActionListener(event -> runUserCode(codeInputArea, consoleOutputArea, statusLabel, run)); - - buttonPane.add(run); - buttonPane.add(close); - bottomPan.add(statusLabel, BorderLayout.WEST); - bottomPan.add(buttonPane, BorderLayout.CENTER); - getRootPane().setDefaultButton(close); - - // Add file panel and code panel to a sub panel - JPanel leftPanel = new JPanel(); - leftPanel.setLayout(new BorderLayout()); - leftPanel.add(codePanel, BorderLayout.CENTER); - - // Add left panel and buttons panel to main panel - mainPanel.add(leftPanel, BorderLayout.WEST); - mainPanel.add(bottomPan, BorderLayout.SOUTH); - - // Add code examples panel to east position of main panel - mainPanel.add(initCodeExamplesPanel(codeInputArea, filePanel), BorderLayout.EAST); - finishUI(mainPanel); - - } - - private JPanel initCodePanel(JLabel codeInputDescription, JScrollPane codeInputScrollPanel, - JLabel consoleOutputDescription, JScrollPane consoleOutputScrollPanel) { - - JPanel codePanel = new JPanel(); - codePanel.setLayout(new BoxLayout(codePanel, BoxLayout.Y_AXIS)); - codePanel.setAlignmentX(Component.LEFT_ALIGNMENT); - - codeInputDescription.setAlignmentX(Component.LEFT_ALIGNMENT); - codeInputScrollPanel.setAlignmentX(Component.LEFT_ALIGNMENT); - consoleOutputDescription.setAlignmentX(Component.LEFT_ALIGNMENT); - consoleOutputScrollPanel.setAlignmentX(Component.LEFT_ALIGNMENT); - - codePanel.add(codeInputDescription); - codePanel.add(codeInputScrollPanel); - codePanel.add(consoleOutputDescription); - codePanel.add(consoleOutputScrollPanel); - - return codePanel; - } - - private JPanel initCodeExamplesPanel(RSyntaxTextArea codeInputArea, JPanel filePanel) { - // Add options on the right to display different code examples - JPanel codeExamplesPanel = new JPanel(); - codeExamplesPanel.setLayout(new BorderLayout()); - codeExamplesPanel.setBorder(new EmptyBorder(10, 10, 10, 10)); - JLabel scriptSelection = new JLabel("Select Template:"); - scriptSelection.setFont(new Font(null, Font.BOLD, DEFAULT_FONT_SIZE)); - - JScrollPane exampleScrollPane = initCodeExamplesListeners(codeInputArea); - JPanel southExamplesPanel = new JPanel(); - southExamplesPanel.setLayout(new BorderLayout()); - southExamplesPanel.add(scriptSelection, BorderLayout.NORTH); - southExamplesPanel.add(exampleScrollPane, BorderLayout.CENTER); - codeExamplesPanel.add(filePanel, BorderLayout.NORTH); - codeExamplesPanel.add(southExamplesPanel, BorderLayout.CENTER); - codeExamplesPanel.setPreferredSize(new Dimension(300, 400)); - - return codeExamplesPanel; - } - - private void finishUI(JPanel mainPanel) { - Container contentPane = getContentPane(); - contentPane.add(mainPanel); - - setTitle("JADXecute"); - pack(); - - // set modal size - setSize(1024, 768); - - setLocationRelativeTo(mainPanel); - this.setModal(false); - setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); - // setModalityType(ModalityType.APPLICATION_MODAL); - UiUtils.addEscapeShortCutToDispose(this); - } - - private JScrollPane initCodeExamplesListeners(RSyntaxTextArea codeInputArea) { - // Populate code examples - Map keyValuePairs = addCodeExamplesList(); - String[] keyArray = keyValuePairs.keySet().toArray(new String[0]); - JList exampleList = new JList<>(keyArray); - // loop through key-value pair and create action listener for each - for (int i = 0; i < keyArray.length; i++) { - exampleList.addListSelectionListener(event -> { - if (!event.getValueIsAdjusting()) { - codeInputArea.setText(keyValuePairs.get(exampleList.getSelectedValue())); - } - }); - } - - exampleList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - - return new JScrollPane(exampleList); - } - - private JPanel initFilePanel(RSyntaxTextArea codeInputArea) { - JPanel filePanel = new JPanel(); - filePanel.setLayout(new BorderLayout()); - filePanel.setBorder(new EmptyBorder(10, 0, 10, 0)); - - JLabel fileLabel = new JLabel("Input Java File: "); - fileLabel.setFont(new Font(null, Font.BOLD, DEFAULT_FONT_SIZE)); - JTextField fileField = new JTextField(20); - JButton fileButton = new JButton("Browse"); - fileButton.addActionListener(e -> { - JFileChooser fileChooser = new JFileChooser(); - int returnValue = fileChooser.showOpenDialog(null); - if (returnValue == JFileChooser.APPROVE_OPTION) { - File selectedFile = fileChooser.getSelectedFile(); - fileField.setText(selectedFile.getAbsolutePath()); - - // Now read in the file and populate the Java input area - try (BufferedReader br = new BufferedReader(new FileReader(selectedFile))) { - String line; - StringBuilder sb = new StringBuilder(); - while ((line = br.readLine()) != null) { - sb.append(line); - sb.append(System.lineSeparator()); - } - codeInputArea.setText(sb.toString()); - } catch (IOException ex) { - ex.printStackTrace(); - } - } - }); - filePanel.add(fileLabel, BorderLayout.NORTH); - filePanel.add(fileField, BorderLayout.CENTER); - filePanel.add(fileButton, BorderLayout.EAST); - - return filePanel; - } - - private Map addCodeExamplesList() { - String resourceName = "/jadxecute/codeexamples.properties"; - try (InputStream is = JadxecuteDialog.class.getResourceAsStream(resourceName); - BufferedReader reader = new BufferedReader(new InputStreamReader(is))) { - - Map keyValuePairs = new HashMap<>(); - - String line; - String currentKey = null; - StringBuilder currentValue = new StringBuilder(); - - boolean readingValue = false; - - while ((line = reader.readLine()) != null) { - if (line.startsWith("###")) { - // store the current key-value pair - if (currentKey != null && currentValue.length() > 0) { - keyValuePairs.put(currentKey, currentValue.toString()); - currentValue = new StringBuilder(); - } - // update the current key - currentKey = line.substring(3); - } else if ((readingValue || line.startsWith("```")) && currentKey != null) { - if (!readingValue) { - readingValue = true; - } else if(readingValue && line.startsWith("```")) { - readingValue = false; - } else if (readingValue) { - // add the line to the current value - currentValue.append(line + "\n"); - } - } - } - - // store the last key-value pair - if (currentKey != null && currentValue.length() > 0) { - keyValuePairs.put(currentKey, currentValue.toString()); - } - - // print the key-value pairs - for (Map.Entry entry : keyValuePairs.entrySet()) { - LOG.info("Key: " + entry.getKey()); - LOG.info("Value: " + entry.getValue()); - } - - return keyValuePairs; - - } catch (IOException e) { - LOG.error("Failed reading codeexamples.properties file."); - e.printStackTrace(); - } - - return null; - } - - // Set the default text to populate the code input field - private String getDefaultCodeInputText() { - String defaultText = "import jadx.gui.ui.MainWindow;\n" + - "\n" + - "public class UserCodeClass {\n" + - " public static String userCodeMain(MainWindow mainWindow) {\n" + - " String jadxecuteOutput = \"Example output...\";\n" + - "\n" + - " // Your code goes here!\n" + - "\n" + - " return jadxecuteOutput;\n" + - " }\n" + - "}\n"; - return defaultText; - } - - private void runUserCode(RSyntaxTextArea codeInputArea, JTextArea consoleOutputArea, JLabel statusLabel, JButton run) { - statusLabel.setText("Status: Running..."); - statusLabel.setForeground(Color.ORANGE); - - BackgroundExecutor executor = mainWindow.getBackgroundExecutor(); - executor.execute("Jadexecute Task", () -> executeBackgroundTask(codeInputArea, consoleOutputArea, statusLabel), - analysisStatus -> finishTask(consoleOutputArea, statusLabel)); - } - - private void finishTask(JTextArea consoleOutputArea, JLabel statusLabel) { - if (consoleOutputArea.getText().contains("Java compilation error")) { - statusLabel.setText("Status: Error"); - statusLabel.setForeground(Color.RED); - } else { - statusLabel.setText("Status: Done"); - statusLabel.setForeground(Color.GREEN); - } - } - - private void executeBackgroundTask(RSyntaxTextArea codeInputArea, JTextArea consoleOutputArea, JLabel statusLabel) { - String codeInput = codeInputArea.getText(); - - try{ - UserCodeLoader userCodeLoader = new UserCodeLoader(); - consoleOutputArea.setText(userCodeLoader.runInputCode(codeInput, mainWindow)); - } catch (Exception e) { - e.printStackTrace(); - consoleOutputArea.setText("Error running user code"); - statusLabel.setText("Status: Error"); - statusLabel.setForeground(Color.RED); - } - } - - private void close() { - dispose(); - } - - @Override - public void dispose() { - settings.saveWindowPos(this); - super.dispose(); - } -} diff --git a/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/plugins/jadxecute/RenameObjectHelper.java b/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/plugins/jadxecute/RenameObjectHelper.java deleted file mode 100644 index f56b4cf..0000000 --- a/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/plugins/jadxecute/RenameObjectHelper.java +++ /dev/null @@ -1,257 +0,0 @@ -package jadx.gui.plugins.jadxecute; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.function.Consumer; -import java.util.stream.Collectors; - -import org.jetbrains.annotations.NotNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import jadx.api.JavaClass; -import jadx.api.JavaMethod; -import jadx.api.JavaNode; -import jadx.api.JavaVariable; -import jadx.api.data.ICodeRename; -import jadx.api.data.impl.JadxCodeData; -import jadx.api.data.impl.JadxCodeRef; -import jadx.api.data.impl.JadxCodeRename; -import jadx.api.data.impl.JadxNodeRef; -import jadx.core.deobf.NameMapper; -import jadx.core.utils.exceptions.JadxRuntimeException; -import jadx.gui.jobs.TaskStatus; -import jadx.gui.settings.JadxProject; -import jadx.gui.treemodel.JClass; -import jadx.gui.treemodel.JField; -import jadx.gui.treemodel.JMethod; -import jadx.gui.treemodel.JNode; -import jadx.gui.treemodel.JPackage; -import jadx.gui.treemodel.JVariable; -import jadx.gui.ui.MainWindow; -import jadx.gui.ui.TabbedPane; -import jadx.gui.ui.codearea.ClassCodeContentPanel; -import jadx.gui.ui.codearea.CodeArea; -import jadx.gui.ui.panel.ContentPanel; -import jadx.gui.utils.CacheObject; -import jadx.gui.utils.JNodeCache; - -public class RenameObjectHelper { - private static final Logger LOG = LoggerFactory.getLogger(RenameObjectHelper.class); - - private MainWindow mainWindow; - private CacheObject cache; - private JNode source; - private JNode node; - private String newObjectName; - private String oldObjectName; - - // Rename class, method, or field - public String renameObject(MainWindow mainWindow, JNode node, String newObjectName) { - this.oldObjectName = node.getName(); - this.mainWindow = mainWindow; - this.cache = mainWindow.getCacheObject(); - this.source = node; - this.node = replaceNode(node); - this.newObjectName = newObjectName; - rename(newObjectName); - - return "Renamed " + oldObjectName + " to " + newObjectName; - } - - // Need the methods from jadx.gui.ui.dialog.RenameDialog but they - // are private and let's not change all the access modifiers - private JNode replaceNode(JNode node) { - if (node instanceof JMethod) { - JavaMethod javaMethod = ((JMethod) node).getJavaMethod(); - if (javaMethod.isClassInit()) { - throw new JadxRuntimeException("Can't rename class init method: " + node); - } - if (javaMethod.isConstructor()) { - // rename class instead constructor - return node.getJParent(); - } - } - return node; - } - - private void rename(String newName) { - if (!checkNewName()) { - return; - } - try { - updateCodeRenames(set -> processRename(node, newName, set)); - refreshState(); - } catch (Exception e) { - LOG.error("Rename failed", e); - } - } - - private boolean checkNewName() { - String newName = newObjectName; - if (newName.isEmpty()) { - // use empty name to reset rename (revert to original) - return true; - } - - boolean valid = NameMapper.isValidIdentifier(newName); - return valid; - } - - private void processRename(JNode node, String newName, Set renames) { - JadxCodeRename rename = buildRename(node, newName, renames); - renames.remove(rename); - JavaNode javaNode = node.getJavaNode(); - if (javaNode != null) { - javaNode.removeAlias(); - } - if (!newName.isEmpty()) { - renames.add(rename); - } - } - - @NotNull - private JadxCodeRename buildRename(JNode node, String newName, Set renames) { - if (node instanceof JMethod) { - JavaMethod javaMethod = ((JMethod) node).getJavaMethod(); - List relatedMethods = javaMethod.getOverrideRelatedMethods(); - if (!relatedMethods.isEmpty()) { - for (JavaMethod relatedMethod : relatedMethods) { - renames.remove(new JadxCodeRename(JadxNodeRef.forMth(relatedMethod), "")); - } - } - return new JadxCodeRename(JadxNodeRef.forMth(javaMethod), newName); - } - if (node instanceof JField) { - return new JadxCodeRename(JadxNodeRef.forFld(((JField) node).getJavaField()), newName); - } - if (node instanceof JClass) { - return new JadxCodeRename(JadxNodeRef.forCls(((JClass) node).getCls()), newName); - } - if (node instanceof JPackage) { - return new JadxCodeRename(JadxNodeRef.forPkg(((JPackage) node).getFullName()), newName); - } - if (node instanceof JVariable) { - JavaVariable javaVar = ((JVariable) node).getJavaVarNode(); - return new JadxCodeRename(JadxNodeRef.forMth(javaVar.getMth()), JadxCodeRef.forVar(javaVar), newName); - } - throw new JadxRuntimeException("Failed to build rename node for: " + node); - } - - private void updateCodeRenames(Consumer> updater) { - JadxProject project = mainWindow.getProject(); - JadxCodeData codeData = project.getCodeData(); - if (codeData == null) { - codeData = new JadxCodeData(); - } - Set set = new HashSet<>(codeData.getRenames()); - updater.accept(set); - List list = new ArrayList<>(set); - Collections.sort(list); - codeData.setRenames(list); - project.setCodeData(codeData); - mainWindow.getWrapper().reloadCodeData(); - } - - private void refreshState() { - mainWindow.getWrapper().reInitRenameVisitor(); - - JNodeCache nodeCache = cache.getNodeCache(); - JavaNode javaNode = node.getJavaNode(); - - List toUpdate = new ArrayList<>(); - if (source != null && source != node) { - toUpdate.add(source.getJavaNode()); - } - if (javaNode != null) { - toUpdate.add(javaNode); - toUpdate.addAll(javaNode.getUseIn()); - if (node instanceof JMethod) { - toUpdate.addAll(((JMethod) node).getJavaMethod().getOverrideRelatedMethods()); - } - } else if (node instanceof JPackage) { - processPackage(toUpdate); - } else { - throw new JadxRuntimeException("Unexpected node type: " + node); - } - Set updatedTopClasses = toUpdate - .stream() - .map(JavaNode::getTopParentClass) - .map(nodeCache::makeFrom) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); - - LOG.debug("Classes to update: {}", updatedTopClasses); - - refreshTabs(mainWindow.getTabbedPane(), updatedTopClasses); - - if (!updatedTopClasses.isEmpty()) { - mainWindow.getBackgroundExecutor().execute("Refreshing", - () -> refreshClasses(updatedTopClasses), - (status) -> { - if (status == TaskStatus.CANCEL_BY_MEMORY) { - mainWindow.showHeapUsageBar(); - } - if (node instanceof JPackage) { - mainWindow.getTreeRoot().update(); - } - mainWindow.reloadTree(); - }); - } - } - - private void processPackage(List toUpdate) { - String rawFullPkg = ((JPackage) node).getFullName(); - String rawFullPkgDot = rawFullPkg + "."; - for (JavaClass cls : mainWindow.getWrapper().getClasses()) { - String clsPkg = cls.getClassNode().getClassInfo().getPackage(); - // search all classes in package - if (clsPkg.equals(rawFullPkg) || clsPkg.startsWith(rawFullPkgDot)) { - toUpdate.add(cls); - // also include all usages (for import fix) - toUpdate.addAll(cls.getUseIn()); - } - } - } - - private void refreshClasses(Set updatedTopClasses) { - if (updatedTopClasses.size() < 10) { - // small batch => reload - LOG.debug("Classes to reload: {}", updatedTopClasses.size()); - for (JClass cls : updatedTopClasses) { - try { - cls.reload(cache); - } catch (Exception e) { - LOG.error("Failed to reload class: {}", cls.getFullName(), e); - } - } - } else { - // big batch => unload - LOG.debug("Classes to unload: {}", updatedTopClasses.size()); - for (JClass cls : updatedTopClasses) { - try { - cls.unload(cache); - } catch (Exception e) { - LOG.error("Failed to unload class: {}", cls.getFullName(), e); - } - } - } - } - - private void refreshTabs(TabbedPane tabbedPane, Set updatedClasses) { - for (Map.Entry entry : tabbedPane.getOpenTabs().entrySet()) { - JClass rootClass = entry.getKey().getRootClass(); - if (updatedClasses.remove(rootClass)) { - ClassCodeContentPanel contentPanel = (ClassCodeContentPanel) entry.getValue(); - CodeArea codeArea = (CodeArea) contentPanel.getJavaCodePanel().getCodeArea(); - codeArea.refreshClass(); - } - } - } -} - diff --git a/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/plugins/jadxecute/UserCodeLoader.java b/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/plugins/jadxecute/UserCodeLoader.java deleted file mode 100644 index cceae5c..0000000 --- a/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/plugins/jadxecute/UserCodeLoader.java +++ /dev/null @@ -1,127 +0,0 @@ -package jadx.gui.plugins.jadxecute; - -import javax.tools.*; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.lang.reflect.Method; -import java.net.URI; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Iterator; -import java.util.List; - -import jadx.gui.ui.MainWindow; - -public class UserCodeLoader { - public String runInputCode(String program, MainWindow mainWindow) throws Exception { - // Compile the input code - JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); - DiagnosticCollector diagnostics = new DiagnosticCollector<>(); - JavaFileObject compilationUnit = new StringJavaFileObject("UserCodeClass", program); - SimpleJavaFileManager fileManager = new SimpleJavaFileManager(compiler.getStandardFileManager(diagnostics, null, null)); - JavaCompiler.CompilationTask compilationTask = compiler.getTask(null, fileManager, diagnostics, null, null, Arrays.asList(compilationUnit)); - boolean success = compilationTask.call(); - CompiledClassLoader classLoader = new CompiledClassLoader(fileManager.getGeneratedOutputFiles()); - - // Check for compilation errors - if (!success) { - // Append error message to a string - // Need to using html tags for JPanel to respect newlines - String errorMessage = "Java compilation error:\n"; - for (Diagnostic diagnostic : diagnostics.getDiagnostics()) { - errorMessage += diagnostic.getMessage(null) + "\n"; - } - return errorMessage; - } - - // Input code must implement method "public static String userCodeMain(MainWindow mainWindow)" - // Return string will be console output - // This method can invoke other methods of user code placed inside - Class userCodeClass = classLoader.loadClass("UserCodeClass"); - Method method = userCodeClass.getMethod("userCodeMain", MainWindow.class); - - return (String) method.invoke(null, mainWindow); - } - - private static class StringJavaFileObject extends SimpleJavaFileObject { - private final String code; - - public StringJavaFileObject(String name, String code) { - super(URI.create("string:///" + name.replace('.', '/') + Kind.SOURCE.extension), - Kind.SOURCE); - this.code = code; - } - - @Override - public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { - return code; - } - } - - private static class ClassJavaFileObject extends SimpleJavaFileObject { - private final ByteArrayOutputStream outputStream; - private final String className; - - protected ClassJavaFileObject(String className, Kind kind) { - super(URI.create("mem:///" + className.replace('.', '/') + kind.extension), kind); - this.className = className; - outputStream = new ByteArrayOutputStream(); - } - - @Override - public OutputStream openOutputStream() throws IOException { - return outputStream; - } - - public byte[] getBytes() { - return outputStream.toByteArray(); - } - - public String getClassName() { - return className; - } - } - - private static class SimpleJavaFileManager extends ForwardingJavaFileManager { - private final List outputFiles; - - protected SimpleJavaFileManager(JavaFileManager fileManager) { - super(fileManager); - outputFiles = new ArrayList(); - } - - @Override - public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException { - ClassJavaFileObject file = new ClassJavaFileObject(className, kind); - outputFiles.add(file); - return file; - } - - public List getGeneratedOutputFiles() { - return outputFiles; - } - } - - private static class CompiledClassLoader extends ClassLoader { - private final List files; - - private CompiledClassLoader(List files) { - this.files = files; - } - - @Override - protected Class findClass(String name) throws ClassNotFoundException { - Iterator itr = files.iterator(); - while (itr.hasNext()) { - ClassJavaFileObject file = itr.next(); - if (file.getClassName().equals(name)) { - itr.remove(); - byte[] bytes = file.getBytes(); - return super.defineClass(name, bytes, 0, bytes.length); - } - } - return super.findClass(name); - } - } -} \ No newline at end of file diff --git a/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/search/SearchSettings.java b/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/search/SearchSettings.java index a9139f7..b2d4bcf 100644 --- a/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/search/SearchSettings.java +++ b/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/search/SearchSettings.java @@ -5,6 +5,7 @@ import org.jetbrains.annotations.Nullable; import jadx.gui.treemodel.JClass; +import jadx.gui.treemodel.JResource; public class SearchSettings { @@ -13,6 +14,7 @@ public class SearchSettings { private final boolean ignoreCase; private JClass activeCls; + private JResource activeResource; private Pattern regexPattern; private ISearchMethod searchMethod; @@ -64,6 +66,14 @@ public void setActiveCls(JClass activeCls) { this.activeCls = activeCls; } + public JResource getActiveResource() { + return activeResource; + } + + public void setActiveResource(JResource activeResource) { + this.activeResource = activeResource; + } + public ISearchMethod getSearchMethod() { return searchMethod; } diff --git a/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/search/providers/ResourceSearchProvider.java b/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/search/providers/ResourceSearchProvider.java index ecb9b20..c9b3115 100644 --- a/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/search/providers/ResourceSearchProvider.java +++ b/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/search/providers/ResourceSearchProvider.java @@ -1,6 +1,7 @@ package jadx.gui.search.providers; import java.util.ArrayDeque; +import java.util.Collections; import java.util.Deque; import java.util.Enumeration; import java.util.HashSet; @@ -24,12 +25,15 @@ import jadx.gui.treemodel.JResource; import jadx.gui.treemodel.JRoot; import jadx.gui.ui.MainWindow; +import jadx.gui.ui.dialog.SearchDialog; +import jadx.gui.utils.NLS; public class ResourceSearchProvider implements ISearchProvider { private static final Logger LOG = LoggerFactory.getLogger(ResourceSearchProvider.class); private final SearchSettings searchSettings; private final Set extSet; + private final SearchDialog searchDialog; private final int sizeLimit; private boolean anyExt; @@ -39,11 +43,20 @@ public class ResourceSearchProvider implements ISearchProvider { private final Deque resQueue; private int pos; - public ResourceSearchProvider(MainWindow mw, SearchSettings searchSettings) { + private int loadErrors = 0; + private int skipBySize = 0; + + public ResourceSearchProvider(MainWindow mw, SearchSettings searchSettings, SearchDialog searchDialog) { this.searchSettings = searchSettings; this.sizeLimit = mw.getSettings().getSrhResourceSkipSize() * 1048576; this.extSet = buildAllowedFilesExtensions(mw.getSettings().getSrhResourceFileExt()); - this.resQueue = initResQueue(mw); + this.searchDialog = searchDialog; + JResource activeResource = searchSettings.getActiveResource(); + if (activeResource != null) { + this.resQueue = new ArrayDeque<>(Collections.singleton(activeResource)); + } else { + this.resQueue = initResQueue(mw); + } } @Override @@ -93,32 +106,49 @@ private JNode search(JResource resNode) { private @Nullable JResource getNextResFile(Cancelable cancelable) { while (true) { JResource node = resQueue.peekLast(); - if (node == null) { - return null; - } - try { - node.loadNode(); - } catch (Exception e) { - LOG.error("Error load resource node: {}", node, e); - resQueue.removeLast(); - continue; - } - if (cancelable.isCanceled()) { + if (node == null || cancelable.isCanceled()) { return null; } if (node.getType() == JResource.JResType.FILE) { - if (shouldProcess(node)) { + if (shouldProcess(node) && loadResNode(node)) { return node; } resQueue.removeLast(); } else { // dir resQueue.removeLast(); + loadResNode(node); addChildren(node); } } } + private void updateProgressInfo() { + StringBuilder sb = new StringBuilder(); + if (loadErrors != 0) { + sb.append(" ").append(NLS.str("search_dialog.resources_load_errors", loadErrors)); + } + if (skipBySize != 0) { + sb.append(" ").append(NLS.str("search_dialog.resources_skip_by_size", skipBySize)); + } + if (sb.length() != 0) { + sb.append(" ").append(NLS.str("search_dialog.resources_check_logs")); + } + searchDialog.updateProgressLabel(sb.toString()); + } + + private boolean loadResNode(JResource node) { + try { + node.loadNode(); + return true; + } catch (Exception e) { + LOG.error("Error load resource node: {}", node, e); + loadErrors++; + updateProgressInfo(); + return false; + } + } + private void addChildren(JResource resNode) { resQueue.addAll(resNode.getSubNodes()); } @@ -167,19 +197,24 @@ private boolean shouldProcess(JResource resNode) { return false; } } - if (sizeLimit == 0) { + if (sizeLimit <= 0) { return true; } try { int charsCount = resNode.getCodeInfo().getCodeStr().length(); long size = charsCount * 8L; if (size > sizeLimit) { - LOG.debug("Resource search skipped because of size limit: {} res size {} bytes", resNode, size); + LOG.info("Resource search skipped because of size limit. Resource '{}' size {} bytes, limit: {}", + resNode.getName(), size, sizeLimit); + skipBySize++; + updateProgressInfo(); return false; } return true; } catch (Exception e) { LOG.warn("Resource load error: {}", resNode, e); + loadErrors++; + updateProgressInfo(); return false; } } diff --git a/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java b/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java index 9876e2d..6a8359a 100644 --- a/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java +++ b/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java @@ -401,6 +401,10 @@ public void setInlineMethods(boolean inlineMethods) { this.inlineMethods = inlineMethods; } + public void setAllowInlineKotlinLambda(boolean allowInlineKotlinLambda) { + this.allowInlineKotlinLambda = allowInlineKotlinLambda; + } + public void setExtractFinally(boolean extractFinally) { this.extractFinally = extractFinally; } diff --git a/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java b/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java index c1702ec..ab748e0 100644 --- a/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java +++ b/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java @@ -538,6 +538,13 @@ private SettingsGroup makeDecompilationGroup() { needReload(); }); + JCheckBox inlineKotlinLambdas = new JCheckBox(); + inlineKotlinLambdas.setSelected(settings.isAllowInlineKotlinLambda()); + inlineKotlinLambdas.addItemListener(e -> { + settings.setAllowInlineKotlinLambda(e.getStateChange() == ItemEvent.SELECTED); + needReload(); + }); + JCheckBox extractFinally = new JCheckBox(); extractFinally.setSelected(settings.isExtractFinally()); extractFinally.addItemListener(e -> { @@ -581,6 +588,7 @@ private SettingsGroup makeDecompilationGroup() { other.addRow(NLS.str("preferences.useDebugInfo"), useDebugInfo); other.addRow(NLS.str("preferences.inlineAnonymous"), inlineAnonymous); other.addRow(NLS.str("preferences.inlineMethods"), inlineMethods); + other.addRow(NLS.str("preferences.inlineKotlinLambdas"), inlineKotlinLambdas); other.addRow(NLS.str("preferences.extractFinally"), extractFinally); other.addRow(NLS.str("preferences.fsCaseSensitive"), fsCaseSensitive); other.addRow(NLS.str("preferences.useDx"), useDx); diff --git a/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java b/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java index 7e287ae..9f4b51b 100644 --- a/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java +++ b/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java @@ -95,11 +95,9 @@ import jadx.gui.jobs.BackgroundExecutor; import jadx.gui.jobs.DecompileTask; import jadx.gui.jobs.ExportTask; -import jadx.gui.jobs.ProcessResult; import jadx.gui.jobs.TaskStatus; import jadx.gui.plugins.mappings.MappingExporter; import jadx.gui.plugins.quark.QuarkDialog; -import jadx.gui.plugins.jadxecute.JadxecuteDialog; import jadx.gui.settings.JadxProject; import jadx.gui.settings.JadxSettings; import jadx.gui.settings.JadxSettingsWindow; @@ -172,9 +170,9 @@ public class MainWindow extends JFrame { private static final ImageIcon ICON_BACK = UiUtils.openSvgIcon("ui/left"); private static final ImageIcon ICON_FORWARD = UiUtils.openSvgIcon("ui/right"); private static final ImageIcon ICON_QUARK = UiUtils.openSvgIcon("ui/quark"); - private static final ImageIcon ICON_JADXECUTE = UiUtils.openSvgIcon("ui/jadxecute"); private static final ImageIcon ICON_PREF = UiUtils.openSvgIcon("ui/settings"); private static final ImageIcon ICON_DEOBF = UiUtils.openSvgIcon("ui/helmChartLock"); + private static final ImageIcon ICON_DECOMPILE_ALL = UiUtils.openSvgIcon("ui/runAll"); private static final ImageIcon ICON_LOG = UiUtils.openSvgIcon("ui/logVerbose"); private static final ImageIcon ICON_INFO = UiUtils.openSvgIcon("ui/showInfos"); private static final ImageIcon ICON_DEBUGGER = UiUtils.openSvgIcon("ui/startDebugger"); @@ -610,54 +608,17 @@ synchronized void runInitialBackgroundJobs() { new Timer().schedule(new TimerTask() { @Override public void run() { - waitDecompileTask(); + requestFullDecompilation(); } }, 1000); } } - private static final Object DECOMPILER_TASK_SYNC = new Object(); - - public void waitDecompileTask() { - synchronized (DECOMPILER_TASK_SYNC) { - try { - DecompileTask decompileTask = new DecompileTask(wrapper); - backgroundExecutor.executeAndWait(decompileTask); - backgroundExecutor.execute(decompileTask.getTitle(), wrapper::unloadClasses).get(); - processDecompilationResults(decompileTask.getResult()); - System.gc(); - } catch (Exception e) { - LOG.error("Decompile task execution failed", e); - } - } - } - - private void processDecompilationResults(ProcessResult decompile) { - int skippedCls = decompile.getSkipped(); - if (skippedCls == 0) { + public void requestFullDecompilation() { + if (cacheObject.isFullDecompilationFinished()) { return; } - TaskStatus status = decompile.getStatus(); - LOG.warn("Decompile and indexing of some classes skipped: {}, status: {}", skippedCls, status); - switch (status) { - case CANCEL_BY_USER: { - String reason = NLS.str("message.userCancelTask"); - String message = NLS.str("message.indexIncomplete", reason, skippedCls); - JOptionPane.showMessageDialog(this, message); - break; - } - case CANCEL_BY_TIMEOUT: { - String reason = NLS.str("message.taskTimeout", decompile.getTimeLimit()); - String message = NLS.str("message.indexIncomplete", reason, skippedCls); - JOptionPane.showMessageDialog(this, message); - break; - } - case CANCEL_BY_MEMORY: { - showHeapUsageBar(); - JOptionPane.showMessageDialog(this, NLS.str("message.indexingClassesSkipped", skippedCls)); - break; - } - } + backgroundExecutor.execute(new DecompileTask(this)); } public void cancelBackgroundJobs() { @@ -1043,6 +1004,10 @@ public void actionPerformed(ActionEvent e) { commentSearchAction.putValue(Action.ACCELERATOR_KEY, getKeyStroke(KeyEvent.VK_SEMICOLON, UiUtils.ctrlButton() | KeyEvent.SHIFT_DOWN_MASK)); + ActionHandler decompileAllAction = new ActionHandler(ev -> requestFullDecompilation()); + decompileAllAction.setNameAndDesc(NLS.str("menu.decompile_all")); + decompileAllAction.setIcon(ICON_DECOMPILE_ALL); + Action deobfAction = new AbstractAction(NLS.str("menu.deobfuscation"), ICON_DEOBF) { @Override public void actionPerformed(ActionEvent e) { @@ -1103,14 +1068,6 @@ public void actionPerformed(ActionEvent e) { }; quarkAction.putValue(Action.SHORT_DESCRIPTION, "Quark Engine"); - Action jadxecuteAction = new AbstractAction("JADXecute", ICON_JADXECUTE) { - @Override - public void actionPerformed(ActionEvent e) { - new JadxecuteDialog(MainWindow.this).setVisible(true); - } - }; - jadxecuteAction.putValue(Action.SHORT_DESCRIPTION, "JADXecute"); - Action openDeviceAction = new AbstractAction(NLS.str("debugger.process_selector"), ICON_DEBUGGER) { @Override public void actionPerformed(ActionEvent e) { @@ -1162,9 +1119,9 @@ public void actionPerformed(ActionEvent e) { JMenu tools = new JMenu(NLS.str("menu.tools")); tools.setMnemonic(KeyEvent.VK_T); + tools.add(decompileAllAction); tools.add(deobfMenuItem); tools.add(quarkAction); - tools.add(jadxecuteAction); tools.add(openDeviceAction); JMenu help = new JMenu(NLS.str("menu.help")); @@ -1220,7 +1177,6 @@ public void actionPerformed(ActionEvent e) { toolbar.addSeparator(); toolbar.add(deobfToggleBtn); toolbar.add(quarkAction); - toolbar.add(jadxecuteAction); toolbar.add(openDeviceAction); toolbar.addSeparator(); toolbar.add(logAction); @@ -1243,9 +1199,9 @@ public void actionPerformed(ActionEvent e) { exportAction.setEnabled(loaded); saveProjectAsAction.setEnabled(loaded); reload.setEnabled(loaded); + decompileAllAction.setEnabled(loaded); deobfAction.setEnabled(loaded); quarkAction.setEnabled(loaded); - jadxecuteAction.setEnabled(loaded); return false; }); } diff --git a/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/ui/dialog/ADBDialog.java b/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/ui/dialog/ADBDialog.java index 991774d..c3eff17 100644 --- a/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/ui/dialog/ADBDialog.java +++ b/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/ui/dialog/ADBDialog.java @@ -475,28 +475,28 @@ public void jdwpProcessOccurred(ADBDevice device, Set id) { LOG.error("Failed to find device", e); return; } - node.tNode.removeAllChildren(); - DefaultMutableTreeNode tempNode = null; - for (String s : procList) { - DefaultMutableTreeNode pnode = new DefaultMutableTreeNode(s); - node.tNode.add(pnode); - if (!debugSetter.expectPkg.isEmpty() && s.endsWith(debugSetter.expectPkg)) { - if (debugSetter.autoAttachPkg && debugSetter.device.equals(node.device)) { - debugSetter.set(node.device, debugSetter.ver, getPid(s), s); - if (attachProcess(mainWindow)) { - dispose(); - return; + + SwingUtilities.invokeLater(() -> { + node.tNode.removeAllChildren(); + DefaultMutableTreeNode foundNode = null; + for (String procStr : procList) { + DefaultMutableTreeNode pnode = new DefaultMutableTreeNode(procStr); + node.tNode.add(pnode); + if (!debugSetter.expectPkg.isEmpty() && procStr.endsWith(debugSetter.expectPkg)) { + if (debugSetter.autoAttachPkg && debugSetter.device.equals(node.device)) { + debugSetter.set(node.device, debugSetter.ver, getPid(procStr), procStr); + if (attachProcess(mainWindow)) { + dispose(); + return; + } } + foundNode = pnode; } - tempNode = pnode; } - } - DefaultMutableTreeNode theNode = tempNode; - SwingUtilities.invokeLater(() -> { procTreeModel.reload(node.tNode); procTree.expandPath(new TreePath(node.tNode.getPath())); - if (theNode != null) { - TreePath thePath = new TreePath(theNode.getPath()); + if (foundNode != null) { + TreePath thePath = new TreePath(foundNode.getPath()); procTree.scrollPathToVisible(thePath); procTree.setSelectionPath(thePath); } diff --git a/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/ui/dialog/CommentDialog.java b/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/ui/dialog/CommentDialog.java index 963539b..88f0c2a 100644 --- a/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/ui/dialog/CommentDialog.java +++ b/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/ui/dialog/CommentDialog.java @@ -62,11 +62,6 @@ private static void updateCommentsData(CodeArea codeArea, Consumer list = new ArrayList<>(codeData.getComments()); updater.accept(list); - for (ICodeComment comment : list) { - LOG.info("Comment: " + comment.getComment()); - LOG.info("getNodeRef: " + comment.getNodeRef()); - LOG.info("getCodeRef: " + comment.getCodeRef()); - } Collections.sort(list); codeData.setComments(list); project.setCodeData(codeData); diff --git a/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/ui/dialog/CommonSearchDialog.java b/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/ui/dialog/CommonSearchDialog.java index 5c685ea..32ae9e6 100644 --- a/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/ui/dialog/CommonSearchDialog.java +++ b/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/ui/dialog/CommonSearchDialog.java @@ -17,6 +17,7 @@ import java.util.Collections; import java.util.Enumeration; import java.util.List; +import java.util.Objects; import javax.swing.AbstractAction; import javax.swing.BorderFactory; @@ -44,6 +45,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import ch.qos.logback.classic.Level; + +import jadx.api.JavaClass; +import jadx.api.metadata.ICodeAnnotation; +import jadx.api.metadata.annotations.NodeDeclareRef; +import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JNode; import jadx.gui.treemodel.JResSearchNode; import jadx.gui.ui.MainWindow; @@ -73,6 +80,7 @@ public abstract class CommonSearchDialog extends JFrame { protected ResultsModel resultsModel; protected ResultsTable resultsTable; protected JLabel resultsInfoLabel; + protected JLabel progressInfoLabel; protected JLabel warnLabel; protected ProgressPanel progressPane; @@ -142,13 +150,45 @@ protected void openItem(JNode node) { JumpPosition jmpPos = new JumpPosition(((JResSearchNode) node).getResNode(), node.getPos()); tabbedPane.codeJump(jmpPos); } else { - tabbedPane.codeJump(node); + if (!checkForRedirects(node)) { + tabbedPane.codeJump(node); + } } if (!mainWindow.getSettings().getKeepCommonDialogOpen()) { dispose(); } } + // TODO: temp solution, move implementation into corresponding nodes + private boolean checkForRedirects(JNode node) { + if (node instanceof JClass) { + JavaClass cls = ((JClass) node).getCls(); + JavaClass origTopCls = cls.getOriginalTopParentClass(); + JavaClass codeParent = cls.getTopParentClass(); + if (Objects.equals(codeParent, origTopCls)) { + return false; + } + JClass jumpCls = mainWindow.getCacheObject().getNodeCache().makeFrom(codeParent); + mainWindow.getBackgroundExecutor().execute( + NLS.str("progress.load"), + jumpCls::loadNode, // load code in background + status -> { + // search original node in jump class + codeParent.getCodeInfo().getCodeMetadata().searchDown(0, (pos, ann) -> { + if (ann.getAnnType() == ICodeAnnotation.AnnType.DECLARATION) { + if (((NodeDeclareRef) ann).getNode().equals(cls.getClassNode())) { + tabbedPane.codeJump(new JumpPosition(jumpCls, pos)); + return true; + } + } + return null; + }); + }); + return true; + } + return false; + } + @Nullable private JNode getSelectedNode() { try { @@ -260,6 +300,15 @@ public void actionPerformed(ActionEvent e) { resultsInfoLabel = new JLabel(""); resultsInfoLabel.setFont(mainWindow.getSettings().getFont()); + progressInfoLabel = new JLabel(""); + progressInfoLabel.setFont(mainWindow.getSettings().getFont()); + progressInfoLabel.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + LogViewerDialog.openWithLevel(mainWindow, Level.INFO); + } + }); + JPanel resultsActionsPanel = new JPanel(); resultsActionsPanel.setLayout(new BoxLayout(resultsActionsPanel, BoxLayout.LINE_AXIS)); resultsActionsPanel.setBorder(BorderFactory.createEmptyBorder(10, 0, 10, 0)); @@ -276,6 +325,8 @@ public void actionPerformed(ActionEvent e) { protected void addResultsActions(JPanel resultsActionsPanel) { resultsActionsPanel.add(Box.createRigidArea(new Dimension(20, 0))); resultsActionsPanel.add(resultsInfoLabel); + resultsActionsPanel.add(Box.createRigidArea(new Dimension(20, 0))); + resultsActionsPanel.add(progressInfoLabel); resultsActionsPanel.add(Box.createHorizontalGlue()); } diff --git a/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/ui/dialog/SearchDialog.java b/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/ui/dialog/SearchDialog.java index 3b384aa..dd78b2e 100644 --- a/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/ui/dialog/SearchDialog.java +++ b/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/ui/dialog/SearchDialog.java @@ -55,6 +55,7 @@ import jadx.gui.search.providers.ResourceSearchProvider; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JNode; +import jadx.gui.treemodel.JResource; import jadx.gui.ui.MainWindow; import jadx.gui.utils.JumpPosition; import jadx.gui.utils.NLS; @@ -452,9 +453,18 @@ private boolean buildSearch(SearchTask newSearchTask, String text, SearchSetting resultsInfoLabel.setText("Can't search in current tab"); return false; } - JClass activeCls = currentPos.getNode().getRootClass(); - searchSettings.setActiveCls(activeCls); - allClasses = Collections.singletonList(activeCls.getCls()); + JNode currentNode = currentPos.getNode(); + if (currentNode instanceof JClass) { + JClass activeCls = currentNode.getRootClass(); + searchSettings.setActiveCls(activeCls); + allClasses = Collections.singletonList(activeCls.getCls()); + } else if (currentNode instanceof JResource) { + searchSettings.setActiveResource((JResource) currentNode); + allClasses = Collections.emptyList(); + } else { + resultsInfoLabel.setText("Can't search in current tab"); + return false; + } } else { allClasses = mainWindow.getWrapper().getIncludedClassesWithInners(); } @@ -475,9 +485,10 @@ private boolean buildSearch(SearchTask newSearchTask, String text, SearchSetting merged.add(new FieldSearchProvider(mainWindow, searchSettings, allClasses)); } if (options.contains(CODE)) { - if (allClasses.size() == 1) { + int clsCount = allClasses.size(); + if (clsCount == 1) { newSearchTask.addProviderJob(new CodeSearchProvider(mainWindow, searchSettings, allClasses)); - } else { + } else if (clsCount > 1) { List> batches = mainWindow.getCacheObject().getDecompileBatches(); if (batches == null) { List topClasses = ListUtils.filter(allClasses, c -> !c.isInner()); @@ -490,7 +501,7 @@ private boolean buildSearch(SearchTask newSearchTask, String text, SearchSetting } } if (options.contains(RESOURCE)) { - newSearchTask.addProviderJob(new ResourceSearchProvider(mainWindow, searchSettings)); + newSearchTask.addProviderJob(new ResourceSearchProvider(mainWindow, searchSettings, this)); } if (options.contains(COMMENT)) { newSearchTask.addProviderJob(new CommentSearchProvider(mainWindow, searchSettings)); @@ -549,6 +560,7 @@ private void resetSearch() { synchronized (pendingResults) { pendingResults.clear(); } + updateProgressLabel(""); progressPane.setVisible(false); warnLabel.setVisible(false); loadAllButton.setEnabled(false); @@ -598,6 +610,10 @@ private void updateProgress(ITaskProgress progress) { }); } + public void updateProgressLabel(String text) { + UiUtils.uiRun(() -> progressInfoLabel.setText(text)); + } + private void searchFinished(ITaskInfo status, Boolean complete) { UiUtils.uiThreadGuard(); LOG.debug("Search complete: {}, complete: {}", status, complete); diff --git a/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/ui/dialog/UsageDialog.java b/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/ui/dialog/UsageDialog.java index 57014b2..084583d 100644 --- a/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/ui/dialog/UsageDialog.java +++ b/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/ui/dialog/UsageDialog.java @@ -19,11 +19,14 @@ import jadx.api.JavaMethod; import jadx.api.JavaNode; import jadx.api.utils.CodeUtils; +import jadx.core.dex.info.ConstStorage; +import jadx.core.dex.nodes.FieldNode; import jadx.gui.JadxWrapper; import jadx.gui.jobs.TaskStatus; import jadx.gui.settings.JadxSettings; import jadx.gui.treemodel.CodeNode; import jadx.gui.treemodel.JClass; +import jadx.gui.treemodel.JField; import jadx.gui.treemodel.JMethod; import jadx.gui.treemodel.JNode; import jadx.gui.ui.MainWindow; @@ -51,6 +54,7 @@ public UsageDialog(MainWindow mainWindow, JNode node) { @Override protected void openInit() { progressStartCommon(); + prepareUsageData(); mainWindow.getBackgroundExecutor().execute(NLS.str("progress.load"), this::collectUsageData, (status) -> { @@ -63,26 +67,39 @@ protected void openInit() { }); } + private void prepareUsageData() { + if (mainWindow.getSettings().isReplaceConsts() && node instanceof JField) { + FieldNode fld = ((JField) node).getJavaField().getFieldNode(); + boolean constField = ConstStorage.getFieldConstValue(fld) != null; + if (constField && !fld.getAccessFlags().isPrivate()) { + // run full decompilation to prepare for full code scan + mainWindow.requestFullDecompilation(); + } + } + } + private void collectUsageData() { usageList = new ArrayList<>(); - Map> usageQuery = buildUsageQuery(); - usageQuery.forEach((searchNode, useNodes) -> useNodes.stream() - .map(JavaNode::getTopParentClass) - .distinct() - .forEach(u -> processUsage(searchNode, u))); + buildUsageQuery().forEach( + (searchNode, useNodes) -> useNodes.stream() + .map(JavaNode::getTopParentClass) + .distinct() + .forEach(u -> processUsage(searchNode, u))); } /** * Return mapping of 'node to search' to 'use places' */ - private Map> buildUsageQuery() { - Map> map = new HashMap<>(); + private Map> buildUsageQuery() { + Map> map = new HashMap<>(); if (node instanceof JMethod) { JavaMethod javaMethod = ((JMethod) node).getJavaMethod(); for (JavaMethod mth : getMethodWithOverrides(javaMethod)) { map.put(mth, mth.getUseIn()); } - } else if (node instanceof JClass) { + return map; + } + if (node instanceof JClass) { JavaClass javaCls = ((JClass) node).getCls(); map.put(javaCls, javaCls.getUseIn()); // add constructors usage into class usage @@ -91,10 +108,19 @@ private Map> buildUsageQuery() { map.put(javaMth, javaMth.getUseIn()); } } - } else { - JavaNode javaNode = node.getJavaNode(); - map.put(javaNode, javaNode.getUseIn()); + return map; } + if (node instanceof JField && mainWindow.getSettings().isReplaceConsts()) { + FieldNode fld = ((JField) node).getJavaField().getFieldNode(); + boolean constField = ConstStorage.getFieldConstValue(fld) != null; + if (constField && !fld.getAccessFlags().isPrivate()) { + // search all classes to collect usage of replaced constants + map.put(fld.getJavaNode(), mainWindow.getWrapper().getIncludedClasses()); + return map; + } + } + JavaNode javaNode = node.getJavaNode(); + map.put(javaNode, javaNode.getUseIn()); return map; } @@ -108,9 +134,12 @@ private List getMethodWithOverrides(JavaMethod javaMethod) { private void processUsage(JavaNode searchNode, JavaClass topUseClass) { ICodeInfo codeInfo = topUseClass.getCodeInfo(); + List usePositions = topUseClass.getUsePlacesFor(codeInfo, searchNode); + if (usePositions.isEmpty()) { + return; + } String code = codeInfo.getCodeStr(); JadxWrapper wrapper = mainWindow.getWrapper(); - List usePositions = topUseClass.getUsePlacesFor(codeInfo, searchNode); for (int pos : usePositions) { String line = CodeUtils.getLineForPos(code, pos); if (line.startsWith("import ")) { diff --git a/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/ui/panel/JDebuggerPanel.java b/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/ui/panel/JDebuggerPanel.java index 5aef0fd..d98006f 100644 --- a/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/ui/panel/JDebuggerPanel.java +++ b/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/ui/panel/JDebuggerPanel.java @@ -88,6 +88,7 @@ public class JDebuggerPanel extends JPanel { private transient KeyEventDispatcher controllerShortCutDispatcher; public JDebuggerPanel(MainWindow mainWindow) { + UiUtils.uiThreadGuard(); this.mainWindow = mainWindow; controller = new DebugController(); this.setLayout(new BorderLayout()); @@ -287,24 +288,26 @@ public void actionPerformed(ActionEvent e) { @Override public void onStateChanged(boolean suspended, boolean stopped) { - if (!stopped) { - if (isGray) { - stop.putValue(Action.SMALL_ICON, ICON_STOP); + UiUtils.uiRun(() -> { + if (!stopped) { + if (isGray) { + stop.putValue(Action.SMALL_ICON, ICON_STOP); + } + } else { + stop.putValue(Action.SMALL_ICON, ICON_STOP_GRAY); + run.putValue(Action.SMALL_ICON, ICON_RUN); + run.putValue(Action.SHORT_DESCRIPTION, NLS.str("debugger.run")); + isGray = true; + return; } - } else { - stop.putValue(Action.SMALL_ICON, ICON_STOP_GRAY); - run.putValue(Action.SMALL_ICON, ICON_RUN); - run.putValue(Action.SHORT_DESCRIPTION, NLS.str("debugger.run")); - isGray = true; - return; - } - if (suspended) { - run.putValue(Action.SMALL_ICON, ICON_RUN); - run.putValue(Action.SHORT_DESCRIPTION, NLS.str("debugger.run")); - } else { - run.putValue(Action.SMALL_ICON, ICON_PAUSE); - run.putValue(Action.SHORT_DESCRIPTION, NLS.str("debugger.pause")); - } + if (suspended) { + run.putValue(Action.SMALL_ICON, ICON_RUN); + run.putValue(Action.SHORT_DESCRIPTION, NLS.str("debugger.run")); + } else { + run.putValue(Action.SMALL_ICON, ICON_PAUSE); + run.putValue(Action.SHORT_DESCRIPTION, NLS.str("debugger.pause")); + } + }); } }); @@ -387,16 +390,18 @@ private void stackFrameSelected(Point p) { public boolean showDebugger(String procName, String host, int port, int androidVer, ADBDevice device, String pid) { boolean ok = controller.startDebugger(this, host, port, androidVer); if (ok) { - log(String.format("Attached %s %s:%d", procName, host, port)); - try { - logcatPanel.init(device, pid); - } catch (Exception e) { - log(NLS.str("logcat.error_fail_start")); - LOG.error("Logcat failed to start", e); - } - leftSplitter.setDividerLocation(mainWindow.getSettings().getDebuggerStackFrameSplitterLoc()); - rightSplitter.setDividerLocation(mainWindow.getSettings().getDebuggerVarTreeSplitterLoc()); - mainWindow.showDebuggerPanel(); + UiUtils.uiRun(() -> { + log(String.format("Attached %s %s:%d", procName, host, port)); + try { + logcatPanel.init(device, pid); + } catch (Exception e) { + log(NLS.str("logcat.error_fail_start")); + LOG.error("Logcat failed to start", e); + } + leftSplitter.setDividerLocation(mainWindow.getSettings().getDebuggerStackFrameSplitterLoc()); + rightSplitter.setDividerLocation(mainWindow.getSettings().getDebuggerVarTreeSplitterLoc()); + mainWindow.showDebuggerPanel(); + }); } return ok; } @@ -414,6 +419,8 @@ public int getRightSplitterLocation() { } public void loadSettings() { + UiUtils.uiThreadGuard(); + Font font = mainWindow.getSettings().getFont(); variableTree.setFont(font.deriveFont(font.getSize() + 1.f)); variableTree.setRowHeight(-1); @@ -423,6 +430,8 @@ public void loadSettings() { } public void resetUI() { + UiUtils.uiThreadGuard(); + thisTreeNode.removeAllChildren(); regTreeNode.removeAllChildren(); @@ -464,12 +473,11 @@ public void updateThisFieldNodes(List nodes) { } public void refreshThreadBox(List elements) { - if (elements.size() > 0) { - DefaultComboBoxModel model = - (DefaultComboBoxModel) threadBox.getModel(); - elements.forEach(model::addElement); - } - SwingUtilities.invokeLater(() -> { + UiUtils.uiRun(() -> { + if (!elements.isEmpty()) { + DefaultComboBoxModel model = (DefaultComboBoxModel) threadBox.getModel(); + elements.forEach(model::addElement); + } threadBox.updateUI(); stackFrameList.setFont(mainWindow.getSettings().getFont()); }); diff --git a/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java b/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java index dabee1e..7eb5885 100644 --- a/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java +++ b/jadx-with-jadxecute/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java @@ -18,6 +18,8 @@ public class CacheObject { private List> decompileBatches; + private volatile boolean fullDecompilationFinished; + public CacheObject() { reset(); } @@ -27,6 +29,7 @@ public void reset() { jNodeCache = new JNodeCache(); lastSearchOptions = new HashMap<>(); decompileBatches = null; + fullDecompilationFinished = false; } @Nullable @@ -53,4 +56,12 @@ public Map> getLastSe public void setDecompileBatches(List> decompileBatches) { this.decompileBatches = decompileBatches; } + + public boolean isFullDecompilationFinished() { + return fullDecompilationFinished; + } + + public void setFullDecompilationFinished(boolean fullDecompilationFinished) { + this.fullDecompilationFinished = fullDecompilationFinished; + } } diff --git a/jadx-with-jadxecute/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties b/jadx-with-jadxecute/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties index 9800ca4..425d7a1 100644 --- a/jadx-with-jadxecute/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties +++ b/jadx-with-jadxecute/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties @@ -14,6 +14,7 @@ menu.text_search=Textsuche menu.class_search=Klassen-Suche menu.comment_search=Kommentar suchen menu.tools=Tools +#menu.decompile_all=Decompile all classes menu.deobfuscation=Deobfuskierung menu.log=Log-Anzeige menu.help=Hilfe @@ -111,6 +112,9 @@ search_dialog.load_all=Alle laden #search_dialog.stop=Stop search_dialog.results_incomplete=%d+ gefunden search_dialog.results_complete=%d gefunden (komplett) +#search_dialog.resources_load_errors=Load errors: %d +#search_dialog.resources_skip_by_size=Skipped by size: %d +#search_dialog.resources_check_logs=(click to check logs) search_dialog.col_node=Knoten search_dialog.col_code=Code #search_dialog.sort_results=Sort results @@ -157,6 +161,7 @@ preferences.useImports=Import statements generieren preferences.useDebugInfo=Debug-Infos verwenden preferences.inlineAnonymous=Anonyme Inline-Klassen preferences.inlineMethods=Inline-Methoden +#preferences.inlineKotlinLambdas=Allow to inline Kotlin Lambdas #preferences.extractFinally=Extract finally block preferences.fsCaseSensitive=Dateisystem unterscheidet zwischen Groß/Kleinschreibung preferences.skipResourcesDecode=Keine Ressourcen dekodieren diff --git a/jadx-with-jadxecute/jadx-gui/src/main/resources/i18n/Messages_en_US.properties b/jadx-with-jadxecute/jadx-gui/src/main/resources/i18n/Messages_en_US.properties index babdb80..dd3bb29 100644 --- a/jadx-with-jadxecute/jadx-gui/src/main/resources/i18n/Messages_en_US.properties +++ b/jadx-with-jadxecute/jadx-gui/src/main/resources/i18n/Messages_en_US.properties @@ -14,6 +14,7 @@ menu.text_search=Text search menu.class_search=Class search menu.comment_search=Comment searchF menu.tools=Tools +menu.decompile_all=Decompile all classes menu.deobfuscation=Deobfuscation menu.log=Log Viewer menu.help=Help @@ -111,6 +112,9 @@ search_dialog.load_all=Load all search_dialog.stop=Stop search_dialog.results_incomplete=Found %d+ search_dialog.results_complete=Found %d (complete) +search_dialog.resources_load_errors=Load errors: %d +search_dialog.resources_skip_by_size=Skipped by size: %d +search_dialog.resources_check_logs=(click to check logs) search_dialog.col_node=Node search_dialog.col_code=Code search_dialog.sort_results=Sort results @@ -157,6 +161,7 @@ preferences.useImports=Use import statements preferences.useDebugInfo=Use debug info preferences.inlineAnonymous=Inline anonymous classes preferences.inlineMethods=Inline methods +preferences.inlineKotlinLambdas=Allow to inline Kotlin Lambdas preferences.extractFinally=Extract finally block preferences.fsCaseSensitive=File system is case-sensitive preferences.skipResourcesDecode=Don't decode resources @@ -198,7 +203,7 @@ preferences.rename_printable=To make printable preferences.search_group_title=Search preferences.search_results_per_page=Results per page (0 - no limit) preferences.res_file_ext=Resource files extensions ('xml|html', * for all) -preferences.res_skip_file=Skip resources files if larger (MB) +preferences.res_skip_file=Skip resources files if larger (MB) (0 - disable) msg.open_file=Please open file msg.saving_sources=Saving sources diff --git a/jadx-with-jadxecute/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties b/jadx-with-jadxecute/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties index 3772a46..aee3fad 100644 --- a/jadx-with-jadxecute/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties +++ b/jadx-with-jadxecute/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties @@ -14,6 +14,7 @@ menu.text_search=Buscar texto menu.class_search=Buscar clase #menu.comment_search=Comment search menu.tools=Herramientas +#menu.decompile_all=Decompile all classes menu.deobfuscation=Desofuscación menu.log=Visor log menu.help=Ayuda @@ -111,6 +112,9 @@ search_dialog.ignorecase=Ignorar minúsculas/mayúsculas #search_dialog.stop=Stop #search_dialog.results_incomplete=Found %d+ #search_dialog.results_complete=Found %d (complete) +#search_dialog.resources_load_errors=Load errors: %d +#search_dialog.resources_skip_by_size=Skipped by size: %d +#search_dialog.resources_check_logs=(click to check logs) search_dialog.col_node=Nodo search_dialog.col_code=Código #search_dialog.sort_results=Sort results @@ -157,6 +161,7 @@ preferences.replaceConsts=Reemplazar constantes #preferences.useDebugInfo=Use debug info #preferences.inlineAnonymous= #preferences.inlineMethods=Inline methods +#preferences.inlineKotlinLambdas=Allow to inline Kotlin Lambdas #preferences.extractFinally=Extract finally block #preferences.fsCaseSensitive= preferences.skipResourcesDecode=No descodificar recursos diff --git a/jadx-with-jadxecute/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties b/jadx-with-jadxecute/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties index 2fee02a..e774731 100644 --- a/jadx-with-jadxecute/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties +++ b/jadx-with-jadxecute/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties @@ -14,6 +14,7 @@ menu.text_search=텍스트 검색 menu.class_search=클래스 검색 menu.comment_search=주석 검색 menu.tools=도구 +#menu.decompile_all=Decompile all classes menu.deobfuscation=난독화 해제 menu.log=로그 뷰어 menu.help=도움말 @@ -111,6 +112,9 @@ search_dialog.load_all=모두 로드 search_dialog.stop=정지 search_dialog.results_incomplete=%d+개 찾음 search_dialog.results_complete=%d개 찾음 (검색 완료) +#search_dialog.resources_load_errors=Load errors: %d +#search_dialog.resources_skip_by_size=Skipped by size: %d +#search_dialog.resources_check_logs=(click to check logs) search_dialog.col_node=노드 search_dialog.col_code=코드 search_dialog.sort_results=결과 정렬 @@ -157,6 +161,7 @@ preferences.useImports=import 문 사용 preferences.useDebugInfo=디버그 정보 사용 preferences.inlineAnonymous=인라인 익명 클래스 preferences.inlineMethods=인라인 메서드 +#preferences.inlineKotlinLambdas=Allow to inline Kotlin Lambdas preferences.extractFinally=finally 블록 추출 preferences.fsCaseSensitive=파일 시스템 대소문자 구별 preferences.skipResourcesDecode=리소스 디코딩 하지 않기 diff --git a/jadx-with-jadxecute/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties b/jadx-with-jadxecute/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties index 4d9301b..2cfe2c9 100644 --- a/jadx-with-jadxecute/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties +++ b/jadx-with-jadxecute/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties @@ -14,6 +14,7 @@ menu.text_search=Buscar por texto menu.class_search=Buscar por classe menu.comment_search=Busca por comentário menu.tools=Ferramentas +#menu.decompile_all=Decompile all classes menu.deobfuscation=Desofuscar menu.log=Visualizador de log menu.help=Ajuda @@ -111,6 +112,9 @@ search_dialog.load_all=Carregar todas search_dialog.stop=Parar search_dialog.results_incomplete=Encontradas %d+ search_dialog.results_complete=Encontradas %d (completos) +#search_dialog.resources_load_errors=Load errors: %d +#search_dialog.resources_skip_by_size=Skipped by size: %d +#search_dialog.resources_check_logs=(click to check logs) search_dialog.col_node=Nó search_dialog.col_code=Código search_dialog.sort_results=Ordenar resultados @@ -157,6 +161,7 @@ preferences.useImports=Utilizar declaração de imports preferences.useDebugInfo=Utilizar informação de depuração preferences.inlineAnonymous=Classes anônimas de uma linha preferences.inlineMethods=Métodos de uma linha +#preferences.inlineKotlinLambdas=Allow to inline Kotlin Lambdas preferences.extractFinally=Extrair blocos finally preferences.fsCaseSensitive=Sistema de arquivo diferencia maiúsculas de minúsculas preferences.skipResourcesDecode=Não decodificar recursos diff --git a/jadx-with-jadxecute/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties b/jadx-with-jadxecute/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties index 4847c84..3fc4857 100644 --- a/jadx-with-jadxecute/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties +++ b/jadx-with-jadxecute/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties @@ -14,6 +14,7 @@ menu.text_search=Поиск строк menu.class_search=Поиск классов menu.comment_search=Поиск комментариев menu.tools=Инструменты +#menu.decompile_all=Decompile all classes menu.deobfuscation=Деобфускация menu.log=Просмотр логов menu.help=Помощь @@ -111,6 +112,9 @@ search_dialog.load_all=Загрузить все search_dialog.stop=Стоп search_dialog.results_incomplete=Найдено %d+ search_dialog.results_complete=Найдено %d (поиск завершен) +#search_dialog.resources_load_errors=Load errors: %d +#search_dialog.resources_skip_by_size=Skipped by size: %d +#search_dialog.resources_check_logs=(click to check logs) search_dialog.col_node=Вхождения search_dialog.col_code=Код search_dialog.sort_results=Сортировка результатов @@ -157,6 +161,7 @@ preferences.useImports=Использовать импорты preferences.useDebugInfo=Отладочная информация preferences.inlineAnonymous=Объединять анонимные классы preferences.inlineMethods=Объединять методы +#preferences.inlineKotlinLambdas=Allow to inline Kotlin Lambdas preferences.extractFinally=Вычленять finally блоки preferences.fsCaseSensitive=Учитывать регистр в файловой системе preferences.skipResourcesDecode=Не декодировать ресурсы diff --git a/jadx-with-jadxecute/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties b/jadx-with-jadxecute/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties index 71a16b8..2074a25 100644 --- a/jadx-with-jadxecute/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties +++ b/jadx-with-jadxecute/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties @@ -14,23 +14,24 @@ menu.text_search=文本搜索 menu.class_search=类名搜索 menu.comment_search=注释搜索 menu.tools=工具 +menu.decompile_all=反编译所有类 menu.deobfuscation=反混淆 menu.log=日志查看器 menu.help=帮助 menu.about=关于 menu.update_label=发现新版本 %s! -file.open_action=打开文件... +file.open_action=打开文件… file.add_files_action=添加文件 file.open_title=打开文件 file.open_project=打开文件 file.new_project=新建项目 file.save_project=保存项目 -file.save_project_as=另存项目为... +file.save_project_as=另存项目为… file.reload=重新加载文件 file.live_reload=实时重加载 file.live_reload_desc=文件变动时自动重载 -#file.export_mappings_as= +file.export_mappings_as=导出映射为… file.save_all=全部保存 file.export_gradle=另存为 Gradle 项目 file.save_all_msg=请选择保存反编译资源的目录 @@ -42,10 +43,10 @@ start_page.recent=最近项目 tree.sources_title=源代码 tree.resources_title=资源文件 -tree.loading=加载中... +tree.loading=加载中… progress.load=正在加载 -#progress.export_mappings= +progress.export_mappings=导出映射 progress.decompile=反编译中 progress.canceling=正在取消 @@ -111,6 +112,9 @@ search_dialog.load_all=加载所有 search_dialog.stop=停止 search_dialog.results_incomplete=已找到 %d+ search_dialog.results_complete=全部找到 %d +search_dialog.resources_load_errors=加载错误:%d +search_dialog.resources_skip_by_size=按大小跳过:%d +search_dialog.resources_check_logs=(点击查看日志) search_dialog.col_node=节点 search_dialog.col_code=代码 search_dialog.sort_results=结果分类 @@ -119,7 +123,7 @@ search_dialog.active_tab=只在当前页搜索 search_dialog.comments=注释 search_dialog.resource=资源 search_dialog.keep_open=保持窗口 -search_dialog.tip_searching=搜索中... +search_dialog.tip_searching=搜索中… usage_dialog.title=查找 usage_dialog.label=查找用例: @@ -144,7 +148,7 @@ preferences.other=其他 preferences.language=语言 preferences.lineNumbersMode=编辑器行号模式 preferences.jumpOnDoubleClick=启用双击跳转 -#preferences.useAlternativeFileDialog=Use alternative file dialog +preferences.useAlternativeFileDialog=使用选择文件对话框 preferences.check_for_updates=启动时检查更新 preferences.useDx=使用 dx/d8 来转换java字节码 preferences.decompilationMode=反编译模式 @@ -157,6 +161,7 @@ preferences.useImports=使用 import 语句 preferences.useDebugInfo=启用调试信息 preferences.inlineAnonymous=内联匿名类 preferences.inlineMethods=内联方法 +preferences.inlineKotlinLambdas=允许内联Kotlin Lambda preferences.extractFinally=提取finally块 preferences.fsCaseSensitive=文件系统区分大小写 preferences.skipResourcesDecode=不反编译资源文件 @@ -168,7 +173,7 @@ preferences.excludedPackages=排除的包 preferences.excludedPackages.tooltip=排除于反编译或索引的以空格分隔的包名列表(节省 RAM) preferences.excludedPackages.button=编辑 preferences.excludedPackages.editDialog=排除于反编译或索引的以空格分隔的包名列表(节省 RAM)
例如android.support -preferences.cfg=生成方法的 CFG 图('.dot' 格式) +preferences.cfg=生成方法的 CFG 图('.dot') preferences.raw_cfg=生成原始的 CFG 图 preferences.font=编辑器字体 preferences.smali_font=Smali编辑器字体 @@ -192,13 +197,13 @@ preferences.reset_title=重置设置 preferences.copy=复制到剪切板 preferences.copy_message=所有设置都已复制 preferences.rename=重命名标识符 -preferences.rename_case=需要标识符能区分大小写 -preferences.rename_valid=需要标识符能符合规范 -preferences.rename_printable=需要标识符可正常显示 +preferences.rename_case=标识符要能够区分大小写 +preferences.rename_valid=标识符应该符合标准规范 +preferences.rename_printable=标识符必须要能正常显示 preferences.search_group_title=搜索资源 preferences.search_results_per_page=每页结果数(0 - 无限制) -preferences.res_file_ext=文件扩展名 (e.g. .xml|.html),* 表示所有 -preferences.res_skip_file=跳过文件大小(MB) +preferences.res_file_ext=文件扩展名(e.g. .xml|.html),* 表示所有 +preferences.res_skip_file=跳过文件大小(MB) msg.open_file=请打开文件 msg.saving_sources=正在导出源代码 @@ -274,10 +279,10 @@ issues_panel.warnings=%d 警告 issues_panel.tooltip=点击查看日志 debugger.process_selector=选择要调试的进程 -debugger.step_into=步入 (F7) -debugger.step_over=步过 (F8) -debugger.step_out=步出 (Shift + F8) -debugger.run=运行 (F9) +debugger.step_into=步入(F7) +debugger.step_over=步过(F8) +debugger.step_out=步出(Shift + F8) +debugger.run=运行(F9) debugger.stop=停止调试并终止应用 debugger.pause=暂停 debugger.rerun=重新运行 @@ -324,8 +329,8 @@ adb_dialog.refresh=刷新 adb_dialog.tip_devices=%d 台设备 adb_dialog.device_node=设备 adb_dialog.missing_path=必须提供ADB路径才能启动ADB服务。 -adb_dialog.waiting=正在等待连接到ADB服务... -adb_dialog.connecting=正在连接ADB服务,地址: %s:%s... +adb_dialog.waiting=正在等待连接到ADB服务… +adb_dialog.connecting=正在连接ADB服务,地址: %s:%s… adb_dialog.connect_okay=已连接ADB服务,地址: %s:%s adb_dialog.connect_fail=连接ADB服务失败。 adb_dialog.disconnected=ADB服务已断开连接。 @@ -340,4 +345,4 @@ adb_dialog.msg_read_mani_fail=解码AndroidManifest.xml失败 adb_dialog.no_devices=找不到任何设备用来启动APP。 adb_dialog.restart_while_debugging_title=调试时重新启动 adb_dialog.restart_while_debugging_msg=你正在调试一个APP,确定要重新启动一个会话吗? -adb_dialog.starting_debugger=正在启动调试器... +adb_dialog.starting_debugger=正在启动调试器… diff --git a/jadx-with-jadxecute/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties b/jadx-with-jadxecute/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties index ca81942..72676c8 100644 --- a/jadx-with-jadxecute/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties +++ b/jadx-with-jadxecute/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties @@ -14,6 +14,7 @@ menu.text_search=文字搜尋 menu.class_search=類別搜尋 menu.comment_search=註解搜尋 menu.tools=工具 +#menu.decompile_all=Decompile all classes menu.deobfuscation=去模糊化 menu.log=日誌檢視器 menu.help=幫助 @@ -111,6 +112,9 @@ search_dialog.load_all=載入全部 search_dialog.stop=停止 search_dialog.results_incomplete=找到 %d+ search_dialog.results_complete=找到 %d (完整) +#search_dialog.resources_load_errors=Load errors: %d +#search_dialog.resources_skip_by_size=Skipped by size: %d +#search_dialog.resources_check_logs=(click to check logs) search_dialog.col_node=無 search_dialog.col_code=程式碼 search_dialog.sort_results=排序結果 @@ -157,6 +161,7 @@ preferences.useImports=使用 import 陳述式 preferences.useDebugInfo=使用除錯資訊 preferences.inlineAnonymous=內嵌匿名類別 preferences.inlineMethods=內嵌方式 +#preferences.inlineKotlinLambdas=Allow to inline Kotlin Lambdas preferences.extractFinally=擷取 finally 區塊 preferences.fsCaseSensitive=檔案系統區分大小寫 preferences.skipResourcesDecode=不要為資源解碼 diff --git a/jadx-with-jadxecute/jadx-gui/src/main/resources/icons/ui/jadxecute.svg b/jadx-with-jadxecute/jadx-gui/src/main/resources/icons/ui/jadxecute.svg deleted file mode 100644 index e845f7b..0000000 --- a/jadx-with-jadxecute/jadx-gui/src/main/resources/icons/ui/jadxecute.svg +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/jadx-with-jadxecute/jadx-gui/src/main/resources/icons/ui/runAll.svg b/jadx-with-jadxecute/jadx-gui/src/main/resources/icons/ui/runAll.svg new file mode 100644 index 0000000..51c8bc7 --- /dev/null +++ b/jadx-with-jadxecute/jadx-gui/src/main/resources/icons/ui/runAll.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/jadx-with-jadxecute/jadx-gui/src/main/resources/jadxecute/codeexamples.properties b/jadx-with-jadxecute/jadx-gui/src/main/resources/jadxecute/codeexamples.properties deleted file mode 100644 index b6f3c91..0000000 --- a/jadx-with-jadxecute/jadx-gui/src/main/resources/jadxecute/codeexamples.properties +++ /dev/null @@ -1,324 +0,0 @@ -### Blank Template -``` -import jadx.gui.ui.MainWindow; - -public class UserCodeClass { - public static String userCodeMain(MainWindow mainWindow) { - String jadxecuteOutput = ""; - - // Your code goes here! - - return jadxecuteOutput; - } -} -``` - -### JadxWrapper example -``` -import jadx.gui.ui.MainWindow; -import jadx.gui.JadxWrapper; -import jadx.api.JavaClass; - -public class UserCodeClass { - public static String userCodeMain(MainWindow mainWindow) { - String jadxecuteOutput = "Example output..."; - - JadxWrapper wrapper = mainWindow.getWrapper(); - - // Here's an example of using the wrapper object. Update here! - for (JavaClass cls : wrapper.getClasses()) { - jadxecuteOutput += cls.getName() + "\n"; - } - - return jadxecuteOutput; - } -} -``` - -### Print all class names -``` -import jadx.gui.ui.MainWindow; -import jadx.core.dex.nodes.ClassNode; -import jadx.core.dex.nodes.RootNode; -import java.util.List; - -public class UserCodeClass { - public static String userCodeMain(MainWindow mainWindow) { - String jadxecuteOutput = ""; - - // Add all strings to the jadxecute output to be printed - RootNode root = mainWindow.getWrapper().getDecompiler().getRoot(); - List classes = root.getClasses(); - for (ClassNode cls : classes) { - jadxecuteOutput += cls.getFullName() + "\n"; - } - - return jadxecuteOutput; - } -} -``` - -### Print all method names -``` -import jadx.gui.ui.MainWindow; -import jadx.core.dex.nodes.ClassNode; -import jadx.core.dex.nodes.MethodNode; -import jadx.core.dex.nodes.RootNode; -import java.util.List; - -public class UserCodeClass { - public static String userCodeMain(MainWindow mainWindow) { - String jadxecuteOutput = ""; - ClassNode selectedClassNode = null; - String searchClassName = "exampleClassName"; // Update this - - // Add all strings to the jadxecute output to be printed - RootNode root = mainWindow.getWrapper().getDecompiler().getRoot(); - List classes = root.getClasses(); - for (ClassNode cls : classes) { - if (cls.getFullName().contains(searchClassName)) { - jadxecuteOutput += "Found class: " + cls.getFullName() + "\n"; - selectedClassNode = cls; - } - } - - if (selectedClassNode != null) { - jadxecuteOutput += "Methods:\n"; - - // Print all methods in the selected class - for (MethodNode method : selectedClassNode.getMethods()) { - jadxecuteOutput += method.getAlias() + "\n"; // Use the alias since this includes user updates - } - } else { - jadxecuteOutput += "Could not find class " + searchClassName; - } - - return jadxecuteOutput; - } -} -``` - -### Print all field names -``` -import jadx.gui.ui.MainWindow; -import jadx.core.dex.nodes.ClassNode; -import jadx.core.dex.nodes.FieldNode; -import jadx.core.dex.nodes.RootNode; -import java.util.List; - -public class UserCodeClass { - public static String userCodeMain(MainWindow mainWindow) { - String jadxecuteOutput = ""; - ClassNode selectedClassNode = null; - String searchClassName = "exampleClassName"; // Update this - - // Add all strings to the jadxecute output to be printed - RootNode root = mainWindow.getWrapper().getDecompiler().getRoot(); - List classes = root.getClasses(); - for (ClassNode cls : classes) { - if (cls.getFullName().contains(searchClassName)) { - jadxecuteOutput += "Found class: " + cls.getFullName() + "\n"; - selectedClassNode = cls; - } - } - - if (selectedClassNode != null) { - jadxecuteOutput += "Fields:\n"; - - // Print all field in the selected class - for (FieldNode field : selectedClassNode.getFields()) { - jadxecuteOutput += field.getAlias() + "\n"; // Use the alias since this includes user updates - } - } else { - jadxecuteOutput += "Could not find class " + searchClassName; - } - - return jadxecuteOutput; - } -} -``` - -### Print classes inheriting from class -``` -import jadx.gui.ui.MainWindow; -import jadx.core.dex.nodes.ClassNode; -import jadx.core.dex.nodes.RootNode; -import java.util.List; -import jadx.core.dex.instructions.args.ArgType; - -public class UserCodeClass { - public static String userCodeMain(MainWindow mainWindow) { - String jadxecuteOutput = ""; - String searchClassName = "IntentService"; // Update this - ArgType superClassType = null; - - // Add all strings to the jadxecute output to be printed - RootNode root = mainWindow.getWrapper().getDecompiler().getRoot(); - List classes = root.getClasses(); - - jadxecuteOutput += "Classes extending " + searchClassName + ":\n"; - - for (ClassNode cls : classes) { - superClassType = cls.getSuperClass(); - - if (superClassType != null && superClassType.toString().contains(searchClassName)) { - jadxecuteOutput += cls.getFullName() + "\n"; - } - } - - return jadxecuteOutput; - } -} -``` - -### Print classes containing substring -``` -import jadx.gui.ui.MainWindow; -import jadx.core.dex.nodes.ClassNode; -import jadx.core.dex.nodes.RootNode; -import java.util.List; - -public class UserCodeClass { - public static String userCodeMain(MainWindow mainWindow) { - String jadxecuteOutput = ""; - String searchClassName = "exampleClassName"; // Update this - - // Loop through all classes and add desired name to return output - RootNode root = mainWindow.getWrapper().getDecompiler().getRoot(); - List classes = root.getClasses(); - for (ClassNode cls : classes) { - - // Example: finds all user-renamed classes renamed like "mw_MyClassA" - if (cls.getFullName().contains(searchClassName)) { - jadxecuteOutput += cls.getFullName() + "\n"; - } - } - - return jadxecuteOutput; - } -} -``` - -### Rename a class -``` -import jadx.gui.ui.MainWindow; -import jadx.core.dex.nodes.ClassNode; -import jadx.core.dex.nodes.RootNode; -import java.util.List; -import jadx.gui.treemodel.JClass; -import jadx.api.JavaClass; -import jadx.gui.plugins.jadxecute.RenameObjectHelper; - -public class UserCodeClass { - public static String userCodeMain(MainWindow mainWindow) { - String jadxecuteOutput = ""; - String searchClassName = "exampleClassName"; // Update this - - // Find desired class - RootNode root = mainWindow.getWrapper().getDecompiler().getRoot(); - List classes = root.getClasses(); - for (ClassNode cls : classes) { - if (cls.getFullName().contains(searchClassName)) { - RenameObjectHelper renameObjectHelper = new RenameObjectHelper(); - JClass jclass = new JClass(cls.getJavaNode()); - - // Rename found class to desired name - jadxecuteOutput += renameObjectHelper.renameObject(mainWindow, jclass, "newClassName"); - - // Optionally return here or you could add functionality to change all - // matched objects to different names and return out of the loop - return jadxecuteOutput; - } - } - - return jadxecuteOutput; - } -} -``` - -### Print Java code in a class -``` -import jadx.gui.ui.MainWindow; -import jadx.core.dex.nodes.ClassNode; -import jadx.core.dex.nodes.RootNode; -import java.util.List; -import jadx.gui.treemodel.JClass; -import jadx.api.JavaClass; -import jadx.gui.plugins.jadxecute.RenameObjectHelper; - -public class UserCodeClass { - public static String userCodeMain(MainWindow mainWindow) { - String jadxecuteOutput = ""; - String searchClassName = "exampleClassName"; // Update this - - // Find desired class - RootNode root = mainWindow.getWrapper().getDecompiler().getRoot(); - List classes = root.getClasses(); - for (ClassNode cls : classes) { - if (cls.getFullName().contains(searchClassName)) { - RenameObjectHelper renameObjectHelper = new RenameObjectHelper(); - - jadxecuteOutput += "Found class " + searchClassName + ":\n\n"; - jadxecuteOutput += cls.getJavaNode().getCodeInfo().toString(); - - return jadxecuteOutput; - } - } - - return jadxecuteOutput; - } -} -``` - -### Insert a new instruction comment -``` -import jadx.gui.ui.MainWindow; -import jadx.core.dex.nodes.ClassNode; -import jadx.core.dex.nodes.MethodNode; -import jadx.core.dex.nodes.RootNode; -import java.util.List; -import jadx.gui.plugins.jadxecute.AddCommentHelper; - -public class UserCodeClass { - public static String userCodeMain(MainWindow mainWindow) { - String jadxecuteOutput = ""; - ClassNode selectedClassNode = null; - MethodNode selectedMethodNode = null; - String searchClassName = "exampleClassName"; // Update this - String searchMethodName = "exampleMethodName"; // Update this - String commentToAdd = "This is a new comment!"; // Update this - int smaliInstructionIndex = 0; // Update this - - // Add all strings to the jadxecute output to be printed - RootNode root = mainWindow.getWrapper().getDecompiler().getRoot(); - List classes = root.getClasses(); - for (ClassNode cls : classes) { - if (cls.getFullName().contains(searchClassName)) { - jadxecuteOutput += "Found class: " + cls.getFullName() + "\n"; - selectedClassNode = cls; - } - } - - if (selectedClassNode != null) { - for (MethodNode method : selectedClassNode.getMethods()) { - if (method.getAlias().contains(searchMethodName)) { - jadxecuteOutput += "Found method: " + method.getAlias() + "\n"; - selectedMethodNode = method; - } - } - - // Add the comment if the method was found - if (selectedMethodNode != null) { - AddCommentHelper addCommentHelper = new AddCommentHelper(mainWindow, selectedMethodNode.getJavaNode()); - jadxecuteOutput += addCommentHelper.addInstructionComment(commentToAdd, smaliInstructionIndex); - } - - } else { - jadxecuteOutput += "Could not find class " + searchClassName; - } - - return jadxecuteOutput; - } -} -``` - diff --git a/jadx-with-jadxecute/jadx-gui/src/main/resources/jadxecute/codeexamples.properties.bak b/jadx-with-jadxecute/jadx-gui/src/main/resources/jadxecute/codeexamples.properties.bak deleted file mode 100644 index c6e9736..0000000 --- a/jadx-with-jadxecute/jadx-gui/src/main/resources/jadxecute/codeexamples.properties.bak +++ /dev/null @@ -1,325 +0,0 @@ -### Blank Template -``` -import jadx.gui.ui.MainWindow; - -public class UserCodeClass { - public static String userCodeMain(MainWindow mainWindow) { - String jadxecuteOutput = ""; - - // Your code goes here! - - return jadxecuteOutput; - } -} -``` - -### JadxWrapper example -``` -import jadx.gui.ui.MainWindow; -import jadx.gui.JadxWrapper; -import jadx.api.JavaClass; - -public class UserCodeClass { - public static String userCodeMain(MainWindow mainWindow) { - String jadxecuteOutput = "Example output..."; - - JadxWrapper wrapper = mainWindow.getWrapper(); - - // Here's an example of using the wrapper object. Update here! - for (JavaClass cls : wrapper.getClasses()) { - jadxecuteOutput += cls.getName() + "\n"; - } - - return jadxecuteOutput; - } -} -``` - -### Print all class names -``` -import jadx.gui.ui.MainWindow; -import jadx.core.dex.nodes.ClassNode; -import jadx.core.dex.nodes.RootNode; -import java.util.List; - -public class UserCodeClass { - public static String userCodeMain(MainWindow mainWindow) { - String jadxecuteOutput = ""; - - // Add all strings to the jadxecute output to be printed - RootNode root = mainWindow.getWrapper().getDecompiler().getRoot(); - List classes = root.getClasses(); - for (ClassNode cls : classes) { - jadxecuteOutput += cls.getFullName() + "\n"; - } - - return jadxecuteOutput; - } -} -``` - -### Print all method names -``` -import jadx.gui.ui.MainWindow; -import jadx.core.dex.nodes.ClassNode; -import jadx.core.dex.nodes.MethodNode; -import jadx.core.dex.nodes.RootNode; -import java.util.List; - -public class UserCodeClass { - public static String userCodeMain(MainWindow mainWindow) { - String jadxecuteOutput = ""; - ClassNode selectedClassNode = null; - String searchClassName = "exampleClassName"; // Update this - - // Add all strings to the jadxecute output to be printed - RootNode root = mainWindow.getWrapper().getDecompiler().getRoot(); - List classes = root.getClasses(); - for (ClassNode cls : classes) { - if (cls.getFullName().contains(searchClassName)) { - jadxecuteOutput += "Found class: " + cls.getFullName() + "\n"; - selectedClassNode = cls; - } - } - - if (selectedClassNode != null) { - jadxecuteOutput += "Methods:\n"; - - // Print all methods in the selected class - for (MethodNode method : selectedClassNode.getMethods()) { - jadxecuteOutput += method.getAlias() + "\n"; // Use the alias since this includes user updates - } - } else { - jadxecuteOutput += "Could not find class " + searchClassName; - } - - return jadxecuteOutput; - } -} -``` - -### Print all field names -``` -import jadx.gui.ui.MainWindow; -import jadx.core.dex.nodes.ClassNode; -import jadx.core.dex.nodes.FieldNode; -import jadx.core.dex.nodes.RootNode; -import java.util.List; - -public class UserCodeClass { - public static String userCodeMain(MainWindow mainWindow) { - String jadxecuteOutput = ""; - ClassNode selectedClassNode = null; - String searchClassName = "exampleClassName"; // Update this - - // Add all strings to the jadxecute output to be printed - RootNode root = mainWindow.getWrapper().getDecompiler().getRoot(); - List classes = root.getClasses(); - for (ClassNode cls : classes) { - if (cls.getFullName().contains(searchClassName)) { - jadxecuteOutput += "Found class: " + cls.getFullName() + "\n"; - selectedClassNode = cls; - } - } - - if (selectedClassNode != null) { - jadxecuteOutput += "Fields:\n"; - - // Print all field in the selected class - for (FieldNode field : selectedClassNode.getFields()) { - jadxecuteOutput += field.getAlias() + "\n"; // Use the alias since this includes user updates - } - } else { - jadxecuteOutput += "Could not find class " + searchClassName; - } - - return jadxecuteOutput; - } -} -``` - -### Print classes inheriting from class -``` -import jadx.gui.ui.MainWindow; -import jadx.core.dex.nodes.ClassNode; -import jadx.core.dex.nodes.RootNode; -import java.util.List; -import jadx.core.dex.instructions.args.ArgType; - -public class UserCodeClass { - public static String userCodeMain(MainWindow mainWindow) { - String jadxecuteOutput = ""; - String searchClassName = "IntentService"; // Update this - ArgType superClassType = null; - - // Add all strings to the jadxecute output to be printed - RootNode root = mainWindow.getWrapper().getDecompiler().getRoot(); - List classes = root.getClasses(); - - jadxecuteOutput += "Classes extending " + searchClassName + ":\n"; - - for (ClassNode cls : classes) { - superClassType = cls.getSuperClass(); - - if (superClassType != null && superClassType.toString().contains(searchClassName)) { - jadxecuteOutput += cls.getFullName() + "\n"; - } - } - - return jadxecuteOutput; - } -} -``` - -### Print classes containing substring -``` -import jadx.gui.ui.MainWindow; -import jadx.core.dex.nodes.ClassNode; -import jadx.core.dex.nodes.RootNode; -import java.util.List; - -public class UserCodeClass { - public static String userCodeMain(MainWindow mainWindow) { - String jadxecuteOutput = ""; - String searchClassName = "exampleClassName"; // Update this - - // Loop through all classes and add desired name to return output - RootNode root = mainWindow.getWrapper().getDecompiler().getRoot(); - List classes = root.getClasses(); - for (ClassNode cls : classes) { - - // Example: finds all user-renamed classes renamed like "mw_MyClassA" - if (cls.getFullName().contains(searchClassName)) { - jadxecuteOutput += cls.getFullName() + "\n"; - } - } - - return jadxecuteOutput; - } -} -``` - -### Rename a class -``` -import jadx.gui.ui.MainWindow; -import jadx.core.dex.nodes.ClassNode; -import jadx.core.dex.nodes.RootNode; -import java.util.List; -import jadx.gui.treemodel.JClass; -import jadx.api.JavaClass; -import jadx.gui.plugins.jadxecute.RenameObjectHelper; - -public class UserCodeClass { - public static String userCodeMain(MainWindow mainWindow) { - String jadxecuteOutput = ""; - String searchClassName = "exampleClassName"; // Update this - - // Find desired class - RootNode root = mainWindow.getWrapper().getDecompiler().getRoot(); - List classes = root.getClasses(); - for (ClassNode cls : classes) { - if (cls.getFullName().contains(searchClassName)) { - RenameObjectHelper renameObjectHelper = new RenameObjectHelper(); - JClass jclass = new JClass(cls.getJavaNode()); - - // Rename found class to desired name - jadxecuteOutput += renameObjectHelper.renameObject(mainWindow, jclass, "newClassName"); - - // Optionally return here or you could add functionality to change all - // matched objects to different names and return out of the loop - return jadxecuteOutput; - } - } - - return jadxecuteOutput; - } -} -``` - -### Print Java code in a class -``` -import jadx.gui.ui.MainWindow; -import jadx.core.dex.nodes.ClassNode; -import jadx.core.dex.nodes.RootNode; -import java.util.List; -import jadx.gui.treemodel.JClass; -import jadx.api.JavaClass; -import jadx.gui.plugins.jadxecute.RenameObjectHelper; - -public class UserCodeClass { - public static String userCodeMain(MainWindow mainWindow) { - String jadxecuteOutput = ""; - String searchClassName = "exampleClassName"; // Update this - - // Find desired class - RootNode root = mainWindow.getWrapper().getDecompiler().getRoot(); - List classes = root.getClasses(); - for (ClassNode cls : classes) { - if (cls.getFullName().contains(searchClassName)) { - RenameObjectHelper renameObjectHelper = new RenameObjectHelper(); - - jadxecuteOutput += "Found class " + searchClassName + ":\n\n"; - jadxecuteOutput += cls.getJavaNode().getCodeInfo().toString(); - - return jadxecuteOutput; - } - } - - return jadxecuteOutput; - } -} -``` - -### Insert a new instruction comment -``` -import jadx.gui.ui.MainWindow; -import jadx.core.dex.nodes.ClassNode; -import jadx.core.dex.nodes.MethodNode; -import jadx.core.dex.nodes.RootNode; -import java.util.List; -import jadx.gui.plugins.jadxecute.AddCommentHelper; - -public class UserCodeClass { - public static String userCodeMain(MainWindow mainWindow) { - String jadxecuteOutput = ""; - ClassNode selectedClassNode = null; - MethodNode selectedMethodNode = null; - String searchClassName = "exampleClassName"; // Update this - String searchMethodName = "exampleMethodName"; // Update this - String commentToAdd = "This is a new comment!"; // Update this - int smaliInstructionIndex = 0; // Update this - - // Add all strings to the jadxecute output to be printed - RootNode root = mainWindow.getWrapper().getDecompiler().getRoot(); - List classes = root.getClasses(); - for (ClassNode cls : classes) { - if (cls.getFullName().contains(searchClassName)) { - jadxecuteOutput += "Found class: " + cls.getFullName() + "\n"; - selectedClassNode = cls; - } - } - - if (selectedClassNode != null) { - for (MethodNode method : selectedClassNode.getMethods()) { - if (method.getAlias().contains(searchMethodName)) { - jadxecuteOutput += "Found method: " + method.getAlias() + "\n"; - selectedMethodNode = method; - } - } - - // Add the comment if the method was found - if (selectedMethodNode != null) { - AddCommentHelper addCommentHelper = new AddCommentHelper(mainWindow, selectedMethodNode.getJavaNode()); - jadxecuteOutput += addCommentHelper.addInstructionComment(commentToAdd, smaliInstructionIndex); - } - - } else { - jadxecuteOutput += "Could not find class " + searchClassName; - } - - return jadxecuteOutput; - } -} - -``` - diff --git a/jadx-with-jadxecute/jadx-plugins/jadx-java-input/build.gradle b/jadx-with-jadxecute/jadx-plugins/jadx-java-input/build.gradle index 27d0872..0ac2fef 100644 --- a/jadx-with-jadxecute/jadx-plugins/jadx-java-input/build.gradle +++ b/jadx-with-jadxecute/jadx-plugins/jadx-java-input/build.gradle @@ -7,4 +7,6 @@ dependencies { // show bytecode disassemble implementation 'io.github.skylot:raung-disasm:0.0.3' + + testImplementation(project(":jadx-core")) } diff --git a/jadx-with-jadxecute/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaFileLoader.java b/jadx-with-jadxecute/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaInputLoader.java similarity index 90% rename from jadx-with-jadxecute/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaFileLoader.java rename to jadx-with-jadxecute/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaInputLoader.java index 073b079..bd7b22e 100644 --- a/jadx-with-jadxecute/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaFileLoader.java +++ b/jadx-with-jadxecute/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaInputLoader.java @@ -19,8 +19,8 @@ import jadx.api.plugins.utils.CommonFileUtils; import jadx.api.plugins.utils.ZipSecurity; -public class JavaFileLoader { - private static final Logger LOG = LoggerFactory.getLogger(JavaFileLoader.class); +public class JavaInputLoader { + private static final Logger LOG = LoggerFactory.getLogger(JavaInputLoader.class); private static final int MAX_MAGIC_SIZE = 4; private static final byte[] JAVA_CLASS_FILE_MAGIC = { (byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE }; @@ -37,6 +37,14 @@ public List collectFiles(List inputFiles) { .collect(Collectors.toList()); } + public List loadInputStream(InputStream in, String name) throws IOException { + return loadReader(in, name, null, null); + } + + public JavaClassReader loadClass(byte[] content, String fileName) { + return new JavaClassReader(getNextUniqId(), fileName, content); + } + private List loadFromFile(File file) { try (InputStream inputStream = new BufferedInputStream(new FileInputStream(file))) { return loadReader(inputStream, file.getName(), file, null); diff --git a/jadx-with-jadxecute/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaInputPlugin.java b/jadx-with-jadxecute/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaInputPlugin.java index 7cc4429..57dc8a1 100644 --- a/jadx-with-jadxecute/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaInputPlugin.java +++ b/jadx-with-jadxecute/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaInputPlugin.java @@ -1,8 +1,11 @@ package jadx.plugins.input.java; import java.io.Closeable; +import java.io.InputStream; import java.nio.file.Path; +import java.util.Collections; import java.util.List; +import java.util.function.Function; import org.jetbrains.annotations.Nullable; @@ -10,6 +13,7 @@ import jadx.api.plugins.input.JadxInputPlugin; import jadx.api.plugins.input.data.ILoadResult; import jadx.api.plugins.input.data.impl.EmptyLoadResult; +import jadx.plugins.input.java.utils.JavaClassParseException; public class JavaInputPlugin implements JadxInputPlugin { @@ -29,10 +33,51 @@ public ILoadResult loadFiles(List inputFiles) { } public static ILoadResult loadClassFiles(List inputFiles, @Nullable Closeable closeable) { - List readers = new JavaFileLoader().collectFiles(inputFiles); + List readers = new JavaInputLoader().collectFiles(inputFiles); if (readers.isEmpty()) { return EmptyLoadResult.INSTANCE; } return new JavaLoadResult(readers, closeable); } + + public static ILoadResult loadClassFiles(List inputFiles) { + return loadClassFiles(inputFiles, null); + } + + /** + * Method for provide several inputs by using load methods from {@link JavaInputLoader} class. + */ + public static ILoadResult load(Function> loader) { + return wrapClassReaders(loader.apply(new JavaInputLoader())); + } + + /** + * Convenient method for load class file or jar from input stream. + * Should be used only once per JadxDecompiler instance. + * For load several times use {@link JavaInputPlugin#load(Function)} method. + */ + public static ILoadResult loadFromInputStream(InputStream in, String fileName) { + try { + return wrapClassReaders(new JavaInputLoader().loadInputStream(in, fileName)); + } catch (Exception e) { + throw new JavaClassParseException("Failed to read input stream", e); + } + } + + /** + * Convenient method for load single class file by content. + * Should be used only once per JadxDecompiler instance. + * For load several times use {@link JavaInputPlugin#load(Function)} method. + */ + public static ILoadResult loadSingleClass(byte[] content, String fileName) { + JavaClassReader reader = new JavaInputLoader().loadClass(content, fileName); + return new JavaLoadResult(Collections.singletonList(reader)); + } + + public static ILoadResult wrapClassReaders(List readers) { + if (readers.isEmpty()) { + return EmptyLoadResult.INSTANCE; + } + return new JavaLoadResult(readers); + } } diff --git a/jadx-with-jadxecute/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaLoadResult.java b/jadx-with-jadxecute/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaLoadResult.java index e14cd70..add098f 100644 --- a/jadx-with-jadxecute/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaLoadResult.java +++ b/jadx-with-jadxecute/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaLoadResult.java @@ -20,6 +20,10 @@ public class JavaLoadResult implements ILoadResult { @Nullable private final Closeable closeable; + public JavaLoadResult(List readers) { + this(readers, null); + } + public JavaLoadResult(List readers, @Nullable Closeable closeable) { this.readers = readers; this.closeable = closeable; @@ -47,7 +51,6 @@ public boolean isEmpty() { @Override public void close() throws IOException { - readers.clear(); if (closeable != null) { closeable.close(); } diff --git a/jadx-with-jadxecute/jadx-plugins/jadx-java-input/src/test/java/jadx/plugins/input/java/CustomLoadTest.java b/jadx-with-jadxecute/jadx-plugins/jadx-java-input/src/test/java/jadx/plugins/input/java/CustomLoadTest.java new file mode 100644 index 0000000..2069cf3 --- /dev/null +++ b/jadx-with-jadxecute/jadx-plugins/jadx-java-input/src/test/java/jadx/plugins/input/java/CustomLoadTest.java @@ -0,0 +1,129 @@ +package jadx.plugins.input.java; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jadx.api.JadxArgs; +import jadx.api.JadxDecompiler; +import jadx.api.plugins.input.data.ILoadResult; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.fail; + +class CustomLoadTest { + + private JadxDecompiler jadx; + + @BeforeEach + void init() { + jadx = new JadxDecompiler(new JadxArgs()); + } + + @AfterEach + void close() { + jadx.close(); + } + + @Test + void loadFiles() { + List files = Stream.of("HelloWorld.class", "HelloWorld$HelloInner.class") + .map(this::getSample) + .collect(Collectors.toList()); + ILoadResult loadResult = JavaInputPlugin.loadClassFiles(files); + loadDecompiler(loadResult); + assertThat(jadx.getClassesWithInners()) + .hasSize(2) + .satisfiesOnlyOnce(cls -> assertThat(cls.getName()).isEqualTo("HelloWorld")) + .satisfiesOnlyOnce(cls -> assertThat(cls.getName()).isEqualTo("HelloInner")); + } + + @Test + void loadFromInputStream() throws IOException { + String fileName = "HelloWorld$HelloInner.class"; + try (InputStream in = Files.newInputStream(getSample(fileName))) { + ILoadResult loadResult = JavaInputPlugin.loadFromInputStream(in, fileName); + loadDecompiler(loadResult); + assertThat(jadx.getClassesWithInners()) + .hasSize(1) + .satisfiesOnlyOnce(cls -> assertThat(cls.getName()).isEqualTo("HelloWorld$HelloInner")); + + System.out.println(jadx.getClassesWithInners().get(0).getCode()); + } + } + + @Test + void loadSingleClass() throws IOException { + String fileName = "HelloWorld.class"; + byte[] content = Files.readAllBytes(getSample(fileName)); + ILoadResult loadResult = JavaInputPlugin.loadSingleClass(content, fileName); + loadDecompiler(loadResult); + assertThat(jadx.getClassesWithInners()) + .hasSize(1) + .satisfiesOnlyOnce(cls -> assertThat(cls.getName()).isEqualTo("HelloWorld")); + + System.out.println(jadx.getClassesWithInners().get(0).getCode()); + } + + @Test + void load() { + ILoadResult loadResult = JavaInputPlugin.load(loader -> { + List inputs = new ArrayList<>(2); + try { + String hello = "HelloWorld.class"; + byte[] content = Files.readAllBytes(getSample(hello)); + inputs.add(loader.loadClass(content, hello)); + + String helloInner = "HelloWorld$HelloInner.class"; + InputStream in = Files.newInputStream(getSample(helloInner)); + inputs.addAll(loader.loadInputStream(in, helloInner)); + } catch (Exception e) { + fail(e); + } + return inputs; + }); + loadDecompiler(loadResult); + assertThat(jadx.getClassesWithInners()) + .hasSize(2) + .satisfiesOnlyOnce(cls -> assertThat(cls.getName()).isEqualTo("HelloWorld")) + .satisfiesOnlyOnce(cls -> { + assertThat(cls.getName()).isEqualTo("HelloInner"); + assertThat(cls.getCode()).isEqualTo(""); // no code for moved inner class + }); + + assertThat(jadx.getClasses()) + .hasSize(1) + .satisfiesOnlyOnce(cls -> assertThat(cls.getName()).isEqualTo("HelloWorld")) + .satisfiesOnlyOnce(cls -> assertThat(cls.getInnerClasses()).hasSize(1) + .satisfiesOnlyOnce(inner -> assertThat(inner.getName()).isEqualTo("HelloInner"))); + + jadx.getClassesWithInners().forEach(cls -> System.out.println(cls.getCode())); + } + + public void loadDecompiler(ILoadResult load) { + try { + jadx.addCustomLoad(load); + jadx.load(); + } catch (Exception e) { + fail(e); + } + } + + public Path getSample(String name) { + try { + return Paths.get(ClassLoader.getSystemResource("samples/" + name).toURI()); + } catch (Exception e) { + return fail(e); + } + } +}