From 449f318658f4dcd9247e077a98561f9ae7442249 Mon Sep 17 00:00:00 2001 From: Xusheng Date: Fri, 27 Mar 2026 15:23:02 +0800 Subject: [PATCH] Add support for pulling TTD strings from esreven adapter --- api/debuggerapi.h | 14 + api/debuggercontroller.cpp | 32 ++ api/ffi.h | 16 + api/python/debuggercontroller.py | 70 ++++ core/adapters/esrevenadapter.cpp | 134 +++++++ core/adapters/esrevenadapter.h | 1 + core/debugadapter.cpp | 6 + core/debugadapter.h | 1 + core/debuggercommon.h | 14 + core/debuggercontroller.cpp | 12 + core/debuggercontroller.h | 1 + core/ffi.cpp | 49 +++ ui/ttdstringswidget.cpp | 610 +++++++++++++++++++++++++++++++ ui/ttdstringswidget.h | 201 ++++++++++ ui/ui.cpp | 2 + 15 files changed, 1163 insertions(+) create mode 100644 ui/ttdstringswidget.cpp create mode 100644 ui/ttdstringswidget.h diff --git a/api/debuggerapi.h b/api/debuggerapi.h index 6fbaf67b..c6e2f2e5 100644 --- a/api/debuggerapi.h +++ b/api/debuggerapi.h @@ -638,6 +638,19 @@ namespace BinaryNinjaDebuggerAPI { : position(pos), viewAddress(addr), note(n) {} }; + struct TTDStringEntry + { + uint64_t id; + std::string data; + uint64_t address; + uint64_t size; + TTDPosition firstAccess; + TTDPosition lastAccess; + std::string encoding; + + TTDStringEntry() : id(0), address(0), size(0) {} + }; + typedef BNDebugAdapterConnectionStatus DebugAdapterConnectionStatus; typedef BNDebugAdapterTargetStatus DebugAdapterTargetStatus; @@ -842,6 +855,7 @@ namespace BinaryNinjaDebuggerAPI { bool SetTTDPosition(const TTDPosition& position); std::pair GetTTDNextMemoryAccess(uint64_t address, uint64_t size, TTDMemoryAccessType accessType); std::pair GetTTDPrevMemoryAccess(uint64_t address, uint64_t size, TTDMemoryAccessType accessType); + std::vector GetTTDStrings(const std::string& pattern = "", uint64_t maxResults = 0); // TTD Position History Navigation bool TTDNavigateBack(); diff --git a/api/debuggercontroller.cpp b/api/debuggercontroller.cpp index 08415362..53095c2a 100644 --- a/api/debuggercontroller.cpp +++ b/api/debuggercontroller.cpp @@ -1461,6 +1461,38 @@ std::vector DebuggerController::GetAllTTDEvents() } +std::vector DebuggerController::GetTTDStrings(const std::string& pattern, uint64_t maxResults) +{ + std::vector result; + + size_t count = 0; + BNDebuggerTTDStringEntry* entries = BNDebuggerGetTTDStrings(m_object, pattern.c_str(), maxResults, &count); + + if (entries && count > 0) + { + result.reserve(count); + for (size_t i = 0; i < count; i++) + { + TTDStringEntry entry; + entry.id = entries[i].id; + entry.data = entries[i].data ? std::string(entries[i].data) : ""; + entry.address = entries[i].address; + entry.size = entries[i].size; + entry.firstAccess.sequence = entries[i].firstAccess.sequence; + entry.firstAccess.step = entries[i].firstAccess.step; + entry.lastAccess.sequence = entries[i].lastAccess.sequence; + entry.lastAccess.step = entries[i].lastAccess.step; + entry.encoding = entries[i].encoding ? std::string(entries[i].encoding) : ""; + + result.push_back(entry); + } + BNDebuggerFreeTTDStrings(entries, count); + } + + return result; +} + + std::vector DebuggerController::GetTTDBookmarks() { std::vector result; diff --git a/api/ffi.h b/api/ffi.h index 783d98eb..e4a777c4 100644 --- a/api/ffi.h +++ b/api/ffi.h @@ -703,6 +703,22 @@ extern "C" DEBUGGER_FFI_API void BNDebuggerFreeTTDCallEvents(BNDebuggerTTDCallEvent* events, size_t count); DEBUGGER_FFI_API void BNDebuggerFreeTTDEvents(BNDebuggerTTDEvent* events, size_t count); + // TTD String Entry structures and functions + typedef struct BNDebuggerTTDStringEntry + { + uint64_t id; + char* data; + uint64_t address; + uint64_t size; + BNDebuggerTTDPosition firstAccess; + BNDebuggerTTDPosition lastAccess; + char* encoding; + } BNDebuggerTTDStringEntry; + + DEBUGGER_FFI_API BNDebuggerTTDStringEntry* BNDebuggerGetTTDStrings( + BNDebuggerController* controller, const char* pattern, uint64_t maxResults, size_t* count); + DEBUGGER_FFI_API void BNDebuggerFreeTTDStrings(BNDebuggerTTDStringEntry* entries, size_t count); + // TTD Bookmark structures and functions typedef struct BNDebuggerTTDBookmark { diff --git a/api/python/debuggercontroller.py b/api/python/debuggercontroller.py index 8e50df53..e1bf3798 100644 --- a/api/python/debuggercontroller.py +++ b/api/python/debuggercontroller.py @@ -1192,6 +1192,35 @@ def __repr__(self): return f"" +class TTDStringEntry: + """ + TTDStringEntry represents a string found in a TTD trace. + + Attributes: + id (int): unique identifier for the string + data (str): the string content + address (int): linear address where the string begins + size (int): size in bytes + first_access (TTDPosition): position of the first access in the trace + last_access (TTDPosition): position of the last access in the trace + encoding (str): string encoding ("utf8" or "utf16") + """ + + def __init__(self, id: int, data: str, address: int, size: int, + first_access: 'TTDPosition', last_access: 'TTDPosition', encoding: str): + self.id = id + self.data = data + self.address = address + self.size = size + self.first_access = first_access + self.last_access = last_access + self.encoding = encoding + + def __repr__(self): + preview = self.data[:40] + "..." if len(self.data) > 40 else self.data + return f"" + + class DebuggerController: """ The ``DebuggerController`` object is the core of the debugger. Most debugger operations can be performed on it. @@ -3122,6 +3151,47 @@ def get_all_ttd_events(self) -> List[TTDEvent]: dbgcore.BNDebuggerFreeTTDEvents(events, count.value) return result + def get_ttd_strings(self, pattern: str = "", max_results: int = 0) -> List[TTDStringEntry]: + """ + Get strings found in the TTD trace, optionally filtered by a pattern. + + This method is only available when debugging with TTD (Time Travel Debugging). + Use the is_ttd property to check if TTD is available before calling this method. + + :param pattern: substring pattern to search for (empty string for all strings) + :param max_results: maximum number of results to return + :return: list of TTDStringEntry objects + :rtype: List[TTDStringEntry] + """ + if self.handle is None: + return [] + + count = ctypes.c_ulonglong() + entries = dbgcore.BNDebuggerGetTTDStrings(self.handle, pattern, max_results, ctypes.byref(count)) + + result = [] + if not entries or count.value == 0: + return result + + for i in range(count.value): + entry = entries[i] + first_access = TTDPosition(entry.firstAccess.sequence, entry.firstAccess.step) + last_access = TTDPosition(entry.lastAccess.sequence, entry.lastAccess.step) + + string_entry = TTDStringEntry( + id=entry.id, + data=entry.data, + address=entry.address, + size=entry.size, + first_access=first_access, + last_access=last_access, + encoding=entry.encoding + ) + result.append(string_entry) + + dbgcore.BNDebuggerFreeTTDStrings(entries, count.value) + return result + def run_code_coverage_analysis(self, start_address: int, end_address: int, start_time: TTDPosition = None, end_time: TTDPosition = None) -> bool: """ diff --git a/core/adapters/esrevenadapter.cpp b/core/adapters/esrevenadapter.cpp index c55084e8..c0a4f4a6 100644 --- a/core/adapters/esrevenadapter.cpp +++ b/core/adapters/esrevenadapter.cpp @@ -3102,6 +3102,140 @@ std::vector EsrevenAdapter::GetTTDCallsForSymbols(const std::strin return events; } +std::vector EsrevenAdapter::GetTTDStrings(const std::string& pattern, uint64_t maxResults) +{ + if (m_isTargetRunning) + return {}; + + if (!m_rspConnector) + return {}; + + // Build the RSP packet: rvn:get-strings[:][:] + // maxResults=0 means no limit + std::string packet = "rvn:get-strings"; + if (!pattern.empty() || maxResults != 0) + { + packet += ":" + pattern; + if (maxResults != 0) + packet += ":" + std::to_string(maxResults); + } + + auto response = m_rspConnector->TransmitAndReceive(RspData(packet)); + std::string jsonStr = response.AsString(); + + if (jsonStr.empty() || jsonStr[0] != '[') + return {}; + + std::vector result; + + // Helper lambda to extract uint64_t value from JSON object + auto extractUInt64 = [](const std::string& json, const std::string& key) -> uint64_t { + size_t keyPos = json.find("\"" + key + "\""); + if (keyPos == std::string::npos) + return 0; + + size_t colonPos = json.find(':', keyPos); + if (colonPos == std::string::npos) + return 0; + + size_t valueStart = colonPos + 1; + while (valueStart < json.length() && std::isspace(json[valueStart])) + valueStart++; + + if (json.substr(valueStart, 4) == "null") + return 0; + + size_t valueEnd = valueStart; + while (valueEnd < json.length() && std::isdigit(json[valueEnd])) + valueEnd++; + + if (valueEnd > valueStart) + return std::stoull(json.substr(valueStart, valueEnd - valueStart)); + return 0; + }; + + // Helper lambda to extract string value from JSON object + auto extractString = [](const std::string& json, const std::string& key) -> std::string { + size_t keyPos = json.find("\"" + key + "\""); + if (keyPos == std::string::npos) + return ""; + + size_t colonPos = json.find(':', keyPos); + if (colonPos == std::string::npos) + return ""; + + size_t valueStart = json.find('"', colonPos); + if (valueStart == std::string::npos) + return ""; + + // Handle escaped characters in strings + std::string value; + size_t i = valueStart + 1; + while (i < json.length()) + { + if (json[i] == '\\' && i + 1 < json.length()) + { + switch (json[i + 1]) + { + case '"': value += '"'; break; + case '\\': value += '\\'; break; + case 'n': value += '\n'; break; + case 't': value += '\t'; break; + case 'r': value += '\r'; break; + default: value += json[i + 1]; break; + } + i += 2; + } + else if (json[i] == '"') + { + break; + } + else + { + value += json[i]; + i++; + } + } + + return value; + }; + + size_t pos = 0; + while (pos < jsonStr.length()) + { + size_t objStart = jsonStr.find('{', pos); + if (objStart == std::string::npos) + break; + + // Find matching closing brace (handle nested braces) + size_t objEnd = jsonStr.find('}', objStart); + if (objEnd == std::string::npos) + break; + + std::string objStr = jsonStr.substr(objStart, objEnd - objStart + 1); + + TTDStringEntry entry; + entry.id = extractUInt64(objStr, "id"); + entry.data = extractString(objStr, "data"); + entry.address = extractUInt64(objStr, "address"); + entry.size = extractUInt64(objStr, "size"); + + uint64_t firstAccess = extractUInt64(objStr, "first_access"); + uint64_t lastAccess = extractUInt64(objStr, "last_access"); + entry.firstAccess = TTDPosition(firstAccess, 0); + entry.lastAccess = TTDPosition(lastAccess, 0); + + entry.encoding = extractString(objStr, "encoding"); + + result.push_back(entry); + + pos = objEnd + 1; + } + + return result; +} + + Ref EsrevenAdapterType::GetAdapterSettings() { static Ref settings = RegisterAdapterSettings(); diff --git a/core/adapters/esrevenadapter.h b/core/adapters/esrevenadapter.h index a1c3689e..71e9fef6 100644 --- a/core/adapters/esrevenadapter.h +++ b/core/adapters/esrevenadapter.h @@ -192,6 +192,7 @@ namespace BinaryNinjaDebugger TTDPosition GetCurrentTTDPosition() override; bool SetTTDPosition(const TTDPosition& position) override; std::vector GetTTDCallsForSymbols(const std::string& symbols, uint64_t startReturnAddress = 0, uint64_t endReturnAddress = 0) override; + std::vector GetTTDStrings(const std::string& pattern = "", uint64_t maxResults = 0) override; void GenerateDefaultAdapterSettings(BinaryView* data); Ref GetAdapterSettings() override; diff --git a/core/debugadapter.cpp b/core/debugadapter.cpp index 2390f183..7854ffbc 100644 --- a/core/debugadapter.cpp +++ b/core/debugadapter.cpp @@ -275,3 +275,9 @@ std::pair DebugAdapter::GetTTDPrevMemoryAccess(uint64_t ad } +std::vector DebugAdapter::GetTTDStrings(const std::string& pattern, uint64_t maxResults) +{ + return {}; +} + + diff --git a/core/debugadapter.h b/core/debugadapter.h index 116a9c26..5e595780 100644 --- a/core/debugadapter.h +++ b/core/debugadapter.h @@ -398,6 +398,7 @@ namespace BinaryNinjaDebugger { virtual bool SetTTDPosition(const TTDPosition& position); virtual std::pair GetTTDNextMemoryAccess(uint64_t address, uint64_t size, TTDMemoryAccessType accessType); virtual std::pair GetTTDPrevMemoryAccess(uint64_t address, uint64_t size, TTDMemoryAccessType accessType); + virtual std::vector GetTTDStrings(const std::string& pattern = "", uint64_t maxResults = 0); }; }; // namespace BinaryNinjaDebugger diff --git a/core/debuggercommon.h b/core/debuggercommon.h index 85a99362..eb99ef1f 100644 --- a/core/debuggercommon.h +++ b/core/debuggercommon.h @@ -256,6 +256,20 @@ namespace BinaryNinjaDebugger { TTDEvent(TTDEventType eventType) : type(eventType) {} }; + // TTD String Entry - represents a string found in the trace + struct TTDStringEntry + { + uint64_t id; + std::string data; // The string content + uint64_t address; // Linear address where string begins + uint64_t size; // Size in bytes + TTDPosition firstAccess; // Position of first access in the trace + TTDPosition lastAccess; // Position of last access in the trace + std::string encoding; // "utf8" or "utf16" + + TTDStringEntry() : id(0), address(0), size(0) {} + }; + // Breakpoint types - used to specify the type of breakpoint to set enum DebugBreakpointType { diff --git a/core/debuggercontroller.cpp b/core/debuggercontroller.cpp index 1713fe19..ab7de82e 100644 --- a/core/debuggercontroller.cpp +++ b/core/debuggercontroller.cpp @@ -3147,6 +3147,18 @@ std::vector DebuggerController::GetAllTTDEvents() } +std::vector DebuggerController::GetTTDStrings(const std::string& pattern, uint64_t maxResults) +{ + if (!m_state->IsConnected() || !IsTTD()) + { + LogWarn("Current adapter does not support TTD"); + return {}; + } + + return m_adapter->GetTTDStrings(pattern, maxResults); +} + + void DebuggerController::RecordTTDPosition() { if (!m_adapter || !m_adapter->SupportFeature(DebugAdapterSupportTTD) || m_suppressTTDPositionRecording) diff --git a/core/debuggercontroller.h b/core/debuggercontroller.h index a7df4703..790a544d 100644 --- a/core/debuggercontroller.h +++ b/core/debuggercontroller.h @@ -407,6 +407,7 @@ namespace BinaryNinjaDebugger { bool SetTTDPosition(const TTDPosition& position); std::pair GetTTDNextMemoryAccess(uint64_t address, uint64_t size, TTDMemoryAccessType accessType); std::pair GetTTDPrevMemoryAccess(uint64_t address, uint64_t size, TTDMemoryAccessType accessType); + std::vector GetTTDStrings(const std::string& pattern = "", uint64_t maxResults = 0); // TTD Position History Navigation bool TTDNavigateBack(); diff --git a/core/ffi.cpp b/core/ffi.cpp index 5b2a7dfd..9656352a 100644 --- a/core/ffi.cpp +++ b/core/ffi.cpp @@ -1763,6 +1763,55 @@ void BNDebuggerFreeTTDEvents(BNDebuggerTTDEvent* events, size_t count) } +BNDebuggerTTDStringEntry* BNDebuggerGetTTDStrings( + BNDebuggerController* controller, const char* pattern, uint64_t maxResults, size_t* count) +{ + if (!count) + return nullptr; + + *count = 0; + + std::string patternStr = pattern ? pattern : ""; + auto entries = controller->object->GetTTDStrings(patternStr, maxResults); + if (entries.empty()) + return nullptr; + + *count = entries.size(); + auto result = new BNDebuggerTTDStringEntry[entries.size()]; + + for (size_t i = 0; i < entries.size(); ++i) + { + result[i].id = entries[i].id; + result[i].data = BNAllocString(entries[i].data.c_str()); + result[i].address = entries[i].address; + result[i].size = entries[i].size; + result[i].firstAccess.sequence = entries[i].firstAccess.sequence; + result[i].firstAccess.step = entries[i].firstAccess.step; + result[i].lastAccess.sequence = entries[i].lastAccess.sequence; + result[i].lastAccess.step = entries[i].lastAccess.step; + result[i].encoding = BNAllocString(entries[i].encoding.c_str()); + } + + return result; +} + + +void BNDebuggerFreeTTDStrings(BNDebuggerTTDStringEntry* entries, size_t count) +{ + if (!entries || count == 0) + return; + + for (size_t i = 0; i < count; ++i) + { + if (entries[i].data) + BNFreeString(entries[i].data); + if (entries[i].encoding) + BNFreeString(entries[i].encoding); + } + + delete[] entries; +} + void BNDebuggerPostDebuggerEvent(BNDebuggerController* controller, BNDebuggerEvent* event) { diff --git a/ui/ttdstringswidget.cpp b/ui/ttdstringswidget.cpp new file mode 100644 index 00000000..6f03cf9e --- /dev/null +++ b/ui/ttdstringswidget.cpp @@ -0,0 +1,610 @@ +/* +Copyright 2020-2026 Vector 35 Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "ttdstringswidget.h" +#include "ui.h" +#include "clickablelabel.h" +#include +#include +#include +#include +#include + +using namespace BinaryNinja; +using namespace std; + +constexpr int SortFilterRole = Qt::UserRole + 1; + + +// TTDStringsListModel implementation + +TTDStringsListModel::TTDStringsListModel(QWidget* parent) : QAbstractTableModel(parent) +{ +} + +TTDStringsListModel::~TTDStringsListModel() {} + +QModelIndex TTDStringsListModel::index(int row, int column, const QModelIndex&) const +{ + if (row < 0 || (size_t)row >= m_entries.size() || column >= columnCount()) + return QModelIndex(); + + return createIndex(row, column, (void*)&m_entries[row]); +} + +int TTDStringsListModel::rowCount(const QModelIndex&) const +{ + return (int)m_entries.size(); +} + +int TTDStringsListModel::columnCount(const QModelIndex&) const +{ + return 7; +} + +QVariant TTDStringsListModel::data(const QModelIndex& index, int role) const +{ + if (index.column() >= columnCount() || (size_t)index.row() >= m_entries.size()) + return QVariant(); + + const TTDStringEntry* entry = static_cast(index.internalPointer()); + if (!entry) + return QVariant(); + + if (role == Qt::ToolTipRole && index.column() == StringColumn) + return QString::fromStdString(entry->data); + + if (role != Qt::DisplayRole && role != Qt::SizeHintRole && role != SortFilterRole) + return QVariant(); + + switch (index.column()) + { + case IndexColumn: + { + QString text = QString::number(index.row()); + if (role == Qt::SizeHintRole) + return QVariant((qulonglong)text.size()); + return QVariant(text); + } + case StringColumn: + { + QString text = QString::fromStdString(entry->data); + if (text.length() > 200) + text = text.left(200) + "..."; + if (role == Qt::SizeHintRole) + return QVariant((qulonglong)text.size()); + return QVariant(text); + } + case AddressColumn: + { + QString text = QString::asprintf("0x%" PRIx64, entry->address); + if (role == Qt::SizeHintRole) + return QVariant((qulonglong)text.size()); + return QVariant(text); + } + case SizeColumn: + { + QString text = QString::number(entry->size); + if (role == Qt::SizeHintRole) + return QVariant((qulonglong)text.size()); + return QVariant(text); + } + case FirstAccessColumn: + { + QString text = QString("%1:%2") + .arg(entry->firstAccess.sequence, 0, 16) + .arg(entry->firstAccess.step, 0, 16); + if (role == Qt::SizeHintRole) + return QVariant((qulonglong)text.size()); + return QVariant(text); + } + case LastAccessColumn: + { + QString text = QString("%1:%2") + .arg(entry->lastAccess.sequence, 0, 16) + .arg(entry->lastAccess.step, 0, 16); + if (role == Qt::SizeHintRole) + return QVariant((qulonglong)text.size()); + return QVariant(text); + } + case EncodingColumn: + { + QString text = QString::fromStdString(entry->encoding); + if (role == Qt::SizeHintRole) + return QVariant((qulonglong)text.size()); + return QVariant(text); + } + } + return QVariant(); +} + +QVariant TTDStringsListModel::headerData(int column, Qt::Orientation orientation, int role) const +{ + if (role != Qt::DisplayRole) + return QVariant(); + + if (orientation == Qt::Vertical) + return QVariant(); + + switch (column) + { + case IndexColumn: + return "Index"; + case StringColumn: + return "String"; + case AddressColumn: + return "Address"; + case SizeColumn: + return "Size"; + case FirstAccessColumn: + return "First Access"; + case LastAccessColumn: + return "Last Access"; + case EncodingColumn: + return "Encoding"; + } + return QVariant(); +} + +void TTDStringsListModel::updateRows(const std::vector& entries) +{ + beginResetModel(); + m_entries = entries; + endResetModel(); +} + +const TTDStringEntry& TTDStringsListModel::getRow(int row) const +{ + return m_entries[row]; +} + + +// TTDStringsFilterProxyModel implementation + +TTDStringsFilterProxyModel::TTDStringsFilterProxyModel(QObject* parent) : QSortFilterProxyModel(parent) +{ + setFilterKeyColumn(-1); // Search all columns +} + +bool TTDStringsFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const +{ + return QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent); +} + + +// TTDStringsItemDelegate implementation + +TTDStringsItemDelegate::TTDStringsItemDelegate(QWidget* parent) : QStyledItemDelegate(parent) +{ + updateFonts(); +} + +void TTDStringsItemDelegate::updateFonts() +{ + m_font = getMonospaceFont(dynamic_cast(QObject::parent())); + m_font.setKerning(false); + m_baseline = (int)QFontMetricsF(m_font).ascent(); + m_charWidth = getFontWidthAndAdjustSpacing(m_font); + m_charHeight = (int)(QFontMetricsF(m_font).height() + getExtraFontSpacing()); + m_charOffset = getFontVerticalOffset(); +} + +void TTDStringsItemDelegate::paint( + QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& idx) const +{ + painter->setFont(m_font); + + bool selected = (option.state & QStyle::State_Selected) != 0; + if (selected) + painter->setBrush(getThemeColor(SelectionColor)); + else + painter->setBrush(option.backgroundBrush); + + painter->setPen(Qt::NoPen); + + QRect textRect = option.rect; + textRect.setBottom(textRect.top() + m_charHeight + 2); + painter->drawRect(textRect); + + auto data = idx.data(Qt::DisplayRole); + switch (idx.column()) + { + case TTDStringsListModel::AddressColumn: + painter->setPen(getThemeColor(AddressColor).rgba()); + painter->drawText(textRect, data.toString()); + break; + case TTDStringsListModel::SizeColumn: + case TTDStringsListModel::IndexColumn: + painter->setPen(getThemeColor(NumberColor).rgba()); + painter->drawText(textRect, data.toString()); + break; + case TTDStringsListModel::FirstAccessColumn: + case TTDStringsListModel::LastAccessColumn: + painter->setPen(getThemeColor(AddressColor).rgba()); + painter->drawText(textRect, data.toString()); + break; + default: + painter->setPen(option.palette.color(QPalette::WindowText).rgba()); + painter->drawText(textRect, data.toString()); + break; + } +} + +QSize TTDStringsItemDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& idx) const +{ + auto totalWidth = (idx.data(Qt::SizeHintRole).toInt() + 2) * m_charWidth + 4; + return QSize(totalWidth, m_charHeight + 2); +} + + +// TTDStringsWidget implementation + +TTDStringsWidget::TTDStringsWidget(BinaryViewRef data, QWidget* parent) : QTableView(parent), m_data(data) +{ + m_controller = DebuggerController::GetController(data); + + m_model = new TTDStringsListModel(this); + m_filter = new TTDStringsFilterProxyModel(this); + m_filter->setSourceModel(m_model); + setModel(m_filter); + setShowGrid(false); + + m_delegate = new TTDStringsItemDelegate(this); + setItemDelegate(m_delegate); + + setSelectionBehavior(QAbstractItemView::SelectRows); + setSelectionMode(QAbstractItemView::SingleSelection); + + verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); + verticalHeader()->setVisible(false); + + horizontalHeader()->setStretchLastSection(true); + horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); + + setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel); + setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + + setSortingEnabled(true); + + connect(this, &QTableView::doubleClicked, this, &TTDStringsWidget::onDoubleClicked); + + // Setup actions and context menu + m_actionHandler.setupActionHandler(this); + m_contextMenuManager = new ContextMenuManager(this); + m_menu = new Menu(); + + m_menu->addAction("Copy", "Options", MENU_ORDER_NORMAL); + m_actionHandler.bindAction("Copy", UIAction([&]() { copy(); }, [&]() { return canCopy(); })); + + m_menu->addAction("Copy Row", "Options", MENU_ORDER_NORMAL); + m_actionHandler.bindAction("Copy Row", UIAction([&]() { copySelectedRow(); }, [&]() { return canCopy(); })); + + m_menu->addAction("Copy Table", "Options", MENU_ORDER_NORMAL); + m_actionHandler.bindAction("Copy Table", UIAction([&]() { copyEntireTable(); }, [&]() { return m_model->rowCount() > 0; })); +} + +TTDStringsWidget::~TTDStringsWidget() +{ + delete m_contextMenuManager; +} + +void TTDStringsWidget::setFilter(const string& filter, FilterOptions options) +{ + if (options.testFlag(UseRegexOption)) + m_filter->setFilterRegularExpression(QString::fromStdString(filter)); + else + m_filter->setFilterFixedString(QString::fromStdString(filter)); + m_filter->setFilterCaseSensitivity( + options.testFlag(CaseSensitiveOption) ? Qt::CaseSensitive : Qt::CaseInsensitive); + updateColumnWidths(); +} + +void TTDStringsWidget::scrollToFirstItem() {} +void TTDStringsWidget::scrollToCurrentItem() {} +void TTDStringsWidget::ensureSelection() {} +void TTDStringsWidget::activateSelection() {} + +void TTDStringsWidget::updateColumnWidths() +{ + resizeColumnsToContents(); +} + +void TTDStringsWidget::updateFonts() +{ + m_delegate->updateFonts(); +} + +bool TTDStringsWidget::canCopy() +{ + return selectionModel()->hasSelection(); +} + +void TTDStringsWidget::contextMenuEvent(QContextMenuEvent* event) +{ + m_contextMenuManager->show(m_menu, &m_actionHandler); +} + +void TTDStringsWidget::showContextMenu() +{ + m_contextMenuManager->show(m_menu, &m_actionHandler); +} + +void TTDStringsWidget::performQuery(uint64_t maxResults) +{ + if (!m_controller) + return; + + if (!m_controller->IsConnected() || !m_controller->IsTTD()) + { + emit statusUpdated("Not connected to a TTD target."); + return; + } + + emit statusUpdated("Querying strings..."); + QApplication::processEvents(); + + auto entries = m_controller->GetTTDStrings("", maxResults); + m_model->updateRows(entries); + + updateColumnWidths(); + + emit statusUpdated(QString("Found %1 strings.").arg(entries.size())); +} + +void TTDStringsWidget::clearResults() +{ + m_model->updateRows({}); + emit statusUpdated("Results cleared."); +} + +void TTDStringsWidget::onDoubleClicked(const QModelIndex& proxyIndex) +{ + if (!m_controller) + return; + + QModelIndex sourceIndex = m_filter->mapToSource(proxyIndex); + if (!sourceIndex.isValid()) + return; + + int sourceRow = sourceIndex.row(); + int column = sourceIndex.column(); + + const TTDStringEntry& entry = m_model->getRow(sourceRow); + + // Navigate to TTD position on First Access or Last Access columns + if (column == TTDStringsListModel::FirstAccessColumn) + { + TTDPosition position(entry.firstAccess.sequence, entry.firstAccess.step); + if (m_controller->SetTTDPosition(position)) + emit statusUpdated(QString("Navigated to position %1:%2") + .arg(entry.firstAccess.sequence, 0, 16).arg(entry.firstAccess.step, 0, 16)); + else + emit statusUpdated("Failed to navigate to position"); + } + else if (column == TTDStringsListModel::LastAccessColumn) + { + TTDPosition position(entry.lastAccess.sequence, entry.lastAccess.step); + if (m_controller->SetTTDPosition(position)) + emit statusUpdated(QString("Navigated to position %1:%2") + .arg(entry.lastAccess.sequence, 0, 16).arg(entry.lastAccess.step, 0, 16)); + else + emit statusUpdated("Failed to navigate to position"); + } + // Navigate to address on Address column + else if (column == TTDStringsListModel::AddressColumn) + { + ViewFrame* frame = ViewFrame::viewFrameForWidget(this); + if (frame) + { + frame->navigate(m_data, entry.address); + emit statusUpdated(QString("Navigated to address 0x%1").arg(entry.address, 0, 16)); + } + } +} + +void TTDStringsWidget::copy() +{ + copySelectedRow(); +} + +void TTDStringsWidget::copySelectedRow() +{ + if (!selectionModel()->hasSelection()) + return; + + QModelIndexList selected = selectionModel()->selectedRows(); + if (selected.isEmpty()) + return; + + QStringList rowData; + int row = selected.first().row(); + + for (int col = 0; col < m_filter->columnCount(); ++col) + { + QModelIndex idx = m_filter->index(row, col); + rowData << idx.data(Qt::DisplayRole).toString(); + } + + QApplication::clipboard()->setText(rowData.join('\t')); +} + +void TTDStringsWidget::copyEntireTable() +{ + QStringList tableData; + + // Header row + QStringList headers; + for (int col = 0; col < m_filter->columnCount(); ++col) + headers << m_model->headerData(col, Qt::Horizontal, Qt::DisplayRole).toString(); + tableData << headers.join('\t'); + + // Data rows + for (int row = 0; row < m_filter->rowCount(); ++row) + { + QStringList rowData; + for (int col = 0; col < m_filter->columnCount(); ++col) + { + QModelIndex idx = m_filter->index(row, col); + rowData << idx.data(Qt::DisplayRole).toString(); + } + tableData << rowData.join('\t'); + } + + QApplication::clipboard()->setText(tableData.join('\n')); +} + + +// TTDStringsWithFilter implementation + +TTDStringsWithFilter::TTDStringsWithFilter(BinaryViewRef data, QWidget* parent) + : QWidget(parent), m_data(data) +{ + m_stringsWidget = new TTDStringsWidget(data, this); + + m_separateEdit = new FilterEdit(m_stringsWidget); + m_separateEdit->showRegexToggle(true); + m_filteredView = new FilteredView(this, m_stringsWidget, m_stringsWidget, m_separateEdit); + m_filteredView->setFilterPlaceholderText("Filter strings"); + + auto headerLayout = new QHBoxLayout; + headerLayout->addWidget(m_separateEdit, 1); + headerLayout->setContentsMargins(1, 1, 6, 0); + headerLayout->setAlignment(Qt::AlignBaseline); + + auto* icon = new ClickableIcon(QImage(":/debugger/menu"), QSize(16, 16)); + connect(icon, &ClickableIcon::clicked, m_stringsWidget, &TTDStringsWidget::showContextMenu); + headerLayout->addWidget(icon); + + // Query controls + auto controlLayout = new QHBoxLayout; + controlLayout->setContentsMargins(5, 2, 5, 2); + controlLayout->addWidget(new QLabel("Max Results:")); + m_maxResultsSpinBox = new QSpinBox(); + m_maxResultsSpinBox->setRange(1, 0x7FFFFFFF); + m_maxResultsSpinBox->setValue(100000); + controlLayout->addWidget(m_maxResultsSpinBox); + + controlLayout->addSpacing(10); + + auto* queryButton = new QPushButton("Query"); + queryButton->setDefault(true); + connect(queryButton, &QPushButton::clicked, this, &TTDStringsWithFilter::performQuery); + controlLayout->addWidget(queryButton); + + auto* clearButton = new QPushButton("Clear"); + connect(clearButton, &QPushButton::clicked, this, &TTDStringsWithFilter::clearResults); + controlLayout->addWidget(clearButton); + + controlLayout->addStretch(); + + // Status label + m_statusLabel = new QLabel("Ready to query TTD strings."); + m_statusLabel->setContentsMargins(5, 2, 5, 2); + + connect(m_stringsWidget, &TTDStringsWidget::statusUpdated, this, &TTDStringsWithFilter::updateStatus); + + auto* layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->addLayout(headerLayout); + layout->addLayout(controlLayout); + layout->addWidget(m_filteredView, 1); + layout->addWidget(m_statusLabel); +} + +void TTDStringsWithFilter::updateFonts() +{ + m_stringsWidget->updateFonts(); +} + +void TTDStringsWithFilter::performQuery() +{ + uint64_t maxResults = static_cast(m_maxResultsSpinBox->value()); + m_stringsWidget->performQuery(maxResults); +} + +void TTDStringsWithFilter::clearResults() +{ + m_stringsWidget->clearResults(); +} + +void TTDStringsWithFilter::updateStatus(const QString& message) +{ + m_statusLabel->setText(message); +} + + +// TTDStringsSidebarWidget implementation + +TTDStringsSidebarWidget::TTDStringsSidebarWidget(BinaryViewRef data) + : SidebarWidget("TTD Strings"), m_data(data), m_debuggerEventCallback(0) +{ + m_controller = DebuggerController::GetController(data); + + m_widget = new TTDStringsWithFilter(data, this); + + auto* layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->addWidget(m_widget); + + if (m_controller) + { + connect(this, &TTDStringsSidebarWidget::debuggerEvent, this, &TTDStringsSidebarWidget::onDebuggerEvent); + + m_debuggerEventCallback = m_controller->RegisterEventCallback( + [&](const DebuggerEvent& event) { + emit debuggerEvent(event); + }, + "TTD Strings Widget"); + } +} + +TTDStringsSidebarWidget::~TTDStringsSidebarWidget() +{ + if (m_controller) + m_controller->RemoveEventCallback(m_debuggerEventCallback); +} + +void TTDStringsSidebarWidget::onDebuggerEvent(const DebuggerEvent& event) +{ + switch (event.type) + { + case TargetExitedEventType: + case DetachedEventType: + if (m_widget) + m_widget->clearResults(); + break; + default: + break; + } +} + + +// TTDStringsWidgetType implementation + +TTDStringsWidgetType::TTDStringsWidgetType() + : SidebarWidgetType(QImage(":/debugger/ttd-events"), "TTD Strings") +{ +} + +SidebarWidget* TTDStringsWidgetType::createWidget(ViewFrame* frame, BinaryViewRef data) +{ + return new TTDStringsSidebarWidget(data); +} + +SidebarContentClassifier* TTDStringsWidgetType::contentClassifier(ViewFrame*, BinaryViewRef data) +{ + return new ActiveDebugSessionSidebarContentClassifier(data); +} diff --git a/ui/ttdstringswidget.h b/ui/ttdstringswidget.h new file mode 100644 index 00000000..ece734f5 --- /dev/null +++ b/ui/ttdstringswidget.h @@ -0,0 +1,201 @@ +/* +Copyright 2020-2026 Vector 35 Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "inttypes.h" +#include "binaryninjaapi.h" +#include "debuggerapi.h" +#include "viewframe.h" +#include "filter.h" +#include "fontsettings.h" +#include "theme.h" +#include "debuggeruicommon.h" +#include "menus.h" +#include "uitypes.h" + +using namespace BinaryNinja; +using namespace BinaryNinjaDebuggerAPI; + + +class TTDStringsListModel : public QAbstractTableModel +{ + Q_OBJECT + +public: + enum ColumnHeaders + { + IndexColumn = 0, + StringColumn, + AddressColumn, + SizeColumn, + FirstAccessColumn, + LastAccessColumn, + EncodingColumn, + }; + + TTDStringsListModel(QWidget* parent); + virtual ~TTDStringsListModel(); + + virtual QModelIndex index(int row, int col, const QModelIndex& parent = QModelIndex()) const override; + virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override; + virtual int columnCount(const QModelIndex& parent = QModelIndex()) const override; + virtual QVariant data(const QModelIndex& index, int role) const override; + virtual QVariant headerData(int column, Qt::Orientation orientation, int role) const override; + + void updateRows(const std::vector& entries); + const TTDStringEntry& getRow(int row) const; + +private: + std::vector m_entries; +}; + + +class TTDStringsFilterProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT + +public: + TTDStringsFilterProxyModel(QObject* parent); + +protected: + virtual bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override; +}; + + +class TTDStringsItemDelegate : public QStyledItemDelegate +{ + Q_OBJECT + + QFont m_font; + int m_baseline, m_charWidth, m_charHeight, m_charOffset; + +public: + TTDStringsItemDelegate(QWidget* parent); + void updateFonts(); + void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& idx) const override; + QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& idx) const override; +}; + + +class TTDStringsWidget : public QTableView, public FilterTarget +{ + Q_OBJECT + + BinaryViewRef m_data; + DbgRef m_controller; + + TTDStringsListModel* m_model; + TTDStringsItemDelegate* m_delegate; + TTDStringsFilterProxyModel* m_filter; + + UIActionHandler m_actionHandler; + ContextMenuManager* m_contextMenuManager; + Menu* m_menu; + + virtual void contextMenuEvent(QContextMenuEvent* event) override; + + bool canCopy(); + + virtual void setFilter(const std::string& filter, FilterOptions options) override; + virtual void scrollToFirstItem() override; + virtual void scrollToCurrentItem() override; + virtual void ensureSelection() override; + virtual void activateSelection() override; + +public: + TTDStringsWidget(BinaryViewRef data, QWidget* parent = nullptr); + ~TTDStringsWidget(); + + void updateColumnWidths(); + void updateFonts(); + void performQuery(uint64_t maxResults); + void clearResults(); + void showContextMenu(); + +signals: + void statusUpdated(const QString& message); + +private slots: + void onDoubleClicked(const QModelIndex& index); + void copy(); + void copySelectedRow(); + void copyEntireTable(); +}; + + +class TTDStringsWithFilter : public QWidget +{ + Q_OBJECT + + BinaryViewRef m_data; + TTDStringsWidget* m_stringsWidget; + FilteredView* m_filteredView; + FilterEdit* m_separateEdit; + QSpinBox* m_maxResultsSpinBox; + QLabel* m_statusLabel; + +public: + TTDStringsWithFilter(BinaryViewRef data, QWidget* parent = nullptr); + void updateFonts(); + void performQuery(); + void clearResults(); + void updateStatus(const QString& message); +}; + + +class TTDStringsSidebarWidget : public SidebarWidget +{ + Q_OBJECT + +private: + TTDStringsWithFilter* m_widget; + BinaryViewRef m_data; + DbgRef m_controller; + size_t m_debuggerEventCallback; + +public: + TTDStringsSidebarWidget(BinaryViewRef data); + ~TTDStringsSidebarWidget(); + +signals: + void debuggerEvent(const DebuggerEvent& event); + +private slots: + void onDebuggerEvent(const DebuggerEvent& event); +}; + + +class TTDStringsWidgetType : public SidebarWidgetType +{ +public: + TTDStringsWidgetType(); + SidebarWidget* createWidget(ViewFrame* frame, BinaryViewRef data) override; + SidebarWidgetLocation defaultLocation() const override { return SidebarWidgetLocation::RightContent; } + SidebarContextSensitivity contextSensitivity() const override { return PerViewTypeSidebarContext; } + SidebarIconVisibility defaultIconVisibility() const override { return HideSidebarIconIfNoContent; } + SidebarContentClassifier* contentClassifier(ViewFrame*, BinaryViewRef) override; +}; diff --git a/ui/ui.cpp b/ui/ui.cpp index 60bb5977..9b736436 100644 --- a/ui/ui.cpp +++ b/ui/ui.cpp @@ -49,6 +49,7 @@ limitations under the License. #include "ttdcallswidget.h" #include "ttdeventswidget.h" #include "ttdbookmarkwidget.h" +#include "ttdstringswidget.h" #include "ttdanalysisdialog.h" #include "timestampnavigationdialog.h" #include "freeversion.h" @@ -2066,6 +2067,7 @@ void GlobalDebuggerUI::InitializeUI() Sidebar::addSidebarWidgetType(new TTDCallsWidgetType()); Sidebar::addSidebarWidgetType(new TTDEventsWidgetType()); Sidebar::addSidebarWidgetType(new TTDBookmarkWidgetType()); + Sidebar::addSidebarWidgetType(new TTDStringsWidgetType()); }