diff --git a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java index 4e78b4308..6547fa41a 100644 --- a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java +++ b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java @@ -454,5 +454,4 @@ public final class JadxDecompiler implements Closeable { public String toString() { return "jadx decompiler " + getVersion(); } - } diff --git a/jadx-core/src/main/java/jadx/api/JavaClass.java b/jadx-core/src/main/java/jadx/api/JavaClass.java index 9909c50b0..84470e89d 100644 --- a/jadx-core/src/main/java/jadx/api/JavaClass.java +++ b/jadx-core/src/main/java/jadx/api/JavaClass.java @@ -57,6 +57,11 @@ public final class JavaClass implements JavaNode { cls.decompile(); } + public synchronized void refresh() { + listsLoaded = false; + cls.reRunDecompile(); + } + public synchronized String getSmali() { return cls.getSmali(); } 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 a2b2f6e25..53fe7b982 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 @@ -62,6 +62,8 @@ public final class ClassInfo implements Comparable { ClassAliasInfo newAlias = new ClassAliasInfo(getAliasPkg(), aliasName); fillAliasFullName(newAlias); this.alias = newAlias; + } else { + this.alias = null; } } diff --git a/jadx-core/src/main/java/jadx/core/dex/info/ConstStorage.java b/jadx-core/src/main/java/jadx/core/dex/info/ConstStorage.java index 0b299eec4..46adf6508 100644 --- a/jadx-core/src/main/java/jadx/core/dex/info/ConstStorage.java +++ b/jadx-core/src/main/java/jadx/core/dex/info/ConstStorage.java @@ -2,9 +2,12 @@ package jadx.core.dex.info; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import org.jetbrains.annotations.Nullable; @@ -20,7 +23,7 @@ import jadx.core.dex.nodes.RootNode; public class ConstStorage { private static final class ValueStorage { - private final Map values = new HashMap<>(); + private final Map values = new ConcurrentHashMap<>(); private final Set duplicates = new HashSet<>(); public Map getValues() { @@ -35,22 +38,33 @@ public class ConstStorage { * @return true if this value is duplicated */ public boolean put(Object value, FieldNode fld) { + if (duplicates.contains(value)) { + values.remove(value); + return true; + } FieldNode prev = values.put(value, fld); if (prev != null) { values.remove(value); duplicates.add(value); return true; } - if (duplicates.contains(value)) { - values.remove(value); - return true; - } return false; } public boolean contains(Object value) { return duplicates.contains(value) || values.containsKey(value); } + + void removeForCls(ClassNode cls) { + Iterator> it = values.entrySet().iterator(); + while (it.hasNext()) { + Entry entry = it.next(); + FieldNode field = entry.getValue(); + if (field.getParentClass().equals(cls)) { + it.remove(); + } + } + } } private final boolean replaceEnabled; @@ -81,6 +95,11 @@ public class ConstStorage { } } + public void removeForClass(ClassNode cls) { + classes.remove(cls); + globalValues.removeForCls(cls); + } + private void addConstField(ClassNode cls, FieldNode fld, Object value, boolean isPublic) { if (isPublic) { globalValues.put(value, fld); 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 028842d03..35fa01b60 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 @@ -38,16 +38,19 @@ import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.LiteralArg; import jadx.core.dex.nodes.parser.SignatureParser; +import jadx.core.dex.visitors.ProcessAnonymous; import jadx.core.utils.SmaliUtils; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; import static jadx.core.dex.nodes.ProcessState.LOADED; +import static jadx.core.dex.nodes.ProcessState.NOT_LOADED; public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeNode, Comparable { private static final Logger LOG = LoggerFactory.getLogger(ClassNode.class); private final RootNode root; + private final IClassData cls; private final int clsDefOffset; @Nullable private final Path inputPath; @@ -58,8 +61,8 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN private List interfaces; private List generics = Collections.emptyList(); - private final List methods; - private final List fields; + private List methods; + private List fields; private List innerClasses = Collections.emptyList(); private List inlinedClasses = Collections.emptyList(); @@ -86,6 +89,11 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN this.inputPath = cls.getInputPath(); this.clsDefOffset = cls.getClassDefOffset(); this.clsInfo = ClassInfo.fromType(root, ArgType.object(cls.getType())); + initialLoad(cls); + this.cls = cls.copy(); // TODO: need only for rename feature + } + + private void initialLoad(IClassData cls) { try { String superType = cls.getSuperType(); if (superType == null) { @@ -144,6 +152,7 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN // Create empty class private ClassNode(RootNode root, String name, int accessFlags) { this.root = root; + this.cls = null; this.inputPath = null; this.clsDefOffset = 0; this.clsInfo = ClassInfo.fromName(root, name); @@ -279,10 +288,31 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN return decompile(true); } - public ICodeInfo reloadCode() { + public synchronized ICodeInfo reRunDecompile() { return decompile(false); } + public synchronized ICodeInfo reloadCode() { + unload(); + deepUnload(); + return decompile(false); + } + + public void deepUnload() { + if (cls == null) { + // manually added class + return; + } + clearAttributes(); + root().getConstValues().removeForClass(this); + initialLoad(cls); + ProcessAnonymous.runForClass(this); + + for (ClassNode innerClass : innerClasses) { + innerClass.deepUnload(); + } + } + private synchronized ICodeInfo decompile(boolean searchInCache) { ICodeCache codeCache = root().getCodeCache(); ClassNode topParentClass = getTopParentClass(); @@ -319,6 +349,8 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN innerClasses.forEach(ClassNode::unload); fields.forEach(FieldNode::unloadAttributes); unloadAttributes(); + setState(NOT_LOADED); + this.smali = null; } private void buildCache() { 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 12ae3c8b3..c522fd179 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 @@ -172,6 +172,15 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails, } } + public void reload() { + unload(); + try { + load(); + } catch (DecodeException e) { + throw new JadxRuntimeException("Failed to reload method " + getClass().getName() + "." + getName()); + } + } + public void initMethodTypes() { if (!parseSignature()) { this.retType = mthInfo.getReturnType(); diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/ProcessAnonymous.java b/jadx-core/src/main/java/jadx/core/dex/visitors/ProcessAnonymous.java index faa66b823..749e5eaca 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/ProcessAnonymous.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/ProcessAnonymous.java @@ -5,27 +5,29 @@ import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; -import jadx.core.dex.visitors.regions.RegionMakerVisitor; @JadxVisitor( name = "ProcessAnonymous", - desc = "Mark anonymous and lambda classes (for future inline)", - runAfter = RegionMakerVisitor.class + desc = "Mark anonymous and lambda classes (for future inline)" ) public class ProcessAnonymous extends AbstractVisitor { @Override public void init(RootNode root) { - if (!root.getArgs().isInlineAnonymousClasses()) { - return; + if (root.getArgs().isInlineAnonymousClasses()) { + for (ClassNode cls : root.getClasses(true)) { + markAnonymousClass(cls); + } } + } - for (ClassNode cls : root.getClasses(true)) { + public static void runForClass(ClassNode cls) { + if (cls.root().getArgs().isInlineAnonymousClasses()) { markAnonymousClass(cls); } } - private static boolean markAnonymousClass(ClassNode cls) { + private static void markAnonymousClass(ClassNode cls) { if (isAnonymous(cls) || isLambdaCls(cls)) { cls.add(AFlag.ANONYMOUS_CLASS); cls.add(AFlag.DONT_GENERATE); @@ -35,9 +37,7 @@ public class ProcessAnonymous extends AbstractVisitor { mth.add(AFlag.ANONYMOUS_CONSTRUCTOR); } } - return true; } - return false; } private static boolean isAnonymous(ClassNode cls) { @@ -62,5 +62,4 @@ public class ProcessAnonymous extends AbstractVisitor { } return c; } - } diff --git a/jadx-core/src/test/java/jadx/tests/api/utils/assertj/JadxCodeAssertions.java b/jadx-core/src/test/java/jadx/tests/api/utils/assertj/JadxCodeAssertions.java index adbe9d7d7..2c5c75bae 100644 --- a/jadx-core/src/test/java/jadx/tests/api/utils/assertj/JadxCodeAssertions.java +++ b/jadx-core/src/test/java/jadx/tests/api/utils/assertj/JadxCodeAssertions.java @@ -61,6 +61,11 @@ public class JadxCodeAssertions extends AbstractStringAssert return containsOnlyOnce(sb.toString()); } + public JadxCodeAssertions removeBlockComments() { + String code = actual.replaceAll("/\\*.*\\*/", ""); + return new JadxCodeAssertions(code); + } + public JadxCodeAssertions print() { System.out.println("-----------------------------------------------------------"); System.out.println(actual); diff --git a/jadx-core/src/test/java/jadx/tests/integration/rename/TestAnonymousInline.java b/jadx-core/src/test/java/jadx/tests/integration/rename/TestAnonymousInline.java new file mode 100644 index 000000000..f819046bd --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/rename/TestAnonymousInline.java @@ -0,0 +1,35 @@ +package jadx.tests.integration.rename; + +import org.junit.jupiter.api.Test; + +import jadx.core.dex.nodes.ClassNode; +import jadx.tests.api.IntegrationTest; + +import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; + +public class TestAnonymousInline extends IntegrationTest { + + public static class TestCls { + public Runnable test() { + return new Runnable() { + @Override + public void run() { + System.out.println("run"); + } + }; + } + } + + @Test + public void test() { + ClassNode cls = getClassNode(TestCls.class); + assertThat(cls.getCode()) + .containsOnlyOnce("return new Runnable() {"); + + assertThat(cls.reloadCode()) + .removeBlockComments() // remove comment about inlined class + .print() + .containsOnlyOnce("return new Runnable() {") + .doesNotContain("AnonymousClass1"); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/rename/TestConstReplace.java b/jadx-core/src/test/java/jadx/tests/integration/rename/TestConstReplace.java new file mode 100644 index 000000000..378fc28c0 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/rename/TestConstReplace.java @@ -0,0 +1,30 @@ +package jadx.tests.integration.rename; + +import org.junit.jupiter.api.Test; + +import jadx.core.dex.nodes.ClassNode; +import jadx.tests.api.IntegrationTest; + +import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; + +public class TestConstReplace extends IntegrationTest { + + public static class TestCls { + public static final String CONST = "SOME_CONST"; + + public String test() { + return CONST; + } + } + + @Test + public void test() { + ClassNode cls = getClassNode(TestCls.class); + assertThat(cls.getCode()) + .containsOnlyOnce("return CONST;"); + + assertThat(cls.reloadCode()) + .print() + .containsOnlyOnce("return CONST;"); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/rename/TestRenameEnum.java b/jadx-core/src/test/java/jadx/tests/integration/rename/TestRenameEnum.java new file mode 100644 index 000000000..2ae89b18a --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/rename/TestRenameEnum.java @@ -0,0 +1,44 @@ +package jadx.tests.integration.rename; + +import org.junit.jupiter.api.Test; + +import jadx.core.dex.nodes.ClassNode; +import jadx.tests.api.IntegrationTest; + +import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; + +public class TestRenameEnum extends IntegrationTest { + + public static class TestCls { + + public enum A implements Runnable { + ONE { + @Override + public void run() { + System.out.println("ONE"); + } + }, + TWO { + @Override + public void run() { + System.out.println("TWO"); + } + }; + } + } + + @Test + public void test() { + ClassNode cls = getClassNode(TestCls.class); + assertThat(cls.getCode()) + .containsOnlyOnce("public enum A ") + .containsOnlyOnce("ONE {"); + + cls.getInnerClasses().get(0).getClassInfo().changeShortName("ARenamed"); + + assertThat(cls.reloadCode()) + .print() + .containsOnlyOnce("public enum ARenamed ") + .containsOnlyOnce("ONE {"); + } +} 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 b90da2d32..5f4c04f79 100644 --- a/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundWorker.java +++ b/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundWorker.java @@ -52,6 +52,24 @@ public class BackgroundWorker extends SwingWorker { System.gc(); LOG.debug("Memory usage: Before decompile: {}", UiUtils.memoryInfo()); runJob(cache.getDecompileJob()); + LOG.debug("Memory usage: After decompile: {}", UiUtils.memoryInfo()); + + if (cache.getUnloadJob() != null) { + LOG.info("Memory usage: Before unload: {}", UiUtils.memoryInfo()); + long start = System.nanoTime(); + runJob(cache.getUnloadJob()); + cache.setUnloadJob(null); + LOG.info("Memory usage: After unload: {}, unload took {} ms", UiUtils.memoryInfo(), (System.nanoTime() - start) / 1000000); + } + + if (cache.getRefreshJob() != null) { + LOG.info("Memory usage: Before refresh: {}", UiUtils.memoryInfo()); + long start = System.nanoTime(); + runJob(cache.getRefreshJob()); + cache.setRefreshJob(null); + LOG.info("Memory usage: After refresh: {}, refresh took {} ms", UiUtils.memoryInfo(), + (System.nanoTime() - start) / 1000000); + } LOG.debug("Memory usage: Before index: {}", UiUtils.memoryInfo()); runJob(cache.getIndexJob()); @@ -74,7 +92,7 @@ public class BackgroundWorker extends SwingWorker { } private void runJob(BackgroundJob job) { - if (isCancelled()) { + if (isCancelled() || job == null) { return; } progressPane.changeLabel(this, job.getInfoString()); diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/RefreshJob.java b/jadx-gui/src/main/java/jadx/gui/jobs/RefreshJob.java new file mode 100644 index 000000000..830feab45 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/jobs/RefreshJob.java @@ -0,0 +1,28 @@ +package jadx.gui.jobs; + +import java.util.Set; + +import jadx.api.JavaClass; +import jadx.gui.JadxWrapper; + +public class RefreshJob extends BackgroundJob { + + private Set refreshClasses; + + public RefreshJob(JadxWrapper jadxWrapper, int threadsCount, Set refreshClasses) { + super(jadxWrapper, threadsCount); + this.refreshClasses = refreshClasses; + } + + protected void runJob() { + for (final JavaClass cls : refreshClasses) { + addTask(cls::refresh); + } + } + + @Override + public String getInfoString() { + return "Refreshing: "; + } + +} diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/UnloadJob.java b/jadx-gui/src/main/java/jadx/gui/jobs/UnloadJob.java new file mode 100644 index 000000000..e3c646d6e --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/jobs/UnloadJob.java @@ -0,0 +1,31 @@ +package jadx.gui.jobs; + +import java.util.Set; + +import jadx.api.JavaClass; +import jadx.gui.JadxWrapper; + +public class UnloadJob extends BackgroundJob { + + private Set refreshClasses; + + public UnloadJob(JadxWrapper jadxWrapper, int threadsCount, Set refreshClasses) { + super(jadxWrapper, threadsCount); + this.refreshClasses = refreshClasses; + } + + protected void runJob() { + for (final JavaClass cls : refreshClasses) { + addTask(() -> { + cls.unload(); + cls.getClassNode().deepUnload(); + }); + } + } + + @Override + public String getInfoString() { + return "Refreshing: "; + } + +} diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java index 46becf9ab..8f7aada3c 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java @@ -59,6 +59,13 @@ public class JClass extends JLoadableNode { update(); } + public synchronized void refresh() { + cls.refresh(); + loaded = true; + update(); + cls.unload(); + } + public synchronized void update() { removeAllChildren(); if (!loaded) { 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 17e6ccf5d..103fb24bf 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/CommonSearchDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/CommonSearchDialog.java @@ -516,17 +516,15 @@ public abstract class CommonSearchDialog extends JDialog { } } - protected void loadStartCommon() { + private void loadStartCommon() { setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); progressPane.setIndeterminate(true); progressPane.setVisible(true); - resultsTable.setEnabled(false); warnLabel.setVisible(false); } private void loadFinishedCommon() { setCursor(null); - resultsTable.setEnabled(true); progressPane.setVisible(false); TextSearchIndex textIndex = cache.getTextIndex(); 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 9690d0a55..a659b19c9 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java @@ -397,7 +397,12 @@ public class MainWindow extends JFrame { cacheObject.setIndexJob(new IndexJob(wrapper, cacheObject, threadsCount)); } - private synchronized void runBackgroundJobs() { + public void resetIndex() { + int threadsCount = settings.getThreadsCount(); + cacheObject.setIndexJob(new IndexJob(wrapper, cacheObject, threadsCount)); + } + + synchronized void runBackgroundJobs() { cancelBackgroundJobs(); backgroundWorker = new BackgroundWorker(cacheObject, progressPane); if (settings.isAutoStartJobs()) { @@ -410,6 +415,12 @@ public class MainWindow extends JFrame { } } + synchronized void runBackgroundUnloadRefreshAndIndexJobs() { + cancelBackgroundJobs(); + backgroundWorker = new BackgroundWorker(cacheObject, progressPane); + backgroundWorker.exec(); + } + public synchronized void cancelBackgroundJobs() { backgroundExecutor.cancelAll(); if (backgroundWorker != null) { @@ -513,7 +524,7 @@ public class MainWindow extends JFrame { treeModel.reload(); } - private void reloadTree() { + public void reloadTree() { treeReloading = true; treeModel.reload(); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/RenameDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/RenameDialog.java index 5241181c7..76b1081e0 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/RenameDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/RenameDialog.java @@ -1,7 +1,6 @@ package jadx.gui.ui; import java.awt.*; -import java.awt.event.KeyEvent; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -9,7 +8,10 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; import javax.swing.*; @@ -22,16 +24,22 @@ import jadx.api.JavaField; import jadx.api.JavaMethod; import jadx.api.JavaNode; import jadx.core.dex.nodes.RootNode; +import jadx.core.dex.visitors.RenameVisitor; +import jadx.gui.jobs.IndexJob; +import jadx.gui.jobs.RefreshJob; +import jadx.gui.jobs.UnloadJob; +import jadx.gui.settings.JadxSettings; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JField; import jadx.gui.treemodel.JMethod; import jadx.gui.treemodel.JNode; import jadx.gui.treemodel.JPackage; +import jadx.gui.ui.codearea.ClassCodeContentPanel; import jadx.gui.ui.codearea.CodeArea; -import jadx.gui.utils.NLS; -import jadx.gui.utils.TextStandardActions; +import jadx.gui.ui.codearea.CodePanel; +import jadx.gui.utils.*; -public class RenameDialog extends JDialog { +public class RenameDialog extends CommonSearchDialog { private static final long serialVersionUID = -3269715644416902410L; private static final Logger LOG = LoggerFactory.getLogger(RenameDialog.class); @@ -44,23 +52,62 @@ public class RenameDialog extends JDialog { private CodeArea codeArea; + private JButton renameBtn; + public RenameDialog(CodeArea codeArea, JNode node) { super(codeArea.getMainWindow()); mainWindow = codeArea.getMainWindow(); this.codeArea = codeArea; this.node = node; - initUI(); - loadWindowPos(); + if (isDeobfuscationSettingsValid()) { + initUI(); + registerInitOnOpen(); + loadWindowPos(); + } else { + LOG.error("Deobfuscation settings are invalid - please enable deobfuscation and disable force rewrite deobfuscation map"); + } } - private void loadWindowPos() { - mainWindow.getSettings().loadWindowPos(this); + private boolean isDeobfuscationSettingsValid() { + boolean valid = true; + String errorMessage = null; + JadxSettings settings = mainWindow.getSettings(); + final LangLocale langLocale = settings.getLangLocale(); + if (settings.isDeobfuscationForceSave()) { + valid = false; + errorMessage = NLS.str("msg.rename_disabled_force_rewrite_enabled", langLocale); + } + if (!settings.isDeobfuscationOn()) { + valid = false; + errorMessage = NLS.str("msg.rename_disabled_deobfuscation_disabled", langLocale); + } + if (errorMessage != null) { + showRenameDisabledErrorMessage(langLocale, errorMessage); + } + return valid; + } + + private void showRenameDisabledErrorMessage(LangLocale langLocale, String message) { + JOptionPane.showMessageDialog( + mainWindow, + message, + NLS.str("msg.rename_disabled_title", langLocale), + JOptionPane.ERROR_MESSAGE); } @Override - public void dispose() { - mainWindow.getSettings().saveWindowPos(this); - super.dispose(); + protected void openInit() { + prepare(); + } + + @Override + protected void loadStart() { + renameBtn.setEnabled(false); + } + + @Override + protected void loadFinished() { + renameBtn.setEnabled(true); } private Path getDeobfMapPath(RootNode root) { @@ -103,52 +150,52 @@ public class RenameDialog extends JDialog { return String.format("%s %s = %s", type, id, renameText); } - private boolean updateDeobfMap(String renameText, RootNode root) { - Path deobfMapPath = getDeobfMapPath(root); + private boolean writeDeobfMapFile(Path deobfMapPath, List deobfMap) throws IOException { if (deobfMapPath == null) { - LOG.error("rename(): Failed deofbMapFile is null"); + LOG.error("updateDeobfMapFile(): deobfMapPath is null!"); return false; } - String alias = getNodeAlias(renameText); - LOG.info("rename(): {}", alias); - try { - List deobfMap = readAndUpdateDeobfMap(deobfMapPath, alias); - File tmpFile = File.createTempFile("deobf_tmp_", ".txt"); - try (FileOutputStream fileOut = new FileOutputStream(tmpFile)) { - for (String entry : deobfMap) { - fileOut.write(entry.getBytes()); - fileOut.write(System.lineSeparator().getBytes()); - } - } - File oldMap = File.createTempFile("deobf_bak_", ".txt"); - Files.copy(deobfMapPath, oldMap.toPath(), StandardCopyOption.REPLACE_EXISTING); - Files.copy(tmpFile.toPath(), deobfMapPath, StandardCopyOption.REPLACE_EXISTING); - Files.delete(oldMap.toPath()); - } catch (Exception e) { - LOG.error("rename(): Failed to write deofbMapFile {}", deobfMapPath, e); - return false; + + File tmpFile = File.createTempFile("deobf_tmp_", ".txt"); + FileOutputStream fileOut = new FileOutputStream(tmpFile); + for (String entry : deobfMap) { + fileOut.write(entry.getBytes()); + fileOut.write(System.lineSeparator().getBytes()); } + fileOut.close(); + File oldMap = File.createTempFile("deobf_bak_", ".txt"); + Files.copy(deobfMapPath, oldMap.toPath(), StandardCopyOption.REPLACE_EXISTING); + LOG.trace("Copying " + tmpFile.toPath() + " to " + deobfMapPath); + Files.copy(tmpFile.toPath(), deobfMapPath, StandardCopyOption.REPLACE_EXISTING); + Files.delete(oldMap.toPath()); + Files.delete(tmpFile.toPath()); return true; } - private List readAndUpdateDeobfMap(Path deobfMapPath, String alias) throws IOException { - List deobfMap = Files.readAllLines(deobfMapPath, StandardCharsets.UTF_8); + @NotNull + private List readDeobfMap(Path deobfMapPath) throws IOException { + return Files.readAllLines(deobfMapPath, StandardCharsets.UTF_8); + } + + private List updateDeobfMap(List deobfMap, String alias) { + LOG.trace("updateDeobfMap(): alias = " + alias); String id = alias.split("=")[0]; - LOG.info("Id = {}", id); int i = 0; while (i < deobfMap.size()) { if (deobfMap.get(i).startsWith(id)) { - LOG.info("Removing entry {}", deobfMap.get(i)); + LOG.info("updateDeobfMap(): Removing entry " + deobfMap.get(i)); deobfMap.remove(i); } else { i++; } } + LOG.trace("updateDeobfMap(): Placing alias = " + alias); deobfMap.add(alias); return deobfMap; } private void rename() { + long start = System.nanoTime(); String renameText = renameField.getText(); if (renameText == null || renameText.length() == 0 || codeArea.getText() == null) { return; @@ -159,31 +206,126 @@ public class RenameDialog extends JDialog { dispose(); return; } - if (!updateDeobfMap(renameText, root)) { - LOG.error("rename(): updateDeobfMap() failed"); + if (!refreshDeobfMapFile(renameText, root)) { + LOG.error("rename(): refreshDeobfMapFile() failed!"); dispose(); return; } - mainWindow.reOpenFile(); + long refreshStart = System.nanoTime(); + int classes = refreshState(root); + long refreshTime = (System.nanoTime() - refreshStart) / 1000000; + long totalTime = (System.nanoTime() - start) / 1000000; + LOG.info("refreshState() took {} ms to update state, {} classes will be refreshed in background ({} ms total)", refreshTime, + classes, + totalTime); dispose(); } - private void initCommon() { - KeyStroke stroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0); - getRootPane().registerKeyboardAction(e -> dispose(), stroke, JComponent.WHEN_IN_FOCUSED_WINDOW); + private boolean refreshDeobfMapFile(String renameText, RootNode root) { + List deobfMap; + Path deobfMapPath = getDeobfMapPath(root); + try { + deobfMap = readDeobfMap(deobfMapPath); + } catch (IOException e) { + LOG.error("rename(): readDeobfMap() failed"); + return false; + } + updateDeobfMap(deobfMap, getNodeAlias(renameText)); + try { + writeDeobfMapFile(deobfMapPath, deobfMap); + } catch (IOException e) { + LOG.error("rename(): writeDeobfMap() failed"); + return false; + } + return true; + } + + private int refreshState(RootNode rootNode) { + RenameVisitor renameVisitor = new RenameVisitor(); + renameVisitor.init(rootNode); + + cache.getNodeCache().refresh(node); + + Set updatedClasses = getUpdatedClasses(); + + mainWindow.reloadTree(); + refreshTabs(mainWindow.getTabbedPane(), updatedClasses); + + if (updatedClasses.size() > 0) { + setRefreshTask(updatedClasses); + } + + return updatedClasses.size(); + } + + private void refreshTabs(TabbedPane tabbedPane, Set updatedClasses) { + for (Map.Entry panel : tabbedPane.getOpenTabs().entrySet()) { + ContentPanel contentPanel = panel.getValue(); + if (contentPanel instanceof ClassCodeContentPanel) { + JNode node = panel.getKey(); + JClass rootClass = node.getRootClass(); + JavaClass javaClass = rootClass.getCls(); + if (updatedClasses.contains(javaClass) || node.getRootClass().getCls() == javaClass) { + LOG.info("Refreshing rootClass " + javaClass.getRawName()); + javaClass.unload(); + javaClass.getClassNode().deepUnload(); + rootClass.refresh(); // Update code cache + ClassCodeContentPanel codePanel = (ClassCodeContentPanel) contentPanel; + CodePanel javaPanel = codePanel.getJavaCodePanel(); + javaPanel.refresh(); + tabbedPane.refresh(node); + updatedClasses.remove(javaClass); + } + } + } + } + + private Set getUpdatedClasses() { + Set usageClasses = new HashSet<>(); + CodeUsageInfo usageInfo = cache.getUsageInfo(); + if (usageInfo != null) { + usageInfo.getUsageList(node).forEach((node) -> { + JavaClass rootClass = node.getRootClass().getCls(); + // LOG.info("updateUsages(): Going to update class {}", rootClass.getRealFullName()); + usageClasses.add(rootClass); + }); + // usageClasses.parallelStream().forEach(JavaClass::refresh); + } + return usageClasses; + } + + private void setRefreshTask(Set refreshClasses) { + UnloadJob unloadJob = new UnloadJob(mainWindow.getWrapper(), mainWindow.getSettings().getThreadsCount(), refreshClasses); + RefreshJob refreshJob = new RefreshJob(mainWindow.getWrapper(), mainWindow.getSettings().getThreadsCount(), refreshClasses); + LOG.info("Waiting for old unloadJob and refreshJob"); + while (cache.getUnloadJob() != null || cache.getRefreshJob() != null) { + try { + Thread.sleep(10); + } catch (InterruptedException e) { + return; + } + } + LOG.info("Old unloadJob and refreshJob finished"); + cache.setUnloadJob(unloadJob); + cache.setRefreshJob(refreshJob); + cache.setIndexJob(new IndexJob(mainWindow.getWrapper(), cache, mainWindow.getSettings().getThreadsCount())); + mainWindow.runBackgroundUnloadRefreshAndIndexJobs(); } @NotNull - private JPanel initButtonsPanel() { + protected JPanel initButtonsPanel() { JButton cancelButton = new JButton(NLS.str("search_dialog.cancel")); cancelButton.addActionListener(event -> dispose()); - JButton renameBtn = new JButton(NLS.str("popup.rename")); + renameBtn = new JButton(NLS.str("popup.rename")); renameBtn.addActionListener(event -> rename()); getRootPane().setDefaultButton(renameBtn); + progressPane = new ProgressPanel(mainWindow, false); + JPanel buttonPane = new JPanel(); buttonPane.setLayout(new BoxLayout(buttonPane, BoxLayout.LINE_AXIS)); buttonPane.setBorder(BorderFactory.createEmptyBorder(0, 10, 10, 10)); + buttonPane.add(progressPane); buttonPane.add(Box.createRigidArea(new Dimension(5, 0))); buttonPane.add(Box.createHorizontalGlue()); buttonPane.add(renameBtn); @@ -209,8 +351,14 @@ public class RenameDialog extends JDialog { renamePane.add(nodeLabel); renamePane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + warnLabel = new JLabel(); + warnLabel.setForeground(Color.RED); + warnLabel.setVisible(false); + JPanel textPane = new JPanel(); - textPane.setLayout(new FlowLayout(FlowLayout.LEFT)); + textPane.setLayout(new BoxLayout(textPane, BoxLayout.PAGE_AXIS)); + textPane.add(warnLabel); + textPane.add(Box.createRigidArea(new Dimension(0, 5))); textPane.add(renameField); textPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); 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 39fd2624f..50cdc1f25 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/SearchDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/SearchDialog.java @@ -282,11 +282,13 @@ public class SearchDialog extends CommonSearchDialog { @Override protected void loadFinished() { + resultsTable.setEnabled(true); searchField.setEnabled(true); } @Override protected void loadStart() { + resultsTable.setEnabled(false); searchField.setEnabled(false); } } 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 ae0627b10..72ff21eb7 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/TabbedPane.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/TabbedPane.java @@ -155,6 +155,13 @@ public class TabbedPane extends JTabbedPane { return panel; } + public void refresh(JNode node) { + ContentPanel panel = openTabs.get(node); + if (panel != null) { + setTabComponentAt(indexOfComponent(panel), makeTabComponent(panel)); + } + } + @Nullable private ContentPanel makeContentPanel(JNode node) { if (node instanceof JResource) { diff --git a/jadx-gui/src/main/java/jadx/gui/ui/UsageDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/UsageDialog.java index 06d7e5df2..e2e8cd9ad 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/UsageDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/UsageDialog.java @@ -30,12 +30,13 @@ public class UsageDialog extends CommonSearchDialog { @Override protected void loadFinished() { + resultsTable.setEnabled(true); performSearch(); } @Override protected void loadStart() { - // no op + resultsTable.setEnabled(false); } @Override diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java index 90068eaa8..308f8f633 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java @@ -53,6 +53,12 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea { */ public abstract void load(); + /** + * Implement in this method the code that reloads node from cache and sets the new content to be + * displayed + */ + public abstract void refresh(); + public static RSyntaxTextArea getDefaultArea(MainWindow mainWindow) { RSyntaxTextArea area = new RSyntaxTextArea(); area.setEditable(false); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/ClassCodeContentPanel.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/ClassCodeContentPanel.java index ff9b1ebde..937795ef5 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/ClassCodeContentPanel.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/ClassCodeContentPanel.java @@ -70,4 +70,8 @@ public final class ClassCodeContentPanel extends AbstractCodeContentPanel { return javaCodePanel.getCodeArea(); } + public CodePanel getJavaCodePanel() { + return javaCodePanel; + } + } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java index 867dae715..87719890d 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java @@ -67,6 +67,11 @@ public final class CodeArea extends AbstractCodeArea { } } + @Override + public void refresh() { + setText(node.getContent()); + } + private void addMenuItems() { FindUsageAction findUsage = new FindUsageAction(this); GoToDeclarationAction goToDeclaration = new GoToDeclarationAction(this); 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 f715092f3..45c97831a 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 @@ -87,4 +87,14 @@ public class CodePanel extends JPanel { public JScrollPane getCodeScrollPane() { return codeScrollPane; } + + public void refresh() { + JViewport viewport = getCodeScrollPane().getViewport(); + Point viewPosition = viewport.getViewPosition(); + codeArea.refresh(); + initLineNumbers(); + SwingUtilities.invokeLater(() -> { + viewport.setViewPosition(viewPosition); + }); + } } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/SmaliArea.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/SmaliArea.java index 41200a3bf..c2bacad60 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/SmaliArea.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/SmaliArea.java @@ -25,6 +25,11 @@ public final class SmaliArea extends AbstractCodeArea { } } + @Override + public void refresh() { + load(); + } + @Override public JNode getNode() { // this area contains only smali without other node attributes diff --git a/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java b/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java index fc47dd81c..b367114bb 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java @@ -7,6 +7,8 @@ import org.jetbrains.annotations.Nullable; import jadx.gui.jobs.DecompileJob; import jadx.gui.jobs.IndexJob; +import jadx.gui.jobs.RefreshJob; +import jadx.gui.jobs.UnloadJob; import jadx.gui.ui.SearchDialog; import jadx.gui.utils.search.TextSearchIndex; @@ -14,6 +16,8 @@ public class CacheObject { private DecompileJob decompileJob; private IndexJob indexJob; + private UnloadJob unloadJob; + private RefreshJob refreshJob; private TextSearchIndex textIndex; private CodeUsageInfo usageInfo; @@ -89,4 +93,20 @@ public class CacheObject { public Set getLastSearchOptions() { return lastSearchOptions; } + + public RefreshJob getRefreshJob() { + return refreshJob; + } + + public void setRefreshJob(RefreshJob refreshJob) { + this.refreshJob = refreshJob; + } + + public UnloadJob getUnloadJob() { + return unloadJob; + } + + public void setUnloadJob(UnloadJob unloadJob) { + this.unloadJob = unloadJob; + } } diff --git a/jadx-gui/src/main/java/jadx/gui/utils/JNodeCache.java b/jadx-gui/src/main/java/jadx/gui/utils/JNodeCache.java index e16ac30bf..1185c3489 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/JNodeCache.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/JNodeCache.java @@ -29,6 +29,12 @@ public class JNodeCache { return jNode; } + public void refresh(JNode node) { + JavaNode javaNode = node.getJavaNode(); + cache.remove(javaNode); + makeFrom(javaNode); + } + private JNode convert(JavaNode node) { if (node == null) { return null; diff --git a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties index df4db452c..0f1996034 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties @@ -138,6 +138,8 @@ msg.project_error_title=Fehler msg.project_error=Projekt konnte nicht geladen werden msg.rename_disabled_title=Umbenennen deaktiviert msg.rename_disabled=Einige der Umbenennungseinstellungen sind deaktiviert, bitte beachten Sie dies. +msg.rename_disabled_force_rewrite_enabled=Deaktivieren Sie zum Umbenennen die Option "Deobfuscationskartendatei umschreiben erzwingen". +msg.rename_disabled_deobfuscation_disabled=Bitte aktivieren Sie die Umbenennung von Deobfuscation. msg.cmd_select_class_error=Klasse\n%s auswählen nicht möglich\nSie existiert nicht. popup.undo=Rückgängig 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 636d06faa..9a4e7be22 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties @@ -138,6 +138,8 @@ msg.project_error_title=Error msg.project_error=Project could not be loaded msg.rename_disabled_title=Rename disabled msg.rename_disabled=Some of rename settings are disabled, please take this into consideration +msg.rename_disabled_force_rewrite_enabled=Please disable "Force rewrite deobfuscation map file" option to rename. +msg.rename_disabled_deobfuscation_disabled=Please enable deobfuscation to rename. msg.cmd_select_class_error=Failed to select the class\n%s\nThe class does not exist. popup.undo=Undo diff --git a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties index 727e2cbac..bbad9221b 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties @@ -138,6 +138,8 @@ msg.index_not_initialized=Índice no inicializado, ¡la bósqueda se desactivar #msg.project_error= #msg.rename_disabled_title= #msg.rename_disabled= +#msg.rename_disabled_force_rewrite_enabled= +#msg.rename_disabled_deobfuscation_disabled= #msg.cmd_select_class_error= popup.undo=Deshacer diff --git a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties index e9cfdc721..f2183db81 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties @@ -138,6 +138,8 @@ msg.project_error_title=错误 msg.project_error=项目无法加载 msg.rename_disabled_title=重命名已禁用 msg.rename_disabled=某些重命名设置已禁用,请将此考虑在内 +msg.rename_disabled_force_rewrite_enabled=请禁用“强制覆盖反重构映射文件”选项以重命名。 +msg.rename_disabled_deobfuscation_disabled=请启用反混淆以重命名。 msg.cmd_select_class_error=无法选择类\n%s\n该类不存在。 popup.undo=撤销 diff --git a/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/sections/DexClassData.java b/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/sections/DexClassData.java index e351e7b1b..59c62c526 100644 --- a/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/sections/DexClassData.java +++ b/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/sections/DexClassData.java @@ -27,6 +27,11 @@ public class DexClassData implements IClassData { this.annotationsParser = annotationsParser; } + @Override + public IClassData copy() { + return new DexClassData(in.copy(), annotationsParser); + } + @Override public String getType() { int typeIdx = in.pos(0).readInt(); diff --git a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/data/IClassData.java b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/data/IClassData.java index f35757fa0..9fec50b1e 100644 --- a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/data/IClassData.java +++ b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/data/IClassData.java @@ -10,6 +10,7 @@ import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.api.plugins.input.data.annotations.IAnnotation; public interface IClassData { + IClassData copy(); String getType();