diff --git a/WrapLayout.java b/WrapLayout.java
new file mode 100644
index 000000000..b87fe6c73
--- /dev/null
+++ b/WrapLayout.java
@@ -0,0 +1,190 @@
+import java.awt.*;
+import javax.swing.JScrollPane;
+import javax.swing.SwingUtilities;
+
+/**
+ * FlowLayout subclass that fully supports wrapping of components.
+ */
+public class WrapLayout extends FlowLayout
+{
+ private Dimension preferredLayoutSize;
+
+ /**
+ * Constructs a new WrapLayout with a left
+ * alignment and a default 5-unit horizontal and vertical gap.
+ */
+ public WrapLayout()
+ {
+ super();
+ }
+
+ /**
+ * Constructs a new FlowLayout with the specified
+ * alignment and a default 5-unit horizontal and vertical gap.
+ * The value of the alignment argument must be one of
+ * WrapLayout, WrapLayout,
+ * or WrapLayout.
+ * @param align the alignment value
+ */
+ public WrapLayout(int align)
+ {
+ super(align);
+ }
+
+ /**
+ * Creates a new flow layout manager with the indicated alignment
+ * and the indicated horizontal and vertical gaps.
+ *
+ * The value of the alignment argument must be one of
+ * WrapLayout, WrapLayout,
+ * or WrapLayout.
+ * @param align the alignment value
+ * @param hgap the horizontal gap between components
+ * @param vgap the vertical gap between components
+ */
+ public WrapLayout(int align, int hgap, int vgap)
+ {
+ super(align, hgap, vgap);
+ }
+
+ /**
+ * Returns the preferred dimensions for this layout given the
+ * visible components in the specified target container.
+ * @param target the component which needs to be laid out
+ * @return the preferred dimensions to lay out the
+ * subcomponents of the specified container
+ */
+ @Override
+ public Dimension preferredLayoutSize(Container target)
+ {
+ return layoutSize(target, true);
+ }
+
+ /**
+ * Returns the minimum dimensions needed to layout the visible
+ * components contained in the specified target container.
+ * @param target the component which needs to be laid out
+ * @return the minimum dimensions to lay out the
+ * subcomponents of the specified container
+ */
+ @Override
+ public Dimension minimumLayoutSize(Container target)
+ {
+ Dimension minimum = layoutSize(target, false);
+ minimum.width -= (getHgap() + 1);
+ return minimum;
+ }
+
+ /**
+ * Returns the minimum or preferred dimension needed to layout the target
+ * container.
+ *
+ * @param target target to get layout size for
+ * @param preferred should preferred size be calculated
+ * @return the dimension to layout the target container
+ */
+ private Dimension layoutSize(Container target, boolean preferred)
+ {
+ synchronized (target.getTreeLock())
+ {
+ // Each row must fit with the width allocated to the containter.
+ // When the container width = 0, the preferred width of the container
+ // has not yet been calculated so lets ask for the maximum.
+
+ int targetWidth = target.getSize().width;
+ Container container = target;
+
+ while (container.getSize().width == 0 && container.getParent() != null)
+ {
+ container = container.getParent();
+ }
+
+ targetWidth = container.getSize().width;
+
+ if (targetWidth == 0)
+ targetWidth = Integer.MAX_VALUE;
+
+ int hgap = getHgap();
+ int vgap = getVgap();
+ Insets insets = target.getInsets();
+ int horizontalInsetsAndGap = insets.left + insets.right + (hgap * 2);
+ int maxWidth = targetWidth - horizontalInsetsAndGap;
+
+ // Fit components into the allowed width
+
+ Dimension dim = new Dimension(0, 0);
+ int rowWidth = 0;
+ int rowHeight = 0;
+
+ int nmembers = target.getComponentCount();
+
+ for (int i = 0; i < nmembers; i++)
+ {
+ Component m = target.getComponent(i);
+
+ if (m.isVisible())
+ {
+ Dimension d = preferred ? m.getPreferredSize() : m.getMinimumSize();
+
+ // Can't add the component to current row. Start a new row.
+
+ if (rowWidth + d.width > maxWidth)
+ {
+ addRow(dim, rowWidth, rowHeight);
+ rowWidth = 0;
+ rowHeight = 0;
+ }
+
+ // Add a horizontal gap for all components after the first
+
+ if (rowWidth != 0)
+ {
+ rowWidth += hgap;
+ }
+
+ rowWidth += d.width;
+ rowHeight = Math.max(rowHeight, d.height);
+ }
+ }
+
+ addRow(dim, rowWidth, rowHeight);
+
+ dim.width += horizontalInsetsAndGap;
+ dim.height += insets.top + insets.bottom + vgap * 2;
+
+ // When using a scroll pane or the DecoratedLookAndFeel we need to
+ // make sure the preferred size is less than the size of the
+ // target containter so shrinking the container size works
+ // correctly. Removing the horizontal gap is an easy way to do this.
+
+ Container scrollPane = SwingUtilities.getAncestorOfClass(JScrollPane.class, target);
+
+ if (scrollPane != null && target.isValid())
+ {
+ dim.width -= (hgap + 1);
+ }
+
+ return dim;
+ }
+ }
+
+ /*
+ * A new row has been completed. Use the dimensions of this row
+ * to update the preferred size for the container.
+ *
+ * @param dim update the width and height when appropriate
+ * @param rowWidth the width of the row to add
+ * @param rowHeight the height of the row to add
+ */
+ private void addRow(Dimension dim, int rowWidth, int rowHeight)
+ {
+ dim.width = Math.max(dim.width, rowWidth);
+
+ if (dim.height > 0)
+ {
+ dim.height += getVgap();
+ }
+
+ dim.height += rowHeight;
+ }
+}
diff --git a/jadx-core/src/main/java/jadx/api/CodePosition.java b/jadx-core/src/main/java/jadx/api/CodePosition.java
index bd58e1b52..503f3c368 100644
--- a/jadx-core/src/main/java/jadx/api/CodePosition.java
+++ b/jadx-core/src/main/java/jadx/api/CodePosition.java
@@ -2,42 +2,27 @@ package jadx.api;
public final class CodePosition {
- private final JavaNode node;
private final int line;
private final int offset;
- private int usagePosition = -1;
+ private final int pos;
- public CodePosition(JavaNode node, int line, int offset) {
- this.node = node;
+ 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.node = null;
- this.line = line;
- this.offset = offset;
+ this(line, offset, -1);
}
- public int getUsagePosition() {
- return usagePosition;
- }
-
- public CodePosition setUsagePosition(int usagePosition) {
- this.usagePosition = usagePosition;
- return this;
- }
-
- public JavaNode getNode() {
- return node;
- }
-
- public JavaClass getJavaClass() {
- JavaClass parent = node.getDeclaringClass();
- if (parent == null && node instanceof JavaClass) {
- return (JavaClass) node;
- }
- return parent;
+ public int getPos() {
+ return pos;
}
public int getLine() {
@@ -72,8 +57,8 @@ public final class CodePosition {
if (offset != 0) {
sb.append(':').append(offset);
}
- if (node != null) {
- sb.append(' ').append(node);
+ if (pos > 0) {
+ sb.append('@').append(pos);
}
return sb.toString();
}
diff --git a/jadx-core/src/main/java/jadx/api/ICodeWriter.java b/jadx-core/src/main/java/jadx/api/ICodeWriter.java
index d46b5044e..2fae589f2 100644
--- a/jadx-core/src/main/java/jadx/api/ICodeWriter.java
+++ b/jadx-core/src/main/java/jadx/api/ICodeWriter.java
@@ -1,5 +1,7 @@
package jadx.api;
+import java.util.Map;
+
import jadx.core.dex.attributes.nodes.LineAttrNode;
public interface ICodeWriter {
@@ -51,4 +53,8 @@ public interface ICodeWriter {
String getCodeStr();
int getLength();
+
+ StringBuilder getRawBuf();
+
+ Map getRawAnnotations();
}
diff --git a/jadx-core/src/main/java/jadx/api/JadxArgs.java b/jadx-core/src/main/java/jadx/api/JadxArgs.java
index 4864bea76..a01f2da9c 100644
--- a/jadx-core/src/main/java/jadx/api/JadxArgs.java
+++ b/jadx-core/src/main/java/jadx/api/JadxArgs.java
@@ -9,6 +9,7 @@ import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
+import jadx.api.data.ICodeData;
import jadx.api.impl.AnnotatedCodeWriter;
import jadx.api.impl.InMemoryCodeCache;
@@ -78,6 +79,8 @@ public class JadxArgs {
private OutputFormatEnum outputFormat = OutputFormatEnum.JAVA;
+ private ICodeData codeData;
+
public JadxArgs() {
// use default options
}
@@ -384,6 +387,14 @@ public class JadxArgs {
this.codeWriterProvider = codeWriterProvider;
}
+ public ICodeData getCodeData() {
+ return codeData;
+ }
+
+ public void setCodeData(ICodeData codeData) {
+ this.codeData = codeData;
+ }
+
@Override
public String toString() {
return "JadxArgs{" + "inputFiles=" + inputFiles
diff --git a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java
index 0bc32ed4e..30b1d3a51 100644
--- a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java
+++ b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java
@@ -30,7 +30,11 @@ import jadx.api.plugins.input.data.ILoadResult;
import jadx.core.Jadx;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.nodes.LineAttrNode;
-import jadx.core.dex.nodes.*;
+import jadx.core.dex.nodes.ClassNode;
+import jadx.core.dex.nodes.FieldNode;
+import jadx.core.dex.nodes.MethodNode;
+import jadx.core.dex.nodes.RootNode;
+import jadx.core.dex.nodes.VariableNode;
import jadx.core.dex.visitors.SaveCode;
import jadx.core.export.ExportGradleProject;
import jadx.core.utils.Utils;
@@ -428,6 +432,15 @@ public final class JadxDecompiler implements Closeable {
throw new JadxRuntimeException("JavaField not found by FieldNode: " + fld);
}
+ @Nullable
+ public JavaClass searchJavaClassByOrigFullName(String fullName) {
+ return getRoot().getClasses().stream()
+ .filter(cls -> cls.getClassInfo().getFullName().equals(fullName))
+ .findFirst()
+ .map(this::getJavaClassByNode)
+ .orElse(null);
+ }
+
@Nullable
JavaNode convertNode(Object obj) {
if (!(obj instanceof LineAttrNode)) {
@@ -481,7 +494,7 @@ public final class JadxDecompiler implements Closeable {
if (defLine == 0) {
return null;
}
- return new CodePosition(jCls, defLine, 0);
+ return new CodePosition(defLine, 0, javaNode.getDefPos());
}
public JadxArgs getArgs() {
diff --git a/jadx-core/src/main/java/jadx/api/JavaClass.java b/jadx-core/src/main/java/jadx/api/JavaClass.java
index 40ad7b9f6..caa786006 100644
--- a/jadx-core/src/main/java/jadx/api/JavaClass.java
+++ b/jadx-core/src/main/java/jadx/api/JavaClass.java
@@ -131,7 +131,7 @@ public final class JavaClass implements JavaNode {
return decompiler;
}
- private Map getCodeAnnotations() {
+ public Map getCodeAnnotations() {
ICodeInfo code = getCodeInfo();
if (code == null) {
return Collections.emptyMap();
@@ -139,6 +139,10 @@ public final class JavaClass implements JavaNode {
return code.getAnnotations();
}
+ public Object getAnnotationAt(CodePosition pos) {
+ return getCodeAnnotations().get(pos);
+ }
+
public Map getUsageMap() {
Map map = getCodeAnnotations();
if (map.isEmpty() || decompiler == null) {
@@ -229,6 +233,11 @@ public final class JavaClass implements JavaNode {
return cls.getDecompiledLine();
}
+ @Override
+ public int getDefPos() {
+ return cls.getDefPosition();
+ }
+
@Override
public boolean equals(Object o) {
return this == o || o instanceof JavaClass && cls.equals(((JavaClass) o).cls);
diff --git a/jadx-core/src/main/java/jadx/api/JavaField.java b/jadx-core/src/main/java/jadx/api/JavaField.java
index e616f0f8d..5dd747048 100644
--- a/jadx-core/src/main/java/jadx/api/JavaField.java
+++ b/jadx-core/src/main/java/jadx/api/JavaField.java
@@ -49,6 +49,11 @@ public final class JavaField implements JavaNode {
return field.getDecompiledLine();
}
+ @Override
+ public int getDefPos() {
+ return field.getDefPosition();
+ }
+
@Override
public List getUseIn() {
return getDeclaringClass().getRootDecompiler().convertNodes(field.getUseIn());
diff --git a/jadx-core/src/main/java/jadx/api/JavaMethod.java b/jadx-core/src/main/java/jadx/api/JavaMethod.java
index d6bc3c73c..80110c845 100644
--- a/jadx-core/src/main/java/jadx/api/JavaMethod.java
+++ b/jadx-core/src/main/java/jadx/api/JavaMethod.java
@@ -85,6 +85,11 @@ public final class JavaMethod implements JavaNode {
return mth.getDecompiledLine();
}
+ @Override
+ public int getDefPos() {
+ return mth.getDefPosition();
+ }
+
/**
* Internal API. Not Stable!
*/
diff --git a/jadx-core/src/main/java/jadx/api/JavaNode.java b/jadx-core/src/main/java/jadx/api/JavaNode.java
index 1a34cc7cb..d8887214b 100644
--- a/jadx-core/src/main/java/jadx/api/JavaNode.java
+++ b/jadx-core/src/main/java/jadx/api/JavaNode.java
@@ -14,5 +14,7 @@ public interface JavaNode {
int getDecompiledLine();
+ int getDefPos();
+
List getUseIn();
}
diff --git a/jadx-core/src/main/java/jadx/api/JavaPackage.java b/jadx-core/src/main/java/jadx/api/JavaPackage.java
index 3f1799618..7ff63ffe9 100644
--- a/jadx-core/src/main/java/jadx/api/JavaPackage.java
+++ b/jadx-core/src/main/java/jadx/api/JavaPackage.java
@@ -44,6 +44,11 @@ public final class JavaPackage implements JavaNode, Comparable {
return 0;
}
+ @Override
+ public int getDefPos() {
+ return 0;
+ }
+
@Override
public List getUseIn() {
return Collections.emptyList();
diff --git a/jadx-core/src/main/java/jadx/api/JavaVariable.java b/jadx-core/src/main/java/jadx/api/JavaVariable.java
index 8ae723e1b..db99ed735 100644
--- a/jadx-core/src/main/java/jadx/api/JavaVariable.java
+++ b/jadx-core/src/main/java/jadx/api/JavaVariable.java
@@ -43,6 +43,11 @@ public class JavaVariable implements JavaNode {
return node.getDecompiledLine();
}
+ @Override
+ public int getDefPos() {
+ return node.getDefPosition();
+ }
+
@Override
public List getUseIn() {
return Collections.emptyList();
diff --git a/jadx-core/src/main/java/jadx/api/data/ICodeComment.java b/jadx-core/src/main/java/jadx/api/data/ICodeComment.java
new file mode 100644
index 000000000..a817f39e7
--- /dev/null
+++ b/jadx-core/src/main/java/jadx/api/data/ICodeComment.java
@@ -0,0 +1,22 @@
+package jadx.api.data;
+
+import org.jetbrains.annotations.Nullable;
+
+public interface ICodeComment extends Comparable {
+
+ IJavaNodeRef getNodeRef();
+
+ String getComment();
+
+ /**
+ * Instruction offset inside method
+ */
+ int getOffset();
+
+ enum AttachType {
+ VAR_DECLARE
+ }
+
+ @Nullable
+ AttachType getAttachType();
+}
diff --git a/jadx-core/src/main/java/jadx/api/data/ICodeData.java b/jadx-core/src/main/java/jadx/api/data/ICodeData.java
new file mode 100644
index 000000000..9dc193ae5
--- /dev/null
+++ b/jadx-core/src/main/java/jadx/api/data/ICodeData.java
@@ -0,0 +1,10 @@
+package jadx.api.data;
+
+import java.util.List;
+
+public interface ICodeData {
+
+ long getUpdateId();
+
+ List getComments();
+}
diff --git a/jadx-core/src/main/java/jadx/api/data/IJavaNodeRef.java b/jadx-core/src/main/java/jadx/api/data/IJavaNodeRef.java
new file mode 100644
index 000000000..f3c775bbb
--- /dev/null
+++ b/jadx-core/src/main/java/jadx/api/data/IJavaNodeRef.java
@@ -0,0 +1,14 @@
+package jadx.api.data;
+
+public interface IJavaNodeRef extends Comparable {
+
+ enum RefType {
+ CLASS, FIELD, METHOD
+ }
+
+ RefType getType();
+
+ String getDeclaringClass();
+
+ String getShortId();
+}
diff --git a/jadx-core/src/main/java/jadx/api/data/annotations/CustomOffsetRef.java b/jadx-core/src/main/java/jadx/api/data/annotations/CustomOffsetRef.java
new file mode 100644
index 000000000..239561db5
--- /dev/null
+++ b/jadx-core/src/main/java/jadx/api/data/annotations/CustomOffsetRef.java
@@ -0,0 +1,27 @@
+package jadx.api.data.annotations;
+
+import jadx.api.data.ICodeComment;
+
+public class CustomOffsetRef implements ICodeRawOffset {
+ private final int offset;
+ private final ICodeComment.AttachType attachType;
+
+ public CustomOffsetRef(int offset, ICodeComment.AttachType attachType) {
+ this.offset = offset;
+ this.attachType = attachType;
+ }
+
+ @Override
+ public int getOffset() {
+ return offset;
+ }
+
+ public ICodeComment.AttachType getAttachType() {
+ return attachType;
+ }
+
+ @Override
+ public String toString() {
+ return "CustomOffsetRef{offset=" + offset + ", attachType=" + attachType + '}';
+ }
+}
diff --git a/jadx-core/src/main/java/jadx/api/data/annotations/ICodeRawOffset.java b/jadx-core/src/main/java/jadx/api/data/annotations/ICodeRawOffset.java
new file mode 100644
index 000000000..9c2186d92
--- /dev/null
+++ b/jadx-core/src/main/java/jadx/api/data/annotations/ICodeRawOffset.java
@@ -0,0 +1,5 @@
+package jadx.api.data.annotations;
+
+public interface ICodeRawOffset {
+ int getOffset();
+}
diff --git a/jadx-core/src/main/java/jadx/api/data/annotations/InsnCodeOffset.java b/jadx-core/src/main/java/jadx/api/data/annotations/InsnCodeOffset.java
new file mode 100644
index 000000000..86c893deb
--- /dev/null
+++ b/jadx-core/src/main/java/jadx/api/data/annotations/InsnCodeOffset.java
@@ -0,0 +1,52 @@
+package jadx.api.data.annotations;
+
+import org.jetbrains.annotations.Nullable;
+
+import jadx.api.ICodeWriter;
+import jadx.core.dex.nodes.InsnNode;
+
+public class InsnCodeOffset implements ICodeRawOffset {
+
+ public static void attach(ICodeWriter code, InsnNode insn) {
+ if (insn == null) {
+ return;
+ }
+ if (code.isMetadataSupported()) {
+ InsnCodeOffset ann = from(insn);
+ if (ann != null) {
+ code.attachLineAnnotation(ann);
+ }
+ }
+ }
+
+ public static void attach(ICodeWriter code, int offset) {
+ if (offset >= 0 && code.isMetadataSupported()) {
+ code.attachLineAnnotation(new InsnCodeOffset(offset));
+ }
+ }
+
+ @Nullable
+ public static InsnCodeOffset from(InsnNode insn) {
+ int offset = insn.getOffset();
+ if (offset < 0) {
+ return null;
+ }
+ return new InsnCodeOffset(offset);
+ }
+
+ private final int offset;
+
+ public InsnCodeOffset(int offset) {
+ this.offset = offset;
+ }
+
+ @Override
+ public int getOffset() {
+ return offset;
+ }
+
+ @Override
+ public String toString() {
+ return "offset=" + offset;
+ }
+}
diff --git a/jadx-core/src/main/java/jadx/api/data/impl/JadxCodeComment.java b/jadx-core/src/main/java/jadx/api/data/impl/JadxCodeComment.java
new file mode 100644
index 000000000..5226607e9
--- /dev/null
+++ b/jadx-core/src/main/java/jadx/api/data/impl/JadxCodeComment.java
@@ -0,0 +1,85 @@
+package jadx.api.data.impl;
+
+import java.util.Comparator;
+
+import org.jetbrains.annotations.NotNull;
+
+import jadx.api.data.ICodeComment;
+import jadx.api.data.IJavaNodeRef;
+
+public class JadxCodeComment implements ICodeComment {
+
+ private IJavaNodeRef nodeRef;
+ private String comment;
+ private int offset;
+ private AttachType attachType;
+
+ public JadxCodeComment(IJavaNodeRef nodeRef, String comment) {
+ this(nodeRef, comment, -1, null);
+ }
+
+ public JadxCodeComment(IJavaNodeRef nodeRef, String comment, int offset) {
+ this(nodeRef, comment, offset, null);
+ }
+
+ public JadxCodeComment(IJavaNodeRef nodeRef, String comment, int offset, AttachType attachType) {
+ this.nodeRef = nodeRef;
+ this.comment = comment;
+ this.offset = offset;
+ this.attachType = attachType;
+ }
+
+ public JadxCodeComment() {
+ // for json deserialization
+ }
+
+ @Override
+ public IJavaNodeRef getNodeRef() {
+ return nodeRef;
+ }
+
+ public void setNodeRef(IJavaNodeRef nodeRef) {
+ this.nodeRef = nodeRef;
+ }
+
+ @Override
+ public String getComment() {
+ return comment;
+ }
+
+ public void setComment(String comment) {
+ this.comment = comment;
+ }
+
+ @Override
+ public int getOffset() {
+ return offset;
+ }
+
+ public void setOffset(int offset) {
+ this.offset = offset;
+ }
+
+ @Override
+ public AttachType getAttachType() {
+ return attachType;
+ }
+
+ public void setAttachType(AttachType attachType) {
+ this.attachType = attachType;
+ }
+
+ private static final Comparator COMPARATOR = Comparator
+ .comparing(ICodeComment::getNodeRef)
+ .thenComparing(ICodeComment::getOffset);
+
+ @Override
+ public int compareTo(@NotNull ICodeComment other) {
+ return COMPARATOR.compare(this, other);
+ }
+
+ @Override
+ public String toString() {
+ return "JadxCodeComment{" + nodeRef + ", comment='" + comment + '\'' + ", offset=" + offset + '}';
+ }
+}
diff --git a/jadx-core/src/main/java/jadx/api/data/impl/JadxCodeData.java b/jadx-core/src/main/java/jadx/api/data/impl/JadxCodeData.java
new file mode 100644
index 000000000..8b736a5f0
--- /dev/null
+++ b/jadx-core/src/main/java/jadx/api/data/impl/JadxCodeData.java
@@ -0,0 +1,49 @@
+package jadx.api.data.impl;
+
+import java.util.Collections;
+import java.util.List;
+
+import jadx.api.data.ICodeComment;
+import jadx.api.data.ICodeData;
+
+public class JadxCodeData implements ICodeData {
+
+ private long updateId = System.currentTimeMillis();
+ private List comments = Collections.emptyList();
+
+ @Override
+ public long getUpdateId() {
+ return updateId;
+ }
+
+ public void markUpdate() {
+ updateId = System.currentTimeMillis();
+ }
+
+ @Override
+ public List getComments() {
+ return comments;
+ }
+
+ public void setComments(List comments) {
+ markUpdate();
+ this.comments = comments;
+ }
+
+ @Override
+ public int hashCode() {
+ return Long.hashCode(updateId);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof JadxCodeData)) {
+ return false;
+ }
+ JadxCodeData that = (JadxCodeData) o;
+ return updateId == that.updateId;
+ }
+}
diff --git a/jadx-core/src/main/java/jadx/api/data/impl/JadxNodeRef.java b/jadx-core/src/main/java/jadx/api/data/impl/JadxNodeRef.java
new file mode 100644
index 000000000..50d4452cd
--- /dev/null
+++ b/jadx-core/src/main/java/jadx/api/data/impl/JadxNodeRef.java
@@ -0,0 +1,135 @@
+package jadx.api.data.impl;
+
+import java.util.Comparator;
+import java.util.Objects;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import jadx.api.JavaClass;
+import jadx.api.JavaField;
+import jadx.api.JavaMethod;
+import jadx.api.JavaNode;
+import jadx.api.data.IJavaNodeRef;
+
+public class JadxNodeRef implements IJavaNodeRef, Comparable {
+
+ @Nullable
+ public static JadxNodeRef forJavaNode(JavaNode javaNode) {
+ if (javaNode instanceof JavaClass) {
+ return forCls((JavaClass) javaNode);
+ }
+ if (javaNode instanceof JavaMethod) {
+ return forMth((JavaMethod) javaNode);
+ }
+ if (javaNode instanceof JavaField) {
+ return forFld((JavaField) javaNode);
+ }
+ return null;
+ }
+
+ public static JadxNodeRef forCls(JavaClass cls) {
+ return new JadxNodeRef(RefType.CLASS, cls.getClassNode().getClassInfo().getFullName(), null);
+ }
+
+ public static JadxNodeRef forCls(String clsFullName) {
+ return new JadxNodeRef(RefType.CLASS, clsFullName, null);
+ }
+
+ public static JadxNodeRef forMth(JavaMethod mth) {
+ return new JadxNodeRef(RefType.METHOD,
+ mth.getDeclaringClass().getClassNode().getClassInfo().getFullName(),
+ mth.getMethodNode().getMethodInfo().getShortId());
+ }
+
+ public static JadxNodeRef forFld(JavaField fld) {
+ return new JadxNodeRef(RefType.FIELD,
+ fld.getDeclaringClass().getClassNode().getClassInfo().getFullName(),
+ fld.getFieldNode().getFieldInfo().getShortId());
+ }
+
+ private RefType refType;
+ private String declClass;
+ @Nullable
+ private String shortId;
+
+ public JadxNodeRef(RefType refType, String declClass, @Nullable String shortId) {
+ this.refType = refType;
+ this.declClass = declClass;
+ this.shortId = shortId;
+ }
+
+ public JadxNodeRef() {
+ // for json deserialization
+ }
+
+ @Override
+ public RefType getType() {
+ return refType;
+ }
+
+ public void setRefType(RefType refType) {
+ this.refType = refType;
+ }
+
+ @Override
+ public String getDeclaringClass() {
+ return declClass;
+ }
+
+ public void setDeclClass(String declClass) {
+ this.declClass = declClass;
+ }
+
+ @Nullable
+ @Override
+ public String getShortId() {
+ return shortId;
+ }
+
+ public void setShortId(@Nullable String shortId) {
+ this.shortId = shortId;
+ }
+
+ private static final Comparator COMPARATOR = Comparator
+ .comparing(IJavaNodeRef::getType)
+ .thenComparing(IJavaNodeRef::getDeclaringClass)
+ .thenComparing(IJavaNodeRef::getShortId);
+
+ @Override
+ public int compareTo(@NotNull IJavaNodeRef other) {
+ return COMPARATOR.compare(this, other);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(refType, declClass, shortId);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof JadxNodeRef)) {
+ return false;
+ }
+ JadxNodeRef that = (JadxNodeRef) o;
+ return refType == that.refType
+ && Objects.equals(declClass, that.declClass)
+ && Objects.equals(shortId, that.shortId);
+ }
+
+ @Override
+ public String toString() {
+ switch (refType) {
+ case CLASS:
+ return declClass;
+ case FIELD:
+ case METHOD:
+ return declClass + "->" + shortId;
+ default:
+ return "unknown node ref type";
+ }
+ }
+}
diff --git a/jadx-core/src/main/java/jadx/api/impl/AnnotatedCodeWriter.java b/jadx-core/src/main/java/jadx/api/impl/AnnotatedCodeWriter.java
index 7a7c1072d..4c5b177e7 100644
--- a/jadx-core/src/main/java/jadx/api/impl/AnnotatedCodeWriter.java
+++ b/jadx-core/src/main/java/jadx/api/impl/AnnotatedCodeWriter.java
@@ -65,17 +65,13 @@ public class AnnotatedCodeWriter extends SimpleCodeWriter implements ICodeWriter
}
AnnotatedCodeWriter code = ((AnnotatedCodeWriter) cw);
line--;
+ int startLine = line;
+ int startPos = getLength();
for (Map.Entry entry : code.annotations.entrySet()) {
- Object val = entry.getValue();
- if (val instanceof DefinitionWrapper) {
- LineAttrNode node = ((DefinitionWrapper) val).getNode();
- node.setDefPosition(node.getDefPosition() + this.buf.length());
- }
- CodePosition pos = entry.getKey();
- int usagePos = pos.getUsagePosition() + getLength();
- attachAnnotation(val,
- new CodePosition(line + pos.getLine(), pos.getOffset())
- .setUsagePosition(usagePos));
+ CodePosition codePos = entry.getKey();
+ int newLine = startLine + codePos.getLine();
+ int newPos = startPos + codePos.getPos();
+ attachAnnotation(entry.getValue(), new CodePosition(newLine, codePos.getOffset(), newPos));
}
for (Map.Entry entry : code.lineMap.entrySet()) {
attachSourceLine(line + entry.getKey(), entry.getValue());
@@ -119,19 +115,21 @@ public class AnnotatedCodeWriter extends SimpleCodeWriter implements ICodeWriter
@Override
public void attachDefinition(LineAttrNode obj) {
- obj.setDefPosition(buf.length());
attachAnnotation(obj);
- attachAnnotation(new DefinitionWrapper(obj), new CodePosition(line, offset));
+ attachAnnotation(new DefinitionWrapper(obj), new CodePosition(line, offset, getLength()));
}
@Override
public void attachAnnotation(Object obj) {
- attachAnnotation(obj, new CodePosition(line, offset + 1).setUsagePosition(getLength()));
+ attachAnnotation(obj, new CodePosition(line, offset + 1, getLength()));
}
@Override
public void attachLineAnnotation(Object obj) {
- attachAnnotation(obj, new CodePosition(line, 0));
+ if (obj == null) {
+ return;
+ }
+ attachAnnotation(obj, new CodePosition(line, 0, getLength() - offset));
}
private void attachAnnotation(Object obj, CodePosition pos) {
@@ -165,13 +163,20 @@ public class AnnotatedCodeWriter extends SimpleCodeWriter implements ICodeWriter
return new AnnotatedCodeInfo(code, lineMap, annotations);
}
+ @Override
+ public Map getRawAnnotations() {
+ return annotations;
+ }
+
private void processDefinitionAnnotations() {
if (!annotations.isEmpty()) {
annotations.entrySet().removeIf(entry -> {
Object v = entry.getValue();
if (v instanceof DefinitionWrapper) {
LineAttrNode l = ((DefinitionWrapper) v).getNode();
- l.setDecompiledLine(entry.getKey().getLine());
+ CodePosition codePos = entry.getKey();
+ l.setDecompiledLine(codePos.getLine());
+ l.setDefPosition(codePos.getPos());
return true;
}
return false;
diff --git a/jadx-core/src/main/java/jadx/api/impl/SimpleCodeWriter.java b/jadx-core/src/main/java/jadx/api/impl/SimpleCodeWriter.java
index 704531abb..4d9a1b3de 100644
--- a/jadx-core/src/main/java/jadx/api/impl/SimpleCodeWriter.java
+++ b/jadx-core/src/main/java/jadx/api/impl/SimpleCodeWriter.java
@@ -1,8 +1,12 @@
package jadx.api.impl;
+import java.util.Collections;
+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;
@@ -228,6 +232,16 @@ public class SimpleCodeWriter implements ICodeWriter {
return buf.length();
}
+ @Override
+ public StringBuilder getRawBuf() {
+ return buf;
+ }
+
+ @Override
+ public Map getRawAnnotations() {
+ return Collections.emptyMap();
+ }
+
@Override
public String getCodeStr() {
removeFirstEmptyLine();
diff --git a/jadx-core/src/main/java/jadx/core/Jadx.java b/jadx-core/src/main/java/jadx/core/Jadx.java
index 5e9071d3d..9b26b8064 100644
--- a/jadx-core/src/main/java/jadx/core/Jadx.java
+++ b/jadx-core/src/main/java/jadx/core/Jadx.java
@@ -11,6 +11,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.JadxArgs;
+import jadx.core.dex.visitors.AttachCommentsVisitor;
import jadx.core.dex.visitors.AttachMethodDetails;
import jadx.core.dex.visitors.AttachTryCatchVisitor;
import jadx.core.dex.visitors.ClassModifier;
@@ -71,8 +72,9 @@ public class Jadx {
}
public static List getFallbackPassesList() {
- List passes = new ArrayList<>(3);
+ List passes = new ArrayList<>();
passes.add(new AttachTryCatchVisitor());
+ passes.add(new AttachCommentsVisitor());
passes.add(new ProcessInstructionsVisitor());
passes.add(new FallbackModeVisitor());
return passes;
@@ -82,6 +84,7 @@ public class Jadx {
List passes = new ArrayList<>();
passes.add(new SignatureProcessor());
passes.add(new OverrideMethodVisitor());
+ passes.add(new ProcessAnonymous());
passes.add(new RenameVisitor());
passes.add(new UsageInfoVisitor());
return passes;
@@ -97,6 +100,7 @@ public class Jadx {
passes.add(new DebugInfoAttachVisitor());
}
passes.add(new AttachTryCatchVisitor());
+ passes.add(new AttachCommentsVisitor());
passes.add(new ProcessInstructionsVisitor());
passes.add(new BlockSplitter());
@@ -144,7 +148,6 @@ public class Jadx {
passes.add(new EnumVisitor());
passes.add(new ExtractFieldInit());
passes.add(new FixAccessModifiers());
- passes.add(new ProcessAnonymous());
passes.add(new ClassModifier());
passes.add(new LoopRegionVisitor());
diff --git a/jadx-core/src/main/java/jadx/core/ProcessClass.java b/jadx-core/src/main/java/jadx/core/ProcessClass.java
index 2457f98bc..c4867f7ac 100644
--- a/jadx-core/src/main/java/jadx/core/ProcessClass.java
+++ b/jadx-core/src/main/java/jadx/core/ProcessClass.java
@@ -33,7 +33,6 @@ public final class ProcessClass {
try {
if (cls.contains(AFlag.CLASS_DEEP_RELOAD)) {
cls.remove(AFlag.CLASS_DEEP_RELOAD);
- cls.unload();
cls.deepUnload();
cls.root().runPreDecompileStageForClass(cls);
}
diff --git a/jadx-core/src/main/java/jadx/core/codegen/ClassGen.java b/jadx-core/src/main/java/jadx/core/codegen/ClassGen.java
index a549cb37a..31418c1f9 100644
--- a/jadx-core/src/main/java/jadx/core/codegen/ClassGen.java
+++ b/jadx-core/src/main/java/jadx/core/codegen/ClassGen.java
@@ -121,8 +121,9 @@ public class ClassGen {
if (Consts.DEBUG_USAGE) {
addClassUsageInfo(code, cls);
}
- CodeGenUtils.addComments(code, cls);
insertDecompilationProblems(code, cls);
+ CodeGenUtils.addSourceFileInfo(code, cls);
+ CodeGenUtils.addComments(code, cls);
addClassDeclaration(code);
addClassBody(code);
}
@@ -145,7 +146,6 @@ public class ClassGen {
annotationGen.addForClass(clsCode);
insertRenameInfo(clsCode, cls);
- CodeGenUtils.addSourceFileInfo(clsCode, cls);
clsCode.startLineWithNum(cls.getSourceLine()).add(af.makeString());
if (af.isInterface()) {
if (af.isAnnotation()) {
diff --git a/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java b/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java
index b24a88a69..282ff259a 100644
--- a/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java
+++ b/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java
@@ -10,6 +10,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.ICodeWriter;
+import jadx.api.data.annotations.InsnCodeOffset;
import jadx.api.plugins.input.data.MethodHandleType;
import jadx.core.deobf.NameMapper;
import jadx.core.dex.attributes.AFlag;
@@ -69,7 +70,6 @@ public class InsnGen {
protected final MethodNode mth;
protected final RootNode root;
protected final boolean fallback;
- protected final boolean attachInsns;
protected enum Flags {
BODY_ONLY,
@@ -82,7 +82,6 @@ public class InsnGen {
this.mth = mgen.getMethodNode();
this.root = mth.root();
this.fallback = fallback;
- this.attachInsns = root.getArgs().isJsonOutput();
}
private boolean isFallback() {
@@ -260,9 +259,7 @@ public class InsnGen {
} else {
if (flag != Flags.INLINE) {
code.startLineWithNum(insn.getSourceLine());
- if (attachInsns) {
- code.attachLineAnnotation(insn);
- }
+ InsnCodeOffset.attach(code, insn);
if (insn.contains(AFlag.COMMENT_OUT)) {
code.add("// ");
}
@@ -278,6 +275,7 @@ public class InsnGen {
makeInsnBody(code, insn, EMPTY_FLAGS);
if (flag != Flags.INLINE) {
code.add(';');
+ CodeGenUtils.addCodeComments(code, insn);
}
}
} catch (Exception e) {
diff --git a/jadx-core/src/main/java/jadx/core/codegen/MethodGen.java b/jadx-core/src/main/java/jadx/core/codegen/MethodGen.java
index a4db4b412..0d743d167 100644
--- a/jadx-core/src/main/java/jadx/core/codegen/MethodGen.java
+++ b/jadx-core/src/main/java/jadx/core/codegen/MethodGen.java
@@ -10,6 +10,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.ICodeWriter;
+import jadx.api.data.annotations.InsnCodeOffset;
import jadx.api.plugins.input.data.AccessFlags;
import jadx.api.plugins.input.data.annotations.EncodedValue;
import jadx.core.Consts;
@@ -44,7 +45,7 @@ import jadx.core.utils.exceptions.JadxOverflowException;
import static jadx.core.codegen.MethodGen.FallbackOption.BLOCK_DUMP;
import static jadx.core.codegen.MethodGen.FallbackOption.COMMENTED_DUMP;
import static jadx.core.codegen.MethodGen.FallbackOption.FALLBACK_MODE;
-import static jadx.core.dex.nodes.VariableNode.*;
+import static jadx.core.dex.nodes.VariableNode.VarKind;
public class MethodGen {
private static final Logger LOG = LoggerFactory.getLogger(MethodGen.class);
@@ -289,17 +290,19 @@ public class MethodGen {
}
public void addFallbackMethodCode(ICodeWriter code, FallbackOption fallbackOption) {
- // load original instructions
- try {
- mth.unload();
- mth.load();
- for (IDexTreeVisitor visitor : Jadx.getFallbackPassesList()) {
- DepthTraversal.visit(visitor, mth);
+ if (fallbackOption != FALLBACK_MODE) {
+ // load original instructions
+ try {
+ mth.unload();
+ mth.load();
+ for (IDexTreeVisitor visitor : Jadx.getFallbackPassesList()) {
+ DepthTraversal.visit(visitor, mth);
+ }
+ } catch (DecodeException e) {
+ LOG.error("Error reload instructions in fallback mode:", e);
+ code.startLine("// Can't load method instructions: " + e.getMessage());
+ return;
}
- } catch (DecodeException e) {
- LOG.error("Error reload instructions in fallback mode:", e);
- code.startLine("// Can't load method instructions: " + e.getMessage());
- return;
}
InsnNode[] insnArr = mth.getInstructions();
if (insnArr == null) {
@@ -333,7 +336,6 @@ public class MethodGen {
public static void addFallbackInsns(ICodeWriter code, MethodNode mth, InsnNode[] insnArr, FallbackOption option) {
int startIndent = code.getIndent();
InsnGen insnGen = new InsnGen(getFallbackMethodGen(mth), true);
- boolean attachInsns = mth.root().getArgs().isJsonOutput();
InsnNode prevInsn = null;
for (InsnNode insn : insnArr) {
if (insn == null) {
@@ -360,11 +362,9 @@ public class MethodGen {
code.startLine("*/");
code.startLine("// ");
} else {
- code.startLine();
- }
- if (attachInsns) {
- code.attachLineAnnotation(insn);
+ code.startLineWithNum(insn.getSourceLine());
}
+ InsnCodeOffset.attach(code, insn);
RegisterArg resArg = insn.getResult();
if (resArg != null) {
ArgType varType = resArg.getInitType();
@@ -382,6 +382,7 @@ public class MethodGen {
if (catchAttr != null) {
code.add(" // " + catchAttr);
}
+ CodeGenUtils.addCodeComments(code, insn);
} catch (Exception e) {
LOG.debug("Error generate fallback instruction: ", e.getCause());
code.setIndent(startIndent);
diff --git a/jadx-core/src/main/java/jadx/core/codegen/NameGen.java b/jadx-core/src/main/java/jadx/core/codegen/NameGen.java
index 1665b2d2a..12f684ab5 100644
--- a/jadx-core/src/main/java/jadx/core/codegen/NameGen.java
+++ b/jadx-core/src/main/java/jadx/core/codegen/NameGen.java
@@ -135,9 +135,6 @@ public class NameGen {
if (!NameMapper.isValidAndPrintable(name)) {
name = getFallbackName(var);
}
- if (Consts.DEBUG) {
- name += '_' + getFallbackName(var);
- }
return name;
}
diff --git a/jadx-core/src/main/java/jadx/core/codegen/RegionGen.java b/jadx-core/src/main/java/jadx/core/codegen/RegionGen.java
index 7a00a158f..8e956b437 100644
--- a/jadx-core/src/main/java/jadx/core/codegen/RegionGen.java
+++ b/jadx-core/src/main/java/jadx/core/codegen/RegionGen.java
@@ -9,6 +9,9 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.ICodeWriter;
+import jadx.api.data.ICodeComment;
+import jadx.api.data.annotations.CustomOffsetRef;
+import jadx.api.data.annotations.InsnCodeOffset;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.fldinit.FieldInitAttr;
@@ -26,7 +29,6 @@ import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.IBlock;
import jadx.core.dex.nodes.IContainer;
-import jadx.core.dex.nodes.IRegion;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.VariableNode;
import jadx.core.dex.regions.Region;
@@ -44,6 +46,7 @@ import jadx.core.dex.trycatch.ExceptionHandler;
import jadx.core.utils.BlockUtils;
import jadx.core.utils.CodeGenUtils;
import jadx.core.utils.RegionUtils;
+import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.CodegenException;
import jadx.core.utils.exceptions.JadxRuntimeException;
@@ -57,28 +60,8 @@ public class RegionGen extends InsnGen {
}
public void makeRegion(ICodeWriter code, IContainer cont) throws CodegenException {
- if (cont instanceof IBlock) {
- makeSimpleBlock((IBlock) cont, code);
- } else if (cont instanceof IRegion) {
- if (cont instanceof Region) {
- makeSimpleRegion(code, (Region) cont);
- } else {
- declareVars(code, cont);
- if (cont instanceof IfRegion) {
- makeIf((IfRegion) cont, code, true);
- } else if (cont instanceof SwitchRegion) {
- makeSwitch((SwitchRegion) cont, code);
- } else if (cont instanceof LoopRegion) {
- makeLoop((LoopRegion) cont, code);
- } else if (cont instanceof TryCatchRegion) {
- makeTryCatch((TryCatchRegion) cont, code);
- } else if (cont instanceof SynchronizedRegion) {
- makeSynchronizedRegion((SynchronizedRegion) cont, code);
- }
- }
- } else {
- throw new CodegenException("Not processed container: " + cont);
- }
+ declareVars(code, cont);
+ cont.generate(this, code);
}
private void declareVars(ICodeWriter code, IContainer cont) {
@@ -88,24 +71,30 @@ public class RegionGen extends InsnGen {
code.startLine();
declareVar(code, v);
code.add(';');
+ attachVariableCommentsData(code, v);
}
}
}
- private void makeSimpleRegion(ICodeWriter code, Region region) throws CodegenException {
- declareVars(code, region);
- for (IContainer c : region.getSubBlocks()) {
- makeRegion(code, c);
+ private void attachVariableCommentsData(ICodeWriter code, CodeVar v) {
+ RegisterArg assignReg = v.getSsaVars().get(0).getAssign();
+ if (code.isMetadataSupported()) {
+ InsnNode parentInsn = assignReg.getParentInsn();
+ if (parentInsn != null) {
+ int offset = parentInsn.getOffset();
+ code.attachLineAnnotation(new CustomOffsetRef(offset, ICodeComment.AttachType.VAR_DECLARE));
+ }
}
+ CodeGenUtils.addCodeComments(code, assignReg);
}
- public void makeRegionIndent(ICodeWriter code, IContainer region) throws CodegenException {
+ private void makeRegionIndent(ICodeWriter code, IContainer region) throws CodegenException {
code.incIndent();
makeRegion(code, region);
code.decIndent();
}
- private void makeSimpleBlock(IBlock block, ICodeWriter code) throws CodegenException {
+ public void makeSimpleBlock(IBlock block, ICodeWriter code) throws CodegenException {
if (block.contains(AFlag.DONT_GENERATE)) {
return;
}
@@ -121,22 +110,12 @@ public class RegionGen extends InsnGen {
}
}
- private void makeIf(IfRegion region, ICodeWriter code, boolean newLine) throws CodegenException {
+ public void makeIf(IfRegion region, ICodeWriter code, boolean newLine) throws CodegenException {
if (newLine) {
code.startLineWithNum(region.getSourceLine());
} else {
code.attachSourceLine(region.getSourceLine());
}
- if (attachInsns) {
- List conditionBlocks = region.getConditionBlocks();
- if (!conditionBlocks.isEmpty()) {
- BlockNode blockNode = conditionBlocks.get(0);
- InsnNode lastInsn = BlockUtils.getLastInsn(blockNode);
- if (lastInsn != null) {
- code.attachLineAnnotation(lastInsn);
- }
- }
- }
boolean comment = region.contains(AFlag.COMMENT_OUT);
if (comment) {
code.add("// ");
@@ -145,6 +124,15 @@ public class RegionGen extends InsnGen {
code.add("if (");
new ConditionGen(this).add(code, region.getCondition());
code.add(") {");
+ if (code.isMetadataSupported()) {
+ List conditionBlocks = region.getConditionBlocks();
+ if (!conditionBlocks.isEmpty()) {
+ BlockNode blockNode = conditionBlocks.get(0);
+ InsnNode lastInsn = BlockUtils.getLastInsn(blockNode);
+ InsnCodeOffset.attach(code, lastInsn);
+ CodeGenUtils.addCodeComments(code, lastInsn);
+ }
+ }
makeRegionIndent(code, region.getThenRegion());
if (comment) {
code.startLine("// }");
@@ -186,43 +174,49 @@ public class RegionGen extends InsnGen {
return false;
}
- private void makeLoop(LoopRegion region, ICodeWriter code) throws CodegenException {
+ public void makeLoop(LoopRegion region, ICodeWriter code) throws CodegenException {
LoopLabelAttr labelAttr = region.getInfo().getStart().get(AType.LOOP_LABEL);
if (labelAttr != null) {
code.startLine(mgen.getNameGen().getLoopLabel(labelAttr)).add(':');
}
+ code.startLineWithNum(region.getConditionSourceLine());
IfCondition condition = region.getCondition();
if (condition == null) {
// infinite loop
- code.startLine("while (true) {");
+ code.add("while (true) {");
makeRegionIndent(code, region.getBody());
code.startLine('}');
return;
}
+ InsnNode condInsn = condition.getFirstInsn();
+ InsnCodeOffset.attach(code, condInsn);
+
ConditionGen conditionGen = new ConditionGen(this);
LoopType type = region.getType();
if (type != null) {
if (type instanceof ForLoop) {
ForLoop forLoop = (ForLoop) type;
- code.startLine("for (");
+ code.add("for (");
makeInsn(forLoop.getInitInsn(), code, Flags.INLINE);
code.add("; ");
conditionGen.add(code, condition);
code.add("; ");
makeInsn(forLoop.getIncrInsn(), code, Flags.INLINE);
code.add(") {");
+ CodeGenUtils.addCodeComments(code, condInsn);
makeRegionIndent(code, region.getBody());
code.startLine('}');
return;
}
if (type instanceof ForEachLoop) {
ForEachLoop forEachLoop = (ForEachLoop) type;
- code.startLine("for (");
+ code.add("for (");
declareVar(code, forEachLoop.getVarArg());
code.add(" : ");
addArg(code, forEachLoop.getIterableArg(), false);
code.add(") {");
+ CodeGenUtils.addCodeComments(code, condInsn);
makeRegionIndent(code, region.getBody());
code.startLine('}');
return;
@@ -230,37 +224,45 @@ public class RegionGen extends InsnGen {
throw new JadxRuntimeException("Unknown loop type: " + type.getClass());
}
if (region.isConditionAtEnd()) {
- code.startLine("do {");
+ code.add("do {");
+ CodeGenUtils.addCodeComments(code, condInsn);
makeRegionIndent(code, region.getBody());
code.startLineWithNum(region.getConditionSourceLine());
code.add("} while (");
conditionGen.add(code, condition);
code.add(");");
} else {
- code.startLineWithNum(region.getConditionSourceLine());
code.add("while (");
conditionGen.add(code, condition);
code.add(") {");
+ CodeGenUtils.addCodeComments(code, condInsn);
makeRegionIndent(code, region.getBody());
code.startLine('}');
}
}
- private void makeSynchronizedRegion(SynchronizedRegion cont, ICodeWriter code) throws CodegenException {
+ public void makeSynchronizedRegion(SynchronizedRegion cont, ICodeWriter code) throws CodegenException {
code.startLine("synchronized (");
- addArg(code, cont.getEnterInsn().getArg(0));
+ InsnNode monitorEnterInsn = cont.getEnterInsn();
+ addArg(code, monitorEnterInsn.getArg(0));
code.add(") {");
+
+ InsnCodeOffset.attach(code, monitorEnterInsn);
+ CodeGenUtils.addCodeComments(code, monitorEnterInsn);
+
makeRegionIndent(code, cont.getRegion());
code.startLine('}');
}
- private void makeSwitch(SwitchRegion sw, ICodeWriter code) throws CodegenException {
+ public void makeSwitch(SwitchRegion sw, ICodeWriter code) throws CodegenException {
SwitchInsn insn = (SwitchInsn) BlockUtils.getLastInsn(sw.getHeader());
Objects.requireNonNull(insn, "Switch insn not found in header");
InsnArg arg = insn.getArg(0);
code.startLine("switch (");
addArg(code, arg, false);
code.add(") {");
+ InsnCodeOffset.attach(code, insn);
+ CodeGenUtils.addCodeComments(code, insn);
code.incIndent();
for (CaseInfo caseInfo : sw.getCases()) {
@@ -304,8 +306,13 @@ public class RegionGen extends InsnGen {
}
}
- private void makeTryCatch(TryCatchRegion region, ICodeWriter code) throws CodegenException {
+ public void makeTryCatch(TryCatchRegion region, ICodeWriter code) throws CodegenException {
code.startLine("try {");
+
+ InsnNode insn = Utils.first(region.getTryCatchBlock().getInsns());
+ InsnCodeOffset.attach(code, insn);
+ CodeGenUtils.addCodeComments(code, insn);
+
makeRegionIndent(code, region.getTryRegion());
// TODO: move search of 'allHandler' to 'TryCatchRegion'
ExceptionHandler allHandler = null;
@@ -381,6 +388,10 @@ public class RegionGen extends InsnGen {
throw new JadxRuntimeException("Unexpected arg type in catch block: " + arg + ", class: " + arg.getClass().getSimpleName());
}
code.add(") {");
+
+ InsnCodeOffset.attach(code, handler.getHandleOffset());
+ CodeGenUtils.addCodeComments(code, handler.getHandlerBlock());
+
makeRegionIndent(code, region);
}
}
diff --git a/jadx-core/src/main/java/jadx/core/codegen/json/JsonCodeGen.java b/jadx-core/src/main/java/jadx/core/codegen/json/JsonCodeGen.java
index 4326c8159..b8cc75c91 100644
--- a/jadx-core/src/main/java/jadx/core/codegen/json/JsonCodeGen.java
+++ b/jadx-core/src/main/java/jadx/core/codegen/json/JsonCodeGen.java
@@ -16,6 +16,7 @@ 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.core.codegen.ClassGen;
@@ -29,7 +30,6 @@ import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
-import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.CodeGenUtils;
@@ -193,9 +193,9 @@ public class JsonCodeGen {
JsonCodeLine jsonCodeLine = new JsonCodeLine();
jsonCodeLine.setCode(codeLine);
jsonCodeLine.setSourceLine(lineMapping.get(line));
- Object obj = annotations.get(new CodePosition(line, 0));
- if (obj instanceof InsnNode) {
- long offset = ((InsnNode) obj).getOffset();
+ Object obj = annotations.get(new CodePosition(line));
+ if (obj instanceof InsnCodeOffset) {
+ long offset = ((InsnCodeOffset) obj).getOffset();
jsonCodeLine.setOffset("0x" + Long.toHexString(mthCodeOffset + offset * 2));
}
codeLines.add(jsonCodeLine);
diff --git a/jadx-core/src/main/java/jadx/core/dex/attributes/AType.java b/jadx-core/src/main/java/jadx/core/dex/attributes/AType.java
index b1ea46968..c02bdc5b4 100644
--- a/jadx-core/src/main/java/jadx/core/dex/attributes/AType.java
+++ b/jadx-core/src/main/java/jadx/core/dex/attributes/AType.java
@@ -42,6 +42,9 @@ import jadx.core.dex.trycatch.SplitterBlockAttr;
@SuppressWarnings("InstantiationOfUtilityClass")
public class AType {
+ // class, method, field, insn
+ public static final AType> CODE_COMMENTS = new AType<>();
+
// class, method, field
public static final AType ANNOTATION_LIST = new AType<>();
public static final AType RENAME_REASON = new AType<>();
diff --git a/jadx-core/src/main/java/jadx/core/dex/attributes/AttrNode.java b/jadx-core/src/main/java/jadx/core/dex/attributes/AttrNode.java
index c1b28275f..949133e20 100644
--- a/jadx-core/src/main/java/jadx/core/dex/attributes/AttrNode.java
+++ b/jadx-core/src/main/java/jadx/core/dex/attributes/AttrNode.java
@@ -33,6 +33,23 @@ public abstract class AttrNode implements IAttributeNode {
}
}
+ @Override
+ public void copyAttributeFrom(AttrNode attrNode, AType attrType) {
+ IAttribute attr = attrNode.get(attrType);
+ if (attr != null) {
+ this.addAttr(attr);
+ }
+ }
+
+ /**
+ * Remove attribute in this node, add copy from other if exists
+ */
+ @Override
+ public void rewriteAttributeFrom(AttrNode attrNode, AType attrType) {
+ remove(attrType);
+ copyAttributeFrom(attrNode, attrType);
+ }
+
private AttributeStorage initStorage() {
AttributeStorage store = storage;
if (store == EMPTY_ATTR_STORAGE) {
diff --git a/jadx-core/src/main/java/jadx/core/dex/attributes/IAttributeNode.java b/jadx-core/src/main/java/jadx/core/dex/attributes/IAttributeNode.java
index f227fa372..df3985acf 100644
--- a/jadx-core/src/main/java/jadx/core/dex/attributes/IAttributeNode.java
+++ b/jadx-core/src/main/java/jadx/core/dex/attributes/IAttributeNode.java
@@ -14,6 +14,10 @@ public interface IAttributeNode {
void copyAttributesFrom(AttrNode attrNode);
+ void copyAttributeFrom(AttrNode attrNode, AType attrType);
+
+ void rewriteAttributeFrom(AttrNode attrNode, AType attrType);
+
boolean contains(AFlag flag);
boolean contains(AType type);
diff --git a/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/NotificationAttrNode.java b/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/NotificationAttrNode.java
index 4f2f55261..e8d854899 100644
--- a/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/NotificationAttrNode.java
+++ b/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/NotificationAttrNode.java
@@ -35,11 +35,9 @@ public abstract class NotificationAttrNode extends LineAttrNode implements ICode
public void addComment(String commentStr) {
addAttr(AType.COMMENTS, commentStr);
- LOG.info("{} in {}", commentStr, this);
}
public void addDebugComment(String commentStr) {
addAttr(AType.COMMENTS, "JADX DEBUG: " + commentStr);
- LOG.debug("{} in {}", commentStr, this);
}
}
diff --git a/jadx-core/src/main/java/jadx/core/dex/info/FieldInfo.java b/jadx-core/src/main/java/jadx/core/dex/info/FieldInfo.java
index 8aec5c2d2..5d8bb84fc 100644
--- a/jadx-core/src/main/java/jadx/core/dex/info/FieldInfo.java
+++ b/jadx-core/src/main/java/jadx/core/dex/info/FieldInfo.java
@@ -60,6 +60,10 @@ public final class FieldInfo {
return declClass.getFullName() + '.' + name + ':' + TypeGen.signature(type);
}
+ public String getShortId() {
+ return name + ':' + TypeGen.signature(type);
+ }
+
public String getRawFullId() {
return declClass.makeRawFullName() + '.' + name + ':' + TypeGen.signature(type);
}
diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java
index 37db7ef34..aad6a7b13 100644
--- a/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java
+++ b/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java
@@ -35,7 +35,6 @@ import jadx.core.dex.info.FieldInfo;
import jadx.core.dex.info.MethodInfo;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.instructions.args.LiteralArg;
-import jadx.core.dex.visitors.ProcessAnonymous;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxRuntimeException;
@@ -191,9 +190,7 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN
if (fileName.endsWith(".java")) {
fileName = fileName.substring(0, fileName.length() - 5);
}
- if (fileName.isEmpty()
- || fileName.equals("SourceFile")
- || fileName.equals("\"")) {
+ if (fileName.isEmpty() || fileName.equals("SourceFile")) {
return;
}
if (clsInfo != null) {
@@ -201,12 +198,7 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN
if (fileName.equals(name)) {
return;
}
- if (fileName.contains("$")
- && fileName.endsWith('$' + name)) {
- return;
- }
- ClassInfo parentCls = clsInfo.getTopParentClass();
- if (parentCls != null && fileName.equals(parentCls.getShortName())) {
+ if (fileName.contains("$") && fileName.endsWith('$' + name)) {
return;
}
}
@@ -240,14 +232,12 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN
// manually added class
return;
}
+ unload();
clearAttributes();
root().getConstValues().removeForClass(this);
initialLoad(clsData);
- ProcessAnonymous.runForClass(this);
- for (ClassNode innerClass : innerClasses) {
- innerClass.deepUnload();
- }
+ innerClasses.forEach(ClassNode::deepUnload);
}
private synchronized ICodeInfo decompile(boolean searchInCache) {
@@ -375,6 +365,15 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN
return null;
}
+ public FieldNode searchFieldByShortId(String shortId) {
+ for (FieldNode f : fields) {
+ if (f.getFieldInfo().getShortId().equals(shortId)) {
+ return f;
+ }
+ }
+ return null;
+ }
+
public MethodNode searchMethod(MethodInfo mth) {
return mthInfoMap.get(mth);
}
diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/IBlock.java b/jadx-core/src/main/java/jadx/core/dex/nodes/IBlock.java
index 9c53ed0ee..cb30bcd8d 100644
--- a/jadx-core/src/main/java/jadx/core/dex/nodes/IBlock.java
+++ b/jadx-core/src/main/java/jadx/core/dex/nodes/IBlock.java
@@ -2,7 +2,16 @@ package jadx.core.dex.nodes;
import java.util.List;
+import jadx.api.ICodeWriter;
+import jadx.core.codegen.RegionGen;
+import jadx.core.utils.exceptions.CodegenException;
+
public interface IBlock extends IContainer {
List getInstructions();
+
+ @Override
+ default void generate(RegionGen regionGen, ICodeWriter code) throws CodegenException {
+ regionGen.makeSimpleBlock(this, code);
+ }
}
diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/IContainer.java b/jadx-core/src/main/java/jadx/core/dex/nodes/IContainer.java
index 71e33e277..f246684bb 100644
--- a/jadx-core/src/main/java/jadx/core/dex/nodes/IContainer.java
+++ b/jadx-core/src/main/java/jadx/core/dex/nodes/IContainer.java
@@ -1,6 +1,9 @@
package jadx.core.dex.nodes;
+import jadx.api.ICodeWriter;
+import jadx.core.codegen.RegionGen;
import jadx.core.dex.attributes.IAttributeNode;
+import jadx.core.utils.exceptions.CodegenException;
public interface IContainer extends IAttributeNode {
@@ -8,4 +11,11 @@ public interface IContainer extends IAttributeNode {
* Unique id for use in 'toString()' method
*/
String baseString();
+
+ /**
+ * Dispatch to needed generate method in RegionGen
+ */
+ default void generate(RegionGen regionGen, ICodeWriter code) throws CodegenException {
+ throw new CodegenException("Code generate not implemented for container: " + getClass().getSimpleName());
+ }
}
diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java
index 78ab9ab0c..32cbf6481 100644
--- a/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java
+++ b/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java
@@ -109,7 +109,7 @@ public class RootNode {
// sort classes by name, expect top classes before inner
classes.sort(Comparator.comparing(ClassNode::getFullName));
initInnerClasses();
- LOG.debug("Classes loaded: {}", classes.size());
+ LOG.info("Classes loaded: {}", classes.size());
}
private void addDummyClass(IClassData classData, Exception exc) {
@@ -239,6 +239,7 @@ public class RootNode {
public void runPreDecompileStage() {
for (IDexTreeVisitor pass : preDecompilePasses) {
+ long start = System.currentTimeMillis();
try {
pass.init(this);
} catch (Exception e) {
@@ -247,6 +248,9 @@ public class RootNode {
for (ClassNode cls : classes) {
DepthTraversal.visit(pass, cls);
}
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("{} time: {}ms", pass.getClass().getSimpleName(), System.currentTimeMillis() - start);
+ }
}
}
diff --git a/jadx-core/src/main/java/jadx/core/dex/regions/Region.java b/jadx-core/src/main/java/jadx/core/dex/regions/Region.java
index 70e179aa4..11ab6b709 100644
--- a/jadx-core/src/main/java/jadx/core/dex/regions/Region.java
+++ b/jadx-core/src/main/java/jadx/core/dex/regions/Region.java
@@ -3,9 +3,12 @@ package jadx.core.dex.regions;
import java.util.ArrayList;
import java.util.List;
+import jadx.api.ICodeWriter;
+import jadx.core.codegen.RegionGen;
import jadx.core.dex.nodes.IContainer;
import jadx.core.dex.nodes.IRegion;
import jadx.core.utils.Utils;
+import jadx.core.utils.exceptions.CodegenException;
public final class Region extends AbstractRegion {
@@ -26,6 +29,13 @@ public final class Region extends AbstractRegion {
blocks.add(region);
}
+ @Override
+ public void generate(RegionGen regionGen, ICodeWriter code) throws CodegenException {
+ for (IContainer c : blocks) {
+ regionGen.makeRegion(code, c);
+ }
+ }
+
@Override
public boolean replaceSubBlock(IContainer oldBlock, IContainer newBlock) {
int i = blocks.indexOf(oldBlock);
diff --git a/jadx-core/src/main/java/jadx/core/dex/regions/SwitchRegion.java b/jadx-core/src/main/java/jadx/core/dex/regions/SwitchRegion.java
index c303fdaf1..0b4f2d185 100644
--- a/jadx-core/src/main/java/jadx/core/dex/regions/SwitchRegion.java
+++ b/jadx-core/src/main/java/jadx/core/dex/regions/SwitchRegion.java
@@ -5,11 +5,13 @@ import java.util.Collections;
import java.util.List;
import jadx.api.ICodeWriter;
+import jadx.core.codegen.RegionGen;
import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.IBranchRegion;
import jadx.core.dex.nodes.IContainer;
import jadx.core.dex.nodes.IRegion;
import jadx.core.utils.Utils;
+import jadx.core.utils.exceptions.CodegenException;
public final class SwitchRegion extends AbstractRegion implements IBranchRegion {
@@ -72,6 +74,11 @@ public final class SwitchRegion extends AbstractRegion implements IBranchRegion
return Collections.unmodifiableList(getCaseContainers());
}
+ @Override
+ public void generate(RegionGen regionGen, ICodeWriter code) throws CodegenException {
+ regionGen.makeSwitch(this, code);
+ }
+
@Override
public String baseString() {
return header.baseString();
diff --git a/jadx-core/src/main/java/jadx/core/dex/regions/SynchronizedRegion.java b/jadx-core/src/main/java/jadx/core/dex/regions/SynchronizedRegion.java
index 8a88fe106..a10a89621 100644
--- a/jadx-core/src/main/java/jadx/core/dex/regions/SynchronizedRegion.java
+++ b/jadx-core/src/main/java/jadx/core/dex/regions/SynchronizedRegion.java
@@ -3,9 +3,12 @@ package jadx.core.dex.regions;
import java.util.ArrayList;
import java.util.List;
+import jadx.api.ICodeWriter;
+import jadx.core.codegen.RegionGen;
import jadx.core.dex.nodes.IContainer;
import jadx.core.dex.nodes.IRegion;
import jadx.core.dex.nodes.InsnNode;
+import jadx.core.utils.exceptions.CodegenException;
public final class SynchronizedRegion extends AbstractRegion {
@@ -36,6 +39,11 @@ public final class SynchronizedRegion extends AbstractRegion {
return region.getSubBlocks();
}
+ @Override
+ public void generate(RegionGen regionGen, ICodeWriter code) throws CodegenException {
+ regionGen.makeSynchronizedRegion(this, code);
+ }
+
@Override
public String baseString() {
return Integer.toHexString(enterInsn.getOffset());
diff --git a/jadx-core/src/main/java/jadx/core/dex/regions/TryCatchRegion.java b/jadx-core/src/main/java/jadx/core/dex/regions/TryCatchRegion.java
index 4a597b40f..7bd6aa788 100644
--- a/jadx-core/src/main/java/jadx/core/dex/regions/TryCatchRegion.java
+++ b/jadx-core/src/main/java/jadx/core/dex/regions/TryCatchRegion.java
@@ -6,12 +6,15 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import jadx.api.ICodeWriter;
+import jadx.core.codegen.RegionGen;
import jadx.core.dex.nodes.IBranchRegion;
import jadx.core.dex.nodes.IContainer;
import jadx.core.dex.nodes.IRegion;
import jadx.core.dex.trycatch.ExceptionHandler;
import jadx.core.dex.trycatch.TryCatchBlock;
import jadx.core.utils.Utils;
+import jadx.core.utils.exceptions.CodegenException;
public final class TryCatchRegion extends AbstractRegion implements IBranchRegion {
@@ -77,6 +80,11 @@ public final class TryCatchRegion extends AbstractRegion implements IBranchRegio
return getSubBlocks();
}
+ @Override
+ public void generate(RegionGen regionGen, ICodeWriter code) throws CodegenException {
+ regionGen.makeTryCatch(this, code);
+ }
+
@Override
public String baseString() {
return tryRegion.baseString();
diff --git a/jadx-core/src/main/java/jadx/core/dex/regions/conditions/IfCondition.java b/jadx-core/src/main/java/jadx/core/dex/regions/conditions/IfCondition.java
index d8dbe0734..31faa8bf0 100644
--- a/jadx-core/src/main/java/jadx/core/dex/regions/conditions/IfCondition.java
+++ b/jadx-core/src/main/java/jadx/core/dex/regions/conditions/IfCondition.java
@@ -8,6 +8,8 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
+import org.jetbrains.annotations.Nullable;
+
import jadx.core.dex.attributes.AttrNode;
import jadx.core.dex.instructions.ArithNode;
import jadx.core.dex.instructions.ArithOp;
@@ -262,6 +264,14 @@ public final class IfCondition extends AttrNode {
return list;
}
+ @Nullable
+ public InsnNode getFirstInsn() {
+ if (mode == Mode.COMPARE) {
+ return compare.getInsn();
+ }
+ return args.get(0).getFirstInsn();
+ }
+
@Override
public String toString() {
switch (mode) {
@@ -313,5 +323,4 @@ public final class IfCondition extends AttrNode {
result = 31 * result + (compare != null ? compare.hashCode() : 0);
return result;
}
-
}
diff --git a/jadx-core/src/main/java/jadx/core/dex/regions/conditions/IfRegion.java b/jadx-core/src/main/java/jadx/core/dex/regions/conditions/IfRegion.java
index 3dd91d4cb..d479fe71c 100644
--- a/jadx-core/src/main/java/jadx/core/dex/regions/conditions/IfRegion.java
+++ b/jadx-core/src/main/java/jadx/core/dex/regions/conditions/IfRegion.java
@@ -5,6 +5,8 @@ import java.util.Collections;
import java.util.List;
import java.util.Set;
+import jadx.api.ICodeWriter;
+import jadx.core.codegen.RegionGen;
import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.IBranchRegion;
import jadx.core.dex.nodes.IContainer;
@@ -12,6 +14,7 @@ import jadx.core.dex.nodes.IRegion;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.regions.AbstractRegion;
import jadx.core.utils.BlockUtils;
+import jadx.core.utils.exceptions.CodegenException;
public final class IfRegion extends AbstractRegion implements IBranchRegion {
@@ -129,6 +132,11 @@ public final class IfRegion extends AbstractRegion implements IBranchRegion {
return false;
}
+ @Override
+ public void generate(RegionGen regionGen, ICodeWriter code) throws CodegenException {
+ regionGen.makeIf(this, code, true);
+ }
+
@Override
public String baseString() {
StringBuilder sb = new StringBuilder();
diff --git a/jadx-core/src/main/java/jadx/core/dex/regions/loops/LoopRegion.java b/jadx-core/src/main/java/jadx/core/dex/regions/loops/LoopRegion.java
index 51d424296..265c8a125 100644
--- a/jadx-core/src/main/java/jadx/core/dex/regions/loops/LoopRegion.java
+++ b/jadx-core/src/main/java/jadx/core/dex/regions/loops/LoopRegion.java
@@ -6,6 +6,8 @@ import java.util.List;
import org.jetbrains.annotations.Nullable;
+import jadx.api.ICodeWriter;
+import jadx.core.codegen.RegionGen;
import jadx.core.dex.attributes.nodes.LoopInfo;
import jadx.core.dex.instructions.IfNode;
import jadx.core.dex.instructions.args.RegisterArg;
@@ -16,6 +18,7 @@ import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.regions.AbstractRegion;
import jadx.core.dex.regions.conditions.IfCondition;
import jadx.core.utils.BlockUtils;
+import jadx.core.utils.exceptions.CodegenException;
public final class LoopRegion extends AbstractRegion {
@@ -165,6 +168,11 @@ public final class LoopRegion extends AbstractRegion {
return false;
}
+ @Override
+ public void generate(RegionGen regionGen, ICodeWriter code) throws CodegenException {
+ regionGen.makeLoop(this, code);
+ }
+
@Override
public String baseString() {
return body == null ? "-" : body.baseString();
diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/AttachCommentsVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/AttachCommentsVisitor.java
new file mode 100644
index 000000000..fc7fde7aa
--- /dev/null
+++ b/jadx-core/src/main/java/jadx/core/dex/visitors/AttachCommentsVisitor.java
@@ -0,0 +1,156 @@
+package jadx.core.dex.visitors;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import jadx.api.data.ICodeComment;
+import jadx.api.data.ICodeData;
+import jadx.api.data.IJavaNodeRef;
+import jadx.core.dex.attributes.AType;
+import jadx.core.dex.attributes.IAttributeNode;
+import jadx.core.dex.instructions.args.RegisterArg;
+import jadx.core.dex.nodes.ClassNode;
+import jadx.core.dex.nodes.FieldNode;
+import jadx.core.dex.nodes.InsnNode;
+import jadx.core.dex.nodes.MethodNode;
+import jadx.core.utils.exceptions.JadxRuntimeException;
+
+@JadxVisitor(
+ name = "Attach comments",
+ desc = "Attach comments",
+ runBefore = {
+ ProcessInstructionsVisitor.class
+ }
+)
+public class AttachCommentsVisitor extends AbstractVisitor {
+
+ private static final Logger LOG = LoggerFactory.getLogger(AttachCommentsVisitor.class);
+
+ private final CommentsData cachedCommentsData = new CommentsData();
+
+ @Override
+ public boolean visit(ClassNode cls) {
+ List clsComments = getCommentsData(cls);
+ if (!clsComments.isEmpty()) {
+ applyComments(cls, clsComments);
+ }
+ cls.getInnerClasses().forEach(this::visit);
+ return false;
+ }
+
+ private static void applyComments(ClassNode cls, List clsComments) {
+ for (ICodeComment comment : clsComments) {
+ IJavaNodeRef nodeRef = comment.getNodeRef();
+ switch (nodeRef.getType()) {
+ case CLASS:
+ addComment(cls, comment.getComment());
+ break;
+
+ case FIELD:
+ FieldNode fieldNode = cls.searchFieldByShortId(nodeRef.getShortId());
+ if (fieldNode == null) {
+ LOG.warn("Field reference not found: {}", nodeRef);
+ } else {
+ addComment(fieldNode, comment.getComment());
+ }
+ break;
+
+ case METHOD:
+ MethodNode methodNode = cls.searchMethodByShortId(nodeRef.getShortId());
+ if (methodNode == null) {
+ LOG.warn("Method reference not found: {}", nodeRef);
+ } else {
+ int offset = comment.getOffset();
+ if (offset < 0) {
+ addComment(methodNode, comment.getComment());
+ } else if (comment.getAttachType() != null) {
+ processCustomAttach(methodNode, comment);
+ } else {
+ InsnNode insn = getInsnByOffset(methodNode, offset);
+ addComment(insn, comment.getComment());
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ private static InsnNode getInsnByOffset(MethodNode mth, int offset) {
+ try {
+ return mth.getInstructions()[offset];
+ } catch (Exception e) {
+ LOG.warn("Insn reference not found in: {} with offset: {}", mth, offset);
+ return null;
+ }
+ }
+
+ private static void processCustomAttach(MethodNode mth, ICodeComment comment) {
+ ICodeComment.AttachType attachType = comment.getAttachType();
+ if (attachType == null) {
+ return;
+ }
+ switch (attachType) {
+ case VAR_DECLARE:
+ InsnNode insn = getInsnByOffset(mth, comment.getOffset());
+ if (insn != null) {
+ RegisterArg result = insn.getResult();
+ if (result != null) {
+ result.addAttr(AType.CODE_COMMENTS, comment.getComment());
+ }
+ }
+ break;
+
+ default:
+ throw new JadxRuntimeException("Unexpected attach type: " + attachType);
+ }
+ }
+
+ private static void addComment(@Nullable IAttributeNode node, String comment) {
+ if (node == null) {
+ return;
+ }
+ node.remove(AType.CODE_COMMENTS);
+ node.addAttr(AType.CODE_COMMENTS, comment);
+ }
+
+ private static final class CommentsData {
+ long updateId;
+ Map> clsCommentsMap;
+ }
+
+ private List getCommentsData(ClassNode cls) {
+ ICodeData additionalData = cls.root().getArgs().getCodeData();
+ if (additionalData == null || additionalData.getComments().isEmpty()) {
+ return Collections.emptyList();
+ }
+ synchronized (cachedCommentsData) {
+ CommentsData commentsData = this.cachedCommentsData;
+ if (commentsData.updateId != additionalData.getUpdateId()) {
+ updateCommentsData(additionalData, commentsData);
+ }
+ List clsComments = commentsData.clsCommentsMap.get(cls.getClassInfo().getFullName());
+ if (clsComments == null) {
+ return Collections.emptyList();
+ }
+ return clsComments;
+ }
+ }
+
+ private static void updateCommentsData(ICodeData data, CommentsData commentsData) {
+ Map> map = new HashMap<>();
+ for (ICodeComment comment : data.getComments()) {
+ String declClsId = comment.getNodeRef().getDeclaringClass();
+ List comments = map.computeIfAbsent(declClsId, s -> new ArrayList<>());
+ comments.add(comment);
+ }
+ commentsData.clsCommentsMap = map;
+ commentsData.updateId = data.getUpdateId();
+ }
+}
diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/ConstInlineVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/ConstInlineVisitor.java
index 0be0c0012..4f76ecb88 100644
--- a/jadx-core/src/main/java/jadx/core/dex/visitors/ConstInlineVisitor.java
+++ b/jadx-core/src/main/java/jadx/core/dex/visitors/ConstInlineVisitor.java
@@ -4,6 +4,7 @@ import java.util.ArrayList;
import java.util.List;
import jadx.core.dex.attributes.AFlag;
+import jadx.core.dex.attributes.AType;
import jadx.core.dex.info.MethodInfo;
import jadx.core.dex.instructions.BaseInvokeNode;
import jadx.core.dex.instructions.ConstStringNode;
@@ -225,6 +226,12 @@ public class ConstInlineVisitor extends AbstractVisitor {
}
if (insnType == InsnType.RETURN) {
useInsn.setSourceLine(constInsn.getSourceLine());
+ if (useInsn.contains(AFlag.SYNTHETIC)) {
+ useInsn.setOffset(constInsn.getOffset());
+ useInsn.rewriteAttributeFrom(constInsn, AType.CODE_COMMENTS);
+ } else {
+ useInsn.copyAttributeFrom(constInsn, AType.CODE_COMMENTS);
+ }
}
return true;
}
diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/ModVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/ModVisitor.java
index c6f174580..33d8a203f 100644
--- a/jadx-core/src/main/java/jadx/core/dex/visitors/ModVisitor.java
+++ b/jadx-core/src/main/java/jadx/core/dex/visitors/ModVisitor.java
@@ -608,5 +608,6 @@ public class ModVisitor extends AbstractVisitor {
excHandler.setArg(namedArg);
replaceInsn(mth, block, 0, moveInsn);
}
+ block.copyAttributeFrom(insn, AType.CODE_COMMENTS); // save comment
}
}
diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/OverrideMethodVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/OverrideMethodVisitor.java
index 5a46e9c04..2119d3108 100644
--- a/jadx-core/src/main/java/jadx/core/dex/visitors/OverrideMethodVisitor.java
+++ b/jadx-core/src/main/java/jadx/core/dex/visitors/OverrideMethodVisitor.java
@@ -12,8 +12,6 @@ import java.util.stream.Collectors;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import jadx.core.clsp.ClspClass;
import jadx.core.clsp.ClspMethod;
@@ -41,27 +39,20 @@ import jadx.core.utils.exceptions.JadxException;
}
)
public class OverrideMethodVisitor extends AbstractVisitor {
- private static final Logger LOG = LoggerFactory.getLogger(OverrideMethodVisitor.class);
@Override
- public void init(RootNode root) throws JadxException {
- long startTime = System.currentTimeMillis();
- for (ClassNode cls : root.getClasses()) {
- processCls(cls);
- }
- if (LOG.isDebugEnabled()) {
- LOG.debug("OverrideMethod pass time: {}ms", System.currentTimeMillis() - startTime);
- }
+ public boolean visit(ClassNode cls) throws JadxException {
+ processCls(cls);
+ return true;
}
- public boolean processCls(ClassNode cls) {
+ private void processCls(ClassNode cls) {
List superTypes = collectSuperTypes(cls);
if (!superTypes.isEmpty()) {
for (MethodNode mth : cls.getMethods()) {
processMth(cls, superTypes, mth);
}
}
- return true;
}
private void processMth(ClassNode cls, List superTypes, MethodNode mth) {
diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/ProcessAnonymous.java b/jadx-core/src/main/java/jadx/core/dex/visitors/ProcessAnonymous.java
index 749e5eaca..f4fcfb075 100644
--- a/jadx-core/src/main/java/jadx/core/dex/visitors/ProcessAnonymous.java
+++ b/jadx-core/src/main/java/jadx/core/dex/visitors/ProcessAnonymous.java
@@ -5,6 +5,7 @@ import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode;
+import jadx.core.utils.exceptions.JadxException;
@JadxVisitor(
name = "ProcessAnonymous",
@@ -12,19 +13,20 @@ import jadx.core.dex.nodes.RootNode;
)
public class ProcessAnonymous extends AbstractVisitor {
+ private boolean inlineAnonymous;
+
@Override
public void init(RootNode root) {
- if (root.getArgs().isInlineAnonymousClasses()) {
- for (ClassNode cls : root.getClasses(true)) {
- markAnonymousClass(cls);
- }
- }
+ inlineAnonymous = root.getArgs().isInlineAnonymousClasses();
}
- public static void runForClass(ClassNode cls) {
- if (cls.root().getArgs().isInlineAnonymousClasses()) {
- markAnonymousClass(cls);
+ @Override
+ public boolean visit(ClassNode cls) throws JadxException {
+ if (!inlineAnonymous) {
+ return false;
}
+ markAnonymousClass(cls);
+ return true;
}
private static void markAnonymousClass(ClassNode cls) {
diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/RenameVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/RenameVisitor.java
index 0c0a6b4f9..24c17da2a 100644
--- a/jadx-core/src/main/java/jadx/core/dex/visitors/RenameVisitor.java
+++ b/jadx-core/src/main/java/jadx/core/dex/visitors/RenameVisitor.java
@@ -7,8 +7,6 @@ import java.util.List;
import java.util.Set;
import org.jetbrains.annotations.Nullable;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import jadx.api.JadxArgs;
import jadx.core.Consts;
@@ -26,7 +24,6 @@ import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode;
public class RenameVisitor extends AbstractVisitor {
- private static final Logger LOG = LoggerFactory.getLogger(RenameVisitor.class);
@Override
public void init(RootNode root) {
@@ -34,11 +31,7 @@ public class RenameVisitor extends AbstractVisitor {
if (inputFiles.isEmpty()) {
return;
}
- long startTime = System.currentTimeMillis();
process(root);
- if (LOG.isDebugEnabled()) {
- LOG.debug("Rename pass time: {}ms", System.currentTimeMillis() - startTime);
- }
}
private void process(RootNode root) {
diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/BlockProcessor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/BlockProcessor.java
index 1c7d697e6..11d6800ec 100644
--- a/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/BlockProcessor.java
+++ b/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/BlockProcessor.java
@@ -749,7 +749,9 @@ public class BlockProcessor extends AbstractVisitor {
first = false;
} else {
for (InsnNode oldInsn : exitBlock.getInstructions()) {
- newRetBlock.getInstructions().add(oldInsn.copyWithoutSsa());
+ InsnNode copyInsn = oldInsn.copyWithoutSsa();
+ copyInsn.add(AFlag.SYNTHETIC);
+ newRetBlock.getInstructions().add(copyInsn);
}
}
BlockSplitter.replaceConnection(pred, exitBlock, newRetBlock);
diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/shrink/CodeShrinkVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/shrink/CodeShrinkVisitor.java
index c681087a6..04ccea061 100644
--- a/jadx-core/src/main/java/jadx/core/dex/visitors/shrink/CodeShrinkVisitor.java
+++ b/jadx-core/src/main/java/jadx/core/dex/visitors/shrink/CodeShrinkVisitor.java
@@ -6,7 +6,10 @@ import java.util.List;
import java.util.ListIterator;
import java.util.Set;
+import org.jetbrains.annotations.Nullable;
+
import jadx.core.dex.attributes.AFlag;
+import jadx.core.dex.attributes.AType;
import jadx.core.dex.instructions.InsnType;
import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.instructions.args.InsnWrapArg;
@@ -142,10 +145,6 @@ public class CodeShrinkVisitor extends AbstractVisitor {
}
private static boolean inline(MethodNode mth, RegisterArg arg, InsnNode insn, BlockNode block) {
- InsnNode parentInsn = arg.getParentInsn();
- if (parentInsn != null && parentInsn.getType() == InsnType.RETURN) {
- parentInsn.setSourceLine(insn.getSourceLine());
- }
if (insn.contains(AFlag.FORCE_ASSIGN_INLINE)) {
return assignInline(mth, arg, insn, block);
}
@@ -153,11 +152,27 @@ public class CodeShrinkVisitor extends AbstractVisitor {
InsnArg wrappedArg = arg.wrapInstruction(mth, insn, false);
boolean replaced = wrappedArg != null;
if (replaced) {
+ processCodeComment(insn, arg.getParentInsn());
InsnRemover.removeWithoutUnbind(mth, block, insn);
}
return replaced;
}
+ private static void processCodeComment(InsnNode insn, @Nullable InsnNode parentInsn) {
+ if (parentInsn == null) {
+ return;
+ }
+ if (parentInsn.getType() == InsnType.RETURN) {
+ parentInsn.setSourceLine(insn.getSourceLine());
+ if (parentInsn.contains(AFlag.SYNTHETIC)) {
+ parentInsn.setOffset(insn.getOffset());
+ parentInsn.rewriteAttributeFrom(insn, AType.CODE_COMMENTS);
+ return;
+ }
+ }
+ parentInsn.copyAttributeFrom(insn, AType.CODE_COMMENTS);
+ }
+
private static boolean canMoveBetweenBlocks(MethodNode mth, InsnNode assignInsn, BlockNode assignBlock,
BlockNode useBlock, InsnNode useInsn) {
if (!BlockUtils.isPathExists(assignBlock, useBlock)) {
diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/usage/UsageInfoVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/usage/UsageInfoVisitor.java
index 28e7a3d5b..8539dc563 100644
--- a/jadx-core/src/main/java/jadx/core/dex/visitors/usage/UsageInfoVisitor.java
+++ b/jadx-core/src/main/java/jadx/core/dex/visitors/usage/UsageInfoVisitor.java
@@ -1,8 +1,5 @@
package jadx.core.dex.visitors.usage;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
import jadx.api.plugins.input.data.ICodeReader;
import jadx.api.plugins.input.insns.InsnData;
import jadx.api.plugins.input.insns.Opcode;
@@ -27,17 +24,14 @@ import jadx.core.dex.visitors.RenameVisitor;
}
)
public class UsageInfoVisitor extends AbstractVisitor {
- private static final Logger LOG = LoggerFactory.getLogger(UsageInfoVisitor.class);
@Override
public void init(RootNode root) {
- long startTime = System.currentTimeMillis();
UsageInfo usageInfo = new UsageInfo(root);
for (ClassNode cls : root.getClasses()) {
processClass(cls, usageInfo);
}
usageInfo.apply();
- LOG.debug("Dependency collection done in {}ms", System.currentTimeMillis() - startTime);
}
private static void processClass(ClassNode cls, UsageInfo usageInfo) {
diff --git a/jadx-core/src/main/java/jadx/core/utils/CodeGenUtils.java b/jadx-core/src/main/java/jadx/core/utils/CodeGenUtils.java
index 042c3e0ad..b759c7aa4 100644
--- a/jadx-core/src/main/java/jadx/core/utils/CodeGenUtils.java
+++ b/jadx-core/src/main/java/jadx/core/utils/CodeGenUtils.java
@@ -2,24 +2,79 @@ package jadx.core.utils;
import java.util.List;
+import org.jetbrains.annotations.Nullable;
+
+import jadx.api.CodePosition;
import jadx.api.ICodeWriter;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.AttrNode;
+import jadx.core.dex.attributes.IAttributeNode;
import jadx.core.dex.attributes.nodes.RenameReasonAttr;
import jadx.core.dex.attributes.nodes.SourceFileAttr;
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.ClassNode;
+import jadx.core.dex.nodes.ICodeNode;
public class CodeGenUtils {
- public static void addComments(ICodeWriter code, AttrNode node) {
+ public static void addComments(ICodeWriter code, IAttributeNode node) {
List comments = node.getAll(AType.COMMENTS);
if (!comments.isEmpty()) {
comments.stream().distinct()
.forEach(comment -> code.startLine("/* ").addMultiLine(comment).add(" */"));
}
+ addCodeComments(code, node);
+ }
+
+ public static void addCodeComments(ICodeWriter code, @Nullable IAttributeNode node) {
+ if (node == null) {
+ return;
+ }
+ List comments = node.getAll(AType.CODE_COMMENTS);
+ if (comments.isEmpty()) {
+ return;
+ }
+ if (node instanceof ICodeNode) {
+ // for classes, fields and methods add on line before node declaration
+ code.startLine();
+ } else {
+ code.add(' ');
+ }
+ if (comments.size() == 1) {
+ String comment = comments.get(0);
+ if (!comment.contains("\n")) {
+ code.add("// ").add(comment);
+ return;
+ }
+ }
+ addMultiLineComment(code, comments);
+ }
+
+ private static void addMultiLineComment(ICodeWriter code, List comments) {
+ boolean first = true;
+ String indent = "";
+ Object lineAnn = null;
+ for (String comment : comments) {
+ for (String line : comment.split("\n")) {
+ if (first) {
+ first = false;
+ StringBuilder buf = code.getRawBuf();
+ int startLinePos = buf.lastIndexOf(ICodeWriter.NL) + 1;
+ indent = Utils.strRepeat(" ", buf.length() - startLinePos);
+ if (code.isMetadataSupported()) {
+ lineAnn = code.getRawAnnotations().get(new CodePosition(code.getLine()));
+ }
+ } else {
+ code.newLine().add(indent);
+ if (lineAnn != null) {
+ code.attachLineAnnotation(lineAnn);
+ }
+ }
+ code.add("// ").add(line);
+ }
+ }
}
public static void addRenamedComment(ICodeWriter code, AttrNode node, String origName) {
@@ -35,7 +90,13 @@ public class CodeGenUtils {
public static void addSourceFileInfo(ICodeWriter code, ClassNode node) {
SourceFileAttr sourceFileAttr = node.get(AType.SOURCE_FILE);
if (sourceFileAttr != null) {
- code.startLine("/* compiled from: ").add(sourceFileAttr.getFileName()).add(" */");
+ String fileName = sourceFileAttr.getFileName();
+ String topClsName = node.getTopParentClass().getClassInfo().getShortName();
+ if (topClsName.contains(fileName)) {
+ // ignore similar name
+ return;
+ }
+ code.startLine("/* compiled from: ").add(fileName).add(" */");
}
}
diff --git a/jadx-core/src/main/java/jadx/core/utils/GsonUtils.java b/jadx-core/src/main/java/jadx/core/utils/GsonUtils.java
new file mode 100644
index 000000000..00ec89c1d
--- /dev/null
+++ b/jadx-core/src/main/java/jadx/core/utils/GsonUtils.java
@@ -0,0 +1,35 @@
+package jadx.core.utils;
+
+import java.lang.reflect.Type;
+
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+
+public class GsonUtils {
+
+ public static InterfaceReplace interfaceReplace(Class replaceCls) {
+ return new InterfaceReplace<>(replaceCls);
+ }
+
+ private static final class InterfaceReplace implements JsonSerializer, JsonDeserializer {
+ private final Class replaceCls;
+
+ private InterfaceReplace(Class replaceCls) {
+ this.replaceCls = replaceCls;
+ }
+
+ @Override
+ public T deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
+ return context.deserialize(json, this.replaceCls);
+ }
+
+ @Override
+ public JsonElement serialize(T src, Type typeOfSrc, JsonSerializationContext context) {
+ return context.serialize(src, this.replaceCls);
+ }
+ }
+}
diff --git a/jadx-core/src/main/java/jadx/core/utils/RegionUtils.java b/jadx-core/src/main/java/jadx/core/utils/RegionUtils.java
index 4f57fd8a1..285aae96f 100644
--- a/jadx-core/src/main/java/jadx/core/utils/RegionUtils.java
+++ b/jadx-core/src/main/java/jadx/core/utils/RegionUtils.java
@@ -58,6 +58,28 @@ public class RegionUtils {
}
}
+ public static InsnNode getFirstInsn(IContainer container) {
+ if (container instanceof IBlock) {
+ IBlock block = (IBlock) container;
+ List insnList = block.getInstructions();
+ if (insnList.isEmpty()) {
+ return null;
+ }
+ return insnList.get(0);
+ } else if (container instanceof IBranchRegion) {
+ return null;
+ } else if (container instanceof IRegion) {
+ IRegion region = (IRegion) container;
+ List blocks = region.getSubBlocks();
+ if (blocks.isEmpty()) {
+ return null;
+ }
+ return getFirstInsn(blocks.get(0));
+ } else {
+ throw new JadxRuntimeException(unknownContainerType(container));
+ }
+ }
+
public static InsnNode getLastInsn(IContainer container) {
if (container instanceof IBlock) {
IBlock block = (IBlock) container;
diff --git a/jadx-core/src/main/java/jadx/core/utils/Utils.java b/jadx-core/src/main/java/jadx/core/utils/Utils.java
index d8bdb2513..54204d304 100644
--- a/jadx-core/src/main/java/jadx/core/utils/Utils.java
+++ b/jadx-core/src/main/java/jadx/core/utils/Utils.java
@@ -308,6 +308,23 @@ public class Utils {
return list.get(0);
}
+ @Nullable
+ public static T first(List list) {
+ if (list.isEmpty()) {
+ return null;
+ }
+ return list.get(0);
+ }
+
+ @Nullable
+ public static T first(Iterable list) {
+ Iterator it = list.iterator();
+ if (!it.hasNext()) {
+ return null;
+ }
+ return it.next();
+ }
+
@Nullable
public static T last(List list) {
if (list.isEmpty()) {
@@ -316,6 +333,20 @@ public class Utils {
return list.get(list.size() - 1);
}
+ @Nullable
+ public static T last(Iterable list) {
+ Iterator it = list.iterator();
+ if (!it.hasNext()) {
+ return null;
+ }
+ while (true) {
+ T next = it.next();
+ if (!it.hasNext()) {
+ return next;
+ }
+ }
+ }
+
public static T getOrElse(@Nullable T obj, T defaultObj) {
if (obj == null) {
return defaultObj;
diff --git a/jadx-core/src/test/java/jadx/tests/api/utils/assertj/JadxAssertions.java b/jadx-core/src/test/java/jadx/tests/api/utils/assertj/JadxAssertions.java
index 41913303d..7d49eee6c 100644
--- a/jadx-core/src/test/java/jadx/tests/api/utils/assertj/JadxAssertions.java
+++ b/jadx-core/src/test/java/jadx/tests/api/utils/assertj/JadxAssertions.java
@@ -7,13 +7,18 @@ import jadx.core.dex.nodes.ClassNode;
public class JadxAssertions extends Assertions {
- public static JadxClassNodeAssertions assertThat(ClassNode actual) {
- Assertions.assertThat(actual).isNotNull();
- return new JadxClassNodeAssertions(actual);
+ public static JadxClassNodeAssertions assertThat(ClassNode cls) {
+ Assertions.assertThat(cls).isNotNull();
+ return new JadxClassNodeAssertions(cls);
}
- public static JadxCodeAssertions assertThat(ICodeInfo actual) {
- Assertions.assertThat(actual).isNotNull();
- return new JadxCodeAssertions(actual.getCodeStr());
+ public static JadxCodeInfoAssertions assertThat(ICodeInfo codeInfo) {
+ Assertions.assertThat(codeInfo).isNotNull();
+ return new JadxCodeInfoAssertions(codeInfo);
+ }
+
+ public static JadxCodeAssertions assertThat(String code) {
+ Assertions.assertThat(code).isNotNull();
+ return new JadxCodeAssertions(code);
}
}
diff --git a/jadx-core/src/test/java/jadx/tests/api/utils/assertj/JadxClassNodeAssertions.java b/jadx-core/src/test/java/jadx/tests/api/utils/assertj/JadxClassNodeAssertions.java
index 4b14a2af6..abbe7f584 100644
--- a/jadx-core/src/test/java/jadx/tests/api/utils/assertj/JadxClassNodeAssertions.java
+++ b/jadx-core/src/test/java/jadx/tests/api/utils/assertj/JadxClassNodeAssertions.java
@@ -1,6 +1,7 @@
package jadx.tests.api.utils.assertj;
import org.assertj.core.api.AbstractObjectAssert;
+import org.assertj.core.api.Assertions;
import jadx.api.ICodeInfo;
import jadx.core.dex.nodes.ClassNode;
@@ -13,10 +14,17 @@ public class JadxClassNodeAssertions extends AbstractObjectAssert {
+ public JadxCodeInfoAssertions(ICodeInfo cls) {
+ super(cls, JadxCodeInfoAssertions.class);
+ }
+
+ public JadxCodeAssertions code() {
+ isNotNull();
+ String codeStr = actual.getCodeStr();
+ assertThat(codeStr).isNotBlank();
+ return new JadxCodeAssertions(codeStr);
+ }
+
+ public JadxCodeInfoAssertions checkCodeOffsets() {
+ long dupOffsetCount = actual.getAnnotations().values().stream()
+ .filter(o -> o instanceof ICodeRawOffset)
+ .collect(Collectors.groupingBy(o -> ((ICodeRawOffset) o).getOffset(), Collectors.toList()))
+ .values().stream()
+ .filter(list -> list.size() > 1)
+ .count();
+ assertThat(dupOffsetCount)
+ .describedAs("Found duplicated code offsets")
+ .isEqualTo(0);
+ return this;
+ }
+}
diff --git a/jadx-core/src/test/java/jadx/tests/integration/others/TestClassReGen.java b/jadx-core/src/test/java/jadx/tests/integration/others/TestClassReGen.java
index 29a743d42..11685e960 100644
--- a/jadx-core/src/test/java/jadx/tests/integration/others/TestClassReGen.java
+++ b/jadx-core/src/test/java/jadx/tests/integration/others/TestClassReGen.java
@@ -23,7 +23,8 @@ public class TestClassReGen extends IntegrationTest {
@Test
public void test() {
ClassNode cls = getClassNode(TestCls.class);
- assertThat(cls.getCode())
+ assertThat(cls)
+ .code()
.containsOnlyOnce("private int intField = 5;")
.containsOnlyOnce("public static class A {")
.containsOnlyOnce("public int test() {");
@@ -32,8 +33,8 @@ public class TestClassReGen extends IntegrationTest {
cls.searchMethodByShortName("test").getMethodInfo().setAlias("testRenamed");
cls.searchFieldByName("intField").getFieldInfo().setAlias("intFieldRenamed");
- assertThat(cls.reloadCode())
- .print()
+ assertThat(cls)
+ .reloadCode(this)
.containsOnlyOnce("private int intFieldRenamed = 5;")
.containsOnlyOnce("public static class ARenamed {")
.containsOnlyOnce("public int testRenamed() {");
diff --git a/jadx-core/src/test/java/jadx/tests/integration/others/TestCodeComments.java b/jadx-core/src/test/java/jadx/tests/integration/others/TestCodeComments.java
new file mode 100644
index 000000000..2ade28cce
--- /dev/null
+++ b/jadx-core/src/test/java/jadx/tests/integration/others/TestCodeComments.java
@@ -0,0 +1,71 @@
+package jadx.tests.integration.others;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import org.junit.jupiter.api.Test;
+
+import jadx.api.data.ICodeComment;
+import jadx.api.data.IJavaNodeRef.RefType;
+import jadx.api.data.impl.JadxCodeComment;
+import jadx.api.data.impl.JadxCodeData;
+import jadx.api.data.impl.JadxNodeRef;
+import jadx.core.dex.nodes.ClassNode;
+import jadx.tests.api.IntegrationTest;
+
+import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
+
+public class TestCodeComments extends IntegrationTest {
+
+ public static class TestCls {
+ private int intField = 5;
+
+ public static class A {
+ }
+
+ public int test() {
+ System.out.println("Hello");
+ System.out.println("comment");
+ return intField;
+ }
+ }
+
+ @Test
+ public void test() {
+ String baseClsId = TestCls.class.getName();
+ ICodeComment clsComment = new JadxCodeComment(JadxNodeRef.forCls(baseClsId), "class comment");
+ ICodeComment innerClsComment = new JadxCodeComment(JadxNodeRef.forCls(baseClsId + ".A"), "inner class comment");
+ ICodeComment fldComment = new JadxCodeComment(new JadxNodeRef(RefType.FIELD, baseClsId, "intField:I"), "field comment");
+ JadxNodeRef mthRef = new JadxNodeRef(RefType.METHOD, baseClsId, "test()I");
+ ICodeComment mthComment = new JadxCodeComment(mthRef, "method comment");
+ ICodeComment insnComment = new JadxCodeComment(mthRef, "insn comment", 11);
+
+ JadxCodeData codeData = new JadxCodeData();
+ getArgs().setCodeData(codeData);
+ codeData.setComments(Arrays.asList(clsComment, innerClsComment, fldComment, mthComment, insnComment));
+
+ ClassNode cls = getClassNode(TestCls.class);
+ assertThat(cls)
+ .decompile()
+ .checkCodeOffsets()
+ .code()
+ .containsOne("// class comment")
+ .containsOne("// inner class comment")
+ .containsOne("// field comment")
+ .containsOne("// method comment")
+ .containsOne("System.out.println(\"comment\"); // insn comment");
+
+ String code = cls.getCode().getCodeStr();
+ assertThat(cls)
+ .reloadCode(this)
+ .isEqualTo(code);
+
+ ICodeComment updInsnComment = new JadxCodeComment(mthRef, "updated insn comment", 11);
+ codeData.setComments(Collections.singletonList(updInsnComment));
+ assertThat(cls)
+ .reloadCode(this)
+ .containsOne("System.out.println(\"comment\"); // updated insn comment")
+ .doesNotContain("class comment")
+ .containsOne(" comment");
+ }
+}
diff --git a/jadx-core/src/test/java/jadx/tests/integration/others/TestCodeComments2.java b/jadx-core/src/test/java/jadx/tests/integration/others/TestCodeComments2.java
new file mode 100644
index 000000000..063c338d9
--- /dev/null
+++ b/jadx-core/src/test/java/jadx/tests/integration/others/TestCodeComments2.java
@@ -0,0 +1,46 @@
+package jadx.tests.integration.others;
+
+import java.util.Arrays;
+
+import org.junit.jupiter.api.Test;
+
+import jadx.api.data.ICodeComment;
+import jadx.api.data.IJavaNodeRef.RefType;
+import jadx.api.data.impl.JadxCodeComment;
+import jadx.api.data.impl.JadxCodeData;
+import jadx.api.data.impl.JadxNodeRef;
+import jadx.tests.api.IntegrationTest;
+
+import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
+
+public class TestCodeComments2 extends IntegrationTest {
+
+ public static class TestCls {
+ public int test(boolean z) {
+ if (z) {
+ System.out.println("z");
+ return 1;
+ }
+ return 3;
+ }
+ }
+
+ @Test
+ public void test() {
+ String baseClsId = TestCls.class.getName();
+ JadxNodeRef mthRef = new JadxNodeRef(RefType.METHOD, baseClsId, "test(Z)I");
+ ICodeComment insnComment = new JadxCodeComment(mthRef, "return comment", 10);
+ ICodeComment insnComment2 = new JadxCodeComment(mthRef, "another return comment", 11);
+
+ JadxCodeData codeData = new JadxCodeData();
+ codeData.setComments(Arrays.asList(insnComment, insnComment2));
+ getArgs().setCodeData(codeData);
+
+ assertThat(getClassNode(TestCls.class))
+ .decompile()
+ .checkCodeOffsets()
+ .code()
+ .containsOne("// " + insnComment.getComment())
+ .containsOne("// " + insnComment2.getComment());
+ }
+}
diff --git a/jadx-core/src/test/java/jadx/tests/integration/others/TestCodeComments2a.java b/jadx-core/src/test/java/jadx/tests/integration/others/TestCodeComments2a.java
new file mode 100644
index 000000000..01fbe28f2
--- /dev/null
+++ b/jadx-core/src/test/java/jadx/tests/integration/others/TestCodeComments2a.java
@@ -0,0 +1,49 @@
+package jadx.tests.integration.others;
+
+import java.util.Arrays;
+import java.util.Random;
+
+import org.junit.jupiter.api.Test;
+
+import jadx.api.data.ICodeComment;
+import jadx.api.data.IJavaNodeRef.RefType;
+import jadx.api.data.impl.JadxCodeComment;
+import jadx.api.data.impl.JadxCodeData;
+import jadx.api.data.impl.JadxNodeRef;
+import jadx.tests.api.IntegrationTest;
+
+import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
+
+public class TestCodeComments2a extends IntegrationTest {
+
+ public static class TestCls {
+ private int f;
+
+ public int test(boolean z) {
+ if (z) {
+ System.out.println("z");
+ return new Random().nextInt();
+ }
+ return f;
+ }
+ }
+
+ @Test
+ public void test() {
+ String baseClsId = TestCls.class.getName();
+ JadxNodeRef mthRef = new JadxNodeRef(RefType.METHOD, baseClsId, "test(Z)I");
+ ICodeComment insnComment = new JadxCodeComment(mthRef, "return comment", 18);
+ ICodeComment insnComment2 = new JadxCodeComment(mthRef, "another return comment", 19);
+
+ JadxCodeData codeData = new JadxCodeData();
+ codeData.setComments(Arrays.asList(insnComment, insnComment2));
+ getArgs().setCodeData(codeData);
+
+ assertThat(getClassNode(TestCls.class))
+ .decompile()
+ .checkCodeOffsets()
+ .code()
+ .containsOne("// " + insnComment.getComment())
+ .containsOne("// " + insnComment2.getComment());
+ }
+}
diff --git a/jadx-core/src/test/java/jadx/tests/integration/others/TestCodeCommentsMultiline.java b/jadx-core/src/test/java/jadx/tests/integration/others/TestCodeCommentsMultiline.java
new file mode 100644
index 000000000..3cf158cd5
--- /dev/null
+++ b/jadx-core/src/test/java/jadx/tests/integration/others/TestCodeCommentsMultiline.java
@@ -0,0 +1,44 @@
+package jadx.tests.integration.others;
+
+import java.util.Collections;
+
+import org.junit.jupiter.api.Test;
+
+import jadx.api.data.ICodeComment;
+import jadx.api.data.IJavaNodeRef.RefType;
+import jadx.api.data.impl.JadxCodeComment;
+import jadx.api.data.impl.JadxCodeData;
+import jadx.api.data.impl.JadxNodeRef;
+import jadx.tests.api.IntegrationTest;
+
+import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
+
+public class TestCodeCommentsMultiline extends IntegrationTest {
+
+ public static class TestCls {
+ public int test(boolean z) {
+ if (z) {
+ System.out.println("z");
+ return 1;
+ }
+ return 3;
+ }
+ }
+
+ @Test
+ public void test() {
+ String baseClsId = TestCls.class.getName();
+ JadxNodeRef mthRef = new JadxNodeRef(RefType.METHOD, baseClsId, "test(Z)I");
+ ICodeComment insnComment = new JadxCodeComment(mthRef, "multi\nline\ncomment", 11);
+
+ JadxCodeData codeData = new JadxCodeData();
+ codeData.setComments(Collections.singletonList(insnComment));
+ getArgs().setCodeData(codeData);
+
+ assertThat(getClassNode(TestCls.class))
+ .code()
+ .containsOne("// multi")
+ .containsOne("// line")
+ .containsOne("// comment");
+ }
+}
diff --git a/jadx-core/src/test/java/jadx/tests/integration/others/TestCodeCommentsOverride.java b/jadx-core/src/test/java/jadx/tests/integration/others/TestCodeCommentsOverride.java
new file mode 100644
index 000000000..0e45c00a1
--- /dev/null
+++ b/jadx-core/src/test/java/jadx/tests/integration/others/TestCodeCommentsOverride.java
@@ -0,0 +1,60 @@
+package jadx.tests.integration.others;
+
+import java.util.Arrays;
+
+import org.junit.jupiter.api.Test;
+
+import jadx.api.data.ICodeComment;
+import jadx.api.data.IJavaNodeRef.RefType;
+import jadx.api.data.impl.JadxCodeComment;
+import jadx.api.data.impl.JadxCodeData;
+import jadx.api.data.impl.JadxNodeRef;
+import jadx.core.dex.nodes.ClassNode;
+import jadx.tests.api.IntegrationTest;
+
+import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
+
+public class TestCodeCommentsOverride extends IntegrationTest {
+
+ public static class TestCls {
+ public interface I {
+ void mth();
+ }
+
+ public static class A implements I {
+ @Override
+ public void mth() {
+ System.out.println("mth");
+ }
+ }
+ }
+
+ @Test
+ public void test() {
+ String baseClsId = TestCls.class.getName();
+ JadxNodeRef iMthRef = new JadxNodeRef(RefType.METHOD, baseClsId + ".I", "mth()V");
+ ICodeComment iMthComment = new JadxCodeComment(iMthRef, "interface mth comment");
+
+ JadxNodeRef mthRef = new JadxNodeRef(RefType.METHOD, baseClsId + ".A", "mth()V");
+ ICodeComment mthComment = new JadxCodeComment(mthRef, "mth comment");
+
+ JadxCodeData codeData = new JadxCodeData();
+ codeData.setComments(Arrays.asList(iMthComment, mthComment));
+ getArgs().setCodeData(codeData);
+
+ ClassNode cls = getClassNode(TestCls.class);
+ assertThat(cls)
+ .decompile()
+ .checkCodeOffsets()
+ .code()
+ .containsOne("@Override")
+ .containsOne("// " + iMthComment.getComment())
+ .containsOne("// " + mthComment.getComment());
+
+ assertThat(cls)
+ .reloadCode(this)
+ .containsOne("@Override")
+ .containsOne("// " + iMthComment.getComment())
+ .containsOne("// " + mthComment.getComment());
+ }
+}
diff --git a/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java b/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java
index d59038d55..84f67e849 100644
--- a/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java
+++ b/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java
@@ -20,6 +20,7 @@ import jadx.api.JadxDecompiler;
import jadx.api.JavaClass;
import jadx.api.JavaPackage;
import jadx.api.ResourceFile;
+import jadx.gui.settings.JadxProject;
import jadx.gui.settings.JadxSettings;
import static jadx.gui.utils.FileUtils.toFiles;
@@ -29,6 +30,7 @@ public class JadxWrapper {
private final JadxSettings settings;
private JadxDecompiler decompiler;
+ private JadxProject project;
private List openPaths = Collections.emptyList();
public JadxWrapper(JadxSettings settings) {
@@ -41,6 +43,7 @@ public class JadxWrapper {
try {
JadxArgs jadxArgs = settings.toJadxArgs();
jadxArgs.setInputFiles(toFiles(paths));
+ jadxArgs.setCodeData(project.getCodeData());
this.decompiler = new JadxDecompiler(jadxArgs);
this.decompiler.load();
@@ -155,10 +158,14 @@ public class JadxWrapper {
return decompiler.getArgs();
}
+ public void setProject(JadxProject project) {
+ this.project = project;
+ }
+
/**
* @param fullName Full name of an outer class. Inner classes are not supported.
*/
- public @Nullable JavaClass searchJavaClassByClassName(String fullName) {
+ public @Nullable JavaClass searchJavaClassByFullAlias(String fullName) {
return decompiler.getClasses().stream()
.filter(cls -> cls.getFullName().equals(fullName))
.findFirst()
@@ -166,10 +173,7 @@ public class JadxWrapper {
}
public @Nullable JavaClass searchJavaClassByOrigClassName(String fullName) {
- return decompiler.getClasses().stream()
- .filter(cls -> cls.getClassNode().getClassInfo().getFullName().equals(fullName))
- .findFirst()
- .orElse(null);
+ return decompiler.searchJavaClassByOrigFullName(fullName);
}
/**
diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/IndexJob.java b/jadx-gui/src/main/java/jadx/gui/jobs/IndexJob.java
index 323b31b0a..43a2c60d8 100644
--- a/jadx-gui/src/main/java/jadx/gui/jobs/IndexJob.java
+++ b/jadx-gui/src/main/java/jadx/gui/jobs/IndexJob.java
@@ -20,6 +20,7 @@ import jadx.gui.utils.search.TextSearchIndex;
public class IndexJob extends BackgroundJob {
private static final Logger LOG = LoggerFactory.getLogger(IndexJob.class);
+
private final CacheObject cache;
public IndexJob(JadxWrapper wrapper, CacheObject cache, int threadsCount) {
@@ -29,18 +30,14 @@ public class IndexJob extends BackgroundJob {
@Override
protected void runJob() {
- TextSearchIndex index = new TextSearchIndex(cache);
- CodeUsageInfo usageInfo = new CodeUsageInfo(cache.getNodeCache());
-
- cache.setTextIndex(index);
- cache.setUsageInfo(usageInfo);
+ TextSearchIndex index = cache.getTextIndex();
addTask(index::indexResource);
for (final JavaClass cls : wrapper.getIncludedClasses()) {
addTask(() -> indexCls(cache, cls));
}
}
- public static void indexCls(CacheObject cache, JavaClass cls) {
+ private static void indexCls(CacheObject cache, JavaClass cls) {
try {
TextSearchIndex index = cache.getTextIndex();
CodeUsageInfo usageInfo = cache.getUsageInfo();
diff --git a/jadx-gui/src/main/java/jadx/gui/settings/JadxProject.java b/jadx-gui/src/main/java/jadx/gui/settings/JadxProject.java
index 7a2020d78..c91b571bc 100644
--- a/jadx-gui/src/main/java/jadx/gui/settings/JadxProject.java
+++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxProject.java
@@ -1,11 +1,12 @@
package jadx.gui.settings;
-import java.io.BufferedWriter;
+import java.io.Reader;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Iterator;
import java.util.List;
import org.slf4j.Logger;
@@ -14,63 +15,76 @@ import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
+import jadx.api.data.ICodeComment;
+import jadx.api.data.IJavaNodeRef;
+import jadx.api.data.impl.JadxCodeComment;
+import jadx.api.data.impl.JadxCodeData;
+import jadx.api.data.impl.JadxNodeRef;
+import jadx.core.utils.GsonUtils;
+import jadx.core.utils.exceptions.JadxRuntimeException;
+import jadx.gui.ui.MainWindow;
import jadx.gui.utils.PathTypeAdapter;
public class JadxProject {
-
private static final Logger LOG = LoggerFactory.getLogger(JadxProject.class);
- private static final int CURRENT_SETTINGS_VERSION = 0;
+ private static final int CURRENT_PROJECT_VERSION = 1;
public static final String PROJECT_EXTENSION = "jadx";
private static final Gson GSON = new GsonBuilder()
.registerTypeHierarchyAdapter(Path.class, PathTypeAdapter.singleton())
+ .registerTypeAdapter(ICodeComment.class, GsonUtils.interfaceReplace(JadxCodeComment.class))
+ .registerTypeAdapter(IJavaNodeRef.class, GsonUtils.interfaceReplace(JadxNodeRef.class))
+ .setPrettyPrinting()
.create();
+ private transient MainWindow mainWindow;
private transient JadxSettings settings;
+
private transient String name = "New Project";
private transient Path projectPath;
- private List filesPath;
- private List treeExpansions = new ArrayList<>();
- private transient boolean saved;
private transient boolean initial = true;
+ private transient boolean saved;
- private int projectVersion = 0;
+ private List files;
+ private List treeExpansions = new ArrayList<>();
+ private JadxCodeData codeData = new JadxCodeData();
+
+ private int projectVersion;
- // Don't remove. Used in json serialization
public JadxProject() {
}
- public JadxProject(JadxSettings settings) {
- this.settings = settings;
- }
-
public void setSettings(JadxSettings settings) {
this.settings = settings;
}
+ public void setMainWindow(MainWindow mainWindow) {
+ this.mainWindow = mainWindow;
+ }
+
public Path getProjectPath() {
return projectPath;
}
private void setProjectPath(Path projectPath) {
this.projectPath = projectPath;
- if (projectVersion != CURRENT_SETTINGS_VERSION) {
- upgradeSettings(projectVersion);
- }
name = projectPath.getFileName().toString();
- name = name.substring(0, name.lastIndexOf('.'));
+ int dotPos = name.lastIndexOf('.');
+ if (dotPos != -1) {
+ name = name.substring(0, dotPos);
+ }
changed();
}
public List getFilePaths() {
- return filesPath;
+ return files;
}
public void setFilePath(List files) {
if (!files.equals(getFilePaths())) {
- this.filesPath = files;
+ this.files = files;
changed();
}
}
@@ -85,11 +99,7 @@ public class JadxProject {
}
public void removeTreeExpansion(String[] expansion) {
- for (Iterator it = treeExpansions.iterator(); it.hasNext();) {
- if (isParentOfExpansion(expansion, it.next())) {
- it.remove();
- }
- }
+ treeExpansions.removeIf(strings -> isParentOfExpansion(expansion, strings));
changed();
}
@@ -106,13 +116,25 @@ public class JadxProject {
return false;
}
+ public JadxCodeData getCodeData() {
+ return codeData;
+ }
+
+ public void setCodeData(JadxCodeData codeData) {
+ this.codeData = codeData;
+ changed();
+ }
+
private void changed() {
- if (settings.isAutoSaveProject()) {
+ if (settings != null && settings.isAutoSaveProject()) {
save();
} else {
saved = false;
}
initial = false;
+ if (mainWindow != null) {
+ mainWindow.updateProject(this);
+ }
}
public String getName() {
@@ -134,8 +156,8 @@ public class JadxProject {
public void save() {
if (getProjectPath() != null) {
- try (BufferedWriter writer = Files.newBufferedWriter(getProjectPath())) {
- writer.write(GSON.toJson(this));
+ try (Writer writer = Files.newBufferedWriter(getProjectPath(), StandardCharsets.UTF_8)) {
+ GSON.toJson(this, writer);
saved = true;
} catch (Exception e) {
LOG.error("Error saving project", e);
@@ -143,29 +165,29 @@ public class JadxProject {
}
}
- public static JadxProject from(Path path, JadxSettings settings) {
- try {
- List lines = Files.readAllLines(path);
-
- if (!lines.isEmpty()) {
- JadxProject project = GSON.fromJson(lines.get(0), JadxProject.class);
- project.settings = settings;
- project.setProjectPath(path);
- project.saved = true;
- return project;
- }
+ public static JadxProject from(Path path) {
+ try (Reader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
+ JadxProject project = GSON.fromJson(reader, JadxProject.class);
+ project.saved = true;
+ project.setProjectPath(path);
+ project.upgrade();
+ return project;
} catch (Exception e) {
LOG.error("Error loading project", e);
+ return null;
}
- return null;
}
- private void upgradeSettings(int fromVersion) {
- LOG.debug("upgrade settings from version: {} to {}", fromVersion, CURRENT_SETTINGS_VERSION);
+ private void upgrade() {
+ int fromVersion = projectVersion;
+ LOG.debug("upgrade settings from version: {} to {}", fromVersion, CURRENT_PROJECT_VERSION);
if (fromVersion == 0) {
fromVersion++;
}
- projectVersion = CURRENT_SETTINGS_VERSION;
+ if (fromVersion != CURRENT_PROJECT_VERSION) {
+ throw new JadxRuntimeException("Project update failed");
+ }
+ projectVersion = CURRENT_PROJECT_VERSION;
save();
}
}
diff --git a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java
index e1702cf76..ad8a9bc10 100644
--- a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java
+++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java
@@ -166,7 +166,10 @@ public class JadxSettings extends JadxCLIArgs {
return Collections.unmodifiableList(recentProjects);
}
- public void addRecentProject(Path projectPath) {
+ public void addRecentProject(@Nullable Path projectPath) {
+ if (projectPath == null) {
+ return;
+ }
recentProjects.remove(projectPath);
recentProjects.add(0, projectPath);
int count = recentProjects.size();
diff --git a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsAdapter.java b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsAdapter.java
index b861bb00d..fc4752aaa 100644
--- a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsAdapter.java
+++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsAdapter.java
@@ -64,9 +64,6 @@ public class JadxSettingsAdapter {
} else {
settings.fixOnLoad();
}
- if (LOG.isDebugEnabled()) {
- LOG.debug("Loaded settings: {}", makeString(settings));
- }
return settings;
} catch (Exception e) {
LOG.error("Error load settings. Settings will reset.\n Loaded json string: {}", jsonSettings, e);
@@ -77,7 +74,6 @@ public class JadxSettingsAdapter {
public static void store(JadxSettings settings) {
try {
String jsonSettings = makeString(settings);
- LOG.debug("Saving settings: {}", jsonSettings);
PREFS.put(JADX_GUI_KEY, jsonSettings);
PREFS.sync();
} catch (Exception e) {
diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/CodeNode.java b/jadx-gui/src/main/java/jadx/gui/treemodel/CodeNode.java
index 9442d6d82..a5885c2b6 100644
--- a/jadx-gui/src/main/java/jadx/gui/treemodel/CodeNode.java
+++ b/jadx-gui/src/main/java/jadx/gui/treemodel/CodeNode.java
@@ -1,6 +1,6 @@
package jadx.gui.treemodel;
-import javax.swing.*;
+import javax.swing.Icon;
import jadx.api.JavaNode;
import jadx.gui.utils.search.StringRef;
@@ -13,14 +13,14 @@ public class CodeNode extends JNode {
private final transient JClass jParent;
private final transient StringRef line;
private final transient int lineNum;
- private transient int pos = -1;
- private transient boolean precise;
+ private transient int pos;
- public CodeNode(JNode jNode, int lineNum, StringRef lineStr) {
+ public CodeNode(JNode jNode, StringRef lineStr, int lineNum, int pos) {
this.jNode = jNode;
this.jParent = this.jNode.getJParent();
this.line = lineStr;
this.lineNum = lineNum;
+ this.pos = pos;
}
@Override
@@ -79,6 +79,11 @@ public class CodeNode extends JNode {
return makeString();
}
+ @Override
+ public String getSyntaxName() {
+ return jNode.getSyntaxName();
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) {
@@ -96,24 +101,8 @@ public class CodeNode extends JNode {
return jNode.hashCode();
}
+ @Override
public int getPos() {
return pos;
}
-
- public CodeNode setPos(int pos) {
- this.pos = pos;
- return this;
- }
-
- public CodeNode setPrecisePos(int pos) {
- this.pos = pos;
- if (pos > -1) {
- this.precise = true;
- }
- return this;
- }
-
- public boolean isPrecisePos() {
- return precise;
- }
}
diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JField.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JField.java
index 83275615d..c84fdf43f 100644
--- a/jadx-gui/src/main/java/jadx/gui/treemodel/JField.java
+++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JField.java
@@ -3,6 +3,8 @@ package jadx.gui.treemodel;
import javax.swing.Icon;
import javax.swing.ImageIcon;
+import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
+
import jadx.api.JavaField;
import jadx.api.JavaNode;
import jadx.core.dex.attributes.AFlag;
@@ -71,6 +73,11 @@ public class JField extends JNode {
return icon;
}
+ @Override
+ public String getSyntaxName() {
+ return SyntaxConstants.SYNTAX_STYLE_JAVA;
+ }
+
@Override
public String makeString() {
return UiUtils.typeFormat(field.getName(), field.getType());
diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JMethod.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JMethod.java
index bf0ca7ce9..9c41d5cbb 100644
--- a/jadx-gui/src/main/java/jadx/gui/treemodel/JMethod.java
+++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JMethod.java
@@ -5,6 +5,8 @@ import java.util.Iterator;
import javax.swing.Icon;
import javax.swing.ImageIcon;
+import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
+
import jadx.api.JavaMethod;
import jadx.api.JavaNode;
import jadx.core.dex.attributes.AFlag;
@@ -73,6 +75,11 @@ public class JMethod extends JNode {
return icon;
}
+ @Override
+ public String getSyntaxName() {
+ return SyntaxConstants.SYNTAX_STYLE_JAVA;
+ }
+
@Override
public boolean canRename() {
return !mth.getMethodNode().contains(AFlag.DONT_RENAME);
diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JNode.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JNode.java
index 56f31fa94..9c21d4493 100644
--- a/jadx-gui/src/main/java/jadx/gui/treemodel/JNode.java
+++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JNode.java
@@ -101,6 +101,14 @@ public abstract class JNode extends DefaultMutableTreeNode {
return makeLongString();
}
+ public int getPos() {
+ JavaNode javaNode = getJavaNode();
+ if (javaNode == null) {
+ return -1;
+ }
+ return javaNode.getDefPos();
+ }
+
@Override
public String toString() {
return makeString();
diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JVariable.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JVariable.java
index 357bda908..c2a9d005e 100644
--- a/jadx-gui/src/main/java/jadx/gui/treemodel/JVariable.java
+++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JVariable.java
@@ -6,6 +6,8 @@ import jadx.api.JavaNode;
import jadx.api.JavaVariable;
public class JVariable extends JNode {
+ private static final long serialVersionUID = -3002100457834453783L;
+
JClass cls;
JavaVariable var;
diff --git a/jadx-gui/src/main/java/jadx/gui/ui/CommentDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/CommentDialog.java
new file mode 100644
index 000000000..7f357bd2e
--- /dev/null
+++ b/jadx-gui/src/main/java/jadx/gui/ui/CommentDialog.java
@@ -0,0 +1,247 @@
+package jadx.gui.ui;
+
+import java.awt.BorderLayout;
+import java.awt.Component;
+import java.awt.Container;
+import java.awt.Dialog;
+import java.awt.Dimension;
+import java.awt.event.KeyAdapter;
+import java.awt.event.KeyEvent;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+import javax.swing.BorderFactory;
+import javax.swing.Box;
+import javax.swing.BoxLayout;
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTextArea;
+import javax.swing.SwingConstants;
+import javax.swing.WindowConstants;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import jadx.api.data.ICodeComment;
+import jadx.api.data.impl.JadxCodeComment;
+import jadx.api.data.impl.JadxCodeData;
+import jadx.gui.settings.JadxProject;
+import jadx.gui.ui.codearea.CodeArea;
+import jadx.gui.utils.NLS;
+import jadx.gui.utils.TextStandardActions;
+import jadx.gui.utils.UiUtils;
+
+public class CommentDialog extends JDialog {
+ private static final long serialVersionUID = -1865682124935757528L;
+
+ private static final Logger LOG = LoggerFactory.getLogger(CommentDialog.class);
+
+ public static void show(CodeArea codeArea, ICodeComment blankComment) {
+ ICodeComment existComment = searchForExistComment(codeArea, blankComment);
+ Dialog dialog;
+ if (existComment != null) {
+ dialog = new CommentDialog(codeArea, existComment, true);
+ } else {
+ dialog = new CommentDialog(codeArea, blankComment, false);
+ }
+ dialog.setVisible(true);
+ }
+
+ private static void updateCommentsData(CodeArea codeArea, Consumer> updater) {
+ try {
+ JadxProject project = codeArea.getProject();
+ JadxCodeData codeData = project.getCodeData();
+ if (codeData == null) {
+ codeData = new JadxCodeData();
+ }
+ List list = new ArrayList<>(codeData.getComments());
+ updater.accept(list);
+ Collections.sort(list);
+ codeData.setComments(list);
+ project.setCodeData(codeData);
+ } catch (Exception e) {
+ LOG.error("Comment action failed", e);
+ }
+ try {
+ // refresh code
+ codeArea.refreshClass();
+ } catch (Exception e) {
+ LOG.error("Failed to reload code", e);
+ }
+ }
+
+ private static ICodeComment searchForExistComment(CodeArea codeArea, ICodeComment blankComment) {
+ try {
+ JadxProject project = codeArea.getProject();
+ JadxCodeData codeData = project.getCodeData();
+ if (codeData == null || codeData.getComments().isEmpty()) {
+ return null;
+ }
+ for (ICodeComment comment : codeData.getComments()) {
+ if (Objects.equals(comment.getNodeRef(), blankComment.getNodeRef())
+ && comment.getOffset() == blankComment.getOffset()
+ && comment.getAttachType() == blankComment.getAttachType()) {
+ return comment;
+ }
+ }
+ } catch (Exception e) {
+ LOG.error("Error searching for exists comment", e);
+ }
+ return null;
+ }
+
+ private final transient CodeArea codeArea;
+ private final transient ICodeComment comment;
+ private final transient boolean updateComment;
+
+ private transient JTextArea commentArea;
+
+ public CommentDialog(CodeArea codeArea, ICodeComment comment, boolean updateComment) {
+ super(codeArea.getMainWindow());
+ this.codeArea = codeArea;
+ this.comment = comment;
+ this.updateComment = updateComment;
+ initUI();
+ }
+
+ private void apply() {
+ String newCommentStr = commentArea.getText().trim();
+ if (newCommentStr.isEmpty()) {
+ if (updateComment) {
+ remove();
+ } else {
+ cancel();
+ }
+ return;
+ }
+ ICodeComment newComment = new JadxCodeComment(comment.getNodeRef(),
+ newCommentStr, comment.getOffset(), comment.getAttachType());
+ if (updateComment) {
+ updateCommentsData(codeArea, list -> {
+ list.remove(comment);
+ list.add(newComment);
+ });
+ } else {
+ updateCommentsData(codeArea, list -> list.add(newComment));
+ }
+ dispose();
+ }
+
+ private void remove() {
+ updateCommentsData(codeArea, list -> list.removeIf(c -> c == comment));
+ dispose();
+ }
+
+ private void cancel() {
+ dispose();
+ }
+
+ private void initUI() {
+ commentArea = new JTextArea();
+ TextStandardActions.attach(commentArea);
+ commentArea.setEditable(true);
+ commentArea.setFont(codeArea.getMainWindow().getSettings().getFont());
+ commentArea.setAlignmentX(Component.LEFT_ALIGNMENT);
+
+ commentArea.addKeyListener(new KeyAdapter() {
+ @Override
+ public void keyPressed(KeyEvent e) {
+ switch (e.getKeyCode()) {
+ case KeyEvent.VK_ENTER:
+ if (e.isShiftDown() || e.isControlDown()) {
+ commentArea.append("\n");
+ } else {
+ apply();
+ }
+ break;
+
+ case KeyEvent.VK_ESCAPE:
+ cancel();
+ break;
+ }
+ }
+ });
+ if (updateComment) {
+ commentArea.setText(comment.getComment());
+ }
+
+ JScrollPane textAreaScrollPane = new JScrollPane(commentArea);
+ textAreaScrollPane.setAlignmentX(LEFT_ALIGNMENT);
+
+ JLabel commentLabel = new JLabel(NLS.str("comment_dialog.label"), SwingConstants.LEFT);
+ JLabel usageLabel = new JLabel(NLS.str("comment_dialog.usage"), SwingConstants.LEFT);
+
+ JPanel mainPanel = new JPanel();
+ mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.PAGE_AXIS));
+ mainPanel.add(commentLabel);
+ mainPanel.add(Box.createRigidArea(new Dimension(0, 5)));
+ mainPanel.add(textAreaScrollPane);
+ mainPanel.add(Box.createRigidArea(new Dimension(0, 5)));
+ mainPanel.add(usageLabel);
+ mainPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
+
+ JPanel buttonPane = initButtonsPanel();
+
+ Container contentPane = getContentPane();
+ contentPane.add(mainPanel, BorderLayout.CENTER);
+ contentPane.add(buttonPane, BorderLayout.PAGE_END);
+
+ if (updateComment) {
+ setTitle(NLS.str("comment_dialog.title.update"));
+ } else {
+ setTitle(NLS.str("comment_dialog.title.add"));
+ }
+ pack();
+ if (!codeArea.getMainWindow().getSettings().loadWindowPos(this)) {
+ setSize(800, 140);
+ }
+ setLocationRelativeTo(null);
+ setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
+ setModalityType(ModalityType.APPLICATION_MODAL);
+ UiUtils.addEscapeShortCutToDispose(this);
+ }
+
+ protected JPanel initButtonsPanel() {
+ JButton cancelButton = new JButton(NLS.str("common_dialog.cancel"));
+ cancelButton.addActionListener(event -> cancel());
+
+ String applyStr = updateComment ? NLS.str("common_dialog.update") : NLS.str("common_dialog.add");
+ JButton renameBtn = new JButton(applyStr);
+ renameBtn.addActionListener(event -> apply());
+ getRootPane().setDefaultButton(renameBtn);
+
+ JButton removeBtn;
+ if (updateComment) {
+ removeBtn = new JButton(NLS.str("common_dialog.remove"));
+ removeBtn.addActionListener(event -> remove());
+ } else {
+ removeBtn = null;
+ }
+
+ JPanel buttonPane = new JPanel();
+ buttonPane.setLayout(new BoxLayout(buttonPane, BoxLayout.LINE_AXIS));
+ buttonPane.setBorder(BorderFactory.createEmptyBorder(0, 10, 10, 10));
+ buttonPane.add(Box.createRigidArea(new Dimension(5, 0)));
+ buttonPane.add(Box.createHorizontalGlue());
+ buttonPane.add(renameBtn);
+ if (removeBtn != null) {
+ buttonPane.add(Box.createRigidArea(new Dimension(10, 0)));
+ buttonPane.add(removeBtn);
+ }
+ buttonPane.add(Box.createRigidArea(new Dimension(10, 0)));
+ buttonPane.add(cancelButton);
+ return buttonPane;
+ }
+
+ @Override
+ public void dispose() {
+ codeArea.getMainWindow().getSettings().saveWindowPos(this);
+ super.dispose();
+ }
+}
diff --git a/jadx-gui/src/main/java/jadx/gui/ui/CommonSearchDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/CommonSearchDialog.java
index 0f22db7f4..84078b303 100644
--- a/jadx-gui/src/main/java/jadx/gui/ui/CommonSearchDialog.java
+++ b/jadx-gui/src/main/java/jadx/gui/ui/CommonSearchDialog.java
@@ -1,6 +1,11 @@
package jadx.gui.ui;
-import java.awt.*;
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Cursor;
+import java.awt.Dimension;
+import java.awt.Font;
+import java.awt.Rectangle;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
@@ -13,14 +18,27 @@ import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
-import javax.swing.*;
+import javax.swing.BorderFactory;
+import javax.swing.Box;
+import javax.swing.BoxLayout;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import javax.swing.ListSelectionModel;
+import javax.swing.ScrollPaneConstants;
+import javax.swing.SwingConstants;
+import javax.swing.SwingUtilities;
+import javax.swing.SwingWorker;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
-import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
import org.fife.ui.rtextarea.SearchContext;
import org.fife.ui.rtextarea.SearchEngine;
import org.jetbrains.annotations.NotNull;
@@ -31,7 +49,8 @@ import org.slf4j.LoggerFactory;
import jadx.gui.jobs.BackgroundJob;
import jadx.gui.jobs.BackgroundWorker;
import jadx.gui.jobs.DecompileJob;
-import jadx.gui.treemodel.*;
+import jadx.gui.treemodel.JNode;
+import jadx.gui.treemodel.JResSearchNode;
import jadx.gui.ui.codearea.AbstractCodeArea;
import jadx.gui.utils.CacheObject;
import jadx.gui.utils.JumpPosition;
@@ -112,16 +131,9 @@ public abstract class CommonSearchDialog extends JDialog {
JumpPosition jmpPos;
JNode node = (JNode) resultsModel.getValueAt(selectedId, 0);
if (node instanceof JResSearchNode) {
- jmpPos = new JumpPosition(((JResSearchNode) node).getResNode(), node.getLine())
- .setPrecise(((JResSearchNode) node).getPos());
- } else if (node instanceof CodeNode) {
- CodeNode codeNode = (CodeNode) node;
- jmpPos = new JumpPosition(node.getRootClass(), node.getLine(), codeNode.getPos());
- if (codeNode.isPrecisePos()) {
- jmpPos.setPrecise(codeNode.getPos());
- }
+ jmpPos = new JumpPosition(((JResSearchNode) node).getResNode(), node.getLine(), node.getPos());
} else {
- jmpPos = new JumpPosition(node.getRootClass(), node.getLine());
+ jmpPos = new JumpPosition(node.getRootClass(), node.getLine(), node.getPos());
}
tabbedPane.codeJump(jmpPos);
if (!mainWindow.getSettings().getKeepCommonDialogOpen()) {
@@ -285,11 +297,11 @@ public abstract class CommonSearchDialog extends JDialog {
int firstColMaxWidth = (int) (width * 0.5);
int rowCount = getRowCount();
int columnCount = getColumnCount();
- if (!model.isAddDescColumn()) {
+ boolean addDescColumn = model.isAddDescColumn();
+ if (!addDescColumn) {
firstColMaxWidth = width;
}
- Component nodeComp = null;
- Component codeComp = null;
+ setRowHeight(10); // reset all rows height
for (int col = 0; col < columnCount; col++) {
int colWidth = 50;
for (int row = 0; row < rowCount; row++) {
@@ -298,11 +310,9 @@ public abstract class CommonSearchDialog extends JDialog {
continue;
}
colWidth = Math.max(comp.getPreferredSize().width, colWidth);
- if (nodeComp == null && col == 0) {
- nodeComp = comp;
- }
- if (codeComp == null && col == 1) {
- codeComp = comp;
+ int h = Math.max(getRowHeight(row), getHeight(comp));
+ if (h > 1) {
+ setRowHeight(row, h);
}
}
colWidth += 10;
@@ -314,10 +324,7 @@ public abstract class CommonSearchDialog extends JDialog {
TableColumn column = columnModel.getColumn(col);
column.setPreferredWidth(colWidth);
}
- // setRowHeight(Math.max(nodeComp.getPreferredSize().height, codeComp.getPreferredSize().height +
- // 4));
updateUI();
- setRowHeight(Math.max(getHeight(nodeComp), getHeight(codeComp) + 4));
}
private int getHeight(@Nullable Component nodeComp) {
@@ -487,20 +494,25 @@ public abstract class CommonSearchDialog extends JDialog {
if (!node.hasDescString()) {
return emptyLabel;
}
+
RSyntaxTextArea textArea = AbstractCodeArea.getDefaultArea(mainWindow);
- textArea.setLayout(new GridLayout(1, 1));
- textArea.setEditable(false);
- textArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVA);
- textArea.setText(" " + node.makeDescString());
- textArea.setRows(1);
- textArea.setColumns(textArea.getText().length() + 1);
+ textArea.setSyntaxEditingStyle(node.getSyntaxName());
+ String descStr = node.makeDescString();
+ textArea.setText(descStr);
+ if (descStr.contains("\n")) {
+ textArea.setRows(textArea.getLineCount());
+ } else {
+ textArea.setRows(1);
+ textArea.setColumns(descStr.length() + 1);
+ }
if (highlightText != null) {
SearchContext searchContext = new SearchContext(highlightText);
searchContext.setMatchCase(!highlightTextCaseInsensitive);
- searchContext.setMarkAll(true);
searchContext.setRegularExpression(highlightTextUseRegex);
+ searchContext.setMarkAll(true);
SearchEngine.markAll(textArea, searchContext);
}
+ textArea.setBorder(BorderFactory.createEmptyBorder(0, 10, 0, 10));
return textArea;
}
diff --git a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java
index 1e8025fdc..fd7678ab0 100644
--- a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java
+++ b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java
@@ -106,12 +106,15 @@ import jadx.gui.update.JadxUpdate;
import jadx.gui.update.JadxUpdate.IUpdateCallback;
import jadx.gui.update.data.Release;
import jadx.gui.utils.CacheObject;
+import jadx.gui.utils.CodeUsageInfo;
import jadx.gui.utils.FontUtils;
import jadx.gui.utils.JumpPosition;
import jadx.gui.utils.Link;
import jadx.gui.utils.NLS;
import jadx.gui.utils.SystemInfo;
import jadx.gui.utils.UiUtils;
+import jadx.gui.utils.search.CommentsIndex;
+import jadx.gui.utils.search.TextSearchIndex;
import static io.reactivex.internal.functions.Functions.EMPTY_RUNNABLE;
import static jadx.gui.utils.FileUtils.fileNamesToPaths;
@@ -137,6 +140,7 @@ public class MainWindow extends JFrame {
private static final ImageIcon ICON_FLAT_PKG = UiUtils.openIcon("empty_logical_package_obj");
private static final ImageIcon ICON_SEARCH = UiUtils.openIcon("wand");
private static final ImageIcon ICON_FIND = UiUtils.openIcon("magnifier");
+ private static final ImageIcon ICON_COMMENT_SEARCH = UiUtils.openIcon("table_edit");
private static final ImageIcon ICON_BACK = UiUtils.openIcon("icon_back");
private static final ImageIcon ICON_FORWARD = UiUtils.openIcon("icon_forward");
private static final ImageIcon ICON_PREF = UiUtils.openIcon("wrench");
@@ -219,7 +223,7 @@ public class MainWindow extends JFrame {
private void handleSelectClassOption() {
if (settings.getCmdSelectClass() != null) {
- JavaNode javaNode = wrapper.searchJavaClassByClassName(settings.getCmdSelectClass());
+ JavaNode javaNode = wrapper.searchJavaClassByFullAlias(settings.getCmdSelectClass());
if (javaNode == null) {
javaNode = wrapper.searchJavaClassByOrigClassName(settings.getCmdSelectClass());
}
@@ -230,8 +234,7 @@ public class MainWindow extends JFrame {
return;
}
JNode node = cacheObject.getNodeCache().makeFrom(javaNode);
- tabbedPane.codeJump(new JumpPosition(node.getRootClass(), node.getLine())
- .setPrecise(JumpPosition.getDefPos(node)));
+ tabbedPane.codeJump(new JumpPosition(node.getRootClass(), node.getLine(), JumpPosition.getDefPos(node)));
}
}
@@ -311,9 +314,10 @@ public class MainWindow extends JFrame {
if (!ensureProjectIsSaved()) {
return;
}
- project = new JadxProject(settings);
- update();
+ cancelBackgroundJobs();
clearTree();
+ wrapper.close();
+ updateProject(new JadxProject());
}
private void saveProject() {
@@ -356,6 +360,7 @@ public class MainWindow extends JFrame {
}
}
project.saveAs(path);
+ settings.addRecentProject(path);
update();
}
}
@@ -408,18 +413,18 @@ public class MainWindow extends JFrame {
if (!ensureProjectIsSaved()) {
return;
}
- project = JadxProject.from(path, settings);
- if (project == null) {
+ JadxProject jadxProject = JadxProject.from(path);
+ if (jadxProject == null) {
JOptionPane.showMessageDialog(
this,
NLS.str("msg.project_error"),
NLS.str("msg.project_error_title"),
JOptionPane.INFORMATION_MESSAGE);
- return;
+ jadxProject = new JadxProject();
}
- update();
+ updateProject(jadxProject);
settings.addRecentProject(path);
- List filePaths = project.getFilePaths();
+ List filePaths = jadxProject.getFilePaths();
if (filePaths == null) {
clearTree();
} else {
@@ -427,6 +432,15 @@ public class MainWindow extends JFrame {
}
}
+ public void updateProject(JadxProject jadxProject) {
+ jadxProject.setSettings(settings);
+ jadxProject.setMainWindow(this);
+ this.project = jadxProject;
+ this.wrapper.setProject(jadxProject);
+ this.cacheObject.setCommentsIndex(new CommentsIndex(wrapper, cacheObject, jadxProject));
+ update();
+ }
+
private void update() {
newProjectAction.setEnabled(!project.isInitial());
saveProjectAction.setEnabled(!project.isSaved());
@@ -444,17 +458,14 @@ public class MainWindow extends JFrame {
protected void resetCache() {
cacheObject.reset();
- // TODO: decompilation freezes sometime with several threads
- this.cacheObject.setJRoot(treeRoot);
- this.cacheObject.setJadxSettings(settings);
+ cacheObject.setJRoot(treeRoot);
+ cacheObject.setJadxSettings(settings);
+
int threadsCount = settings.getThreadsCount();
cacheObject.setDecompileJob(new DecompileJob(wrapper, threadsCount));
cacheObject.setIndexJob(new IndexJob(wrapper, cacheObject, threadsCount));
- }
-
- public void resetIndex() {
- int threadsCount = settings.getThreadsCount();
- cacheObject.setIndexJob(new IndexJob(wrapper, cacheObject, threadsCount));
+ cacheObject.setUsageInfo(new CodeUsageInfo(cacheObject.getNodeCache()));
+ cacheObject.setTextIndex(new TextSearchIndex(this));
}
synchronized void runBackgroundJobs() {
@@ -471,7 +482,9 @@ public class MainWindow extends JFrame {
}
public synchronized void cancelBackgroundJobs() {
- backgroundExecutor.cancelAll();
+ if (backgroundExecutor != null) {
+ backgroundExecutor.cancelAll();
+ }
if (backgroundWorker != null) {
backgroundWorker.stop();
backgroundWorker = new BackgroundWorker(cacheObject, progressPane);
@@ -518,8 +531,7 @@ public class MainWindow extends JFrame {
continue;
}
JNode newNode = cacheObject.getNodeCache().makeFrom(newClass);
- tabbedPane.codeJump(new JumpPosition(newNode, position)
- .setPrecise(JumpPosition.getDefPos(newNode)));
+ tabbedPane.codeJump(new JumpPosition(newNode, position, JumpPosition.getDefPos(newNode)));
}
}
@@ -651,12 +663,7 @@ public class MainWindow extends JFrame {
} else if (obj instanceof ApkSignature) {
tabbedPane.showSimpleNode((JNode) obj);
} else if (obj instanceof JNode) {
- JNode node = (JNode) obj;
- JClass cls = node.getRootClass();
- if (cls != null) {
- tabbedPane.codeJump(new JumpPosition(cls, node.getLine())
- .setPrecise(JumpPosition.getDefPos(node)));
- }
+ tabbedPane.codeJump(new JumpPosition((JNode) obj));
}
} catch (Exception e) {
LOG.error("Content loading error", e);
@@ -829,7 +836,7 @@ public class MainWindow extends JFrame {
return;
}
}
- new SearchDialog(MainWindow.this, true).setVisible(true);
+ SearchDialog.search(MainWindow.this, SearchDialog.SearchPreset.TEXT);
}
};
textSearchAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("menu.text_search"));
@@ -839,12 +846,22 @@ public class MainWindow extends JFrame {
Action clsSearchAction = new AbstractAction(NLS.str("menu.class_search"), ICON_FIND) {
@Override
public void actionPerformed(ActionEvent e) {
- new SearchDialog(MainWindow.this, false).setVisible(true);
+ SearchDialog.search(MainWindow.this, SearchDialog.SearchPreset.CLASS);
}
};
clsSearchAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("menu.class_search"));
clsSearchAction.putValue(Action.ACCELERATOR_KEY, getKeyStroke(KeyEvent.VK_N, UiUtils.ctrlButton()));
+ Action commentSearchAction = new AbstractAction(NLS.str("menu.comment_search"), ICON_COMMENT_SEARCH) {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ SearchDialog.search(MainWindow.this, SearchDialog.SearchPreset.COMMENT);
+ }
+ };
+ commentSearchAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("menu.comment_search"));
+ commentSearchAction.putValue(Action.ACCELERATOR_KEY, getKeyStroke(KeyEvent.VK_SEMICOLON,
+ UiUtils.ctrlButton() | KeyEvent.SHIFT_DOWN_MASK));
+
Action deobfAction = new AbstractAction(NLS.str("menu.deobfuscation"), ICON_DEOBF) {
@Override
public void actionPerformed(ActionEvent e) {
@@ -925,6 +942,7 @@ public class MainWindow extends JFrame {
nav.setMnemonic(KeyEvent.VK_N);
nav.add(textSearchAction);
nav.add(clsSearchAction);
+ nav.add(commentSearchAction);
nav.addSeparator();
nav.add(backAction);
nav.add(forwardAction);
@@ -969,6 +987,7 @@ public class MainWindow extends JFrame {
toolbar.addSeparator();
toolbar.add(textSearchAction);
toolbar.add(clsSearchAction);
+ toolbar.add(commentSearchAction);
toolbar.addSeparator();
toolbar.add(backAction);
toolbar.add(forwardAction);
@@ -1202,6 +1221,10 @@ public class MainWindow extends JFrame {
return wrapper;
}
+ public JadxProject getProject() {
+ return project;
+ }
+
public TabbedPane getTabbedPane() {
return tabbedPane;
}
diff --git a/jadx-gui/src/main/java/jadx/gui/ui/RenameDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/RenameDialog.java
index 3e5b9d4df..419090bf3 100644
--- a/jadx-gui/src/main/java/jadx/gui/ui/RenameDialog.java
+++ b/jadx-gui/src/main/java/jadx/gui/ui/RenameDialog.java
@@ -4,7 +4,11 @@ import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.FlowLayout;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
import java.util.stream.Collectors;
import javax.swing.BorderFactory;
@@ -23,7 +27,11 @@ import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import jadx.api.*;
+import jadx.api.ICodeWriter;
+import jadx.api.JavaClass;
+import jadx.api.JavaField;
+import jadx.api.JavaMethod;
+import jadx.api.JavaNode;
import jadx.core.deobf.DeobfPresets;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.MethodOverrideAttr;
@@ -35,10 +43,19 @@ import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.gui.jobs.IndexJob;
import jadx.gui.settings.JadxSettings;
-import jadx.gui.treemodel.*;
+import jadx.gui.treemodel.JClass;
+import jadx.gui.treemodel.JField;
+import jadx.gui.treemodel.JMethod;
+import jadx.gui.treemodel.JNode;
+import jadx.gui.treemodel.JPackage;
+import jadx.gui.treemodel.JVariable;
import jadx.gui.ui.codearea.ClassCodeContentPanel;
-import jadx.gui.ui.codearea.CodePanel;
-import jadx.gui.utils.*;
+import jadx.gui.ui.codearea.CodeArea;
+import jadx.gui.utils.CacheObject;
+import jadx.gui.utils.JNodeCache;
+import jadx.gui.utils.NLS;
+import jadx.gui.utils.TextStandardActions;
+import jadx.gui.utils.UiUtils;
public class RenameDialog extends JDialog {
private static final long serialVersionUID = -3269715644416902410L;
@@ -235,17 +252,11 @@ public class RenameDialog extends JDialog {
private void refreshTabs(TabbedPane tabbedPane, Set updatedClasses) {
for (Map.Entry entry : tabbedPane.getOpenTabs().entrySet()) {
- ContentPanel contentPanel = entry.getValue();
- if (contentPanel instanceof ClassCodeContentPanel) {
- JNode node = entry.getKey();
- JClass rootClass = node.getRootClass();
- if (updatedClasses.contains(rootClass)) {
- refreshJClass(rootClass);
- ClassCodeContentPanel codePanel = (ClassCodeContentPanel) contentPanel;
- CodePanel javaPanel = codePanel.getJavaCodePanel();
- javaPanel.refresh();
- tabbedPane.refresh(rootClass);
- }
+ JClass rootClass = entry.getKey().getRootClass();
+ if (updatedClasses.remove(rootClass)) {
+ ClassCodeContentPanel contentPanel = (ClassCodeContentPanel) entry.getValue();
+ CodeArea codeArea = (CodeArea) contentPanel.getJavaCodePanel().getCodeArea();
+ codeArea.refreshClass();
}
}
}
@@ -254,7 +265,7 @@ public class RenameDialog extends JDialog {
protected JPanel initButtonsPanel() {
JButton cancelButton = new JButton(NLS.str("search_dialog.cancel"));
cancelButton.addActionListener(event -> dispose());
- JButton renameBtn = new JButton(NLS.str("popup.rename"));
+ JButton renameBtn = new JButton(NLS.str("common_dialog.ok"));
renameBtn.addActionListener(event -> rename());
getRootPane().setDefaultButton(renameBtn);
diff --git a/jadx-gui/src/main/java/jadx/gui/ui/SearchDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/SearchDialog.java
index 5082d85f9..f201bba90 100644
--- a/jadx-gui/src/main/java/jadx/gui/ui/SearchDialog.java
+++ b/jadx-gui/src/main/java/jadx/gui/ui/SearchDialog.java
@@ -1,13 +1,26 @@
package jadx.gui.ui;
-import java.awt.*;
+import java.awt.BorderLayout;
+import java.awt.Container;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
+import java.util.Collections;
import java.util.EnumSet;
+import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
-import javax.swing.*;
+import javax.swing.BorderFactory;
+import javax.swing.Box;
+import javax.swing.BoxLayout;
+import javax.swing.JCheckBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JTextField;
+import javax.swing.WindowConstants;
+import javax.swing.event.ChangeListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
@@ -21,109 +34,195 @@ import io.reactivex.Flowable;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
-import jadx.core.utils.StringUtils;
import jadx.gui.treemodel.JNode;
import jadx.gui.utils.NLS;
import jadx.gui.utils.TextStandardActions;
+import jadx.gui.utils.layout.WrapLayout;
import jadx.gui.utils.search.TextSearchIndex;
public class SearchDialog extends CommonSearchDialog {
+ private static final long serialVersionUID = -5105405456969134105L;
private static final Logger LOG = LoggerFactory.getLogger(SearchDialog.class);
- private static final long serialVersionUID = -5105405456969134105L;
- private final boolean textSearch;
+
+ public static void search(MainWindow window, SearchPreset preset) {
+ SearchDialog searchDialog = new SearchDialog(window, preset, Collections.emptySet());
+ searchDialog.setVisible(true);
+ }
+
+ public static void searchInActiveTab(MainWindow window, SearchPreset preset) {
+ SearchDialog searchDialog = new SearchDialog(window, preset, EnumSet.of(SearchOptions.ACTIVE_TAB));
+ searchDialog.setVisible(true);
+ }
+
+ public static void searchText(MainWindow window, String text) {
+ SearchDialog searchDialog = new SearchDialog(window, SearchPreset.TEXT, Collections.emptySet());
+ searchDialog.initSearchText = text;
+ searchDialog.setVisible(true);
+ }
+
+ public enum SearchPreset {
+ TEXT, CLASS, COMMENT
+ }
public enum SearchOptions {
CLASS,
METHOD,
FIELD,
CODE,
+ RESOURCE,
+ COMMENT,
+
IGNORE_CASE,
USE_REGEX,
- Resource
+ ACTIVE_TAB
}
- private transient Set options;
+ private final transient SearchPreset searchPreset;
+ private final transient Set options;
private transient JTextField searchField;
private transient Disposable searchDisposable;
private transient SearchEventEmitter searchEmitter;
- private transient String text = null;
+ private transient ChangeListener activeTabListener;
- public SearchDialog(MainWindow mainWindow, boolean textSearch) {
+ private transient String initSearchText = null;
+
+ private SearchDialog(MainWindow mainWindow, SearchPreset preset, Set additionalOptions) {
super(mainWindow);
- this.textSearch = textSearch;
- if (textSearch) {
- Set lastSearchOptions = cache.getLastSearchOptions();
- if (!lastSearchOptions.isEmpty()) {
- this.options = lastSearchOptions;
- } else {
- this.options = EnumSet.of(SearchOptions.CODE, SearchOptions.IGNORE_CASE);
- }
- } else {
- this.options = EnumSet.of(SearchOptions.CLASS);
- }
+ this.searchPreset = preset;
+ this.options = buildOptions(preset);
+ this.options.addAll(additionalOptions);
initUI();
+ searchFieldSubscribe();
registerInitOnOpen();
loadWindowPos();
+ registerActiveTabListener();
+ }
+
+ @Override
+ public void dispose() {
+ if (searchDisposable != null && !searchDisposable.isDisposed()) {
+ searchDisposable.dispose();
+ }
+ removeActiveTabListener();
+ super.dispose();
+ }
+
+ private Set buildOptions(SearchPreset preset) {
+ Set searchOptions = cache.getLastSearchOptions().get(preset);
+ if (searchOptions == null) {
+ searchOptions = new HashSet<>();
+ }
+ switch (preset) {
+ case TEXT:
+ if (searchOptions.isEmpty()) {
+ searchOptions.add(SearchOptions.CODE);
+ searchOptions.add(SearchOptions.IGNORE_CASE);
+ }
+ break;
+
+ case CLASS:
+ searchOptions.add(SearchOptions.CLASS);
+ break;
+
+ case COMMENT:
+ searchOptions.add(SearchOptions.COMMENT);
+ searchOptions.remove(SearchOptions.ACTIVE_TAB);
+ break;
+ }
+ return searchOptions;
}
@Override
protected void openInit() {
- prepare();
- String lastSearch = cache.getLastSearch();
- if (lastSearch != null) {
- searchField.setText(lastSearch);
+ String searchText = initSearchText != null ? initSearchText : cache.getLastSearch();
+ if (searchText != null) {
+ searchField.setText(searchText);
searchField.selectAll();
}
searchField.requestFocus();
+
+ if (searchField.getText().isEmpty()) {
+ checkIndex();
+ }
+ searchEmitter.emitSearch();
+ }
+
+ private TextSearchIndex checkIndex() {
+ if (!cache.getIndexJob().isComplete()) {
+ if (isFullIndexNeeded()) {
+ prepare();
+ }
+ }
+ return cache.getTextIndex();
+ }
+
+ private boolean isFullIndexNeeded() {
+ for (SearchOptions option : options) {
+ switch (option) {
+ case CLASS:
+ case METHOD:
+ case FIELD:
+ // TODO: split indexes so full decompilation not needed for these
+ return true;
+
+ case CODE:
+ return true;
+
+ case RESOURCE:
+ case COMMENT:
+ // full index not needed
+ break;
+ }
+ }
+ return false;
}
private void initUI() {
- JLabel findLabel = new JLabel(NLS.str("search_dialog.open_by_name"));
searchField = new JTextField();
searchField.setAlignmentX(LEFT_ALIGNMENT);
- new TextStandardActions(searchField);
- searchFieldSubscribe();
+ TextStandardActions.attach(searchField);
- JCheckBox caseChBox = makeOptionsCheckBox(NLS.str("search_dialog.ignorecase"), SearchOptions.IGNORE_CASE);
- JCheckBox regexChBox = makeOptionsCheckBox(NLS.str("search_dialog.regex"), SearchOptions.USE_REGEX);
+ JLabel findLabel = new JLabel(NLS.str("search_dialog.open_by_name"));
+ findLabel.setAlignmentX(LEFT_ALIGNMENT);
- JCheckBox resChBox = makeOptionsCheckBox(NLS.str("search_dialog.resource"), SearchOptions.Resource);
- JCheckBox clsChBox = makeOptionsCheckBox(NLS.str("search_dialog.class"), SearchOptions.CLASS);
- JCheckBox mthChBox = makeOptionsCheckBox(NLS.str("search_dialog.method"), SearchOptions.METHOD);
- JCheckBox fldChBox = makeOptionsCheckBox(NLS.str("search_dialog.field"), SearchOptions.FIELD);
- JCheckBox codeChBox = makeOptionsCheckBox(NLS.str("search_dialog.code"), SearchOptions.CODE);
+ JPanel searchFieldPanel = new JPanel();
+ searchFieldPanel.setLayout(new BoxLayout(searchFieldPanel, BoxLayout.PAGE_AXIS));
+ searchFieldPanel.setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 5));
+ searchFieldPanel.setAlignmentX(LEFT_ALIGNMENT);
+ searchFieldPanel.add(findLabel);
+ searchFieldPanel.add(Box.createRigidArea(new Dimension(0, 5)));
+ searchFieldPanel.add(searchField);
JPanel searchInPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
searchInPanel.setBorder(BorderFactory.createTitledBorder(NLS.str("search_dialog.search_in")));
- searchInPanel.add(resChBox);
- searchInPanel.add(clsChBox);
- searchInPanel.add(mthChBox);
- searchInPanel.add(fldChBox);
- searchInPanel.add(codeChBox);
+ searchInPanel.add(makeOptionsCheckBox(NLS.str("search_dialog.class"), SearchOptions.CLASS));
+ searchInPanel.add(makeOptionsCheckBox(NLS.str("search_dialog.method"), SearchOptions.METHOD));
+ searchInPanel.add(makeOptionsCheckBox(NLS.str("search_dialog.field"), SearchOptions.FIELD));
+ searchInPanel.add(makeOptionsCheckBox(NLS.str("search_dialog.code"), SearchOptions.CODE));
+ searchInPanel.add(makeOptionsCheckBox(NLS.str("search_dialog.resource"), SearchOptions.RESOURCE));
+ searchInPanel.add(makeOptionsCheckBox(NLS.str("search_dialog.comments"), SearchOptions.COMMENT));
JPanel searchOptions = new JPanel(new FlowLayout(FlowLayout.LEFT));
searchOptions.setBorder(BorderFactory.createTitledBorder(NLS.str("search_dialog.options")));
- searchOptions.add(caseChBox);
- searchOptions.add(regexChBox);
+ searchOptions.add(makeOptionsCheckBox(NLS.str("search_dialog.ignorecase"), SearchOptions.IGNORE_CASE));
+ searchOptions.add(makeOptionsCheckBox(NLS.str("search_dialog.regex"), SearchOptions.USE_REGEX));
+ searchOptions.add(makeOptionsCheckBox(NLS.str("search_dialog.active_tab"), SearchOptions.ACTIVE_TAB));
- Box box = Box.createHorizontalBox();
- box.setAlignmentX(LEFT_ALIGNMENT);
- box.add(searchInPanel);
- box.add(searchOptions);
+ JPanel optionsPanel = new JPanel(new WrapLayout(WrapLayout.LEFT));
+ optionsPanel.setAlignmentX(LEFT_ALIGNMENT);
+ optionsPanel.add(searchInPanel);
+ optionsPanel.add(searchOptions);
JPanel searchPane = new JPanel();
searchPane.setLayout(new BoxLayout(searchPane, BoxLayout.PAGE_AXIS));
- findLabel.setLabelFor(searchField);
- searchPane.add(findLabel);
- searchPane.add(Box.createRigidArea(new Dimension(0, 5)));
- searchPane.add(searchField);
- searchPane.add(Box.createRigidArea(new Dimension(0, 5)));
- searchPane.add(box);
searchPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
+ searchPane.add(searchFieldPanel);
+ searchPane.add(Box.createRigidArea(new Dimension(0, 5)));
+ searchPane.add(optionsPanel);
initCommon();
JPanel resultsPanel = initResultsTable();
@@ -181,9 +280,7 @@ public class SearchDialog extends CommonSearchDialog {
Flowable textChanges = onTextFieldChanges(searchField);
Flowable searchEvents = Flowable.merge(textChanges, searchEmitter.getFlowable());
searchDisposable = searchEvents
- .filter(text -> text.length() > 0)
.subscribeOn(Schedulers.single())
- .doOnNext(r -> LOG.debug("search event: {}", r))
.switchMap(text -> prepareSearch(text)
.doOnError(e -> LOG.error("Error prepare search: {}", e.getMessage(), e))
.subscribeOn(Schedulers.single())
@@ -195,13 +292,19 @@ public class SearchDialog extends CommonSearchDialog {
}
private Flowable prepareSearch(String text) {
- if (text == null || text.isEmpty() || options.isEmpty()) {
+ if (text == null || options.isEmpty()) {
return Flowable.empty();
}
- TextSearchIndex index = cache.getTextIndex();
+ // allow empty text for comments search
+ if (text.isEmpty() && !options.contains(SearchOptions.COMMENT)) {
+ return Flowable.empty();
+ }
+
+ TextSearchIndex index = checkIndex();
if (index == null) {
return Flowable.empty();
}
+ LOG.debug("search event: {}", text);
showSearchState();
return index.buildSearch(text, options);
}
@@ -214,9 +317,7 @@ public class SearchDialog extends CommonSearchDialog {
highlightTextUseRegex = options.contains(SearchOptions.USE_REGEX);
cache.setLastSearch(text);
- if (textSearch) {
- cache.setLastSearchOptions(options);
- }
+ cache.getLastSearchOptions().put(searchPreset, options);
resultsModel.clear();
resultsModel.addAll(results);
@@ -266,14 +367,6 @@ public class SearchDialog extends CommonSearchDialog {
.distinctUntilChanged();
}
- @Override
- public void dispose() {
- if (searchDisposable != null && !searchDisposable.isDisposed()) {
- searchDisposable.dispose();
- }
- super.dispose();
- }
-
private JCheckBox makeOptionsCheckBox(String name, final SearchOptions opt) {
final JCheckBox chBox = new JCheckBox(name);
chBox.setAlignmentX(LEFT_ALIGNMENT);
@@ -291,23 +384,32 @@ public class SearchDialog extends CommonSearchDialog {
@Override
protected void loadFinished() {
- if (!StringUtils.isEmpty(text)) {
- searchField.setText(text);
- }
resultsTable.setEnabled(true);
searchField.setEnabled(true);
+ searchEmitter.emitSearch();
}
@Override
protected void loadStart() {
- text = cache.getLastSearch(); // SearchDialog is opened by menu item, let loadFinished to set text
- cache.setLastSearch("");
resultsTable.setEnabled(false);
searchField.setEnabled(false);
}
- public static void searchText(MainWindow window, String text) {
- window.getCacheObject().setLastSearch(text);
- new SearchDialog(window, true).setVisible(true);
+ private void registerActiveTabListener() {
+ removeActiveTabListener();
+ activeTabListener = e -> {
+ if (options.contains(SearchOptions.ACTIVE_TAB)) {
+ LOG.debug("active tab change event received");
+ searchEmitter.emitSearch();
+ }
+ };
+ mainWindow.getTabbedPane().addChangeListener(activeTabListener);
+ }
+
+ private void removeActiveTabListener() {
+ if (activeTabListener != null) {
+ mainWindow.getTabbedPane().removeChangeListener(activeTabListener);
+ activeTabListener = null;
+ }
}
}
diff --git a/jadx-gui/src/main/java/jadx/gui/ui/TabbedPane.java b/jadx-gui/src/main/java/jadx/gui/ui/TabbedPane.java
index 15565be24..ae5e0452d 100644
--- a/jadx-gui/src/main/java/jadx/gui/ui/TabbedPane.java
+++ b/jadx-gui/src/main/java/jadx/gui/ui/TabbedPane.java
@@ -1,11 +1,19 @@
package jadx.gui.ui;
-import java.awt.*;
-import java.awt.event.*;
-import java.util.*;
+import java.awt.Component;
+import java.awt.KeyEventDispatcher;
+import java.awt.KeyboardFocusManager;
+import java.awt.event.FocusEvent;
+import java.awt.event.FocusListener;
+import java.awt.event.KeyEvent;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
+import java.util.Objects;
-import javax.swing.*;
+import javax.swing.JTabbedPane;
+import javax.swing.SwingUtilities;
import javax.swing.text.BadLocationException;
import org.jetbrains.annotations.Nullable;
@@ -19,14 +27,17 @@ import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.gui.treemodel.ApkSignature;
import jadx.gui.treemodel.JNode;
import jadx.gui.treemodel.JResource;
-import jadx.gui.ui.codearea.*;
+import jadx.gui.ui.codearea.AbstractCodeArea;
+import jadx.gui.ui.codearea.AbstractCodeContentPanel;
+import jadx.gui.ui.codearea.ClassCodeContentPanel;
+import jadx.gui.ui.codearea.CodeContentPanel;
import jadx.gui.utils.JumpManager;
import jadx.gui.utils.JumpPosition;
public class TabbedPane extends JTabbedPane {
+ private static final long serialVersionUID = -8833600618794570904L;
private static final Logger LOG = LoggerFactory.getLogger(TabbedPane.class);
- private static final long serialVersionUID = -8833600618794570904L;
private final transient MainWindow mainWindow;
private final transient Map openTabs = new LinkedHashMap<>();
@@ -148,42 +159,42 @@ public class TabbedPane extends JTabbedPane {
return mainWindow;
}
- private void showCode(final JumpPosition pos) {
- final AbstractCodeContentPanel contentPanel = (AbstractCodeContentPanel) getContentPanel(pos.getNode());
+ private void showCode(final JumpPosition jumpPos) {
+ JNode jumpNode = jumpPos.getNode();
+ Objects.requireNonNull(jumpNode, "Null node in JumpPosition");
+
+ final AbstractCodeContentPanel contentPanel = (AbstractCodeContentPanel) getContentPanel(jumpNode);
if (contentPanel == null) {
return;
}
SwingUtilities.invokeLater(() -> {
setSelectedComponent(contentPanel);
AbstractCodeArea codeArea = contentPanel.getCodeArea();
- if (pos.isPrecise()) {
- codeArea.scrollToPos(pos.getPos());
+ int pos = jumpPos.getPos();
+ if (pos > 0) {
+ codeArea.scrollToPos(pos);
} else {
- int line = pos.getLine();
+ int line = jumpPos.getLine();
if (line < 0) {
try {
line = 1 + codeArea.getLineOfOffset(-line);
} catch (BadLocationException e) {
- LOG.error("Can't get line for: {}", pos, e);
- line = pos.getNode().getLine();
+ LOG.error("Can't get line for: {}", jumpPos, e);
+ line = jumpNode.getLine();
}
}
- if (pos.getPos() < 0) {
- codeArea.scrollToLine(line);
- } else {
- int lineNum = Math.max(0, line - 1);
- try {
- int offs = codeArea.getLineStartOffset(lineNum);
- while (StringUtils.isWhite(codeArea.getText(offs, 1).charAt(0))) {
- offs += 1;
- }
- offs += pos.getPos();
- pos.setPrecise(offs);
- codeArea.scrollToPos(offs);
- } catch (BadLocationException e) {
- e.printStackTrace();
- codeArea.scrollToLine(line);
+ int lineNum = Math.max(0, line - 1);
+ try {
+ int offs = codeArea.getLineStartOffset(lineNum);
+ while (StringUtils.isWhite(codeArea.getText(offs, 1).charAt(0))) {
+ offs += 1;
}
+ offs += pos;
+ jumpPos.setPos(offs);
+ codeArea.scrollToPos(offs);
+ } catch (BadLocationException e) {
+ e.printStackTrace();
+ codeArea.scrollToLine(line);
}
}
codeArea.requestFocus();
@@ -216,7 +227,7 @@ public class TabbedPane extends JTabbedPane {
}
@Nullable
- JumpPosition getCurrentPosition() {
+ public JumpPosition getCurrentPosition() {
ContentPanel selectedCodePanel = getSelectedCodePanel();
if (selectedCodePanel instanceof AbstractCodeContentPanel) {
return ((AbstractCodeContentPanel) selectedCodePanel).getCodeArea().getCurrentPosition();
@@ -273,6 +284,7 @@ public class TabbedPane extends JTabbedPane {
ContentPanel panel = openTabs.get(node);
if (panel != null) {
setTabComponentAt(indexOfComponent(panel), makeTabComponent(panel));
+ fireStateChanged();
}
}
diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java
index 0d3e88f1d..72a81fda6 100644
--- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java
+++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java
@@ -1,14 +1,25 @@
package jadx.gui.ui.codearea;
-import java.awt.*;
-import java.awt.event.*;
+import java.awt.Dimension;
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.event.ActionEvent;
+import java.awt.event.FocusEvent;
+import java.awt.event.FocusListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
-import javax.swing.*;
+import javax.swing.AbstractAction;
+import javax.swing.JCheckBoxMenuItem;
+import javax.swing.JPopupMenu;
+import javax.swing.JViewport;
+import javax.swing.SwingUtilities;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.event.PopupMenuEvent;
-import javax.swing.event.PopupMenuListener;
-import javax.swing.text.*;
+import javax.swing.text.BadLocationException;
+import javax.swing.text.Caret;
+import javax.swing.text.DefaultCaret;
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
import org.fife.ui.rtextarea.SearchContext;
@@ -19,9 +30,11 @@ import org.slf4j.LoggerFactory;
import jadx.core.utils.StringUtils;
import jadx.gui.settings.JadxSettings;
+import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.ContentPanel;
import jadx.gui.ui.MainWindow;
+import jadx.gui.utils.DefaultPopupMenuListener;
import jadx.gui.utils.JumpPosition;
import jadx.gui.utils.NLS;
@@ -66,21 +79,11 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea {
}
});
popupMenu.add(wrapItem);
- popupMenu.addPopupMenuListener(new PopupMenuListener() {
+ popupMenu.addPopupMenuListener(new DefaultPopupMenuListener() {
@Override
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
wrapItem.setState(getLineWrap());
}
-
- @Override
- public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
-
- }
-
- @Override
- public void popupMenuCanceled(PopupMenuEvent e) {
-
- }
});
Caret caret = getCaret();
@@ -305,9 +308,14 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea {
}
public JumpPosition getCurrentPosition() {
- JumpPosition jp = new JumpPosition(node, getCaretLineNumber() + 1);
- jp.setPrecise(getCaretPosition());
- return jp;
+ return new JumpPosition(node, getCaretLineNumber() + 1, getCaretPosition());
+ }
+
+ public String getLineText(int line) throws BadLocationException {
+ int lineNum = line - 1;
+ int startOffset = getLineStartOffset(lineNum);
+ int endOffset = getLineEndOffset(lineNum);
+ return getText(startOffset, endOffset - startOffset);
}
@Nullable
@@ -322,4 +330,12 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea {
public JNode getNode() {
return node;
}
+
+ @Nullable
+ public JClass getJClass() {
+ if (node instanceof JClass) {
+ return (JClass) node;
+ }
+ return null;
+ }
}
diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/ClassCodeContentPanel.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/ClassCodeContentPanel.java
index cbfc6c9b9..cf465c4c9 100644
--- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/ClassCodeContentPanel.java
+++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/ClassCodeContentPanel.java
@@ -1,8 +1,8 @@
package jadx.gui.ui.codearea;
-import java.awt.*;
+import java.awt.BorderLayout;
-import javax.swing.*;
+import javax.swing.JTabbedPane;
import javax.swing.border.EmptyBorder;
import jadx.gui.treemodel.JNode;
@@ -86,5 +86,4 @@ public final class ClassCodeContentPanel extends AbstractCodeContentPanel {
public AbstractCodeArea getSmaliCodeArea() {
return smaliCodePanel.getCodeArea();
}
-
}
diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java
index 4e2048774..7697b8540 100644
--- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java
+++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java
@@ -1,9 +1,12 @@
package jadx.gui.ui.codearea;
-import java.awt.*;
-import java.awt.event.*;
+import java.awt.Point;
+import java.awt.event.InputEvent;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
-import javax.swing.*;
+import javax.swing.JPopupMenu;
+import javax.swing.event.PopupMenuEvent;
import org.fife.ui.rsyntaxtextarea.RSyntaxDocument;
import org.fife.ui.rsyntaxtextarea.Token;
@@ -15,12 +18,17 @@ import org.slf4j.LoggerFactory;
import jadx.api.CodePosition;
import jadx.api.JadxDecompiler;
import jadx.api.JavaNode;
+import jadx.gui.jobs.IndexJob;
+import jadx.gui.settings.JadxProject;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.ContentPanel;
import jadx.gui.ui.MainWindow;
+import jadx.gui.utils.CaretPositionFix;
+import jadx.gui.utils.DefaultPopupMenuListener;
import jadx.gui.utils.JNodeCache;
import jadx.gui.utils.JumpPosition;
+import jadx.gui.utils.UiUtils;
/**
* The {@link AbstractCodeArea} implementation used for displaying Java code and text based
@@ -85,15 +93,33 @@ public final class CodeArea extends AbstractCodeArea {
FindUsageAction findUsage = new FindUsageAction(this);
GoToDeclarationAction goToDeclaration = new GoToDeclarationAction(this);
RenameAction rename = new RenameAction(this);
+ CommentAction comment = new CommentAction(this);
JPopupMenu popup = getPopupMenu();
popup.addSeparator();
popup.add(findUsage);
popup.add(goToDeclaration);
+ popup.add(comment);
+ popup.add(new CommentSearchAction(this));
popup.add(rename);
popup.addPopupMenuListener(findUsage);
popup.addPopupMenuListener(goToDeclaration);
+ popup.addPopupMenuListener(comment);
popup.addPopupMenuListener(rename);
+
+ // move caret on mouse right button click
+ popup.addPopupMenuListener(new DefaultPopupMenuListener() {
+ @Override
+ public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
+ CodeArea codeArea = CodeArea.this;
+ if (codeArea.getSelectedText() == null) {
+ int offset = UiUtils.getOffsetAtMousePosition(codeArea);
+ if (offset >= 0) {
+ codeArea.setCaretPosition(offset);
+ }
+ }
+ }
+ });
}
public int adjustOffsetForToken(@Nullable Token token) {
@@ -145,8 +171,7 @@ public final class CodeArea extends AbstractCodeArea {
return null;
}
JNode jNode = convertJavaNode(foundNode);
- return new JumpPosition(jNode.getRootClass(), pos.getLine())
- .setPrecise(JumpPosition.getDefPos(jNode));
+ return new JumpPosition(jNode.getRootClass(), pos.getLine(), JumpPosition.getDefPos(jNode));
}
private JNode convertJavaNode(JavaNode javaNode) {
@@ -189,11 +214,34 @@ public final class CodeArea extends AbstractCodeArea {
return null;
}
+ public void refreshClass() {
+ if (node instanceof JClass) {
+ JClass cls = (JClass) node;
+ try {
+ CaretPositionFix caretFix = new CaretPositionFix(this);
+ caretFix.save();
+
+ cls.reload();
+ IndexJob.refreshIndex(getMainWindow().getCacheObject(), cls.getCls());
+
+ ClassCodeContentPanel codeContentPanel = (ClassCodeContentPanel) this.contentPanel;
+ codeContentPanel.getTabbedPane().refresh(cls);
+ codeContentPanel.getJavaCodePanel().refresh(caretFix);
+ } catch (Exception e) {
+ LOG.error("Failed to reload class: {}", cls.getFullName(), e);
+ }
+ }
+ }
+
public MainWindow getMainWindow() {
return contentPanel.getTabbedPane().getMainWindow();
}
- private JadxDecompiler getDecompiler() {
+ public JadxDecompiler getDecompiler() {
return getMainWindow().getWrapper().getDecompiler();
}
+
+ public JadxProject getProject() {
+ return getMainWindow().getProject();
+ }
}
diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodePanel.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodePanel.java
index 71c4a2ef3..e0b729bd7 100644
--- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodePanel.java
+++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodePanel.java
@@ -1,25 +1,35 @@
package jadx.gui.ui.codearea;
-import java.awt.*;
+import java.awt.BorderLayout;
+import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
-import javax.swing.*;
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.JMenuItem;
+import javax.swing.JPanel;
+import javax.swing.JPopupMenu;
import javax.swing.JPopupMenu.Separator;
+import javax.swing.JScrollPane;
+import javax.swing.JViewport;
+import javax.swing.KeyStroke;
+import javax.swing.SwingUtilities;
import javax.swing.border.EmptyBorder;
import javax.swing.event.PopupMenuEvent;
-import javax.swing.event.PopupMenuListener;
-import javax.swing.text.BadLocationException;
-import org.fife.ui.rsyntaxtextarea.Token;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import jadx.api.ICodeInfo;
import jadx.core.utils.StringUtils;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.SearchDialog;
+import jadx.gui.utils.CaretPositionFix;
+import jadx.gui.utils.DefaultPopupMenuListener;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
@@ -27,11 +37,13 @@ import jadx.gui.utils.UiUtils;
* A panel combining a {@link SearchBar and a scollable {@link CodeArea}
*/
public class CodePanel extends JPanel {
+ private static final Logger LOG = LoggerFactory.getLogger(CodePanel.class);
private static final long serialVersionUID = 1117721869391885865L;
private final SearchBar searchBar;
private final AbstractCodeArea codeArea;
private final JScrollPane codeScrollPane;
+ private LineNumbers lineNumbers;
public CodePanel(AbstractCodeArea codeArea) {
this.codeArea = codeArea;
@@ -71,7 +83,7 @@ public class CodePanel extends JPanel {
globalSearchItem.setAction(globalSearchAction);
Separator separator = new Separator();
JPopupMenu popupMenu = codeArea.getPopupMenu();
- popupMenu.addPopupMenuListener(new PopupMenuListener() {
+ popupMenu.addPopupMenuListener(new DefaultPopupMenuListener() {
@Override
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
String preferText = codeArea.getSelectedText();
@@ -90,18 +102,7 @@ public class CodePanel extends JPanel {
popupMenu.remove(searchItem);
}
}
-
- @Override
- public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
-
- }
-
- @Override
- public void popupMenuCanceled(PopupMenuEvent e) {
-
- }
});
-
}
public void loadSettings() {
@@ -115,9 +116,13 @@ public class CodePanel extends JPanel {
}
private void initLineNumbers() {
- LineNumbers numbers = new LineNumbers(codeArea);
- numbers.setUseSourceLines(isUseSourceLines());
- codeScrollPane.setRowHeaderView(numbers);
+ initLineNumbers(isUseSourceLines());
+ }
+
+ private void initLineNumbers(boolean useSourceLines) {
+ lineNumbers = new LineNumbers(codeArea);
+ lineNumbers.setUseSourceLines(useSourceLines);
+ codeScrollPane.setRowHeaderView(lineNumbers);
}
private boolean isUseSourceLines() {
@@ -148,79 +153,15 @@ public class CodePanel extends JPanel {
return codeScrollPane;
}
- public void refresh() {
- int line = 0;
- int tokenIndex;
- int pos = codeArea.getCaretPosition();
- int lineCount = codeArea.getLineCount();
- try {
- // after rename the change of document is undetectable, so
- // use Token offset to calculate the new caret position.
- line = codeArea.getLineOfOffset(pos);
- Token token = codeArea.getTokenListForLine(line);
- tokenIndex = getTokenIndexByOffset(token, pos);
- } catch (BadLocationException e) {
- e.printStackTrace();
- tokenIndex = -1;
- }
- if (tokenIndex == -1) {
- refreshToViewport();
- return;
- }
- codeArea.refresh();
- initLineNumbers();
- int lineDiff = codeArea.getLineCount() - lineCount;
- if (lineDiff > 0) {
- lineDiff--;
- } else if (lineDiff < 0) {
- lineDiff++;
- }
- Token token = codeArea.getTokenListForLine(line + lineDiff);
- int newPos = getOffsetOfTokenByIndex(tokenIndex, token);
- SwingUtilities.invokeLater(() -> {
- if (newPos != -1) {
- codeArea.scrollToPos(newPos);
- } else {
- codeArea.scrollToLine(codeArea.getLineCount() - 1);
- }
- });
- }
-
- private void refreshToViewport() {
+ public void refresh(CaretPositionFix caretFix) {
JViewport viewport = getCodeScrollPane().getViewport();
Point viewPosition = viewport.getViewPosition();
codeArea.refresh();
- initLineNumbers();
+ initLineNumbers(lineNumbers.isUseSourceLines());
+
SwingUtilities.invokeLater(() -> {
viewport.setViewPosition(viewPosition);
+ caretFix.restore();
});
}
-
- private int getTokenIndexByOffset(Token token, int offset) {
- if (token != null) {
- int index = 1;
- while (token.getEndOffset() < offset) {
- token = token.getNextToken();
- if (token == null) {
- return -1;
- }
- index++;
- }
- return index;
- }
- return -1;
- }
-
- private int getOffsetOfTokenByIndex(int index, Token token) {
- if (token != null && index != -1) {
- for (int i = 0; i < index; i++) {
- token = token.getNextToken();
- if (token == null) {
- return -1;
- }
- }
- return token.getOffset();
- }
- return -1;
- }
}
diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CommentAction.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CommentAction.java
new file mode 100644
index 000000000..4ea45b7b6
--- /dev/null
+++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CommentAction.java
@@ -0,0 +1,153 @@
+package jadx.gui.ui.codearea;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+import javax.swing.event.PopupMenuEvent;
+
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import jadx.api.CodePosition;
+import jadx.api.JavaClass;
+import jadx.api.JavaMethod;
+import jadx.api.JavaNode;
+import jadx.api.data.ICodeComment;
+import jadx.api.data.annotations.CustomOffsetRef;
+import jadx.api.data.annotations.InsnCodeOffset;
+import jadx.api.data.impl.JadxCodeComment;
+import jadx.api.data.impl.JadxNodeRef;
+import jadx.gui.treemodel.JClass;
+import jadx.gui.treemodel.JNode;
+import jadx.gui.ui.CommentDialog;
+import jadx.gui.utils.CodeLinesInfo;
+import jadx.gui.utils.DefaultPopupMenuListener;
+import jadx.gui.utils.NLS;
+import jadx.gui.utils.UiUtils;
+
+import static javax.swing.KeyStroke.getKeyStroke;
+
+public class CommentAction extends AbstractAction implements DefaultPopupMenuListener {
+ private static final long serialVersionUID = 4753838562204629112L;
+
+ private static final Logger LOG = LoggerFactory.getLogger(CommentAction.class);
+ private final CodeArea codeArea;
+ private final JavaClass topCls;
+
+ private ICodeComment actionComment;
+
+ public CommentAction(CodeArea codeArea) {
+ super(NLS.str("popup.add_comment") + " (;)");
+ this.codeArea = codeArea;
+ JNode topNode = codeArea.getNode();
+ if (topNode instanceof JClass) {
+ this.topCls = ((JClass) topNode).getCls();
+ } else {
+ this.topCls = null;
+ }
+
+ KeyStroke key = getKeyStroke(KeyEvent.VK_SEMICOLON, 0);
+ codeArea.getInputMap().put(key, "popup.add_comment");
+ codeArea.getActionMap().put("popup.add_comment", new AbstractAction() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ int line = codeArea.getCaretLineNumber() + 1;
+ ICodeComment codeComment = getCommentRef(line);
+ showCommentDialog(codeComment);
+ }
+ });
+ }
+
+ @Override
+ public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
+ ICodeComment codeComment = getCommentRef(getMouseLine());
+ setEnabled(codeComment != null);
+ this.actionComment = codeComment;
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ showCommentDialog(this.actionComment);
+ }
+
+ private void showCommentDialog(ICodeComment codeComment) {
+ if (codeComment == null) {
+ UiUtils.showMessageBox(codeArea.getMainWindow(), NLS.str("msg.cant_add_comment"));
+ return;
+ }
+ CommentDialog.show(codeArea, codeComment);
+ }
+
+ /**
+ * Check if possible insert comment at current line.
+ *
+ * @return blank code comment object (comment string empty)
+ */
+ @Nullable
+ private ICodeComment getCommentRef(int line) {
+ if (line == -1 || this.topCls == null) {
+ return null;
+ }
+ try {
+ CodeLinesInfo linesInfo = new CodeLinesInfo(topCls, true); // TODO: cache and update on class refresh
+ // add comment if node definition at this line
+ JavaNode nodeAtLine = linesInfo.getDefAtLine(line);
+ if (nodeAtLine != null) {
+ // at node definition -> add comment for it
+ JadxNodeRef nodeRef = JadxNodeRef.forJavaNode(nodeAtLine);
+ return new JadxCodeComment(nodeRef, "");
+ }
+ Object ann = topCls.getAnnotationAt(new CodePosition(line));
+ if (ann == null) {
+ // check if line with comment above node definition
+ try {
+ JavaNode defNode = linesInfo.getJavaNodeBelowLine(line);
+ if (defNode != null) {
+ String lineStr = codeArea.getLineText(line).trim();
+ if (lineStr.startsWith("//")) {
+ return new JadxCodeComment(JadxNodeRef.forJavaNode(defNode), "");
+ }
+ }
+ } catch (Exception e) {
+ LOG.error("Failed to check comment line: " + line, e);
+ }
+ return null;
+ }
+
+ // try to add method line comment
+ JavaNode node = linesInfo.getJavaNodeByLine(line);
+ if (node instanceof JavaMethod) {
+ JadxNodeRef nodeRef = JadxNodeRef.forMth((JavaMethod) node);
+ if (ann instanceof InsnCodeOffset) {
+ int rawOffset = ((InsnCodeOffset) ann).getOffset();
+ return new JadxCodeComment(nodeRef, "", rawOffset);
+ }
+ if (ann instanceof CustomOffsetRef) {
+ CustomOffsetRef customRef = (CustomOffsetRef) ann;
+ JadxCodeComment comment = new JadxCodeComment(nodeRef, "", customRef.getOffset());
+ comment.setAttachType(customRef.getAttachType());
+ return comment;
+ }
+ }
+ } catch (Exception e) {
+ LOG.error("Failed to add comment at line: " + line, e);
+ }
+ return null;
+ }
+
+ private int getMouseLine() {
+ int closestOffset = UiUtils.getOffsetAtMousePosition(codeArea);
+ if (closestOffset == -1) {
+ return -1;
+ }
+ try {
+ return codeArea.getLineOfOffset(closestOffset) + 1;
+ } catch (Exception e) {
+ LOG.debug("Failed to get line by offset: {}", closestOffset);
+ return -1;
+ }
+ }
+}
diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CommentSearchAction.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CommentSearchAction.java
new file mode 100644
index 000000000..d205df866
--- /dev/null
+++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CommentSearchAction.java
@@ -0,0 +1,44 @@
+package jadx.gui.ui.codearea;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.KeyStroke;
+
+import jadx.gui.ui.SearchDialog;
+import jadx.gui.utils.NLS;
+import jadx.gui.utils.UiUtils;
+
+import static javax.swing.KeyStroke.getKeyStroke;
+
+public class CommentSearchAction extends AbstractAction {
+ private static final long serialVersionUID = -3646341661734961590L;
+
+ private final CodeArea codeArea;
+
+ public CommentSearchAction(CodeArea codeArea) {
+ this.codeArea = codeArea;
+
+ KeyStroke key = getKeyStroke(KeyEvent.VK_SEMICOLON, UiUtils.ctrlButton());
+ putValue(Action.NAME, NLS.str("popup.search_comment") + " (Ctrl + ;)");
+
+ codeArea.getInputMap().put(key, "popup.search_comment");
+ codeArea.getActionMap().put("popup.search_comment", new AbstractAction() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ startSearch();
+ }
+ });
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ startSearch();
+ }
+
+ private void startSearch() {
+ SearchDialog.searchInActiveTab(codeArea.getMainWindow(), SearchDialog.SearchPreset.COMMENT);
+ }
+}
diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/JNodeMenuAction.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/JNodeMenuAction.java
index 091d15af3..2c83b3602 100644
--- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/JNodeMenuAction.java
+++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/JNodeMenuAction.java
@@ -1,15 +1,17 @@
package jadx.gui.ui.codearea;
-import java.awt.*;
+import java.awt.Point;
import java.awt.event.ActionEvent;
-import javax.swing.*;
+import javax.swing.AbstractAction;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import org.fife.ui.rsyntaxtextarea.Token;
import org.jetbrains.annotations.Nullable;
+import jadx.gui.utils.UiUtils;
+
public abstract class JNodeMenuAction extends AbstractAction implements PopupMenuListener {
private static final long serialVersionUID = -2600154727884853550L;
@@ -36,8 +38,7 @@ public abstract class JNodeMenuAction extends AbstractAction implements Popup
@Nullable
private T getNode() {
- Point pos = MouseInfo.getPointerInfo().getLocation();
- SwingUtilities.convertPointFromScreen(pos, codeArea);
+ Point pos = UiUtils.getMousePosition(codeArea);
Token token = codeArea.viewToToken(pos);
int offset = codeArea.adjustOffsetForToken(token);
return getNodeByOffset(offset);
diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/LineNumbers.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/LineNumbers.java
index 7091b99d1..cd8effa84 100644
--- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/LineNumbers.java
+++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/LineNumbers.java
@@ -107,6 +107,7 @@ public class LineNumbers extends JPanel implements CaretListener {
}
}
+ @SuppressWarnings("deprecation")
@Override
public void paintComponent(Graphics g) {
codeInfo = codeArea.getNode().getCodeInfo();
@@ -265,6 +266,10 @@ public class LineNumbers extends JPanel implements CaretListener {
}
}
+ public boolean isUseSourceLines() {
+ return useSourceLines;
+ }
+
public void setUseSourceLines(boolean useSourceLines) {
this.useSourceLines = useSourceLines;
}
diff --git a/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java b/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java
index c26c34db7..3c47b6bf3 100644
--- a/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java
+++ b/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java
@@ -1,6 +1,7 @@
package jadx.gui.utils;
-import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.Map;
import java.util.Set;
import org.jetbrains.annotations.Nullable;
@@ -10,6 +11,7 @@ import jadx.gui.jobs.IndexJob;
import jadx.gui.settings.JadxSettings;
import jadx.gui.treemodel.JRoot;
import jadx.gui.ui.SearchDialog;
+import jadx.gui.utils.search.CommentsIndex;
import jadx.gui.utils.search.TextSearchIndex;
public class CacheObject {
@@ -19,9 +21,11 @@ public class CacheObject {
private TextSearchIndex textIndex;
private CodeUsageInfo usageInfo;
+ private CommentsIndex commentsIndex;
private String lastSearch;
private JNodeCache jNodeCache;
- private Set lastSearchOptions;
+ private Map> lastSearchOptions;
+
private JRoot jRoot;
private JadxSettings settings;
@@ -38,7 +42,7 @@ public class CacheObject {
lastSearch = null;
jNodeCache = new JNodeCache();
usageInfo = null;
- lastSearchOptions = EnumSet.noneOf(SearchDialog.SearchOptions.class);
+ lastSearchOptions = new HashMap<>();
}
public DecompileJob getDecompileJob() {
@@ -49,7 +53,6 @@ public class CacheObject {
this.decompileJob = decompileJob;
}
- @Nullable
public TextSearchIndex getTextIndex() {
return textIndex;
}
@@ -76,6 +79,14 @@ public class CacheObject {
this.usageInfo = usageInfo;
}
+ public CommentsIndex getCommentsIndex() {
+ return commentsIndex;
+ }
+
+ public void setCommentsIndex(CommentsIndex commentsIndex) {
+ this.commentsIndex = commentsIndex;
+ }
+
public IndexJob getIndexJob() {
return indexJob;
}
@@ -88,11 +99,7 @@ public class CacheObject {
return jNodeCache;
}
- public void setLastSearchOptions(Set lastSearchOptions) {
- this.lastSearchOptions = lastSearchOptions;
- }
-
- public Set getLastSearchOptions() {
+ public Map> getLastSearchOptions() {
return lastSearchOptions;
}
diff --git a/jadx-gui/src/main/java/jadx/gui/utils/CaretPositionFix.java b/jadx-gui/src/main/java/jadx/gui/utils/CaretPositionFix.java
new file mode 100644
index 000000000..9d203d7a1
--- /dev/null
+++ b/jadx-gui/src/main/java/jadx/gui/utils/CaretPositionFix.java
@@ -0,0 +1,183 @@
+package jadx.gui.utils;
+
+import java.util.Map;
+
+import org.fife.ui.rsyntaxtextarea.Token;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import jadx.api.CodePosition;
+import jadx.api.JavaClass;
+import jadx.api.JavaNode;
+import jadx.api.data.annotations.ICodeRawOffset;
+import jadx.gui.treemodel.JClass;
+import jadx.gui.ui.codearea.AbstractCodeArea;
+
+/**
+ * After class refresh (rename, comment, etc) the change of document is undetectable.
+ * So use Token index or offset in line to calculate the new caret position.
+ */
+public class CaretPositionFix {
+ private static final Logger LOG = LoggerFactory.getLogger(CaretPositionFix.class);
+
+ private final AbstractCodeArea codeArea;
+
+ private int linesCount;
+ private int line;
+ private int lineOffset;
+ private TokenInfo tokenInfo;
+
+ private int javaNodeLine = -1;
+ private int codeRawOffset = -1;
+
+ public CaretPositionFix(AbstractCodeArea codeArea) {
+ this.codeArea = codeArea;
+ }
+
+ /**
+ * Save caret position by anchor to token under caret
+ */
+ public void save() {
+ try {
+ linesCount = codeArea.getLineCount();
+ int pos = codeArea.getCaretPosition();
+ line = codeArea.getLineOfOffset(pos);
+ lineOffset = pos - codeArea.getLineStartOffset(line);
+
+ tokenInfo = getTokenInfoByOffset(codeArea.getTokenListForLine(line), pos);
+
+ JClass cls = codeArea.getJClass();
+ if (cls != null) {
+ JavaClass topParentClass = cls.getJavaNode().getTopParentClass();
+ Object ann = topParentClass.getAnnotationAt(new CodePosition(line));
+ if (ann instanceof ICodeRawOffset) {
+ codeRawOffset = ((ICodeRawOffset) ann).getOffset();
+ CodeLinesInfo codeLinesInfo = new CodeLinesInfo(topParentClass);
+ JavaNode javaNodeAtLine = codeLinesInfo.getJavaNodeByLine(line);
+ if (javaNodeAtLine != null) {
+ javaNodeLine = javaNodeAtLine.getDecompiledLine();
+ }
+ }
+ }
+ LOG.debug("Saved position data: line={}, lineOffset={}, token={}, codeRawOffset={}, javaNodeLine={}",
+ line, lineOffset, tokenInfo, codeRawOffset, javaNodeLine);
+ } catch (Exception e) {
+ LOG.error("Failed to save caret position before refresh", e);
+ line = -1;
+ }
+ }
+
+ /**
+ * Restore caret position in refreshed code.
+ * Expected to be called in UI thread.
+ */
+ public void restore() {
+ if (line == -1) {
+ return;
+ }
+ try {
+ int newLine = getNewLine();
+ int lineStartOffset = codeArea.getLineStartOffset(newLine);
+ int lineEndOffset = codeArea.getLineEndOffset(newLine) - 1;
+ int lineLength = lineEndOffset - lineStartOffset;
+ Token token = codeArea.getTokenListForLine(newLine);
+ int newPos = getOffsetFromTokenInfo(tokenInfo, token);
+ if (newPos == -1) {
+ // can't restore using token -> just restore by line offset
+ if (lineOffset < lineLength) {
+ newPos = lineStartOffset + lineOffset;
+ } else {
+ // line truncated -> set caret at line end
+ newPos = lineEndOffset;
+ }
+ }
+ codeArea.setCaretPosition(newPos);
+ LOG.debug("Restored caret position: {}, line: {}", newPos, newLine);
+ } catch (Exception e) {
+ LOG.warn("Failed to restore caret position", e);
+ }
+ }
+
+ private int getNewLine() {
+ int newLinesCount = codeArea.getLineCount();
+ if (linesCount == newLinesCount) {
+ return line;
+ }
+ // lines count changes, try find line by raw offset
+ if (javaNodeLine != -1) {
+ JClass cls = codeArea.getJClass();
+ if (cls != null) {
+ JavaClass topParentClass = cls.getJavaNode().getTopParentClass();
+ for (Map.Entry entry : topParentClass.getCodeAnnotations().entrySet()) {
+ CodePosition pos = entry.getKey();
+ if (pos.getOffset() == 0 && pos.getLine() >= javaNodeLine) {
+ Object ann = entry.getValue();
+ if (ann instanceof ICodeRawOffset && ((ICodeRawOffset) ann).getOffset() == codeRawOffset) {
+ return pos.getLine() - 1;
+ }
+ }
+ }
+ }
+ }
+ // fallback: assume lines added/removed before caret
+ return line - (linesCount - newLinesCount);
+ }
+
+ private TokenInfo getTokenInfoByOffset(Token token, int offset) {
+ if (token == null) {
+ return null;
+ }
+ int index = 1;
+ while (token.getEndOffset() < offset) {
+ token = token.getNextToken();
+ if (token == null) {
+ return null;
+ }
+ index++;
+ }
+ return new TokenInfo(index, token.getType());
+ }
+
+ private int getOffsetFromTokenInfo(TokenInfo tokenInfo, Token token) {
+ if (tokenInfo == null || token == null) {
+ return -1;
+ }
+ int index = tokenInfo.getIndex();
+ if (index == -1) {
+ return -1;
+ }
+ for (int i = 0; i < index; i++) {
+ token = token.getNextToken();
+ if (token == null) {
+ return -1;
+ }
+ }
+ if (token.getType() != tokenInfo.getType()) {
+ return -1;
+ }
+ return token.getOffset();
+ }
+
+ private static final class TokenInfo {
+ private final int index;
+ private final int type;
+
+ public TokenInfo(int index, int type) {
+ this.index = index;
+ this.type = type;
+ }
+
+ public int getIndex() {
+ return index;
+ }
+
+ public int getType() {
+ return type;
+ }
+
+ @Override
+ public String toString() {
+ return "Token{index=" + index + ", type=" + type + '}';
+ }
+ }
+}
diff --git a/jadx-gui/src/main/java/jadx/gui/utils/CodeLinesInfo.java b/jadx-gui/src/main/java/jadx/gui/utils/CodeLinesInfo.java
index e2d2d0061..feb3b437f 100644
--- a/jadx-gui/src/main/java/jadx/gui/utils/CodeLinesInfo.java
+++ b/jadx-gui/src/main/java/jadx/gui/utils/CodeLinesInfo.java
@@ -5,6 +5,7 @@ import java.util.NavigableMap;
import java.util.TreeMap;
import jadx.api.JavaClass;
+import jadx.api.JavaField;
import jadx.api.JavaMethod;
import jadx.api.JavaNode;
@@ -12,18 +13,27 @@ public class CodeLinesInfo {
private final NavigableMap map = new TreeMap<>();
public CodeLinesInfo(JavaClass cls) {
- addClass(cls);
+ addClass(cls, false);
}
- public void addClass(JavaClass cls) {
+ public CodeLinesInfo(JavaClass cls, boolean includeFields) {
+ addClass(cls, includeFields);
+ }
+
+ private void addClass(JavaClass cls, boolean includeFields) {
map.put(cls.getDecompiledLine(), cls);
for (JavaClass innerCls : cls.getInnerClasses()) {
map.put(innerCls.getDecompiledLine(), innerCls);
- addClass(innerCls);
+ addClass(innerCls, includeFields);
}
for (JavaMethod mth : cls.getMethods()) {
map.put(mth.getDecompiledLine(), mth);
}
+ if (includeFields) {
+ for (JavaField field : cls.getFields()) {
+ map.put(field.getDecompiledLine(), field);
+ }
+ }
}
public JavaNode getJavaNodeByLine(int line) {
@@ -33,4 +43,16 @@ public class CodeLinesInfo {
}
return entry.getValue();
}
+
+ public JavaNode getJavaNodeBelowLine(int line) {
+ Map.Entry entry = map.ceilingEntry(line);
+ if (entry == null) {
+ return null;
+ }
+ return entry.getValue();
+ }
+
+ public JavaNode getDefAtLine(int line) {
+ return map.get(line);
+ }
}
diff --git a/jadx-gui/src/main/java/jadx/gui/utils/CodeUsageInfo.java b/jadx-gui/src/main/java/jadx/gui/utils/CodeUsageInfo.java
index 2f577463c..f1c957481 100644
--- a/jadx-gui/src/main/java/jadx/gui/utils/CodeUsageInfo.java
+++ b/jadx-gui/src/main/java/jadx/gui/utils/CodeUsageInfo.java
@@ -64,7 +64,7 @@ public class CodeUsageInfo {
JavaNode javaNodeByLine = linesInfo.getJavaNodeByLine(line);
StringRef codeLine = lines.get(line - 1);
JNode node = nodeCache.makeFrom(javaNodeByLine == null ? javaClass : javaNodeByLine);
- CodeNode codeNode = new CodeNode(node, line, codeLine).setPrecisePos(codePosition.getUsagePosition());
+ CodeNode codeNode = new CodeNode(node, codeLine, line, codePosition.getPos());
usageInfo.addUsage(codeNode);
}
diff --git a/jadx-gui/src/main/java/jadx/gui/utils/DefaultPopupMenuListener.java b/jadx-gui/src/main/java/jadx/gui/utils/DefaultPopupMenuListener.java
new file mode 100644
index 000000000..4bb0c08d6
--- /dev/null
+++ b/jadx-gui/src/main/java/jadx/gui/utils/DefaultPopupMenuListener.java
@@ -0,0 +1,18 @@
+package jadx.gui.utils;
+
+import javax.swing.event.PopupMenuEvent;
+import javax.swing.event.PopupMenuListener;
+
+public interface DefaultPopupMenuListener extends PopupMenuListener {
+ @Override
+ default void popupMenuWillBecomeVisible(PopupMenuEvent e) {
+ }
+
+ @Override
+ default void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
+ }
+
+ @Override
+ default void popupMenuCanceled(PopupMenuEvent e) {
+ }
+}
diff --git a/jadx-gui/src/main/java/jadx/gui/utils/JumpPosition.java b/jadx-gui/src/main/java/jadx/gui/utils/JumpPosition.java
index dd87d1606..1f89aaf43 100644
--- a/jadx-gui/src/main/java/jadx/gui/utils/JumpPosition.java
+++ b/jadx-gui/src/main/java/jadx/gui/utils/JumpPosition.java
@@ -1,17 +1,20 @@
package jadx.gui.utils;
-import jadx.api.*;
-import jadx.core.utils.exceptions.JadxRuntimeException;
-import jadx.gui.treemodel.*;
+import jadx.api.CodePosition;
+import jadx.api.JavaNode;
+import jadx.gui.treemodel.JNode;
public class JumpPosition {
private final JNode node;
private final int line;
private int pos;
- private boolean precise;
- public JumpPosition(JNode node, int line) {
- this(node, line, -1);
+ public JumpPosition(JNode jumpNode) {
+ this(jumpNode.getRootClass(), jumpNode.getLine(), jumpNode.getPos());
+ }
+
+ public JumpPosition(JNode jumpNode, CodePosition codePos) {
+ this(jumpNode.getRootClass(), codePos.getLine(), codePos.getPos());
}
public JumpPosition(JNode node, int line, int pos) {
@@ -20,20 +23,14 @@ public class JumpPosition {
this.pos = pos;
}
- public boolean isPrecise() {
- return precise;
- }
-
- public JumpPosition setPrecise(int pos) {
- this.pos = pos;
- this.precise = true;
- return this;
- }
-
public int getPos() {
return pos;
}
+ public void setPos(int pos) {
+ this.pos = pos;
+ }
+
public JNode getNode() {
return node;
}
@@ -43,35 +40,11 @@ public class JumpPosition {
}
public static int getDefPos(JNode node) {
- if (node instanceof JClass) {
- return ((JClass) node).getCls().getClassNode().getDefPosition();
+ JavaNode javaNode = node.getJavaNode();
+ if (javaNode == null) {
+ return -1;
}
- if (node instanceof JMethod) {
- return ((JMethod) node).getJavaMethod().getMethodNode().getDefPosition();
- }
- if (node instanceof JField) {
- return ((JField) node).getJavaField().getFieldNode().getDefPosition();
- }
- if (node instanceof JVariable) {
- return ((JVariable) node).getJavaVarNode().getVariableNode().getDefPosition();
- }
- throw new JadxRuntimeException("Unexpected node " + node);
- }
-
- public static int getDefPos(JavaNode node) {
- if (node instanceof JavaClass) {
- return ((JavaClass) node).getClassNode().getDefPosition();
- }
- if (node instanceof JavaMethod) {
- return ((JavaMethod) node).getMethodNode().getDefPosition();
- }
- if (node instanceof JavaField) {
- return ((JavaField) node).getFieldNode().getDefPosition();
- }
- if (node instanceof JavaVariable) {
- return ((JavaVariable) node).getVariableNode().getDefPosition();
- }
- throw new JadxRuntimeException("Unexpected node " + node);
+ return javaNode.getDefPos();
}
@Override
diff --git a/jadx-gui/src/main/java/jadx/gui/utils/TextStandardActions.java b/jadx-gui/src/main/java/jadx/gui/utils/TextStandardActions.java
index 5a1f54f83..24120f295 100644
--- a/jadx-gui/src/main/java/jadx/gui/utils/TextStandardActions.java
+++ b/jadx-gui/src/main/java/jadx/gui/utils/TextStandardActions.java
@@ -11,7 +11,6 @@ import javax.swing.*;
import javax.swing.text.JTextComponent;
import javax.swing.undo.UndoManager;
-@SuppressWarnings("serial")
public class TextStandardActions {
private final JTextComponent textComponent;
@@ -27,6 +26,10 @@ public class TextStandardActions {
private Action deleteAction;
private Action selectAllAction;
+ public static void attach(JTextComponent textComponent) {
+ new TextStandardActions(textComponent);
+ }
+
public TextStandardActions(JTextComponent textComponent) {
this.textComponent = textComponent;
this.undoManager = new UndoManager();
diff --git a/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java b/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java
index 93ad71162..0b01e4d94 100644
--- a/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java
+++ b/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java
@@ -1,6 +1,11 @@
package jadx.gui.utils;
-import java.awt.*;
+import java.awt.Component;
+import java.awt.Image;
+import java.awt.MouseInfo;
+import java.awt.Point;
+import java.awt.Toolkit;
+import java.awt.Window;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
@@ -10,7 +15,14 @@ import java.net.URL;
import java.util.ArrayList;
import java.util.List;
-import javax.swing.*;
+import javax.swing.Action;
+import javax.swing.Icon;
+import javax.swing.ImageIcon;
+import javax.swing.JComponent;
+import javax.swing.JDialog;
+import javax.swing.JOptionPane;
+import javax.swing.KeyStroke;
+import javax.swing.SwingUtilities;
import org.intellij.lang.annotations.MagicConstant;
import org.slf4j.Logger;
@@ -19,6 +31,7 @@ import org.slf4j.LoggerFactory;
import jadx.core.dex.info.AccessInfo;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.utils.exceptions.JadxRuntimeException;
+import jadx.gui.ui.codearea.AbstractCodeArea;
public class UiUtils {
private static final Logger LOG = LoggerFactory.getLogger(UiUtils.class);
@@ -190,7 +203,7 @@ public class UiUtils {
@SuppressWarnings("deprecation")
@MagicConstant(flagsFromClass = InputEvent.class)
private static int getCtrlButton() {
- if (System.getProperty("os.name").toLowerCase().contains("mac")) {
+ if (SystemInfo.IS_MAC) {
return Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
} else {
return InputEvent.CTRL_DOWN_MASK;
@@ -210,4 +223,26 @@ public class UiUtils {
KeyStroke stroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
dialog.getRootPane().registerKeyboardAction(e -> dialog.dispose(), stroke, JComponent.WHEN_IN_FOCUSED_WINDOW);
}
+
+ /**
+ * Get closest offset at mouse position
+ *
+ * @return -1 on error
+ */
+ @SuppressWarnings("deprecation")
+ public static int getOffsetAtMousePosition(AbstractCodeArea codeArea) {
+ try {
+ Point mousePos = getMousePosition(codeArea);
+ return codeArea.viewToModel(mousePos);
+ } catch (Exception e) {
+ LOG.error("Failed to get offset at mouse position", e);
+ return -1;
+ }
+ }
+
+ public static Point getMousePosition(Component comp) {
+ Point pos = MouseInfo.getPointerInfo().getLocation();
+ SwingUtilities.convertPointFromScreen(pos, comp);
+ return pos;
+ }
}
diff --git a/jadx-gui/src/main/java/jadx/gui/utils/layout/WrapLayout.java b/jadx-gui/src/main/java/jadx/gui/utils/layout/WrapLayout.java
new file mode 100644
index 000000000..8800b184b
--- /dev/null
+++ b/jadx-gui/src/main/java/jadx/gui/utils/layout/WrapLayout.java
@@ -0,0 +1,187 @@
+package jadx.gui.utils.layout;
+
+import java.awt.Component;
+import java.awt.Container;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.Insets;
+
+import javax.swing.JScrollPane;
+import javax.swing.SwingUtilities;
+
+/**
+ * FlowLayout subclass that fully supports wrapping of components.
+ */
+public class WrapLayout extends FlowLayout {
+ private static final long serialVersionUID = 6109752116520941346L;
+
+ private Dimension preferredLayoutSize;
+
+ /**
+ * Constructs a new WrapLayout with a left
+ * alignment and a default 5-unit horizontal and vertical gap.
+ */
+ public WrapLayout() {
+ super();
+ }
+
+ /**
+ * Constructs a new FlowLayout with the specified
+ * alignment and a default 5-unit horizontal and vertical gap.
+ * The value of the alignment argument must be one of
+ * WrapLayout, WrapLayout,
+ * or WrapLayout.
+ *
+ * @param align the alignment value
+ */
+ public WrapLayout(int align) {
+ super(align);
+ }
+
+ /**
+ * Creates a new flow layout manager with the indicated alignment
+ * and the indicated horizontal and vertical gaps.
+ *
+ * The value of the alignment argument must be one of
+ * WrapLayout, WrapLayout,
+ * or WrapLayout.
+ *
+ * @param align the alignment value
+ * @param hgap the horizontal gap between components
+ * @param vgap the vertical gap between components
+ */
+ public WrapLayout(int align, int hgap, int vgap) {
+ super(align, hgap, vgap);
+ }
+
+ /**
+ * Returns the preferred dimensions for this layout given the
+ * visible components in the specified target container.
+ *
+ * @param target the component which needs to be laid out
+ * @return the preferred dimensions to lay out the
+ * subcomponents of the specified container
+ */
+ @Override
+ public Dimension preferredLayoutSize(Container target) {
+ return layoutSize(target, true);
+ }
+
+ /**
+ * Returns the minimum dimensions needed to layout the visible
+ * components contained in the specified target container.
+ *
+ * @param target the component which needs to be laid out
+ * @return the minimum dimensions to lay out the
+ * subcomponents of the specified container
+ */
+ @Override
+ public Dimension minimumLayoutSize(Container target) {
+ Dimension minimum = layoutSize(target, false);
+ minimum.width -= (getHgap() + 1);
+ return minimum;
+ }
+
+ /**
+ * Returns the minimum or preferred dimension needed to layout the target
+ * container.
+ *
+ * @param target target to get layout size for
+ * @param preferred should preferred size be calculated
+ * @return the dimension to layout the target container
+ */
+ private Dimension layoutSize(Container target, boolean preferred) {
+ synchronized (target.getTreeLock()) {
+ // Each row must fit with the width allocated to the containter.
+ // When the container width = 0, the preferred width of the container
+ // has not yet been calculated so lets ask for the maximum.
+
+ int targetWidth = target.getSize().width;
+ Container container = target;
+
+ while (container.getSize().width == 0 && container.getParent() != null) {
+ container = container.getParent();
+ }
+
+ targetWidth = container.getSize().width;
+
+ if (targetWidth == 0) {
+ targetWidth = Integer.MAX_VALUE;
+ }
+
+ int hgap = getHgap();
+ int vgap = getVgap();
+ Insets insets = target.getInsets();
+ int horizontalInsetsAndGap = insets.left + insets.right + (hgap * 2);
+ int maxWidth = targetWidth - horizontalInsetsAndGap;
+
+ // Fit components into the allowed width
+
+ Dimension dim = new Dimension(0, 0);
+ int rowWidth = 0;
+ int rowHeight = 0;
+
+ int nmembers = target.getComponentCount();
+
+ for (int i = 0; i < nmembers; i++) {
+ Component m = target.getComponent(i);
+
+ if (m.isVisible()) {
+ Dimension d = preferred ? m.getPreferredSize() : m.getMinimumSize();
+
+ // Can't add the component to current row. Start a new row.
+
+ if (rowWidth + d.width > maxWidth) {
+ addRow(dim, rowWidth, rowHeight);
+ rowWidth = 0;
+ rowHeight = 0;
+ }
+
+ // Add a horizontal gap for all components after the first
+
+ if (rowWidth != 0) {
+ rowWidth += hgap;
+ }
+
+ rowWidth += d.width;
+ rowHeight = Math.max(rowHeight, d.height);
+ }
+ }
+
+ addRow(dim, rowWidth, rowHeight);
+
+ dim.width += horizontalInsetsAndGap;
+ dim.height += insets.top + insets.bottom + vgap * 2;
+
+ // When using a scroll pane or the DecoratedLookAndFeel we need to
+ // make sure the preferred size is less than the size of the
+ // target containter so shrinking the container size works
+ // correctly. Removing the horizontal gap is an easy way to do this.
+
+ Container scrollPane = SwingUtilities.getAncestorOfClass(JScrollPane.class, target);
+
+ if (scrollPane != null && target.isValid()) {
+ dim.width -= (hgap + 1);
+ }
+
+ return dim;
+ }
+ }
+
+ /*
+ * A new row has been completed. Use the dimensions of this row
+ * to update the preferred size for the container.
+ * @param dim update the width and height when appropriate
+ * @param rowWidth the width of the row to add
+ * @param rowHeight the height of the row to add
+ */
+ private void addRow(Dimension dim, int rowWidth, int rowHeight) {
+ dim.width = Math.max(dim.width, rowWidth);
+
+ if (dim.height > 0) {
+ dim.height += getVgap();
+ }
+
+ dim.height += rowHeight;
+ }
+}
diff --git a/jadx-gui/src/main/java/jadx/gui/utils/search/CodeIndex.java b/jadx-gui/src/main/java/jadx/gui/utils/search/CodeIndex.java
index 769fe195c..dbcabcef1 100644
--- a/jadx-gui/src/main/java/jadx/gui/utils/search/CodeIndex.java
+++ b/jadx-gui/src/main/java/jadx/gui/utils/search/CodeIndex.java
@@ -11,6 +11,7 @@ import io.reactivex.Flowable;
import jadx.api.JavaClass;
import jadx.gui.treemodel.CodeNode;
+import jadx.gui.treemodel.JClass;
import jadx.gui.utils.UiUtils;
public class CodeIndex {
@@ -32,13 +33,15 @@ public class CodeIndex {
}
public Flowable search(final SearchSettings searchSettings) {
+ JClass activeCls = searchSettings.getActiveCls();
return Flowable.create(emitter -> {
LOG.debug("Code search started: {} ...", searchSettings.getSearchString());
for (CodeNode node : values) {
- int pos = searchSettings.find(node.getLineStr());
- node.setPos(pos);
- if (pos > -1) {
- emitter.onNext(node);
+ if (activeCls == null || node.getRootClass().equals(activeCls)) {
+ int pos = searchSettings.find(node.getLineStr());
+ if (pos > -1) {
+ emitter.onNext(node);
+ }
}
if (emitter.isCancelled()) {
LOG.debug("Code search canceled: {}", searchSettings.getSearchString());
diff --git a/jadx-gui/src/main/java/jadx/gui/utils/search/CommentsIndex.java b/jadx-gui/src/main/java/jadx/gui/utils/search/CommentsIndex.java
new file mode 100644
index 000000000..7ae2cbdf3
--- /dev/null
+++ b/jadx-gui/src/main/java/jadx/gui/utils/search/CommentsIndex.java
@@ -0,0 +1,230 @@
+package jadx.gui.utils.search;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import javax.swing.Icon;
+
+import org.jetbrains.annotations.NotNull;
+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;
+import jadx.api.JavaMethod;
+import jadx.api.JavaNode;
+import jadx.api.data.ICodeComment;
+import jadx.api.data.IJavaNodeRef;
+import jadx.api.data.annotations.ICodeRawOffset;
+import jadx.gui.JadxWrapper;
+import jadx.gui.settings.JadxProject;
+import jadx.gui.treemodel.JClass;
+import jadx.gui.treemodel.JMethod;
+import jadx.gui.treemodel.JNode;
+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);
+ private final JadxWrapper wrapper;
+ private final CacheObject cacheObject;
+ private final JadxProject project;
+
+ public CommentsIndex(JadxWrapper wrapper, CacheObject cacheObject, JadxProject project) {
+ this.wrapper = wrapper;
+ this.cacheObject = cacheObject;
+ this.project = project;
+ }
+
+ @Nullable
+ private JNode isMatch(SearchSettings searchSettings, ICodeComment comment) {
+ boolean all = searchSettings.getSearchString().isEmpty();
+ if (all || searchSettings.isMatch(comment.getComment())) {
+ JNode refNode = getRefNode(comment);
+ if (refNode != null) {
+ JClass activeCls = searchSettings.getActiveCls();
+ if (activeCls == null || Objects.equals(activeCls, refNode.getRootClass())) {
+ return getCommentNode(comment, refNode);
+ }
+ } else {
+ LOG.warn("Failed to get ref node for comment: {}", comment);
+ }
+ }
+ return null;
+ }
+
+ public Flowable search(SearchSettings searchSettings) {
+ List comments = project.getCodeData().getComments();
+ if (comments == null || comments.isEmpty()) {
+ return Flowable.empty();
+ }
+ LOG.debug("Total comments count: {}", comments.size());
+ return Flowable.create(emitter -> {
+ for (ICodeComment comment : comments) {
+ JNode foundNode = isMatch(searchSettings, comment);
+ if (foundNode != null) {
+ emitter.onNext(foundNode);
+ }
+ if (emitter.isCancelled()) {
+ return;
+ }
+ }
+ emitter.onComplete();
+ }, BackpressureStrategy.BUFFER);
+ }
+
+ private @NotNull RefCommentNode getCommentNode(ICodeComment comment, JNode refNode) {
+ IJavaNodeRef nodeRef = comment.getNodeRef();
+ if (nodeRef.getType() == IJavaNodeRef.RefType.METHOD && comment.getOffset() > 0) {
+ return new CodeCommentNode((JMethod) refNode, comment);
+ }
+ return new RefCommentNode(refNode, comment.getComment());
+ }
+
+ @Nullable
+ private JNode getRefNode(ICodeComment comment) {
+ IJavaNodeRef nodeRef = comment.getNodeRef();
+ JavaClass javaClass = wrapper.searchJavaClassByOrigClassName(nodeRef.getDeclaringClass());
+ if (javaClass == null) {
+ return null;
+ }
+ JNodeCache nodeCache = cacheObject.getNodeCache();
+ switch (nodeRef.getType()) {
+ case CLASS:
+ return nodeCache.makeFrom(javaClass);
+
+ case FIELD:
+ for (JavaField field : javaClass.getFields()) {
+ if (field.getFieldNode().getFieldInfo().getShortId().equals(nodeRef.getShortId())) {
+ return nodeCache.makeFrom(field);
+ }
+ }
+ break;
+
+ case METHOD:
+ for (JavaMethod mth : javaClass.getMethods()) {
+ if (mth.getMethodNode().getMethodInfo().getShortId().equals(nodeRef.getShortId())) {
+ return nodeCache.makeFrom(mth);
+ }
+ }
+ break;
+ }
+ return null;
+ }
+
+ private static final class CodeCommentNode extends RefCommentNode {
+ private static final long serialVersionUID = 6208192811789176886L;
+
+ private final int offset;
+ private JumpPosition pos;
+
+ public CodeCommentNode(JMethod node, ICodeComment comment) {
+ super(node, comment.getComment());
+ this.offset = comment.getOffset();
+ }
+
+ @Override
+ public int getLine() {
+ return getCachedPos().getLine();
+ }
+
+ @Override
+ public int getPos() {
+ return getCachedPos().getPos();
+ }
+
+ private synchronized JumpPosition getCachedPos() {
+ if (pos == null) {
+ pos = getJumpPos();
+ }
+ return pos;
+ }
+
+ /**
+ * Lazy decompilation to get comment location if requested
+ */
+ private JumpPosition getJumpPos() {
+ JavaMethod javaMethod = ((JMethod) node).getJavaMethod();
+ int methodLine = javaMethod.getDecompiledLine();
+ ICodeInfo codeInfo = javaMethod.getTopParentClass().getCodeInfo();
+ for (Map.Entry entry : codeInfo.getAnnotations().entrySet()) {
+ CodePosition codePos = entry.getKey();
+ if (codePos.getOffset() == 0 && codePos.getLine() > methodLine) {
+ Object ann = entry.getValue();
+ if (ann instanceof ICodeRawOffset) {
+ if (((ICodeRawOffset) ann).getOffset() == offset) {
+ return new JumpPosition(node, codePos);
+ }
+ }
+ }
+ }
+ return new JumpPosition(node);
+ }
+ }
+
+ private static class RefCommentNode extends JNode {
+ private static final long serialVersionUID = 3887992236082515752L;
+
+ protected final JNode node;
+ protected final String comment;
+
+ public RefCommentNode(JNode node, String comment) {
+ this.node = node;
+ this.comment = comment;
+ }
+
+ @Override
+ public JClass getRootClass() {
+ return node.getRootClass();
+ }
+
+ @Override
+ public JavaNode getJavaNode() {
+ return node.getJavaNode();
+ }
+
+ @Override
+ public JClass getJParent() {
+ return node.getJParent();
+ }
+
+ @Override
+ public Icon getIcon() {
+ return node.getIcon();
+ }
+
+ @Override
+ public String getSyntaxName() {
+ return node.getSyntaxName();
+ }
+
+ @Override
+ public String makeString() {
+ return node.makeString();
+ }
+
+ @Override
+ public int getLine() {
+ return node.getLine();
+ }
+
+ @Override
+ public String makeDescString() {
+ return comment;
+ }
+
+ @Override
+ public boolean hasDescString() {
+ return true;
+ }
+ }
+}
diff --git a/jadx-gui/src/main/java/jadx/gui/utils/search/SearchSettings.java b/jadx-gui/src/main/java/jadx/gui/utils/search/SearchSettings.java
index 69028e9c5..e2a66f6de 100644
--- a/jadx-gui/src/main/java/jadx/gui/utils/search/SearchSettings.java
+++ b/jadx-gui/src/main/java/jadx/gui/utils/search/SearchSettings.java
@@ -7,18 +7,19 @@ import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import jadx.gui.treemodel.JClass;
+
public class SearchSettings {
private static final Logger LOG = LoggerFactory.getLogger(SearchSettings.class);
private final String searchString;
-
private final boolean useRegex;
-
private final boolean ignoreCase;
- private Pattern regexPattern;
+ private JClass activeCls;
+ private Pattern regexPattern;
private int startPos = 0;
public SearchSettings(String searchString, boolean ignoreCase, boolean useRegex) {
@@ -27,109 +28,81 @@ public class SearchSettings {
this.ignoreCase = ignoreCase;
}
- /*
- * Return whether Regex search should be done
- */
public boolean isUseRegex() {
return this.useRegex;
}
- /*
- * Return whether case will be ignored
- */
public boolean isIgnoreCase() {
return this.ignoreCase;
}
- /*
- * Return search string
- */
public String getSearchString() {
return this.searchString;
}
- /*
- * Return the starting index
- */
public int getStartPos() {
return this.startPos;
}
- /*
- * Set Starting Index
- */
public void setStartPos(int startPos) {
this.startPos = startPos;
}
- /*
- * get Regex Pattern
- */
public Pattern getPattern() {
return this.regexPattern;
}
- /*
- * Runs Pattern.compile if using Regex. If not using Regex return true
- * return false is invalid Regex
- */
public boolean preCompile() {
- try {
- if (this.useRegex && this.ignoreCase) {
- this.regexPattern = Pattern.compile(this.searchString, Pattern.CASE_INSENSITIVE);
- } else if (this.useRegex) {
- this.regexPattern = Pattern.compile(this.searchString);
+ if (useRegex) {
+ try {
+ int flags = ignoreCase ? Pattern.CASE_INSENSITIVE : 0;
+ this.regexPattern = Pattern.compile(searchString, flags);
+ } catch (Exception e) {
+ LOG.warn("Invalid Regex: {}", this.searchString, e);
+ return false;
}
- } catch (Exception e) {
- LOG.warn("Invalid Regex: {}", this.searchString);
- return false;
}
return true;
}
- /*
- * Checks if searchArea matches the searched string found in searchSettings
- */
public boolean isMatch(StringRef searchArea) {
- return isMatch(searchArea.toString());
+ return find(searchArea) != -1;
}
- /*
- * Checks if searchArea matches the searched string found in searchSettings
- */
public boolean isMatch(String searchArea) {
return find(searchArea) != -1;
}
- /*
- * Returns the position within searchArea that the searched string found in searchSettings was
- * identified.
- * returns -1 if a match is not found
- */
public int find(StringRef searchArea) {
- return find(searchArea.toString());
- }
-
- /*
- * Returns the position within searchArea that the searched string found in searchSettings was
- * identified.
- * returns -1 if a match is not found
- */
- public int find(String searchArea) {
- int pos;
- if (this.useRegex) {
- Matcher matcher = this.regexPattern.matcher(searchArea);
- if (matcher.find(this.startPos)) {
- pos = matcher.start();
- } else {
- pos = -1;
- }
- } else if (this.ignoreCase) {
- pos = StringUtils.indexOfIgnoreCase(searchArea, this.searchString, this.startPos);
- } else {
- pos = searchArea.indexOf(this.searchString, this.startPos);
+ if (useRegex) {
+ return findWithRegex(searchArea.toString());
}
- return pos;
+ return searchArea.indexOf(this.searchString, this.startPos, this.ignoreCase);
}
+ public int find(String searchArea) {
+ if (useRegex) {
+ return findWithRegex(searchArea);
+ }
+ if (ignoreCase) {
+ return StringUtils.indexOfIgnoreCase(searchArea, searchString, startPos);
+ }
+ return searchArea.indexOf(searchString, startPos);
+ }
+
+ private int findWithRegex(String searchArea) {
+ Matcher matcher = regexPattern.matcher(searchArea);
+ if (matcher.find(startPos)) {
+ return matcher.start();
+ }
+ return -1;
+ }
+
+ public JClass getActiveCls() {
+ return activeCls;
+ }
+
+ public void setActiveCls(JClass activeCls) {
+ this.activeCls = activeCls;
+ }
}
diff --git a/jadx-gui/src/main/java/jadx/gui/utils/search/SimpleIndex.java b/jadx-gui/src/main/java/jadx/gui/utils/search/SimpleIndex.java
index 4f295079b..1283b22f7 100644
--- a/jadx-gui/src/main/java/jadx/gui/utils/search/SimpleIndex.java
+++ b/jadx-gui/src/main/java/jadx/gui/utils/search/SimpleIndex.java
@@ -1,12 +1,14 @@
package jadx.gui.utils.search;
import java.util.Map;
+import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import io.reactivex.BackpressureStrategy;
import io.reactivex.Flowable;
import jadx.api.JavaClass;
+import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JNode;
public class SimpleIndex {
@@ -20,15 +22,23 @@ public class SimpleIndex {
data.entrySet().removeIf(e -> e.getKey().getJavaNode().getTopParentClass().equals(cls));
}
- private boolean isMatched(String str, SearchSettings searchSettings) {
- return searchSettings.isMatch(str);
+ private boolean isMatched(String str, JNode node, SearchSettings searchSettings) {
+ if (searchSettings.isMatch(str)) {
+ JClass activeCls = searchSettings.getActiveCls();
+ if (activeCls == null) {
+ return true;
+ }
+ return Objects.equals(node.getRootClass(), activeCls);
+ }
+ return false;
}
public Flowable search(final SearchSettings searchSettings) {
return Flowable.create(emitter -> {
for (Map.Entry entry : data.entrySet()) {
- if (isMatched(entry.getValue(), searchSettings)) {
- emitter.onNext(entry.getKey());
+ JNode node = entry.getKey();
+ if (isMatched(entry.getValue(), node, searchSettings)) {
+ emitter.onNext(node);
}
if (emitter.isCancelled()) {
return;
diff --git a/jadx-gui/src/main/java/jadx/gui/utils/search/TextSearchIndex.java b/jadx-gui/src/main/java/jadx/gui/utils/search/TextSearchIndex.java
index 960989711..307e2bfb8 100644
--- a/jadx-gui/src/main/java/jadx/gui/utils/search/TextSearchIndex.java
+++ b/jadx-gui/src/main/java/jadx/gui/utils/search/TextSearchIndex.java
@@ -18,24 +18,30 @@ import jadx.api.JavaMethod;
import jadx.api.JavaNode;
import jadx.gui.treemodel.CodeNode;
import jadx.gui.treemodel.JNode;
+import jadx.gui.ui.MainWindow;
import jadx.gui.ui.SearchDialog;
import jadx.gui.utils.CacheObject;
import jadx.gui.utils.CodeLinesInfo;
import jadx.gui.utils.JNodeCache;
+import jadx.gui.utils.JumpPosition;
import jadx.gui.utils.UiUtils;
+import static jadx.gui.ui.SearchDialog.SearchOptions.ACTIVE_TAB;
import static jadx.gui.ui.SearchDialog.SearchOptions.CLASS;
import static jadx.gui.ui.SearchDialog.SearchOptions.CODE;
+import static jadx.gui.ui.SearchDialog.SearchOptions.COMMENT;
import static jadx.gui.ui.SearchDialog.SearchOptions.FIELD;
import static jadx.gui.ui.SearchDialog.SearchOptions.IGNORE_CASE;
import static jadx.gui.ui.SearchDialog.SearchOptions.METHOD;
-import static jadx.gui.ui.SearchDialog.SearchOptions.Resource;
+import static jadx.gui.ui.SearchDialog.SearchOptions.RESOURCE;
import static jadx.gui.ui.SearchDialog.SearchOptions.USE_REGEX;
public class TextSearchIndex {
private static final Logger LOG = LoggerFactory.getLogger(TextSearchIndex.class);
+ private final CacheObject cache;
+ private final MainWindow mainWindow;
private final JNodeCache nodeCache;
private final SimpleIndex clsNamesIndex;
@@ -46,7 +52,9 @@ public class TextSearchIndex {
private final List skippedClasses = new ArrayList<>();
- public TextSearchIndex(CacheObject cache) {
+ public TextSearchIndex(MainWindow mainWindow) {
+ this.mainWindow = mainWindow;
+ this.cache = mainWindow.getCacheObject();
this.nodeCache = cache.getNodeCache();
this.resIndex = new ResourceIndex(cache);
this.clsNamesIndex = new SimpleIndex();
@@ -81,8 +89,9 @@ public class TextSearchIndex {
}
int lineNum = i + 1;
JavaNode node = linesInfo.getJavaNodeByLine(lineNum);
- JNode nodeAtLine = nodeCache.makeFrom(node == null ? cls : node);
- codeIndex.put(new CodeNode(nodeAtLine, lineNum, line));
+ JavaNode javaNode = node == null ? cls : node;
+ JNode nodeAtLine = nodeCache.makeFrom(javaNode);
+ codeIndex.put(new CodeNode(nodeAtLine, line, lineNum, javaNode.getDefPos()));
}
} catch (Exception e) {
LOG.warn("Failed to index class: {}", cls, e);
@@ -108,10 +117,29 @@ public class TextSearchIndex {
Flowable result = Flowable.empty();
SearchSettings searchSettings = new SearchSettings(text, options.contains(IGNORE_CASE), options.contains(USE_REGEX));
+ if (options.contains(ACTIVE_TAB)) {
+ JumpPosition activeNode = mainWindow.getTabbedPane().getCurrentPosition();
+ if (activeNode != null) {
+ searchSettings.setActiveCls(activeNode.getNode().getRootClass());
+ }
+ if (searchSettings.getActiveCls() == null) {
+ return result;
+ }
+ }
if (!searchSettings.preCompile()) {
return result;
}
+ if (options.contains(COMMENT)) {
+ CommentsIndex commentsIndex = cache.getCommentsIndex();
+ result = Flowable.concat(result, commentsIndex.search(searchSettings));
+ if (text.isEmpty()) {
+ // return all comments on empty search string
+ // other searches don't support empty string, so return immediately
+ return result;
+ }
+ }
+
if (options.contains(CLASS)) {
result = Flowable.concat(result, clsNamesIndex.search(searchSettings));
}
@@ -129,7 +157,7 @@ public class TextSearchIndex {
result = Flowable.concat(result, searchInSkippedClasses(searchSettings));
}
}
- if (options.contains(Resource)) {
+ if (options.contains(RESOURCE)) {
result = Flowable.concat(result, resIndex.search(searchSettings));
}
return result;
@@ -168,7 +196,7 @@ public class TextSearchIndex {
int lineStart = 1 + code.lastIndexOf(ICodeWriter.NL, pos);
int lineEnd = code.indexOf(ICodeWriter.NL, pos + searchSettings.getSearchString().length());
StringRef line = StringRef.subString(code, lineStart, lineEnd == -1 ? code.length() : lineEnd);
- emitter.onNext(new CodeNode(nodeCache.makeFrom(javaClass), -pos, line.trim()).setPos(pos));
+ emitter.onNext(new CodeNode(nodeCache.makeFrom(javaClass), line.trim(), -1, pos));
return lineEnd;
}
diff --git a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties
index 5fc6c43db..409caf344 100644
--- a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties
+++ b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties
@@ -11,6 +11,7 @@ menu.heapUsageBar=Speicherverbrauchsleiste anzeigen
menu.navigation=Navigation
menu.text_search=Textsuche
menu.class_search=Klassen-Suche
+#menu.comment_search=Comment search
menu.tools=Tools
menu.deobfuscation=Deobfuscation
menu.log=Log-Anzeige
@@ -61,6 +62,12 @@ message.indexingClassesSkipped=Jadx hat nur noch wenig Speicherplatz. Dahe
heapUsage.text=JADX-Speicherauslastung: %.2f GB von %.2f GB
+#common_dialog.ok=
+#common_dialog.cancel=
+#common_dialog.add=
+#common_dialog.update=
+#common_dialog.remove=
+
search_dialog.open=Öffnen
search_dialog.cancel=Beenden
search_dialog.open_by_name=Nach Text suchen:
@@ -77,6 +84,8 @@ search_dialog.info_label=Zeige Ergebnisse %1$d bis %2$d von %3$d
search_dialog.col_node=Knoten
search_dialog.col_code=Code
search_dialog.regex=Regex
+#search_dialog.active_tab=Active tab only
+#search_dialog.comments=Comments
#search_dialog.resource=
#search_dialog.keep_open=
#search_dialog.tip_searching=
@@ -84,6 +93,11 @@ search_dialog.regex=Regex
usage_dialog.title=Verwendungssuche
usage_dialog.label=Verwendung für:
+#comment_dialog.title.add=Add code comment
+#comment_dialog.title.update=Update code comment
+#comment_dialog.label=Comment:
+#comment_dialog.usage=
+
log_viewer.title=Log-Anzeige
log_viewer.log_level=Log-Level:
@@ -155,6 +169,7 @@ msg.rename_disabled_deobfuscation_disabled=Bitte aktivieren Sie die Umbenennung
msg.cmd_select_class_error=Klasse\n%s auswählen nicht möglich\nSie existiert nicht.
#msg.rename_node_disabled=
#msg.rename_node_failed=
+#msg.cant_add_comment=Can't add comment here
#popup.bytecode_col=
#popup.line_wrap=
@@ -168,6 +183,8 @@ popup.select_all=Alle auswählen
popup.find_usage=Verwendung suchen
popup.go_to_declaration=Zur Erklärung gehen
popup.exclude=Ausschließen
+#popup.add_comment=Comment
+#popup.search_comment=Search comments
popup.rename=Umbennen
#popup.search=
#popup.search_global=
diff --git a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties
index da9c06cb3..28c314f15 100644
--- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties
+++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties
@@ -11,6 +11,7 @@ menu.heapUsageBar=Show memory usage bar
menu.navigation=Navigation
menu.text_search=Text search
menu.class_search=Class search
+menu.comment_search=Comment search
menu.tools=Tools
menu.deobfuscation=Deobfuscation
menu.log=Log Viewer
@@ -61,6 +62,12 @@ message.indexingClassesSkipped=Jadx is running low on memory. Therefore %d
heapUsage.text=JADX memory usage: %.2f GB of %.2f GB
+common_dialog.ok=Ok
+common_dialog.cancel=Cancel
+common_dialog.add=Add
+common_dialog.update=Update
+common_dialog.remove=Remove
+
search_dialog.open=Open
search_dialog.cancel=Cancel
search_dialog.open_by_name=Search for text:
@@ -77,6 +84,8 @@ search_dialog.info_label=Showing results %1$d to %2$d of %3$d
search_dialog.col_node=Node
search_dialog.col_code=Code
search_dialog.regex=Regex
+search_dialog.active_tab=Active tab only
+search_dialog.comments=Comments
search_dialog.resource=Resource
search_dialog.keep_open=Keep open
search_dialog.tip_searching=Searching ...
@@ -84,6 +93,11 @@ search_dialog.tip_searching=Searching ...
usage_dialog.title=Usage search
usage_dialog.label=Usage for:
+comment_dialog.title.add=Add code comment
+comment_dialog.title.update=Update code comment
+comment_dialog.label=Comment:
+comment_dialog.usage=Use Shift + Enter for start a new line
+
log_viewer.title=Log Viewer
log_viewer.log_level=Log level:
@@ -155,6 +169,7 @@ msg.rename_disabled_deobfuscation_disabled=Enable deobfuscation.
msg.cmd_select_class_error=Failed to select the class\n%s\nThe class does not exist.
msg.rename_node_disabled=Can't rename this node
msg.rename_node_failed=Can't rename %s
+msg.cant_add_comment=Can't add comment here
popup.bytecode_col=Show Bytecode
popup.line_wrap=Line Wrap
@@ -168,6 +183,8 @@ popup.select_all=Select All
popup.find_usage=Find Usage
popup.go_to_declaration=Go to declaration
popup.exclude=Exclude
+popup.add_comment=Comment
+popup.search_comment=Search comments
popup.rename=Rename
popup.search=Search "%s"
popup.search_global=Global Search "%s"
diff --git a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties
index 1e2e2ca5d..653ff2138 100644
--- a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties
+++ b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties
@@ -11,6 +11,7 @@ menu.flatten=Mostrar paquetes en vista plana
menu.navigation=Navegación
menu.text_search=Buscar texto
menu.class_search=Buscar clase
+#menu.comment_search=Comment search
menu.tools=Herramientas
menu.deobfuscation=Desofuscación
menu.log=Visor log
@@ -61,6 +62,12 @@ nav.forward=Adelante
#heapUsage.text=
+#common_dialog.ok=Ok
+#common_dialog.cancel=Cancel
+#common_dialog.add=Add
+#common_dialog.update=Update
+#common_dialog.remove=Remove
+
search_dialog.open=Abrir
search_dialog.cancel=Cancelar
search_dialog.open_by_name=Buscar texto:
@@ -77,6 +84,8 @@ search_dialog.info_label=Mostrando resultados %1$d a %2$d de %3$d
search_dialog.col_node=Nodo
search_dialog.col_code=Código
search_dialog.regex=Regex
+#search_dialog.active_tab=Active tab only
+#search_dialog.comments=Comments
#search_dialog.resource=
#search_dialog.keep_open=
#search_dialog.tip_searching=
@@ -84,6 +93,11 @@ search_dialog.regex=Regex
usage_dialog.title=Usage search
usage_dialog.label=Usage for:
+#comment_dialog.title.add=Add code comment
+#comment_dialog.title.update=Update code comment
+#comment_dialog.label=Comment:
+#comment_dialog.usage=
+
log_viewer.title=Visor log
log_viewer.log_level=Nivel log:
@@ -155,6 +169,7 @@ msg.index_not_initialized=Índice no inicializado, ¡la bósqueda se desactivar
#msg.cmd_select_class_error=
#msg.rename_node_disabled=
#msg.rename_node_failed=
+#msg.cant_add_comment=Can't add comment here
#popup.bytecode_col=
#popup.line_wrap=
@@ -168,6 +183,8 @@ popup.select_all=Seleccionar todo
#popup.find_usage=
#popup.go_to_declaration=
#popup.exclude=
+#popup.add_comment=Comment
+#popup.search_comment=Search comments
popup.rename=Nimeta ümber
#popup.search=
#popup.search_global=
diff --git a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties
index 7ae289aa7..2142aaacb 100644
--- a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties
+++ b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties
@@ -11,6 +11,7 @@ menu.heapUsageBar=메모리 사용량 표시
menu.navigation=네비게이션
menu.text_search=텍스트 검색
menu.class_search=클래스 검색
+#menu.comment_search=Comment search
menu.tools=도구
menu.deobfuscation=난독화 해제
menu.log=로그 뷰어
@@ -61,6 +62,12 @@ message.indexingClassesSkipped=Jadx의 메모리가 부족합니다. 따
heapUsage.text=JADX 메모리 사용량 : %.2f GB / %.2f GB
+#common_dialog.ok=Ok
+#common_dialog.cancel=Cancel
+#common_dialog.add=Add
+#common_dialog.update=Update
+#common_dialog.remove=Remove
+
search_dialog.open=열기
search_dialog.cancel=취소
search_dialog.open_by_name=텍스트 검색 :
@@ -77,6 +84,8 @@ search_dialog.info_label=%3$d 중 %1$d-%2$d 결과 표시
search_dialog.col_node=노드
search_dialog.col_code=코드
search_dialog.regex=정규식
+#search_dialog.active_tab=Active tab only
+#search_dialog.comments=Comments
search_dialog.resource=리소스
search_dialog.keep_open=열어 두기
search_dialog.tip_searching=검색 중...
@@ -84,6 +93,11 @@ search_dialog.tip_searching=검색 중...
usage_dialog.title=사용 검색
usage_dialog.label=다음의 사용 검색 결과:
+#comment_dialog.title.add=Add code comment
+#comment_dialog.title.update=Update code comment
+#comment_dialog.label=Comment:
+#comment_dialog.usage=
+
log_viewer.title=로그 뷰어
log_viewer.log_level=로그 레벨:
@@ -155,6 +169,7 @@ msg.rename_disabled_deobfuscation_disabled=난독 해제 활성화
msg.cmd_select_class_error=클래스를 선택하지 못했습니다.\n%s\n클래스가 없습니다.
msg.rename_node_disabled=이 노드의 이름을 바꿀 수 없습니다.
msg.rename_node_failed=%s의 이름을 바꿀 수 없습니다.
+#msg.cant_add_comment=Can't add comment here
#popup.bytecode_col=
popup.line_wrap=줄 바꿈
@@ -168,6 +183,8 @@ popup.select_all=모두 선택
popup.find_usage=사용 찾기
popup.go_to_declaration=선언문으로 이동
popup.exclude=제외
+#popup.add_comment=Comment
+#popup.search_comment=Search comments
popup.rename=이름 바꾸기
popup.search="%s" 검색
popup.search_global="%s" 전역 검색
diff --git a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties
index fd17184fc..e1aa47abc 100644
--- a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties
+++ b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties
@@ -11,6 +11,7 @@ menu.heapUsageBar=显示内存使用栏
menu.navigation=导航
menu.text_search=搜索文本
menu.class_search=搜索类
+#menu.comment_search=Comment search
menu.tools=工具
menu.deobfuscation=反混淆
menu.log=日志查看器
@@ -61,6 +62,12 @@ message.indexingClassesSkipped=Jadx 的内存不足。因此,%d 类没
heapUsage.text=JADX 内存使用率:%.2f GB 共 %.2f GB
+#common_dialog.ok=Ok
+#common_dialog.cancel=Cancel
+#common_dialog.add=Add
+#common_dialog.update=Update
+#common_dialog.remove=Remove
+
search_dialog.open=转到
search_dialog.cancel=取消
search_dialog.open_by_name=搜索文本:
@@ -77,6 +84,8 @@ search_dialog.info_label=显示了 %3$d 个结果中的第 %1$d 至第 %2$d 个
search_dialog.col_node=节点
search_dialog.col_code=代码
search_dialog.regex=正则表达式
+#search_dialog.active_tab=Active tab only
+#search_dialog.comments=Comments
#search_dialog.resource=
#search_dialog.keep_open=
#search_dialog.tip_searching=
@@ -84,6 +93,11 @@ search_dialog.regex=正则表达式
usage_dialog.title=查找
usage_dialog.label=查找用例:
+#comment_dialog.title.add=Add code comment
+#comment_dialog.title.update=Update code comment
+#comment_dialog.label=Comment:
+#comment_dialog.usage=
+
log_viewer.title=日志查看器
log_viewer.log_level=日志等级:
@@ -155,6 +169,7 @@ msg.rename_disabled_deobfuscation_disabled=请启用反混淆以重命名。
msg.cmd_select_class_error=无法选择类\n%s\n该类不存在。
#msg.rename_node_disabled=
#msg.rename_node_failed=
+#msg.cant_add_comment=Can't add comment here
#popup.bytecode_col=
#popup.line_wrap=
@@ -168,6 +183,8 @@ popup.select_all=全选
popup.find_usage=查找用例
popup.go_to_declaration=跳到声明
popup.exclude=排除
+#popup.add_comment=Comment
+#popup.search_comment=Search comments
popup.rename=改名
#popup.search=
#popup.search_global=
diff --git a/jadx-gui/src/main/resources/icons-16/table_edit.png b/jadx-gui/src/main/resources/icons-16/table_edit.png
new file mode 100644
index 000000000..bfcb0249a
Binary files /dev/null and b/jadx-gui/src/main/resources/icons-16/table_edit.png differ
diff --git a/jadx-gui/src/test/java/jadx/gui/utils/JumpManagerTest.java b/jadx-gui/src/test/java/jadx/gui/utils/JumpManagerTest.java
index 02b90806d..ce53e0547 100644
--- a/jadx-gui/src/test/java/jadx/gui/utils/JumpManagerTest.java
+++ b/jadx-gui/src/test/java/jadx/gui/utils/JumpManagerTest.java
@@ -118,6 +118,6 @@ class JumpManagerTest {
}
private JumpPosition makeJumpPos() {
- return new JumpPosition(new TextNode(""), 0);
+ return new JumpPosition(new TextNode(""), 0, 0);
}
}