feat(gui): add code comments (#359) (PR #1127)

* feat(gui): add code comments (#359)
* refactor: replace instanceof search with method dispatch in RegionGen
* fix: various bug fixes and improvements for code comments
* fix(gui): support multiline code comments
* fix: resolve code differences after class reload
* fix(gui): add search for comments, allow search in active tab only
* fix: correct search for inner classes
* fix(gui): run full index on search dialog open
This commit is contained in:
skylot
2021-03-04 17:45:48 +03:00
committed by GitHub
parent 7a14aaa17e
commit 4e5fac4b88
113 changed files with 3527 additions and 722 deletions
+190
View File
@@ -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 <code>WrapLayout</code> with a left
* alignment and a default 5-unit horizontal and vertical gap.
*/
public WrapLayout()
{
super();
}
/**
* Constructs a new <code>FlowLayout</code> with the specified
* alignment and a default 5-unit horizontal and vertical gap.
* The value of the alignment argument must be one of
* <code>WrapLayout</code>, <code>WrapLayout</code>,
* or <code>WrapLayout</code>.
* @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.
* <p>
* The value of the alignment argument must be one of
* <code>WrapLayout</code>, <code>WrapLayout</code>,
* or <code>WrapLayout</code>.
* @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
* <i>visible</i> 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 <i>visible</i>
* 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;
}
}
@@ -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();
}
@@ -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<CodePosition, Object> getRawAnnotations();
}
@@ -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
@@ -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() {
@@ -131,7 +131,7 @@ public final class JavaClass implements JavaNode {
return decompiler;
}
private Map<CodePosition, Object> getCodeAnnotations() {
public Map<CodePosition, Object> 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<CodePosition, JavaNode> getUsageMap() {
Map<CodePosition, Object> 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);
@@ -49,6 +49,11 @@ public final class JavaField implements JavaNode {
return field.getDecompiledLine();
}
@Override
public int getDefPos() {
return field.getDefPosition();
}
@Override
public List<JavaNode> getUseIn() {
return getDeclaringClass().getRootDecompiler().convertNodes(field.getUseIn());
@@ -85,6 +85,11 @@ public final class JavaMethod implements JavaNode {
return mth.getDecompiledLine();
}
@Override
public int getDefPos() {
return mth.getDefPosition();
}
/**
* Internal API. Not Stable!
*/
@@ -14,5 +14,7 @@ public interface JavaNode {
int getDecompiledLine();
int getDefPos();
List<JavaNode> getUseIn();
}
@@ -44,6 +44,11 @@ public final class JavaPackage implements JavaNode, Comparable<JavaPackage> {
return 0;
}
@Override
public int getDefPos() {
return 0;
}
@Override
public List<JavaNode> getUseIn() {
return Collections.emptyList();
@@ -43,6 +43,11 @@ public class JavaVariable implements JavaNode {
return node.getDecompiledLine();
}
@Override
public int getDefPos() {
return node.getDefPosition();
}
@Override
public List<JavaNode> getUseIn() {
return Collections.emptyList();
@@ -0,0 +1,22 @@
package jadx.api.data;
import org.jetbrains.annotations.Nullable;
public interface ICodeComment extends Comparable<ICodeComment> {
IJavaNodeRef getNodeRef();
String getComment();
/**
* Instruction offset inside method
*/
int getOffset();
enum AttachType {
VAR_DECLARE
}
@Nullable
AttachType getAttachType();
}
@@ -0,0 +1,10 @@
package jadx.api.data;
import java.util.List;
public interface ICodeData {
long getUpdateId();
List<ICodeComment> getComments();
}
@@ -0,0 +1,14 @@
package jadx.api.data;
public interface IJavaNodeRef extends Comparable<IJavaNodeRef> {
enum RefType {
CLASS, FIELD, METHOD
}
RefType getType();
String getDeclaringClass();
String getShortId();
}
@@ -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 + '}';
}
}
@@ -0,0 +1,5 @@
package jadx.api.data.annotations;
public interface ICodeRawOffset {
int getOffset();
}
@@ -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;
}
}
@@ -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<ICodeComment> 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 + '}';
}
}
@@ -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<ICodeComment> comments = Collections.emptyList();
@Override
public long getUpdateId() {
return updateId;
}
public void markUpdate() {
updateId = System.currentTimeMillis();
}
@Override
public List<ICodeComment> getComments() {
return comments;
}
public void setComments(List<ICodeComment> 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;
}
}
@@ -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<IJavaNodeRef> {
@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<IJavaNodeRef> 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";
}
}
}
@@ -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<CodePosition, Object> 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<Integer, Integer> 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<CodePosition, Object> 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;
@@ -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<CodePosition, Object> getRawAnnotations() {
return Collections.emptyMap();
}
@Override
public String getCodeStr() {
removeFirstEmptyLine();
+5 -2
View File
@@ -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<IDexTreeVisitor> getFallbackPassesList() {
List<IDexTreeVisitor> passes = new ArrayList<>(3);
List<IDexTreeVisitor> 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<IDexTreeVisitor> 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());
@@ -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);
}
@@ -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()) {
@@ -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) {
@@ -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);
@@ -135,9 +135,6 @@ public class NameGen {
if (!NameMapper.isValidAndPrintable(name)) {
name = getFallbackName(var);
}
if (Consts.DEBUG) {
name += '_' + getFallbackName(var);
}
return name;
}
@@ -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<BlockNode> 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<BlockNode> 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);
}
}
@@ -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);
@@ -42,6 +42,9 @@ import jadx.core.dex.trycatch.SplitterBlockAttr;
@SuppressWarnings("InstantiationOfUtilityClass")
public class AType<T extends IAttribute> {
// class, method, field, insn
public static final AType<AttrList<String>> CODE_COMMENTS = new AType<>();
// class, method, field
public static final AType<AnnotationsList> ANNOTATION_LIST = new AType<>();
public static final AType<RenameReasonAttr> RENAME_REASON = new AType<>();
@@ -33,6 +33,23 @@ public abstract class AttrNode implements IAttributeNode {
}
}
@Override
public <T extends IAttribute> void copyAttributeFrom(AttrNode attrNode, AType<T> 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 <T extends IAttribute> void rewriteAttributeFrom(AttrNode attrNode, AType<T> attrType) {
remove(attrType);
copyAttributeFrom(attrNode, attrType);
}
private AttributeStorage initStorage() {
AttributeStorage store = storage;
if (store == EMPTY_ATTR_STORAGE) {
@@ -14,6 +14,10 @@ public interface IAttributeNode {
void copyAttributesFrom(AttrNode attrNode);
<T extends IAttribute> void copyAttributeFrom(AttrNode attrNode, AType<T> attrType);
<T extends IAttribute> void rewriteAttributeFrom(AttrNode attrNode, AType<T> attrType);
boolean contains(AFlag flag);
<T extends IAttribute> boolean contains(AType<T> type);
@@ -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);
}
}
@@ -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);
}
@@ -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);
}
@@ -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<InsnNode> getInstructions();
@Override
default void generate(RegionGen regionGen, ICodeWriter code) throws CodegenException {
regionGen.makeSimpleBlock(this, code);
}
}
@@ -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());
}
}
@@ -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);
}
}
}
@@ -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);
@@ -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();
@@ -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());
@@ -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();
@@ -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;
}
}
@@ -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();
@@ -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();
@@ -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<ICodeComment> clsComments = getCommentsData(cls);
if (!clsComments.isEmpty()) {
applyComments(cls, clsComments);
}
cls.getInnerClasses().forEach(this::visit);
return false;
}
private static void applyComments(ClassNode cls, List<ICodeComment> 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<String, List<ICodeComment>> clsCommentsMap;
}
private List<ICodeComment> 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<ICodeComment> clsComments = commentsData.clsCommentsMap.get(cls.getClassInfo().getFullName());
if (clsComments == null) {
return Collections.emptyList();
}
return clsComments;
}
}
private static void updateCommentsData(ICodeData data, CommentsData commentsData) {
Map<String, List<ICodeComment>> map = new HashMap<>();
for (ICodeComment comment : data.getComments()) {
String declClsId = comment.getNodeRef().getDeclaringClass();
List<ICodeComment> comments = map.computeIfAbsent(declClsId, s -> new ArrayList<>());
comments.add(comment);
}
commentsData.clsCommentsMap = map;
commentsData.updateId = data.getUpdateId();
}
}
@@ -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;
}
@@ -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
}
}
@@ -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<ArgType> superTypes = collectSuperTypes(cls);
if (!superTypes.isEmpty()) {
for (MethodNode mth : cls.getMethods()) {
processMth(cls, superTypes, mth);
}
}
return true;
}
private void processMth(ClassNode cls, List<ArgType> superTypes, MethodNode mth) {
@@ -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) {
@@ -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) {
@@ -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);
@@ -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)) {
@@ -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) {
@@ -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<String> 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<String> 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<String> 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(" */");
}
}
@@ -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 <T> InterfaceReplace<T> interfaceReplace(Class<T> replaceCls) {
return new InterfaceReplace<>(replaceCls);
}
private static final class InterfaceReplace<T> implements JsonSerializer<T>, JsonDeserializer<T> {
private final Class<T> replaceCls;
private InterfaceReplace(Class<T> 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);
}
}
}
@@ -58,6 +58,28 @@ public class RegionUtils {
}
}
public static InsnNode getFirstInsn(IContainer container) {
if (container instanceof IBlock) {
IBlock block = (IBlock) container;
List<InsnNode> 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<IContainer> 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;
@@ -308,6 +308,23 @@ public class Utils {
return list.get(0);
}
@Nullable
public static <T> T first(List<T> list) {
if (list.isEmpty()) {
return null;
}
return list.get(0);
}
@Nullable
public static <T> T first(Iterable<T> list) {
Iterator<T> it = list.iterator();
if (!it.hasNext()) {
return null;
}
return it.next();
}
@Nullable
public static <T> T last(List<T> list) {
if (list.isEmpty()) {
@@ -316,6 +333,20 @@ public class Utils {
return list.get(list.size() - 1);
}
@Nullable
public static <T> T last(Iterable<T> list) {
Iterator<T> it = list.iterator();
if (!it.hasNext()) {
return null;
}
while (true) {
T next = it.next();
if (!it.hasNext()) {
return next;
}
}
}
public static <T> T getOrElse(@Nullable T obj, T defaultObj) {
if (obj == null) {
return defaultObj;
@@ -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);
}
}
@@ -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<JadxClassNodeA
super(cls, JadxClassNodeAssertions.class);
}
public JadxCodeInfoAssertions decompile() {
isNotNull();
ICodeInfo codeInfo = actual.getCode();
Assertions.assertThat(codeInfo).isNotNull();
return new JadxCodeInfoAssertions(codeInfo);
}
public JadxCodeAssertions code() {
isNotNull();
ICodeInfo code = actual.getCode();
assertThat(code).isNotNull();
Assertions.assertThat(code).isNotNull();
String codeStr = code.getCodeStr();
assertThat(codeStr).isNotBlank();
return new JadxCodeAssertions(codeStr);
@@ -25,9 +33,9 @@ public class JadxClassNodeAssertions extends AbstractObjectAssert<JadxClassNodeA
public JadxCodeAssertions reloadCode(IntegrationTest testInstance) {
isNotNull();
ICodeInfo code = actual.reloadCode();
assertThat(code).isNotNull();
Assertions.assertThat(code).isNotNull();
String codeStr = code.getCodeStr();
assertThat(codeStr).isNotBlank();
Assertions.assertThat(codeStr).isNotBlank();
JadxCodeAssertions codeAssertions = new JadxCodeAssertions(codeStr);
codeAssertions.print();
@@ -0,0 +1,36 @@
package jadx.tests.api.utils.assertj;
import java.util.stream.Collectors;
import org.assertj.core.api.AbstractObjectAssert;
import jadx.api.ICodeInfo;
import jadx.api.data.annotations.ICodeRawOffset;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class JadxCodeInfoAssertions extends AbstractObjectAssert<JadxCodeInfoAssertions, ICodeInfo> {
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;
}
}
@@ -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() {");
@@ -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");
}
}
@@ -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());
}
}
@@ -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());
}
}
@@ -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");
}
}
@@ -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());
}
}
@@ -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<Path> 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);
}
/**
@@ -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();
@@ -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<Path> filesPath;
private List<String[]> treeExpansions = new ArrayList<>();
private transient boolean saved;
private transient boolean initial = true;
private transient boolean saved;
private int projectVersion = 0;
private List<Path> files;
private List<String[]> 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<Path> getFilePaths() {
return filesPath;
return files;
}
public void setFilePath(List<Path> 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<String[]> 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<String> 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();
}
}
@@ -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();
@@ -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) {
@@ -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;
}
}
@@ -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());
@@ -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);
@@ -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();
@@ -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;
@@ -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<List<ICodeComment>> updater) {
try {
JadxProject project = codeArea.getProject();
JadxCodeData codeData = project.getCodeData();
if (codeData == null) {
codeData = new JadxCodeData();
}
List<ICodeComment> 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();
}
}
@@ -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;
}
@@ -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<Path> filePaths = project.getFilePaths();
List<Path> 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;
}
@@ -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<JClass> updatedClasses) {
for (Map.Entry<JNode, ContentPanel> 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);
@@ -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<SearchOptions> options;
private final transient SearchPreset searchPreset;
private final transient Set<SearchOptions> 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<SearchOptions> additionalOptions) {
super(mainWindow);
this.textSearch = textSearch;
if (textSearch) {
Set<SearchOptions> 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<SearchOptions> buildOptions(SearchPreset preset) {
Set<SearchOptions> 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<String> textChanges = onTextFieldChanges(searchField);
Flowable<String> 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<JNode> 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;
}
}
}
@@ -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<JNode, ContentPanel> 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();
}
}
@@ -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;
}
}
@@ -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();
}
}
@@ -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();
}
}
@@ -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;
}
}
@@ -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;
}
}
}
@@ -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);
}
}
@@ -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<T> extends AbstractAction implements PopupMenuListener {
private static final long serialVersionUID = -2600154727884853550L;
@@ -36,8 +38,7 @@ public abstract class JNodeMenuAction<T> 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);
@@ -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;
}
@@ -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<SearchDialog.SearchOptions> lastSearchOptions;
private Map<SearchDialog.SearchPreset, Set<SearchDialog.SearchOptions>> 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<SearchDialog.SearchOptions> lastSearchOptions) {
this.lastSearchOptions = lastSearchOptions;
}
public Set<SearchDialog.SearchOptions> getLastSearchOptions() {
public Map<SearchDialog.SearchPreset, Set<SearchDialog.SearchOptions>> getLastSearchOptions() {
return lastSearchOptions;
}
@@ -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<CodePosition, Object> 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 + '}';
}
}
}
@@ -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<Integer, JavaNode> 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<Integer, JavaNode> entry = map.ceilingEntry(line);
if (entry == null) {
return null;
}
return entry.getValue();
}
public JavaNode getDefAtLine(int line) {
return map.get(line);
}
}
@@ -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);
}
@@ -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) {
}
}
@@ -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
@@ -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();
@@ -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;
}
}

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