1
0
Fork 0
x64dbg/src/gui/Src/Gui/LogView.cpp

549 lines
19 KiB
C++

#include "LogView.h"
#include "Configuration.h"
#include "Bridge.h"
#include <QRegularExpression>
#include <QDesktopServices>
#include <QClipboard>
#include <QMimeData>
#include <QTimer>
#include <QApplication>
#include <QContextMenuEvent>
#include "BrowseDialog.h"
#include "MiscUtil.h"
#include "StringUtil.h"
/**
* @brief LogView::LogView The constructor constructs a rich text browser
* @param parent The parent
*/
LogView::LogView(QWidget* parent) : QTextBrowser(parent), logRedirection(NULL)
{
updateStyle();
this->setUndoRedoEnabled(false);
this->setReadOnly(true);
this->setOpenExternalLinks(false);
this->setOpenLinks(false);
this->setLoggingEnabled(true);
autoScroll = true;
flushTimer = new QTimer(this);
flushTimer->setInterval(500);
connect(flushTimer, SIGNAL(timeout()), this, SLOT(flushTimerSlot()));
connect(Bridge::getBridge(), SIGNAL(close()), flushTimer, SLOT(stop()));
connect(Config(), SIGNAL(colorsUpdated()), this, SLOT(updateStyle()));
connect(Config(), SIGNAL(fontsUpdated()), this, SLOT(updateStyle()));
connect(Bridge::getBridge(), SIGNAL(addMsgToLog(QByteArray)), this, SLOT(addMsgToLogSlot(QByteArray)));
connect(Bridge::getBridge(), SIGNAL(addMsgToLogHtml(QByteArray)), this, SLOT(addMsgToLogSlotHtml(QByteArray)));
connect(Bridge::getBridge(), SIGNAL(clearLog()), this, SLOT(clearLogSlot()));
connect(Bridge::getBridge(), SIGNAL(setLogEnabled(bool)), this, SLOT(setLoggingEnabled(bool)));
connect(Bridge::getBridge(), SIGNAL(flushLog()), this, SLOT(flushLogSlot()));
connect(this, SIGNAL(anchorClicked(QUrl)), this, SLOT(onAnchorClicked(QUrl)));
dialogFindInLog = new LineEditDialog(this);
dialogFindInLog->setWindowTitle(tr("Find For"));
duint setting;
if(BridgeSettingGetUint("Misc", "Utf16LogRedirect", &setting))
utf16Redirect = !!setting;
setupContextMenu();
}
/**
* @brief LogView::~LogView The destructor
*/
LogView::~LogView()
{
if(logRedirection != NULL)
fclose(logRedirection);
logRedirection = NULL;
}
void LogView::updateStyle()
{
setFont(ConfigFont("Log"));
setStyleSheet(QString("QTextEdit { color: %1; background-color: %2 }").arg(ConfigColor("AbstractTableViewTextColor").name(), ConfigColor("AbstractTableViewBackgroundColor").name()));
QColor LogLinkBackgroundColor = ConfigColor("LogLinkBackgroundColor");
this->document()->setDefaultStyleSheet(QString("a {color: %1; background-color: %2 }").arg(ConfigColor("LogLinkColor").name(), LogLinkBackgroundColor == Qt::transparent ? "transparent" : LogLinkBackgroundColor.name()));
}
template<class T> static QAction* setupAction(const QIcon & icon, const QString & text, LogView* this_object, T slot)
{
QAction* action = new QAction(icon, text, this_object);
action->setShortcutContext(Qt::WidgetShortcut);
this_object->addAction(action);
this_object->connect(action, SIGNAL(triggered()), this_object, slot);
return action;
}
template<class T> static QAction* setupAction(const QString & text, LogView* this_object, T slot)
{
QAction* action = new QAction(text, this_object);
action->setShortcutContext(Qt::WidgetShortcut);
this_object->addAction(action);
this_object->connect(action, SIGNAL(triggered()), this_object, slot);
return action;
}
void LogView::setupContextMenu()
{
actionClear = setupAction(DIcon("eraser"), tr("Clea&r"), this, SLOT(clearLogSlot()));
actionCopy = setupAction(DIcon("copy"), tr("&Copy"), this, SLOT(copy()));
actionPaste = setupAction(DIcon("binary_paste"), tr("&Paste"), this, SLOT(pasteSlot()));
actionSelectAll = setupAction(DIcon("copy_full_table"), tr("Select &All"), this, SLOT(selectAll()));
actionSave = setupAction(DIcon("binary_save"), tr("&Save"), this, SLOT(saveSlot()));
actionToggleLogging = setupAction(DIcon("lock"), tr("Disable &Logging"), this, SLOT(toggleLoggingSlot()));
actionRedirectLog = setupAction(DIcon("database-export"), tr("&Redirect Log..."), this, SLOT(redirectLogSlot()));
actionAutoScroll = setupAction(tr("Auto Scrolling"), this, SLOT(autoScrollSlot()));
menuCopyToNotes = new QMenu(tr("Copy To Notes"), this);
menuCopyToNotes->setIcon(DIcon("notes"));
actionCopyToGlobalNotes = new QAction(tr("&Global"), menuCopyToNotes);
actionCopyToDebuggeeNotes = new QAction(tr("&Debuggee"), menuCopyToNotes);
connect(actionCopyToGlobalNotes, SIGNAL(triggered()), this, SLOT(copyToGlobalNotes()));
connect(actionCopyToDebuggeeNotes, SIGNAL(triggered()), this, SLOT(copyToDebuggeeNotes()));
menuCopyToNotes->addAction(actionCopyToGlobalNotes);
menuCopyToNotes->addAction(actionCopyToDebuggeeNotes);
actionAutoScroll->setCheckable(true);
actionAutoScroll->setChecked(autoScroll);
actionFindInLog = setupAction(tr("Find"), this, SLOT(findInLogSlot()));
actionFindNext = setupAction(tr("Find Next Occurance"), this, SLOT(findNextInLogSlot()));;
actionFindPrevious = setupAction(tr("Find Previous Occurance"), this, SLOT(findPreviousInLogSlot()));
refreshShortcutsSlot();
connect(Config(), SIGNAL(shortcutsUpdated()), this, SLOT(refreshShortcutsSlot()));
}
void LogView::refreshShortcutsSlot()
{
actionClear->setShortcut(ConfigShortcut("ActionClear"));
actionCopy->setShortcut(ConfigShortcut("ActionCopy"));
actionToggleLogging->setShortcut(ConfigShortcut("ActionToggleLogging"));
actionRedirectLog->setShortcut(ConfigShortcut("ActionRedirectLog"));
actionFindInLog->setShortcut(ConfigShortcut("ActionFind"));
actionFindNext->setShortcut(ConfigShortcut("ActionGotoNext"));
actionFindPrevious->setShortcut(ConfigShortcut("ActionGotoPrevious"));
}
void LogView::contextMenuEvent(QContextMenuEvent* event)
{
QMenu wMenu(this);
wMenu.addAction(actionClear);
wMenu.addAction(actionSelectAll);
wMenu.addAction(actionCopy);
if(QApplication::clipboard()->mimeData()->hasText())
wMenu.addAction(actionPaste);
wMenu.addAction(actionSave);
if(getLoggingEnabled())
actionToggleLogging->setText(tr("Disable &Logging"));
else
actionToggleLogging->setText(tr("Enable &Logging"));
actionCopyToDebuggeeNotes->setEnabled(DbgIsDebugging());
wMenu.addMenu(menuCopyToNotes);
wMenu.addAction(actionToggleLogging);
actionAutoScroll->setChecked(autoScroll);
wMenu.addAction(actionAutoScroll);
wMenu.addAction(actionFindInLog);
wMenu.addAction(actionFindNext);
wMenu.addAction(actionFindPrevious);
if(logRedirection == NULL)
actionRedirectLog->setText(tr("&Redirect Log..."));
else
actionRedirectLog->setText(tr("Stop &Redirection"));
wMenu.addAction(actionRedirectLog);
wMenu.exec(event->globalPos());
}
void LogView::showEvent(QShowEvent* event)
{
flushTimerSlot();
flushTimer->start();
QTextBrowser::showEvent(event);
}
void LogView::hideEvent(QHideEvent* event)
{
flushTimer->stop();
QTextBrowser::hideEvent(event);
}
/**
* @brief linkify Add hyperlink HTML to the message where applicable.
* @param msg The message passed by reference.
* Url format:
* x64dbg:// localhost / address64 # address
* ^fixed ^host(probably will be changed to PID + Host when remote debugging and child debugging are supported) ^token ^parameter
*/
#ifdef _WIN64
static QRegularExpression addressRegExp("([0-9A-Fa-f]{16})");
#else //x86
static QRegularExpression addressRegExp("([0-9A-Fa-f]{8})");
#endif //_WIN64
static void linkify(QString & msg)
{
#ifdef _WIN64
msg.replace(addressRegExp, "<a href=\"x64dbg://localhost/address64#\\1\">\\1</a>");
#else //x86
msg.replace(addressRegExp, "<a href=\"x64dbg://localhost/address32#\\1\">\\1</a>");
#endif //_WIN64
}
/**
* @brief LogView::addMsgToLogSlotHtml Adds a HTML message to the log view. This function is a slot for Bridge::addMsgToLogHtml.
* @param msg The log message (Which is assumed to contain HTML)
*/
void LogView::addMsgToLogSlotHtml(QByteArray msg)
{
LogView::addMsgToLogSlotRaw(msg, false);
}
/**
* @brief LogView::addMsgToLogSlot Adds a message to the log view. This function is a slot for Bridge::addMsgToLog.
* @param msg The log message
*/
void LogView::addMsgToLogSlot(QByteArray msg)
{
LogView::addMsgToLogSlotRaw(msg, true);
}
/**
* @brief LogView::addMsgToLogSlotRaw Adds a message to the log view.
* @param msg The log message
* @param encodeHTML HTML-encode the log message or not
*/
void LogView::addMsgToLogSlotRaw(QByteArray msg, bool encodeHTML)
{
/*
* This supports the 'UTF-8 Everywhere' manifesto.
* - UTF-8 (http://utf8everywhere.org);
* - No BOM (http://utf8everywhere.org/#faq.boms);
* - No carriage return (http://utf8everywhere.org/#faq.crlf).
*/
// fix Unix-style line endings.
// redirect the log
QString msgUtf16;
bool redirectError = false;
if(logRedirection != NULL)
{
if(utf16Redirect)
{
msgUtf16 = QString::fromUtf8(msg);
msgUtf16.replace("\n", "\r\n");
if(!fwrite(msgUtf16.utf16(), msgUtf16.length(), 2, logRedirection))
{
fclose(logRedirection);
logRedirection = NULL;
redirectError = true;
}
}
else
{
const char* data;
std::string temp;
size_t offset = 0;
size_t buffersize = 0;
if(strstr(msg.constData(), "\r\n") != nullptr) // Don't replace "\r\n" to "\n" if there is none
{
temp = msg.constData();
while(true)
{
size_t index = temp.find("\r\n", offset);
if(index == std::string::npos)
break;
temp.erase(index);
offset = index;
}
data = temp.c_str();
buffersize = temp.size();
}
else
{
data = msg.constData();
buffersize = strlen(msg);
}
if(!fwrite(data, buffersize, 1, logRedirection))
{
fclose(logRedirection);
logRedirection = NULL;
redirectError = true;
}
if(loggingEnabled)
msgUtf16 = QString::fromUtf8(data, int(buffersize));
}
}
else msgUtf16 = QString::fromUtf8(msg);
if(!loggingEnabled)
return;
if(encodeHTML)
{
msgUtf16 = msgUtf16.toHtmlEscaped();
/* Below line will break HTML tags with spaces separating the HTML tag name and attributes.
ie <a href="aaaa"> -> <a&nbsp;href="aaaa">
so we don't escape spaces where we deliberately passed in HTML.
*/
msgUtf16.replace(QChar(' '), QString("&nbsp;"));
}
if(logRedirection)
{
if(utf16Redirect)
msgUtf16.replace(QString("\r\n"), QString("<br/>\n"));
else
msgUtf16.replace(QChar('\n'), QString("<br/>\n"));
}
else
{
msgUtf16.replace(QChar('\n'), QString("<br/>\n"));
msgUtf16.replace(QString("\r\n"), QString("<br/>\n"));
}
if(encodeHTML)
{
/* If we passed in non-html log string, we look for address links.
* otherwise, if our HTML contains any address-looking word, ie in our CSS, it would be mangled
* linking to addresses when passing in HTML log message is an exercise left to the plugin developer */
linkify(msgUtf16);
}
if(redirectError)
msgUtf16.append(tr("fwrite() failed (GetLastError()= %1 ). Log redirection stopped.\n").arg(GetLastError()));
if(logBuffer.length() >= MAX_LOG_BUFFER_SIZE)
logBuffer.clear();
logBuffer.append(msgUtf16);
if(flushLog)
{
flushTimerSlot();
flushLog = false;
}
}
/**
* @brief LogView::onAnchorClicked Called when a hyperlink is clicked
* @param link The clicked link
*/
void LogView::onAnchorClicked(const QUrl & link)
{
if(link.scheme() == "x64dbg")
{
if(link.path() == "/address32" || link.path() == "/address64")
{
if(DbgIsDebugging())
{
bool ok = false;
auto address = duint(link.fragment(QUrl::DecodeReserved).toULongLong(&ok, 16));
if(ok && DbgMemIsValidReadPtr(address))
{
if(DbgFunctions()->MemIsCodePage(address, true))
DbgCmdExec(QString("disasm %1").arg(link.fragment()));
else
{
DbgCmdExecDirect(QString("dump %1").arg(link.fragment()));
emit Bridge::getBridge()->getDumpAttention();
}
}
else
SimpleErrorBox(this, tr("Invalid address!"), tr("The address %1 is not a valid memory location...").arg(ToPtrString(address)));
}
}
else
SimpleErrorBox(this, tr("Url is not valid!"), tr("The Url %1 is not supported").arg(link.toString()));
}
else
QDesktopServices::openUrl(link); // external Url
}
void LogView::clearLogSlot()
{
this->clear();
}
void LogView::redirectLogSlot()
{
if(logRedirection != NULL)
{
fclose(logRedirection);
logRedirection = NULL;
}
else
{
BrowseDialog browse(this, tr("Redirect log to file"), tr("Enter the file to which you want to redirect log messages."), tr("Log files (*.txt);;All files (*.*)"), QCoreApplication::applicationDirPath(), true);
if(browse.exec() == QDialog::Accepted)
{
logRedirection = _wfopen(browse.path.toStdWString().c_str(), L"ab");
if(logRedirection == NULL)
GuiAddLogMessage(tr("_wfopen() failed. Log will not be redirected to %1.\n").arg(browse.path).toUtf8().constData());
else
{
if(utf16Redirect && ftell(logRedirection) == 0)
{
unsigned short BOM = 0xfeff;
fwrite(&BOM, 2, 1, logRedirection);
}
GuiAddLogMessage(tr("Log will be redirected to %1.\n").arg(browse.path).toUtf8().constData());
}
}
}
}
void LogView::setLoggingEnabled(bool enabled)
{
if(enabled)
{
loggingEnabled = true;
GuiAddStatusBarMessage(tr("Logging will be enabled.\n").toUtf8().constData());
}
else
{
GuiAddStatusBarMessage(tr("Logging will be disabled.\n").toUtf8().constData());
loggingEnabled = false;
}
}
bool LogView::getLoggingEnabled()
{
return loggingEnabled;
}
void LogView::autoScrollSlot()
{
autoScroll = !autoScroll;
}
/**
* @brief LogView::saveSlot Called by "save" action
*/
void LogView::saveSlot()
{
QString fileName;
fileName = QString("log-%1.txt").arg(QDateTime::currentDateTime().toString().replace(QChar(':'), QChar('-')));
QFile savedLog(fileName);
savedLog.open(QIODevice::Append | QIODevice::Text);
if(savedLog.error() != QFile::NoError)
{
GuiAddLogMessage(tr("Error, log have not been saved.\n").toUtf8().constData());
}
else
{
savedLog.write(this->document()->toPlainText().toUtf8().constData());
savedLog.close();
GuiAddLogMessage(tr("Log have been saved as %1\n").arg(fileName).toUtf8().constData());
}
}
void LogView::toggleLoggingSlot()
{
setLoggingEnabled(!getLoggingEnabled());
}
void LogView::copyToGlobalNotes()
{
char* NotesBuffer;
emit Bridge::getBridge()->getGlobalNotes(&NotesBuffer);
QString Notes = QString::fromUtf8(NotesBuffer);
BridgeFree(NotesBuffer);
Notes.append(this->textCursor().selectedText());
emit Bridge::getBridge()->setGlobalNotes(Notes);
}
void LogView::copyToDebuggeeNotes()
{
char* NotesBuffer;
emit Bridge::getBridge()->getDebuggeeNotes(&NotesBuffer);
QString Notes = QString::fromUtf8(NotesBuffer);
BridgeFree(NotesBuffer);
Notes.append(this->textCursor().selectedText());
emit Bridge::getBridge()->setDebuggeeNotes(Notes);
}
void LogView::findNextInLogSlot()
{
find(QRegExp(lastFindText));
}
void LogView::findPreviousInLogSlot()
{
find(QRegExp(lastFindText), QTextDocument::FindBackward);
}
void LogView::findInLogSlot()
{
dialogFindInLog->show();
if(dialogFindInLog->exec() == QDialog::Accepted)
{
QList<QTextEdit::ExtraSelection> extraSelections;
QColor highlight = ConfigColor("SearchListViewHighlightColor");
QColor background = ConfigColor("SearchListViewHighlightBackgroundColor");
// capturing the current location, so that we can reset it once capturing all the
// extra selections
QTextCursor resetCursorLoc = textCursor();
moveCursor(QTextCursor::Start);
// finding all occurances matching the regex given from start
while(find(QRegExp(dialogFindInLog->editText)))
{
QTextEdit::ExtraSelection extra;
extra.format.setForeground(highlight);
extra.format.setBackground(background);
extra.cursor = textCursor();
extraSelections.append(extra);
}
// highlighting all those selections
setExtraSelections(extraSelections);
setTextCursor(resetCursorLoc); // resetting the cursor location
find(QRegExp(dialogFindInLog->editText));
lastFindText = dialogFindInLog->editText;
}
}
void LogView::pasteSlot()
{
QString clipboardText = QApplication::clipboard()->text();
if(clipboardText.isEmpty())
return;
if(!clipboardText.endsWith('\n'))
clipboardText.append('\n');
addMsgToLogSlot(clipboardText.toUtf8());
}
void LogView::flushTimerSlot()
{
if(logBuffer.isEmpty())
return;
setUpdatesEnabled(false);
static unsigned char counter = 100;
counter--;
if(counter == 0)
{
if(document()->characterCount() > MAX_LOG_BUFFER_SIZE)
clear();
counter = 100;
}
QTextCursor cursor(document());
cursor.movePosition(QTextCursor::End);
cursor.beginEditBlock();
cursor.insertBlock();
// hack to not insert too many newlines: https://lists.qt-project.org/pipermail/qt-interest-old/2011-July/034725.html
cursor.deletePreviousChar();
cursor.insertHtml(logBuffer);
cursor.endEditBlock();
if(autoScroll)
moveCursor(QTextCursor::End);
setUpdatesEnabled(true);
logBuffer.clear();
}
void LogView::flushLogSlot()
{
flushLog = true;
if(flushTimer->isActive())
flushTimerSlot();
}