diff --git a/jadx-core/build.gradle b/jadx-core/build.gradle index f6ccd1389..50fb3e792 100644 --- a/jadx-core/build.gradle +++ b/jadx-core/build.gradle @@ -1,7 +1,6 @@ -ext.jadxClasspath = 'clsp-data/android-5.1.jar' - dependencies { - runtime files(jadxClasspath) + runtime files('clsp-data/android-29-clst.jar') + runtime files('clsp-data/android-29-res.jar') compile files('lib/dx-1.16.jar') // TODO: dx don't support java version > 9 (53) diff --git a/jadx-core/clsp-data/android-29-clst.jar b/jadx-core/clsp-data/android-29-clst.jar new file mode 100644 index 000000000..5a4e2de98 Binary files /dev/null and b/jadx-core/clsp-data/android-29-clst.jar differ diff --git a/jadx-core/clsp-data/android-5.1.jar b/jadx-core/clsp-data/android-29-res.jar similarity index 58% rename from jadx-core/clsp-data/android-5.1.jar rename to jadx-core/clsp-data/android-29-res.jar index 873cf8287..9baf62bc0 100644 Binary files a/jadx-core/clsp-data/android-5.1.jar and b/jadx-core/clsp-data/android-29-res.jar differ 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 49205c061..d241c1339 100644 --- a/jadx-core/src/main/java/jadx/core/clsp/ClsSet.java +++ b/jadx-core/src/main/java/jadx/core/clsp/ClsSet.java @@ -12,6 +12,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -22,9 +23,11 @@ import java.util.zip.ZipOutputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import jadx.core.dex.info.AccessInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.ClassNode; +import jadx.core.dex.nodes.GenericInfo; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.exceptions.DecodeException; @@ -55,7 +58,16 @@ public class ClsSet { private NClass[] classes; - public void load(RootNode root) { + public void loadFromClstFile() throws IOException, DecodeException { + try (InputStream input = getClass().getResourceAsStream(CLST_FILENAME)) { + if (input == null) { + throw new JadxRuntimeException("Can't load classpath file: " + CLST_FILENAME); + } + load(input); + } + } + + public void loadFrom(RootNode root) { List list = root.getClasses(true); Map names = new HashMap<>(list.size()); int k = 0; @@ -68,7 +80,8 @@ public class ClsSet { throw new JadxRuntimeException("Duplicate class: " + clsRawName); } k++; - nClass.setMethods(loadMethods(cls, nClass)); + nClass.setGenerics(cls.getGenerics()); + nClass.setMethods(getMethodsDetails(cls)); } else { names.put(clsRawName, null); } @@ -88,45 +101,43 @@ public class ClsSet { } } - private NMethod[] loadMethods(ClassNode cls, NClass nClass) { + private List getMethodsDetails(ClassNode cls) { List methods = new ArrayList<>(); for (MethodNode m : cls.getMethods()) { - if (!m.getAccessFlags().isPublic() - && !m.getAccessFlags().isProtected()) { - continue; - } - - List args = new ArrayList<>(); - - boolean genericArg = false; - for (RegisterArg r : m.getArguments(false)) { - ArgType argType = r.getType(); - if (argType.isGeneric() || argType.isGenericType()) { - args.add(argType); - genericArg = true; - } else { - args.add(null); - } - } - - ArgType retType = m.getReturnType(); - if (!retType.isGeneric() && !retType.isGenericType()) { - retType = null; - } - - boolean varArgs = m.getAccessFlags().isVarArgs(); - - if (genericArg || retType != null || varArgs) { - methods.add(new NMethod( - m.getMethodInfo().getShortId(), - args.isEmpty() - ? new ArgType[0] - : args.toArray(new ArgType[args.size()]), - retType, - varArgs)); + AccessInfo accessFlags = m.getAccessFlags(); + if (accessFlags.isPublic() || accessFlags.isProtected()) { + processMethodDetails(methods, m, accessFlags); } } - return methods.toArray(new NMethod[methods.size()]); + return methods; + } + + private void processMethodDetails(List methods, MethodNode mth, AccessInfo accessFlags) { + List args = mth.getArguments(false); + 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++) { + RegisterArg arg = args.get(i); + ArgType argType = arg.getType(); + if (argType.isGeneric() || argType.isGenericType()) { + genericArgs[i] = argType; + genericArg = true; + } + } + } + ArgType retType = mth.getReturnType(); + if (!retType.isGeneric() && !retType.isGenericType()) { + retType = null; + } + boolean varArgs = accessFlags.isVarArgs(); + if (genericArg || retType != null || varArgs) { + methods.add(new NMethod(mth.getMethodInfo().getShortId(), genericArgs, retType, varArgs)); + } } public static NClass[] makeParentsArray(ClassNode cls, Map names) { @@ -207,42 +218,58 @@ public class ClsSet { for (NClass parent : parents) { out.writeInt(parent.getId()); } - NMethod[] methods = cls.getMethods(); - out.writeByte(methods.length); + writeGenerics(out, cls, names); + List methods = cls.getMethodsList(); + out.writeByte(methods.size()); for (NMethod method : methods) { writeMethod(out, method, names); } } } + 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 { - int argCount = 0; - ArgType[] argTypes = method.getArgType(); - for (ArgType arg : argTypes) { - if (arg != null) { - argCount++; - } - } - writeLongString(out, method.getShortId()); - 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); + ArgType[] argTypes = method.getGenericArgs(); + if (argTypes == null) { + 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); + } } } - - if (method.getReturnType() != null) { + if (method.getReturnType() == null) { + out.writeBoolean(false); + } else { out.writeBoolean(true); writeArgType(out, method.getReturnType(), names); - } else { - out.writeBoolean(false); } - out.writeBoolean(method.isVarArgs()); } @@ -283,16 +310,7 @@ public class ClsSet { } } - public void load() throws IOException, DecodeException { - try (InputStream input = getClass().getResourceAsStream(CLST_FILENAME)) { - if (input == null) { - throw new JadxRuntimeException("Can't load classpath file: " + CLST_FILENAME); - } - load(input); - } - } - - public void load(File input) throws IOException, DecodeException { + private void load(File input) throws IOException, DecodeException { String name = input.getName(); try (InputStream inputStream = new FileInputStream(input)) { if (name.endsWith(CLST_EXTENSION)) { @@ -313,7 +331,7 @@ public class ClsSet { } } - public void load(InputStream input) throws IOException, DecodeException { + private void load(InputStream input) throws IOException, DecodeException { try (DataInputStream in = new DataInputStream(input)) { byte[] header = new byte[JADX_CLS_SET_HEADER.length()]; int readHeaderLength = in.read(header); @@ -335,18 +353,46 @@ public class ClsSet { for (int j = 0; j < pCount; j++) { parents[j] = classes[in.readInt()]; } - classes[i].setParents(parents); - - int mCount = in.readByte(); - NMethod[] methods = new NMethod[mCount]; - for (int j = 0; j < mCount; j++) { - methods[j] = readMethod(in); - } - classes[i].setMethods(methods); + NClass nClass = classes[i]; + nClass.setParents(parents); + nClass.setGenerics(readGenerics(in)); + nClass.setMethods(readClsMethods(in)); } } } + private List readGenerics(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++) { + 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)); + } + return list; + } + + private List readClsMethods(DataInputStream in) throws IOException { + int mCount = in.readByte(); + List methods = new ArrayList<>(mCount); + for (int j = 0; j < mCount; j++) { + methods.add(readMethod(in)); + } + return methods; + } + private NMethod readMethod(DataInputStream in) throws IOException { String shortId = readLongString(in); int argCount = in.readByte(); @@ -372,6 +418,7 @@ public class ClsSet { return bounds == 0 ? ArgType.wildcard() : ArgType.wildcard(readArgType(in), bounds); + case GENERIC: String obj = classes[in.readInt()].getName(); int typeLength = in.readByte(); @@ -385,34 +432,20 @@ public class ClsSet { } } return ArgType.generic(obj, generics); + case GENERIC_TYPE: return ArgType.genericType(readString(in)); + case OBJECT: return ArgType.object(classes[in.readInt()].getName()); + case ARRAY: return ArgType.array(readArgType(in)); + case PRIMITIVE: - int shortName = in.readByte(); - switch (shortName) { - case 'Z': - return ArgType.BOOLEAN; - case 'C': - return ArgType.CHAR; - case 'B': - return ArgType.BYTE; - case 'S': - return ArgType.SHORT; - case 'I': - return ArgType.INT; - case 'F': - return ArgType.FLOAT; - case 'J': - return ArgType.LONG; - case 'D': - return ArgType.DOUBLE; - default: - return ArgType.VOID; - } + char shortName = (char) in.readByte(); + return ArgType.parse(shortName); + default: throw new JadxRuntimeException("Unsupported Arg Type: " + ordinal); } 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 d26090c9b..abdfc9475 100644 --- a/jadx-core/src/main/java/jadx/core/clsp/ClspGraph.java +++ b/jadx-core/src/main/java/jadx/core/clsp/ClspGraph.java @@ -10,15 +10,18 @@ import java.util.Map; import java.util.Set; import java.util.WeakHashMap; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; 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.utils.exceptions.DecodeException; import jadx.core.utils.exceptions.JadxRuntimeException; /** - * Classes hierarchy graph + * Classes hierarchy graph with methods additional info */ public class ClspGraph { private static final Logger LOG = LoggerFactory.getLogger(ClspGraph.class); @@ -30,7 +33,7 @@ public class ClspGraph { public void load() throws IOException, DecodeException { ClsSet set = new ClsSet(); - set.load(); + set.loadFromClstFile(); addClasspath(set); } @@ -62,6 +65,19 @@ public class ClspGraph { return nameMap.containsKey(fullName); } + public NClass getClsDetails(ArgType type) { + return nameMap.get(type.getObject()); + } + + @Nullable + public NMethod getMethodDetails(MethodInfo methodInfo) { + NClass cls = nameMap.get(methodInfo.getDeclClass().getRawName()); + if (cls == null) { + return null; + } + return cls.getMethodsMap().get(methodInfo.getShortId()); + } + private NClass addClass(ClassNode cls) { String rawName = cls.getRawName(); NClass nClass = new NClass(rawName, -1); 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 e27b51a4d..adf4addb2 100644 --- a/jadx-core/src/main/java/jadx/core/clsp/ConvertToClsSet.java +++ b/jadx-core/src/main/java/jadx/core/clsp/ConvertToClsSet.java @@ -49,7 +49,7 @@ public class ConvertToClsSet { root.load(inputFiles); ClsSet set = new ClsSet(); - set.load(root); + set.loadFrom(root); set.save(output); LOG.info("Output: {}", output); 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 index d7465c723..08dfac6e8 100644 --- a/jadx-core/src/main/java/jadx/core/clsp/NClass.java +++ b/jadx-core/src/main/java/jadx/core/clsp/NClass.java @@ -1,14 +1,24 @@ 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 NClass[] parents; - private NMethod[] methods; 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; @@ -31,6 +41,37 @@ public class NClass { 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(); @@ -52,12 +93,4 @@ public class NClass { public String toString() { return name; } - - public void setMethods(NMethod[] methods) { - this.methods = methods; - } - - public NMethod[] getMethods() { - return methods; - } } diff --git a/jadx-core/src/main/java/jadx/core/clsp/NMethod.java b/jadx-core/src/main/java/jadx/core/clsp/NMethod.java index c94c8dc69..a9fff0262 100644 --- a/jadx-core/src/main/java/jadx/core/clsp/NMethod.java +++ b/jadx-core/src/main/java/jadx/core/clsp/NMethod.java @@ -1,20 +1,31 @@ package jadx.core.clsp; +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 { +public class NMethod implements Comparable { private final String shortId; - private final ArgType[] argType; + + /** + * 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, ArgType[] argType, ArgType retType, boolean varArgs) { + public NMethod(String shortId, @Nullable ArgType[] genericArgs, @Nullable ArgType retType, boolean varArgs) { this.shortId = shortId; - this.argType = argType; + this.genericArgs = genericArgs; this.retType = retType; this.varArgs = varArgs; } @@ -23,10 +34,21 @@ public class NMethod { return shortId; } - public ArgType[] getArgType() { - return argType; + @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; } @@ -34,4 +56,35 @@ public class NMethod { 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=" + genericArgs + + ", retType=" + retType + + ", varArgs=" + varArgs + + '}'; + } } 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 00610c444..3adcc830a 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/ClassGen.java +++ b/jadx-core/src/main/java/jadx/core/codegen/ClassGen.java @@ -5,8 +5,6 @@ import java.util.Comparator; import java.util.HashSet; import java.util.Iterator; import java.util.List; -import java.util.Map; -import java.util.Map.Entry; import java.util.Set; import com.android.dx.rop.code.AccessFlags; @@ -27,6 +25,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.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.parser.FieldInitAttr; @@ -143,7 +142,7 @@ public class ClassGen { clsCode.attachDefinition(cls); clsCode.add(cls.getClassInfo().getAliasShortName()); - addGenericMap(clsCode, cls.getGenericMap(), true); + addGenericMap(clsCode, cls.getGenerics(), true); clsCode.add(' '); ArgType sup = cls.getSuperClass(); @@ -174,23 +173,23 @@ public class ClassGen { } } - public boolean addGenericMap(CodeWriter code, Map> gmap, boolean classDeclaration) { - if (gmap == null || gmap.isEmpty()) { + public boolean addGenericMap(CodeWriter code, List generics, boolean classDeclaration) { + if (generics == null || generics.isEmpty()) { return false; } code.add('<'); int i = 0; - for (Entry> e : gmap.entrySet()) { - ArgType type = e.getKey(); - List list = e.getValue(); + for (GenericInfo genericInfo : generics) { if (i != 0) { code.add(", "); } + ArgType type = genericInfo.getGenericType(); if (type.isGenericType()) { code.add(type.getObject()); } else { useClass(code, type); } + List list = genericInfo.getExtendsList(); if (list != null && !list.isEmpty()) { code.add(" extends "); for (Iterator it = list.iterator(); it.hasNext();) { 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 42e4d2466..1d8e0faf4 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/MethodGen.java +++ b/jadx-core/src/main/java/jadx/core/codegen/MethodGen.java @@ -99,7 +99,7 @@ public class MethodGen { code.add(mth.isVirtual() ? "/* virtual */ " : "/* direct */ "); } - if (classGen.addGenericMap(code, mth.getGenericMap(), false)) { + if (classGen.addGenericMap(code, mth.getGenerics(), false)) { code.add(' '); } if (ai.isConstructor()) { 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 9bf57accf..28da64260 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 @@ -622,6 +622,24 @@ public abstract class ArgType { return 1; } + public boolean containsGenericType() { + if (isGenericType()) { + return true; + } + if (isGeneric()) { + ArgType[] genericTypes = getGenericTypes(); + if (genericTypes != null) { + for (ArgType genericType : genericTypes) { + if (genericType.containsGenericType()) { + return true; + } + } + } + return false; + } + return false; + } + public static ArgType tryToResolveClassAlias(DexNode dex, ArgType type) { if (!type.isObject() || type.isGenericType()) { return type; 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 d5124a161..4f18cdc92 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 @@ -10,6 +10,7 @@ import org.slf4j.LoggerFactory; import com.android.dx.io.instructions.DecodedInstruction; import jadx.core.dex.attributes.AFlag; +import jadx.core.dex.instructions.InsnType; import jadx.core.dex.nodes.InsnNode; import jadx.core.utils.InsnUtils; @@ -109,6 +110,18 @@ public abstract class InsnArg extends Typed { if (i == -1) { return null; } + if (insn.getType() == InsnType.MOVE && this.isRegister()) { + // preserve variable name for move insn (needed in `for-each` loop for iteration variable) + String name = ((RegisterArg) this).getName(); + if (name != null) { + InsnArg arg = insn.getArg(0); + if (arg.isRegister()) { + ((RegisterArg) arg).setNameIfUnknown(name); + } else if (arg.isInsnWrap()) { + ((InsnWrapArg) arg).getWrapInsn().getResult().setNameIfUnknown(name); + } + } + } insn.add(AFlag.WRAPPED); InsnArg arg = wrapArg(insn); parent.setArg(i, arg); 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 3295987a5..ed03eb8d9 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 @@ -97,6 +97,12 @@ public class RegisterArg extends InsnArg implements Named { } } + public void setNameIfUnknown(String name) { + if (getName() == null) { + setName(name); + } + } + public boolean isNameEquals(InsnArg arg) { String n = getName(); if (n == null || !(arg instanceof Named)) { 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 74542b1e1..152565d16 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 @@ -46,7 +46,7 @@ public class ClassNode extends LineAttrNode implements ILoadable, ICodeNode { private AccessInfo accessFlags; private ArgType superClass; private List interfaces; - private Map> genericMap; + private List generics = Collections.emptyList(); private final List methods; private final List fields; @@ -180,7 +180,7 @@ public class ClassNode extends LineAttrNode implements ILoadable, ICodeNode { } try { // parse class generic map - genericMap = sp.consumeGenericMap(); + generics = sp.consumeGenericMap(); // parse super class signature superClass = sp.consumeType(); // parse interfaces signatures @@ -283,8 +283,8 @@ public class ClassNode extends LineAttrNode implements ILoadable, ICodeNode { return interfaces; } - public Map> getGenericMap() { - return genericMap; + public List getGenerics() { + return generics; } public List getMethods() { 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 new file mode 100644 index 000000000..e70e5649b --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/GenericInfo.java @@ -0,0 +1,46 @@ +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/MethodNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/MethodNode.java index 9fe3e1996..df3654a1d 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,7 +4,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Set; import org.jetbrains.annotations.NotNull; @@ -67,7 +66,7 @@ public class MethodNode extends LineAttrNode implements ILoadable, ICodeNode { private RegisterArg thisArg; private List argsList; private List sVars; - private Map> genericMap; + private List generics; private List blocks; private BlockNode enterBlock; @@ -95,7 +94,7 @@ public class MethodNode extends LineAttrNode implements ILoadable, ICodeNode { // don't unload retType and argsList, will be used in jadx-gui after class unload thisArg = null; sVars = Collections.emptyList(); - genericMap = null; + generics = Collections.emptyList(); instructions = null; blocks = null; enterBlock = null; @@ -174,7 +173,7 @@ public class MethodNode extends LineAttrNode implements ILoadable, ICodeNode { return false; } try { - genericMap = sp.consumeGenericMap(); + generics = sp.consumeGenericMap(); List argsTypes = sp.consumeMethodArgs(); retType = sp.consumeType(); @@ -261,8 +260,8 @@ public class MethodNode extends LineAttrNode implements ILoadable, ICodeNode { return retType; } - public Map> getGenericMap() { - return genericMap; + public List getGenerics() { + return generics; } private static void initTryCatches(MethodNode mth, Code mthCode, InsnNode[] insnByOffset) { 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 c25249520..a8d2f2294 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 @@ -13,16 +13,19 @@ import jadx.api.ResourceFile; import jadx.api.ResourceType; import jadx.api.ResourcesLoader; import jadx.core.clsp.ClspGraph; +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.visitors.typeinference.TypeUpdate; import jadx.core.utils.CacheStorage; import jadx.core.utils.ErrorsCounter; import jadx.core.utils.StringUtils; import jadx.core.utils.android.AndroidResourcesUtils; +import jadx.core.utils.exceptions.DecodeException; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.files.DexFile; import jadx.core.utils.files.InputFile; @@ -191,6 +194,31 @@ public class RootNode { return cls.dex().deepResolveField(cls, field); } + @Nullable + public ArgType getMethodGenericReturnType(MethodInfo callMth) { + MethodNode methodNode = deepResolveMethod(callMth); + if (methodNode != null) { + ArgType returnType = methodNode.getReturnType(); + if (returnType == null) { + try { + methodNode.load(); + returnType = methodNode.getReturnType(); + } catch (DecodeException e) { + LOG.error("Method load error", e); + } + } + 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 getDexNodes() { return dexNodes; } 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 5d7b983e9..dfcc71e38 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 @@ -1,10 +1,9 @@ package jadx.core.dex.nodes.parser; +import java.util.ArrayList; import java.util.Collections; -import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; -import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -13,6 +12,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.utils.exceptions.JadxRuntimeException; public class SignatureParser { @@ -219,11 +219,11 @@ public class SignatureParser { *

* Example: "" */ - public Map> consumeGenericMap() { + public List consumeGenericMap() { if (!lookAhead('<')) { - return Collections.emptyMap(); + return Collections.emptyList(); } - Map> map = new LinkedHashMap<>(2); + List list = new ArrayList<>(); consume('<'); while (true) { if (lookAhead('>') || next() == STOP_CHAR) { @@ -231,15 +231,15 @@ public class SignatureParser { } String id = consumeUntil(':'); if (id == null) { - LOG.error("Can't parse generic map: {}", sign); - return Collections.emptyMap(); + LOG.error("Failed to parse generic map: {}", sign); + return Collections.emptyList(); } tryConsume(':'); List types = consumeExtendsTypesList(); - map.put(ArgType.genericType(id), types); + list.add(new GenericInfo(ArgType.genericType(id), types)); } consume('>'); - return map; + return list; } /** diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/PrepareForCodeGen.java b/jadx-core/src/main/java/jadx/core/dex/visitors/PrepareForCodeGen.java index 2fb10f056..88fca6889 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/PrepareForCodeGen.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/PrepareForCodeGen.java @@ -259,11 +259,11 @@ public class PrepareForCodeGen extends AbstractVisitor { private void addMethodMsg(MethodNode mth) { if (commentedCount > 0) { - String msg = "JADX WARN: Illegal instructions before constructor call commented (this can break semantics)"; + String msg = "Illegal instructions before constructor call commented (this can break semantics)"; if (brokenCode || regionDepth > 1) { mth.addWarn(msg); } else { - mth.addComment(msg); + mth.addComment("JADX WARN: " + msg); } } } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/LoopRegionVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/LoopRegionVisitor.java index a4a6335b0..9b1e170c4 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/LoopRegionVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/LoopRegionVisitor.java @@ -327,7 +327,7 @@ public class LoopRegionVisitor extends AbstractVisitor implements IRegionVisitor LOG.warn("Generic type differs: '{}' and '{}' in {}", gType, varType, mth); return false; } - if (!iterableArg.isRegister()) { + if (!iterableArg.isRegister() || !iterableType.isObject()) { return true; } // TODO: add checks diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/variables/ProcessVariables.java b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/variables/ProcessVariables.java index b6b5834bb..89424e09c 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/variables/ProcessVariables.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/variables/ProcessVariables.java @@ -26,6 +26,8 @@ import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.regions.loops.LoopRegion; import jadx.core.dex.visitors.AbstractVisitor; import jadx.core.dex.visitors.regions.DepthRegionTraversal; +import jadx.core.dex.visitors.typeinference.TypeCompare; +import jadx.core.dex.visitors.typeinference.TypeCompareEnum; import jadx.core.utils.RegionUtils; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxException; @@ -64,19 +66,25 @@ public class ProcessVariables extends AbstractVisitor { private void checkCodeVars(MethodNode mth, List codeVars) { int unknownTypesCount = 0; for (CodeVar codeVar : codeVars) { - codeVar.getSsaVars().stream() - .filter(ssaVar -> ssaVar.contains(AFlag.IMMUTABLE_TYPE)) - .forEach(ssaVar -> { - ArgType ssaType = ssaVar.getAssign().getInitType(); - if (ssaType.isTypeKnown() && !ssaType.equals(codeVar.getType())) { - mth.addWarn("Incorrect type for immutable var: ssa=" + ssaType - + ", code=" + codeVar.getType() - + ", for " + ssaVar.getDetailedVarInfo(mth)); - } - }); - if (codeVar.getType() == null) { + ArgType codeVarType = codeVar.getType(); + if (codeVarType == null) { codeVar.setType(ArgType.UNKNOWN); unknownTypesCount++; + } else { + codeVar.getSsaVars().stream() + .filter(ssaVar -> ssaVar.contains(AFlag.IMMUTABLE_TYPE)) + .forEach(ssaVar -> { + ArgType ssaType = ssaVar.getAssign().getInitType(); + if (ssaType.isTypeKnown()) { + TypeCompare comparator = mth.root().getTypeUpdate().getComparator(); + TypeCompareEnum result = comparator.compareTypes(ssaType, codeVarType); + if (result == TypeCompareEnum.CONFLICT || result.isNarrow()) { + mth.addWarn("Incorrect type for immutable var: ssa=" + ssaType + + ", code=" + codeVarType + + ", for " + ssaVar.getDetailedVarInfo(mth)); + } + } + }); } } if (unknownTypesCount != 0) { diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/ITypeBound.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/ITypeBound.java index 6d93df77f..f1e932b09 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/ITypeBound.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/ITypeBound.java @@ -5,7 +5,11 @@ import org.jetbrains.annotations.Nullable; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.RegisterArg; +/** + * Information to restrict types by applying constraints (or boundaries) + */ public interface ITypeBound { + BoundEnum getBound(); ArgType getType(); diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/ITypeBoundDynamic.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/ITypeBoundDynamic.java new file mode 100644 index 000000000..9611e7fad --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/ITypeBoundDynamic.java @@ -0,0 +1,16 @@ +package jadx.core.dex.visitors.typeinference; + +import jadx.core.dex.instructions.args.ArgType; + +/** + * 'Dynamic' type bound allows to use requested and not yet applied types + * from {@link TypeUpdateInfo} for more precise restrictions + */ +public interface ITypeBoundDynamic extends ITypeBound { + + /** + * This method will be executed instead of {@link ITypeBound#getType()} + * if {@link TypeUpdateInfo} is available. + */ + ArgType getType(TypeUpdateInfo updateInfo); +} 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 new file mode 100644 index 000000000..de9cb4cae --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeBoundInvokeAssign.java @@ -0,0 +1,77 @@ +package jadx.core.dex.visitors.typeinference; + +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; + +/** + * Special dynamic bound for invoke with generics. + * Bound type calculated using instance generic type. + * TODO: also can depends on argument types + */ +public final class TypeBoundInvokeAssign implements ITypeBoundDynamic { + private final RootNode root; + private final InvokeNode invokeNode; + private final ArgType genericReturnType; + + public TypeBoundInvokeAssign(RootNode root, InvokeNode invokeNode, ArgType genericReturnType) { + this.root = root; + this.invokeNode = invokeNode; + this.genericReturnType = genericReturnType; + } + + @Override + public BoundEnum getBound() { + return BoundEnum.ASSIGN; + } + + @Override + public ArgType getType(TypeUpdateInfo updateInfo) { + return getReturnType(updateInfo.getType(invokeNode.getArg(0))); + } + + @Override + public ArgType getType() { + return getReturnType(invokeNode.getArg(0).getType()); + } + + private ArgType getReturnType(ArgType instanceType) { + ArgType resultGeneric = TypeUpdate.getResultGeneric(root, instanceType, genericReturnType); + if (resultGeneric != null) { + return resultGeneric; + } + return invokeNode.getCallMth().getReturnType(); + } + + @Override + public RegisterArg getArg() { + return invokeNode.getResult(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + TypeBoundInvokeAssign that = (TypeBoundInvokeAssign) o; + return invokeNode.equals(that.invokeNode); + } + + @Override + public int hashCode() { + return invokeNode.hashCode(); + } + + @Override + public String toString() { + return "InvokeAssign{" + invokeNode.getCallMth().getShortId() + + ", returnType=" + genericReturnType + + ", currentType=" + getType() + + ", instanceArg=" + invokeNode.getArg(0) + + '}'; + } +} 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 37f331298..5e2340a64 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,8 +16,11 @@ 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.IndexInsnNode; import jadx.core.dex.instructions.InsnType; +import jadx.core.dex.instructions.InvokeNode; +import jadx.core.dex.instructions.InvokeType; import jadx.core.dex.instructions.PhiInsn; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.CodeVar; @@ -51,11 +54,13 @@ import jadx.core.utils.Utils; public final class TypeInferenceVisitor extends AbstractVisitor { private static final Logger LOG = LoggerFactory.getLogger(TypeInferenceVisitor.class); + private RootNode root; private TypeUpdate typeUpdate; @Override public void init(RootNode root) { - typeUpdate = root.getTypeUpdate(); + this.root = root; + this.typeUpdate = root.getTypeUpdate(); } @Override @@ -239,6 +244,10 @@ public final class TypeInferenceVisitor extends AbstractVisitor { } break; + case INVOKE: + addBound(typeInfo, makeAssignInvokeBound((InvokeNode) insn)); + break; + default: ArgType type = insn.getResult().getInitType(); addBound(typeInfo, new TypeBoundConst(BoundEnum.ASSIGN, type)); @@ -246,6 +255,24 @@ public final class TypeInferenceVisitor extends AbstractVisitor { } } + private ITypeBound makeAssignInvokeBound(InvokeNode invokeNode) { + MethodInfo callMth = invokeNode.getCallMth(); + ArgType boundType = callMth.getReturnType(); + ArgType genericReturnType = root.getMethodGenericReturnType(callMth); + if (genericReturnType != null) { + if (genericReturnType.containsGenericType()) { + InvokeType invokeType = invokeNode.getInvokeType(); + if (invokeNode.getArgsCount() != 0 + && invokeType != InvokeType.STATIC && invokeType != InvokeType.SUPER) { + return new TypeBoundInvokeAssign(root, invokeNode, genericReturnType); + } + } else { + boundType = genericReturnType; + } + } + return new TypeBoundConst(BoundEnum.ASSIGN, boundType); + } + @Nullable private ITypeBound makeUseBound(RegisterArg regArg) { InsnNode insn = regArg.getParentInsn(); 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 746f2350b..3abeb8400 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 @@ -2,23 +2,29 @@ package jadx.core.dex.visitors.typeinference; import java.util.Comparator; import java.util.EnumMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.Consts; +import jadx.core.clsp.NClass; import jadx.core.dex.attributes.AFlag; +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; import jadx.core.dex.instructions.args.InsnArg; 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.GenericInfo; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.exceptions.JadxOverflowException; @@ -34,10 +40,12 @@ public final class TypeUpdate { private static final TypeUpdateFlags FLAGS_EMPTY = new TypeUpdateFlags(); private static final TypeUpdateFlags FLAGS_WIDER = new TypeUpdateFlags().allowWider(); + private final RootNode root; private final Map listenerRegistry; private final TypeCompare comparator; public TypeUpdate(RootNode root) { + this.root = root; this.listenerRegistry = initListenerRegistry(); this.comparator = new TypeCompare(root); } @@ -74,7 +82,7 @@ public final class TypeUpdate { LOG.debug("Applying types, init for {} -> {}", ssaVar, candidateType); updates.forEach(updateEntry -> LOG.debug(" {} -> {}", updateEntry.getType(), updateEntry.getArg())); } - updates.forEach(TypeUpdateEntry::apply); + updateInfo.applyUpdates(); return CHANGED; } @@ -112,7 +120,7 @@ public final class TypeUpdate { private TypeUpdateResult updateTypeForSsaVar(TypeUpdateInfo updateInfo, SSAVar ssaVar, ArgType candidateType) { TypeInfo typeInfo = ssaVar.getTypeInfo(); - if (!inBounds(typeInfo.getBounds(), candidateType)) { + if (!inBounds(updateInfo, typeInfo.getBounds(), candidateType)) { if (Consts.DEBUG) { LOG.debug("Reject type '{}' for {} by bounds: {}", candidateType, ssaVar, typeInfo.getBounds()); } @@ -176,8 +184,17 @@ public final class TypeUpdate { } boolean inBounds(Set bounds, ArgType candidateType) { + return inBounds(null, bounds, candidateType); + } + + private boolean inBounds(@Nullable TypeUpdateInfo updateInfo, Set bounds, ArgType candidateType) { for (ITypeBound bound : bounds) { - ArgType boundType = bound.getType(); + ArgType boundType; + if (updateInfo != null && bound instanceof ITypeBoundDynamic) { + boundType = ((ITypeBoundDynamic) bound).getType(updateInfo); + } else { + boundType = bound.getType(); + } if (boundType != null && !checkBound(candidateType, bound, boundType)) { return false; } @@ -185,10 +202,10 @@ public final class TypeUpdate { return true; } - private boolean inBounds(InsnArg arg, ArgType candidateType) { + private boolean inBounds(TypeUpdateInfo updateInfo, InsnArg arg, ArgType candidateType) { if (arg.isRegister()) { TypeInfo typeInfo = ((RegisterArg) arg).getSVar().getTypeInfo(); - return inBounds(typeInfo.getBounds(), candidateType); + return inBounds(updateInfo, typeInfo.getBounds(), candidateType); } return arg.getType().equals(candidateType); } @@ -258,9 +275,83 @@ public final class TypeUpdate { registry.put(InsnType.NEG, this::suggestAllSameListener); registry.put(InsnType.NOT, this::suggestAllSameListener); registry.put(InsnType.CHECK_CAST, this::checkCastListener); + registry.put(InsnType.INVOKE, this::invokeListener); return registry; } + private TypeUpdateResult invokeListener(TypeUpdateInfo updateInfo, InsnNode insn, InsnArg arg, ArgType candidateType) { + if (insn.getResult() == null) { + return SAME; + } + if (candidateType.isGeneric() || candidateType.isGenericType()) { + 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); + if (returnType == null) { + return SAME; + } + ArgType resultGeneric = getResultGeneric(root, candidateType, returnType); + if (resultGeneric == null) { + return SAME; + } + return updateTypeChecked(updateInfo, insn.getResult(), resultGeneric); + } + } + return SAME; + } + + @Nullable + public static ArgType getResultGeneric(RootNode root, ArgType instanceType, ArgType genericRetType) { + if (genericRetType == null) { + return null; + } + if (instanceType.isGeneric()) { + NClass clsDetails = root.getClsp().getClsDetails(instanceType); + if (clsDetails == null || clsDetails.getGenerics().isEmpty()) { + return null; + } + List generics = clsDetails.getGenerics(); + ArgType[] actualTypes = instanceType.getGenericTypes(); + if (generics.size() != actualTypes.length) { + return null; + } + Map replaceMap = new LinkedHashMap<>(); + for (int i = 0; i < actualTypes.length; i++) { + ArgType actualType = actualTypes[i]; + ArgType genericType = generics.get(i).getGenericType(); + replaceMap.put(genericType, actualType); + } + return replaceGenericTypes(genericRetType, replaceMap); + } + return null; + } + + private static ArgType replaceGenericTypes(ArgType replaceType, Map replaceMap) { + if (replaceType.isGenericType()) { + return replaceMap.get(replaceType); + } + + 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 = replaceGenericTypes(genericType, replaceMap); + if (type == null) { + type = genericType; + } + newTypes[i] = type; + } + return ArgType.generic(replaceType.getObject(), newTypes); + } + return null; + } + private TypeUpdateResult sameFirstArgListener(TypeUpdateInfo updateInfo, InsnNode insn, InsnArg arg, ArgType candidateType) { InsnArg changeArg = isAssign(insn, arg) ? insn.getArg(0) : insn.getResult(); return updateTypeChecked(updateInfo, changeArg, candidateType); @@ -275,7 +366,7 @@ public final class TypeUpdate { TypeCompareEnum compareTypes = comparator.compareTypes(candidateType, changeArg.getType()); boolean correctType = compareTypes == TypeCompareEnum.EQUAL || (assignChanged ? compareTypes.isWider() : compareTypes.isNarrow()); - if (correctType && inBounds(changeArg, candidateType)) { + if (correctType && inBounds(updateInfo, changeArg, candidateType)) { allowReject = true; } else { return REJECT; diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeUpdateEntry.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeUpdateEntry.java index 0923ea0a2..76448f984 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeUpdateEntry.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeUpdateEntry.java @@ -12,10 +12,6 @@ public final class TypeUpdateEntry { this.type = type; } - public void apply() { - arg.setType(type); - } - public InsnArg getArg() { return arg; } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeUpdateInfo.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeUpdateInfo.java index b6e31df7b..1b2a84ced 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeUpdateInfo.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeUpdateInfo.java @@ -18,6 +18,13 @@ public class TypeUpdateInfo { updates.add(new TypeUpdateEntry(arg, changeType)); } + public void applyUpdates() { + for (TypeUpdateEntry updateEntry : updates) { + InsnArg arg = updateEntry.getArg(); + arg.setType(updateEntry.getType()); + } + } + public boolean isProcessed(InsnArg arg) { if (updates.isEmpty()) { return false; @@ -30,6 +37,15 @@ public class TypeUpdateInfo { return false; } + public ArgType getType(InsnArg arg) { + for (TypeUpdateEntry update : updates) { + if (update.getArg() == arg) { + return update.getType(); + } + } + return arg.getType(); + } + public void rollbackUpdate(InsnArg arg) { updates.removeIf(updateEntry -> updateEntry.getArg() == arg); } diff --git a/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java b/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java index 4462e54bf..15020424c 100644 --- a/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java +++ b/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java @@ -353,7 +353,7 @@ public abstract class IntegrationTest extends TestUtils { return dynamicCompiler.invoke(cls, methodName, types, args); } - public File getJarForClass(Class cls) throws IOException { + private File getJarForClass(Class cls) throws IOException { List files = compileClass(cls); assertThat("File list is empty", files, not(empty())); 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 149bf7dfa..b0a881746 100644 --- a/jadx-core/src/test/java/jadx/tests/functional/SignatureParserTest.java +++ b/jadx-core/src/test/java/jadx/tests/functional/SignatureParserTest.java @@ -1,12 +1,12 @@ package jadx.tests.functional; -import java.util.LinkedHashMap; +import java.util.ArrayList; import java.util.List; -import java.util.Map; import org.junit.jupiter.api.Test; import jadx.core.dex.instructions.args.ArgType; +import jadx.core.dex.nodes.GenericInfo; import jadx.core.dex.nodes.parser.SignatureParser; import static jadx.core.dex.instructions.args.ArgType.INT; @@ -20,7 +20,6 @@ import static jadx.core.dex.instructions.args.ArgType.wildcard; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.anEmptyMap; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; @@ -92,14 +91,14 @@ class SignatureParserTest { @SuppressWarnings("unchecked") private static void checkGenerics(String g, Object... objs) { - Map> map = new SignatureParser(g).consumeGenericMap(); - Map> expectedMap = new LinkedHashMap<>(); + List genericsList = new SignatureParser(g).consumeGenericMap(); + 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]; - expectedMap.put(generic, list); + expectedList.add(new GenericInfo(generic, list)); } - assertThat(map, is(expectedMap)); + assertThat(genericsList, is(expectedList)); } @Test @@ -122,7 +121,7 @@ class SignatureParserTest { @Test public void testBadGenericMap() { - Map> map = new SignatureParser(" list = new SignatureParser(" map = this.field; + useInt(map.size()); + Iterator> it = map.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry next = it.next(); + useInt(next.getKey().intValue()); + next.getValue().trim(); + } + } + */ + // @formatter:on + + @Test + public void test() { + ClassNode cls = getClassNodeFromSmali(); + String code = cls.getCode().toString(); + + assertThat(code, containsOne("Entry next")); + assertThat(code, containsOne("useInt(next.getKey().intValue());")); // no Integer cast + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/types/TestGenerics3.java b/jadx-core/src/test/java/jadx/tests/integration/types/TestGenerics3.java new file mode 100644 index 000000000..ea5be769e --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/types/TestGenerics3.java @@ -0,0 +1,57 @@ +package jadx.tests.integration.types; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; + +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; + +public class TestGenerics3 extends IntegrationTest { + + public static class TestCls { + public static void test() { + List classes = getClasses(); + Collections.sort(classes); + int passed = 0; + for (String cls : classes) { + if (runTest(cls)) { + passed++; + } + } + int failed = classes.size() - passed; + System.out.println("failed: " + failed); + } + + private static boolean runTest(String clsName) { + return false; + } + + private static List getClasses() { + return new ArrayList<>(); + } + } + + @Test + public void test() { + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + assertThat(code, containsOne("List classes")); + assertThat(code, containsOne("for (String cls : classes) {")); + } + + @Test + public void testNoDebug() { + noDebugInfo(); + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + assertThat(code, containsOne("List classes")); + } +} diff --git a/jadx-core/src/test/smali/types/TestGenerics2.smali b/jadx-core/src/test/smali/types/TestGenerics2.smali new file mode 100644 index 000000000..3b7519b09 --- /dev/null +++ b/jadx-core/src/test/smali/types/TestGenerics2.smali @@ -0,0 +1,68 @@ +.class public final Ltypes/TestGenerics2; +.super Ljava/lang/Object; +.source "SourceFile" + +# instance fields +.field private field:Ljava/util/Map; + .annotation system Ldalvik/annotation/Signature; + value = { + "Ljava/util/Map<", + "Ljava/lang/Integer;", + "Ljava/lang/String;", + ">;" + } + .end annotation +.end field + +.method public test()V + .registers 5 + + iget-object v4, p0, Ltypes/TestGenerics2;->field:Ljava/util/Map; + + invoke-interface {v4}, Ljava/util/Map;->size()I + move-result v0 + + invoke-static {v0}, Ltypes/TestGenerics2;->useInt(I)V + + invoke-interface {v4}, Ljava/util/Map;->entrySet()Ljava/util/Set; + move-result-object v4 + + invoke-interface {v4}, Ljava/util/Set;->iterator()Ljava/util/Iterator; + move-result-object v4 + + :goto_16 + invoke-interface {v4}, Ljava/util/Iterator;->hasNext()Z + move-result v0 + + if-eqz v0, :ret + + invoke-interface {v4}, Ljava/util/Iterator;->next()Ljava/lang/Object; + move-result-object v0 + + invoke-interface {v0}, Ljava/util/Map$Entry;->getKey()Ljava/lang/Object; + move-result-object v1 + + check-cast v1, Ljava/lang/Integer; + + invoke-virtual {v1}, Ljava/lang/Integer;->intValue()I + move-result v1 + + invoke-static {v1}, Ltypes/TestGenerics2;->useInt(I)V + + invoke-interface {v0}, Ljava/util/Map$Entry;->getValue()Ljava/lang/Object; + move-result-object v0 + + check-cast v0, Ljava/lang/String; + + invoke-interface {v0, p1}, Ljava/lang/String;->trim()Ljava/lang/String; + + goto :goto_16 + + :ret + return-void +.end method + +.method public static useInt(I)V + .registers 3 + return-void +.end method