From d1af75122694aef279d102e60376025f2e95c0c9 Mon Sep 17 00:00:00 2001 From: Jan S Date: Fri, 18 Jan 2019 10:26:22 +0100 Subject: [PATCH 01/12] feat(gui): APK signature check v1/v2 using the apksig library from Google (#431) * feat: APK signature check v1/v2 using the apksig library from Google * fix: proposed changes implemented --- build.gradle | 1 + jadx-gui/build.gradle | 2 + .../java/jadx/gui/treemodel/ApkSignature.java | 184 ++++++++++++++++++ .../main/java/jadx/gui/treemodel/JRoot.java | 5 + .../src/main/java/jadx/gui/ui/HtmlPanel.java | 58 ++++++ .../src/main/java/jadx/gui/ui/MainWindow.java | 12 +- .../src/main/java/jadx/gui/ui/TabbedPane.java | 46 +++-- .../jadx/gui/utils/CertificateManager.java | 14 +- .../resources/i18n/Messages_en_US.properties | 11 ++ 9 files changed, 301 insertions(+), 32 deletions(-) create mode 100644 jadx-gui/src/main/java/jadx/gui/treemodel/ApkSignature.java create mode 100644 jadx-gui/src/main/java/jadx/gui/ui/HtmlPanel.java 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-gui/build.gradle b/jadx-gui/build.gradle index 65b3dd065..f3c23a35f 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:2.3.0' } applicationDistribution.with { 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..d59136bd7 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/ApkSignature.java @@ -0,0 +1,184 @@ +package jadx.gui.treemodel; + +import com.android.apksig.ApkVerifier; +import jadx.api.ResourceType; +import jadx.gui.JadxWrapper; +import jadx.gui.utils.CertificateManager; +import jadx.gui.utils.NLS; +import jadx.gui.utils.Utils; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.apache.commons.text.StringEscapeUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.swing.*; +import java.io.File; +import java.security.cert.Certificate; +import java.util.List; +import java.util.stream.Collectors; + +public class ApkSignature extends JNode { + + 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 = null; + + 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().anyMatch(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().size() > 0) { + 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().size() > 0) { + 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.size() > 0) { + 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.size() > 0) { + 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.size() > 0) { + 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/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/ui/HtmlPanel.java b/jadx-gui/src/main/java/jadx/gui/ui/HtmlPanel.java new file mode 100644 index 000000000..8a93034f7 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/HtmlPanel.java @@ -0,0 +1,58 @@ +package jadx.gui.ui; + +import jadx.gui.settings.JadxSettings; +import jadx.gui.treemodel.JNode; +import jadx.gui.ui.codearea.CodeArea; +import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; + +import javax.swing.*; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.plaf.PanelUI; +import java.awt.*; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; + +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 class JHtmlPane extends JEditorPane { + + boolean antiAliasingEnabled; + + public JHtmlPane() { + setContentType("text/html"); + } + + 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/MainWindow.java b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java index 7315cc828..e6280950b 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java @@ -28,6 +28,7 @@ import java.util.Arrays; import java.util.Timer; import java.util.TimerTask; +import jadx.gui.treemodel.*; import org.fife.ui.rsyntaxtextarea.Theme; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,12 +40,6 @@ import jadx.gui.jobs.DecompileJob; import jadx.gui.jobs.IndexJob; import jadx.gui.settings.JadxSettings; import jadx.gui.settings.JadxSettingsWindow; -import jadx.gui.treemodel.JCertificate; -import jadx.gui.treemodel.JClass; -import jadx.gui.treemodel.JLoadableNode; -import jadx.gui.treemodel.JNode; -import jadx.gui.treemodel.JResource; -import jadx.gui.treemodel.JRoot; import jadx.gui.update.JadxUpdate; import jadx.gui.update.JadxUpdate.IUpdateCallback; import jadx.gui.update.data.Release; @@ -296,9 +291,8 @@ public class MainWindow extends JFrame { 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(); 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..1fc5b1e06 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/TabbedPane.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/TabbedPane.java @@ -1,22 +1,8 @@ package jadx.gui.ui; -import javax.swing.*; -import javax.swing.plaf.basic.BasicButtonUI; -import javax.swing.text.BadLocationException; -import java.awt.*; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; -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; @@ -27,6 +13,29 @@ import jadx.gui.utils.JumpManager; import jadx.gui.utils.JumpPosition; import jadx.gui.utils.NLS; import jadx.gui.utils.Utils; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.swing.BorderFactory; +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JTabbedPane; +import javax.swing.SwingUtilities; +import javax.swing.plaf.basic.BasicButtonUI; +import javax.swing.text.BadLocationException; +import java.awt.Component; +import java.awt.FlowLayout; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; public class TabbedPane extends JTabbedPane { @@ -93,8 +102,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 +179,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/utils/CertificateManager.java b/jadx-gui/src/main/java/jadx/gui/utils/CertificateManager.java index b4069539d..29928aa7b 100755 --- a/jadx-gui/src/main/java/jadx/gui/utils/CertificateManager.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/CertificateManager.java @@ -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/resources/i18n/Messages_en_US.properties b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties index 839443e44..c1e3aba38 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties @@ -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. \ No newline at end of file From 9e0cd2e14e858ee5b86dd2489000a5e3be2d6d26 Mon Sep 17 00:00:00 2001 From: Jan S Date: Fri, 18 Jan 2019 14:47:44 +0100 Subject: [PATCH 02/12] fix(gui): add synchronizations to search index creation (#433) * fix: unsynchronized search index creation (code usage) results in ArrayIndexOutOfBoundsException and stuck at 99% * fix: use computeIfAbsent instead of synchronized block --- .../main/java/jadx/gui/ui/HeapUsageBar.java | 4 ++-- .../java/jadx/gui/utils/CodeUsageInfo.java | 23 ++++++++++--------- 2 files changed, 14 insertions(+), 13 deletions(-) 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..260333ab8 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/HeapUsageBar.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/HeapUsageBar.java @@ -35,7 +35,7 @@ public class HeapUsageBar extends JProgressBar implements ActionListener { setMaximum(maxKB); maxGB = maxKB / TWO_TO_20; update(); - timer = new Timer(1000, this); + timer = new Timer(2000, this); } public void update() { @@ -44,7 +44,7 @@ public class HeapUsageBar extends JProgressBar implements ActionListener { setValue(usedKB); setString(String.format(textFormat, (usedKB / TWO_TO_20), maxGB)); - if (used > r.totalMemory() * 0.8) { + if (used > r.maxMemory() * 0.8) { setForeground(RED); } else { setForeground(GREEN); 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..581e5c3c2 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/CodeUsageInfo.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/CodeUsageInfo.java @@ -5,6 +5,7 @@ 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 +22,10 @@ public class CodeUsageInfo { public List getUsageList() { return usageList; } + + public synchronized void addUsage(CodeNode codeNode) { + usageList.add(codeNode); + } } private final JNodeCache nodeCache; @@ -29,7 +34,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 +47,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); - } - int line = codePosition.getLine(); - JavaNode javaNodeByLine = linesInfo.getJavaNodeByLine(line); - StringRef codeLine = lines.get(line - 1); - JNode node = nodeCache.makeFrom(javaNodeByLine == null ? javaClass : javaNodeByLine); + 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) { From da41efa3db7f8f2c978667b5c3a74c3398e9c068 Mon Sep 17 00:00:00 2001 From: Skylot Date: Fri, 18 Jan 2019 14:04:00 +0300 Subject: [PATCH 03/12] fix: force rename by checks from RenameVisitor (#432) --- .../java/jadx/core/deobf/Deobfuscator.java | 11 ++++ .../java/jadx/core/dex/info/FieldInfo.java | 4 ++ .../java/jadx/core/dex/nodes/ClassNode.java | 9 ++++ .../java/jadx/core/dex/nodes/DexNode.java | 7 ++- .../jadx/core/dex/visitors/RenameVisitor.java | 24 ++++----- .../names/TestDuplicatedNames.java | 51 +++++++++++++++++++ .../smali/names/TestDuplicatedNames.smali | 40 +++++++++++++++ 7 files changed, 128 insertions(+), 18 deletions(-) create mode 100644 jadx-core/src/test/java/jadx/tests/integration/names/TestDuplicatedNames.java create mode 100644 jadx-core/src/test/smali/names/TestDuplicatedNames.smali 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/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 ce0d5ea34..832ec122d 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 @@ -150,16 +150,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; } @@ -168,7 +167,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/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/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/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 From b0e3cfedf499aaa83cb88c48d48ebc60a7fb044d Mon Sep 17 00:00:00 2001 From: Skylot Date: Sat, 19 Jan 2019 09:49:20 +0300 Subject: [PATCH 04/12] fix: update apksig library to latest version (#431) --- jadx-gui/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jadx-gui/build.gradle b/jadx-gui/build.gradle index f3c23a35f..9ddfac028 100644 --- a/jadx-gui/build.gradle +++ b/jadx-gui/build.gradle @@ -20,7 +20,7 @@ dependencies { compile 'io.reactivex.rxjava2:rxjava:2.2.5' compile "com.github.akarnokd:rxjava2-swing:0.3.3" - compile 'com.android.tools.build:apksig:2.3.0' + compile 'com.android.tools.build:apksig:3.3.0' } applicationDistribution.with { From aec986447ed3c90acc54d888f155275d882a66b0 Mon Sep 17 00:00:00 2001 From: Skylot Date: Sat, 19 Jan 2019 16:28:25 +0300 Subject: [PATCH 05/12] fix: support multi-exception catch blocks (#421) --- .../java/jadx/core/codegen/RegionGen.java | 23 ++-- .../java/jadx/core/dex/info/ClassInfo.java | 9 +- .../java/jadx/core/dex/nodes/MethodNode.java | 51 +++++---- .../core/dex/trycatch/ExcHandlerAttr.java | 2 +- .../core/dex/trycatch/ExceptionHandler.java | 102 +++++++++++++----- .../jadx/core/dex/trycatch/TryCatchBlock.java | 12 ++- .../jadx/core/dex/visitors/ModVisitor.java | 2 +- .../blocksmaker/BlockExceptionHandler.java | 2 +- .../visitors/blocksmaker/BlockProcessor.java | 102 +++++++++++++++++- .../visitors/blocksmaker/BlockSplitter.java | 9 ++ .../src/main/java/jadx/core/utils/Utils.java | 10 +- .../test/java/jadx/tests/api/SmaliTest.java | 4 + .../trycatch/TestMultiExceptionCatch.java | 38 +++++++ .../TestMultiExceptionCatchSameJump.java | 35 ++++++ .../TestMultiExceptionCatchSameJump.smali | 37 +++++++ 15 files changed, 372 insertions(+), 66 deletions(-) create mode 100644 jadx-core/src/test/java/jadx/tests/integration/trycatch/TestMultiExceptionCatch.java create mode 100644 jadx-core/src/test/java/jadx/tests/integration/trycatch/TestMultiExceptionCatchSameJump.java create mode 100644 jadx-core/src/test/smali/trycatch/TestMultiExceptionCatchSameJump.smali 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/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/nodes/MethodNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/MethodNode.java index 50affbac3..d1d291e31 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 @@ -96,16 +96,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) { @@ -257,37 +257,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 @@ -295,7 +295,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); } } @@ -309,6 +309,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); } } @@ -335,8 +336,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) { @@ -484,7 +484,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/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 740ab5092..35dbac739 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 @@ -438,7 +438,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/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 da40d6792..5ac645a4b 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.List; @@ -19,6 +20,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; @@ -360,7 +364,10 @@ public class BlockProcessor extends AbstractVisitor { throw new JadxRuntimeException("Unreachable block: " + block); } } - + if (mergeExceptionHandlers(mth)) { + removeMarkedBlocks(mth); + return true; + } for (BlockNode block : basicBlocks) { if (checkLoops(mth, block)) { return true; @@ -445,6 +452,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 */ @@ -543,6 +629,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 5e4d3d680..a199d10db 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); @@ -296,6 +297,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 1532cdb83..be49a4138 100644 --- a/jadx-core/src/main/java/jadx/core/utils/Utils.java +++ b/jadx-core/src/main/java/jadx/core/utils/Utils.java @@ -39,8 +39,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(); } diff --git a/jadx-core/src/test/java/jadx/tests/api/SmaliTest.java b/jadx-core/src/test/java/jadx/tests/api/SmaliTest.java index 75d507ca4..e77e80b51 100644 --- a/jadx-core/src/test/java/jadx/tests/api/SmaliTest.java +++ b/jadx-core/src/test/java/jadx/tests/api/SmaliTest.java @@ -24,6 +24,10 @@ public abstract class SmaliTest extends IntegrationTest { return getClassNodeFromSmali(path + File.separatorChar + clsName, clsName); } + protected ClassNode getClassNodeFromSmaliWithPkg(String pkg, String clsName) { + return getClassNodeFromSmali(pkg + File.separatorChar + clsName, pkg + '.' + clsName); + } + protected ClassNode getClassNodeFromSmali(String clsName) { return getClassNodeFromSmali(clsName, clsName); } 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/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 + From ffedaea50593826164084f9bbf2cf06500a49352 Mon Sep 17 00:00:00 2001 From: Jan S Date: Mon, 21 Jan 2019 07:52:00 +0100 Subject: [PATCH 06/12] fix(gui): limit the spare memory to max. 512MiB (#434) --- .../main/java/jadx/gui/ui/HeapUsageBar.java | 3 ++- .../src/main/java/jadx/gui/utils/Utils.java | 24 +++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) 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 260333ab8..680d1c2f9 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/HeapUsageBar.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/HeapUsageBar.java @@ -1,6 +1,7 @@ package jadx.gui.ui; import jadx.gui.utils.NLS; +import jadx.gui.utils.Utils; import javax.swing.*; import java.awt.*; @@ -44,7 +45,7 @@ public class HeapUsageBar extends JProgressBar implements ActionListener { setValue(usedKB); setString(String.format(textFormat, (usedKB / TWO_TO_20), maxGB)); - if (used > r.maxMemory() * 0.8) { + if ((used + Utils.MIN_FREE_MEMORY) > r.maxMemory()) { setForeground(RED); } else { setForeground(GREEN); 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..c709350dd 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() { } @@ -107,11 +117,21 @@ 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); + minFree = Math.min(minFree, 512 * 1048576); + return minFree; + } + 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() { From bcaca781b176c14d96e961d608fafcb8a4574b4e Mon Sep 17 00:00:00 2001 From: Skylot Date: Mon, 21 Jan 2019 12:58:15 +0300 Subject: [PATCH 07/12] style(gui): reformat code and fix some warnings --- .../src/main/java/jadx/gui/JadxWrapper.java | 8 +- .../java/jadx/gui/jobs/BackgroundWorker.java | 11 +- .../main/java/jadx/gui/jobs/DecompileJob.java | 7 +- .../java/jadx/gui/settings/JadxSettings.java | 1 - .../gui/settings/JadxSettingsAdapter.java | 2 +- .../jadx/gui/settings/JadxSettingsWindow.java | 6 +- .../java/jadx/gui/treemodel/ApkSignature.java | 50 ++--- .../java/jadx/gui/treemodel/JSources.java | 2 +- .../main/java/jadx/gui/ui/AboutDialog.java | 8 +- .../java/jadx/gui/ui/CommonSearchDialog.java | 6 +- .../main/java/jadx/gui/ui/HeapUsageBar.java | 33 ++- .../src/main/java/jadx/gui/ui/HtmlPanel.java | 20 +- .../main/java/jadx/gui/ui/MainDropTarget.java | 2 +- .../src/main/java/jadx/gui/ui/MainWindow.java | 19 +- .../src/main/java/jadx/gui/ui/TabbedPane.java | 38 ++-- .../jadx/gui/ui/codearea/LineNumbers.java | 16 +- .../main/java/jadx/gui/update/JadxUpdate.java | 40 ++-- .../jadx/gui/utils/CertificateManager.java | 2 +- .../java/jadx/gui/utils/CodeLinesInfo.java | 2 +- .../java/jadx/gui/utils/CodeUsageInfo.java | 11 +- .../main/java/jadx/gui/utils/JumpManager.java | 2 +- .../main/java/jadx/gui/utils/LangLocale.java | 2 +- .../src/main/java/jadx/gui/utils/Link.java | 2 +- .../src/main/java/jadx/gui/utils/NLS.java | 29 ++- .../src/main/java/jadx/gui/utils/Utils.java | 7 +- .../resources/i18n/Messages_en_US.properties | 4 +- .../jadx/gui/tests/TestStringRef.groovy | 1 + .../gui/utils/CertificateManagerTest.java | 200 +++++++++--------- .../resources/{ => certificate-test}/CERT.DSA | Bin .../resources/{ => certificate-test}/CERT.RSA | Bin .../{ => certificate-test}/EMPTY.txt | 0 31 files changed, 249 insertions(+), 282 deletions(-) rename jadx-gui/src/test/resources/{ => certificate-test}/CERT.DSA (100%) rename jadx-gui/src/test/resources/{ => certificate-test}/CERT.RSA (100%) rename jadx-gui/src/test/resources/{ => certificate-test}/EMPTY.txt (100%) diff --git a/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java b/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java index f11a0bcf1..cec9b4fb9 100644 --- a/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java +++ b/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java @@ -67,7 +67,6 @@ public class JadxWrapper { /** * Get the complete list of classes - * @return */ public List getClasses() { return decompiler.getClasses(); @@ -75,19 +74,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()); 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..927d99916 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; @@ -245,11 +243,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 index d59136bd7..9e9f3f266 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/ApkSignature.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/ApkSignature.java @@ -1,34 +1,36 @@ package jadx.gui.treemodel; -import com.android.apksig.ApkVerifier; -import jadx.api.ResourceType; -import jadx.gui.JadxWrapper; -import jadx.gui.utils.CertificateManager; -import jadx.gui.utils.NLS; -import jadx.gui.utils.Utils; -import org.apache.commons.lang3.exception.ExceptionUtils; -import org.apache.commons.text.StringEscapeUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import javax.swing.*; import java.io.File; import java.security.cert.Certificate; import java.util.List; import java.util.stream.Collectors; -public class ApkSignature extends JNode { +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 Logger log = LoggerFactory.getLogger(ApkSignature.class); private static final ImageIcon CERTIFICATE_ICON = Utils.openIcon("certificate_obj"); private final transient File openFile; - private String content = null; + 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().anyMatch(r -> "AndroidManifest.xml".equals(r.getName()))) { + if (wrapper.getResources().stream().noneMatch(r -> "AndroidManifest.xml".equals(r.getName()))) { return null; } File openFile = wrapper.getOpenFile(); @@ -56,8 +58,9 @@ public class ApkSignature extends JNode { @Override public String getContent() { - if (content != null) + if (content != null) { return this.content; + } ApkVerifier verifier = new ApkVerifier.Builder(openFile).build(); try { ApkVerifier.Result result = verifier.verify(); @@ -80,7 +83,7 @@ public class ApkSignature extends JNode { writeIssues(builder, err, result.getErrors()); writeIssues(builder, warn, result.getWarnings()); - if (result.getV1SchemeSigners().size() > 0) { + if (!result.getV1SchemeSigners().isEmpty()) { builder.append("

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

\n"); @@ -101,7 +104,7 @@ public class ApkSignature extends JNode { } builder.append(""); } - if (result.getV2SchemeSigners().size() > 0) { + if (!result.getV2SchemeSigners().isEmpty()) { builder.append("

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

\n"); @@ -121,7 +124,7 @@ public class ApkSignature extends JNode { } this.content = builder.toString(); } catch (Exception e) { - log.error(e.getMessage(), e); + LOG.error(e.getMessage(), e); StringEscapeUtils.Builder builder = StringEscapeUtils.builder(StringEscapeUtils.ESCAPE_HTML4); builder.append("

"); builder.escape(NLS.str("apkSignature.exception")); @@ -147,7 +150,7 @@ public class ApkSignature extends JNode { } private void writeIssues(StringEscapeUtils.Builder builder, String issueType, List issueList) { - if (issueList.size() > 0) { + if (!issueList.isEmpty()) { builder.append("

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

"); @@ -155,7 +158,7 @@ public class ApkSignature extends JNode { // 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.size() > 0) { + if (!unprotIssues.isEmpty()) { builder.append("

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

"); @@ -167,7 +170,7 @@ public class ApkSignature extends JNode { } List remainingIssues = issueList.stream().filter(i -> i.getIssue() != ApkVerifier.Issue.JAR_SIG_UNPROTECTED_ZIP_ENTRY).collect(Collectors.toList()); - if (remainingIssues.size() > 0) { + if (!remainingIssues.isEmpty()) { builder.append("
\n");
 				for (ApkVerifier.IssueWithParams issue : remainingIssues) {
 					builder.escape(issue.toString());
@@ -177,8 +180,5 @@ public class ApkSignature extends JNode {
 			}
 			builder.append("
"); } - } - - } 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..4f5b73088 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; @@ -399,7 +399,7 @@ public abstract class CommonSearchDialog extends JDialog { private final JLabel emptyLabel = new JLabel(); 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); 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 680d1c2f9..2881833b1 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/HeapUsageBar.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/HeapUsageBar.java @@ -1,38 +1,33 @@ package jadx.gui.ui; -import jadx.gui.utils.NLS; -import jadx.gui.utils.Utils; - import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import jadx.gui.utils.NLS; +import jadx.gui.utils.Utils; + public class HeapUsageBar extends JProgressBar implements ActionListener { + 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(); @@ -40,12 +35,12 @@ public class HeapUsageBar extends JProgressBar implements ActionListener { } 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 + Utils.MIN_FREE_MEMORY) > r.maxMemory()) { + 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 index 8a93034f7..52af564cd 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/HtmlPanel.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/HtmlPanel.java @@ -1,17 +1,10 @@ package jadx.gui.ui; +import javax.swing.*; +import java.awt.*; + import jadx.gui.settings.JadxSettings; import jadx.gui.treemodel.JNode; -import jadx.gui.ui.codearea.CodeArea; -import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; - -import javax.swing.*; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; -import javax.swing.plaf.PanelUI; -import java.awt.*; -import java.awt.event.KeyEvent; -import java.awt.event.KeyListener; public final class HtmlPanel extends ContentPanel { private static final long serialVersionUID = -6251262855835426245L; @@ -36,14 +29,14 @@ public final class HtmlPanel extends ContentPanel { textArea.setFont(settings.getFont()); } - private static class JHtmlPane extends JEditorPane { - - boolean antiAliasingEnabled; + 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 { @@ -53,6 +46,5 @@ public final class HtmlPanel extends ContentPanel { g2d.dispose(); } } - } } 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 e6280950b..e39470ea7 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; @@ -28,7 +27,6 @@ import java.util.Arrays; import java.util.Timer; import java.util.TimerTask; -import jadx.gui.treemodel.*; import org.fife.ui.rsyntaxtextarea.Theme; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,13 +38,20 @@ 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; +import jadx.gui.treemodel.JNode; +import jadx.gui.treemodel.JResource; +import jadx.gui.treemodel.JRoot; 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; @@ -90,7 +95,6 @@ public class MainWindow extends JFrame { private boolean isFlattenPackage; private JToggleButton flatPkgButton; private JCheckBoxMenuItem flatPkgMenuItem; - private JCheckBoxMenuItem heapUsageBarMenuItem; private JToggleButton deobfToggleBtn; private JCheckBoxMenuItem deobfMenuItem; @@ -382,7 +386,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()); @@ -589,7 +593,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) { @@ -598,7 +602,8 @@ public class MainWindow extends JFrame { } @Override - public void treeWillCollapse(TreeExpansionEvent event) throws ExpandVetoException { + public void treeWillCollapse(TreeExpansionEvent event) { + // ignore } }); 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 1fc5b1e06..5e6183e15 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/TabbedPane.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/TabbedPane.java @@ -1,5 +1,20 @@ package jadx.gui.ui; +import javax.swing.*; +import javax.swing.plaf.basic.BasicButtonUI; +import javax.swing.text.BadLocationException; +import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import jadx.api.ResourceFile; import jadx.api.ResourceType; import jadx.gui.treemodel.ApkSignature; @@ -13,29 +28,6 @@ import jadx.gui.utils.JumpManager; import jadx.gui.utils.JumpPosition; import jadx.gui.utils.NLS; import jadx.gui.utils.Utils; -import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.swing.BorderFactory; -import javax.swing.ImageIcon; -import javax.swing.JButton; -import javax.swing.JLabel; -import javax.swing.JMenuItem; -import javax.swing.JPanel; -import javax.swing.JPopupMenu; -import javax.swing.JTabbedPane; -import javax.swing.SwingUtilities; -import javax.swing.plaf.basic.BasicButtonUI; -import javax.swing.text.BadLocationException; -import java.awt.Component; -import java.awt.FlowLayout; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; public class TabbedPane extends JTabbedPane { 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..2b58f9c07 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()); } } 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 29928aa7b..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(); 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 581e5c3c2..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,7 +2,6 @@ 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; @@ -47,11 +46,11 @@ public class CodeUsageInfo { private void addUsage(JNode jNode, JavaClass javaClass, CodeLinesInfo linesInfo, CodePosition codePosition, List lines) { - 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); + 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.addUsage(codeNode); } 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 c709350dd..4dadef845 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/Utils.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/Utils.java @@ -30,7 +30,7 @@ public class Utils { * 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. */ @@ -123,8 +123,7 @@ public class Utils { public static long calculateMinFreeMemory() { Runtime runtime = Runtime.getRuntime(); long minFree = (long) (runtime.maxMemory() * 0.2); - minFree = Math.min(minFree, 512 * 1048576); - return minFree; + return Math.min(minFree, 512 * 1024L * 1024L); } public static boolean isFreeMemoryAvailable() { @@ -151,7 +150,7 @@ public class Utils { } 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/resources/i18n/Messages_en_US.properties b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties index c1e3aba38..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 @@ -152,4 +152,4 @@ 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. \ No newline at end of file +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/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 From 82d0d622a8e3a588b8916ecdb7b7e224551dc3fb Mon Sep 17 00:00:00 2001 From: Skylot Date: Tue, 22 Jan 2019 18:13:50 +0300 Subject: [PATCH 08/12] fix: refactor, improve performance and fix some issues in resource processing fix(gui): instead gradle export was executed normal export fix(gui): content of some resource files was not shown perf: direct resource files saving without full length buffer in memory perf(gui): line numbers will be disabled on big files due to performance issue feat(gui): click on HeapUsageBar will run GC and update memory info feat(gui): add more file types for syntax highlights refactor: ResContainer class changed for support more types of data (added link to resource file) --- .../src/main/java/jadx/api/ResourceFile.java | 14 +- .../java/jadx/api/ResourceFileContent.java | 13 +- .../main/java/jadx/api/ResourcesLoader.java | 55 +++---- .../main/java/jadx/core/codegen/NameGen.java | 31 ++-- .../java/jadx/core/dex/nodes/RootNode.java | 12 +- .../src/main/java/jadx/core/utils/Utils.java | 14 ++ .../java/jadx/core/xmlgen/ResContainer.java | 114 +++++--------- .../java/jadx/core/xmlgen/ResTableParser.java | 20 +-- .../main/java/jadx/core/xmlgen/ResXmlGen.java | 2 +- .../java/jadx/core/xmlgen/ResourcesSaver.java | 104 ++++++------- .../src/main/java/jadx/gui/JadxWrapper.java | 5 +- .../java/jadx/gui/treemodel/JResource.java | 140 +++++++++++------- .../main/java/jadx/gui/ui/HeapUsageBar.java | 16 ++ .../src/main/java/jadx/gui/ui/ImagePanel.java | 46 +++++- .../src/main/java/jadx/gui/ui/MainWindow.java | 19 ++- .../java/jadx/gui/ui/codearea/CodePanel.java | 22 ++- .../jadx/gui/ui/codearea/LineNumbers.java | 4 + .../src/main/java/jadx/gui/utils/Utils.java | 13 +- 18 files changed, 347 insertions(+), 297 deletions(-) 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 8ae4c46fb..665adcbb3 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,20 +31,21 @@ 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 = 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" + ); } public NameGen(MethodNode mth, boolean fallback) { 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 8598ac872..e71cbab6b 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 @@ -21,7 +21,6 @@ import jadx.core.dex.info.MethodInfo; 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; @@ -81,17 +80,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/utils/Utils.java b/jadx-core/src/main/java/jadx/core/utils/Utils.java index be49a4138..07267bd97 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; @@ -161,4 +163,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-gui/src/main/java/jadx/gui/JadxWrapper.java b/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java index cec9b4fb9..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; @@ -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/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/ui/HeapUsageBar.java b/jadx-gui/src/main/java/jadx/gui/ui/HeapUsageBar.java index 2881833b1..5155437b1 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/HeapUsageBar.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/HeapUsageBar.java @@ -4,11 +4,17 @@ 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; @@ -32,6 +38,16 @@ public class HeapUsageBar extends JProgressBar implements ActionListener { maxGB = maxKB / TWO_TO_20; update(); 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() { 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/MainWindow.java b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java index e39470ea7..c5be06f59 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java @@ -31,6 +31,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; @@ -221,12 +222,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")); @@ -238,6 +233,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); @@ -289,6 +293,9 @@ 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(); 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 2b58f9c07..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 @@ -219,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/utils/Utils.java b/jadx-gui/src/main/java/jadx/gui/utils/Utils.java index 4dadef845..6dd69da75 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/Utils.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/Utils.java @@ -135,18 +135,15 @@ public class Utils { 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) { From 293161720283284bfc2ccb3d9e333ebe9d812b2a Mon Sep 17 00:00:00 2001 From: Skylot Date: Tue, 22 Jan 2019 19:21:20 +0300 Subject: [PATCH 09/12] fix(gui): use editor font in files tree and fix bundled font loading --- .../jadx/gui/settings/JadxSettingsWindow.java | 11 -------- .../src/main/java/jadx/gui/ui/MainWindow.java | 26 +++++++++++++------ 2 files changed, 18 insertions(+), 19 deletions(-) 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 927d99916..0ada8165b 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java @@ -17,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; @@ -38,7 +36,6 @@ public class JadxSettingsWindow extends JDialog { this.prevLang = settings.getLangLocale(); initUI(); - registerBundledFonts(); setTitle(NLS.str("preferences.title")); setSize(400, 550); @@ -48,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)); @@ -202,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(); }); 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 c5be06f59..6895e3d05 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java @@ -111,16 +111,11 @@ public class MainWindow extends JFrame { this.cacheObject = new CacheObject(); resetCache(); + registerBundledFonts(); initUI(); initMenuAndToolbar(); - applySettings(); - checkForUpdate(); - } - - private void applySettings() { - setFont(settings.getFont()); - setEditorTheme(settings.getEditorThemePath()); loadSettings(); + checkForUpdate(); } public void open() { @@ -649,7 +644,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) { @@ -667,6 +669,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(); } From be509c71043f26c74d41f110eba824fe0c258026 Mon Sep 17 00:00:00 2001 From: Skylot Date: Wed, 23 Jan 2019 10:05:00 +0300 Subject: [PATCH 10/12] fix(gui): use editor font in search node column --- .../src/main/java/jadx/gui/ui/CommonSearchDialog.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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 4f5b73088..307d6ed80 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/CommonSearchDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/CommonSearchDialog.java @@ -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 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; From b28eaa1a94e1ccb558839debbf8c8ada2b9c6dd7 Mon Sep 17 00:00:00 2001 From: Skylot Date: Wed, 23 Jan 2019 10:06:13 +0300 Subject: [PATCH 11/12] fix(gui): add synchronization to SimpleIndex class (#435) --- .../main/java/jadx/gui/ui/SearchDialog.java | 1 + .../jadx/gui/utils/search/SimpleIndex.java | 28 ++++++++++++------- 2 files changed, 19 insertions(+), 10 deletions(-) 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/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(); + } } } From f8c0449d4e43d9c66cc95de6f83af466a6e76239 Mon Sep 17 00:00:00 2001 From: skylot Date: Wed, 23 Jan 2019 11:00:24 +0300 Subject: [PATCH 12/12] feat(gui): add icons to jadx-gui (#420) (PR #428) --- jadx-gui/build.gradle | 2 +- .../src/main/java/jadx/gui/ui/MainWindow.java | 12 ++++++++++++ .../src/main/java/jadx/gui/utils/Utils.java | 8 ++++++++ .../src/main/resources/logos/jadx-logo-16px.png | Bin 0 -> 814 bytes .../src/main/resources/logos/jadx-logo-32px.png | Bin 0 -> 4575 bytes .../src/main/resources/logos/jadx-logo-48px.png | Bin 0 -> 6975 bytes jadx-gui/src/main/resources/logos/jadx-logo.ico | Bin 0 -> 9662 bytes jadx-gui/src/main/resources/logos/jadx-logo.png | Bin 0 -> 15610 bytes 8 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 jadx-gui/src/main/resources/logos/jadx-logo-16px.png create mode 100644 jadx-gui/src/main/resources/logos/jadx-logo-32px.png create mode 100644 jadx-gui/src/main/resources/logos/jadx-logo-48px.png create mode 100644 jadx-gui/src/main/resources/logos/jadx-logo.ico create mode 100644 jadx-gui/src/main/resources/logos/jadx-logo.png diff --git a/jadx-gui/build.gradle b/jadx-gui/build.gradle index 9ddfac028..789ec0244 100644 --- a/jadx-gui/build.gradle +++ b/jadx-gui/build.gradle @@ -56,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/ui/MainWindow.java b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java index 6895e3d05..14be9ca2b 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java @@ -23,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; @@ -114,6 +116,16 @@ public class MainWindow extends JFrame { registerBundledFonts(); initUI(); initMenuAndToolbar(); + setWindowIcons(); + } + + 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(); } 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 6dd69da75..120aafb0f 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/Utils.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/Utils.java @@ -48,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"; 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 0000000000000000000000000000000000000000..a9a2d08566be02ca93b41eb6d9a24255ed28e18e GIT binary patch literal 814 zcmV+}1JV46P)LQ+-I&K^T6VTj^}m+0vGdtvStENM&WgqEbS%tYKd2tslqx!1{ zj^Dj^@AKSy_j}#|LI@@!PrGgJdX1*c;c!F=g+jLD^Z6i^O5e4ZOeZcjT&|g%?-w|j zTbi;|lWMZLor1H5T1J`-Dwda@4~b;vhpLLo#c%93JKLrL6$QH%58GXY!XPUkdf7&D z_eu+kmn}&qF*3P-tWD1#-Cf<|F*3QFaye8)ox1Y2^WJm+dD74bq-_RbULoS`e-+eIrPpR*I?L3_mE#8u_T>+$ISgm&@4dgcr;1^lELSNfCL6I zk%58tV#?*v2Wv81^^+&KpP<^R#)R7eZvTA*$HrlP@>lf6d+~f*2CR8muxT~WJI%<< z&f3GJudUs=1GDDEwp1y>6$k)<08=vyndmJAAE)^dzYdwg!}}>b*u5SJcX@0y z=BiZSMMmRu<1z43a`4Ue5@Y*vSaNq#5zH}ah`jxHyQ>myRtu^hw&QVODLP;GA>eKE zB_cK+#oy36B!Q~Zhy}~Tv8e7Ok|a?$w`K#ZUJuK@{h$M!R=XEB+~5A7cYF!Qh2C-; zD*5qu(xQ~8ebR}Z0Vfoqa1`Hc!tmHAIMiXsTtUByv<>sI!q5&5g8ggLoiiW`3Wh>7 zoyBUY91&vi2#4B}I!tHzf*RX6dWH!ciRTa^;Ih(pSF0Go+DvdhZ3COrN^~n$<4)Hj z6LaFV#>PSFFr`dMQl<5z?y!Zqkiy*SBZrEgk%~=ESX|V4oy@O2PVAl!6O_w(K`Eat zZ!{V$XZBmU&KEyQ{1^DEo??4;?D*c@WpT70{LK_6Pf#4u*Ypg!JYfy>CXDz-f71{M sr$JkwfjBbMTUAxM@MkmqUpoW%4nfsj#JY?JumAu607*qoM6N<$f=D)juK)l5 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..48c5305b00da9ee238cb17a6582e7252d38677f8 GIT binary patch literal 4575 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..1f02bd689dfd128eea9db30c8a7103fd5a77153b GIT binary patch literal 9662 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7010d2b3ab7ceed38132ac25c48dbf83f1f108f2 GIT binary patch literal 15610 zcmX9_Wk6KV*G580Vo4E~6zP&~$rUN-ZUpIuWob}4L`oV7=~%i$x_jwvmS*XC_xFE4 z%)MV`&Yd_j_dI8wC;W>t2oL8C4hjkip8O{nHRSu@zk>A=`BX0%utL7Doj&QhqM+as z{a0R0F8d*C(4ff6NNRvz9A#jGNi<<0v8M6t*kO`1KV#5eqZ0jOkRPvSbQzWiPPEG(@Oav}%0~d#6;DoA)M#l@uk1uG$UJ`vm(X~)O(ItA?EzvNYaPwR) zZmMr;YF^n52Yf0{-YRiD7Frd%j=&}7_$j>x*o`=1k+9yb#-$cu^o(Q^GuGQg>`z6X zC5Ow)4_B1kbYl;Wj3hp_+sqV;D145HwbyC?^F0h_i=Yzqd4RRwc>Q4kohoL{UNN1O zjLxCVXDNMYgw#V;N9y7Rj6*4FhP{f+2jmD1NEj_r6>yjj!aT zvp%4#ET^GChe*N!l@eAV(P(*j@I0u1x`e`yF@lRn^R`p}J5gtDPL5i+<6@mv^VLxa zgNI_y&PZBlH(dM1bXnM1o@N!rMMD6E5v>vhbc(8)1BUb4Q`t=YRhrs5W|k!F9Lcdj z8A|5Jr512l%FN1oP9lEn*}%%=sp_B7XQ*jv28|hA(zrBNv>%}!pk(>|l3w+>xLjGr zXa+o11J5pc0HT;%Zmr~Ww?NO8`Ds9-7s*-0&6PgGIj75{--Te1fMhyro%WQa67})) ze(x5dlOm=c<7{<%D8I(vRC&})=ztSvklf})%$hqL=mN2L(y+NM;(gW7r0-HymYLOL zj2{sZQCL{nzkAJ;T@m}#hI;qNXA2{_vAYc9dp`d`^5JOK@Sm-A_0IM9C?2-q1%11Z18U%#^Dk~3Ki^{xf z^|!Me*^=0|LLYhoZH{j{y=g~D)2KEhQXcxvWBK+nQdu6(5}tTv@B`^>Fh4ZBprH)7 zDLbX4SU5L&I3dTEWA5>`ik>JwMHjohF^<0g*zMuWj@B zn9->Yv-txkQG{RLu-V9OVXX?ptbP62)YseF_VkcIo)$^eq-Gz|k0utBCj0q0{#T)A z7BB6@bI$4R*ec|3Z=x?1GZr(Ff$T?X%-NW^& zFS+dlNP4-SxGE^twMR})R(C*uwqhRwYvqo4rNX3*$DW z6Y+!|y=Iziz5I6sG}e0Rqt5fFui=Tv$C`fg z6aN;q@BNsc(=f4S)Zf#+v5j&noVKekbbRbeL~RqRxHqg+eoo&8>dN$~abFtyb>Kc_ zUM)+coFe9GEQ3&rB{EawEt9;)R3P~6^e?hDFiY1|f1CiP_LB>C zTXgC@ELfN}1g;0aja$=7~5q8xmC*{RFbzZd|0Y>(xErR}-Ti^^tu6 zw1u)-A0+IngSrb=^6tII$g=*Pgjh{vew8phzwX;tSx^G)YYWSW>=E`^!3nW`hhM2g zzYd2j6C)>`2W>kP$Q5)l#eX|gq;AyIljzfWeDdhKjDL3*IJ&7<%hw@?WLxD=e1-W`qzXZt#Z>-w+MZCgW(fyAeT!@zI?6vNhT(YkEdf1jORw z_Z0XkQA;YCh70!{b+*~@tnukYr~yI7cZnANRllWj@ub6HwEp|Y+fW%VFwT~&v)0P6eR?I=I`7>Utq>9Rj0PFCk=Jd`jr?qyMrl0Jcz z&qm)e73W0eFcQZ%zpMK;e=GK{OM}O6RD;%x0R2D>uPNM&xq8e*A=CbZmvJ!dMW}C1 zPZ8r}ZqMC0!Wvs<%M-WU7=xg>R29>ZOO?I}zbAjiWPsIW5_0hxAZB^gD(bt?A*fwy z9Qy4>=~SReFZzdo_;e3`YMw5=hf9R#5*N$ebPaFVila2|10C3o)yMR1C_c*WJGR`$ zom&+`bG_dZ^FLE;Qv=M*JP)+9!lT6?17W=;a_q7KFCEt?@m6S@DM z@6udtWMpJYYnlwLiq_1{&1ct~uvn?5MzeK1mCShBMZ4T2$yUC|R_v)Ncs+Od(~eXj zDshLkKWpCt*=x zM6A<;UTu4I_;Z=KLq{{3?-AJlJZIWXxQeMLIpnMg{^sLV7l$z#oPLgr>0{37l!>oV z-g2x~9h+X^rD@6CI`gc*6@HD$zjRpYHM|-d{GpU(1Djkl#vfvd_*U>~SFR;1!u@zD z6{dcEBk?(-1@Zty@OVwW+h0N>s~pbwaJl^^uGXfN@ve-F1zfu545NfMJtEdO>9S(% z3xTUY56uK4;1|jJnEb02Hvetj8-J^WdgsbaE8vgl?c>~7F_;*@8#e`Kw_) z^}NxiKF=qPgZ;S(vfz;n?J?pduoIdKNNi3T@N7SmQ|Rw|vgi%uz>wypf|y{rZd1W; zw-QK7cbg+1tUs`fj_dtLAxY$xz5esbtK)Ja>Ow84U)#L2Y8*aB+$^Ry#iUX-#8I_X z{U|CD|AX%F{-9{7D}fjQfa+-Y8-=YQL&}T5OuzeCf(Lb1OP|UBvyA0{MFIgjve87X zY(0M?Y3u0-xv3GwU@rciy7Q@YK0{&M4m4XPqDBF0#}6 z+~$FKQedios8*^T>>#Q5nNoVSgzE6cnTV{!n28PkRZn%m{0-6-#r`m?j0Kmh#f2=n zz{{vB@h|$qx#S4Kv1H2SZ7drdGfAlg=)#Pd?}SL7hbW58#W)MDF1!k^3^1rkNiXk^ z|7)J@kb+2+&wUaz6qRa{)$$vMQ1~q%T{*iD>8UL|2&}nW_^y@Bf{E@HOS-`mgSz{$(7Am3+nYThCrmhMfE8N zu!BiRkB?_PN#MHQ;u)2H896b%0Gx5awnrZ;YTmg>oUE`Z-QA4QXP4g)WW$4-_Uavu z^meAHLQQz>ruXEjFhr+le%sMrEGoxhpeunZviN4rf zw?|?w!VjW$KVjZCy`|d0!vH=!-k)uM7Go3htf80UP9*2vG8;Qr(ol2Lx$_6V{<(6o zb<(xy+?uJ2JfNHQ$X{)M{zgV6V`#n@)7YL(SflLg88H=ewY`c;p-mo)&!#2!PI}t9 z7_)3F_V%<{sr-3=u@mUsL#+_-y7=qR?jY(1BbcH4oSZHHASIco9tJX)POx5&0ZLy~ zRYi$wbkN=I2$93T$6oF)Rn!#OyH!?XX&fv?vS^B` z2=G4tV9{jS_VdkUG9y4}7~vi0m+Hmkw)|;pap>F}{a^V1^;R#B`42?Rn)pRIA7UlW z%+sOqC}*6MBc0{%1!epC9XL^VmBF`n)2+>+#9rnZ+a8+*%4$MJHT37Z57_8GeCh%;S3`G-@qY z1UtvUhTbM!?a?Br`!nTe0DIx*$it@T?tC~XHX`E9KbLZFA+lMIu$XGI+cl?#2bg-c zufhlJoBU`fWf#bBBpe1TY7rP@!Pp%YhA~MLK16-o*#O3`Z5lw!k?=EkfQNc;!f^U0 zZma73Ve(R)io&O!<{co%tw;^Lsb=ZD2BLkEHp@&ReLD60q&d<9|5*Co;p8)>NPWR3 zd+PW;L|a{V`}4odWYS7GFOu=x+}w4jCsdRjhLttyvhb}R5%`#=QaPQRZWu` zL%>L*sfhgX_Bb-x{l$2>bgbXZ7<)bdhIDOTT}xUc8CUkv*~0g}=}|@g1KL`TkA#{} zo3Ax*k%di2Uh|rI<4XJt9TIe}tl+I<1Co=otRy+*r5BakUYW1PoIkf$wUW^gf&h^zCfjz zeVt{?RiM(@blu=OkGurzxAw&T&hh~S(=VNfJ6vAGI`V~`vx)3aQbc-+t3yaoUw*aV z+AhMC7=S*ERN~Tw82psqHE5iA#7dZI`YP~mq(-&9@{ z=DTb?c1expcyeTz&t!*L%0XB~_D_9k@^oMqxKgmDUvGUur~u_i$QALqh<-&iwS`L+ z(|EDyzqS%zF?Y42knWiouyWjlJJIX$%c$P;I;2 zMd4D!-c0jtiz>+zE<2m}+U06oZ?pD18nzLJR8n@thM~*n&p{Q9o^5nK5EoB-D?=u7 zc%>$v^>o=%g>ByTadfru8Q(ggw4O@Uo`}t1Zs^=@)~Pr`k~s;3QV!>PX^)6*wNY-% zY>pu^8{o-vxsn{RvH+Co``HJPA1O4O=3A<8M4G`vY&oMzc1Dxy^@w29z-jI@_4N&| z9P`(`eItPF2tt{{Y)u-`r#|kxgJ#X?-MU5JHu|bZApAyA<@uQVA3ej~M0N6x{D+3k z?`lwG3h_UO^4DA9n=elAxLV5Ux?gY0L&jo^>+<>89#E8o)*mf9ZA!5*rZN6uFog0M zL(^e>UPIM$=jVQeFpc0;D zC9@}6rlr_gWJhyyePa(rw+9wSkR{1o?tH7VQA}@Pz9az7CF^J;ILO$uf!0M@10WMq$3(J$ZET;AFc#sB(b z)Msot)vS10H6KzWOkKL)!uf|4T%D4NAu?5{Y@(6R>UrG5c4MrPG|?^L;q~b0|JoKc zPFAGO28XJ%$;-zlh>u{%61o^rCzFBmO%Qk)4(w7-3AZYJa@EtrKd= z2=CGK$JEYlajDuT9eUr0bfv=zz{+q=Vjm3+4VYrmI&(f#5^Aj7h{ z;ccz12I(xAlV6!V4$A=MUFyAaPnd+*cN(wO*{dXh@I&0%fxtnNf9-ulrwc{K$uch_Md;`@s`}l+*Hi}`XWM-F$|6lYJZQ9cN&l>t zOYh<60obR%M0g%EnXNik4Z}rHF;nS19%;&~{z#jdHF$fEiv9CcVpGBJsXF%N27$uo zoOkfmJm{sgPe~`F5?uCVrMA#R9R5sIwC2X`(u6icH;_paK5`TnM$Dw@Uc1 zC0|jozzqvC)m*_Jf%n$`^muDj8e>G_gUm!bSa@k2!#%?u^R3l6pD1{$y1KgDTR*C* zn}gR$;4Qff9xhRQ|K`;4fa_^}-T)uPsG)jE?M%ah1f0cJ>uP}^Y zhp%l%SRli$ju`{~SnuS!sE5Z~y8sF?j!!N$SNf3aTwjoAmZl2Qj-bd`hGl9HS{cg^ z5r%*Dmdtb84y@#&#b<=g71cXVrHJ(U)$^2<`uq*yo>({0$VSW_7&3Q>CC`i$?h!bVc_)3ro6zG!KT5-y z?D9$h3{pt7>$=H%0QVN^SlP{szwz}yf@ZbN|F#*%el3XDFIuPfjaa5IGX-M()9dlj zT_k+SX-6O9I^DQ|nybO1d!h>hDA96!g}T_h=|9(32>9Zu^6OF27kbf*G%vOpE$j@U zc8lA6^uKe-JqhRAs}<3a?hH3ijiuKcWasC?X8Gd}qCDUSnsPfTT168^@8*6dfQ z>gb0RAD!-)t2FacXi{1GWAQE6CI5-DGt>}}lzpibN&uuw;@LS2niq1&2{E9_p{rWJ zfbs0@L;>@eJrD~(sQyr$aV-WXXM6tV)_34r{Iz&tZAuZoF6#7qhdAqGLsH2_ld(Cb zruc(b!p0A_lj?}Ue-%D{(dMOR1fi_ezzKGdVUrCJS#Q`HG|&c`HNouhbe9ijTOHW7Fn>DD)n2(j;qVRB^Sud#90obaq zm3(@3XKa()bL4&ms?TD}ec@4k;C__=0w*c)1a_pT81onsc1ecQ`CNz|;LxeoFKnKq8SS7e^W6c&^?8vZw{*p8#x<^`~-y+9ZE zoDOldgF}>{+KuC1h?=FQ@Mah9YB5CE(5$y~Nz1bdMd^0;wW;`gfvVCRd1k+QN{9R1i3?={QOIR*CC}QU5_=oj!b3Om6I3wsr5VTX#zaSPh~Oyk=W{K zSs*_1Wh2Jho&NYos~UpHfH}&97zC5Y7jI+xh&khVSOt%csr>%e3nh!W{iy;h}=8=o<+%P`|^{Y+Lg_Hi^ee4N<~;{EMXZ(ktYcS7Z~x(JT|Km#kl+%M|vet0{{Pf;q9Ab8k`Oh)_3LZ)K37eDm$jvMInj zqsmX?;ve4dtW1wzSH_MfOA}AEH^CcNpfygKgOgyc?N#KY$)^d$y93`OH?W?stZe7Rmi!&6C(nzS-Iebh{gwpaSZ(1dfmST~Z~!g1@&YNGaSKZ6Y3_ z{sttIbg2-_DNQT43S~0Rm(kwn@BW@RWPN>>E45M-#-goene#g>Jhh*-M9p$T=^u3; zCDW3jhjt zAuKynY)!@5`5w$(Nh>o?5`<`w^6@SvoH8i(ss8)OH99^(tk(X<_F{60+RpG0M?xuv9>m!6vSf)oS=@NPHfIiBmiFzM?(%#UqR(d35?TLvjxzl!;>n z8V%vM?R;DgC1i9{{)1qkc5df)l7$h~y?fE@ah#VlYglmWZh&6#G6)YnUY_avBsgEu z%>Q~*fC0+7_WXQiQHp>*#6KcdSeto&tb(P92ak~19z)K31@XT@eOUk^X3Y0B`Pd~% z*x~05DPbG=v1H?`hN&a`;g!OP6@*tZZx7MB#h>b{#j{i_z=n{=BW+GVvq;#DBb!3r z3l$>;zJIO$h6sFeO^&nW10Ga0@6uh_xaC~VYgW%_Yqn)YnSiF=^f7WCW1&)C^k>w_ z;8qjTdl2 zxcTqluL|EE4Zt`g0t8DkG69g6L(mDEe=(FIEX%Yg0duu%)h1WdveUu?4Fv1uop|}* zjO*9);Rg{`tZKt!+)^urO6@1P;U(R9SA^dnQX(9)gU4P1$?yiW6O;oAi1Ya^%@57P zu~_dc2e|nPk+;1Mo;)I2-#}Oa#k#eSNy2sc`TOOLmyFjEuNhqnr`K?RaEKX+aISgF zZ~7aW?=#f?jt4x`0=dNX0w^5|`pzgQZ?pg70@&Uc3C*exE?;sy0dstEvwGgn{4fTW zn#!X5Nd<^0@;hNhpj_fkbCYmghHTSN5NZu`5#Wlshjx5C>pKzI)Iaq6`zk+tD%8ne ziSY4MdxI0;dziGNvCg)d*@6nI)ESCEv3!d)i{*42{$Ugr0P^{Nv=Khk!G?u6#)@A6$LsU68OXGuhUb07H*^^^xB0f&Whaz#Z?b08ON zQXvC!-Mv)PjKgoD6E;L)rx>N%I2N-b^30ei$8=U>Bonqq;IAwJCbXe?az1ERTt z6I`;+p@OMTK?SX{vcoDUnkFE`)7KsVKh)ezsjRA9P1VlY@aoJj^-8AP4edAhnQWm- zYpDa%GvU%F9&h2kZX}?V1J=?KNYR(Jt)<~+Risc!a->|Sc1OxaP@3tBIdOfzA7!!X zR!P>f`>owt^-(HKK{@mc{r!BD;=O9bgBEtX`E^PDw2oUsU#hl@Bh^0-^4E}KI<}fZ zQ03u>74yP?r0)-s^;y3PvDt+T-*Yxs5CYbD5Sttm#4=*yXB758_|z{hG1*D?MCWYP zajLVfYD5v6vWhGZD&G0gnR@wt@frZNbutq5CbS#V(;lqxB}kNZ-e zxid?X9}3II8Z)aXqo6uIJ2}IhbDW|m3frQ22^--O1imk3>-srI`dK->06VyT{Jvrf z_tBbeuuER$1S3b<201>i)Y_{bdXtQ9tOqfw{x{Lmc!I~vL?@K9 zOHd+VB!u%B?)F-Xs~(@!SCwXPRjoPyXB^B%=5mMoC(&jl806V1=BfR?u3qZ+U)ej^ zQ%abl3Ck2K=A;)|j&bE!N?|9i7BpBm|FI$5@CB)FTpU%JE!U(TuMV!GcxBdoZrHc3 ze9os&vw^P-4Hw2FIp*a3D7UYg}cJO@oCb3E)#8FVoV6!p; z*qbW+^`VcYIaq3Cnz6fd*KyB}Pj-7YAIGXERsm$gnIkZUc6i0}fgmei%6N<82Fcz) zP2%~hOWzV?-ZMimWD1}uw$xWL$5R=vB92?=3JZQgQJ&SK=%J}DPNXm#Bg~WCW?~^B z0T;CYM)`*E5+$lDl%QhjPVOzsSXGSnrjGNx>1CgJn5q$32!qL3EJ5#J)C)mx^!h|E~6wS00*g zs;i3yM#&1*zTdHG+ikl2m(}V;FXqX|h76e}>UVdjv~fJP#A`i94rZRedU4UiZxk~7 zZ*gfId%j-w#ru$u#7>!lq1)ag++j^cl!nL8oYcXZv%hB7D^^-2GMnEZk0|)HLS1{a ze!A8}rE;~7%E0n%x;wBq$4ZP1Rg`y+K|efWt0wchJH40P#^B+(0ne<@qzJ%AqyXn0 z$&?(c@=FGKv|)@@39Q(C*G8KN{l%&hD?tU4#G~fi;gYKUh!8CT80~M&$PhGZ(A%uG z2z(LX(&v&9iTV22<}?4{ocTYOQeuOH-`bkj=$(XrsoJ26wgPzXc8g2J1a0&r@%cxMsCf7-#TRJ9&gadAEJ#F$zg(kt|SKRv4spiyH5t zQ@71w4Dk|HSwZLYhpLQcPT5v}rF~u2P#P-B)aub3xy)j7QetnNU|eHfQfXVkH21Y( zw>*TQY04O2q<^|M|JdN1d$j`|?GfSYwCMSYocFm-ovy(FV%RHT=!Ze`plBv115&sp zxp5^H)zfWGPR_`|Q*%(?0NU;Ctt0eR_b1YyFjn+n(&QOx6=WR#rCS-b7_ss(fn01d zb*nRh?)_Y0rzDx&SQ?`c5?~B|9v!b1drYfWrDqj?dz2>KydrT_QllaDU50$fomXMO$%jBJvnt>|Bes+%Il&k}{tG$=(by-eEZ$<~2 z)V#?$yXx+TRMM^?3cK?3{}g8Ko;hauaZP}ZmP4gRhn77vo3R8W=Fw!wqk`3yI_^Wc zqFHgUr%&6?mFZFF)1j&=I-fEGo8_eTuy)9+l~DKctWonOf$Mw#*gXkV`Aw;04)fdSoafp#R_qcV zW)p9>T>Owgdgc@Omr5x18z`<$d-7W=*W(nRwSB0@3edPn3~riuAyA%c^W(n2);Htt z8j=J=-gKAst#fV%a=qxjw@xkpiYo|4LOXrfeR+OAqbKdzeSmEf*M|aZ8 zZdkX?$7>)ni{CfgCwL6*~Z9wn%*?y@bxH{2KDige?V*vsMva8u$DHQzF6x}B%|?Q zRSAWnD*ld+*kMf_5zc9r^39mChdN_$2sERbHWyr_gxigIj{cI>@V#DPW-}go2W|ul z7>ipiFt)0zKay?BSCVRvS^j1rfAhJ>r_y=$(I+qndAg3X!9$sz!my zd%6UXH2ZjEam<&*dNan~!!OZIt8Jx2SvhHUnHN^m4lK+pm`6`b|<$YMM%e*fp+C2K)yf>ETDMB!JHljqwQnv@+TxkXu2Fx~LV46PjfW@VcUi?lc z-uxrW$dcG+p|9AW*>O6A1bBGuh?hCuHx(FT2an>*L1V1_W0uk9%fI3uH`*>*T()Z}>x;iqm-YA%4QJbE;B7wMg{+SQ9~RdgHNwQl?R^%=L0weM|YZH=ey1 z&NP!sl$HUki;n42G#?J({d79~hv9Rz=ao})5@asFP-A7Wh+8Ym$egz+UbRy})$i${ zj7^ma?StA>w@S}MFz5dkpCqsM4TkJtj>i@IvxP*WQ@T%yjF+8EX9x-4hKNtO0-)OA z3W_*ZNT%i@S{^7|>xAZlsHcdx%a%CDg8ti6{yvp&Y(FC5{M;rdQrhN443Ig>!J$kMhj%>7b`Q7&M98LUH&#l||$58@0L|)#caMhFI z`7;<5&H0CTkJ(O-%_U?#+twwkI)HszfZ% z5+P=3-NJeze%`ysWf)L$T5gdk zu$=b)joq&@$ONA4SMsD_j;F2Ebn|ObM2XjJY_IVXoW%`qP)C!>1tIa zRgjrdkHq~Pf<_rtm^e!%mZ22t~ zr#9A9*6_AvjcTw%5EeNNj(hn$XR7cSsUHdz`@j7xKOal*Nps_p(9Q4zyTImBpI!%U z-ZWLK2bCGY*Ds0Wv>vKS#*?~*7cXN`S5`LrzUhuyp(z>4U?3SnN&=j$w&FufY`@gK zMdlsE{sV^Dd@vmh*b$wY7n}OaSLCh>C2>+20>fmdy=@=tA15)MH(+bK@4+d<1#&Sg zmdqqe2pRa?6h9igC5fqZ2O|wI<6wQ(~B)xxSpLud2sn7P%Ncw9p%~#`_ZLsEe*;vL-qsM?$^?@< ztshHZL5GXIzDcQ@7PW*)W$=Ei1{oVFH&aBqD7T4u_>h;YbmRRADmeGOKts}==eT^IPHxsoGF8sKa5>qB?tehL8$c~_4Em9s zw8bp_RXn50eg)u8|q_`_ZNd@&m!%H*%Fj_9NP%dEYq9K1XxP{5>yKw`)$J$;Cza01 zUuBvr=bhwp=B1Qm=#J9rlbLgEg^c|?hG^~kQXC1J3;EMHe-4^+h4xEjz!rdJ` z4I7^cH3eO@?pzZkVpQkmhg1l9iAZ#rS-Jk=Q6)HUAd1wvbM%}tD>Y6vIPPHok+&Vr zcAqUXbvhIfPyDbs-P6Ll%{j|FxIXOdWn}-1NnK{)UYq|jbMf;i`u;O(QhLs32F9W) zwyolO0=4`Nb+v9w*@`)+*aP+Bv5@n_%^8PQ>EetI2bhK27hV|lrY3g{7b6}yocKO%_q9)bp1TBH-Nv;oXDBe@w|)BRHaK02h%mhL z7R9P%S0`{lX~)VOl+6-iqBnnOS6liU(AwUEujgKK+;7$#J{R-$RT`+rv;Uh)s8B$R5fMO9KC^eGd$vZB zeogIuum9bjwsqiA^Z9f?sE~75JW-L{lldTMFZyl!bT-%U-om3-30l#M=LS}Tk_X-* zt69QSXY-^H$XiCq$OYlPWD0DU;pppb75{(|=Uq#KZrPUgx4ATqCd zYN3X3ic)&rGViilFTIA=DSd-dOBXU+l zsS?mYT(K`|X^}(XTNyQDY?cFK$UugPz+QJX=&cyUzc6`qpjL~tf*m0lR#i6Y&3SOQ zcB;lP2U}?TGKPwOxZhtRP$p{2rr@hiXo~O+YTYijZkgN3Xz&Gbm!4^{WY+SiS;0rb z7F4^2B5P~3%PUEFcNgY5h_GNo@aEs>^p7P~UiZtAeV;q9GPm#q{v%sQk*IfAx#I3W z?l-4@JD~}bZy?@z|5{j|$}di}3v}a79|b`DeEQC@thM~>qW@^hh0$fx#9cHWX5>>| zk;;~y@ZW(>Ka*auUL#71%0Q`eX=$o%zbn(Hxec3;0AkSrY2G(8d(~HEu^`Rx$O`kl zH$txUUtp~8qOI?3Jne5$(`<8WC#!iJ{=(!h@mlfc*W3*_Ms&JX z|AbP%|GOoOPAynZ=uVxkWrsck@g)m^zC5QSISpd;GF;91I;35o=-U`#WR;0}ymql; zsp_FcmLcdY)jj1_`Fn5g+x|QH^e=WKg1ebn2!oI1vtj^}|5{S(mcYDY#jkV}a+$>k zti?y`jeo~?UET`mVDh)*Sy_x{cT$8@i`SEgixJMkGqK)uveNXZD9ck=*0>i0@Zw82 zsbZQ6hM9hmdnxuL^LIb%QJUaYClTkGI@mKv`M)El8~^}B8mIE}Eh!3vTyQO6Q7JK} z_V|``ANS;D_i>Q=lAL)Ss~ntFTt*tq#e0&3pn~?|oB973%Oi5$F5eyX!PthHzxCM) zPG9K$n5FxMQ9+1Q9FhV6+*zO+M%^2xW1%w9=RiK;@0sF)T{YwTlZE(yIB}Ld<%_X^}@hK>F#4BvdeVx3(IO5hTcRoU?+FDr1M8n^9N`q7~1=RXl zSZp<4=NAa@+H^Q!&Mf`N+WK@PS`f?m@>Om?zn8@~x1x2M`nNN|4rRyDKZ#n92e*e* zmt1t_TQLtsC8Ktkcc52ctKoM$lDsDwN$;^UKyrTr5WY_>UV9~DNR49F+AR4#rj*+0 zR?<#ay6JS~Cbo=knBVQ>*HJxVOr12=i56)yx>?(+2Q6Av%IFkFXLabG3L&kJTDU(_sU|Ia)W&M?`X_W)c$qwAZ8?CGx-TASvL}qF4HeQ(C;_>@!Gckuz&7VPe$8>q z-?5#GX<`F08nHXq{CC6Y!JInGJ{a3C1LB>X(XghAC1ilq)G8zT-MHipLG*6?Z#!*i z3hs!1sjD@a@PrhSOcU#%etc+Lbg_0+1#Iu^eD<$XcpuQe{4r)_0xvgTBg5(}9z&Dc zA)n2S(fvA*9xP|EUH}4t2xPiJf5Jm=yO_eh5sEv!{Aho>ZTc+Btt6D84d8#`Xp2B9$v*X*#WnBp+W>n5T$UMTzzyqBMhr9MP(LD6`_eZ5QyW3)U7fATu>UOC8(V=Gz z$S(WTIuansP&M|#+PjVofvPWIoraY7p`Y$cKa6Z{djB1K5Ac_JG(i_AQTfWFuBtj= zrn@D1eR_;dGZAt!U z*G5)*)$OR6FZ@&To%XQ&qN{6M73Z&9uTPX;!JAk{G*vgvgVz42Ieuz=?-W=cJurY+ zB;>R%jTl8T=cF$kBDw*x0wt)FwsN z7+RJ^~DY#0iv$9u_wdI4NI_metce2Cb>pK3T%T| z&^n^C-N9fmITm#cU(Ij=W#BSSAX8;0$@l)~F(=GHag^7l`~%d===Iph`cXc_-^%Vf zl2$nS(LdEyqBbm<#L#+~<5Z*tHTe?+6GVwe6ef;URpM?D$gjmT97WRKpvZbW#if&? zBtQHa|FQJeRj)w^@kK`cDvCAey$}Oq-#l3W;lwjqZm{~V(Ot