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
This commit is contained in:
skylot 2022-05-18 17:19:31 +03:00 committed by GitHub
parent 65ade379a6
commit 0606c90f22
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
166 changed files with 4408 additions and 2917 deletions

3
.gitignore vendored
View File

@ -35,4 +35,5 @@ jadx-output/
*.orig
quark.json
cliff.toml
cliff.toml
jadx-gui/src/main/resources/logback.xml

View File

@ -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();
}
}

View File

@ -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);
}

View File

@ -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<Integer, Integer> getLineMapping();
ICodeMetadata getCodeMetadata();
Map<CodePosition, Object> getAnnotations();
boolean hasMetadata();
}

View File

@ -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<CodePosition, Object> getRawAnnotations();
@ApiStatus.Internal
Map<Integer, ICodeAnnotation> getRawAnnotations();
}

View File

@ -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

View File

@ -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<ClassNode> classNodeList = root.getClasses(false);
List<ClassNode> classNodeList = root.getClasses();
List<JavaClass> 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<JavaClass> getClassesWithInners() {
return Utils.collectionMap(root.getClasses(), this::convertClassNode);
}
public List<ResourceFile> 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<JavaNode> 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<JavaNode> convertNodes(Collection<? extends ICodeNodeRef> 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<CodePosition, Object> 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() {

View File

@ -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<JavaClass> 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<JavaField> 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<CodePosition, Object> 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<CodePosition, JavaNode> getUsageMap() {
Map<CodePosition, Object> map = getCodeAnnotations();
public Map<Integer, JavaNode> getUsageMap() {
Map<Integer, ICodeAnnotation> map = getCodeInfo().getCodeMetadata().getAsMap();
if (map.isEmpty() || decompiler == null) {
return Collections.emptyMap();
}
Map<CodePosition, JavaNode> resultMap = new HashMap<>(map.size());
for (Map.Entry<CodePosition, Object> entry : map.entrySet()) {
CodePosition codePosition = entry.getKey();
Object obj = entry.getValue();
JavaNode node = getRootDecompiler().convertNode(obj);
if (node != null) {
resultMap.put(codePosition, node);
Map<Integer, JavaNode> resultMap = new HashMap<>(map.size());
for (Map.Entry<Integer, ICodeAnnotation> 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<CodePosition> getUsageFor(JavaNode javaNode) {
Map<CodePosition, Object> map = getCodeAnnotations();
public List<Integer> getUsePlacesFor(ICodeInfo codeInfo, JavaNode javaNode) {
Map<Integer, ICodeAnnotation> map = codeInfo.getCodeMetadata().getAsMap();
if (map.isEmpty() || decompiler == null) {
return Collections.emptyList();
}
Object internalNode = getRootDecompiler().getInternalNode(javaNode);
List<CodePosition> result = new ArrayList<>();
for (Map.Entry<CodePosition, Object> entry : map.entrySet()) {
CodePosition codePosition = entry.getKey();
Object obj = entry.getValue();
if (internalNode.equals(obj)) {
result.add(codePosition);
JadxDecompiler rootDec = getRootDecompiler();
List<Integer> result = new ArrayList<>();
for (Map.Entry<Integer, ICodeAnnotation> 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<JavaClass> getInnerClasses() {
loadLists();
load();
return innerClasses;
}
public List<JavaClass> getInlinedClasses() {
loadLists();
load();
return inlinedClasses;
}
public List<JavaField> getFields() {
loadLists();
load();
return fields;
}
public List<JavaMethod> 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();

View File

@ -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();

View File

@ -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();

View File

@ -12,8 +12,6 @@ public interface JavaNode {
JavaClass getTopParentClass();
int getDecompiledLine();
int getDefPos();
List<JavaNode> getUseIn();

View File

@ -39,11 +39,6 @@ public final class JavaPackage implements JavaNode, Comparable<JavaPackage> {
return null;
}
@Override
public int getDecompiledLine() {
return 0;
}
@Override
public int getDefPos() {
return 0;

View File

@ -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);
}
}

View File

@ -1,5 +0,0 @@
package jadx.api.data.annotations;
public interface ICodeRawOffset {
int getOffset();
}

View File

@ -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() + '}';
}
}

View File

@ -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 + '}';
}
}

View File

@ -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) {

View File

@ -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<Integer, Integer> lineMapping;
private final Map<CodePosition, Object> annotations;
private final ICodeMetadata metadata;
public AnnotatedCodeInfo(ICodeInfo codeInfo) {
this(codeInfo.getCodeStr(), codeInfo.getLineMapping(), codeInfo.getAnnotations());
}
public AnnotatedCodeInfo(String code, Map<Integer, Integer> lineMapping, Map<CodePosition, Object> annotations) {
public AnnotatedCodeInfo(String code, Map<Integer, Integer> lineMapping, Map<Integer, ICodeAnnotation> 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<Integer, Integer> getLineMapping() {
return lineMapping;
public ICodeMetadata getCodeMetadata() {
return metadata;
}
@Override
public Map<CodePosition, Object> getAnnotations() {
return annotations;
public boolean hasMetadata() {
return metadata != ICodeMetadata.EMPTY;
}
@Override

View File

@ -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<CodePosition, Object> annotations = Collections.emptyMap();
private Map<Integer, ICodeAnnotation> annotations = Collections.emptyMap();
private Map<Integer, Integer> 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<CodePosition, Object> 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<Integer, ICodeAnnotation> entry : code.annotations.entrySet()) {
int pos = entry.getKey();
int newPos = startPos + pos;
attachAnnotation(entry.getValue(), newPos);
}
for (Map.Entry<Integer, Integer> 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<CodePosition, Object> getRawAnnotations() {
public Map<Integer, ICodeAnnotation> 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;
});
}
}

View File

@ -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();
}
}

View File

@ -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

View File

@ -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";

View File

@ -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<Integer, Integer> getLineMapping() {
return Collections.emptyMap();
public ICodeMetadata getCodeMetadata() {
return ICodeMetadata.EMPTY;
}
@Override
public Map<CodePosition, Object> getAnnotations() {
return Collections.emptyMap();
public boolean hasMetadata() {
return false;
}
@Override

View File

@ -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<CodePosition, Object> getRawAnnotations() {
public Map<Integer, ICodeAnnotation> getRawAnnotations() {
return Collections.emptyMap();
}

View File

@ -0,0 +1,16 @@
package jadx.api.metadata;
public interface ICodeAnnotation {
enum AnnType {
CLASS,
FIELD,
METHOD,
VAR,
VAR_REF,
DECLARATION,
OFFSET
}
AnnType getAnnType();
}

View File

@ -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> T searchUp(int startPos, BiFunction<Integer, ICodeAnnotation, T> visitor);
/**
* Iterate code annotations from {@code startPos} to higher positions.
*
* @param visitor
* return not null value to stop iterations
*/
@Nullable
<T> T searchDown(int startPos, BiFunction<Integer, ICodeAnnotation, T> 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<Integer, ICodeAnnotation> getAsMap();
Map<Integer, Integer> getLineMapping();
}

View File

@ -0,0 +1,7 @@
package jadx.api.metadata;
public interface ICodeNodeRef extends ICodeAnnotation {
int getDefPosition();
void setDefPosition(int pos);
}

View File

@ -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;

View File

@ -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 + '}';
}
}

View File

@ -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 + '}';
}
}

View File

@ -0,0 +1,68 @@
package jadx.api.metadata.annotations;
import jadx.api.metadata.ICodeAnnotation;
/**
* Variable reference by position of VarNode in code metadata.
* <br>
* Because on creation position not yet known,
* VarRef created using VarNode as a source of ref pos during serialization.
* <br>
* 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() + '}';
}
}

View File

@ -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<Integer, Integer> lines, Map<Integer, ICodeAnnotation> map) {
if (map.isEmpty() && lines.isEmpty()) {
return ICodeMetadata.EMPTY;
}
Comparator<Integer> reverseCmp = Comparator.comparingInt(Integer::intValue).reversed();
NavigableMap<Integer, ICodeAnnotation> 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<Integer, Integer> lines;
private final NavigableMap<Integer, ICodeAnnotation> navMap;
private CodeMetadataStorage(Map<Integer, Integer> lines, NavigableMap<Integer, ICodeAnnotation> 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<Integer, ICodeAnnotation> 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 <T> @Nullable T searchUp(int startPos, BiFunction<Integer, ICodeAnnotation, T> visitor) {
for (Map.Entry<Integer, ICodeAnnotation> entry : navMap.tailMap(startPos, true).entrySet()) {
T value = visitor.apply(entry.getKey(), entry.getValue());
if (value != null) {
return value;
}
}
return null;
}
@Override
public <T> @Nullable T searchDown(int startPos, BiFunction<Integer, ICodeAnnotation, T> visitor) {
NavigableMap<Integer, ICodeAnnotation> map = navMap.headMap(startPos, true).descendingMap();
for (Map.Entry<Integer, ICodeAnnotation> 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<ICodeNodeRef> 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<Integer, ICodeAnnotation> getAsMap() {
return navMap;
}
@Override
public Map<Integer, Integer> getLineMapping() {
return lines;
}
@Override
public String toString() {
return "CodeMetadata{lines=" + lines
+ ", annotations=\n" + Utils.listToString(navMap.entrySet(), "\n") + "\n}";
}
}

View File

@ -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++;
}
}
}

View File

@ -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();

View File

@ -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);

View File

@ -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 {

View File

@ -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<Integer, Integer> lineMapping = code.getLineMapping();
Map<CodePosition, Object> annotations = code.getAnnotations();
Map<Integer, Integer> lineMapping = code.getCodeMetadata().getLineMapping();
ICodeMetadata metadata = code.getCodeMetadata();
long mthCodeOffset = mth.getMethodCodeOffset() + 16;
int linesCount = lines.length;
List<JsonCodeLine> 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;
}

View File

@ -5,10 +5,6 @@ public interface ILineAttributeNode {
void setSourceLine(int sourceLine);
int getDecompiledLine();
void setDecompiledLine(int line);
int getDefPosition();
void setDefPosition(int pos);

View File

@ -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());
}
}

View File

@ -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;
}
/**

View File

@ -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();

View File

@ -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();

View File

@ -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);

View File

@ -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();

View File

@ -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<String> 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);

View File

@ -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<Path> 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<Path> toPaths(List<File> files) {
return files.stream().map(File::toPath).collect(Collectors.toList());
}
public static List<Path> toPaths(File[] files) {
return Stream.of(files).map(File::toPath).collect(Collectors.toList());
}
public static List<Path> fileNamesToPaths(List<String> fileNames) {
return fileNames.stream().map(Paths::get).collect(Collectors.toList());
}
public static List<File> toFiles(List<Path> 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);
}
}
}

View File

@ -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<Integer, Integer> lineMapping = code.getLineMapping();
Map<Integer, Integer> 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<CodePosition, Object> 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;
}
}

View File

@ -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<JadxClassNodeA
int codePos = code.getCodeStr().indexOf(refStr);
assertThat(codePos).describedAs("String '%s' not found", refStr).isNotEqualTo(-1);
int refPos = codePos + refOffset;
for (Map.Entry<CodePosition, Object> entry : code.getAnnotations().entrySet()) {
if (entry.getKey().getPos() == refPos) {
for (Map.Entry<Integer, ICodeAnnotation> entry : code.getCodeMetadata().getAsMap().entrySet()) {
if (entry.getKey() == refPos) {
Assertions.assertThat(entry.getValue()).isEqualTo(node);
return;
}

View File

@ -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<JadxCodeInfoAss
}
public JadxCodeInfoAssertions checkCodeOffsets() {
long dupOffsetCount = actual.getAnnotations().values().stream()
.filter(ICodeRawOffset.class::isInstance)
.collect(Collectors.groupingBy(o -> ((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();

View File

@ -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<Integer, MethodNode> 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<Integer, MethodNode> getMethodsMap(ClassNode classNode) {
Map<Integer, MethodNode> 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) {

View File

@ -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));
}
}

View File

@ -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 {

View File

@ -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}");
}
}

View File

@ -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);
}

View File

@ -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<String> list, String str) {
list.removeIf(s -> s.equals(str));
}
public void check() {
List<String> 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());
}
}

View File

@ -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<Integer> 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);
}
}

View File

@ -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<String> 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<String> 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);
}
}

View File

@ -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 {

View File

@ -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<JavaClass> getIncludedClassesWithInners() {
List<JavaClass> classes = decompiler.getClassesWithInners();
List<String> excludedPackages = getExcludedPackages();
if (excludedPackages.isEmpty()) {
return classes;
}
return classes.stream()
.filter(cls -> isClassIncluded(excludedPackages, cls))
.collect(Collectors.toList());
}
private static boolean isClassIncluded(List<String> 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<List<JavaClass>> buildDecompileBatches(List<JavaClass> 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()

View File

@ -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<Integer, Integer> getLineMapping() {
return Collections.emptyMap();
}
@Override
public Map<CodePosition, Object> getAnnotations() {
return Collections.emptyMap();
}
};
return new SimpleCodeInfo(buf.toString());
}
}

View File

@ -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<Long, IBackgroundTask> 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<TaskStatus> execute(IBackgroundTask task) {
TaskWorker taskWorker = new TaskWorker(task);
public synchronized Future<TaskStatus> 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<TaskStatus> 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<TaskStatus, Void> 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<Runnable> jobs = task.scheduleJobs();
List<? extends Runnable> 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<ITaskProgress> 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<TaskStatus> 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<Runnable> jobs;
private final Consumer<TaskStatus> onFinish;
public SimpleTask(String title, List<Runnable> jobs, @Nullable Consumer<TaskStatus> onFinish) {
this.title = title;
this.jobs = jobs;
this.onFinish = onFinish;
}
@Override
public String getTitle() {
return title;
}
@Override
public List<Runnable> scheduleJobs() {
return jobs;
}
@Override
public void onFinish(ITaskInfo taskInfo) {
if (onFinish != null) {
onFinish.accept(taskInfo.getStatus());
}
}
@Override
public boolean checkMemoryUsage() {
return true;
}
}
}

View File

@ -0,0 +1,7 @@
package jadx.gui.jobs;
public interface Cancelable {
boolean isCanceled();
void cancel();
}

View File

@ -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;
}
}

View File

@ -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<Runnable> scheduleJobs() {
IndexService indexService = mainWindow.getCacheObject().getIndexService();
List<JavaClass> classes = wrapper.getIncludedClasses();
expectedCompleteCount = classes.size();
indexService.setComplete(false);
complete.set(0);
List<List<JavaClass>> 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<Runnable> jobs = new ArrayList<>(batches.size());
for (List<JavaClass> 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 }"

View File

@ -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;

View File

@ -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<Runnable> scheduleJobs();
/**
* Jobs to run in parallel
*/
List<? extends Runnable> 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<ITaskProgress> getOnProgressListener() {
return null;
}
}

View File

@ -0,0 +1,8 @@
package jadx.gui.jobs;
public interface ITaskProgress {
int progress();
int total();
}

View File

@ -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<JavaClass> 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<StringRef> 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<StringRef> splitLines(String code) {
List<StringRef> 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;
}
}

View File

@ -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<Runnable> scheduleJobs() {
IndexService indexService = mainWindow.getCacheObject().getIndexService();
List<JavaClass> classesForIndex = wrapper.getIncludedClasses()
.stream()
.filter(indexService::isIndexNeeded)
.collect(Collectors.toList());
expectedCompleteCount = classesForIndex.size();
indexService.setComplete(false);
complete.set(0);
List<Runnable> 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;
}
}

View File

@ -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<Runnable> jobs;
private final Consumer<TaskStatus> onFinish;
public SimpleTask(String title, List<Runnable> jobs) {
this(title, jobs, null);
}
public SimpleTask(String title, List<Runnable> jobs, @Nullable Consumer<TaskStatus> onFinish) {
this.title = title;
this.jobs = jobs;
this.onFinish = onFinish;
}
@Override
public String getTitle() {
return title;
}
@Override
public List<Runnable> 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() {
}
}

View File

@ -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;
}
}

