fix: resolve class/package name conflicts on case-insensitive filesystems (PR #2828)
* Fix: resolve class/package name conflicts on case-insensitive filesystems **RenameVisitor** - Added package-level conflict detection for case-insensitive filesystems: when two packages differ only by case (e.g. com.Example vs com.example), rename the conflicting one and loop until the new name is also unique - Added class-level conflict detection (same pattern): when two classes in the same package differ only by case (e.g. Sink vs sink), rename the conflicting one to prevent file overwrite on Windows export **Tests** - Added TestCaseSensitivePkgChecks: verifies package rename when packages differ only by case; fixed smali data (2.smali changed Bar→Foo to create a genuine path conflict under case-insensitive FS) - Added TestCaseSensitiveClassInPkgChecks + smali fixtures: verifies class rename when two classes in a named package differ only by case (com.example.User vs com.example.user) * Apply suggestions from code review Co-authored-by: skylot <118523+skylot@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: skylot <118523+skylot@users.noreply.github.com> --------- Co-authored-by: john <johnwsa@qq.com> Co-authored-by: skylot <118523+skylot@users.noreply.github.com>
This commit is contained in:
@@ -59,24 +59,43 @@ public class RenameVisitor extends AbstractVisitor {
|
||||
checkFields(aliasProvider, cls, args);
|
||||
checkMethods(aliasProvider, cls, args);
|
||||
}
|
||||
boolean pkgUpdated = false;
|
||||
for (PackageNode pkg : root.getPackages()) {
|
||||
pkgUpdated |= checkPackage(args, aliasProvider, pkg);
|
||||
}
|
||||
if (!args.isFsCaseSensitive() && args.isRenameCaseSensitive()) {
|
||||
// check for package directory conflicts on case insensitive filesystems
|
||||
Set<String> pkgPaths = new HashSet<>();
|
||||
for (PackageNode pkg : root.getPackages()) {
|
||||
String pkgPath = pkg.getAliasPkgInfo().getFullName().toLowerCase();
|
||||
if (!pkgPaths.add(pkgPath)) {
|
||||
pkg.setLeafAlias(aliasProvider.forPackage(pkg), false);
|
||||
pkgUpdated = true;
|
||||
// verify the new name also doesn't conflict
|
||||
if (!pkgPaths.add(pkg.getAliasPkgInfo().getFullName().toLowerCase())) {
|
||||
pkg.setLeafAlias(aliasProvider.forPackage(pkg), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pkgUpdated) {
|
||||
root.runPackagesUpdate();
|
||||
}
|
||||
if (!args.isFsCaseSensitive() && args.isRenameCaseSensitive()) {
|
||||
// check for class file conflicts on case insensitive filesystems (run after package rename)
|
||||
Set<String> clsFullPaths = new HashSet<>(classes.size());
|
||||
for (ClassNode cls : classes) {
|
||||
ClassInfo clsInfo = cls.getClassInfo();
|
||||
if (!clsFullPaths.add(clsInfo.getAliasFullPath().toLowerCase())) {
|
||||
clsInfo.changeShortName(aliasProvider.forClass(cls));
|
||||
cls.addAttr(new RenameReasonAttr(cls).append("case insensitive filesystem"));
|
||||
clsFullPaths.add(clsInfo.getAliasFullPath().toLowerCase());
|
||||
// verify the new name also doesn't conflict
|
||||
if (!clsFullPaths.add(clsInfo.getAliasFullPath().toLowerCase())) {
|
||||
clsInfo.changeShortName(aliasProvider.forClass(cls));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
boolean pkgUpdated = false;
|
||||
for (PackageNode pkg : root.getPackages()) {
|
||||
pkgUpdated |= checkPackage(args, aliasProvider, pkg);
|
||||
}
|
||||
if (pkgUpdated) {
|
||||
root.runPackagesUpdate();
|
||||
}
|
||||
processRootPackages(aliasProvider, root, classes);
|
||||
}
|
||||
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package jadx.tests.integration.names;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import jadx.core.dex.nodes.ClassNode;
|
||||
import jadx.tests.api.SmaliTest;
|
||||
|
||||
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
|
||||
|
||||
public class TestCaseSensitiveClassInPkgChecks extends SmaliTest {
|
||||
/*
|
||||
* com.example.User and com.example.user - class names differ only by case in the same package.
|
||||
* On case-insensitive FS both would map to the same file path, requiring class rename.
|
||||
*/
|
||||
|
||||
@Test
|
||||
public void testClassConflictOnCaseInsensitiveFS() {
|
||||
args.setFsCaseSensitive(false);
|
||||
|
||||
List<ClassNode> classes = loadFromSmaliFiles();
|
||||
assertThat(classes).hasSize(2);
|
||||
|
||||
long distinct = classes.stream()
|
||||
.map(cls -> cls.getClassInfo().getAliasFullPath().toLowerCase())
|
||||
.distinct()
|
||||
.count();
|
||||
assertThat(distinct).isEqualTo(2L);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClassConflictOnCaseSensitiveFS() {
|
||||
args.setFsCaseSensitive(true);
|
||||
|
||||
List<ClassNode> classes = loadFromSmaliFiles();
|
||||
assertThat(classes).hasSize(2);
|
||||
|
||||
long distinct = classes.stream()
|
||||
.map(cls -> cls.getClassInfo().getAliasFullPath())
|
||||
.distinct()
|
||||
.count();
|
||||
assertThat(distinct).isEqualTo(2L);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package jadx.tests.integration.names;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import jadx.core.dex.nodes.ClassNode;
|
||||
import jadx.tests.api.SmaliTest;
|
||||
|
||||
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
|
||||
|
||||
public class TestCaseSensitivePkgChecks extends SmaliTest {
|
||||
/*
|
||||
* com.Example.Foo and com.example.Foo - same class name in packages that differ only by case.
|
||||
* On case-insensitive FS both would map to the same path (com/example/foo), requiring package
|
||||
* rename.
|
||||
*/
|
||||
|
||||
@Test
|
||||
public void testPkgConflictOnCaseInsensitiveFS() {
|
||||
args.setFsCaseSensitive(false);
|
||||
|
||||
List<ClassNode> classes = loadFromSmaliFiles();
|
||||
assertThat(classes).hasSize(2);
|
||||
|
||||
// all package paths must be distinct when lowercased (no two classes share same dir)
|
||||
long distinctPkgPaths = classes.stream()
|
||||
.map(cls -> cls.getClassInfo().getAliasFullPath().toLowerCase())
|
||||
.distinct()
|
||||
.count();
|
||||
assertThat(distinctPkgPaths).isEqualTo(2L);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPkgConflictOnCaseSensitiveFS() {
|
||||
args.setFsCaseSensitive(true);
|
||||
|
||||
List<ClassNode> classes = loadFromSmaliFiles();
|
||||
assertThat(classes).hasSize(2);
|
||||
|
||||
// on case-sensitive FS, original package names should be preserved
|
||||
long distinctPkgPaths = classes.stream()
|
||||
.map(cls -> cls.getClassInfo().getAliasFullPath())
|
||||
.distinct()
|
||||
.count();
|
||||
assertThat(distinctPkgPaths).isEqualTo(2L);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
.class public Lcom/example/User;
|
||||
.super Ljava/lang/Object;
|
||||
@@ -0,0 +1,2 @@
|
||||
.class public Lcom/example/user;
|
||||
.super Ljava/lang/Object;
|
||||
@@ -0,0 +1,2 @@
|
||||
.class public Lcom/Example/Foo;
|
||||
.super Ljava/lang/Object;
|
||||
@@ -0,0 +1,2 @@
|
||||
.class public Lcom/example/Foo;
|
||||
.super Ljava/lang/Object;
|
||||
Reference in New Issue
Block a user