fix: resolve edge cases for select best classes from duplicates (#2701)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user