diff --git a/build.gradle b/build.gradle index d0aef43b7..fee10c7d3 100644 --- a/build.gradle +++ b/build.gradle @@ -48,6 +48,7 @@ allprojects { mavenLocal() mavenCentral() jcenter() + google() } jacoco { diff --git a/jadx-core/src/main/java/jadx/api/ResourceFile.java b/jadx-core/src/main/java/jadx/api/ResourceFile.java index 1fdb02885..c550f403e 100644 --- a/jadx-core/src/main/java/jadx/api/ResourceFile.java +++ b/jadx-core/src/main/java/jadx/api/ResourceFile.java @@ -35,6 +35,13 @@ public class ResourceFile { private final ResourceType type; private ZipRef zipRef; + public static ResourceFile createResourceFile(JadxDecompiler decompiler, String name, ResourceType type) { + if (!ZipSecurity.isValidZipEntryName(name)) { + return null; + } + return new ResourceFile(decompiler, name, type); + } + protected ResourceFile(JadxDecompiler decompiler, String name, ResourceType type) { this.decompiler = decompiler; this.name = name; @@ -65,11 +72,4 @@ public class ResourceFile { public String toString() { return "ResourceFile{name='" + name + '\'' + ", type=" + type + "}"; } - - public static ResourceFile createResourceFileInstance(JadxDecompiler decompiler, String name, ResourceType type) { - if (!ZipSecurity.isValidZipEntryName(name)) { - return null; - } - return new ResourceFile(decompiler, name, type); - } } diff --git a/jadx-core/src/main/java/jadx/api/ResourceFileContent.java b/jadx-core/src/main/java/jadx/api/ResourceFileContent.java index be6da2b2c..194c91b20 100644 --- a/jadx-core/src/main/java/jadx/api/ResourceFileContent.java +++ b/jadx-core/src/main/java/jadx/api/ResourceFileContent.java @@ -1,27 +1,18 @@ package jadx.api; import jadx.core.codegen.CodeWriter; -import jadx.core.utils.files.ZipSecurity; import jadx.core.xmlgen.ResContainer; public class ResourceFileContent extends ResourceFile { - private final CodeWriter content; - private ResourceFileContent(String name, ResourceType type, CodeWriter content) { + public ResourceFileContent(String name, ResourceType type, CodeWriter content) { super(null, name, type); this.content = content; } @Override public ResContainer loadContent() { - return ResContainer.singleFile(getName(), content); - } - - public static ResourceFileContent createResourceFileContentInstance(String name, ResourceType type, CodeWriter content) { - if (!ZipSecurity.isValidZipEntryName(name)) { - return null; - } - return new ResourceFileContent(name, type, content); + return ResContainer.textResource(getName(), content); } } diff --git a/jadx-core/src/main/java/jadx/api/ResourcesLoader.java b/jadx-core/src/main/java/jadx/api/ResourcesLoader.java index 054cc181a..d93fe5413 100644 --- a/jadx-core/src/main/java/jadx/api/ResourcesLoader.java +++ b/jadx-core/src/main/java/jadx/api/ResourcesLoader.java @@ -18,6 +18,7 @@ import org.slf4j.LoggerFactory; import jadx.api.ResourceFile.ZipRef; import jadx.core.codegen.CodeWriter; import jadx.core.utils.Utils; +import jadx.core.utils.android.Res9patchStreamDecoder; import jadx.core.utils.exceptions.JadxException; import jadx.core.utils.files.InputFile; import jadx.core.utils.files.ZipSecurity; @@ -31,8 +32,6 @@ import static jadx.core.utils.files.FileUtils.copyStream; public final class ResourcesLoader { private static final Logger LOG = LoggerFactory.getLogger(ResourcesLoader.class); - private static final int LOAD_SIZE_LIMIT = 10 * 1024 * 1024; - private final JadxDecompiler jadxRef; ResourcesLoader(JadxDecompiler jadxRef) { @@ -47,11 +46,11 @@ public final class ResourcesLoader { return list; } - public interface ResourceDecoder { - ResContainer decode(long size, InputStream is) throws IOException; + public interface ResourceDecoder { + T decode(long size, InputStream is) throws IOException; } - public static ResContainer decodeStream(ResourceFile rf, ResourceDecoder decoder) throws JadxException { + public static T decodeStream(ResourceFile rf, ResourceDecoder decoder) throws JadxException { try { ZipRef zipRef = rf.getZipRef(); if (zipRef == null) { @@ -80,46 +79,50 @@ public final class ResourcesLoader { static ResContainer loadContent(JadxDecompiler jadxRef, ResourceFile rf) { try { - return decodeStream(rf, (size, is) -> loadContent(jadxRef, rf, is, size)); + return decodeStream(rf, (size, is) -> loadContent(jadxRef, rf, is)); } catch (JadxException e) { LOG.error("Decode error", e); CodeWriter cw = new CodeWriter(); cw.add("Error decode ").add(rf.getType().toString().toLowerCase()); cw.startLine(Utils.getStackTrace(e.getCause())); - return ResContainer.singleFile(rf.getName(), cw); + return ResContainer.textResource(rf.getName(), cw); } } private static ResContainer loadContent(JadxDecompiler jadxRef, ResourceFile rf, - InputStream inputStream, long size) throws IOException { + InputStream inputStream) throws IOException { switch (rf.getType()) { case MANIFEST: case XML: - return ResContainer.singleFile(rf.getName(), - jadxRef.getXmlParser().parse(inputStream)); + CodeWriter content = jadxRef.getXmlParser().parse(inputStream); + return ResContainer.textResource(rf.getName(), content); case ARSC: - return new ResTableParser() - .decodeFiles(inputStream); + return new ResTableParser().decodeFiles(inputStream); case IMG: - return ResContainer.singleImageFile(rf.getName(), inputStream); - - case CODE: - case LIB: - case FONT: - case UNKNOWN: - return ResContainer.singleBinaryFile(rf.getName(), inputStream); + return decodeImage(rf, inputStream); default: - if (size > LOAD_SIZE_LIMIT) { - return ResContainer.singleFile(rf.getName(), - new CodeWriter().add("File too big, size: " + String.format("%.2f KB", size / 1024.))); - } - return ResContainer.singleFile(rf.getName(), loadToCodeWriter(inputStream)); + return ResContainer.resourceFileLink(rf); } } + private static ResContainer decodeImage(ResourceFile rf, InputStream inputStream) { + String name = rf.getName(); + if (name.endsWith(".9.png")) { + Res9patchStreamDecoder decoder = new Res9patchStreamDecoder(); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + try { + decoder.decode(inputStream, os); + return ResContainer.decodedData(rf.getName(), os.toByteArray()); + } catch (Exception e) { + LOG.error("Failed to decode 9-patch png image, path: {}", name, e); + } + } + return ResContainer.resourceFileLink(rf); + } + private void loadFile(List list, File file) { if (file == null) { return; @@ -141,7 +144,7 @@ public final class ResourcesLoader { private void addResourceFile(List list, File file) { String name = file.getAbsolutePath(); ResourceType type = ResourceType.getFileType(name); - ResourceFile rf = ResourceFile.createResourceFileInstance(jadxRef, name, type); + ResourceFile rf = ResourceFile.createResourceFile(jadxRef, name, type); if (rf != null) { list.add(rf); } @@ -153,7 +156,7 @@ public final class ResourcesLoader { } String name = entry.getName(); ResourceType type = ResourceType.getFileType(name); - ResourceFile rf = ResourceFile.createResourceFileInstance(jadxRef, name, type); + ResourceFile rf = ResourceFile.createResourceFile(jadxRef, name, type); if (rf != null) { rf.setZipRef(new ZipRef(zipFile, name)); list.add(rf); diff --git a/jadx-core/src/main/java/jadx/core/codegen/NameGen.java b/jadx-core/src/main/java/jadx/core/codegen/NameGen.java index b29c8176b..8ee769792 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/NameGen.java +++ b/jadx-core/src/main/java/jadx/core/codegen/NameGen.java @@ -1,6 +1,5 @@ package jadx.core.codegen; -import java.util.HashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; @@ -21,6 +20,7 @@ import jadx.core.dex.instructions.mods.ConstructorInsn; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.StringUtils; +import jadx.core.utils.Utils; public class NameGen { @@ -31,21 +31,22 @@ public class NameGen { private final boolean fallback; static { - OBJ_ALIAS = new HashMap<>(); - OBJ_ALIAS.put(Consts.CLASS_STRING, "str"); - OBJ_ALIAS.put(Consts.CLASS_CLASS, "cls"); - OBJ_ALIAS.put(Consts.CLASS_THROWABLE, "th"); - OBJ_ALIAS.put(Consts.CLASS_OBJECT, "obj"); - OBJ_ALIAS.put("java.util.Iterator", "it"); - OBJ_ALIAS.put("java.lang.Boolean", "bool"); - OBJ_ALIAS.put("java.lang.Short", "sh"); - OBJ_ALIAS.put("java.lang.Integer", "num"); - OBJ_ALIAS.put("java.lang.Character", "ch"); - OBJ_ALIAS.put("java.lang.Byte", "b"); - OBJ_ALIAS.put("java.lang.Float", "f"); - OBJ_ALIAS.put("java.lang.Long", "l"); - OBJ_ALIAS.put("java.lang.Double", "d"); - OBJ_ALIAS.put("java.lang.StringBuilder", "sb"); + OBJ_ALIAS = Utils.newConstStringMap( + Consts.CLASS_STRING, "str", + Consts.CLASS_CLASS, "cls", + Consts.CLASS_THROWABLE, "th", + Consts.CLASS_OBJECT, "obj", + "java.util.Iterator", "it", + "java.lang.Boolean", "bool", + "java.lang.Short", "sh", + "java.lang.Integer", "num", + "java.lang.Character", "ch", + "java.lang.Byte", "b", + "java.lang.Float", "f", + "java.lang.Long", "l", + "java.lang.Double", "d", + "java.lang.StringBuilder", "sb" + ); } public NameGen(MethodNode mth, boolean fallback) { 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 72f241525..e0cfbe431 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/RegionGen.java +++ b/jadx-core/src/main/java/jadx/core/codegen/RegionGen.java @@ -1,5 +1,6 @@ package jadx.core.codegen; +import java.util.Iterator; import java.util.List; import java.util.Map; @@ -11,6 +12,7 @@ import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.DeclareVariablesAttr; import jadx.core.dex.attributes.nodes.ForceReturnAttr; import jadx.core.dex.attributes.nodes.LoopLabelAttr; +import jadx.core.dex.info.ClassInfo; import jadx.core.dex.instructions.SwitchNode; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.NamedArg; @@ -306,16 +308,23 @@ public class RegionGen extends InsnGen { return; } code.startLine("} catch ("); + if (handler.isCatchAll()) { + code.add("Throwable"); + } else { + Iterator it = handler.getCatchTypes().iterator(); + if (it.hasNext()) { + useClass(code, it.next()); + } + while (it.hasNext()) { + code.add(" | "); + useClass(code, it.next()); + } + } + code.add(' '); InsnArg arg = handler.getArg(); if (arg instanceof RegisterArg) { - declareVar(code, (RegisterArg) arg); + code.add(mgen.getNameGen().assignArg((RegisterArg) arg)); } else if (arg instanceof NamedArg) { - if (handler.isCatchAll()) { - code.add("Throwable"); - } else { - useClass(code, handler.getCatchType()); - } - code.add(' '); code.add(mgen.getNameGen().assignNamedArg((NamedArg) arg)); } code.add(") {"); 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 7e18b1930..859341f95 100644 --- a/jadx-core/src/main/java/jadx/core/deobf/Deobfuscator.java +++ b/jadx-core/src/main/java/jadx/core/deobf/Deobfuscator.java @@ -243,6 +243,10 @@ public class Deobfuscator { } } + public void forceRenameField(FieldNode field) { + field.getFieldInfo().setAlias(makeFieldAlias(field)); + } + public void renameMethod(MethodNode mth) { String alias = getMethodAlias(mth); if (alias != null) { @@ -253,6 +257,13 @@ public class Deobfuscator { } } + public void forceRenameMethod(MethodNode mth) { + mth.getMethodInfo().setAlias(makeMethodAlias(mth)); + if (mth.isVirtual()) { + resolveOverriding(mth); + } + } + public void addPackagePreset(String origPkgName, String pkgAlias) { PackageNode pkg = getPackageNode(origPkgName, true); pkg.setAlias(pkgAlias); 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 dcade6689..42e5688f0 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 @@ -2,12 +2,14 @@ package jadx.core.dex.info; import java.io.File; +import org.jetbrains.annotations.NotNull; + import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.DexNode; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.exceptions.JadxRuntimeException; -public final class ClassInfo { +public final class ClassInfo implements Comparable { private final ArgType type; private String pkg; @@ -194,4 +196,9 @@ public final class ClassInfo { } return false; } + + @Override + public int compareTo(@NotNull ClassInfo o) { + return fullName.compareTo(o.fullName); + } } diff --git a/jadx-core/src/main/java/jadx/core/dex/info/FieldInfo.java b/jadx-core/src/main/java/jadx/core/dex/info/FieldInfo.java index fd35db1de..451c757b2 100644 --- a/jadx-core/src/main/java/jadx/core/dex/info/FieldInfo.java +++ b/jadx-core/src/main/java/jadx/core/dex/info/FieldInfo.java @@ -65,6 +65,10 @@ public final class FieldInfo { return !name.equals(alias); } + public boolean equalsNameAndType(FieldInfo other) { + return name.equals(other.name) && type.equals(other.type); + } + @Override public boolean equals(Object o) { if (this == o) { 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 51a179084..dc5e0a70a 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 @@ -321,6 +321,15 @@ public class ClassNode extends LineAttrNode implements ILoadable, IDexNode { return null; } + public FieldNode searchFieldByNameAndType(FieldInfo field) { + for (FieldNode f : fields) { + if (f.getFieldInfo().equalsNameAndType(field)) { + return f; + } + } + return null; + } + public FieldNode searchFieldByName(String name) { for (FieldNode f : fields) { if (f.getName().equals(name)) { diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/DexNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/DexNode.java index c5edd6444..6a2ef869b 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/DexNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/DexNode.java @@ -152,16 +152,15 @@ public class DexNode implements IDexNode { @Nullable FieldNode deepResolveField(@NotNull ClassNode cls, FieldInfo fieldInfo) { - FieldNode field = cls.searchFieldByName(fieldInfo.getName()); + FieldNode field = cls.searchFieldByNameAndType(fieldInfo); if (field != null) { return field; } - FieldNode found; ArgType superClass = cls.getSuperClass(); if (superClass != null) { ClassNode superNode = resolveClass(superClass); if (superNode != null) { - found = deepResolveField(superNode, fieldInfo); + FieldNode found = deepResolveField(superNode, fieldInfo); if (found != null) { return found; } @@ -170,7 +169,7 @@ public class DexNode implements IDexNode { for (ArgType iFaceType : cls.getInterfaces()) { ClassNode iFaceNode = resolveClass(iFaceType); if (iFaceNode != null) { - found = deepResolveField(iFaceNode, fieldInfo); + FieldNode found = deepResolveField(iFaceNode, fieldInfo); if (found != null) { return found; } 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 21baaf2ed..ca4ef15ea 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 @@ -117,16 +117,16 @@ public class MethodNode extends LineAttrNode implements ILoadable, IDexNode { DexNode dex = parentClass.dex(); Code mthCode = dex.readCode(methodData); - regsCount = mthCode.getRegistersSize(); + this.regsCount = mthCode.getRegistersSize(); initMethodTypes(); InsnDecoder decoder = new InsnDecoder(this); decoder.decodeInsns(mthCode); - instructions = decoder.process(); - codeSize = instructions.length; + this.instructions = decoder.process(); + this.codeSize = instructions.length; - initTryCatches(mthCode); - initJumps(); + initTryCatches(this, mthCode, instructions); + initJumps(instructions); this.debugInfoOffset = mthCode.getDebugInfoOffset(); } catch (Exception e) { @@ -263,37 +263,37 @@ public class MethodNode extends LineAttrNode implements ILoadable, IDexNode { return genericMap; } - private void initTryCatches(Code mthCode) { - InsnNode[] insnByOffset = instructions; + private static void initTryCatches(MethodNode mth, Code mthCode, InsnNode[] insnByOffset) { CatchHandler[] catchBlocks = mthCode.getCatchHandlers(); Try[] tries = mthCode.getTries(); if (catchBlocks.length == 0 && tries.length == 0) { return; } - int hc = 0; + int handlersCount = 0; Set addrs = new HashSet<>(); List catches = new ArrayList<>(catchBlocks.length); for (CatchHandler handler : catchBlocks) { TryCatchBlock tcBlock = new TryCatchBlock(); catches.add(tcBlock); - for (int i = 0; i < handler.getAddresses().length; i++) { - int addr = handler.getAddresses()[i]; - ClassInfo type = ClassInfo.fromDex(parentClass.dex(), handler.getTypeIndexes()[i]); - tcBlock.addHandler(this, addr, type); + int[] handlerAddrArr = handler.getAddresses(); + for (int i = 0; i < handlerAddrArr.length; i++) { + int addr = handlerAddrArr[i]; + ClassInfo type = ClassInfo.fromDex(mth.dex(), handler.getTypeIndexes()[i]); + tcBlock.addHandler(mth, addr, type); addrs.add(addr); - hc++; + handlersCount++; } int addr = handler.getCatchAllAddress(); if (addr >= 0) { - tcBlock.addHandler(this, addr, null); + tcBlock.addHandler(mth, addr, null); addrs.add(addr); - hc++; + handlersCount++; } } - if (hc > 0 && hc != addrs.size()) { + if (handlersCount > 0 && handlersCount != addrs.size()) { // resolve nested try blocks: // inner block contains all handlers from outer block => remove these handlers from inner block // each handler must be only in one try/catch block @@ -301,7 +301,7 @@ public class MethodNode extends LineAttrNode implements ILoadable, IDexNode { for (TryCatchBlock ct2 : catches) { if (ct1 != ct2 && ct2.containsAllHandlers(ct1)) { for (ExceptionHandler h : ct1.getHandlers()) { - ct2.removeHandler(this, h); + ct2.removeHandler(mth, h); h.setTryBlock(ct1); } } @@ -315,6 +315,7 @@ public class MethodNode extends LineAttrNode implements ILoadable, IDexNode { for (ExceptionHandler eh : ct.getHandlers()) { int addr = eh.getHandleOffset(); ExcHandlerAttr ehAttr = new ExcHandlerAttr(ct, eh); + // TODO: don't override existing attribute insnByOffset[addr].addAttr(ehAttr); } } @@ -341,8 +342,7 @@ public class MethodNode extends LineAttrNode implements ILoadable, IDexNode { } } - private void initJumps() { - InsnNode[] insnByOffset = instructions; + private static void initJumps(InsnNode[] insnByOffset) { for (int offset = 0; offset < insnByOffset.length; offset++) { InsnNode insn = insnByOffset[offset]; if (insn == null) { @@ -490,7 +490,18 @@ public class MethodNode extends LineAttrNode implements ILoadable, IDexNode { exceptionHandlers = new ArrayList<>(2); } else { for (ExceptionHandler h : exceptionHandlers) { - if (h == handler || h.getHandleOffset() == handler.getHandleOffset()) { + if (h.equals(handler)) { + return h; + } + if (h.getHandleOffset() == handler.getHandleOffset()) { + if (h.getTryBlock() == handler.getTryBlock()) { + for (ClassInfo catchType : handler.getCatchTypes()) { + h.addCatchType(catchType); + } + } else { + // same handlers from different try blocks + // will merge later + } return h; } } 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 e5e6f1d7e..067d4f42c 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 @@ -22,7 +22,6 @@ import jadx.core.dex.visitors.typeinference.TypeUpdate; import jadx.core.utils.ErrorsCounter; import jadx.core.utils.StringUtils; import jadx.core.utils.android.AndroidResourcesUtils; -import jadx.core.utils.exceptions.JadxException; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.files.DexFile; import jadx.core.utils.files.InputFile; @@ -84,17 +83,16 @@ public class RootNode { LOG.debug("'.arsc' file not found"); return; } - ResTableParser parser = new ResTableParser(); try { - ResourcesLoader.decodeStream(arsc, (size, is) -> { + ResourceStorage resStorage = ResourcesLoader.decodeStream(arsc, (size, is) -> { + ResTableParser parser = new ResTableParser(); parser.decode(is); - return null; + return parser.getResStorage(); }); - } catch (JadxException e) { + processResources(resStorage); + } catch (Exception e) { LOG.error("Failed to parse '.arsc' file", e); - return; } - processResources(parser.getResStorage()); } public void processResources(ResourceStorage resStorage) { diff --git a/jadx-core/src/main/java/jadx/core/dex/trycatch/ExcHandlerAttr.java b/jadx-core/src/main/java/jadx/core/dex/trycatch/ExcHandlerAttr.java index f456ed8fb..c4ac90640 100644 --- a/jadx-core/src/main/java/jadx/core/dex/trycatch/ExcHandlerAttr.java +++ b/jadx-core/src/main/java/jadx/core/dex/trycatch/ExcHandlerAttr.java @@ -30,6 +30,6 @@ public class ExcHandlerAttr implements IAttribute { public String toString() { return "ExcHandler: " + (handler.isFinally() ? " FINALLY" - : (handler.isCatchAll() ? "all" : handler.getCatchType()) + " " + handler.getArg()); + : handler.catchTypeStr() + " " + handler.getArg()); } } diff --git a/jadx-core/src/main/java/jadx/core/dex/trycatch/ExceptionHandler.java b/jadx-core/src/main/java/jadx/core/dex/trycatch/ExceptionHandler.java index 659b31540..191dc074e 100644 --- a/jadx-core/src/main/java/jadx/core/dex/trycatch/ExceptionHandler.java +++ b/jadx-core/src/main/java/jadx/core/dex/trycatch/ExceptionHandler.java @@ -1,18 +1,27 @@ package jadx.core.dex.trycatch; import java.util.ArrayList; +import java.util.Collection; import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; + +import org.jetbrains.annotations.Nullable; import jadx.core.Consts; import jadx.core.dex.info.ClassInfo; +import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.IContainer; import jadx.core.utils.InsnUtils; +import jadx.core.utils.Utils; +import jadx.core.utils.exceptions.JadxRuntimeException; public class ExceptionHandler { - private final ClassInfo catchType; + private final Set catchTypes = new TreeSet<>(); private final int handleOffset; private BlockNode handlerBlock; @@ -23,17 +32,57 @@ public class ExceptionHandler { private TryCatchBlock tryBlock; private boolean isFinally; - public ExceptionHandler(int addr, ClassInfo type) { + public ExceptionHandler(int addr, @Nullable ClassInfo type) { this.handleOffset = addr; - this.catchType = type; + addCatchType(type); } - public ClassInfo getCatchType() { - return catchType; + /** + * Add exception type to catch block + * @param type - null for 'all' or 'Throwable' handler + */ + public void addCatchType(@Nullable ClassInfo type) { + if (type != null) { + this.catchTypes.add(type); + } else { + if (!this.catchTypes.isEmpty()) { + throw new JadxRuntimeException("Null type added to not empty exception handler: " + this); + } + } + } + + public void addCatchTypes(Collection types) { + for (ClassInfo type : types) { + addCatchType(type); + } + } + + public Set getCatchTypes() { + return catchTypes; + } + + public ArgType getArgType() { + if (isCatchAll()) { + return ArgType.THROWABLE; + } + Set types = getCatchTypes(); + if (types.size() == 1) { + return types.iterator().next().getType(); + } else { + return ArgType.THROWABLE; + } } public boolean isCatchAll() { - return catchType == null || catchType.getFullName().equals(Consts.CLASS_THROWABLE); + if (catchTypes.isEmpty()) { + return true; + } + for (ClassInfo classInfo : catchTypes) { + if (classInfo.getFullName().equals(Consts.CLASS_THROWABLE)) { + return true; + } + } + return false; } public int getHandleOffset() { @@ -89,35 +138,30 @@ public class ExceptionHandler { } @Override - public int hashCode() { - return (catchType == null ? 0 : 31 * catchType.hashCode()) + handleOffset; + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ExceptionHandler that = (ExceptionHandler) o; + return handleOffset == that.handleOffset && + catchTypes.equals(that.catchTypes) && + Objects.equals(tryBlock, that.tryBlock); } @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - ExceptionHandler other = (ExceptionHandler) obj; - if (catchType == null) { - if (other.catchType != null) { - return false; - } - } else if (!catchType.equals(other.catchType)) { - return false; - } - return handleOffset == other.handleOffset; + public int hashCode() { + return Objects.hash(catchTypes, handleOffset /*, tryBlock*/); + } + + public String catchTypeStr() { + return catchTypes.isEmpty() ? "all" : Utils.listToString(catchTypes, " | ", ClassInfo::getShortName); } @Override public String toString() { - return (catchType == null ? "all" - : catchType.getShortName()) + " -> " + InsnUtils.formatOffset(handleOffset); + return catchTypeStr() + " -> " + InsnUtils.formatOffset(handleOffset); } } diff --git a/jadx-core/src/main/java/jadx/core/dex/trycatch/TryCatchBlock.java b/jadx-core/src/main/java/jadx/core/dex/trycatch/TryCatchBlock.java index 78154adf2..20a0b8b5a 100644 --- a/jadx-core/src/main/java/jadx/core/dex/trycatch/TryCatchBlock.java +++ b/jadx-core/src/main/java/jadx/core/dex/trycatch/TryCatchBlock.java @@ -5,6 +5,8 @@ import java.util.Iterator; import java.util.LinkedList; import java.util.List; +import org.jetbrains.annotations.Nullable; + import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.info.ClassInfo; @@ -40,12 +42,14 @@ public class TryCatchBlock { return handlers.containsAll(tb.handlers); } - public ExceptionHandler addHandler(MethodNode mth, int addr, ClassInfo type) { + public ExceptionHandler addHandler(MethodNode mth, int addr, @Nullable ClassInfo type) { ExceptionHandler handler = new ExceptionHandler(addr, type); - handler = mth.addExceptionHandler(handler); - handlers.add(handler); handler.setTryBlock(this); - return handler; + ExceptionHandler addedHandler = mth.addExceptionHandler(handler); + if (addedHandler == handler || addedHandler.getTryBlock() != this) { + handlers.add(addedHandler); + } + return addedHandler; } public void removeHandler(MethodNode mth, ExceptionHandler handler) { 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 5198b8271..498494bbd 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 @@ -343,7 +343,7 @@ public class ModVisitor extends AbstractVisitor { // result arg used both in this insn and exception handler, RegisterArg resArg = insn.getResult(); - ArgType type = excHandler.isCatchAll() ? ArgType.THROWABLE : excHandler.getCatchType().getType(); + ArgType type = excHandler.getArgType(); String name = excHandler.isCatchAll() ? "th" : "e"; if (resArg.getName() == null) { resArg.setName(name); diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/RenameVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/RenameVisitor.java index a3ca4fa91..802e27942 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/RenameVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/RenameVisitor.java @@ -12,6 +12,7 @@ import jadx.core.Consts; import jadx.core.deobf.Deobfuscator; import jadx.core.deobf.NameMapper; import jadx.core.dex.attributes.AFlag; +import jadx.core.dex.info.AccessInfo; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.info.FieldInfo; import jadx.core.dex.nodes.ClassNode; @@ -19,7 +20,6 @@ import jadx.core.dex.nodes.DexNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; -import jadx.core.utils.exceptions.JadxException; import jadx.core.utils.files.FileUtils; import jadx.core.utils.files.InputFile; @@ -49,20 +49,12 @@ public class RenameVisitor extends AbstractVisitor { checkClasses(root, isCaseSensitive); } - @Override - public boolean visit(ClassNode cls) throws JadxException { - checkFields(cls); - checkMethods(cls); - for (ClassNode inner : cls.getInnerClasses()) { - visit(inner); - } - return false; - } - private void checkClasses(RootNode root, boolean caseSensitive) { Set clsNames = new HashSet<>(); for (ClassNode cls : root.getClasses(true)) { checkClassName(cls); + checkFields(cls); + checkMethods(cls); if (!caseSensitive) { ClassInfo classInfo = cls.getClassInfo(); String clsFileName = classInfo.getAlias().getFullPath(); @@ -103,7 +95,7 @@ public class RenameVisitor extends AbstractVisitor { FieldInfo fieldInfo = field.getFieldInfo(); String fieldName = fieldInfo.getAlias(); if (!names.add(fieldName) || !NameMapper.isValidIdentifier(fieldName)) { - deobfuscator.renameField(field); + deobfuscator.forceRenameField(field); } } } @@ -111,12 +103,16 @@ public class RenameVisitor extends AbstractVisitor { private void checkMethods(ClassNode cls) { Set names = new HashSet<>(); for (MethodNode mth : cls.getMethods()) { - if (mth.contains(AFlag.DONT_GENERATE) || mth.getAccessFlags().isConstructor()) { + AccessInfo accessFlags = mth.getAccessFlags(); + if (accessFlags.isConstructor() + || accessFlags.isBridge() + || accessFlags.isSynthetic() + || mth.contains(AFlag.DONT_GENERATE)) { continue; } String signature = mth.getMethodInfo().makeSignature(false); if (!names.add(signature) || !NameMapper.isValidIdentifier(mth.getAlias())) { - deobfuscator.renameMethod(mth); + deobfuscator.forceRenameMethod(mth); } } } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/BlockExceptionHandler.java b/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/BlockExceptionHandler.java index 9d37b769b..1fabd9828 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/BlockExceptionHandler.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/BlockExceptionHandler.java @@ -48,7 +48,7 @@ public class BlockExceptionHandler extends AbstractVisitor { return; } ExceptionHandler excHandler = handlerAttr.getHandler(); - ArgType argType = excHandler.isCatchAll() ? ArgType.THROWABLE : excHandler.getCatchType().getType(); + ArgType argType = excHandler.getArgType(); if (!block.getInstructions().isEmpty()) { InsnNode me = block.getInstructions().get(0); if (me.getType() == InsnType.MOVE_EXCEPTION) { 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 97466c1da..610119c64 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 @@ -2,6 +2,7 @@ package jadx.core.dex.visitors.blocksmaker; import java.util.ArrayList; import java.util.BitSet; +import java.util.Collections; import java.util.Iterator; import java.util.LinkedList; import java.util.List; @@ -20,6 +21,9 @@ import jadx.core.dex.nodes.Edge; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.trycatch.CatchAttr; +import jadx.core.dex.trycatch.ExcHandlerAttr; +import jadx.core.dex.trycatch.ExceptionHandler; +import jadx.core.dex.trycatch.TryCatchBlock; import jadx.core.dex.visitors.AbstractVisitor; import jadx.core.utils.BlockUtils; import jadx.core.utils.exceptions.JadxOverflowException; @@ -372,7 +376,17 @@ public class BlockProcessor extends AbstractVisitor { } private static boolean modifyBlocksTree(MethodNode mth) { - for (BlockNode block : mth.getBasicBlocks()) { + List basicBlocks = mth.getBasicBlocks(); + for (BlockNode block : basicBlocks) { + if (block.getPredecessors().isEmpty() && block != mth.getEnterBlock()) { + throw new JadxRuntimeException("Unreachable block: " + block); + } + } + if (mergeExceptionHandlers(mth)) { + removeMarkedBlocks(mth); + return true; + } + for (BlockNode block : basicBlocks) { if (checkLoops(mth, block)) { return true; } @@ -492,6 +506,85 @@ public class BlockProcessor extends AbstractVisitor { return false; } + /** + * Merge handlers for multi-exception catch + */ + private static boolean mergeExceptionHandlers(MethodNode mth) { + for (BlockNode block : mth.getBasicBlocks()) { + ExcHandlerAttr excHandlerAttr = block.get(AType.EXC_HANDLER); + if (excHandlerAttr != null) { + List blocksForMerge = collectExcHandlerBlocks(block, excHandlerAttr); + if (mergeHandlers(mth, blocksForMerge, excHandlerAttr)) { + return true; + } + } + } + return false; + } + + private static List collectExcHandlerBlocks(BlockNode block, ExcHandlerAttr excHandlerAttr) { + List successors = block.getSuccessors(); + if (successors.size() != 1) { + return Collections.emptyList(); + } + RegisterArg reg = getMoveExceptionRegister(block); + if (reg == null) { + return Collections.emptyList(); + } + TryCatchBlock tryBlock = excHandlerAttr.getTryBlock(); + List blocksForMerge = new ArrayList<>(); + BlockNode nextBlock = successors.get(0); + for (BlockNode predBlock : nextBlock.getPredecessors()) { + if (predBlock != block + && checkOtherExcHandler(predBlock, tryBlock, reg)) { + blocksForMerge.add(predBlock); + } + } + return blocksForMerge; + } + + private static boolean checkOtherExcHandler(BlockNode predBlock, TryCatchBlock tryBlock, RegisterArg reg) { + ExcHandlerAttr otherExcHandlerAttr = predBlock.get(AType.EXC_HANDLER); + if (otherExcHandlerAttr == null) { + return false; + } + TryCatchBlock otherTryBlock = otherExcHandlerAttr.getTryBlock(); + if (tryBlock != otherTryBlock) { + return false; + } + RegisterArg otherReg = getMoveExceptionRegister(predBlock); + if (otherReg == null || reg.getRegNum() != otherReg.getRegNum()) { + return false; + } + return true; + } + + private static RegisterArg getMoveExceptionRegister(BlockNode block) { + if (block.getInstructions().isEmpty()) { + return null; + } + InsnNode insn = block.getInstructions().get(0); + if (insn.getType() != InsnType.MOVE_EXCEPTION) { + return null; + } + return insn.getResult(); + } + + private static boolean mergeHandlers(MethodNode mth, List blocksForMerge, ExcHandlerAttr excHandlerAttr) { + if (blocksForMerge.isEmpty()) { + return false; + } + TryCatchBlock tryBlock = excHandlerAttr.getTryBlock(); + for (BlockNode block : blocksForMerge) { + ExcHandlerAttr otherExcHandlerAttr = block.get(AType.EXC_HANDLER); + ExceptionHandler excHandler = otherExcHandlerAttr.getHandler(); + excHandlerAttr.getHandler().addCatchTypes(excHandler.getCatchTypes()); + tryBlock.removeHandler(mth, excHandler); + detachBlock(block); + } + return true; + } + /** * Splice return block if several predecessors presents */ @@ -590,6 +683,20 @@ public class BlockProcessor extends AbstractVisitor { }); } + private static void detachBlock(BlockNode block) { + for (BlockNode pred : block.getPredecessors()) { + pred.getSuccessors().remove(block); + pred.updateCleanSuccessors(); + } + for (BlockNode successor : block.getSuccessors()) { + successor.getPredecessors().remove(block); + } + block.add(AFlag.REMOVE); + block.getInstructions().clear(); + block.getPredecessors().clear(); + block.getSuccessors().clear(); + } + private static void clearBlocksState(MethodNode mth) { mth.getBasicBlocks().forEach(block -> { block.remove(AType.LOOP); diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/BlockSplitter.java b/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/BlockSplitter.java index 572a2610a..ac4d80d89 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/BlockSplitter.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/BlockSplitter.java @@ -44,6 +44,7 @@ public class BlockSplitter extends AbstractVisitor { mth.initBasicBlocks(); splitBasicBlocks(mth); + removeJumpAttr(mth); removeInsns(mth); removeEmptyDetachedBlocks(mth); initBlocksInTargetNodes(mth); @@ -299,6 +300,14 @@ public class BlockSplitter extends AbstractVisitor { return block; } + private void removeJumpAttr(MethodNode mth) { + for (BlockNode block : mth.getBasicBlocks()) { + for (InsnNode insn : block.getInstructions()) { + insn.remove(AType.JUMP); + } + } + } + private static void removeInsns(MethodNode mth) { for (BlockNode block : mth.getBasicBlocks()) { block.getInstructions().removeIf(insn -> { 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 c1e31ec8d..a573aa39c 100644 --- a/jadx-core/src/main/java/jadx/core/utils/Utils.java +++ b/jadx-core/src/main/java/jadx/core/utils/Utils.java @@ -5,8 +5,10 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.function.Function; import jadx.api.JadxDecompiler; @@ -39,8 +41,16 @@ public class Utils { if (objects == null) { return ""; } + return listToString(objects, joiner, Object::toString); + } + + public static String listToString(Iterable objects, Function toStr) { + return listToString(objects, ", ", toStr); + } + + public static String listToString(Iterable objects, String joiner, Function toStr) { StringBuilder sb = new StringBuilder(); - listToString(sb, objects, joiner, Object::toString); + listToString(sb, objects, joiner, toStr); return sb.toString(); } @@ -152,4 +162,16 @@ public class Utils { } return new ImmutableList<>(list); } + + public static Map newConstStringMap(String... parameters) { + int len = parameters.length; + if (len == 0) { + return Collections.emptyMap(); + } + Map result = new HashMap<>(len / 2); + for (int i = 0; i < len; i += 2) { + result.put(parameters[i], parameters[i + 1]); + } + return Collections.unmodifiableMap(result); + } } diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/ResContainer.java b/jadx-core/src/main/java/jadx/core/xmlgen/ResContainer.java index 8dfbf2fc1..cf8e72772 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/ResContainer.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/ResContainer.java @@ -1,84 +1,47 @@ package jadx.core.xmlgen; -import javax.imageio.ImageIO; -import java.awt.image.BufferedImage; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.File; -import java.io.InputStream; -import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; -import org.apache.commons.io.IOUtils; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import jadx.api.ResourceFile; import jadx.core.codegen.CodeWriter; -import jadx.core.utils.android.Res9patchStreamDecoder; -import jadx.core.utils.exceptions.JadxRuntimeException; public class ResContainer implements Comparable { - private static final Logger LOG = LoggerFactory.getLogger(ResContainer.class); + public enum DataType { + TEXT, DECODED_DATA, RES_LINK, RES_TABLE + } + private final DataType dataType; private final String name; + private final Object data; private final List subFiles; - @Nullable - private CodeWriter content; - @Nullable - private BufferedImage image; - @Nullable - private InputStream binary; - - private ResContainer(String name, List subFiles) { - this.name = name; - this.subFiles = subFiles; + public static ResContainer textResource(String name, CodeWriter content) { + return new ResContainer(name, Collections.emptyList(), content, DataType.TEXT); } - public static ResContainer singleFile(String name, CodeWriter content) { - ResContainer resContainer = new ResContainer(name, Collections.emptyList()); - resContainer.content = content; - return resContainer; + public static ResContainer decodedData(String name, byte[] data) { + return new ResContainer(name, Collections.emptyList(), data, DataType.DECODED_DATA); } - public static ResContainer singleImageFile(String name, InputStream content) { - ResContainer resContainer = new ResContainer(name, Collections.emptyList()); - InputStream newContent = content; - if (name.endsWith(".9.png")) { - Res9patchStreamDecoder decoder = new Res9patchStreamDecoder(); - ByteArrayOutputStream os = new ByteArrayOutputStream(); - try { - decoder.decode(content, os); - } catch (Exception e) { - LOG.error("Failed to decode 9-patch png image, path: {}", name, e); - } - newContent = new ByteArrayInputStream(os.toByteArray()); - } - try { - resContainer.image = ImageIO.read(newContent); - } catch (Exception e) { - throw new JadxRuntimeException("Image load error", e); - } - return resContainer; + public static ResContainer resourceFileLink(ResourceFile resFile) { + return new ResContainer(resFile.getName(), Collections.emptyList(), resFile, DataType.RES_LINK); } - public static ResContainer singleBinaryFile(String name, InputStream content) { - ResContainer resContainer = new ResContainer(name, Collections.emptyList()); - try { - // TODO: don't store binary files in memory - resContainer.binary = new ByteArrayInputStream(IOUtils.toByteArray(content)); - } catch (Exception e) { - LOG.warn("Contents of the binary resource '{}' not saved, got exception", name, e); - } - return resContainer; + public static ResContainer resourceTable(String name, List subFiles, CodeWriter rootContent) { + return new ResContainer(name, subFiles, rootContent, DataType.RES_TABLE); } - public static ResContainer multiFile(String name) { - return new ResContainer(name, new ArrayList<>()); + private ResContainer(String name, List subFiles, Object data, DataType dataType) { + this.name = Objects.requireNonNull(name); + this.subFiles = Objects.requireNonNull(subFiles); + this.data = Objects.requireNonNull(data); + this.dataType = Objects.requireNonNull(dataType); } public String getName() { @@ -89,29 +52,26 @@ public class ResContainer implements Comparable { return name.replace("/", File.separator); } - @Nullable - public CodeWriter getContent() { - return content; - } - - @Nullable - public InputStream getBinary() { - return binary; - } - - public void setContent(@Nullable CodeWriter content) { - this.content = content; - } - - @Nullable - public BufferedImage getImage() { - return image; - } - public List getSubFiles() { return subFiles; } + public DataType getDataType() { + return dataType; + } + + public CodeWriter getText() { + return (CodeWriter) data; + } + + public byte[] getDecodedData() { + return (byte[]) data; + } + + public ResourceFile getResLink() { + return (ResourceFile) data; + } + @Override public int compareTo(@NotNull ResContainer o) { return name.compareTo(o.name); @@ -136,6 +96,6 @@ public class ResContainer implements Comparable { @Override public String toString() { - return "Res{" + name + ", subFiles=" + subFiles + "}"; + return "Res{" + name + ", type=" + dataType + ", subFiles=" + subFiles + "}"; } } diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/ResTableParser.java b/jadx-core/src/main/java/jadx/core/xmlgen/ResTableParser.java index fa8c496b7..dd04ae6c1 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/ResTableParser.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/ResTableParser.java @@ -66,23 +66,9 @@ public class ResTableParser extends CommonBinaryParser { ValuesParser vp = new ValuesParser(strings, resStorage.getResourcesNames()); ResXmlGen resGen = new ResXmlGen(resStorage, vp); - ResContainer res = ResContainer.multiFile("res"); - res.setContent(makeXmlDump()); - res.getSubFiles().addAll(resGen.makeResourcesXml()); - return res; - } - - public CodeWriter makeDump() { - CodeWriter writer = new CodeWriter(); - writer.add("app package: ").add(resStorage.getAppPackage()); - writer.startLine(); - - ValuesParser vp = new ValuesParser(strings, resStorage.getResourcesNames()); - for (ResourceEntry ri : resStorage.getResources()) { - writer.startLine(ri + ": " + vp.getValueString(ri)); - } - writer.finish(); - return writer; + CodeWriter content = makeXmlDump(); + List xmlFiles = resGen.makeResourcesXml(); + return ResContainer.resourceTable("res", xmlFiles, content); } public CodeWriter makeXmlDump() { diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/ResXmlGen.java b/jadx-core/src/main/java/jadx/core/xmlgen/ResXmlGen.java index 68a8773a4..452744809 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/ResXmlGen.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/ResXmlGen.java @@ -57,7 +57,7 @@ public class ResXmlGen { content.decIndent(); content.startLine(""); content.finish(); - files.add(ResContainer.singleFile(fileName, content)); + files.add(ResContainer.textResource(fileName, content)); } Collections.sort(files); return files; diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/ResourcesSaver.java b/jadx-core/src/main/java/jadx/core/xmlgen/ResourcesSaver.java index 4e9145a91..dd3c129c0 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/ResourcesSaver.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/ResourcesSaver.java @@ -1,25 +1,21 @@ package jadx.core.xmlgen; -import javax.imageio.ImageIO; -import java.awt.image.BufferedImage; import java.io.File; import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.List; +import java.nio.file.Files; -import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.ResourceFile; +import jadx.api.ResourcesLoader; import jadx.core.codegen.CodeWriter; +import jadx.core.utils.exceptions.JadxException; +import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.files.FileUtils; import jadx.core.utils.files.ZipSecurity; -import static jadx.core.utils.files.FileUtils.prepareFile; - public class ResourcesSaver implements Runnable { private static final Logger LOG = LoggerFactory.getLogger(ResourcesSaver.class); @@ -33,76 +29,74 @@ public class ResourcesSaver implements Runnable { @Override public void run() { - ResContainer rc = resourceFile.loadContent(); - if (rc != null) { - saveResources(rc); - } + saveResources(resourceFile.loadContent()); } private void saveResources(ResContainer rc) { if (rc == null) { return; } - List subFiles = rc.getSubFiles(); - if (subFiles.isEmpty()) { - save(rc, outDir); - } else { + if (rc.getDataType() == ResContainer.DataType.RES_TABLE) { saveToFile(rc, new File(outDir, "res/values/public.xml")); - for (ResContainer subFile : subFiles) { + for (ResContainer subFile : rc.getSubFiles()) { saveResources(subFile); } + } else { + save(rc, outDir); } } private void save(ResContainer rc, File outDir) { File outFile = new File(outDir, rc.getFileName()); - BufferedImage image = rc.getImage(); - if (image != null) { - String ext = FilenameUtils.getExtension(outFile.getName()); - try { - outFile = prepareFile(outFile); - - if (!ZipSecurity.isInSubDirectory(outDir, outFile)) { - LOG.error("Path traversal attack detected, invalid resource name: {}", - outFile.getPath()); - return; - } - - ImageIO.write(image, ext, outFile); - } catch (IOException e) { - LOG.error("Failed to save image: {}", rc.getName(), e); - } - return; - } - if (!ZipSecurity.isInSubDirectory(outDir, outFile)) { - LOG.error("Path traversal attack detected, invalid resource name: {}", - rc.getFileName()); + LOG.error("Path traversal attack detected, invalid resource name: {}", outFile.getPath()); return; } saveToFile(rc, outFile); } private void saveToFile(ResContainer rc, File outFile) { - CodeWriter cw = rc.getContent(); - if (cw != null) { - cw.save(outFile); - return; - } - InputStream binary = rc.getBinary(); - if (binary != null) { - try { + switch (rc.getDataType()) { + case TEXT: + case RES_TABLE: + CodeWriter cw = rc.getText(); + cw.save(outFile); + return; + + case DECODED_DATA: + byte[] data = rc.getDecodedData(); FileUtils.makeDirsForFile(outFile); - try (FileOutputStream binaryFileStream = new FileOutputStream(outFile)) { - IOUtils.copy(binary, binaryFileStream); - } finally { - binary.close(); + try { + Files.write(outFile.toPath(), data); + } catch (Exception e) { + LOG.warn("Resource '{}' not saved, got exception", rc.getName(), e); } - } catch (Exception e) { - LOG.warn("Resource '{}' not saved, got exception", rc.getName(), e); - } - return; + return; + + case RES_LINK: + ResourceFile resFile = rc.getResLink(); + FileUtils.makeDirsForFile(outFile); + try { + saveResourceFile(resFile, outFile); + } catch (Exception e) { + LOG.warn("Resource '{}' not saved, got exception", rc.getName(), e); + } + return; + + default: + LOG.warn("Resource '{}' not saved, unknown type", rc.getName()); + break; } - LOG.warn("Resource '{}' not saved, unknown type", rc.getName()); + } + + private void saveResourceFile(ResourceFile resFile, File outFile) throws JadxException { + ResourcesLoader.decodeStream(resFile, (size, is) -> { + try (FileOutputStream fileStream = new FileOutputStream(outFile)) { + IOUtils.copy(is, fileStream); + } catch (Exception e) { + throw new JadxRuntimeException("Resource file save error", e); + } + return null; + }); } } diff --git a/jadx-core/src/test/java/jadx/tests/integration/names/TestDuplicatedNames.java b/jadx-core/src/test/java/jadx/tests/integration/names/TestDuplicatedNames.java new file mode 100644 index 000000000..7adc7f184 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/names/TestDuplicatedNames.java @@ -0,0 +1,51 @@ +package jadx.tests.integration.names; + +import org.junit.Test; + +import jadx.core.dex.nodes.ClassNode; +import jadx.tests.api.SmaliTest; + +import static jadx.tests.api.utils.JadxMatchers.containsOne; +import static org.junit.Assert.assertThat; + +public class TestDuplicatedNames extends SmaliTest { +/* + public static class TestCls { + + public Object fieldName; + public String fieldName; + + public Object run() { + return this.fieldName; + } + + public String run() { + return this.fieldName; + } + } +*/ + @Test + public void test() { + commonChecks(); + } + + @Test + public void testWithDeobf() { + enableDeobfuscation(); + commonChecks(); + } + + private void commonChecks() { + ClassNode cls = getClassNodeFromSmaliWithPath("names", "TestDuplicatedNames"); + String code = cls.getCode().toString(); + + assertThat(code, containsOne("Object fieldName;")); + assertThat(code, containsOne("String f0fieldName")); + + assertThat(code, containsOne("this.fieldName")); + assertThat(code, containsOne("this.f0fieldName")); + + assertThat(code, containsOne("public Object run() {")); + assertThat(code, containsOne("public String m0run() {")); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestMultiExceptionCatch.java b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestMultiExceptionCatch.java new file mode 100644 index 000000000..8c7970927 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestMultiExceptionCatch.java @@ -0,0 +1,38 @@ +package jadx.tests.integration.trycatch; + +import java.security.ProviderException; +import java.time.DateTimeException; + +import org.junit.Test; + +import jadx.core.dex.nodes.ClassNode; +import jadx.tests.api.IntegrationTest; + +import static jadx.tests.api.utils.JadxMatchers.containsOne; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.not; +import static org.junit.Assert.assertThat; + +public class TestMultiExceptionCatch extends IntegrationTest { + + public static class TestCls { + public void test() { + try { + System.out.println("Test"); + } catch (ProviderException | DateTimeException e) { + throw new RuntimeException(e); + } + } + } + + @Test + public void test() { + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + assertThat(code, containsOne("try {")); + assertThat(code, containsOne("} catch (ProviderException | DateTimeException e) {")); + assertThat(code, containsOne("throw new RuntimeException(e);")); + assertThat(code, not(containsString("RuntimeException e;"))); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestMultiExceptionCatchSameJump.java b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestMultiExceptionCatchSameJump.java new file mode 100644 index 000000000..6081f810f --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestMultiExceptionCatchSameJump.java @@ -0,0 +1,35 @@ +package jadx.tests.integration.trycatch; + +import org.junit.Test; + +import jadx.core.dex.nodes.ClassNode; +import jadx.tests.api.SmaliTest; + +import static jadx.tests.api.utils.JadxMatchers.containsOne; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.not; +import static org.junit.Assert.assertThat; + +public class TestMultiExceptionCatchSameJump extends SmaliTest { +/* + public static class TestCls { + public void test() { + try { + System.out.println("Test"); + } catch (ProviderException | DateTimeException e) { + throw new RuntimeException(e); + } + } + } +*/ + @Test + public void test() { + ClassNode cls = getClassNodeFromSmaliWithPkg("trycatch", "TestMultiExceptionCatchSameJump"); + String code = cls.getCode().toString(); + + assertThat(code, containsOne("try {")); + assertThat(code, containsOne("} catch (ProviderException | DateTimeException e) {")); + assertThat(code, containsOne("throw new RuntimeException(e);")); + assertThat(code, not(containsString("RuntimeException e;"))); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/types/TestPrimitivesInIf.java b/jadx-core/src/test/java/jadx/tests/integration/types/TestPrimitivesInIf.java index bd738c35a..88af91d5f 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/types/TestPrimitivesInIf.java +++ b/jadx-core/src/test/java/jadx/tests/integration/types/TestPrimitivesInIf.java @@ -37,7 +37,6 @@ public class TestPrimitivesInIf extends IntegrationTest { @Test public void test2() { - setOutputCFG(); noDebugInfo(); ClassNode cls = getClassNode(TestCls.class); String code = cls.getCode().toString(); diff --git a/jadx-core/src/test/smali/names/TestDuplicatedNames.smali b/jadx-core/src/test/smali/names/TestDuplicatedNames.smali new file mode 100644 index 000000000..065a3445d --- /dev/null +++ b/jadx-core/src/test/smali/names/TestDuplicatedNames.smali @@ -0,0 +1,40 @@ +.class public LTestDuplicatedNames; +.super Ljava/lang/Object; +.source "TestDuplicatedNames.java" + + +# instance fields +.field public fieldName:Ljava/lang/String; +.field public fieldName:Ljava/lang/Object; + + +# direct methods +.method public constructor ()V + .registers 1 + + .prologue + .line 3 + invoke-direct {p0}, Ljava/lang/Object;->()V + + return-void +.end method + + +# virtual methods +.method public run()Ljava/lang/String; + .registers 2 + + .prologue + iget-object v0, p0, LTestDuplicatedNames;->fieldName:Ljava/lang/String; + + return-object v0 +.end method + +.method public run()Ljava/lang/Object; + .registers 2 + + .prologue + iget-object v0, p0, LTestDuplicatedNames;->fieldName:Ljava/lang/Object; + + return-object v0 +.end method diff --git a/jadx-core/src/test/smali/trycatch/TestMultiExceptionCatchSameJump.smali b/jadx-core/src/test/smali/trycatch/TestMultiExceptionCatchSameJump.smali new file mode 100644 index 000000000..e90710605 --- /dev/null +++ b/jadx-core/src/test/smali/trycatch/TestMultiExceptionCatchSameJump.smali @@ -0,0 +1,37 @@ +.class public Ltrycatch/TestMultiExceptionCatchSameJump; +.super Ljava/lang/Object; +.source "TestMultiExceptionCatchSameJump.java" + +.method public test()V + .locals 2 + + .line 17 + :try_start_0 + sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream; + + const-string v1, "Test" + + invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V + :try_end_0 + .catch Ljava/security/ProviderException; {:try_start_0 .. :try_end_0} :catch_0 + .catch Ljava/time/DateTimeException; {:try_start_0 .. :try_end_0} :catch_0 + + .line 20 + nop + + .line 22 + return-void + + .line 18 + :catch_0 + move-exception v0 + + .line 19 + .local v0, "e":Ljava/lang/RuntimeException; + new-instance v1, Ljava/lang/RuntimeException; + + invoke-direct {v1, v0}, Ljava/lang/RuntimeException;->(Ljava/lang/Throwable;)V + + throw v1 +.end method + diff --git a/jadx-gui/build.gradle b/jadx-gui/build.gradle index 65b3dd065..789ec0244 100644 --- a/jadx-gui/build.gradle +++ b/jadx-gui/build.gradle @@ -16,9 +16,11 @@ dependencies { compile 'hu.kazocsaba:image-viewer:1.2.3' compile 'org.apache.commons:commons-lang3:3.8.1' + compile 'org.apache.commons:commons-text:1.6' compile 'io.reactivex.rxjava2:rxjava:2.2.5' compile "com.github.akarnokd:rxjava2-swing:0.3.3" + compile 'com.android.tools.build:apksig:3.3.0' } applicationDistribution.with { @@ -54,7 +56,7 @@ launch4j { mainClassName = 'jadx.gui.JadxGUI' copyConfigurable = project.tasks.shadowJar.outputs.files jar = "lib/${project.tasks.shadowJar.archiveName}" -// icon = "${projectDir}/icons/myApp.ico" + icon = "${projectDir}/src/main/resources/logos/jadx-logo.ico" outfile = "jadx-gui-${version}.exe" copyright = 'Skylot' windowTitle = 'jadx' diff --git a/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java b/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java index f11a0bcf1..11bf8c68c 100644 --- a/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java +++ b/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java @@ -10,6 +10,7 @@ import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import jadx.api.JadxArgs; import jadx.api.JadxDecompiler; import jadx.api.JavaClass; import jadx.api.JavaPackage; @@ -67,7 +68,6 @@ public class JadxWrapper { /** * Get the complete list of classes - * @return */ public List getClasses() { return decompiler.getClasses(); @@ -75,19 +75,20 @@ public class JadxWrapper { /** * Get all classes that are not excluded by the excluded packages settings - * @return */ public List getIncludedClasses() { List classList = decompiler.getClasses(); String excludedPackages = settings.getExcludedPackages().trim(); - if (excludedPackages.length() == 0) + if (excludedPackages.length() == 0) { return classList; + } String[] excluded = excludedPackages.split("[ ]+"); return classList.stream().filter(cls -> { for (String exclude : excluded) { - if (cls.getFullName().startsWith(exclude)) + if (cls.getFullName().startsWith(exclude)) { return false; + } } return true; }).collect(Collectors.toList()); @@ -105,7 +106,7 @@ public class JadxWrapper { return openFile; } - public JadxSettings getSettings() { - return settings; + public JadxArgs getArgs() { + return decompiler.getArgs(); } } diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundWorker.java b/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundWorker.java index 59ad04e39..8d666e5e1 100644 --- a/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundWorker.java +++ b/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundWorker.java @@ -3,12 +3,12 @@ package jadx.gui.jobs; import javax.swing.*; import java.util.concurrent.Future; -import jadx.gui.utils.NLS; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.gui.ui.ProgressPanel; import jadx.gui.utils.CacheObject; +import jadx.gui.utils.NLS; import jadx.gui.utils.Utils; import jadx.gui.utils.search.TextSearchIndex; @@ -27,12 +27,7 @@ public class BackgroundWorker extends SwingWorker { if (isDone()) { return; } - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - progressPane.setVisible(true); - } - }); + SwingUtilities.invokeLater(() -> progressPane.setVisible(true)); addPropertyChangeListener(progressPane); execute(); } @@ -46,7 +41,7 @@ public class BackgroundWorker extends SwingWorker { } @Override - protected Void doInBackground() throws Exception { + protected Void doInBackground() { try { System.gc(); LOG.debug("Memory usage: Before decompile: {}", Utils.memoryInfo()); diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/DecompileJob.java b/jadx-gui/src/main/java/jadx/gui/jobs/DecompileJob.java index 983165067..65675072c 100644 --- a/jadx-gui/src/main/java/jadx/gui/jobs/DecompileJob.java +++ b/jadx-gui/src/main/java/jadx/gui/jobs/DecompileJob.java @@ -11,12 +11,7 @@ public class DecompileJob extends BackgroundJob { protected void runJob() { for (final JavaClass cls : wrapper.getIncludedClasses()) { - addTask(new Runnable() { - @Override - public void run() { - cls.decompile(); - } - }); + addTask(cls::decompile); } } 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 c88a6ba3e..4c134d0bc 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java @@ -150,7 +150,6 @@ public class JadxSettings extends JadxCLIArgs { return true; } - public boolean isShowHeapUsageBar() { return showHeapUsageBar; } diff --git a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsAdapter.java b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsAdapter.java index 66a7a09e5..30f3c44ca 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsAdapter.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsAdapter.java @@ -21,7 +21,7 @@ public class JadxSettingsAdapter { private static final Preferences PREFS = Preferences.userNodeForPackage(JadxGUI.class); - private static ExclusionStrategy EXCLUDE_FIELDS = new ExclusionStrategy() { + private static final ExclusionStrategy EXCLUDE_FIELDS = new ExclusionStrategy() { @Override public boolean shouldSkipField(FieldAttributes f) { return JadxSettings.SKIP_FIELDS.contains(f.getName()) 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 e61027f74..0ada8165b 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java @@ -1,8 +1,6 @@ package jadx.gui.settings; import javax.swing.*; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; import java.awt.*; import java.awt.event.ItemEvent; import java.awt.event.MouseAdapter; @@ -19,8 +17,6 @@ import jadx.gui.ui.codearea.EditorTheme; import jadx.gui.utils.LangLocale; import jadx.gui.utils.NLS; -import static jadx.gui.utils.Utils.FONT_HACK; - public class JadxSettingsWindow extends JDialog { private static final long serialVersionUID = -1804570470377354148L; @@ -40,7 +36,6 @@ public class JadxSettingsWindow extends JDialog { this.prevLang = settings.getLangLocale(); initUI(); - registerBundledFonts(); setTitle(NLS.str("preferences.title")); setSize(400, 550); @@ -50,13 +45,6 @@ public class JadxSettingsWindow extends JDialog { setLocationRelativeTo(null); } - public static void registerBundledFonts() { - GraphicsEnvironment grEnv = GraphicsEnvironment.getLocalGraphicsEnvironment(); - if (FONT_HACK != null) { - grEnv.registerFont(FONT_HACK); - } - } - private void initUI() { JPanel panel = new JPanel(); panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS)); @@ -204,7 +192,6 @@ public class JadxSettingsWindow extends JDialog { int i = themesCbx.getSelectedIndex(); EditorTheme editorTheme = editorThemes[i]; settings.setEditorThemePath(editorTheme.getPath()); - mainWindow.setEditorTheme(editorTheme.getPath()); mainWindow.loadSettings(); }); @@ -245,11 +232,11 @@ public class JadxSettingsWindow extends JDialog { }); JButton editExcludedPackages = new JButton(NLS.str("preferences.excludedPackages.button")); - editExcludedPackages.addActionListener( event -> { + editExcludedPackages.addActionListener(event -> { String result = JOptionPane.showInputDialog(this, NLS.str("preferences.excludedPackages.editDialog"), settings.getExcludedPackages()); - if (result !=null) { + if (result != null) { settings.setExcludedPackages(result); } }); diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/ApkSignature.java b/jadx-gui/src/main/java/jadx/gui/treemodel/ApkSignature.java new file mode 100644 index 000000000..9e9f3f266 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/ApkSignature.java @@ -0,0 +1,184 @@ +package jadx.gui.treemodel; + +import javax.swing.*; +import java.io.File; +import java.security.cert.Certificate; +import java.util.List; +import java.util.stream.Collectors; + +import com.android.apksig.ApkVerifier; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.apache.commons.text.StringEscapeUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.gui.JadxWrapper; +import jadx.gui.utils.CertificateManager; +import jadx.gui.utils.NLS; +import jadx.gui.utils.Utils; + +public class ApkSignature extends JNode { + private static final long serialVersionUID = -9121321926113143407L; + + private static final Logger LOG = LoggerFactory.getLogger(ApkSignature.class); + + private static final ImageIcon CERTIFICATE_ICON = Utils.openIcon("certificate_obj"); + + private final transient File openFile; + private String content; + + public static ApkSignature getApkSignature(JadxWrapper wrapper) { + // Only show the ApkSignature node if an AndroidManifest.xml is present. + // Without a manifest the Google ApkVerifier refuses to work. + if (wrapper.getResources().stream().noneMatch(r -> "AndroidManifest.xml".equals(r.getName()))) { + return null; + } + File openFile = wrapper.getOpenFile(); + return new ApkSignature(openFile); + } + + public ApkSignature(File openFile) { + this.openFile = openFile; + } + + @Override + public JClass getJParent() { + return null; + } + + @Override + public Icon getIcon() { + return CERTIFICATE_ICON; + } + + @Override + public String makeString() { + return "APK signature"; + } + + @Override + public String getContent() { + if (content != null) { + return this.content; + } + ApkVerifier verifier = new ApkVerifier.Builder(openFile).build(); + try { + ApkVerifier.Result result = verifier.verify(); + StringEscapeUtils.Builder builder = StringEscapeUtils.builder(StringEscapeUtils.ESCAPE_HTML4); + builder.append("

APK signature verification result:

"); + + builder.append("

"); + if (result.isVerified()) { + builder.escape(NLS.str("apkSignature.verificationSuccess")); + } else { + builder.escape(NLS.str("apkSignature.verificationFailed")); + } + builder.append("

"); + + final String err = NLS.str("apkSignature.errors"); + final String warn = NLS.str("apkSignature.warnings"); + final String sigSucc = NLS.str("apkSignature.signatureSuccess"); + final String sigFail = NLS.str("apkSignature.signatureFailed"); + + writeIssues(builder, err, result.getErrors()); + writeIssues(builder, warn, result.getWarnings()); + + if (!result.getV1SchemeSigners().isEmpty()) { + builder.append("

"); + builder.escape(String.format(result.isVerifiedUsingV1Scheme() ? sigSucc : sigFail, 1)); + builder.append("

\n"); + + builder.append("
"); + for (ApkVerifier.Result.V1SchemeSignerInfo signer : result.getV1SchemeSigners()) { + builder.append("

"); + builder.escape(NLS.str("apkSignature.signer")); + builder.append(" "); + builder.escape(signer.getName()); + builder.append(" ("); + builder.escape(signer.getSignatureFileName()); + builder.append(")"); + builder.append("

"); + writeCertificate(builder, signer.getCertificate()); + writeIssues(builder, err, signer.getErrors()); + writeIssues(builder, warn, signer.getWarnings()); + } + builder.append("
"); + } + if (!result.getV2SchemeSigners().isEmpty()) { + builder.append("

"); + builder.escape(String.format(result.isVerifiedUsingV2Scheme() ? sigSucc : sigFail, 2)); + builder.append("

\n"); + + builder.append("
"); + for (ApkVerifier.Result.V2SchemeSignerInfo signer : result.getV2SchemeSigners()) { + builder.append("

"); + builder.escape(NLS.str("apkSignature.signer")); + builder.append(" "); + builder.append(Integer.toString(signer.getIndex() + 1)); + builder.append("

"); + writeCertificate(builder, signer.getCertificate()); + writeIssues(builder, err, signer.getErrors()); + writeIssues(builder, warn, signer.getWarnings()); + } + builder.append("
"); + } + this.content = builder.toString(); + } catch (Exception e) { + LOG.error(e.getMessage(), e); + StringEscapeUtils.Builder builder = StringEscapeUtils.builder(StringEscapeUtils.ESCAPE_HTML4); + builder.append("

"); + builder.escape(NLS.str("apkSignature.exception")); + builder.append("

");
+			builder.escape(ExceptionUtils.getStackTrace(e));
+			builder.append("
"); + return builder.toString(); + } + return this.content; + } + + private void writeCertificate(StringEscapeUtils.Builder builder, Certificate cert) { + CertificateManager certMgr = new CertificateManager(cert); + builder.append("
");
+		builder.escape(certMgr.generateHeader());
+		builder.append("
");
+		builder.escape(certMgr.generatePublicKey());
+		builder.append("
");
+		builder.escape(certMgr.generateSignature());
+		builder.append("
");
+		builder.append(certMgr.generateFingerprint());
+		builder.append("
"); + } + + private void writeIssues(StringEscapeUtils.Builder builder, String issueType, List issueList) { + if (!issueList.isEmpty()) { + builder.append("

"); + builder.escape(issueType); + builder.append("

"); + builder.append("
"); + // Unprotected Zip entry issues are very common, handle them separately + List unprotIssues = issueList.stream().filter(i -> + i.getIssue() == ApkVerifier.Issue.JAR_SIG_UNPROTECTED_ZIP_ENTRY).collect(Collectors.toList()); + if (!unprotIssues.isEmpty()) { + builder.append("

"); + builder.escape(NLS.str("apkSignature.unprotectedEntry")); + builder.append("

"); + for (ApkVerifier.IssueWithParams issue : unprotIssues) { + builder.escape((String) issue.getParams()[0]); + builder.append("
"); + } + builder.append("
"); + } + List remainingIssues = issueList.stream().filter(i -> + i.getIssue() != ApkVerifier.Issue.JAR_SIG_UNPROTECTED_ZIP_ENTRY).collect(Collectors.toList()); + if (!remainingIssues.isEmpty()) { + builder.append("
\n");
+				for (ApkVerifier.IssueWithParams issue : remainingIssues) {
+					builder.escape(issue.toString());
+					builder.append("\n");
+				}
+				builder.append("
\n"); + } + builder.append("
"); + } + } +} 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 73f8b967b..753af2523 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java @@ -2,6 +2,7 @@ package jadx.gui.treemodel; import javax.swing.*; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -11,14 +12,13 @@ import org.jetbrains.annotations.NotNull; import jadx.api.ResourceFile; import jadx.api.ResourceFileContent; import jadx.api.ResourceType; +import jadx.api.ResourcesLoader; import jadx.core.codegen.CodeWriter; import jadx.core.xmlgen.ResContainer; import jadx.gui.utils.NLS; import jadx.gui.utils.OverlayIcon; import jadx.gui.utils.Utils; -import static jadx.api.ResourceFileContent.createResourceFileContentInstance; - public class JResource extends JLoadableNode implements Comparable { private static final long serialVersionUID = -201018424302612434L; @@ -43,7 +43,7 @@ public class JResource extends JLoadableNode implements Comparable { private transient boolean loaded; private transient String content; - private transient Map lineMapping; + private transient Map lineMapping = Collections.emptyMap(); public JResource(ResourceFile resFile, String name, JResType type) { this(resFile, name, name, type); @@ -58,15 +58,16 @@ public class JResource extends JLoadableNode implements Comparable { } public final void update() { - removeAllChildren(); - if (!loaded) { + if (files.isEmpty()) { if (type == JResType.DIR || type == JResType.ROOT || resFile.getType() == ResourceType.ARSC) { + // fake leaf to force show expand button + // real sub nodes will load on expand in loadNode() method add(new TextNode(NLS.str("tree.loading"))); } } else { - loadContent(); + removeAllChildren(); for (JResource res : files) { res.update(); add(res); @@ -76,13 +77,8 @@ public class JResource extends JLoadableNode implements Comparable { @Override public void loadNode() { - loadContent(); - loaded = true; - update(); - } - - private void loadContent() { getContent(); + update(); } @Override @@ -95,40 +91,68 @@ public class JResource extends JLoadableNode implements Comparable { } @Override - public String getContent() { - if (!loaded && resFile != null && type == JResType.FILE) { - loaded = true; - if (isSupportedForView(resFile.getType())) { - ResContainer rc = resFile.loadContent(); - if (rc != null) { - addSubFiles(rc, this, 0); - } - } + public synchronized String getContent() { + if (loaded) { + return content; } - return content; + if (resFile == null || type != JResType.FILE) { + return null; + } + if (!isSupportedForView(resFile.getType())) { + return null; + } + ResContainer rc = resFile.loadContent(); + if (rc == null) { + return null; + } + if (rc.getDataType() == ResContainer.DataType.RES_TABLE) { + content = loadCurrentSingleRes(rc); + for (ResContainer subFile : rc.getSubFiles()) { + loadSubNodes(this, subFile, 1); + } + loaded = true; + return content; + } + // single node + return loadCurrentSingleRes(rc); } - private void addSubFiles(ResContainer rc, JResource root, int depth) { - CodeWriter cw = rc.getContent(); - if (cw != null) { - if (depth == 0) { - root.lineMapping = cw.getLineMapping(); - root.content = cw.toString(); - } else { - String resName = rc.getName(); - String[] path = resName.split("/"); - String resShortName = path.length == 0 ? resName : path[path.length - 1]; - ResourceFileContent fileContent = createResourceFileContentInstance(resShortName, ResourceType.XML, cw); - if (fileContent != null) { - addPath(path, root, new JResource(fileContent, resName, resShortName, JResType.FILE)); + private String loadCurrentSingleRes(ResContainer rc) { + switch (rc.getDataType()) { + case TEXT: + case RES_TABLE: + CodeWriter cw = rc.getText(); + lineMapping = cw.getLineMapping(); + return cw.toString(); + + case RES_LINK: + try { + return ResourcesLoader.decodeStream(rc.getResLink(), (size, is) -> { + if (size > 10 * 1024 * 1024L) { + return "File too large for view"; + } + return ResourcesLoader.loadToCodeWriter(is).toString(); + }); + } catch (Exception e) { + return "Failed to load resource file: \n" + jadx.core.utils.Utils.getStackTrace(e); } - } + + case DECODED_DATA: + default: + return "Unexpected resource type: " + rc; } - List subFiles = rc.getSubFiles(); - if (!subFiles.isEmpty()) { - for (ResContainer subFile : subFiles) { - addSubFiles(subFile, root, depth + 1); - } + } + + private void loadSubNodes(JResource root, ResContainer rc, int depth) { + String resName = rc.getName(); + String[] path = resName.split("/"); + String resShortName = path.length == 0 ? resName : path[path.length - 1]; + CodeWriter cw = rc.getText(); + ResourceFileContent fileContent = new ResourceFileContent(resShortName, ResourceType.XML, cw); + addPath(path, root, new JResource(fileContent, resName, resShortName, JResType.FILE)); + + for (ResContainer subFile : rc.getSubFiles()) { + loadSubNodes(root, subFile, depth + 1); } } @@ -190,25 +214,29 @@ public class JResource extends JLoadableNode implements Comparable { } } + private static final Map EXTENSION_TO_FILE_SYNTAX = jadx.core.utils.Utils.newConstStringMap( + "java", SyntaxConstants.SYNTAX_STYLE_JAVA, + "js", SyntaxConstants.SYNTAX_STYLE_JAVASCRIPT, + "ts", SyntaxConstants.SYNTAX_STYLE_TYPESCRIPT, + "json", SyntaxConstants.SYNTAX_STYLE_JSON, + "css", SyntaxConstants.SYNTAX_STYLE_CSS, + "less", SyntaxConstants.SYNTAX_STYLE_LESS, + "html", SyntaxConstants.SYNTAX_STYLE_HTML, + "xml", SyntaxConstants.SYNTAX_STYLE_XML, + "yaml", SyntaxConstants.SYNTAX_STYLE_YAML, + "properties", SyntaxConstants.SYNTAX_STYLE_PROPERTIES_FILE, + "ini", SyntaxConstants.SYNTAX_STYLE_INI, + "sql", SyntaxConstants.SYNTAX_STYLE_SQL, + "arsc", SyntaxConstants.SYNTAX_STYLE_XML + ); + private String getSyntaxByExtension(String name) { int dot = name.lastIndexOf('.'); if (dot == -1) { return null; } String ext = name.substring(dot + 1); - if (ext.equals("js")) { - return SyntaxConstants.SYNTAX_STYLE_JAVASCRIPT; - } - if (ext.equals("css")) { - return SyntaxConstants.SYNTAX_STYLE_CSS; - } - if (ext.equals("html")) { - return SyntaxConstants.SYNTAX_STYLE_HTML; - } - if (ext.equals("arsc")) { - return SyntaxConstants.SYNTAX_STYLE_XML; - } - return null; + return EXTENSION_TO_FILE_SYNTAX.get(ext); } @Override @@ -256,6 +284,10 @@ public class JResource extends JLoadableNode implements Comparable { return resFile; } + public Map getLineMapping() { + return lineMapping; + } + @Override public JClass getJParent() { return null; diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JRoot.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JRoot.java index e21fc0bf6..e90335d11 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JRoot.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JRoot.java @@ -37,6 +37,11 @@ public class JRoot extends JNode { add(jRes); } + ApkSignature signature = ApkSignature.getApkSignature(wrapper); + if (signature != null) { + add(signature); + } + JCertificate certificate = getCertificate(wrapper.getResources()); if (certificate != null) { add(certificate); diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JSources.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JSources.java index a0208e1ac..d6541fb8c 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JSources.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JSources.java @@ -85,7 +85,7 @@ public class JSources extends JNode { } } // use identity set for collect inner packages - Set innerPackages = Collections.newSetFromMap(new IdentityHashMap()); + Set innerPackages = Collections.newSetFromMap(new IdentityHashMap<>()); for (JPackage pkg : pkgMap.values()) { innerPackages.addAll(pkg.getInnerPackages()); } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/AboutDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/AboutDialog.java index 33c9252d6..9be61b539 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/AboutDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/AboutDialog.java @@ -2,8 +2,6 @@ package jadx.gui.ui; import javax.swing.*; import java.awt.*; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; import jadx.api.JadxDecompiler; import jadx.gui.utils.NLS; @@ -42,11 +40,7 @@ class AboutDialog extends JDialog { textPane.add(Box.createRigidArea(new Dimension(0, 20))); JButton close = new JButton(NLS.str("tabs.close")); - close.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent event) { - dispose(); - } - }); + close.addActionListener(event -> dispose()); close.setAlignmentX(0.5f); Container contentPane = getContentPane(); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/CommonSearchDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/CommonSearchDialog.java index 597239bc0..307d6ed80 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/CommonSearchDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/CommonSearchDialog.java @@ -33,14 +33,14 @@ import jadx.gui.jobs.DecompileJob; import jadx.gui.treemodel.JNode; import jadx.gui.ui.codearea.CodeArea; import jadx.gui.utils.CacheObject; -import jadx.gui.utils.NLS; import jadx.gui.utils.JumpPosition; +import jadx.gui.utils.NLS; import jadx.gui.utils.search.TextSearchIndex; public abstract class CommonSearchDialog extends JDialog { + private static final long serialVersionUID = 8939332306115370276L; private static final Logger LOG = LoggerFactory.getLogger(CommonSearchDialog.class); - private static final long serialVersionUID = 8939332306115370276L; public static final int RESULTS_PER_PAGE = 100; @@ -397,12 +397,14 @@ public abstract class CommonSearchDialog extends JDialog { protected class ResultsTableCellRenderer implements TableCellRenderer { private final JLabel emptyLabel = new JLabel(); + private final Font font; private final Color codeSelectedColor; private final Color codeBackground; - private Map componentCache = new HashMap<>(); + private final Map componentCache = new HashMap<>(); public ResultsTableCellRenderer() { RSyntaxTextArea area = CodeArea.getDefaultArea(mainWindow); + this.font = area.getFont(); this.codeSelectedColor = area.getSelectionColor(); this.codeBackground = area.getBackground(); } @@ -414,7 +416,7 @@ public abstract class CommonSearchDialog extends JDialog { Component comp = componentCache.get(id); if (comp == null) { if (obj instanceof JNode) { - comp = makeCell(table, (JNode) obj, column); + comp = makeCell((JNode) obj, column); componentCache.put(id, comp); } else { comp = emptyLabel; @@ -442,10 +444,10 @@ public abstract class CommonSearchDialog extends JDialog { } } - private Component makeCell(JTable table, JNode node, int column) { + private Component makeCell(JNode node, int column) { if (column == 0) { JLabel label = new JLabel(node.makeLongString() + " ", node.getIcon(), SwingConstants.LEFT); - label.setFont(table.getFont()); + label.setFont(font); label.setOpaque(true); label.setToolTipText(label.getText()); return label; diff --git a/jadx-gui/src/main/java/jadx/gui/ui/HeapUsageBar.java b/jadx-gui/src/main/java/jadx/gui/ui/HeapUsageBar.java index 29abbdb45..5155437b1 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/HeapUsageBar.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/HeapUsageBar.java @@ -1,50 +1,62 @@ package jadx.gui.ui; -import jadx.gui.utils.NLS; - import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.gui.utils.NLS; +import jadx.gui.utils.Utils; public class HeapUsageBar extends JProgressBar implements ActionListener { + private static final Logger LOG = LoggerFactory.getLogger(HeapUsageBar.class); + private static final long serialVersionUID = -8739563124249884967L; - private static final double TWO_TO_20 = 1048576d; // 1024 * 1024 + private static final double TWO_TO_20 = 1048576d; - private final Color GREEN = new Color(0, 180, 0); - private final Color RED = new Color(200, 0, 0); + private static final Color GREEN = new Color(0, 180, 0); + private static final Color RED = new Color(200, 0, 0); - private final Runtime r; - - private String maxHeapStr; - - private final Timer timer; - - private final double maxGB; + private final transient Runtime runtime = Runtime.getRuntime(); + private final transient Timer timer; private final String textFormat; + private final double maxGB; public HeapUsageBar() { - super(); - textFormat = NLS.str("heapUsage.text"); - r = Runtime.getRuntime(); + this.textFormat = NLS.str("heapUsage.text"); setBorderPainted(false); setStringPainted(true); setValue(10); - int maxKB = (int) (r.maxMemory() / 1024); + int maxKB = (int) (runtime.maxMemory() / 1024); setMaximum(maxKB); maxGB = maxKB / TWO_TO_20; update(); - timer = new Timer(1000, this); + timer = new Timer(2000, this); + addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + Runtime.getRuntime().gc(); + update(); + if (LOG.isDebugEnabled()) { + LOG.debug("Memory used: {}", Utils.memoryInfo()); + } + } + }); } public void update() { - long used = r.totalMemory() - r.freeMemory(); + long used = runtime.totalMemory() - runtime.freeMemory(); int usedKB = (int) (used / 1024); setValue(usedKB); setString(String.format(textFormat, (usedKB / TWO_TO_20), maxGB)); - if (used > r.totalMemory() * 0.8) { + if ((used + Utils.MIN_FREE_MEMORY) > runtime.maxMemory()) { setForeground(RED); } else { setForeground(GREEN); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/HtmlPanel.java b/jadx-gui/src/main/java/jadx/gui/ui/HtmlPanel.java new file mode 100644 index 000000000..52af564cd --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/HtmlPanel.java @@ -0,0 +1,50 @@ +package jadx.gui.ui; + +import javax.swing.*; +import java.awt.*; + +import jadx.gui.settings.JadxSettings; +import jadx.gui.treemodel.JNode; + +public final class HtmlPanel extends ContentPanel { + private static final long serialVersionUID = -6251262855835426245L; + + private final JHtmlPane textArea; + + public HtmlPanel(TabbedPane panel, JNode jnode) { + super(panel, jnode); + setLayout(new BorderLayout()); + textArea = new JHtmlPane(); + loadSettings(); + textArea.setText(jnode.getContent()); + textArea.setCaretPosition(0); // otherwise the start view will be the last line + textArea.setEditable(false); + JScrollPane sp = new JScrollPane(textArea); + add(sp); + } + + @Override + public void loadSettings() { + JadxSettings settings = getTabbedPane().getMainWindow().getSettings(); + textArea.setFont(settings.getFont()); + } + + private static final class JHtmlPane extends JEditorPane { + private static final long serialVersionUID = 6886040384052136157L; + + public JHtmlPane() { + setContentType("text/html"); + } + + @Override + public void paint(Graphics g) { + Graphics2D g2d = (Graphics2D) g.create(); + try { + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + super.paint(g2d); + } finally { + g2d.dispose(); + } + } + } +} 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 d5a8c137e..404bf66ed 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/ImagePanel.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/ImagePanel.java @@ -1,27 +1,57 @@ package jadx.gui.ui; +import javax.imageio.ImageIO; import java.awt.*; import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; import hu.kazocsaba.imageviewer.ImageViewer; +import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import jadx.api.ResourceFile; +import jadx.api.ResourcesLoader; +import jadx.core.utils.Utils; +import jadx.core.utils.exceptions.JadxRuntimeException; +import jadx.core.xmlgen.ResContainer; import jadx.gui.treemodel.JResource; +import jadx.gui.ui.codearea.CodeArea; public class ImagePanel extends ContentPanel { - private static final long serialVersionUID = 4071356367073142688L; ImagePanel(TabbedPane panel, JResource res) { super(panel, res); - - ResourceFile resFile = res.getResFile(); - BufferedImage img = resFile.loadContent().getImage(); - ImageViewer imageViewer = new ImageViewer(img); - imageViewer.setZoomFactor(2.); - setLayout(new BorderLayout()); - add(imageViewer.getComponent()); + try { + BufferedImage img = loadImage(res); + ImageViewer imageViewer = new ImageViewer(img); + add(imageViewer.getComponent()); + } catch (Exception e) { + RSyntaxTextArea textArea = CodeArea.getDefaultArea(panel.getMainWindow()); + textArea.setText("Image load error: \n" + Utils.getStackTrace(e)); + add(textArea); + } + } + + private BufferedImage loadImage(JResource res) { + ResourceFile resFile = res.getResFile(); + ResContainer resContainer = resFile.loadContent(); + ResContainer.DataType dataType = resContainer.getDataType(); + if (dataType == ResContainer.DataType.DECODED_DATA) { + try { + return ImageIO.read(new ByteArrayInputStream(resContainer.getDecodedData())); + } catch (Exception e) { + throw new JadxRuntimeException("Failed to load image", e); + } + } else if (dataType == ResContainer.DataType.RES_LINK) { + try { + return ResourcesLoader.decodeStream(resFile, (size, is) -> ImageIO.read(is)); + } catch (Exception e) { + throw new JadxRuntimeException("Failed to load image", e); + } + } else { + throw new JadxRuntimeException("Unsupported resource image data type: " + resFile); + } } @Override diff --git a/jadx-gui/src/main/java/jadx/gui/ui/MainDropTarget.java b/jadx-gui/src/main/java/jadx/gui/ui/MainDropTarget.java index 604b2615f..c635cbfd4 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/MainDropTarget.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/MainDropTarget.java @@ -59,7 +59,7 @@ public class MainDropTarget implements DropTargetListener { try { Transferable transferable = dtde.getTransferable(); List transferData = (List) transferable.getTransferData(DataFlavor.javaFileListFlavor); - if (transferData != null && transferData.size() > 0) { + if (!transferData.isEmpty()) { dtde.dropComplete(true); // load first file mainWindow.openFile(transferData.get(0)); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java index 7315cc828..14be9ca2b 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java @@ -9,7 +9,6 @@ import javax.swing.filechooser.FileNameExtensionFilter; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeCellRenderer; import javax.swing.tree.DefaultTreeModel; -import javax.swing.tree.ExpandVetoException; import javax.swing.tree.TreeNode; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; @@ -24,7 +23,9 @@ import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.io.File; import java.io.FileInputStream; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.Timer; import java.util.TimerTask; @@ -32,6 +33,7 @@ import org.fife.ui.rsyntaxtextarea.Theme; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import jadx.api.JadxArgs; import jadx.api.ResourceFile; import jadx.gui.JadxWrapper; import jadx.gui.jobs.BackgroundWorker; @@ -39,6 +41,7 @@ import jadx.gui.jobs.DecompileJob; import jadx.gui.jobs.IndexJob; import jadx.gui.settings.JadxSettings; import jadx.gui.settings.JadxSettingsWindow; +import jadx.gui.treemodel.ApkSignature; import jadx.gui.treemodel.JCertificate; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JLoadableNode; @@ -49,9 +52,9 @@ import jadx.gui.update.JadxUpdate; import jadx.gui.update.JadxUpdate.IUpdateCallback; import jadx.gui.update.data.Release; import jadx.gui.utils.CacheObject; +import jadx.gui.utils.JumpPosition; import jadx.gui.utils.Link; import jadx.gui.utils.NLS; -import jadx.gui.utils.JumpPosition; import jadx.gui.utils.Utils; import static javax.swing.KeyStroke.getKeyStroke; @@ -95,7 +98,6 @@ public class MainWindow extends JFrame { private boolean isFlattenPackage; private JToggleButton flatPkgButton; private JCheckBoxMenuItem flatPkgMenuItem; - private JCheckBoxMenuItem heapUsageBarMenuItem; private JToggleButton deobfToggleBtn; private JCheckBoxMenuItem deobfMenuItem; @@ -111,16 +113,21 @@ public class MainWindow extends JFrame { this.cacheObject = new CacheObject(); resetCache(); + registerBundledFonts(); initUI(); initMenuAndToolbar(); - applySettings(); - checkForUpdate(); + setWindowIcons(); } - private void applySettings() { - setFont(settings.getFont()); - setEditorTheme(settings.getEditorThemePath()); + private void setWindowIcons() { + List icons = new ArrayList<>(); + icons.add(Utils.openImage("/logos/jadx-logo-16px.png")); + icons.add(Utils.openImage("/logos/jadx-logo-32px.png")); + icons.add(Utils.openImage("/logos/jadx-logo-48px.png")); + icons.add(Utils.openImage("/logos/jadx-logo.png")); + setIconImages(icons); loadSettings(); + checkForUpdate(); } public void open() { @@ -222,12 +229,6 @@ public class MainWindow extends JFrame { } private void saveAll(boolean export) { - settings.setExportAsGradleProject(export); - if (export) { - settings.setSkipSources(false); - settings.setSkipResources(false); - } - JFileChooser fileChooser = new JFileChooser(); fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); fileChooser.setToolTipText(NLS.str("file.save_all_msg")); @@ -239,6 +240,15 @@ public class MainWindow extends JFrame { int ret = fileChooser.showDialog(mainPanel, NLS.str("file.select")); if (ret == JFileChooser.APPROVE_OPTION) { + JadxArgs decompilerArgs = wrapper.getArgs(); + decompilerArgs.setExportAsGradleProject(export); + if (export) { + decompilerArgs.setSkipSources(false); + decompilerArgs.setSkipResources(false); + } else { + decompilerArgs.setSkipSources(settings.isSkipSources()); + decompilerArgs.setSkipResources(settings.isSkipResources()); + } settings.setLastSaveFilePath(fileChooser.getCurrentDirectory().getPath()); ProgressMonitor progressMonitor = new ProgressMonitor(mainPanel, NLS.str("msg.saving_sources"), "", 0, 100); progressMonitor.setMillisToPopup(0); @@ -290,15 +300,17 @@ public class MainWindow extends JFrame { private void treeClickAction() { try { Object obj = tree.getLastSelectedPathComponent(); + if (obj == null) { + return; + } if (obj instanceof JResource) { JResource res = (JResource) obj; ResourceFile resFile = res.getResFile(); if (resFile != null && JResource.isSupportedForView(resFile.getType())) { tabbedPane.showResource(res); } - } else if (obj instanceof JCertificate) { - JCertificate cert = (JCertificate) obj; - tabbedPane.showCertificate(cert); + } else if ((obj instanceof JCertificate) || (obj instanceof ApkSignature)) { + tabbedPane.showSimpleNode((JNode) obj); } else if (obj instanceof JNode) { JNode node = (JNode) obj; JClass cls = node.getRootClass(); @@ -388,7 +400,7 @@ public class MainWindow extends JFrame { flatPkgMenuItem = new JCheckBoxMenuItem(NLS.str("menu.flatten"), ICON_FLAT_PKG); flatPkgMenuItem.setState(isFlattenPackage); - heapUsageBarMenuItem = new JCheckBoxMenuItem(NLS.str("menu.heapUsageBar")); + JCheckBoxMenuItem heapUsageBarMenuItem = new JCheckBoxMenuItem(NLS.str("menu.heapUsageBar")); heapUsageBarMenuItem.setState(settings.isShowHeapUsageBar()); heapUsageBarMenuItem.addActionListener(event -> { settings.setShowHeapUsageBar(!settings.isShowHeapUsageBar()); @@ -595,7 +607,7 @@ public class MainWindow extends JFrame { }); tree.addTreeWillExpandListener(new TreeWillExpandListener() { @Override - public void treeWillExpand(TreeExpansionEvent event) throws ExpandVetoException { + public void treeWillExpand(TreeExpansionEvent event) { TreePath path = event.getPath(); Object node = path.getLastPathComponent(); if (node instanceof JLoadableNode) { @@ -604,7 +616,8 @@ public class MainWindow extends JFrame { } @Override - public void treeWillCollapse(TreeExpansionEvent event) throws ExpandVetoException { + public void treeWillCollapse(TreeExpansionEvent event) { + // ignore } }); @@ -643,7 +656,14 @@ public class MainWindow extends JFrame { setFont(font); } - public void setEditorTheme(String editorThemePath) { + public static void registerBundledFonts() { + GraphicsEnvironment grEnv = GraphicsEnvironment.getLocalGraphicsEnvironment(); + if (Utils.FONT_HACK != null) { + grEnv.registerFont(Utils.FONT_HACK); + } + } + + private void setEditorTheme(String editorThemePath) { try { editorTheme = Theme.load(getClass().getResourceAsStream(editorThemePath)); } catch (Exception e) { @@ -661,6 +681,14 @@ public class MainWindow extends JFrame { } public void loadSettings() { + Font font = settings.getFont(); + Font largerFont = font.deriveFont(font.getSize() + 2.f); + + setFont(largerFont); + setEditorTheme(settings.getEditorThemePath()); + tree.setFont(largerFont); + tree.setRowHeight(-1); + tabbedPane.loadSettings(); } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/SearchDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/SearchDialog.java index 62c9dad58..69fdf98f2 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/SearchDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/SearchDialog.java @@ -176,6 +176,7 @@ public class SearchDialog extends CommonSearchDialog { .subscribeOn(Schedulers.single()) .doOnNext(r -> LOG.debug("search event: {}", r)) .switchMap(text -> prepareSearch(text) + .doOnError(e -> LOG.error("Error prepare search: {}", e.getMessage(), e)) .subscribeOn(Schedulers.single()) .toList() .toFlowable(), 1) diff --git a/jadx-gui/src/main/java/jadx/gui/ui/TabbedPane.java b/jadx-gui/src/main/java/jadx/gui/ui/TabbedPane.java index 08c6c226b..5e6183e15 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/TabbedPane.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/TabbedPane.java @@ -17,6 +17,7 @@ import org.slf4j.LoggerFactory; import jadx.api.ResourceFile; import jadx.api.ResourceType; +import jadx.gui.treemodel.ApkSignature; import jadx.gui.treemodel.JCertificate; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JNode; @@ -93,8 +94,8 @@ public class TabbedPane extends JTabbedPane { SwingUtilities.invokeLater(() -> setSelectedComponent(contentPanel)); } - public void showCertificate(JCertificate cert) { - final ContentPanel contentPanel = getContentPanel(cert); + public void showSimpleNode(JNode node) { + final ContentPanel contentPanel = getContentPanel(node); if (contentPanel == null) { return; } @@ -170,6 +171,9 @@ public class TabbedPane extends JTabbedPane { return null; } } + if (node instanceof ApkSignature) { + return new HtmlPanel(this, node); + } if (node instanceof JCertificate) { return new CertificatePanel(this, node); } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodePanel.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodePanel.java index aaabef16a..20de62c77 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodePanel.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodePanel.java @@ -6,13 +6,14 @@ import java.awt.event.ActionEvent; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; +import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JNode; +import jadx.gui.treemodel.JResource; import jadx.gui.ui.ContentPanel; import jadx.gui.ui.TabbedPane; import jadx.gui.utils.Utils; public final class CodePanel extends ContentPanel { - private static final long serialVersionUID = 5310536092010045565L; private final SearchBar searchBar; @@ -24,7 +25,6 @@ public final class CodePanel extends ContentPanel { codeArea = new CodeArea(this); searchBar = new SearchBar(codeArea); - scrollPane = new JScrollPane(codeArea); initLineNumbers(); @@ -37,7 +37,23 @@ public final class CodePanel extends ContentPanel { } private void initLineNumbers() { - scrollPane.setRowHeaderView(new LineNumbers(codeArea)); + // TODO: fix slow line rendering on big files + if (codeArea.getDocument().getLength() <= 100_000) { + LineNumbers numbers = new LineNumbers(codeArea); + numbers.setUseSourceLines(isUseSourceLines()); + scrollPane.setRowHeaderView(numbers); + } + } + + private boolean isUseSourceLines() { + if (node instanceof JClass) { + return true; + } + if (node instanceof JResource) { + JResource resNode = (JResource) node; + return !resNode.getLineMapping().isEmpty(); + } + return false; } private class SearchAction extends AbstractAction { diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/LineNumbers.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/LineNumbers.java index 04e2b86ba..468e890b9 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/LineNumbers.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/LineNumbers.java @@ -32,16 +32,16 @@ public class LineNumbers extends JPanel implements CaretListener { private static final int NUM_HEIGHT = Integer.MAX_VALUE - 1000000; private static final Map DESKTOP_HINTS = (Map) Toolkit.getDefaultToolkit().getDesktopProperty("awt.font.desktophints"); - private CodeArea codeArea; + private final CodeArea codeArea; private boolean useSourceLines = true; private int lastDigits; private int lastLine; private Map fonts; - private transient final Color numberColor; - private transient final Color currentColor; - private transient final Border border; + private final transient Color numberColor; + private final transient Color currentColor; + private final transient Border border; public LineNumbers(CodeArea component) { this.codeArea = component; @@ -199,12 +199,10 @@ public class LineNumbers extends JPanel implements CaretListener { String fontFamily = (String) as.getAttribute(StyleConstants.FontFamily); Integer fontSize = (Integer) as.getAttribute(StyleConstants.FontSize); String key = fontFamily + fontSize; - FontMetrics fm = fonts.get(key); - if (fm == null) { + FontMetrics fm = fonts.computeIfAbsent(key, k -> { Font font = new Font(fontFamily, Font.PLAIN, fontSize); - fm = codeArea.getFontMetrics(font); - fonts.put(key, fm); - } + return codeArea.getFontMetrics(font); + }); descent = Math.max(descent, fm.getDescent()); } } @@ -221,4 +219,8 @@ public class LineNumbers extends JPanel implements CaretListener { lastLine = currentLine; } } + + public void setUseSourceLines(boolean useSourceLines) { + this.useSourceLines = useSourceLines; + } } diff --git a/jadx-gui/src/main/java/jadx/gui/update/JadxUpdate.java b/jadx-gui/src/main/java/jadx/gui/update/JadxUpdate.java index f3c1fd2c1..c88ceca4f 100644 --- a/jadx-gui/src/main/java/jadx/gui/update/JadxUpdate.java +++ b/jadx-gui/src/main/java/jadx/gui/update/JadxUpdate.java @@ -6,9 +6,8 @@ import java.io.Reader; import java.lang.reflect.Type; import java.net.HttpURLConnection; import java.net.URL; -import java.util.Collections; +import java.nio.charset.StandardCharsets; import java.util.Comparator; -import java.util.Iterator; import java.util.List; import com.google.gson.Gson; @@ -32,12 +31,8 @@ public class JadxUpdate { private static final Type RELEASES_LIST_TYPE = new TypeToken>() { }.getType(); - private static final Comparator RELEASE_COMPARATOR = new Comparator() { - @Override - public int compare(Release o1, Release o2) { - return VersionComparator.checkAndCompare(o1.getName(), o2.getName()); - } - }; + private static final Comparator RELEASE_COMPARATOR = (o1, o2) -> + VersionComparator.checkAndCompare(o1.getName(), o2.getName()); public interface IUpdateCallback { void onUpdate(Release r); @@ -47,17 +42,14 @@ public class JadxUpdate { } public static void check(final IUpdateCallback callback) { - Runnable run = new Runnable() { - @Override - public void run() { - try { - Release release = checkForNewRelease(); - if (release != null) { - callback.onUpdate(release); - } - } catch (Exception e) { - LOG.debug("Jadx update error", e); + Runnable run = () -> { + try { + Release release = checkForNewRelease(); + if (release != null) { + callback.onUpdate(release); } + } catch (Exception e) { + LOG.debug("Jadx update error", e); } }; Thread thread = new Thread(run); @@ -77,17 +69,11 @@ public class JadxUpdate { if (list == null) { return null; } - for (Iterator it = list.iterator(); it.hasNext(); ) { - Release release = it.next(); - if (release.getName().equalsIgnoreCase(version) - || release.isPreRelease()) { - it.remove(); - } - } + list.removeIf(release -> release.getName().equalsIgnoreCase(version) || release.isPreRelease()); if (list.isEmpty()) { return null; } - Collections.sort(list, RELEASE_COMPARATOR); + list.sort(RELEASE_COMPARATOR); Release latest = list.get(list.size() - 1); if (VersionComparator.checkAndCompare(version, latest.getName()) >= 0) { return null; @@ -101,7 +87,7 @@ public class JadxUpdate { HttpURLConnection con = (HttpURLConnection) obj.openConnection(); con.setRequestMethod("GET"); if (con.getResponseCode() == 200) { - Reader reader = new InputStreamReader(con.getInputStream(), "UTF-8"); + Reader reader = new InputStreamReader(con.getInputStream(), StandardCharsets.UTF_8); return GSON.fromJson(reader, type); } return null; diff --git a/jadx-gui/src/main/java/jadx/gui/utils/CertificateManager.java b/jadx-gui/src/main/java/jadx/gui/utils/CertificateManager.java index b4069539d..a64ec479f 100755 --- a/jadx-gui/src/main/java/jadx/gui/utils/CertificateManager.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/CertificateManager.java @@ -21,8 +21,8 @@ public class CertificateManager { private static final Logger LOG = LoggerFactory.getLogger(CertificateManager.class); private static final String CERTIFICATE_TYPE_NAME = "X.509"; + private final Certificate cert; private X509Certificate x509cert; - private Certificate cert; public static String decode(InputStream in) { StringBuilder strBuild = new StringBuilder(); @@ -54,7 +54,7 @@ public class CertificateManager { } } - String generateHeader() { + public String generateHeader() { StringBuilder builder = new StringBuilder(); append(builder, NLS.str("certificate.cert_type"), x509cert.getType()); append(builder, NLS.str("certificate.serialSigVer"), ((Integer) x509cert.getVersion()).toString()); @@ -70,14 +70,14 @@ public class CertificateManager { return builder.toString(); } - String generateSignature() { + public String generateSignature() { StringBuilder builder = new StringBuilder(); append(builder, NLS.str("certificate.serialSigType"), x509cert.getSigAlgName()); append(builder, NLS.str("certificate.serialSigOID"), x509cert.getSigAlgOID()); return builder.toString(); } - String generateFingerprint() { + public String generateFingerprint() { StringBuilder builder = new StringBuilder(); try { append(builder, NLS.str("certificate.serialMD5"), getThumbPrint(x509cert, "MD5")); @@ -89,7 +89,7 @@ public class CertificateManager { return builder.toString(); } - String generatePublicKey() { + public String generatePublicKey() { PublicKey publicKey = x509cert.getPublicKey(); if (publicKey instanceof RSAPublicKey) { return generateRSAPublicKey(); @@ -106,6 +106,8 @@ public class CertificateManager { append(builder, NLS.str("certificate.serialPubKeyType"), pub.getAlgorithm()); append(builder, NLS.str("certificate.serialPubKeyExponent"), pub.getPublicExponent().toString(10)); + append(builder, NLS.str("certificate.serialPubKeyModulusSize"), Integer.toString( + pub.getModulus().toString(2).length())); append(builder, NLS.str("certificate.serialPubKeyModulus"), pub.getModulus().toString(10)); return builder.toString(); @@ -120,7 +122,7 @@ public class CertificateManager { return builder.toString(); } - String generateTextForX509() { + public String generateTextForX509() { StringBuilder builder = new StringBuilder(); if (x509cert != null) { builder.append(generateHeader()); @@ -136,7 +138,7 @@ public class CertificateManager { return builder.toString(); } - private String generateText() { + public String generateText() { StringBuilder str = new StringBuilder(); String type = cert.getType(); if (type.equals(CERTIFICATE_TYPE_NAME)) { diff --git a/jadx-gui/src/main/java/jadx/gui/utils/CodeLinesInfo.java b/jadx-gui/src/main/java/jadx/gui/utils/CodeLinesInfo.java index cb7a3a890..e2d2d0061 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/CodeLinesInfo.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/CodeLinesInfo.java @@ -9,7 +9,7 @@ import jadx.api.JavaMethod; import jadx.api.JavaNode; public class CodeLinesInfo { - private NavigableMap map = new TreeMap<>(); + private final NavigableMap map = new TreeMap<>(); public CodeLinesInfo(JavaClass cls) { addClass(cls); diff --git a/jadx-gui/src/main/java/jadx/gui/utils/CodeUsageInfo.java b/jadx-gui/src/main/java/jadx/gui/utils/CodeUsageInfo.java index 299caedab..6a5869334 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/CodeUsageInfo.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/CodeUsageInfo.java @@ -2,9 +2,9 @@ package jadx.gui.utils; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import jadx.api.CodePosition; import jadx.api.JavaClass; @@ -21,6 +21,10 @@ public class CodeUsageInfo { public List getUsageList() { return usageList; } + + public synchronized void addUsage(CodeNode codeNode) { + usageList.add(codeNode); + } } private final JNodeCache nodeCache; @@ -29,7 +33,7 @@ public class CodeUsageInfo { this.nodeCache = nodeCache; } - private final Map usageMap = new HashMap<>(); + private final Map usageMap = new ConcurrentHashMap<>(); public void processClass(JavaClass javaClass, CodeLinesInfo linesInfo, List lines) { Map usage = javaClass.getUsageMap(); @@ -42,17 +46,13 @@ public class CodeUsageInfo { private void addUsage(JNode jNode, JavaClass javaClass, CodeLinesInfo linesInfo, CodePosition codePosition, List lines) { - UsageInfo usageInfo = usageMap.get(jNode); - if (usageInfo == null) { - usageInfo = new UsageInfo(); - usageMap.put(jNode, usageInfo); - } + UsageInfo usageInfo = usageMap.computeIfAbsent(jNode, key -> new UsageInfo()); int line = codePosition.getLine(); JavaNode javaNodeByLine = linesInfo.getJavaNodeByLine(line); StringRef codeLine = lines.get(line - 1); JNode node = nodeCache.makeFrom(javaNodeByLine == null ? javaClass : javaNodeByLine); CodeNode codeNode = new CodeNode(node, line, codeLine); - usageInfo.getUsageList().add(codeNode); + usageInfo.addUsage(codeNode); } public List getUsageList(JNode node) { diff --git a/jadx-gui/src/main/java/jadx/gui/utils/JumpManager.java b/jadx-gui/src/main/java/jadx/gui/utils/JumpManager.java index aef5bc0f1..e0d9608f5 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/JumpManager.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/JumpManager.java @@ -5,7 +5,7 @@ import java.util.List; public class JumpManager { - private List list = new ArrayList<>(); + private final List list = new ArrayList<>(); private int currentPos = 0; public void addPosition(JumpPosition pos) { diff --git a/jadx-gui/src/main/java/jadx/gui/utils/LangLocale.java b/jadx-gui/src/main/java/jadx/gui/utils/LangLocale.java index b3bead3bf..922d61426 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/LangLocale.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/LangLocale.java @@ -3,7 +3,7 @@ package jadx.gui.utils; import java.util.Locale; public class LangLocale { - private Locale locale; + private final Locale locale; public LangLocale(Locale locale) { this.locale = locale; diff --git a/jadx-gui/src/main/java/jadx/gui/utils/Link.java b/jadx-gui/src/main/java/jadx/gui/utils/Link.java index 27880e912..66fa6d0d7 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/Link.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/Link.java @@ -16,7 +16,7 @@ public class Link extends JLabel implements MouseListener { private static final Logger LOG = LoggerFactory.getLogger(Link.class); - private String url; + private final String url; public Link(String text, String url) { super(text); diff --git a/jadx-gui/src/main/java/jadx/gui/utils/NLS.java b/jadx-gui/src/main/java/jadx/gui/utils/NLS.java index 4e30a6727..74ee8594e 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/NLS.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/NLS.java @@ -4,24 +4,29 @@ import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; import java.net.URL; -import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; -import java.util.*; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.MissingResourceException; +import java.util.PropertyResourceBundle; +import java.util.ResourceBundle; +import java.util.Vector; -import org.jetbrains.annotations.NotNull; +import jadx.core.utils.exceptions.JadxRuntimeException; public class NLS { - private static Vector i18nLocales = new Vector<>(); + private static final Vector i18nLocales = new Vector<>(); - private static Map i18nMessagesMap = new HashMap<>(); + private static final Map i18nMessagesMap = new HashMap<>(); + + private static final ResourceBundle fallbackMessagesMap; + private static final LangLocale localLocale; // Use these two fields to avoid invoking Map.get() method twice. private static ResourceBundle localizedMessagesMap; - private static ResourceBundle fallbackMessagesMap; - private static LangLocale currentLocale; - private static LangLocale localLocale; static { localLocale = new LangLocale(Locale.getDefault()); @@ -45,10 +50,13 @@ public class NLS { ClassLoader classLoader = ClassLoader.getSystemClassLoader(); String resName = String.format("i18n/Messages_%s.properties", locale.get()); URL bundleUrl = classLoader.getResource(resName); + if (bundleUrl == null) { + throw new JadxRuntimeException("Locale resource not found: " + resName); + } try (Reader reader = new InputStreamReader(bundleUrl.openStream(), StandardCharsets.UTF_8)) { bundle = new PropertyResourceBundle(reader); } catch (IOException e) { - throw new RuntimeException("Failed to load " + resName, e); + throw new JadxRuntimeException("Failed to load " + resName, e); } i18nMessagesMap.put(locale, bundle); } @@ -66,7 +74,8 @@ public class NLS { if (bundle != null) { try { return bundle.getString(key); - } catch (MissingResourceException e) { + } catch (MissingResourceException ignored) { + // use fallback string } } return fallbackMessagesMap.getString(key); // definitely exists diff --git a/jadx-gui/src/main/java/jadx/gui/utils/Utils.java b/jadx-gui/src/main/java/jadx/gui/utils/Utils.java index 1a90f8999..120aafb0f 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/Utils.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/Utils.java @@ -26,6 +26,16 @@ public class Utils { public static final Font FONT_HACK = openFontTTF("Hack-Regular"); + /** + * The minimum about of memory in bytes we are trying to keep free, otherwise the application may run out of heap + * which ends up in a Java garbage collector running "amok" (CPU utilization 100% for each core and the UI is + * not responsive). + *

+ * We can calculate and store this value here as the maximum heap is fixed for each JVM instance + * and can't be changed at runtime. + */ + public static final long MIN_FREE_MEMORY = calculateMinFreeMemory(); + private Utils() { } @@ -38,6 +48,14 @@ public class Utils { return new ImageIcon(resource); } + public static Image openImage(String path) { + URL resource = Utils.class.getResource(path); + if (resource == null) { + throw new JadxRuntimeException("Image not found: " + path); + } + return Toolkit.getDefaultToolkit().createImage(resource); + } + @Nullable public static Font openFontTTF(String name) { String fontPath = "/fonts/" + name + ".ttf"; @@ -107,31 +125,37 @@ public class Utils { return overIcon; } + /** + * @return 20% of the maximum heap size limited to 512 MB (bytes) + */ + public static long calculateMinFreeMemory() { + Runtime runtime = Runtime.getRuntime(); + long minFree = (long) (runtime.maxMemory() * 0.2); + return Math.min(minFree, 512 * 1024L * 1024L); + } + public static boolean isFreeMemoryAvailable() { Runtime runtime = Runtime.getRuntime(); long maxMemory = runtime.maxMemory(); - long totalFree = runtime.freeMemory() + maxMemory - runtime.totalMemory(); - return totalFree > maxMemory * 0.2; + long totalFree = runtime.freeMemory() + (maxMemory - runtime.totalMemory()); + return totalFree > MIN_FREE_MEMORY; } public static String memoryInfo() { Runtime runtime = Runtime.getRuntime(); - StringBuilder sb = new StringBuilder(); long maxMemory = runtime.maxMemory(); long allocatedMemory = runtime.totalMemory(); long freeMemory = runtime.freeMemory(); - sb.append("heap: ").append(format(allocatedMemory - freeMemory)); - sb.append(", allocated: ").append(format(allocatedMemory)); - sb.append(", free: ").append(format(freeMemory)); - sb.append(", total free: ").append(format(freeMemory + maxMemory - allocatedMemory)); - sb.append(", max: ").append(format(maxMemory)); - - return sb.toString(); + return "heap: " + format(allocatedMemory - freeMemory) + + ", allocated: " + format(allocatedMemory) + + ", free: " + format(freeMemory) + + ", total free: " + format(freeMemory + maxMemory - allocatedMemory) + + ", max: " + format(maxMemory); } private static String format(long mem) { - return Long.toString((long) (mem / 1024. / 1024.)) + "MB"; + return (long) (mem / (double) (1024L * 1024L)) + "MB"; } /** diff --git a/jadx-gui/src/main/java/jadx/gui/utils/search/SimpleIndex.java b/jadx-gui/src/main/java/jadx/gui/utils/search/SimpleIndex.java index 81a3f72f4..471d29e17 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/search/SimpleIndex.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/search/SimpleIndex.java @@ -12,10 +12,14 @@ public class SimpleIndex implements SearchIndex { private final List keys = new ArrayList<>(); private final List values = new ArrayList<>(); + private final Object syncData = new Object(); + @Override public void put(String str, T value) { - keys.add(str); - values.add(value); + synchronized (syncData) { + keys.add(str); + values.add(value); + } } @Override @@ -39,13 +43,15 @@ public class SimpleIndex implements SearchIndex { @Override public Flowable search(final String searchStr, final boolean caseInsensitive) { return Flowable.create(emitter -> { - int size = size(); - for (int i = 0; i < size; i++) { - if (isMatched(keys.get(i), searchStr, caseInsensitive)) { - emitter.onNext(values.get(i)); - } - if (emitter.isCancelled()) { - return; + synchronized (syncData) { + int size = keys.size(); + for (int i = 0; i < size; i++) { + if (isMatched(keys.get(i), searchStr, caseInsensitive)) { + emitter.onNext(values.get(i)); + } + if (emitter.isCancelled()) { + return; + } } } emitter.onComplete(); @@ -54,6 +60,8 @@ public class SimpleIndex implements SearchIndex { @Override public int size() { - return keys.size(); + synchronized (syncData) { + return keys.size(); + } } } 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 839443e44..ad43b2ff4 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties @@ -47,7 +47,7 @@ tabs.closeAll=Close All nav.back=Back nav.forward=Forward -message.indexingClassesSkipped=Jadx is running low on memory. Therefore %d classes were not indexed.
If you want all classes to be indexed restart Jadx with increased maximum heap size. +message.indexingClassesSkipped=Jadx is running low on memory. Therefore %d classes were not indexed.
If you want all classes to be indexed restart Jadx with increased maximum heap size. heapUsage.text=JADX memory usage: %.2f GB of %.2f GB @@ -136,9 +136,20 @@ certificate.serialValidUntil=Valid until certificate.serialPubKeyType=Public key type certificate.serialPubKeyExponent=Exponent certificate.serialPubKeyModulus=Modulus +certificate.serialPubKeyModulusSize=Modulus size (bits) certificate.serialSigType=Signature type certificate.serialSigOID=Signature OID certificate.serialMD5=MD5 Fingerprint certificate.serialSHA1=SHA-1 Fingerprint certificate.serialSHA256=SHA-256 Fingerprint certificate.serialPubKeyY=Y + +apkSignature.signer=Signer +apkSignature.verificationSuccess=Signature verification succeeded +apkSignature.verificationFailed=Signature verification succeeded +apkSignature.signatureSuccess=Valid APK signature v%d found +apkSignature.signatureFailed=Invalid APK signature v%d found +apkSignature.errors=Errors +apkSignature.warnings=Warnings +apkSignature.exception=APK verification failed +apkSignature.unprotectedEntry=Files that are not protected by signature. Unauthorized modifications to this JAR entry will not be detected. diff --git a/jadx-gui/src/main/resources/logos/jadx-logo-16px.png b/jadx-gui/src/main/resources/logos/jadx-logo-16px.png new file mode 100644 index 000000000..a9a2d0856 Binary files /dev/null and b/jadx-gui/src/main/resources/logos/jadx-logo-16px.png differ diff --git a/jadx-gui/src/main/resources/logos/jadx-logo-32px.png b/jadx-gui/src/main/resources/logos/jadx-logo-32px.png new file mode 100644 index 000000000..48c5305b0 Binary files /dev/null and b/jadx-gui/src/main/resources/logos/jadx-logo-32px.png differ diff --git a/jadx-gui/src/main/resources/logos/jadx-logo-48px.png b/jadx-gui/src/main/resources/logos/jadx-logo-48px.png new file mode 100644 index 000000000..d7a7ed044 Binary files /dev/null and b/jadx-gui/src/main/resources/logos/jadx-logo-48px.png differ diff --git a/jadx-gui/src/main/resources/logos/jadx-logo.ico b/jadx-gui/src/main/resources/logos/jadx-logo.ico new file mode 100644 index 000000000..1f02bd689 Binary files /dev/null and b/jadx-gui/src/main/resources/logos/jadx-logo.ico differ diff --git a/jadx-gui/src/main/resources/logos/jadx-logo.png b/jadx-gui/src/main/resources/logos/jadx-logo.png new file mode 100644 index 000000000..7010d2b3a Binary files /dev/null and b/jadx-gui/src/main/resources/logos/jadx-logo.png differ diff --git a/jadx-gui/src/test/groovy/jadx/gui/tests/TestStringRef.groovy b/jadx-gui/src/test/groovy/jadx/gui/tests/TestStringRef.groovy index 352978ab8..b6d30cdd3 100644 --- a/jadx-gui/src/test/groovy/jadx/gui/tests/TestStringRef.groovy +++ b/jadx-gui/src/test/groovy/jadx/gui/tests/TestStringRef.groovy @@ -1,4 +1,5 @@ package jadx.gui.tests + import jadx.gui.utils.search.StringRef import spock.lang.Specification diff --git a/jadx-gui/src/test/java/jadx/gui/utils/CertificateManagerTest.java b/jadx-gui/src/test/java/jadx/gui/utils/CertificateManagerTest.java index 0fd763e62..0e0e15268 100644 --- a/jadx-gui/src/test/java/jadx/gui/utils/CertificateManagerTest.java +++ b/jadx-gui/src/test/java/jadx/gui/utils/CertificateManagerTest.java @@ -1,112 +1,122 @@ package jadx.gui.utils; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.security.cert.Certificate; +import java.util.Collection; + import org.junit.Assert; import org.junit.Before; import org.junit.Test; -import java.io.*; -import java.security.cert.Certificate; -import java.util.Collection; +public class CertificateManagerTest { + private static final String CERTIFICATE_TEST_DIR = "certificate-test/"; + private static final String DSA = "CERT.DSA"; + private static final String RSA = "CERT.RSA"; + private static final String EMPTY = "EMPTY.txt"; + private String emptyPath; + private CertificateManager certificateManagerRSA; + private CertificateManager certificateManagerDSA; -public class CertificateManagerTest { - private static final String DSA = "CERT.DSA"; - private static final String RSA = "CERT.RSA"; - private static final String EMPTY = "EMPTY.txt"; - private String emptyPath; - CertificateManager certificateManagerRSA; - CertificateManager certificateManagerDSA; + private CertificateManager getCertificateManger(String resName) { + String certPath = getResourcePath(resName); + try (InputStream in = new FileInputStream(certPath)) { + Collection certificates = CertificateManager.readCertificates(in); + Certificate cert = certificates.iterator().next(); + return new CertificateManager(cert); + } catch (Exception e) { + throw new RuntimeException("Failed to create CertificateManager"); + } + } - private CertificateManager getCertificateManger(String resName) - { - String sertPath = getClass().getClassLoader().getResource(resName).getPath(); - try (InputStream in = new FileInputStream(sertPath)) { - Collection certificates = CertificateManager.readCertificates(in); - Certificate cert = (Certificate)certificates.toArray()[0]; - return new CertificateManager(cert); - } catch (FileNotFoundException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - return null; - } + @Before + public void setUp() { + emptyPath = getResourcePath(EMPTY); + certificateManagerRSA = getCertificateManger(RSA); + certificateManagerDSA = getCertificateManger(DSA); + } - @Before - public void setUp() { - emptyPath = getClass().getClassLoader().getResource(EMPTY).getPath(); - certificateManagerRSA = getCertificateManger(RSA); - certificateManagerDSA = getCertificateManger(DSA); - } + @Test + public void decodeNotCertificateFile() throws IOException { + try (InputStream in = new FileInputStream(emptyPath)) { + String result = CertificateManager.decode(in); + Assert.assertEquals("", result); + } + } + @Test + public void decodeRSAKeyHeader() { + String string = certificateManagerRSA.generateHeader(); + Assert.assertTrue(string.contains("X.509")); + Assert.assertTrue(string.contains("0x4bd68052")); + Assert.assertTrue(string.contains("CN=test cert, OU=test unit, O=OOO TestOrg, L=St.Peterburg, ST=Russia, C=123456")); + } - @Test - public void decodeNotCertificateFile() { - try (InputStream in = new FileInputStream(emptyPath)) { - String result = CertificateManager.decode(in); - Assert.assertEquals(result, ""); - } catch (FileNotFoundException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - } + @Test + public void decodeDSAKeyHeader() { + String string = certificateManagerDSA.generateHeader(); + Assert.assertTrue(string.contains("X.509")); + Assert.assertTrue(string.contains("0x16420ba2")); + Assert.assertTrue(string.contains("O=\"UJMRFVV CN=EDCVBGT C=TG\"")); + } - @Test - public void decodeRSAKeyHeader() { - String string = certificateManagerRSA.generateHeader(); - Assert.assertTrue(string.contains("X.509")); - Assert.assertTrue(string.contains("0x4bd68052")); - Assert.assertTrue(string.contains("CN=test cert, OU=test unit, O=OOO TestOrg, L=St.Peterburg, ST=Russia, C=123456")); + @Test + public void decodeRSAKeySignature() { + String string = certificateManagerRSA.generateSignature(); + Assert.assertTrue(string.contains("SHA256withRSA")); + Assert.assertTrue(string.contains("1.2.840.113549.1.1.11")); + } - } + @Test + public void decodeDSAKeySignature() { + String string = certificateManagerDSA.generateSignature(); + Assert.assertTrue(string.contains("SHA1withDSA")); + Assert.assertTrue(string.contains("1.2.840.10040.4.3")); + } - @Test - public void decodeDSAKeyHeader() { - String string = certificateManagerDSA.generateHeader(); - Assert.assertTrue(string.contains("X.509")); - Assert.assertTrue(string.contains("0x16420ba2")); - Assert.assertTrue(string.contains("O=\"UJMRFVV CN=EDCVBGT C=TG\"")); + @Test + public void decodeRSAFingerprint() { + String string = certificateManagerRSA.generateFingerprint(); + Assert.assertTrue(string.contains("61 18 0A 71 3F C9 55 16 4E 04 E3 C5 45 08 D9 11")); + Assert.assertTrue(string.contains("A0 6E A6 06 DB 2C 6F 3A 16 56 7F 75 97 7B AE 85 C2 13 09 37")); + Assert.assertTrue(string.contains("12 53 E8 BB C8 AA 27 A8 49 9B F8 0D 6E 68 CE 32 35 50 DE 55 A7 E7 8C 29 51 00 96 D7 56 F4 54 " + + "44")); + } - } - @Test - public void decodeRSAKeySignature() { - String string = certificateManagerRSA.generateSignature(); - Assert.assertTrue(string.contains("SHA256withRSA")); - Assert.assertTrue(string.contains("1.2.840.113549.1.1.11")); - } - @Test - public void decodeDSAKeySignature() { - String string = certificateManagerDSA.generateSignature(); - Assert.assertTrue(string.contains("SHA1withDSA")); - Assert.assertTrue(string.contains("1.2.840.10040.4.3")); - } - @Test - public void decodeRSAFingerprint() { - String string = certificateManagerRSA.generateFingerprint(); - Assert.assertTrue(string.contains("61 18 0A 71 3F C9 55 16 4E 04 E3 C5 45 08 D9 11")); - Assert.assertTrue(string.contains("A0 6E A6 06 DB 2C 6F 3A 16 56 7F 75 97 7B AE 85 C2 13 09 37")); - Assert.assertTrue(string.contains("12 53 E8 BB C8 AA 27 A8 49 9B F8 0D 6E 68 CE 32 35 50 DE 55 A7 E7 8C 29 51 00 96 D7 56 F4 54 44")); - } - @Test - public void decodeDSAFingerprint() { - String string = certificateManagerDSA.generateFingerprint(); - Assert.assertTrue(string.contains("D9 06 A6 2D 1F 79 8C 9D A6 EF 40 C7 2E C2 EA 0B")); - Assert.assertTrue(string.contains("18 E9 9C D4 A1 40 8F 63 FA EC 2E 62 A0 F2 AE B7 3F C3 C2 04")); - Assert.assertTrue(string.contains("74 F9 48 64 EE AC 92 26 53 2C 7A 0E 55 BE 5E D8 2F A7 D9 A9 99 F5 D5 21 2C 51 21 C4 31 AD 73 40")); - } - @Test - public void decodeRSAPubKey() { - String string = certificateManagerRSA.generatePublicKey(); - Assert.assertTrue(string.contains("RSA")); - Assert.assertTrue(string.contains("65537")); - Assert.assertTrue(string.contains("16819531290318044625546437357099080306019392752925688951114880688329201213180109168890384305768067101521914473763638669503560977521269328582980060332888147680193318231260043189411794465899645633586173494259691101582064441956032924396850221679489313043628562082670183392670094163371858684118480409374749790551473773845213427476236147328434427272177623018935282929152308753854314219987617604037468769472089902090243358285991739642170211970862773121939911777280101937073243006335384636193260583579409760790138329893534549366882523130765297472656435892831796545149793228897111760122091442123535919361963075454640516520743")); - } - @Test - public void decodeDSAPubKey() { - String string = certificateManagerDSA.generatePublicKey(); - Assert.assertTrue(string.contains("DSA")); - Assert.assertTrue(string.contains("19323367605058154682563301282345453222279312104889899001698209626254725581511375469963812461090495963838615773832867364330457010553974237985991904800958394169421485070378434746792379708805563793253282995274293621162504943287538455944652344378242226897507369146942411692220922477368782490423187845815262510366")); - } + @Test + public void decodeDSAFingerprint() { + String string = certificateManagerDSA.generateFingerprint(); + Assert.assertTrue(string.contains("D9 06 A6 2D 1F 79 8C 9D A6 EF 40 C7 2E C2 EA 0B")); + Assert.assertTrue(string.contains("18 E9 9C D4 A1 40 8F 63 FA EC 2E 62 A0 F2 AE B7 3F C3 C2 04")); + Assert.assertTrue(string.contains("74 F9 48 64 EE AC 92 26 53 2C 7A 0E 55 BE 5E D8 2F A7 D9 A9 99 F5 D5 21 2C 51 21 C4 31 AD 73 " + + "40")); + } -} \ No newline at end of file + @Test + public void decodeRSAPubKey() { + String string = certificateManagerRSA.generatePublicKey(); + Assert.assertTrue(string.contains("RSA")); + Assert.assertTrue(string.contains("65537")); + Assert.assertTrue(string.contains( + "16819531290318044625546437357099080306019392752925688951114880688329201213180109168890384305768067101521914473763638669503560977521269328582980060332888147680193318231260043189411794465899645633586173494259691101582064441956032924396850221679489313043628562082670183392670094163371858684118480409374749790551473773845213427476236147328434427272177623018935282929152308753854314219987617604037468769472089902090243358285991739642170211970862773121939911777280101937073243006335384636193260583579409760790138329893534549366882523130765297472656435892831796545149793228897111760122091442123535919361963075454640516520743")); + } + + @Test + public void decodeDSAPubKey() { + String string = certificateManagerDSA.generatePublicKey(); + Assert.assertTrue(string.contains("DSA")); + Assert.assertTrue(string.contains( + "19323367605058154682563301282345453222279312104889899001698209626254725581511375469963812461090495963838615773832867364330457010553974237985991904800958394169421485070378434746792379708805563793253282995274293621162504943287538455944652344378242226897507369146942411692220922477368782490423187845815262510366")); + } + + private String getResourcePath(String resName) { + URL resource = getClass().getClassLoader().getResource(CERTIFICATE_TEST_DIR + resName); + if (resource == null) { + throw new RuntimeException("Resource not found: " + resName); + } + return resource.getPath(); + } +} diff --git a/jadx-gui/src/test/resources/CERT.DSA b/jadx-gui/src/test/resources/certificate-test/CERT.DSA similarity index 100% rename from jadx-gui/src/test/resources/CERT.DSA rename to jadx-gui/src/test/resources/certificate-test/CERT.DSA diff --git a/jadx-gui/src/test/resources/CERT.RSA b/jadx-gui/src/test/resources/certificate-test/CERT.RSA similarity index 100% rename from jadx-gui/src/test/resources/CERT.RSA rename to jadx-gui/src/test/resources/certificate-test/CERT.RSA diff --git a/jadx-gui/src/test/resources/EMPTY.txt b/jadx-gui/src/test/resources/certificate-test/EMPTY.txt similarity index 100% rename from jadx-gui/src/test/resources/EMPTY.txt rename to jadx-gui/src/test/resources/certificate-test/EMPTY.txt