Skip to content

Commit b3d86ae

Browse files
johnwasjohnskylot
authored
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>
1 parent 8b7d3f4 commit b3d86ae

7 files changed

Lines changed: 128 additions & 8 deletions

File tree

jadx-core/src/main/java/jadx/core/dex/visitors/rename/RenameVisitor.java

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,24 +59,43 @@ private static void checkNames(RootNode root) {
5959
checkFields(aliasProvider, cls, args);
6060
checkMethods(aliasProvider, cls, args);
6161
}
62+
boolean pkgUpdated = false;
63+
for (PackageNode pkg : root.getPackages()) {
64+
pkgUpdated |= checkPackage(args, aliasProvider, pkg);
65+
}
66+
if (!args.isFsCaseSensitive() && args.isRenameCaseSensitive()) {
67+
// check for package directory conflicts on case insensitive filesystems
68+
Set<String> pkgPaths = new HashSet<>();
69+
for (PackageNode pkg : root.getPackages()) {
70+
String pkgPath = pkg.getAliasPkgInfo().getFullName().toLowerCase();
71+
if (!pkgPaths.add(pkgPath)) {
72+
pkg.setLeafAlias(aliasProvider.forPackage(pkg), false);
73+
pkgUpdated = true;
74+
// verify the new name also doesn't conflict
75+
if (!pkgPaths.add(pkg.getAliasPkgInfo().getFullName().toLowerCase())) {
76+
pkg.setLeafAlias(aliasProvider.forPackage(pkg), false);
77+
}
78+
}
79+
}
80+
}
81+
if (pkgUpdated) {
82+
root.runPackagesUpdate();
83+
}
6284
if (!args.isFsCaseSensitive() && args.isRenameCaseSensitive()) {
85+
// check for class file conflicts on case insensitive filesystems (run after package rename)
6386
Set<String> clsFullPaths = new HashSet<>(classes.size());
6487
for (ClassNode cls : classes) {
6588
ClassInfo clsInfo = cls.getClassInfo();
6689
if (!clsFullPaths.add(clsInfo.getAliasFullPath().toLowerCase())) {
6790
clsInfo.changeShortName(aliasProvider.forClass(cls));
6891
cls.addAttr(new RenameReasonAttr(cls).append("case insensitive filesystem"));
69-
clsFullPaths.add(clsInfo.getAliasFullPath().toLowerCase());
92+
// verify the new name also doesn't conflict
93+
if (!clsFullPaths.add(clsInfo.getAliasFullPath().toLowerCase())) {
94+
clsInfo.changeShortName(aliasProvider.forClass(cls));
95+
}
7096
}
7197
}
7298
}
73-
boolean pkgUpdated = false;
74-
for (PackageNode pkg : root.getPackages()) {
75-
pkgUpdated |= checkPackage(args, aliasProvider, pkg);
76-
}
77-
if (pkgUpdated) {
78-
root.runPackagesUpdate();
79-
}
8099
processRootPackages(aliasProvider, root, classes);
81100
}
82101

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package jadx.tests.integration.names;
2+
3+
import java.util.List;
4+
5+
import org.junit.jupiter.api.Test;
6+
7+
import jadx.core.dex.nodes.ClassNode;
8+
import jadx.tests.api.SmaliTest;
9+
10+
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
11+
12+
public class TestCaseSensitiveClassInPkgChecks extends SmaliTest {
13+
/*
14+
* com.example.User and com.example.user - class names differ only by case in the same package.
15+
* On case-insensitive FS both would map to the same file path, requiring class rename.
16+
*/
17+
18+
@Test
19+
public void testClassConflictOnCaseInsensitiveFS() {
20+
args.setFsCaseSensitive(false);
21+
22+
List<ClassNode> classes = loadFromSmaliFiles();
23+
assertThat(classes).hasSize(2);
24+
25+
long distinct = classes.stream()
26+
.map(cls -> cls.getClassInfo().getAliasFullPath().toLowerCase())
27+
.distinct()
28+
.count();
29+
assertThat(distinct).isEqualTo(2L);
30+
}
31+
32+
@Test
33+
public void testClassConflictOnCaseSensitiveFS() {
34+
args.setFsCaseSensitive(true);
35+
36+
List<ClassNode> classes = loadFromSmaliFiles();
37+
assertThat(classes).hasSize(2);
38+
39+
long distinct = classes.stream()
40+
.map(cls -> cls.getClassInfo().getAliasFullPath())
41+
.distinct()
42+
.count();
43+
assertThat(distinct).isEqualTo(2L);
44+
}
45+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package jadx.tests.integration.names;
2+
3+
import java.util.List;
4+
5+
import org.junit.jupiter.api.Test;
6+
7+
import jadx.core.dex.nodes.ClassNode;
8+
import jadx.tests.api.SmaliTest;
9+
10+
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
11+
12+
public class TestCaseSensitivePkgChecks extends SmaliTest {
13+
/*
14+
* com.Example.Foo and com.example.Foo - same class name in packages that differ only by case.
15+
* On case-insensitive FS both would map to the same path (com/example/foo), requiring package
16+
* rename.
17+
*/
18+
19+
@Test
20+
public void testPkgConflictOnCaseInsensitiveFS() {
21+
args.setFsCaseSensitive(false);
22+
23+
List<ClassNode> classes = loadFromSmaliFiles();
24+
assertThat(classes).hasSize(2);
25+
26+
// all package paths must be distinct when lowercased (no two classes share same dir)
27+
long distinctPkgPaths = classes.stream()
28+
.map(cls -> cls.getClassInfo().getAliasFullPath().toLowerCase())
29+
.distinct()
30+
.count();
31+
assertThat(distinctPkgPaths).isEqualTo(2L);
32+
}
33+
34+
@Test
35+
public void testPkgConflictOnCaseSensitiveFS() {
36+
args.setFsCaseSensitive(true);
37+
38+
List<ClassNode> classes = loadFromSmaliFiles();
39+
assertThat(classes).hasSize(2);
40+
41+
// on case-sensitive FS, original package names should be preserved
42+
long distinctPkgPaths = classes.stream()
43+
.map(cls -> cls.getClassInfo().getAliasFullPath())
44+
.distinct()
45+
.count();
46+
assertThat(distinctPkgPaths).isEqualTo(2L);
47+
}
48+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.class public Lcom/example/User;
2+
.super Ljava/lang/Object;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.class public Lcom/example/user;
2+
.super Ljava/lang/Object;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.class public Lcom/Example/Foo;
2+
.super Ljava/lang/Object;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.class public Lcom/example/Foo;
2+
.super Ljava/lang/Object;

0 commit comments

Comments
 (0)