View File

@ -7,5 +7,5 @@ public enum TaskStatus {
CANCEL_BY_USER,
CANCEL_BY_TIMEOUT,
CANCEL_BY_MEMORY,
ERROR
ERROR;
}

View File

@ -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("<pre>");
builder.escape(ExceptionUtils.getStackTrace(e));
builder.append("</pre>");
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;
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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<ITaskInfo> onFinish;
private final Consumer<JNode> results;
private final List<SearchJob> 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<TaskStatus> future;
private Consumer<ITaskProgress> progressListener;
public SearchTask(MainWindow mainWindow, Consumer<JNode> results, Consumer<ITaskInfo> 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<? extends Runnable> 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<ITaskProgress> progressListener) {
this.progressListener = progressListener;
}
@Override
public @Nullable Consumer<ITaskProgress> getOnProgressListener() {
return this.progressListener;
}
}

View File

@ -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<JavaClass> classes;
public BaseSearchProvider(MainWindow mw, SearchSettings searchSettings, List<JavaClass> 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();
}
}

View File

@ -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<JavaClass> 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;
}
}

View File

@ -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<JavaClass> 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;
}
}

View File

@ -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<ICodeComment> 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<JNode> search(SearchSettings searchSettings) {
List<ICodeComment> 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<CodePosition, Object> 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();
}
}

