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