fix: resolve edge cases for select best classes from duplicates (#2701)

This commit is contained in:
Skylot
2025-12-03 19:21:19 +00:00
parent ea68024851
commit 365e258180
4 changed files with 121 additions and 20 deletions
@@ -72,6 +72,7 @@ public class ClassNode extends NotificationAttrNode
private ArgType superClass;
private List<ArgType> interfaces;
private List<ArgType> generics = Collections.emptyList();
private String inputFileName;
private List<MethodNode> methods;
private List<FieldNode> 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<IFieldData, FieldNode> fieldsConsumer = new ListConsumer<>(fld -> FieldNode.build(this, fld));
ListConsumer<IMethodData, MethodNode> 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() {
@@ -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<ClassNode> 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;
}
}
}
@@ -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<ClassNode> 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;
}
}
@@ -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);
}
}
}