Faster AS3 Debugging - export/import ByteArray variable data

This commit is contained in:
Jindra Petřík
2023-11-19 23:30:08 +01:00
parent 6a5de14c3c
commit 9f883d42e6
14 changed files with 446 additions and 82 deletions

View File

@@ -16,6 +16,7 @@
*/
package com.jpexs.decompiler.flash.gui;
import com.jpexs.decompiler.flash.gui.debugger.DebugAdapter;
import com.jpexs.decompiler.flash.gui.debugger.DebugListener;
import com.jpexs.decompiler.flash.gui.debugger.Debugger;
import java.awt.BorderLayout;
@@ -54,18 +55,13 @@ public class DebugLogDialog extends AppDialog {
JScrollPane spane = new FasterScrollPane(logTextArea);
spane.setPreferredSize(new Dimension(800, 500));
debug.addMessageListener(new DebugListener() {
debug.addMessageListener(new DebugAdapter() {
@Override
public void onMessage(String clientId, String msg) {
log(translate("msg.header").replace("%clientid%", clientId) + msg);
}
@Override
public void onFinish(String clientId) {
}
@Override
public void onLoaderURL(String clientId, String url) {
log(translate("msg.header").replace("%clientid%", clientId) + " LOADURL:" + url);
@@ -75,6 +71,11 @@ public class DebugLogDialog extends AppDialog {
public void onLoaderBytes(String clientId, byte[] data) {
log(translate("msg.header").replace("%clientid%", clientId) + " LOADBYTES: " + data.length + "B");
}
@Override
public void onDumpByteArray(String clientId, byte[] data) {
log(translate("msg.header").replace("%clientid%", clientId) + " DUMPBYTEARRAY: " + data.length + "B");
}
});
Container cnt = getContentPane();
cnt.setLayout(new BorderLayout());

View File

@@ -20,9 +20,14 @@ import com.jpexs.debugger.flash.Variable;
import com.jpexs.debugger.flash.messages.in.InBreakAtExt;
import com.jpexs.debugger.flash.messages.in.InConstantPool;
import com.jpexs.debugger.flash.messages.in.InFrame;
import com.jpexs.debugger.flash.messages.in.InGetVariable;
import com.jpexs.decompiler.flash.configuration.Configuration;
import com.jpexs.decompiler.flash.gui.DebuggerHandler.BreakListener;
import com.jpexs.decompiler.flash.gui.abc.ABCPanel;
import com.jpexs.decompiler.flash.gui.debugger.DebugAdapter;
import com.jpexs.decompiler.flash.gui.debugger.DebugListener;
import com.jpexs.decompiler.flash.gui.debugger.Debugger;
import com.jpexs.decompiler.flash.gui.debugger.DebuggerTools;
import com.jpexs.helpers.Helper;
import de.hameister.treetable.MyTreeTable;
import de.hameister.treetable.MyTreeTableModel;
@@ -38,6 +43,7 @@ import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -201,19 +207,66 @@ public class DebugPanel extends JPanel {
if (isByteArray) {
JMenu exportMenu = new JMenu(AppStrings.translate("debug.export").replace("%name%", v.name));
JMenuItem exportByteArrayMenuItem = new JMenuItem(AppStrings.translate("debug.export.bytearray"));
exportByteArrayMenuItem.addActionListener((ActionEvent e1) -> {
exportByteArrayMenuItem.addActionListener((ActionEvent e1) -> {
JFileChooser fc = new JFileChooser();
fc.setCurrentDirectory(new File(Configuration.lastExportDir.get()));
if (fc.showSaveDialog(Main.getDefaultMessagesComponent()) == JFileChooser.APPROVE_OPTION) {
File file = Helper.fixDialogFile(fc.getSelectedFile());
//Variant with direct calling readByte - SLOW
/*
try (FileOutputStream fos = new FileOutputStream(file)) {
Main.debugExportByteArray(v, fos);
fos.write(data);
Configuration.lastExportDir.set(file.getParentFile().getAbsolutePath());
} catch (IOException ex) {
Logger.getLogger(DebugPanel.class.getName()).log(Level.SEVERE, null, ex);
ViewMessages.showMessageDialog(Main.getDefaultMessagesComponent(), AppStrings.translate("error.file.save") + ": " + ex.getLocalizedMessage(), AppStrings.translate("error"), JOptionPane.ERROR_MESSAGE);
ViewMessages.showMessageDialog(Main.getDefaultMessagesComponent(), AppStrings.translate("error.file.save") + ": " + ex.getLocalizedMessage(), AppStrings.translate("error"), JOptionPane.ERROR_MESSAGE);
}
}
*/
//Asynchronous variant
/*DebuggerTools.initDebugger().addMessageListener(new DebugAdapter() {
@Override
public void onDumpByteArray(String clientId, byte[] data) {
try (FileOutputStream fos = new FileOutputStream(file)) {
fos.write(data);
Configuration.lastExportDir.set(file.getParentFile().getAbsolutePath());
} catch (IOException ex) {
Logger.getLogger(DebugPanel.class.getName()).log(Level.SEVERE, null, ex);
ViewMessages.showMessageDialog(Main.getDefaultMessagesComponent(), AppStrings.translate("error.file.save") + ": " + ex.getLocalizedMessage(), AppStrings.translate("error"), JOptionPane.ERROR_MESSAGE);
}
DebuggerTools.initDebugger().removeMessageListener(this);
}
});
Variable debugConnectionClass = Main.getDebugHandler().getVariable(0, Main.currentDebuggerPackage + "::DebugConnection", false, false).parent;
try {
Main.getDebugHandler().callMethod(debugConnectionClass, "writeMsg", Arrays.asList(v, (Double) (double) Debugger.MSG_DUMP_BYTEARRAY));
} catch (DebuggerHandler.ActionScriptException ex) {
Logger.getLogger(DebugPanel.class.getName()).log(Level.SEVERE, "Error exporting ByteArray", ex);
}*/
//Variant using comma separated bytes, pretty fast
try {
Variable debugConnectionClass = Main.getDebugHandler().getVariable(0, Main.currentDebuggerPackage + "::DebugConnection", false, false).parent;
String dataStr = (String) Main.getDebugHandler().callMethod(debugConnectionClass, "readCommaSeparatedFromByteArray", Arrays.asList(v)).variables.get(0).value;
String[] parts = dataStr.split(",");
byte[] data = new byte[parts.length];
for (int i = 0; i < parts.length; i++) {
data[i] = (byte) Integer.parseInt(parts[i]);
}
try (FileOutputStream fos = new FileOutputStream(file)) {
fos.write(data);
Configuration.lastExportDir.set(file.getParentFile().getAbsolutePath());
} catch (IOException ex) {
Logger.getLogger(DebugPanel.class.getName()).log(Level.SEVERE, null, ex);
ViewMessages.showMessageDialog(Main.getDefaultMessagesComponent(), AppStrings.translate("error.file.save") + ": " + ex.getLocalizedMessage(), AppStrings.translate("error"), JOptionPane.ERROR_MESSAGE);
}
} catch (DebuggerHandler.ActionScriptException ex) {
Logger.getLogger(DebugPanel.class.getName()).log(Level.SEVERE, "Error exporting ByteArray", ex);
}
}
});
exportMenu.add(exportByteArrayMenuItem);
@@ -227,11 +280,57 @@ public class DebugPanel extends JPanel {
if (fc.showOpenDialog(Main.getDefaultMessagesComponent()) == JFileChooser.APPROVE_OPTION) {
File file = Helper.fixDialogFile(fc.getSelectedFile());
Configuration.lastOpenDir.set(file.getParentFile().getAbsolutePath());
try (FileInputStream fis = new FileInputStream(file)) {
//Variant with asynchronous connection
/*DebuggerTools.initDebugger().addMessageListener(new DebugAdapter() {
@Override
public byte[] onRequestBytes(String clientId) {
byte[] data = null;
try {
data = Helper.readFileEx(file.getAbsolutePath());
} catch (IOException ex) {
Logger.getLogger(DebugPanel.class.getName()).log(Level.SEVERE, null, ex);
ViewMessages.showMessageDialog(Main.getDefaultMessagesComponent(), AppStrings.translate("error.file.save") + ": " + ex.getLocalizedMessage(), AppStrings.translate("error"), JOptionPane.ERROR_MESSAGE);
}
DebuggerTools.initDebugger().removeMessageListener(this);
return data;
}
});
Variable debugConnectionClass = Main.getDebugHandler().getVariable(0, Main.currentDebuggerPackage + "::DebugConnection", false, false).parent;
try {
Main.getDebugHandler().callMethod(debugConnectionClass, "writeMsg", Arrays.asList(v, (Double) (double) Debugger.MSG_REQUEST_BYTEARRAY));
} catch (DebuggerHandler.ActionScriptException ex) {
Logger.getLogger(DebugPanel.class.getName()).log(Level.SEVERE, "Error exporting ByteArray", ex);
}*/
//Variant with direct writeByte calls - SLOW
/*try (FileInputStream fis = new FileInputStream(file)) {
Main.debugImportByteArray(v, fis);
} catch (IOException ex) {
Logger.getLogger(DebugPanel.class.getName()).log(Level.SEVERE, null, ex);
ViewMessages.showMessageDialog(Main.getDefaultMessagesComponent(), AppStrings.translate("error.file.save") + ": " + ex.getLocalizedMessage(), AppStrings.translate("error"), JOptionPane.ERROR_MESSAGE);
}*/
//Variant with using comma separated bytes, pretty fast
try {
byte[] data = Helper.readFileEx(file.getAbsolutePath());
String splitter = "";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < data.length; i++) {
sb.append(splitter);
sb.append(data[i] & 0xff);
splitter = ",";
}
String dataStr = sb.toString();
Variable debugConnectionClass = Main.getDebugHandler().getVariable(0, Main.currentDebuggerPackage + "::DebugConnection", false, false).parent;
try {
Main.getDebugHandler().callMethod(debugConnectionClass, "writeCommaSeparatedToByteArray", Arrays.asList(dataStr, v));
} catch (DebuggerHandler.ActionScriptException ex) {
Logger.getLogger(DebugPanel.class.getName()).log(Level.SEVERE, "Error exporting ByteArray", ex);
}
} catch (IOException ex) {
Logger.getLogger(DebugPanel.class.getName()).log(Level.SEVERE, null, ex);
ViewMessages.showMessageDialog(Main.getDefaultMessagesComponent(), AppStrings.translate("error.file.save") + ": " + ex.getLocalizedMessage(), AppStrings.translate("error"), JOptionPane.ERROR_MESSAGE);
}
}
});
@@ -264,8 +363,8 @@ public class DebugPanel extends JPanel {
addWatchMenu.add(watchReadMenuItem);
addWatchMenu.add(watchWriteMenuItem);
addWatchMenu.add(watchReadWriteMenuItem);
pm.add(addWatchMenu);
pm.show(e.getComponent(), e.getX(), e.getY());
pm.add(addWatchMenu);
pm.show(e.getComponent(), e.getX(), e.getY());
}
};

View File

@@ -46,6 +46,7 @@ import com.jpexs.decompiler.flash.console.CommandLineArgumentParser;
import com.jpexs.decompiler.flash.console.ContextMenuTools;
import com.jpexs.decompiler.flash.exporters.modes.ExeExportMode;
import com.jpexs.decompiler.flash.gfx.GfxConvertor;
import com.jpexs.decompiler.flash.gui.debugger.DebugAdapter;
import com.jpexs.decompiler.flash.gui.debugger.DebugListener;
import com.jpexs.decompiler.flash.gui.debugger.DebuggerTools;
import com.jpexs.decompiler.flash.gui.pipes.FirstInstance;
@@ -217,6 +218,8 @@ public class Main {
public static CancellableWorker importWorker = null;
public static CancellableWorker deobfuscatePCodeWorker = null;
public static CancellableWorker swfPrepareWorker = null;
public static String currentDebuggerPackage = null;
public static boolean isSwfAir(Openable openable) {
SwfSpecificCustomConfiguration conf = Configuration.getSwfSpecificCustomConfiguration(openable.getShortPathTitle());
@@ -524,7 +527,13 @@ public class Main {
}
instrSWF.removeEventListener(prepEventListner);
instrSWF = super.prepare(instrSWF);
//instrSWF = super.prepare(instrSWF);
if (!DebuggerTools.hasDebugger(instrSWF)) {
DebuggerTools.switchDebugger(instrSWF);
}
DebuggerTools.injectDebugLoader(instrSWF);
currentDebuggerPackage = instrSWF.debuggerPackage;
return instrSWF;
}
@@ -2399,15 +2408,8 @@ public class Main {
watcherWorker.execute();
}
DebuggerTools.initDebugger().addMessageListener(new DebugListener() {
@Override
public void onMessage(String clientId, String msg) {
}
@Override
public void onLoaderURL(String clientId, String url) {
}
DebuggerTools.initDebugger().addMessageListener(new DebugAdapter() {
@Override
public void onLoaderBytes(String clientId, byte[] data) {
String hash = md5(data);
@@ -2438,11 +2440,7 @@ public class Main {
} catch (IOException ex) {
logger.log(Level.SEVERE, "Cannot create tempfile");
}
}
@Override
public void onFinish(String clientId) {
}
}
});
try {

View File

@@ -0,0 +1,50 @@
/*
* Copyright (C) 2010-2023 JPEXS
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.jpexs.decompiler.flash.gui.debugger;
/**
*
* @author JPEXS
*/
public class DebugAdapter implements DebugListener {
@Override
public void onMessage(String clientId, String msg) {
}
@Override
public void onLoaderURL(String clientId, String url) {
}
@Override
public void onLoaderBytes(String clientId, byte[] data) {
}
@Override
public void onDumpByteArray(String clientId, byte[] data) {
}
@Override
public void onFinish(String clientId) {
}
@Override
public byte[] onRequestBytes(String clientId) {
return null;
}
}

View File

@@ -27,6 +27,10 @@ public interface DebugListener {
public void onLoaderURL(String clientId, String url);
public void onLoaderBytes(String clientId, byte[] data);
public void onDumpByteArray(String clientId, byte[] data);
public void onFinish(String clientId);
public byte[] onRequestBytes(String clientId);
}

View File

@@ -30,6 +30,8 @@ import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
*
@@ -37,8 +39,29 @@ import java.util.WeakHashMap;
*/
public class Debugger {
public static final int MSG_STRING = 0;
public static final int MSG_LOADER_URL = 1;
public static final int MSG_LOADER_BYTES = 2;
public static final int MSG_DUMP_BYTEARRAY = 3;
public static final int MSG_REQUEST_BYTEARRAY = 4;
private static final Set<DebugListener> listeners = new HashSet<>();
private static Logger logger = Logger.getLogger(Debugger.class.getName());
private static boolean active = false;
public static boolean isActive() {
return active;
}
public synchronized void addMessageListener(DebugListener l) {
listeners.add(l);
}
@@ -61,12 +84,7 @@ public class Debugger {
private final Map<String, String> parameters = new HashMap<>();
public static final int MSG_STRING = 0;
public static final int MSG_LOADER_URL = 1;
public static final int MSG_LOADER_BYTES = 2;
public String getParameter(String name, String defValue) {
if (parameters.containsKey(name)) {
return parameters.get(name);
@@ -108,6 +126,14 @@ public class Debugger {
return type;
}
private void writeBytes(OutputStream os, byte[] data) throws IOException {
os.write((data.length >> 24) & 0xff);
os.write((data.length >> 16) & 0xff);
os.write((data.length >> 8) & 0xff);
os.write(data.length & 0xff);
os.write(data);
}
private byte[] readBytes(InputStream is) throws IOException {
int len = is.read();
if (len == -1) {
@@ -161,7 +187,8 @@ public class Debugger {
@Override
public void run() {
String clientName = Integer.toString(id);
try (InputStream is = s.getInputStream()) {
try (InputStream is = s.getInputStream();
OutputStream os = s.getOutputStream()) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int c;
@@ -175,11 +202,10 @@ public class Debugger {
String ret = baos.toString("UTF-8");
if (ret.equals("<policy-file-request/>")) {
try (OutputStream os = s.getOutputStream()) {
os.write(("<cross-domain-policy><allow-access-from domain=\"*\" to-ports=\"" + serverPort + "\" /></cross-domain-policy>").getBytes("UTF-8"));
}
os.write(("<cross-domain-policy><allow-access-from domain=\"*\" to-ports=\"*\" secure=\"false\" /></cross-domain-policy>").getBytes("UTF-8"));
} else {
if (!ret.isEmpty()) {
logger.log(Level.FINER, "Connected. Header: {0}", ret);
String[] param = (ret.contains(";") ? ret.split(";") : new String[]{ret});
for (String p : param) {
if (p.contains("=")) {
@@ -190,35 +216,79 @@ public class Debugger {
parameters.put(p, "true");
}
}
active = true;
}
boolean hasType = hasMsgType();
logger.log(Level.FINER, "reading name...");
String name = readString(is);
logger.log(Level.FINER, "name = {0}", name);
if (!name.isEmpty()) {
clientName = name;
}
while (true) {
int type = 0;
logger.finer("reading type...");
if (hasType) {
type = readType(is);
}
logger.log(Level.FINE, "received type {0}", type);
switch (type) {
case MSG_STRING:
logger.finer("reading string...");
ret = readString(is);
logger.finer("informing listeners...");
for (DebugListener l : listeners) {
l.onMessage(clientName, ret);
}
logger.finer("listeners informed");
break;
case MSG_LOADER_URL:
logger.finer("reading string...");
ret = readString(is);
logger.finer("informing listeners...");
for (DebugListener l : listeners) {
l.onLoaderURL(clientName, ret);
}
logger.finer("listeners informed");
break;
case MSG_LOADER_BYTES:
logger.finer("reading bytes...");
byte[] retB = readBytes(is);
logger.finer("informing listeners...");
for (DebugListener l : listeners) {
l.onLoaderBytes(clientName, retB);
}
logger.finer("listeners informed");
break;
case MSG_DUMP_BYTEARRAY:
logger.finer("reading bytes...");
byte[] retBa = readBytes(is);
logger.finer("informing listeners...");
for (DebugListener l : listeners) {
l.onDumpByteArray(clientName, retBa);
}
logger.finer("listeners informed");
break;
case MSG_REQUEST_BYTEARRAY:
logger.finer("checking listeners for data...");
boolean dataFound = false;
for (DebugListener l : listeners) {
byte[] data = l.onRequestBytes(clientName);
if (data != null) {
logger.finer("found listener with data");
logger.log(Level.FINER, "writing data.length = {0}", data.length);
writeBytes(os, data);
logger.finer("data written");
dataFound = true;
break;
}
}
if (!dataFound) {
logger.finer("listener not found, writing empty array");
writeBytes(os, new byte[0]);
}
os.flush();
logger.finer("listeners checked");
break;
}
}
@@ -233,6 +303,7 @@ public class Debugger {
//ignore
}
finished = true;
active = false;
for (DebugListener l : listeners) {
l.onFinish(clientName);
}

View File

@@ -46,8 +46,8 @@ public class DebuggerTools {
public static final String DEBUGGER_PACKAGE = "com.jpexs.decompiler.flash.debugger";
private static volatile Debugger debugger;
private static volatile Debugger debugger;
private static ScriptPack getDebuggerScriptPack(SWF swf) {
List<ABC> allAbcList = new ArrayList<>();
for (ABCContainerTag ac : swf.getAbcList()) {
@@ -234,6 +234,7 @@ public class DebuggerTools {
if (Configuration.randomDebuggerPackage.get()) {
newdebuggerpkg += ".pkg" + rhex;
}
swf.debuggerPackage = newdebuggerpkg;
//add debug ABC tags to main SWF
for (ABCContainerTag ds : debugSWF.getAbcList()) {
@@ -261,6 +262,46 @@ public class DebuggerTools {
ft.useNetwork = true;
ft.setModified(true);
}
//Add call to DebugConnection.initClient("") to the document class
/*String documentClass = swf.getDocumentClass();
if (documentClass != null) {
List<String> searchClassNames = new ArrayList<>();
searchClassNames.add(documentClass);
List<ScriptPack> documentClassPacks = swf.getScriptPacksByClassNames(searchClassNames);
if (!documentClassPacks.isEmpty()) {
ScriptPack documentClassPack = documentClassPacks.get(0);
Trait publicTrait = documentClassPack.getPublicTrait();
if (publicTrait != null) {
if (publicTrait instanceof TraitClass) {
TraitClass classTrait = (TraitClass) publicTrait;
int classIndex = classTrait.class_info;
ABC a = documentClassPack.abc;
int cinitMethodInfo = a.class_info.get(classIndex).cinit_index;
MethodBody body = a.findBody(cinitMethodInfo);
AVM2Code code = body.getCode();
int debugConnectionMultiname = a.constants.getMultinameId(
Multiname.createQName(false, a.constants.getStringId("DebugConnection", true),
a.constants.getNamespaceId(Namespace.KIND_PACKAGE, newdebuggerpkg, 0, true)
), true);
int initClientMultiname = a.constants.getMultinameId(
Multiname.createQName(false, a.constants.getStringId("initClient", true),
a.constants.getNamespaceId(Namespace.KIND_PACKAGE, "", 0, true)
), true);
code.insertInstruction(0, new AVM2Instruction(0, AVM2Instructions.GetLex, new int[] {debugConnectionMultiname}), true, body);
code.insertInstruction(1, new AVM2Instruction(0, AVM2Instructions.PushString, new int[] {a.constants.getStringId("", true)}), true, body);
code.insertInstruction(2, new AVM2Instruction(0, AVM2Instructions.CallPropVoid, new int[] {initClientMultiname, 1}), true, body);
if (body.max_stack < 2) {
body.max_stack = 2;
}
body.setModified();
}
}
}
}
*/
} catch (Exception ex) {
logger.log(Level.SEVERE, "Error while attaching debugger", ex);