diff --git a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java index c3b1a42dc..efd44b964 100644 --- a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java +++ b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java @@ -6,7 +6,6 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -45,6 +44,7 @@ import jadx.core.dex.attributes.AFlag; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.MethodNode; +import jadx.core.dex.nodes.PackageNode; import jadx.core.dex.nodes.RootNode; import jadx.core.dex.visitors.SaveCode; import jadx.core.export.ExportGradleProject; @@ -444,25 +444,7 @@ public final class JadxDecompiler implements IJadxDecompiler, Closeable { } public List getPackages() { - List classList = getClasses(); - if (classList.isEmpty()) { - return Collections.emptyList(); - } - Map> map = new HashMap<>(); - for (JavaClass javaClass : classList) { - String pkg = javaClass.getPackage(); - List clsList = map.computeIfAbsent(pkg, k -> new ArrayList<>()); - clsList.add(javaClass); - } - List packages = new ArrayList<>(map.size()); - for (Map.Entry> entry : map.entrySet()) { - packages.add(new JavaPackage(entry.getKey(), entry.getValue())); - } - Collections.sort(packages); - for (JavaPackage pkg : packages) { - pkg.getClasses().sort(Comparator.comparing(JavaClass::getName, String.CASE_INSENSITIVE_ORDER)); - } - return Collections.unmodifiableList(packages); + return Utils.collectionMap(root.getPackages(), this::convertPackageNode); } public int getErrorsCount() { @@ -545,6 +527,26 @@ public final class JadxDecompiler implements IJadxDecompiler, Closeable { return javaMethod; } + @ApiStatus.Internal + synchronized JavaPackage convertPackageNode(PackageNode pkg) { + JavaPackage foundPkg = pkg.getJavaNode(); + if (foundPkg != null) { + return foundPkg; + } + List clsList = Utils.collectionMap(pkg.getClasses(), this::convertClassNode); + int subPkgsCount = pkg.getSubPackages().size(); + List subPkgs = subPkgsCount == 0 ? Collections.emptyList() : new ArrayList<>(subPkgsCount); + JavaPackage javaPkg = new JavaPackage(pkg, clsList, subPkgs); + if (subPkgsCount != 0) { + // add subpackages after parent to avoid endless recursion + for (PackageNode subPackage : pkg.getSubPackages()) { + subPkgs.add(convertPackageNode(subPackage)); + } + } + pkg.setJavaNode(javaPkg); + return javaPkg; + } + @Nullable public JavaClass searchJavaClassByOrigFullName(String fullName) { return getRoot().getClasses().stream() diff --git a/jadx-core/src/main/java/jadx/api/JavaNode.java b/jadx-core/src/main/java/jadx/api/JavaNode.java index 3bb431231..86ea79747 100644 --- a/jadx-core/src/main/java/jadx/api/JavaNode.java +++ b/jadx-core/src/main/java/jadx/api/JavaNode.java @@ -18,8 +18,7 @@ public interface JavaNode { List getUseIn(); - default void removeAlias() { - } + void removeAlias(); boolean isOwnCodeAnnotation(ICodeAnnotation ann); } diff --git a/jadx-core/src/main/java/jadx/api/JavaPackage.java b/jadx-core/src/main/java/jadx/api/JavaPackage.java index 391b71710..ac665392f 100644 --- a/jadx-core/src/main/java/jadx/api/JavaPackage.java +++ b/jadx-core/src/main/java/jadx/api/JavaPackage.java @@ -1,36 +1,85 @@ package jadx.api; -import java.util.Collections; +import java.util.ArrayList; import java.util.List; +import java.util.Objects; +import org.jetbrains.annotations.ApiStatus.Internal; import org.jetbrains.annotations.NotNull; import jadx.api.metadata.ICodeAnnotation; +import jadx.core.dex.info.PackageInfo; +import jadx.core.dex.nodes.PackageNode; public final class JavaPackage implements JavaNode, Comparable { - private final String name; + private final PackageNode pkgNode; private final List classes; + private final List subPkgs; - JavaPackage(String name, List classes) { - this.name = name; + JavaPackage(PackageNode pkgNode, List classes, List subPkgs) { + this.pkgNode = pkgNode; this.classes = classes; + this.subPkgs = subPkgs; } @Override public String getName() { - return name; + return pkgNode.getAliasPkgInfo().getName(); } @Override public String getFullName() { - // TODO: store full package name - return name; + return pkgNode.getAliasPkgInfo().getFullName(); + } + + public String getRawName() { + return pkgNode.getPkgInfo().getName(); + } + + public String getRawFullName() { + return pkgNode.getPkgInfo().getFullName(); + } + + public List getSubPackages() { + return subPkgs; } public List getClasses() { return classes; } + public boolean isRoot() { + return pkgNode.isRoot(); + } + + public boolean isLeaf() { + return pkgNode.isLeaf(); + } + + public boolean isDefault() { + return getFullName().isEmpty(); + } + + public void rename(String alias) { + pkgNode.rename(alias); + } + + @Override + public void removeAlias() { + pkgNode.removeAlias(); + } + + public boolean isParentRenamed() { + PackageInfo parent = pkgNode.getPkgInfo().getParentPkg(); + PackageInfo aliasParent = pkgNode.getAliasPkgInfo().getParentPkg(); + return !Objects.equals(parent, aliasParent); + } + + @Internal + public PackageNode getPkgNode() { + return pkgNode; + } + @Override public JavaClass getDeclaringClass() { return null; @@ -48,7 +97,16 @@ public final class JavaPackage implements JavaNode, Comparable { @Override public List getUseIn() { - return Collections.emptyList(); + List list = new ArrayList<>(); + addUseIn(list); + return list; + } + + public void addUseIn(List list) { + list.addAll(classes); + for (JavaPackage subPkg : subPkgs) { + subPkg.addUseIn(list); + } } @Override @@ -58,7 +116,7 @@ public final class JavaPackage implements JavaNode, Comparable { @Override public int compareTo(@NotNull JavaPackage o) { - return name.compareTo(o.name); + return pkgNode.compareTo(o.pkgNode); } @Override @@ -70,16 +128,16 @@ public final class JavaPackage implements JavaNode, Comparable { return false; } JavaPackage that = (JavaPackage) o; - return name.equals(that.name); + return pkgNode.equals(that.pkgNode); } @Override public int hashCode() { - return name.hashCode(); + return pkgNode.hashCode(); } @Override public String toString() { - return name; + return pkgNode.toString(); } } diff --git a/jadx-core/src/main/java/jadx/api/JavaVariable.java b/jadx-core/src/main/java/jadx/api/JavaVariable.java index 5b4aae682..5112e61fb 100644 --- a/jadx-core/src/main/java/jadx/api/JavaVariable.java +++ b/jadx-core/src/main/java/jadx/api/JavaVariable.java @@ -71,6 +71,10 @@ public class JavaVariable implements JavaNode { return Collections.singletonList(mth); } + @Override + public void removeAlias() { + } + @Override public boolean isOwnCodeAnnotation(ICodeAnnotation ann) { if (ann.getAnnType() == ICodeAnnotation.AnnType.VAR_REF) { diff --git a/jadx-core/src/main/java/jadx/api/data/impl/JadxCodeRename.java b/jadx-core/src/main/java/jadx/api/data/impl/JadxCodeRename.java index 2005a0fe2..78aa88436 100644 --- a/jadx-core/src/main/java/jadx/api/data/impl/JadxCodeRename.java +++ b/jadx-core/src/main/java/jadx/api/data/impl/JadxCodeRename.java @@ -85,4 +85,12 @@ public class JadxCodeRename implements ICodeRename { public int hashCode() { return 31 * getNodeRef().hashCode() + Objects.hashCode(getCodeRef()); } + + @Override + public String toString() { + return "JadxCodeRename{" + nodeRef + + ", codeRef=" + codeRef + + ", newName='" + newName + '\'' + + '}'; + } } diff --git a/jadx-core/src/main/java/jadx/core/deobf/NameMapper.java b/jadx-core/src/main/java/jadx/core/deobf/NameMapper.java index ce10bd7f7..9e1d9367f 100644 --- a/jadx-core/src/main/java/jadx/core/deobf/NameMapper.java +++ b/jadx-core/src/main/java/jadx/core/deobf/NameMapper.java @@ -11,7 +11,7 @@ import static jadx.core.utils.StringUtils.notEmpty; public class NameMapper { - private static final Pattern VALID_JAVA_IDENTIFIER = Pattern.compile( + public static final Pattern VALID_JAVA_IDENTIFIER = Pattern.compile( "\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*"); private static final Pattern VALID_JAVA_FULL_IDENTIFIER = Pattern.compile( diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/PackageNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/PackageNode.java index 4e3ca6dd3..b5ff66f16 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/PackageNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/PackageNode.java @@ -7,6 +7,7 @@ import java.util.Objects; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import jadx.api.JavaPackage; import jadx.core.dex.info.PackageInfo; public class PackageNode implements IPackageUpdate, IDexNode, Comparable { @@ -19,6 +20,8 @@ public class PackageNode implements IPackageUpdate, IDexNode, Comparable> customPasses) { diff --git a/jadx-core/src/main/java/jadx/core/utils/DebugUtils.java b/jadx-core/src/main/java/jadx/core/utils/DebugUtils.java index aa251b8f8..22877be20 100644 --- a/jadx-core/src/main/java/jadx/core/utils/DebugUtils.java +++ b/jadx-core/src/main/java/jadx/core/utils/DebugUtils.java @@ -225,7 +225,7 @@ public class DebugUtils { } public static void printStackTrace(String label) { - LOG.debug("StackTrace: {}\n{}", label, Utils.getStackTrace(new Exception())); + LOG.debug("StackTrace: {}\n{}", label, Utils.getFullStackTrace(new Exception())); } public static void printMethodOverrideTop(RootNode root) { diff --git a/jadx-core/src/main/java/jadx/core/utils/Utils.java b/jadx-core/src/main/java/jadx/core/utils/Utils.java index 648a75506..53786eafd 100644 --- a/jadx-core/src/main/java/jadx/core/utils/Utils.java +++ b/jadx-core/src/main/java/jadx/core/utils/Utils.java @@ -3,10 +3,12 @@ package jadx.core.utils; import java.io.OutputStream; import java.io.PrintWriter; import java.io.StringWriter; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.Deque; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -14,6 +16,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.function.Consumer; import java.util.function.Function; import org.jetbrains.annotations.Nullable; @@ -116,13 +119,23 @@ public class Utils { return sb.toString(); } + public static String getFullStackTrace(Throwable throwable) { + return getStackTrace(throwable, false); + } + public static String getStackTrace(Throwable throwable) { + return getStackTrace(throwable, true); + } + + private static String getStackTrace(Throwable throwable, boolean filter) { if (throwable == null) { return ""; } StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw, true); - filterRecursive(throwable); + if (filter) { + filterRecursive(throwable); + } throwable.printStackTrace(pw); return sw.getBuffer().toString(); } @@ -345,6 +358,27 @@ public class Utils { return map; } + /** + * Simple DFS visit for tree (cycles not allowed) + */ + public static void treeDfsVisit(T root, Function> childrenProvider, Consumer visitor) { + multiRootTreeDfsVisit(Collections.singletonList(root), childrenProvider, visitor); + } + + public static void multiRootTreeDfsVisit(List roots, Function> childrenProvider, Consumer visitor) { + Deque queue = new ArrayDeque<>(roots); + while (true) { + T current = queue.pollLast(); + if (current == null) { + return; + } + visitor.accept(current); + for (T child : childrenProvider.apply(current)) { + queue.addLast(child); + } + } + } + @Nullable public static T getOne(@Nullable List list) { if (list == null || list.size() != 1) { diff --git a/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java b/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java index 380c7bb80..033759f87 100644 --- a/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java +++ b/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java @@ -32,6 +32,7 @@ import jadx.gui.plugins.context.PluginsContext; import jadx.gui.settings.JadxProject; import jadx.gui.settings.JadxSettings; import jadx.gui.ui.MainWindow; +import jadx.gui.utils.CacheObject; import jadx.gui.utils.codecache.CodeStringCache; import jadx.gui.utils.codecache.disk.BufferCodeCache; import jadx.gui.utils.codecache.disk.DiskCodeCache; @@ -264,6 +265,10 @@ public class JadxWrapper { return mainWindow.getSettings(); } + public CacheObject getCache() { + return mainWindow.getCacheObject(); + } + /** * @param fullName * Full name of an outer class. Inner classes are not supported. diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java index 3b216ea8c..854bbf107 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java @@ -1,5 +1,8 @@ package jadx.gui.treemodel; +import java.util.List; +import java.util.Set; + import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JPopupMenu; @@ -12,6 +15,10 @@ import jadx.api.JavaClass; import jadx.api.JavaField; import jadx.api.JavaMethod; import jadx.api.JavaNode; +import jadx.api.data.ICodeRename; +import jadx.api.data.impl.JadxCodeRename; +import jadx.api.data.impl.JadxNodeRef; +import jadx.core.deobf.NameMapper; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.info.AccessInfo; import jadx.gui.ui.MainWindow; @@ -24,7 +31,7 @@ import jadx.gui.utils.Icons; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; -public class JClass extends JLoadableNode { +public class JClass extends JLoadableNode implements JRenameNode { private static final long serialVersionUID = -1239986875244097177L; private static final ImageIcon ICON_CLASS_ABSTRACT = UiUtils.openSvgIcon("nodes/abstractClass"); @@ -39,16 +46,10 @@ public class JClass extends JLoadableNode { private final transient JClass jParent; private transient boolean loaded; - public JClass(JavaClass cls) { - this.cls = cls; - this.jParent = null; - this.loaded = false; - } - public JClass(JavaClass cls, JClass parent) { this.cls = cls; this.jParent = parent; - this.loaded = true; + this.loaded = parent != null; } public JavaClass getCls() { @@ -185,6 +186,37 @@ public class JClass extends JLoadableNode { return cls.getFullName(); } + @Override + public String getTitle() { + return makeLongStringHtml(); + } + + @Override + public boolean isValidName(String newName) { + return NameMapper.isValidIdentifier(newName); + } + + @Override + public ICodeRename buildCodeRename(String newName, Set renames) { + return new JadxCodeRename(JadxNodeRef.forCls(cls), newName); + } + + @Override + public void removeAlias() { + cls.removeAlias(); + } + + @Override + public void addUpdateNodes(List toUpdate) { + toUpdate.add(cls); + toUpdate.addAll(cls.getUseIn()); + } + + @Override + public void reload(MainWindow mainWindow) { + mainWindow.reloadTree(); + } + @Override public int hashCode() { return cls.hashCode(); diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JField.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JField.java index 723f3d483..9c170abba 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JField.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JField.java @@ -1,6 +1,8 @@ package jadx.gui.treemodel; import java.util.Comparator; +import java.util.List; +import java.util.Set; import javax.swing.Icon; import javax.swing.ImageIcon; @@ -11,6 +13,10 @@ import org.jetbrains.annotations.NotNull; import jadx.api.JavaField; import jadx.api.JavaNode; +import jadx.api.data.ICodeRename; +import jadx.api.data.impl.JadxCodeRename; +import jadx.api.data.impl.JadxNodeRef; +import jadx.core.deobf.NameMapper; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.info.AccessInfo; import jadx.gui.ui.MainWindow; @@ -18,7 +24,7 @@ import jadx.gui.ui.dialog.RenameDialog; import jadx.gui.utils.Icons; import jadx.gui.utils.UiUtils; -public class JField extends JNode { +public class JField extends JNode implements JRenameNode { private static final long serialVersionUID = 1712572192106793359L; private static final ImageIcon ICON_FLD_PRI = UiUtils.openSvgIcon("nodes/privateField"); @@ -61,6 +67,37 @@ public class JField extends JNode { return RenameDialog.buildRenamePopup(mainWindow, this); } + @Override + public String getTitle() { + return makeLongStringHtml(); + } + + @Override + public ICodeRename buildCodeRename(String newName, Set renames) { + return new JadxCodeRename(JadxNodeRef.forFld(field), newName); + } + + @Override + public boolean isValidName(String newName) { + return NameMapper.isValidIdentifier(newName); + } + + @Override + public void removeAlias() { + field.removeAlias(); + } + + @Override + public void addUpdateNodes(List toUpdate) { + toUpdate.add(field); + toUpdate.addAll(field.getUseIn()); + } + + @Override + public void reload(MainWindow mainWindow) { + mainWindow.reloadTree(); + } + @Override public Icon getIcon() { AccessInfo af = field.getAccessFlags(); diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JMethod.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JMethod.java index e42bb026b..66d056fcb 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JMethod.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JMethod.java @@ -2,6 +2,8 @@ package jadx.gui.treemodel; import java.util.Comparator; import java.util.Iterator; +import java.util.List; +import java.util.Set; import javax.swing.Icon; import javax.swing.ImageIcon; @@ -12,6 +14,10 @@ import org.jetbrains.annotations.NotNull; import jadx.api.JavaMethod; import jadx.api.JavaNode; +import jadx.api.data.ICodeRename; +import jadx.api.data.impl.JadxCodeRename; +import jadx.api.data.impl.JadxNodeRef; +import jadx.core.deobf.NameMapper; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.info.AccessInfo; import jadx.core.dex.instructions.args.ArgType; @@ -21,7 +27,7 @@ import jadx.gui.utils.Icons; import jadx.gui.utils.OverlayIcon; import jadx.gui.utils.UiUtils; -public class JMethod extends JNode { +public class JMethod extends JNode implements JRenameNode { private static final long serialVersionUID = 3834526867464663751L; private static final ImageIcon ICON_METHOD_ABSTRACT = UiUtils.openSvgIcon("nodes/abstractMethod"); private static final ImageIcon ICON_METHOD_PRIVATE = UiUtils.openSvgIcon("nodes/privateMethod"); @@ -100,14 +106,6 @@ public class JMethod extends JNode { return SyntaxConstants.SYNTAX_STYLE_JAVA; } - @Override - public boolean canRename() { - if (mth.isClassInit()) { - return false; - } - return !mth.getMethodNode().contains(AFlag.DONT_RENAME); - } - @Override public JPopupMenu onTreePopupMenu(MainWindow mainWindow) { return RenameDialog.buildRenamePopup(mainWindow, this); @@ -139,6 +137,65 @@ public class JMethod extends JNode { return mth.getName(); } + @Override + public String getTitle() { + return makeLongStringHtml(); + } + + @Override + public boolean canRename() { + if (mth.isClassInit()) { + return false; + } + return !mth.getMethodNode().contains(AFlag.DONT_RENAME); + } + + @Override + public JRenameNode replace() { + if (mth.isConstructor()) { + // rename class instead constructor + return jParent; + } + return this; + } + + @Override + public ICodeRename buildCodeRename(String newName, Set renames) { + List relatedMethods = mth.getOverrideRelatedMethods(); + if (!relatedMethods.isEmpty()) { + for (JavaMethod relatedMethod : relatedMethods) { + renames.remove(new JadxCodeRename(JadxNodeRef.forMth(relatedMethod), "")); + } + } + return new JadxCodeRename(JadxNodeRef.forMth(mth), newName); + } + + @Override + public boolean isValidName(String newName) { + return NameMapper.isValidIdentifier(newName); + } + + @Override + public void removeAlias() { + mth.removeAlias(); + } + + @Override + public void addUpdateNodes(List toUpdate) { + toUpdate.add(mth); + toUpdate.addAll(mth.getUseIn()); + List overrideRelatedMethods = mth.getOverrideRelatedMethods(); + toUpdate.addAll(overrideRelatedMethods); + for (JavaMethod ovrdMth : overrideRelatedMethods) { + toUpdate.addAll(ovrdMth.getUseIn()); + } + } + + @Override + public void reload(MainWindow mainWindow) { + mainWindow.reloadTree(); + } + @Override public String makeString() { return UiUtils.typeFormat(makeBaseString(), getReturnType()); diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JNode.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JNode.java index 421377b21..46144bc65 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JNode.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JNode.java @@ -63,10 +63,6 @@ public abstract class JNode extends DefaultMutableTreeNode implements Comparable return javaNode.getName(); } - public boolean canRename() { - return false; - } - public @Nullable JPopupMenu onTreePopupMenu(MainWindow mainWindow) { return null; } diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JPackage.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JPackage.java index fb0f70b1f..008d6798d 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JPackage.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JPackage.java @@ -1,63 +1,50 @@ package jadx.gui.treemodel; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; import javax.swing.Icon; import javax.swing.JPopupMenu; +import jadx.api.JavaNode; import jadx.api.JavaPackage; -import jadx.core.utils.Utils; -import jadx.gui.JadxWrapper; import jadx.gui.ui.MainWindow; import jadx.gui.ui.popupmenu.JPackagePopupMenu; import jadx.gui.utils.Icons; +import static jadx.gui.utils.UiUtils.escapeHtml; +import static jadx.gui.utils.UiUtils.fadeHtml; +import static jadx.gui.utils.UiUtils.wrapHtml; + public class JPackage extends JNode { private static final long serialVersionUID = -4120718634156839804L; - private String fullName; + public static final String PACKAGE_DEFAULT_HTML_STR = wrapHtml(fadeHtml(escapeHtml(""))); + + private final JavaPackage pkg; + private final boolean enabled; + private final List classes; + private final List subPackages; + + /** + * Package created by full package alias, don't have a raw package reference. + * `pkg` field point to the closest raw package leaf. + */ + private final boolean synthetic; + private String name; - private boolean enabled; - private List classes; - private List innerPackages; - public JPackage(JavaPackage pkg, JadxWrapper wrapper) { - this(pkg.getName(), pkg.getName(), - isPkgEnabled(wrapper, pkg.getName()), - Utils.collectionMap(pkg.getClasses(), JClass::new), - new ArrayList<>()); - update(); - } - - public JPackage(String fullName, JadxWrapper wrapper) { - this(fullName, fullName, isPkgEnabled(wrapper, fullName), new ArrayList<>(), new ArrayList<>()); - } - - public JPackage(String fullName, String name) { - this(fullName, name, true, Collections.emptyList(), Collections.emptyList()); - } - - private JPackage(String fullName, String name, boolean enabled, List classes, List innerPackages) { - this.fullName = fullName; - this.name = name; + public JPackage(JavaPackage pkg, boolean enabled, List classes, List subPackages, boolean synthetic) { + this.pkg = pkg; this.enabled = enabled; this.classes = classes; - this.innerPackages = innerPackages; + this.subPackages = subPackages; + this.synthetic = synthetic; } - private static boolean isPkgEnabled(JadxWrapper wrapper, String fullPkgName) { - List excludedPackages = wrapper.getExcludedPackages(); - return excludedPackages.isEmpty() - || excludedPackages.stream().filter(p -> !p.isEmpty()) - .noneMatch(p -> fullPkgName.equals(p) || fullPkgName.startsWith(p + '.')); - } - - public final void update() { + public void update() { removeAllChildren(); if (isEnabled()) { - for (JPackage pkg : innerPackages) { + for (JPackage pkg : subPackages) { pkg.update(); add(pkg); } @@ -73,44 +60,37 @@ public class JPackage extends JNode { return new JPackagePopupMenu(mainWindow, this); } + public JavaPackage getPkg() { + return pkg; + } + + public JavaNode getJavaNode() { + return pkg; + } + @Override public String getName() { return name; } - @Override - public boolean canRename() { - return true; - } - - public String getFullName() { - return fullName; - } - - public void updateBothNames(String fullName, String name, JadxWrapper wrapper) { - this.fullName = fullName; - this.name = name; - this.enabled = isPkgEnabled(wrapper, fullName); - } - - public void updateName(String name) { + public void setName(String name) { this.name = name; } - public List getInnerPackages() { - return innerPackages; - } - - public void setInnerPackages(List innerPackages) { - this.innerPackages = innerPackages; + public List getSubPackages() { + return subPackages; } public List getClasses() { return classes; } - public void setClasses(List classes) { - this.classes = classes; + public boolean isEnabled() { + return enabled; + } + + public boolean isSynthetic() { + return synthetic; } @Override @@ -131,12 +111,12 @@ public class JPackage extends JNode { if (o == null || getClass() != o.getClass()) { return false; } - return name.equals(((JPackage) o).name); + return pkg.equals(((JPackage) o).pkg); } @Override public int hashCode() { - return name.hashCode(); + return pkg.hashCode(); } @Override @@ -145,11 +125,20 @@ public class JPackage extends JNode { } @Override - public String makeLongString() { + public String makeStringHtml() { + if (name.isEmpty()) { + return PACKAGE_DEFAULT_HTML_STR; + } return name; } - public boolean isEnabled() { - return enabled; + @Override + public String makeLongString() { + return pkg.getFullName(); + } + + @Override + public String toString() { + return name; } } diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JRenameNode.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JRenameNode.java new file mode 100644 index 000000000..9e3626a3e --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JRenameNode.java @@ -0,0 +1,35 @@ +package jadx.gui.treemodel; + +import java.util.List; +import java.util.Set; + +import javax.swing.Icon; + +import jadx.api.JavaNode; +import jadx.api.data.ICodeRename; +import jadx.gui.ui.MainWindow; + +public interface JRenameNode { + + String getTitle(); + + String getName(); + + Icon getIcon(); + + boolean canRename(); + + default JRenameNode replace() { + return this; + } + + ICodeRename buildCodeRename(String newName, Set renames); + + boolean isValidName(String newName); + + void removeAlias(); + + void addUpdateNodes(List toUpdate); + + void reload(MainWindow mainWindow); +} diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JSources.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JSources.java index 0106e9926..1129dd3e8 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JSources.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JSources.java @@ -1,20 +1,14 @@ package jadx.gui.treemodel; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.IdentityHashMap; import java.util.List; -import java.util.Map; -import java.util.Set; import javax.swing.Icon; import javax.swing.ImageIcon; -import jadx.api.JavaPackage; import jadx.gui.JadxWrapper; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; +import jadx.gui.utils.pkgs.PackageHelper; public class JSources extends JNode { private static final long serialVersionUID = 8962924556824862801L; @@ -32,89 +26,15 @@ public class JSources extends JNode { public final void update() { removeAllChildren(); - if (flatPackages) { - for (JavaPackage pkg : wrapper.getPackages()) { - add(new JPackage(pkg, wrapper)); - } - } else { - // build packages hierarchy - List rootPkgs = getHierarchyPackages(wrapper.getPackages()); - for (JPackage jPackage : rootPkgs) { - jPackage.update(); - add(jPackage); - } + PackageHelper packageHelper = wrapper.getCache().getPackageHelper(); + if (packageHelper == null) { + packageHelper = new PackageHelper(wrapper); + wrapper.getCache().setPackageHelper(packageHelper); } - } - - /** - * Convert packages list to hierarchical packages representation - * - * @param packages input packages list - * @return root packages - */ - List getHierarchyPackages(List packages) { - Map pkgMap = new HashMap<>(); - for (JavaPackage pkg : packages) { - addPackage(pkgMap, new JPackage(pkg, wrapper)); - } - // merge packages without classes - boolean repeat; - do { - repeat = false; - for (JPackage pkg : pkgMap.values()) { - List innerPackages = pkg.getInnerPackages(); - if (innerPackages.size() == 1 && pkg.getClasses().isEmpty()) { - JPackage innerPkg = innerPackages.get(0); - pkg.setInnerPackages(innerPkg.getInnerPackages()); - pkg.setClasses(innerPkg.getClasses()); - String innerName = '.' + innerPkg.getName(); - pkg.updateBothNames(pkg.getFullName() + innerName, pkg.getName() + innerName, wrapper); - - innerPkg.setInnerPackages(Collections.emptyList()); - innerPkg.setClasses(Collections.emptyList()); - repeat = true; - break; - } - } - } while (repeat); - - // remove empty packages - pkgMap.values().removeIf(pkg -> pkg.getInnerPackages().isEmpty() && pkg.getClasses().isEmpty()); - - // use identity set for collect inner packages - Set innerPackages = Collections.newSetFromMap(new IdentityHashMap<>()); - for (JPackage pkg : pkgMap.values()) { - innerPackages.addAll(pkg.getInnerPackages()); - } - // find root packages - List rootPkgs = new ArrayList<>(); - for (JPackage pkg : pkgMap.values()) { - if (!innerPackages.contains(pkg)) { - rootPkgs.add(pkg); - } - } - Collections.sort(rootPkgs); - return rootPkgs; - } - - private void addPackage(Map pkgs, JPackage pkg) { - String pkgName = pkg.getFullName(); - JPackage replaced = pkgs.put(pkgName, pkg); - if (replaced != null) { - pkg.getInnerPackages().addAll(replaced.getInnerPackages()); - pkg.getClasses().addAll(replaced.getClasses()); - } - int dot = pkgName.lastIndexOf('.'); - if (dot > 0) { - String prevPart = pkgName.substring(0, dot); - String shortName = pkgName.substring(dot + 1); - pkg.updateName(shortName); - JPackage prevPkg = pkgs.get(prevPart); - if (prevPkg == null) { - prevPkg = new JPackage(prevPart, wrapper); - addPackage(pkgs, prevPkg); - } - prevPkg.getInnerPackages().add(pkg); + List roots = packageHelper.getRoots(flatPackages); + for (JPackage rootPkg : roots) { + rootPkg.update(); + add(rootPkg); } } diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JVariable.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JVariable.java index da2c79193..dd8a1fd89 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JVariable.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JVariable.java @@ -1,12 +1,21 @@ package jadx.gui.treemodel; +import java.util.List; +import java.util.Set; + import javax.swing.Icon; import jadx.api.JavaNode; import jadx.api.JavaVariable; +import jadx.api.data.ICodeRename; +import jadx.api.data.impl.JadxCodeRef; +import jadx.api.data.impl.JadxCodeRename; +import jadx.api.data.impl.JadxNodeRef; +import jadx.core.deobf.NameMapper; +import jadx.gui.ui.MainWindow; import jadx.gui.utils.UiUtils; -public class JVariable extends JNode { +public class JVariable extends JNode implements JRenameNode { private static final long serialVersionUID = -3002100457834453783L; private final JMethod jMth; @@ -77,4 +86,33 @@ public class JVariable extends JNode { public boolean canRename() { return var.getName() != null; } + + @Override + public String getTitle() { + return makeLongStringHtml(); + } + + @Override + public boolean isValidName(String newName) { + return NameMapper.isValidIdentifier(newName); + } + + @Override + public ICodeRename buildCodeRename(String newName, Set renames) { + return new JadxCodeRename(JadxNodeRef.forMth(var.getMth()), JadxCodeRef.forVar(var), newName); + } + + @Override + public void removeAlias() { + var.removeAlias(); + } + + @Override + public void addUpdateNodes(List toUpdate) { + toUpdate.add(var.getMth()); + } + + @Override + public void reload(MainWindow mainWindow) { + } } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/RenameAction.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/RenameAction.java index ae3c573e3..1026e7e1b 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/RenameAction.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/RenameAction.java @@ -1,6 +1,7 @@ package jadx.gui.ui.codearea; import jadx.gui.treemodel.JNode; +import jadx.gui.treemodel.JRenameNode; import jadx.gui.ui.dialog.RenameDialog; import jadx.gui.utils.NLS; @@ -17,11 +18,17 @@ public final class RenameAction extends JNodeAction { @Override public boolean isActionEnabled(JNode node) { - return node != null && node.canRename(); + if (node == null) { + return false; + } + if (node instanceof JRenameNode) { + return ((JRenameNode) node).canRename(); + } + return false; } @Override public void runAction(JNode node) { - RenameDialog.rename(getCodeArea().getMainWindow(), getCodeArea().getNode(), node); + RenameDialog.rename(getCodeArea().getMainWindow(), getCodeArea().getNode(), (JRenameNode) node); } } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/dialog/RenameDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/dialog/RenameDialog.java index d02fa3ed0..81e8370bc 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/dialog/RenameDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/dialog/RenameDialog.java @@ -27,29 +27,19 @@ import javax.swing.JTextField; import javax.swing.WindowConstants; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import jadx.api.JavaClass; -import jadx.api.JavaMethod; import jadx.api.JavaNode; -import jadx.api.JavaVariable; import jadx.api.data.ICodeRename; import jadx.api.data.impl.JadxCodeData; -import jadx.api.data.impl.JadxCodeRef; -import jadx.api.data.impl.JadxCodeRename; -import jadx.api.data.impl.JadxNodeRef; -import jadx.core.deobf.NameMapper; import jadx.core.utils.Utils; -import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.gui.jobs.TaskStatus; import jadx.gui.settings.JadxProject; 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.treemodel.JRenameNode; import jadx.gui.ui.MainWindow; import jadx.gui.ui.TabbedPane; import jadx.gui.ui.codearea.ClassCodeContentPanel; @@ -70,36 +60,32 @@ public class RenameDialog extends JDialog { private final transient MainWindow mainWindow; private final transient CacheObject cache; - private final transient JNode source; - private final transient JNode node; + private final transient @Nullable JNode source; + private final transient JRenameNode node; private transient JTextField renameField; private transient JButton renameBtn; - public static boolean rename(MainWindow mainWindow, JNode node) { - return rename(mainWindow, node, node); - } - - public static boolean rename(MainWindow mainWindow, JNode source, JNode node) { + public static boolean rename(MainWindow mainWindow, JNode source, JRenameNode node) { RenameDialog renameDialog = new RenameDialog(mainWindow, source, node); UiUtils.uiRun(() -> renameDialog.setVisible(true)); UiUtils.uiRun(renameDialog::initRenameField); // wait for UI events to propagate return true; } - public static JPopupMenu buildRenamePopup(MainWindow mainWindow, JNode node) { + public static JPopupMenu buildRenamePopup(MainWindow mainWindow, JRenameNode node) { JMenuItem jmi = new JMenuItem(NLS.str("popup.rename")); - jmi.addActionListener(action -> RenameDialog.rename(mainWindow, node)); + jmi.addActionListener(action -> RenameDialog.rename(mainWindow, null, node)); JPopupMenu menu = new JPopupMenu(); menu.add(jmi); return menu; } - private RenameDialog(MainWindow mainWindow, JNode source, JNode node) { + private RenameDialog(MainWindow mainWindow, JNode source, JRenameNode node) { super(mainWindow); this.mainWindow = mainWindow; this.cache = mainWindow.getCacheObject(); this.source = source; - this.node = replaceNode(node); + this.node = node.replace(); initUI(); } @@ -108,27 +94,12 @@ public class RenameDialog extends JDialog { renameField.selectAll(); } - private JNode replaceNode(JNode node) { - if (node instanceof JMethod) { - JavaMethod javaMethod = ((JMethod) node).getJavaMethod(); - if (javaMethod.isClassInit()) { - throw new JadxRuntimeException("Can't rename class init method: " + node); - } - if (javaMethod.isConstructor()) { - // rename class instead constructor - return node.getJParent(); - } - } - return node; - } - - private boolean checkNewName() { - String newName = renameField.getText(); + private boolean checkNewName(String newName) { if (newName.isEmpty()) { // use empty name to reset rename (revert to original) return true; } - boolean valid = NameMapper.isValidIdentifier(newName); + boolean valid = node.isValidName(newName); if (renameBtn.isEnabled() != valid) { renameBtn.setEnabled(valid); renameField.putClientProperty("JComponent.outline", valid ? "" : "error"); @@ -137,11 +108,12 @@ public class RenameDialog extends JDialog { } private void rename() { - if (!checkNewName()) { + String newName = renameField.getText().trim(); + if (!checkNewName(newName)) { return; } try { - updateCodeRenames(set -> processRename(node, renameField.getText(), set)); + updateCodeRenames(set -> processRename(newName, set)); refreshState(); } catch (Exception e) { LOG.error("Rename failed", e); @@ -150,46 +122,15 @@ public class RenameDialog extends JDialog { dispose(); } - private void processRename(JNode node, String newName, Set renames) { - JadxCodeRename rename = buildRename(node, newName, renames); + private void processRename(String newName, Set renames) { + ICodeRename rename = node.buildCodeRename(newName, renames); renames.remove(rename); - JavaNode javaNode = node.getJavaNode(); - if (javaNode != null) { - javaNode.removeAlias(); - } + node.removeAlias(); if (!newName.isEmpty()) { renames.add(rename); } } - @NotNull - private JadxCodeRename buildRename(JNode node, String newName, Set renames) { - if (node instanceof JMethod) { - JavaMethod javaMethod = ((JMethod) node).getJavaMethod(); - List relatedMethods = javaMethod.getOverrideRelatedMethods(); - if (!relatedMethods.isEmpty()) { - for (JavaMethod relatedMethod : relatedMethods) { - renames.remove(new JadxCodeRename(JadxNodeRef.forMth(relatedMethod), "")); - } - } - return new JadxCodeRename(JadxNodeRef.forMth(javaMethod), newName); - } - if (node instanceof JField) { - return new JadxCodeRename(JadxNodeRef.forFld(((JField) node).getJavaField()), newName); - } - if (node instanceof JClass) { - return new JadxCodeRename(JadxNodeRef.forCls(((JClass) node).getCls()), newName); - } - if (node instanceof JPackage) { - return new JadxCodeRename(JadxNodeRef.forPkg(((JPackage) node).getFullName()), newName); - } - if (node instanceof JVariable) { - JavaVariable javaVar = ((JVariable) node).getJavaVarNode(); - return new JadxCodeRename(JadxNodeRef.forMth(javaVar.getMth()), JadxCodeRef.forVar(javaVar), newName); - } - throw new JadxRuntimeException("Failed to build rename node for: " + node); - } - private void updateCodeRenames(Consumer> updater) { JadxProject project = mainWindow.getProject(); JadxCodeData codeData = project.getCodeData(); @@ -208,24 +149,13 @@ public class RenameDialog extends JDialog { private void refreshState() { mainWindow.getWrapper().reInitRenameVisitor(); - JNodeCache nodeCache = cache.getNodeCache(); - JavaNode javaNode = node.getJavaNode(); - List toUpdate = new ArrayList<>(); if (source != null && source != node) { toUpdate.add(source.getJavaNode()); } - if (javaNode != null) { - toUpdate.add(javaNode); - toUpdate.addAll(javaNode.getUseIn()); - if (node instanceof JMethod) { - toUpdate.addAll(((JMethod) node).getJavaMethod().getOverrideRelatedMethods()); - } - } else if (node instanceof JPackage) { - processPackage(toUpdate); - } else { - throw new JadxRuntimeException("Unexpected node type: " + node); - } + node.addUpdateNodes(toUpdate); + + JNodeCache nodeCache = cache.getNodeCache(); Set updatedTopClasses = toUpdate .stream() .map(JavaNode::getTopParentClass) @@ -245,28 +175,11 @@ public class RenameDialog extends JDialog { mainWindow.showHeapUsageBar(); UiUtils.errorMessage(this, NLS.str("message.memoryLow")); } - if (node instanceof JPackage) { - mainWindow.getTreeRoot().update(); - } - mainWindow.reloadTree(); + node.reload(mainWindow); }); } } - private void processPackage(List toUpdate) { - String rawFullPkg = ((JPackage) node).getFullName(); - String rawFullPkgDot = rawFullPkg + "."; - for (JavaClass cls : mainWindow.getWrapper().getClasses()) { - String clsPkg = cls.getClassNode().getClassInfo().getPackage(); - // search all classes in package - if (clsPkg.equals(rawFullPkg) || clsPkg.startsWith(rawFullPkgDot)) { - toUpdate.add(cls); - // also include all usages (for import fix) - toUpdate.addAll(cls.getUseIn()); - } - } - } - private void refreshClasses(Set updatedTopClasses) { if (updatedTopClasses.size() < 10) { // small batch => reload @@ -323,11 +236,15 @@ public class RenameDialog extends JDialog { private void initUI() { JLabel lbl = new JLabel(NLS.str("popup.rename")); - JLabel nodeLabel = NodeLabel.longName(node); + NodeLabel nodeLabel = new NodeLabel(node.getTitle()); + nodeLabel.setIcon(node.getIcon()); + if (node instanceof JNode) { + nodeLabel.disableHtml(((JNode) node).disableHtml()); + } lbl.setLabelFor(nodeLabel); renameField = new JTextField(40); - renameField.getDocument().addDocumentListener(new DocumentUpdateListener(ev -> checkNewName())); + renameField.getDocument().addDocumentListener(new DocumentUpdateListener(ev -> checkNewName(renameField.getText()))); renameField.addActionListener(e -> rename()); new TextStandardActions(renameField); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/popupmenu/JPackagePopupMenu.java b/jadx-gui/src/main/java/jadx/gui/ui/popupmenu/JPackagePopupMenu.java index 6b624626f..4a5744e95 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/popupmenu/JPackagePopupMenu.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/popupmenu/JPackagePopupMenu.java @@ -1,7 +1,6 @@ package jadx.gui.ui.popupmenu; import java.awt.event.ActionEvent; -import java.util.Arrays; import java.util.List; import javax.swing.AbstractAction; @@ -10,17 +9,17 @@ import javax.swing.JMenu; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; -import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.gui.JadxWrapper; -import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JPackage; import jadx.gui.ui.MainWindow; import jadx.gui.ui.dialog.ExcludePkgDialog; import jadx.gui.ui.dialog.RenameDialog; import jadx.gui.utils.NLS; +import jadx.gui.utils.pkgs.JRenamePackage; +import jadx.gui.utils.pkgs.PackageHelper; public class JPackagePopupMenu extends JPopupMenu { private static final long serialVersionUID = -7781009781149224131L; @@ -34,79 +33,24 @@ public class JPackagePopupMenu extends JPopupMenu { add(makeExcludeItem(pkg)); add(makeExcludeItem()); - JMenuItem menuItem = makeRenameMenuItem(pkg); - if (menuItem != null) { - add(menuItem); - } + add(makeRenameMenuItem(pkg)); } - @Nullable private JMenuItem makeRenameMenuItem(JPackage pkg) { - List aliasShortParts = splitPackage(pkg.getName()); - int count = aliasShortParts.size(); - if (count == 0) { - return null; - } - String rawPackage = getRawPackage(pkg); - if (rawPackage == null) { - return null; - } - List aliasParts = splitPackage(pkg.getFullName()); - List rawParts = splitPackage(rawPackage); // can be longer then alias parts - int start = aliasParts.size() - count; - if (count == 1) { - // single case => no submenu - JPackage renamePkg = new JPackage(concat(rawParts, start), aliasParts.get(start)); - JMenuItem pkgItem = new JMenuItem(NLS.str("popup.rename")); - pkgItem.addActionListener(e -> rename(renamePkg)); - return pkgItem; - } JMenuItem renameSubMenu = new JMenu(NLS.str("popup.rename")); - for (int i = start; i < aliasParts.size(); i++) { - String aliasShortPkg = aliasParts.get(i); - JPackage pkgPart = new JPackage(concat(rawParts, i), aliasShortPkg); - JMenuItem pkgPartItem = new JMenuItem(aliasShortPkg); - pkgPartItem.addActionListener(e -> rename(pkgPart)); + PackageHelper packageHelper = mainWindow.getCacheObject().getPackageHelper(); + List nodes = packageHelper.getRenameNodes(pkg); + for (JRenamePackage node : nodes) { + JMenuItem pkgPartItem = new JMenuItem(node.getTitle(), node.getIcon()); + pkgPartItem.addActionListener(e -> rename(node)); renameSubMenu.add(pkgPartItem); } return renameSubMenu; } - private String concat(List parts, int n) { - if (n == 0) { - return parts.get(0); - } - StringBuilder sb = new StringBuilder(); - sb.append(parts.get(0)); - int count = parts.size(); - for (int i = 1; i < count && i <= n; i++) { - sb.append('.'); - sb.append(parts.get(i)); - } - return sb.toString(); - } - - private void rename(JPackage pkg) { - LOG.debug("Renaming package: fullName={}, name={}", pkg.getFullName(), pkg.getName()); - RenameDialog.rename(mainWindow, pkg); - } - - private List splitPackage(String rawPackage) { - return Arrays.asList(rawPackage.split("\\.")); - } - - private String getRawPackage(JPackage pkg) { - List classes = pkg.getClasses(); - if (!classes.isEmpty()) { - return classes.get(0).getRootClass().getCls().getClassNode().getClassInfo().getPackage(); - } - for (JPackage innerPkg : pkg.getInnerPackages()) { - String rawPackage = getRawPackage(innerPkg); - if (rawPackage != null) { - return rawPackage; - } - } - return null; + private void rename(JRenamePackage pkg) { + LOG.debug("Renaming package: {}", pkg); + RenameDialog.rename(mainWindow, null, pkg); } private JMenuItem makeExcludeItem(JPackage pkg) { @@ -114,7 +58,7 @@ public class JPackagePopupMenu extends JPopupMenu { excludeItem.setSelected(!pkg.isEnabled()); excludeItem.addItemListener(e -> { JadxWrapper wrapper = mainWindow.getWrapper(); - String fullName = pkg.getFullName(); + String fullName = pkg.getPkg().getFullName(); if (excludeItem.isSelected()) { wrapper.addExcludedPackage(fullName); } else { diff --git a/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java b/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java index 7eb588517..8875710b8 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java @@ -9,6 +9,7 @@ import org.jetbrains.annotations.Nullable; import jadx.api.JavaClass; import jadx.gui.ui.dialog.SearchDialog; +import jadx.gui.utils.pkgs.PackageHelper; public class CacheObject { @@ -17,6 +18,7 @@ public class CacheObject { private Map> lastSearchOptions; private List> decompileBatches; + private PackageHelper packageHelper; private volatile boolean fullDecompilationFinished; @@ -29,6 +31,7 @@ public class CacheObject { jNodeCache = new JNodeCache(); lastSearchOptions = new HashMap<>(); decompileBatches = null; + packageHelper = null; fullDecompilationFinished = false; } @@ -57,6 +60,14 @@ public class CacheObject { this.decompileBatches = decompileBatches; } + public PackageHelper getPackageHelper() { + return packageHelper; + } + + public void setPackageHelper(PackageHelper packageHelper) { + this.packageHelper = packageHelper; + } + public boolean isFullDecompilationFinished() { return fullDecompilationFinished; } diff --git a/jadx-gui/src/main/java/jadx/gui/utils/JNodeCache.java b/jadx-gui/src/main/java/jadx/gui/utils/JNodeCache.java index f573a3253..e6b20bc90 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/JNodeCache.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/JNodeCache.java @@ -23,7 +23,7 @@ public class JNodeCache { if (javaNode == null) { return null; } - // don't use 'computeIfAbsent' method here, it this cause 'Recursive update' exception + // don't use 'computeIfAbsent' method here, it will cause 'Recursive update' exception JNode jNode = cache.get(javaNode); if (jNode == null || jNode.getJavaNode() != javaNode) { jNode = convert(javaNode); diff --git a/jadx-gui/src/main/java/jadx/gui/utils/pkgs/JRenamePackage.java b/jadx-gui/src/main/java/jadx/gui/utils/pkgs/JRenamePackage.java new file mode 100644 index 000000000..0a8d9db0b --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/utils/pkgs/JRenamePackage.java @@ -0,0 +1,103 @@ +package jadx.gui.utils.pkgs; + +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.swing.Icon; + +import org.apache.commons.lang3.StringUtils; + +import jadx.api.JavaNode; +import jadx.api.JavaPackage; +import jadx.api.data.ICodeRename; +import jadx.api.data.impl.JadxCodeRename; +import jadx.api.data.impl.JadxNodeRef; +import jadx.core.deobf.NameMapper; +import jadx.gui.treemodel.JRenameNode; +import jadx.gui.ui.MainWindow; +import jadx.gui.utils.Icons; + +import static jadx.core.deobf.NameMapper.VALID_JAVA_IDENTIFIER; + +public class JRenamePackage implements JRenameNode { + + private final JavaPackage refPkg; + private final String rawFullName; + private final String fullName; + private final String name; + + public JRenamePackage(JavaPackage refPkg, String rawFullName, String fullName, String name) { + this.refPkg = refPkg; + this.rawFullName = rawFullName; + this.fullName = fullName; + this.name = name; + } + + @Override + public String getTitle() { + return fullName; + } + + @Override + public String getName() { + return name; + } + + @Override + public Icon getIcon() { + return Icons.PACKAGE; + } + + @Override + public boolean canRename() { + return true; + } + + @Override + public ICodeRename buildCodeRename(String newName, Set renames) { + return new JadxCodeRename(JadxNodeRef.forPkg(rawFullName), newName); + } + + @Override + public boolean isValidName(String newName) { + return isValidPackageName(newName); + } + + private static final Pattern PACKAGE_RENAME_PATTERN = Pattern.compile( + "PKG(\\.PKG)*(\\.)?".replace("PKG", VALID_JAVA_IDENTIFIER.pattern())); + + static boolean isValidPackageName(String newName) { + if (newName == null || newName.isEmpty() || NameMapper.isReserved(newName)) { + return false; + } + Matcher matcher = PACKAGE_RENAME_PATTERN.matcher(newName); + if (!matcher.matches()) { + return false; + } + for (String part : StringUtils.split(newName, '.')) { + if (NameMapper.isReserved(part)) { + return false; + } + } + return true; + } + + @Override + public void removeAlias() { + refPkg.removeAlias(); + } + + @Override + public void addUpdateNodes(List toUpdate) { + refPkg.addUseIn(toUpdate); + } + + @Override + public void reload(MainWindow mainWindow) { + mainWindow.getCacheObject().setPackageHelper(null); + mainWindow.getTreeRoot().update(); + mainWindow.reloadTree(); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/pkgs/PackageHelper.java b/jadx-gui/src/main/java/jadx/gui/utils/pkgs/PackageHelper.java new file mode 100644 index 000000000..6d7320060 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/utils/pkgs/PackageHelper.java @@ -0,0 +1,187 @@ +package jadx.gui.utils.pkgs; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.api.JavaPackage; +import jadx.core.dex.info.PackageInfo; +import jadx.core.utils.ListUtils; +import jadx.core.utils.Utils; +import jadx.gui.JadxWrapper; +import jadx.gui.treemodel.JClass; +import jadx.gui.treemodel.JPackage; +import jadx.gui.utils.JNodeCache; + +public class PackageHelper { + private static final Logger LOG = LoggerFactory.getLogger(PackageHelper.class); + + private static final Comparator CLASS_COMPARATOR = Comparator.comparing(JClass::getName, String.CASE_INSENSITIVE_ORDER); + private static final Comparator PKG_COMPARATOR = Comparator.comparing(JPackage::getName, String.CASE_INSENSITIVE_ORDER); + + private final JadxWrapper wrapper; + private List excludedPackages; + private JNodeCache nodeCache; + + private final Map pkgInfoMap = new HashMap<>(); + + public PackageHelper(JadxWrapper wrapper) { + this.wrapper = wrapper; + } + + public List getRoots(boolean flatPackages) { + excludedPackages = wrapper.getExcludedPackages(); + nodeCache = wrapper.getCache().getNodeCache(); + pkgInfoMap.clear(); + if (flatPackages) { + return prepareFlatPackages(); + } + long start = System.currentTimeMillis(); + List roots = prepareHierarchyPackages(); + if (LOG.isDebugEnabled()) { + LOG.debug("Prepare hierarchy packages in {} ms", System.currentTimeMillis() - start); + } + return roots; + } + + public List getRenameNodes(JPackage pkg) { + List list = new ArrayList<>(); + PackageInfo pkgInfo = pkg.getPkg().getPkgNode().getAliasPkgInfo(); + Set added = new HashSet<>(); + do { + JPackage jPkg = pkgInfoMap.get(pkgInfo); + if (jPkg != null) { + JavaPackage javaPkg = jPkg.getPkg(); + String fullName = javaPkg.isDefault() ? JPackage.PACKAGE_DEFAULT_HTML_STR : javaPkg.getFullName(); + String name = jPkg.isSynthetic() || javaPkg.isParentRenamed() ? fullName : javaPkg.getName(); + JRenamePackage renamePkg = new JRenamePackage(javaPkg, javaPkg.getRawFullName(), fullName, name); + if (added.add(fullName)) { + list.add(renamePkg); + } + } + pkgInfo = pkgInfo.getParentPkg(); + } while (pkgInfo != null); + return list; + } + + private List prepareFlatPackages() { + List list = new ArrayList<>(); + for (JavaPackage javaPkg : wrapper.getPackages()) { + if (javaPkg.isLeaf()) { + JPackage pkg = buildJPackage(javaPkg, false); + pkg.setName(javaPkg.getFullName()); + list.add(pkg); + pkgInfoMap.put(javaPkg.getPkgNode().getAliasPkgInfo(), pkg); + } + } + list.sort(PKG_COMPARATOR); + return list; + } + + private List prepareHierarchyPackages() { + JPackage root = new JPackage(null, true, Collections.emptyList(), new ArrayList<>(), true); + List packages = wrapper.getPackages(); + List jPackages = new ArrayList<>(packages.size()); + // create nodes for exists packages + for (JavaPackage javaPkg : packages) { + JPackage jPkg = buildJPackage(javaPkg, false); + jPackages.add(jPkg); + PackageInfo aliasPkgInfo = javaPkg.getPkgNode().getAliasPkgInfo(); + jPkg.setName(aliasPkgInfo.getName()); + pkgInfoMap.put(aliasPkgInfo, jPkg); + if (aliasPkgInfo.isRoot()) { + root.getSubPackages().add(jPkg); + } + } + // link subpackages, create missing packages created by renames + for (JPackage jPkg : jPackages) { + if (jPkg.getPkg().isLeaf()) { + buildLeafPath(jPkg, root, pkgInfoMap); + } + } + List toMerge = new ArrayList<>(); + traverseMiddlePackages(root, toMerge); + Utils.treeDfsVisit(root, JPackage::getSubPackages, v -> v.getSubPackages().sort(PKG_COMPARATOR)); + return root.getSubPackages(); + } + + private void buildLeafPath(JPackage jPkg, JPackage root, Map pkgMap) { + JPackage currentJPkg = jPkg; + PackageInfo current = jPkg.getPkg().getPkgNode().getAliasPkgInfo(); + while (true) { + current = current.getParentPkg(); + if (current == null) { + break; + } + JPackage parentJPkg = pkgMap.get(current); + if (parentJPkg == null) { + parentJPkg = buildJPackage(currentJPkg.getPkg(), true); + parentJPkg.setName(current.getName()); + pkgMap.put(current, parentJPkg); + if (current.isRoot()) { + root.getSubPackages().add(parentJPkg); + } + } + List subPackages = parentJPkg.getSubPackages(); + String pkgName = currentJPkg.getName(); + if (ListUtils.noneMatch(subPackages, p -> p.getName().equals(pkgName))) { + subPackages.add(currentJPkg); + } + currentJPkg = parentJPkg; + } + } + + private static void traverseMiddlePackages(JPackage pkg, List toMerge) { + List subPackages = pkg.getSubPackages(); + int count = subPackages.size(); + for (int i = 0; i < count; i++) { + JPackage subPackage = subPackages.get(i); + JPackage replacePkg = mergeMiddlePackages(subPackage, toMerge); + if (replacePkg != subPackage) { + subPackages.set(i, replacePkg); + } + traverseMiddlePackages(replacePkg, toMerge); + } + } + + private static JPackage mergeMiddlePackages(JPackage jPkg, List merged) { + List subPackages = jPkg.getSubPackages(); + if (subPackages.size() == 1 && jPkg.getClasses().isEmpty()) { + merged.add(jPkg); + JPackage endPkg = mergeMiddlePackages(subPackages.get(0), merged); + merged.clear(); + return endPkg; + } + if (!merged.isEmpty()) { + merged.add(jPkg); + jPkg.setName(Utils.listToString(merged, ".", JPackage::getName)); + } + return jPkg; + } + + private JPackage buildJPackage(JavaPackage javaPkg, boolean synthetic) { + boolean pkgEnabled = isPkgEnabled(javaPkg.getRawFullName(), excludedPackages); + List classes; + if (synthetic) { + classes = Collections.emptyList(); + } else { + classes = Utils.collectionMap(javaPkg.getClasses(), nodeCache::makeFrom); + classes.sort(CLASS_COMPARATOR); + } + return new JPackage(javaPkg, pkgEnabled, classes, new ArrayList<>(), synthetic); + } + + private static boolean isPkgEnabled(String fullPkgName, List excludedPackages) { + return excludedPackages.isEmpty() + || excludedPackages.stream() + .noneMatch(p -> fullPkgName.equals(p) || fullPkgName.startsWith(p + '.')); + } +} diff --git a/jadx-gui/src/test/java/jadx/api/Factory.java b/jadx-gui/src/test/java/jadx/api/Factory.java deleted file mode 100644 index 1fc243399..000000000 --- a/jadx-gui/src/test/java/jadx/api/Factory.java +++ /dev/null @@ -1,16 +0,0 @@ -package jadx.api; - -import java.util.List; - -import jadx.core.dex.nodes.ClassNode; - -public class Factory { - - public static JavaPackage newPackage(String name, List classes) { - return new JavaPackage(name, classes); - } - - public static JavaClass newClass(JadxDecompiler decompiler, ClassNode classNode) { - return new JavaClass(classNode, decompiler); - } -} diff --git a/jadx-gui/src/test/java/jadx/gui/treemodel/JSourcesTest.java b/jadx-gui/src/test/java/jadx/gui/treemodel/JSourcesTest.java deleted file mode 100644 index d2f789c3f..000000000 --- a/jadx-gui/src/test/java/jadx/gui/treemodel/JSourcesTest.java +++ /dev/null @@ -1,105 +0,0 @@ -package jadx.gui.treemodel; - -import java.util.Collections; -import java.util.List; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import jadx.api.Factory; -import jadx.api.JadxArgs; -import jadx.api.JadxDecompiler; -import jadx.api.JavaClass; -import jadx.api.JavaPackage; -import jadx.core.dex.nodes.ClassNode; -import jadx.gui.JadxWrapper; - -import static java.util.Arrays.asList; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.hasSize; -import static org.hamcrest.Matchers.is; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class JSourcesTest { - - private JSources sources; - private JadxDecompiler decompiler; - - @BeforeEach - public void init() { - JRoot root = mock(JRoot.class); - when(root.isFlatPackages()).thenReturn(false); - JadxWrapper wrapper = mock(JadxWrapper.class); - sources = new JSources(root, wrapper); - decompiler = new JadxDecompiler(new JadxArgs()); - } - - @Test - public void testHierarchyPackages() { - String pkgName = "a.b.c.d.e"; - - List packages = Collections.singletonList(newPkg(pkgName)); - List out = sources.getHierarchyPackages(packages); - - assertThat(out, hasSize(1)); - JPackage jPkg = out.get(0); - assertThat(jPkg.getName(), is(pkgName)); - assertThat(jPkg.getClasses(), hasSize(1)); - } - - @Test - public void testHierarchyPackages2() { - List packages = asList( - newPkg("a.b"), - newPkg("a.c"), - newPkg("a.d")); - List out = sources.getHierarchyPackages(packages); - - assertThat(out, hasSize(1)); - JPackage jPkg = out.get(0); - assertThat(jPkg.getName(), is("a")); - assertThat(jPkg.getClasses(), hasSize(0)); - assertThat(jPkg.getInnerPackages(), hasSize(3)); - } - - @Test - public void testHierarchyPackages3() { - List packages = asList( - newPkg("a.b.p1"), - newPkg("a.b.p2"), - newPkg("a.b.p3")); - List out = sources.getHierarchyPackages(packages); - - assertThat(out, hasSize(1)); - JPackage jPkg = out.get(0); - assertThat(jPkg.getName(), is("a.b")); - assertThat(jPkg.getClasses(), hasSize(0)); - assertThat(jPkg.getInnerPackages(), hasSize(3)); - } - - @Test - public void testHierarchyPackages4() { - List packages = asList( - newPkg("a.p1"), - newPkg("a.b.c.p2"), - newPkg("a.b.c.p3"), - newPkg("d.e"), - newPkg("d.f.a")); - List out = sources.getHierarchyPackages(packages); - - assertThat(out, hasSize(2)); - assertThat(out.get(0).getName(), is("a")); - assertThat(out.get(0).getInnerPackages(), hasSize(2)); - assertThat(out.get(1).getName(), is("d")); - assertThat(out.get(1).getInnerPackages(), hasSize(2)); - } - - private JavaPackage newPkg(String name) { - return Factory.newPackage(name, Collections.singletonList(newClass())); - } - - private JavaClass newClass() { - return Factory.newClass(decompiler, mock(ClassNode.class)); - } -} diff --git a/jadx-gui/src/test/java/jadx/gui/utils/pkgs/TestJRenamePackage.java b/jadx-gui/src/test/java/jadx/gui/utils/pkgs/TestJRenamePackage.java new file mode 100644 index 000000000..2c7b9d604 --- /dev/null +++ b/jadx-gui/src/test/java/jadx/gui/utils/pkgs/TestJRenamePackage.java @@ -0,0 +1,34 @@ +package jadx.gui.utils.pkgs; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class TestJRenamePackage { + + @Test + void isValidName() { + valid("foo"); + valid("foo.bar"); + valid("foo.bar."); + + invalid(""); + invalid("0foo"); + invalid(".foo"); + invalid("do"); + invalid("foo.if"); + invalid("foo.if.bar"); + } + + private void valid(String name) { + assertThat(JRenamePackage.isValidPackageName(name)) + .as("expect valid: %s", name) + .isEqualTo(true); + } + + private void invalid(String name) { + assertThat(JRenamePackage.isValidPackageName(name)) + .as("expect invalid: %s", name) + .isEqualTo(false); + } +} diff --git a/jadx-plugins/jadx-script/examples/build.gradle.kts b/jadx-plugins/jadx-script/examples/build.gradle.kts index 3a650d352..c0763afe5 100644 --- a/jadx-plugins/jadx-script/examples/build.gradle.kts +++ b/jadx-plugins/jadx-script/examples/build.gradle.kts @@ -10,7 +10,7 @@ dependencies { implementation("io.github.microutils:kotlin-logging-jvm:3.0.2") - // manual imports ( IDE can't import dependencies by scripts annotations) + // manual imports (IDE can't import dependencies by scripts annotations) implementation("com.github.javafaker:javafaker:1.0.2") }