diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java index e75f0662c..4d2429c89 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java @@ -72,6 +72,7 @@ public class ClassNode extends NotificationAttrNode private ArgType superClass; private List interfaces; private List generics = Collections.emptyList(); + private String inputFileName; private List methods; private List fields; @@ -123,6 +124,7 @@ public class ClassNode extends NotificationAttrNode this.accessFlags = new AccessInfo(getAccessFlags(cls), AFType.CLASS); this.superClass = checkSuperType(cls); this.interfaces = Utils.collectionMap(cls.getInterfacesTypes(), ArgType::object); + setInputFileName(cls.getInputFileName()); ListConsumer fieldsConsumer = new ListConsumer<>(fld -> FieldNode.build(this, fld)); ListConsumer methodsConsumer = new ListConsumer<>(mth -> MethodNode.build(this, mth)); @@ -228,6 +230,7 @@ public class ClassNode extends NotificationAttrNode public static ClassNode addSyntheticClass(RootNode root, ClassInfo clsInfo, int accessFlags) { ClassNode cls = new ClassNode(root, clsInfo, accessFlags); cls.add(AFlag.SYNTHETIC); + cls.setInputFileName("synthetic"); cls.setState(ProcessState.PROCESS_COMPLETE); root.addClassNode(cls); return cls; @@ -961,7 +964,11 @@ public class ClassNode extends NotificationAttrNode @Override public String getInputFileName() { - return clsData == null ? "synthetic" : clsData.getInputFileName(); + return inputFileName; + } + + public void setInputFileName(String inputFileName) { + this.inputFileName = inputFileName; } public JavaClass getJavaNode() { diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/utils/SelectFromDuplicates.java b/jadx-core/src/main/java/jadx/core/dex/nodes/utils/SelectFromDuplicates.java index 8873b9ae3..3771f09bb 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/utils/SelectFromDuplicates.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/utils/SelectFromDuplicates.java @@ -9,55 +9,66 @@ import org.slf4j.LoggerFactory; import jadx.core.dex.nodes.ClassNode; +/** + * Select best class from list of classes with same full name + * Current implementation: use class with source file as 'classesN.dex' where N is minimal + */ public class SelectFromDuplicates { private static final Logger LOG = LoggerFactory.getLogger(SelectFromDuplicates.class); - private static final Pattern CLASSES_DEX_PATTERN = Pattern.compile("classes(\\d*)\\.dex"); + private static final Pattern CLASSES_DEX_PATTERN = Pattern.compile("classes([1-9]\\d*)\\.dex"); public static ClassNode process(List dupClsList) { ClassNode bestCls = null; + int bestClsIndex = -1; for (ClassNode clsNode : dupClsList) { + boolean selectCurrent = false; if (bestCls == null) { - bestCls = clsNode; + selectCurrent = true; } else { - String bestFileName = bestCls.getInputFileName(); - String fileName = clsNode.getInputFileName(); - if (isClassesDex(fileName)) { - if (isClassesDex(bestFileName)) { + int clsIndex = getClassesIndex(clsNode.getInputFileName()); + if (clsIndex != -1) { + if (bestClsIndex != -1) { // if both are valid, the lower index has precedence - if (getClassesIndex(fileName) < getClassesIndex(bestFileName)) { - bestCls = clsNode; + if (clsIndex < bestClsIndex) { + selectCurrent = true; } } else { // valid dex names have precedence - bestCls = clsNode; + selectCurrent = true; } } } + if (selectCurrent) { + bestCls = clsNode; + bestClsIndex = getClassesIndex(clsNode.getInputFileName()); + } } return bestCls; } - private static boolean isClassesDex(String source) { - return source != null - && !source.isEmpty() - && CLASSES_DEX_PATTERN.matcher(source).matches(); - } - + /** + * Get N from classesN.dex + * + * @return -1 if source is not valid dex name + */ private static int getClassesIndex(String source) { + if ("classes.dex".equals(source)) { + return 1; + } try { Matcher matcher = CLASSES_DEX_PATTERN.matcher(source); if (!matcher.matches()) { - return Integer.MAX_VALUE; + return -1; } String num = matcher.group(1); - if (num.isEmpty()) { - return 0; + if (num.equals("1")) { + return -1; } return Integer.parseInt(num); } catch (Exception e) { LOG.debug("Failed to parse source classes index", e); - return Integer.MAX_VALUE; + return -1; } } } diff --git a/jadx-core/src/test/java/jadx/core/dex/nodes/utils/SelectFromDuplicatesTest.java b/jadx-core/src/test/java/jadx/core/dex/nodes/utils/SelectFromDuplicatesTest.java new file mode 100644 index 000000000..ae2ad66dc --- /dev/null +++ b/jadx-core/src/test/java/jadx/core/dex/nodes/utils/SelectFromDuplicatesTest.java @@ -0,0 +1,72 @@ +package jadx.core.dex.nodes.utils; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.Test; + +import jadx.api.JadxArgs; +import jadx.api.JadxDecompiler; +import jadx.core.dex.info.ClassInfo; +import jadx.core.dex.nodes.ClassNode; +import jadx.core.dex.nodes.RootNode; +import jadx.tests.api.utils.TestUtils; + +import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; + +class SelectFromDuplicatesTest { + + private RootNode root; + + @BeforeEach + public void init() { + JadxArgs args = new JadxArgs(); + args.addInputFile(TestUtils.getFileForSample("test-samples/hello.dex")); + JadxDecompiler decompiler = new JadxDecompiler(args); + decompiler.load(); + root = decompiler.getRoot(); + } + + @Test + void testSelectBySource() { + selectBySources(0, false, "classes.dex", "classes2.dex"); + selectBySources(2, false, "classes10.dex", "classes20.dex", "classes2.dex"); + } + + @RepeatedTest(10) + void testSelectBySourceShuffled() { + selectFirstByShuffleSources("classes.dex", "classes2.dex", "classes4.dex"); + selectFirstByShuffleSources("classes2.dex", "classes10.dex", "classes20.dex"); + selectFirstByShuffleSources("classes10.dex", "classes1.dex", "classes01.dex", "classes000.dex", "classes02.dex"); + } + + private void selectFirstByShuffleSources(String... sources) { + selectBySources(0, true, sources); + } + + private void selectBySources(int selectedPos, boolean shuffle, String... sources) { + List clsList = Arrays.stream(sources) + .map(this::buildClassNodeBySource) + .collect(Collectors.toList()); + ClassNode expected = clsList.get(selectedPos); + if (shuffle) { + Collections.shuffle(clsList, new Random(System.currentTimeMillis() + System.nanoTime())); + } + ClassNode selectedCls = SelectFromDuplicates.process(clsList); + assertThat(selectedCls) + .describedAs("Expect %s, but got %s from list: %s", expected, selectedCls, clsList) + .isSameAs(expected); + } + + private ClassNode buildClassNodeBySource(String clsSource) { + ClassInfo clsInfo = ClassInfo.fromName(root, "ClassFromSource:" + clsSource); + ClassNode cls = ClassNode.addSyntheticClass(root, clsInfo, 0); + cls.setInputFileName(clsSource); + return cls; + } +} diff --git a/jadx-core/src/test/java/jadx/tests/api/utils/TestUtils.java b/jadx-core/src/test/java/jadx/tests/api/utils/TestUtils.java index 2ace66222..ed8401e40 100644 --- a/jadx-core/src/test/java/jadx/tests/api/utils/TestUtils.java +++ b/jadx-core/src/test/java/jadx/tests/api/utils/TestUtils.java @@ -1,5 +1,7 @@ package jadx.tests.api.utils; +import java.io.File; + import org.junit.jupiter.api.extension.ExtendWith; import jadx.NotYetImplementedExtension; @@ -12,6 +14,7 @@ import jadx.core.dex.attributes.nodes.JadxCommentsAttr; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.Utils; +import jadx.core.utils.exceptions.JadxRuntimeException; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; import static org.assertj.core.api.Assertions.fail; @@ -71,4 +74,12 @@ public class TestUtils { } return false; } + + public static File getFileForSample(String resPath) { + try { + return new File(ClassLoader.getSystemResource(resPath).toURI().getRawPath()); + } catch (Exception e) { + throw new JadxRuntimeException("Resource load failed: " + resPath, e); + } + } }