diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml index ebed36431..f552c8dc6 100644 --- a/.github/workflows/gradle-wrapper-validation.yml +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -6,5 +6,5 @@ jobs: name: "Validation" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - uses: gradle/wrapper-validation-action@v1 diff --git a/build.gradle b/build.gradle index db6f1a322..991af3a57 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ plugins { id 'org.sonarqube' version '2.8' id 'com.github.ben-manes.versions' version '0.27.0' - id "com.diffplug.gradle.spotless" version "3.26.0" + id "com.diffplug.gradle.spotless" version "3.27.1" } ext.jadxVersion = System.getenv('JADX_VERSION') ?: "dev" @@ -33,15 +33,15 @@ allprojects { } dependencies { - compile 'org.slf4j:slf4j-api:1.7.29' + compile 'org.slf4j:slf4j-api:1.7.30' testCompile 'ch.qos.logback:logback-classic:1.2.3' testCompile 'org.hamcrest:hamcrest-library:2.2' - testCompile 'org.mockito:mockito-core:3.1.0' - testCompile 'org.assertj:assertj-core:3.14.0' + testCompile 'org.mockito:mockito-core:3.2.4' + testCompile 'org.assertj:assertj-core:3.15.0' - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.5.2' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.5.2' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.0' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.6.0' testCompile 'org.eclipse.jdt.core.compiler:ecj:4.6.1' } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index cc4fdc293..f3d88b1c2 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 94920145f..1b16c34a7 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java b/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java index 56ecf5972..31a2aea7c 100644 --- a/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java +++ b/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java @@ -85,6 +85,9 @@ public class JadxCLIArgs { @Parameter(names = { "--deobf-use-sourcename" }, description = "use source file name as class name alias") protected boolean deobfuscationUseSourceNameAsAlias = false; + @Parameter(names = { "--deobf-parse-kotlin-metadata" }, description = "parse kotlin metadata to class and package names") + protected boolean deobfuscationParseKotlinMetadata = false; + @Parameter( names = { "--rename-flags" }, description = "what to rename, comma-separated," @@ -194,6 +197,7 @@ public class JadxCLIArgs { args.setDeobfuscationMinLength(deobfuscationMinLength); args.setDeobfuscationMaxLength(deobfuscationMaxLength); args.setUseSourceNameAsClassAlias(deobfuscationUseSourceNameAsAlias); + args.setParseKotlinMetadata(deobfuscationParseKotlinMetadata); args.setEscapeUnicode(escapeUnicode); args.setRespectBytecodeAccModifiers(respectBytecodeAccessModifiers); args.setExportAsGradleProject(exportAsGradleProject); @@ -275,6 +279,10 @@ public class JadxCLIArgs { return deobfuscationUseSourceNameAsAlias; } + public boolean isDeobfuscationParseKotlinMetadata() { + return deobfuscationParseKotlinMetadata; + } + public boolean isEscapeUnicode() { return escapeUnicode; } diff --git a/jadx-core/build.gradle b/jadx-core/build.gradle index 83fdb0b29..e12f8a98b 100644 --- a/jadx-core/build.gradle +++ b/jadx-core/build.gradle @@ -4,15 +4,15 @@ dependencies { compile files('lib/dx-1.16.jar') // TODO: dx don't support java version > 9 (53) - compile 'org.ow2.asm:asm:7.2' + compile 'org.ow2.asm:asm:7.3.1' compile 'org.jetbrains:annotations:18.0.0' compile 'com.google.code.gson:gson:2.8.6' - compile 'org.smali:baksmali:2.3.4' - compile('org.smali:smali:2.3.4') { + compile 'org.smali:baksmali:2.4.0' + compile('org.smali:smali:2.4.0') { exclude group: 'com.google.guava' } - compile 'com.google.guava:guava:28.1-jre' + compile 'com.google.guava:guava:28.2-jre' testCompile 'org.apache.commons:commons-lang3:3.9' } diff --git a/jadx-core/clsp-data/android-29-clst.jar b/jadx-core/clsp-data/android-29-clst.jar index 5a4e2de98..cc5c4432d 100644 Binary files a/jadx-core/clsp-data/android-29-clst.jar and b/jadx-core/clsp-data/android-29-clst.jar differ diff --git a/jadx-core/src/main/java/jadx/api/JadxArgs.java b/jadx-core/src/main/java/jadx/api/JadxArgs.java index a7cc08e8a..93e52d30a 100644 --- a/jadx-core/src/main/java/jadx/api/JadxArgs.java +++ b/jadx-core/src/main/java/jadx/api/JadxArgs.java @@ -49,6 +49,7 @@ public class JadxArgs { private boolean deobfuscationOn = false; private boolean deobfuscationForceSave = false; private boolean useSourceNameAsClassAlias = false; + private boolean parseKotlinMetadata = false; private int deobfuscationMinLength = 0; private int deobfuscationMaxLength = Integer.MAX_VALUE; @@ -230,6 +231,14 @@ public class JadxArgs { this.useSourceNameAsClassAlias = useSourceNameAsClassAlias; } + public boolean isParseKotlinMetadata() { + return parseKotlinMetadata; + } + + public void setParseKotlinMetadata(boolean parseKotlinMetadata) { + this.parseKotlinMetadata = parseKotlinMetadata; + } + public int getDeobfuscationMinLength() { return deobfuscationMinLength; } @@ -318,6 +327,14 @@ public class JadxArgs { } } + public void setRenameFlags(Set renameFlags) { + this.renameFlags = renameFlags; + } + + public Set getRenameFlags() { + return renameFlags; + } + public OutputFormatEnum getOutputFormat() { return outputFormat; } @@ -355,6 +372,7 @@ public class JadxArgs { + ", deobfuscationOn=" + deobfuscationOn + ", deobfuscationForceSave=" + deobfuscationForceSave + ", useSourceNameAsClassAlias=" + useSourceNameAsClassAlias + + ", parseKotlinMetadata=" + parseKotlinMetadata + ", deobfuscationMinLength=" + deobfuscationMinLength + ", deobfuscationMaxLength=" + deobfuscationMaxLength + ", escapeUnicode=" + escapeUnicode diff --git a/jadx-core/src/main/java/jadx/api/JavaMethod.java b/jadx-core/src/main/java/jadx/api/JavaMethod.java index 53a51e94b..82b5ac492 100644 --- a/jadx-core/src/main/java/jadx/api/JavaMethod.java +++ b/jadx-core/src/main/java/jadx/api/JavaMethod.java @@ -47,18 +47,12 @@ public final class JavaMethod implements JavaNode { return Collections.emptyList(); } List arguments = mth.getArgTypes(); - if (arguments == null) { - arguments = infoArgTypes; - } return Utils.collectionMap(arguments, type -> ArgType.tryToResolveClassAlias(mth.dex(), type)); } public ArgType getReturnType() { ArgType retType = mth.getReturnType(); - if (retType == null) { - retType = mth.getMethodInfo().getReturnType(); - } return ArgType.tryToResolveClassAlias(mth.dex(), retType); } diff --git a/jadx-core/src/main/java/jadx/core/Jadx.java b/jadx-core/src/main/java/jadx/core/Jadx.java index 506c8a4ab..fd8162f90 100644 --- a/jadx-core/src/main/java/jadx/core/Jadx.java +++ b/jadx-core/src/main/java/jadx/core/Jadx.java @@ -11,26 +11,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.JadxArgs; -import jadx.core.dex.visitors.ClassModifier; -import jadx.core.dex.visitors.ConstInlineVisitor; -import jadx.core.dex.visitors.ConstructorVisitor; -import jadx.core.dex.visitors.DeboxingVisitor; -import jadx.core.dex.visitors.DependencyCollector; -import jadx.core.dex.visitors.DotGraphVisitor; -import jadx.core.dex.visitors.EnumVisitor; -import jadx.core.dex.visitors.ExtractFieldInit; -import jadx.core.dex.visitors.FallbackModeVisitor; -import jadx.core.dex.visitors.FixAccessModifiers; -import jadx.core.dex.visitors.IDexTreeVisitor; -import jadx.core.dex.visitors.InitCodeVariables; -import jadx.core.dex.visitors.MarkFinallyVisitor; -import jadx.core.dex.visitors.MethodInlineVisitor; -import jadx.core.dex.visitors.ModVisitor; -import jadx.core.dex.visitors.PrepareForCodeGen; -import jadx.core.dex.visitors.ProcessAnonymous; -import jadx.core.dex.visitors.ReSugarCode; -import jadx.core.dex.visitors.RenameVisitor; -import jadx.core.dex.visitors.SimplifyVisitor; +import jadx.core.dex.visitors.*; import jadx.core.dex.visitors.blocksmaker.BlockExceptionHandler; import jadx.core.dex.visitors.blocksmaker.BlockFinish; import jadx.core.dex.visitors.blocksmaker.BlockProcessor; @@ -69,6 +50,7 @@ public class Jadx { passes.add(new DebugInfoParseVisitor()); } + passes.add(new FindSuperUsageVisitor()); passes.add(new BlockSplitter()); if (args.isRawCFGOutput()) { passes.add(DotGraphVisitor.dumpRaw()); @@ -83,6 +65,7 @@ public class Jadx { passes.add(new InitCodeVariables()); passes.add(new MarkFinallyVisitor()); passes.add(new ConstInlineVisitor()); + passes.add(new AttachMethodDetails()); passes.add(new TypeInferenceVisitor()); if (args.isDebugInfo()) { passes.add(new DebugInfoApplyVisitor()); @@ -102,15 +85,16 @@ public class Jadx { passes.add(new CleanRegions()); passes.add(new CodeShrinkVisitor()); + passes.add(new MethodInvokeVisitor()); passes.add(new SimplifyVisitor()); passes.add(new CheckRegions()); + passes.add(new EnumVisitor()); passes.add(new ExtractFieldInit()); passes.add(new FixAccessModifiers()); passes.add(new ProcessAnonymous()); passes.add(new ClassModifier()); passes.add(new MethodInlineVisitor()); - passes.add(new EnumVisitor()); passes.add(new LoopRegionVisitor()); passes.add(new ProcessVariables()); diff --git a/jadx-core/src/main/java/jadx/core/clsp/ClsSet.java b/jadx-core/src/main/java/jadx/core/clsp/ClsSet.java index 426b8a8a7..565a11b9b 100644 --- a/jadx-core/src/main/java/jadx/core/clsp/ClsSet.java +++ b/jadx-core/src/main/java/jadx/core/clsp/ClsSet.java @@ -1,5 +1,6 @@ package jadx.core.clsp; +import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; @@ -12,21 +13,26 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.dex.info.AccessInfo; +import jadx.core.dex.info.ClassInfo; +import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.ClassNode; -import jadx.core.dex.nodes.GenericInfo; +import jadx.core.dex.nodes.GenericTypeParameter; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.exceptions.DecodeException; @@ -34,6 +40,9 @@ import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.files.FileUtils; import jadx.core.utils.files.ZipSecurity; +import static jadx.core.utils.Utils.isEmpty; +import static jadx.core.utils.Utils.notEmpty; + /** * Classes list for import into classpath graph */ @@ -45,123 +54,132 @@ public class ClsSet { private static final String CLST_PKG_PATH = ClsSet.class.getPackage().getName().replace('.', '/'); private static final String JADX_CLS_SET_HEADER = "jadx-cst"; - private static final int VERSION = 2; + private static final int VERSION = 3; private static final String STRING_CHARSET = "US-ASCII"; - private static final NClass[] EMPTY_NCLASS_ARRAY = new NClass[0]; + private static final ArgType[] EMPTY_ARGTYPE_ARRAY = new ArgType[0]; - private enum TypeEnum { - WILDCARD, GENERIC, GENERIC_TYPE, OBJECT, ARRAY, PRIMITIVE + private final RootNode root; + + public ClsSet(RootNode root) { + this.root = root; } - private NClass[] classes; + private enum TypeEnum { + WILDCARD, + GENERIC, + GENERIC_TYPE_VARIABLE, + OUTER_GENERIC, + OBJECT, + ARRAY, + PRIMITIVE + } + + private ClspClass[] classes; public void loadFromClstFile() throws IOException, DecodeException { + long startTime = System.currentTimeMillis(); try (InputStream input = getClass().getResourceAsStream(CLST_FILENAME)) { if (input == null) { throw new JadxRuntimeException("Can't load classpath file: " + CLST_FILENAME); } load(input); } + if (LOG.isDebugEnabled()) { + long time = System.currentTimeMillis() - startTime; + int methodsCount = Arrays.stream(classes).mapToInt(clspClass -> clspClass.getMethodsMap().size()).sum(); + LOG.debug("Load class set in {}ms, classes: {}, methods: {}", time, classes.length, methodsCount); + } } public void loadFrom(RootNode root) { List list = root.getClasses(true); - Map names = new HashMap<>(list.size()); + Map names = new HashMap<>(list.size()); int k = 0; for (ClassNode cls : list) { - String clsRawName = cls.getRawName(); - if (cls.getAccessFlags().isPublic()) { - cls.load(); - NClass nClass = new NClass(clsRawName, k); - if (names.put(clsRawName, nClass) != null) { - throw new JadxRuntimeException("Duplicate class: " + clsRawName); - } - k++; - nClass.setGenerics(cls.getGenerics()); - nClass.setMethods(getMethodsDetails(cls)); - } else { - names.put(clsRawName, null); + ArgType clsType = cls.getClassInfo().getType(); + String clsRawName = clsType.getObject(); + cls.load(); + ClspClass nClass = new ClspClass(clsType, k); + if (names.put(clsRawName, nClass) != null) { + throw new JadxRuntimeException("Duplicate class: " + clsRawName); } + k++; + nClass.setTypeParameters(cls.getGenericTypeParameters()); + nClass.setMethods(getMethodsDetails(cls)); } - classes = new NClass[k]; + classes = new ClspClass[k]; k = 0; for (ClassNode cls : list) { - if (cls.getAccessFlags().isPublic()) { - NClass nClass = getCls(cls.getRawName(), names); - if (nClass == null) { - throw new JadxRuntimeException("Missing class: " + cls); - } - nClass.setParents(makeParentsArray(cls, names)); - classes[k] = nClass; - k++; + ClspClass nClass = getCls(cls, names); + if (nClass == null) { + throw new JadxRuntimeException("Missing class: " + cls); } + nClass.setParents(makeParentsArray(cls)); + classes[k] = nClass; + k++; } } - private List getMethodsDetails(ClassNode cls) { - List methods = new ArrayList<>(); - for (MethodNode m : cls.getMethods()) { - AccessInfo accessFlags = m.getAccessFlags(); - if (accessFlags.isPublic() || accessFlags.isProtected()) { - processMethodDetails(methods, m, accessFlags); - } + private List getMethodsDetails(ClassNode cls) { + List methodsList = cls.getMethods(); + List methods = new ArrayList<>(methodsList.size()); + for (MethodNode mth : methodsList) { + processMethodDetails(mth, methods); } return methods; } - private void processMethodDetails(List methods, MethodNode mth, AccessInfo accessFlags) { - List args = mth.getArgTypes(); - boolean genericArg = false; - ArgType[] genericArgs; - if (args.isEmpty()) { - genericArgs = null; - } else { - int argsCount = args.size(); - genericArgs = new ArgType[argsCount]; - for (int i = 0; i < argsCount; i++) { - ArgType argType = args.get(i); - if (argType.isGeneric() || argType.isGenericType()) { - genericArgs[i] = argType; - genericArg = true; - } - } - } - ArgType retType = mth.getReturnType(); - if (!retType.isGeneric() && !retType.isGenericType()) { - retType = null; + private void processMethodDetails(MethodNode mth, List methods) { + AccessInfo accessFlags = mth.getAccessFlags(); + if (accessFlags.isPrivate()) { + return; } + ArgType genericRetType = mth.getReturnType(); boolean varArgs = accessFlags.isVarArgs(); - if (genericArg || retType != null || varArgs) { - methods.add(new NMethod(mth.getMethodInfo().getShortId(), genericArgs, retType, varArgs)); + List throwList = mth.getThrows(); + List typeParameters = mth.getTypeParameters(); + // add only methods with additional info + if (varArgs + || notEmpty(throwList) + || notEmpty(typeParameters) + || genericRetType.containsGeneric() + || mth.containsGenericArgs() + || mth.isArgsOverloaded()) { + ClspMethod clspMethod = new ClspMethod(mth.getMethodInfo(), + mth.getArgTypes(), genericRetType, + typeParameters, varArgs, throwList); + methods.add(clspMethod); } } - public static NClass[] makeParentsArray(ClassNode cls, Map names) { - List parents = new ArrayList<>(1 + cls.getInterfaces().size()); + public static ArgType[] makeParentsArray(ClassNode cls) { ArgType superClass = cls.getSuperClass(); - if (superClass != null) { - NClass c = getCls(superClass.getObject(), names); - if (c != null) { - parents.add(c); - } + if (superClass == null) { + // cls is java.lang.Object + return EMPTY_ARGTYPE_ARRAY; } + ArgType[] parents = new ArgType[1 + cls.getInterfaces().size()]; + parents[0] = superClass; + int k = 1; for (ArgType iface : cls.getInterfaces()) { - NClass c = getCls(iface.getObject(), names); - if (c != null) { - parents.add(c); - } + parents[k] = iface; + k++; } - int size = parents.size(); - if (size == 0) { - return EMPTY_NCLASS_ARRAY; - } - return parents.toArray(new NClass[size]); + return parents; } - private static NClass getCls(String fullName, Map names) { - NClass cls = names.get(fullName); + private static ClspClass getCls(ClassNode cls, Map names) { + return getCls(cls.getRawName(), names); + } + + private static ClspClass getCls(ArgType clsType, Map names) { + return getCls(clsType.getObject(), names); + } + + private static ClspClass getCls(String fullName, Map names) { + ClspClass cls = names.get(fullName); if (cls == null) { LOG.debug("Class not found: {}", fullName); } @@ -182,16 +200,25 @@ public class ClsSet { try (ZipOutputStream out = new ZipOutputStream(Files.newOutputStream(path)); ZipInputStream in = new ZipInputStream(Files.newInputStream(temp))) { String clst = CLST_PKG_PATH + '/' + CLST_FILENAME; - out.putNextEntry(new ZipEntry(clst)); - save(out); + boolean clstReplaced = false; ZipEntry entry = in.getNextEntry(); while (entry != null) { - if (!entry.getName().equals(clst)) { - out.putNextEntry(new ZipEntry(entry.getName())); + String entryName = entry.getName(); + ZipEntry copyEntry = new ZipEntry(entryName); + copyEntry.setLastModifiedTime(entry.getLastModifiedTime()); // preserve modified time + out.putNextEntry(copyEntry); + if (entryName.equals(clst)) { + save(out); + clstReplaced = true; + } else { FileUtils.copyStream(in, out); } entry = in.getNextEntry(); } + if (!clstReplaced) { + out.putNextEntry(new ZipEntry(clst)); + save(out); + } } } else { throw new JadxRuntimeException("Unknown file format: " + outputName); @@ -203,76 +230,90 @@ public class ClsSet { out.writeBytes(JADX_CLS_SET_HEADER); out.writeByte(VERSION); - LOG.info("Classes count: {}", classes.length); - Map names = new HashMap<>(classes.length); + Map names = new HashMap<>(classes.length); out.writeInt(classes.length); - for (NClass cls : classes) { - writeString(out, cls.getName()); - names.put(cls.getName(), cls); + for (ClspClass cls : classes) { + String clsName = cls.getName(); + writeString(out, clsName); + names.put(clsName, cls); } - for (NClass cls : classes) { - NClass[] parents = cls.getParents(); - out.writeByte(parents.length); - for (NClass parent : parents) { - out.writeInt(parent.getId()); - } - writeGenerics(out, cls, names); - List methods = cls.getMethodsList(); - out.writeByte(methods.size()); - for (NMethod method : methods) { + for (ClspClass cls : classes) { + writeArgTypesArray(out, cls.getParents(), names); + writeGenericTypeParameters(out, cls.getTypeParameters(), names); + List methods = cls.getSortedMethodsList(); + out.writeShort(methods.size()); + for (ClspMethod method : methods) { writeMethod(out, method, names); } } + int methodsCount = Arrays.stream(classes).mapToInt(c -> c.getMethodsMap().size()).sum(); + LOG.info("Classes: {}, methods: {}, file size: {}B", classes.length, methodsCount, out.size()); } - private static void writeGenerics(DataOutputStream out, NClass cls, Map names) throws IOException { - List genericsList = cls.getGenerics(); - out.writeByte(genericsList.size()); - for (GenericInfo genericInfo : genericsList) { - writeArgType(out, genericInfo.getGenericType(), names); - List extendsList = genericInfo.getExtendsList(); - out.writeByte(extendsList.size()); - for (ArgType type : extendsList) { - writeArgType(out, type, names); - } - - } - } - - private static void writeMethod(DataOutputStream out, NMethod method, Map names) throws IOException { - writeLongString(out, method.getShortId()); - - ArgType[] argTypes = method.getGenericArgs(); - if (argTypes == null) { + private static void writeGenericTypeParameters(DataOutputStream out, + List typeParameters, + Map names) throws IOException { + if (isEmpty(typeParameters)) { out.writeByte(0); } else { - int argCount = 0; - for (ArgType arg : argTypes) { - if (arg != null) { - argCount++; - } - } - out.writeByte(argCount); - // last argument first - for (int i = argTypes.length - 1; i >= 0; i--) { - ArgType argType = argTypes[i]; - if (argType != null) { - out.writeByte(i); - writeArgType(out, argType, names); - } + writeUnsignedByte(out, typeParameters.size()); + for (GenericTypeParameter typeParameter : typeParameters) { + writeArgType(out, typeParameter.getTypeVariable(), names); + writeArgTypesList(out, typeParameter.getExtendsList(), names); } } - if (method.getReturnType() == null) { - out.writeBoolean(false); - } else { - out.writeBoolean(true); - writeArgType(out, method.getReturnType(), names); - } - out.writeBoolean(method.isVarArgs()); } - private static void writeArgType(DataOutputStream out, ArgType argType, Map names) throws IOException { - if (argType.getWildcardType() != null) { + private static void writeMethod(DataOutputStream out, ClspMethod method, Map names) throws IOException { + MethodInfo methodInfo = method.getMethodInfo(); + writeString(out, methodInfo.getName()); + writeArgTypesList(out, methodInfo.getArgumentsTypes(), names); + writeArgType(out, methodInfo.getReturnType(), names); + + writeArgTypesList(out, method.containsGenericArgs() ? method.getArgTypes() : Collections.emptyList(), names); + writeArgType(out, method.getReturnType(), names); + writeGenericTypeParameters(out, method.getTypeParameters(), names); + out.writeBoolean(method.isVarArg()); + writeArgTypesList(out, method.getThrows(), names); + } + + private static void writeArgTypesList(DataOutputStream out, List list, Map names) throws IOException { + int size = list.size(); + writeUnsignedByte(out, size); + if (size != 0) { + for (ArgType type : list) { + writeArgType(out, type, names); + } + } + } + + private static void writeArgTypesArray(DataOutputStream out, @Nullable ArgType[] arr, Map names) throws IOException { + if (arr == null) { + out.writeByte(-1); + return; + } + int size = arr.length; + out.writeByte(size); + if (size != 0) { + for (ArgType type : arr) { + writeArgType(out, type, names); + } + } + } + + private static void writeArgType(DataOutputStream out, ArgType argType, Map names) throws IOException { + if (argType == null) { + out.writeByte(-1); + return; + } + if (argType.isPrimitive()) { + out.writeByte(TypeEnum.PRIMITIVE.ordinal()); + out.writeByte(argType.getPrimitiveType().getShortName().charAt(0)); + } else if (argType.getOuterType() != null) { + out.writeByte(TypeEnum.OUTER_GENERIC.ordinal()); + writeArgType(out, argType.getOuterType(), names); + writeArgType(out, argType.getInnerType(), names); + } else if (argType.getWildcardType() != null) { out.writeByte(TypeEnum.WILDCARD.ordinal()); ArgType.WildcardBound bound = argType.getWildcardBound(); out.writeByte(bound.getNum()); @@ -281,28 +322,18 @@ public class ClsSet { } } else if (argType.isGeneric()) { out.writeByte(TypeEnum.GENERIC.ordinal()); - out.writeInt(names.get(argType.getObject()).getId()); + out.writeInt(getCls(argType, names).getId()); ArgType[] types = argType.getGenericTypes(); - if (types == null) { - out.writeByte(0); - } else { - out.writeByte(types.length); - for (ArgType type : types) { - writeArgType(out, type, names); - } - } + writeArgTypesArray(out, types, names); } else if (argType.isGenericType()) { - out.writeByte(TypeEnum.GENERIC_TYPE.ordinal()); + out.writeByte(TypeEnum.GENERIC_TYPE_VARIABLE.ordinal()); writeString(out, argType.getObject()); } else if (argType.isObject()) { out.writeByte(TypeEnum.OBJECT.ordinal()); - out.writeInt(names.get(argType.getObject()).getId()); + out.writeInt(getCls(argType, names).getId()); } else if (argType.isArray()) { out.writeByte(TypeEnum.ARRAY.ordinal()); writeArgType(out, argType.getArrayElement(), names); - } else if (argType.isPrimitive()) { - out.writeByte(TypeEnum.PRIMITIVE.ordinal()); - out.writeByte(argType.getPrimitiveType().getShortName().charAt(0)); } else { throw new JadxRuntimeException("Cannot save type: " + argType); } @@ -330,7 +361,7 @@ public class ClsSet { } private void load(InputStream input) throws IOException, DecodeException { - try (DataInputStream in = new DataInputStream(input)) { + try (DataInputStream in = new DataInputStream(new BufferedInputStream(input))) { byte[] header = new byte[JADX_CLS_SET_HEADER.length()]; int readHeaderLength = in.read(header); int version = in.readByte(); @@ -339,87 +370,119 @@ public class ClsSet { || version != VERSION) { throw new DecodeException("Wrong jadx class set header"); } - int count = in.readInt(); - classes = new NClass[count]; - for (int i = 0; i < count; i++) { + int clsCount = in.readInt(); + classes = new ClspClass[clsCount]; + for (int i = 0; i < clsCount; i++) { String name = readString(in); - classes[i] = new NClass(name, i); + classes[i] = new ClspClass(ArgType.object(name), i); } - for (int i = 0; i < count; i++) { - int pCount = in.readByte(); - NClass[] parents = new NClass[pCount]; - for (int j = 0; j < pCount; j++) { - parents[j] = classes[in.readInt()]; - } - NClass nClass = classes[i]; - nClass.setParents(parents); - nClass.setGenerics(readGenerics(in)); - nClass.setMethods(readClsMethods(in)); + for (int i = 0; i < clsCount; i++) { + ClspClass nClass = classes[i]; + ClassInfo clsInfo = ClassInfo.fromType(root, nClass.getClsType()); + nClass.setParents(readArgTypesArray(in)); + nClass.setTypeParameters(readGenericTypeParameters(in)); + nClass.setMethods(readClsMethods(in, clsInfo)); } } } - private List readGenerics(DataInputStream in) throws IOException { - int count = in.readByte(); + private List readGenericTypeParameters(DataInputStream in) throws IOException { + int count = readUnsignedByte(in); if (count == 0) { return Collections.emptyList(); } - List list = new ArrayList<>(count); + List list = new ArrayList<>(count); for (int i = 0; i < count; i++) { - ArgType genericType = readArgType(in); - List extendsList; - byte extCount = in.readByte(); - if (extCount == 0) { - extendsList = Collections.emptyList(); - } else { - extendsList = new ArrayList<>(extCount); - for (int j = 0; j < extCount; j++) { - extendsList.add(readArgType(in)); - } - } - list.add(new GenericInfo(genericType, extendsList)); + ArgType typeVariable = readArgType(in); + List extendsList = readArgTypesList(in); + list.add(new GenericTypeParameter(typeVariable, extendsList)); } return list; } - private List readClsMethods(DataInputStream in) throws IOException { - int mCount = in.readByte(); - List methods = new ArrayList<>(mCount); + private List readClsMethods(DataInputStream in, ClassInfo clsInfo) throws IOException { + int mCount = in.readShort(); + List methods = new ArrayList<>(mCount); for (int j = 0; j < mCount; j++) { - methods.add(readMethod(in)); + methods.add(readMethod(in, clsInfo)); } return methods; } - private NMethod readMethod(DataInputStream in) throws IOException { - String shortId = readLongString(in); - int argCount = in.readByte(); - ArgType[] argTypes = null; - for (int i = 0; i < argCount; i++) { - int index = in.readByte(); - ArgType argType = readArgType(in); - if (argTypes == null) { - argTypes = new ArgType[index + 1]; - } - argTypes[index] = argType; + private ClspMethod readMethod(DataInputStream in, ClassInfo clsInfo) throws IOException { + String name = readString(in); + List argTypes = readArgTypesList(in); + ArgType retType = readArgType(in); + List genericArgTypes = readArgTypesList(in); + if (genericArgTypes.isEmpty() || Objects.equals(genericArgTypes, argTypes)) { + genericArgTypes = argTypes; } - ArgType retType = in.readBoolean() ? readArgType(in) : null; + ArgType genericRetType = readArgType(in); + if (Objects.equals(genericRetType, retType)) { + genericRetType = retType; + } + List typeParameters = readGenericTypeParameters(in); boolean varArgs = in.readBoolean(); - return new NMethod(shortId, argTypes, retType, varArgs); + List throwList = readArgTypesList(in); + MethodInfo methodInfo = MethodInfo.fromDetails(root, clsInfo, name, argTypes, retType); + return new ClspMethod(methodInfo, + genericArgTypes, genericRetType, + typeParameters, varArgs, throwList); + } + + private List readArgTypesList(DataInputStream in) throws IOException { + int count = in.readByte(); + if (count == 0) { + return Collections.emptyList(); + } + List list = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + list.add(readArgType(in)); + } + return list; + } + + @Nullable + private ArgType[] readArgTypesArray(DataInputStream in) throws IOException { + int count = in.readByte(); + if (count == -1) { + return null; + } + if (count == 0) { + return EMPTY_ARGTYPE_ARRAY; + } + ArgType[] arr = new ArgType[count]; + for (int i = 0; i < count; i++) { + arr[i] = readArgType(in); + } + return arr; } private ArgType readArgType(DataInputStream in) throws IOException { int ordinal = in.readByte(); + if (ordinal == -1) { + return null; + } + if (ordinal >= TypeEnum.values().length) { + throw new JadxRuntimeException("Incorrect ordinal for type enum: " + ordinal); + } switch (TypeEnum.values()[ordinal]) { case WILDCARD: - int bounds = in.readByte(); - return bounds == 0 - ? ArgType.wildcard() - : ArgType.wildcard(readArgType(in), ArgType.WildcardBound.getByNum(bounds)); + ArgType.WildcardBound bound = ArgType.WildcardBound.getByNum(in.readByte()); + if (bound == ArgType.WildcardBound.UNBOUND) { + return ArgType.WILDCARD; + } + ArgType objType = readArgType(in); + return ArgType.wildcard(objType, bound); + + case OUTER_GENERIC: + ArgType outerType = readArgType(in); + ArgType innerType = readArgType(in); + return ArgType.outerGeneric(outerType, innerType); case GENERIC: - String obj = classes[in.readInt()].getName(); - int typeLength = in.readByte(); + ArgType clsType = classes[in.readInt()].getClsType(); + int typeLength = readUnsignedByte(in); ArgType[] generics; if (typeLength == 0) { generics = null; @@ -429,13 +492,13 @@ public class ClsSet { generics[i] = readArgType(in); } } - return ArgType.generic(obj, generics); + return ArgType.generic(clsType, generics); - case GENERIC_TYPE: + case GENERIC_TYPE_VARIABLE: return ArgType.genericType(readString(in)); case OBJECT: - return ArgType.object(classes[in.readInt()].getName()); + return classes[in.readInt()].getClsType(); case ARRAY: return ArgType.array(readArgType(in)); @@ -451,23 +514,16 @@ public class ClsSet { private static void writeString(DataOutputStream out, String name) throws IOException { byte[] bytes = name.getBytes(STRING_CHARSET); - out.writeByte(bytes.length); - out.write(bytes); - } - - private static void writeLongString(DataOutputStream out, String name) throws IOException { - byte[] bytes = name.getBytes(STRING_CHARSET); - out.writeShort(bytes.length); + int len = bytes.length; + if (len >= 0xFF) { + throw new JadxRuntimeException("String is too long: " + name); + } + writeUnsignedByte(out, bytes.length); out.write(bytes); } private static String readString(DataInputStream in) throws IOException { - int len = in.readByte(); - return readString(in, len); - } - - private static String readLongString(DataInputStream in) throws IOException { - int len = in.readShort(); + int len = readUnsignedByte(in); return readString(in, len); } @@ -485,12 +541,23 @@ public class ClsSet { return new String(bytes, STRING_CHARSET); } + private static void writeUnsignedByte(DataOutputStream out, int value) throws IOException { + if (value < 0 || value >= 0xFF) { + throw new JadxRuntimeException("Unsigned byte value is too big: " + value); + } + out.writeByte(value); + } + + private static int readUnsignedByte(DataInputStream in) throws IOException { + return ((int) in.readByte()) & 0xFF; + } + public int getClassesCount() { return classes.length; } - public void addToMap(Map nameMap) { - for (NClass cls : classes) { + public void addToMap(Map nameMap) { + for (ClspClass cls : classes) { nameMap.put(cls.getName(), cls); } } diff --git a/jadx-core/src/main/java/jadx/core/clsp/ClspClass.java b/jadx-core/src/main/java/jadx/core/clsp/ClspClass.java new file mode 100644 index 000000000..97a01ccb3 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/clsp/ClspClass.java @@ -0,0 +1,101 @@ +package jadx.core.clsp; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import jadx.core.dex.instructions.args.ArgType; +import jadx.core.dex.nodes.GenericTypeParameter; + +/** + * Class node in classpath graph + */ +public class ClspClass { + + private final ArgType clsType; + private final int id; + private ArgType[] parents; + private Map methodsMap = Collections.emptyMap(); + private List typeParameters = Collections.emptyList(); + + public ClspClass(ArgType clsType, int id) { + this.clsType = clsType; + this.id = id; + } + + public String getName() { + return clsType.getObject(); + } + + public ArgType getClsType() { + return clsType; + } + + public int getId() { + return id; + } + + public ArgType[] getParents() { + return parents; + } + + public void setParents(ArgType[] parents) { + this.parents = parents; + } + + public Map getMethodsMap() { + return methodsMap; + } + + public List getSortedMethodsList() { + List list = new ArrayList<>(methodsMap.size()); + list.addAll(methodsMap.values()); + Collections.sort(list); + return list; + } + + public void setMethodsMap(Map methodsMap) { + this.methodsMap = Objects.requireNonNull(methodsMap); + } + + public void setMethods(List methods) { + Map map = new HashMap<>(methods.size()); + for (ClspMethod mth : methods) { + map.put(mth.getMethodInfo().getShortId(), mth); + } + setMethodsMap(map); + } + + public List getTypeParameters() { + return typeParameters; + } + + public void setTypeParameters(List typeParameters) { + this.typeParameters = typeParameters; + } + + @Override + public int hashCode() { + return clsType.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ClspClass nClass = (ClspClass) o; + return clsType.equals(nClass.clsType); + } + + @Override + public String toString() { + return clsType.toString(); + } +} diff --git a/jadx-core/src/main/java/jadx/core/clsp/ClspGraph.java b/jadx-core/src/main/java/jadx/core/clsp/ClspGraph.java index abdfc9475..c8a7e9eaa 100644 --- a/jadx-core/src/main/java/jadx/core/clsp/ClspGraph.java +++ b/jadx-core/src/main/java/jadx/core/clsp/ClspGraph.java @@ -17,6 +17,8 @@ import org.slf4j.LoggerFactory; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.ClassNode; +import jadx.core.dex.nodes.IMethodDetails; +import jadx.core.dex.nodes.RootNode; import jadx.core.utils.exceptions.DecodeException; import jadx.core.utils.exceptions.JadxRuntimeException; @@ -26,13 +28,18 @@ import jadx.core.utils.exceptions.JadxRuntimeException; public class ClspGraph { private static final Logger LOG = LoggerFactory.getLogger(ClspGraph.class); + private final RootNode root; private final Map> ancestorCache = Collections.synchronizedMap(new WeakHashMap<>()); - private Map nameMap; + private Map nameMap; private final Set missingClasses = new HashSet<>(); + public ClspGraph(RootNode rootNode) { + this.root = rootNode; + } + public void load() throws IOException, DecodeException { - ClsSet set = new ClsSet(); + ClsSet set = new ClsSet(root); set.loadFromClstFile(); addClasspath(set); } @@ -51,13 +58,13 @@ public class ClspGraph { throw new JadxRuntimeException("Classpath must be loaded first"); } int size = classes.size(); - NClass[] nClasses = new NClass[size]; + ClspClass[] nClasses = new ClspClass[size]; int k = 0; for (ClassNode cls : classes) { nClasses[k++] = addClass(cls); } for (int i = 0; i < size; i++) { - nClasses[i].setParents(ClsSet.makeParentsArray(classes.get(i), nameMap)); + nClasses[i].setParents(ClsSet.makeParentsArray(classes.get(i))); } } @@ -65,24 +72,44 @@ public class ClspGraph { return nameMap.containsKey(fullName); } - public NClass getClsDetails(ArgType type) { + public ClspClass getClsDetails(ArgType type) { return nameMap.get(type.getObject()); } @Nullable - public NMethod getMethodDetails(MethodInfo methodInfo) { - NClass cls = nameMap.get(methodInfo.getDeclClass().getRawName()); + public IMethodDetails getMethodDetails(MethodInfo methodInfo) { + ClspClass cls = nameMap.get(methodInfo.getDeclClass().getRawName()); if (cls == null) { return null; } + ClspMethod clspMethod = getMethodFromClass(cls, methodInfo); + if (clspMethod != null) { + return clspMethod; + } + // deep search + for (ArgType parent : cls.getParents()) { + ClspClass clspParent = getClspClass(parent); + if (clspParent != null) { + ClspMethod methodFromParent = getMethodFromClass(clspParent, methodInfo); + if (methodFromParent != null) { + return methodFromParent; + } + } + } + // all other methods in known ClspClass are 'simple' + return new SimpleMethodDetails(methodInfo); + } + + private ClspMethod getMethodFromClass(ClspClass cls, MethodInfo methodInfo) { return cls.getMethodsMap().get(methodInfo.getShortId()); } - private NClass addClass(ClassNode cls) { - String rawName = cls.getRawName(); - NClass nClass = new NClass(rawName, -1); - nameMap.put(rawName, nClass); - return nClass; + private ClspClass addClass(ClassNode cls) { + ArgType clsType = cls.getClassInfo().getType(); + String rawName = clsType.getObject(); + ClspClass clspClass = new ClspClass(clsType, -1); + nameMap.put(rawName, clspClass); + return clspClass; } /** @@ -107,7 +134,7 @@ public class ClspGraph { if (clsName.equals(implClsName)) { return clsName; } - NClass cls = nameMap.get(implClsName); + ClspClass cls = nameMap.get(implClsName); if (cls == null) { missingClasses.add(clsName); return null; @@ -119,15 +146,18 @@ public class ClspGraph { return searchCommonParent(anc, cls); } - private String searchCommonParent(Set anc, NClass cls) { - for (NClass p : cls.getParents()) { - String name = p.getName(); + private String searchCommonParent(Set anc, ClspClass cls) { + for (ArgType p : cls.getParents()) { + String name = p.getObject(); if (anc.contains(name)) { return name; } - String r = searchCommonParent(anc, p); - if (r != null) { - return r; + ClspClass nCls = getClspClass(p); + if (nCls != null) { + String r = searchCommonParent(anc, nCls); + if (r != null) { + return r; + } } } return null; @@ -138,7 +168,7 @@ public class ClspGraph { if (result != null) { return result; } - NClass cls = nameMap.get(clsName); + ClspClass cls = nameMap.get(clsName); if (cls == null) { missingClasses.add(clsName); return Collections.emptySet(); @@ -152,15 +182,32 @@ public class ClspGraph { return result; } - private void addAncestorsNames(NClass cls, Set result) { + private void addAncestorsNames(ClspClass cls, Set result) { boolean isNew = result.add(cls.getName()); if (isNew) { - for (NClass p : cls.getParents()) { - addAncestorsNames(p, result); + for (ArgType parentType : cls.getParents()) { + if (parentType == null) { + continue; + } + ClspClass parentCls = getClspClass(parentType); + if (parentCls != null) { + addAncestorsNames(parentCls, result); + } } } } + @Nullable + private ClspClass getClspClass(ArgType clsType) { + ClspClass clspClass = nameMap.get(clsType.getObject()); + if (clspClass == null) { + if (LOG.isDebugEnabled()) { + LOG.debug("External class not found: {}", clsType.getObject()); + } + } + return clspClass; + } + public void printMissingClasses() { int count = missingClasses.size(); if (count == 0) { diff --git a/jadx-core/src/main/java/jadx/core/clsp/ClspMethod.java b/jadx-core/src/main/java/jadx/core/clsp/ClspMethod.java new file mode 100644 index 000000000..250adeeb6 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/clsp/ClspMethod.java @@ -0,0 +1,122 @@ +package jadx.core.clsp; + +import java.util.List; +import java.util.Objects; + +import org.jetbrains.annotations.NotNull; + +import jadx.core.dex.info.MethodInfo; +import jadx.core.dex.instructions.args.ArgType; +import jadx.core.dex.nodes.GenericTypeParameter; +import jadx.core.dex.nodes.IMethodDetails; +import jadx.core.utils.Utils; + +/** + * Method node in classpath graph. + */ +public class ClspMethod implements IMethodDetails, Comparable { + + private final MethodInfo methodInfo; + private final List argTypes; + private final ArgType returnType; + private final List typeParameters; + private final List throwList; + private final boolean varArg; + + public ClspMethod(MethodInfo methodInfo, + List argTypes, ArgType returnType, + List typeParameters, + boolean varArgs, List throwList) { + this.methodInfo = methodInfo; + this.argTypes = argTypes; + this.returnType = returnType; + this.typeParameters = typeParameters; + this.throwList = throwList; + this.varArg = varArgs; + } + + @Override + public MethodInfo getMethodInfo() { + return methodInfo; + } + + @Override + public ArgType getReturnType() { + return returnType; + } + + @Override + public List getArgTypes() { + return argTypes; + } + + public boolean containsGenericArgs() { + return !Objects.equals(argTypes, methodInfo.getArgumentsTypes()); + } + + public int getArgsCount() { + return argTypes.size(); + } + + @Override + public List getTypeParameters() { + return typeParameters; + } + + @Override + public List getThrows() { + return throwList; + } + + @Override + public boolean isVarArg() { + return varArg; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ClspMethod)) { + return false; + } + ClspMethod other = (ClspMethod) o; + return methodInfo.equals(other.methodInfo); + } + + @Override + public int hashCode() { + return methodInfo.hashCode(); + } + + @Override + public int compareTo(@NotNull ClspMethod other) { + return this.methodInfo.compareTo(other.methodInfo); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("ClspMth{"); + if (Utils.notEmpty(getTypeParameters())) { + sb.append('<'); + sb.append(Utils.listToString(getTypeParameters())); + sb.append("> "); + } + sb.append(getMethodInfo().getFullName()); + sb.append('('); + sb.append(Utils.listToString(getArgTypes())); + sb.append("):"); + sb.append(getReturnType()); + if (isVarArg()) { + sb.append(" VARARG"); + } + List throwsList = getThrows(); + if (Utils.notEmpty(throwsList)) { + sb.append(" throws ").append(Utils.listToString(throwsList)); + } + sb.append('}'); + return sb.toString(); + } +} diff --git a/jadx-core/src/main/java/jadx/core/clsp/ConvertToClsSet.java b/jadx-core/src/main/java/jadx/core/clsp/ConvertToClsSet.java index adf4addb2..95ae11ac2 100644 --- a/jadx-core/src/main/java/jadx/core/clsp/ConvertToClsSet.java +++ b/jadx-core/src/main/java/jadx/core/clsp/ConvertToClsSet.java @@ -5,6 +5,7 @@ import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.EnumSet; import java.util.List; import org.slf4j.Logger; @@ -45,13 +46,15 @@ public class ConvertToClsSet { LOG.info("Loaded: {}", inputFile.getFile()); } - RootNode root = new RootNode(new JadxArgs()); + JadxArgs jadxArgs = new JadxArgs(); + jadxArgs.setRenameFlags(EnumSet.noneOf(JadxArgs.RenameEnum.class)); + RootNode root = new RootNode(jadxArgs); root.load(inputFiles); - ClsSet set = new ClsSet(); + ClsSet set = new ClsSet(root); set.loadFrom(root); set.save(output); - LOG.info("Output: {}", output); + LOG.info("Output: {}, file size: {}B", output, output.toFile().length()); LOG.info("done"); } diff --git a/jadx-core/src/main/java/jadx/core/clsp/NClass.java b/jadx-core/src/main/java/jadx/core/clsp/NClass.java deleted file mode 100644 index 08dfac6e8..000000000 --- a/jadx-core/src/main/java/jadx/core/clsp/NClass.java +++ /dev/null @@ -1,96 +0,0 @@ -package jadx.core.clsp; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -import jadx.core.dex.nodes.GenericInfo; - -/** - * Class node in classpath graph - */ -public class NClass { - - private final String name; - private final int id; - private NClass[] parents; - private Map methodsMap = Collections.emptyMap(); - private List generics = Collections.emptyList(); - - public NClass(String name, int id) { - this.name = name; - this.id = id; - } - - public String getName() { - return name; - } - - public int getId() { - return id; - } - - public NClass[] getParents() { - return parents; - } - - public void setParents(NClass[] parents) { - this.parents = parents; - } - - public Map getMethodsMap() { - return methodsMap; - } - - public List getMethodsList() { - List list = new ArrayList<>(methodsMap.size()); - list.addAll(methodsMap.values()); - Collections.sort(list); - return list; - } - - public void setMethodsMap(Map methodsMap) { - this.methodsMap = Objects.requireNonNull(methodsMap); - } - - public void setMethods(List methods) { - Map map = new HashMap<>(methods.size()); - for (NMethod mth : methods) { - map.put(mth.getShortId(), mth); - } - setMethodsMap(map); - } - - public List getGenerics() { - return generics; - } - - public void setGenerics(List generics) { - this.generics = generics; - } - - @Override - public int hashCode() { - return name.hashCode(); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - NClass nClass = (NClass) o; - return name.equals(nClass.name); - } - - @Override - public String toString() { - return name; - } -} diff --git a/jadx-core/src/main/java/jadx/core/clsp/NMethod.java b/jadx-core/src/main/java/jadx/core/clsp/NMethod.java deleted file mode 100644 index 70cf7c084..000000000 --- a/jadx-core/src/main/java/jadx/core/clsp/NMethod.java +++ /dev/null @@ -1,92 +0,0 @@ -package jadx.core.clsp; - -import java.util.Arrays; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import jadx.core.dex.instructions.args.ArgType; - -/** - * Generic method node in classpath graph. - */ -public class NMethod implements Comparable { - - private final String shortId; - - /** - * Array contains only generic args, others set to 'null', size can be less than total args count - */ - @Nullable - private final ArgType[] genericArgs; - - @Nullable - private final ArgType retType; - - private final boolean varArgs; - - public NMethod(String shortId, @Nullable ArgType[] genericArgs, @Nullable ArgType retType, boolean varArgs) { - this.shortId = shortId; - this.genericArgs = genericArgs; - this.retType = retType; - this.varArgs = varArgs; - } - - public String getShortId() { - return shortId; - } - - @Nullable - public ArgType[] getGenericArgs() { - return genericArgs; - } - - @Nullable - public ArgType getGenericArg(int i) { - ArgType[] args = this.genericArgs; - if (args != null && i < args.length) { - return args[i]; - } - return null; - } - - @Nullable - public ArgType getReturnType() { - return retType; - } - - public boolean isVarArgs() { - return varArgs; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (!(o instanceof NMethod)) { - return false; - } - NMethod other = (NMethod) o; - return shortId.equals(other.shortId); - } - - @Override - public int hashCode() { - return shortId.hashCode(); - } - - @Override - public int compareTo(@NotNull NMethod other) { - return this.shortId.compareTo(other.shortId); - } - - @Override - public String toString() { - return "NMethod{'" + shortId + '\'' - + ", argTypes=" + Arrays.toString(genericArgs) - + ", retType=" + retType - + ", varArgs=" + varArgs - + '}'; - } -} diff --git a/jadx-core/src/main/java/jadx/core/clsp/SimpleMethodDetails.java b/jadx-core/src/main/java/jadx/core/clsp/SimpleMethodDetails.java new file mode 100644 index 000000000..d1ada832c --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/clsp/SimpleMethodDetails.java @@ -0,0 +1,53 @@ +package jadx.core.clsp; + +import java.util.Collections; +import java.util.List; + +import jadx.core.dex.info.MethodInfo; +import jadx.core.dex.instructions.args.ArgType; +import jadx.core.dex.nodes.GenericTypeParameter; +import jadx.core.dex.nodes.IMethodDetails; + +public class SimpleMethodDetails implements IMethodDetails { + + private final MethodInfo methodInfo; + + public SimpleMethodDetails(MethodInfo methodInfo) { + this.methodInfo = methodInfo; + } + + @Override + public MethodInfo getMethodInfo() { + return methodInfo; + } + + @Override + public ArgType getReturnType() { + return methodInfo.getReturnType(); + } + + @Override + public List getArgTypes() { + return methodInfo.getArgumentsTypes(); + } + + @Override + public List getTypeParameters() { + return Collections.emptyList(); + } + + @Override + public List getThrows() { + return Collections.emptyList(); + } + + @Override + public boolean isVarArg() { + return false; + } + + @Override + public String toString() { + return "SimpleMethodDetails{" + methodInfo + '}'; + } +} diff --git a/jadx-core/src/main/java/jadx/core/codegen/AnnotationGen.java b/jadx-core/src/main/java/jadx/core/codegen/AnnotationGen.java index 7bcf11ab5..01f4f7889 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/AnnotationGen.java +++ b/jadx-core/src/main/java/jadx/core/codegen/AnnotationGen.java @@ -113,13 +113,11 @@ public class AnnotationGen { return paramName; } - @SuppressWarnings("unchecked") public void addThrows(MethodNode mth, CodeWriter code) { - Annotation an = mth.getAnnotation(Consts.DALVIK_THROWS); - if (an != null) { - Object exs = an.getDefaultValue(); + List throwList = mth.getThrows(); + if (!throwList.isEmpty()) { code.add(" throws "); - for (Iterator it = ((List) exs).iterator(); it.hasNext();) { + for (Iterator it = throwList.iterator(); it.hasNext();) { ArgType ex = it.next(); classGen.useType(code, ex); if (it.hasNext()) { diff --git a/jadx-core/src/main/java/jadx/core/codegen/ClassGen.java b/jadx-core/src/main/java/jadx/core/codegen/ClassGen.java index 4a83d5c35..a35b7e7ef 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/ClassGen.java +++ b/jadx-core/src/main/java/jadx/core/codegen/ClassGen.java @@ -29,7 +29,7 @@ import jadx.core.dex.instructions.mods.ConstructorInsn; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.DexNode; import jadx.core.dex.nodes.FieldNode; -import jadx.core.dex.nodes.GenericInfo; +import jadx.core.dex.nodes.GenericTypeParameter; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.parser.FieldInitAttr; @@ -149,7 +149,7 @@ public class ClassGen { clsCode.attachDefinition(cls); clsCode.add(cls.getClassInfo().getAliasShortName()); - addGenericMap(clsCode, cls.getGenerics(), true); + addGenericTypeParameters(clsCode, cls.getGenericTypeParameters(), true); clsCode.add(' '); ArgType sup = cls.getSuperClass(); @@ -180,17 +180,17 @@ public class ClassGen { } } - public boolean addGenericMap(CodeWriter code, List generics, boolean classDeclaration) { + public boolean addGenericTypeParameters(CodeWriter code, List generics, boolean classDeclaration) { if (generics == null || generics.isEmpty()) { return false; } code.add('<'); int i = 0; - for (GenericInfo genericInfo : generics) { + for (GenericTypeParameter genericInfo : generics) { if (i != 0) { code.add(", "); } - ArgType type = genericInfo.getGenericType(); + ArgType type = genericInfo.getTypeVariable(); if (type.isGenericType()) { code.add(type.getObject()); } else { @@ -223,10 +223,25 @@ public class ClassGen { } public void addClassBody(CodeWriter clsCode) throws CodegenException { + addClassBody(clsCode, false); + } + + /** + * + * @param clsCode + * @param printClassName allows to print the original class name as comment (e.g. for inlined + * classes) + * @throws CodegenException + */ + public void addClassBody(CodeWriter clsCode, boolean printClassName) throws CodegenException { clsCode.add('{'); setBodyGenStarted(true); clsDeclLine = clsCode.getLine(); clsCode.incIndent(); + if (printClassName) { + clsCode.startLine(); + clsCode.add("/* class " + cls.getFullName() + " */"); + } addFields(clsCode); addInnerClsAndMethods(clsCode); clsCode.decIndent(); diff --git a/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java b/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java index 92219107a..e6830d4e8 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java +++ b/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java @@ -23,7 +23,7 @@ import jadx.core.dex.info.FieldInfo; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.ArithNode; import jadx.core.dex.instructions.ArithOp; -import jadx.core.dex.instructions.CallMthInterface; +import jadx.core.dex.instructions.BaseInvokeNode; import jadx.core.dex.instructions.ConstClassNode; import jadx.core.dex.instructions.ConstStringNode; import jadx.core.dex.instructions.FillArrayNode; @@ -53,7 +53,6 @@ import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.RegionUtils; -import jadx.core.utils.TypeUtils; import jadx.core.utils.exceptions.CodegenException; import jadx.core.utils.exceptions.JadxRuntimeException; @@ -607,6 +606,7 @@ public class InsnGen { if (cls != null && cls.isAnonymous() && !fallback) { cls.ensureProcessed(); inlineAnonymousConstructor(code, cls, insn); + mth.getParentClass().addInlinedClass(cls); return; } if (insn.isSelf()) { @@ -620,7 +620,7 @@ public class InsnGen { code.add("new "); useClass(code, insn.getClassType()); ArgType argType = insn.getResult().getSVar().getCodeVar().getType(); - boolean genericCls = cls == null || !cls.getGenerics().isEmpty(); + boolean genericCls = cls == null || !cls.getGenericTypeParameters().isEmpty(); if (argType != null && argType.getGenericTypes() != null && genericCls) { @@ -675,7 +675,7 @@ public class InsnGen { MethodNode callMth = mth.dex().resolveMethod(insn.getCallMth()); generateMethodArguments(code, insn, 0, callMth); code.add(' '); - new ClassGen(cls, mgen.getClassGen().getParentGen()).addClassBody(code); + new ClassGen(cls, mgen.getClassGen().getParentGen()).addClassBody(code, true); } private void makeInvoke(InvokeNode insn, CodeWriter code) throws CodegenException { @@ -760,30 +760,31 @@ public class InsnGen { return useCls.getParentClass().getClassInfo(); } - void generateMethodArguments(CodeWriter code, InsnNode insn, int startArgNum, - @Nullable MethodNode callMth) throws CodegenException { + void generateMethodArguments(CodeWriter code, BaseInvokeNode insn, int startArgNum, + @Nullable MethodNode mthNode) throws CodegenException { int k = startArgNum; - if (callMth != null && callMth.contains(AFlag.SKIP_FIRST_ARG)) { + if (mthNode != null && mthNode.contains(AFlag.SKIP_FIRST_ARG)) { k++; } int argsCount = insn.getArgsCount(); code.add('('); boolean firstArg = true; if (k < argsCount) { - boolean overloaded = callMth != null && callMth.isArgsOverload(); for (int i = k; i < argsCount; i++) { InsnArg arg = insn.getArg(i); if (arg.contains(AFlag.SKIP_ARG)) { continue; } - if (SkipMethodArgsAttr.isSkip(callMth, i - startArgNum)) { + int argOrigPos = i - startArgNum; + if (SkipMethodArgsAttr.isSkip(mthNode, argOrigPos)) { continue; } if (!firstArg) { code.add(", "); + } else { + firstArg = false; } - boolean cast = addArgCast(code, insn, callMth, arg, i - startArgNum, overloaded); - if (!cast && i == argsCount - 1 && processVarArg(code, callMth, arg)) { + if (i == argsCount - 1 && processVarArg(code, insn, arg)) { continue; } addArg(code, arg, false); @@ -793,91 +794,29 @@ public class InsnGen { code.add(')'); } - /** - * Add additional cast for method argument. - */ - private boolean addArgCast(CodeWriter code, InsnNode insn, @Nullable MethodNode callMth, - InsnArg arg, int origPos, boolean overloaded) { - ArgType castType = null; - if (callMth != null) { - List argTypes = callMth.getArgTypes(); - ArgType origType = argTypes.get(origPos); - if (origType.isGenericType() && !callMth.getParentClass().equals(mth.getParentClass())) { - // cancel cast - return false; - } - if (insn instanceof CallMthInterface && origType.containsGenericType()) { - ArgType clsType; - CallMthInterface mthCall = (CallMthInterface) insn; - RegisterArg instanceArg = mthCall.getInstanceArg(); - if (instanceArg != null) { - clsType = instanceArg.getType(); - } else { - clsType = mthCall.getCallMth().getDeclClass().getType(); - } - ArgType replacedType = TypeUtils.replaceClassGenerics(root, clsType, origType); - if (replacedType != null) { - castType = replacedType; - } - if (castType == null) { - ArgType invReplType = TypeUtils.replaceMethodGenerics(root, insn, origType); - if (invReplType != null) { - castType = invReplType; - } - } - } - if (castType == null) { - castType = origType; - } - } else { - castType = arg.getType(); - } - // TODO: check castType for left type variables - - if (isCastNeeded(arg, castType, overloaded)) { - code.add('('); - useType(code, castType); - code.add(") "); - return true; - } - return false; - } - - private boolean isCastNeeded(InsnArg arg, ArgType origType, boolean overloaded) { - ArgType argType = arg.getType(); - if (arg.isLiteral() && ((LiteralArg) arg).getLiteral() == 0 - && (argType.isObject() || argType.isArray())) { - return true; - } - if (argType.equals(origType)) { - return false; - } - return overloaded; - } - /** * Expand varArgs from filled array. */ - private boolean processVarArg(CodeWriter code, MethodNode callMth, InsnArg lastArg) throws CodegenException { - if (callMth == null || !callMth.getAccessFlags().isVarArgs()) { + private boolean processVarArg(CodeWriter code, BaseInvokeNode invokeInsn, InsnArg lastArg) throws CodegenException { + if (!invokeInsn.contains(AFlag.VARARG_CALL)) { return false; } if (!lastArg.getType().isArray() || !lastArg.isInsnWrap()) { return false; } InsnNode insn = ((InsnWrapArg) lastArg).getWrapInsn(); - if (insn.getType() == InsnType.FILLED_NEW_ARRAY) { - int count = insn.getArgsCount(); - for (int i = 0; i < count; i++) { - InsnArg elemArg = insn.getArg(i); - addArg(code, elemArg, false); - if (i < count - 1) { - code.add(", "); - } - } - return true; + if (insn.getType() != InsnType.FILLED_NEW_ARRAY) { + return false; } - return false; + int count = insn.getArgsCount(); + for (int i = 0; i < count; i++) { + InsnArg elemArg = insn.getArg(i); + addArg(code, elemArg, false); + if (i < count - 1) { + code.add(", "); + } + } + return true; } private boolean inlineMethod(MethodNode callMthNode, InvokeNode insn, CodeWriter code) throws CodegenException { @@ -935,7 +874,7 @@ public class InsnGen { if (parentInsn.contains(AFlag.WRAPPED)) { return false; } - return !callMthNode.getReturnType().equals(ArgType.VOID); + return !callMthNode.isVoidReturn(); } private void makeTernary(TernaryInsn insn, CodeWriter code, Set state) throws CodegenException { diff --git a/jadx-core/src/main/java/jadx/core/codegen/MethodGen.java b/jadx-core/src/main/java/jadx/core/codegen/MethodGen.java index b9ee51edb..09d4ed00f 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/MethodGen.java +++ b/jadx-core/src/main/java/jadx/core/codegen/MethodGen.java @@ -98,8 +98,12 @@ public class MethodGen { if (Consts.DEBUG) { code.add(mth.isVirtual() ? "/* virtual */ " : "/* direct */ "); } + if (clsAccFlags.isInterface() && !mth.isNoCode()) { + // add 'default' for method with code in interface + code.add("default "); + } - if (classGen.addGenericMap(code, mth.getGenerics(), false)) { + if (classGen.addGenericTypeParameters(code, mth.getTypeParameters(), false)) { code.add(' '); } if (ai.isConstructor()) { diff --git a/jadx-core/src/main/java/jadx/core/codegen/RegionGen.java b/jadx-core/src/main/java/jadx/core/codegen/RegionGen.java index 4596d8c48..59069d679 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/RegionGen.java +++ b/jadx-core/src/main/java/jadx/core/codegen/RegionGen.java @@ -275,37 +275,44 @@ public class RegionGen extends InsnGen { List keys = caseInfo.getKeys(); IContainer c = caseInfo.getContainer(); for (Object k : keys) { - code.startLine("case "); - if (k instanceof FieldNode) { - FieldNode fn = (FieldNode) k; - if (fn.getParentClass().isEnum()) { - code.add(fn.getAlias()); - } else { - staticField(code, fn.getFieldInfo()); - // print original value, sometimes replace with incorrect field - FieldInitAttr valueAttr = fn.get(AType.FIELD_INIT); - if (valueAttr != null && valueAttr.getValue() != null) { - code.add(" /*").add(valueAttr.getValue().toString()).add("*/"); - } - } - } else if (k instanceof Integer) { - code.add(TypeGen.literalToString((Integer) k, arg.getType(), mth, fallback)); + if (k == SwitchRegion.DEFAULT_CASE_KEY) { + code.startLine("default:"); } else { - throw new JadxRuntimeException("Unexpected key in switch: " + (k != null ? k.getClass() : null)); + code.startLine("case "); + addCaseKey(code, arg, k); + code.add(':'); } - code.add(':'); } makeRegionIndent(code, c); } - if (sw.getDefaultCase() != null) { - code.startLine("default:"); - makeRegionIndent(code, sw.getDefaultCase()); - } code.decIndent(); code.startLine('}'); return code; } + private void addCaseKey(CodeWriter code, InsnArg arg, Object k) { + if (k instanceof FieldNode) { + FieldNode fn = (FieldNode) k; + if (fn.getParentClass().isEnum()) { + code.add(fn.getAlias()); + } else { + staticField(code, fn.getFieldInfo()); + // print original value, sometimes replaced with incorrect field + FieldInitAttr valueAttr = fn.get(AType.FIELD_INIT); + if (valueAttr != null) { + Object value = valueAttr.getValue(); + if (value != null && valueAttr.getValueType() == FieldInitAttr.InitType.CONST) { + code.add(" /*").add(value.toString()).add("*/"); + } + } + } + } else if (k instanceof Integer) { + code.add(TypeGen.literalToString((Integer) k, arg.getType(), mth, fallback)); + } else { + throw new JadxRuntimeException("Unexpected key in switch: " + (k != null ? k.getClass() : null)); + } + } + private void makeTryCatch(TryCatchRegion region, CodeWriter code) throws CodegenException { code.startLine("try {"); makeRegionIndent(code, region.getTryRegion()); diff --git a/jadx-core/src/main/java/jadx/core/deobf/Deobfuscator.java b/jadx-core/src/main/java/jadx/core/deobf/Deobfuscator.java index 56a355697..54fc89373 100644 --- a/jadx-core/src/main/java/jadx/core/deobf/Deobfuscator.java +++ b/jadx-core/src/main/java/jadx/core/deobf/Deobfuscator.java @@ -37,6 +37,9 @@ public class Deobfuscator { public static final String CLASS_NAME_SEPARATOR = "."; public static final String INNER_CLASS_SEPARATOR = "$"; + public static final String KOTLIN_METADATA_ANNOTATION = "kotlin.Metadata"; + public static final String KOTLIN_METADATA_D2_PARAMETER = "d2"; + public static final String KOTLIN_METADATA_CLASSNAME_REGEX = "(L.*;)"; private final JadxArgs args; @NotNull @@ -57,6 +60,7 @@ public class Deobfuscator { private final int maxLength; private final int minLength; private final boolean useSourceNameAsAlias; + private final boolean parseKotlinMetadata; private int pkgIndex = 0; private int clsIndex = 0; @@ -70,6 +74,7 @@ public class Deobfuscator { this.minLength = args.getDeobfuscationMinLength(); this.maxLength = args.getDeobfuscationMaxLength(); this.useSourceNameAsAlias = args.isUseSourceNameAsClassAlias(); + this.parseKotlinMetadata = args.isParseKotlinMetadata(); this.deobfPresets = new DeobfPresets(this, deobfMapFile); } @@ -392,6 +397,18 @@ public class Deobfuscator { private String makeClsAlias(ClassNode cls) { ClassInfo classInfo = cls.getClassInfo(); + + String metadataClassName = ""; + String metadataPackageName = ""; + if (this.parseKotlinMetadata) { + String rawClassName = getRawClassNameFromMetadata(cls); + if (rawClassName != null) { + metadataClassName = rawClassName.substring(rawClassName.lastIndexOf(".") + 1, rawClassName.length() - 1); + if (rawClassName.lastIndexOf(".") != -1) { + metadataPackageName = rawClassName.substring(1, rawClassName.lastIndexOf(".")); + } + } + } String alias = null; if (this.useSourceNameAsAlias) { @@ -399,14 +416,40 @@ public class Deobfuscator { } if (alias == null) { - String clsName = classInfo.getShortName(); - alias = String.format("C%04d%s", clsIndex++, prepareNamePart(clsName)); + if (metadataClassName.isEmpty()) { + String clsName = classInfo.getShortName(); + alias = String.format("C%04d%s", clsIndex++, prepareNamePart(clsName)); + } else { + alias = metadataClassName; + } + } + PackageNode pkg; + if (metadataPackageName.isEmpty()) { + pkg = getPackageNode(classInfo.getPackage(), true); + } else { + pkg = getPackageNode(metadataPackageName, true); } - PackageNode pkg = getPackageNode(classInfo.getPackage(), true); clsMap.put(classInfo, new DeobfClsInfo(this, cls, pkg, alias)); return alias; } + @Nullable + private String getRawClassNameFromMetadata(ClassNode cls) { + if (cls.getAnnotation(KOTLIN_METADATA_ANNOTATION) != null + && cls.getAnnotation(KOTLIN_METADATA_ANNOTATION).getValues().get(KOTLIN_METADATA_D2_PARAMETER) != null + && cls.getAnnotation(KOTLIN_METADATA_ANNOTATION).getValues().get(KOTLIN_METADATA_D2_PARAMETER) instanceof List) { + Object rawClassNameObject = + ((List) cls.getAnnotation(KOTLIN_METADATA_ANNOTATION).getValues().get(KOTLIN_METADATA_D2_PARAMETER)).get(0); + if (rawClassNameObject instanceof String) { + String rawClassName = ((String) rawClassNameObject).trim().replace("/", "."); + if (rawClassName.length() > 1 && rawClassName.matches(KOTLIN_METADATA_CLASSNAME_REGEX)) { + return rawClassName; + } + } + } + return null; + } + @Nullable private String getAliasFromSourceFile(ClassNode cls) { SourceFileAttr sourceFileAttr = cls.get(AType.SOURCE_FILE); diff --git a/jadx-core/src/main/java/jadx/core/dex/attributes/AFlag.java b/jadx-core/src/main/java/jadx/core/dex/attributes/AFlag.java index 69348444e..85d54f45c 100644 --- a/jadx-core/src/main/java/jadx/core/dex/attributes/AFlag.java +++ b/jadx-core/src/main/java/jadx/core/dex/attributes/AFlag.java @@ -33,6 +33,7 @@ public enum AFlag { ANONYMOUS_CLASS, THIS, + SUPER, /** * RegisterArg attribute for method arguments @@ -60,6 +61,7 @@ public enum AFlag { FALL_THROUGH, EXPLICIT_GENERICS, + VARARG_CALL, /** * Use constants with explicit type: cast '(byte) 1' or type letter '7L' diff --git a/jadx-core/src/main/java/jadx/core/dex/attributes/AType.java b/jadx-core/src/main/java/jadx/core/dex/attributes/AType.java index 2cdab5b73..00d018af4 100644 --- a/jadx-core/src/main/java/jadx/core/dex/attributes/AType.java +++ b/jadx-core/src/main/java/jadx/core/dex/attributes/AType.java @@ -24,6 +24,7 @@ import jadx.core.dex.attributes.nodes.RegDebugInfoAttr; import jadx.core.dex.attributes.nodes.RenameReasonAttr; import jadx.core.dex.attributes.nodes.SkipMethodArgsAttr; import jadx.core.dex.attributes.nodes.SourceFileAttr; +import jadx.core.dex.nodes.IMethodDetails; import jadx.core.dex.nodes.parser.FieldInitAttr; import jadx.core.dex.trycatch.CatchAttr; import jadx.core.dex.trycatch.ExcHandlerAttr; @@ -79,6 +80,7 @@ public class AType { // instruction public static final AType LOOP_LABEL = new AType<>(); public static final AType> JUMP = new AType<>(); + public static final AType METHOD_DETAILS = new AType<>(); // register public static final AType REG_DEBUG_INFO = new AType<>(); diff --git a/jadx-core/src/main/java/jadx/core/dex/attributes/AttrList.java b/jadx-core/src/main/java/jadx/core/dex/attributes/AttrList.java index d378b7e3e..86e18ffa4 100644 --- a/jadx-core/src/main/java/jadx/core/dex/attributes/AttrList.java +++ b/jadx-core/src/main/java/jadx/core/dex/attributes/AttrList.java @@ -3,6 +3,7 @@ package jadx.core.dex.attributes; import java.util.ArrayList; import java.util.List; +import jadx.core.codegen.CodeWriter; import jadx.core.utils.Utils; public class AttrList implements IAttribute { @@ -25,6 +26,6 @@ public class AttrList implements IAttribute { @Override public String toString() { - return Utils.listToString(list, "\n"); + return Utils.listToString(list, CodeWriter.NL); } } diff --git a/jadx-core/src/main/java/jadx/core/dex/attributes/AttributeStorage.java b/jadx-core/src/main/java/jadx/core/dex/attributes/AttributeStorage.java index 789bac206..2d342b9d1 100644 --- a/jadx-core/src/main/java/jadx/core/dex/attributes/AttributeStorage.java +++ b/jadx-core/src/main/java/jadx/core/dex/attributes/AttributeStorage.java @@ -11,6 +11,7 @@ import java.util.Set; import jadx.core.dex.attributes.annotations.Annotation; import jadx.core.dex.attributes.annotations.AnnotationsList; import jadx.core.utils.Utils; +import jadx.core.utils.exceptions.JadxRuntimeException; /** * Storage for different attribute types: @@ -19,6 +20,13 @@ import jadx.core.utils.Utils; */ public class AttributeStorage { + static { + int flagsCount = AFlag.values().length; + if (flagsCount >= 64) { + throw new JadxRuntimeException("Try to reduce flags count to 64 for use one long in EnumSet, now " + flagsCount); + } + } + private final Set flags; private Map, IAttribute> attributes; @@ -127,7 +135,7 @@ public class AttributeStorage { list.add(a.toString()); } for (IAttribute a : attributes.values()) { - list.add(a.toString()); + list.add(a.toAttrString()); } return list; } diff --git a/jadx-core/src/main/java/jadx/core/dex/attributes/IAttribute.java b/jadx-core/src/main/java/jadx/core/dex/attributes/IAttribute.java index cac361c26..9eb94d8e5 100644 --- a/jadx-core/src/main/java/jadx/core/dex/attributes/IAttribute.java +++ b/jadx-core/src/main/java/jadx/core/dex/attributes/IAttribute.java @@ -2,4 +2,8 @@ package jadx.core.dex.attributes; public interface IAttribute { AType getType(); + + default String toAttrString() { + return this.toString(); + } } diff --git a/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/JadxError.java b/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/JadxError.java index d42cfdd0f..6b8667516 100644 --- a/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/JadxError.java +++ b/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/JadxError.java @@ -4,6 +4,7 @@ import java.util.Objects; import org.jetbrains.annotations.NotNull; +import jadx.core.codegen.CodeWriter; import jadx.core.utils.Utils; public class JadxError implements Comparable { @@ -58,7 +59,7 @@ public class JadxError implements Comparable { str.append(cause.getClass()); str.append(':'); str.append(cause.getMessage()); - str.append('\n'); + str.append(CodeWriter.NL); str.append(Utils.getStackTrace(cause)); } return str.toString(); diff --git a/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/LocalVarsDebugInfoAttr.java b/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/LocalVarsDebugInfoAttr.java index c4efe9235..ad2966e84 100644 --- a/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/LocalVarsDebugInfoAttr.java +++ b/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/LocalVarsDebugInfoAttr.java @@ -7,6 +7,8 @@ import jadx.core.dex.attributes.IAttribute; import jadx.core.dex.visitors.debuginfo.LocalVar; import jadx.core.utils.Utils; +import static jadx.core.codegen.CodeWriter.NL; + public class LocalVarsDebugInfoAttr implements IAttribute { private final List localVars; @@ -25,6 +27,6 @@ public class LocalVarsDebugInfoAttr implements IAttribute { @Override public String toString() { - return "Debug Info:\n " + Utils.listToString(localVars, "\n "); + return "Debug Info:" + NL + " " + Utils.listToString(localVars, NL + " "); } } diff --git a/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/PhiListAttr.java b/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/PhiListAttr.java index cba4c9010..d4bf9f34a 100644 --- a/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/PhiListAttr.java +++ b/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/PhiListAttr.java @@ -7,6 +7,8 @@ import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.IAttribute; import jadx.core.dex.instructions.PhiInsn; +import static com.google.common.base.Ascii.NL; + public class PhiListAttr implements IAttribute { private final List list = new LinkedList<>(); @@ -28,7 +30,7 @@ public class PhiListAttr implements IAttribute { sb.append('r').append(phiInsn.getResult().getRegNum()).append(' '); } for (PhiInsn phiInsn : list) { - sb.append("\n ").append(phiInsn).append(' ').append(phiInsn.getAttributesString()); + sb.append(NL).append(" ").append(phiInsn).append(' ').append(phiInsn.getAttributesString()); } return sb.toString(); } diff --git a/jadx-core/src/main/java/jadx/core/dex/info/ClassInfo.java b/jadx-core/src/main/java/jadx/core/dex/info/ClassInfo.java index 7e9858cb4..16dd8bf08 100644 --- a/jadx-core/src/main/java/jadx/core/dex/info/ClassInfo.java +++ b/jadx-core/src/main/java/jadx/core/dex/info/ClassInfo.java @@ -39,7 +39,7 @@ public final class ClassInfo implements Comparable { public static ClassInfo fromDex(DexNode dex, int clsIndex) { if (clsIndex == DexNode.NO_INDEX) { - return null; + throw new JadxRuntimeException("NO_INDEX for class"); } return fromType(dex.root(), dex.getType(clsIndex)); } diff --git a/jadx-core/src/main/java/jadx/core/dex/info/InfoStorage.java b/jadx-core/src/main/java/jadx/core/dex/info/InfoStorage.java index 80e4e8f36..b0a79a5a2 100644 --- a/jadx-core/src/main/java/jadx/core/dex/info/InfoStorage.java +++ b/jadx-core/src/main/java/jadx/core/dex/info/InfoStorage.java @@ -5,12 +5,15 @@ import java.util.Map; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.DexNode; +import jadx.core.utils.exceptions.JadxRuntimeException; public class InfoStorage { private final Map classes = new HashMap<>(); - private final Map methods = new HashMap<>(); private final Map fields = new HashMap<>(); + private final Map methods = new HashMap<>(); + // use only one MethodInfo instance + private final Map uniqueMethods = new HashMap<>(); public ClassInfo getCls(ArgType type) { return classes.get(type); @@ -23,18 +26,35 @@ public class InfoStorage { } } - private int generateMethodLookupId(DexNode dex, int mthId) { + private static int generateMethodLookupId(DexNode dex, int mthId) { return dex.getDexId() << 16 | mthId; } public MethodInfo getMethod(DexNode dex, int mtdId) { - return methods.get(generateMethodLookupId(dex, mtdId)); + synchronized (methods) { + return methods.get(generateMethodLookupId(dex, mtdId)); + } } - public MethodInfo putMethod(DexNode dex, int mthId, MethodInfo mth) { + public MethodInfo putMethod(DexNode dex, int mthId, MethodInfo methodInfo) { synchronized (methods) { - MethodInfo prev = methods.put(generateMethodLookupId(dex, mthId), mth); - return prev == null ? mth : prev; + MethodInfo uniqueMethodInfo = putMethod(methodInfo); + MethodInfo prev = methods.put(generateMethodLookupId(dex, mthId), uniqueMethodInfo); + if (prev != null && prev != uniqueMethodInfo) { + throw new JadxRuntimeException("Method lookup id collision: " + methodInfo + ", " + prev + ", " + uniqueMethodInfo); + } + return uniqueMethodInfo; + } + } + + public MethodInfo putMethod(MethodInfo newMth) { + synchronized (uniqueMethods) { + MethodInfo prev = uniqueMethods.get(newMth); + if (prev != null) { + return prev; + } + uniqueMethods.put(newMth, newMth); + return newMth; } } diff --git a/jadx-core/src/main/java/jadx/core/dex/info/MethodInfo.java b/jadx-core/src/main/java/jadx/core/dex/info/MethodInfo.java index a37c45dbc..f8058cb0f 100644 --- a/jadx-core/src/main/java/jadx/core/dex/info/MethodInfo.java +++ b/jadx-core/src/main/java/jadx/core/dex/info/MethodInfo.java @@ -1,6 +1,9 @@ package jadx.core.dex.info; import java.util.List; +import java.util.Objects; + +import org.jetbrains.annotations.Nullable; import com.android.dex.MethodId; import com.android.dex.ProtoId; @@ -8,52 +11,48 @@ import com.android.dex.ProtoId; import jadx.core.codegen.TypeGen; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.DexNode; +import jadx.core.dex.nodes.RootNode; import jadx.core.utils.Utils; -public final class MethodInfo { +public final class MethodInfo implements Comparable { private final String name; private final ArgType retType; - private final List args; + private final List argTypes; private final ClassInfo declClass; private final String shortId; private String alias; private boolean aliasFromPreset; - private MethodInfo(DexNode dex, int mthIndex) { - MethodId mthId = dex.getMethodId(mthIndex); - name = dex.getString(mthId.getNameIndex()); - alias = name; - aliasFromPreset = false; - declClass = ClassInfo.fromDex(dex, mthId.getDeclaringClassIndex()); - - ProtoId proto = dex.getProtoId(mthId.getProtoIndex()); - retType = dex.getType(proto.getReturnTypeIndex()); - args = dex.readParamList(proto.getParametersOffset()); - shortId = makeSignature(true); - } - private MethodInfo(ClassInfo declClass, String name, List args, ArgType retType) { this.name = name; this.alias = name; this.aliasFromPreset = false; this.declClass = declClass; - this.args = args; + this.argTypes = args; this.retType = retType; - this.shortId = makeSignature(true); - } - - public static MethodInfo externalMth(ClassInfo declClass, String name, List args, ArgType retType) { - return new MethodInfo(declClass, name, args, retType); + this.shortId = makeShortId(name, argTypes, retType); } public static MethodInfo fromDex(DexNode dex, int mthIndex) { - MethodInfo mth = dex.root().getInfoStorage().getMethod(dex, mthIndex); - if (mth != null) { - return mth; + MethodInfo storageMth = dex.root().getInfoStorage().getMethod(dex, mthIndex); + if (storageMth != null) { + return storageMth; } - mth = new MethodInfo(dex, mthIndex); - return dex.root().getInfoStorage().putMethod(dex, mthIndex, mth); + MethodId mthId = dex.getMethodId(mthIndex); + String mthName = dex.getString(mthId.getNameIndex()); + ClassInfo parentClass = ClassInfo.fromDex(dex, mthId.getDeclaringClassIndex()); + + ProtoId proto = dex.getProtoId(mthId.getProtoIndex()); + ArgType returnType = dex.getType(proto.getReturnTypeIndex()); + List args = dex.readParamList(proto.getParametersOffset()); + MethodInfo newMth = new MethodInfo(parentClass, mthName, args, returnType); + return dex.root().getInfoStorage().putMethod(dex, mthIndex, newMth); + } + + public static MethodInfo fromDetails(RootNode rootNode, ClassInfo declClass, String name, List args, ArgType retType) { + MethodInfo newMth = new MethodInfo(declClass, name, args, retType); + return rootNode.getInfoStorage().putMethod(newMth); } public String makeSignature(boolean includeRetType) { @@ -61,17 +60,29 @@ public final class MethodInfo { } public String makeSignature(boolean useAlias, boolean includeRetType) { - StringBuilder signature = new StringBuilder(); - signature.append(useAlias ? alias : name); - signature.append('('); - for (ArgType arg : args) { - signature.append(TypeGen.signature(arg)); + return makeShortId(useAlias ? alias : name, + argTypes, + includeRetType ? retType : null); + } + + public static String makeShortId(String name, List argTypes, @Nullable ArgType retType) { + StringBuilder sb = new StringBuilder(); + sb.append(name); + sb.append('('); + for (ArgType arg : argTypes) { + sb.append(TypeGen.signature(arg)); } - signature.append(')'); - if (includeRetType) { - signature.append(TypeGen.signature(retType)); + sb.append(')'); + if (retType != null) { + sb.append(TypeGen.signature(retType)); } - return signature.toString(); + return sb.toString(); + } + + public boolean isOverloadedBy(MethodInfo otherMthInfo) { + return argTypes.size() == otherMthInfo.argTypes.size() + && name.equals(otherMthInfo.name) + && !Objects.equals(this.shortId, otherMthInfo.shortId); } public String getName() { @@ -106,11 +117,11 @@ public final class MethodInfo { } public List getArgumentsTypes() { - return args; + return argTypes; } public int getArgsCount() { - return args.size(); + return argTypes.size(); } public boolean isConstructor() { @@ -143,10 +154,7 @@ public final class MethodInfo { @Override public int hashCode() { - int result = declClass.hashCode(); - result = 31 * result + retType.hashCode(); - result = 31 * result + shortId.hashCode(); - return result; + return shortId.hashCode() + 31 * declClass.hashCode(); } @Override @@ -159,13 +167,21 @@ public final class MethodInfo { } MethodInfo other = (MethodInfo) obj; return shortId.equals(other.shortId) - && retType.equals(other.retType) && declClass.equals(other.declClass); } + @Override + public int compareTo(MethodInfo other) { + int clsCmp = declClass.compareTo(other.declClass); + if (clsCmp != 0) { + return clsCmp; + } + return shortId.compareTo(other.shortId); + } + @Override public String toString() { return declClass.getFullName() + '.' + name - + '(' + Utils.listToString(args) + "):" + retType; + + '(' + Utils.listToString(argTypes) + "):" + retType; } } diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/BaseInvokeNode.java b/jadx-core/src/main/java/jadx/core/dex/instructions/BaseInvokeNode.java new file mode 100644 index 000000000..02234e741 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/instructions/BaseInvokeNode.java @@ -0,0 +1,25 @@ +package jadx.core.dex.instructions; + +import org.jetbrains.annotations.Nullable; + +import jadx.core.dex.info.MethodInfo; +import jadx.core.dex.instructions.args.InsnArg; +import jadx.core.dex.nodes.InsnNode; + +public abstract class BaseInvokeNode extends InsnNode { + public BaseInvokeNode(InsnType type, int argsCount) { + super(type, argsCount); + } + + public abstract MethodInfo getCallMth(); + + @Nullable + public abstract InsnArg getInstanceArg(); + + public abstract boolean isStaticCall(); + + /** + * Return offset to match method args from {@link #getCallMth()} + */ + public abstract int getFirstArgOffset(); +} diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/CallMthInterface.java b/jadx-core/src/main/java/jadx/core/dex/instructions/CallMthInterface.java deleted file mode 100644 index 823ca0e22..000000000 --- a/jadx-core/src/main/java/jadx/core/dex/instructions/CallMthInterface.java +++ /dev/null @@ -1,19 +0,0 @@ -package jadx.core.dex.instructions; - -import org.jetbrains.annotations.Nullable; - -import jadx.core.dex.info.MethodInfo; -import jadx.core.dex.instructions.args.RegisterArg; - -public interface CallMthInterface { - - MethodInfo getCallMth(); - - @Nullable - RegisterArg getInstanceArg(); - - /** - * Return offset to match method args from {@link #getCallMth()} - */ - int getFirstArgOffset(); -} diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/InsnDecoder.java b/jadx-core/src/main/java/jadx/core/dex/instructions/InsnDecoder.java index ace669f08..67b318a6a 100644 --- a/jadx-core/src/main/java/jadx/core/dex/instructions/InsnDecoder.java +++ b/jadx-core/src/main/java/jadx/core/dex/instructions/InsnDecoder.java @@ -623,7 +623,7 @@ public class InsnDecoder { targets[i] = targets[i] - payloadOffset + offset; } int nextOffset = getNextInsnOffset(insnArr, offset); - return new SwitchNode(InsnArg.reg(insn, 0, ArgType.NARROW), keys, targets, nextOffset); + return new SwitchNode(InsnArg.reg(insn, 0, ArgType.NARROW), keys, targets, nextOffset, packed); } private InsnNode fillArray(DecodedInstruction insn) { diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/InvokeNode.java b/jadx-core/src/main/java/jadx/core/dex/instructions/InvokeNode.java index f0099e76f..9b850c919 100644 --- a/jadx-core/src/main/java/jadx/core/dex/instructions/InvokeNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/instructions/InvokeNode.java @@ -7,11 +7,10 @@ import com.android.dx.io.instructions.DecodedInstruction; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; -import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.InsnNode; import jadx.core.utils.InsnUtils; -public class InvokeNode extends InsnNode implements CallMthInterface { +public final class InvokeNode extends BaseInvokeNode { private final InvokeType type; private final MethodInfo mth; @@ -55,17 +54,18 @@ public class InvokeNode extends InsnNode implements CallMthInterface { @Override @Nullable - public RegisterArg getInstanceArg() { + public InsnArg getInstanceArg() { if (type != InvokeType.STATIC && getArgsCount() > 0) { - InsnArg firstArg = getArg(0); - if (firstArg.isRegister()) { - return ((RegisterArg) firstArg); - } + return getArg(0); } return null; } @Override + public boolean isStaticCall() { + return type == InvokeType.STATIC; + } + public int getFirstArgOffset() { return type == InvokeType.STATIC ? 0 : 1; } @@ -89,6 +89,6 @@ public class InvokeNode extends InsnNode implements CallMthInterface { @Override public String toString() { - return super.toString() + ' ' + mth + " type: " + type; + return super.toString() + " type: " + type + " call: " + mth; } } diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/SwitchNode.java b/jadx-core/src/main/java/jadx/core/dex/instructions/SwitchNode.java index 641b45ac0..ed4f79076 100644 --- a/jadx-core/src/main/java/jadx/core/dex/instructions/SwitchNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/instructions/SwitchNode.java @@ -16,20 +16,22 @@ public class SwitchNode extends TargetInsnNode { private final Object[] keys; private final int[] targets; private final int def; // next instruction + private final boolean packed; // type of switch insn, if true can contain filler keys private BlockNode[] targetBlocks; private BlockNode defTargetBlock; - public SwitchNode(InsnArg arg, Object[] keys, int[] targets, int def) { - this(keys, targets, def); + public SwitchNode(InsnArg arg, Object[] keys, int[] targets, int def, boolean packed) { + this(keys, targets, def, packed); addArg(arg); } - private SwitchNode(Object[] keys, int[] targets, int def) { + private SwitchNode(Object[] keys, int[] targets, int def, boolean packed) { super(InsnType.SWITCH, 1); this.keys = keys; this.targets = targets; this.def = def; + this.packed = packed; } public int getCasesCount() { @@ -48,6 +50,10 @@ public class SwitchNode extends TargetInsnNode { return def; } + public boolean isPacked() { + return packed; + } + public BlockNode[] getTargetBlocks() { return targetBlocks; } @@ -103,7 +109,7 @@ public class SwitchNode extends TargetInsnNode { @Override public InsnNode copy() { - SwitchNode copy = new SwitchNode(keys, targets, def); + SwitchNode copy = new SwitchNode(keys, targets, def, packed); copy.targetBlocks = targetBlocks; copy.defTargetBlock = defTargetBlock; return copyCommonParams(copy); @@ -114,9 +120,13 @@ public class SwitchNode extends TargetInsnNode { StringBuilder sb = new StringBuilder(); sb.append(super.toString()); for (int i = 0; i < targets.length; i++) { - sb.append(" case ").append(keys[i]) - .append(": goto ").append(InsnUtils.formatOffset(targets[i])); sb.append(CodeWriter.NL); + sb.append(" case ").append(keys[i]); + sb.append(": goto ").append(InsnUtils.formatOffset(targets[i])); + } + if (def != -1) { + sb.append(CodeWriter.NL); + sb.append(" default: goto ").append(InsnUtils.formatOffset(def)); } return sb.toString(); } diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/args/ArgType.java b/jadx-core/src/main/java/jadx/core/dex/instructions/args/ArgType.java index 783e147d5..fe6fff83d 100644 --- a/jadx-core/src/main/java/jadx/core/dex/instructions/args/ArgType.java +++ b/jadx-core/src/main/java/jadx/core/dex/instructions/args/ArgType.java @@ -10,7 +10,6 @@ import jadx.core.dex.info.ClassInfo; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.DexNode; import jadx.core.dex.nodes.RootNode; -import jadx.core.dex.nodes.parser.SignatureParser; import jadx.core.utils.Utils; public abstract class ArgType { @@ -24,12 +23,13 @@ public abstract class ArgType { public static final ArgType LONG = primitive(PrimitiveType.LONG); public static final ArgType VOID = primitive(PrimitiveType.VOID); - public static final ArgType OBJECT = object(Consts.CLASS_OBJECT); - public static final ArgType CLASS = object(Consts.CLASS_CLASS); - public static final ArgType STRING = object(Consts.CLASS_STRING); - public static final ArgType ENUM = object(Consts.CLASS_ENUM); - public static final ArgType THROWABLE = object(Consts.CLASS_THROWABLE); + public static final ArgType OBJECT = objectNoCache(Consts.CLASS_OBJECT); + public static final ArgType CLASS = objectNoCache(Consts.CLASS_CLASS); + public static final ArgType STRING = objectNoCache(Consts.CLASS_STRING); + public static final ArgType ENUM = objectNoCache(Consts.CLASS_ENUM); + public static final ArgType THROWABLE = objectNoCache(Consts.CLASS_THROWABLE); public static final ArgType OBJECT_ARRAY = array(OBJECT); + public static final ArgType WILDCARD = wildcard(); public static final ArgType UNKNOWN = unknown(PrimitiveType.values()); public static final ArgType UNKNOWN_OBJECT = unknown(PrimitiveType.OBJECT, PrimitiveType.ARRAY); @@ -66,10 +66,25 @@ public abstract class ArgType { return new PrimitiveArg(stype); } - public static ArgType object(String obj) { + private static ArgType objectNoCache(String obj) { return new ObjectType(obj); } + public static ArgType object(String obj) { + // TODO: add caching + String cleanObjectName = Utils.cleanObjectName(obj); + switch (cleanObjectName) { + case Consts.CLASS_OBJECT: + return OBJECT; + case Consts.CLASS_STRING: + return STRING; + case Consts.CLASS_CLASS: + return CLASS; + default: + return new ObjectType(cleanObjectName); + } + } + public static ArgType genericType(String type) { return new GenericType(type); } @@ -82,12 +97,16 @@ public abstract class ArgType { return new WildcardType(obj, bound); } - public static ArgType parseGenericSignature(String sign) { - return new SignatureParser(sign).consumeType(); + public static ArgType generic(ArgType obj, ArgType... generics) { + if (!obj.isObject()) { + throw new IllegalArgumentException("Expected Object as ArgType, got: " + obj); + } + return new GenericObject(obj.getObject(), generics); } public static ArgType generic(String obj, ArgType... generics) { - return new GenericObject(obj, generics); + String cleanObjectName = Utils.cleanObjectName(obj); + return new GenericObject(cleanObjectName, generics); } public static ArgType outerGeneric(ArgType genericOuterType, ArgType innerType) { @@ -160,7 +179,7 @@ public abstract class ArgType { protected final String objName; public ObjectType(String obj) { - this.objName = Utils.cleanObjectName(obj); + this.objName = obj; this.hash = objName.hashCode(); } @@ -555,12 +574,12 @@ public abstract class ArgType { public abstract PrimitiveType[] getPossibleTypes(); - public static boolean isCastNeeded(DexNode dex, ArgType from, ArgType to) { + public static boolean isCastNeeded(RootNode root, ArgType from, ArgType to) { if (from.equals(to)) { return false; } if (from.isObject() && to.isObject() - && dex.root().getClsp().isImplements(from.getObject(), to.getObject())) { + && root.getClsp().isImplements(from.getObject(), to.getObject())) { return false; } return true; @@ -679,25 +698,44 @@ public abstract class ArgType { return 1; } - public boolean containsGenericType() { + public boolean containsGeneric() { + if (isGeneric() || isGenericType()) { + return true; + } + if (isArray()) { + ArgType arrayElement = getArrayElement(); + if (arrayElement != null) { + return arrayElement.containsGeneric(); + } + } + return false; + } + + public boolean containsTypeVariable() { if (isGenericType()) { return true; } ArgType wildcardType = getWildcardType(); if (wildcardType != null) { - return wildcardType.containsGenericType(); + return wildcardType.containsTypeVariable(); } if (isGeneric()) { ArgType[] genericTypes = getGenericTypes(); if (genericTypes != null) { for (ArgType genericType : genericTypes) { - if (genericType.containsGenericType()) { + if (genericType.containsTypeVariable()) { return true; } } } return false; } + if (isArray()) { + ArgType arrayElement = getArrayElement(); + if (arrayElement != null) { + return arrayElement.containsTypeVariable(); + } + } return false; } diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/args/InsnArg.java b/jadx-core/src/main/java/jadx/core/dex/instructions/args/InsnArg.java index 869a50bb1..cf35cc5eb 100644 --- a/jadx-core/src/main/java/jadx/core/dex/instructions/args/InsnArg.java +++ b/jadx-core/src/main/java/jadx/core/dex/instructions/args/InsnArg.java @@ -65,6 +65,7 @@ public abstract class InsnArg extends Typed { } private static InsnWrapArg wrap(InsnNode insn) { + insn.add(AFlag.WRAPPED); return new InsnWrapArg(insn); } @@ -140,9 +141,17 @@ public abstract class InsnArg extends Typed { InsnArg arg; InsnType type = insn.getType(); if (type == InsnType.CONST || type == InsnType.MOVE) { - arg = insn.getArg(0); - insn.add(AFlag.REMOVE); - insn.add(AFlag.DONT_GENERATE); + if (insn.contains(AFlag.FORCE_ASSIGN_INLINE)) { + RegisterArg resArg = insn.getResult(); + arg = wrap(insn); + if (resArg != null) { + arg.setType(resArg.getType()); + } + } else { + arg = insn.getArg(0); + insn.add(AFlag.REMOVE); + insn.add(AFlag.DONT_GENERATE); + } } else { arg = wrapArg(insn); } @@ -156,8 +165,6 @@ public abstract class InsnArg extends Typed { public static InsnArg wrapArg(InsnNode insn) { RegisterArg resArg = insn.getResult(); InsnArg arg = wrap(insn); - insn.add(AFlag.WRAPPED); - switch (insn.getType()) { case CONST: case MOVE: diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/args/RegisterArg.java b/jadx-core/src/main/java/jadx/core/dex/instructions/args/RegisterArg.java index 6434a3f71..6b8ada9d4 100644 --- a/jadx-core/src/main/java/jadx/core/dex/instructions/args/RegisterArg.java +++ b/jadx-core/src/main/java/jadx/core/dex/instructions/args/RegisterArg.java @@ -11,6 +11,7 @@ import jadx.core.utils.exceptions.JadxRuntimeException; public class RegisterArg extends InsnArg implements Named { public static final String THIS_ARG_NAME = "this"; + public static final String SUPER_ARG_NAME = "super"; protected final int regNum; // not null after SSATransform pass @@ -87,6 +88,9 @@ public class RegisterArg extends InsnArg implements Named { @Override public String getName() { + if (isSuper()) { + return SUPER_ARG_NAME; + } if (isThis()) { return THIS_ARG_NAME; } @@ -96,6 +100,10 @@ public class RegisterArg extends InsnArg implements Named { return sVar.getName(); } + private boolean isSuper() { + return contains(AFlag.SUPER); + } + @Override public void setName(String name) { if (sVar != null && name != null) { diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/mods/ConstructorInsn.java b/jadx-core/src/main/java/jadx/core/dex/instructions/mods/ConstructorInsn.java index 7a590cbc1..7a900d340 100644 --- a/jadx-core/src/main/java/jadx/core/dex/instructions/mods/ConstructorInsn.java +++ b/jadx-core/src/main/java/jadx/core/dex/instructions/mods/ConstructorInsn.java @@ -4,14 +4,14 @@ import org.jetbrains.annotations.Nullable; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.info.MethodInfo; -import jadx.core.dex.instructions.CallMthInterface; +import jadx.core.dex.instructions.BaseInvokeNode; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.InvokeNode; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; -public final class ConstructorInsn extends InsnNode implements CallMthInterface { +public final class ConstructorInsn extends BaseInvokeNode { private final MethodInfo callMth; private final CallType callType; @@ -59,6 +59,7 @@ public final class ConstructorInsn extends InsnNode implements CallMthInterface this.callType = callType; } + @Override public MethodInfo getCallMth() { return callMth; } @@ -93,6 +94,11 @@ public final class ConstructorInsn extends InsnNode implements CallMthInterface return callType == CallType.SELF; } + @Override + public boolean isStaticCall() { + return false; + } + @Override public int getFirstArgOffset() { return 0; 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 51e6cc486..5064b1e53 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 @@ -4,8 +4,10 @@ import java.io.StringWriter; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Set; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @@ -54,12 +56,14 @@ public class ClassNode extends LineAttrNode implements ILoadable, ICodeNode { private AccessInfo accessFlags; private ArgType superClass; private List interfaces; - private List generics = Collections.emptyList(); + private List generics = Collections.emptyList(); private List methods; private List fields; private List innerClasses = Collections.emptyList(); + private List inlinedClasses = Collections.emptyList(); + // store smali private String smali; // store parent for inner classes or 'this' otherwise @@ -83,6 +87,10 @@ public class ClassNode extends LineAttrNode implements ILoadable, ICodeNode { try { if (cls.getSupertypeIndex() == DexNode.NO_INDEX) { this.superClass = null; + // only java.lang.Object don't have super class + if (!clsInfo.getType().getObject().equals(Consts.CLASS_OBJECT)) { + throw new JadxRuntimeException("No super class in " + clsInfo.getType()); + } } else { this.superClass = dex.getType(cls.getSupertypeIndex()); } @@ -178,6 +186,7 @@ public class ClassNode extends LineAttrNode implements ILoadable, ICodeNode { private void loadStaticValues(ClassDef cls, List staticFields) throws DecodeException { for (FieldNode f : staticFields) { if (f.getAccessFlags().isFinal()) { + // incorrect initialization will be removed if assign found in constructor f.addAttr(FieldInitAttr.NULL_VALUE); } } @@ -200,7 +209,7 @@ public class ClassNode extends LineAttrNode implements ILoadable, ICodeNode { } try { // parse class generic map - generics = sp.consumeGenericMap(); + generics = sp.consumeGenericTypeParameters(); // parse super class signature superClass = sp.consumeType(); // parse interfaces signatures @@ -360,7 +369,7 @@ public class ClassNode extends LineAttrNode implements ILoadable, ICodeNode { return interfaces; } - public List getGenerics() { + public List getGenericTypeParameters() { return generics; } @@ -481,6 +490,24 @@ public class ClassNode extends LineAttrNode implements ILoadable, ICodeNode { return innerClasses; } + /** + * Get all inner and inlined classes recursively + * + * @param resultClassesSet all identified inner and inlined classes are added to this set + */ + public void getInnerAndInlinedClassesRecursive(Set resultClassesSet) { + for (ClassNode innerCls : innerClasses) { + if (resultClassesSet.add(innerCls)) { + innerCls.getInnerAndInlinedClassesRecursive(resultClassesSet); + } + } + for (ClassNode inlinedCls : inlinedClasses) { + if (resultClassesSet.add(inlinedCls)) { + inlinedCls.getInnerAndInlinedClassesRecursive(resultClassesSet); + } + } + } + public void addInnerClass(ClassNode cls) { if (innerClasses.isEmpty()) { innerClasses = new ArrayList<>(5); @@ -489,6 +516,13 @@ public class ClassNode extends LineAttrNode implements ILoadable, ICodeNode { cls.parentClass = this; } + public void addInlinedClass(ClassNode cls) { + if (inlinedClasses.isEmpty()) { + inlinedClasses = new ArrayList<>(5); + } + inlinedClasses.add(cls); + } + public boolean isEnum() { return getAccessFlags().isEnum() && getSuperClass() != null @@ -567,7 +601,9 @@ public class ClassNode extends LineAttrNode implements ILoadable, ICodeNode { StringWriter stringWriter = new StringWriter(4096); getSmali(this, stringWriter); stringWriter.append(System.lineSeparator()); - for (ClassNode innerClass : innerClasses) { + Set allInlinedClasses = new LinkedHashSet<>(); + getInnerAndInlinedClassesRecursive(allInlinedClasses); + for (ClassNode innerClass : allInlinedClasses) { getSmali(innerClass, stringWriter); stringWriter.append(System.lineSeparator()); } diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/GenericInfo.java b/jadx-core/src/main/java/jadx/core/dex/nodes/GenericInfo.java deleted file mode 100644 index e70e5649b..000000000 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/GenericInfo.java +++ /dev/null @@ -1,46 +0,0 @@ -package jadx.core.dex.nodes; - -import java.util.List; - -import jadx.core.dex.instructions.args.ArgType; - -public class GenericInfo { - private final ArgType genericType; - private final List extendsList; - - public GenericInfo(ArgType genericType, List extendsList) { - this.genericType = genericType; - this.extendsList = extendsList; - } - - public ArgType getGenericType() { - return genericType; - } - - public List getExtendsList() { - return extendsList; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - GenericInfo other = (GenericInfo) o; - return genericType.equals(other.genericType) - && extendsList.equals(other.extendsList); - } - - @Override - public int hashCode() { - return 31 * genericType.hashCode() + extendsList.hashCode(); - } - - @Override - public String toString() { - return "GenericInfo{" + genericType + " extends: " + extendsList + '}'; - } -} diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/GenericTypeParameter.java b/jadx-core/src/main/java/jadx/core/dex/nodes/GenericTypeParameter.java new file mode 100644 index 000000000..61f974755 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/GenericTypeParameter.java @@ -0,0 +1,55 @@ +package jadx.core.dex.nodes; + +import java.util.List; + +import jadx.core.dex.instructions.args.ArgType; +import jadx.core.utils.Utils; + +import static jadx.core.utils.Utils.notEmpty; + +public class GenericTypeParameter { + private final ArgType typeVariable; + private final List extendsList; + + public GenericTypeParameter(ArgType typeVariable, List extendsList) { + this.typeVariable = typeVariable; + this.extendsList = extendsList; + } + + public ArgType getTypeVariable() { + return typeVariable; + } + + public List getExtendsList() { + return extendsList; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + GenericTypeParameter other = (GenericTypeParameter) o; + return typeVariable.equals(other.typeVariable) + && extendsList.equals(other.extendsList); + } + + @Override + public int hashCode() { + return 31 * typeVariable.hashCode() + extendsList.hashCode(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(typeVariable); + if (notEmpty(extendsList)) { + sb.append(" extends "); + sb.append(Utils.listToString(extendsList, " & ")); + } + return sb.toString(); + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/IMethodDetails.java b/jadx-core/src/main/java/jadx/core/dex/nodes/IMethodDetails.java new file mode 100644 index 000000000..eaf04388f --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/IMethodDetails.java @@ -0,0 +1,52 @@ +package jadx.core.dex.nodes; + +import java.util.List; + +import jadx.core.dex.attributes.AType; +import jadx.core.dex.attributes.IAttribute; +import jadx.core.dex.info.MethodInfo; +import jadx.core.dex.instructions.args.ArgType; +import jadx.core.utils.Utils; + +public interface IMethodDetails extends IAttribute { + + MethodInfo getMethodInfo(); + + ArgType getReturnType(); + + List getArgTypes(); + + List getTypeParameters(); + + List getThrows(); + + boolean isVarArg(); + + @Override + default AType getType() { + return AType.METHOD_DETAILS; + } + + @Override + default String toAttrString() { + StringBuilder sb = new StringBuilder(); + sb.append("MD:"); + if (Utils.notEmpty(getTypeParameters())) { + sb.append('<'); + sb.append(Utils.listToString(getTypeParameters())); + sb.append(">:"); + } + sb.append('('); + sb.append(Utils.listToString(getArgTypes())); + sb.append("):"); + sb.append(getReturnType()); + if (isVarArg()) { + sb.append(" VARARG"); + } + List throwsList = getThrows(); + if (Utils.notEmpty(throwsList)) { + sb.append(" throws ").append(Utils.listToString(throwsList)); + } + return sb.toString(); + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/InsnNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/InsnNode.java index 703c824c5..58012dc4b 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/InsnNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/InsnNode.java @@ -10,6 +10,7 @@ import org.jetbrains.annotations.Nullable; import com.android.dx.io.instructions.DecodedInstruction; +import jadx.core.codegen.CodeWriter; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.nodes.LineAttrNode; import jadx.core.dex.instructions.InsnType; @@ -404,13 +405,13 @@ public class InsnNode extends LineAttrNode { protected void appendArgs(StringBuilder sb) { String argsStr = Utils.listToString(arguments); - if (argsStr.length() < 60) { + if (argsStr.length() < 120) { sb.append(argsStr); } else { // wrap args - String separator = "\n "; + String separator = CodeWriter.NL + " "; sb.append(separator).append(Utils.listToString(arguments, separator)); - sb.append('\n'); + sb.append(CodeWriter.NL); } } diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/MethodNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/MethodNode.java index 5006bb30e..8f0184f48 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/MethodNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/MethodNode.java @@ -4,6 +4,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Set; import org.jetbrains.annotations.NotNull; @@ -16,8 +17,10 @@ import com.android.dex.Code; import com.android.dex.Code.CatchHandler; import com.android.dex.Code.Try; +import jadx.core.Consts; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; +import jadx.core.dex.attributes.annotations.Annotation; import jadx.core.dex.attributes.nodes.JumpInfo; import jadx.core.dex.attributes.nodes.LineAttrNode; import jadx.core.dex.attributes.nodes.LoopInfo; @@ -46,7 +49,7 @@ import jadx.core.utils.exceptions.JadxRuntimeException; import static jadx.core.utils.Utils.lockList; -public class MethodNode extends LineAttrNode implements ILoadable, ICodeNode { +public class MethodNode extends LineAttrNode implements IMethodDetails, ILoadable, ICodeNode { private static final Logger LOG = LoggerFactory.getLogger(MethodNode.class); private final MethodInfo mthInfo; @@ -66,7 +69,7 @@ public class MethodNode extends LineAttrNode implements ILoadable, ICodeNode { // additional info available after load, keep on unload private ArgType retType; private List argTypes; - private List generics; + private List typeParameters; // decompilation data, reset on unload private RegisterArg thisArg; @@ -96,7 +99,7 @@ public class MethodNode extends LineAttrNode implements ILoadable, ICodeNode { if (noCode) { return; } - // don't unload retType, argTypes, generics + // don't unload retType, argTypes, typeParameters thisArg = null; argsList = null; sVars = Collections.emptyList(); @@ -199,7 +202,7 @@ public class MethodNode extends LineAttrNode implements ILoadable, ICodeNode { return null; } try { - this.generics = sp.consumeGenericMap(); + this.typeParameters = sp.consumeGenericTypeParameters(); List argsTypes = sp.consumeMethodArgs(); this.retType = sp.consumeType(); @@ -217,7 +220,7 @@ public class MethodNode extends LineAttrNode implements ILoadable, ICodeNode { } return argsTypes; } catch (Exception e) { - addWarningComment("Failed to parse method signature: " + sp.getSignature(), e); + addWarnComment("Failed to parse method signature: " + sp.getSignature(), e); return null; } } @@ -273,17 +276,32 @@ public class MethodNode extends LineAttrNode implements ILoadable, ICodeNode { } } + @Override @NotNull public List getArgTypes() { if (argTypes == null) { - throw new JadxRuntimeException("Method types not initialized: " + this); + throw new JadxRuntimeException("Method generic types not initialized: " + this); } return argTypes; } + public boolean containsGenericArgs() { + return !Objects.equals(mthInfo.getArgumentsTypes(), getArgTypes()); + } + + @Override + @NotNull + public ArgType getReturnType() { + return retType; + } + + public boolean isVoidReturn() { + return mthInfo.getReturnType().equals(ArgType.VOID); + } + public List getArgRegs() { if (argsList == null) { - throw new JadxRuntimeException("Method args not loaded: " + this + throw new JadxRuntimeException("Method arg registers not loaded: " + this + ", class status: " + parentClass.getTopParentClass().getState()); } return argsList; @@ -309,12 +327,9 @@ public class MethodNode extends LineAttrNode implements ILoadable, ICodeNode { this.add(AFlag.SKIP_FIRST_ARG); } - public ArgType getReturnType() { - return retType; - } - - public List getGenerics() { - return generics; + @Override + public List getTypeParameters() { + return typeParameters; } private static void initTryCatches(MethodNode mth, Code mthCode, InsnNode[] insnByOffset) { @@ -584,25 +599,31 @@ public class MethodNode extends LineAttrNode implements ILoadable, ICodeNode { return exceptionHandlers.size(); } + @Override + @SuppressWarnings("unchecked") + public List getThrows() { + Annotation an = getAnnotation(Consts.DALVIK_THROWS); + if (an == null) { + return Collections.emptyList(); + } + return (List) an.getDefaultValue(); + } + /** * Return true if exists method with same name and arguments count */ - public boolean isArgsOverload() { - int argsCount = mthInfo.getArgumentsTypes().size(); - if (argsCount == 0) { - return false; - } - - String name = getName(); + public boolean isArgsOverloaded() { + MethodInfo thisMthInfo = this.mthInfo; + // quick check in current class for (MethodNode method : parentClass.getMethods()) { - MethodInfo otherMthInfo = method.mthInfo; - if (this != method - && otherMthInfo.getArgumentsTypes().size() == argsCount - && otherMthInfo.getName().equals(name)) { + if (method == this) { + continue; + } + if (method.getMethodInfo().isOverloadedBy(thisMthInfo)) { return true; } } - return false; + return root().getMethodUtils().isMethodArgsOverloaded(parentClass.getClassInfo().getType(), thisMthInfo); } public boolean isConstructor() { @@ -707,11 +728,11 @@ public class MethodNode extends LineAttrNode implements ILoadable, ICodeNode { ErrorsCounter.methodWarn(this, warnStr); } - public void addWarningComment(String warn) { - addWarningComment(warn, null); + public void addWarnComment(String warn) { + addWarnComment(warn, null); } - public void addWarningComment(String warn, @Nullable Throwable exc) { + public void addWarnComment(String warn, @Nullable Throwable exc) { String commentStr = "JADX WARN: " + warn; addAttr(AType.COMMENTS, commentStr); if (exc != null) { @@ -730,6 +751,7 @@ public class MethodNode extends LineAttrNode implements ILoadable, ICodeNode { ErrorsCounter.methodError(this, errStr, e); } + @Override public MethodInfo getMethodInfo() { return mthInfo; } @@ -752,6 +774,11 @@ public class MethodNode extends LineAttrNode implements ILoadable, ICodeNode { return -1; } + @Override + public boolean isVarArg() { + return accFlags.isVarArgs(); + } + public boolean isLoaded() { return loaded; } @@ -776,7 +803,7 @@ public class MethodNode extends LineAttrNode implements ILoadable, ICodeNode { @Override public String toString() { return parentClass + "." + mthInfo.getName() - + '(' + Utils.listToString(mthInfo.getArgumentsTypes()) + "):" + + '(' + Utils.listToString(argTypes) + "):" + retType; } } diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java index 781a047d5..49c54759b 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java @@ -16,14 +16,14 @@ import jadx.api.ResourceType; import jadx.api.ResourcesLoader; import jadx.core.Jadx; import jadx.core.clsp.ClspGraph; -import jadx.core.clsp.NClass; -import jadx.core.clsp.NMethod; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.info.ConstStorage; import jadx.core.dex.info.FieldInfo; import jadx.core.dex.info.InfoStorage; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.args.ArgType; +import jadx.core.dex.nodes.utils.MethodUtils; +import jadx.core.dex.nodes.utils.TypeUtils; import jadx.core.dex.visitors.IDexTreeVisitor; import jadx.core.dex.visitors.typeinference.TypeUpdate; import jadx.core.utils.CacheStorage; @@ -48,6 +48,8 @@ public class RootNode { private final InfoStorage infoStorage = new InfoStorage(); private final CacheStorage cacheStorage = new CacheStorage(); private final TypeUpdate typeUpdate; + private final MethodUtils methodUtils; + private final TypeUtils typeUtils; private final ICodeCache codeCache; @@ -65,6 +67,10 @@ public class RootNode { this.constValues = new ConstStorage(args); this.typeUpdate = new TypeUpdate(this); this.codeCache = args.getCodeCache(); + this.methodUtils = new MethodUtils(this); + this.typeUtils = new TypeUtils(this); + + this.dexNodes = Collections.emptyList(); } public void load(List inputFiles) { @@ -119,7 +125,7 @@ public class RootNode { public void initClassPath() { try { if (this.clsp == null) { - ClspGraph newClsp = new ClspGraph(); + ClspGraph newClsp = new ClspGraph(this); newClsp.load(); List classes = new ArrayList<>(); @@ -168,6 +174,26 @@ public class RootNode { return null; } + @Nullable + public ClassNode resolveClass(ArgType clsType) { + if (!clsType.isTypeKnown() || clsType.isGenericType()) { + return null; + } + if (clsType.getWildcardBound() == ArgType.WildcardBound.UNBOUND) { + return null; + } + if (clsType.isGeneric()) { + clsType = ArgType.object(clsType.getObject()); + } + for (DexNode dexNode : dexNodes) { + ClassNode cls = dexNode.resolveClass(clsType); + if (cls != null) { + return cls; + } + } + return null; + } + @Nullable public ClassNode searchClassByName(String fullName) { ClassInfo clsInfo = ClassInfo.fromName(this, fullName); @@ -206,6 +232,10 @@ public class RootNode { if (cls == null) { return null; } + MethodNode methodNode = cls.searchMethod(mth); + if (methodNode != null) { + return methodNode; + } return cls.dex().deepResolveMethod(cls, mth.makeSignature(false)); } @@ -232,60 +262,6 @@ public class RootNode { } } - @Nullable - public ArgType getMethodGenericReturnType(MethodInfo callMth) { - MethodNode methodNode = deepResolveMethod(callMth); - if (methodNode != null) { - ArgType returnType = methodNode.getReturnType(); - if (returnType != null && (returnType.isGeneric() || returnType.isGenericType())) { - return returnType; - } - return null; - } - NMethod methodDetails = clsp.getMethodDetails(callMth); - if (methodDetails != null) { - return methodDetails.getReturnType(); - } - return null; - } - - public List getMethodArgTypes(MethodInfo callMth) { - MethodNode methodNode = deepResolveMethod(callMth); - if (methodNode != null) { - return methodNode.getArgTypes(); - } - NMethod methodDetails = clsp.getMethodDetails(callMth); - if (methodDetails != null && methodDetails.getGenericArgs() != null) { - List argTypes = callMth.getArgumentsTypes(); - int argsCount = argTypes.size(); - List list = new ArrayList<>(argsCount); - for (int i = 0; i < argsCount; i++) { - ArgType genericArgType = methodDetails.getGenericArg(i); - if (genericArgType != null) { - list.add(genericArgType); - } else { - list.add(argTypes.get(i)); - } - } - return list; - } - return Collections.emptyList(); - } - - @NotNull - public List getClassGenerics(ArgType type) { - ClassNode classNode = resolveClass(ClassInfo.fromType(this, type)); - if (classNode != null) { - return classNode.getGenerics(); - } - NClass clsDetails = getClsp().getClsDetails(type); - if (clsDetails == null || clsDetails.getGenerics().isEmpty()) { - return Collections.emptyList(); - } - List generics = clsDetails.getGenerics(); - return generics == null ? Collections.emptyList() : generics; - } - public List getDexNodes() { return dexNodes; } @@ -335,4 +311,11 @@ public class RootNode { return codeCache; } + public MethodUtils getMethodUtils() { + return methodUtils; + } + + public TypeUtils getTypeUtils() { + return typeUtils; + } } diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/parser/SignatureParser.java b/jadx-core/src/main/java/jadx/core/dex/nodes/parser/SignatureParser.java index ebdb3fbaf..4b70dfd7c 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/parser/SignatureParser.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/parser/SignatureParser.java @@ -5,6 +5,7 @@ import java.util.Collections; import java.util.LinkedList; import java.util.List; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -12,7 +13,7 @@ import jadx.core.Consts; import jadx.core.dex.attributes.IAttributeNode; import jadx.core.dex.attributes.annotations.Annotation; import jadx.core.dex.instructions.args.ArgType; -import jadx.core.dex.nodes.GenericInfo; +import jadx.core.dex.nodes.GenericTypeParameter; import jadx.core.utils.exceptions.JadxRuntimeException; public class SignatureParser { @@ -65,32 +66,40 @@ public class SignatureParser { * @return string from 'mark' to current position (not including current character) */ private String slice() { - if (mark >= pos) { + int start = mark == -1 ? 0 : mark; + if (start >= pos) { return ""; } - return sign.substring(mark, pos); + return sign.substring(start, pos); } /** * Inclusive slice (includes current character) */ private String inclusiveSlice() { - if (mark >= pos) { + int start = mark; + if (start == -1) { + start = 0; + } + int last = pos + 1; + if (start >= last) { return ""; } - return sign.substring(mark, pos + 1); + return sign.substring(start, last); } - private boolean forwardTo(char lastChar) { + private boolean skipUntil(char untilChar) { int startPos = pos; - char ch; - while ((ch = next()) != STOP_CHAR) { - if (ch == lastChar) { + while (true) { + if (lookAhead(untilChar)) { return true; } + char ch = next(); + if (ch == STOP_CHAR) { + pos = startPos; + return false; + } } - pos = startPos; - return false; } private void consume(char exp) { @@ -109,14 +118,14 @@ public class SignatureParser { return false; } - private String consumeUntil(char lastChar) { + @Nullable + public String consumeUntil(char lastChar) { mark(); - return forwardTo(lastChar) ? slice() : null; + return skipUntil(lastChar) ? inclusiveSlice() : null; } public ArgType consumeType() { char ch = next(); - mark(); switch (ch) { case 'L': ArgType obj = consumeObjectType(false); @@ -127,10 +136,13 @@ public class SignatureParser { case 'T': next(); mark(); - if (forwardTo(';')) { - return ArgType.genericType(slice()); + String typeVarName = consumeUntil(';'); + if (typeVarName != null) { + consume(';'); + return ArgType.genericType(typeVarName); } break; + case '[': return ArgType.array(consumeType()); @@ -145,7 +157,7 @@ public class SignatureParser { } break; } - throw new JadxRuntimeException("Can't parse type: " + debugString()); + throw new JadxRuntimeException("Can't parse type: " + debugString() + ", unexpected: " + ch); } private ArgType consumeObjectType(boolean incompleteType) { @@ -220,11 +232,11 @@ public class SignatureParser { *

* Example: "" */ - public List consumeGenericMap() { + public List consumeGenericTypeParameters() { if (!lookAhead('<')) { return Collections.emptyList(); } - List list = new ArrayList<>(); + List list = new ArrayList<>(); consume('<'); while (true) { if (lookAhead('>') || next() == STOP_CHAR) { @@ -232,12 +244,13 @@ public class SignatureParser { } String id = consumeUntil(':'); if (id == null) { - LOG.error("Failed to parse generic map: {}", sign); + LOG.error("Failed to parse generic types map: {}", sign); return Collections.emptyList(); } + consume(':'); tryConsume(':'); List types = consumeExtendsTypesList(); - list.add(new GenericInfo(ArgType.genericType(id), types)); + list.add(new GenericTypeParameter(ArgType.genericType(id), types)); } consume('>'); return list; diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/utils/MethodUtils.java b/jadx-core/src/main/java/jadx/core/dex/nodes/utils/MethodUtils.java new file mode 100644 index 000000000..4c7c82e7a --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/utils/MethodUtils.java @@ -0,0 +1,125 @@ +package jadx.core.dex.nodes.utils; + +import java.util.ArrayList; +import java.util.List; + +import org.jetbrains.annotations.Nullable; + +import jadx.core.clsp.ClspClass; +import jadx.core.clsp.ClspMethod; +import jadx.core.dex.attributes.AType; +import jadx.core.dex.info.MethodInfo; +import jadx.core.dex.instructions.BaseInvokeNode; +import jadx.core.dex.instructions.args.ArgType; +import jadx.core.dex.nodes.ClassNode; +import jadx.core.dex.nodes.IMethodDetails; +import jadx.core.dex.nodes.MethodNode; +import jadx.core.dex.nodes.RootNode; + +public class MethodUtils { + private final RootNode root; + + public MethodUtils(RootNode rootNode) { + this.root = rootNode; + } + + @Nullable + public IMethodDetails getMethodDetails(BaseInvokeNode invokeNode) { + IMethodDetails methodDetails = invokeNode.get(AType.METHOD_DETAILS); + if (methodDetails != null) { + return methodDetails; + } + return getMethodDetails(invokeNode.getCallMth()); + } + + @Nullable + public IMethodDetails getMethodDetails(MethodInfo callMth) { + MethodNode mthNode = root.deepResolveMethod(callMth); + if (mthNode != null) { + return mthNode; + } + return root.getClsp().getMethodDetails(callMth); + } + + /** + * Search methods with same name and args count in class hierarchy starting from {@code startCls} + * Beware {@code startCls} can be different from {@code mthInfo.getDeclClass()} + */ + public boolean isMethodArgsOverloaded(ArgType startCls, MethodInfo mthInfo) { + return processMethodArgsOverloaded(startCls, mthInfo, null); + } + + public List collectOverloadedMethods(ArgType startCls, MethodInfo mthInfo) { + List list = new ArrayList<>(); + processMethodArgsOverloaded(startCls, mthInfo, list); + return list; + } + + @Nullable + public ArgType getMethodGenericReturnType(BaseInvokeNode invokeNode) { + IMethodDetails methodDetails = getMethodDetails(invokeNode); + if (methodDetails != null) { + ArgType returnType = methodDetails.getReturnType(); + if (returnType != null && returnType.containsGeneric()) { + return returnType; + } + } + return null; + } + + public boolean processMethodArgsOverloaded(ArgType startCls, MethodInfo mthInfo, @Nullable List collectedMths) { + if (startCls == null || !startCls.isObject()) { + return false; + } + boolean isMthConstructor = mthInfo.isConstructor() || mthInfo.isClassInit(); + ClassNode classNode = root.resolveClass(startCls); + if (classNode != null) { + for (MethodNode mth : classNode.getMethods()) { + if (mthInfo.isOverloadedBy(mth.getMethodInfo())) { + if (collectedMths == null) { + return true; + } + collectedMths.add(mth); + } + } + if (!isMthConstructor) { + if (processMethodArgsOverloaded(classNode.getSuperClass(), mthInfo, collectedMths)) { + if (collectedMths == null) { + return true; + } + } + for (ArgType parentInterface : classNode.getInterfaces()) { + if (processMethodArgsOverloaded(parentInterface, mthInfo, collectedMths)) { + if (collectedMths == null) { + return true; + } + } + } + } + } else { + ClspClass clsDetails = root.getClsp().getClsDetails(startCls); + if (clsDetails == null) { + // class info not available + return false; + } + for (ClspMethod clspMth : clsDetails.getMethodsMap().values()) { + if (mthInfo.isOverloadedBy(clspMth.getMethodInfo())) { + if (collectedMths == null) { + return true; + } + collectedMths.add(clspMth); + } + } + if (!isMthConstructor) { + for (ArgType parent : clsDetails.getParents()) { + if (processMethodArgsOverloaded(parent, mthInfo, collectedMths)) { + if (collectedMths == null) { + return true; + } + } + } + } + } + return false; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/utils/TypeUtils.java b/jadx-core/src/main/java/jadx/core/dex/nodes/utils/TypeUtils.java new file mode 100644 index 000000000..0bb936df0 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/utils/TypeUtils.java @@ -0,0 +1,142 @@ +package jadx.core.dex.nodes.utils; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import jadx.core.clsp.ClspClass; +import jadx.core.dex.instructions.BaseInvokeNode; +import jadx.core.dex.instructions.args.ArgType; +import jadx.core.dex.instructions.args.InsnArg; +import jadx.core.dex.nodes.ClassNode; +import jadx.core.dex.nodes.GenericTypeParameter; +import jadx.core.dex.nodes.IMethodDetails; +import jadx.core.dex.nodes.RootNode; + +public class TypeUtils { + private final RootNode root; + + public TypeUtils(RootNode rootNode) { + this.root = rootNode; + } + + @NotNull + public List getClassGenerics(ArgType type) { + ClassNode classNode = root.resolveClass(type); + if (classNode != null) { + return classNode.getGenericTypeParameters(); + } + ClspClass clsDetails = root.getClsp().getClsDetails(type); + if (clsDetails == null || clsDetails.getTypeParameters().isEmpty()) { + return Collections.emptyList(); + } + List generics = clsDetails.getTypeParameters(); + return generics == null ? Collections.emptyList() : generics; + } + + /** + * Replace generic types in {@code typeWithGeneric} using instance types + *
+ * Example: + *

    + *
  • {@code instanceType: Set} + *
  • {@code typeWithGeneric: Iterator} + *
  • {@code return: Iterator} + *
+ */ + @Nullable + public ArgType replaceClassGenerics(ArgType instanceType, ArgType typeWithGeneric) { + if (typeWithGeneric != null) { + Map replaceMap = getTypeVariablesMapping(instanceType); + if (!replaceMap.isEmpty()) { + return replaceTypeVariablesUsingMap(typeWithGeneric, replaceMap); + } + } + return null; + } + + public Map getTypeVariablesMapping(ArgType clsType) { + if (!clsType.isGeneric()) { + return Collections.emptyMap(); + } + + List typeParameters = root.getTypeUtils().getClassGenerics(clsType); + if (typeParameters.isEmpty()) { + return Collections.emptyMap(); + } + ArgType[] actualTypes = clsType.getGenericTypes(); + if (actualTypes == null) { + return Collections.emptyMap(); + } + int genericParamsCount = actualTypes.length; + if (genericParamsCount != typeParameters.size()) { + return Collections.emptyMap(); + } + Map replaceMap = new HashMap<>(genericParamsCount); + for (int i = 0; i < genericParamsCount; i++) { + ArgType actualType = actualTypes[i]; + ArgType genericType = typeParameters.get(i).getTypeVariable(); + replaceMap.put(genericType, actualType); + } + return replaceMap; + } + + @Nullable + public ArgType replaceMethodGenerics(BaseInvokeNode invokeInsn, IMethodDetails details, ArgType typeWithGeneric) { + if (typeWithGeneric == null) { + return null; + } + List methodArgTypes = details.getArgTypes(); + if (methodArgTypes.isEmpty()) { + return null; + } + int firstArgOffset = invokeInsn.getFirstArgOffset(); + int argsCount = methodArgTypes.size(); + for (int i = 0; i < argsCount; i++) { + ArgType methodArgType = methodArgTypes.get(i); + InsnArg insnArg = invokeInsn.getArg(i + firstArgOffset); + ArgType insnType = insnArg.getType(); + if (methodArgType.equals(typeWithGeneric)) { + return insnType; + } + } + // TODO build complete map for type variables + return null; + } + + @Nullable + public ArgType replaceTypeVariablesUsingMap(ArgType replaceType, Map replaceMap) { + if (replaceType.isGenericType()) { + return replaceMap.get(replaceType); + } + + ArgType wildcardType = replaceType.getWildcardType(); + if (wildcardType != null && wildcardType.containsTypeVariable()) { + ArgType newWildcardType = replaceTypeVariablesUsingMap(wildcardType, replaceMap); + if (newWildcardType == null) { + return null; + } + return ArgType.wildcard(newWildcardType, replaceType.getWildcardBound()); + } + + ArgType[] genericTypes = replaceType.getGenericTypes(); + if (replaceType.isGeneric() && genericTypes != null && genericTypes.length != 0) { + int size = genericTypes.length; + ArgType[] newTypes = new ArgType[size]; + for (int i = 0; i < size; i++) { + ArgType genericType = genericTypes[i]; + ArgType type = replaceTypeVariablesUsingMap(genericType, replaceMap); + if (type == null) { + type = genericType; + } + newTypes[i] = type; + } + return ArgType.generic(replaceType.getObject(), newTypes); + } + return null; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/regions/SwitchRegion.java b/jadx-core/src/main/java/jadx/core/dex/regions/SwitchRegion.java index bbdfbcb91..482f40495 100644 --- a/jadx-core/src/main/java/jadx/core/dex/regions/SwitchRegion.java +++ b/jadx-core/src/main/java/jadx/core/dex/regions/SwitchRegion.java @@ -13,10 +13,11 @@ import jadx.core.utils.Utils; public final class SwitchRegion extends AbstractRegion implements IBranchRegion { + public static final Object DEFAULT_CASE_KEY = new Object(); + private final BlockNode header; private final List cases; - private IContainer defCase; public SwitchRegion(IRegion parent, BlockNode header) { super(parent); @@ -50,14 +51,6 @@ public final class SwitchRegion extends AbstractRegion implements IBranchRegion cases.add(new CaseInfo(keysList, c)); } - public void setDefaultCase(IContainer block) { - defCase = block; - } - - public IContainer getDefaultCase() { - return defCase; - } - public List getCases() { return cases; } @@ -68,23 +61,15 @@ public final class SwitchRegion extends AbstractRegion implements IBranchRegion @Override public List getSubBlocks() { - List all = new ArrayList<>(cases.size() + 2); + List all = new ArrayList<>(cases.size() + 1); all.add(header); all.addAll(getCaseContainers()); - if (defCase != null) { - all.add(defCase); - } return Collections.unmodifiableList(all); } @Override public List getBranches() { - List branches = new ArrayList<>(cases.size() + 1); - branches.addAll(getCaseContainers()); - if (defCase != null) { - branches.add(defCase); - } - return Collections.unmodifiableList(branches); + return Collections.unmodifiableList(getCaseContainers()); } @Override @@ -97,13 +82,12 @@ public final class SwitchRegion extends AbstractRegion implements IBranchRegion StringBuilder sb = new StringBuilder(); sb.append("Switch: ").append(cases.size()); for (CaseInfo caseInfo : cases) { + List keyStrings = Utils.collectionMap(caseInfo.getKeys(), + k -> k == DEFAULT_CASE_KEY ? "default" : k.toString()); sb.append(CodeWriter.NL).append(" case ") - .append(Utils.listToString(caseInfo.getKeys())) + .append(Utils.listToString(keyStrings)) .append(" -> ").append(caseInfo.getContainer()); } - if (defCase != null) { - sb.append(CodeWriter.NL).append(" default -> ").append(defCase); - } return sb.toString(); } } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/AttachMethodDetails.java b/jadx-core/src/main/java/jadx/core/dex/visitors/AttachMethodDetails.java new file mode 100644 index 000000000..24e2ec09a --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/AttachMethodDetails.java @@ -0,0 +1,50 @@ +package jadx.core.dex.visitors; + +import jadx.core.dex.instructions.BaseInvokeNode; +import jadx.core.dex.nodes.BlockNode; +import jadx.core.dex.nodes.IMethodDetails; +import jadx.core.dex.nodes.InsnNode; +import jadx.core.dex.nodes.MethodNode; +import jadx.core.dex.nodes.RootNode; +import jadx.core.dex.nodes.utils.MethodUtils; +import jadx.core.dex.visitors.shrink.CodeShrinkVisitor; +import jadx.core.utils.exceptions.JadxException; + +@JadxVisitor( + name = "Attach Method Details", + desc = "Attach method details for invoke instructions", + runBefore = { + CodeShrinkVisitor.class + } +) +public class AttachMethodDetails extends AbstractVisitor { + + private MethodUtils methodUtils; + + @Override + public void init(RootNode root) { + methodUtils = root.getMethodUtils(); + } + + @Override + public void visit(MethodNode mth) throws JadxException { + if (mth.isNoCode()) { + return; + } + for (BlockNode blockNode : mth.getBasicBlocks()) { + for (InsnNode insn : blockNode.getInstructions()) { + if (insn instanceof BaseInvokeNode) { + attachMethodDetails((BaseInvokeNode) insn); + } + } + } + } + + private void attachMethodDetails(BaseInvokeNode insn) { + IMethodDetails methodDetails = methodUtils.getMethodDetails(insn.getCallMth()); + if (methodDetails != null) { + insn.addAttr(methodDetails); + } + } + +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/ConstInlineVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/ConstInlineVisitor.java index 44c210c69..4dfe07854 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/ConstInlineVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/ConstInlineVisitor.java @@ -5,7 +5,7 @@ import java.util.List; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.info.MethodInfo; -import jadx.core.dex.instructions.CallMthInterface; +import jadx.core.dex.instructions.BaseInvokeNode; import jadx.core.dex.instructions.ConstStringNode; import jadx.core.dex.instructions.IndexInsnNode; import jadx.core.dex.instructions.InsnType; @@ -240,8 +240,8 @@ public class ConstInlineVisitor extends AbstractVisitor { } private static boolean needExplicitCast(InsnNode insn, LiteralArg arg) { - if (insn instanceof CallMthInterface) { - CallMthInterface callInsn = (CallMthInterface) insn; + if (insn instanceof BaseInvokeNode) { + BaseInvokeNode callInsn = (BaseInvokeNode) insn; MethodInfo callMth = callInsn.getCallMth(); int offset = callInsn.getFirstArgOffset(); int argIndex = insn.getArgIndex(arg); diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/DeboxingVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/DeboxingVisitor.java index 4dd248496..d10fbee2c 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/DeboxingVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/DeboxingVisitor.java @@ -54,7 +54,7 @@ public class DeboxingVisitor extends AbstractVisitor { private static MethodInfo valueOfMth(RootNode root, ArgType argType, String clsName) { ArgType boxType = ArgType.object(clsName); ClassInfo boxCls = ClassInfo.fromType(root, boxType); - return MethodInfo.externalMth(boxCls, "valueOf", Collections.singletonList(argType), boxType); + return MethodInfo.fromDetails(root, boxCls, "valueOf", Collections.singletonList(argType), boxType); } @Override diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/DotGraphVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/DotGraphVisitor.java index e5a944e74..29b084930 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/DotGraphVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/DotGraphVisitor.java @@ -27,6 +27,7 @@ public class DotGraphVisitor extends AbstractVisitor { private static final String NL = "\\l"; private static final boolean PRINT_DOMINATORS = false; + private static final boolean PRINT_DOMINATORS_INFO = false; private final boolean useRegions; private final boolean rawInsn; @@ -182,6 +183,14 @@ public class DotGraphVisitor extends AbstractVisitor { if (!attrs.isEmpty()) { dot.add('|').add(attrs); } + if (PRINT_DOMINATORS_INFO) { + dot.add('|'); + dot.startLine("doms: ").add(escape(block.getDoms())); + dot.startLine("\\lidom: ").add(escape(block.getIDom())); + dot.startLine("\\ldom-f: ").add(escape(block.getDomFrontier())); + dot.startLine("\\ldoms-on: ").add(escape(Utils.listToString(block.getDominatesOn()))); + dot.startLine("\\l"); + } String insns = insertInsns(mth, block); if (!insns.isEmpty()) { dot.add('|').add(insns); @@ -272,6 +281,13 @@ public class DotGraphVisitor extends AbstractVisitor { } } + private String escape(Object obj) { + if (obj == null) { + return "null"; + } + return escape(obj.toString()); + } + private String escape(String string) { return string .replace("\\", "") // TODO replace \" diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/EnumVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/EnumVisitor.java index 60d6a9fc1..dc653702f 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/EnumVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/EnumVisitor.java @@ -37,6 +37,7 @@ import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.visitors.shrink.CodeShrinkVisitor; +import jadx.core.utils.BlockInsnPair; import jadx.core.utils.InsnRemover; import jadx.core.utils.InsnUtils; import jadx.core.utils.exceptions.JadxException; @@ -44,7 +45,8 @@ import jadx.core.utils.exceptions.JadxException; @JadxVisitor( name = "EnumVisitor", desc = "Restore enum classes", - runAfter = { CodeShrinkVisitor.class, ModVisitor.class } + runAfter = { CodeShrinkVisitor.class, ModVisitor.class }, + runBefore = { ExtractFieldInit.class } ) public class EnumVisitor extends AbstractVisitor { @@ -72,8 +74,6 @@ public class EnumVisitor extends AbstractVisitor { if (classInitMth.getBasicBlocks().isEmpty()) { return false; } - BlockNode staticBlock = classInitMth.getBasicBlocks().get(0); - ArgType clsType = cls.getClassInfo().getType(); // search "$VALUES" field (holds all enum values) @@ -104,34 +104,32 @@ public class EnumVisitor extends AbstractVisitor { List toRemove = new ArrayList<>(); // search "$VALUES" array init and collect enum fields + BlockInsnPair valuesInitPair = getValuesInitInsn(classInitMth, valuesField); + if (valuesInitPair == null) { + return false; + } + BlockNode staticBlock = valuesInitPair.getBlock(); + InsnNode valuesInitInsn = valuesInitPair.getInsn(); + List enumFields = null; - for (InsnNode insn : staticBlock.getInstructions()) { - if (insn.getType() != InsnType.SPUT) { - continue; - } - FieldInfo f = (FieldInfo) ((IndexInsnNode) insn).getIndex(); - if (f.equals(valuesField.getFieldInfo())) { - InsnArg arrArg = insn.getArg(0); - if (arrArg.isInsnWrap()) { - InsnNode arrFillInsn = ((InsnWrapArg) arrArg).getWrapInsn(); - InsnType insnType = arrFillInsn.getType(); - if (insnType == InsnType.FILLED_NEW_ARRAY) { - enumFields = extractEnumFields(cls, arrFillInsn, staticBlock, toRemove); - } else if (insnType == InsnType.NEW_ARRAY) { - // empty enum - InsnArg arg = arrFillInsn.getArg(0); - if (arg.isLiteral() && ((LiteralArg) arg).getLiteral() == 0) { - enumFields = Collections.emptyList(); - } - } + InsnArg arrArg = valuesInitInsn.getArg(0); + if (arrArg.isInsnWrap()) { + InsnNode arrFillInsn = ((InsnWrapArg) arrArg).getWrapInsn(); + InsnType insnType = arrFillInsn.getType(); + if (insnType == InsnType.FILLED_NEW_ARRAY) { + enumFields = extractEnumFields(cls, arrFillInsn, staticBlock, toRemove); + } else if (insnType == InsnType.NEW_ARRAY) { + // empty enum + InsnArg arg = arrFillInsn.getArg(0); + if (arg.isLiteral() && ((LiteralArg) arg).getLiteral() == 0) { + enumFields = Collections.emptyList(); } - toRemove.add(insn); - break; } } if (enumFields == null) { return false; } + toRemove.add(valuesInitInsn); // all checks complete, perform transform EnumClassAttr attr = new EnumClassAttr(enumFields.size()); @@ -169,6 +167,22 @@ public class EnumVisitor extends AbstractVisitor { return true; } + private BlockInsnPair getValuesInitInsn(MethodNode classInitMth, FieldNode valuesField) { + FieldInfo searchField = valuesField.getFieldInfo(); + for (BlockNode blockNode : classInitMth.getBasicBlocks()) { + for (InsnNode insn : blockNode.getInstructions()) { + if (insn.getType() == InsnType.SPUT) { + IndexInsnNode indexInsnNode = (IndexInsnNode) insn; + FieldInfo f = (FieldInfo) indexInsnNode.getIndex(); + if (f.equals(searchField)) { + return new BlockInsnPair(blockNode, indexInsnNode); + } + } + } + } + return null; + } + private List extractEnumFields(ClassNode cls, InsnNode arrFillInsn, BlockNode staticBlock, List toRemove) { List enumFields = new ArrayList<>(); for (InsnArg arg : arrFillInsn.getArguments()) { diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/ExtractFieldInit.java b/jadx-core/src/main/java/jadx/core/dex/visitors/ExtractFieldInit.java index 1356cfb9e..d219f9979 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/ExtractFieldInit.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/ExtractFieldInit.java @@ -35,9 +35,6 @@ public class ExtractFieldInit extends AbstractVisitor { @Override public boolean visit(ClassNode cls) throws JadxException { - if (cls.isEnum()) { - return false; - } for (ClassNode inner : cls.getInnerClasses()) { visit(inner); } @@ -66,12 +63,11 @@ public class ExtractFieldInit extends AbstractVisitor { } /** - * Remove final field in place initialization if it assign in class init method + * Remove a final field in place initialization if it an assign found in class init method */ private static void processStaticFieldAssign(ClassNode cls, IndexInsnNode insn) { FieldInfo field = (FieldInfo) insn.getIndex(); - String thisClass = cls.getClassInfo().getFullName(); - if (field.getDeclClass().getFullName().equals(thisClass)) { + if (field.getDeclClass().equals(cls.getClassInfo())) { FieldNode fn = cls.searchField(field); if (fn != null && fn.getAccessFlags().isFinal()) { fn.remove(AType.FIELD_INIT); diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/FindSuperUsageVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/FindSuperUsageVisitor.java new file mode 100644 index 000000000..fe8a63220 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/FindSuperUsageVisitor.java @@ -0,0 +1,49 @@ +package jadx.core.dex.visitors; + +import jadx.core.dex.attributes.AFlag; +import jadx.core.dex.instructions.args.ArgType; +import jadx.core.dex.instructions.args.InsnArg; +import jadx.core.dex.instructions.args.RegisterArg; +import jadx.core.dex.nodes.InsnNode; +import jadx.core.dex.nodes.MethodNode; +import jadx.core.dex.visitors.blocksmaker.BlockSplitter; +import jadx.core.utils.exceptions.JadxException; + +@JadxVisitor( + name = "FindSuperUsageVisitor", + desc = "Finds variables where a member of the super class is used and marks them.", + runBefore = BlockSplitter.class +) +public class FindSuperUsageVisitor extends AbstractVisitor { + + @Override + public void visit(MethodNode mth) throws JadxException { + if (mth.isNoCode()) { + return; + } + process(mth); + } + + private static void process(MethodNode methodNode) { + ArgType superClass = methodNode.getParentClass().getSuperClass(); + if (superClass == null) { + return; + } + String superClassName = superClass.getObject(); + if (superClassName.equals("java.lang.Object")) { + return; + } + for (InsnNode instruction : methodNode.getInstructions()) { + if (instruction != null) { + for (InsnArg argument : instruction.getArguments()) { + if (argument.isRegister()) { + ArgType argumentType = ((RegisterArg) argument).getInitType(); + if (argumentType.isObject() && argumentType.getObject().equals(superClassName)) { + argument.add(AFlag.SUPER); + } + } + } + } + } + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/MethodInvokeVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/MethodInvokeVisitor.java new file mode 100644 index 000000000..d44559752 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/MethodInvokeVisitor.java @@ -0,0 +1,374 @@ +package jadx.core.dex.visitors; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import jadx.core.Consts; +import jadx.core.dex.attributes.AFlag; +import jadx.core.dex.info.MethodInfo; +import jadx.core.dex.instructions.BaseInvokeNode; +import jadx.core.dex.instructions.IndexInsnNode; +import jadx.core.dex.instructions.InsnType; +import jadx.core.dex.instructions.args.ArgType; +import jadx.core.dex.instructions.args.InsnArg; +import jadx.core.dex.instructions.args.InsnWrapArg; +import jadx.core.dex.instructions.args.LiteralArg; +import jadx.core.dex.instructions.args.RegisterArg; +import jadx.core.dex.instructions.mods.ConstructorInsn; +import jadx.core.dex.nodes.BlockNode; +import jadx.core.dex.nodes.IMethodDetails; +import jadx.core.dex.nodes.InsnNode; +import jadx.core.dex.nodes.MethodNode; +import jadx.core.dex.nodes.RootNode; +import jadx.core.dex.visitors.shrink.CodeShrinkVisitor; +import jadx.core.dex.visitors.typeinference.TypeCompare; +import jadx.core.dex.visitors.typeinference.TypeCompareEnum; +import jadx.core.utils.Utils; +import jadx.core.utils.exceptions.JadxRuntimeException; + +import static jadx.core.codegen.CodeWriter.NL; + +@JadxVisitor( + name = "MethodInvokeVisitor", + desc = "Process additional info for method invocation (overload, vararg)", + runAfter = { + CodeShrinkVisitor.class, + ModVisitor.class + }, + runBefore = { + SimplifyVisitor.class // run before cast remove and StringBuilder replace + } +) +public class MethodInvokeVisitor extends AbstractVisitor { + private RootNode root; + + @Override + public void init(RootNode root) { + this.root = root; + } + + @Override + public void visit(MethodNode mth) { + if (mth.isNoCode()) { + return; + } + for (BlockNode block : mth.getBasicBlocks()) { + if (block.contains(AFlag.DONT_GENERATE)) { + continue; + } + for (InsnNode insn : block.getInstructions()) { + if (insn.contains(AFlag.DONT_GENERATE)) { + continue; + } + processInsn(mth, insn); + } + } + } + + private void processInsn(MethodNode mth, InsnNode insn) { + if (insn instanceof BaseInvokeNode) { + processInvoke(mth, ((BaseInvokeNode) insn)); + } + for (InsnArg insnArg : insn.getArguments()) { + if (insnArg instanceof InsnWrapArg) { + InsnNode wrapInsn = ((InsnWrapArg) insnArg).getWrapInsn(); + processInsn(mth, wrapInsn); + } + } + } + + private void processInvoke(MethodNode parentMth, BaseInvokeNode invokeInsn) { + MethodInfo callMth = invokeInsn.getCallMth(); + if (callMth.getArgsCount() == 0) { + return; + } + IMethodDetails mthDetails = root.getMethodUtils().getMethodDetails(invokeInsn); + if (mthDetails == null) { + if (Consts.DEBUG) { + parentMth.addComment("JADX DEBUG: Method info not found: " + callMth); + } + processUnknown(invokeInsn); + } else { + // parentMth.addComment("JADX DEBUG: got method details: " + mthDetails); + if (mthDetails.isVarArg()) { + ArgType last = Utils.last(mthDetails.getArgTypes()); + if (last != null && last.isArray()) { + invokeInsn.add(AFlag.VARARG_CALL); + } + } + processOverloaded(parentMth, invokeInsn, mthDetails); + } + } + + private void processOverloaded(MethodNode parentMth, BaseInvokeNode invokeInsn, IMethodDetails mthDetails) { + MethodInfo callMth = invokeInsn.getCallMth(); + ArgType callCls = getCallClassFromInvoke(parentMth, invokeInsn, callMth); + List overloadMethods = root.getMethodUtils().collectOverloadedMethods(callCls, callMth); + if (overloadMethods.isEmpty()) { + // not overloaded + return; + } + + overloadMethods.add(mthDetails); + resolveTypeVariablesInMethodArgs(invokeInsn, mthDetails, overloadMethods); + + int argsOffset = invokeInsn.getFirstArgOffset(); + List compilerVarTypes = collectCompilerVarTypes(invokeInsn, argsOffset); + List castTypes = searchCastTypes(parentMth, mthDetails, overloadMethods, compilerVarTypes); + applyArgsCast(invokeInsn, argsOffset, compilerVarTypes, castTypes); + } + + /** + * Method details not found => add cast for 'null' args + */ + private void processUnknown(BaseInvokeNode invokeInsn) { + int argsOffset = invokeInsn.getFirstArgOffset(); + List compilerVarTypes = collectCompilerVarTypes(invokeInsn, argsOffset); + List castTypes = new ArrayList<>(compilerVarTypes); + if (replaceUnknownTypes(castTypes, invokeInsn.getCallMth().getArgumentsTypes())) { + applyArgsCast(invokeInsn, argsOffset, compilerVarTypes, castTypes); + } + } + + private ArgType getCallClassFromInvoke(MethodNode parentMth, BaseInvokeNode invokeInsn, MethodInfo callMth) { + if (invokeInsn instanceof ConstructorInsn) { + ConstructorInsn constrInsn = (ConstructorInsn) invokeInsn; + if (constrInsn.isSuper()) { + return parentMth.getParentClass().getSuperClass(); + } + } + InsnArg instanceArg = invokeInsn.getInstanceArg(); + if (instanceArg != null) { + return instanceArg.getType(); + } + // static call + return callMth.getDeclClass().getType(); + } + + private void resolveTypeVariablesInMethodArgs(BaseInvokeNode invokeInsn, IMethodDetails mthDetails, + List overloadedMethods) { + MethodInfo callMth = invokeInsn.getCallMth(); + ArgType declClsType = callMth.getDeclClass().getType(); + ArgType callClsType; + InsnArg instanceArg = invokeInsn.getInstanceArg(); + if (instanceArg != null) { + callClsType = instanceArg.getType(); + } else { + callClsType = declClsType; + } + + Map typeVarsMapping = root.getTypeUtils().getTypeVariablesMapping(callClsType); + resolveTypeVars(mthDetails, typeVarsMapping); + for (IMethodDetails m : overloadedMethods) { + resolveTypeVars(m, typeVarsMapping); + } + } + + private void applyArgsCast(BaseInvokeNode invokeInsn, int argsOffset, List compilerVarTypes, List castTypes) { + int argsCount = invokeInsn.getArgsCount(); + for (int i = argsOffset; i < argsCount; i++) { + InsnArg arg = invokeInsn.getArg(i); + int origPos = i - argsOffset; + ArgType compilerType = compilerVarTypes.get(origPos); + ArgType castType = castTypes.get(origPos); + if (castType != null) { + if (!castType.equals(compilerType)) { + if (arg.isLiteral() && compilerType.isPrimitive() && castType.isPrimitive()) { + arg.setType(castType); + arg.add(AFlag.EXPLICIT_PRIMITIVE_TYPE); + } else { + InsnNode castInsn = new IndexInsnNode(InsnType.CAST, castType, 1); + castInsn.addArg(arg); + castInsn.add(AFlag.EXPLICIT_CAST); + InsnArg wrapCast = InsnArg.wrapArg(castInsn); + wrapCast.setType(castType); + invokeInsn.setArg(i, wrapCast); + } + } else { + // protect already existed cast + if (arg.isInsnWrap()) { + InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn(); + if (wrapInsn.getType() == InsnType.CHECK_CAST) { + wrapInsn.add(AFlag.EXPLICIT_CAST); + } + } + } + } + } + } + + private void resolveTypeVars(IMethodDetails mthDetails, Map typeVarsMapping) { + List argTypes = mthDetails.getArgTypes(); + int argsCount = argTypes.size(); + for (int argNum = 0; argNum < argsCount; argNum++) { + ArgType argType = argTypes.get(argNum); + if (argType == null) { + throw new JadxRuntimeException("Null arg type in " + mthDetails + " at: " + argNum + " in: " + argTypes); + } + if (argType.containsTypeVariable()) { + ArgType resolvedType = root.getTypeUtils().replaceTypeVariablesUsingMap(argType, typeVarsMapping); + if (resolvedType == null || resolvedType.containsTypeVariable()) { + // type variables erased from method info by compiler + resolvedType = mthDetails.getMethodInfo().getArgumentsTypes().get(argNum); + } + argTypes.set(argNum, resolvedType); + } + } + } + + private List searchCastTypes(MethodNode parentMth, IMethodDetails mthDetails, List overloadedMethods, + List compilerVarTypes) { + // try compile types + if (isOverloadResolved(mthDetails, overloadedMethods, compilerVarTypes)) { + return compilerVarTypes; + } + int argsCount = compilerVarTypes.size(); + List castTypes = new ArrayList<>(compilerVarTypes); + + // replace unknown types + boolean changed = replaceUnknownTypes(castTypes, mthDetails.getArgTypes()); + if (changed && isOverloadResolved(mthDetails, overloadedMethods, castTypes)) { + return castTypes; + } + + // replace generic types + changed = false; + for (int i = 0; i < argsCount; i++) { + ArgType castType = castTypes.get(i); + ArgType mthType = mthDetails.getArgTypes().get(i); + if (!castType.isGeneric() && mthType.isGeneric()) { + castTypes.set(i, mthType); + changed = true; + } + } + if (changed && isOverloadResolved(mthDetails, overloadedMethods, castTypes)) { + return castTypes; + } + + // if just one arg => cast will resolve + if (argsCount == 1) { + return mthDetails.getArgTypes(); + } + // TODO: try to minimize casts count + parentMth.addComment("JADX DEBUG: Failed to find minimal casts for resolve overloaded methods, cast all args instead" + + NL + " method: " + mthDetails + + NL + " arg types: " + compilerVarTypes + + NL + " candidates:" + + NL + " " + Utils.listToString(overloadedMethods, NL + " ")); + + // not resolved -> cast all args + return mthDetails.getArgTypes(); + } + + private boolean replaceUnknownTypes(List castTypes, List mthArgTypes) { + int argsCount = castTypes.size(); + boolean changed = false; + for (int i = 0; i < argsCount; i++) { + ArgType castType = castTypes.get(i); + if (!castType.isTypeKnown()) { + ArgType mthType = mthArgTypes.get(i); + castTypes.set(i, mthType); + changed = true; + } + } + return changed; + } + + private boolean isOverloadResolved(IMethodDetails expectedMthDetails, List overloadedMethods, List castTypes) { + if (overloadedMethods.isEmpty()) { + return false; + } + // TODO: search closest method, instead filtering + List strictMethods = filterApplicableMethods(overloadedMethods, castTypes, MethodInvokeVisitor::isStrictTypes); + if (strictMethods.size() == 1) { + return strictMethods.get(0).equals(expectedMthDetails); + } + List resolvedMethods = filterApplicableMethods(overloadedMethods, castTypes, MethodInvokeVisitor::isTypeApplicable); + if (resolvedMethods.size() == 1) { + return resolvedMethods.get(0).equals(expectedMthDetails); + } + return false; + } + + private static boolean isStrictTypes(TypeCompareEnum result) { + return result.isEqual(); + } + + private static boolean isTypeApplicable(TypeCompareEnum result) { + return result.isNarrowOrEqual() || result == TypeCompareEnum.WIDER_BY_GENERIC; + } + + private List filterApplicableMethods(List methods, List types, + Function acceptFunction) { + List list = new ArrayList<>(methods.size()); + for (IMethodDetails m : methods) { + if (isMethodAcceptable(m, types, acceptFunction)) { + list.add(m); + } + } + return list; + } + + private boolean isMethodAcceptable(IMethodDetails methodDetails, List types, + Function acceptFunction) { + List mthTypes = methodDetails.getArgTypes(); + int argCount = mthTypes.size(); + if (argCount != types.size()) { + return false; + } + TypeCompare typeCompare = root.getTypeUpdate().getTypeCompare(); + for (int i = 0; i < argCount; i++) { + ArgType mthType = mthTypes.get(i); + ArgType argType = types.get(i); + TypeCompareEnum result = typeCompare.compareTypes(argType, mthType); + if (!acceptFunction.apply(result)) { + return false; + } + } + return true; + } + + private List collectCompilerVarTypes(BaseInvokeNode insn, int argOffset) { + int argsCount = insn.getArgsCount(); + List result = new ArrayList<>(argsCount); + for (int i = argOffset; i < argsCount; i++) { + InsnArg arg = insn.getArg(i); + result.add(getCompilerVarType(arg)); + } + return result; + } + + /** + * Return type as seen by compiler + */ + private ArgType getCompilerVarType(InsnArg arg) { + if (arg instanceof LiteralArg) { + LiteralArg literalArg = (LiteralArg) arg; + ArgType type = literalArg.getType(); + if (literalArg.getLiteral() == 0) { + if (type.isObject() || type.isArray()) { + // null + return ArgType.UNKNOWN_OBJECT; + } + } + if (type.isPrimitive() && !arg.contains(AFlag.EXPLICIT_PRIMITIVE_TYPE)) { + return ArgType.INT; + } + return arg.getType(); + } + if (arg instanceof RegisterArg) { + return arg.getType(); + } + if (arg instanceof InsnWrapArg) { + InsnWrapArg wrapArg = (InsnWrapArg) arg; + InsnNode wrapInsn = wrapArg.getWrapInsn(); + if (wrapInsn.getResult() != null) { + return wrapInsn.getResult().getType(); + } + return arg.getType(); + } + throw new JadxRuntimeException("Unknown var type for: " + arg); + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/ModVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/ModVisitor.java index a7e854060..a8b57758c 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/ModVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/ModVisitor.java @@ -86,6 +86,7 @@ public class ModVisitor extends AbstractVisitor { InsnRemover remover = new InsnRemover(mth); replaceStep(mth, remover); removeStep(mth, remover); + iterativeRemoveStep(mth); } private static void replaceStep(MethodNode mth, InsnRemover remover) { @@ -199,15 +200,27 @@ public class ModVisitor extends AbstractVisitor { continue; } for (Map.Entry entry : annotation.getValues().entrySet()) { - Object value = entry.getValue(); - FieldNode constField = parentCls.getConstField(value); - if (constField != null) { - entry.setValue(constField.getFieldInfo()); - } + entry.setValue(replaceConstValue(parentCls, entry.getValue())); } } } + @SuppressWarnings({ "unchecked", "rawtypes" }) + private Object replaceConstValue(ClassNode parentCls, @Nullable Object value) { + if (value instanceof List) { + List listVal = (List) value; + if (!listVal.isEmpty()) { + listVal.replaceAll(v -> replaceConstValue(parentCls, v)); + } + return listVal; + } + FieldNode constField = parentCls.getConstField(value); + if (constField != null) { + return constField.getFieldInfo(); + } + return value; + } + private static void replaceConst(MethodNode mth, ClassNode parentClass, BlockNode block, int i, InsnNode insn) { FieldNode f; if (insn.getType() == InsnType.CONST_STR) { @@ -254,7 +267,7 @@ public class ModVisitor extends AbstractVisitor { private static void removeRedundantCast(MethodNode mth, BlockNode block, int i, IndexInsnNode insn) { InsnArg castArg = insn.getArg(0); ArgType castType = (ArgType) insn.getIndex(); - if (!ArgType.isCastNeeded(mth.dex(), castArg.getType(), castType) + if (!ArgType.isCastNeeded(mth.root(), castArg.getType(), castType) || isCastDuplicate(insn)) { InsnNode insnNode = new InsnNode(InsnType.MOVE, 1); insnNode.setResult(insn.getResult()); @@ -293,6 +306,9 @@ public class ModVisitor extends AbstractVisitor { break; default: + if (insn.contains(AFlag.REMOVE)) { + remover.addAndUnbind(insn); + } break; } } @@ -300,6 +316,33 @@ public class ModVisitor extends AbstractVisitor { } } + private static void iterativeRemoveStep(MethodNode mth) { + boolean changed; + do { + changed = false; + for (BlockNode block : mth.getBasicBlocks()) { + for (InsnNode insn : block.getInstructions()) { + if (insn.getType() == InsnType.MOVE + && insn.isAttrStorageEmpty() + && isResultArgNotUsed(insn)) { + InsnRemover.remove(mth, block, insn); + changed = true; + break; + } + } + } + } while (changed); + } + + private static boolean isResultArgNotUsed(InsnNode insn) { + RegisterArg result = insn.getResult(); + if (result != null) { + SSAVar ssaVar = result.getSVar(); + return ssaVar.getUseCount() == 0; + } + return false; + } + private static void processAnonymousConstructor(MethodNode mth, ConstructorInsn co) { MethodInfo callMth = co.getCallMth(); MethodNode callMthNode = mth.dex().resolveMethod(callMth); diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/SimplifyVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/SimplifyVisitor.java index 2e8ae2ddf..33e487345 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/SimplifyVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/SimplifyVisitor.java @@ -49,7 +49,8 @@ public class SimplifyVisitor extends AbstractVisitor { @Override public void init(RootNode root) { - stringGetBytesMth = MethodInfo.externalMth( + stringGetBytesMth = MethodInfo.fromDetails( + root, ClassInfo.fromType(root, ArgType.STRING), "getBytes", Collections.emptyList(), @@ -224,7 +225,7 @@ public class SimplifyVisitor extends AbstractVisitor { } ArgType castToType = (ArgType) castInsn.getIndex(); - if (!ArgType.isCastNeeded(mth.dex(), argType, castToType) + if (!ArgType.isCastNeeded(mth.root(), argType, castToType) || isCastDuplicate(castInsn)) { InsnNode insnNode = new InsnNode(InsnType.MOVE, 1); insnNode.setOffset(castInsn.getOffset()); diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/BlockProcessor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/BlockProcessor.java index ffdd7d220..4e703458e 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/BlockProcessor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/BlockProcessor.java @@ -16,7 +16,6 @@ import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.LoopInfo; import jadx.core.dex.instructions.InsnType; -import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.LiteralArg; import jadx.core.dex.instructions.args.RegisterArg; @@ -422,7 +421,7 @@ public class BlockProcessor extends AbstractVisitor { } private static boolean mergeConstReturn(MethodNode mth) { - if (mth.getReturnType() == ArgType.VOID) { + if (mth.isVoidReturn()) { return false; } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/debuginfo/DebugInfoApplyVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/debuginfo/DebugInfoApplyVisitor.java index d1c7f101c..561e92cd3 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/debuginfo/DebugInfoApplyVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/debuginfo/DebugInfoApplyVisitor.java @@ -64,7 +64,7 @@ public class DebugInfoApplyVisitor extends AbstractVisitor { mth.getSVars().forEach(var -> { ArgType type = var.getTypeInfo().getType(); if (!type.isTypeKnown()) { - mth.addComment("JADX WARNING: type inference failed for: " + var.getDetailedVarInfo(mth)); + mth.addWarnComment("Type inference failed for: " + var.getDetailedVarInfo(mth)); } }); } @@ -90,7 +90,7 @@ public class DebugInfoApplyVisitor extends AbstractVisitor { RegDebugInfoAttr debugInfo = debugInfoSet.iterator().next(); applyDebugInfo(mth, ssaVar, debugInfo.getRegType(), debugInfo.getName()); } else { - LOG.warn("Multiple debug info for {}: {}", ssaVar, debugInfoSet); + mth.addComment("JADX INFO: Multiple debug info for " + ssaVar + ": " + debugInfoSet); for (RegDebugInfoAttr debugInfo : debugInfoSet) { applyDebugInfo(mth, ssaVar, debugInfo.getRegType(), debugInfo.getName()); } @@ -150,14 +150,6 @@ public class DebugInfoApplyVisitor extends AbstractVisitor { if (NameMapper.isValidAndPrintable(varName)) { ssaVar.setName(varName); } - detachDebugInfo(ssaVar.getAssign()); - ssaVar.getUseList().forEach(DebugInfoApplyVisitor::detachDebugInfo); - } - } - - private static void detachDebugInfo(RegisterArg reg) { - if (reg != null) { - reg.remove(AType.REG_DEBUG_INFO); } } @@ -172,7 +164,7 @@ public class DebugInfoApplyVisitor extends AbstractVisitor { * Fix debug info for splitter 'return' instructions */ private static void fixLinesForReturn(MethodNode mth) { - if (mth.getReturnType().equals(ArgType.VOID)) { + if (mth.isVoidReturn()) { return; } InsnNode origReturn = null; diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/debuginfo/DebugInfoParseVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/debuginfo/DebugInfoParseVisitor.java index 3bc4368ae..185ab03a3 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/debuginfo/DebugInfoParseVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/debuginfo/DebugInfoParseVisitor.java @@ -20,6 +20,8 @@ import jadx.core.utils.ErrorsCounter; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxException; +import static jadx.core.codegen.CodeWriter.NL; + @JadxVisitor( name = "Debug Info Parser", desc = "Parse debug information (variable names and types, instruction lines)", @@ -42,7 +44,7 @@ public class DebugInfoParseVisitor extends AbstractVisitor { } catch (Exception e) { mth.addComment("JADX WARNING: Error to parse debug info: " + ErrorsCounter.formatMsg(mth, e.getMessage()) - + '\n' + Utils.getStackTrace(e)); + + NL + Utils.getStackTrace(e)); } } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/debuginfo/LocalVar.java b/jadx-core/src/main/java/jadx/core/dex/visitors/debuginfo/LocalVar.java index f25ca9b64..35a5fa122 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/debuginfo/LocalVar.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/debuginfo/LocalVar.java @@ -5,6 +5,7 @@ import org.slf4j.LoggerFactory; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.DexNode; +import jadx.core.dex.nodes.parser.SignatureParser; import jadx.core.utils.InsnUtils; public final class LocalVar { @@ -31,7 +32,7 @@ public final class LocalVar { this.name = name; if (sign != null) { try { - ArgType gType = ArgType.parseGenericSignature(sign); + ArgType gType = new SignatureParser(sign).consumeType(); if (checkSignature(type, gType)) { type = gType; } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/IfRegionVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/IfRegionVisitor.java index dc6b1e3b0..b3e09b379 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/IfRegionVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/IfRegionVisitor.java @@ -3,7 +3,6 @@ package jadx.core.dex.visitors.regions; import java.util.List; import jadx.core.dex.attributes.AFlag; -import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.IContainer; import jadx.core.dex.nodes.IRegion; import jadx.core.dex.nodes.MethodNode; @@ -96,7 +95,7 @@ public class IfRegionVisitor extends AbstractVisitor { } private static void moveReturnToThenBlock(MethodNode mth, IfRegion ifRegion) { - if (!mth.getReturnType().equals(ArgType.VOID) + if (!mth.isVoidReturn() && hasSimpleReturnBlock(ifRegion.getElseRegion()) /* && insnsCount(ifRegion.getThenRegion()) < 2 */) { invertIfRegion(ifRegion); @@ -139,7 +138,7 @@ public class IfRegionVisitor extends AbstractVisitor { // code style check: // will remove 'return;' from 'then' and 'else' with one instruction // see #jadx.tests.integration.conditions.TestConditions9 - if (mth.getReturnType() == ArgType.VOID + if (mth.isVoidReturn() && insnsCount(ifRegion.getThenRegion()) == 2 && insnsCount(ifRegion.getElseRegion()) == 2) { return false; diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/RegionMaker.java b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/RegionMaker.java index 4632fb1d5..832b8123a 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/RegionMaker.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/RegionMaker.java @@ -2,6 +2,7 @@ package jadx.core.dex.visitors.regions; import java.util.ArrayList; import java.util.BitSet; +import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; @@ -10,6 +11,7 @@ import java.util.Map.Entry; import java.util.Optional; import java.util.Set; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -737,135 +739,71 @@ public class RegionMaker { } private BlockNode processSwitch(IRegion currentRegion, BlockNode block, SwitchNode insn, RegionStack stack) { - SwitchRegion sw = new SwitchRegion(currentRegion, block); - currentRegion.getSubBlocks().add(sw); - + // map case blocks to keys int len = insn.getTargets().length; - // sort by target Map> blocksMap = new LinkedHashMap<>(len); + Object[] keysArr = insn.getKeys(); + BlockNode[] targetBlocksArr = insn.getTargetBlocks(); for (int i = 0; i < len; i++) { - Object key = insn.getKeys()[i]; - BlockNode targ = insn.getTargetBlocks()[i]; - List keys = blocksMap.computeIfAbsent(targ, k -> new ArrayList<>(2)); - keys.add(key); + List keys = blocksMap.computeIfAbsent(targetBlocksArr[i], k -> new ArrayList<>(2)); + keys.add(keysArr[i]); } BlockNode defCase = insn.getDefTargetBlock(); if (defCase != null) { - blocksMap.remove(defCase); + List keys = blocksMap.computeIfAbsent(defCase, k -> new ArrayList<>(1)); + keys.add(SwitchRegion.DEFAULT_CASE_KEY); } + + // search 'out' block - 'next' block after whole switch statement + BlockNode out; LoopInfo loop = mth.getLoopForBlock(block); - - Map fallThroughCases = new LinkedHashMap<>(); - - List basicBlocks = mth.getBasicBlocks(); - BitSet outs = new BitSet(basicBlocks.size()); - outs.or(block.getDomFrontier()); - for (BlockNode s : block.getCleanSuccessors()) { - BitSet df = s.getDomFrontier(); - // fall through case block - if (df.cardinality() > 1) { - if (df.cardinality() > 2) { - LOG.debug("Unexpected case pattern, block: {}, mth: {}", s, mth); - } else { - BlockNode first = basicBlocks.get(df.nextSetBit(0)); - BlockNode second = basicBlocks.get(df.nextSetBit(first.getId() + 1)); - if (second.getDomFrontier().get(first.getId())) { - fallThroughCases.put(s, second); - df = new BitSet(df.size()); - df.set(first.getId()); - } else if (first.getDomFrontier().get(second.getId())) { - fallThroughCases.put(s, first); - df = new BitSet(df.size()); - df.set(second.getId()); - } - } + if (loop == null) { + out = calcPostDomOut(mth, block, mth.getExitBlocks()); + } else { + BlockNode loopEnd = loop.getEnd(); + // treat 'continue' as exit + out = calcPostDomOut(mth, block, loopEnd.getPredecessors()); + if (out != null) { + insertContinueInSwitch(block, out, loopEnd); + } else { + // no 'continue' + out = calcPostDomOut(mth, block, Collections.singletonList(loopEnd)); } - outs.or(df); - } - outs.clear(block.getId()); - if (loop != null) { - outs.clear(loop.getStart().getId()); } + SwitchRegion sw = new SwitchRegion(currentRegion, block); + currentRegion.getSubBlocks().add(sw); stack.push(sw); - stack.addExits(BlockUtils.bitSetToBlocks(mth, outs)); + stack.addExit(out); - // check cases order if fall through case exists - if (!fallThroughCases.isEmpty() - && isBadCasesOrder(blocksMap, fallThroughCases)) { - LOG.debug("Fixing incorrect switch cases order, method: {}", mth); - blocksMap = reOrderSwitchCases(blocksMap, fallThroughCases); - if (isBadCasesOrder(blocksMap, fallThroughCases)) { - mth.addWarn("Can't fix incorrect switch cases order"); + // detect fallthrough cases + Map fallThroughCases = new LinkedHashMap<>(); + if (out != null) { + BitSet caseBlocks = BlockUtils.blocksToBitSet(mth, blocksMap.keySet()); + caseBlocks.clear(out.getId()); + for (BlockNode successor : block.getCleanSuccessors()) { + BlockNode fallThroughBlock = searchFallThroughCase(successor, out, caseBlocks); + if (fallThroughBlock != null) { + fallThroughCases.put(successor, fallThroughBlock); + } } - } - - // filter 'out' block - if (outs.cardinality() > 1) { - // remove exception handlers - BlockUtils.cleanBitSet(mth, outs); - } - if (outs.cardinality() > 1) { - // filter loop start and successors of other blocks - for (int i = outs.nextSetBit(0); i >= 0; i = outs.nextSetBit(i + 1)) { - BlockNode b = basicBlocks.get(i); - outs.andNot(b.getDomFrontier()); - if (b.contains(AFlag.LOOP_START)) { - outs.clear(b.getId()); + // check fallthrough cases order + if (!fallThroughCases.isEmpty() && isBadCasesOrder(blocksMap, fallThroughCases)) { + Map> newBlocksMap = reOrderSwitchCases(blocksMap, fallThroughCases); + if (isBadCasesOrder(newBlocksMap, fallThroughCases)) { + mth.addComment("JADX INFO: Can't fix incorrect switch cases order, some code will duplicate"); + fallThroughCases.clear(); } else { - for (BlockNode s : b.getCleanSuccessors()) { - outs.clear(s.getId()); - } + blocksMap = newBlocksMap; } } } - if (loop != null && outs.cardinality() > 1) { - outs.clear(loop.getEnd().getId()); - } - if (outs.cardinality() == 0) { - // one or several case blocks are empty, - // run expensive algorithm for find 'out' block - for (BlockNode maybeOut : block.getSuccessors()) { - boolean allReached = true; - for (BlockNode s : block.getSuccessors()) { - if (!isPathExists(s, maybeOut)) { - allReached = false; - break; - } - } - if (allReached) { - outs.set(maybeOut.getId()); - break; - } - } - } - BlockNode out = null; - if (outs.cardinality() == 1) { - out = basicBlocks.get(outs.nextSetBit(0)); - stack.addExit(out); - } else if (loop == null && outs.cardinality() > 1) { - LOG.warn("Can't detect out node for switch block: {} in {}", block, mth); - } - if (loop != null) { - // check if 'continue' must be inserted - BlockNode end = loop.getEnd(); - if (out != end && out != null) { - insertContinueInSwitch(block, out, end); - } - } - - if (!stack.containsExit(defCase)) { - Region defRegion = makeRegion(defCase, stack); - if (RegionUtils.notEmpty(defRegion)) { - sw.setDefaultCase(defRegion); - } - } for (Entry> entry : blocksMap.entrySet()) { + List keysList = entry.getValue(); BlockNode caseBlock = entry.getKey(); if (stack.containsExit(caseBlock)) { - // empty case block - sw.addCase(entry.getValue(), new Region(stack.peekRegion())); + sw.addCase(keysList, new Region(stack.peekRegion())); } else { BlockNode next = fallThroughCases.get(caseBlock); stack.addExit(next); @@ -875,17 +813,102 @@ public class RegionMaker { next.add(AFlag.FALL_THROUGH); caseRegion.add(AFlag.FALL_THROUGH); } - sw.addCase(entry.getValue(), caseRegion); + sw.addCase(keysList, caseRegion); // 'break' instruction will be inserted in RegionMakerVisitor.PostRegionVisitor } } + removeEmptyCases(insn, sw, defCase); + stack.pop(); return out; } - private boolean isBadCasesOrder(Map> blocksMap, - Map fallThroughCases) { + @Nullable + private BlockNode searchFallThroughCase(BlockNode successor, BlockNode out, BitSet caseBlocks) { + BitSet df = successor.getDomFrontier(); + if (df.intersects(caseBlocks)) { + return getOneIntersectionBlock(out, caseBlocks, df); + } + Set allPathsBlocks = BlockUtils.getAllPathsBlocks(successor, out); + Map bitSetMap = BlockUtils.calcPartialPostDominance(mth, allPathsBlocks, out); + BitSet pdoms = bitSetMap.get(successor); + if (pdoms != null && pdoms.intersects(caseBlocks)) { + return getOneIntersectionBlock(out, caseBlocks, pdoms); + } + return null; + } + + @Nullable + private BlockNode getOneIntersectionBlock(BlockNode out, BitSet caseBlocks, BitSet fallThroughSet) { + BitSet caseExits = BlockUtils.copyBlocksBitSet(mth, fallThroughSet); + caseExits.clear(out.getId()); + caseExits.and(caseBlocks); + return BlockUtils.bitSetToOneBlock(mth, caseExits); + } + + @Nullable + private static BlockNode calcPostDomOut(MethodNode mth, BlockNode block, List exits) { + if (exits.size() == 1 && mth.getExitBlocks().equals(exits)) { + // simple case: for only one exit which is equal to method exit block + return BlockUtils.calcImmediatePostDominator(mth, block); + } + // fast search: union of blocks dominance frontier + // work if no fallthrough cases and no returns inside switch + BitSet outs = BlockUtils.copyBlocksBitSet(mth, block.getDomFrontier()); + for (BlockNode s : block.getCleanSuccessors()) { + outs.or(s.getDomFrontier()); + } + outs.clear(block.getId()); + + if (outs.cardinality() != 1) { + // slow search: calculate partial post-dominance for every exit node + BitSet ipdoms = BlockUtils.newBlocksBitSet(mth); + for (BlockNode exitBlock : exits) { + if (BlockUtils.isAnyPathExists(block, exitBlock)) { + Set pathBlocks = BlockUtils.getAllPathsBlocks(block, exitBlock); + BlockNode ipdom = BlockUtils.calcPartialImmediatePostDominator(mth, block, pathBlocks, exitBlock); + if (ipdom != null) { + ipdoms.set(ipdom.getId()); + } + } + } + outs.and(ipdoms); + } + return BlockUtils.bitSetToOneBlock(mth, outs); + } + + /** + * Remove empty case blocks: + * 1. single 'default' case + * 2. filler cases if switch is 'packed' and 'default' case is empty + */ + private void removeEmptyCases(SwitchNode insn, SwitchRegion sw, BlockNode defCase) { + boolean defaultCaseIsEmpty; + if (defCase == null) { + defaultCaseIsEmpty = true; + } else { + defaultCaseIsEmpty = sw.getCases().stream() + .anyMatch(c -> c.getKeys().contains(SwitchRegion.DEFAULT_CASE_KEY) + && RegionUtils.isEmpty(c.getContainer())); + } + if (defaultCaseIsEmpty) { + sw.getCases().removeIf(caseInfo -> { + if (RegionUtils.isEmpty(caseInfo.getContainer())) { + List keys = caseInfo.getKeys(); + if (keys.contains(SwitchRegion.DEFAULT_CASE_KEY)) { + return true; + } + if (insn.isPacked()) { + return true; + } + } + return false; + }); + } + } + + private boolean isBadCasesOrder(Map> blocksMap, Map fallThroughCases) { BlockNode nextCaseBlock = null; for (BlockNode caseBlock : blocksMap.keySet()) { if (nextCaseBlock != null && !caseBlock.equals(nextCaseBlock)) { diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/RegionMakerVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/RegionMakerVisitor.java index 9a3f1a7b3..2726b83db 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/RegionMakerVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/RegionMakerVisitor.java @@ -25,6 +25,7 @@ import jadx.core.dex.regions.SwitchRegion; import jadx.core.dex.regions.SynchronizedRegion; import jadx.core.dex.regions.loops.LoopRegion; import jadx.core.dex.visitors.AbstractVisitor; +import jadx.core.dex.visitors.shrink.CodeShrinkVisitor; import jadx.core.utils.InsnRemover; import jadx.core.utils.RegionUtils; import jadx.core.utils.exceptions.JadxException; @@ -58,6 +59,8 @@ public class RegionMakerVisitor extends AbstractVisitor { } private static void postProcessRegions(MethodNode mth) { + processForceInlineInsns(mth); + // make try-catch regions ProcessTryCatchRegions.process(mth); @@ -70,6 +73,15 @@ public class RegionMakerVisitor extends AbstractVisitor { } } + private static void processForceInlineInsns(MethodNode mth) { + boolean needShrink = mth.getBasicBlocks().stream() + .flatMap(block -> block.getInstructions().stream()) + .anyMatch(insn -> insn.contains(AFlag.FORCE_ASSIGN_INLINE)); + if (needShrink) { + CodeShrinkVisitor.shrinkMethod(mth); + } + } + private static final class PostRegionVisitor extends AbstractRegionVisitor { @Override public void leaveRegion(MethodNode mth, IRegion region) { diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/ReturnVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/ReturnVisitor.java index 5fad69e3e..87d3c7598 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/ReturnVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/ReturnVisitor.java @@ -4,7 +4,6 @@ import java.util.List; import java.util.ListIterator; import jadx.core.dex.attributes.AFlag; -import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.IBlock; import jadx.core.dex.nodes.IBranchRegion; @@ -26,7 +25,7 @@ public class ReturnVisitor extends AbstractVisitor { @Override public void visit(MethodNode mth) throws JadxException { // remove useless returns in void methods - if (mth.getReturnType().equals(ArgType.VOID)) { + if (mth.isVoidReturn()) { DepthRegionTraversal.traverse(mth, new ReturnRemoverVisitor()); } } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/TernaryMod.java b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/TernaryMod.java index 6fc11c4d0..36b0042fd 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/TernaryMod.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/TernaryMod.java @@ -7,7 +7,6 @@ import java.util.Map; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.PhiInsn; -import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.InsnWrapArg; import jadx.core.dex.instructions.args.RegisterArg; @@ -123,7 +122,7 @@ public class TernaryMod implements IRegionIterativeVisitor { return true; } - if (!mth.getReturnType().equals(ArgType.VOID) + if (!mth.isVoidReturn() && thenInsn.getType() == InsnType.RETURN && elseInsn.getType() == InsnType.RETURN) { InsnArg thenArg = thenInsn.getArg(0); diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/shrink/CodeShrinkVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/shrink/CodeShrinkVisitor.java index 1cfb9d643..40d731ce7 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/shrink/CodeShrinkVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/shrink/CodeShrinkVisitor.java @@ -100,10 +100,6 @@ public class CodeShrinkVisitor extends AbstractVisitor { int assignPos = insnList.getIndex(assignInsn); if (assignPos != -1) { - if (assignInline) { - // TODO? - return; - } WrapInfo wrapInfo = argsInfo.checkInline(assignPos, arg); if (wrapInfo != null) { wrapList.add(wrapInfo); @@ -123,25 +119,19 @@ public class CodeShrinkVisitor extends AbstractVisitor { } } - private static void assignInline(MethodNode mth, RegisterArg arg, InsnNode assignInsn, BlockNode assignBlock) { + private static boolean assignInline(MethodNode mth, RegisterArg arg, InsnNode assignInsn, BlockNode assignBlock) { RegisterArg useArg = arg.getSVar().getUseList().get(0); InsnNode useInsn = useArg.getParentInsn(); if (useInsn == null || useInsn.contains(AFlag.DONT_GENERATE)) { - return; - } - - InsnArg replaceArg; - InsnType assignInsnType = assignInsn.getType(); - if (assignInsnType == InsnType.MOVE || assignInsnType == InsnType.CONST) { - replaceArg = assignInsn.getArg(0).duplicate(); - } else { - replaceArg = InsnArg.wrapArg(assignInsn.copy()); + return false; } + InsnArg replaceArg = InsnArg.wrapInsnIntoArg(assignInsn.copy()); useInsn.replaceArg(useArg, replaceArg); assignInsn.add(AFlag.REMOVE); assignInsn.add(AFlag.DONT_GENERATE); InsnRemover.remove(mth, assignBlock, assignInsn); + return true; } private static boolean inline(MethodNode mth, RegisterArg arg, InsnNode insn, BlockNode block) { @@ -149,6 +139,9 @@ public class CodeShrinkVisitor extends AbstractVisitor { if (parentInsn != null && parentInsn.getType() == InsnType.RETURN) { parentInsn.setSourceLine(insn.getSourceLine()); } + if (insn.contains(AFlag.FORCE_ASSIGN_INLINE)) { + return assignInline(mth, arg, insn, block); + } boolean replaced = arg.wrapInstruction(mth, insn) != null; if (replaced) { InsnList.remove(block, insn); diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/ssa/SSATransform.java b/jadx-core/src/main/java/jadx/core/dex/visitors/ssa/SSATransform.java index 5dfc501e3..8451d6031 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/ssa/SSATransform.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/ssa/SSATransform.java @@ -422,6 +422,7 @@ public class SSATransform extends AbstractVisitor { if (resArg.getRegNum() != arg.getRegNum() && !resArg.getSVar().isUsedInPhi()) { markThisArgs(resArg); + parentInsn.add(AFlag.REMOVE); parentInsn.add(AFlag.DONT_GENERATE); } } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeBoundInvokeAssign.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeBoundInvokeAssign.java index ad263a789..b40227a3c 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeBoundInvokeAssign.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeBoundInvokeAssign.java @@ -4,7 +4,6 @@ import jadx.core.dex.instructions.InvokeNode; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.RootNode; -import jadx.core.utils.TypeUtils; /** * Special dynamic bound for invoke with generics. @@ -38,7 +37,7 @@ public final class TypeBoundInvokeAssign implements ITypeBoundDynamic { } private ArgType getReturnType(ArgType instanceType) { - ArgType resultGeneric = TypeUtils.replaceClassGenerics(root, instanceType, genericReturnType); + ArgType resultGeneric = root.getTypeUtils().replaceClassGenerics(instanceType, genericReturnType); if (resultGeneric != null) { return resultGeneric; } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeCompare.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeCompare.java index ad1239fe3..45480b9f5 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeCompare.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeCompare.java @@ -8,11 +8,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.dex.instructions.args.ArgType; +import jadx.core.dex.instructions.args.ArgType.WildcardBound; import jadx.core.dex.instructions.args.PrimitiveType; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.exceptions.JadxRuntimeException; import static jadx.core.dex.visitors.typeinference.TypeCompareEnum.CONFLICT; +import static jadx.core.dex.visitors.typeinference.TypeCompareEnum.EQUAL; import static jadx.core.dex.visitors.typeinference.TypeCompareEnum.NARROW; import static jadx.core.dex.visitors.typeinference.TypeCompareEnum.NARROW_BY_GENERIC; import static jadx.core.dex.visitors.typeinference.TypeCompareEnum.UNKNOWN; @@ -143,18 +145,56 @@ public class TypeCompare { if (firstGenericType && secondGenericType && !objectsEquals) { return CONFLICT; } + boolean firstGeneric = first.isGeneric(); + boolean secondGeneric = second.isGeneric(); + if (firstGenericType || secondGenericType) { + ArgType firstWildcardType = first.getWildcardType(); + ArgType secondWildcardType = second.getWildcardType(); + if (firstWildcardType != null || secondWildcardType != null) { + if (firstWildcardType != null && secondGenericType && first.getWildcardBound() == WildcardBound.UNBOUND) { + return CONFLICT; + } + if (firstGenericType && secondWildcardType != null && second.getWildcardBound() == WildcardBound.UNBOUND) { + return CONFLICT; + } + } if (firstGenericType) { return compareGenericTypeWithObject(first, second); } else { return compareGenericTypeWithObject(second, first).invert(); } } - boolean firstGeneric = first.isGeneric(); - boolean secondGeneric = second.isGeneric(); - if (firstGeneric != secondGeneric && objectsEquals) { - // don't check generics for now - return firstGeneric ? NARROW_BY_GENERIC : WIDER_BY_GENERIC; + if (objectsEquals) { + if (firstGeneric != secondGeneric) { + return firstGeneric ? NARROW_BY_GENERIC : WIDER_BY_GENERIC; + } + // both generics on same object + if (first.getWildcardBound() != null && second.getWildcardBound() != null) { + // both wildcards + return compareWildcardTypes(first, second); + } + ArgType[] firstGenericTypes = first.getGenericTypes(); + ArgType[] secondGenericTypes = second.getGenericTypes(); + if (firstGenericTypes == null || secondGenericTypes == null) { + // check outer types + ArgType firstOuterType = first.getOuterType(); + ArgType secondOuterType = second.getOuterType(); + if (firstOuterType != null && secondOuterType != null) { + return compareTypes(firstOuterType, secondOuterType); + } + } else { + // compare generics arrays + int len = firstGenericTypes.length; + if (len == secondGenericTypes.length) { + for (int i = 0; i < len; i++) { + TypeCompareEnum res = compareTypes(firstGenericTypes[i], secondGenericTypes[i]); + if (res != EQUAL) { + return res; + } + } + } + } } boolean firstIsObjCls = first.equals(ArgType.OBJECT); if (firstIsObjCls || second.equals(ArgType.OBJECT)) { @@ -172,6 +212,22 @@ public class TypeCompare { return TypeCompareEnum.CONFLICT; } + private TypeCompareEnum compareWildcardTypes(ArgType first, ArgType second) { + WildcardBound firstWildcardBound = first.getWildcardBound(); + WildcardBound secondWildcardBound = second.getWildcardBound(); + if (firstWildcardBound == WildcardBound.UNBOUND) { + return WIDER; + } + if (secondWildcardBound == WildcardBound.UNBOUND) { + return NARROW; + } + TypeCompareEnum wildcardCompare = compareTypes(first.getWildcardType(), second.getWildcardType()); + if (firstWildcardBound == secondWildcardBound) { + return wildcardCompare; + } + return CONFLICT; + } + private TypeCompareEnum compareGenericTypeWithObject(ArgType genericType, ArgType objType) { List extendTypes = genericType.getExtendTypes(); if (extendTypes == null || extendTypes.isEmpty()) { diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeCompareEnum.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeCompareEnum.java index 1a42c8a32..7af383b2d 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeCompareEnum.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeCompareEnum.java @@ -42,4 +42,12 @@ public enum TypeCompareEnum { public boolean isNarrow() { return this == NARROW || this == NARROW_BY_GENERIC; } + + public boolean isNarrowOrEqual() { + return isEqual() || isNarrow(); + } + + public boolean isGeneric() { + return this == WIDER_BY_GENERIC || this == NARROW_BY_GENERIC; + } } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeInferenceVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeInferenceVisitor.java index f0b3aeef3..a86e3f4c7 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeInferenceVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeInferenceVisitor.java @@ -16,7 +16,7 @@ import jadx.core.clsp.ClspGraph; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.info.ClassInfo; -import jadx.core.dex.info.MethodInfo; +import jadx.core.dex.instructions.BaseInvokeNode; import jadx.core.dex.instructions.IndexInsnNode; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.InvokeNode; @@ -30,11 +30,13 @@ import jadx.core.dex.instructions.args.PrimitiveType; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.nodes.BlockNode; +import jadx.core.dex.nodes.IMethodDetails; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.dex.trycatch.ExcHandlerAttr; import jadx.core.dex.visitors.AbstractVisitor; +import jadx.core.dex.visitors.AttachMethodDetails; import jadx.core.dex.visitors.ConstInlineVisitor; import jadx.core.dex.visitors.InitCodeVariables; import jadx.core.dex.visitors.JadxVisitor; @@ -48,7 +50,8 @@ import jadx.core.utils.Utils; desc = "Calculate best types for SSA variables", runAfter = { SSATransform.class, - ConstInlineVisitor.class + ConstInlineVisitor.class, + AttachMethodDetails.class } ) public final class TypeInferenceVisitor extends AbstractVisitor { @@ -68,11 +71,16 @@ public final class TypeInferenceVisitor extends AbstractVisitor { if (mth.isNoCode()) { return; } + if (Consts.DEBUG) { + LOG.info("Start type inference in method: {}", mth); + } boolean resolved = runTypePropagation(mth); if (!resolved) { boolean moveAdded = false; for (SSAVar var : new ArrayList<>(mth.getSVars())) { - moveAdded |= tryInsertAdditionalInsn(mth, var); + if (tryInsertAdditionalInsn(mth, var)) { + moveAdded = true; + } } if (moveAdded) { InitCodeVariables.rerun(mth); @@ -269,11 +277,10 @@ public final class TypeInferenceVisitor extends AbstractVisitor { } private ITypeBound makeAssignInvokeBound(InvokeNode invokeNode) { - MethodInfo callMth = invokeNode.getCallMth(); - ArgType boundType = callMth.getReturnType(); - ArgType genericReturnType = root.getMethodGenericReturnType(callMth); + ArgType boundType = invokeNode.getCallMth().getReturnType(); + ArgType genericReturnType = root.getMethodUtils().getMethodGenericReturnType(invokeNode); if (genericReturnType != null) { - if (genericReturnType.containsGenericType()) { + if (genericReturnType.containsTypeVariable()) { InvokeType invokeType = invokeNode.getInvokeType(); if (invokeNode.getArgsCount() != 0 && invokeType != InvokeType.STATIC && invokeType != InvokeType.SUPER) { @@ -292,6 +299,15 @@ public final class TypeInferenceVisitor extends AbstractVisitor { if (insn == null) { return null; } + if (insn instanceof BaseInvokeNode) { + IMethodDetails methodDetails = root.getMethodUtils().getMethodDetails((BaseInvokeNode) insn); + if (methodDetails != null) { + if (methodDetails.getArgTypes().stream().anyMatch(ArgType::containsTypeVariable)) { + // don't add const bound for generic type variables + return null; + } + } + } return new TypeBoundConst(BoundEnum.USE, regArg.getInitType(), regArg); } @@ -361,50 +377,67 @@ public final class TypeInferenceVisitor extends AbstractVisitor { } } for (PhiInsn phiInsn : usedInPhiList) { - if (!insertMoveForPhi(mth, phiInsn, var)) { + if (!insertMoveForPhi(mth, phiInsn, var, false)) { return false; } } + + // all check passed => apply + for (PhiInsn phiInsn : usedInPhiList) { + insertMoveForPhi(mth, phiInsn, var, true); + } + mth.addComment("JADX INFO: additional move instructions added (" + usedInPhiList.size() + ") to help type inference"); return true; } - private boolean insertMoveForPhi(MethodNode mth, PhiInsn phiInsn, SSAVar var) { + private boolean insertMoveForPhi(MethodNode mth, PhiInsn phiInsn, SSAVar var, boolean apply) { int argsCount = phiInsn.getArgsCount(); for (int argIndex = 0; argIndex < argsCount; argIndex++) { RegisterArg reg = phiInsn.getArg(argIndex); if (reg.getSVar() == var) { - BlockNode blockNode = phiInsn.getBlockByArgIndex(argIndex); - InsnNode lastInsn = BlockUtils.getLastInsn(blockNode); - if (lastInsn != null && BlockSplitter.isSeparate(lastInsn.getType())) { - // can't insert move in block with separate instruction - // trying previous block - List preds = blockNode.getPredecessors(); - if (preds.size() == 1) { - blockNode = preds.get(0); - } else { - mth.addWarn("Failed to insert additional move for type inference"); - return false; - } + BlockNode startBlock = phiInsn.getBlockByArgIndex(argIndex); + BlockNode blockNode = checkBlockForInsnInsert(startBlock); + if (blockNode == null) { + mth.addWarnComment("Failed to insert an additional move for type inference into block " + startBlock); + return false; } + if (apply) { + int regNum = reg.getRegNum(); + RegisterArg resultArg = reg.duplicate(regNum, null); + SSAVar newSsaVar = mth.makeNewSVar(regNum, resultArg); + RegisterArg arg = reg.duplicate(regNum, var); - int regNum = reg.getRegNum(); - RegisterArg resultArg = reg.duplicate(regNum, null); - SSAVar newSsaVar = mth.makeNewSVar(regNum, resultArg); - RegisterArg arg = reg.duplicate(regNum, var); + InsnNode moveInsn = new InsnNode(InsnType.MOVE, 1); + moveInsn.setResult(resultArg); + moveInsn.addArg(arg); + moveInsn.add(AFlag.SYNTHETIC); + blockNode.getInstructions().add(moveInsn); - InsnNode moveInsn = new InsnNode(InsnType.MOVE, 1); - moveInsn.setResult(resultArg); - moveInsn.addArg(arg); - moveInsn.add(AFlag.SYNTHETIC); - blockNode.getInstructions().add(moveInsn); - - phiInsn.replaceArg(reg, reg.duplicate(regNum, newSsaVar)); + phiInsn.replaceArg(reg, reg.duplicate(regNum, newSsaVar)); + } return true; } } return false; } + @Nullable + private BlockNode checkBlockForInsnInsert(BlockNode blockNode) { + if (blockNode.isSynthetic()) { + return null; + } + InsnNode lastInsn = BlockUtils.getLastInsn(blockNode); + if (lastInsn != null && BlockSplitter.isSeparate(lastInsn.getType())) { + // can't insert move in a block with 'separate' instruction => try previous block by simple path + List preds = blockNode.getPredecessors(); + if (preds.size() == 1) { + return checkBlockForInsnInsert(preds.get(0)); + } + return null; + } + return blockNode; + } + private boolean tryWiderObjects(MethodNode mth, SSAVar var) { Set objTypes = new LinkedHashSet<>(); for (ITypeBound bound : var.getTypeInfo().getBounds()) { diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeSearch.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeSearch.java index ab5517339..bd8e7873d 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeSearch.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeSearch.java @@ -67,9 +67,10 @@ public class TypeSearch { LOG.warn("Multi-variable search failed in {}", mth); } } - - boolean applySuccess = applyResolvedVars(); - return searchSuccess && applySuccess; + if (searchSuccess) { + return applyResolvedVars(); + } + return false; } private boolean applyResolvedVars() { diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeUpdate.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeUpdate.java index 636293a9c..35f6ccb51 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeUpdate.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeUpdate.java @@ -12,7 +12,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.Consts; -import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.InvokeNode; import jadx.core.dex.instructions.args.ArgType; @@ -22,7 +21,6 @@ import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.RootNode; -import jadx.core.utils.TypeUtils; import jadx.core.utils.exceptions.JadxOverflowException; import jadx.core.utils.exceptions.JadxRuntimeException; @@ -79,8 +77,9 @@ public final class TypeUpdate { return SAME; } if (Consts.DEBUG) { - LOG.debug("Applying types, init for {} -> {}", ssaVar, candidateType); - updates.forEach(updateEntry -> LOG.debug(" {} -> {}", updateEntry.getType(), updateEntry.getArg())); + LOG.debug("Applying types for {} -> {}", ssaVar, candidateType); + updates.forEach(updateEntry -> LOG.debug(" {} -> {}, insn: {}", + updateEntry.getType(), updateEntry.getArg(), updateEntry.getArg().getParentInsn())); } updateInfo.applyUpdates(); return CHANGED; @@ -97,13 +96,13 @@ public final class TypeUpdate { TypeCompareEnum compareResult = comparator.compareTypes(candidateType, currentType); if (arg.isTypeImmutable() && currentType != ArgType.UNKNOWN) { // don't changed type - if (compareResult == TypeCompareEnum.CONFLICT) { - if (Consts.DEBUG) { - LOG.debug("Type rejected for {} due to conflict: candidate={}, current={}", arg, candidateType, currentType); - } - return REJECT; + if (compareResult == TypeCompareEnum.EQUAL) { + return SAME; } - return SAME; + if (Consts.DEBUG) { + LOG.debug("Type rejected for {} due to conflict: candidate={}, current={}", arg, candidateType, currentType); + } + return REJECT; } if (compareResult.isWider() && !updateInfo.getFlags().isAllowWider()) { if (Consts.DEBUG) { @@ -282,18 +281,17 @@ public final class TypeUpdate { if (insn.getResult() == null) { return SAME; } - if (candidateType.isGeneric() || candidateType.isGenericType()) { + if (candidateType.containsTypeVariable()) { InvokeNode invokeNode = (InvokeNode) insn; - MethodInfo callMth = invokeNode.getCallMth(); if (isAssign(insn, arg)) { // TODO: implement backward type propagation (from result to instance) return SAME; } else { - ArgType returnType = root.getMethodGenericReturnType(callMth); + ArgType returnType = root.getMethodUtils().getMethodGenericReturnType(invokeNode); if (returnType == null) { return SAME; } - ArgType resultGeneric = TypeUtils.replaceClassGenerics(root, candidateType, returnType); + ArgType resultGeneric = root.getTypeUtils().replaceClassGenerics(candidateType, returnType); if (resultGeneric == null) { return SAME; } @@ -414,7 +412,7 @@ public final class TypeUpdate { TypeUpdateResult result = updateTypeChecked(updateInfo, putArg, arrayElement); if (result == REJECT) { ArgType putType = putArg.getType(); - if (putType.isTypeKnown() && putType.isObject()) { + if (putType.isTypeKnown() && !putType.isPrimitive()) { TypeCompareEnum compResult = comparator.compareTypes(arrayElement, putType); if (compResult == TypeCompareEnum.WIDER || compResult == TypeCompareEnum.WIDER_BY_GENERIC) { // allow wider result (i.e allow put in Object[] any objects) diff --git a/jadx-core/src/main/java/jadx/core/utils/BlockInsnPair.java b/jadx-core/src/main/java/jadx/core/utils/BlockInsnPair.java new file mode 100644 index 000000000..44c56048d --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/utils/BlockInsnPair.java @@ -0,0 +1,46 @@ +package jadx.core.utils; + +import java.util.Objects; + +import jadx.core.dex.nodes.BlockNode; +import jadx.core.dex.nodes.InsnNode; + +public class BlockInsnPair { + private final BlockNode block; + private final InsnNode insn; + + public BlockInsnPair(BlockNode block, InsnNode insn) { + this.block = block; + this.insn = insn; + } + + public BlockNode getBlock() { + return block; + } + + public InsnNode getInsn() { + return insn; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof BlockInsnPair)) { + return false; + } + BlockInsnPair that = (BlockInsnPair) o; + return block.equals(that.block) && insn.equals(that.insn); + } + + @Override + public int hashCode() { + return Objects.hash(block, insn); + } + + @Override + public String toString() { + return "BlockInsnPair{" + block + ": " + insn + '}'; + } +} diff --git a/jadx-core/src/main/java/jadx/core/utils/BlockUtils.java b/jadx-core/src/main/java/jadx/core/utils/BlockUtils.java index 256a594fd..d076e03a7 100644 --- a/jadx-core/src/main/java/jadx/core/utils/BlockUtils.java +++ b/jadx-core/src/main/java/jadx/core/utils/BlockUtils.java @@ -4,9 +4,11 @@ import java.util.ArrayList; import java.util.BitSet; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Set; import org.jetbrains.annotations.Nullable; @@ -285,7 +287,19 @@ public class BlockUtils { return null; } - public static BitSet blocksToBitSet(MethodNode mth, List blocks) { + public static BitSet newBlocksBitSet(MethodNode mth) { + return new BitSet(mth.getBasicBlocks().size()); + } + + public static BitSet copyBlocksBitSet(MethodNode mth, BitSet bitSet) { + BitSet copy = new BitSet(mth.getBasicBlocks().size()); + if (!bitSet.isEmpty()) { + copy.or(bitSet); + } + return copy; + } + + public static BitSet blocksToBitSet(MethodNode mth, Collection blocks) { BitSet bs = new BitSet(mth.getBasicBlocks().size()); for (BlockNode block : blocks) { bs.set(block.getId()); @@ -293,8 +307,16 @@ public class BlockUtils { return bs; } + @Nullable + public static BlockNode bitSetToOneBlock(MethodNode mth, BitSet bs) { + if (bs == null || bs.cardinality() != 1) { + return null; + } + return mth.getBasicBlocks().get(bs.nextSetBit(0)); + } + public static List bitSetToBlocks(MethodNode mth, BitSet bs) { - if (bs == null) { + if (bs == null || bs == EmptyBitSet.EMPTY) { return Collections.emptyList(); } int size = bs.cardinality(); @@ -649,4 +671,102 @@ public class BlockUtils { } return false; } + + public static Map calcPostDominance(MethodNode mth) { + return calcPartialPostDominance(mth, mth.getBasicBlocks(), mth.getExitBlocks().get(0)); + } + + public static Map calcPartialPostDominance(MethodNode mth, Collection blockNodes, BlockNode exitBlock) { + int blocksCount = mth.getBasicBlocks().size(); + Map map = new HashMap<>(blocksCount); + + BitSet initSet = new BitSet(blocksCount); + for (BlockNode block : blockNodes) { + initSet.set(block.getId()); + } + + for (BlockNode block : blockNodes) { + BitSet postDoms = new BitSet(blocksCount); + postDoms.or(initSet); + map.put(block, postDoms); + } + BitSet exitBitSet = map.get(exitBlock); + exitBitSet.clear(); + exitBitSet.set(exitBlock.getId()); + + BitSet domSet = new BitSet(blocksCount); + boolean changed; + do { + changed = false; + for (BlockNode block : blockNodes) { + if (block == exitBlock) { + continue; + } + BitSet d = map.get(block); + if (!changed) { + domSet.clear(); + domSet.or(d); + } + for (BlockNode scc : block.getSuccessors()) { + BitSet scPDoms = map.get(scc); + if (scPDoms != null) { + d.and(scPDoms); + } + } + d.set(block.getId()); + if (!changed && !d.equals(domSet)) { + changed = true; + map.put(block, d); + } + } + } while (changed); + + blockNodes.forEach(block -> { + BitSet postDoms = map.get(block); + postDoms.clear(block.getId()); + if (postDoms.isEmpty()) { + map.put(block, EmptyBitSet.EMPTY); + } + }); + return map; + } + + @Nullable + public static BlockNode calcImmediatePostDominator(MethodNode mth, BlockNode block) { + BlockNode oneSuccessor = Utils.getOne(block.getSuccessors()); + if (oneSuccessor != null) { + return oneSuccessor; + } + return calcImmediatePostDominator(mth, block, calcPostDominance(mth)); + } + + @Nullable + public static BlockNode calcPartialImmediatePostDominator(MethodNode mth, BlockNode block, + Collection blockNodes, BlockNode exitBlock) { + BlockNode oneSuccessor = Utils.getOne(block.getSuccessors()); + if (oneSuccessor != null) { + return oneSuccessor; + } + Map pDomsMap = calcPartialPostDominance(mth, blockNodes, exitBlock); + return calcImmediatePostDominator(mth, block, pDomsMap); + } + + @Nullable + public static BlockNode calcImmediatePostDominator(MethodNode mth, BlockNode block, Map postDomsMap) { + BlockNode oneSuccessor = Utils.getOne(block.getSuccessors()); + if (oneSuccessor != null) { + return oneSuccessor; + } + List basicBlocks = mth.getBasicBlocks(); + BitSet postDoms = postDomsMap.get(block); + BitSet bs = copyBlocksBitSet(mth, postDoms); + for (int i = bs.nextSetBit(0); i >= 0; i = bs.nextSetBit(i + 1)) { + BlockNode pdomBlock = basicBlocks.get(i); + BitSet pdoms = postDomsMap.get(pdomBlock); + if (pdoms != null) { + bs.andNot(pdoms); + } + } + return bitSetToOneBlock(mth, bs); + } } diff --git a/jadx-core/src/main/java/jadx/core/utils/DebugChecks.java b/jadx-core/src/main/java/jadx/core/utils/DebugChecks.java index 409d9ea41..3a1b3b28a 100644 --- a/jadx-core/src/main/java/jadx/core/utils/DebugChecks.java +++ b/jadx-core/src/main/java/jadx/core/utils/DebugChecks.java @@ -21,6 +21,8 @@ import jadx.core.dex.visitors.PrepareForCodeGen; import jadx.core.dex.visitors.RenameVisitor; import jadx.core.utils.exceptions.JadxRuntimeException; +import static jadx.core.codegen.CodeWriter.NL; + /** * Check invariants and information consistency for registers and SSA variables */ @@ -86,7 +88,8 @@ public class DebugChecks { List useList = sVar.getUseList(); boolean assignReg = insn.getResult() == reg; if (!assignReg && !Utils.containsInListByRef(useList, reg)) { - throw new JadxRuntimeException("Incorrect use list in ssa var: " + sVar + ", register not listed.\n insn: " + insn); + throw new JadxRuntimeException("Incorrect use list in ssa var: " + sVar + ", register not listed." + + NL + " insn: " + insn); } for (RegisterArg useArg : useList) { checkRegisterArg(mth, useArg); @@ -107,8 +110,8 @@ public class DebugChecks { } BlockNode parentInsnBlock = BlockUtils.getBlockByInsn(mth, parentInsn); if (parentInsnBlock == null) { - parentInsnBlock = BlockUtils.getBlockByInsn(mth, parentInsn); - throw new JadxRuntimeException("Parent insn not found in blocks tree for: " + reg + ",\n insn: " + parentInsn); + throw new JadxRuntimeException("Parent insn not found in blocks tree for: " + reg + + NL + " insn: " + parentInsn); } } } diff --git a/jadx-core/src/main/java/jadx/core/utils/DebugUtils.java b/jadx-core/src/main/java/jadx/core/utils/DebugUtils.java index 16d8dbcb0..217e78a38 100644 --- a/jadx-core/src/main/java/jadx/core/utils/DebugUtils.java +++ b/jadx-core/src/main/java/jadx/core/utils/DebugUtils.java @@ -5,6 +5,7 @@ import java.util.Arrays; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -30,6 +31,8 @@ import jadx.core.dex.visitors.regions.TracedRegionVisitor; import jadx.core.utils.exceptions.CodegenException; import jadx.core.utils.exceptions.JadxException; +import static jadx.core.codegen.CodeWriter.NL; + @Deprecated @TestOnly public class DebugUtils { @@ -97,7 +100,7 @@ public class DebugUtils { CodeWriter cw = new CodeWriter(); cw.startLine('|').add(mth.toString()); printRegion(mth, region, cw, "| ", printInsns); - LOG.debug("\n{}", cw.finish().getCodeStr()); + LOG.debug("{}{}", NL, cw.finish().getCodeStr()); } private static void printRegion(MethodNode mth, IRegion region, CodeWriter cw, String indent, boolean printInsns) { @@ -125,7 +128,7 @@ public class DebugUtils { ig.makeInsn(insn, code); String codeStr = code.finish().getCodeStr(); - List insnStrings = Arrays.stream(codeStr.split(CodeWriter.NL)) + List insnStrings = Arrays.stream(codeStr.split(NL)) .filter(StringUtils::notBlank) .map(s -> "|> " + s) .collect(Collectors.toList()); @@ -147,7 +150,7 @@ public class DebugUtils { private static void printWithAttributes(CodeWriter cw, String indent, String codeStr, IAttributeNode attrNode) { String str = attrNode.isAttrStorageEmpty() ? codeStr : codeStr + ' ' + attrNode.getAttributesString(); - List attrStrings = Arrays.stream(str.split(CodeWriter.NL)) + List attrStrings = Arrays.stream(str.split(NL)) .filter(StringUtils::notBlank) .collect(Collectors.toList()); Iterator it = attrStrings.iterator(); @@ -159,4 +162,11 @@ public class DebugUtils { cw.startLine(indent).add("|+ ").add(it.next()); } } + + public static void printMap(Map map, String desc) { + LOG.debug("Map {} (size = {}):", desc, map.size()); + for (Map.Entry entry : map.entrySet()) { + LOG.debug(" {}: {}", entry.getKey(), entry.getValue()); + } + } } diff --git a/jadx-core/src/main/java/jadx/core/utils/ImmutableList.java b/jadx-core/src/main/java/jadx/core/utils/ImmutableList.java index c399e5d16..70f226241 100644 --- a/jadx-core/src/main/java/jadx/core/utils/ImmutableList.java +++ b/jadx-core/src/main/java/jadx/core/utils/ImmutableList.java @@ -76,7 +76,12 @@ public final class ImmutableList implements List, RandomAccess { @Override public boolean containsAll(@NotNull Collection c) { - throw new UnsupportedOperationException(); + for (Object obj : c) { + if (!contains(obj)) { + return false; + } + } + return true; } @NotNull diff --git a/jadx-core/src/main/java/jadx/core/utils/InsnRemover.java b/jadx-core/src/main/java/jadx/core/utils/InsnRemover.java index 97baa0e1c..d5ff17af0 100644 --- a/jadx-core/src/main/java/jadx/core/utils/InsnRemover.java +++ b/jadx-core/src/main/java/jadx/core/utils/InsnRemover.java @@ -20,6 +20,8 @@ import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.exceptions.JadxRuntimeException; +import static jadx.core.codegen.CodeWriter.NL; + /** * Helper class for correct instructions removing, * can be used while iterating over instructions list @@ -130,10 +132,10 @@ public class InsnRemover { return; } if (Consts.DEBUG) { // TODO: enable this - throw new JadxRuntimeException("Can't remove SSA var, still in use, count: " + useCount - + ", list:\n " + ssaVar.getUseList().stream() + throw new JadxRuntimeException("Can't remove SSA var, still in use, count: " + useCount + ", list:" + + NL + " " + ssaVar.getUseList().stream() .map(arg -> arg + " from " + arg.getParentInsn()) - .collect(Collectors.joining("\n "))); + .collect(Collectors.joining(NL + " "))); } } @@ -168,8 +170,10 @@ public class InsnRemover { } } if (!found && Consts.DEBUG) { // TODO: enable this - throw new JadxRuntimeException("Can't remove insn:\n " + rem - + "\nnot found in list:\n " + Utils.listToString(insns, "\n ")); + throw new JadxRuntimeException("Can't remove insn:" + + NL + " " + rem + + NL + " not found in list:" + + NL + " " + Utils.listToString(insns, NL + " ")); } } } diff --git a/jadx-core/src/main/java/jadx/core/utils/SmaliUtils.java b/jadx-core/src/main/java/jadx/core/utils/SmaliUtils.java index aea2a162f..6ac93d7e4 100644 --- a/jadx-core/src/main/java/jadx/core/utils/SmaliUtils.java +++ b/jadx-core/src/main/java/jadx/core/utils/SmaliUtils.java @@ -37,7 +37,7 @@ public class SmaliUtils { try { Path path = dex.getDexFile().getPath(); DexBackedDexFile dexFile = DexFileFactory.loadDexFile(path.toFile(), null); - DexBackedClassDef dexBackedClassDef = new DexBackedClassDef(dexFile, clsDefOffset); + DexBackedClassDef dexBackedClassDef = new DexBackedClassDef(dexFile, clsDefOffset, 0); getSmaliCode(dexBackedClassDef, stringWriter); return true; } catch (Exception e) { diff --git a/jadx-core/src/main/java/jadx/core/utils/TypeUtils.java b/jadx-core/src/main/java/jadx/core/utils/TypeUtils.java deleted file mode 100644 index 13a35bb9b..000000000 --- a/jadx-core/src/main/java/jadx/core/utils/TypeUtils.java +++ /dev/null @@ -1,119 +0,0 @@ -package jadx.core.utils; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.jetbrains.annotations.Nullable; - -import jadx.core.dex.info.MethodInfo; -import jadx.core.dex.instructions.CallMthInterface; -import jadx.core.dex.instructions.args.ArgType; -import jadx.core.dex.instructions.args.InsnArg; -import jadx.core.dex.nodes.GenericInfo; -import jadx.core.dex.nodes.InsnNode; -import jadx.core.dex.nodes.RootNode; -import jadx.core.utils.exceptions.JadxRuntimeException; - -public class TypeUtils { - /** - * Replace generic types in {@code typeWithGeneric} using instance types - *
- * Example: - *
    - *
  • {@code instanceType: Set} - *
  • {@code typeWithGeneric: Iterator} - *
  • {@code return: Iterator} - *
- */ - @Nullable - public static ArgType replaceClassGenerics(RootNode root, ArgType instanceType, ArgType typeWithGeneric) { - if (typeWithGeneric == null) { - return null; - } - if (instanceType.isGeneric()) { - List generics = root.getClassGenerics(instanceType); - if (generics.isEmpty()) { - return null; - } - ArgType[] actualTypes = instanceType.getGenericTypes(); - if (actualTypes == null) { - return null; - } - int genericParamsCount = actualTypes.length; - if (genericParamsCount != generics.size()) { - return null; - } - Map replaceMap = new HashMap<>(genericParamsCount); - for (int i = 0; i < genericParamsCount; i++) { - ArgType actualType = actualTypes[i]; - ArgType genericType = generics.get(i).getGenericType(); - replaceMap.put(genericType, actualType); - } - return replaceGenericUsingTypeMap(typeWithGeneric, replaceMap); - } - return null; - } - - @Nullable - public static ArgType replaceMethodGenerics(RootNode root, InsnNode invokeInsn, ArgType typeWithGeneric) { - if (typeWithGeneric == null) { - return null; - } - if (!(invokeInsn instanceof CallMthInterface)) { - throw new JadxRuntimeException("Expected CallMthInterface, got: " + invokeInsn.getClass()); - } - CallMthInterface callInsn = (CallMthInterface) invokeInsn; - MethodInfo mthInfo = callInsn.getCallMth(); - List methodArgTypes = root.getMethodArgTypes(mthInfo); - if (methodArgTypes.isEmpty()) { - return null; - } - int firstArgOffset = callInsn.getFirstArgOffset(); - int argsCount = methodArgTypes.size(); - for (int i = 0; i < argsCount; i++) { - ArgType methodArgType = methodArgTypes.get(i); - InsnArg insnArg = invokeInsn.getArg(i + firstArgOffset); - ArgType insnType = insnArg.getType(); - if (methodArgType.equals(typeWithGeneric)) { - return insnType; - } - } - // TODO build complete map for type variables - return null; - } - - private static ArgType replaceGenericUsingTypeMap(ArgType replaceType, Map replaceMap) { - if (replaceType.isGenericType()) { - return replaceMap.get(replaceType); - } - - ArgType wildcardType = replaceType.getWildcardType(); - if (wildcardType != null && wildcardType.containsGenericType()) { - ArgType newWildcardType = replaceGenericUsingTypeMap(wildcardType, replaceMap); - if (newWildcardType == null) { - return null; - } - return ArgType.wildcard(newWildcardType, replaceType.getWildcardBound()); - } - - ArgType[] genericTypes = replaceType.getGenericTypes(); - if (replaceType.isGeneric() && genericTypes != null && genericTypes.length != 0) { - int size = genericTypes.length; - ArgType[] newTypes = new ArgType[size]; - for (int i = 0; i < size; i++) { - ArgType genericType = genericTypes[i]; - ArgType type = replaceGenericUsingTypeMap(genericType, replaceMap); - if (type == null) { - type = genericType; - } - newTypes[i] = type; - } - return ArgType.generic(replaceType.getObject(), newTypes); - } - return null; - } - - private TypeUtils() { - } -} diff --git a/jadx-core/src/main/java/jadx/core/utils/Utils.java b/jadx-core/src/main/java/jadx/core/utils/Utils.java index a775df368..daf73e7e8 100644 --- a/jadx-core/src/main/java/jadx/core/utils/Utils.java +++ b/jadx-core/src/main/java/jadx/core/utils/Utils.java @@ -247,6 +247,13 @@ public class Utils { return list.get(list.size() - 1); } + public static T getOrElse(@Nullable T obj, T defaultObj) { + if (obj == null) { + return defaultObj; + } + return obj; + } + public static boolean isEmpty(Collection col) { return col == null || col.isEmpty(); } diff --git a/jadx-core/src/main/java/jadx/core/utils/files/InputFile.java b/jadx-core/src/main/java/jadx/core/utils/files/InputFile.java index 6b1cf9ee2..27c3bf188 100644 --- a/jadx-core/src/main/java/jadx/core/utils/files/InputFile.java +++ b/jadx-core/src/main/java/jadx/core/utils/files/InputFile.java @@ -26,6 +26,7 @@ import jadx.core.utils.exceptions.DecodeException; import jadx.core.utils.exceptions.JadxException; import jadx.core.utils.exceptions.JadxRuntimeException; +import static jadx.core.codegen.CodeWriter.NL; import static jadx.core.utils.files.FileUtils.isApkFile; import static jadx.core.utils.files.FileUtils.isZipDexFile; @@ -197,7 +198,7 @@ public class InputFile { } return pathList; } catch (Exception e) { - throw new DecodeException("java class to dex conversion error:\n " + e.getMessage(), e); + throw new DecodeException("java class to dex conversion error:" + NL + " " + e.getMessage(), e); } finally { if (j2d.isError()) { LOG.warn("dx message: {}", j2d.getDxErrors()); diff --git a/jadx-core/src/test/java/jadx/core/dex/instructions/args/ArgTypeTest.java b/jadx-core/src/test/java/jadx/core/dex/instructions/args/ArgTypeTest.java index fbed8cad4..8a70a8e2d 100644 --- a/jadx-core/src/test/java/jadx/core/dex/instructions/args/ArgTypeTest.java +++ b/jadx-core/src/test/java/jadx/core/dex/instructions/args/ArgTypeTest.java @@ -24,10 +24,10 @@ class ArgTypeTest { @Test void testContainsGenericType() { ArgType wildcard = ArgType.wildcard(ArgType.genericType("T"), ArgType.WildcardBound.SUPER); - assertTrue(wildcard.containsGenericType()); + assertTrue(wildcard.containsTypeVariable()); ArgType type = ArgType.generic("java.lang.List", wildcard); - assertTrue(type.containsGenericType()); + assertTrue(type.containsTypeVariable()); } @Test diff --git a/jadx-core/src/test/java/jadx/core/dex/visitors/typeinference/TypeCompareTest.java b/jadx-core/src/test/java/jadx/core/dex/visitors/typeinference/TypeCompareTest.java index f96c01b69..035a35892 100644 --- a/jadx-core/src/test/java/jadx/core/dex/visitors/typeinference/TypeCompareTest.java +++ b/jadx-core/src/test/java/jadx/core/dex/visitors/typeinference/TypeCompareTest.java @@ -5,28 +5,38 @@ import java.util.Collections; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import jadx.NotYetImplemented; import jadx.NotYetImplementedExtension; import jadx.api.JadxArgs; import jadx.core.dex.instructions.args.ArgType; +import jadx.core.dex.instructions.args.ArgType.WildcardBound; import jadx.core.dex.nodes.RootNode; import static jadx.core.dex.instructions.args.ArgType.BOOLEAN; +import static jadx.core.dex.instructions.args.ArgType.BYTE; import static jadx.core.dex.instructions.args.ArgType.CHAR; +import static jadx.core.dex.instructions.args.ArgType.CLASS; import static jadx.core.dex.instructions.args.ArgType.INT; import static jadx.core.dex.instructions.args.ArgType.NARROW; import static jadx.core.dex.instructions.args.ArgType.NARROW_INTEGRAL; import static jadx.core.dex.instructions.args.ArgType.OBJECT; +import static jadx.core.dex.instructions.args.ArgType.STRING; import static jadx.core.dex.instructions.args.ArgType.UNKNOWN; import static jadx.core.dex.instructions.args.ArgType.UNKNOWN_ARRAY; import static jadx.core.dex.instructions.args.ArgType.UNKNOWN_OBJECT; import static jadx.core.dex.instructions.args.ArgType.array; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; +import static jadx.core.dex.instructions.args.ArgType.generic; +import static jadx.core.dex.instructions.args.ArgType.object; +import static jadx.core.dex.instructions.args.ArgType.wildcard; +import static org.assertj.core.api.Assertions.assertThat; @ExtendWith(NotYetImplementedExtension.class) public class TypeCompareTest { + private static final Logger LOG = LoggerFactory.getLogger(TypeCompareTest.class); + private TypeCompare compare; @BeforeEach @@ -65,23 +75,58 @@ public class TypeCompareTest { firstIsNarrow(array(OBJECT), OBJECT); firstIsNarrow(array(OBJECT), array(UNKNOWN_OBJECT)); + firstIsNarrow(array(STRING), array(UNKNOWN_OBJECT)); + firstIsNarrow(array(STRING), array(OBJECT)); firstIsNarrow(UNKNOWN_ARRAY, OBJECT); + + firstIsNarrow(array(BYTE), OBJECT); + firstIsNarrow(array(array(BYTE)), array(OBJECT)); + + check(array(OBJECT), array(INT), TypeCompareEnum.CONFLICT); + + ArgType integerType = object("java.lang.Integer"); + check(array(OBJECT), array(integerType), TypeCompareEnum.WIDER); + check(array(INT), array(integerType), TypeCompareEnum.CONFLICT); + check(array(INT), array(INT), TypeCompareEnum.EQUAL); + + ArgType wildClass = generic(CLASS, wildcard()); + check(array(wildClass), array(CLASS), TypeCompareEnum.NARROW_BY_GENERIC); + check(array(CLASS), array(wildClass), TypeCompareEnum.WIDER_BY_GENERIC); } @Test public void compareGenerics() { - ArgType mapCls = ArgType.object("java.util.Map"); - ArgType setCls = ArgType.object("java.util.Set"); + ArgType mapCls = object("java.util.Map"); + ArgType setCls = object("java.util.Set"); ArgType keyType = ArgType.genericType("K"); ArgType valueType = ArgType.genericType("V"); ArgType mapGeneric = ArgType.generic(mapCls.getObject(), keyType, valueType); - check(mapGeneric, mapCls, TypeCompareEnum.NARROW_BY_GENERIC); check(mapCls, mapGeneric, TypeCompareEnum.WIDER_BY_GENERIC); - check(mapCls, setCls, TypeCompareEnum.CONFLICT); + + ArgType setGeneric = ArgType.generic(setCls.getObject(), valueType); + ArgType setWildcard = ArgType.generic(setCls.getObject(), ArgType.wildcard()); + + check(setWildcard, setGeneric, TypeCompareEnum.CONFLICT); + check(setWildcard, setCls, TypeCompareEnum.NARROW_BY_GENERIC); + // TODO implement compare for wildcard with bounds + } + + @Test + public void compareWildCards() { + ArgType clsWildcard = generic(CLASS.getObject(), wildcard()); + ArgType clsExtendedWildcard = generic(CLASS.getObject(), wildcard(STRING, WildcardBound.EXTENDS)); + check(clsWildcard, clsExtendedWildcard, TypeCompareEnum.WIDER); + + ArgType listWildcard = generic(CLASS.getObject(), wildcard(object("java.util.List"), WildcardBound.EXTENDS)); + ArgType collWildcard = generic(CLASS.getObject(), wildcard(object("java.util.Collection"), WildcardBound.EXTENDS)); + check(listWildcard, collWildcard, TypeCompareEnum.NARROW); + + ArgType collSuperWildcard = generic(CLASS.getObject(), wildcard(object("java.util.Collection"), WildcardBound.SUPER)); + check(collSuperWildcard, listWildcard, TypeCompareEnum.CONFLICT); } @Test @@ -99,10 +144,7 @@ public class TypeCompareTest { tType.setExtendTypes(Collections.singletonList(ArgType.STRING)); check(tType, ArgType.STRING, TypeCompareEnum.NARROW_BY_GENERIC); - check(ArgType.STRING, tType, TypeCompareEnum.WIDER_BY_GENERIC); - check(tType, ArgType.OBJECT, TypeCompareEnum.NARROW_BY_GENERIC); - check(ArgType.OBJECT, tType, TypeCompareEnum.WIDER_BY_GENERIC); } @Test @@ -111,18 +153,31 @@ public class TypeCompareTest { ArgType vType = ArgType.genericType("V"); // TODO: use extend types from generic declaration for more strict checks check(vType, ArgType.STRING, TypeCompareEnum.CONFLICT); - check(ArgType.STRING, vType, TypeCompareEnum.CONFLICT); + } + + @Test + public void compareOuterGenerics() { + ArgType hashMapType = object("java.util.HashMap"); + ArgType innerEntrySetType = object("EntrySet"); + ArgType firstInstance = ArgType.outerGeneric(generic(hashMapType, STRING, STRING), innerEntrySetType); + ArgType secondInstance = ArgType.outerGeneric(generic(hashMapType, OBJECT, OBJECT), innerEntrySetType); + + check(firstInstance, secondInstance, TypeCompareEnum.NARROW); } private void firstIsNarrow(ArgType first, ArgType second) { check(first, second, TypeCompareEnum.NARROW); - // reverse - check(second, first, TypeCompareEnum.WIDER); } private void check(ArgType first, ArgType second, TypeCompareEnum expectedResult) { - TypeCompareEnum result = compare.compareTypes(first, second); - assertThat("Compare '" + first + "' vs '" + second + '\'', - result, is(expectedResult)); + LOG.debug("Compare: '{}' and '{}', expect: '{}'", first, second, expectedResult); + + assertThat(compare.compareTypes(first, second)) + .as("Compare '%s' and '%s'", first, second) + .isEqualTo(expectedResult); + + assertThat(compare.compareTypes(second, first)) + .as("Compare '%s' and '%s'", second, first) + .isEqualTo(expectedResult.invert()); } } diff --git a/jadx-core/src/test/java/jadx/core/utils/TypeUtilsTest.java b/jadx-core/src/test/java/jadx/core/utils/TypeUtilsTest.java index c04cce9ab..ce1ae066e 100644 --- a/jadx-core/src/test/java/jadx/core/utils/TypeUtilsTest.java +++ b/jadx-core/src/test/java/jadx/core/utils/TypeUtilsTest.java @@ -1,6 +1,5 @@ package jadx.core.utils; -import java.util.Collections; import java.util.List; import org.junit.jupiter.api.BeforeAll; @@ -10,7 +9,7 @@ import org.slf4j.LoggerFactory; import jadx.api.JadxArgs; import jadx.core.dex.instructions.args.ArgType; -import jadx.core.dex.nodes.GenericInfo; +import jadx.core.dex.nodes.GenericTypeParameter; import jadx.core.dex.nodes.RootNode; import static org.hamcrest.MatcherAssert.assertThat; @@ -25,17 +24,16 @@ class TypeUtilsTest { @BeforeAll public static void init() { root = new RootNode(new JadxArgs()); - root.load(Collections.emptyList()); root.initClassPath(); } @Test public void testReplaceGenericsWithWildcards() { // check classpath graph - List classGenerics = root.getClassGenerics(ArgType.object("java.util.ArrayList")); + List classGenerics = root.getTypeUtils().getClassGenerics(ArgType.object("java.util.ArrayList")); assertThat(classGenerics, hasSize(1)); - GenericInfo genericInfo = classGenerics.get(0); - assertThat(genericInfo.getGenericType(), is(ArgType.genericType("E"))); + GenericTypeParameter genericInfo = classGenerics.get(0); + assertThat(genericInfo.getTypeVariable(), is(ArgType.genericType("E"))); assertThat(genericInfo.getExtendsList(), hasSize(0)); // prepare input @@ -46,7 +44,7 @@ class TypeUtilsTest { LOG.debug("generic: {}", generic); // replace - ArgType result = TypeUtils.replaceClassGenerics(root, instanceType, generic); + ArgType result = root.getTypeUtils().replaceClassGenerics(instanceType, generic); LOG.debug("result: {}", result); ArgType expected = ArgType.generic("java.util.List", ArgType.wildcard(ArgType.OBJECT, ArgType.WildcardBound.SUPER)); diff --git a/jadx-core/src/test/java/jadx/tests/api/utils/assertj/JadxCodeAssertions.java b/jadx-core/src/test/java/jadx/tests/api/utils/assertj/JadxCodeAssertions.java index 1a24ac94c..adbe9d7d7 100644 --- a/jadx-core/src/test/java/jadx/tests/api/utils/assertj/JadxCodeAssertions.java +++ b/jadx-core/src/test/java/jadx/tests/api/utils/assertj/JadxCodeAssertions.java @@ -10,6 +10,10 @@ public class JadxCodeAssertions extends AbstractStringAssert super(code, JadxCodeAssertions.class); } + public JadxCodeAssertions containsOne(String substring) { + return countString(1, substring); + } + public JadxCodeAssertions countString(int count, String substring) { isNotNull(); int actualCount = TestUtils.count(actual, substring); diff --git a/jadx-core/src/test/java/jadx/tests/functional/JadxClasspathTest.java b/jadx-core/src/test/java/jadx/tests/functional/JadxClasspathTest.java index e9911980e..8437c6e3f 100644 --- a/jadx-core/src/test/java/jadx/tests/functional/JadxClasspathTest.java +++ b/jadx-core/src/test/java/jadx/tests/functional/JadxClasspathTest.java @@ -1,39 +1,34 @@ package jadx.tests.functional; -import java.io.IOException; +import java.util.Collections; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import jadx.api.JadxArgs; import jadx.core.clsp.ClspGraph; import jadx.core.dex.instructions.args.ArgType; -import jadx.core.dex.nodes.DexNode; import jadx.core.dex.nodes.RootNode; -import jadx.core.utils.exceptions.DecodeException; import static jadx.core.dex.instructions.args.ArgType.STRING; import static jadx.core.dex.instructions.args.ArgType.object; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; public class JadxClasspathTest { private static final String JAVA_LANG_EXCEPTION = "java.lang.Exception"; private static final String JAVA_LANG_THROWABLE = "java.lang.Throwable"; - private DexNode dex; + private RootNode root; private ClspGraph clsp; @BeforeEach - public void initClsp() throws IOException, DecodeException { - clsp = new ClspGraph(); - clsp.load(); - dex = mock(DexNode.class); - RootNode rootNode = mock(RootNode.class); - when(rootNode.getClsp()).thenReturn(clsp); - when(dex.root()).thenReturn(rootNode); + public void initClsp() { + this.root = new RootNode(new JadxArgs()); + this.root.load(Collections.emptyList()); + this.root.initClassPath(); + this.clsp = root.getClsp(); } @Test @@ -44,9 +39,9 @@ public class JadxClasspathTest { assertTrue(clsp.isImplements(JAVA_LANG_EXCEPTION, JAVA_LANG_THROWABLE)); assertFalse(clsp.isImplements(JAVA_LANG_THROWABLE, JAVA_LANG_EXCEPTION)); - assertFalse(ArgType.isCastNeeded(dex, objExc, objThr)); - assertTrue(ArgType.isCastNeeded(dex, objThr, objExc)); + assertFalse(ArgType.isCastNeeded(root, objExc, objThr)); + assertTrue(ArgType.isCastNeeded(root, objThr, objExc)); - assertTrue(ArgType.isCastNeeded(dex, ArgType.OBJECT, STRING)); + assertTrue(ArgType.isCastNeeded(root, ArgType.OBJECT, STRING)); } } diff --git a/jadx-core/src/test/java/jadx/tests/functional/SignatureParserTest.java b/jadx-core/src/test/java/jadx/tests/functional/SignatureParserTest.java index 116023bea..c724aa9cc 100644 --- a/jadx-core/src/test/java/jadx/tests/functional/SignatureParserTest.java +++ b/jadx-core/src/test/java/jadx/tests/functional/SignatureParserTest.java @@ -7,7 +7,7 @@ import org.junit.jupiter.api.Test; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.ArgType.WildcardBound; -import jadx.core.dex.nodes.GenericInfo; +import jadx.core.dex.nodes.GenericTypeParameter; import jadx.core.dex.nodes.parser.SignatureParser; import static jadx.core.dex.instructions.args.ArgType.INT; @@ -92,12 +92,12 @@ class SignatureParserTest { @SuppressWarnings("unchecked") private static void checkGenerics(String g, Object... objs) { - List genericsList = new SignatureParser(g).consumeGenericMap(); - List expectedList = new ArrayList<>(); + List genericsList = new SignatureParser(g).consumeGenericTypeParameters(); + List expectedList = new ArrayList<>(); for (int i = 0; i < objs.length; i += 2) { ArgType generic = genericType((String) objs[i]); List list = (List) objs[i + 1]; - expectedList.add(new GenericInfo(generic, list)); + expectedList.add(new GenericTypeParameter(generic, list)); } assertThat(genericsList, is(expectedList)); } @@ -122,7 +122,7 @@ class SignatureParserTest { @Test public void testBadGenericMap() { - List list = new SignatureParser(" list = new SignatureParser(" 0; + return num; + } + } + } + + @Test + public void test() { + assertThat(getClassNode(TestCls.class)).code() + .containsOne("ONE(1)") + .doesNotContain("Failed to restore enum class"); + } + + @NotYetImplemented("handle java assert") + @Test + public void testNYI() { + assertThat(getClassNode(TestCls.class)).code() + .containsOne("assert num > 0;") + .doesNotContain("$assertionsDisabled") + .doesNotContain("throw new AssertionError()"); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/enums/TestEnumsWithStaticFields.java b/jadx-core/src/test/java/jadx/tests/integration/enums/TestEnumsWithStaticFields.java index f4c669bbb..c7c15d1a5 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/enums/TestEnumsWithStaticFields.java +++ b/jadx-core/src/test/java/jadx/tests/integration/enums/TestEnumsWithStaticFields.java @@ -14,7 +14,7 @@ public class TestEnumsWithStaticFields extends SmaliTest { assertThat(getClassNodeFromSmali()) .code() .containsOnlyOnce("INSTANCE;") - .containsOnlyOnce("private static c sB;") + .containsOnlyOnce("private static c sB") .doesNotContain(" sA") .doesNotContain(" sC") .doesNotContain("private TestEnumsWithStaticFields(String str) {"); diff --git a/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass14.java b/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass14.java index ad2e4df72..f13332a25 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass14.java +++ b/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass14.java @@ -22,6 +22,8 @@ public class TestAnonymousClass14 extends SmaliTest { public void makeAnonymousCls() { use(new Thread(this) { + / * class inner.OuterCls.AnonymousClass1 * / + public void someMethod() { } }); @@ -44,6 +46,7 @@ public class TestAnonymousClass14 extends SmaliTest { public void test() { ClassNode clsNode = getClassNodeFromSmaliFiles("inner", "TestAnonymousClass14", "OuterCls"); String code = clsNode.getCode().toString(); + code = code.replaceAll("/\\*.*?\\*/", ""); // remove block comments assertThat(code, not(containsString("AnonymousClass1"))); assertThat(code, not(containsString("synthetic"))); diff --git a/jadx-core/src/test/java/jadx/tests/integration/inner/TestReplaceConstsInAnnotations2.java b/jadx-core/src/test/java/jadx/tests/integration/inner/TestReplaceConstsInAnnotations2.java new file mode 100644 index 000000000..f4a9c958c --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/inner/TestReplaceConstsInAnnotations2.java @@ -0,0 +1,44 @@ +package jadx.tests.integration.inner; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.Test; + +import jadx.tests.api.IntegrationTest; + +import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; + +public class TestReplaceConstsInAnnotations2 extends IntegrationTest { + + public static class TestCls { + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + public @interface A { + int[] value(); + } + + @A(C.INT_CONST) + public static class C { + public static final int INT_CONST = 23412342; + } + + @A({ C.INT_CONST, C2.INT_CONST }) + public static class C2 { + public static final int INT_CONST = 34563456; + } + } + + @Test + public void test() { + assertThat(getClassNode(TestCls.class)) + .code() + // .containsOne("@A(C.INT_CONST)") // TODO: remove brackets for single element + .containsOne("@A({C.INT_CONST}") + .containsOne("@A({C.INT_CONST, C2.INT_CONST})") + .containsOne("23412342") + .containsOne("34563456"); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/invoke/TestCastInOverloadedInvoke.java b/jadx-core/src/test/java/jadx/tests/integration/invoke/TestCastInOverloadedInvoke.java index e8af5bcd0..1a081f927 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/invoke/TestCastInOverloadedInvoke.java +++ b/jadx-core/src/test/java/jadx/tests/integration/invoke/TestCastInOverloadedInvoke.java @@ -29,24 +29,33 @@ public class TestCastInOverloadedInvoke extends IntegrationTest { } } + public void test3() { + call((String) null); + call((List) null); + call((ArrayList) null); + } + public void call(String str) { c += 1; } public void call(List list) { - c += 2; + c += 10; } public void call(ArrayList list) { - c += 4; + c += 100; } public void check() { test(); - assertThat(c, is(2 + 4)); + assertThat(c, is(10 + 100)); c = 0; test2("str"); assertThat(c, is(1)); + c = 0; + test3(); + assertThat(c, is(111)); } } @@ -63,7 +72,7 @@ public class TestCastInOverloadedInvoke extends IntegrationTest { @NotYetImplemented @Test - public void test2() { + public void testNYI() { ClassNode cls = getClassNode(TestCls.class); String code = cls.getCode().toString(); diff --git a/jadx-core/src/test/java/jadx/tests/integration/invoke/TestCastInOverloadedInvoke3.java b/jadx-core/src/test/java/jadx/tests/integration/invoke/TestCastInOverloadedInvoke3.java new file mode 100644 index 000000000..f60353456 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/invoke/TestCastInOverloadedInvoke3.java @@ -0,0 +1,41 @@ +package jadx.tests.integration.invoke; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import jadx.tests.api.IntegrationTest; + +import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; + +/** + * Test cast for 'unknown' but overloaded method + */ +public class TestCastInOverloadedInvoke3 extends IntegrationTest { + + public static class OuterCls { + static int c = 0; + + public static void call(String str) { + c = 1; + } + + public static void call(List list) { + c = 10; + } + } + + public static class TestCls { + public void test() { + OuterCls.call((String) null); + } + } + + @Test + public void test() { + disableCompilation(); + assertThat(getClassNode(TestCls.class)) + .code() + .containsOne("OuterCls.call((String) null);"); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/invoke/TestHierarchyOverloadedInvoke.java b/jadx-core/src/test/java/jadx/tests/integration/invoke/TestHierarchyOverloadedInvoke.java new file mode 100644 index 000000000..7569de41a --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/invoke/TestHierarchyOverloadedInvoke.java @@ -0,0 +1,100 @@ +package jadx.tests.integration.invoke; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import jadx.NotYetImplemented; +import jadx.core.dex.nodes.ClassNode; +import jadx.tests.api.IntegrationTest; + +import static jadx.tests.api.utils.JadxMatchers.containsOne; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +public class TestHierarchyOverloadedInvoke extends IntegrationTest { + + public static class TestCls { + static int c = 0; + B b = new B(); + + public interface I { + default void call(String str) { + c += 1; + } + } + + public static class A implements I { + public void call(List list) { + c += 10; + } + } + + public static class B extends A { + public void call(ArrayList list) { + c += 100; + } + } + + public void test() { + b.call(new ArrayList<>()); + b.call((List) new ArrayList()); + } + + public void test2(Object obj) { + if (obj instanceof String) { + b.call((String) obj); + } + } + + public void test3() { + b.call((String) null); + b.call((List) null); + b.call((ArrayList) null); + } + + public void test4() { + ((I) b).call(null); + ((A) b).call((String) null); + ((A) b).call((List) null); + } + + public void check() { + test(); + assertThat(c, is(10 + 100)); + + c = 0; + test2("str"); + assertThat(c, is(1)); + + c = 0; + test3(); + assertThat(c, is(111)); + + c = 0; + test4(); + assertThat(c, is(12)); + } + } + + @Test + public void test() { + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + assertThat(code, containsOne("b.call((ArrayList) new ArrayList());")); + assertThat(code, containsOne("b.call((List) new ArrayList());")); + + assertThat(code, containsOne("b.call((String) obj);")); + } + + @NotYetImplemented + @Test + public void test2() { + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + assertThat(code, containsOne("b.call(new ArrayList<>());")); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/invoke/TestSuperInvokeWithGenerics.java b/jadx-core/src/test/java/jadx/tests/integration/invoke/TestSuperInvokeWithGenerics.java index f7693bc21..dd6f96e58 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/invoke/TestSuperInvokeWithGenerics.java +++ b/jadx-core/src/test/java/jadx/tests/integration/invoke/TestSuperInvokeWithGenerics.java @@ -12,7 +12,7 @@ public class TestSuperInvokeWithGenerics extends IntegrationTest { public static class TestCls { - public class A { + public static class A { public A(T t) { System.out.println("t" + t); } @@ -22,7 +22,7 @@ public class TestSuperInvokeWithGenerics extends IntegrationTest { } } - public class B extends A { + public static class B extends A { public B(String s) { super(s); } diff --git a/jadx-core/src/test/java/jadx/tests/integration/others/TestDeboxing2.java b/jadx-core/src/test/java/jadx/tests/integration/others/TestDeboxing2.java index 7bdd21745..91066c011 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/others/TestDeboxing2.java +++ b/jadx-core/src/test/java/jadx/tests/integration/others/TestDeboxing2.java @@ -39,7 +39,7 @@ public class TestDeboxing2 extends IntegrationTest { assertThat(code, containsOne("l = 0L;")); // checks for 'check' method - assertThat(code, containsOne("test((Long) null)")); // TODO: cast not needed + assertThat(code, containsOne("test(null)")); assertThat(code, containsOne("test(0L)")); assertThat(code, countString(2, "is(0L)")); assertThat(code, containsOne("test(7L)")); diff --git a/jadx-core/src/test/java/jadx/tests/integration/others/TestInsnsBeforeSuper2.java b/jadx-core/src/test/java/jadx/tests/integration/others/TestInsnsBeforeSuper2.java new file mode 100644 index 000000000..92dbd26ff --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/others/TestInsnsBeforeSuper2.java @@ -0,0 +1,41 @@ +package jadx.tests.integration.others; + +import org.junit.jupiter.api.Test; + +import jadx.tests.api.SmaliTest; + +import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; + +public class TestInsnsBeforeSuper2 extends SmaliTest { + // @formatter:off + /* + public class TestInsnsBeforeSuper2 extends java.lang.Exception { + private int mErrorType; + + public TestInsnsBeforeSuper2(java.lang.String r9, int r10) { + r8 = this; + r0 = r8 + r1 = r9 + r2 = r10 + r3 = r0 + r4 = r1 + r5 = r2 + r6 = r1 + r0.(r6) + r7 = 0 + r0.mErrorType = r7 + r0.mErrorType = r2 + return + } + } + */ + // @formatter:on + + @Test + public void test() { + assertThat(getClassNodeFromSmali()) + .code() + .containsOne("super(message);") + .containsOne("this.mErrorType = errorType;"); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/others/TestLoopInTry2.java b/jadx-core/src/test/java/jadx/tests/integration/others/TestLoopInTry2.java index 24e789b24..9942167eb 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/others/TestLoopInTry2.java +++ b/jadx-core/src/test/java/jadx/tests/integration/others/TestLoopInTry2.java @@ -45,7 +45,7 @@ public class TestLoopInTry2 extends IntegrationTest { assertThat(code, containsOne("try {")); assertThat(code, containsOne("while (in.hasMore()) {")); - assertThat(code, containsOne("decoded[in.cursor()] = DecodedInstruction.decode(in);")); + assertThat(code, containsOne("decoded[in.cursor()] = DecodedInstruction.decode(")); assertThat(code, containsOne("} catch (EOFException e) {")); assertThat(code, containsOne("throw new DecodeException")); } diff --git a/jadx-core/src/test/java/jadx/tests/integration/others/TestShadowingSuperMember.java b/jadx-core/src/test/java/jadx/tests/integration/others/TestShadowingSuperMember.java new file mode 100644 index 000000000..22aaf374d --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/others/TestShadowingSuperMember.java @@ -0,0 +1,47 @@ +package jadx.tests.integration.others; + +import org.junit.jupiter.api.Test; + +import jadx.core.dex.nodes.ClassNode; +import jadx.tests.api.SmaliTest; + +import static jadx.tests.api.utils.JadxMatchers.containsOne; +import static org.hamcrest.MatcherAssert.assertThat; + +public class TestShadowingSuperMember extends SmaliTest { + // @formatter:off + /* + + public class C { + public C(String s) { + } + } + + public class A { + public int A00; + public A(String s) { + } + } + + public class B extends A { + public C A00; + public B(String str) { + super(str); + } + + public int add(int b) { + return super.A00 + b; + } + } + */ + // @formatter:on + + @Test + public void test() { + allowWarnInCode(); + ClassNode cls = getClassNodeFromSmaliFiles("B"); + String code = cls.getCode().toString(); + + assertThat(code, containsOne("return super.A00 + ")); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/others/TestStringBuilderElimination2.java b/jadx-core/src/test/java/jadx/tests/integration/others/TestStringBuilderElimination2.java index d478c0309..bab664d8a 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/others/TestStringBuilderElimination2.java +++ b/jadx-core/src/test/java/jadx/tests/integration/others/TestStringBuilderElimination2.java @@ -27,7 +27,7 @@ public class TestStringBuilderElimination2 extends IntegrationTest { public void test1() { ClassNode cls = getClassNode(TestStringBuilderElimination2.TestCls1.class); String code = cls.getCode().toString(); - assertThat(code, containsString("return \"[init]\" + \"a1\" + 'c' + 2 + 0 + 1.0f + 2.0d + true;")); + assertThat(code, containsString("return \"[init]\" + \"a1\" + 'c' + 2 + 0L + 1.0f + 2.0d + true;")); } public static class TestCls2 { @@ -49,7 +49,7 @@ public class TestStringBuilderElimination2 extends IntegrationTest { public void test2() { ClassNode cls = getClassNode(TestStringBuilderElimination2.TestCls2.class); String code = cls.getCode().toString(); - assertThat(code, containsString("return \"[init]\" + \"a1\" + 'c' + 1 + 2 + 1.0f + 2.0d + true;")); + assertThat(code, containsString("return \"[init]\" + \"a1\" + 'c' + 1 + 2L + 1.0f + 2.0d + true;")); } public static class TestClsStringUtilsReverse { diff --git a/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitch2.java b/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitch2.java index 15f471d7e..38e33eb33 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitch2.java +++ b/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitch2.java @@ -17,7 +17,7 @@ public class TestSwitch2 extends IntegrationTest { boolean isScrolling; float multiTouchZoomOldDist; - void test(int action) { + public void test(int action) { switch (action & 255) { case 0: this.isLongtouchable = true; diff --git a/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchFallThrough.java b/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchFallThrough.java index 7e2a0fe9d..4f56cac47 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchFallThrough.java +++ b/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchFallThrough.java @@ -2,7 +2,6 @@ package jadx.tests.integration.switches; import org.junit.jupiter.api.Test; -import jadx.NotYetImplemented; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; @@ -12,6 +11,7 @@ public class TestSwitchFallThrough extends IntegrationTest { public static class TestCls { public int r; + @SuppressWarnings("fallthrough") public void test(int a) { int i = 10; switch (a) { @@ -43,12 +43,14 @@ public class TestSwitchFallThrough extends IntegrationTest { } } - @NotYetImplemented("switch fallthrough") @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() - .containsOnlyOnce("switch"); + .containsOne("switch (a) {") + .containsOne("r = i;") + .containsOne("r = -1;") + .countString(2, "break;"); // code correctness checks done in 'check' method } } diff --git a/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchReturnFromCase.java b/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchReturnFromCase.java index c77012de0..2fec12709 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchReturnFromCase.java +++ b/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchReturnFromCase.java @@ -47,6 +47,8 @@ public class TestSwitchReturnFromCase extends IntegrationTest { String code = cls.getCode().toString(); assertThat(code, containsString("switch (a % 10) {")); + + // case 5: removed assertEquals(5, count(code, "case ")); assertEquals(3, count(code, "break;")); diff --git a/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchWithFallThroughCase.java b/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchWithFallThroughCase.java index 3c6bb05ae..e3ccad501 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchWithFallThroughCase.java +++ b/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchWithFallThroughCase.java @@ -26,6 +26,7 @@ public class TestSwitchWithFallThroughCase extends IntegrationTest { } break; } + // fallthrough case 2: if (b) { str += "2"; diff --git a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally2.java b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally2.java index eceb4f2c5..7a2966302 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally2.java +++ b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally2.java @@ -6,7 +6,8 @@ import java.io.OutputStream; import org.junit.jupiter.api.Test; -import jadx.core.clsp.NClass; +import jadx.core.clsp.ClspClass; +import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; @@ -16,21 +17,21 @@ import static org.hamcrest.MatcherAssert.assertThat; public class TestTryCatchFinally2 extends IntegrationTest { public static class TestCls { - private NClass[] classes; + private ClspClass[] classes; public void test(OutputStream output) throws IOException { DataOutputStream out = new DataOutputStream(output); try { out.writeByte(1); out.writeInt(classes.length); - for (NClass cls : classes) { + for (ClspClass cls : classes) { writeString(out, cls.getName()); } - for (NClass cls : classes) { - NClass[] parents = cls.getParents(); + for (ClspClass cls : classes) { + ArgType[] parents = cls.getParents(); out.writeByte(parents.length); - for (NClass parent : parents) { - out.writeInt(parent.getId()); + for (ArgType parent : parents) { + out.writeInt(parent.getObject().hashCode()); } } } finally { @@ -50,9 +51,9 @@ public class TestTryCatchFinally2 extends IntegrationTest { assertThat(code, containsOne("} finally {")); assertThat(code, containsOne("out.close();")); - assertThat(code, containsOne("for (NClass parent : parents) {")); + assertThat(code, containsOne("for (ArgType parent : parents) {")); - assertThat(code, containsOne("for (NClass cls : this.classes) {")); - assertThat(code, containsOne("for (NClass cls2 : this.classes) {")); + assertThat(code, containsOne("for (ClspClass cls : this.classes) {")); + assertThat(code, containsOne("for (ClspClass cls2 : this.classes) {")); } } diff --git a/jadx-core/src/test/java/jadx/tests/integration/types/TestGenerics2.java b/jadx-core/src/test/java/jadx/tests/integration/types/TestGenerics2.java index 6d1fb65f1..d6e529026 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/types/TestGenerics2.java +++ b/jadx-core/src/test/java/jadx/tests/integration/types/TestGenerics2.java @@ -32,5 +32,6 @@ public class TestGenerics2 extends SmaliTest { assertThat(code, containsOne("Entry next")); assertThat(code, containsOne("useInt(next.getKey().intValue());")); // no Integer cast + assertThat(code, containsOne("next.getValue().trim();")); // no String cast } } diff --git a/jadx-core/src/test/java/jadx/tests/integration/types/TestGenerics4.java b/jadx-core/src/test/java/jadx/tests/integration/types/TestGenerics4.java index 7ed964d60..35d5dd4fb 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/types/TestGenerics4.java +++ b/jadx-core/src/test/java/jadx/tests/integration/types/TestGenerics4.java @@ -51,7 +51,7 @@ public class TestGenerics4 extends IntegrationTest { @NotYetImplemented @Test - public void testNYI() { + public void testOmitCast() { ClassNode cls = getClassNode(TestCls.class); String code = cls.getCode().toString(); diff --git a/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver11.java b/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver11.java new file mode 100644 index 000000000..be491dda7 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver11.java @@ -0,0 +1,51 @@ +package jadx.tests.integration.types; + +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +import jadx.tests.api.IntegrationTest; + +import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; + +public class TestTypeResolver11 extends IntegrationTest { + + public static class TestCls { + public Void test(Object... objects) { + int val = (Integer) objects[0]; + String str = (String) objects[1]; + call(str, str, val, val); + return null; + } + + private void call(String a, String b, int... val) { + } + + private boolean test2(String s1, String... args) { + String str = Arrays.toString(args); + return s1.length() + str.length() > 0; + } + + public void check() { + test(1, "str"); + assertThat(test2("1", "2", "34")).isTrue(); + } + } + + @Test + public void test() { + assertThat(getClassNode(TestCls.class)) + .code() + .containsOne("(Integer) objects[0]") + .containsOne("String str = (String) objects[1];"); + } + + @Test + public void testNoDebug() { + noDebugInfo(); + assertThat(getClassNode(TestCls.class)) + .code() + .containsOne("(Integer) objArr[0]") + .containsOne("String str = (String) objArr[1];"); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver12.java b/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver12.java new file mode 100644 index 000000000..078b85985 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver12.java @@ -0,0 +1,41 @@ +package jadx.tests.integration.types; + +import java.lang.ref.WeakReference; + +import org.junit.jupiter.api.Test; + +import jadx.tests.api.IntegrationTest; + +import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; + +public class TestTypeResolver12 extends IntegrationTest { + + public abstract static class TestCls { + private WeakReference ref; + + public void test(String str) { + T obj = this.ref.get(); + if (obj != null) { + call(obj, str); + } + } + + public abstract void call(T t, String str); + } + + @Test + public void test() { + assertThat(getClassNode(TestCls.class)) + .code() + .containsOne("T obj = this.ref.get();"); + } + + @Test + public void testNoDebug() { + noDebugInfo(); + assertThat(getClassNode(TestCls.class)) + .code() + .doesNotContain("Object obj") + .containsOne("T t = this.ref.get();"); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver13.java b/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver13.java new file mode 100644 index 000000000..ae40038e5 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver13.java @@ -0,0 +1,41 @@ +package jadx.tests.integration.types; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import jadx.NotYetImplemented; +import jadx.tests.api.IntegrationTest; + +import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; + +public class TestTypeResolver13 extends IntegrationTest { + + @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") + public static class TestCls { + private static final Set CONST = new HashSet<>(); + private Map, List> map = new HashMap<>(); + + @SuppressWarnings("unchecked") + public List test(Set type) { + List obj = this.map.get(type == null ? CONST : type); + if (obj != null) { + return (List) obj; + } + return null; + } + } + + @NotYetImplemented("additional cast for generic types") + @Test + public void test() { + assertThat(getClassNode(TestCls.class)) + .code() + .containsOne("public List test(Set type) {") + .containsOne("return (List) obj;"); + } +} diff --git a/jadx-core/src/test/smali/conditions/TestInnerAssign3.smali b/jadx-core/src/test/smali/conditions/TestInnerAssign3.smali new file mode 100644 index 000000000..a24878423 --- /dev/null +++ b/jadx-core/src/test/smali/conditions/TestInnerAssign3.smali @@ -0,0 +1,30 @@ +.class public Lconditions/TestInnerAssign3; +.super LTestSuper; +.source "Test.java" + +.method public test()V + .locals 5 + + const/4 v0, 0 + const/4 v1, 0 + const/4 v4, 0 + + if-eqz v4, :cond_0 + + const/4 v2, 0 + + invoke-virtual {v2}, LTestClass1;->testMethod()LTestClass2; + + move-result-object v0 + + if-eqz v0, :cond_0 + + if-eq v1, v0, :cond_0 + + iget-object v3, v2, LTestClass1;->testField:LTestClass3; + + if-eqz v3, :cond_0 + + :cond_0 + return-void +.end method diff --git a/jadx-core/src/test/smali/others/TestInsnsBeforeSuper2.smali b/jadx-core/src/test/smali/others/TestInsnsBeforeSuper2.smali new file mode 100644 index 000000000..c1701b859 --- /dev/null +++ b/jadx-core/src/test/smali/others/TestInsnsBeforeSuper2.smali @@ -0,0 +1,51 @@ +.class public Lothers/TestInsnsBeforeSuper2; +.super Ljava/lang/Exception; +.source "MyException.java" + +# instance fields +.field private mErrorType:I + + +# direct methods +.method public constructor (Ljava/lang/String;I)V + .locals 8 + + .prologue + move-object v0, p0 + + .local v0, "this":Lothers/TestInsnsBeforeSuper2; + move-object v1, p1 + + .local v1, "message":Ljava/lang/String; + move v2, p2 + + .line 39 + .local v2, "errorType":I + move-object v3, v0 + + .local v3, "this":Lothers/TestInsnsBeforeSuper2; + move-object v4, v1 + + .local v4, "message":Ljava/lang/String; + move v5, v2 + + .line 51 + .end local v0 # "this":Lothers/TestInsnsBeforeSuper2; + .end local v1 # "message":Ljava/lang/String; + .end local v2 # "errorType":I + .local v5, "errorType":I + move-object v6, v1 + + invoke-direct {v0, v6}, Ljava/lang/Exception;->(Ljava/lang/String;)V + + .line 39 + const/4 v7, 0x0 + + iput v7, v0, Lothers/TestInsnsBeforeSuper2;->mErrorType:I + + .line 52 + iput v2, v0, Lothers/TestInsnsBeforeSuper2;->mErrorType:I + + .line 53 + return-void +.end method diff --git a/jadx-core/src/test/smali/others/TestShadowingSuperMember/A.smali b/jadx-core/src/test/smali/others/TestShadowingSuperMember/A.smali new file mode 100644 index 000000000..dee54405e --- /dev/null +++ b/jadx-core/src/test/smali/others/TestShadowingSuperMember/A.smali @@ -0,0 +1,16 @@ +.class public Lothers/A; +.super Ljava/lang/Object; + +# instance fields +.field public A00:I + +# direct methods +.method public constructor (Ljava/lang/String;)V + .registers 3 + + .prologue + + invoke-direct {p0}, Ljava/lang/Object;->()V + + return-void +.end method diff --git a/jadx-core/src/test/smali/others/TestShadowingSuperMember/B.smali b/jadx-core/src/test/smali/others/TestShadowingSuperMember/B.smali new file mode 100644 index 000000000..ddcaa6a6c --- /dev/null +++ b/jadx-core/src/test/smali/others/TestShadowingSuperMember/B.smali @@ -0,0 +1,25 @@ +.class public Lothers/B; +.super Lothers/A; + +.field public A00:Lothers/C; + +# direct methods +.method public constructor (Ljava/lang/String;)V + .registers 3 + + .prologue + invoke-direct {p0, p1}, Lothers/A;->(Ljava/lang/String;)V + + return-void +.end method + + +.method public add(I)I + .registers 3 + + iget v1, p0, Lothers/A;->A00:I + + add-int/2addr v1, p1 + + return v1 +.end method diff --git a/jadx-core/src/test/smali/others/TestShadowingSuperMember/C.smali b/jadx-core/src/test/smali/others/TestShadowingSuperMember/C.smali new file mode 100644 index 000000000..98f21ed6e --- /dev/null +++ b/jadx-core/src/test/smali/others/TestShadowingSuperMember/C.smali @@ -0,0 +1,12 @@ +.class public Lothers/C; +.super Ljava/lang/Object; + +.method public constructor (Ljava/lang/String;)V + .registers 3 + + .prologue + + invoke-direct {p0}, Ljava/lang/Object;->()V + + return-void +.end method diff --git a/jadx-gui/build.gradle b/jadx-gui/build.gradle index e95dafe94..27cf4fa17 100644 --- a/jadx-gui/build.gradle +++ b/jadx-gui/build.gradle @@ -8,16 +8,16 @@ dependencies { compile(project(":jadx-core")) compile(project(":jadx-cli")) - compile 'com.fifesoft:rsyntaxtextarea:3.0.4' + compile 'com.fifesoft:rsyntaxtextarea:3.0.8' compile files('libs/jfontchooser-1.0.5.jar') compile 'hu.kazocsaba:image-viewer:1.2.3' compile 'org.apache.commons:commons-lang3:3.9' compile 'org.apache.commons:commons-text:1.8' - compile 'io.reactivex.rxjava2:rxjava:2.2.15' + compile 'io.reactivex.rxjava2:rxjava:2.2.17' compile "com.github.akarnokd:rxjava2-swing:0.3.7" - compile 'com.android.tools.build:apksig:3.5.2' + compile 'com.android.tools.build:apksig:3.5.3' } application { diff --git a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java index 12ce00a69..81abde3d9 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java @@ -279,6 +279,10 @@ public class JadxSettings extends JadxCLIArgs { this.deobfuscationUseSourceNameAsAlias = deobfuscationUseSourceNameAsAlias; } + public void setDeobfuscationParseKotlinMetadata(boolean deobfuscationParseKotlinMetadata) { + this.deobfuscationParseKotlinMetadata = deobfuscationParseKotlinMetadata; + } + public void updateRenameFlag(JadxArgs.RenameEnum flag, boolean enabled) { if (enabled) { renameFlags.add(flag); @@ -387,6 +391,7 @@ public class JadxSettings extends JadxCLIArgs { if (fromVersion == 0) { setDeobfuscationMinLength(3); setDeobfuscationUseSourceNameAsAlias(true); + setDeobfuscationParseKotlinMetadata(true); setDeobfuscationForceSave(true); setThreadsCount(1); setReplaceConsts(true); diff --git a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java index 140eedf28..3557c57d0 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java @@ -167,15 +167,24 @@ public class JadxSettingsWindow extends JDialog { needReload(); }); + JCheckBox deobfKotlinMetadata = new JCheckBox(); + deobfKotlinMetadata.setSelected(settings.isDeobfuscationParseKotlinMetadata()); + deobfKotlinMetadata.addItemListener(e -> { + settings.setDeobfuscationParseKotlinMetadata(e.getStateChange() == ItemEvent.SELECTED); + needReload(); + }); + SettingsGroup deobfGroup = new SettingsGroup(NLS.str("preferences.deobfuscation")); deobfGroup.addRow(NLS.str("preferences.deobfuscation_on"), deobfOn); deobfGroup.addRow(NLS.str("preferences.deobfuscation_force"), deobfForce); deobfGroup.addRow(NLS.str("preferences.deobfuscation_min_len"), minLenSpinner); deobfGroup.addRow(NLS.str("preferences.deobfuscation_max_len"), maxLenSpinner); deobfGroup.addRow(NLS.str("preferences.deobfuscation_source_alias"), deobfSourceAlias); + deobfGroup.addRow(NLS.str("preferences.deobfuscation_kotlin_metadata"), deobfKotlinMetadata); deobfGroup.end(); - Collection connectedComponents = Arrays.asList(deobfForce, minLenSpinner, maxLenSpinner, deobfSourceAlias); + Collection connectedComponents = + Arrays.asList(deobfForce, minLenSpinner, maxLenSpinner, deobfSourceAlias, deobfKotlinMetadata); deobfOn.addItemListener(e -> enableComponentList(connectedComponents, e.getStateChange() == ItemEvent.SELECTED)); enableComponentList(connectedComponents, settings.isDeobfuscationOn()); return deobfGroup; diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java index 569f28e0b..72932a374 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java @@ -17,11 +17,14 @@ import jadx.api.ResourceFileContent; import jadx.api.ResourceType; import jadx.api.ResourcesLoader; import jadx.api.impl.SimpleCodeInfo; +import jadx.core.utils.Utils; import jadx.core.xmlgen.ResContainer; import jadx.gui.utils.NLS; import jadx.gui.utils.OverlayIcon; import jadx.gui.utils.UiUtils; +import static jadx.core.codegen.CodeWriter.NL; + public class JResource extends JLoadableNode implements Comparable { private static final long serialVersionUID = -201018424302612434L; @@ -151,7 +154,7 @@ public class JResource extends JLoadableNode implements Comparable { return ResourcesLoader.loadToCodeWriter(is); }); } catch (Exception e) { - return new SimpleCodeInfo("Failed to load resource file: \n" + jadx.core.utils.Utils.getStackTrace(e)); + return new SimpleCodeInfo("Failed to load resource file:" + NL + Utils.getStackTrace(e)); } case DECODED_DATA: diff --git a/jadx-gui/src/main/java/jadx/gui/ui/ImagePanel.java b/jadx-gui/src/main/java/jadx/gui/ui/ImagePanel.java index f3d64a029..7693d3a12 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/ImagePanel.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/ImagePanel.java @@ -18,6 +18,8 @@ import jadx.core.xmlgen.ResContainer; import jadx.gui.treemodel.JResource; import jadx.gui.ui.codearea.AbstractCodeArea; +import static jadx.core.codegen.CodeWriter.NL; + public class ImagePanel extends ContentPanel { private static final long serialVersionUID = 4071356367073142688L; @@ -30,7 +32,7 @@ public class ImagePanel extends ContentPanel { add(imageViewer.getComponent()); } catch (Exception e) { RSyntaxTextArea textArea = AbstractCodeArea.getDefaultArea(panel.getMainWindow()); - textArea.setText("Image load error: \n" + Utils.getStackTrace(e)); + textArea.setText("Image load error:" + NL + Utils.getStackTrace(e)); add(textArea); } } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/RenameDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/RenameDialog.java index 9c15ef7ad..c30171c96 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/RenameDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/RenameDialog.java @@ -16,7 +16,6 @@ import java.util.Set; import javax.swing.*; -import jadx.gui.utils.CodeUsageInfo; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,6 +40,7 @@ import jadx.gui.ui.codearea.ClassCodeContentPanel; import jadx.gui.ui.codearea.CodeArea; import jadx.gui.ui.codearea.CodePanel; import jadx.gui.utils.CacheObject; +import jadx.gui.utils.CodeUsageInfo; import jadx.gui.utils.NLS; import jadx.gui.utils.TextStandardActions; diff --git a/jadx-gui/src/main/java/jadx/gui/utils/JNodeCache.java b/jadx-gui/src/main/java/jadx/gui/utils/JNodeCache.java index dee949204..1185c3489 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/JNodeCache.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/JNodeCache.java @@ -1,7 +1,7 @@ package jadx.gui.utils; -import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import jadx.api.JavaClass; import jadx.api.JavaField; @@ -15,7 +15,7 @@ import jadx.gui.treemodel.JNode; public class JNodeCache { - private final Map cache = new HashMap<>(); + private final Map cache = new ConcurrentHashMap<>(); public JNode makeFrom(JavaNode javaNode) { if (javaNode == null) { diff --git a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties index 70573379e..df4db452c 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties @@ -118,6 +118,7 @@ preferences.deobfuscation_force=Deobfuscationskartendatei umschreiben erzwingen preferences.deobfuscation_min_len=Minimale Namenlänge preferences.deobfuscation_max_len=Maximale Namenlänge preferences.deobfuscation_source_alias=Quelldateiname als Klassennamen-Alias verwenden +preferences.deobfuscation_kotlin_metadata=Analysieren Sie Kotlin-Metadaten nach Klassen- und Paketnamen preferences.save=Speichern preferences.cancel=Abbrechen preferences.reset=Zurücksetzen diff --git a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties index 4f08db2c5..636d06faa 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties @@ -118,6 +118,7 @@ preferences.deobfuscation_force=Force rewrite deobfuscation map file preferences.deobfuscation_min_len=Minimum name length preferences.deobfuscation_max_len=Maximum name length preferences.deobfuscation_source_alias=Use source file name as class name alias +preferences.deobfuscation_kotlin_metadata=Parse Kotlin metadata for class and package names preferences.save=Save preferences.cancel=Cancel preferences.reset=Reset diff --git a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties index 07e73eea9..727e2cbac 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties @@ -118,6 +118,7 @@ preferences.deobfuscation_force=Forzar reescritura del fichero de ofuscación preferences.deobfuscation_min_len=Longitud mínima del nombre preferences.deobfuscation_max_len=Longitud máxima del nombre preferences.deobfuscation_source_alias=Usar el nombre del source como alias para la clase +preferences.deobfuscation_kotlin_metadata=Parse Kotlin metadatos para nombres de clase y paquete preferences.save=Guardar preferences.cancel=Cancelar preferences.reset=Reestablecer diff --git a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties index 2e12bc098..9e23bddc8 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties @@ -118,6 +118,7 @@ preferences.deobfuscation_force=强制覆盖反混淆映射文件 preferences.deobfuscation_min_len=最小命名长度 preferences.deobfuscation_max_len=最大命名长度 preferences.deobfuscation_source_alias=使用资源名作为类的别名 +preferences.deobfuscation_kotlin_metadata=解析Kotlin元数据以获得类和包名 preferences.save=保存 preferences.cancel=取消 preferences.reset=重置 diff --git a/lgtm.yml b/lgtm.yml new file mode 100644 index 000000000..846a6e74e --- /dev/null +++ b/lgtm.yml @@ -0,0 +1,14 @@ +path_classifiers: + test: + - jadx-samples + - "**/test/**/*.java" + +extraction: + java: + index: + gradle: + version: 6.1.1 + build_command: ./gradlew clean build + java_version: 11 + properties_files: true + xml_mode: disabled