diff --git a/pom.xml b/pom.xml
index 43face833..ccdefdc35 100644
--- a/pom.xml
+++ b/pom.xml
@@ -82,6 +82,13 @@
HTTPRequest
${httprequest.version}
+
+
+ junit
+ junit
+ 4.13.2
+ test
+
org.jetbrains
annotations
diff --git a/src/main/java/the/bytecode/club/bytecodeviewer/deobfuscator/Deobfuscator.java b/src/main/java/the/bytecode/club/bytecodeviewer/deobfuscator/Deobfuscator.java
new file mode 100644
index 000000000..0334abf70
--- /dev/null
+++ b/src/main/java/the/bytecode/club/bytecodeviewer/deobfuscator/Deobfuscator.java
@@ -0,0 +1,56 @@
+package the.bytecode.club.bytecodeviewer.deobfuscator;
+
+import org.objectweb.asm.tree.ClassNode;
+import the.bytecode.club.bytecodeviewer.BytecodeViewer;
+import the.bytecode.club.bytecodeviewer.api.ASMResourceUtil;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Deobfuscator logic for renaming obfuscated class names to semantic names.
+ * Prefixes: C for Class, AC for Abstract Class, I for Interface.
+ */
+public class Deobfuscator {
+ private final Map nameMapping = new HashMap<>();
+ private int classCount = 1;
+ private int abstractClassCount = 1;
+ private int interfaceCount = 1;
+
+ public void run() {
+ for (ClassNode cn : BytecodeViewer.getLoadedClasses()) {
+ String newName = generateNewName(cn);
+ // Handle inner/anonymous classes
+ if (cn.name.contains("$")) {
+ // Split outer and inner
+ String[] parts = cn.name.split("\\$");
+ String outer = parts[0];
+ String inner = parts[1];
+ String outerNew = nameMapping.getOrDefault(outer, generateNewName(cn));
+ // If anonymous (numeric), keep numeric
+ if (inner.matches("\\d+")) {
+ newName = outerNew + "$" + inner;
+ } else {
+ newName = outerNew + "$" + inner;
+ }
+ }
+ // Optionally handle package renaming (preserve for now)
+ nameMapping.put(cn.name, newName);
+ ASMResourceUtil.renameClassNode(cn.name, newName);
+ }
+ }
+
+ String generateNewName(ClassNode cn) {
+ int access = cn.access;
+ if ((access & org.objectweb.asm.Opcodes.ACC_INTERFACE) != 0) {
+ return "I" + String.format("%03d", interfaceCount++);
+ } else if ((access & org.objectweb.asm.Opcodes.ACC_ABSTRACT) != 0) {
+ return "AC" + String.format("%03d", abstractClassCount++);
+ } else {
+ return "C" + String.format("%03d", classCount++);
+ }
+ }
+
+ public Map getNameMapping() {
+ return nameMapping;
+ }
+}
diff --git a/src/main/java/the/bytecode/club/bytecodeviewer/gui/MainViewerGUI.java b/src/main/java/the/bytecode/club/bytecodeviewer/gui/MainViewerGUI.java
index 8a6b84fa7..a06fd1790 100644
--- a/src/main/java/the/bytecode/club/bytecodeviewer/gui/MainViewerGUI.java
+++ b/src/main/java/the/bytecode/club/bytecodeviewer/gui/MainViewerGUI.java
@@ -682,6 +682,56 @@ public void buildPluginMenu()
pluginsMainMenu.add(stackFramesRemover);
pluginsMainMenu.add(changeClassFileVersions);
+ // Deobfuscator integration
+ JMenuItem deobfuscateClasses = new JMenuItem("Deobfuscate Classes");
+ pluginsMainMenu.add(deobfuscateClasses);
+
+ JMenuItem previewDeobfuscation = new JMenuItem("Preview Deobfuscation Mapping");
+ pluginsMainMenu.add(previewDeobfuscation);
+
+ JMenuItem undoDeobfuscation = new JMenuItem("Undo Deobfuscation");
+ pluginsMainMenu.add(undoDeobfuscation);
+
+ // Store last mapping for undo
+ final the.bytecode.club.bytecodeviewer.deobfuscator.Deobfuscator[] lastDeobfuscator = new the.bytecode.club.bytecodeviewer.deobfuscator.Deobfuscator[1];
+
+ deobfuscateClasses.addActionListener(e -> {
+ the.bytecode.club.bytecodeviewer.deobfuscator.Deobfuscator deobfuscator = new the.bytecode.club.bytecodeviewer.deobfuscator.Deobfuscator();
+ deobfuscator.run();
+ lastDeobfuscator[0] = deobfuscator;
+ JOptionPane.showMessageDialog(this, "Deobfuscation complete.", "Deobfuscator", JOptionPane.INFORMATION_MESSAGE);
+ BytecodeViewer.refreshAllTabs();
+ });
+
+ previewDeobfuscation.addActionListener(e -> {
+ the.bytecode.club.bytecodeviewer.deobfuscator.Deobfuscator deobfuscator = new the.bytecode.club.bytecodeviewer.deobfuscator.Deobfuscator();
+ // Generate mapping without renaming
+ StringBuilder sb = new StringBuilder();
+ for (org.objectweb.asm.tree.ClassNode cn : the.bytecode.club.bytecodeviewer.BytecodeViewer.getLoadedClasses()) {
+ String newName = deobfuscator.generateNewName(cn);
+ sb.append(cn.name).append(" -> ").append(newName).append("\n");
+ }
+ JTextArea textArea = new JTextArea(sb.toString());
+ textArea.setEditable(false);
+ JScrollPane scrollPane = new JScrollPane(textArea);
+ scrollPane.setPreferredSize(new Dimension(500, 400));
+ JOptionPane.showMessageDialog(this, scrollPane, "Preview Deobfuscation Mapping", JOptionPane.INFORMATION_MESSAGE);
+ });
+
+ undoDeobfuscation.addActionListener(e -> {
+ if (lastDeobfuscator[0] == null) {
+ JOptionPane.showMessageDialog(this, "No deobfuscation to undo.", "Undo Deobfuscation", JOptionPane.WARNING_MESSAGE);
+ return;
+ }
+ // Reverse mapping
+ for (java.util.Map.Entry entry : lastDeobfuscator[0].getNameMapping().entrySet()) {
+ the.bytecode.club.bytecodeviewer.api.ASMResourceUtil.renameClassNode(entry.getValue(), entry.getKey());
+ }
+ lastDeobfuscator[0] = null;
+ JOptionPane.showMessageDialog(this, "Deobfuscation undone.", "Undo Deobfuscation", JOptionPane.INFORMATION_MESSAGE);
+ BytecodeViewer.refreshAllTabs();
+ });
+
//allatori is disabled since they are just placeholders
//ZKM and ZStringArray decrypter are disabled until deobfuscation has been extended
//mnNewMenu_1.add(mntmNewMenuItem_2);
diff --git a/src/test/java/the/bytecode/club/bytecodeviewer/deobfuscator/DeobfuscatorTest.java b/src/test/java/the/bytecode/club/bytecodeviewer/deobfuscator/DeobfuscatorTest.java
new file mode 100644
index 000000000..99d56194b
--- /dev/null
+++ b/src/test/java/the/bytecode/club/bytecodeviewer/deobfuscator/DeobfuscatorTest.java
@@ -0,0 +1,41 @@
+package the.bytecode.club.bytecodeviewer.deobfuscator;
+
+import org.junit.Test;
+import org.objectweb.asm.tree.ClassNode;
+import java.util.ArrayList;
+import java.util.List;
+import static org.junit.Assert.*;
+
+public class DeobfuscatorTest {
+ @Test
+ public void testGenerateNewName() {
+ Deobfuscator deobfuscator = new Deobfuscator();
+ List nodes = new ArrayList<>();
+ // Concrete class
+ ClassNode classNode = new ClassNode();
+ classNode.name = "a";
+ classNode.access = 0; // No abstract/interface
+ nodes.add(classNode);
+ // Abstract class
+ ClassNode abstractNode = new ClassNode();
+ abstractNode.name = "b";
+ abstractNode.access = org.objectweb.asm.Opcodes.ACC_ABSTRACT;
+ nodes.add(abstractNode);
+ // Interface
+ ClassNode interfaceNode = new ClassNode();
+ interfaceNode.name = "c";
+ interfaceNode.access = org.objectweb.asm.Opcodes.ACC_INTERFACE;
+ nodes.add(interfaceNode);
+ // Simulate BytecodeViewer.getLoadedClasses()
+ for (ClassNode cn : nodes) {
+ String newName = deobfuscator.generateNewName(cn);
+ if (cn.access == 0) {
+ assertTrue(newName.startsWith("C"));
+ } else if ((cn.access & org.objectweb.asm.Opcodes.ACC_ABSTRACT) != 0) {
+ assertTrue(newName.startsWith("AC"));
+ } else if ((cn.access & org.objectweb.asm.Opcodes.ACC_INTERFACE) != 0) {
+ assertTrue(newName.startsWith("I"));
+ }
+ }
+ }
+}