From 0606c90f22934145e16bf0cc145def50cfbbde32 Mon Sep 17 00:00:00 2001 From: skylot <118523+skylot@users.noreply.github.com> Date: Wed, 18 May 2022 17:19:31 +0300 Subject: [PATCH] feat(gui): disk code cache and search rewrite (PR #1483) * feat: implement disk code cache * feat: rewrite code metadata handling, remove code index * feat: rewrite search * fix: code cleanup and fixes for previous commits * feat: run code search in parallel * fix: reset code strings cache on low memory, code cleanup * fix: include input files timestamp into code hash --- .gitignore | 3 +- .../src/main/java/jadx/api/CodePosition.java | 65 ---- .../src/main/java/jadx/api/ICodeCache.java | 12 +- .../src/main/java/jadx/api/ICodeInfo.java | 7 +- .../src/main/java/jadx/api/ICodeWriter.java | 22 +- .../src/main/java/jadx/api/JadxArgs.java | 19 ++ .../main/java/jadx/api/JadxDecompiler.java | 148 ++++---- .../src/main/java/jadx/api/JavaClass.java | 128 ++++--- .../src/main/java/jadx/api/JavaField.java | 5 - .../src/main/java/jadx/api/JavaMethod.java | 7 +- .../src/main/java/jadx/api/JavaNode.java | 2 - .../src/main/java/jadx/api/JavaPackage.java | 5 - .../src/main/java/jadx/api/JavaVariable.java | 38 +-- .../api/data/annotations/ICodeRawOffset.java | 5 - .../api/data/annotations/VarDeclareRef.java | 57 ---- .../jadx/api/data/annotations/VarRef.java | 98 ------ .../java/jadx/api/data/impl/JadxCodeRef.java | 6 +- .../java/jadx/api/impl/AnnotatedCodeInfo.java | 24 +- .../jadx/api/impl/AnnotatedCodeWriter.java | 78 ++--- .../java/jadx/api/impl/DelegateCodeCache.java | 49 +++ .../java/jadx/api/impl/InMemoryCodeCache.java | 30 +- .../java/jadx/api/impl/NoOpCodeCache.java | 21 +- .../java/jadx/api/impl/SimpleCodeInfo.java | 13 +- .../java/jadx/api/impl/SimpleCodeWriter.java | 17 +- .../jadx/api/metadata/ICodeAnnotation.java | 16 + .../java/jadx/api/metadata/ICodeMetadata.java | 59 ++++ .../java/jadx/api/metadata/ICodeNodeRef.java | 7 + .../annotations/InsnCodeOffset.java | 11 +- .../metadata/annotations/NodeDeclareRef.java | 39 +++ .../api/metadata/annotations/VarNode.java | 147 ++++++++ .../jadx/api/metadata/annotations/VarRef.java | 68 ++++ .../metadata/impl/CodeMetadataStorage.java | 141 ++++++++ .../main/java/jadx/api/utils/CodeUtils.java | 38 +++ .../main/java/jadx/core/codegen/InsnGen.java | 21 +- .../java/jadx/core/codegen/MethodGen.java | 6 +- .../java/jadx/core/codegen/RegionGen.java | 11 +- .../jadx/core/codegen/json/JsonCodeGen.java | 13 +- .../dex/attributes/ILineAttributeNode.java | 4 - .../dex/attributes/nodes/LineAttrNode.java | 26 +- .../core/dex/instructions/args/CodeVar.java | 12 +- .../java/jadx/core/dex/nodes/ClassNode.java | 7 +- .../java/jadx/core/dex/nodes/FieldNode.java | 5 + .../java/jadx/core/dex/nodes/ICodeNode.java | 3 +- .../java/jadx/core/dex/nodes/MethodNode.java | 5 + .../java/jadx/core/utils/CodeGenUtils.java | 6 +- .../java/jadx/core/utils/files/FileUtils.java | 52 ++- .../java/jadx/tests/api/IntegrationTest.java | 18 +- .../assertj/JadxClassNodeAssertions.java | 6 +- .../utils/assertj/JadxCodeInfoAssertions.java | 8 +- .../jadx/tests/external/BaseExternalTest.java | 74 ++-- .../debuginfo/TestLineNumbers.java | 23 +- .../debuginfo/TestLineNumbers2.java | 2 +- .../debuginfo/TestLineNumbers3.java | 2 +- .../debuginfo/TestReturnSourceLine.java | 6 +- .../integration/java8/TestLambdaExtVar.java | 39 +++ .../integration/others/TestCodeMetadata.java | 66 ++++ .../TestVariablesDeclAnnotation.java | 38 ++- jadx-gui/build.gradle | 4 +- .../src/main/java/jadx/gui/JadxWrapper.java | 95 +++++- .../device/debugger/smali/SmaliWriter.java | 22 +- .../jadx/gui/jobs/BackgroundExecutor.java | 126 +++---- .../main/java/jadx/gui/jobs/Cancelable.java | 7 + .../gui/jobs/CancelableBackgroundTask.java | 27 ++ .../java/jadx/gui/jobs/DecompileTask.java | 21 +- .../main/java/jadx/gui/jobs/ExportTask.java | 4 +- .../java/jadx/gui/jobs/IBackgroundTask.java | 24 +- .../java/jadx/gui/jobs/ITaskProgress.java | 8 + .../main/java/jadx/gui/jobs/IndexService.java | 110 ------ .../main/java/jadx/gui/jobs/IndexTask.java | 94 ----- .../main/java/jadx/gui/jobs/SimpleTask.java | 61 ++++ .../main/java/jadx/gui/jobs/TaskProgress.java | 39 +++ .../main/java/jadx/gui/jobs/TaskStatus.java | 2 +- .../gui/plugins/quark/QuarkReportNode.java | 10 +- .../gui/plugins/quark/QuarkReportPanel.java | 7 +- .../java/jadx/gui/search/ISearchMethod.java | 27 ++ .../java/jadx/gui/search/ISearchProvider.java | 16 + .../main/java/jadx/gui/search/SearchJob.java | 40 +++ .../java/jadx/gui/search/SearchSettings.java | 70 ++++ .../main/java/jadx/gui/search/SearchTask.java | 132 +++++++ .../search/providers/BaseSearchProvider.java | 44 +++ .../search/providers/ClassSearchProvider.java | 46 +++ .../search/providers/CodeSearchProvider.java | 106 ++++++ .../providers/CommentSearchProvider.java} | 125 ++++--- .../search/providers/FieldSearchProvider.java | 56 +++ .../providers/MergedSearchProvider.java | 58 ++++ .../providers/MethodSearchProvider.java | 57 ++++ .../providers/ResourceSearchProvider.java} | 180 +++++----- .../java/jadx/gui/settings/JadxSettings.java | 35 +- .../jadx/gui/settings/JadxSettingsWindow.java | 14 +- .../java/jadx/gui/treemodel/ApkSignature.java | 10 +- .../java/jadx/gui/treemodel/CodeNode.java | 37 +- .../main/java/jadx/gui/treemodel/JClass.java | 41 +-- .../main/java/jadx/gui/treemodel/JField.java | 27 +- .../main/java/jadx/gui/treemodel/JMethod.java | 44 ++- .../main/java/jadx/gui/treemodel/JNode.java | 44 +-- .../java/jadx/gui/treemodel/JPackage.java | 14 +- .../jadx/gui/treemodel/JResSearchNode.java | 9 +- .../java/jadx/gui/treemodel/JResource.java | 50 ++- .../main/java/jadx/gui/treemodel/JRoot.java | 5 - .../java/jadx/gui/treemodel/JVariable.java | 5 + .../java/jadx/gui/treemodel/TextNode.java | 5 - .../main/java/jadx/gui/ui/MainDropTarget.java | 4 +- .../src/main/java/jadx/gui/ui/MainWindow.java | 64 +--- .../src/main/java/jadx/gui/ui/TabbedPane.java | 119 +++---- .../gui/ui/codearea/AbstractCodeArea.java | 46 +-- .../java/jadx/gui/ui/codearea/CodeArea.java | 68 +++- .../gui/ui/codearea/CodeLinkGenerator.java | 38 +-- .../java/jadx/gui/ui/codearea/CodePanel.java | 6 +- .../jadx/gui/ui/codearea/CommentAction.java | 109 +++--- .../jadx/gui/ui/codearea/FridaAction.java | 40 ++- .../ui/codearea/GoToDeclarationAction.java | 24 +- .../jadx/gui/ui/codearea/JadxTokenMaker.java | 21 +- .../jadx/gui/ui/codearea/LineNumbers.java | 10 +- .../ui/codearea/MouseHoverHighlighter.java | 2 +- .../java/jadx/gui/ui/codearea/SmaliArea.java | 6 + .../jadx/gui/ui/codearea/mode/JCodeMode.java | 8 +- .../gui/ui/dialog/CommonSearchDialog.java | 322 +++++++----------- .../java/jadx/gui/ui/dialog/FileDialog.java | 2 +- .../java/jadx/gui/ui/dialog/RenameDialog.java | 1 - .../java/jadx/gui/ui/dialog/SearchDialog.java | 292 ++++++++++++---- .../java/jadx/gui/ui/dialog/UsageDialog.java | 119 +++---- .../java/jadx/gui/ui/panel/HtmlPanel.java | 2 +- .../java/jadx/gui/ui/panel/ProgressPanel.java | 27 +- .../jadx/gui/ui/treenodes/SummaryNode.java | 6 +- .../main/java/jadx/gui/utils/CacheObject.java | 33 -- .../java/jadx/gui/utils/CaretPositionFix.java | 83 ++--- .../java/jadx/gui/utils/CodeLinesInfo.java | 58 ---- .../main/java/jadx/gui/utils/FileUtils.java | 26 -- .../java/jadx/gui/utils/FixedCodeCache.java | 33 -- .../main/java/jadx/gui/utils/JNodeCache.java | 2 + .../main/java/jadx/gui/utils/JumpManager.java | 18 +- .../java/jadx/gui/utils/JumpPosition.java | 30 +- .../src/main/java/jadx/gui/utils/UiUtils.java | 64 +++- .../gui/utils/codecache/CodeCacheMode.java | 27 ++ .../gui/utils/codecache/CodeStringCache.java | 92 +++++ .../gui/utils/codecache/FixedCodeCache.java | 25 ++ .../utils/codecache/disk/BufferCodeCache.java | 84 +++++ .../codecache/disk/CodeMetadataAdapter.java | 113 ++++++ .../utils/codecache/disk/DiskCodeCache.java | 258 ++++++++++++++ .../disk/adapters/ArgTypeAdapter.java | 39 +++ .../disk/adapters/BaseDataAdapter.java | 26 ++ .../disk/adapters/ClassNodeAdapter.java | 26 ++ .../disk/adapters/CodeAnnotationAdapter.java | 90 +++++ .../codecache/disk/adapters/DataAdapter.java | 12 + .../disk/adapters/FieldNodeAdapter.java | 40 +++ .../disk/adapters/InsnCodeOffsetAdapter.java | 22 ++ .../disk/adapters/MethodNodeAdapter.java | 40 +++ .../disk/adapters/NodeDeclareRefAdapter.java | 37 ++ .../disk/adapters/VarNodeAdapter.java | 36 ++ .../disk/adapters/VarRefAdapter.java | 26 ++ .../java/jadx/gui/utils/search/CodeIndex.java | 59 ---- .../jadx/gui/utils/search/SearchIndex.java | 12 - .../jadx/gui/utils/search/SearchSettings.java | 114 ------- .../jadx/gui/utils/search/SimpleIndex.java | 54 --- .../java/jadx/gui/utils/search/StringRef.java | 208 ----------- .../gui/utils/search/TextSearchIndex.java | 211 ------------ .../resources/i18n/Messages_de_DE.properties | 10 +- .../resources/i18n/Messages_en_US.properties | 12 +- .../resources/i18n/Messages_es_ES.properties | 10 +- .../resources/i18n/Messages_ko_KR.properties | 10 +- .../resources/i18n/Messages_zh_CN.properties | 10 +- .../resources/i18n/Messages_zh_TW.properties | 10 +- .../java/jadx/gui/utils/JumpManagerTest.java | 2 +- .../utils/codecache/DiskCodeCacheTest.java | 47 +++ .../jadx/gui/utils/search/StringRefTest.java | 71 ---- jadx-gui/src/test/resources/logback-test.xml | 13 + 166 files changed, 4408 insertions(+), 2917 deletions(-) delete mode 100644 jadx-core/src/main/java/jadx/api/CodePosition.java delete mode 100644 jadx-core/src/main/java/jadx/api/data/annotations/ICodeRawOffset.java delete mode 100644 jadx-core/src/main/java/jadx/api/data/annotations/VarDeclareRef.java delete mode 100644 jadx-core/src/main/java/jadx/api/data/annotations/VarRef.java create mode 100644 jadx-core/src/main/java/jadx/api/impl/DelegateCodeCache.java create mode 100644 jadx-core/src/main/java/jadx/api/metadata/ICodeAnnotation.java create mode 100644 jadx-core/src/main/java/jadx/api/metadata/ICodeMetadata.java create mode 100644 jadx-core/src/main/java/jadx/api/metadata/ICodeNodeRef.java rename jadx-core/src/main/java/jadx/api/{data => metadata}/annotations/InsnCodeOffset.java (82%) create mode 100644 jadx-core/src/main/java/jadx/api/metadata/annotations/NodeDeclareRef.java create mode 100644 jadx-core/src/main/java/jadx/api/metadata/annotations/VarNode.java create mode 100644 jadx-core/src/main/java/jadx/api/metadata/annotations/VarRef.java create mode 100644 jadx-core/src/main/java/jadx/api/metadata/impl/CodeMetadataStorage.java create mode 100644 jadx-core/src/main/java/jadx/api/utils/CodeUtils.java create mode 100644 jadx-core/src/test/java/jadx/tests/integration/java8/TestLambdaExtVar.java create mode 100644 jadx-core/src/test/java/jadx/tests/integration/others/TestCodeMetadata.java create mode 100644 jadx-gui/src/main/java/jadx/gui/jobs/Cancelable.java create mode 100644 jadx-gui/src/main/java/jadx/gui/jobs/CancelableBackgroundTask.java create mode 100644 jadx-gui/src/main/java/jadx/gui/jobs/ITaskProgress.java delete mode 100644 jadx-gui/src/main/java/jadx/gui/jobs/IndexService.java delete mode 100644 jadx-gui/src/main/java/jadx/gui/jobs/IndexTask.java create mode 100644 jadx-gui/src/main/java/jadx/gui/jobs/SimpleTask.java create mode 100644 jadx-gui/src/main/java/jadx/gui/jobs/TaskProgress.java create mode 100644 jadx-gui/src/main/java/jadx/gui/search/ISearchMethod.java create mode 100644 jadx-gui/src/main/java/jadx/gui/search/ISearchProvider.java create mode 100644 jadx-gui/src/main/java/jadx/gui/search/SearchJob.java create mode 100644 jadx-gui/src/main/java/jadx/gui/search/SearchSettings.java create mode 100644 jadx-gui/src/main/java/jadx/gui/search/SearchTask.java create mode 100644 jadx-gui/src/main/java/jadx/gui/search/providers/BaseSearchProvider.java create mode 100644 jadx-gui/src/main/java/jadx/gui/search/providers/ClassSearchProvider.java create mode 100644 jadx-gui/src/main/java/jadx/gui/search/providers/CodeSearchProvider.java rename jadx-gui/src/main/java/jadx/gui/{utils/search/CommentsIndex.java => search/providers/CommentSearchProvider.java} (70%) create mode 100644 jadx-gui/src/main/java/jadx/gui/search/providers/FieldSearchProvider.java create mode 100644 jadx-gui/src/main/java/jadx/gui/search/providers/MergedSearchProvider.java create mode 100644 jadx-gui/src/main/java/jadx/gui/search/providers/MethodSearchProvider.java rename jadx-gui/src/main/java/jadx/gui/{utils/search/ResourceIndex.java => search/providers/ResourceSearchProvider.java} (55%) delete mode 100644 jadx-gui/src/main/java/jadx/gui/utils/CodeLinesInfo.java delete mode 100644 jadx-gui/src/main/java/jadx/gui/utils/FileUtils.java delete mode 100644 jadx-gui/src/main/java/jadx/gui/utils/FixedCodeCache.java create mode 100644 jadx-gui/src/main/java/jadx/gui/utils/codecache/CodeCacheMode.java create mode 100644 jadx-gui/src/main/java/jadx/gui/utils/codecache/CodeStringCache.java create mode 100644 jadx-gui/src/main/java/jadx/gui/utils/codecache/FixedCodeCache.java create mode 100644 jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/BufferCodeCache.java create mode 100644 jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/CodeMetadataAdapter.java create mode 100644 jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/DiskCodeCache.java create mode 100644 jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/ArgTypeAdapter.java create mode 100644 jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/BaseDataAdapter.java create mode 100644 jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/ClassNodeAdapter.java create mode 100644 jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/CodeAnnotationAdapter.java create mode 100644 jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/DataAdapter.java create mode 100644 jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/FieldNodeAdapter.java create mode 100644 jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/InsnCodeOffsetAdapter.java create mode 100644 jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/MethodNodeAdapter.java create mode 100644 jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/NodeDeclareRefAdapter.java create mode 100644 jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/VarNodeAdapter.java create mode 100644 jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/VarRefAdapter.java delete mode 100644 jadx-gui/src/main/java/jadx/gui/utils/search/CodeIndex.java delete mode 100644 jadx-gui/src/main/java/jadx/gui/utils/search/SearchIndex.java delete mode 100644 jadx-gui/src/main/java/jadx/gui/utils/search/SearchSettings.java delete mode 100644 jadx-gui/src/main/java/jadx/gui/utils/search/SimpleIndex.java delete mode 100644 jadx-gui/src/main/java/jadx/gui/utils/search/StringRef.java delete mode 100644 jadx-gui/src/main/java/jadx/gui/utils/search/TextSearchIndex.java create mode 100644 jadx-gui/src/test/java/jadx/gui/utils/codecache/DiskCodeCacheTest.java delete mode 100644 jadx-gui/src/test/java/jadx/gui/utils/search/StringRefTest.java create mode 100644 jadx-gui/src/test/resources/logback-test.xml diff --git a/.gitignore b/.gitignore index dd3b11e7..bc0fbc38 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,5 @@ jadx-output/ *.orig quark.json -cliff.toml \ No newline at end of file +cliff.toml +jadx-gui/src/main/resources/logback.xml diff --git a/jadx-core/src/main/java/jadx/api/CodePosition.java b/jadx-core/src/main/java/jadx/api/CodePosition.java deleted file mode 100644 index 503f3c36..00000000 --- a/jadx-core/src/main/java/jadx/api/CodePosition.java +++ /dev/null @@ -1,65 +0,0 @@ -package jadx.api; - -public final class CodePosition { - - private final int line; - private final int offset; - private final int pos; - - public CodePosition(int line, int offset, int pos) { - this.line = line; - this.offset = offset; - this.pos = pos; - } - - public CodePosition(int line) { - this(line, 0, -1); - } - - @Deprecated - public CodePosition(int line, int offset) { - this(line, offset, -1); - } - - public int getPos() { - return pos; - } - - public int getLine() { - return line; - } - - public int getOffset() { - return offset; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - CodePosition that = (CodePosition) o; - return line == that.line && offset == that.offset; - } - - @Override - public int hashCode() { - return line + 31 * offset; - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append(line); - if (offset != 0) { - sb.append(':').append(offset); - } - if (pos > 0) { - sb.append('@').append(pos); - } - return sb.toString(); - } -} diff --git a/jadx-core/src/main/java/jadx/api/ICodeCache.java b/jadx-core/src/main/java/jadx/api/ICodeCache.java index 4dd8eab2..27608e21 100644 --- a/jadx-core/src/main/java/jadx/api/ICodeCache.java +++ b/jadx-core/src/main/java/jadx/api/ICodeCache.java @@ -1,13 +1,21 @@ package jadx.api; +import java.io.Closeable; + +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -public interface ICodeCache { +public interface ICodeCache extends Closeable { void add(String clsFullName, ICodeInfo codeInfo); void remove(String clsFullName); - @Nullable + @NotNull ICodeInfo get(String clsFullName); + + @Nullable + String getCode(String clsFullName); + + boolean contains(String clsFullName); } diff --git a/jadx-core/src/main/java/jadx/api/ICodeInfo.java b/jadx-core/src/main/java/jadx/api/ICodeInfo.java index 9f8b1a9c..a175f3b6 100644 --- a/jadx-core/src/main/java/jadx/api/ICodeInfo.java +++ b/jadx-core/src/main/java/jadx/api/ICodeInfo.java @@ -1,8 +1,7 @@ package jadx.api; -import java.util.Map; - import jadx.api.impl.SimpleCodeInfo; +import jadx.api.metadata.ICodeMetadata; public interface ICodeInfo { @@ -10,7 +9,7 @@ public interface ICodeInfo { String getCodeStr(); - Map getLineMapping(); + ICodeMetadata getCodeMetadata(); - Map getAnnotations(); + boolean hasMetadata(); } diff --git a/jadx-core/src/main/java/jadx/api/ICodeWriter.java b/jadx-core/src/main/java/jadx/api/ICodeWriter.java index e1facd5c..fd49ee92 100644 --- a/jadx-core/src/main/java/jadx/api/ICodeWriter.java +++ b/jadx-core/src/main/java/jadx/api/ICodeWriter.java @@ -2,7 +2,10 @@ package jadx.api; import java.util.Map; -import jadx.core.dex.attributes.ILineAttributeNode; +import org.jetbrains.annotations.ApiStatus; + +import jadx.api.metadata.ICodeAnnotation; +import jadx.api.metadata.ICodeNodeRef; public interface ICodeWriter { String NL = System.getProperty("line.separator"); @@ -38,13 +41,21 @@ public interface ICodeWriter { void setIndent(int indent); + /** + * Return current line (only if metadata is supported) + */ int getLine(); - void attachDefinition(ILineAttributeNode obj); + /** + * Return start line position (only if metadata is supported) + */ + int getLineStartPos(); - void attachAnnotation(Object obj); + void attachDefinition(ICodeNodeRef obj); - void attachLineAnnotation(Object obj); + void attachAnnotation(ICodeAnnotation obj); + + void attachLineAnnotation(ICodeAnnotation obj); void attachSourceLine(int sourceLine); @@ -56,5 +67,6 @@ public interface ICodeWriter { StringBuilder getRawBuf(); - Map getRawAnnotations(); + @ApiStatus.Internal + Map getRawAnnotations(); } diff --git a/jadx-core/src/main/java/jadx/api/JadxArgs.java b/jadx-core/src/main/java/jadx/api/JadxArgs.java index a058a351..0183b4bd 100644 --- a/jadx-core/src/main/java/jadx/api/JadxArgs.java +++ b/jadx-core/src/main/java/jadx/api/JadxArgs.java @@ -1,6 +1,7 @@ package jadx.api; import java.io.File; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; @@ -15,6 +16,7 @@ import jadx.api.args.DeobfuscationMapFileMode; import jadx.api.data.ICodeData; import jadx.api.impl.AnnotatedCodeWriter; import jadx.api.impl.InMemoryCodeCache; +import jadx.core.utils.files.FileUtils; public class JadxArgs { @@ -514,6 +516,21 @@ public class JadxArgs { this.pluginOptions = pluginOptions; } + /** + * Hash of all options that can change result code + */ + public String makeCodeArgsHash() { + String argStr = "args:" + decompilationMode + useImports + showInconsistentCode + + inlineAnonymousClasses + inlineMethods + + deobfuscationOn + deobfuscationMinLength + deobfuscationMaxLength + + parseKotlinMetadata + useKotlinMethodsForVarNames + + insertDebugLines + extractFinally + + debugInfo + useSourceNameAsClassAlias + escapeUnicode + replaceConsts + + respectBytecodeAccModifiers + fsCaseSensitive + renameFlags + + commentsLevel + useDxInput + pluginOptions; + return FileUtils.md5Sum(argStr.getBytes(StandardCharsets.US_ASCII)); + } + @Override public String toString() { return "JadxArgs{" + "inputFiles=" + inputFiles @@ -533,6 +550,8 @@ public class JadxArgs { + ", useSourceNameAsClassAlias=" + useSourceNameAsClassAlias + ", parseKotlinMetadata=" + parseKotlinMetadata + ", useKotlinMethodsForVarNames=" + useKotlinMethodsForVarNames + + ", insertDebugLines=" + insertDebugLines + + ", extractFinally=" + extractFinally + ", deobfuscationMinLength=" + deobfuscationMinLength + ", deobfuscationMaxLength=" + deobfuscationMaxLength + ", escapeUnicode=" + escapeUnicode diff --git a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java index 688fcca1..354e7596 100644 --- a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java +++ b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java @@ -25,7 +25,11 @@ import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import jadx.api.data.annotations.VarRef; +import jadx.api.metadata.ICodeAnnotation; +import jadx.api.metadata.ICodeNodeRef; +import jadx.api.metadata.annotations.NodeDeclareRef; +import jadx.api.metadata.annotations.VarNode; +import jadx.api.metadata.annotations.VarRef; import jadx.api.plugins.JadxPlugin; import jadx.api.plugins.JadxPluginManager; import jadx.api.plugins.input.JadxInputPlugin; @@ -35,7 +39,6 @@ import jadx.core.Jadx; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.InlinedAttr; -import jadx.core.dex.attributes.nodes.LineAttrNode; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.MethodNode; @@ -240,6 +243,7 @@ public final class JadxDecompiler implements Closeable { save(false, true); } + @SuppressWarnings("ResultOfMethodCallIgnored") private void save(boolean saveSources, boolean saveResources) { ExecutorService ex = getSaveExecutor(saveSources, saveResources); ex.shutdown(); @@ -368,14 +372,14 @@ public final class JadxDecompiler implements Closeable { return Collections.emptyList(); } if (classes == null) { - List classNodeList = root.getClasses(false); + List classNodeList = root.getClasses(); List clsList = new ArrayList<>(classNodeList.size()); - classesMap.clear(); for (ClassNode classNode : classNodeList) { - if (!classNode.contains(AFlag.DONT_GENERATE)) { - JavaClass javaClass = new JavaClass(classNode, this); - clsList.add(javaClass); - classesMap.put(classNode, javaClass); + if (classNode.contains(AFlag.DONT_GENERATE)) { + continue; + } + if (!classNode.getClassInfo().isInner()) { + clsList.add(convertClassNode(classNode)); } } classes = Collections.unmodifiableList(clsList); @@ -383,6 +387,10 @@ public final class JadxDecompiler implements Closeable { return classes; } + public List getClassesWithInners() { + return Utils.collectionMap(root.getClasses(), this::convertClassNode); + } + public List getResources() { if (resources == null) { if (root == null) { @@ -440,6 +448,7 @@ public final class JadxDecompiler implements Closeable { /** * Internal API. Not Stable! */ + @ApiStatus.Internal public RootNode getRoot() { return root; } @@ -519,8 +528,9 @@ public final class JadxDecompiler implements Closeable { throw new JadxRuntimeException("JavaClass not found by ClassNode: " + cls); } + @ApiStatus.Internal @Nullable - private JavaMethod getJavaMethodByNode(MethodNode mth) { + public JavaMethod getJavaMethodByNode(MethodNode mth) { JavaMethod javaMethod = methodsMap.get(mth); if (javaMethod != null && javaMethod.getMethodNode() == mth) { return javaMethod; @@ -560,8 +570,9 @@ public final class JadxDecompiler implements Closeable { return getCodeParentClass(codeCls); } + @ApiStatus.Internal @Nullable - private JavaField getJavaFieldByNode(FieldNode fld) { + public JavaField getJavaFieldByNode(FieldNode fld) { JavaField javaField = fieldsMap.get(fld); if (javaField != null && javaField.getFieldNode() == fld) { return javaField; @@ -626,81 +637,84 @@ public final class JadxDecompiler implements Closeable { } @Nullable - JavaNode convertNode(Object obj) { - if (obj instanceof VarRef) { - VarRef varRef = (VarRef) obj; - MethodNode mthNode = varRef.getMth(); - JavaMethod mth = getJavaMethodByNode(mthNode); - if (mth == null) { + public JavaNode getJavaNodeByRef(ICodeNodeRef ann) { + return getJavaNodeByCodeAnnotation(null, ann); + } + + @Nullable + public JavaNode getJavaNodeByCodeAnnotation(@Nullable ICodeInfo codeInfo, @Nullable ICodeAnnotation ann) { + if (ann == null) { + return null; + } + switch (ann.getAnnType()) { + case CLASS: + return convertClassNode((ClassNode) ann); + case METHOD: + return getJavaMethodByNode((MethodNode) ann); + case FIELD: + return getJavaFieldByNode((FieldNode) ann); + case DECLARATION: + return getJavaNodeByCodeAnnotation(codeInfo, ((NodeDeclareRef) ann).getNode()); + case VAR: + return resolveVarNode((VarNode) ann); + case VAR_REF: + return resolveVarRef(codeInfo, (VarRef) ann); + case OFFSET: + // offset annotation don't have java node object return null; - } - return new JavaVariable(mth, varRef); + default: + throw new JadxRuntimeException("Unknown annotation type: " + ann.getAnnType() + ", class: " + ann.getClass()); } - if (!(obj instanceof LineAttrNode)) { - return null; - } - LineAttrNode node = (LineAttrNode) obj; - if (node.contains(AFlag.DONT_GENERATE)) { - return null; - } - if (obj instanceof ClassNode) { - return convertClassNode((ClassNode) obj); - } - if (obj instanceof MethodNode) { - return getJavaMethodByNode(((MethodNode) obj)); - } - if (obj instanceof FieldNode) { - return getJavaFieldByNode((FieldNode) obj); - } - throw new JadxRuntimeException("Unexpected node type: " + obj); } - // TODO: make interface for all nodes in code annotations and add common method instead this - Object getInternalNode(JavaNode javaNode) { - if (javaNode instanceof JavaClass) { - return ((JavaClass) javaNode).getClassNode(); + @Nullable + private JavaVariable resolveVarNode(VarNode varNode) { + MethodNode mthNode = varNode.getMth(); + JavaMethod mth = getJavaMethodByNode(mthNode); + if (mth == null) { + return null; } - if (javaNode instanceof JavaMethod) { - return ((JavaMethod) javaNode).getMethodNode(); - } - if (javaNode instanceof JavaField) { - return ((JavaField) javaNode).getFieldNode(); - } - if (javaNode instanceof JavaVariable) { - return ((JavaVariable) javaNode).getVarRef(); - } - throw new JadxRuntimeException("Unexpected node type: " + javaNode); + return new JavaVariable(mth, varNode); } - List convertNodes(Collection nodesList) { + @Nullable + private JavaVariable resolveVarRef(ICodeInfo codeInfo, VarRef varRef) { + if (codeInfo == null) { + throw new JadxRuntimeException("Missing code info for resolve VarRef: " + varRef); + } + ICodeAnnotation varNodeAnn = codeInfo.getCodeMetadata().getAt(varRef.getRefPos()); + if (varNodeAnn == null) { + return null; + } + return (JavaVariable) getJavaNodeByCodeAnnotation(codeInfo, varNodeAnn); + } + + List convertNodes(Collection nodesList) { return nodesList.stream() - .map(this::convertNode) + .map(this::getJavaNodeByRef) .filter(Objects::nonNull) .collect(Collectors.toList()); } @Nullable - public JavaNode getJavaNodeAtPosition(ICodeInfo codeInfo, int line, int offset) { - Map map = codeInfo.getAnnotations(); - if (map.isEmpty()) { - return null; - } - Object obj = map.get(new CodePosition(line, offset)); - if (obj == null) { - return null; - } - return convertNode(obj); + public JavaNode getJavaNodeAtPosition(ICodeInfo codeInfo, int pos) { + ICodeAnnotation ann = codeInfo.getCodeMetadata().getAt(pos); + return getJavaNodeByCodeAnnotation(codeInfo, ann); } @Nullable - public CodePosition getDefinitionPosition(JavaNode javaNode) { - JavaClass jCls = javaNode.getTopParentClass(); - jCls.decompile(); - int defLine = javaNode.getDecompiledLine(); - if (defLine == 0) { + public JavaNode getClosestJavaNode(ICodeInfo codeInfo, int pos) { + ICodeAnnotation ann = codeInfo.getCodeMetadata().getClosestUp(pos); + return getJavaNodeByCodeAnnotation(codeInfo, ann); + } + + @Nullable + public JavaNode getEnclosingNode(ICodeInfo codeInfo, int pos) { + ICodeNodeRef obj = codeInfo.getCodeMetadata().getNodeAt(pos); + if (obj == null) { return null; } - return new CodePosition(defLine, 0, javaNode.getDefPos()); + return getJavaNodeByRef(obj); } public void reloadCodeData() { diff --git a/jadx-core/src/main/java/jadx/api/JavaClass.java b/jadx-core/src/main/java/jadx/api/JavaClass.java index 785514e6..572b54ea 100644 --- a/jadx-core/src/main/java/jadx/api/JavaClass.java +++ b/jadx-core/src/main/java/jadx/api/JavaClass.java @@ -8,8 +8,11 @@ import java.util.List; import java.util.Map; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import jadx.api.metadata.ICodeAnnotation; +import jadx.api.metadata.ICodeNodeRef; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.AnonymousClassAttr; @@ -46,24 +49,21 @@ public final class JavaClass implements JavaNode { } public String getCode() { - ICodeInfo code = getCodeInfo(); - if (code == null) { - return ""; - } - return code.getCodeStr(); + return getCodeInfo().getCodeStr(); } - public ICodeInfo getCodeInfo() { + public @NotNull ICodeInfo getCodeInfo() { + load(); return cls.decompile(); } public void decompile() { - cls.decompile(); + load(); } - public synchronized void reload() { + public synchronized ICodeInfo reload() { listsLoaded = false; - cls.reloadCode(); + return cls.reloadCode(); } public void unload() { @@ -75,6 +75,10 @@ public final class JavaClass implements JavaNode { return cls.contains(AFlag.DONT_GENERATE); } + public boolean isInner() { + return cls.isInner(); + } + public synchronized String getSmali() { return cls.getDisassembledCode(); } @@ -87,13 +91,22 @@ public final class JavaClass implements JavaNode { return cls; } - private synchronized void loadLists() { + /** + * Decompile class and loads internal lists of fields, methods, etc. + * Do nothing if already loaded. + * Return not null on first call only (for actual loading) + */ + @Nullable + private synchronized void load() { if (listsLoaded) { return; } listsLoaded = true; - decompile(); JadxDecompiler rootDecompiler = getRootDecompiler(); + ICodeCache codeCache = rootDecompiler.getArgs().getCodeCache(); + if (!codeCache.contains(cls.getRawName())) { + cls.decompile(); + } int inClsCount = cls.getInnerClasses().size(); if (inClsCount != 0) { @@ -101,7 +114,7 @@ public final class JavaClass implements JavaNode { for (ClassNode inner : cls.getInnerClasses()) { if (!inner.contains(AFlag.DONT_GENERATE)) { JavaClass javaClass = rootDecompiler.convertClassNode(inner); - javaClass.loadLists(); + javaClass.load(); list.add(javaClass); } } @@ -112,7 +125,7 @@ public final class JavaClass implements JavaNode { List list = new ArrayList<>(inlinedClsCount); for (ClassNode inner : cls.getInlinedClasses()) { JavaClass javaClass = rootDecompiler.convertClassNode(inner); - javaClass.loadLists(); + javaClass.load(); list.add(javaClass); } this.inlinedClasses = Collections.unmodifiableList(list); @@ -122,10 +135,10 @@ public final class JavaClass implements JavaNode { if (fieldsCount != 0) { List flds = new ArrayList<>(fieldsCount); for (FieldNode f : cls.getFields()) { - if (!f.contains(AFlag.DONT_GENERATE)) { - JavaField javaField = new JavaField(this, f); - flds.add(javaField); - } + // if (!f.contains(AFlag.DONT_GENERATE)) { + JavaField javaField = new JavaField(this, f); + flds.add(javaField); + // } } this.fields = Collections.unmodifiableList(flds); } @@ -151,47 +164,45 @@ public final class JavaClass implements JavaNode { return decompiler; } - public Map getCodeAnnotations() { - ICodeInfo code = getCodeInfo(); - if (code == null) { - return Collections.emptyMap(); - } - return code.getAnnotations(); + public ICodeAnnotation getAnnotationAt(int pos) { + return getCodeInfo().getCodeMetadata().getAt(pos); } - public Object getAnnotationAt(CodePosition pos) { - return getCodeAnnotations().get(pos); - } - - public Map getUsageMap() { - Map map = getCodeAnnotations(); + public Map getUsageMap() { + Map map = getCodeInfo().getCodeMetadata().getAsMap(); if (map.isEmpty() || decompiler == null) { return Collections.emptyMap(); } - Map resultMap = new HashMap<>(map.size()); - for (Map.Entry entry : map.entrySet()) { - CodePosition codePosition = entry.getKey(); - Object obj = entry.getValue(); - JavaNode node = getRootDecompiler().convertNode(obj); - if (node != null) { - resultMap.put(codePosition, node); + Map resultMap = new HashMap<>(map.size()); + for (Map.Entry entry : map.entrySet()) { + int codePosition = entry.getKey(); + ICodeAnnotation obj = entry.getValue(); + if (obj instanceof ICodeNodeRef) { + JavaNode node = getRootDecompiler().getJavaNodeByRef((ICodeNodeRef) obj); + if (node != null) { + resultMap.put(codePosition, node); + } } } return resultMap; } - public List getUsageFor(JavaNode javaNode) { - Map map = getCodeAnnotations(); + public List getUsePlacesFor(ICodeInfo codeInfo, JavaNode javaNode) { + Map map = codeInfo.getCodeMetadata().getAsMap(); if (map.isEmpty() || decompiler == null) { return Collections.emptyList(); } - Object internalNode = getRootDecompiler().getInternalNode(javaNode); - List result = new ArrayList<>(); - for (Map.Entry entry : map.entrySet()) { - CodePosition codePosition = entry.getKey(); - Object obj = entry.getValue(); - if (internalNode.equals(obj)) { - result.add(codePosition); + JadxDecompiler rootDec = getRootDecompiler(); + List result = new ArrayList<>(); + for (Map.Entry entry : map.entrySet()) { + ICodeAnnotation ann = entry.getValue(); + if (ann.getAnnType() == ICodeAnnotation.AnnType.DECLARATION) { + // ignore declarations + continue; + } + JavaNode annNode = rootDec.getJavaNodeByCodeAnnotation(codeInfo, ann); + if (javaNode.equals(annNode)) { + result.add(entry.getKey()); } } return result; @@ -202,20 +213,8 @@ public final class JavaClass implements JavaNode { return getRootDecompiler().convertNodes(cls.getUseIn()); } - @Nullable - @Deprecated - public JavaNode getJavaNodeAtPosition(int line, int offset) { - return getRootDecompiler().getJavaNodeAtPosition(getCodeInfo(), line, offset); - } - - @Nullable - @Deprecated - public CodePosition getDefinitionPosition() { - return getRootDecompiler().getDefinitionPosition(this); - } - public Integer getSourceLine(int decompiledLine) { - return getCodeInfo().getLineMapping().get(decompiledLine); + return getCodeInfo().getCodeMetadata().getLineMapping().get(decompiledLine); } @Override @@ -261,22 +260,22 @@ public final class JavaClass implements JavaNode { } public List getInnerClasses() { - loadLists(); + load(); return innerClasses; } public List getInlinedClasses() { - loadLists(); + load(); return inlinedClasses; } public List getFields() { - loadLists(); + load(); return fields; } public List getMethods() { - loadLists(); + load(); return methods; } @@ -294,11 +293,6 @@ public final class JavaClass implements JavaNode { this.cls.getClassInfo().removeAlias(); } - @Override - public int getDecompiledLine() { - return cls.getDecompiledLine(); - } - @Override public int getDefPos() { return cls.getDefPosition(); diff --git a/jadx-core/src/main/java/jadx/api/JavaField.java b/jadx-core/src/main/java/jadx/api/JavaField.java index 15c2f2ae..8344843e 100644 --- a/jadx-core/src/main/java/jadx/api/JavaField.java +++ b/jadx-core/src/main/java/jadx/api/JavaField.java @@ -50,11 +50,6 @@ public final class JavaField implements JavaNode { return ArgType.tryToResolveClassAlias(field.root(), field.getType()); } - @Override - public int getDecompiledLine() { - return field.getDecompiledLine(); - } - @Override public int getDefPos() { return field.getDefPosition(); diff --git a/jadx-core/src/main/java/jadx/api/JavaMethod.java b/jadx-core/src/main/java/jadx/api/JavaMethod.java index 4a6bd046..09ffecd4 100644 --- a/jadx-core/src/main/java/jadx/api/JavaMethod.java +++ b/jadx-core/src/main/java/jadx/api/JavaMethod.java @@ -78,7 +78,7 @@ public final class JavaMethod implements JavaNode { JadxDecompiler decompiler = getDeclaringClass().getRootDecompiler(); return ovrdAttr.getRelatedMthNodes().stream() .map(m -> { - JavaMethod javaMth = (JavaMethod) decompiler.convertNode(m); + JavaMethod javaMth = decompiler.getJavaMethodByNode(m); if (javaMth == null) { LOG.warn("Failed convert to java method: {}", m); } @@ -96,11 +96,6 @@ public final class JavaMethod implements JavaNode { return mth.getMethodInfo().isClassInit(); } - @Override - public int getDecompiledLine() { - return mth.getDecompiledLine(); - } - @Override public int getDefPos() { return mth.getDefPosition(); diff --git a/jadx-core/src/main/java/jadx/api/JavaNode.java b/jadx-core/src/main/java/jadx/api/JavaNode.java index c126ebc1..0d67c119 100644 --- a/jadx-core/src/main/java/jadx/api/JavaNode.java +++ b/jadx-core/src/main/java/jadx/api/JavaNode.java @@ -12,8 +12,6 @@ public interface JavaNode { JavaClass getTopParentClass(); - int getDecompiledLine(); - int getDefPos(); List getUseIn(); diff --git a/jadx-core/src/main/java/jadx/api/JavaPackage.java b/jadx-core/src/main/java/jadx/api/JavaPackage.java index 7ff63ffe..203b848f 100644 --- a/jadx-core/src/main/java/jadx/api/JavaPackage.java +++ b/jadx-core/src/main/java/jadx/api/JavaPackage.java @@ -39,11 +39,6 @@ public final class JavaPackage implements JavaNode, Comparable { return null; } - @Override - public int getDecompiledLine() { - return 0; - } - @Override public int getDefPos() { return 0; diff --git a/jadx-core/src/main/java/jadx/api/JavaVariable.java b/jadx-core/src/main/java/jadx/api/JavaVariable.java index 0cd29656..d2b5ef3f 100644 --- a/jadx-core/src/main/java/jadx/api/JavaVariable.java +++ b/jadx-core/src/main/java/jadx/api/JavaVariable.java @@ -5,16 +5,15 @@ import java.util.List; import org.jetbrains.annotations.ApiStatus; -import jadx.api.data.annotations.VarDeclareRef; -import jadx.api.data.annotations.VarRef; +import jadx.api.metadata.annotations.VarNode; public class JavaVariable implements JavaNode { private final JavaMethod mth; - private final VarRef varRef; + private final VarNode varNode; - public JavaVariable(JavaMethod mth, VarRef varRef) { + public JavaVariable(JavaMethod mth, VarNode varNode) { this.mth = mth; - this.varRef = varRef; + this.varNode = varNode; } public JavaMethod getMth() { @@ -22,26 +21,26 @@ public class JavaVariable implements JavaNode { } public int getReg() { - return varRef.getReg(); + return varNode.getReg(); } public int getSsa() { - return varRef.getSsa(); + return varNode.getSsa(); } @Override public String getName() { - return varRef.getName(); + return varNode.getName(); } @ApiStatus.Internal - public VarRef getVarRef() { - return varRef; + public VarNode getVarNode() { + return varNode; } @Override public String getFullName() { - return varRef.getType() + " " + varRef.getName() + " (r" + varRef.getReg() + "v" + varRef.getSsa() + ")"; + return varNode.getType() + " " + varNode.getName() + " (r" + varNode.getReg() + "v" + varNode.getSsa() + ")"; } @Override @@ -54,20 +53,9 @@ public class JavaVariable implements JavaNode { return mth.getTopParentClass(); } - @Override - public int getDecompiledLine() { - if (varRef instanceof VarDeclareRef) { - return ((VarDeclareRef) varRef).getDecompiledLine(); - } - return 0; - } - @Override public int getDefPos() { - if (varRef instanceof VarDeclareRef) { - return ((VarDeclareRef) varRef).getDefPosition(); - } - return 0; + return varNode.getDefPosition(); } @Override @@ -77,7 +65,7 @@ public class JavaVariable implements JavaNode { @Override public int hashCode() { - return varRef.hashCode(); + return varNode.hashCode(); } @Override @@ -88,6 +76,6 @@ public class JavaVariable implements JavaNode { if (!(o instanceof JavaVariable)) { return false; } - return varRef.equals(((JavaVariable) o).varRef); + return varNode.equals(((JavaVariable) o).varNode); } } diff --git a/jadx-core/src/main/java/jadx/api/data/annotations/ICodeRawOffset.java b/jadx-core/src/main/java/jadx/api/data/annotations/ICodeRawOffset.java deleted file mode 100644 index 9c2186d9..00000000 --- a/jadx-core/src/main/java/jadx/api/data/annotations/ICodeRawOffset.java +++ /dev/null @@ -1,5 +0,0 @@ -package jadx.api.data.annotations; - -public interface ICodeRawOffset { - int getOffset(); -} diff --git a/jadx-core/src/main/java/jadx/api/data/annotations/VarDeclareRef.java b/jadx-core/src/main/java/jadx/api/data/annotations/VarDeclareRef.java deleted file mode 100644 index fff5f083..00000000 --- a/jadx-core/src/main/java/jadx/api/data/annotations/VarDeclareRef.java +++ /dev/null @@ -1,57 +0,0 @@ -package jadx.api.data.annotations; - -import jadx.core.dex.attributes.ILineAttributeNode; -import jadx.core.dex.instructions.args.CodeVar; -import jadx.core.dex.nodes.MethodNode; - -public class VarDeclareRef extends VarRef implements ILineAttributeNode { - - public static VarDeclareRef get(MethodNode mth, CodeVar codeVar) { - VarDeclareRef ref = new VarDeclareRef(mth, codeVar); - codeVar.setCachedVarRef(ref); - return ref; - } - - private int sourceLine; - private int decompiledLine; - private int defPosition; - - private VarDeclareRef(MethodNode mth, CodeVar codeVar) { - super(mth, codeVar.getAnySsaVar()); - } - - @Override - public int getSourceLine() { - return sourceLine; - } - - @Override - public void setSourceLine(int sourceLine) { - this.sourceLine = sourceLine; - } - - @Override - public int getDecompiledLine() { - return decompiledLine; - } - - @Override - public void setDecompiledLine(int decompiledLine) { - this.decompiledLine = decompiledLine; - } - - @Override - public int getDefPosition() { - return defPosition; - } - - @Override - public void setDefPosition(int pos) { - this.defPosition = pos; - } - - @Override - public String toString() { - return "VarDeclareRef{r" + getReg() + 'v' + getSsa() + '}'; - } -} diff --git a/jadx-core/src/main/java/jadx/api/data/annotations/VarRef.java b/jadx-core/src/main/java/jadx/api/data/annotations/VarRef.java deleted file mode 100644 index 1612c109..00000000 --- a/jadx-core/src/main/java/jadx/api/data/annotations/VarRef.java +++ /dev/null @@ -1,98 +0,0 @@ -package jadx.api.data.annotations; - -import org.jetbrains.annotations.Nullable; - -import jadx.core.dex.instructions.args.ArgType; -import jadx.core.dex.instructions.args.CodeVar; -import jadx.core.dex.instructions.args.RegisterArg; -import jadx.core.dex.instructions.args.SSAVar; -import jadx.core.dex.nodes.MethodNode; - -public class VarRef { - - @Nullable - public static VarRef get(MethodNode mth, RegisterArg reg) { - SSAVar ssaVar = reg.getSVar(); - if (ssaVar == null) { - return null; - } - CodeVar codeVar = ssaVar.getCodeVar(); - VarRef cachedVarRef = codeVar.getCachedVarRef(); - if (cachedVarRef != null) { - if (cachedVarRef.getName() == null) { - cachedVarRef.setName(codeVar.getName()); - } - return cachedVarRef; - } - VarRef newVarRef = new VarRef(mth, ssaVar); - codeVar.setCachedVarRef(newVarRef); - return newVarRef; - } - - private final MethodNode mth; - private final int reg; - private final int ssa; - private final ArgType type; - private String name; - - protected VarRef(MethodNode mth, SSAVar ssaVar) { - this(mth, ssaVar.getRegNum(), ssaVar.getVersion(), - ssaVar.getCodeVar().getType(), ssaVar.getCodeVar().getName()); - } - - private VarRef(MethodNode mth, int reg, int ssa, ArgType type, String name) { - this.mth = mth; - this.reg = reg; - this.ssa = ssa; - this.type = type; - this.name = name; - } - - public MethodNode getMth() { - return mth; - } - - public int getReg() { - return reg; - } - - public int getSsa() { - return ssa; - } - - public ArgType getType() { - return type; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (!(o instanceof VarRef)) { - return false; - } - VarRef other = (VarRef) o; - return getReg() == other.getReg() - && getSsa() == other.getSsa() - && getMth().equals(other.getMth()); - } - - @Override - public int hashCode() { - return 31 * getReg() + getSsa(); - } - - @Override - public String toString() { - return "VarUseRef{r" + reg + 'v' + ssa + '}'; - } -} diff --git a/jadx-core/src/main/java/jadx/api/data/impl/JadxCodeRef.java b/jadx-core/src/main/java/jadx/api/data/impl/JadxCodeRef.java index 17ebe1c3..593a50e7 100644 --- a/jadx-core/src/main/java/jadx/api/data/impl/JadxCodeRef.java +++ b/jadx-core/src/main/java/jadx/api/data/impl/JadxCodeRef.java @@ -3,7 +3,7 @@ package jadx.api.data.impl; import jadx.api.JavaVariable; import jadx.api.data.CodeRefType; import jadx.api.data.IJavaCodeRef; -import jadx.api.data.annotations.VarRef; +import jadx.api.metadata.annotations.VarNode; public class JadxCodeRef implements IJavaCodeRef { @@ -23,8 +23,8 @@ public class JadxCodeRef implements IJavaCodeRef { return forVar(javaVariable.getReg(), javaVariable.getSsa()); } - public static JadxCodeRef forVar(VarRef varRef) { - return forVar(varRef.getReg(), varRef.getSsa()); + public static JadxCodeRef forVar(VarNode varNode) { + return forVar(varNode.getReg(), varNode.getSsa()); } public static JadxCodeRef forCatch(int handlerOffset) { diff --git a/jadx-core/src/main/java/jadx/api/impl/AnnotatedCodeInfo.java b/jadx-core/src/main/java/jadx/api/impl/AnnotatedCodeInfo.java index afc5ff60..1b651912 100644 --- a/jadx-core/src/main/java/jadx/api/impl/AnnotatedCodeInfo.java +++ b/jadx-core/src/main/java/jadx/api/impl/AnnotatedCodeInfo.java @@ -2,23 +2,19 @@ package jadx.api.impl; import java.util.Map; -import jadx.api.CodePosition; import jadx.api.ICodeInfo; +import jadx.api.metadata.ICodeAnnotation; +import jadx.api.metadata.ICodeMetadata; +import jadx.api.metadata.impl.CodeMetadataStorage; public class AnnotatedCodeInfo implements ICodeInfo { private final String code; - private final Map lineMapping; - private final Map annotations; + private final ICodeMetadata metadata; - public AnnotatedCodeInfo(ICodeInfo codeInfo) { - this(codeInfo.getCodeStr(), codeInfo.getLineMapping(), codeInfo.getAnnotations()); - } - - public AnnotatedCodeInfo(String code, Map lineMapping, Map annotations) { + public AnnotatedCodeInfo(String code, Map lineMapping, Map annotations) { this.code = code; - this.lineMapping = lineMapping; - this.annotations = annotations; + this.metadata = CodeMetadataStorage.build(lineMapping, annotations); } @Override @@ -27,13 +23,13 @@ public class AnnotatedCodeInfo implements ICodeInfo { } @Override - public Map getLineMapping() { - return lineMapping; + public ICodeMetadata getCodeMetadata() { + return metadata; } @Override - public Map getAnnotations() { - return annotations; + public boolean hasMetadata() { + return metadata != ICodeMetadata.EMPTY; } @Override diff --git a/jadx-core/src/main/java/jadx/api/impl/AnnotatedCodeWriter.java b/jadx-core/src/main/java/jadx/api/impl/AnnotatedCodeWriter.java index ccb716a2..f4f2d5d0 100644 --- a/jadx-core/src/main/java/jadx/api/impl/AnnotatedCodeWriter.java +++ b/jadx-core/src/main/java/jadx/api/impl/AnnotatedCodeWriter.java @@ -5,18 +5,20 @@ import java.util.HashMap; import java.util.Map; import java.util.TreeMap; -import jadx.api.CodePosition; import jadx.api.ICodeInfo; import jadx.api.ICodeWriter; import jadx.api.JadxArgs; -import jadx.core.dex.attributes.ILineAttributeNode; +import jadx.api.metadata.ICodeAnnotation; +import jadx.api.metadata.ICodeNodeRef; +import jadx.api.metadata.annotations.NodeDeclareRef; +import jadx.api.metadata.annotations.VarRef; import jadx.core.utils.StringUtils; public class AnnotatedCodeWriter extends SimpleCodeWriter implements ICodeWriter { private int line = 1; private int offset; - private Map annotations = Collections.emptyMap(); + private Map annotations = Collections.emptyMap(); private Map lineMap = Collections.emptyMap(); public AnnotatedCodeWriter() { @@ -59,19 +61,17 @@ public class AnnotatedCodeWriter extends SimpleCodeWriter implements ICodeWriter @Override public ICodeWriter add(ICodeWriter cw) { - if ((!(cw instanceof AnnotatedCodeWriter))) { + if (!cw.isMetadataSupported()) { buf.append(cw.getCodeStr()); return this; } AnnotatedCodeWriter code = ((AnnotatedCodeWriter) cw); line--; - int startLine = line; int startPos = getLength(); - for (Map.Entry entry : code.annotations.entrySet()) { - CodePosition codePos = entry.getKey(); - int newLine = startLine + codePos.getLine(); - int newPos = startPos + codePos.getPos(); - attachAnnotation(entry.getValue(), new CodePosition(newLine, codePos.getOffset(), newPos)); + for (Map.Entry entry : code.annotations.entrySet()) { + int pos = entry.getKey(); + int newPos = startPos + pos; + attachAnnotation(entry.getValue(), newPos); } for (Map.Entry entry : code.lineMap.entrySet()) { attachSourceLine(line + entry.getKey(), entry.getValue()); @@ -101,44 +101,36 @@ public class AnnotatedCodeWriter extends SimpleCodeWriter implements ICodeWriter return line; } - private static final class DefinitionWrapper { - private final ILineAttributeNode node; - - private DefinitionWrapper(ILineAttributeNode node) { - this.node = node; - } - - public ILineAttributeNode getNode() { - return node; - } + @Override + public int getLineStartPos() { + return getLength() - offset; } @Override - public void attachDefinition(ILineAttributeNode obj) { + public void attachDefinition(ICodeNodeRef obj) { if (obj == null) { return; } - attachAnnotation(obj); - attachAnnotation(new DefinitionWrapper(obj), new CodePosition(line, offset, getLength())); + attachAnnotation(new NodeDeclareRef(obj)); } @Override - public void attachAnnotation(Object obj) { + public void attachAnnotation(ICodeAnnotation obj) { if (obj == null) { return; } - attachAnnotation(obj, new CodePosition(line, offset + 1, getLength())); + attachAnnotation(obj, getLength()); } @Override - public void attachLineAnnotation(Object obj) { + public void attachLineAnnotation(ICodeAnnotation obj) { if (obj == null) { return; } - attachAnnotation(obj, new CodePosition(line, 0, getLength() - offset)); + attachAnnotation(obj, getLineStartPos()); } - private void attachAnnotation(Object obj, CodePosition pos) { + private void attachAnnotation(ICodeAnnotation obj, int pos) { if (annotations.isEmpty()) { annotations = new HashMap<>(); } @@ -164,29 +156,39 @@ public class AnnotatedCodeWriter extends SimpleCodeWriter implements ICodeWriter public ICodeInfo finish() { removeFirstEmptyLine(); processDefinitionAnnotations(); + validateAnnotations(); String code = buf.toString(); buf = null; return new AnnotatedCodeInfo(code, lineMap, annotations); } @Override - public Map getRawAnnotations() { + public Map getRawAnnotations() { return annotations; } private void processDefinitionAnnotations() { if (!annotations.isEmpty()) { - annotations.entrySet().removeIf(entry -> { - Object v = entry.getValue(); - if (v instanceof DefinitionWrapper) { - ILineAttributeNode l = ((DefinitionWrapper) v).getNode(); - CodePosition codePos = entry.getKey(); - l.setDecompiledLine(codePos.getLine()); - l.setDefPosition(codePos.getPos()); - return true; + annotations.forEach((k, v) -> { + if (v instanceof NodeDeclareRef) { + NodeDeclareRef declareRef = (NodeDeclareRef) v; + declareRef.setDefPos(k); + declareRef.getNode().setDefPosition(k); } - return false; }); } } + + private void validateAnnotations() { + if (annotations.isEmpty()) { + return; + } + annotations.values().removeIf(v -> { + if (v.getAnnType() == ICodeAnnotation.AnnType.VAR_REF) { + VarRef varRef = (VarRef) v; + return varRef.getRefPos() == 0; + } + return false; + }); + } } diff --git a/jadx-core/src/main/java/jadx/api/impl/DelegateCodeCache.java b/jadx-core/src/main/java/jadx/api/impl/DelegateCodeCache.java new file mode 100644 index 00000000..9b62d47a --- /dev/null +++ b/jadx-core/src/main/java/jadx/api/impl/DelegateCodeCache.java @@ -0,0 +1,49 @@ +package jadx.api.impl; + +import java.io.IOException; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import jadx.api.ICodeCache; +import jadx.api.ICodeInfo; + +public abstract class DelegateCodeCache implements ICodeCache { + + protected final ICodeCache backCache; + + public DelegateCodeCache(ICodeCache backCache) { + this.backCache = backCache; + } + + @Override + public void add(String clsFullName, ICodeInfo codeInfo) { + backCache.add(clsFullName, codeInfo); + } + + @Override + public void remove(String clsFullName) { + backCache.remove(clsFullName); + } + + @Override + public @NotNull ICodeInfo get(String clsFullName) { + return backCache.get(clsFullName); + } + + @Override + @Nullable + public String getCode(String clsFullName) { + return backCache.getCode(clsFullName); + } + + @Override + public boolean contains(String clsFullName) { + return backCache.contains(clsFullName); + } + + @Override + public void close() throws IOException { + backCache.close(); + } +} diff --git a/jadx-core/src/main/java/jadx/api/impl/InMemoryCodeCache.java b/jadx-core/src/main/java/jadx/api/impl/InMemoryCodeCache.java index a72f120e..c4eec028 100644 --- a/jadx-core/src/main/java/jadx/api/impl/InMemoryCodeCache.java +++ b/jadx-core/src/main/java/jadx/api/impl/InMemoryCodeCache.java @@ -1,8 +1,10 @@ package jadx.api.impl; +import java.io.IOException; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import jadx.api.ICodeCache; @@ -22,9 +24,33 @@ public class InMemoryCodeCache implements ICodeCache { storage.remove(clsFullName); } + @NotNull @Override - public @Nullable ICodeInfo get(String clsFullName) { - return storage.get(clsFullName); + public ICodeInfo get(String clsFullName) { + ICodeInfo codeInfo = storage.get(clsFullName); + if (codeInfo == null) { + return ICodeInfo.EMPTY; + } + return codeInfo; + } + + @Override + public @Nullable String getCode(String clsFullName) { + ICodeInfo codeInfo = storage.get(clsFullName); + if (codeInfo == null) { + return null; + } + return codeInfo.getCodeStr(); + } + + @Override + public boolean contains(String clsFullName) { + return storage.containsKey(clsFullName); + } + + @Override + public void close() throws IOException { + storage.clear(); } @Override diff --git a/jadx-core/src/main/java/jadx/api/impl/NoOpCodeCache.java b/jadx-core/src/main/java/jadx/api/impl/NoOpCodeCache.java index f52601f4..be78e162 100644 --- a/jadx-core/src/main/java/jadx/api/impl/NoOpCodeCache.java +++ b/jadx-core/src/main/java/jadx/api/impl/NoOpCodeCache.java @@ -1,5 +1,6 @@ package jadx.api.impl; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import jadx.api.ICodeCache; @@ -7,6 +8,8 @@ import jadx.api.ICodeInfo; public class NoOpCodeCache implements ICodeCache { + public static final NoOpCodeCache INSTANCE = new NoOpCodeCache(); + @Override public void add(String clsFullName, ICodeInfo codeInfo) { // do nothing @@ -18,10 +21,26 @@ public class NoOpCodeCache implements ICodeCache { } @Override - public @Nullable ICodeInfo get(String clsFullName) { + @NotNull + public ICodeInfo get(String clsFullName) { + return ICodeInfo.EMPTY; + } + + @Override + public @Nullable String getCode(String clsFullName) { return null; } + @Override + public boolean contains(String clsFullName) { + return false; + } + + @Override + public void close() { + // do nothing + } + @Override public String toString() { return "NoOpCodeCache"; diff --git a/jadx-core/src/main/java/jadx/api/impl/SimpleCodeInfo.java b/jadx-core/src/main/java/jadx/api/impl/SimpleCodeInfo.java index c3d4582b..4422cf89 100644 --- a/jadx-core/src/main/java/jadx/api/impl/SimpleCodeInfo.java +++ b/jadx-core/src/main/java/jadx/api/impl/SimpleCodeInfo.java @@ -1,10 +1,7 @@ package jadx.api.impl; -import java.util.Collections; -import java.util.Map; - -import jadx.api.CodePosition; import jadx.api.ICodeInfo; +import jadx.api.metadata.ICodeMetadata; public class SimpleCodeInfo implements ICodeInfo { @@ -20,13 +17,13 @@ public class SimpleCodeInfo implements ICodeInfo { } @Override - public Map getLineMapping() { - return Collections.emptyMap(); + public ICodeMetadata getCodeMetadata() { + return ICodeMetadata.EMPTY; } @Override - public Map getAnnotations() { - return Collections.emptyMap(); + public boolean hasMetadata() { + return false; } @Override diff --git a/jadx-core/src/main/java/jadx/api/impl/SimpleCodeWriter.java b/jadx-core/src/main/java/jadx/api/impl/SimpleCodeWriter.java index 9e99b3f4..6f12e148 100644 --- a/jadx-core/src/main/java/jadx/api/impl/SimpleCodeWriter.java +++ b/jadx-core/src/main/java/jadx/api/impl/SimpleCodeWriter.java @@ -6,11 +6,11 @@ import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import jadx.api.CodePosition; import jadx.api.ICodeInfo; import jadx.api.ICodeWriter; import jadx.api.JadxArgs; -import jadx.core.dex.attributes.ILineAttributeNode; +import jadx.api.metadata.ICodeAnnotation; +import jadx.api.metadata.ICodeNodeRef; import jadx.core.utils.Utils; /** @@ -193,17 +193,22 @@ public class SimpleCodeWriter implements ICodeWriter { } @Override - public void attachDefinition(ILineAttributeNode obj) { + public int getLineStartPos() { + return 0; + } + + @Override + public void attachDefinition(ICodeNodeRef obj) { // no op } @Override - public void attachAnnotation(Object obj) { + public void attachAnnotation(ICodeAnnotation obj) { // no op } @Override - public void attachLineAnnotation(Object obj) { + public void attachLineAnnotation(ICodeAnnotation obj) { // no op } @@ -238,7 +243,7 @@ public class SimpleCodeWriter implements ICodeWriter { } @Override - public Map getRawAnnotations() { + public Map getRawAnnotations() { return Collections.emptyMap(); } diff --git a/jadx-core/src/main/java/jadx/api/metadata/ICodeAnnotation.java b/jadx-core/src/main/java/jadx/api/metadata/ICodeAnnotation.java new file mode 100644 index 00000000..70502670 --- /dev/null +++ b/jadx-core/src/main/java/jadx/api/metadata/ICodeAnnotation.java @@ -0,0 +1,16 @@ +package jadx.api.metadata; + +public interface ICodeAnnotation { + + enum AnnType { + CLASS, + FIELD, + METHOD, + VAR, + VAR_REF, + DECLARATION, + OFFSET + } + + AnnType getAnnType(); +} diff --git a/jadx-core/src/main/java/jadx/api/metadata/ICodeMetadata.java b/jadx-core/src/main/java/jadx/api/metadata/ICodeMetadata.java new file mode 100644 index 00000000..916eb44b --- /dev/null +++ b/jadx-core/src/main/java/jadx/api/metadata/ICodeMetadata.java @@ -0,0 +1,59 @@ +package jadx.api.metadata; + +import java.util.Map; +import java.util.function.BiFunction; + +import org.jetbrains.annotations.Nullable; + +import jadx.api.metadata.impl.CodeMetadataStorage; + +public interface ICodeMetadata { + + ICodeMetadata EMPTY = CodeMetadataStorage.empty(); + + @Nullable + ICodeAnnotation getAt(int position); + + @Nullable + ICodeAnnotation getClosestUp(int position); + + @Nullable + ICodeAnnotation searchUp(int position, ICodeAnnotation.AnnType annType); + + @Nullable + ICodeAnnotation searchUp(int position, int limitPos, ICodeAnnotation.AnnType annType); + + /** + * Iterate code annotations from {@code startPos} to smaller positions. + * + * @param visitor + * return not null value to stop iterations + */ + @Nullable + T searchUp(int startPos, BiFunction visitor); + + /** + * Iterate code annotations from {@code startPos} to higher positions. + * + * @param visitor + * return not null value to stop iterations + */ + @Nullable + T searchDown(int startPos, BiFunction visitor); + + /** + * Get current node at position (can be enclosing class or method) + */ + @Nullable + ICodeNodeRef getNodeAt(int position); + + /** + * Any definition of class or method below position + */ + @Nullable + ICodeNodeRef getNodeBelow(int position); + + Map getAsMap(); + + Map getLineMapping(); +} diff --git a/jadx-core/src/main/java/jadx/api/metadata/ICodeNodeRef.java b/jadx-core/src/main/java/jadx/api/metadata/ICodeNodeRef.java new file mode 100644 index 00000000..e7805f3c --- /dev/null +++ b/jadx-core/src/main/java/jadx/api/metadata/ICodeNodeRef.java @@ -0,0 +1,7 @@ +package jadx.api.metadata; + +public interface ICodeNodeRef extends ICodeAnnotation { + int getDefPosition(); + + void setDefPosition(int pos); +} diff --git a/jadx-core/src/main/java/jadx/api/data/annotations/InsnCodeOffset.java b/jadx-core/src/main/java/jadx/api/metadata/annotations/InsnCodeOffset.java similarity index 82% rename from jadx-core/src/main/java/jadx/api/data/annotations/InsnCodeOffset.java rename to jadx-core/src/main/java/jadx/api/metadata/annotations/InsnCodeOffset.java index 86c893de..4193caea 100644 --- a/jadx-core/src/main/java/jadx/api/data/annotations/InsnCodeOffset.java +++ b/jadx-core/src/main/java/jadx/api/metadata/annotations/InsnCodeOffset.java @@ -1,11 +1,12 @@ -package jadx.api.data.annotations; +package jadx.api.metadata.annotations; import org.jetbrains.annotations.Nullable; import jadx.api.ICodeWriter; +import jadx.api.metadata.ICodeAnnotation; import jadx.core.dex.nodes.InsnNode; -public class InsnCodeOffset implements ICodeRawOffset { +public class InsnCodeOffset implements ICodeAnnotation { public static void attach(ICodeWriter code, InsnNode insn) { if (insn == null) { @@ -40,11 +41,15 @@ public class InsnCodeOffset implements ICodeRawOffset { this.offset = offset; } - @Override public int getOffset() { return offset; } + @Override + public AnnType getAnnType() { + return AnnType.OFFSET; + } + @Override public String toString() { return "offset=" + offset; diff --git a/jadx-core/src/main/java/jadx/api/metadata/annotations/NodeDeclareRef.java b/jadx-core/src/main/java/jadx/api/metadata/annotations/NodeDeclareRef.java new file mode 100644 index 00000000..c956e70a --- /dev/null +++ b/jadx-core/src/main/java/jadx/api/metadata/annotations/NodeDeclareRef.java @@ -0,0 +1,39 @@ +package jadx.api.metadata.annotations; + +import java.util.Objects; + +import jadx.api.metadata.ICodeAnnotation; +import jadx.api.metadata.ICodeNodeRef; + +public class NodeDeclareRef implements ICodeAnnotation { + + private final ICodeNodeRef node; + + private int defPos; + + public NodeDeclareRef(ICodeNodeRef node) { + this.node = Objects.requireNonNull(node); + } + + public ICodeNodeRef getNode() { + return node; + } + + public int getDefPos() { + return defPos; + } + + public void setDefPos(int defPos) { + this.defPos = defPos; + } + + @Override + public AnnType getAnnType() { + return AnnType.DECLARATION; + } + + @Override + public String toString() { + return "NodeDeclareRef{" + node + '}'; + } +} diff --git a/jadx-core/src/main/java/jadx/api/metadata/annotations/VarNode.java b/jadx-core/src/main/java/jadx/api/metadata/annotations/VarNode.java new file mode 100644 index 00000000..35b593e6 --- /dev/null +++ b/jadx-core/src/main/java/jadx/api/metadata/annotations/VarNode.java @@ -0,0 +1,147 @@ +package jadx.api.metadata.annotations; + +import org.jetbrains.annotations.Nullable; + +import jadx.api.metadata.ICodeAnnotation; +import jadx.api.metadata.ICodeNodeRef; +import jadx.core.dex.instructions.args.ArgType; +import jadx.core.dex.instructions.args.CodeVar; +import jadx.core.dex.instructions.args.RegisterArg; +import jadx.core.dex.instructions.args.SSAVar; +import jadx.core.dex.nodes.MethodNode; + +/** + * Variable info + */ +public class VarNode implements ICodeNodeRef { + + @Nullable + public static VarNode get(MethodNode mth, RegisterArg reg) { + SSAVar ssaVar = reg.getSVar(); + if (ssaVar == null) { + return null; + } + return get(mth, ssaVar); + } + + @Nullable + public static VarNode get(MethodNode mth, CodeVar codeVar) { + return get(mth, codeVar.getAnySsaVar()); + } + + @Nullable + public static VarNode get(MethodNode mth, SSAVar ssaVar) { + CodeVar codeVar = ssaVar.getCodeVar(); + if (codeVar.isThis()) { + return null; + } + VarNode cachedVarNode = codeVar.getCachedVarNode(); + if (cachedVarNode != null) { + return cachedVarNode; + } + VarNode newVarNode = new VarNode(mth, ssaVar); + codeVar.setCachedVarNode(newVarNode); + return newVarNode; + } + + @Nullable + public static ICodeAnnotation getRef(MethodNode mth, RegisterArg reg) { + VarNode varNode = get(mth, reg); + if (varNode == null) { + return null; + } + return varNode.getVarRef(); + } + + private final MethodNode mth; + private final int reg; + private final int ssa; + private final ArgType type; + private @Nullable String name; + private int defPos; + + private final VarRef varRef; + + protected VarNode(MethodNode mth, SSAVar ssaVar) { + this(mth, ssaVar.getRegNum(), ssaVar.getVersion(), + ssaVar.getCodeVar().getType(), ssaVar.getCodeVar().getName()); + } + + public VarNode(MethodNode mth, int reg, int ssa, ArgType type, String name) { + this.mth = mth; + this.reg = reg; + this.ssa = ssa; + this.type = type; + this.name = name; + this.varRef = VarRef.fromVarNode(this); + } + + public MethodNode getMth() { + return mth; + } + + public int getReg() { + return reg; + } + + public int getSsa() { + return ssa; + } + + public ArgType getType() { + return type; + } + + @Nullable + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public VarRef getVarRef() { + return varRef; + } + + @Override + public int getDefPosition() { + return defPos; + } + + @Override + public void setDefPosition(int pos) { + this.defPos = pos; + } + + @Override + public AnnType getAnnType() { + return AnnType.VAR; + } + + @Override + public int hashCode() { + int h = 31 * getReg() + getSsa(); + return 31 * h + mth.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof VarNode)) { + return false; + } + VarNode other = (VarNode) o; + return getReg() == other.getReg() + && getSsa() == other.getSsa() + && getMth().equals(other.getMth()); + } + + @Override + public String toString() { + return "VarNode{r" + reg + 'v' + ssa + '}'; + } +} diff --git a/jadx-core/src/main/java/jadx/api/metadata/annotations/VarRef.java b/jadx-core/src/main/java/jadx/api/metadata/annotations/VarRef.java new file mode 100644 index 00000000..7f8be4a1 --- /dev/null +++ b/jadx-core/src/main/java/jadx/api/metadata/annotations/VarRef.java @@ -0,0 +1,68 @@ +package jadx.api.metadata.annotations; + +import jadx.api.metadata.ICodeAnnotation; + +/** + * Variable reference by position of VarNode in code metadata. + *
+ * Because on creation position not yet known, + * VarRef created using VarNode as a source of ref pos during serialization. + *
+ * On metadata deserialization created with ref pos directly. + */ +public abstract class VarRef implements ICodeAnnotation { + + public static VarRef fromPos(int refPos) { + if (refPos == 0) { + throw new IllegalArgumentException("Zero refPos"); + } + return new FixedVarRef(refPos); + } + + public static VarRef fromVarNode(VarNode varNode) { + return new RelatedVarRef(varNode); + } + + public abstract int getRefPos(); + + @Override + public AnnType getAnnType() { + return AnnType.VAR_REF; + } + + public static final class FixedVarRef extends VarRef { + private final int refPos; + + public FixedVarRef(int refPos) { + this.refPos = refPos; + } + + @Override + public int getRefPos() { + return refPos; + } + } + + public static final class RelatedVarRef extends VarRef { + private final VarNode varNode; + + public RelatedVarRef(VarNode varNode) { + this.varNode = varNode; + } + + @Override + public int getRefPos() { + return varNode.getDefPosition(); + } + + @Override + public String toString() { + return "VarRef{" + varNode + ", name=" + varNode.getName() + ", mth=" + varNode.getMth() + '}'; + } + } + + @Override + public String toString() { + return "VarRef{" + getRefPos() + '}'; + } +} diff --git a/jadx-core/src/main/java/jadx/api/metadata/impl/CodeMetadataStorage.java b/jadx-core/src/main/java/jadx/api/metadata/impl/CodeMetadataStorage.java new file mode 100644 index 00000000..de1d0e2f --- /dev/null +++ b/jadx-core/src/main/java/jadx/api/metadata/impl/CodeMetadataStorage.java @@ -0,0 +1,141 @@ +package jadx.api.metadata.impl; + +import java.util.Collections; +import java.util.Comparator; +import java.util.Map; +import java.util.NavigableMap; +import java.util.TreeMap; +import java.util.function.BiFunction; +import java.util.stream.Stream; + +import org.jetbrains.annotations.Nullable; + +import jadx.api.metadata.ICodeAnnotation; +import jadx.api.metadata.ICodeMetadata; +import jadx.api.metadata.ICodeNodeRef; +import jadx.api.metadata.annotations.NodeDeclareRef; +import jadx.core.dex.nodes.ClassNode; +import jadx.core.dex.nodes.MethodNode; +import jadx.core.utils.Utils; + +public class CodeMetadataStorage implements ICodeMetadata { + + public static ICodeMetadata build(Map lines, Map map) { + if (map.isEmpty() && lines.isEmpty()) { + return ICodeMetadata.EMPTY; + } + Comparator reverseCmp = Comparator.comparingInt(Integer::intValue).reversed(); + NavigableMap navMap = new TreeMap<>(reverseCmp); + navMap.putAll(map); + return new CodeMetadataStorage(lines, navMap); + } + + public static ICodeMetadata empty() { + return new CodeMetadataStorage(Collections.emptyMap(), Collections.emptyNavigableMap()); + } + + private final Map lines; + + private final NavigableMap navMap; + + private CodeMetadataStorage(Map lines, NavigableMap navMap) { + this.lines = lines; + this.navMap = navMap; + } + + @Override + public ICodeAnnotation getAt(int position) { + return navMap.get(position); + } + + @Override + public @Nullable ICodeAnnotation getClosestUp(int position) { + Map.Entry entryBefore = navMap.higherEntry(position); + return entryBefore != null ? entryBefore.getValue() : null; + } + + @Override + public @Nullable ICodeAnnotation searchUp(int position, ICodeAnnotation.AnnType annType) { + for (ICodeAnnotation v : navMap.tailMap(position, true).values()) { + if (v.getAnnType() == annType) { + return v; + } + } + return null; + } + + @Override + public @Nullable ICodeAnnotation searchUp(int position, int limitPos, ICodeAnnotation.AnnType annType) { + for (ICodeAnnotation v : navMap.subMap(position, true, limitPos, true).values()) { + if (v.getAnnType() == annType) { + return v; + } + } + return null; + } + + @Override + public @Nullable T searchUp(int startPos, BiFunction visitor) { + for (Map.Entry entry : navMap.tailMap(startPos, true).entrySet()) { + T value = visitor.apply(entry.getKey(), entry.getValue()); + if (value != null) { + return value; + } + } + return null; + } + + @Override + public @Nullable T searchDown(int startPos, BiFunction visitor) { + NavigableMap map = navMap.headMap(startPos, true).descendingMap(); + for (Map.Entry entry : map.entrySet()) { + T value = visitor.apply(entry.getKey(), entry.getValue()); + if (value != null) { + return value; + } + } + return null; + } + + @Override + public ICodeNodeRef getNodeAt(int position) { + return navMap.tailMap(position, true) + .values().stream() + .flatMap(CodeMetadataStorage::mapEnclosingNode) + .findFirst().orElse(null); + } + + @Override + public ICodeNodeRef getNodeBelow(int position) { + return navMap.headMap(position, true).descendingMap() + .values().stream() + .flatMap(CodeMetadataStorage::mapEnclosingNode) + .findFirst().orElse(null); + } + + private static Stream mapEnclosingNode(ICodeAnnotation ann) { + if (ann instanceof NodeDeclareRef) { + ICodeNodeRef node = ((NodeDeclareRef) ann).getNode(); + if (node instanceof ClassNode || node instanceof MethodNode) { + return Stream.of(node); + } + } + return Stream.empty(); + } + + @Override + public NavigableMap getAsMap() { + return navMap; + } + + @Override + public Map getLineMapping() { + return lines; + } + + @Override + public String toString() { + return "CodeMetadata{lines=" + lines + + ", annotations=\n" + Utils.listToString(navMap.entrySet(), "\n") + "\n}"; + } +} diff --git a/jadx-core/src/main/java/jadx/api/utils/CodeUtils.java b/jadx-core/src/main/java/jadx/api/utils/CodeUtils.java new file mode 100644 index 00000000..66ef4b2d --- /dev/null +++ b/jadx-core/src/main/java/jadx/api/utils/CodeUtils.java @@ -0,0 +1,38 @@ +package jadx.api.utils; + +import jadx.api.ICodeWriter; + +public class CodeUtils { + + public static String getLineForPos(String code, int pos) { + int start = getLineStartForPos(code, pos); + int end = getLineEndForPos(code, pos); + return code.substring(start, end); + } + + public static int getLineStartForPos(String code, int pos) { + String newLine = ICodeWriter.NL; + int start = code.lastIndexOf(newLine, pos); + return start == -1 ? 0 : start + newLine.length(); + } + + public static int getLineEndForPos(String code, int pos) { + int end = code.indexOf(ICodeWriter.NL, pos); + return end == -1 ? code.length() : end; + } + + public static int getLineNumForPos(String code, int pos) { + String newLine = ICodeWriter.NL; + int newLineLen = newLine.length(); + int line = 1; + int prev = 0; + while (true) { + int next = code.indexOf(newLine, prev); + if (next >= pos) { + return line; + } + prev = next + newLineLen; + line++; + } + } +} diff --git a/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java b/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java index fa847dca..e8fd8f9b 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java +++ b/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java @@ -11,9 +11,8 @@ import org.slf4j.LoggerFactory; import jadx.api.CommentsLevel; import jadx.api.ICodeWriter; -import jadx.api.data.annotations.InsnCodeOffset; -import jadx.api.data.annotations.VarDeclareRef; -import jadx.api.data.annotations.VarRef; +import jadx.api.metadata.annotations.InsnCodeOffset; +import jadx.api.metadata.annotations.VarNode; import jadx.api.plugins.input.data.MethodHandleType; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; @@ -109,7 +108,7 @@ public class InsnGen { if (arg.isRegister()) { RegisterArg reg = (RegisterArg) arg; if (code.isMetadataSupported()) { - code.attachAnnotation(VarRef.get(mth, reg)); + code.attachAnnotation(VarNode.getRef(mth, reg)); } code.add(mgen.getNameGen().useArg(reg)); } else if (arg.isLiteral()) { @@ -162,8 +161,15 @@ public class InsnGen { } useType(code, codeVar.getType()); code.add(' '); + defVar(code, codeVar); + } + + /** + * Variable definition without type, only var name + */ + private void defVar(ICodeWriter code, CodeVar codeVar) { if (code.isMetadataSupported()) { - code.attachDefinition(VarDeclareRef.get(mth, codeVar)); + code.attachDefinition(VarNode.get(mth, codeVar)); } code.add(mgen.getNameGen().assignArg(codeVar)); } @@ -939,7 +945,7 @@ public class InsnGen { code.add(", "); } CodeVar argCodeVar = callArgs.get(i).getSVar().getCodeVar(); - code.add(nameGen.assignArg(argCodeVar)); + defVar(code, argCodeVar); } } // force set external arg names into call method args @@ -947,7 +953,8 @@ public class InsnGen { int startArg = customNode.getHandleType() == MethodHandleType.INVOKE_STATIC ? 0 : 1; // skip 'this' arg for (int i = startArg; i < extArgsCount; i++) { RegisterArg extArg = (RegisterArg) customNode.getArg(i); - callArgs.get(i).setName(extArg.getName()); + RegisterArg callRegArg = callArgs.get(i); + callRegArg.getSVar().setCodeVar(extArg.getSVar().getCodeVar()); } code.add(" -> {"); code.incIndent(); diff --git a/jadx-core/src/main/java/jadx/core/codegen/MethodGen.java b/jadx-core/src/main/java/jadx/core/codegen/MethodGen.java index 1fcd189f..3266d76e 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/MethodGen.java +++ b/jadx-core/src/main/java/jadx/core/codegen/MethodGen.java @@ -13,8 +13,8 @@ import org.slf4j.LoggerFactory; import jadx.api.CommentsLevel; import jadx.api.ICodeWriter; import jadx.api.JadxArgs; -import jadx.api.data.annotations.InsnCodeOffset; -import jadx.api.data.annotations.VarDeclareRef; +import jadx.api.metadata.annotations.InsnCodeOffset; +import jadx.api.metadata.annotations.VarNode; import jadx.api.plugins.input.data.AccessFlags; import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.api.plugins.input.data.attributes.JadxAttrType; @@ -243,7 +243,7 @@ public class MethodGen { code.add(' '); String varName = nameGen.assignArg(var); if (code.isMetadataSupported() && ssaVar != null /* for fallback mode */) { - code.attachDefinition(VarDeclareRef.get(mth, var)); + code.attachDefinition(VarNode.get(mth, var)); } code.add(varName); diff --git a/jadx-core/src/main/java/jadx/core/codegen/RegionGen.java b/jadx-core/src/main/java/jadx/core/codegen/RegionGen.java index 41843187..03bd5e73 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/RegionGen.java +++ b/jadx-core/src/main/java/jadx/core/codegen/RegionGen.java @@ -10,8 +10,8 @@ import org.slf4j.LoggerFactory; import jadx.api.CommentsLevel; import jadx.api.ICodeWriter; -import jadx.api.data.annotations.InsnCodeOffset; -import jadx.api.data.annotations.VarDeclareRef; +import jadx.api.metadata.annotations.InsnCodeOffset; +import jadx.api.metadata.annotations.VarNode; import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.api.plugins.input.data.attributes.JadxAttrType; import jadx.core.dex.attributes.AFlag; @@ -26,6 +26,7 @@ import jadx.core.dex.instructions.args.CodeVar; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.NamedArg; import jadx.core.dex.instructions.args.RegisterArg; +import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.IBlock; @@ -346,11 +347,11 @@ public class RegionGen extends InsnGen { if (arg == null) { code.add("unknown"); // throwing exception is too late at this point } else if (arg instanceof RegisterArg) { - CodeVar codeVar = ((RegisterArg) arg).getSVar().getCodeVar(); + SSAVar ssaVar = ((RegisterArg) arg).getSVar(); if (code.isMetadataSupported()) { - code.attachDefinition(VarDeclareRef.get(mth, codeVar)); + code.attachDefinition(VarNode.get(mth, ssaVar)); } - code.add(mgen.getNameGen().assignArg(codeVar)); + code.add(mgen.getNameGen().assignArg(ssaVar.getCodeVar())); } else if (arg instanceof NamedArg) { code.add(mgen.getNameGen().assignNamedArg((NamedArg) arg)); } else { diff --git a/jadx-core/src/main/java/jadx/core/codegen/json/JsonCodeGen.java b/jadx-core/src/main/java/jadx/core/codegen/json/JsonCodeGen.java index ad7148dd..ccef0bda 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/json/JsonCodeGen.java +++ b/jadx-core/src/main/java/jadx/core/codegen/json/JsonCodeGen.java @@ -12,13 +12,13 @@ import com.google.gson.FieldNamingPolicy; import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import jadx.api.CodePosition; import jadx.api.ICodeInfo; import jadx.api.ICodeWriter; import jadx.api.JadxArgs; -import jadx.api.data.annotations.InsnCodeOffset; import jadx.api.impl.AnnotatedCodeWriter; import jadx.api.impl.SimpleCodeWriter; +import jadx.api.metadata.ICodeMetadata; +import jadx.api.metadata.annotations.InsnCodeOffset; import jadx.core.codegen.ClassGen; import jadx.core.codegen.MethodGen; import jadx.core.codegen.json.cls.JsonClass; @@ -180,24 +180,27 @@ public class JsonCodeGen { } String[] lines = codeStr.split(ICodeWriter.NL); - Map lineMapping = code.getLineMapping(); - Map annotations = code.getAnnotations(); + Map lineMapping = code.getCodeMetadata().getLineMapping(); + ICodeMetadata metadata = code.getCodeMetadata(); long mthCodeOffset = mth.getMethodCodeOffset() + 16; int linesCount = lines.length; List codeLines = new ArrayList<>(linesCount); + int lineStartPos = 0; + int newLineLen = ICodeWriter.NL.length(); for (int i = 0; i < linesCount; i++) { String codeLine = lines[i]; int line = i + 2; JsonCodeLine jsonCodeLine = new JsonCodeLine(); jsonCodeLine.setCode(codeLine); jsonCodeLine.setSourceLine(lineMapping.get(line)); - Object obj = annotations.get(new CodePosition(line)); + Object obj = metadata.getAt(lineStartPos); if (obj instanceof InsnCodeOffset) { long offset = ((InsnCodeOffset) obj).getOffset(); jsonCodeLine.setOffset("0x" + Long.toHexString(mthCodeOffset + offset * 2)); } codeLines.add(jsonCodeLine); + lineStartPos += codeLine.length() + newLineLen; } return codeLines; } diff --git a/jadx-core/src/main/java/jadx/core/dex/attributes/ILineAttributeNode.java b/jadx-core/src/main/java/jadx/core/dex/attributes/ILineAttributeNode.java index 2df6770b..ddf75067 100644 --- a/jadx-core/src/main/java/jadx/core/dex/attributes/ILineAttributeNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/attributes/ILineAttributeNode.java @@ -5,10 +5,6 @@ public interface ILineAttributeNode { void setSourceLine(int sourceLine); - int getDecompiledLine(); - - void setDecompiledLine(int line); - int getDefPosition(); void setDefPosition(int pos); diff --git a/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/LineAttrNode.java b/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/LineAttrNode.java index d7348a4b..4e46fec9 100644 --- a/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/LineAttrNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/LineAttrNode.java @@ -7,21 +7,11 @@ public abstract class LineAttrNode extends AttrNode implements ILineAttributeNod private int sourceLine; - private int decompiledLine; - - // the position exactly where a node declared at in decompiled java code. + /** + * Position where a node declared at in decompiled code + */ private int defPosition; - @Override - public int getDefPosition() { - return this.defPosition; - } - - @Override - public void setDefPosition(int defPosition) { - this.defPosition = defPosition; - } - @Override public int getSourceLine() { return sourceLine; @@ -33,13 +23,13 @@ public abstract class LineAttrNode extends AttrNode implements ILineAttributeNod } @Override - public int getDecompiledLine() { - return decompiledLine; + public int getDefPosition() { + return this.defPosition; } @Override - public void setDecompiledLine(int decompiledLine) { - this.decompiledLine = decompiledLine; + public void setDefPosition(int defPosition) { + this.defPosition = defPosition; } public void addSourceLineFrom(LineAttrNode lineAttrNode) { @@ -50,6 +40,6 @@ public abstract class LineAttrNode extends AttrNode implements ILineAttributeNod public void copyLines(LineAttrNode lineAttrNode) { setSourceLine(lineAttrNode.getSourceLine()); - setDecompiledLine(lineAttrNode.getDecompiledLine()); + setDefPosition(lineAttrNode.getDefPosition()); } } diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/args/CodeVar.java b/jadx-core/src/main/java/jadx/core/dex/instructions/args/CodeVar.java index a4411d29..b74dd59a 100644 --- a/jadx-core/src/main/java/jadx/core/dex/instructions/args/CodeVar.java +++ b/jadx-core/src/main/java/jadx/core/dex/instructions/args/CodeVar.java @@ -4,7 +4,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import jadx.api.data.annotations.VarRef; +import jadx.api.metadata.annotations.VarNode; public class CodeVar { private String name; @@ -15,7 +15,7 @@ public class CodeVar { private boolean isThis; private boolean isDeclared; - private VarRef cachedVarRef; // set and used at codegen stage + private VarNode cachedVarNode; // set and used at codegen stage public static CodeVar fromMthArg(RegisterArg mthArg, boolean linkRegister) { CodeVar var = new CodeVar(); @@ -94,12 +94,12 @@ public class CodeVar { isDeclared = declared; } - public VarRef getCachedVarRef() { - return cachedVarRef; + public VarNode getCachedVarNode() { + return cachedVarNode; } - public void setCachedVarRef(VarRef cachedVarRef) { - this.cachedVarRef = cachedVarRef; + public void setCachedVarNode(VarNode varNode) { + this.cachedVarNode = varNode; } /** diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java index 79387204..811ea2ad 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java @@ -374,7 +374,7 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN String clsRawName = getRawName(); if (searchInCache) { ICodeInfo code = codeCache.get(clsRawName); - if (code != null && code != ICodeInfo.EMPTY) { + if (code != ICodeInfo.EMPTY) { return code; } } @@ -817,6 +817,11 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN return clsData == null ? "synthetic" : clsData.getInputFileName(); } + @Override + public AnnType getAnnType() { + return AnnType.CLASS; + } + @Override public int hashCode() { return clsInfo.hashCode(); diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/FieldNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/FieldNode.java index da68768e..f2a7d2b9 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/FieldNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/FieldNode.java @@ -100,6 +100,11 @@ public class FieldNode extends NotificationAttrNode implements ICodeNode { return parentClass.root(); } + @Override + public AnnType getAnnType() { + return AnnType.FIELD; + } + @Override public int hashCode() { return fieldInfo.hashCode(); diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/ICodeNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/ICodeNode.java index e131e2ed..5f7d8d7f 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/ICodeNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/ICodeNode.java @@ -1,9 +1,10 @@ package jadx.core.dex.nodes; +import jadx.api.metadata.ICodeNodeRef; import jadx.core.dex.attributes.IAttributeNode; import jadx.core.dex.info.AccessInfo; -public interface ICodeNode extends IDexNode, IAttributeNode, IUsageInfoNode { +public interface ICodeNode extends IDexNode, IAttributeNode, IUsageInfoNode, ICodeNodeRef { AccessInfo getAccessFlags(); void setAccessFlags(AccessInfo newAccessFlags); diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/MethodNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/MethodNode.java index 6c792437..430abaeb 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/MethodNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/MethodNode.java @@ -592,6 +592,11 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails, this.useIn = useIn; } + @Override + public AnnType getAnnType() { + return AnnType.METHOD; + } + @Override public int hashCode() { return mthInfo.hashCode(); diff --git a/jadx-core/src/main/java/jadx/core/utils/CodeGenUtils.java b/jadx-core/src/main/java/jadx/core/utils/CodeGenUtils.java index bdc6df38..294bdbba 100644 --- a/jadx-core/src/main/java/jadx/core/utils/CodeGenUtils.java +++ b/jadx-core/src/main/java/jadx/core/utils/CodeGenUtils.java @@ -4,9 +4,9 @@ import java.util.List; import org.jetbrains.annotations.Nullable; -import jadx.api.CodePosition; import jadx.api.CommentsLevel; import jadx.api.ICodeWriter; +import jadx.api.metadata.ICodeAnnotation; import jadx.api.plugins.input.data.attributes.JadxAttrType; import jadx.api.plugins.input.data.attributes.types.SourceFileAttr; import jadx.core.dex.attributes.AType; @@ -95,7 +95,7 @@ public class CodeGenUtils { private static void addMultiLineComment(ICodeWriter code, List comments) { boolean first = true; String indent = ""; - Object lineAnn = null; + ICodeAnnotation lineAnn = null; for (String comment : comments) { for (String line : comment.split("\n")) { if (first) { @@ -104,7 +104,7 @@ public class CodeGenUtils { int startLinePos = buf.lastIndexOf(ICodeWriter.NL) + 1; indent = Utils.strRepeat(" ", buf.length() - startLinePos); if (code.isMetadataSupported()) { - lineAnn = code.getRawAnnotations().get(new CodePosition(code.getLine())); + lineAnn = code.getRawAnnotations().get(startLinePos); } } else { code.newLine().add(indent); diff --git a/jadx-core/src/main/java/jadx/core/utils/files/FileUtils.java b/jadx-core/src/main/java/jadx/core/utils/files/FileUtils.java index c09b76b8..3c197936 100644 --- a/jadx-core/src/main/java/jadx/core/utils/files/FileUtils.java +++ b/jadx-core/src/main/java/jadx/core/utils/files/FileUtils.java @@ -8,16 +8,19 @@ import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.nio.charset.StandardCharsets; import java.nio.file.FileVisitOption; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.security.MessageDigest; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Objects; import java.util.jar.JarEntry; import java.util.jar.JarOutputStream; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.jetbrains.annotations.NotNull; @@ -110,6 +113,12 @@ public class FileUtils { return dir.delete(); } + public static void deleteDirIfExists(Path dir) { + if (Files.exists(dir)) { + deleteDir(dir); + } + } + public static void deleteDir(Path dir) { try (Stream pathStream = Files.walk(dir)) { pathStream.sorted(Comparator.reverseOrder()) @@ -230,18 +239,19 @@ public class FileUtils { return new File(file.getParentFile(), name); } - private static String bytesToHex(byte[] bytes) { - char[] hexArray = "0123456789abcdef".toCharArray(); - if (bytes == null || bytes.length <= 0) { - return null; + private static final byte[] HEX_ARRAY = "0123456789abcdef".getBytes(StandardCharsets.US_ASCII); + + public static String bytesToHex(byte[] bytes) { + if (bytes == null || bytes.length == 0) { + return ""; } - char[] hexChars = new char[bytes.length * 2]; + byte[] hexChars = new byte[bytes.length * 2]; for (int j = 0; j < bytes.length; j++) { int v = bytes[j] & 0xFF; - hexChars[j * 2] = hexArray[v >>> 4]; - hexChars[j * 2 + 1] = hexArray[v & 0x0F]; + hexChars[j * 2] = HEX_ARRAY[v >>> 4]; + hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; } - return new String(hexChars); + return new String(hexChars, StandardCharsets.UTF_8); } public static boolean isZipFile(File file) { @@ -275,4 +285,30 @@ public class FileUtils { } return new File(path); } + + public static List toPaths(List files) { + return files.stream().map(File::toPath).collect(Collectors.toList()); + } + + public static List toPaths(File[] files) { + return Stream.of(files).map(File::toPath).collect(Collectors.toList()); + } + + public static List fileNamesToPaths(List fileNames) { + return fileNames.stream().map(Paths::get).collect(Collectors.toList()); + } + + public static List toFiles(List paths) { + return paths.stream().map(Path::toFile).collect(Collectors.toList()); + } + + public static String md5Sum(byte[] data) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + md.update(data); + return bytesToHex(md.digest()); + } catch (Exception e) { + throw new JadxRuntimeException("Failed to build hash", e); + } + } } diff --git a/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java b/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java index 16d70bf3..620fd36d 100644 --- a/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java +++ b/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java @@ -32,7 +32,6 @@ import org.junit.jupiter.api.BeforeEach; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import jadx.api.CodePosition; import jadx.api.CommentsLevel; import jadx.api.ICodeInfo; import jadx.api.ICodeWriter; @@ -41,7 +40,8 @@ import jadx.api.JadxDecompiler; import jadx.api.JadxInternalAccess; import jadx.api.JavaClass; import jadx.api.args.DeobfuscationMapFileMode; -import jadx.api.data.annotations.InsnCodeOffset; +import jadx.api.metadata.ICodeMetadata; +import jadx.api.metadata.annotations.InsnCodeOffset; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.IAttributeNode; @@ -300,7 +300,7 @@ public abstract class IntegrationTest extends TestUtils { private void printCodeWithLineNumbers(ICodeInfo code) { String codeStr = code.getCodeStr(); - Map lineMapping = code.getLineMapping(); + Map lineMapping = code.getCodeMetadata().getLineMapping(); String[] lines = codeStr.split(ICodeWriter.NL); for (int i = 0; i < lines.length; i++) { String line = lines[i]; @@ -316,18 +316,18 @@ public abstract class IntegrationTest extends TestUtils { private void printCodeWithOffsets(ICodeInfo code) { String codeStr = code.getCodeStr(); - Map annotations = code.getAnnotations(); - String[] lines = codeStr.split(ICodeWriter.NL); - for (int i = 0; i < lines.length; i++) { - String line = lines[i]; - int curLine = i + 1; - Object ann = annotations.get(new CodePosition(curLine, 0)); + ICodeMetadata metadata = code.getCodeMetadata(); + int lineStartPos = 0; + int newLineLen = ICodeWriter.NL.length(); + for (String line : codeStr.split(ICodeWriter.NL)) { + Object ann = metadata.getAt(lineStartPos); String offsetStr = ""; if (ann instanceof InsnCodeOffset) { int offset = ((InsnCodeOffset) ann).getOffset(); offsetStr = "/* " + leftPad(String.valueOf(offset), 5) + " */"; } System.out.println(rightPad(offsetStr, 12) + line); + lineStartPos += line.length() + newLineLen; } } diff --git a/jadx-core/src/test/java/jadx/tests/api/utils/assertj/JadxClassNodeAssertions.java b/jadx-core/src/test/java/jadx/tests/api/utils/assertj/JadxClassNodeAssertions.java index e1cec9ef..b273a8f7 100644 --- a/jadx-core/src/test/java/jadx/tests/api/utils/assertj/JadxClassNodeAssertions.java +++ b/jadx-core/src/test/java/jadx/tests/api/utils/assertj/JadxClassNodeAssertions.java @@ -5,8 +5,8 @@ import java.util.Map; import org.assertj.core.api.AbstractObjectAssert; import org.assertj.core.api.Assertions; -import jadx.api.CodePosition; import jadx.api.ICodeInfo; +import jadx.api.metadata.ICodeAnnotation; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.ICodeNode; import jadx.tests.api.IntegrationTest; @@ -67,8 +67,8 @@ public class JadxClassNodeAssertions extends AbstractObjectAssert entry : code.getAnnotations().entrySet()) { - if (entry.getKey().getPos() == refPos) { + for (Map.Entry entry : code.getCodeMetadata().getAsMap().entrySet()) { + if (entry.getKey() == refPos) { Assertions.assertThat(entry.getValue()).isEqualTo(node); return; } diff --git a/jadx-core/src/test/java/jadx/tests/api/utils/assertj/JadxCodeInfoAssertions.java b/jadx-core/src/test/java/jadx/tests/api/utils/assertj/JadxCodeInfoAssertions.java index 1fd5509d..e6162e0f 100644 --- a/jadx-core/src/test/java/jadx/tests/api/utils/assertj/JadxCodeInfoAssertions.java +++ b/jadx-core/src/test/java/jadx/tests/api/utils/assertj/JadxCodeInfoAssertions.java @@ -5,7 +5,7 @@ import java.util.stream.Collectors; import org.assertj.core.api.AbstractObjectAssert; import jadx.api.ICodeInfo; -import jadx.api.data.annotations.ICodeRawOffset; +import jadx.api.metadata.annotations.InsnCodeOffset; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; @@ -22,9 +22,9 @@ public class JadxCodeInfoAssertions extends AbstractObjectAssert ((ICodeRawOffset) o).getOffset(), Collectors.toList())) + long dupOffsetCount = actual.getCodeMetadata().getAsMap().values().stream() + .filter(InsnCodeOffset.class::isInstance) + .collect(Collectors.groupingBy(o -> ((InsnCodeOffset) o).getOffset(), Collectors.toList())) .values().stream() .filter(list -> list.size() > 1) .count(); diff --git a/jadx-core/src/test/java/jadx/tests/external/BaseExternalTest.java b/jadx-core/src/test/java/jadx/tests/external/BaseExternalTest.java index 12584bbf..de9fb6bb 100644 --- a/jadx-core/src/test/java/jadx/tests/external/BaseExternalTest.java +++ b/jadx-core/src/test/java/jadx/tests/external/BaseExternalTest.java @@ -1,20 +1,19 @@ package jadx.tests.external; import java.io.File; -import java.util.HashMap; -import java.util.Map; -import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.CommentsLevel; +import jadx.api.ICodeInfo; import jadx.api.ICodeWriter; import jadx.api.JadxArgs; import jadx.api.JadxDecompiler; import jadx.api.JadxInternalAccess; +import jadx.api.metadata.ICodeNodeRef; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; @@ -127,37 +126,15 @@ public abstract class BaseExternalTest extends IntegrationTest { } private void printMethods(ClassNode classNode, @NotNull String mthPattern) { - String code = classNode.getCode().getCodeStr(); + ICodeInfo codeInfo = classNode.getCode(); + String code = codeInfo.getCodeStr(); if (code == null) { return; } - String dashLine = "======================================================================================"; - Map methodsMap = getMethodsMap(classNode); - String[] lines = code.split(ICodeWriter.NL); for (MethodNode mth : classNode.getMethods()) { if (isMthMatch(mth, mthPattern)) { - int decompiledLine = mth.getDecompiledLine() - 1; - StringBuilder mthCode = new StringBuilder(); - int startLine = getCommentLinesCount(lines, decompiledLine); - int brackets = 0; - for (int i = startLine; i > 0 && i < lines.length; i++) { - // stop if next method started - MethodNode mthAtLine = methodsMap.get(i); - if (mthAtLine != null && !mthAtLine.equals(mth)) { - break; - } - String line = lines[i]; - mthCode.append(line).append(ICodeWriter.NL); - // also count brackets for detect method end - if (i >= decompiledLine) { - brackets += StringUtils.countMatches(line, '{'); - brackets -= StringUtils.countMatches(line, '}'); - if (brackets <= 0) { - break; - } - } - } + String mthCode = cutMethodCode(codeInfo, code, mth); LOG.info("Print method: {}\n{}\n{}\n{}", mth.getMethodInfo().getShortId(), dashLine, mthCode, @@ -166,22 +143,37 @@ public abstract class BaseExternalTest extends IntegrationTest { } } - public Map getMethodsMap(ClassNode classNode) { - Map linesMap = new HashMap<>(); - for (MethodNode method : classNode.getMethods()) { - linesMap.put(method.getDecompiledLine() - 1, method); + @NotNull + private String cutMethodCode(ICodeInfo codeInfo, String code, MethodNode mth) { + int defPos = mth.getDefPosition(); + int startPos = getCommentStartPos(code, defPos); + ICodeNodeRef nodeBelow = codeInfo.getCodeMetadata().getNodeBelow(defPos); + int stopPos = nodeBelow == null ? code.length() : nodeBelow.getDefPosition(); + int brackets = 0; + StringBuilder mthCode = new StringBuilder(); + for (int i = startPos; i > 0 && i < stopPos;) { + int codePoint = code.codePointAt(i); + mthCode.appendCodePoint(codePoint); + if (i >= defPos) { + // also count brackets for detect method end + if (codePoint == '{') { + brackets++; + } else if (codePoint == '}') { + brackets--; + if (brackets <= 0) { + break; + } + } + } + i += Character.charCount(codePoint); } - return linesMap; + return mthCode.toString(); } - protected int getCommentLinesCount(String[] lines, int line) { - for (int i = line - 1; i > 0 && i < lines.length; i--) { - String str = lines[i]; - if (str.isEmpty() || str.equals(ICodeWriter.NL)) { - return i + 1; - } - } - return 0; + protected int getCommentStartPos(String code, int pos) { + String emptyLine = ICodeWriter.NL + ICodeWriter.NL; + int emptyLinePos = code.lastIndexOf(emptyLine, pos); + return emptyLinePos == -1 ? pos : emptyLinePos + emptyLine.length(); } private void printErrorReport(JadxDecompiler jadx) { diff --git a/jadx-core/src/test/java/jadx/tests/integration/debuginfo/TestLineNumbers.java b/jadx-core/src/test/java/jadx/tests/integration/debuginfo/TestLineNumbers.java index 454d8807..5d0be472 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/debuginfo/TestLineNumbers.java +++ b/jadx-core/src/test/java/jadx/tests/integration/debuginfo/TestLineNumbers.java @@ -2,7 +2,7 @@ package jadx.tests.integration.debuginfo; import org.junit.jupiter.api.Test; -import jadx.api.ICodeWriter; +import jadx.api.utils.CodeUtils; import jadx.core.dex.attributes.nodes.LineAttrNode; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; @@ -42,6 +42,7 @@ public class TestLineNumbers extends IntegrationTest { @Test public void test() { + printLineNumbers(); ClassNode cls = getClassNode(TestCls.class); String code = cls.getCode().toString(); @@ -61,19 +62,17 @@ public class TestLineNumbers extends IntegrationTest { assertEquals(testClassLine + 20, innerFunc3.getSourceLine()); // check decompiled lines - String[] lines = code.split(ICodeWriter.NL); - checkLine(lines, field, "int field;"); - checkLine(lines, func, "public void func() {"); - checkLine(lines, inner, "public static class Inner {"); - checkLine(lines, innerField, "int innerField;"); - checkLine(lines, innerFunc, "public void innerFunc() {"); - checkLine(lines, innerFunc2, "public void innerFunc2() {"); - checkLine(lines, innerFunc3, "public void innerFunc3() {"); + checkLine(code, field, "int field;"); + checkLine(code, func, "public void func() {"); + checkLine(code, inner, "public static class Inner {"); + checkLine(code, innerField, "int innerField;"); + checkLine(code, innerFunc, "public void innerFunc() {"); + checkLine(code, innerFunc2, "public void innerFunc2() {"); + checkLine(code, innerFunc3, "public void innerFunc3() {"); } - private static void checkLine(String[] lines, LineAttrNode node, String str) { - int lineNumber = node.getDecompiledLine(); - String line = lines[lineNumber - 1]; + private static void checkLine(String code, LineAttrNode node, String str) { + String line = CodeUtils.getLineForPos(code, node.getDefPosition()); assertThat(line, containsString(str)); } } diff --git a/jadx-core/src/test/java/jadx/tests/integration/debuginfo/TestLineNumbers2.java b/jadx-core/src/test/java/jadx/tests/integration/debuginfo/TestLineNumbers2.java index 895b740c..a7bac343 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/debuginfo/TestLineNumbers2.java +++ b/jadx-core/src/test/java/jadx/tests/integration/debuginfo/TestLineNumbers2.java @@ -37,7 +37,7 @@ public class TestLineNumbers2 extends IntegrationTest { printLineNumbers(); ClassNode cls = getClassNode(TestCls.class); - String linesMapStr = cls.getCode().getLineMapping().toString(); + String linesMapStr = cls.getCode().getCodeMetadata().getLineMapping().toString(); if (isJavaInput()) { assertEquals("{6=16, 9=17, 12=21, 13=22, 14=23, 15=24, 16=25, 18=27, 21=30, 22=31}", linesMapStr); } else { diff --git a/jadx-core/src/test/java/jadx/tests/integration/debuginfo/TestLineNumbers3.java b/jadx-core/src/test/java/jadx/tests/integration/debuginfo/TestLineNumbers3.java index e71dc30e..25f96107 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/debuginfo/TestLineNumbers3.java +++ b/jadx-core/src/test/java/jadx/tests/integration/debuginfo/TestLineNumbers3.java @@ -33,7 +33,7 @@ public class TestLineNumbers3 extends IntegrationTest { public void test() { ClassNode cls = getClassNode(TestCls.class); assertThat(cls).code().containsOne("super(message == null ? \"\" : message.toString());"); - String linesMapStr = cls.getCode().getLineMapping().toString(); + String linesMapStr = cls.getCode().getCodeMetadata().getLineMapping().toString(); assertThat(linesMapStr).isEqualTo("{4=13, 5=14, 6=15}"); } } diff --git a/jadx-core/src/test/java/jadx/tests/integration/debuginfo/TestReturnSourceLine.java b/jadx-core/src/test/java/jadx/tests/integration/debuginfo/TestReturnSourceLine.java index 5f353d99..84207f74 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/debuginfo/TestReturnSourceLine.java +++ b/jadx-core/src/test/java/jadx/tests/integration/debuginfo/TestReturnSourceLine.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.Test; import jadx.api.ICodeInfo; import jadx.api.ICodeWriter; +import jadx.api.utils.CodeUtils; import jadx.core.dex.attributes.nodes.LineAttrNode; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.MethodNode; @@ -71,9 +72,10 @@ public class TestReturnSourceLine extends IntegrationTest { } private static void checkLine(String[] lines, ICodeInfo cw, LineAttrNode node, int offset, String str) { - int decompiledLine = node.getDecompiledLine() + offset; + int nodeDefLine = CodeUtils.getLineNumForPos(cw.getCodeStr(), node.getDefPosition()); + int decompiledLine = nodeDefLine + offset; assertThat(lines[decompiledLine - 1], containsOne(str)); - Integer sourceLine = cw.getLineMapping().get(decompiledLine); + Integer sourceLine = cw.getCodeMetadata().getLineMapping().get(decompiledLine); assertNotNull(sourceLine); assertEquals(node.getSourceLine() + offset, (int) sourceLine); } diff --git a/jadx-core/src/test/java/jadx/tests/integration/java8/TestLambdaExtVar.java b/jadx-core/src/test/java/jadx/tests/integration/java8/TestLambdaExtVar.java new file mode 100644 index 00000000..285d8371 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/java8/TestLambdaExtVar.java @@ -0,0 +1,39 @@ +package jadx.tests.integration.java8; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import jadx.core.dex.nodes.ClassNode; +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 TestLambdaExtVar extends IntegrationTest { + + public static class TestCls { + + public void test(List list, String str) { + list.removeIf(s -> s.equals(str)); + } + + public void check() { + List list = new ArrayList<>(Arrays.asList("a", "str", "b")); + test(list, "str"); + assertThat(list).isEqualTo(Arrays.asList("a", "b")); + } + } + + @TestWithProfiles(TestProfile.DX_J8) + public void test() { + ClassNode cls = getClassNode(TestCls.class); + assertThat(cls) + .code() + .doesNotContain("lambda$") + .containsOne("return s.equals(str);"); // TODO: simplify to expression + + System.out.println(cls.getCode().getCodeMetadata()); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/others/TestCodeMetadata.java b/jadx-core/src/test/java/jadx/tests/integration/others/TestCodeMetadata.java new file mode 100644 index 00000000..d569b593 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/others/TestCodeMetadata.java @@ -0,0 +1,66 @@ +package jadx.tests.integration.others; + +import java.util.List; +import java.util.Objects; + +import org.junit.jupiter.api.Test; + +import jadx.api.JavaClass; +import jadx.api.JavaMethod; +import jadx.api.metadata.ICodeAnnotation; +import jadx.api.metadata.ICodeMetadata; +import jadx.api.metadata.ICodeNodeRef; +import jadx.core.dex.nodes.ClassNode; +import jadx.core.dex.nodes.FieldNode; +import jadx.core.dex.nodes.MethodNode; +import jadx.tests.api.IntegrationTest; + +import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; + +public class TestCodeMetadata extends IntegrationTest { + + public static class TestCls { + public static class A { + public String str; + } + + public String test() { + A a = new A(); + a.str = call(); + return a.str; + } + + public static String call() { + return "str"; + } + } + + @Test + public void test() { + ClassNode cls = getClassNode(TestCls.class); + assertThat(cls).code().containsOne("return a.str;"); + + MethodNode testMth = getMethod(cls, "test"); + MethodNode callMth = getMethod(cls, "call"); + + int callDefPos = callMth.getDefPosition(); + assertThat(callDefPos).isNotZero(); + + JavaClass javaClass = Objects.requireNonNull(jadxDecompiler.getJavaClassByNode(cls)); + JavaMethod callJavaMethod = Objects.requireNonNull(jadxDecompiler.getJavaMethodByNode(callMth)); + List callUsePlaces = javaClass.getUsePlacesFor(javaClass.getCodeInfo(), callJavaMethod); + assertThat(callUsePlaces).hasSize(1); + int callUse = callUsePlaces.get(0); + + ICodeMetadata metadata = cls.getCode().getCodeMetadata(); + ICodeNodeRef callDef = metadata.getNodeAt(callUse); + assertThat(callDef).isSameAs(testMth); + + int beforeCallDef = callDefPos - 10; + ICodeAnnotation closest = metadata.getClosestUp(beforeCallDef); + assertThat(closest).isInstanceOf(FieldNode.class); // field reference from 'return a.str;' + + ICodeNodeRef nodeBelow = metadata.getNodeBelow(beforeCallDef); + assertThat(nodeBelow).isSameAs(callMth); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/variables/TestVariablesDeclAnnotation.java b/jadx-core/src/test/java/jadx/tests/integration/variables/TestVariablesDeclAnnotation.java index ac1ab186..541632ed 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/variables/TestVariablesDeclAnnotation.java +++ b/jadx-core/src/test/java/jadx/tests/integration/variables/TestVariablesDeclAnnotation.java @@ -1,12 +1,15 @@ package jadx.tests.integration.variables; -import java.util.Comparator; +import java.util.ArrayList; import java.util.List; -import java.util.stream.Collectors; import org.junit.jupiter.api.Test; -import jadx.api.data.annotations.VarDeclareRef; +import jadx.api.ICodeInfo; +import jadx.api.metadata.ICodeNodeRef; +import jadx.api.metadata.annotations.NodeDeclareRef; +import jadx.api.metadata.annotations.VarNode; +import jadx.api.utils.CodeUtils; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.MethodNode; import jadx.tests.api.IntegrationTest; @@ -39,14 +42,27 @@ public class TestVariablesDeclAnnotation extends IntegrationTest { MethodNode testMth = cls.searchMethodByShortName(mthName); assertThat(testMth).isNotNull(); - int mthLine = testMth.getDecompiledLine(); - List argNames = cls.getCode().getAnnotations().entrySet().stream() - .filter(e -> e.getKey().getLine() == mthLine && e.getValue() instanceof VarDeclareRef) - .sorted(Comparator.comparingInt(e -> e.getKey().getPos())) - .map(e -> ((VarDeclareRef) e.getValue()).getName()) - .collect(Collectors.toList()); + ICodeInfo codeInfo = cls.getCode(); + int mthDefPos = testMth.getDefPosition(); + int lineEndPos = CodeUtils.getLineEndForPos(codeInfo.getCodeStr(), mthDefPos); + List argNames2 = new ArrayList<>(); + codeInfo.getCodeMetadata().searchDown(mthDefPos, (pos, ann) -> { + if (pos > lineEndPos) { + return Boolean.TRUE; // stop at line end + } + if (ann instanceof NodeDeclareRef) { + ICodeNodeRef declRef = ((NodeDeclareRef) ann).getNode(); + if (declRef instanceof VarNode) { + VarNode varNode = (VarNode) declRef; + if (varNode.getMth().equals(testMth)) { + argNames2.add(varNode.getName()); + } + } + } + return null; + }); - assertThat(argNames).doesNotContainNull(); - assertThat(argNames.toString()).isEqualTo(expectedVars); + assertThat(argNames2).doesNotContainNull(); + assertThat(argNames2.toString()).isEqualTo(expectedVars); } } diff --git a/jadx-gui/build.gradle b/jadx-gui/build.gradle index 9d39b738..50b01147 100644 --- a/jadx-gui/build.gradle +++ b/jadx-gui/build.gradle @@ -29,6 +29,8 @@ dependencies { implementation "com.github.akarnokd:rxjava2-swing:0.3.7" implementation 'com.android.tools.build:apksig:4.2.1' implementation 'io.github.hqktech:jdwp:1.0' + + testImplementation project(":jadx-core").sourceSets.test.output } application { @@ -98,7 +100,7 @@ runtime { addModules( 'java.desktop', 'java.naming', - 'java.sql', // TODO: GSON register adapter for java.sql.Time + //'java.sql', // TODO: GSON register adapter for java.sql.Time 'java.xml', ) jpackage { diff --git a/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java b/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java index 9a275c51..aad6b8ab 100644 --- a/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java +++ b/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java @@ -11,20 +11,26 @@ import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import jadx.api.ICodeCache; import jadx.api.JadxArgs; import jadx.api.JadxDecompiler; import jadx.api.JavaClass; import jadx.api.JavaPackage; import jadx.api.ResourceFile; +import jadx.api.impl.InMemoryCodeCache; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.ProcessState; +import jadx.core.utils.exceptions.JadxRuntimeException; +import jadx.core.utils.files.FileUtils; import jadx.gui.settings.JadxProject; import jadx.gui.settings.JadxSettings; +import jadx.gui.utils.codecache.CodeStringCache; +import jadx.gui.utils.codecache.disk.BufferCodeCache; +import jadx.gui.utils.codecache.disk.DiskCodeCache; import static jadx.core.dex.nodes.ProcessState.GENERATED_AND_UNLOADED; import static jadx.core.dex.nodes.ProcessState.NOT_LOADED; import static jadx.core.dex.nodes.ProcessState.PROCESS_COMPLETE; -import static jadx.gui.utils.FileUtils.toFiles; public class JadxWrapper { private static final Logger LOG = LoggerFactory.getLogger(JadxWrapper.class); @@ -44,12 +50,14 @@ public class JadxWrapper { this.openPaths = paths; try { JadxArgs jadxArgs = settings.toJadxArgs(); - jadxArgs.setInputFiles(toFiles(paths)); + jadxArgs.setInputFiles(FileUtils.toFiles(paths)); if (project != null) { jadxArgs.setCodeData(project.getCodeData()); } + closeCodeCache(); this.decompiler = new JadxDecompiler(jadxArgs); this.decompiler.load(); + initCodeCache(jadxArgs); } catch (Exception e) { LOG.error("Jadx init error", e); close(); @@ -68,12 +76,55 @@ public class JadxWrapper { public void close() { try { decompiler.close(); + closeCodeCache(); } catch (Exception e) { LOG.error("jadx decompiler close error", e); } this.openPaths = Collections.emptyList(); } + private void initCodeCache(JadxArgs jadxArgs) { + switch (settings.getCodeCacheMode()) { + case MEMORY: + jadxArgs.setCodeCache(new InMemoryCodeCache()); + break; + case DISK_WITH_CACHE: + jadxArgs.setCodeCache(new CodeStringCache(buildBufferedDiskCache())); + break; + case DISK: + jadxArgs.setCodeCache(buildBufferedDiskCache()); + break; + } + } + + private BufferCodeCache buildBufferedDiskCache() { + DiskCodeCache diskCache = new DiskCodeCache(decompiler.getRoot(), getCacheDir()); + return new BufferCodeCache(diskCache); + } + + private Path getCacheDir() { + if (project != null && project.getProjectPath() != null) { + Path projectPath = project.getProjectPath(); + return projectPath.resolveSibling(projectPath.getFileName() + ".cache"); + } + if (!openPaths.isEmpty()) { + Path path = openPaths.get(0); + return path.resolveSibling(path.getFileName() + ".cache"); + } + throw new JadxRuntimeException("Can't get working dir"); + } + + public void closeCodeCache() { + ICodeCache codeCache = getArgs().getCodeCache(); + if (codeCache != null) { + try { + codeCache.close(); + } catch (Exception e) { + throw new JadxRuntimeException("Error on cache close", e); + } + } + } + /** * Get the complete list of classes */ @@ -90,16 +141,34 @@ public class JadxWrapper { if (excludedPackages.isEmpty()) { return classList; } + return classList.stream() + .filter(cls -> isClassIncluded(excludedPackages, cls)) + .collect(Collectors.toList()); + } - return classList.stream().filter(cls -> { - for (String exclude : excludedPackages) { - if (cls.getFullName().equals(exclude) - || cls.getFullName().startsWith(exclude + '.')) { - return false; - } + /** + * Get all classes that are not excluded by the excluded packages settings including inner classes + */ + public List getIncludedClassesWithInners() { + List classes = decompiler.getClassesWithInners(); + List excludedPackages = getExcludedPackages(); + if (excludedPackages.isEmpty()) { + return classes; + } + return classes.stream() + .filter(cls -> isClassIncluded(excludedPackages, cls)) + .collect(Collectors.toList()); + } + + private static boolean isClassIncluded(List excludedPackages, JavaClass cls) { + for (String exclude : excludedPackages) { + String clsFullName = cls.getFullName(); + if (clsFullName.equals(exclude) + || clsFullName.startsWith(exclude + '.')) { + return false; } - return true; - }).collect(Collectors.toList()); + } + return true; } public List> buildDecompileBatches(List classes) { @@ -158,7 +227,8 @@ public class JadxWrapper { } /** - * @param fullName Full name of an outer class. Inner classes are not supported. + * @param fullName + * Full name of an outer class. Inner classes are not supported. */ public @Nullable JavaClass searchJavaClassByFullAlias(String fullName) { return decompiler.getClasses().stream() @@ -172,7 +242,8 @@ public class JadxWrapper { } /** - * @param rawName Full raw name of an outer class. Inner classes are not supported. + * @param rawName + * Full raw name of an outer class. Inner classes are not supported. */ public @Nullable JavaClass searchJavaClassByRawName(String rawName) { return decompiler.getClasses().stream() diff --git a/jadx-gui/src/main/java/jadx/gui/device/debugger/smali/SmaliWriter.java b/jadx-gui/src/main/java/jadx/gui/device/debugger/smali/SmaliWriter.java index c5d80ba5..16bec2af 100644 --- a/jadx-gui/src/main/java/jadx/gui/device/debugger/smali/SmaliWriter.java +++ b/jadx-gui/src/main/java/jadx/gui/device/debugger/smali/SmaliWriter.java @@ -1,10 +1,7 @@ package jadx.gui.device.debugger.smali; -import java.util.Collections; -import java.util.Map; - -import jadx.api.CodePosition; import jadx.api.ICodeInfo; +import jadx.api.impl.SimpleCodeInfo; import jadx.api.impl.SimpleCodeWriter; import jadx.core.dex.nodes.ClassNode; @@ -34,21 +31,6 @@ public class SmaliWriter extends SimpleCodeWriter { @Override public ICodeInfo finish() { - return new ICodeInfo() { - @Override - public String getCodeStr() { - return buf.toString(); - } - - @Override - public Map getLineMapping() { - return Collections.emptyMap(); - } - - @Override - public Map getAnnotations() { - return Collections.emptyMap(); - } - }; + return new SimpleCodeInfo(buf.toString()); } } diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundExecutor.java b/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundExecutor.java index e84a1266..8adc75d1 100644 --- a/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundExecutor.java +++ b/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundExecutor.java @@ -2,16 +2,19 @@ package jadx.gui.jobs; import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; import java.util.function.Supplier; +import javax.swing.SwingUtilities; import javax.swing.SwingWorker; -import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -21,6 +24,8 @@ import jadx.gui.ui.panel.ProgressPanel; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; +import static jadx.gui.utils.UiUtils.calcProgress; + /** * Class for run tasks in background with progress bar indication. * Use instance created in {@link MainWindow}. @@ -32,15 +37,19 @@ public class BackgroundExecutor { private final ProgressPanel progressPane; private ThreadPoolExecutor taskQueueExecutor; + private final Map taskRunning = new ConcurrentHashMap<>(); + private final AtomicLong idSupplier = new AtomicLong(0); public BackgroundExecutor(MainWindow mainWindow) { this.mainWindow = mainWindow; this.progressPane = mainWindow.getProgressPane(); - this.taskQueueExecutor = makeTaskQueueExecutor(); + reset(); } - public Future execute(IBackgroundTask task) { - TaskWorker taskWorker = new TaskWorker(task); + public synchronized Future execute(IBackgroundTask task) { + long id = idSupplier.incrementAndGet(); + TaskWorker taskWorker = new TaskWorker(id, task); + taskRunning.put(id, task); taskQueueExecutor.execute(() -> { taskWorker.init(); taskWorker.run(); @@ -56,15 +65,16 @@ public class BackgroundExecutor { } } - public void cancelAll() { + public synchronized void cancelAll() { try { - taskQueueExecutor.shutdownNow(); - boolean complete = taskQueueExecutor.awaitTermination(2, TimeUnit.SECONDS); + taskRunning.values().forEach(Cancelable::cancel); + taskQueueExecutor.shutdown(); + boolean complete = taskQueueExecutor.awaitTermination(30, TimeUnit.SECONDS); LOG.debug("Background task executor terminated with status: {}", complete ? "complete" : "interrupted"); } catch (Exception e) { LOG.error("Error terminating task executor", e); } finally { - taskQueueExecutor = makeTaskQueueExecutor(); + reset(); } } @@ -77,14 +87,21 @@ public class BackgroundExecutor { } public Future execute(String title, Runnable backgroundRunnable) { - return execute(new SimpleTask(title, Collections.singletonList(backgroundRunnable), null)); + return execute(new SimpleTask(title, Collections.singletonList(backgroundRunnable))); } - private ThreadPoolExecutor makeTaskQueueExecutor() { - return (ThreadPoolExecutor) Executors.newFixedThreadPool(1); + private synchronized void reset() { + taskQueueExecutor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1); + taskRunning.clear(); + idSupplier.set(0); + } + + private void taskComplete(long id) { + taskRunning.remove(id); } private final class TaskWorker extends SwingWorker implements ITaskInfo { + private final long id; private final IBackgroundTask task; private ThreadPoolExecutor executor; private TaskStatus status = TaskStatus.WAIT; @@ -92,26 +109,36 @@ public class BackgroundExecutor { private long jobsComplete; private long time; - public TaskWorker(IBackgroundTask task) { + public TaskWorker(long id, IBackgroundTask task) { + this.id = id; this.task = task; } public void init() { addPropertyChangeListener(progressPane); - progressPane.reset(); + SwingUtilities.invokeLater(() -> { + progressPane.reset(); + if (task.getTaskProgress() != null) { + progressPane.setIndeterminate(false); + } + }); } @Override protected TaskStatus doInBackground() throws Exception { progressPane.changeLabel(this, task.getTitle() + "… "); progressPane.changeCancelBtnVisible(this, task.canBeCanceled()); - - runJobs(); + try { + runJobs(); + } finally { + taskComplete(id); + task.onDone(this); + } return status; } private void runJobs() throws InterruptedException { - List jobs = task.scheduleJobs(); + List jobs = task.scheduleJobs(); jobsCount = jobs.size(); LOG.debug("Starting background task '{}', jobs count: {}, time limit: {} ms, memory check: {}", task.getTitle(), jobsCount, task.timeLimit(), task.checkMemoryUsage()); @@ -129,7 +156,6 @@ public class BackgroundExecutor { status = waitTermination(executor, buildCancelCheck(startTime)); time = System.currentTimeMillis() - startTime; jobsComplete = executor.getCompletedTaskCount(); - task.onDone(this); } @SuppressWarnings("BusyWait") @@ -145,9 +171,9 @@ public class BackgroundExecutor { performCancel(executor); return cancelStatus; } - setProgress(calcProgress(executor.getCompletedTaskCount())); + updateProgress(executor); k++; - Thread.sleep(k < 20 ? 100 : 1000); // faster update for short tasks + Thread.sleep(k < 10 ? 200 : 1000); // faster update for short tasks if (jobsCount == 1 && k == 3) { // small delay before show progress to reduce blinking on short tasks progressPane.changeVisibility(this, true); @@ -164,19 +190,39 @@ public class BackgroundExecutor { } } + private void updateProgress(ThreadPoolExecutor executor) { + Consumer onProgressListener = task.getOnProgressListener(); + ITaskProgress taskProgress = task.getTaskProgress(); + if (taskProgress == null) { + setProgress(calcProgress(executor.getCompletedTaskCount(), jobsCount)); + if (onProgressListener != null) { + onProgressListener.accept(new TaskProgress(executor.getCompletedTaskCount(), jobsCount)); + } + } else { + setProgress(calcProgress(taskProgress)); + if (onProgressListener != null) { + onProgressListener.accept(taskProgress); + } + } + } + private void performCancel(ThreadPoolExecutor executor) throws InterruptedException { progressPane.changeLabel(this, task.getTitle() + " (" + NLS.str("progress.canceling") + ")… "); progressPane.changeIndeterminate(this, true); // force termination - executor.shutdownNow(); + task.cancel(); + executor.shutdown(); boolean complete = executor.awaitTermination(5, TimeUnit.SECONDS); - LOG.debug("Task cancel complete: {}", complete); + LOG.debug("Task cancel complete: {}", complete ? "success" : "aborted"); } private Supplier buildCancelCheck(long startTime) { long waitUntilTime = task.timeLimit() == 0 ? 0 : startTime + task.timeLimit(); boolean checkMemoryUsage = task.checkMemoryUsage(); return () -> { + if (task.isCanceled()) { + return TaskStatus.CANCEL_BY_USER; + } if (waitUntilTime != 0 && waitUntilTime < System.currentTimeMillis()) { LOG.error("Task '{}' execution timeout, force cancel", task.getTitle()); return TaskStatus.CANCEL_BY_TIMEOUT; @@ -205,10 +251,6 @@ public class BackgroundExecutor { }; } - private int calcProgress(long done) { - return Math.round(done * 100 / (float) jobsCount); - } - @Override protected void done() { progressPane.setVisible(false); @@ -240,38 +282,4 @@ public class BackgroundExecutor { return time; } } - - private static final class SimpleTask implements IBackgroundTask { - private final String title; - private final List jobs; - private final Consumer onFinish; - - public SimpleTask(String title, List jobs, @Nullable Consumer onFinish) { - this.title = title; - this.jobs = jobs; - this.onFinish = onFinish; - } - - @Override - public String getTitle() { - return title; - } - - @Override - public List scheduleJobs() { - return jobs; - } - - @Override - public void onFinish(ITaskInfo taskInfo) { - if (onFinish != null) { - onFinish.accept(taskInfo.getStatus()); - } - } - - @Override - public boolean checkMemoryUsage() { - return true; - } - } } diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/Cancelable.java b/jadx-gui/src/main/java/jadx/gui/jobs/Cancelable.java new file mode 100644 index 00000000..ec173b24 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/jobs/Cancelable.java @@ -0,0 +1,7 @@ +package jadx.gui.jobs; + +public interface Cancelable { + boolean isCanceled(); + + void cancel(); +} diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/CancelableBackgroundTask.java b/jadx-gui/src/main/java/jadx/gui/jobs/CancelableBackgroundTask.java new file mode 100644 index 00000000..cee2c28d --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/jobs/CancelableBackgroundTask.java @@ -0,0 +1,27 @@ +package jadx.gui.jobs; + +import java.util.concurrent.atomic.AtomicBoolean; + +public abstract class CancelableBackgroundTask implements IBackgroundTask { + + private final AtomicBoolean cancel = new AtomicBoolean(false); + + @Override + public boolean isCanceled() { + return cancel.get(); + } + + @Override + public void cancel() { + cancel.set(true); + } + + public void resetCancel() { + cancel.set(false); + } + + @Override + public boolean canBeCanceled() { + return true; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/DecompileTask.java b/jadx-gui/src/main/java/jadx/gui/jobs/DecompileTask.java index c7cc0d33..b288df29 100644 --- a/jadx-gui/src/main/java/jadx/gui/jobs/DecompileTask.java +++ b/jadx-gui/src/main/java/jadx/gui/jobs/DecompileTask.java @@ -8,13 +8,13 @@ import java.util.concurrent.atomic.AtomicInteger; 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; -public class DecompileTask implements IBackgroundTask { +public class DecompileTask extends CancelableBackgroundTask { private static final Logger LOG = LoggerFactory.getLogger(DecompileTask.class); private static final int CLS_LIMIT = Integer.parseInt(UiUtils.getEnvVar("JADX_CLS_PROCESS_LIMIT", "50")); @@ -23,15 +23,13 @@ public class DecompileTask implements IBackgroundTask { 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(MainWindow mainWindow, JadxWrapper wrapper) { - this.mainWindow = mainWindow; + public DecompileTask(JadxWrapper wrapper) { this.wrapper = wrapper; } @@ -42,11 +40,8 @@ public class DecompileTask implements IBackgroundTask { @Override public List scheduleJobs() { - IndexService indexService = mainWindow.getCacheObject().getIndexService(); List classes = wrapper.getIncludedClasses(); expectedCompleteCount = classes.size(); - - indexService.setComplete(false); complete.set(0); List> batches; @@ -56,12 +51,18 @@ public class DecompileTask implements IBackgroundTask { LOG.error("Decompile batches build error", e); return Collections.emptyList(); } + ICodeCache codeCache = wrapper.getArgs().getCodeCache(); List jobs = new ArrayList<>(batches.size()); for (List batch : batches) { jobs.add(() -> { for (JavaClass cls : batch) { + if (isCanceled()) { + return; + } try { - cls.decompile(); + if (!codeCache.contains(cls.getRawName())) { + cls.decompile(); + } } catch (Throwable e) { LOG.error("Failed to decompile class: {}", cls, e); } finally { @@ -80,7 +81,7 @@ public class DecompileTask implements IBackgroundTask { int timeLimit = timeLimit(); int skippedCls = expectedCompleteCount - complete.get(); if (LOG.isInfoEnabled()) { - LOG.info("Decompile task complete in " + taskTime + " ms (avg " + avgPerCls + " ms per class)" + LOG.info("Decompile and index task complete in " + taskTime + " ms (avg " + avgPerCls + " ms per class)" + ", classes: " + expectedCompleteCount + ", skipped: " + skippedCls + ", time limit:{ total: " + timeLimit + "ms, per cls: " + CLS_LIMIT + "ms }" diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/ExportTask.java b/jadx-gui/src/main/java/jadx/gui/jobs/ExportTask.java index 5d950d9d..4d9a3ca6 100644 --- a/jadx-gui/src/main/java/jadx/gui/jobs/ExportTask.java +++ b/jadx-gui/src/main/java/jadx/gui/jobs/ExportTask.java @@ -9,10 +9,10 @@ import jadx.api.ICodeCache; import jadx.api.JadxDecompiler; import jadx.gui.JadxWrapper; import jadx.gui.ui.MainWindow; -import jadx.gui.utils.FixedCodeCache; import jadx.gui.utils.NLS; +import jadx.gui.utils.codecache.FixedCodeCache; -public class ExportTask implements IBackgroundTask { +public class ExportTask extends CancelableBackgroundTask { private final MainWindow mainWindow; private final JadxWrapper wrapper; diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/IBackgroundTask.java b/jadx-gui/src/main/java/jadx/gui/jobs/IBackgroundTask.java index 0636097a..d1b70a0a 100644 --- a/jadx-gui/src/main/java/jadx/gui/jobs/IBackgroundTask.java +++ b/jadx-gui/src/main/java/jadx/gui/jobs/IBackgroundTask.java @@ -1,12 +1,18 @@ package jadx.gui.jobs; import java.util.List; +import java.util.function.Consumer; -public interface IBackgroundTask { +import org.jetbrains.annotations.Nullable; + +public interface IBackgroundTask extends Cancelable { String getTitle(); - List scheduleJobs(); + /** + * Jobs to run in parallel + */ + List scheduleJobs(); /** * Called on executor thread after the all jobs finished. @@ -37,4 +43,18 @@ public interface IBackgroundTask { default boolean checkMemoryUsage() { return false; } + + /** + * Get task progress (Optional) + */ + default @Nullable ITaskProgress getTaskProgress() { + return null; + } + + /** + * Return progress notifications listener (use executor tick rate and thread) (Optional) + */ + default @Nullable Consumer getOnProgressListener() { + return null; + } } diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/ITaskProgress.java b/jadx-gui/src/main/java/jadx/gui/jobs/ITaskProgress.java new file mode 100644 index 00000000..53aecbe9 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/jobs/ITaskProgress.java @@ -0,0 +1,8 @@ +package jadx.gui.jobs; + +public interface ITaskProgress { + + int progress(); + + int total(); +} diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/IndexService.java b/jadx-gui/src/main/java/jadx/gui/jobs/IndexService.java deleted file mode 100644 index 65922e8e..00000000 --- a/jadx-gui/src/main/java/jadx/gui/jobs/IndexService.java +++ /dev/null @@ -1,110 +0,0 @@ -package jadx.gui.jobs; - -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import jadx.api.ICodeInfo; -import jadx.api.ICodeWriter; -import jadx.api.JavaClass; -import jadx.gui.utils.CacheObject; -import jadx.gui.utils.CodeLinesInfo; -import jadx.gui.utils.search.StringRef; -import jadx.gui.utils.search.TextSearchIndex; - -public class IndexService { - - private static final Logger LOG = LoggerFactory.getLogger(IndexService.class); - - private final CacheObject cache; - private boolean indexComplete; - private final Set indexSet = new HashSet<>(); - - public IndexService(CacheObject cache) { - this.cache = cache; - } - - /** - * Warning! Not ready for parallel execution. Use only in a single thread. - */ - public boolean indexCls(JavaClass cls) { - try { - TextSearchIndex index = cache.getTextIndex(); - if (index == null) { - return false; - } - // get code from cache to avoid decompilation here - String code = getCodeFromCache(cls); - if (code == null) { - return cls.isNoCode(); - } - List lines = splitLines(code); - CodeLinesInfo linesInfo = new CodeLinesInfo(cls); - index.indexCode(cls, linesInfo, lines); - index.indexNames(cls); - indexSet.add(cls); - return true; - } catch (Exception e) { - LOG.error("Index error in class: {}", cls.getFullName(), e); - return false; - } - } - - // TODO: add to API - @Nullable - private String getCodeFromCache(JavaClass cls) { - ICodeInfo codeInfo = cls.getClassNode().getCodeFromCache(); - return codeInfo != null ? codeInfo.getCodeStr() : null; - } - - public void indexResources() { - TextSearchIndex index = cache.getTextIndex(); - index.indexResource(); - } - - public synchronized void refreshIndex(JavaClass cls) { - TextSearchIndex index = cache.getTextIndex(); - if (index == null) { - return; - } - indexSet.remove(cls); - index.remove(cls); - indexCls(cls); - } - - public synchronized void remove(JavaClass cls) { - TextSearchIndex index = cache.getTextIndex(); - if (index == null) { - return; - } - indexSet.remove(cls); - index.remove(cls); - } - - public boolean isIndexNeeded(JavaClass cls) { - return !indexSet.contains(cls); - } - - @NotNull - protected static List splitLines(String code) { - List lines = StringRef.split(code, ICodeWriter.NL); - int size = lines.size(); - for (int i = 0; i < size; i++) { - lines.set(i, lines.get(i).trim()); - } - return lines; - } - - public boolean isComplete() { - return indexComplete; - } - - public void setComplete(boolean indexComplete) { - this.indexComplete = indexComplete; - } -} diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/IndexTask.java b/jadx-gui/src/main/java/jadx/gui/jobs/IndexTask.java deleted file mode 100644 index f1e9d8be..00000000 --- a/jadx-gui/src/main/java/jadx/gui/jobs/IndexTask.java +++ /dev/null @@ -1,94 +0,0 @@ -package jadx.gui.jobs; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Collectors; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import jadx.api.JavaClass; -import jadx.gui.JadxWrapper; -import jadx.gui.ui.MainWindow; -import jadx.gui.utils.NLS; - -public class IndexTask implements IBackgroundTask { - private static final Logger LOG = LoggerFactory.getLogger(IndexTask.class); - - private final MainWindow mainWindow; - private final JadxWrapper wrapper; - private final AtomicInteger complete = new AtomicInteger(0); - private int expectedCompleteCount; - - private ProcessResult result; - - public IndexTask(MainWindow mainWindow, JadxWrapper wrapper) { - this.mainWindow = mainWindow; - this.wrapper = wrapper; - } - - @Override - public String getTitle() { - return NLS.str("progress.index"); - } - - @Override - public List scheduleJobs() { - IndexService indexService = mainWindow.getCacheObject().getIndexService(); - List classesForIndex = wrapper.getIncludedClasses() - .stream() - .filter(indexService::isIndexNeeded) - .collect(Collectors.toList()); - expectedCompleteCount = classesForIndex.size(); - - indexService.setComplete(false); - complete.set(0); - - List jobs = new ArrayList<>(2); - jobs.add(indexService::indexResources); - jobs.add(() -> { - for (JavaClass cls : classesForIndex) { - try { - // TODO: a lot of synchronizations to index object, not efficient for parallel usage - if (indexService.indexCls(cls)) { - complete.incrementAndGet(); - } else { - LOG.debug("Index skipped for {}", cls); - } - } catch (Throwable e) { - LOG.error("Failed to index class: {}", cls, e); - } - } - }); - return jobs; - } - - @Override - public void onDone(ITaskInfo taskInfo) { - int skippedCls = expectedCompleteCount - complete.get(); - if (LOG.isInfoEnabled()) { - LOG.info("Index task complete in " + taskInfo.getTime() + " ms" - + ", classes: " + expectedCompleteCount - + ", skipped: " + skippedCls - + ", status: " + taskInfo.getStatus()); - } - IndexService indexService = mainWindow.getCacheObject().getIndexService(); - indexService.setComplete(true); - this.result = new ProcessResult(skippedCls, taskInfo.getStatus(), 0); - } - - @Override - public boolean canBeCanceled() { - return true; - } - - @Override - public boolean checkMemoryUsage() { - return true; - } - - public ProcessResult getResult() { - return result; - } -} diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/SimpleTask.java b/jadx-gui/src/main/java/jadx/gui/jobs/SimpleTask.java new file mode 100644 index 00000000..7352cbbb --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/jobs/SimpleTask.java @@ -0,0 +1,61 @@ +package jadx.gui.jobs; + +import java.util.List; +import java.util.function.Consumer; + +import org.jetbrains.annotations.Nullable; + +/** + * Simple not cancelable task with memory check + */ +public class SimpleTask implements IBackgroundTask { + private final String title; + private final List jobs; + private final Consumer onFinish; + + public SimpleTask(String title, List jobs) { + this(title, jobs, null); + } + + public SimpleTask(String title, List jobs, @Nullable Consumer onFinish) { + this.title = title; + this.jobs = jobs; + this.onFinish = onFinish; + } + + @Override + public String getTitle() { + return title; + } + + @Override + public List scheduleJobs() { + return jobs; + } + + @Override + public void onFinish(ITaskInfo taskInfo) { + if (onFinish != null) { + onFinish.accept(taskInfo.getStatus()); + } + } + + @Override + public boolean checkMemoryUsage() { + return true; + } + + @Override + public boolean canBeCanceled() { + return false; + } + + @Override + public boolean isCanceled() { + return false; + } + + @Override + public void cancel() { + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/TaskProgress.java b/jadx-gui/src/main/java/jadx/gui/jobs/TaskProgress.java new file mode 100644 index 00000000..a2bf0f11 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/jobs/TaskProgress.java @@ -0,0 +1,39 @@ +package jadx.gui.jobs; + +import jadx.gui.utils.UiUtils; + +public class TaskProgress implements ITaskProgress { + private int progress; + private int total; + + public TaskProgress() { + this(0, 100); + } + + public TaskProgress(long progress, long total) { + this(UiUtils.calcProgress(progress, total), 100); + } + + public TaskProgress(int progress, int total) { + this.progress = progress; + this.total = total; + } + + @Override + public int progress() { + return progress; + } + + @Override + public int total() { + return total; + } + + public void updateProgress(int progress) { + this.progress = progress; + } + + public void updateTotal(int total) { + this.total = total; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/TaskStatus.java b/jadx-gui/src/main/java/jadx/gui/jobs/TaskStatus.java index defcd719..40603343 100644 --- a/jadx-gui/src/main/java/jadx/gui/jobs/TaskStatus.java +++ b/jadx-gui/src/main/java/jadx/gui/jobs/TaskStatus.java @@ -7,5 +7,5 @@ public enum TaskStatus { CANCEL_BY_USER, CANCEL_BY_TIMEOUT, CANCEL_BY_MEMORY, - ERROR + ERROR; } diff --git a/jadx-gui/src/main/java/jadx/gui/plugins/quark/QuarkReportNode.java b/jadx-gui/src/main/java/jadx/gui/plugins/quark/QuarkReportNode.java index f7e2687e..43c295ad 100644 --- a/jadx-gui/src/main/java/jadx/gui/plugins/quark/QuarkReportNode.java +++ b/jadx-gui/src/main/java/jadx/gui/plugins/quark/QuarkReportNode.java @@ -14,6 +14,8 @@ import org.slf4j.LoggerFactory; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import jadx.api.ICodeInfo; +import jadx.api.impl.SimpleCodeInfo; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JNode; import jadx.gui.ui.TabbedPane; @@ -33,7 +35,7 @@ public class QuarkReportNode extends JNode { private final Path apkFile; - private String errorContent; + private ICodeInfo errorContent; public QuarkReportNode(Path apkFile) { this.apkFile = apkFile; @@ -71,13 +73,13 @@ public class QuarkReportNode extends JNode { builder.append("
");
 			builder.escape(ExceptionUtils.getStackTrace(e));
 			builder.append("
"); - errorContent = builder.toString(); + errorContent = new SimpleCodeInfo(builder.toString()); return new HtmlPanel(tabbedPane, this); } } @Override - public String getContent() { - return this.errorContent; + public ICodeInfo getCodeInfo() { + return errorContent; } } diff --git a/jadx-gui/src/main/java/jadx/gui/plugins/quark/QuarkReportPanel.java b/jadx-gui/src/main/java/jadx/gui/plugins/quark/QuarkReportPanel.java index 2313b389..5d680060 100644 --- a/jadx-gui/src/main/java/jadx/gui/plugins/quark/QuarkReportPanel.java +++ b/jadx-gui/src/main/java/jadx/gui/plugins/quark/QuarkReportPanel.java @@ -39,7 +39,6 @@ import jadx.api.JavaClass; import jadx.api.JavaMethod; import jadx.core.utils.Utils; import jadx.gui.JadxWrapper; -import jadx.gui.jobs.BackgroundExecutor; import jadx.gui.treemodel.JMethod; import jadx.gui.ui.MainWindow; import jadx.gui.ui.TabbedPane; @@ -117,11 +116,7 @@ public class QuarkReportPanel extends ContentPanel { Object node = getNodeUnderMouse(tree, event); if (node instanceof MethodTreeNode) { JMethod method = ((MethodTreeNode) node).getJMethod(); - BackgroundExecutor executor = tabbedPane.getMainWindow().getBackgroundExecutor(); - executor.execute("Decompiling class", - () -> tabbedPane.codeJump(method), - status -> tabbedPane.codeJump(method) // TODO: fix bug with incorrect jump on just decompiled code - ); + tabbedPane.codeJump(method); } } } diff --git a/jadx-gui/src/main/java/jadx/gui/search/ISearchMethod.java b/jadx-gui/src/main/java/jadx/gui/search/ISearchMethod.java new file mode 100644 index 00000000..a446a690 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/search/ISearchMethod.java @@ -0,0 +1,27 @@ +package jadx.gui.search; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.commons.lang3.StringUtils; + +public interface ISearchMethod { + int find(String input, String subStr, int start); + + static ISearchMethod build(SearchSettings searchSettings) { + if (searchSettings.isUseRegex()) { + Pattern pattern = searchSettings.getPattern(); + return (input, subStr, start) -> { + Matcher matcher = pattern.matcher(input); + if (matcher.find(start)) { + return matcher.start(); + } + return -1; + }; + } + if (searchSettings.isIgnoreCase()) { + return StringUtils::indexOfIgnoreCase; + } + return String::indexOf; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/search/ISearchProvider.java b/jadx-gui/src/main/java/jadx/gui/search/ISearchProvider.java new file mode 100644 index 00000000..ea9ee30c --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/search/ISearchProvider.java @@ -0,0 +1,16 @@ +package jadx.gui.search; + +import org.jetbrains.annotations.Nullable; + +import jadx.gui.jobs.Cancelable; +import jadx.gui.jobs.ITaskProgress; +import jadx.gui.treemodel.JNode; + +public interface ISearchProvider extends ITaskProgress { + + /** + * Return next result or null if search complete + */ + @Nullable + JNode next(Cancelable cancelable); +} diff --git a/jadx-gui/src/main/java/jadx/gui/search/SearchJob.java b/jadx-gui/src/main/java/jadx/gui/search/SearchJob.java new file mode 100644 index 00000000..381e77fe --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/search/SearchJob.java @@ -0,0 +1,40 @@ +package jadx.gui.search; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.gui.treemodel.JNode; + +public class SearchJob implements Runnable { + + private static final Logger LOG = LoggerFactory.getLogger(SearchJob.class); + private final SearchTask searchTask; + private final ISearchProvider provider; + + public SearchJob(SearchTask task, ISearchProvider provider) { + this.searchTask = task; + this.provider = provider; + } + + @Override + public void run() { + while (true) { + try { + JNode result = provider.next(searchTask); + if (result == null) { + return; + } + if (searchTask.addResult(result)) { + return; + } + } catch (Exception e) { + LOG.warn("Search error, provider: {}", provider.getClass().getSimpleName(), e); + return; + } + } + } + + public ISearchProvider getProvider() { + return provider; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/search/SearchSettings.java b/jadx-gui/src/main/java/jadx/gui/search/SearchSettings.java new file mode 100644 index 00000000..a9139f71 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/search/SearchSettings.java @@ -0,0 +1,70 @@ +package jadx.gui.search; + +import java.util.regex.Pattern; + +import org.jetbrains.annotations.Nullable; + +import jadx.gui.treemodel.JClass; + +public class SearchSettings { + + private final String searchString; + private final boolean useRegex; + private final boolean ignoreCase; + + private JClass activeCls; + private Pattern regexPattern; + private ISearchMethod searchMethod; + + public SearchSettings(String searchString, boolean ignoreCase, boolean useRegex) { + this.searchString = searchString; + this.useRegex = useRegex; + this.ignoreCase = ignoreCase; + } + + @Nullable + public String prepare() { + if (useRegex) { + try { + int flags = ignoreCase ? Pattern.CASE_INSENSITIVE : 0; + this.regexPattern = Pattern.compile(searchString, flags); + } catch (Exception e) { + return "Invalid Regex: " + e.getMessage(); + } + } + searchMethod = ISearchMethod.build(this); + return null; + } + + public boolean isMatch(String searchArea) { + return searchMethod.find(searchArea, this.searchString, 0) != -1; + } + + public boolean isUseRegex() { + return this.useRegex; + } + + public boolean isIgnoreCase() { + return this.ignoreCase; + } + + public String getSearchString() { + return this.searchString; + } + + public Pattern getPattern() { + return this.regexPattern; + } + + public JClass getActiveCls() { + return activeCls; + } + + public void setActiveCls(JClass activeCls) { + this.activeCls = activeCls; + } + + public ISearchMethod getSearchMethod() { + return searchMethod; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/search/SearchTask.java b/jadx-gui/src/main/java/jadx/gui/search/SearchTask.java new file mode 100644 index 00000000..d49e5169 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/search/SearchTask.java @@ -0,0 +1,132 @@ +package jadx.gui.search; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.gui.jobs.BackgroundExecutor; +import jadx.gui.jobs.CancelableBackgroundTask; +import jadx.gui.jobs.ITaskInfo; +import jadx.gui.jobs.ITaskProgress; +import jadx.gui.jobs.TaskProgress; +import jadx.gui.jobs.TaskStatus; +import jadx.gui.treemodel.JNode; +import jadx.gui.ui.MainWindow; +import jadx.gui.utils.NLS; + +public class SearchTask extends CancelableBackgroundTask { + private static final Logger LOG = LoggerFactory.getLogger(SearchTask.class); + + private final BackgroundExecutor backgroundExecutor; + private final Consumer onFinish; + private final Consumer results; + private final List jobs = new ArrayList<>(); + private final TaskProgress taskProgress = new TaskProgress(); + + private final AtomicInteger resultsCount = new AtomicInteger(0); + private final AtomicBoolean complete = new AtomicBoolean(false); + private int resultsLimit; + private Future future; + + private Consumer progressListener; + + public SearchTask(MainWindow mainWindow, Consumer results, Consumer onFinish) { + this.backgroundExecutor = mainWindow.getBackgroundExecutor(); + this.results = results; + this.onFinish = onFinish; + } + + public void addProviderJob(ISearchProvider provider) { + jobs.add(new SearchJob(this, provider)); + } + + public void setResultsLimit(int limit) { + this.resultsLimit = limit; + } + + public synchronized void fetchResults() { + if (future != null) { + cancel(); + waitTask(); + } + resetCancel(); + complete.set(false); + resultsCount.set(0); + taskProgress.updateTotal(jobs.stream().mapToInt(s -> s.getProvider().total()).sum()); + future = backgroundExecutor.execute(this); + } + + public synchronized boolean addResult(JNode resultNode) { + this.results.accept(resultNode); + if (resultsLimit != 0 && resultsCount.incrementAndGet() >= resultsLimit) { + cancel(); + return true; + } + return false; + } + + public synchronized void waitTask() { + if (future != null) { + try { + future.get(); + } catch (Exception e) { + LOG.error("Wait search task failed", e); + } finally { + future.cancel(true); + future = null; + } + } + } + + @Override + public String getTitle() { + return NLS.str("search_dialog.tip_searching"); + } + + @Override + public List scheduleJobs() { + return jobs; + } + + public boolean isSearchComplete() { + return complete.get() && !isCanceled(); + } + + @Override + public void onDone(ITaskInfo taskInfo) { + this.complete.set(true); + } + + @Override + public void onFinish(ITaskInfo status) { + this.onFinish.accept(status); + } + + @Override + public boolean checkMemoryUsage() { + return true; + } + + @Override + public @NotNull ITaskProgress getTaskProgress() { + taskProgress.updateProgress(jobs.stream().mapToInt(s -> s.getProvider().progress()).sum()); + return taskProgress; + } + + public void setProgressListener(Consumer progressListener) { + this.progressListener = progressListener; + } + + @Override + public @Nullable Consumer getOnProgressListener() { + return this.progressListener; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/search/providers/BaseSearchProvider.java b/jadx-gui/src/main/java/jadx/gui/search/providers/BaseSearchProvider.java new file mode 100644 index 00000000..b580b6a7 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/search/providers/BaseSearchProvider.java @@ -0,0 +1,44 @@ +package jadx.gui.search.providers; + +import java.util.List; + +import jadx.api.JavaClass; +import jadx.api.JavaNode; +import jadx.gui.search.ISearchMethod; +import jadx.gui.search.ISearchProvider; +import jadx.gui.search.SearchSettings; +import jadx.gui.treemodel.JNode; +import jadx.gui.ui.MainWindow; +import jadx.gui.utils.JNodeCache; + +public abstract class BaseSearchProvider implements ISearchProvider { + + private final JNodeCache nodeCache; + protected final ISearchMethod searchMth; + protected final String searchStr; + protected final List classes; + + public BaseSearchProvider(MainWindow mw, SearchSettings searchSettings, List classes) { + this.nodeCache = mw.getCacheObject().getNodeCache(); + this.searchMth = searchSettings.getSearchMethod(); + this.searchStr = searchSettings.getSearchString(); + this.classes = classes; + } + + protected boolean isMatch(String str) { + return searchMth.find(str, searchStr, 0) != -1; + } + + protected JNode convert(JavaNode node) { + return nodeCache.makeFrom(node); + } + + protected JNode convert(JavaClass cls) { + return nodeCache.makeFrom(cls); + } + + @Override + public int total() { + return classes.size(); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/search/providers/ClassSearchProvider.java b/jadx-gui/src/main/java/jadx/gui/search/providers/ClassSearchProvider.java new file mode 100644 index 00000000..67d3755e --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/search/providers/ClassSearchProvider.java @@ -0,0 +1,46 @@ +package jadx.gui.search.providers; + +import java.util.List; + +import org.jetbrains.annotations.Nullable; + +import jadx.api.JavaClass; +import jadx.core.dex.info.ClassInfo; +import jadx.gui.jobs.Cancelable; +import jadx.gui.search.SearchSettings; +import jadx.gui.treemodel.JNode; +import jadx.gui.ui.MainWindow; + +public final class ClassSearchProvider extends BaseSearchProvider { + + private int clsNum = 0; + + public ClassSearchProvider(MainWindow mw, SearchSettings searchSettings, List classes) { + super(mw, searchSettings, classes); + } + + @Override + public @Nullable JNode next(Cancelable cancelable) { + while (true) { + if (cancelable.isCanceled() || clsNum >= classes.size()) { + return null; + } + JavaClass curCls = classes.get(clsNum++); + if (checkCls(curCls)) { + return convert(curCls); + } + } + } + + private boolean checkCls(JavaClass cls) { + ClassInfo clsInfo = cls.getClassNode().getClassInfo(); + return isMatch(clsInfo.getShortName()) + || isMatch(clsInfo.getFullName()) + || isMatch(clsInfo.getAliasFullName()); + } + + @Override + public int progress() { + return clsNum; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/search/providers/CodeSearchProvider.java b/jadx-gui/src/main/java/jadx/gui/search/providers/CodeSearchProvider.java new file mode 100644 index 00000000..c41b61dd --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/search/providers/CodeSearchProvider.java @@ -0,0 +1,106 @@ +package jadx.gui.search.providers; + +import java.util.List; + +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.api.ICodeCache; +import jadx.api.ICodeWriter; +import jadx.api.JadxDecompiler; +import jadx.api.JavaClass; +import jadx.api.JavaNode; +import jadx.api.metadata.ICodeMetadata; +import jadx.api.metadata.ICodeNodeRef; +import jadx.gui.jobs.Cancelable; +import jadx.gui.search.SearchSettings; +import jadx.gui.treemodel.CodeNode; +import jadx.gui.treemodel.JNode; +import jadx.gui.ui.MainWindow; + +public final class CodeSearchProvider extends BaseSearchProvider { + private static final Logger LOG = LoggerFactory.getLogger(CodeSearchProvider.class); + + private final ICodeCache codeCache; + private final JadxDecompiler decompiler; + + private @Nullable String code; + private int clsNum = 0; + private int pos = 0; + + public CodeSearchProvider(MainWindow mw, SearchSettings searchSettings, List classes) { + super(mw, searchSettings, classes); + this.codeCache = mw.getWrapper().getArgs().getCodeCache(); + this.decompiler = mw.getWrapper().getDecompiler(); + } + + @Override + public @Nullable JNode next(Cancelable cancelable) { + while (true) { + if (cancelable.isCanceled() || clsNum >= classes.size()) { + return null; + } + JavaClass cls = classes.get(clsNum); + if (!cls.getClassNode().isInner()) { + if (code == null) { + code = getClassCode(cls, codeCache); + } + JNode newResult = searchNext(cls, code); + if (newResult != null) { + return newResult; + } + } + clsNum++; + pos = 0; + code = null; + } + } + + @Nullable + private JNode searchNext(JavaClass javaClass, String clsCode) { + int newPos = searchMth.find(clsCode, searchStr, pos); + if (newPos == -1) { + return null; + } + int lineStart = 1 + clsCode.lastIndexOf(ICodeWriter.NL, newPos); + int lineEnd = clsCode.indexOf(ICodeWriter.NL, newPos); + int end = lineEnd == -1 ? clsCode.length() : lineEnd; + String line = clsCode.substring(lineStart, end); + this.pos = end; + return new CodeNode(getEnclosingNode(javaClass, end), line.trim(), newPos); + } + + private JNode getEnclosingNode(JavaClass javaCls, int pos) { + try { + ICodeMetadata metadata = javaCls.getCodeInfo().getCodeMetadata(); + ICodeNodeRef nodeRef = metadata.getNodeAt(pos); + JavaNode encNode = decompiler.getJavaNodeByRef(nodeRef); + if (encNode != null) { + return convert(encNode); + } + } catch (Exception e) { + LOG.debug("Failed to resolve enclosing node", e); + } + return convert(javaCls); + } + + private String getClassCode(JavaClass javaClass, ICodeCache codeCache) { + try { + // quick check for if code already in cache + String code = codeCache.getCode(javaClass.getRawName()); + if (code != null) { + return code; + } + return javaClass.getCode(); + } catch (Exception e) { + LOG.warn("Failed to get class code: " + javaClass, e); + return ""; + } + } + + @Override + public int progress() { + return clsNum; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/search/CommentsIndex.java b/jadx-gui/src/main/java/jadx/gui/search/providers/CommentSearchProvider.java similarity index 70% rename from jadx-gui/src/main/java/jadx/gui/utils/search/CommentsIndex.java rename to jadx-gui/src/main/java/jadx/gui/search/providers/CommentSearchProvider.java index 0158a672..61646250 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/search/CommentsIndex.java +++ b/jadx-gui/src/main/java/jadx/gui/search/providers/CommentSearchProvider.java @@ -1,7 +1,6 @@ -package jadx.gui.utils.search; +package jadx.gui.search.providers; import java.util.List; -import java.util.Map; import java.util.Objects; import javax.swing.Icon; @@ -11,10 +10,6 @@ import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.reactivex.BackpressureStrategy; -import io.reactivex.Flowable; - -import jadx.api.CodePosition; import jadx.api.ICodeInfo; import jadx.api.JavaClass; import jadx.api.JavaField; @@ -23,27 +18,52 @@ import jadx.api.JavaNode; import jadx.api.data.ICodeComment; import jadx.api.data.IJavaCodeRef; import jadx.api.data.IJavaNodeRef; -import jadx.api.data.annotations.ICodeRawOffset; +import jadx.api.metadata.annotations.InsnCodeOffset; import jadx.gui.JadxWrapper; +import jadx.gui.jobs.Cancelable; +import jadx.gui.search.ISearchProvider; +import jadx.gui.search.SearchSettings; import jadx.gui.settings.JadxProject; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JMethod; import jadx.gui.treemodel.JNode; +import jadx.gui.ui.MainWindow; import jadx.gui.utils.CacheObject; import jadx.gui.utils.JNodeCache; import jadx.gui.utils.JumpPosition; -public class CommentsIndex { - - private static final Logger LOG = LoggerFactory.getLogger(CommentsIndex.class); +public class CommentSearchProvider implements ISearchProvider { + private static final Logger LOG = LoggerFactory.getLogger(CommentSearchProvider.class); private final JadxWrapper wrapper; private final CacheObject cacheObject; private final JadxProject project; + private final SearchSettings searchSettings; - public CommentsIndex(JadxWrapper wrapper, CacheObject cacheObject, JadxProject project) { - this.wrapper = wrapper; - this.cacheObject = cacheObject; - this.project = project; + private int progress = 0; + + public CommentSearchProvider(MainWindow mw, SearchSettings searchSettings) { + this.wrapper = mw.getWrapper(); + this.cacheObject = mw.getCacheObject(); + this.project = mw.getProject(); + this.searchSettings = searchSettings; + } + + @Override + public @Nullable JNode next(Cancelable cancelable) { + while (true) { + if (cancelable.isCanceled()) { + return null; + } + List comments = project.getCodeData().getComments(); + if (progress >= comments.size()) { + return null; + } + ICodeComment comment = comments.get(progress++); + JNode result = isMatch(searchSettings, comment); + if (result != null) { + return result; + } + } } @Nullable @@ -63,26 +83,6 @@ public class CommentsIndex { return null; } - public Flowable search(SearchSettings searchSettings) { - List comments = project.getCodeData().getComments(); - if (comments == null || comments.isEmpty()) { - return Flowable.empty(); - } - LOG.debug("Total comments count: {}", comments.size()); - return Flowable.create(emitter -> { - for (ICodeComment comment : comments) { - JNode foundNode = isMatch(searchSettings, comment); - if (foundNode != null) { - emitter.onNext(foundNode); - } - if (emitter.isCancelled()) { - return; - } - } - emitter.onComplete(); - }, BackpressureStrategy.BUFFER); - } - private @NotNull RefCommentNode getCommentNode(ICodeComment comment, JNode refNode) { IJavaNodeRef nodeRef = comment.getNodeRef(); if (nodeRef.getType() == IJavaNodeRef.RefType.METHOD && comment.getCodeRef() != null) { @@ -134,11 +134,6 @@ public class CommentsIndex { this.offset = codeRef.getIndex(); } - @Override - public int getLine() { - return getCachedPos().getLine(); - } - @Override public int getPos() { return getCachedPos().getPos(); @@ -156,18 +151,14 @@ public class CommentsIndex { */ private JumpPosition getJumpPos() { JavaMethod javaMethod = ((JMethod) node).getJavaMethod(); - int methodLine = javaMethod.getDecompiledLine(); ICodeInfo codeInfo = javaMethod.getTopParentClass().getCodeInfo(); - for (Map.Entry entry : codeInfo.getAnnotations().entrySet()) { - CodePosition codePos = entry.getKey(); - if (codePos.getOffset() == 0 && codePos.getLine() > methodLine) { - Object ann = entry.getValue(); - if (ann instanceof ICodeRawOffset) { - if (((ICodeRawOffset) ann).getOffset() == offset) { - return new JumpPosition(node, codePos); - } - } - } + int methodDefPos = javaMethod.getDefPos(); + JumpPosition jump = codeInfo.getCodeMetadata().searchDown(methodDefPos, + (pos, ann) -> ann instanceof InsnCodeOffset && ((InsnCodeOffset) ann).getOffset() == offset + ? new JumpPosition(node, pos) + : null); + if (jump != null) { + return jump; } return new JumpPosition(node); } @@ -215,8 +206,28 @@ public class CommentsIndex { } @Override - public int getLine() { - return node.getLine(); + public String makeLongString() { + return node.makeLongString(); + } + + @Override + public String makeStringHtml() { + return node.makeStringHtml(); + } + + @Override + public String makeLongStringHtml() { + return node.makeLongStringHtml(); + } + + @Override + public int getPos() { + return node.getPos(); + } + + @Override + public String getTooltip() { + return node.getTooltip(); } @Override @@ -229,4 +240,14 @@ public class CommentsIndex { return true; } } + + @Override + public int progress() { + return progress; + } + + @Override + public int total() { + return project.getCodeData().getComments().size(); + } } diff --git a/jadx-gui/src/main/java/jadx/gui/search/providers/FieldSearchProvider.java b/jadx-gui/src/main/java/jadx/gui/search/providers/FieldSearchProvider.java new file mode 100644 index 00000000..b9775780 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/search/providers/FieldSearchProvider.java @@ -0,0 +1,56 @@ +package jadx.gui.search.providers; + +import java.util.List; + +import org.jetbrains.annotations.Nullable; + +import jadx.api.JavaClass; +import jadx.api.JavaField; +import jadx.core.dex.info.FieldInfo; +import jadx.gui.jobs.Cancelable; +import jadx.gui.search.SearchSettings; +import jadx.gui.treemodel.JNode; +import jadx.gui.ui.MainWindow; + +public final class FieldSearchProvider extends BaseSearchProvider { + + private int clsNum = 0; + private int fldNum = 0; + + public FieldSearchProvider(MainWindow mw, SearchSettings searchSettings, List classes) { + super(mw, searchSettings, classes); + } + + @Override + public @Nullable JNode next(Cancelable cancelable) { + while (true) { + if (cancelable.isCanceled()) { + return null; + } + JavaClass cls = classes.get(clsNum); + List fields = cls.getFields(); + if (fldNum < fields.size()) { + JavaField fld = fields.get(fldNum++); + if (checkField(fld)) { + return convert(fld); + } + } else { + clsNum++; + fldNum = 0; + if (clsNum >= classes.size()) { + return null; + } + } + } + } + + private boolean checkField(JavaField field) { + FieldInfo fieldInfo = field.getFieldNode().getFieldInfo(); + return isMatch(fieldInfo.getName()) || isMatch(fieldInfo.getAlias()); + } + + @Override + public int progress() { + return clsNum; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/search/providers/MergedSearchProvider.java b/jadx-gui/src/main/java/jadx/gui/search/providers/MergedSearchProvider.java new file mode 100644 index 00000000..13e02043 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/search/providers/MergedSearchProvider.java @@ -0,0 +1,58 @@ +package jadx.gui.search.providers; + +import java.util.ArrayList; +import java.util.List; + +import org.jetbrains.annotations.Nullable; + +import jadx.gui.jobs.Cancelable; +import jadx.gui.search.ISearchProvider; +import jadx.gui.treemodel.JNode; + +/** + * Search provider for sequential execution of nested search providers + */ +public class MergedSearchProvider implements ISearchProvider { + + private final List list = new ArrayList<>(); + private int current; + private int total; + + public void add(ISearchProvider provider) { + list.add(provider); + } + + public void prepare() { + current = list.isEmpty() ? -1 : 0; + total = list.stream().mapToInt(ISearchProvider::total).sum(); + } + + @Override + public @Nullable JNode next(Cancelable cancelable) { + if (current == -1) { + return null; + } + while (true) { + JNode next = list.get(current).next(cancelable); + if (next != null) { + return next; + } + current++; + if (current >= list.size() || cancelable.isCanceled()) { + // search complete + current = -1; + return null; + } + } + } + + @Override + public int progress() { + return list.stream().mapToInt(ISearchProvider::progress).sum(); + } + + @Override + public int total() { + return total; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/search/providers/MethodSearchProvider.java b/jadx-gui/src/main/java/jadx/gui/search/providers/MethodSearchProvider.java new file mode 100644 index 00000000..ce64c981 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/search/providers/MethodSearchProvider.java @@ -0,0 +1,57 @@ +package jadx.gui.search.providers; + +import java.util.List; + +import org.jetbrains.annotations.Nullable; + +import jadx.api.JavaClass; +import jadx.api.JavaMethod; +import jadx.core.dex.info.MethodInfo; +import jadx.gui.jobs.Cancelable; +import jadx.gui.search.SearchSettings; +import jadx.gui.treemodel.JNode; +import jadx.gui.ui.MainWindow; + +public final class MethodSearchProvider extends BaseSearchProvider { + + private int clsNum = 0; + private int mthNum = 0; + + public MethodSearchProvider(MainWindow mw, SearchSettings searchSettings, List classes) { + super(mw, searchSettings, classes); + } + + @Override + public @Nullable JNode next(Cancelable cancelable) { + while (true) { + if (cancelable.isCanceled()) { + return null; + } + JavaClass cls = classes.get(clsNum); + List methods = cls.getMethods(); + if (mthNum < methods.size()) { + JavaMethod mth = methods.get(mthNum++); + if (checkMth(mth)) { + return convert(mth); + } + } else { + clsNum++; + mthNum = 0; + if (clsNum >= classes.size()) { + return null; + } + } + } + } + + private boolean checkMth(JavaMethod mth) { + MethodInfo mthInfo = mth.getMethodNode().getMethodInfo(); + return isMatch(mthInfo.getShortId()) + || isMatch(mthInfo.getAlias()); + } + + @Override + public int progress() { + return clsNum; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/search/ResourceIndex.java b/jadx-gui/src/main/java/jadx/gui/search/providers/ResourceSearchProvider.java similarity index 55% rename from jadx-gui/src/main/java/jadx/gui/utils/search/ResourceIndex.java rename to jadx-gui/src/main/java/jadx/gui/search/providers/ResourceSearchProvider.java index fc6fc2bb..64908036 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/search/ResourceIndex.java +++ b/jadx-gui/src/main/java/jadx/gui/search/providers/ResourceSearchProvider.java @@ -1,4 +1,4 @@ -package jadx.gui.utils.search; +package jadx.gui.search.providers; import java.io.File; import java.io.IOException; @@ -15,98 +15,104 @@ import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.reactivex.BackpressureStrategy; -import io.reactivex.Flowable; -import io.reactivex.FlowableEmitter; - +import jadx.api.ICodeWriter; import jadx.api.ResourceFile; import jadx.api.ResourceType; import jadx.core.utils.files.FileUtils; +import jadx.gui.jobs.Cancelable; +import jadx.gui.search.ISearchProvider; +import jadx.gui.search.SearchSettings; +import jadx.gui.treemodel.JNode; import jadx.gui.treemodel.JResSearchNode; import jadx.gui.treemodel.JResource; +import jadx.gui.ui.MainWindow; import jadx.gui.utils.CacheObject; -import static jadx.core.utils.StringUtils.countLinesByPos; -import static jadx.core.utils.StringUtils.getLine; +public class ResourceSearchProvider implements ISearchProvider { + private static final Logger LOG = LoggerFactory.getLogger(ResourceSearchProvider.class); -public class ResourceIndex { - private static final Logger LOG = LoggerFactory.getLogger(ResourceIndex.class); - - private final List resNodes = new ArrayList<>(); - private final Set extSet = new HashSet<>(); private final CacheObject cache; + private final SearchSettings searchSettings; + private final Set extSet = new HashSet<>(); + private List resNodes; private String fileExts; private boolean anyExt; private int sizeLimit; - public ResourceIndex(CacheObject cache) { - this.cache = cache; + private int progress; + private int pos; + + public ResourceSearchProvider(MainWindow mw, SearchSettings searchSettings) { + this.cache = mw.getCacheObject(); + this.searchSettings = searchSettings; } - private void search(final JResource resNode, - FlowableEmitter emitter, - SearchSettings searchSettings) { - int line = 0; - int lastPos = 0; - int lastLineOccurred = -1; - JResSearchNode lastNode = null; - int searchStrLen = searchSettings.getSearchString().length(); + @Override + public @Nullable JNode next(Cancelable cancelable) { + if (resNodes == null) { + load(); + } + if (resNodes.isEmpty()) { + return null; + } + while (true) { + if (cancelable.isCanceled()) { + return null; + } + JResource resNode = resNodes.get(progress); + JNode newResult = search(resNode); + if (newResult != null) { + return newResult; + } + progress++; + pos = 0; + if (progress >= resNodes.size()) { + return null; + } + } + } + + private JNode search(JResource resNode) { String content; try { - content = resNode.getContent(); + content = resNode.getCodeInfo().getCodeStr(); } catch (Exception e) { - LOG.error("Error load resource node content", e); - return; + LOG.error("Failed to load resource node content", e); + return null; } - do { - searchSettings.setStartPos(lastPos); - int pos = searchSettings.find(content); - if (pos > -1) { - line += countLinesByPos(content, pos, lastPos); - lastPos = pos + searchStrLen; - String lineText = getLine(content, pos, lastPos); - if (lastLineOccurred != line) { - lastLineOccurred = line; - if (lastNode != null) { - emitter.onNext(lastNode); - } - lastNode = new JResSearchNode(resNode, lineText.trim(), line + 1, pos); - } - } else { - if (lastNode != null) { // commit the final result node. - emitter.onNext(lastNode); - } - break; - } - } while (!emitter.isCancelled() && lastPos < content.length()); - } - - public Flowable search(SearchSettings settings) { - refreshSettings(); - if (resNodes.size() == 0) { - return Flowable.empty(); + String searchString = searchSettings.getSearchString(); + int newPos = searchSettings.getSearchMethod().find(content, searchString, pos); + if (newPos == -1) { + return null; } - return Flowable.create(emitter -> { - for (JResource resNode : resNodes) { - if (!emitter.isCancelled()) { - search(resNode, emitter, settings); + int lineStart = content.lastIndexOf(ICodeWriter.NL, newPos) + ICodeWriter.NL.length(); + int lineEnd = content.indexOf(ICodeWriter.NL, newPos + searchString.length()); + int end = lineEnd == -1 ? content.length() : lineEnd; + String line = content.substring(lineStart, end); + this.pos = end; + return new JResSearchNode(resNode, line.trim(), newPos); + } + + private synchronized void load() { + resNodes = new ArrayList<>(); + sizeLimit = cache.getJadxSettings().getSrhResourceSkipSize() * 1048576; + fileExts = cache.getJadxSettings().getSrhResourceFileExt(); + for (String extStr : fileExts.split("\\|")) { + String ext = extStr.trim(); + if (!ext.isEmpty()) { + anyExt = ext.equals("*"); + if (anyExt) { + break; } + extSet.add(ext); } - emitter.onComplete(); - }, BackpressureStrategy.BUFFER); - } - - public void index() { - refreshSettings(); - } - - private void clear() { - anyExt = false; - sizeLimit = -1; - fileExts = ""; - extSet.clear(); - resNodes.clear(); + } + try (ZipFile zipFile = getZipFile(cache.getJRoot())) { + traverseTree(cache.getJRoot(), zipFile); // reindex + } catch (Exception e) { + LOG.error("Failed to apply settings to resource index", e); + } } private void traverseTree(TreeNode root, @Nullable ZipFile zip) { @@ -186,7 +192,7 @@ public class ResourceIndex { } if (size == -1) { // resource from ARSC is unknown size try { - size = resNode.getContent().length(); + size = resNode.getCodeInfo().getCodeStr().length(); } catch (Exception ignore) { return; } @@ -208,29 +214,13 @@ public class ResourceIndex { } } - private void refreshSettings() { - int size = cache.getJadxSettings().getSrhResourceSkipSize() * 1048576; - if (size != sizeLimit - || !cache.getJadxSettings().getSrhResourceFileExt().equals(fileExts)) { - clear(); - sizeLimit = size; - fileExts = cache.getJadxSettings().getSrhResourceFileExt(); - String[] exts = fileExts.split("\\|"); - for (String ext : exts) { - ext = ext.trim(); - if (!ext.isEmpty()) { - anyExt = ext.equals("*"); - if (anyExt) { - break; - } - extSet.add(ext); - } - } - try (ZipFile zipFile = getZipFile(cache.getJRoot())) { - traverseTree(cache.getJRoot(), zipFile); // reindex - } catch (Exception e) { - LOG.error("Failed to apply settings to resource index", e); - } - } + @Override + public int progress() { + return progress; + } + + @Override + public int total() { + return resNodes == null ? 0 : resNodes.size(); } } diff --git a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java index dc79e26b..80366a08 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java @@ -38,6 +38,7 @@ import jadx.gui.utils.FontUtils; import jadx.gui.utils.LafManager; import jadx.gui.utils.LangLocale; import jadx.gui.utils.NLS; +import jadx.gui.utils.codecache.CodeCacheMode; public class JadxSettings extends JadxCLIArgs { private static final Logger LOG = LoggerFactory.getLogger(JadxSettings.class); @@ -89,6 +90,8 @@ public class JadxSettings extends JadxCLIArgs { private String adbDialogHost = "localhost"; private String adbDialogPort = "5037"; + private CodeCacheMode codeCacheMode = CodeCacheMode.DISK_WITH_CACHE; + /** * UI setting: the width of the tree showing the classes, resources, ... */ @@ -412,12 +415,20 @@ public class JadxSettings extends JadxCLIArgs { partialSync(settings -> settings.treeWidth = JadxSettings.this.treeWidth); } + @JadxSettingsAdapter.GsonExclude + private Font cachedFont = null; + public Font getFont() { + if (cachedFont != null) { + return cachedFont; + } if (fontStr.isEmpty()) { return DEFAULT_FONT; } try { - return FontUtils.loadByStr(fontStr); + Font font = FontUtils.loadByStr(fontStr); + this.cachedFont = font; + return font; } catch (Exception e) { LOG.warn("Failed to load font: {}, reset to default", fontStr, e); setFont(DEFAULT_FONT); @@ -427,12 +438,22 @@ public class JadxSettings extends JadxCLIArgs { public void setFont(@Nullable Font font) { if (font == null) { - this.fontStr = ""; + setFontStr(""); } else { - this.fontStr = FontUtils.convertToStr(font); + setFontStr(FontUtils.convertToStr(font)); + this.cachedFont = font; } } + public String getFontStr() { + return fontStr; + } + + public void setFontStr(String fontStr) { + this.fontStr = fontStr; + this.cachedFont = null; + } + public Font getSmaliFont() { if (smaliFontStr.isEmpty()) { return DEFAULT_FONT; @@ -590,6 +611,14 @@ public class JadxSettings extends JadxCLIArgs { this.pluginOptions = pluginOptions; } + public CodeCacheMode getCodeCacheMode() { + return codeCacheMode; + } + + public void setCodeCacheMode(CodeCacheMode codeCacheMode) { + this.codeCacheMode = codeCacheMode; + } + private void upgradeSettings(int fromVersion) { LOG.debug("upgrade settings from version: {} to {}", fromVersion, CURRENT_SETTINGS_VERSION); if (fromVersion == 0) { diff --git a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java index 556e2233..e02fa279 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java @@ -75,6 +75,7 @@ import jadx.gui.utils.LafManager; import jadx.gui.utils.LangLocale; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; +import jadx.gui.utils.codecache.CodeCacheMode; import jadx.gui.utils.ui.DocumentUpdateListener; public class JadxSettingsWindow extends JDialog { @@ -434,6 +435,14 @@ public class JadxSettingsWindow extends JDialog { needReload(); }); + JComboBox codeCacheModeComboBox = new JComboBox<>(CodeCacheMode.values()); + codeCacheModeComboBox.setSelectedItem(settings.getCodeCacheMode()); + codeCacheModeComboBox.addActionListener(e -> { + settings.setCodeCacheMode((CodeCacheMode) codeCacheModeComboBox.getSelectedItem()); + needReload(); + }); + String codeCacheModeToolTip = CodeCacheMode.buildToolTip(); + JCheckBox showInconsistentCode = new JCheckBox(); showInconsistentCode.setSelected(settings.isShowInconsistentCode()); showInconsistentCode.addItemListener(e -> { @@ -541,10 +550,11 @@ public class JadxSettingsWindow extends JDialog { SettingsGroup other = new SettingsGroup(NLS.str("preferences.decompile")); other.addRow(NLS.str("preferences.threads"), threadsCount); - other.addRow(NLS.str("preferences.excludedPackages"), NLS.str("preferences.excludedPackages.tooltip"), - editExcludedPackages); + other.addRow(NLS.str("preferences.excludedPackages"), + NLS.str("preferences.excludedPackages.tooltip"), editExcludedPackages); other.addRow(NLS.str("preferences.start_jobs"), autoStartJobs); other.addRow(NLS.str("preferences.decompilationMode"), decompilationModeComboBox); + other.addRow(NLS.str("preferences.codeCacheMode"), codeCacheModeToolTip, codeCacheModeComboBox); other.addRow(NLS.str("preferences.showInconsistentCode"), showInconsistentCode); other.addRow(NLS.str("preferences.escapeUnicode"), escapeUnicode); other.addRow(NLS.str("preferences.replaceConsts"), replaceConsts); diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/ApkSignature.java b/jadx-gui/src/main/java/jadx/gui/treemodel/ApkSignature.java index b63d9cb4..d0bab7c4 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/ApkSignature.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/ApkSignature.java @@ -16,8 +16,10 @@ import org.slf4j.LoggerFactory; import com.android.apksig.ApkVerifier; +import jadx.api.ICodeInfo; import jadx.api.ResourceFile; import jadx.api.ResourceType; +import jadx.api.impl.SimpleCodeInfo; import jadx.gui.JadxWrapper; import jadx.gui.ui.TabbedPane; import jadx.gui.ui.panel.ContentPanel; @@ -34,7 +36,7 @@ public class ApkSignature extends JNode { private static final ImageIcon CERTIFICATE_ICON = UiUtils.openSvgIcon("nodes/styleKeyPack"); private final transient File openFile; - private String content; + private ICodeInfo content; @Nullable public static ApkSignature getApkSignature(JadxWrapper wrapper) { @@ -81,7 +83,7 @@ public class ApkSignature extends JNode { } @Override - public String getContent() { + public ICodeInfo getCodeInfo() { if (content != null) { return this.content; } @@ -165,7 +167,7 @@ public class ApkSignature extends JNode { } writeIssues(builder, warn, result.getWarnings()); - this.content = builder.toString(); + this.content = new SimpleCodeInfo(builder.toString()); } catch (Exception e) { LOG.error(e.getMessage(), e); StringEscapeUtils.Builder builder = StringEscapeUtils.builder(StringEscapeUtils.ESCAPE_HTML4); @@ -174,7 +176,7 @@ public class ApkSignature extends JNode { builder.append("
");
 			builder.escape(ExceptionUtils.getStackTrace(e));
 			builder.append("
"); - return builder.toString(); + return new SimpleCodeInfo(builder.toString()); } return this.content; } diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/CodeNode.java b/jadx-gui/src/main/java/jadx/gui/treemodel/CodeNode.java index b94987e6..28250650 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/CodeNode.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/CodeNode.java @@ -1,28 +1,21 @@ package jadx.gui.treemodel; -import java.util.Comparator; - import javax.swing.Icon; import org.jetbrains.annotations.NotNull; import jadx.api.JavaNode; -import jadx.gui.utils.search.StringRef; -public class CodeNode extends JNode implements Comparable { +public class CodeNode extends JNode { private static final long serialVersionUID = 1658650786734966545L; private final transient JNode jNode; - private final transient JClass jParent; - private final transient StringRef line; - private final transient int lineNum; + private final transient String line; private final transient int pos; - public CodeNode(JNode jNode, StringRef lineStr, int lineNum, int pos) { + public CodeNode(JNode jNode, String lineStr, int pos) { this.jNode = jNode; - this.jParent = this.jNode.getJParent(); this.line = lineStr; - this.lineNum = lineNum; this.pos = pos; } @@ -43,7 +36,7 @@ public class CodeNode extends JNode implements Comparable { @Override public JClass getRootClass() { - JClass parent = jParent; + JClass parent = jNode.getJParent(); if (parent != null) { return parent.getRootClass(); } @@ -53,18 +46,9 @@ public class CodeNode extends JNode implements Comparable { return null; } - public StringRef getLineStr() { - return line; - } - - @Override - public int getLine() { - return lineNum; - } - @Override public String makeDescString() { - return line.toString(); + return line; } @Override @@ -119,12 +103,11 @@ public class CodeNode extends JNode implements Comparable { return jNode.hashCode(); } - public static final Comparator COMPARATOR = Comparator - .comparing(CodeNode::makeLongString) - .thenComparingInt(CodeNode::getPos); - @Override - public int compareTo(@NotNull CodeNode other) { - return COMPARATOR.compare(this, other); + public int compareTo(@NotNull JNode other) { + if (other instanceof CodeNode) { + return jNode.compareTo(((CodeNode) other).jNode); + } + return super.compareTo(other); } } diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java index 76473891..ca970af5 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java @@ -5,7 +5,6 @@ import javax.swing.ImageIcon; import org.fife.ui.rsyntaxtextarea.SyntaxConstants; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import jadx.api.ICodeInfo; import jadx.api.JavaClass; @@ -21,7 +20,7 @@ import jadx.gui.utils.CacheObject; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; -public class JClass extends JLoadableNode implements Comparable { +public class JClass extends JLoadableNode { private static final long serialVersionUID = -1239986875244097177L; private static final ImageIcon ICON_CLASS = UiUtils.openSvgIcon("nodes/class"); @@ -72,18 +71,16 @@ public class JClass extends JLoadableNode implements Comparable { update(); } - public synchronized void reload(CacheObject cache) { + public synchronized ICodeInfo reload(CacheObject cache) { cache.getNodeCache().removeWholeClass(cls); - cache.getIndexService().remove(cls); - cls.reload(); + ICodeInfo codeInfo = cls.reload(); loaded = true; update(); - cache.getIndexService().indexCls(cls); + return codeInfo; } public synchronized void unload(CacheObject cache) { cache.getNodeCache().removeWholeClass(cls); - cache.getIndexService().remove(cls); cls.unload(); loaded = false; } @@ -108,15 +105,10 @@ public class JClass extends JLoadableNode implements Comparable { } @Override - public @Nullable ICodeInfo getCodeInfo() { + public ICodeInfo getCodeInfo() { return cls.getCodeInfo(); } - @Override - public String getContent() { - return cls.getCode(); - } - @Override public ContentPanel getContentPanel(TabbedPane tabbedPane) { return new ClassCodeContentPanel(tabbedPane, this); @@ -185,11 +177,6 @@ public class JClass extends JLoadableNode implements Comparable { return cls.getFullName(); } - @Override - public int getLine() { - return cls.getDecompiledLine(); - } - @Override public int hashCode() { return cls.hashCode(); @@ -210,8 +197,22 @@ public class JClass extends JLoadableNode implements Comparable { return cls.getFullName(); } + public int compareToCls(@NotNull JClass otherCls) { + return this.getCls().getRawName().compareTo(otherCls.getCls().getRawName()); + } + @Override - public int compareTo(@NotNull JClass o) { - return this.getFullName().compareTo(o.getFullName()); + public int compareTo(@NotNull JNode other) { + if (other instanceof JClass) { + return compareToCls((JClass) other); + } + if (other instanceof JMethod) { + int cmp = compareToCls(other.getJParent()); + if (cmp != 0) { + return cmp; + } + return -1; + } + return super.compareTo(other); } } diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JField.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JField.java index cbfc8499..edd816f0 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JField.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JField.java @@ -1,9 +1,12 @@ package jadx.gui.treemodel; +import java.util.Comparator; + import javax.swing.Icon; import javax.swing.ImageIcon; import org.fife.ui.rsyntaxtextarea.SyntaxConstants; +import org.jetbrains.annotations.NotNull; import jadx.api.JavaField; import jadx.api.JavaNode; @@ -51,11 +54,6 @@ public class JField extends JNode { return !field.getFieldNode().contains(AFlag.DONT_RENAME); } - @Override - public int getLine() { - return field.getDecompiledLine(); - } - @Override public Icon getIcon() { AccessInfo af = field.getAccessFlags(); @@ -95,7 +93,7 @@ public class JField extends JNode { @Override public boolean hasDescString() { - return true; + return false; } @Override @@ -107,4 +105,21 @@ public class JField extends JNode { public boolean equals(Object o) { return this == o || o instanceof JField && field.equals(((JField) o).field); } + + private static final Comparator COMPARATOR = Comparator + .comparing(JField::getJParent) + .thenComparing(JNode::getName) + .thenComparingInt(JField::getPos); + + public int compareToFld(@NotNull JField other) { + return COMPARATOR.compare(this, other); + } + + @Override + public int compareTo(@NotNull JNode other) { + if (other instanceof JField) { + return compareToFld(((JField) other)); + } + return super.compareTo(other); + } } diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JMethod.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JMethod.java index a0f2865c..8756ba32 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JMethod.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JMethod.java @@ -1,11 +1,13 @@ package jadx.gui.treemodel; +import java.util.Comparator; import java.util.Iterator; import javax.swing.Icon; import javax.swing.ImageIcon; import org.fife.ui.rsyntaxtextarea.SyntaxConstants; +import org.jetbrains.annotations.NotNull; import jadx.api.JavaMethod; import jadx.api.JavaNode; @@ -57,11 +59,6 @@ public class JMethod extends JNode { return jParent.getRootClass(); } - @Override - public int getLine() { - return mth.getDecompiledLine(); - } - @Override public Icon getIcon() { AccessInfo accessFlags = mth.getAccessFlags(); @@ -130,6 +127,11 @@ public class JMethod extends JNode { return base.toString(); } + @Override + public String getName() { + return mth.getName(); + } + @Override public String makeString() { return UiUtils.typeFormat(makeBaseString(), getReturnType()); @@ -159,7 +161,12 @@ public class JMethod extends JNode { @Override public boolean hasDescString() { - return true; + return false; + } + + @Override + public int getPos() { + return mth.getDefPos(); } @Override @@ -171,4 +178,29 @@ public class JMethod extends JNode { public boolean equals(Object o) { return this == o || o instanceof JMethod && mth.equals(((JMethod) o).mth); } + + private static final Comparator COMPARATOR = Comparator + .comparing(JMethod::getJParent) + .thenComparing(jMethod -> jMethod.mth.getMethodNode().getMethodInfo().getShortId()) + .thenComparingInt(JMethod::getPos); + + public int compareToMth(@NotNull JMethod other) { + return COMPARATOR.compare(this, other); + } + + @Override + public int compareTo(@NotNull JNode other) { + if (other instanceof JMethod) { + return compareToMth(((JMethod) other)); + } + if (other instanceof JClass) { + JClass cls = (JClass) other; + int cmp = jParent.compareToCls(cls); + if (cmp != 0) { + return cmp; + } + return 1; + } + return super.compareTo(other); + } } diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JNode.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JNode.java index 5af0c390..f0e646a8 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JNode.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JNode.java @@ -1,18 +1,20 @@ package jadx.gui.treemodel; +import java.util.Comparator; + import javax.swing.Icon; import javax.swing.tree.DefaultMutableTreeNode; import org.fife.ui.rsyntaxtextarea.SyntaxConstants; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import jadx.api.ICodeInfo; -import jadx.api.JadxDecompiler; import jadx.api.JavaNode; import jadx.gui.ui.TabbedPane; import jadx.gui.ui.panel.ContentPanel; -public abstract class JNode extends DefaultMutableTreeNode { +public abstract class JNode extends DefaultMutableTreeNode implements Comparable { private static final long serialVersionUID = -5154479091781041008L; @@ -29,10 +31,6 @@ public abstract class JNode extends DefaultMutableTreeNode { return null; } - public String getContent() { - return null; - } - @Nullable public ContentPanel getContentPanel(TabbedPane tabbedPane) { return null; @@ -42,30 +40,9 @@ public abstract class JNode extends DefaultMutableTreeNode { return SyntaxConstants.SYNTAX_STYLE_NONE; } - public int getLine() { - return 0; - } - - @Nullable + @NotNull public ICodeInfo getCodeInfo() { - return null; - } - - public final Integer getSourceLine(int line) { - ICodeInfo codeInfo = getCodeInfo(); - if (codeInfo == null) { - return null; - } - return codeInfo.getLineMapping().get(line); - } - - @Nullable - public JavaNode getJavaNodeAtPosition(JadxDecompiler decompiler, int line, int offset) { - ICodeInfo codeInfo = getCodeInfo(); - if (codeInfo == null) { - return null; - } - return decompiler.getJavaNodeAtPosition(codeInfo, line, offset); + return ICodeInfo.EMPTY; } public abstract Icon getIcon(); @@ -116,6 +93,15 @@ public abstract class JNode extends DefaultMutableTreeNode { return null; } + private static final Comparator COMPARATOR = Comparator + .comparing(JNode::makeLongString) + .thenComparingInt(JNode::getPos); + + @Override + public int compareTo(@NotNull JNode other) { + return COMPARATOR.compare(this, other); + } + @Override public String toString() { return makeString(); diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JPackage.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JPackage.java index 85c763dc..44c01b50 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JPackage.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JPackage.java @@ -7,14 +7,12 @@ import java.util.List; import javax.swing.Icon; import javax.swing.ImageIcon; -import org.jetbrains.annotations.NotNull; - import jadx.api.JavaPackage; import jadx.core.utils.Utils; import jadx.gui.JadxWrapper; import jadx.gui.utils.UiUtils; -public class JPackage extends JNode implements Comparable { +public class JPackage extends JNode { private static final long serialVersionUID = -4120718634156839804L; private static final ImageIcon PACKAGE_ICON = UiUtils.openSvgIcon("nodes/package"); @@ -120,16 +118,6 @@ public class JPackage extends JNode implements Comparable { return null; } - @Override - public int getLine() { - return 0; - } - - @Override - public int compareTo(@NotNull JPackage o) { - return name.compareTo(o.name); - } - @Override public boolean equals(Object o) { if (this == o) { diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JResSearchNode.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JResSearchNode.java index 976fc913..13a84945 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JResSearchNode.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JResSearchNode.java @@ -8,13 +8,11 @@ public class JResSearchNode extends JNode { private static final long serialVersionUID = -2222084945157778639L; private final transient JResource resNode; private final transient String text; - private final transient int line; private final transient int pos; - public JResSearchNode(JResource resNode, String text, int line, int pos) { + public JResSearchNode(JResource resNode, String text, int pos) { this.pos = pos; this.text = text; - this.line = line; this.resNode = resNode; } @@ -36,11 +34,6 @@ public class JResSearchNode extends JNode { return resNode.getJParent(); } - @Override - public int getLine() { - return line; - } - @Override public String makeLongStringHtml() { return getName(); diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java index 2014600c..4be0b962 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java @@ -9,7 +9,6 @@ import javax.swing.Icon; import javax.swing.ImageIcon; import org.fife.ui.rsyntaxtextarea.SyntaxConstants; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import jadx.api.ICodeInfo; @@ -28,7 +27,7 @@ import jadx.gui.ui.panel.ImagePanel; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; -public class JResource extends JLoadableNode implements Comparable { +public class JResource extends JLoadableNode { private static final long serialVersionUID = -201018424302612434L; private static final ImageIcon ROOT_ICON = UiUtils.openSvgIcon("nodes/resourcesRoot"); @@ -94,7 +93,7 @@ public class JResource extends JLoadableNode implements Comparable { @Override public void loadNode() { - getContent(); + getCodeInfo(); update(); } @@ -107,12 +106,6 @@ public class JResource extends JLoadableNode implements Comparable { return files; } - @Override - public @Nullable ICodeInfo getCodeInfo() { - getContent(); - return content; - } - @Override public @Nullable ContentPanel getContentPanel(TabbedPane tabbedPane) { if (resFile == null) { @@ -125,35 +118,36 @@ public class JResource extends JLoadableNode implements Comparable { } @Override - public synchronized String getContent() { + public synchronized ICodeInfo getCodeInfo() { if (loaded) { - if (content == null) { - return null; - } - return content.getCodeStr(); + return content; } + ICodeInfo codeInfo = loadContent(); + content = codeInfo; + loaded = true; + return codeInfo; + } + + private ICodeInfo loadContent() { if (resFile == null || type != JResType.FILE) { - return null; + return ICodeInfo.EMPTY; } if (!isSupportedForView(resFile.getType())) { - return null; + return ICodeInfo.EMPTY; } ResContainer rc = resFile.loadContent(); if (rc == null) { - loaded = true; - return null; + return ICodeInfo.EMPTY; } if (rc.getDataType() == ResContainer.DataType.RES_TABLE) { - content = loadCurrentSingleRes(rc); + ICodeInfo codeInfo = loadCurrentSingleRes(rc); for (ResContainer subFile : rc.getSubFiles()) { loadSubNodes(this, subFile, 1); } - } else { - // single node - content = loadCurrentSingleRes(rc); + return codeInfo; } - loaded = true; - return content.getCodeStr(); + // single node + return loadCurrentSingleRes(rc); } private ICodeInfo loadCurrentSingleRes(ResContainer rc) { @@ -326,13 +320,13 @@ public class JResource extends JLoadableNode implements Comparable { } @Override - public int compareTo(@NotNull JResource o) { - return name.compareTo(o.name); + public String makeString() { + return shortName; } @Override - public String makeString() { - return shortName; + public String makeLongString() { + return name; } @Override diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JRoot.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JRoot.java index 106f26df..00b3107d 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JRoot.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JRoot.java @@ -131,11 +131,6 @@ public class JRoot extends JNode { return null; } - @Override - public int getLine() { - return 0; - } - @Override public String makeString() { List paths = wrapper.getOpenPaths(); diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JVariable.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JVariable.java index 567d7ae8..9a8e932b 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JVariable.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JVariable.java @@ -35,6 +35,11 @@ public class JVariable extends JNode { return jMth.getJParent(); } + @Override + public int getPos() { + return var.getDefPos(); + } + @Override public Icon getIcon() { return null; diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/TextNode.java b/jadx-gui/src/main/java/jadx/gui/treemodel/TextNode.java index 9b32cb23..8bd8d5f1 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/TextNode.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/TextNode.java @@ -17,11 +17,6 @@ public class TextNode extends JNode { return null; } - @Override - public int getLine() { - return 0; - } - @Override public Icon getIcon() { return null; diff --git a/jadx-gui/src/main/java/jadx/gui/ui/MainDropTarget.java b/jadx-gui/src/main/java/jadx/gui/ui/MainDropTarget.java index 8f69cd35..9646a770 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/MainDropTarget.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/MainDropTarget.java @@ -13,7 +13,7 @@ import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static jadx.gui.utils.FileUtils.toPaths; +import jadx.core.utils.files.FileUtils; /** * Enables drop support from external applications for the {@link MainWindow} (load dropped APK @@ -64,7 +64,7 @@ public class MainDropTarget implements DropTargetListener { List transferData = (List) transferable.getTransferData(DataFlavor.javaFileListFlavor); if (!transferData.isEmpty()) { dtde.dropComplete(true); - mainWindow.open(toPaths(transferData)); + mainWindow.open(FileUtils.toPaths(transferData)); } } catch (Exception e) { LOG.error("File drop operation failed", e); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java index 0ca366a7..0b04873e 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java @@ -30,15 +30,12 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.EnumSet; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.Timer; import java.util.TimerTask; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.stream.Collectors; import javax.swing.AbstractAction; @@ -95,8 +92,6 @@ import jadx.gui.device.debugger.BreakpointManager; import jadx.gui.jobs.BackgroundExecutor; import jadx.gui.jobs.DecompileTask; import jadx.gui.jobs.ExportTask; -import jadx.gui.jobs.IndexService; -import jadx.gui.jobs.IndexTask; import jadx.gui.jobs.ProcessResult; import jadx.gui.jobs.TaskStatus; import jadx.gui.plugins.quark.QuarkDialog; @@ -133,18 +128,14 @@ import jadx.gui.update.JadxUpdate.IUpdateCallback; import jadx.gui.update.data.Release; import jadx.gui.utils.CacheObject; import jadx.gui.utils.FontUtils; -import jadx.gui.utils.JumpPosition; import jadx.gui.utils.LafManager; import jadx.gui.utils.Link; import jadx.gui.utils.NLS; import jadx.gui.utils.SystemInfo; import jadx.gui.utils.UiUtils; import jadx.gui.utils.logs.LogCollector; -import jadx.gui.utils.search.CommentsIndex; -import jadx.gui.utils.search.TextSearchIndex; import static io.reactivex.internal.functions.Functions.EMPTY_RUNNABLE; -import static jadx.gui.utils.FileUtils.fileNamesToPaths; import static javax.swing.KeyStroke.getKeyStroke; public class MainWindow extends JFrame { @@ -209,9 +200,9 @@ public class MainWindow extends JFrame { private JSplitPane verticalSplitter; public MainWindow(JadxSettings settings) { - this.wrapper = new JadxWrapper(settings); this.settings = settings; this.cacheObject = new CacheObject(); + this.wrapper = new JadxWrapper(settings); resetCache(); FontUtils.registerBundledFonts(); @@ -248,7 +239,7 @@ public class MainWindow extends JFrame { if (settings.getFiles().isEmpty()) { openFileOrProject(); } else { - open(fileNamesToPaths(settings.getFiles()), this::handleSelectClassOption); + open(FileUtils.fileNamesToPaths(settings.getFiles()), this::handleSelectClassOption); } } @@ -422,9 +413,11 @@ public class MainWindow extends JFrame { deobfToggleBtn.setSelected(settings.isDeobfuscationOn()); initTree(); update(); - restoreOpenTabs(); - runInitialBackgroundJobs(); BreakpointManager.init(paths.get(0).toAbsolutePath().getParent()); + + backgroundExecutor.execute(NLS.str("progress.load"), + this::restoreOpenTabs, + status -> runInitialBackgroundJobs()); } private void addTreeCustomNodes() { @@ -472,7 +465,6 @@ public class MainWindow extends JFrame { jadxProject.setMainWindow(this); this.project = jadxProject; this.wrapper.setProject(jadxProject); - this.cacheObject.setCommentsIndex(new CommentsIndex(wrapper, cacheObject, jadxProject)); update(); } @@ -495,9 +487,6 @@ public class MainWindow extends JFrame { cacheObject.reset(); cacheObject.setJRoot(treeRoot); cacheObject.setJadxSettings(settings); - - cacheObject.setIndexService(new IndexService(cacheObject)); - cacheObject.setTextIndex(new TextSearchIndex(this)); } synchronized void runInitialBackgroundJobs() { @@ -515,20 +504,11 @@ public class MainWindow extends JFrame { public void waitDecompileTask() { synchronized (DECOMPILER_TASK_SYNC) { - if (cacheObject.getIndexService().isComplete()) { - return; - } try { - DecompileTask decompileTask = new DecompileTask(this, wrapper); + DecompileTask decompileTask = new DecompileTask(wrapper); backgroundExecutor.executeAndWait(decompileTask); - backgroundExecutor.execute(decompileTask.getTitle(), wrapper::unloadClasses).get(); - System.gc(); - - IndexTask indexTask = new IndexTask(this, wrapper); - backgroundExecutor.executeAndWait(indexTask); - - processDecompilationResults(decompileTask.getResult(), indexTask.getResult()); + processDecompilationResults(decompileTask.getResult()); System.gc(); } catch (Exception e) { LOG.error("Decompile task execution failed", e); @@ -536,12 +516,12 @@ public class MainWindow extends JFrame { } } - private void processDecompilationResults(ProcessResult decompile, ProcessResult index) { - int skippedCls = Math.max(decompile.getSkipped(), index.getSkipped()); + private void processDecompilationResults(ProcessResult decompile) { + int skippedCls = decompile.getSkipped(); if (skippedCls == 0) { return; } - TaskStatus status = mergeStatus(EnumSet.of(decompile.getStatus(), index.getStatus())); + TaskStatus status = decompile.getStatus(); LOG.warn("Decompile and indexing of some classes skipped: {}, status: {}", skippedCls, status); switch (status) { case CANCEL_BY_USER: { @@ -564,26 +544,8 @@ public class MainWindow extends JFrame { } } - private TaskStatus mergeStatus(Set statuses) { - if (statuses.size() == 1) { - return statuses.iterator().next(); - } - if (statuses.contains(TaskStatus.CANCEL_BY_MEMORY)) { - return TaskStatus.CANCEL_BY_MEMORY; - } - if (statuses.contains(TaskStatus.CANCEL_BY_TIMEOUT)) { - return TaskStatus.CANCEL_BY_TIMEOUT; - } - if (statuses.contains(TaskStatus.CANCEL_BY_USER)) { - return TaskStatus.CANCEL_BY_USER; - } - return TaskStatus.COMPLETE; - } - public void cancelBackgroundJobs() { - ExecutorService worker = Executors.newSingleThreadExecutor(); - worker.execute(backgroundExecutor::cancelAll); - worker.shutdown(); + backgroundExecutor.cancelAll(); } public void reOpenFile() { @@ -706,7 +668,7 @@ public class MainWindow extends JFrame { } else if (obj instanceof JNode) { JNode node = (JNode) obj; if (node.getRootClass() != null) { - tabbedPane.codeJump(new JumpPosition(node)); + tabbedPane.codeJump(node); return true; } return tabbedPane.showNode(node); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/TabbedPane.java b/jadx-gui/src/main/java/jadx/gui/ui/TabbedPane.java index 65ad4783..7d884045 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/TabbedPane.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/TabbedPane.java @@ -10,17 +10,14 @@ import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import javax.swing.JTabbedPane; import javax.swing.SwingUtilities; -import javax.swing.text.BadLocationException; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import jadx.core.utils.StringUtils; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JNode; @@ -168,53 +165,43 @@ public class TabbedPane extends JTabbedPane { return mainWindow; } - private void showCode(final JumpPosition jumpPos) { - JNode jumpNode = jumpPos.getNode(); - Objects.requireNonNull(jumpNode, "Null node in JumpPosition"); - + /** + * Jump to node definition + */ + public void codeJump(JNode node) { + if (node.getPos() != 0 || node.getRootClass() == null) { + codeJump(new JumpPosition(node)); + return; + } + // node need loading mainWindow.getBackgroundExecutor().execute( NLS.str("progress.load"), - jumpNode::getContent, // run heavy loading in background - status -> { - // show the code in UI thread - AbstractCodeContentPanel contentPanel = (AbstractCodeContentPanel) getContentPanel(jumpNode); - if (contentPanel != null) { - scrollToPos(contentPanel, jumpPos); - selectTab(contentPanel); - } - }); + () -> node.getRootClass().getCodeInfo(), // run heavy loading in background + status -> codeJump(new JumpPosition(node))); } - private void scrollToPos(AbstractCodeContentPanel contentPanel, JumpPosition jumpPos) { - AbstractCodeArea codeArea = contentPanel.getCodeArea(); - int pos = jumpPos.getPos(); - if (pos > 0) { - codeArea.scrollToPos(pos); - } else { - int line = jumpPos.getLine(); - if (line < 0) { - try { - line = 1 + codeArea.getLineOfOffset(-line); - } catch (BadLocationException e) { - LOG.error("Can't get line for: {}", jumpPos, e); - line = jumpPos.getNode().getLine(); - } - } - int lineNum = Math.max(0, line - 1); - try { - int offs = codeArea.getLineStartOffset(lineNum); - while (StringUtils.isWhite(codeArea.getText(offs, 1).charAt(0))) { - offs += 1; - } - offs += pos; - jumpPos.setPos(offs); - codeArea.scrollToPos(offs); - } catch (BadLocationException e) { - LOG.error("Failed to jump to position: {}", pos, e); - codeArea.scrollToLine(line); - } + /** + * Prefer {@link TabbedPane#codeJump(JNode)} method + */ + public void codeJump(JumpPosition pos) { + saveJump(pos); + showCode(pos); + } + + private void saveJump(JumpPosition pos) { + JumpPosition curPos = getCurrentPosition(); + if (curPos != null) { + jumps.addPosition(curPos); + jumps.addPosition(pos); + } + } + + private void showCode(JumpPosition jumpPos) { + ContentPanel contentPanel = getContentPanel(jumpPos.getNode()); + if (contentPanel != null) { + scrollToPos(contentPanel, jumpPos.getPos()); + selectTab(contentPanel); } - codeArea.requestFocus(); } public boolean showNode(JNode node) { @@ -226,6 +213,18 @@ public class TabbedPane extends JTabbedPane { return true; } + private void scrollToPos(ContentPanel contentPanel, int pos) { + if (pos == 0) { + LOG.warn("Ignore zero jump!", new JadxRuntimeException()); + return; + } + if (contentPanel instanceof AbstractCodeContentPanel) { + AbstractCodeArea codeArea = ((AbstractCodeContentPanel) contentPanel).getCodeArea(); + codeArea.scrollToPos(pos); + codeArea.requestFocus(); + } + } + public void selectTab(ContentPanel contentPanel) { setSelectedComponent(contentPanel); if (mainWindow.getSettings().isAlwaysSelectOpened()) { @@ -233,26 +232,10 @@ public class TabbedPane extends JTabbedPane { } } - /** - * Jump to node definition - */ - public void codeJump(JNode node) { - codeJump(new JumpPosition(Objects.requireNonNull(node))); - } - - public void codeJump(JumpPosition pos) { - JumpPosition curPos = getCurrentPosition(); - if (curPos != null) { - jumps.addPosition(curPos); - jumps.addPosition(pos); - } - showCode(pos); - } - public void smaliJump(JClass cls, int pos, boolean debugMode) { ContentPanel panel = getOpenTabs().get(cls); if (panel == null) { - showCode(new JumpPosition(cls, 0, 1)); + showCode(new JumpPosition(cls, 1)); panel = getOpenTabs().get(cls); if (panel == null) { throw new JadxRuntimeException("Failed to open panel for JClass: " + cls); @@ -440,21 +423,15 @@ public class TabbedPane extends JTabbedPane { static void focusOnCodePanel(ContentPanel pane) { if (pane instanceof ClassCodeContentPanel) { - SwingUtilities.invokeLater(() -> { - ((ClassCodeContentPanel) pane).getCurrentCodeArea().requestFocus(); - }); + SwingUtilities.invokeLater(() -> ((ClassCodeContentPanel) pane).getCurrentCodeArea().requestFocus()); return; } if (pane instanceof AbstractCodeContentPanel) { - SwingUtilities.invokeLater(() -> { - ((AbstractCodeContentPanel) pane).getCodeArea().requestFocus(); - }); + SwingUtilities.invokeLater(() -> ((AbstractCodeContentPanel) pane).getCodeArea().requestFocus()); return; } if (pane instanceof HtmlPanel) { - SwingUtilities.invokeLater(() -> { - ((HtmlPanel) pane).getHtmlArea().requestFocusInWindow(); - }); + SwingUtilities.invokeLater(() -> ((HtmlPanel) pane).getHtmlArea().requestFocusInWindow()); return; } if (pane instanceof ImagePanel) { diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java index 7336adf8..fd45ca5c 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java @@ -28,10 +28,12 @@ import org.fife.ui.rsyntaxtextarea.TokenMakerFactory; import org.fife.ui.rsyntaxtextarea.TokenTypes; import org.fife.ui.rtextarea.SearchContext; import org.fife.ui.rtextarea.SearchEngine; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import jadx.api.ICodeInfo; import jadx.core.utils.StringUtils; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.gui.settings.JadxSettings; @@ -230,6 +232,8 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea { } } + public abstract @NotNull ICodeInfo getCodeInfo(); + /** * Implement in this method the code that loads and sets the content to be displayed */ @@ -264,28 +268,10 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea { public void scrollToPos(int pos) { try { setCaretPosition(pos); + centerCurrentLine(); + forceCurrentLineHighlightRepaint(); } catch (Exception e) { - LOG.debug("Can't scroll to position {}", pos, e); - } - centerCurrentLine(); - forceCurrentLineHighlightRepaint(); - } - - public void scrollToLine(int line) { - int lineNum = line - 1; - if (lineNum < 0) { - lineNum = 0; - } - setCaretAtLine(lineNum); - centerCurrentLine(); - forceCurrentLineHighlightRepaint(); - } - - private void setCaretAtLine(int line) { - try { - setCaretPosition(getLineStartOffset(line)); - } catch (BadLocationException e) { - LOG.debug("Can't scroll to {}", line, e); + LOG.warn("Can't scroll to position {}", pos, e); } } @@ -317,7 +303,8 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea { } /** - * @param str - if null -> reset current highlights + * @param str + * - if null -> reset current highlights */ private void highlightAllMatches(@Nullable String str) { SearchContext context = new SearchContext(str); @@ -328,7 +315,15 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea { } public JumpPosition getCurrentPosition() { - return new JumpPosition(node, getCaretLineNumber() + 1, getCaretPosition()); + return new JumpPosition(node, getCaretPosition()); + } + + public int getLineStartFor(int pos) throws BadLocationException { + return getLineStartOffset(getLineOfOffset(pos)); + } + + public String getLineAt(int pos) throws BadLocationException { + return getLineText(getLineOfOffset(pos) + 1); } public String getLineText(int line) throws BadLocationException { @@ -338,11 +333,6 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea { return getText(startOffset, endOffset - startOffset); } - @Nullable - Integer getSourceLine(int line) { - return node.getSourceLine(line); - } - public ContentPanel getContentPanel() { return contentPanel; } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java index 2e134689..e85ff6f3 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java @@ -4,6 +4,7 @@ import java.awt.Point; import java.awt.event.InputEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; +import java.util.Objects; import javax.swing.event.PopupMenuEvent; @@ -14,9 +15,12 @@ import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import jadx.api.CodePosition; +import jadx.api.ICodeInfo; import jadx.api.JadxDecompiler; +import jadx.api.JavaClass; import jadx.api.JavaNode; +import jadx.api.metadata.ICodeAnnotation; +import jadx.core.dex.nodes.ClassNode; import jadx.gui.settings.JadxProject; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JNode; @@ -37,6 +41,8 @@ public final class CodeArea extends AbstractCodeArea { private static final long serialVersionUID = 6312736869579635796L; + private @Nullable ICodeInfo cachedCodeInfo; + CodeArea(ContentPanel contentPanel, JNode node) { super(contentPanel, node); setSyntaxEditingStyle(node.getSyntaxName()); @@ -67,23 +73,32 @@ public final class CodeArea extends AbstractCodeArea { @SuppressWarnings("deprecation") private void navToDecl(Point point, CodeLinkGenerator codeLinkGenerator) { int offs = viewToModel(point); - JumpPosition jump = codeLinkGenerator.getJumpLinkAtOffset(CodeArea.this, offs); - if (jump != null) { - contentPanel.getTabbedPane().codeJump(jump); + JNode node = getJNodeAtOffset(codeLinkGenerator.getLinkSourceOffset(offs)); + if (node != null) { + contentPanel.getTabbedPane().codeJump(node); } } + @Override + public ICodeInfo getCodeInfo() { + if (cachedCodeInfo == null) { + cachedCodeInfo = Objects.requireNonNull(node.getCodeInfo()); + } + return cachedCodeInfo; + } + @Override public void load() { if (getText().isEmpty()) { - setText(node.getContent()); + setText(getCodeInfo().getCodeStr()); setCaretPosition(0); } } @Override public void refresh() { - setText(node.getContent()); + cachedCodeInfo = null; + setText(getCodeInfo().getCodeStr()); } private void addMenuItems() { @@ -157,12 +172,12 @@ public final class CodeArea extends AbstractCodeArea { if (foundNode == null) { return null; } - CodePosition pos = getDecompiler().getDefinitionPosition(foundNode); - if (pos == null) { - return null; + if (foundNode == node.getJavaNode()) { + // current node + return new JumpPosition(node); } JNode jNode = convertJavaNode(foundNode); - return new JumpPosition(jNode.getRootClass(), pos); + return new JumpPosition(jNode); } private JNode convertJavaNode(JavaNode javaNode) { @@ -208,24 +223,45 @@ public final class CodeArea extends AbstractCodeArea { return null; } try { - // TODO: add direct mapping for code offset to CodeWriter (instead of line and line offset pair) - int line = this.getLineOfOffset(offset); - int lineOffset = offset - this.getLineStartOffset(line); - return node.getJavaNodeAtPosition(getDecompiler(), line + 1, lineOffset + 1); + return getDecompiler().getJavaNodeAtPosition(getCodeInfo(), offset); } catch (Exception e) { LOG.error("Can't get java node by offset: {}", offset, e); } return null; } + public JavaNode getClosestJavaNode(int offset) { + try { + return getDecompiler().getClosestJavaNode(getCodeInfo(), offset); + } catch (Exception e) { + LOG.error("Can't get java node by offset: {}", offset, e); + return null; + } + } + + public JavaClass getJavaClassIfAtPos(int pos) { + try { + ICodeInfo codeInfo = getCodeInfo(); + if (codeInfo.hasMetadata()) { + ICodeAnnotation ann = codeInfo.getCodeMetadata().getAt(pos); + if (ann instanceof ClassNode) { + return getDecompiler().getJavaClassByNode(((ClassNode) ann)); + } + } + } catch (Exception e) { + LOG.error("Can't get java node by offset: {}", pos, e); + } + return null; + } + public void refreshClass() { if (node instanceof JClass) { - JClass cls = (JClass) node; + JClass cls = node.getRootClass(); try { CaretPositionFix caretFix = new CaretPositionFix(this); caretFix.save(); - cls.reload(getMainWindow().getCacheObject()); + cachedCodeInfo = cls.reload(getMainWindow().getCacheObject()); ClassCodeContentPanel codeContentPanel = (ClassCodeContentPanel) this.contentPanel; codeContentPanel.getTabbedPane().refresh(cls); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeLinkGenerator.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeLinkGenerator.java index 939230ef..87b6775a 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeLinkGenerator.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeLinkGenerator.java @@ -3,7 +3,6 @@ package jadx.gui.ui.codearea; import java.util.Objects; import javax.swing.event.HyperlinkEvent; -import javax.swing.text.BadLocationException; import org.fife.ui.rsyntaxtextarea.LinkGenerator; import org.fife.ui.rsyntaxtextarea.LinkGeneratorResult; @@ -28,12 +27,12 @@ public class CodeLinkGenerator implements LinkGenerator { this.jNode = codeArea.getNode(); } - public JavaNode getNodeAtOffset(RSyntaxTextArea textArea, int offset) { + public JavaNode getNodeAtOffset(int offset) { try { - if (jNode.getCodeInfo() == null) { + if (!codeArea.getCodeInfo().hasMetadata()) { return null; } - int sourceOffset = getLinkSourceOffset(textArea, offset); + int sourceOffset = getLinkSourceOffset(offset); if (sourceOffset == -1) { return null; } @@ -44,34 +43,17 @@ public class CodeLinkGenerator implements LinkGenerator { } } - @Nullable - public JumpPosition getJumpLinkAtOffset(RSyntaxTextArea textArea, int offset) { - try { - if (jNode.getCodeInfo() == null) { - return null; - } - int sourceOffset = getLinkSourceOffset(textArea, offset); - if (sourceOffset == -1) { - return null; - } - return getJumpBySourceOffset(textArea, sourceOffset); - } catch (Exception e) { - LOG.error("getJumpLinkAtOffset error", e); - return null; - } - } - @Override public LinkGeneratorResult isLinkAtOffset(RSyntaxTextArea textArea, int offset) { try { - if (jNode.getCodeInfo() == null) { + if (!codeArea.getCodeInfo().hasMetadata()) { return null; } - int sourceOffset = getLinkSourceOffset(textArea, offset); + int sourceOffset = getLinkSourceOffset(offset); if (sourceOffset == -1) { return null; } - JumpPosition defPos = getJumpBySourceOffset(textArea, sourceOffset); + JumpPosition defPos = getJumpBySourceOffset(sourceOffset); if (defPos == null) { return null; } @@ -93,19 +75,19 @@ public class CodeLinkGenerator implements LinkGenerator { } } - private int getLinkSourceOffset(RSyntaxTextArea textArea, int offset) { - Token token = textArea.modelToToken(offset); + public int getLinkSourceOffset(int offset) { + Token token = codeArea.modelToToken(offset); return codeArea.adjustOffsetForToken(token); } @Nullable - private JumpPosition getJumpBySourceOffset(RSyntaxTextArea textArea, int sourceOffset) throws BadLocationException { + private JumpPosition getJumpBySourceOffset(int sourceOffset) { final JumpPosition defPos = codeArea.getDefPosForNodeAtOffset(sourceOffset); if (defPos == null) { return null; } if (Objects.equals(defPos.getNode().getRootClass(), jNode) - && defPos.getLine() == textArea.getLineOfOffset(sourceOffset) + 1) { + && defPos.getPos() == sourceOffset) { // ignore self jump return null; } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodePanel.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodePanel.java index 2d6f70dd..04ebfa3f 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodePanel.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodePanel.java @@ -155,11 +155,11 @@ public class CodePanel extends JPanel { } private boolean canShowDebugLines() { - ICodeInfo codeInfo = codeArea.getNode().getCodeInfo(); - if (codeInfo == null) { + ICodeInfo codeInfo = codeArea.getCodeInfo(); + if (!codeInfo.hasMetadata()) { return false; } - Map lineMapping = codeInfo.getLineMapping(); + Map lineMapping = codeInfo.getCodeMetadata().getLineMapping(); if (lineMapping.isEmpty()) { return false; } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CommentAction.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CommentAction.java index 0312ae43..9ce396b1 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CommentAction.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CommentAction.java @@ -10,19 +10,24 @@ import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import jadx.api.CodePosition; +import jadx.api.ICodeInfo; +import jadx.api.JadxDecompiler; import jadx.api.JavaClass; import jadx.api.JavaMethod; import jadx.api.JavaNode; import jadx.api.data.ICodeComment; -import jadx.api.data.annotations.InsnCodeOffset; import jadx.api.data.impl.JadxCodeComment; import jadx.api.data.impl.JadxCodeRef; import jadx.api.data.impl.JadxNodeRef; +import jadx.api.metadata.ICodeAnnotation; +import jadx.api.metadata.ICodeAnnotation.AnnType; +import jadx.api.metadata.ICodeMetadata; +import jadx.api.metadata.ICodeNodeRef; +import jadx.api.metadata.annotations.InsnCodeOffset; +import jadx.api.metadata.annotations.NodeDeclareRef; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JNode; import jadx.gui.ui.dialog.CommentDialog; -import jadx.gui.utils.CodeLinesInfo; import jadx.gui.utils.DefaultPopupMenuListener; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; @@ -47,15 +52,13 @@ public class CommentAction extends AbstractAction implements DefaultPopupMenuLis } else { this.topCls = null; } - UiUtils.addKeyBinding(codeArea, getKeyStroke(KeyEvent.VK_SEMICOLON, 0), "popup.add_comment", () -> { - int line = codeArea.getCaretLineNumber() + 1; - showCommentDialog(getCommentRef(line)); - }); + UiUtils.addKeyBinding(codeArea, getKeyStroke(KeyEvent.VK_SEMICOLON, 0), "popup.add_comment", + () -> showCommentDialog(getCommentRef(codeArea.getCaretPosition()))); } @Override public void popupMenuWillBecomeVisible(PopupMenuEvent e) { - ICodeComment codeComment = getCommentRef(getMouseLine()); + ICodeComment codeComment = getCommentRef(UiUtils.getOffsetAtMousePosition(codeArea)); setEnabled(codeComment != null); this.actionComment = codeComment; } @@ -79,61 +82,59 @@ public class CommentAction extends AbstractAction implements DefaultPopupMenuLis * @return blank code comment object (comment string empty) */ @Nullable - private ICodeComment getCommentRef(int line) { - if (line == -1 || this.topCls == null) { + private ICodeComment getCommentRef(int pos) { + if (pos == -1 || this.topCls == null) { return null; } try { - CodeLinesInfo linesInfo = new CodeLinesInfo(topCls, true); // TODO: cache and update on class refresh - // add comment if node definition at this line - JavaNode nodeAtLine = linesInfo.getDefAtLine(line); - if (nodeAtLine != null) { - // at node definition -> add comment for it - JadxNodeRef nodeRef = JadxNodeRef.forJavaNode(nodeAtLine); - return new JadxCodeComment(nodeRef, ""); - } - Object ann = topCls.getAnnotationAt(new CodePosition(line)); - if (ann == null) { - // check if line with comment above node definition - try { - JavaNode defNode = linesInfo.getJavaNodeBelowLine(line); - if (defNode != null) { - String lineStr = codeArea.getLineText(line).trim(); - if (lineStr.startsWith("//")) { - return new JadxCodeComment(JadxNodeRef.forJavaNode(defNode), ""); - } - } - } catch (Exception e) { - LOG.error("Failed to check comment line: " + line, e); - } - return null; - } + JadxDecompiler decompiler = codeArea.getDecompiler(); + ICodeInfo codeInfo = codeArea.getCodeInfo(); + ICodeMetadata metadata = codeInfo.getCodeMetadata(); + int lineStartPos = codeArea.getLineStartFor(pos); - // try to add method line comment - JavaNode node = linesInfo.getJavaNodeByLine(line); - if (node instanceof JavaMethod) { - JadxNodeRef nodeRef = JadxNodeRef.forMth((JavaMethod) node); - if (ann instanceof InsnCodeOffset) { - int rawOffset = ((InsnCodeOffset) ann).getOffset(); + // add method line comment by instruction offset + ICodeAnnotation offsetAnn = metadata.searchUp(pos, lineStartPos, AnnType.OFFSET); + if (offsetAnn instanceof InsnCodeOffset) { + JavaNode node = decompiler.getJavaNodeByRef(metadata.getNodeAt(pos)); + if (node instanceof JavaMethod) { + int rawOffset = ((InsnCodeOffset) offsetAnn).getOffset(); + JadxNodeRef nodeRef = JadxNodeRef.forMth((JavaMethod) node); return new JadxCodeComment(nodeRef, JadxCodeRef.forInsn(rawOffset), ""); } } + + // check for definition at this line + ICodeNodeRef nodeDef = metadata.searchUp(pos, (off, ann) -> { + if (lineStartPos <= off && ann.getAnnType() == AnnType.DECLARATION) { + ICodeNodeRef defRef = ((NodeDeclareRef) ann).getNode(); + if (defRef.getAnnType() != AnnType.VAR) { + return defRef; + } + } + return null; + }); + if (nodeDef != null) { + JadxNodeRef nodeRef = JadxNodeRef.forJavaNode(decompiler.getJavaNodeByRef(nodeDef)); + return new JadxCodeComment(nodeRef, ""); + } + + // check if at comment above node definition + String lineStr = codeArea.getLineAt(pos).trim(); + if (lineStr.startsWith("//") || lineStr.startsWith("/*")) { + ICodeNodeRef nodeRef = metadata.searchDown(pos, (off, ann) -> { + if (off > pos && ann.getAnnType() == AnnType.DECLARATION) { + return ((NodeDeclareRef) ann).getNode(); + } + return null; + }); + if (nodeRef != null) { + JavaNode defNode = decompiler.getJavaNodeByRef(nodeRef); + return new JadxCodeComment(JadxNodeRef.forJavaNode(defNode), ""); + } + } } catch (Exception e) { - LOG.error("Failed to add comment at line: " + line, e); + LOG.error("Failed to add comment at: " + pos, e); } return null; } - - private int getMouseLine() { - int closestOffset = UiUtils.getOffsetAtMousePosition(codeArea); - if (closestOffset == -1) { - return -1; - } - try { - return codeArea.getLineOfOffset(closestOffset) + 1; - } catch (Exception e) { - LOG.debug("Failed to get line by offset: {}", closestOffset); - return -1; - } - } } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/FridaAction.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/FridaAction.java index 3f16f153..b8e2be5c 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/FridaAction.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/FridaAction.java @@ -1,7 +1,7 @@ package jadx.gui.ui.codearea; import java.awt.event.KeyEvent; -import java.util.Comparator; +import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; @@ -12,10 +12,14 @@ import org.apache.commons.text.StringEscapeUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import jadx.api.ICodeInfo; import jadx.api.JavaClass; import jadx.api.JavaField; import jadx.api.JavaMethod; -import jadx.api.data.annotations.VarDeclareRef; +import jadx.api.metadata.ICodeNodeRef; +import jadx.api.metadata.annotations.NodeDeclareRef; +import jadx.api.metadata.annotations.VarNode; +import jadx.api.utils.CodeUtils; import jadx.core.codegen.TypeGen; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.args.ArgType; @@ -88,14 +92,7 @@ public final class FridaAction extends JNodeAction { } else { functionUntilImplementation = String.format("%s[\"%s\"].implementation", shortClassName, methodName); } - - String functionParametersString = - Objects.requireNonNull(javaMethod.getTopParentClass().getCodeInfo()).getAnnotations().entrySet().stream() - .filter(e -> e.getKey().getLine() == jMth.getLine() && e.getValue() instanceof VarDeclareRef) - .sorted(Comparator.comparingInt(e -> e.getKey().getPos())) - .map(e -> ((VarDeclareRef) e.getValue()).getName()) - .collect(Collectors.joining(", ")); - + String functionParametersString = String.join(", ", collectMethodArgNames(javaMethod)); String functionParameterAndBody = String.format( "%s = function(%s){\n" + " console.log('%s is called');\n" @@ -108,6 +105,29 @@ public final class FridaAction extends JNodeAction { return generateClassSnippet(jMth.getJParent()) + "\n" + functionParameterAndBody; } + private List collectMethodArgNames(JavaMethod javaMethod) { + ICodeInfo codeInfo = javaMethod.getTopParentClass().getCodeInfo(); + int mthDefPos = javaMethod.getDefPos(); + int lineEndPos = CodeUtils.getLineEndForPos(codeInfo.getCodeStr(), mthDefPos); + List argNames = new ArrayList<>(); + codeInfo.getCodeMetadata().searchDown(mthDefPos, (pos, ann) -> { + if (pos > lineEndPos) { + return Boolean.TRUE; // stop at line end + } + if (ann instanceof NodeDeclareRef) { + ICodeNodeRef declRef = ((NodeDeclareRef) ann).getNode(); + if (declRef instanceof VarNode) { + VarNode varNode = (VarNode) declRef; + if (varNode.getMth().equals(javaMethod.getMethodNode())) { + argNames.add(varNode.getName()); + } + } + } + return null; + }); + return argNames; + } + private String generateClassSnippet(JClass jc) { JavaClass javaClass = jc.getCls(); String rawClassName = StringEscapeUtils.escapeEcmaScript(javaClass.getRawName()); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/GoToDeclarationAction.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/GoToDeclarationAction.java index d76f7bcd..89fe1196 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/GoToDeclarationAction.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/GoToDeclarationAction.java @@ -2,11 +2,7 @@ package jadx.gui.ui.codearea; import java.awt.event.KeyEvent; -import org.jetbrains.annotations.Nullable; - -import jadx.api.CodePosition; import jadx.gui.treemodel.JNode; -import jadx.gui.utils.JumpPosition; import jadx.gui.utils.NLS; import static javax.swing.KeyStroke.getKeyStroke; @@ -14,31 +10,13 @@ import static javax.swing.KeyStroke.getKeyStroke; public final class GoToDeclarationAction extends JNodeAction { private static final long serialVersionUID = -1186470538894941301L; - private transient @Nullable JumpPosition declPos; - public GoToDeclarationAction(CodeArea codeArea) { super(NLS.str("popup.go_to_declaration") + " (d)", codeArea); addKeyBinding(getKeyStroke(KeyEvent.VK_D, 0), "trigger goto decl"); } - @Override - public boolean isActionEnabled(JNode node) { - declPos = null; - if (node == null) { - return false; - } - CodePosition defPos = getCodeArea().getDecompiler().getDefinitionPosition(node.getJavaNode()); - if (defPos == null) { - return false; - } - declPos = new JumpPosition(node.getRootClass(), defPos); - return true; - } - @Override public void runAction(JNode node) { - if (declPos != null) { - getCodeArea().getContentPanel().getTabbedPane().codeJump(declPos); - } + getCodeArea().getContentPanel().getTabbedPane().codeJump(node); } } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/JadxTokenMaker.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/JadxTokenMaker.java index 37fde9d7..34a0ec8e 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/JadxTokenMaker.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/JadxTokenMaker.java @@ -29,15 +29,16 @@ public final class JadxTokenMaker extends JavaTokenMaker { @Override public Token getTokenList(Segment text, int initialTokenType, int startOffset) { - Token tokens = super.getTokenList(text, initialTokenType, startOffset); - if (tokens.getType() != TokenTypes.NULL) { - try { + try { + Token tokens = super.getTokenList(text, initialTokenType, startOffset); + if (tokens.getType() != TokenTypes.NULL) { processTokens(tokens); - } catch (Exception e) { - LOG.error("Process tokens failed for text: {}", text, e); } + return tokens; + } catch (Throwable e) { // JavaTokenMaker throws 'java.lang.Error' if failed to parse input string + LOG.error("Process tokens failed for text: {}", text, e); + return new TokenImpl(); } - return tokens; } private void processTokens(Token tokens) { @@ -81,14 +82,14 @@ public final class JadxTokenMaker extends JavaTokenMaker { if (annotation) { offset++; } - JavaNode javaNode = codeArea.getJavaNodeAtOffset(offset); - if (javaNode instanceof JavaClass) { - String name = javaNode.getName(); + JavaClass javaCls = codeArea.getJavaClassIfAtPos(offset); + if (javaCls != null) { + String name = javaCls.getName(); String lexeme = current.getLexeme(); if (annotation && lexeme.length() > 1) { lexeme = lexeme.substring(1); } - if (!lexeme.equals(name) && isClassNameStart(javaNode, lexeme)) { + if (!lexeme.equals(name) && isClassNameStart(javaCls, lexeme)) { // try to replace long class name with one token Token replace = concatTokensUntil(current, name); if (replace != null && prev instanceof TokenImpl) { diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/LineNumbers.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/LineNumbers.java index a1941e72..55e2de57 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/LineNumbers.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/LineNumbers.java @@ -59,7 +59,7 @@ public class LineNumbers extends JPanel implements CaretListener { public LineNumbers(AbstractCodeArea codeArea) { this.codeArea = codeArea; - this.codeInfo = codeArea.getNode().getCodeInfo(); + this.codeInfo = codeArea.getCodeInfo(); setFont(codeArea.getFont()); SyntaxScheme syntaxScheme = codeArea.getSyntaxScheme(); @@ -252,7 +252,7 @@ public class LineNumbers extends JPanel implements CaretListener { if (!useSourceLines) { return String.valueOf(lineNumber); } - Integer sourceLine = codeInfo.getLineMapping().get(lineNumber); + Integer sourceLine = codeInfo.getCodeMetadata().getLineMapping().get(lineNumber); if (sourceLine == null) { return null; } @@ -260,8 +260,10 @@ public class LineNumbers extends JPanel implements CaretListener { } private int getMaxDebugLine() { - return codeInfo.getLineMapping().keySet().stream() - .mapToInt(Integer::intValue).max().orElse(0); + return codeInfo.getCodeMetadata().getLineMapping() + .keySet().stream() + .mapToInt(Integer::intValue) + .max().orElse(0); } @Override diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/MouseHoverHighlighter.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/MouseHoverHighlighter.java index 7ebf8ba2..e3d51282 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/MouseHoverHighlighter.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/MouseHoverHighlighter.java @@ -52,7 +52,7 @@ class MouseHoverHighlighter extends MouseMotionAdapter { // don't repaint highlight return true; } - JavaNode nodeAtOffset = codeLinkGenerator.getNodeAtOffset(codeArea, tokenOffset); + JavaNode nodeAtOffset = codeLinkGenerator.getNodeAtOffset(tokenOffset); if (nodeAtOffset == null) { return false; } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/SmaliArea.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/SmaliArea.java index 9382bfc1..d20efaf4 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/SmaliArea.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/SmaliArea.java @@ -35,6 +35,7 @@ import org.fife.ui.rtextarea.RTextAreaUI; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import jadx.api.ICodeInfo; import jadx.gui.device.debugger.BreakpointManager; import jadx.gui.device.debugger.DbgUtils; import jadx.gui.settings.JadxSettings; @@ -95,6 +96,11 @@ public final class SmaliArea extends AbstractCodeArea { } } + @Override + public ICodeInfo getCodeInfo() { + return ICodeInfo.EMPTY; + } + @Override public void refresh() { load(); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/mode/JCodeMode.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/mode/JCodeMode.java index 61b7f2e5..bebbc885 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/mode/JCodeMode.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/mode/JCodeMode.java @@ -3,7 +3,6 @@ package jadx.gui.ui.codearea.mode; import javax.swing.Icon; import org.fife.ui.rsyntaxtextarea.SyntaxConstants; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import jadx.api.DecompilationMode; @@ -40,7 +39,7 @@ public class JCodeMode extends JNode { } @Override - public @NotNull ICodeInfo getCodeInfo() { + public ICodeInfo getCodeInfo() { if (codeInfo != null) { return codeInfo; } @@ -49,11 +48,6 @@ public class JCodeMode extends JNode { return codeInfo; } - @Override - public String getContent() { - return getCodeInfo().getCodeStr(); - } - @Override public String getSyntaxName() { return SyntaxConstants.SYNTAX_STYLE_JAVA; diff --git a/jadx-gui/src/main/java/jadx/gui/ui/dialog/CommonSearchDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/dialog/CommonSearchDialog.java index 2038a6aa..1d78ba71 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/dialog/CommonSearchDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/dialog/CommonSearchDialog.java @@ -1,11 +1,10 @@ package jadx.gui.ui.dialog; +import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; -import java.awt.Cursor; import java.awt.Dimension; import java.awt.Font; -import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; @@ -15,8 +14,10 @@ import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; +import java.util.List; import java.util.Map; import javax.swing.AbstractAction; @@ -31,14 +32,11 @@ import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.ListSelectionModel; -import javax.swing.ScrollPaneConstants; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; -import javax.swing.SwingWorker; import javax.swing.table.AbstractTableModel; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumn; -import javax.swing.table.TableColumnModel; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import org.fife.ui.rtextarea.SearchContext; @@ -59,14 +57,13 @@ import jadx.gui.utils.JNodeCache; import jadx.gui.utils.JumpPosition; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; -import jadx.gui.utils.search.TextSearchIndex; + +import static javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED; +import static javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED; public abstract class CommonSearchDialog extends JFrame { - private static final long serialVersionUID = 8939332306115370276L; - private static final Logger LOG = LoggerFactory.getLogger(CommonSearchDialog.class); - - public static final int RESULTS_PER_PAGE = 100; + private static final long serialVersionUID = 8939332306115370276L; protected final transient TabbedPane tabbedPane; protected final transient CacheObject cache; @@ -119,17 +116,6 @@ public abstract class CommonSearchDialog extends JFrame { updateTitle(); } - public void prepare() { - if (cache.getIndexService().isComplete()) { - loadFinishedCommon(); - loadFinished(); - return; - } - LoadTask task = new LoadTask(); - task.addPropertyChangeListener(progressPane); - task.execute(); - } - protected void registerInitOnOpen() { addWindowListener(new WindowAdapter() { @Override @@ -139,23 +125,21 @@ public abstract class CommonSearchDialog extends JFrame { }); } - protected synchronized void performSearch() { - resultsTable.updateTable(); - updateProgressLabel(); - } - protected void openSelectedItem() { JNode node = getSelectedNode(); if (node == null) { return; } - JumpPosition jmpPos; + openItem(node); + } + + protected void openItem(JNode node) { if (node instanceof JResSearchNode) { - jmpPos = new JumpPosition(((JResSearchNode) node).getResNode(), node.getLine(), node.getPos()); + JumpPosition jmpPos = new JumpPosition(((JResSearchNode) node).getResNode(), node.getPos()); + tabbedPane.codeJump(jmpPos); } else { - jmpPos = new JumpPosition(node.getRootClass(), node.getLine(), node.getPos()); + tabbedPane.codeJump(node); } - tabbedPane.codeJump(jmpPos); if (!mainWindow.getSettings().getKeepCommonDialogOpen()) { dispose(); } @@ -214,7 +198,7 @@ public abstract class CommonSearchDialog extends JFrame { protected JPanel initResultsTable() { ResultsTableCellRenderer renderer = new ResultsTableCellRenderer(); resultsModel = new ResultsModel(renderer); - resultsModel.addTableModelListener(e -> updateProgressLabel()); + resultsModel.addTableModelListener(e -> updateProgressLabel(false)); resultsTable = new ResultsTable(resultsModel, renderer); resultsTable.setShowHorizontalLines(false); @@ -262,135 +246,134 @@ public abstract class CommonSearchDialog extends JFrame { warnLabel.setForeground(Color.RED); warnLabel.setVisible(false); + JScrollPane scroll = new JScrollPane(resultsTable, VERTICAL_SCROLLBAR_AS_NEEDED, HORIZONTAL_SCROLLBAR_AS_NEEDED); + // scroll.setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 0)); + + JPanel resultsActionsPanel = new JPanel(); + resultsActionsPanel.setLayout(new BoxLayout(resultsActionsPanel, BoxLayout.LINE_AXIS)); + resultsActionsPanel.setBorder(BorderFactory.createEmptyBorder(10, 0, 10, 0)); + addCustomResultsActions(resultsActionsPanel); + resultsInfoLabel = new JLabel(""); + resultsInfoLabel.setFont(mainWindow.getSettings().getFont()); + resultsActionsPanel.add(Box.createRigidArea(new Dimension(20, 0))); + resultsActionsPanel.add(resultsInfoLabel); + resultsActionsPanel.add(Box.createHorizontalGlue()); + JPanel resultsPanel = new JPanel(); resultsPanel.setLayout(new BoxLayout(resultsPanel, BoxLayout.PAGE_AXIS)); - resultsPanel.add(warnLabel); - resultsPanel.add(new JScrollPane(resultsTable, - ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, - ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED)); - - JPanel paginationPanel = new JPanel(); - paginationPanel.setAlignmentX(Component.LEFT_ALIGNMENT); - paginationPanel.setLayout(new BoxLayout(paginationPanel, BoxLayout.X_AXIS)); - resultsInfoLabel = new JLabel(""); - - JButton nextPageButton = new JButton("->"); - nextPageButton.setToolTipText(NLS.str("search_dialog.next_page")); - nextPageButton.addActionListener(e -> { - if (resultsModel.nextPage()) { - switchPage(renderer); - } - }); - - JButton prevPageButton = new JButton("<-"); - prevPageButton.setToolTipText(NLS.str("search_dialog.prev_page")); - prevPageButton.addActionListener(e -> { - if (resultsModel.prevPage()) { - switchPage(renderer); - } - }); - - paginationPanel.add(prevPageButton); - paginationPanel.add(nextPageButton); - paginationPanel.add(resultsInfoLabel); - - resultsPanel.add(paginationPanel); resultsPanel.setBorder(BorderFactory.createEmptyBorder(0, 10, 10, 10)); + resultsPanel.add(warnLabel, BorderLayout.PAGE_START); + resultsPanel.add(scroll, BorderLayout.CENTER); + resultsPanel.add(resultsActionsPanel, BorderLayout.PAGE_END); return resultsPanel; } - private void switchPage(ResultsTableCellRenderer renderer) { - renderer.clear(); - resultsTable.updateTable(); - updateProgressLabel(); - resultsTable.scrollRectToVisible(new Rectangle(0, 0, 1, 1)); + protected void addCustomResultsActions(JPanel actionsPanel) { } - protected void updateProgressLabel() { - String statusText = NLS.str("search_dialog.info_label", - resultsModel.getDisplayedResultsStart(), - resultsModel.getDisplayedResultsEnd(), - resultsModel.getResultCount()); + protected void updateProgressLabel(boolean complete) { + int count = resultsModel.getRowCount(); + String statusText; + if (complete) { + statusText = NLS.str("search_dialog.results_complete", count); + } else { + statusText = NLS.str("search_dialog.results_incomplete", count); + } resultsInfoLabel.setText(statusText); } protected void showSearchState() { - resultsInfoLabel.setText(NLS.str("search_dialog.tip_searching")); + resultsInfoLabel.setText(NLS.str("search_dialog.tip_searching") + "..."); } - protected static class ResultsTable extends JTable { + protected static final class ResultsTable extends JTable { private static final long serialVersionUID = 3901184054736618969L; private final transient ResultsTableCellRenderer renderer; + private final transient ResultsModel model; public ResultsTable(ResultsModel resultsModel, ResultsTableCellRenderer renderer) { super(resultsModel); + this.model = resultsModel; this.renderer = renderer; } - public void updateTable() { - ResultsModel model = (ResultsModel) getModel(); - TableColumnModel columnModel = getColumnModel(); - - int width = getParent().getWidth(); - int firstColMaxWidth = (int) (width * 0.5); - int rowCount = getRowCount(); + public void initColumnWidth() { int columnCount = getColumnCount(); - boolean addDescColumn = model.isAddDescColumn(); - if (!addDescColumn) { - firstColMaxWidth = width; + int width = getParent().getWidth(); + int colWidth = model.isAddDescColumn() ? width / 2 : width; + columnModel.getColumn(0).setPreferredWidth(colWidth); + for (int col = 1; col < columnCount; col++) { + columnModel.getColumn(col).setPreferredWidth(width); } - setRowHeight(10); // reset all rows height - for (int col = 0; col < columnCount; col++) { - int colWidth = 50; - for (int row = 0; row < rowCount; row++) { - Component comp = prepareRenderer(renderer, row, col); - if (comp == null) { - continue; - } - colWidth = Math.max(comp.getPreferredSize().width, colWidth); - int h = Math.max(getRowHeight(row), getHeight(comp)); - if (h > 1) { - setRowHeight(row, h); - } - } - colWidth += 10; - if (col == 0) { - colWidth = Math.min(colWidth, firstColMaxWidth); - } else { - colWidth = Math.max(colWidth, width - columnModel.getColumn(0).getPreferredWidth()); - } - TableColumn column = columnModel.getColumn(col); - column.setPreferredWidth(colWidth); - } - updateUI(); } - private int getHeight(@Nullable Component nodeComp) { - if (nodeComp != null) { - return Math.max(nodeComp.getHeight(), nodeComp.getPreferredSize().height); + public void updateTable() { + UiUtils.uiThreadGuard(); + long start = System.currentTimeMillis(); + int width = getParent().getWidth(); + TableColumn firstColumn = columnModel.getColumn(0); + if (model.isAddDescColumn()) { + if (firstColumn.getWidth() > width * 0.8) { + // first column too big and hide second column, resize it + firstColumn.setPreferredWidth(width / 2); + } + TableColumn secondColumn = columnModel.getColumn(1); + int columnMaxWidth = width * 2; // set big enough size to skip per row check + if (secondColumn.getWidth() < columnMaxWidth) { + secondColumn.setPreferredWidth(columnMaxWidth); + } + } else { + firstColumn.setPreferredWidth(width); } - return 0; + int rowCount = getRowCount(); + int columnCount = getColumnCount(); + Map, Integer> heightByType = new HashMap<>(); + for (int row = 0; row < rowCount; row++) { + Object value = model.getValueAt(row, 0); + Class valueType = value.getClass(); + Integer cachedHeight = heightByType.get(valueType); + if (cachedHeight != null) { + setRowHeight(row, cachedHeight); + } else { + int height = 0; + for (int col = 0; col < columnCount; col++) { + Component comp = prepareRenderer(renderer, row, col); + if (comp == null) { + continue; + } + Dimension preferredSize = comp.getPreferredSize(); + int h = Math.max(comp.getHeight(), preferredSize.height); + height = Math.max(height, h); + } + heightByType.put(valueType, height); + setRowHeight(row, height); + } + } + updateUI(); + if (LOG.isDebugEnabled()) { + LOG.debug("Update results table in {}ms, count: {}", System.currentTimeMillis() - start, rowCount); + } + } + + @Override + public Object getValueAt(int row, int column) { + return model.getValueAt(row, column); } } - protected static class ResultsModel extends AbstractTableModel { + protected static final class ResultsModel extends AbstractTableModel { private static final long serialVersionUID = -7821286846923903208L; - private static final String[] COLUMN_NAMES = { - NLS.str("search_dialog.col_node"), - NLS.str("search_dialog.col_code") - }; + private static final String[] COLUMN_NAMES = { NLS.str("search_dialog.col_node"), NLS.str("search_dialog.col_code") }; - private final transient ArrayList rows = new ArrayList<>(); + private final transient List rows = Collections.synchronizedList(new ArrayList<>()); private final transient ResultsTableCellRenderer renderer; private transient boolean addDescColumn; - private transient int start = 0; public ResultsModel(ResultsTableCellRenderer renderer) { this.renderer = renderer; } - protected void addAll(Collection nodes) { - rows.ensureCapacity(rows.size() + nodes.size()); + public void addAll(Collection nodes) { rows.addAll(nodes); if (!addDescColumn) { for (JNode row : rows) { @@ -403,7 +386,6 @@ public abstract class CommonSearchDialog extends JFrame { } public void clear() { - start = 0; addDescColumn = false; rows.clear(); renderer.clear(); @@ -413,43 +395,9 @@ public abstract class CommonSearchDialog extends JFrame { return addDescColumn; } - public int getResultCount() { - return rows.size(); - } - - public int getDisplayedResultsStart() { - if (rows.isEmpty()) { - return 0; - } - return start + 1; - } - - public int getDisplayedResultsEnd() { - return Math.min(rows.size(), start + RESULTS_PER_PAGE); - } - - public boolean nextPage() { - if (start + RESULTS_PER_PAGE < rows.size()) { - start += RESULTS_PER_PAGE; - return true; - } - return false; - } - - public boolean prevPage() { - if (start - RESULTS_PER_PAGE >= 0) { - start -= RESULTS_PER_PAGE; - return true; - } - return false; - } - @Override public int getRowCount() { - if (rows.isEmpty()) { - return 0; - } - return getDisplayedResultsEnd() - start; + return rows.size(); } @Override @@ -464,11 +412,11 @@ public abstract class CommonSearchDialog extends JFrame { @Override public Object getValueAt(int rowIndex, int columnIndex) { - return rows.get(rowIndex + start); + return rows.get(rowIndex); } } - protected class ResultsTableCellRenderer implements TableCellRenderer { + protected final class ResultsTableCellRenderer implements TableCellRenderer { private final JLabel emptyLabel = new JLabel(); private final Font font; private final Color codeSelectedColor; @@ -483,24 +431,20 @@ public abstract class CommonSearchDialog extends JFrame { } @Override - public Component getTableCellRendererComponent(JTable table, Object obj, boolean isSelected, - boolean hasFocus, int row, int column) { - int id = makeID(row, column); - Component comp = componentCache.get(id); - if (comp == null) { + public Component getTableCellRendererComponent(JTable table, Object obj, + boolean isSelected, boolean hasFocus, int row, int column) { + Component comp = componentCache.computeIfAbsent(makeID(row, column), id -> { if (obj instanceof JNode) { - comp = makeCell((JNode) obj, column); - componentCache.put(id, comp); - } else { - comp = emptyLabel; + return makeCell((JNode) obj, column); } - } + return emptyLabel; + }); updateSelection(table, comp, isSelected); return comp; } private int makeID(int row, int col) { - return row << 2 | col; + return row << 2 | (col & 0b11); } private void updateSelection(JTable table, Component comp, boolean isSelected) { @@ -523,7 +467,7 @@ public abstract class CommonSearchDialog extends JFrame { private Component makeCell(JNode node, int column) { if (column == 0) { - JLabel label = new JLabel(node.makeLongStringHtml() + " ", node.getIcon(), SwingConstants.LEFT); + JLabel label = new JLabel(node.makeLongStringHtml(), node.getIcon(), SwingConstants.LEFT); label.setFont(font); label.setOpaque(true); label.setToolTipText(label.getText()); @@ -559,46 +503,14 @@ public abstract class CommonSearchDialog extends JFrame { } } - private class LoadTask extends SwingWorker { - public LoadTask() { - loadStartCommon(); - loadStart(); - } - - @Override - public Void doInBackground() { - try { - progressPane.changeLabel(this, NLS.str("progress.decompile") + ": "); - mainWindow.waitDecompileTask(); - } catch (Exception e) { - LOG.error("Waiting background tasks failed", e); - } - return null; - } - - @Override - public void done() { - loadFinishedCommon(); - loadFinished(); - } - } - - void loadStartCommon() { - setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + void progressStartCommon() { progressPane.setIndeterminate(true); progressPane.setVisible(true); warnLabel.setVisible(false); } - void loadFinishedCommon() { - setCursor(null); + void progressFinishedCommon() { progressPane.setVisible(false); - - TextSearchIndex textIndex = cache.getTextIndex(); - if (textIndex == null) { - warnLabel.setText(NLS.str("msg.index_not_initialized")); - warnLabel.setVisible(true); - } } protected JNodeCache getNodeCache() { diff --git a/jadx-gui/src/main/java/jadx/gui/ui/dialog/FileDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/dialog/FileDialog.java index 4518bd8d..2e152332 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/dialog/FileDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/dialog/FileDialog.java @@ -19,9 +19,9 @@ import javax.swing.filechooser.FileNameExtensionFilter; import org.jetbrains.annotations.Nullable; import jadx.core.utils.Utils; +import jadx.core.utils.files.FileUtils; import jadx.gui.settings.JadxProject; import jadx.gui.ui.MainWindow; -import jadx.gui.utils.FileUtils; import jadx.gui.utils.NLS; public class FileDialog { diff --git a/jadx-gui/src/main/java/jadx/gui/ui/dialog/RenameDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/dialog/RenameDialog.java index b58638b7..cbcc79fa 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/dialog/RenameDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/dialog/RenameDialog.java @@ -248,7 +248,6 @@ public class RenameDialog extends JDialog { } else { // big batch => unload LOG.debug("Classes to unload: {}", updatedTopClasses.size()); - cache.getIndexService().setComplete(false); for (JClass cls : updatedTopClasses) { try { cls.unload(cache); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/dialog/SearchDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/dialog/SearchDialog.java index 5a367409..bb56c471 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/dialog/SearchDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/dialog/SearchDialog.java @@ -7,15 +7,18 @@ import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; +import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; -import java.util.HashSet; +import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.concurrent.TimeUnit; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; +import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JLabel; import javax.swing.JPanel; @@ -25,6 +28,7 @@ import javax.swing.event.ChangeListener; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,21 +37,43 @@ import io.reactivex.BackpressureStrategy; import io.reactivex.Emitter; import io.reactivex.Flowable; import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; +import jadx.api.JavaClass; +import jadx.core.utils.ListUtils; +import jadx.gui.jobs.ITaskProgress; +import jadx.gui.search.SearchSettings; +import jadx.gui.search.SearchTask; +import jadx.gui.search.providers.ClassSearchProvider; +import jadx.gui.search.providers.CodeSearchProvider; +import jadx.gui.search.providers.CommentSearchProvider; +import jadx.gui.search.providers.FieldSearchProvider; +import jadx.gui.search.providers.MergedSearchProvider; +import jadx.gui.search.providers.MethodSearchProvider; +import jadx.gui.search.providers.ResourceSearchProvider; +import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JNode; import jadx.gui.ui.MainWindow; +import jadx.gui.utils.JumpPosition; import jadx.gui.utils.NLS; import jadx.gui.utils.TextStandardActions; +import jadx.gui.utils.UiUtils; import jadx.gui.utils.layout.WrapLayout; -import jadx.gui.utils.search.SearchSettings; -import jadx.gui.utils.search.TextSearchIndex; + +import static jadx.gui.ui.dialog.SearchDialog.SearchOptions.ACTIVE_TAB; +import static jadx.gui.ui.dialog.SearchDialog.SearchOptions.CLASS; +import static jadx.gui.ui.dialog.SearchDialog.SearchOptions.CODE; +import static jadx.gui.ui.dialog.SearchDialog.SearchOptions.COMMENT; +import static jadx.gui.ui.dialog.SearchDialog.SearchOptions.FIELD; +import static jadx.gui.ui.dialog.SearchDialog.SearchOptions.IGNORE_CASE; +import static jadx.gui.ui.dialog.SearchDialog.SearchOptions.METHOD; +import static jadx.gui.ui.dialog.SearchDialog.SearchOptions.RESOURCE; +import static jadx.gui.ui.dialog.SearchDialog.SearchOptions.USE_REGEX; public class SearchDialog extends CommonSearchDialog { - private static final long serialVersionUID = -5105405456969134105L; - private static final Color SEARCHFIELD_ERROR_COLOR = new Color(255, 150, 150); - private static final Logger LOG = LoggerFactory.getLogger(SearchDialog.class); + private static final long serialVersionUID = -5105405456969134105L; + + private static final Color SEARCH_FIELD_ERROR_COLOR = new Color(255, 150, 150); public static void search(MainWindow window, SearchPreset preset) { SearchDialog searchDialog = new SearchDialog(window, preset, Collections.emptySet()); @@ -85,16 +111,23 @@ public class SearchDialog extends CommonSearchDialog { private final transient SearchPreset searchPreset; private final transient Set options; - private Color searchFieldDefaultBgColor; + private transient Color searchFieldDefaultBgColor; private transient JTextField searchField; + private transient @Nullable SearchTask searchTask; + private transient JButton loadAllButton; + private transient JButton loadMoreButton; + private transient Disposable searchDisposable; private transient SearchEventEmitter searchEmitter; private transient ChangeListener activeTabListener; private transient String initSearchText = null; + // temporal list for pending results + private final List pendingResults = new ArrayList<>(); + private SearchDialog(MainWindow mainWindow, SearchPreset preset, Set additionalOptions) { super(mainWindow, NLS.str("menu.text_search")); this.searchPreset = preset; @@ -110,6 +143,7 @@ public class SearchDialog extends CommonSearchDialog { @Override public void dispose() { + stopSearchTask(); if (searchDisposable != null && !searchDisposable.isDisposed()) { searchDisposable.dispose(); } @@ -120,7 +154,7 @@ public class SearchDialog extends CommonSearchDialog { private Set buildOptions(SearchPreset preset) { Set searchOptions = cache.getLastSearchOptions().get(preset); if (searchOptions == null) { - searchOptions = new HashSet<>(); + searchOptions = EnumSet.noneOf(SearchOptions.class); } switch (preset) { case TEXT: @@ -150,41 +184,12 @@ public class SearchDialog extends CommonSearchDialog { searchField.selectAll(); } searchField.requestFocus(); + resultsTable.initColumnWidth(); - if (searchField.getText().isEmpty()) { - checkIndex(); + if (options.contains(COMMENT)) { + // show all comments on empty input + searchEmitter.emitSearch(); } - searchEmitter.emitSearch(); - } - - private TextSearchIndex checkIndex() { - if (!cache.getIndexService().isComplete()) { - if (isFullIndexNeeded()) { - prepare(); - } - } - return cache.getTextIndex(); - } - - private boolean isFullIndexNeeded() { - for (SearchOptions option : options) { - switch (option) { - case CLASS: - case METHOD: - case FIELD: - // TODO: split indexes so full decompilation not needed for these - return true; - - case CODE: - return true; - - case RESOURCE: - case COMMENT: - // full index not needed - break; - } - } - return false; } private void initUI() { @@ -255,6 +260,20 @@ public class SearchDialog extends CommonSearchDialog { setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); } + protected void addCustomResultsActions(JPanel resultsActionsPanel) { + loadAllButton = new JButton(NLS.str("search_dialog.load_all")); + loadAllButton.addActionListener(e -> loadAll()); + loadAllButton.setEnabled(false); + + loadMoreButton = new JButton(NLS.str("search_dialog.load_more")); + loadMoreButton.addActionListener(e -> loadMore()); + loadMoreButton.setEnabled(false); + + resultsActionsPanel.add(loadAllButton); + resultsActionsPanel.add(Box.createRigidArea(new Dimension(10, 0))); + resultsActionsPanel.add(loadMoreButton); + } + private class SearchEventEmitter { private final Flowable flowable; private Emitter emitter; @@ -278,61 +297,185 @@ public class SearchDialog extends CommonSearchDialog { private void searchFieldSubscribe() { searchEmitter = new SearchEventEmitter(); - Flowable textChanges = onTextFieldChanges(searchField); Flowable searchEvents = Flowable.merge(textChanges, searchEmitter.getFlowable()); searchDisposable = searchEvents - .subscribeOn(Schedulers.single()) - .switchMap(text -> prepareSearch(text) - .doOnError(e -> LOG.error("Error prepare search: {}", e.getMessage(), e)) - .subscribeOn(Schedulers.single()) - .toList() - .toFlowable(), 1) + .debounce(100, TimeUnit.MILLISECONDS) .observeOn(SwingSchedulers.edt()) - .doOnError(e -> LOG.error("Error while searching: {}", e.getMessage(), e)) - .subscribe(this::processSearchResults); + .subscribe(this::search); } - private Flowable prepareSearch(String text) { + @Nullable + private synchronized void search(String text) { + UiUtils.uiThreadGuard(); + resetSearch(); if (text == null || options.isEmpty()) { - return Flowable.empty(); + return; } // allow empty text for comments search if (text.isEmpty() && !options.contains(SearchOptions.COMMENT)) { - return Flowable.empty(); + return; } - - TextSearchIndex index = checkIndex(); - if (index == null) { - return Flowable.empty(); - } - LOG.debug("search event: {}", text); - showSearchState(); - try { - Flowable result = index.buildSearch(text, options); - if (searchField.getBackground() == SEARCHFIELD_ERROR_COLOR) { + LOG.debug("Building search for '{}', options: {}", text, options); + boolean ignoreCase = options.contains(IGNORE_CASE); + boolean useRegex = options.contains(USE_REGEX); + SearchSettings searchSettings = new SearchSettings(text, ignoreCase, useRegex); + String error = searchSettings.prepare(); + if (error == null) { + if (Objects.equals(searchField.getBackground(), SEARCH_FIELD_ERROR_COLOR)) { searchField.setBackground(searchFieldDefaultBgColor); } - return result; - } catch (SearchSettings.InvalidSearchTermException e) { - searchField.setBackground(SEARCHFIELD_ERROR_COLOR); - return Flowable.empty(); + } else { + searchField.setBackground(SEARCH_FIELD_ERROR_COLOR); + resultsInfoLabel.setText(error); + return; + } + searchTask = new SearchTask(mainWindow, this::addSearchResult, s -> searchComplete()); + if (!buildSearch(text, searchSettings)) { + return; + } + + startSearch(); + searchTask.setResultsLimit(100); + searchTask.setProgressListener(this::updateProgress); + searchTask.fetchResults(); + LOG.debug("Total search items count estimation: {}", searchTask.getTaskProgress().total()); + } + + private boolean buildSearch(String text, SearchSettings searchSettings) { + Objects.requireNonNull(searchTask); + + List allClasses; + if (options.contains(ACTIVE_TAB)) { + JumpPosition currentPos = mainWindow.getTabbedPane().getCurrentPosition(); + if (currentPos == null) { + resultsInfoLabel.setText("Can't search in current tab"); + return false; + } + JClass activeCls = currentPos.getNode().getRootClass(); + searchSettings.setActiveCls(activeCls); + allClasses = Collections.singletonList(activeCls.getCls()); + } else { + allClasses = mainWindow.getWrapper().getIncludedClassesWithInners(); + } + // allow empty text for comments search + if (text.isEmpty() && options.contains(SearchOptions.COMMENT)) { + searchTask.addProviderJob(new CommentSearchProvider(mainWindow, searchSettings)); + return true; + } + // using ordered execution for fast tasks + MergedSearchProvider merged = new MergedSearchProvider(); + if (options.contains(CLASS)) { + merged.add(new ClassSearchProvider(mainWindow, searchSettings, allClasses)); + } + if (options.contains(METHOD)) { + merged.add(new MethodSearchProvider(mainWindow, searchSettings, allClasses)); + } + if (options.contains(FIELD)) { + merged.add(new FieldSearchProvider(mainWindow, searchSettings, allClasses)); + } + if (options.contains(CODE)) { + if (allClasses.size() == 1) { + searchTask.addProviderJob(new CodeSearchProvider(mainWindow, searchSettings, allClasses)); + } else { + List topClasses = ListUtils.filter(allClasses, c -> !c.isInner()); + for (List batch : mainWindow.getWrapper().buildDecompileBatches(topClasses)) { + searchTask.addProviderJob(new CodeSearchProvider(mainWindow, searchSettings, batch)); + } + } + } + if (options.contains(RESOURCE)) { + searchTask.addProviderJob(new ResourceSearchProvider(mainWindow, searchSettings)); + } + if (options.contains(COMMENT)) { + searchTask.addProviderJob(new CommentSearchProvider(mainWindow, searchSettings)); + } + merged.prepare(); + searchTask.addProviderJob(merged); + return true; + } + + private synchronized void stopSearchTask() { + if (searchTask != null) { + searchTask.cancel(); + searchTask.waitTask(); } } - private void processSearchResults(java.util.List results) { - LOG.debug("search result size: {}", results.size()); + private synchronized void loadMore() { + if (searchTask == null) { + return; + } + startSearch(); + searchTask.fetchResults(); + } + + private synchronized void loadAll() { + if (searchTask == null) { + return; + } + startSearch(); + searchTask.setResultsLimit(0); + searchTask.fetchResults(); + } + + private synchronized void resetSearch() { + resultsModel.clear(); + updateTable(); + progressPane.setVisible(false); + warnLabel.setVisible(false); + loadAllButton.setEnabled(false); + loadMoreButton.setEnabled(false); + stopSearchTask(); + } + + private void startSearch() { + showSearchState(); + progressStartCommon(); + } + + private void addSearchResult(JNode node) { + synchronized (pendingResults) { + pendingResults.add(node); + } + } + + private void updateTable() { + synchronized (pendingResults) { + Collections.sort(pendingResults); + resultsModel.addAll(pendingResults); + pendingResults.clear(); + } + resultsTable.updateTable(); + } + + private void updateTableHighlight() { String text = searchField.getText(); setHighlightText(text); highlightTextCaseInsensitive = options.contains(SearchOptions.IGNORE_CASE); highlightTextUseRegex = options.contains(SearchOptions.USE_REGEX); - cache.setLastSearch(text); cache.getLastSearchOptions().put(searchPreset, options); + } - resultsModel.clear(); - resultsModel.addAll(results); - super.performSearch(); + private void updateProgress(ITaskProgress progress) { + UiUtils.uiRun(() -> { + progressPane.setProgress(progress); + updateTable(); + }); + } + + private synchronized void searchComplete() { + UiUtils.uiThreadGuard(); + LOG.debug("Search complete"); + updateTableHighlight(); + updateTable(); + + boolean complete = searchTask == null || searchTask.isSearchComplete(); + loadAllButton.setEnabled(!complete); + loadMoreButton.setEnabled(!complete); + updateProgressLabel(complete); + progressFinishedCommon(); } private static Flowable onTextFieldChanges(final JTextField textField) { @@ -374,7 +517,6 @@ public class SearchDialog extends CommonSearchDialog { } }); }, BackpressureStrategy.LATEST) - .debounce(300, TimeUnit.MILLISECONDS) .distinctUntilChanged(); } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/dialog/UsageDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/dialog/UsageDialog.java index a2d06924..fe91033a 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/dialog/UsageDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/dialog/UsageDialog.java @@ -5,9 +5,9 @@ import java.awt.Container; import java.awt.FlowLayout; import java.util.ArrayList; import java.util.Collections; -import java.util.Comparator; +import java.util.HashMap; import java.util.List; -import java.util.stream.Collectors; +import java.util.Map; import javax.swing.BorderFactory; import javax.swing.JLabel; @@ -15,21 +15,19 @@ import javax.swing.JPanel; import javax.swing.SwingConstants; import javax.swing.WindowConstants; -import jadx.api.CodePosition; -import jadx.api.ICodeWriter; +import jadx.api.ICodeInfo; +import jadx.api.JadxDecompiler; import jadx.api.JavaClass; import jadx.api.JavaMethod; import jadx.api.JavaNode; -import jadx.core.dex.attributes.AType; +import jadx.api.utils.CodeUtils; import jadx.gui.jobs.TaskStatus; import jadx.gui.treemodel.CodeNode; import jadx.gui.treemodel.JMethod; import jadx.gui.treemodel.JNode; import jadx.gui.ui.MainWindow; -import jadx.gui.utils.CodeLinesInfo; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; -import jadx.gui.utils.search.StringRef; public class UsageDialog extends CommonSearchDialog { private static final long serialVersionUID = -5105405789969134105L; @@ -49,6 +47,7 @@ public class UsageDialog extends CommonSearchDialog { @Override protected void openInit() { + progressStartCommon(); mainWindow.getBackgroundExecutor().execute(NLS.str("progress.load"), this::collectUsageData, (status) -> { @@ -56,81 +55,77 @@ public class UsageDialog extends CommonSearchDialog { mainWindow.showHeapUsageBar(); UiUtils.errorMessage(this, NLS.str("message.memoryLow")); } - loadFinishedCommon(); + progressFinishedCommon(); loadFinished(); }); } private void collectUsageData() { usageList = new ArrayList<>(); - getMethodUseIn() - .stream() + Map> usageQuery = buildUsageQuery(); + usageQuery.forEach((searchNode, useNodes) -> useNodes.stream() .map(JavaNode::getTopParentClass) .distinct() - .sorted(Comparator.comparing(JavaClass::getFullName)) - .forEach(this::processUsageClass); + .forEach(u -> processUsage(searchNode, u))); } - private List getMethodUseIn() { + /** + * Return mapping of 'node to search' to 'use places' + */ + private Map> buildUsageQuery() { + Map> map = new HashMap<>(); if (node instanceof JMethod) { - JavaMethod method = ((JMethod) node).getJavaMethod(); - if (method.getMethodNode().contains(AType.METHOD_OVERRIDE)) { - return method.getOverrideRelatedMethods() - .stream() - .flatMap(m -> m.getUseIn().stream()) - .collect(Collectors.toList()); + JavaMethod javaMethod = ((JMethod) node).getJavaMethod(); + for (JavaMethod mth : getMethodWithOverrides(javaMethod)) { + map.put(mth, mth.getUseIn()); } + } else { + JavaNode javaNode = node.getJavaNode(); + map.put(javaNode, javaNode.getUseIn()); } - return node.getJavaNode().getUseIn(); + return map; } - private void processUsageClass(JavaNode usageNode) { - JavaClass cls = usageNode.getTopParentClass(); - String code = cls.getCodeInfo().getCodeStr(); - CodeLinesInfo linesInfo = new CodeLinesInfo(cls); - List targetNodes = getMethodWithOverride(); - for (JavaNode javaNode : targetNodes) { - List usage = cls.getUsageFor(javaNode); - for (CodePosition pos : usage) { - if (javaNode.getTopParentClass().equals(cls) && pos.getPos() == javaNode.getDefPos()) { - // skip declaration - continue; - } - StringRef line = getLineStrAt(code, pos.getPos()); - if (line.startsWith("import ")) { - continue; - } - JavaNode javaNodeByLine = linesInfo.getJavaNodeByLine(pos.getLine()); - JNode useAtNode = javaNodeByLine == null ? node : getNodeCache().makeFrom(javaNodeByLine); - usageList.add(new CodeNode(useAtNode, line, pos.getLine(), pos.getPos())); - } + private List getMethodWithOverrides(JavaMethod javaMethod) { + List relatedMethods = javaMethod.getOverrideRelatedMethods(); + if (!relatedMethods.isEmpty()) { + return relatedMethods; } + return Collections.singletonList(javaMethod); } - private List getMethodWithOverride() { - if (node instanceof JMethod) { - JavaMethod method = ((JMethod) node).getJavaMethod(); - if (null != method.getMethodNode().get(AType.METHOD_OVERRIDE)) { - return method.getOverrideRelatedMethods(); + private void processUsage(JavaNode searchNode, JavaClass topUseClass) { + ICodeInfo codeInfo = topUseClass.getCodeInfo(); + String code = codeInfo.getCodeStr(); + JadxDecompiler decompiler = mainWindow.getWrapper().getDecompiler(); + List usePositions = topUseClass.getUsePlacesFor(codeInfo, searchNode); + for (int pos : usePositions) { + if (searchNode.getTopParentClass().equals(topUseClass) && pos == searchNode.getDefPos()) { + // skip declaration + continue; } + String line = CodeUtils.getLineForPos(code, pos); + if (line.startsWith("import ")) { + continue; + } + JavaNode enclosingNode = decompiler.getEnclosingNode(codeInfo, pos); + JavaNode usageNode = enclosingNode == null ? topUseClass : enclosingNode; + usageList.add(new CodeNode(getNodeCache().makeFrom(usageNode), line.trim(), pos)); } - return Collections.singletonList(node.getJavaNode()); - } - - private StringRef getLineStrAt(String code, int pos) { - String newLine = ICodeWriter.NL; - int start = code.lastIndexOf(newLine, pos); - int end = code.indexOf(newLine, pos); - if (start == -1 || end == -1) { - return StringRef.fromStr("line not found"); - } - return StringRef.subString(code, start + newLine.length(), end).trim(); } @Override protected void loadFinished() { resultsTable.setEnabled(true); - performSearch(); + resultsModel.clear(); + + Collections.sort(usageList); + resultsModel.addAll(usageList); + // TODO: highlight only needed node usage + setHighlightText(null); + resultsTable.initColumnWidth(); + resultsTable.updateTable(); + updateProgressLabel(true); } @Override @@ -138,16 +133,6 @@ public class UsageDialog extends CommonSearchDialog { resultsTable.setEnabled(false); } - @Override - protected synchronized void performSearch() { - resultsModel.clear(); - Collections.sort(usageList); - resultsModel.addAll(usageList); - // TODO: highlight only needed node usage - setHighlightText(null); - super.performSearch(); - } - private void initUI() { JLabel lbl = new JLabel(NLS.str("usage_dialog.label")); JLabel nodeLabel = new JLabel(this.node.makeLongStringHtml(), this.node.getIcon(), SwingConstants.LEFT); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/panel/HtmlPanel.java b/jadx-gui/src/main/java/jadx/gui/ui/panel/HtmlPanel.java index ad2275f8..581906aa 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/panel/HtmlPanel.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/panel/HtmlPanel.java @@ -23,7 +23,7 @@ public final class HtmlPanel extends ContentPanel { setLayout(new BorderLayout()); textArea = new JHtmlPane(); loadSettings(); - textArea.setText(jnode.getContent()); + textArea.setText(jnode.getCodeInfo().getCodeStr()); textArea.setCaretPosition(0); // otherwise the start view will be the last line textArea.setEditable(false); JScrollPane sp = new JScrollPane(textArea); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/panel/ProgressPanel.java b/jadx-gui/src/main/java/jadx/gui/ui/panel/ProgressPanel.java index f0b469e3..4e721a7a 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/panel/ProgressPanel.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/panel/ProgressPanel.java @@ -13,6 +13,7 @@ import javax.swing.JPanel; import javax.swing.JProgressBar; import javax.swing.SwingWorker; +import jadx.gui.jobs.ITaskProgress; import jadx.gui.ui.MainWindow; import jadx.gui.utils.UiUtils; @@ -61,15 +62,31 @@ public class ProgressPanel extends JPanel implements PropertyChangeListener { progressBar.setStringPainted(true); } + public void setProgress(ITaskProgress taskProgress) { + int progress = taskProgress.progress(); + int total = taskProgress.total(); + if (progress == 0 || total == 0) { + progressBar.setIndeterminate(true); + } else { + if (progressBar.isIndeterminate()) { + progressBar.setIndeterminate(false); + } + setProgress(UiUtils.calcProgress(progress, total)); + } + } + + private void setProgress(int progress) { + progressBar.setIndeterminate(false); + progressBar.setValue(progress); + progressBar.setString(progress + "%"); + progressBar.setStringPainted(true); + } + @Override public void propertyChange(PropertyChangeEvent evt) { switch (evt.getPropertyName()) { case "progress": - int progress = (Integer) evt.getNewValue(); - progressBar.setIndeterminate(false); - progressBar.setValue(progress); - progressBar.setString(progress + "%"); - progressBar.setStringPainted(true); + setProgress((Integer) evt.getNewValue()); break; case "label": diff --git a/jadx-gui/src/main/java/jadx/gui/ui/treenodes/SummaryNode.java b/jadx-gui/src/main/java/jadx/gui/ui/treenodes/SummaryNode.java index 0146262b..3cb1a81d 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/treenodes/SummaryNode.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/treenodes/SummaryNode.java @@ -12,7 +12,9 @@ import javax.swing.ImageIcon; import org.apache.commons.text.StringEscapeUtils; +import jadx.api.ICodeInfo; import jadx.api.JadxDecompiler; +import jadx.api.impl.SimpleCodeInfo; import jadx.core.dex.attributes.IAttributeNode; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.MethodNode; @@ -39,7 +41,7 @@ public class SummaryNode extends JNode { } @Override - public String getContent() { + public ICodeInfo getCodeInfo() { StringEscapeUtils.Builder builder = StringEscapeUtils.builder(StringEscapeUtils.ESCAPE_HTML4); try { builder.append(""); @@ -53,7 +55,7 @@ public class SummaryNode extends JNode { builder.append(Utils.getStackTrace(e)); builder.append(""); } - return builder.toString(); + return new SimpleCodeInfo(builder.toString()); } private void writeInputSummary(StringEscapeUtils.Builder builder) throws IOException { diff --git a/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java b/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java index cae5123a..ce31eab1 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java @@ -6,19 +6,12 @@ import java.util.Set; import org.jetbrains.annotations.Nullable; -import jadx.gui.jobs.IndexService; import jadx.gui.settings.JadxSettings; import jadx.gui.treemodel.JRoot; import jadx.gui.ui.dialog.SearchDialog; -import jadx.gui.utils.search.CommentsIndex; -import jadx.gui.utils.search.TextSearchIndex; public class CacheObject { - private IndexService indexService; - - private TextSearchIndex textIndex; - private CommentsIndex commentsIndex; private String lastSearch; private JNodeCache jNodeCache; private Map> lastSearchOptions; @@ -33,21 +26,11 @@ public class CacheObject { public void reset() { jRoot = null; settings = null; - indexService = null; - textIndex = null; lastSearch = null; jNodeCache = new JNodeCache(); lastSearchOptions = new HashMap<>(); } - public TextSearchIndex getTextIndex() { - return textIndex; - } - - public void setTextIndex(TextSearchIndex textIndex) { - this.textIndex = textIndex; - } - @Nullable public String getLastSearch() { return lastSearch; @@ -57,22 +40,6 @@ public class CacheObject { this.lastSearch = lastSearch; } - public CommentsIndex getCommentsIndex() { - return commentsIndex; - } - - public void setCommentsIndex(CommentsIndex commentsIndex) { - this.commentsIndex = commentsIndex; - } - - public IndexService getIndexService() { - return indexService; - } - - public void setIndexService(IndexService indexService) { - this.indexService = indexService; - } - public JNodeCache getNodeCache() { return jNodeCache; } diff --git a/jadx-gui/src/main/java/jadx/gui/utils/CaretPositionFix.java b/jadx-gui/src/main/java/jadx/gui/utils/CaretPositionFix.java index 9d203d7a..256cd2de 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/CaretPositionFix.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/CaretPositionFix.java @@ -2,14 +2,17 @@ package jadx.gui.utils; import java.util.Map; +import javax.swing.text.BadLocationException; + import org.fife.ui.rsyntaxtextarea.Token; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import jadx.api.CodePosition; -import jadx.api.JavaClass; -import jadx.api.JavaNode; -import jadx.api.data.annotations.ICodeRawOffset; +import jadx.api.ICodeInfo; +import jadx.api.metadata.ICodeAnnotation; +import jadx.api.metadata.ICodeMetadata; +import jadx.api.metadata.ICodeNodeRef; +import jadx.api.metadata.annotations.InsnCodeOffset; import jadx.gui.treemodel.JClass; import jadx.gui.ui.codearea.AbstractCodeArea; @@ -24,10 +27,11 @@ public class CaretPositionFix { private int linesCount; private int line; + private int pos; private int lineOffset; private TokenInfo tokenInfo; - private int javaNodeLine = -1; + private int javaNodePos = -1; private int codeRawOffset = -1; public CaretPositionFix(AbstractCodeArea codeArea) { @@ -40,27 +44,26 @@ public class CaretPositionFix { public void save() { try { linesCount = codeArea.getLineCount(); - int pos = codeArea.getCaretPosition(); + pos = codeArea.getCaretPosition(); line = codeArea.getLineOfOffset(pos); lineOffset = pos - codeArea.getLineStartOffset(line); tokenInfo = getTokenInfoByOffset(codeArea.getTokenListForLine(line), pos); - JClass cls = codeArea.getJClass(); - if (cls != null) { - JavaClass topParentClass = cls.getJavaNode().getTopParentClass(); - Object ann = topParentClass.getAnnotationAt(new CodePosition(line)); - if (ann instanceof ICodeRawOffset) { - codeRawOffset = ((ICodeRawOffset) ann).getOffset(); - CodeLinesInfo codeLinesInfo = new CodeLinesInfo(topParentClass); - JavaNode javaNodeAtLine = codeLinesInfo.getJavaNodeByLine(line); - if (javaNodeAtLine != null) { - javaNodeLine = javaNodeAtLine.getDecompiledLine(); + ICodeInfo codeInfo = codeArea.getCodeInfo(); + if (codeInfo.hasMetadata()) { + ICodeMetadata metadata = codeInfo.getCodeMetadata(); + ICodeAnnotation ann = metadata.getAt(pos); + if (ann instanceof InsnCodeOffset) { + codeRawOffset = ((InsnCodeOffset) ann).getOffset(); + ICodeNodeRef javaNode = metadata.getNodeAt(pos); + if (javaNode != null) { + javaNodePos = javaNode.getDefPosition(); } } } LOG.debug("Saved position data: line={}, lineOffset={}, token={}, codeRawOffset={}, javaNodeLine={}", - line, lineOffset, tokenInfo, codeRawOffset, javaNodeLine); + line, lineOffset, tokenInfo, codeRawOffset, javaNodePos); } catch (Exception e) { LOG.error("Failed to save caret position before refresh", e); line = -1; @@ -76,51 +79,55 @@ public class CaretPositionFix { return; } try { - int newLine = getNewLine(); - int lineStartOffset = codeArea.getLineStartOffset(newLine); - int lineEndOffset = codeArea.getLineEndOffset(newLine) - 1; - int lineLength = lineEndOffset - lineStartOffset; + int newPos = getNewPos(); + int newLine = codeArea.getLineOfOffset(newPos); Token token = codeArea.getTokenListForLine(newLine); - int newPos = getOffsetFromTokenInfo(tokenInfo, token); - if (newPos == -1) { + int tokenPos = getOffsetFromTokenInfo(tokenInfo, token); + if (tokenPos == -1) { + int lineStartOffset = codeArea.getLineStartOffset(newLine); + int lineEndOffset = codeArea.getLineEndOffset(newLine) - 1; + int lineLength = lineEndOffset - lineStartOffset; // can't restore using token -> just restore by line offset if (lineOffset < lineLength) { - newPos = lineStartOffset + lineOffset; + tokenPos = lineStartOffset + lineOffset; } else { // line truncated -> set caret at line end - newPos = lineEndOffset; + tokenPos = lineEndOffset; } } - codeArea.setCaretPosition(newPos); - LOG.debug("Restored caret position: {}, line: {}", newPos, newLine); + codeArea.setCaretPosition(tokenPos); + LOG.debug("Restored caret position: {}", tokenPos); } catch (Exception e) { LOG.warn("Failed to restore caret position", e); } } - private int getNewLine() { + private int getNewPos() throws BadLocationException { int newLinesCount = codeArea.getLineCount(); if (linesCount == newLinesCount) { - return line; + return pos; } // lines count changes, try find line by raw offset - if (javaNodeLine != -1) { + ICodeInfo codeInfo = codeArea.getCodeInfo(); + if (javaNodePos != -1 && codeInfo.hasMetadata()) { JClass cls = codeArea.getJClass(); if (cls != null) { - JavaClass topParentClass = cls.getJavaNode().getTopParentClass(); - for (Map.Entry entry : topParentClass.getCodeAnnotations().entrySet()) { - CodePosition pos = entry.getKey(); - if (pos.getOffset() == 0 && pos.getLine() >= javaNodeLine) { - Object ann = entry.getValue(); - if (ann instanceof ICodeRawOffset && ((ICodeRawOffset) ann).getOffset() == codeRawOffset) { - return pos.getLine() - 1; + ICodeMetadata codeMetadata = codeInfo.getCodeMetadata(); + for (Map.Entry entry : codeMetadata.getAsMap().entrySet()) { + int annPos = entry.getKey(); + if (annPos >= javaNodePos) { + ICodeAnnotation ann = entry.getValue(); + if (ann instanceof InsnCodeOffset + && ((InsnCodeOffset) ann).getOffset() == codeRawOffset) { + return annPos; } } } } } // fallback: assume lines added/removed before caret - return line - (linesCount - newLinesCount); + int newLine = line - (linesCount - newLinesCount); + return codeArea.getLineStartOffset(newLine); } private TokenInfo getTokenInfoByOffset(Token token, int offset) { diff --git a/jadx-gui/src/main/java/jadx/gui/utils/CodeLinesInfo.java b/jadx-gui/src/main/java/jadx/gui/utils/CodeLinesInfo.java deleted file mode 100644 index feb3b437..00000000 --- a/jadx-gui/src/main/java/jadx/gui/utils/CodeLinesInfo.java +++ /dev/null @@ -1,58 +0,0 @@ -package jadx.gui.utils; - -import java.util.Map; -import java.util.NavigableMap; -import java.util.TreeMap; - -import jadx.api.JavaClass; -import jadx.api.JavaField; -import jadx.api.JavaMethod; -import jadx.api.JavaNode; - -public class CodeLinesInfo { - private final NavigableMap map = new TreeMap<>(); - - public CodeLinesInfo(JavaClass cls) { - addClass(cls, false); - } - - public CodeLinesInfo(JavaClass cls, boolean includeFields) { - addClass(cls, includeFields); - } - - private void addClass(JavaClass cls, boolean includeFields) { - map.put(cls.getDecompiledLine(), cls); - for (JavaClass innerCls : cls.getInnerClasses()) { - map.put(innerCls.getDecompiledLine(), innerCls); - addClass(innerCls, includeFields); - } - for (JavaMethod mth : cls.getMethods()) { - map.put(mth.getDecompiledLine(), mth); - } - if (includeFields) { - for (JavaField field : cls.getFields()) { - map.put(field.getDecompiledLine(), field); - } - } - } - - public JavaNode getJavaNodeByLine(int line) { - Map.Entry entry = map.floorEntry(line); - if (entry == null) { - return null; - } - return entry.getValue(); - } - - public JavaNode getJavaNodeBelowLine(int line) { - Map.Entry entry = map.ceilingEntry(line); - if (entry == null) { - return null; - } - return entry.getValue(); - } - - public JavaNode getDefAtLine(int line) { - return map.get(line); - } -} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/FileUtils.java b/jadx-gui/src/main/java/jadx/gui/utils/FileUtils.java deleted file mode 100644 index baf6526e..00000000 --- a/jadx-gui/src/main/java/jadx/gui/utils/FileUtils.java +++ /dev/null @@ -1,26 +0,0 @@ -package jadx.gui.utils; - -import java.io.File; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -public class FileUtils { - public static List toPaths(List files) { - return files.stream().map(File::toPath).collect(Collectors.toList()); - } - - public static List toPaths(File[] files) { - return Stream.of(files).map(File::toPath).collect(Collectors.toList()); - } - - public static List fileNamesToPaths(List fileNames) { - return fileNames.stream().map(Paths::get).collect(Collectors.toList()); - } - - public static List toFiles(List paths) { - return paths.stream().map(Path::toFile).collect(Collectors.toList()); - } -} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/FixedCodeCache.java b/jadx-gui/src/main/java/jadx/gui/utils/FixedCodeCache.java deleted file mode 100644 index cd2c6171..00000000 --- a/jadx-gui/src/main/java/jadx/gui/utils/FixedCodeCache.java +++ /dev/null @@ -1,33 +0,0 @@ -package jadx.gui.utils; - -import org.jetbrains.annotations.Nullable; - -import jadx.api.ICodeCache; -import jadx.api.ICodeInfo; - -/** - * Code cache with fixed size of wrapper code cache ('remove' and 'add' methods will do nothing). - */ -public class FixedCodeCache implements ICodeCache { - - private final ICodeCache codeCache; - - public FixedCodeCache(ICodeCache codeCache) { - this.codeCache = codeCache; - } - - @Override - public @Nullable ICodeInfo get(String clsFullName) { - return this.codeCache.get(clsFullName); - } - - @Override - public void remove(String clsFullName) { - // no op - } - - @Override - public void add(String clsFullName, ICodeInfo codeInfo) { - // no op - } -} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/JNodeCache.java b/jadx-gui/src/main/java/jadx/gui/utils/JNodeCache.java index 06342bf8..f573a325 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/JNodeCache.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/JNodeCache.java @@ -52,6 +52,8 @@ public class JNodeCache { remove(javaCls); javaCls.getMethods().forEach(this::remove); javaCls.getFields().forEach(this::remove); + javaCls.getInnerClasses().forEach(this::remove); + javaCls.getInlinedClasses().forEach(this::remove); } private JClass convert(JavaClass cls) { diff --git a/jadx-gui/src/main/java/jadx/gui/utils/JumpManager.java b/jadx-gui/src/main/java/jadx/gui/utils/JumpManager.java index 5f74af18..3af1ea8a 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/JumpManager.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/JumpManager.java @@ -2,7 +2,6 @@ package jadx.gui.utils; import java.util.ArrayList; import java.util.List; -import java.util.Objects; import org.jetbrains.annotations.Nullable; @@ -41,22 +40,7 @@ public class JumpManager { if (current == null) { return false; } - if (pos.equals(current)) { - return true; - } - if (Objects.equals(current.getNode(), pos.getNode())) { - // undefined jump line in same node // TODO: find the cause - if (pos.getLine() == 0) { - return true; - } - if (current.getLine() == 0) { - // replace current - getPrev(); - return false; - } - return false; - } - return false; + return pos.equals(current); } @Nullable diff --git a/jadx-gui/src/main/java/jadx/gui/utils/JumpPosition.java b/jadx-gui/src/main/java/jadx/gui/utils/JumpPosition.java index dbc8c8f0..07f0c0e3 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/JumpPosition.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/JumpPosition.java @@ -1,26 +1,18 @@ package jadx.gui.utils; -import java.util.Objects; - -import jadx.api.CodePosition; +import jadx.core.utils.Utils; import jadx.gui.treemodel.JNode; public class JumpPosition { private final JNode node; - private final int line; private int pos; - public JumpPosition(JNode jumpNode) { - this(Objects.requireNonNull(jumpNode.getRootClass()), jumpNode.getLine(), jumpNode.getPos()); + public JumpPosition(JNode node) { + this(node, node.getPos()); } - public JumpPosition(JNode jumpNode, CodePosition codePos) { - this(Objects.requireNonNull(jumpNode.getRootClass()), codePos.getLine(), codePos.getPos()); - } - - public JumpPosition(JNode node, int line, int pos) { - this.node = node; - this.line = line; + public JumpPosition(JNode node, int pos) { + this.node = Utils.getOrElse(node.getRootClass(), node); this.pos = pos; } @@ -36,10 +28,6 @@ public class JumpPosition { return node; } - public int getLine() { - return line; - } - @Override public boolean equals(Object obj) { if (this == obj) { @@ -48,17 +36,17 @@ public class JumpPosition { if (!(obj instanceof JumpPosition)) { return false; } - JumpPosition position = (JumpPosition) obj; - return line == position.line && pos == position.pos && node.equals(position.node); + JumpPosition jump = (JumpPosition) obj; + return pos == jump.pos && node.equals(jump.node); } @Override public int hashCode() { - return 31 * node.hashCode() + line; + return 31 * node.hashCode() + pos; } @Override public String toString() { - return "Position: " + node + " : " + line; + return "Jump: " + node + " : " + pos; } } diff --git a/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java b/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java index 704f35e6..854fe2fa 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java @@ -15,6 +15,8 @@ import java.awt.event.KeyEvent; import java.net.URL; import java.util.ArrayList; import java.util.List; +import java.util.Timer; +import java.util.TimerTask; import javax.swing.AbstractAction; import javax.swing.Action; @@ -27,6 +29,7 @@ import javax.swing.RootPaneContainer; import javax.swing.SwingUtilities; import org.intellij.lang.annotations.MagicConstant; +import org.jetbrains.annotations.TestOnly; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,6 +40,7 @@ import jadx.core.dex.instructions.args.ArgType; import jadx.core.utils.StringUtils; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; +import jadx.gui.jobs.ITaskProgress; import jadx.gui.ui.codearea.AbstractCodeArea; public class UiUtils { @@ -221,13 +225,6 @@ public class UiUtils { return (long) (mem / (double) (1024L * 1024L)) + "MB"; } - /** - * Adapt character case for case insensitive searches - */ - public static char caseChar(char ch, boolean toLower) { - return toLower ? Character.toLowerCase(ch) : ch; - } - public static void setClipboardString(String text) { try { Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); @@ -326,6 +323,18 @@ public class UiUtils { } } + public static int calcProgress(ITaskProgress taskProgress) { + return calcProgress(taskProgress.progress(), taskProgress.total()); + } + + public static int calcProgress(long done, long total) { + if (done > total) { + LOG.debug("Task progress has invalid values: done={}, total={}", done, total); + return 100; + } + return Math.round(done * 100 / (float) total); + } + public static void sleep(int ms) { try { Thread.sleep(ms); @@ -333,4 +342,45 @@ public class UiUtils { // ignore } } + + public static void uiRun(Runnable runnable) { + SwingUtilities.invokeLater(runnable); + } + + public static void uiRunAndWait(Runnable runnable) { + if (SwingUtilities.isEventDispatchThread()) { + runnable.run(); + return; + } + try { + SwingUtilities.invokeAndWait(runnable); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static void uiThreadGuard() { + if (!SwingUtilities.isEventDispatchThread()) { + LOG.warn("Expect UI thread, got: {}", Thread.currentThread(), new JadxRuntimeException()); + } + } + + @TestOnly + public static void debugTimer(int periodInSeconds, Runnable action) { + if (!LOG.isDebugEnabled()) { + return; + } + Timer timer = new Timer(); + timer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + action.run(); + } + }, 0, periodInSeconds * 1000L); + } + + @TestOnly + public static void printStackTrace(String label) { + LOG.debug("StackTrace: {}", label, new Exception(label)); + } } diff --git a/jadx-gui/src/main/java/jadx/gui/utils/codecache/CodeCacheMode.java b/jadx-gui/src/main/java/jadx/gui/utils/codecache/CodeCacheMode.java new file mode 100644 index 00000000..80adc259 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/utils/codecache/CodeCacheMode.java @@ -0,0 +1,27 @@ +package jadx.gui.utils.codecache; + +import java.util.stream.Collectors; +import java.util.stream.Stream; + +// TODO: use localized strings +public enum CodeCacheMode { + MEMORY("Everything in memory: fast search, slow reopen, high memory usage"), + DISK_WITH_CACHE("Code saved on disk with in memory cache: medium search, fast reopen, medium memory usage"), + DISK("Everything on disk: slow search, fast reopen, low memory usage"); + + private final String desc; + + CodeCacheMode(String desc) { + this.desc = desc; + } + + public String getDesc() { + return desc; + } + + public static String buildToolTip() { + return Stream.of(values()) + .map(v -> v.name() + " - " + v.getDesc()) + .collect(Collectors.joining("\n")); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/codecache/CodeStringCache.java b/jadx-gui/src/main/java/jadx/gui/utils/codecache/CodeStringCache.java new file mode 100644 index 00000000..550d6b5d --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/utils/codecache/CodeStringCache.java @@ -0,0 +1,92 @@ +package jadx.gui.utils.codecache; + +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.reactivestreams.Subscriber; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.reactivex.disposables.Disposable; +import io.reactivex.processors.PublishProcessor; + +import jadx.api.ICodeCache; +import jadx.api.ICodeInfo; +import jadx.api.impl.DelegateCodeCache; +import jadx.gui.utils.UiUtils; + +/** + * Keep code strings for faster search + */ +public class CodeStringCache extends DelegateCodeCache { + private static final Logger LOG = LoggerFactory.getLogger(CodeStringCache.class); + + private final Map codeCache = new ConcurrentHashMap<>(); + private final Subscriber subscriber; + private final Disposable disposable; + + public CodeStringCache(ICodeCache backCache) { + super(backCache); + // reset cache if free memory is low + // check only on changes (with debounce) to reduce background checks if app not used + PublishProcessor processor = PublishProcessor.create(); + subscriber = processor; + disposable = processor.debounce(3, TimeUnit.SECONDS) + .map(v -> UiUtils.isFreeMemoryAvailable()) + .filter(v -> !v) + .subscribe(v -> { + LOG.warn("Free memory is low! Reset code strings cache. Cache size {}", codeCache.size()); + codeCache.clear(); + System.gc(); + }); + } + + @Override + @Nullable + public String getCode(String clsFullName) { + subscriber.onNext(Boolean.TRUE); + String code = codeCache.get(clsFullName); + if (code != null) { + return code; + } + String backCode = backCache.getCode(clsFullName); + if (backCode != null) { + codeCache.put(clsFullName, backCode); + } + return backCode; + } + + @Override + public @NotNull ICodeInfo get(String clsFullName) { + subscriber.onNext(Boolean.TRUE); + return super.get(clsFullName); + } + + @Override + public void add(String clsFullName, ICodeInfo codeInfo) { + subscriber.onNext(Boolean.TRUE); + codeCache.put(clsFullName, codeInfo.getCodeStr()); + backCache.add(clsFullName, codeInfo); + } + + @Override + public void remove(String clsFullName) { + codeCache.remove(clsFullName); + backCache.remove(clsFullName); + } + + @Override + public void close() throws IOException { + try { + backCache.close(); + } finally { + codeCache.clear(); + subscriber.onComplete(); + disposable.dispose(); + } + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/codecache/FixedCodeCache.java b/jadx-gui/src/main/java/jadx/gui/utils/codecache/FixedCodeCache.java new file mode 100644 index 00000000..e414bef8 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/utils/codecache/FixedCodeCache.java @@ -0,0 +1,25 @@ +package jadx.gui.utils.codecache; + +import jadx.api.ICodeCache; +import jadx.api.ICodeInfo; +import jadx.api.impl.DelegateCodeCache; + +/** + * Code cache with fixed size of wrapped code cache ('remove' and 'add' methods will do nothing). + */ +public class FixedCodeCache extends DelegateCodeCache { + + public FixedCodeCache(ICodeCache codeCache) { + super(codeCache); + } + + @Override + public void remove(String clsFullName) { + // no op + } + + @Override + public void add(String clsFullName, ICodeInfo codeInfo) { + // no op + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/BufferCodeCache.java b/jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/BufferCodeCache.java new file mode 100644 index 00000000..97388cc9 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/BufferCodeCache.java @@ -0,0 +1,84 @@ +package jadx.gui.utils.codecache.disk; + +import java.io.IOException; +import java.util.Deque; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedDeque; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import jadx.api.ICodeCache; +import jadx.api.ICodeInfo; + +public class BufferCodeCache implements ICodeCache { + + private static final int BUFFER_SIZE = 20; + + private final ICodeCache backCache; + private final Map cache = new ConcurrentHashMap<>(); + private final Deque buffer = new ConcurrentLinkedDeque<>(); + + public BufferCodeCache(ICodeCache backCache) { + this.backCache = backCache; + } + + private void addInternal(String clsFullName, ICodeInfo codeInfo) { + cache.put(clsFullName, codeInfo); + buffer.addLast(clsFullName); + if (buffer.size() > BUFFER_SIZE) { + String removedKey = buffer.removeFirst(); + cache.remove(removedKey); + } + } + + @Override + public boolean contains(String clsFullName) { + if (cache.containsKey(clsFullName)) { + return true; + } + return backCache.contains(clsFullName); + } + + @Override + public void add(String clsFullName, ICodeInfo codeInfo) { + addInternal(clsFullName, codeInfo); + backCache.add(clsFullName, codeInfo); + } + + @Override + public @NotNull ICodeInfo get(String clsFullName) { + ICodeInfo codeInfo = cache.get(clsFullName); + if (codeInfo != null) { + return codeInfo; + } + ICodeInfo backCodeInfo = backCache.get(clsFullName); + if (backCodeInfo != ICodeInfo.EMPTY) { + addInternal(clsFullName, backCodeInfo); + } + return backCodeInfo; + } + + @Override + public @Nullable String getCode(String clsFullName) { + ICodeInfo codeInfo = cache.get(clsFullName); + if (codeInfo != null) { + return codeInfo.getCodeStr(); + } + return backCache.getCode(clsFullName); + } + + @Override + public void remove(String clsFullName) { + cache.remove(clsFullName); + backCache.remove(clsFullName); + } + + @Override + public void close() throws IOException { + cache.clear(); + buffer.clear(); + backCache.close(); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/CodeMetadataAdapter.java b/jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/CodeMetadataAdapter.java new file mode 100644 index 00000000..dff3b6b8 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/CodeMetadataAdapter.java @@ -0,0 +1,113 @@ +package jadx.gui.utils.codecache.disk; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.DataInput; +import java.io.DataInputStream; +import java.io.DataOutput; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import jadx.api.ICodeInfo; +import jadx.api.impl.AnnotatedCodeInfo; +import jadx.api.impl.SimpleCodeInfo; +import jadx.api.metadata.ICodeAnnotation; +import jadx.api.metadata.ICodeMetadata; +import jadx.core.dex.nodes.RootNode; +import jadx.core.utils.files.FileUtils; +import jadx.gui.utils.codecache.disk.adapters.CodeAnnotationAdapter; + +import static java.nio.file.StandardOpenOption.CREATE; +import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; +import static java.nio.file.StandardOpenOption.WRITE; + +public class CodeMetadataAdapter { + private static final byte[] JADX_METADATA_HEADER = "jadxmd".getBytes(StandardCharsets.US_ASCII); + + private final CodeAnnotationAdapter codeAnnotationAdapter; + + public CodeMetadataAdapter(RootNode root) { + codeAnnotationAdapter = new CodeAnnotationAdapter(root); + } + + public void write(Path metadataFile, ICodeMetadata metadata) { + FileUtils.makeDirsForFile(metadataFile); + try (OutputStream fileOutput = Files.newOutputStream(metadataFile, WRITE, CREATE, TRUNCATE_EXISTING); + DataOutputStream out = new DataOutputStream(new BufferedOutputStream(fileOutput))) { + out.write(JADX_METADATA_HEADER); + writeLines(out, metadata.getLineMapping()); + writeAnnotations(out, metadata.getAsMap()); + } catch (Exception e) { + throw new RuntimeException("Failed to write metadata file", e); + } + } + + public ICodeInfo readAndBuild(Path metadataFile, String code) { + if (!Files.exists(metadataFile)) { + return new SimpleCodeInfo(code); + } + try (InputStream fileInput = Files.newInputStream(metadataFile); + DataInputStream in = new DataInputStream(new BufferedInputStream(fileInput))) { + in.skipBytes(JADX_METADATA_HEADER.length); + Map lines = readLines(in); + Map annotations = readAnnotations(in); + return new AnnotatedCodeInfo(code, lines, annotations); + } catch (Exception e) { + throw new RuntimeException("Failed to parse code annotations", e); + } + } + + private void writeLines(DataOutput out, Map lines) throws IOException { + out.writeInt(lines.size()); + for (Map.Entry entry : lines.entrySet()) { + out.writeShort(entry.getKey()); + out.writeShort(entry.getValue()); + } + } + + private Map readLines(DataInput in) throws IOException { + int size = in.readInt(); + if (size == 0) { + return Collections.emptyMap(); + } + Map lines = new HashMap<>(size); + for (int i = 0; i < size; i++) { + int key = in.readShort(); + int value = in.readShort(); + lines.put(key, value); + } + return lines; + } + + private void writeAnnotations(DataOutputStream out, Map annotations) throws IOException { + out.writeInt(annotations.size()); + for (Map.Entry entry : annotations.entrySet()) { + out.writeInt(entry.getKey()); + codeAnnotationAdapter.write(out, entry.getValue()); + } + } + + private Map readAnnotations(DataInputStream in) throws IOException { + int size = in.readInt(); + if (size == 0) { + return Collections.emptyMap(); + } + Map map = new HashMap<>(size); + for (int i = 0; i < size; i++) { + int pos = in.readInt(); + ICodeAnnotation ann = codeAnnotationAdapter.read(in); + if (ann != null) { + map.put(pos, ann); + } + } + return map; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/DiskCodeCache.java b/jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/DiskCodeCache.java new file mode 100644 index 00000000..700bf757 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/DiskCodeCache.java @@ -0,0 +1,258 @@ +package jadx.gui.utils.codecache.disk; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileTime; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.api.ICodeCache; +import jadx.api.ICodeInfo; +import jadx.api.JadxArgs; +import jadx.core.dex.nodes.RootNode; +import jadx.core.utils.Utils; +import jadx.core.utils.exceptions.JadxRuntimeException; +import jadx.core.utils.files.FileUtils; + +public class DiskCodeCache implements ICodeCache { + private static final Logger LOG = LoggerFactory.getLogger(DiskCodeCache.class); + + private static final int DATA_FORMAT_VERSION = 7; + + private final Path srcDir; + private final Path metaDir; + private final Path codeVersionFile; + private final String codeVersion; + private final CodeMetadataAdapter codeMetadataAdapter; + private final ExecutorService writePool; + private final Map writeOps = new ConcurrentHashMap<>(); + private final Set cachedKeys = Collections.synchronizedSet(new HashSet<>()); + + public DiskCodeCache(RootNode root, Path baseDir) { + srcDir = baseDir.resolve("sources"); + metaDir = baseDir.resolve("metadata"); + codeVersionFile = baseDir.resolve("code-version"); + JadxArgs args = root.getArgs(); + codeVersion = buildCodeVersion(args); + writePool = Executors.newFixedThreadPool(args.getThreadsCount()); + codeMetadataAdapter = new CodeMetadataAdapter(root); + if (checkCodeVersion()) { + collectCachedItems(); + } else { + reset(); + } + } + + private boolean checkCodeVersion() { + try { + if (!Files.exists(codeVersionFile)) { + return false; + } + String currentCodeVer = readFileToString(codeVersionFile); + return currentCodeVer.equals(codeVersion); + } catch (Exception e) { + LOG.warn("Failed to load code version file", e); + return false; + } + } + + private void reset() { + try { + long start = System.currentTimeMillis(); + LOG.info("Resetting disk code cache, base dir: {}", srcDir.getParent().toAbsolutePath()); + FileUtils.deleteDirIfExists(srcDir); + FileUtils.deleteDirIfExists(metaDir); + FileUtils.makeDirs(srcDir); + FileUtils.makeDirs(metaDir); + writeFile(codeVersionFile, codeVersion); + cachedKeys.clear(); + if (LOG.isDebugEnabled()) { + LOG.info("Reset done in: {}ms", System.currentTimeMillis() - start); + } + } catch (Exception e) { + throw new JadxRuntimeException("Failed to reset code cache", e); + } + } + + private void collectCachedItems() { + cachedKeys.clear(); + try { + long start = System.currentTimeMillis(); + PathMatcher matcher = metaDir.getFileSystem().getPathMatcher("glob:**.jadxmd"); + Files.walkFileTree(metaDir, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + if (matcher.matches(file)) { + Path relPath = metaDir.relativize(file); + String filePath = relPath.toString(); + String clsName = filePath.substring(0, filePath.length() - 7).replace(File.separatorChar, '.'); + cachedKeys.add(clsName); + } + return FileVisitResult.CONTINUE; + } + }); + LOG.info("Found {} classes in disk cache in {} ms", cachedKeys.size(), System.currentTimeMillis() - start); + } catch (Exception e) { + LOG.error("Failed to collect cached items", e); + } + } + + /** + * Async writes backed by in-memory store + */ + @Override + public void add(String clsFullName, ICodeInfo codeInfo) { + writeOps.put(clsFullName, codeInfo); + cachedKeys.add(clsFullName); + writePool.execute(() -> { + try { + writeFile(getJavaFile(clsFullName), codeInfo.getCodeStr()); + codeMetadataAdapter.write(getMetadataFile(clsFullName), codeInfo.getCodeMetadata()); + } catch (Exception e) { + LOG.error("Failed to write code cache for " + clsFullName, e); + remove(clsFullName); + } finally { + writeOps.remove(clsFullName); + } + }); + } + + @Override + public @Nullable String getCode(String clsFullName) { + try { + if (!contains(clsFullName)) { + return null; + } + ICodeInfo wrtCodeInfo = writeOps.get(clsFullName); + if (wrtCodeInfo != null) { + return wrtCodeInfo.getCodeStr(); + } + Path javaFile = getJavaFile(clsFullName); + if (!Files.exists(javaFile)) { + return null; + } + return readFileToString(javaFile); + } catch (Exception e) { + LOG.error("Failed to read class code for {}", clsFullName, e); + return null; + } + } + + @Override + public ICodeInfo get(String clsFullName) { + try { + if (!contains(clsFullName)) { + return ICodeInfo.EMPTY; + } + ICodeInfo wrtCodeInfo = writeOps.get(clsFullName); + if (wrtCodeInfo != null) { + return wrtCodeInfo; + } + Path javaFile = getJavaFile(clsFullName); + if (!Files.exists(javaFile)) { + return ICodeInfo.EMPTY; + } + String code = readFileToString(javaFile); + return codeMetadataAdapter.readAndBuild(getMetadataFile(clsFullName), code); + } catch (Exception e) { + LOG.error("Failed to read code cache for {}", clsFullName, e); + return ICodeInfo.EMPTY; + } + } + + @Override + public boolean contains(String clsFullName) { + return cachedKeys.contains(clsFullName); + } + + @Override + public void remove(String clsFullName) { + try { + LOG.debug("Removing class info from disk: {}", clsFullName); + cachedKeys.remove(clsFullName); + Files.deleteIfExists(getJavaFile(clsFullName)); + Files.deleteIfExists(getMetadataFile(clsFullName)); + } catch (Exception e) { + throw new JadxRuntimeException("Failed to remove code cache for " + clsFullName, e); + } + } + + private static String readFileToString(Path textFile) throws IOException { + return new String(Files.readAllBytes(textFile), StandardCharsets.UTF_8); + } + + private void writeFile(Path file, String data) { + try { + FileUtils.makeDirsForFile(file); + Files.write(file, data.getBytes(StandardCharsets.UTF_8)); + } catch (Exception e) { + LOG.error("Failed to write file: {}", file.toAbsolutePath(), e); + } + } + + private String buildCodeVersion(JadxArgs args) { + return DATA_FORMAT_VERSION + + ":" + args.makeCodeArgsHash() + + ":" + buildInputsHash(args.getInputFiles()); + } + + /** + * Hash timestamps of all input files + */ + private String buildInputsHash(List inputs) { + try (ByteArrayOutputStream bout = new ByteArrayOutputStream(); + DataOutputStream data = new DataOutputStream(bout)) { + List inputPaths = Utils.collectionMap(inputs, File::toPath); + List inputFiles = FileUtils.expandDirs(inputPaths); + Collections.sort(inputFiles); + data.write(inputs.size()); + data.write(inputFiles.size()); + for (Path inputFile : inputFiles) { + FileTime modifiedTime = Files.getLastModifiedTime(inputFile); + data.writeLong(modifiedTime.toMillis()); + } + return FileUtils.md5Sum(bout.toByteArray()); + } catch (Exception e) { + throw new JadxRuntimeException("Failed to build hash for inputs", e); + } + } + + private Path getJavaFile(String clsFullName) { + return srcDir.resolve(clsFullName.replace('.', File.separatorChar) + ".java"); + } + + private Path getMetadataFile(String clsFullName) { + return metaDir.resolve(clsFullName.replace('.', File.separatorChar) + ".jadxmd"); + } + + @SuppressWarnings("ResultOfMethodCallIgnored") + @Override + public void close() throws IOException { + try { + writePool.shutdown(); + writePool.awaitTermination(2, TimeUnit.MINUTES); + } catch (InterruptedException e) { + LOG.error("Failed to finish file writes", e); + } + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/ArgTypeAdapter.java b/jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/ArgTypeAdapter.java new file mode 100644 index 00000000..fcffae47 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/ArgTypeAdapter.java @@ -0,0 +1,39 @@ +package jadx.gui.utils.codecache.disk.adapters; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +import jadx.core.codegen.TypeGen; +import jadx.core.dex.instructions.args.ArgType; + +public class ArgTypeAdapter implements DataAdapter { + + public static final ArgTypeAdapter INSTANCE = new ArgTypeAdapter(); + + @Override + public void write(DataOutput out, ArgType value) throws IOException { + if (value == null) { + out.writeByte(0); + } else if (!value.isTypeKnown()) { + out.write(1); + } else { + out.writeByte(2); + out.writeUTF(TypeGen.signature(value)); + } + } + + @Override + public ArgType read(DataInput in) throws IOException { + switch (in.readByte()) { + case 0: + return null; + case 1: + return ArgType.UNKNOWN; + case 2: + return ArgType.parse(in.readUTF()); + default: + throw new RuntimeException("Unexpected arg type tag"); + } + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/BaseDataAdapter.java b/jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/BaseDataAdapter.java new file mode 100644 index 00000000..75211545 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/BaseDataAdapter.java @@ -0,0 +1,26 @@ +package jadx.gui.utils.codecache.disk.adapters; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +import org.jetbrains.annotations.Nullable; + +public abstract class BaseDataAdapter implements DataAdapter { + + public void writeNullableUTF(DataOutput out, @Nullable String str) throws IOException { + if (str == null) { + out.writeByte(0); + } else { + out.writeByte(1); + out.writeUTF(str); + } + } + + public @Nullable String readNullableUTF(DataInput in) throws IOException { + if (in.readByte() == 0) { + return null; + } + return in.readUTF(); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/ClassNodeAdapter.java b/jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/ClassNodeAdapter.java new file mode 100644 index 00000000..b8cb29ef --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/ClassNodeAdapter.java @@ -0,0 +1,26 @@ +package jadx.gui.utils.codecache.disk.adapters; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +import jadx.core.dex.nodes.ClassNode; +import jadx.core.dex.nodes.RootNode; + +public class ClassNodeAdapter implements DataAdapter { + private final RootNode root; + + public ClassNodeAdapter(RootNode root) { + this.root = root; + } + + @Override + public void write(DataOutput out, ClassNode value) throws IOException { + out.writeUTF(value.getClassInfo().getRawName()); + } + + @Override + public ClassNode read(DataInput in) throws IOException { + return root.resolveClass(in.readUTF()); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/CodeAnnotationAdapter.java b/jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/CodeAnnotationAdapter.java new file mode 100644 index 00000000..80ec8dd6 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/CodeAnnotationAdapter.java @@ -0,0 +1,90 @@ +package jadx.gui.utils.codecache.disk.adapters; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; +import java.util.EnumMap; +import java.util.Map; + +import jadx.api.metadata.ICodeAnnotation; +import jadx.api.metadata.ICodeAnnotation.AnnType; +import jadx.core.dex.nodes.RootNode; + +public class CodeAnnotationAdapter implements DataAdapter { + private final Map adaptersByCls; + private final TypeInfo[] adaptersByTag; + + public CodeAnnotationAdapter(RootNode root) { + Map> map = registerAdapters(root); + int size = map.size(); + adaptersByCls = new EnumMap<>(AnnType.class); + adaptersByTag = new TypeInfo[size + 1]; + int tag = 1; + for (Map.Entry> entry : map.entrySet()) { + TypeInfo typeInfo = new TypeInfo(tag, entry.getValue()); + adaptersByCls.put(entry.getKey(), typeInfo); + adaptersByTag[tag] = typeInfo; + tag++; + } + } + + private Map> registerAdapters(RootNode root) { + Map> map = new EnumMap<>(AnnType.class); + MethodNodeAdapter mthAdapter = new MethodNodeAdapter(root); + map.put(AnnType.CLASS, new ClassNodeAdapter(root)); + map.put(AnnType.FIELD, new FieldNodeAdapter(root)); + map.put(AnnType.METHOD, mthAdapter); + map.put(AnnType.DECLARATION, new NodeDeclareRefAdapter(this)); + map.put(AnnType.VAR, new VarNodeAdapter(mthAdapter)); + map.put(AnnType.VAR_REF, VarRefAdapter.INSTANCE); + map.put(AnnType.OFFSET, InsnCodeOffsetAdapter.INSTANCE); + return map; + } + + @SuppressWarnings("unchecked") + @Override + public void write(DataOutput out, ICodeAnnotation value) throws IOException { + if (value == null) { + out.writeByte(0); + return; + } + TypeInfo typeInfo = adaptersByCls.get(value.getAnnType()); + if (typeInfo == null) { + throw new RuntimeException("Unexpected code annotation type: " + value.getClass().getSimpleName()); + } + out.writeByte(typeInfo.getTag()); + typeInfo.getAdapter().write(out, value); + } + + @Override + public ICodeAnnotation read(DataInput in) throws IOException { + int tag = in.readByte(); + if (tag == 0) { + return null; + } + TypeInfo typeInfo = adaptersByTag[tag]; + if (typeInfo == null) { + throw new RuntimeException("Unknown type tag: " + tag); + } + return (ICodeAnnotation) typeInfo.getAdapter().read(in); + } + + @SuppressWarnings("rawtypes") + private static class TypeInfo { + private final int tag; + private final DataAdapter adapter; + + private TypeInfo(int tag, DataAdapter adapter) { + this.tag = tag; + this.adapter = adapter; + } + + public int getTag() { + return tag; + } + + public DataAdapter getAdapter() { + return adapter; + } + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/DataAdapter.java b/jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/DataAdapter.java new file mode 100644 index 00000000..b7ca3b93 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/DataAdapter.java @@ -0,0 +1,12 @@ +package jadx.gui.utils.codecache.disk.adapters; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +public interface DataAdapter { + + void write(DataOutput out, T value) throws IOException; + + T read(DataInput in) throws IOException; +} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/FieldNodeAdapter.java b/jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/FieldNodeAdapter.java new file mode 100644 index 00000000..b4e1b0a4 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/FieldNodeAdapter.java @@ -0,0 +1,40 @@ +package jadx.gui.utils.codecache.disk.adapters; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +import jadx.core.dex.info.FieldInfo; +import jadx.core.dex.nodes.ClassNode; +import jadx.core.dex.nodes.FieldNode; +import jadx.core.dex.nodes.RootNode; + +public class FieldNodeAdapter implements DataAdapter { + private final RootNode root; + + public FieldNodeAdapter(RootNode root) { + this.root = root; + } + + @Override + public void write(DataOutput out, FieldNode value) throws IOException { + FieldInfo fieldInfo = value.getFieldInfo(); + out.writeUTF(fieldInfo.getDeclClass().getRawName()); + out.writeUTF(fieldInfo.getShortId()); + } + + @Override + public FieldNode read(DataInput in) throws IOException { + String cls = in.readUTF(); + String sign = in.readUTF(); + ClassNode clsNode = root.resolveClass(cls); + if (clsNode == null) { + throw new RuntimeException("Class not found: " + cls); + } + FieldNode fieldNode = clsNode.searchFieldByShortId(sign); + if (fieldNode == null) { + throw new RuntimeException("Field not found: " + cls + "." + sign); + } + return fieldNode; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/InsnCodeOffsetAdapter.java b/jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/InsnCodeOffsetAdapter.java new file mode 100644 index 00000000..e391f413 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/InsnCodeOffsetAdapter.java @@ -0,0 +1,22 @@ +package jadx.gui.utils.codecache.disk.adapters; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +import jadx.api.metadata.annotations.InsnCodeOffset; + +public class InsnCodeOffsetAdapter implements DataAdapter { + + public static final InsnCodeOffsetAdapter INSTANCE = new InsnCodeOffsetAdapter(); + + @Override + public void write(DataOutput out, InsnCodeOffset value) throws IOException { + out.writeShort(value.getOffset()); + } + + @Override + public InsnCodeOffset read(DataInput in) throws IOException { + return new InsnCodeOffset(in.readShort()); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/MethodNodeAdapter.java b/jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/MethodNodeAdapter.java new file mode 100644 index 00000000..c2f265ef --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/MethodNodeAdapter.java @@ -0,0 +1,40 @@ +package jadx.gui.utils.codecache.disk.adapters; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +import jadx.core.dex.info.MethodInfo; +import jadx.core.dex.nodes.ClassNode; +import jadx.core.dex.nodes.MethodNode; +import jadx.core.dex.nodes.RootNode; + +public class MethodNodeAdapter implements DataAdapter { + private final RootNode root; + + public MethodNodeAdapter(RootNode root) { + this.root = root; + } + + @Override + public void write(DataOutput out, MethodNode value) throws IOException { + MethodInfo methodInfo = value.getMethodInfo(); + out.writeUTF(methodInfo.getDeclClass().getRawName()); + out.writeUTF(methodInfo.getShortId()); + } + + @Override + public MethodNode read(DataInput in) throws IOException { + String cls = in.readUTF(); + String sign = in.readUTF(); + ClassNode clsNode = root.resolveClass(cls); + if (clsNode == null) { + throw new RuntimeException("Class not found: " + cls); + } + MethodNode methodNode = clsNode.searchMethodByShortId(sign); + if (methodNode == null) { + throw new RuntimeException("Method not found: " + cls + "." + sign); + } + return methodNode; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/NodeDeclareRefAdapter.java b/jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/NodeDeclareRefAdapter.java new file mode 100644 index 00000000..69bebab4 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/NodeDeclareRefAdapter.java @@ -0,0 +1,37 @@ +package jadx.gui.utils.codecache.disk.adapters; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +import jadx.api.metadata.ICodeNodeRef; +import jadx.api.metadata.annotations.NodeDeclareRef; + +public class NodeDeclareRefAdapter implements DataAdapter { + private final CodeAnnotationAdapter refAdapter; + + public NodeDeclareRefAdapter(CodeAnnotationAdapter refAdapter) { + this.refAdapter = refAdapter; + } + + @Override + public void write(DataOutput out, NodeDeclareRef value) throws IOException { + ICodeNodeRef node = value.getNode(); + if (node == null) { + throw new RuntimeException("Null node in NodeDeclareRef"); + } + refAdapter.write(out, node); + out.writeShort(value.getDefPos()); + } + + @Override + public NodeDeclareRef read(DataInput in) throws IOException { + ICodeNodeRef ref = (ICodeNodeRef) refAdapter.read(in); + int defPos = in.readShort(); + NodeDeclareRef nodeDeclareRef = new NodeDeclareRef(ref); + nodeDeclareRef.setDefPos(defPos); + // restore def position if loading metadata without actual decompilation + ref.setDefPosition(defPos); + return nodeDeclareRef; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/VarNodeAdapter.java b/jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/VarNodeAdapter.java new file mode 100644 index 00000000..f2f4a3ba --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/VarNodeAdapter.java @@ -0,0 +1,36 @@ +package jadx.gui.utils.codecache.disk.adapters; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +import jadx.api.metadata.annotations.VarNode; +import jadx.core.dex.instructions.args.ArgType; +import jadx.core.dex.nodes.MethodNode; + +public class VarNodeAdapter extends BaseDataAdapter { + private final MethodNodeAdapter mthAdapter; + + public VarNodeAdapter(MethodNodeAdapter mthAdapter) { + this.mthAdapter = mthAdapter; + } + + @Override + public void write(DataOutput out, VarNode value) throws IOException { + mthAdapter.write(out, value.getMth()); + out.writeShort(value.getReg()); + out.writeShort(value.getSsa()); + ArgTypeAdapter.INSTANCE.write(out, value.getType()); + writeNullableUTF(out, value.getName()); + } + + @Override + public VarNode read(DataInput in) throws IOException { + MethodNode mth = mthAdapter.read(in); + int reg = in.readShort(); + int ssa = in.readShort(); + ArgType type = ArgTypeAdapter.INSTANCE.read(in); + String name = readNullableUTF(in); + return new VarNode(mth, reg, ssa, type, name); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/VarRefAdapter.java b/jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/VarRefAdapter.java new file mode 100644 index 00000000..8f5b20a7 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/utils/codecache/disk/adapters/VarRefAdapter.java @@ -0,0 +1,26 @@ +package jadx.gui.utils.codecache.disk.adapters; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +import jadx.api.metadata.annotations.VarRef; + +public class VarRefAdapter extends BaseDataAdapter { + + public static final VarRefAdapter INSTANCE = new VarRefAdapter(); + + @Override + public void write(DataOutput out, VarRef value) throws IOException { + int refPos = value.getRefPos(); + if (refPos == 0) { + throw new RuntimeException("Variable refPos is zero: " + value); + } + out.writeShort(refPos); + } + + @Override + public VarRef read(DataInput in) throws IOException { + return VarRef.fromPos(in.readShort()); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/search/CodeIndex.java b/jadx-gui/src/main/java/jadx/gui/utils/search/CodeIndex.java deleted file mode 100644 index 8b5b0cb4..00000000 --- a/jadx-gui/src/main/java/jadx/gui/utils/search/CodeIndex.java +++ /dev/null @@ -1,59 +0,0 @@ -package jadx.gui.utils.search; - -import java.util.ArrayList; -import java.util.List; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import io.reactivex.BackpressureStrategy; -import io.reactivex.Flowable; - -import jadx.api.JavaClass; -import jadx.gui.treemodel.CodeNode; -import jadx.gui.treemodel.JClass; -import jadx.gui.utils.UiUtils; - -public class CodeIndex { - - private static final Logger LOG = LoggerFactory.getLogger(CodeIndex.class); - - private final List values = new ArrayList<>(); - - public void put(CodeNode value) { - values.add(value); - } - - public void removeForCls(JavaClass cls) { - values.removeIf(v -> v.getJavaNode().getTopParentClass().equals(cls)); - } - - private boolean isMatched(StringRef key, SearchSettings searchSettings) { - return searchSettings.isMatch(key); - } - - public Flowable search(final SearchSettings searchSettings) { - JClass activeCls = searchSettings.getActiveCls(); - return Flowable.create(emitter -> { - LOG.debug("Code search started: {} ...", searchSettings.getSearchString()); - for (CodeNode node : values) { - if (activeCls == null || node.getRootClass().equals(activeCls)) { - int pos = searchSettings.find(node.getLineStr()); - if (pos > -1) { - emitter.onNext(node); - } - } - if (emitter.isCancelled()) { - LOG.debug("Code search canceled: {}", searchSettings.getSearchString()); - return; - } - } - LOG.debug("Code search complete: {}, memory usage: {}", searchSettings.getSearchString(), UiUtils.memoryInfo()); - emitter.onComplete(); - }, BackpressureStrategy.BUFFER); - } - - public int size() { - return values.size(); - } -} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/search/SearchIndex.java b/jadx-gui/src/main/java/jadx/gui/utils/search/SearchIndex.java deleted file mode 100644 index 4bf2e962..00000000 --- a/jadx-gui/src/main/java/jadx/gui/utils/search/SearchIndex.java +++ /dev/null @@ -1,12 +0,0 @@ -package jadx.gui.utils.search; - -import io.reactivex.Flowable; - -public interface SearchIndex { - - void put(String str, V value); - - Flowable search(String searchStr, boolean caseInsensitive); - - int size(); -} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/search/SearchSettings.java b/jadx-gui/src/main/java/jadx/gui/utils/search/SearchSettings.java deleted file mode 100644 index b5a4b295..00000000 --- a/jadx-gui/src/main/java/jadx/gui/utils/search/SearchSettings.java +++ /dev/null @@ -1,114 +0,0 @@ -package jadx.gui.utils.search; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.apache.commons.lang3.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import jadx.gui.treemodel.JClass; - -public class SearchSettings { - - private static final Logger LOG = LoggerFactory.getLogger(SearchSettings.class); - - private final String searchString; - private final boolean useRegex; - private final boolean ignoreCase; - - private JClass activeCls; - - private Pattern regexPattern; - private int startPos = 0; - - public SearchSettings(String searchString, boolean ignoreCase, boolean useRegex) { - this.searchString = searchString; - this.useRegex = useRegex; - this.ignoreCase = ignoreCase; - } - - public boolean isUseRegex() { - return this.useRegex; - } - - public boolean isIgnoreCase() { - return this.ignoreCase; - } - - public String getSearchString() { - return this.searchString; - } - - public int getStartPos() { - return this.startPos; - } - - public void setStartPos(int startPos) { - this.startPos = startPos; - } - - public Pattern getPattern() { - return this.regexPattern; - } - - public boolean preCompile() throws InvalidSearchTermException { - if (useRegex) { - try { - int flags = ignoreCase ? Pattern.CASE_INSENSITIVE : 0; - this.regexPattern = Pattern.compile(searchString, flags); - } catch (Exception e) { - throw new InvalidSearchTermException("Invalid Regex: " + this.searchString, e); - } - } - return true; - } - - public boolean isMatch(StringRef searchArea) { - return find(searchArea) != -1; - } - - public boolean isMatch(String searchArea) { - return find(searchArea) != -1; - } - - public int find(StringRef searchArea) { - if (useRegex) { - return findWithRegex(searchArea.toString()); - } - return searchArea.indexOf(this.searchString, this.startPos, this.ignoreCase); - } - - public int find(String searchArea) { - if (useRegex) { - return findWithRegex(searchArea); - } - if (ignoreCase) { - return StringUtils.indexOfIgnoreCase(searchArea, searchString, startPos); - } - return searchArea.indexOf(searchString, startPos); - } - - private int findWithRegex(String searchArea) { - Matcher matcher = regexPattern.matcher(searchArea); - if (matcher.find(startPos)) { - return matcher.start(); - } - return -1; - } - - public JClass getActiveCls() { - return activeCls; - } - - public void setActiveCls(JClass activeCls) { - this.activeCls = activeCls; - } - - public static class InvalidSearchTermException extends Exception { - - public InvalidSearchTermException(String message, Throwable cause) { - super(message, cause); - } - } -} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/search/SimpleIndex.java b/jadx-gui/src/main/java/jadx/gui/utils/search/SimpleIndex.java deleted file mode 100644 index 811b2cf0..00000000 --- a/jadx-gui/src/main/java/jadx/gui/utils/search/SimpleIndex.java +++ /dev/null @@ -1,54 +0,0 @@ -package jadx.gui.utils.search; - -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; - -import io.reactivex.BackpressureStrategy; -import io.reactivex.Flowable; - -import jadx.api.JavaClass; -import jadx.gui.treemodel.JClass; -import jadx.gui.treemodel.JNode; - -public class SimpleIndex { - private final Map data = new HashMap<>(); - - public void put(String str, JNode value) { - data.put(value, str); - } - - public void removeForCls(JavaClass cls) { - data.entrySet().removeIf(e -> e.getKey().getJavaNode().getTopParentClass().equals(cls)); - } - - private boolean isMatched(String str, JNode node, SearchSettings searchSettings) { - if (searchSettings.isMatch(str)) { - JClass activeCls = searchSettings.getActiveCls(); - if (activeCls == null) { - return true; - } - return Objects.equals(node.getRootClass(), activeCls); - } - return false; - } - - public Flowable search(final SearchSettings searchSettings) { - return Flowable.create(emitter -> { - for (Map.Entry entry : data.entrySet()) { - JNode node = entry.getKey(); - if (isMatched(entry.getValue(), node, searchSettings)) { - emitter.onNext(node); - } - if (emitter.isCancelled()) { - return; - } - } - emitter.onComplete(); - }, BackpressureStrategy.BUFFER); - } - - public int size() { - return data.size(); - } -} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/search/StringRef.java b/jadx-gui/src/main/java/jadx/gui/utils/search/StringRef.java deleted file mode 100644 index acfc328a..00000000 --- a/jadx-gui/src/main/java/jadx/gui/utils/search/StringRef.java +++ /dev/null @@ -1,208 +0,0 @@ -package jadx.gui.utils.search; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import org.jetbrains.annotations.NotNull; - -import static jadx.gui.utils.UiUtils.caseChar; - -public class StringRef implements CharSequence { - - private final String refStr; - private final int offset; - private final int length; - - private int hash; - - public static StringRef subString(String str, int from, int to) { - return new StringRef(str, from, to - from); - } - - public static StringRef subString(String str, int from) { - return subString(str, from, str.length()); - } - - public static StringRef fromStr(String str) { - return new StringRef(str, 0, str.length()); - } - - private StringRef(String str, int from, int length) { - this.refStr = str; - this.offset = from; - this.length = length; - } - - @Override - public int length() { - return length; - } - - @Override - public char charAt(int index) { - return refStr.charAt(offset + index); - } - - @Override - public CharSequence subSequence(int start, int end) { - return subString(refStr, start, end); - } - - public StringRef trim() { - int start = offset; - int end = start + length; - String str = refStr; - - while ((start < end) && (str.charAt(start) <= ' ')) { - start++; - } - while ((start < end) && (str.charAt(end - 1) <= ' ')) { - end--; - } - if ((start > offset) || (end < offset + length)) { - return subString(str, start, end); - } - return this; - } - - public int indexOf(String str) { - return indexOf(str, 0); - } - - public int indexOf(String str, boolean caseInsensitive) { - return indexOf(str, 0, caseInsensitive); - } - - public int indexOf(String str, int from, boolean caseInsensitive) { - return indexOf(refStr, offset, length, str, 0, str.length(), from, caseInsensitive); - } - - public int indexOf(String str, int from) { - return indexOf(refStr, offset, length, str, 0, str.length(), from, false); - } - - private static int indexOf(String source, int sourceOffset, int sourceCount, - String target, int targetOffset, int targetCount, - int fromIndex, boolean caseInsensitive) { - if (fromIndex >= sourceCount) { - return (targetCount == 0 ? sourceCount : -1); - } - if (fromIndex < 0) { - fromIndex = 0; - } - if (targetCount == 0) { - return -1; - } - char first = caseChar(target.charAt(targetOffset), caseInsensitive); - int max = sourceOffset + (sourceCount - targetCount); - for (int i = sourceOffset + fromIndex; i <= max; i++) { - if (caseChar(source.charAt(i), caseInsensitive) != first) { - while (++i <= max && caseChar(source.charAt(i), caseInsensitive) != first) { - } - } - if (i <= max) { - int j = i + 1; - int end = j + targetCount - 1; - int k = targetOffset + 1; - while (j < end && caseChar(source.charAt(j), caseInsensitive) == caseChar(target.charAt(k), caseInsensitive)) { - j++; - k++; - } - if (j == end) { - return i - sourceOffset; - } - } - } - return -1; - } - - public boolean startsWith(String str) { - int strLen = str.length(); - if (this.length < strLen) { - return false; - } - for (int i = 0; i < strLen; i++) { - if (charAt(i) != str.charAt(i)) { - return false; - } - } - return true; - } - - public static List split(String str, String splitBy) { - int len = str.length(); - int targetLen = splitBy.length(); - if (len == 0 || targetLen == 0) { - return Collections.emptyList(); - } - int pos = -targetLen; - List list = new ArrayList<>(); - while (true) { - int start = pos + targetLen; - pos = indexOf(str, 0, len, splitBy, 0, targetLen, start, false); - if (pos == -1) { - if (start != len) { - list.add(subString(str, start, len)); - } - break; - } else { - list.add(subString(str, start, pos)); - } - } - return list; - } - - public int getOffset() { - return offset; - } - - public int hashCode() { - int h = hash; - int len = length; - if (h == 0 && len > 0) { - int off = offset; - String str = this.refStr; - for (int i = 0; i < len; i++) { - h = 31 * h + str.charAt(off++); - } - hash = h; - } - return h; - } - - public boolean equals(Object other) { - if (this == other) { - return true; - } - if (!(other instanceof StringRef)) { - return false; - } - StringRef otherSlice = (StringRef) other; - int len = this.length; - if (len != otherSlice.length) { - return false; - } - int i = offset; - int j = otherSlice.offset; - String refStr = this.refStr; - String otherRefStr = otherSlice.refStr; - while (len-- != 0) { - if (refStr.charAt(i++) != otherRefStr.charAt(j++)) { - return false; - } - } - return true; - } - - @NotNull - @Override - public String toString() { - int len = this.length; - if (len == 0) { - return ""; - } - int offset = this.offset; - return refStr.substring(offset, offset + len); - } -} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/search/TextSearchIndex.java b/jadx-gui/src/main/java/jadx/gui/utils/search/TextSearchIndex.java deleted file mode 100644 index 3ae0d9ee..00000000 --- a/jadx-gui/src/main/java/jadx/gui/utils/search/TextSearchIndex.java +++ /dev/null @@ -1,211 +0,0 @@ -package jadx.gui.utils.search; - -import java.util.ArrayList; -import java.util.List; -import java.util.Set; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import io.reactivex.BackpressureStrategy; -import io.reactivex.Flowable; -import io.reactivex.FlowableEmitter; - -import jadx.api.ICodeWriter; -import jadx.api.JavaClass; -import jadx.api.JavaField; -import jadx.api.JavaMethod; -import jadx.api.JavaNode; -import jadx.gui.treemodel.CodeNode; -import jadx.gui.treemodel.JNode; -import jadx.gui.ui.MainWindow; -import jadx.gui.ui.dialog.SearchDialog; -import jadx.gui.utils.CacheObject; -import jadx.gui.utils.CodeLinesInfo; -import jadx.gui.utils.JNodeCache; -import jadx.gui.utils.JumpPosition; -import jadx.gui.utils.UiUtils; - -import static jadx.gui.ui.dialog.SearchDialog.SearchOptions.ACTIVE_TAB; -import static jadx.gui.ui.dialog.SearchDialog.SearchOptions.CLASS; -import static jadx.gui.ui.dialog.SearchDialog.SearchOptions.CODE; -import static jadx.gui.ui.dialog.SearchDialog.SearchOptions.COMMENT; -import static jadx.gui.ui.dialog.SearchDialog.SearchOptions.FIELD; -import static jadx.gui.ui.dialog.SearchDialog.SearchOptions.IGNORE_CASE; -import static jadx.gui.ui.dialog.SearchDialog.SearchOptions.METHOD; -import static jadx.gui.ui.dialog.SearchDialog.SearchOptions.RESOURCE; -import static jadx.gui.ui.dialog.SearchDialog.SearchOptions.USE_REGEX; - -public class TextSearchIndex { - - private static final Logger LOG = LoggerFactory.getLogger(TextSearchIndex.class); - - private final CacheObject cache; - private final MainWindow mainWindow; - private final JNodeCache nodeCache; - - private final SimpleIndex clsNamesIndex; - private final SimpleIndex mthSignaturesIndex; - private final SimpleIndex fldSignaturesIndex; - private final CodeIndex codeIndex; - private final ResourceIndex resIndex; - - private final List skippedClasses = new ArrayList<>(); - - public TextSearchIndex(MainWindow mainWindow) { - this.mainWindow = mainWindow; - this.cache = mainWindow.getCacheObject(); - this.nodeCache = cache.getNodeCache(); - this.resIndex = new ResourceIndex(cache); - this.clsNamesIndex = new SimpleIndex(); - this.mthSignaturesIndex = new SimpleIndex(); - this.fldSignaturesIndex = new SimpleIndex(); - this.codeIndex = new CodeIndex(); - } - - public void indexNames(JavaClass cls) { - clsNamesIndex.put(cls.getFullName(), nodeCache.makeFrom(cls)); - for (JavaMethod mth : cls.getMethods()) { - JNode mthNode = nodeCache.makeFrom(mth); - mthSignaturesIndex.put(mthNode.makeDescString(), mthNode); - } - for (JavaField fld : cls.getFields()) { - JNode fldNode = nodeCache.makeFrom(fld); - fldSignaturesIndex.put(fldNode.makeDescString(), fldNode); - } - for (JavaClass innerCls : cls.getInnerClasses()) { - indexNames(innerCls); - } - } - - public void indexCode(JavaClass cls, CodeLinesInfo linesInfo, List lines) { - try { - int count = lines.size(); - for (int i = 0; i < count; i++) { - StringRef line = lines.get(i); - int lineLength = line.length(); - if (lineLength == 0 || (lineLength == 1 && line.charAt(0) == '}')) { - continue; - } - int lineNum = i + 1; - JavaNode node = linesInfo.getJavaNodeByLine(lineNum); - JavaNode javaNode = node == null ? cls : node; - JNode nodeAtLine = nodeCache.makeFrom(javaNode); - codeIndex.put(new CodeNode(nodeAtLine, line, lineNum, line.getOffset())); - } - } catch (Exception e) { - LOG.warn("Failed to index class: {}", cls, e); - } - } - - public void indexResource() { - resIndex.index(); - } - - public void remove(JavaClass cls) { - this.clsNamesIndex.removeForCls(cls); - this.mthSignaturesIndex.removeForCls(cls); - this.fldSignaturesIndex.removeForCls(cls); - this.codeIndex.removeForCls(cls); - } - - public Flowable buildSearch(String text, Set options) - throws SearchSettings.InvalidSearchTermException { - boolean ignoreCase = options.contains(IGNORE_CASE); - boolean useRegex = options.contains(USE_REGEX); - - LOG.debug("Building search, ignoreCase: {}, useRegex: {}", ignoreCase, useRegex); - Flowable result = Flowable.empty(); - - SearchSettings searchSettings = new SearchSettings(text, options.contains(IGNORE_CASE), options.contains(USE_REGEX)); - if (options.contains(ACTIVE_TAB)) { - JumpPosition activeNode = mainWindow.getTabbedPane().getCurrentPosition(); - if (activeNode != null) { - searchSettings.setActiveCls(activeNode.getNode().getRootClass()); - } - if (searchSettings.getActiveCls() == null) { - return result; - } - } - if (!searchSettings.preCompile()) { - return result; - } - - if (options.contains(COMMENT)) { - CommentsIndex commentsIndex = cache.getCommentsIndex(); - result = Flowable.concat(result, commentsIndex.search(searchSettings)); - if (text.isEmpty()) { - // return all comments on empty search string - // other searches don't support empty string, so return immediately - return result; - } - } - - if (options.contains(CLASS)) { - result = Flowable.concat(result, clsNamesIndex.search(searchSettings)); - } - if (options.contains(METHOD)) { - result = Flowable.concat(result, mthSignaturesIndex.search(searchSettings)); - } - if (options.contains(FIELD)) { - result = Flowable.concat(result, fldSignaturesIndex.search(searchSettings)); - } - if (options.contains(CODE)) { - if (codeIndex.size() > 0) { - result = Flowable.concat(result, codeIndex.search(searchSettings)); - } - if (!skippedClasses.isEmpty()) { - result = Flowable.concat(result, searchInSkippedClasses(searchSettings)); - } - } - if (options.contains(RESOURCE)) { - result = Flowable.concat(result, resIndex.search(searchSettings)); - } - return result; - } - - public Flowable searchInSkippedClasses(final SearchSettings searchSettings) { - return Flowable.create(emitter -> { - LOG.debug("Skipped code search started: {} ...", searchSettings.getSearchString()); - for (JavaClass javaClass : skippedClasses) { - String code = javaClass.getCode(); - int pos = 0; - while (pos != -1) { - searchSettings.setStartPos(pos); - pos = searchNext(emitter, javaClass, code, searchSettings); - if (emitter.isCancelled()) { - LOG.debug("Skipped Code search canceled: {}", searchSettings.getSearchString()); - return; - } - } - if (!UiUtils.isFreeMemoryAvailable()) { - LOG.warn("Skipped code search stopped due to memory limit: {}", UiUtils.memoryInfo()); - emitter.onComplete(); - return; - } - } - LOG.debug("Skipped code search complete: {}, memory usage: {}", searchSettings.getSearchString(), UiUtils.memoryInfo()); - emitter.onComplete(); - }, BackpressureStrategy.BUFFER); - } - - private int searchNext(FlowableEmitter emitter, JavaNode javaClass, String code, final SearchSettings searchSettings) { - int pos = searchSettings.find(code); - if (pos == -1) { - return -1; - } - int lineStart = 1 + code.lastIndexOf(ICodeWriter.NL, pos); - int lineEnd = code.indexOf(ICodeWriter.NL, pos + searchSettings.getSearchString().length()); - StringRef line = StringRef.subString(code, lineStart, lineEnd == -1 ? code.length() : lineEnd); - emitter.onNext(new CodeNode(nodeCache.makeFrom(javaClass), line.trim(), -1, pos)); - return lineEnd; - } - - public void classCodeIndexSkipped(JavaClass cls) { - this.skippedClasses.add(cls); - } - - public int getSkippedCount() { - return skippedClasses.size(); - } -} diff --git a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties index af06bc14..d24b8d16 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties @@ -37,7 +37,6 @@ tree.loading=Laden… progress.load=Laden progress.decompile=Dekompilieren -progress.index=Indizieren #progress.canceling=Canceling error_dialog.title=Fehler @@ -94,9 +93,10 @@ search_dialog.field=Felder search_dialog.code=Code search_dialog.options=Suchoptionen: search_dialog.ignorecase=Groß/Kleinschreibung ignorieren -search_dialog.next_page=Nächste Seite anzeigen -search_dialog.prev_page=Vorherige Seite anzeigen -search_dialog.info_label=Ergebnisse %1$d bis %2$d von %3$d +#search_dialog.load_more=Load more +#search_dialog.load_all=Load all +#search_dialog.results_incomplete=Found %d+ +#search_dialog.results_complete=Found %d (complete) search_dialog.col_node=Knoten search_dialog.col_code=Code search_dialog.regex=Regex @@ -131,6 +131,7 @@ preferences.lineNumbersMode=Editor Zeilennummern-Modus preferences.check_for_updates=Nach Updates beim Start suchen #preferences.useDx=Use dx/d8 to convert java bytecode #preferences.decompilationMode=Decompilation mode +#preferences.codeCacheMode=Code cache mode preferences.showInconsistentCode=Inkonsistenten Code anzeigen preferences.escapeUnicode=Unicodezeichen escapen preferences.replaceConsts=Konstanten ersetzen @@ -182,7 +183,6 @@ msg.open_file=Bitte Datei öffnen msg.saving_sources=Quelltexte speichern msg.language_changed_title=Sprache speichern msg.language_changed=Die neue Sprache wird beim nächsten Start der Anwendung angezeigt. -msg.index_not_initialized=Index nicht initialisiert, Suche wird deaktiviert! msg.project_error_title=Fehler msg.project_error=Projekt konnte nicht geladen werden msg.cmd_select_class_error=Klasse\n%s auswählen nicht möglich\nSie existiert nicht. diff --git a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties index 47115fd8..d4cbcf83 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties @@ -37,7 +37,6 @@ tree.loading=Loading... progress.load=Loading progress.decompile=Decompiling -progress.index=Indexing progress.canceling=Canceling error_dialog.title=Error @@ -94,9 +93,10 @@ search_dialog.field=Field search_dialog.code=Code search_dialog.options=Search options: search_dialog.ignorecase=Case insensitive -search_dialog.next_page=Show next page -search_dialog.prev_page=Show previous page -search_dialog.info_label=Showing results %1$d to %2$d of %3$d +search_dialog.load_more=Load more +search_dialog.load_all=Load all +search_dialog.results_incomplete=Found %d+ +search_dialog.results_complete=Found %d (complete) search_dialog.col_node=Node search_dialog.col_code=Code search_dialog.regex=Regex @@ -104,7 +104,7 @@ search_dialog.active_tab=Active tab only search_dialog.comments=Comments search_dialog.resource=Resource search_dialog.keep_open=Keep open -search_dialog.tip_searching=Searching ... +search_dialog.tip_searching=Searching usage_dialog.title=Usage search usage_dialog.label=Usage for: @@ -131,6 +131,7 @@ preferences.lineNumbersMode=Editor line numbers mode preferences.check_for_updates=Check for updates on startup preferences.useDx=Use dx/d8 to convert java bytecode preferences.decompilationMode=Decompilation mode +preferences.codeCacheMode=Code cache mode preferences.showInconsistentCode=Show inconsistent code preferences.escapeUnicode=Escape unicode preferences.replaceConsts=Replace constants @@ -182,7 +183,6 @@ msg.open_file=Please open file msg.saving_sources=Saving sources msg.language_changed_title=Language changed msg.language_changed=New language will be displayed the next time application starts. -msg.index_not_initialized=Index not initialized, search will be disabled! msg.project_error_title=Error msg.project_error=Project could not be loaded msg.cmd_select_class_error=Failed to select the class\n%s\nThe class does not exist. diff --git a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties index 4abecd8f..3428f476 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties @@ -37,7 +37,6 @@ tree.loading=Cargando... progress.load=Cargando progress.decompile=Decompiling -#progress.index=Indexing #progress.canceling=Canceling #error_dialog.title= @@ -94,9 +93,10 @@ search_dialog.field=Campo search_dialog.code=Código search_dialog.options=Opciones de búsqueda: search_dialog.ignorecase=Ignorar minúsculas/mayúsculas -search_dialog.next_page=Mostrar página siguiente -search_dialog.prev_page=Mostrar página anterior -search_dialog.info_label=Mostrando resultados %1$d a %2$d de %3$d +#search_dialog.load_more=Load more +#search_dialog.load_all=Load all +#search_dialog.results_incomplete=Found %d+ +#search_dialog.results_complete=Found %d (complete) search_dialog.col_node=Nodo search_dialog.col_code=Código search_dialog.regex=Regex @@ -131,6 +131,7 @@ preferences.language=Idioma preferences.check_for_updates=Buscar actualizaciones al iniciar #preferences.useDx=Use dx/d8 to convert java bytecode #preferences.decompilationMode=Decompilation mode +#preferences.codeCacheMode=Code cache mode preferences.showInconsistentCode=Mostrar código inconsistente preferences.escapeUnicode=Escape unicode preferences.replaceConsts=Reemplazar constantes @@ -182,7 +183,6 @@ msg.open_file=Por favor, abra un archivo msg.saving_sources=Guardando fuente msg.language_changed_title=Idioma cambiado msg.language_changed=El nuevo idioma se mostrará la próxima vez que la aplicación se inicie. -msg.index_not_initialized=Índice no inicializado, ¡la bósqueda se desactivará! #msg.project_error_title= #msg.project_error= #msg.cmd_select_class_error= diff --git a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties index 82ee2e49..0e678b3f 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties @@ -37,7 +37,6 @@ tree.loading=로딩중... progress.load=로딩중 progress.decompile=디컴파일 중 -progress.index=인덱싱 중 #progress.canceling=Canceling error_dialog.title=오류 @@ -94,9 +93,10 @@ search_dialog.field=필드 search_dialog.code=코드 search_dialog.options=옵션 검색: search_dialog.ignorecase=대소문자 구분 안함 -search_dialog.next_page=다음 페이지 보기 -search_dialog.prev_page=이전 페이지 보기 -search_dialog.info_label=%3$d 중 %1$d-%2$d 결과 표시 +#search_dialog.load_more=Load more +#search_dialog.load_all=Load all +#search_dialog.results_incomplete=Found %d+ +#search_dialog.results_complete=Found %d (complete) search_dialog.col_node=노드 search_dialog.col_code=코드 search_dialog.regex=정규식 @@ -131,6 +131,7 @@ preferences.lineNumbersMode=편집기 줄 번호 모드 preferences.check_for_updates=시작시 업데이트 확인 #preferences.useDx=Use dx/d8 to convert java bytecode #preferences.decompilationMode=Decompilation mode +#preferences.codeCacheMode=Code cache mode preferences.showInconsistentCode=디컴파일 안된 코드 표시 preferences.escapeUnicode=유니코드 이스케이프 preferences.replaceConsts=상수 바꾸기 @@ -182,7 +183,6 @@ msg.open_file=파일을 여십시오 msg.saving_sources=소스 저장 중 msg.language_changed_title=언어 변경됨 msg.language_changed=다음에 응용 프로그램이 시작되면 새 언어가 표시됩니다. -msg.index_not_initialized=인덱스가 초기화되지 않았습니다. 검색이 비활성화됩니다! msg.project_error_title=오류 msg.project_error=프로젝트를 로드 할 수 없습니다. msg.cmd_select_class_error=클래스를 선택하지 못했습니다.\n%s\n클래스가 없습니다. diff --git a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties index 97fe96f9..e60cca3d 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties @@ -37,7 +37,6 @@ tree.loading=加载中... progress.load=正在加载 progress.decompile=反编译中 -progress.index=索引中 #progress.canceling=Canceling error_dialog.title=错误 @@ -94,9 +93,10 @@ search_dialog.field=字段名 search_dialog.code=代码 search_dialog.options=搜索选项: search_dialog.ignorecase=忽略大小写 -search_dialog.next_page=下一页 -search_dialog.prev_page=上一页 -search_dialog.info_label=显示了 %3$d 个结果中的第 %1$d 至第 %2$d 个 +#search_dialog.load_more=Load more +#search_dialog.load_all=Load all +#search_dialog.results_incomplete=Found %d+ +#search_dialog.results_complete=Found %d (complete) search_dialog.col_node=节点 search_dialog.col_code=代码 search_dialog.regex=正则表达式 @@ -131,6 +131,7 @@ preferences.lineNumbersMode=编辑器行号模式 preferences.check_for_updates=启动时检查更新 #preferences.useDx=Use dx/d8 to convert java bytecode #preferences.decompilationMode=Decompilation mode +#preferences.codeCacheMode=Code cache mode preferences.showInconsistentCode=显示不一致的代码 preferences.escapeUnicode=将 Unicode 字符转义 preferences.replaceConsts=替换常量 @@ -182,7 +183,6 @@ msg.open_file=请打开文件 msg.saving_sources=正在导出源代码 msg.language_changed_title=语言已更改 msg.language_changed=在下次启动时将会显示新的语言。 -msg.index_not_initialized=索引尚未初始化,无法进行搜索! msg.project_error_title=错误 msg.project_error=项目无法加载 msg.cmd_select_class_error=无法选择类\n%s\n该类不存在。 diff --git a/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties b/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties index 9e1a38a3..6b7bb52c 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties @@ -37,7 +37,6 @@ tree.loading=載入中... progress.load=載入中 progress.decompile=正在反編譯 -progress.index=正在索引 progress.canceling=正在取消 error_dialog.title=錯誤 @@ -94,9 +93,10 @@ search_dialog.field=欄位 search_dialog.code=程式碼 search_dialog.options=搜尋選項: search_dialog.ignorecase=不區分大小寫 -search_dialog.next_page=顯示下一頁 -search_dialog.prev_page=顯示上一頁 -search_dialog.info_label=顯示搜尋結果 %1$d 至 %2$d (共 %3$d) +#search_dialog.load_more=Load more +#search_dialog.load_all=Load all +#search_dialog.results_incomplete=Found %d+ +#search_dialog.results_complete=Found %d (complete) search_dialog.col_node=無 search_dialog.col_code=程式碼 search_dialog.regex=Regex @@ -131,6 +131,7 @@ preferences.lineNumbersMode=編輯器行號模式 preferences.check_for_updates=啟動時檢查更新 preferences.useDx=使用 dx/d8 來轉換 Java 位元組碼 preferences.decompilationMode=反編譯模式 +#preferences.codeCacheMode=Code cache mode preferences.showInconsistentCode=顯示不一致的程式碼 preferences.escapeUnicode=Unicode 逸出 preferences.replaceConsts=替換常數 @@ -182,7 +183,6 @@ msg.open_file=請開啟檔案 msg.saving_sources=正在儲存原始碼 msg.language_changed_title=已更改語言 msg.language_changed=新語言將於下次應用程式啟動時套用。 -msg.index_not_initialized=索引尚未初始化,搜尋將被停用! msg.project_error_title=錯誤 msg.project_error=無法載入專案 msg.cmd_select_class_error=無法選擇類別\n%s\n類別不存在。 diff --git a/jadx-gui/src/test/java/jadx/gui/utils/JumpManagerTest.java b/jadx-gui/src/test/java/jadx/gui/utils/JumpManagerTest.java index ce53e054..02b90806 100644 --- a/jadx-gui/src/test/java/jadx/gui/utils/JumpManagerTest.java +++ b/jadx-gui/src/test/java/jadx/gui/utils/JumpManagerTest.java @@ -118,6 +118,6 @@ class JumpManagerTest { } private JumpPosition makeJumpPos() { - return new JumpPosition(new TextNode(""), 0, 0); + return new JumpPosition(new TextNode(""), 0); } } diff --git a/jadx-gui/src/test/java/jadx/gui/utils/codecache/DiskCodeCacheTest.java b/jadx-gui/src/test/java/jadx/gui/utils/codecache/DiskCodeCacheTest.java new file mode 100644 index 00000000..a41af1d6 --- /dev/null +++ b/jadx-gui/src/test/java/jadx/gui/utils/codecache/DiskCodeCacheTest.java @@ -0,0 +1,47 @@ +package jadx.gui.utils.codecache; + +import java.io.IOException; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.api.ICodeInfo; +import jadx.api.impl.NoOpCodeCache; +import jadx.core.dex.nodes.ClassNode; +import jadx.gui.utils.codecache.disk.DiskCodeCache; +import jadx.tests.api.IntegrationTest; + +import static org.assertj.core.api.Assertions.assertThat; + +class DiskCodeCacheTest extends IntegrationTest { + private static final Logger LOG = LoggerFactory.getLogger(DiskCodeCacheTest.class); + + @TempDir + public Path tempDir; + + @Test + public void test() throws IOException { + disableCompilation(); + getArgs().setCodeCache(NoOpCodeCache.INSTANCE); + ClassNode clsNode = getClassNode(DiskCodeCacheTest.class); + ICodeInfo codeInfo = clsNode.getCode(); + + DiskCodeCache cache = new DiskCodeCache(clsNode.root(), tempDir); + + String clsKey = clsNode.getFullName(); + cache.add(clsKey, codeInfo); + + ICodeInfo readCodeInfo = cache.get(clsKey); + + assertThat(readCodeInfo).isNotNull(); + assertThat(readCodeInfo.getCodeStr()).isEqualTo(codeInfo.getCodeStr()); + assertThat(readCodeInfo.getCodeMetadata().getLineMapping()).isEqualTo(codeInfo.getCodeMetadata().getLineMapping()); + LOG.info("Disk code annotations: {}", readCodeInfo.getCodeMetadata().getAsMap()); + assertThat(readCodeInfo.getCodeMetadata().getAsMap()).hasSameSizeAs(codeInfo.getCodeMetadata().getAsMap()); + + cache.close(); + } +} diff --git a/jadx-gui/src/test/java/jadx/gui/utils/search/StringRefTest.java b/jadx-gui/src/test/java/jadx/gui/utils/search/StringRefTest.java deleted file mode 100644 index b6dd624d..00000000 --- a/jadx-gui/src/test/java/jadx/gui/utils/search/StringRefTest.java +++ /dev/null @@ -1,71 +0,0 @@ -package jadx.gui.utils.search; - -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import org.junit.jupiter.api.Test; - -import static jadx.gui.utils.search.StringRef.fromStr; -import static jadx.gui.utils.search.StringRef.subString; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; - -class StringRefTest { - - @Test - public void testConvert() { - assertThat(fromStr("a").toString(), is("a")); - } - - @Test - public void testSubstring() { - checkStr(subString("a", 0), "a"); - checkStr(subString("a", 1), ""); - checkStr(subString("a", 0, 0), ""); - checkStr(subString("a", 0, 1), "a"); - checkStr(subString("abc", 1, 2), "b"); - checkStr(subString("abc", 2), "c"); - checkStr(subString("abc", 2, 3), "c"); - } - - public static void checkStr(StringRef ref, String str) { - assertThat(ref.toString(), is(str)); - assertThat(ref, is(fromStr(str))); - } - - @Test - public void testTrim() { - checkTrim(fromStr("a"), "a"); - checkTrim(fromStr(" a "), "a"); - checkTrim(fromStr("\ta"), "a"); - checkTrim(subString("a b c", 1), "b c"); - checkTrim(subString("a b\tc", 1, 4), "b"); - checkTrim(subString("a b\tc", 2, 3), "b"); - } - - private static void checkTrim(StringRef ref, String result) { - assertThat(ref.trim().toString(), is(result)); - } - - @Test - public void testSplit() { - checkSplit("abc", "b", "a", "c"); - checkSplit("abc", "a", "", "bc"); - checkSplit("abc", "c", "ab"); - checkSplit("abc", "d", "abc"); - checkSplit("abbbc", "b", "a", "", "", "c"); - checkSplit("abbbc", "bb", "a", "bc"); - checkSplit("abbbc", "bbb", "a", "c"); - checkSplit("abbbc", "bbc", "ab"); - checkSplit("abbbc", "bbbc", "a"); - } - - private static void checkSplit(String str, String splitBy, String... result) { - List expectedStringRegList = Stream.of(result).map(StringRef::fromStr).collect(Collectors.toList()); - assertThat(StringRef.split(str, splitBy), is(expectedStringRegList)); - - // compare with original split - assertThat(str.split(splitBy), is(result)); - } -} diff --git a/jadx-gui/src/test/resources/logback-test.xml b/jadx-gui/src/test/resources/logback-test.xml new file mode 100644 index 00000000..3fcc38c0 --- /dev/null +++ b/jadx-gui/src/test/resources/logback-test.xml @@ -0,0 +1,13 @@ + + + + + %d{HH:mm:ss} %-5level - %msg%n + + + + + + + +