View File

@ -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<JavaClass> 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<JavaField> 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;
}
}

View File

@ -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<ISearchProvider> 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;
}
}

View File

@ -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<JavaClass> 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<JavaMethod> 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;
}
}

View File

@ -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<JResource> resNodes = new ArrayList<>();
private final Set<String> extSet = new HashSet<>();
private final CacheObject cache;
private final SearchSettings searchSettings;
private final Set<String> extSet = new HashSet<>();
private List<JResource> 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<JResSearchNode> 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<JResSearchNode> 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();
}
}

View File

@ -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) {

View File

@ -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<CodeCacheMode> 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);

View File

@ -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("</h1><pre>");
builder.escape(ExceptionUtils.getStackTrace(e));
builder.append("</pre>");
return builder.toString();
return new SimpleCodeInfo(builder.toString());
}
return this.content;
}

View File

@ -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<CodeNode> {
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<CodeNode> {
@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<CodeNode> {
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<CodeNode> {
return jNode.hashCode();
}
public static final Comparator<CodeNode> 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);
}
}

View File

@ -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<JClass> {
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<JClass> {
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<JClass> {
}
@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<JClass> {
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<JClass> {
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);
}
}

View File

@ -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<JField> 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);
}
}

View File

@ -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<JMethod> 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);
}
}

View File

@ -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<JNode> {
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<JNode> 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();

View File

@ -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<JPackage> {
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<JPackage> {
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) {

View File

@ -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();

View File

@ -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<JResource> {
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<JResource> {
@Override
public void loadNode() {
getContent();
getCodeInfo();
update();
}
@ -107,12 +106,6 @@ public class JResource extends JLoadableNode implements Comparable<JResource> {
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<JResource> {
}
@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<JResource> {
}
@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

View File

@ -131,11 +131,6 @@ public class JRoot extends JNode {
return null;
}
@Override
public int getLine() {
return 0;
}
@Override
public String makeString() {
List<Path> paths = wrapper.getOpenPaths();

View File

@ -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;

Some files were not shown because too many files have changed in this diff Show More