feat: allow to move class to another package

This commit is contained in:
Skylot
2022-11-15 19:00:54 +00:00
parent 9a6dec0dbd
commit d5219e7f0c
11 changed files with 191 additions and 61 deletions
@@ -329,7 +329,7 @@ public final class JavaClass implements JavaNode {
@Override
public void removeAlias() {
this.cls.getClassInfo().removeAlias();
cls.removeAlias();
}
@Override
@@ -8,6 +8,7 @@ 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.utils.StringUtils;
public class DeobfAliasProvider implements IAliasProvider {
@@ -74,46 +75,48 @@ public class DeobfAliasProvider implements IAliasProvider {
} else if (cls.getAccessFlags().isAbstract()) {
result.append("Abstract");
}
// Process current class and all super classes
ClassNode currentCls = cls;
outerLoop: while (currentCls != null) {
if (currentCls.getSuperClass() != null) {
String superClsName = currentCls.getSuperClass().getObject();
if (superClsName.startsWith("android.app.")) {
// e.g. Activity or Fragment
result.append(superClsName.substring(12));
break;
} else if (superClsName.startsWith("android.os.")) {
// e.g. AsyncTask
result.append(superClsName.substring(11));
break;
}
}
for (ArgType intf : cls.getInterfaces()) {
String intfClsName = intf.getObject();
if (intfClsName.equals("java.lang.Runnable")) {
result.append("Runnable");
break outerLoop;
} else if (intfClsName.startsWith("java.util.concurrent.")) {
// e.g. Callable
result.append(intfClsName.substring(21));
break outerLoop;
} else if (intfClsName.startsWith("android.view.")) {
// e.g. View.OnClickListener
result.append(intfClsName.substring(13));
break outerLoop;
} else if (intfClsName.startsWith("android.content.")) {
// e.g. DialogInterface.OnClickListener
result.append(intfClsName.substring(16));
break outerLoop;
}
}
if (currentCls.getSuperClass() == null) {
break;
}
currentCls = cls.root().resolveClass(currentCls.getSuperClass());
}
result.append(getBaseName(cls));
return result.toString();
}
/**
* Process current class and all super classes to get meaningful parent name
*/
private static String getBaseName(ClassNode cls) {
ClassNode currentCls = cls;
while (currentCls != null) {
ArgType superCls = currentCls.getSuperClass();
if (superCls != null) {
String superClsName = superCls.getObject();
if (superClsName.startsWith("android.app.") // e.g. Activity or Fragment
|| superClsName.startsWith("android.os.") // e.g. AsyncTask
) {
return getClsName(superClsName);
}
}
for (ArgType interfaceType : cls.getInterfaces()) {
String name = interfaceType.getObject();
if (name.equals("java.lang.Runnable")) {
return "Runnable";
}
if (name.startsWith("java.util.concurrent.") // e.g. Callable
|| name.startsWith("android.view.") // e.g. View.OnClickListener
|| name.startsWith("android.content.") // e.g. DialogInterface.OnClickListener
) {
return getClsName(name);
}
}
if (superCls == null) {
break;
}
currentCls = cls.root().resolveClass(superCls);
}
return "";
}
private static String getClsName(String name) {
int pgkEnd = name.lastIndexOf('.');
String clsName = name.substring(pgkEnd + 1);
return StringUtils.removeChar(clsName, '$');
}
}
@@ -79,6 +79,15 @@ public final class ClassInfo implements Comparable<ClassInfo> {
}
}
public void changePkgAndName(String aliasPkg, String aliasShortName) {
if (isInner()) {
throw new JadxRuntimeException("Can't change package for inner class");
}
ClassAliasInfo newAlias = new ClassAliasInfo(aliasPkg, aliasShortName);
fillAliasFullName(newAlias);
this.alias = newAlias;
}
private void fillAliasFullName(ClassAliasInfo alias) {
if (parentClass == null) {
alias.setFullName(makeFullClsName(alias.getPkg(), alias.getShortName(), null, true, false));
@@ -117,6 +126,10 @@ public final class ClassInfo implements Comparable<ClassInfo> {
return parentClass != null && parentClass.hasAlias();
}
public boolean hasAliasPkg() {
return !getPackage().equals(getAliasPkg());
}
public void removeAlias() {
this.alias = null;
}
@@ -272,14 +285,13 @@ public final class ClassInfo implements Comparable<ClassInfo> {
return true;
}
if (obj instanceof ClassInfo) {
ClassInfo other = (ClassInfo) obj;
return this.type.equals(other.type);
return type.equals(((ClassInfo) obj).type);
}
return false;
}
@Override
public int compareTo(@NotNull ClassInfo o) {
return getFullName().compareTo(o.getFullName());
public int compareTo(@NotNull ClassInfo other) {
return getRawName().compareTo(other.getRawName());
}
}
@@ -60,7 +60,7 @@ public class ClassNode extends NotificationAttrNode
private final IClassData clsData;
private final ClassInfo clsInfo;
private final PackageNode packageNode;
private PackageNode packageNode;
private AccessInfo accessFlags;
private ArgType superClass;
private List<ArgType> interfaces;
@@ -559,9 +559,50 @@ public class ClassNode extends NotificationAttrNode
parentClass = this;
}
/**
* Change class name and package (if full name provided)
* Leading dot can be used to move to default package.
* Package for inner classes can't be changed.
*/
@Override
public void rename(String newName) {
clsInfo.changeShortName(newName);
int lastDot = newName.lastIndexOf('.');
if (lastDot == -1) {
clsInfo.changeShortName(newName);
return;
}
if (isInner()) {
addWarn("Can't change package for inner class: " + this + " to " + newName);
return;
}
// change class package
String newPkg = newName.substring(0, lastDot);
String newShortName = newName.substring(lastDot + 1);
if (changeClassNodePackage(newPkg)) {
clsInfo.changePkgAndName(newPkg, newShortName);
} else {
clsInfo.changeShortName(newShortName);
}
}
private boolean changeClassNodePackage(String fullPkg) {
if (clsInfo.isInner()) {
throw new JadxRuntimeException("Can't change package for inner class");
}
if (fullPkg.equals(clsInfo.getAliasPkg())) {
return false;
}
root.removeClsFromPackage(packageNode, this);
packageNode = PackageNode.getForClass(root, fullPkg, this);
root.sortPackages();
return true;
}
public void removeAlias() {
if (!clsInfo.isInner()) {
changeClassNodePackage(clsInfo.getPackage());
}
clsInfo.removeAlias();
}
@Override
@@ -569,7 +610,7 @@ public class ClassNode extends NotificationAttrNode
if (isInner()) {
return;
}
getClassInfo().changePkg(packageNode.getAliasPkgInfo().getFullName());
clsInfo.changePkg(packageNode.getAliasPkgInfo().getFullName());
}
public PackageNode getPackageNode() {
@@ -884,7 +925,7 @@ public class ClassNode extends NotificationAttrNode
@Override
public int compareTo(@NotNull ClassNode o) {
return this.getFullName().compareTo(o.getFullName());
return this.clsInfo.compareTo(o.clsInfo);
}
@Override
@@ -10,6 +10,8 @@ import org.jetbrains.annotations.Nullable;
import jadx.api.JavaPackage;
import jadx.core.dex.info.PackageInfo;
import static jadx.core.utils.StringUtils.containsChar;
public class PackageNode implements IPackageUpdate, IDexNode, Comparable<PackageNode> {
private final RootNode root;
@@ -66,16 +68,16 @@ public class PackageNode implements IPackageUpdate, IDexNode, Comparable<Package
public void rename(String newName, boolean runUpdates) {
String alias;
boolean isFullAlias;
if (newName.indexOf('/') != -1) {
if (containsChar(newName, '/')) {
alias = newName.replace('/', '.');
isFullAlias = true;
} else if (newName.endsWith(".")) {
// treat as full pkg, remove ending dot
alias = newName.substring(0, newName.length() - 1);
} else if (newName.startsWith(".")) {
// treat as full pkg, remove start dot
alias = newName.substring(1);
isFullAlias = true;
} else {
alias = newName;
isFullAlias = alias.indexOf('.') != -1;
isFullAlias = containsChar(newName, '.');
}
if (isFullAlias) {
setFullAlias(alias, runUpdates);
@@ -151,7 +153,7 @@ public class PackageNode implements IPackageUpdate, IDexNode, Comparable<Package
aliasPkgInfo = pkgInfo;
}
public PackageNode getParentPkg() {
public @Nullable PackageNode getParentPkg() {
return parentPkg;
}
@@ -183,6 +185,10 @@ public class PackageNode implements IPackageUpdate, IDexNode, Comparable<Package
this.javaNode = javaNode;
}
public boolean isEmpty() {
return classes.isEmpty() && subPackages.isEmpty();
}
@Override
public String typeName() {
return "package";
@@ -399,6 +399,33 @@ public class RootNode {
packages.add(pkg);
}
public void removePackage(PackageNode pkg) {
if (pkgMap.remove(pkg.getPkgInfo().getFullName()) != null) {
packages.remove(pkg);
PackageNode parentPkg = pkg.getParentPkg();
if (parentPkg != null) {
parentPkg.getSubPackages().remove(pkg);
if (parentPkg.isEmpty()) {
removePackage(parentPkg);
}
}
for (PackageNode subPkg : pkg.getSubPackages()) {
removePackage(subPkg);
}
}
}
public void sortPackages() {
Collections.sort(packages);
}
public void removeClsFromPackage(PackageNode pkg, ClassNode cls) {
boolean removed = pkg.getClasses().remove(cls);
if (removed && pkg.isEmpty()) {
removePackage(pkg);
}
}
/**
* Update sub packages
*/
@@ -278,6 +278,30 @@ public class StringUtils {
return count;
}
public static boolean containsChar(String str, char ch) {
return str.indexOf(ch) != -1;
}
public static String removeChar(String str, char ch) {
int pos = str.indexOf(ch);
if (pos == -1) {
return str;
}
StringBuilder sb = new StringBuilder(str.length());
int cur = 0;
int next = pos;
while (true) {
sb.append(str, cur, next);
cur = next + 1;
next = str.indexOf(ch, cur);
if (next == -1) {
sb.append(str, cur, str.length());
break;
}
}
return sb.toString();
}
/**
* returns how many lines does it have between start to pos in content.
*/
@@ -199,7 +199,18 @@ public class JClass extends JLoadableNode implements JRenameNode {
@Override
public boolean isValidName(String newName) {
return NameMapper.isValidIdentifier(newName);
if (NameMapper.isValidIdentifier(newName)) {
return true;
}
if (cls.isInner()) {
// disallow to change package for inner classes
return false;
}
if (NameMapper.isValidFullIdentifier(newName)) {
return true;
}
// moving to default pkg
return newName.startsWith(".") && NameMapper.isValidIdentifier(newName.substring(1));
}
@Override
@@ -220,6 +231,8 @@ public class JClass extends JLoadableNode implements JRenameNode {
@Override
public void reload(MainWindow mainWindow) {
// TODO: rebuild packages only if class package has been changed
mainWindow.rebuildPackagesTree();
mainWindow.reloadTree();
}
@@ -819,6 +819,11 @@ public class MainWindow extends JFrame {
treeReloading = false;
}
public void rebuildPackagesTree() {
cacheObject.setPackageHelper(null);
treeRoot.update();
}
private void expand(TreeNode node, List<String[]> treeExpansions) {
TreeNode[] pathNodes = treeModel.getPathToRoot(node);
if (pathNodes == null) {
@@ -66,7 +66,7 @@ public class JRenamePackage implements JRenameNode {
}
private static final Pattern PACKAGE_RENAME_PATTERN = Pattern.compile(
"PKG(\\.PKG)*(\\.)?".replace("PKG", VALID_JAVA_IDENTIFIER.pattern()));
"(\\.)?PKG(\\.PKG)*".replace("PKG", VALID_JAVA_IDENTIFIER.pattern()));
static boolean isValidPackageName(String newName) {
if (newName == null || newName.isEmpty() || NameMapper.isReserved(newName)) {
@@ -96,8 +96,7 @@ public class JRenamePackage implements JRenameNode {
@Override
public void reload(MainWindow mainWindow) {
mainWindow.getCacheObject().setPackageHelper(null);
mainWindow.getTreeRoot().update();
mainWindow.rebuildPackagesTree();
mainWindow.reloadTree();
}
}
@@ -10,11 +10,11 @@ class TestJRenamePackage {
void isValidName() {
valid("foo");
valid("foo.bar");
valid("foo.bar.");
valid(".bar");
invalid("");
invalid("0foo");
invalid(".foo");
invalid("foo.");
invalid("do");
invalid("foo.if");
invalid("foo.if.bar");