From 3cdbe1eca5e1aa4c39d89a337c5482b95aff6f4b Mon Sep 17 00:00:00 2001 From: Mark Rowe Date: Fri, 12 Jun 2026 14:44:01 -0700 Subject: [PATCH 1/3] Add a helper for background sorting and filtering of flat UI models --- ui/backgroundsortfilterrows.h | 573 ++++++++++++++++++++++++++++++++++ 1 file changed, 573 insertions(+) create mode 100644 ui/backgroundsortfilterrows.h diff --git a/ui/backgroundsortfilterrows.h b/ui/backgroundsortfilterrows.h new file mode 100644 index 000000000..754ef1bf5 --- /dev/null +++ b/ui/backgroundsortfilterrows.h @@ -0,0 +1,573 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "base/assertions.h" +#include "binaryninjacore.h" + +/*! The non-template portion of `BackgroundSortFilterRows`. + Contains the background job state machine, watcher lifecycle, and abandonment protocol, + which are independent of the row type. + + \ingroup filter +*/ +class BackgroundSortFilterRowsBase +{ +public: + // Model integration points. The reset and insert hooks bracket changes to the display rows, + // matching the corresponding QAbstractItemModel methods. + struct ModelHooks + { + std::function beginResetModel; + std::function endResetModel; + std::function beginInsertRows; + std::function endInsertRows; + // Optional. Called when background filtering starts and finishes. + std::function filteringChanged; + // Optional. Called when background sorting starts and finishes. + std::function sortingChanged; + }; + + explicit BackgroundSortFilterRowsBase(ModelHooks hooks) : m_hooks(std::move(hooks)) {} + virtual ~BackgroundSortFilterRowsBase() { shutdown(); } + + BackgroundSortFilterRowsBase(const BackgroundSortFilterRowsBase&) = delete; + BackgroundSortFilterRowsBase& operator=(const BackgroundSortFilterRowsBase&) = delete; + + bool busy() const { return m_jobActive; } + bool filtering() const { return m_jobActive && m_jobKind == JobKind::Filter; } + bool sorting() const { return m_jobActive && m_jobKind == JobKind::Sort; } + +protected: + enum class JobKind + { + Filter, + Sort, + }; + + void assertOwningThread() const { BN_ASSERT(QThread::currentThread() == m_owningThread); } + + // Abandon any in-flight job, wait for its workers to notice, and destroy the watcher + // immediately so a queued finished signal cannot run a commit handler afterwards. Derived + // destructors must call this before the row storage the workers read is destroyed. + void shutdown() + { + m_filterGeneration.fetch_add(1); + m_future.waitForFinished(); + m_watcher.reset(); + } + + // Like `shutdown`, but leaves the object reusable. Returns it to the idle state and notifies + // that the abandoned job is no longer running. The job's result is discarded, not committed. + void abandonJob() + { + if (!m_jobActive) + return; + shutdown(); + m_jobActive = false; + if (m_jobKind == JobKind::Filter && m_hooks.filteringChanged) + m_hooks.filteringChanged(false); + else if (m_jobKind == JobKind::Sort && m_hooks.sortingChanged) + m_hooks.sortingChanged(false); + } + + // Registers `future` as the running job. Its completion invokes `commitJob` back on the + // owning thread. `filterGeneration` must be the value the job's workers compare against. + void beginJob(JobKind kind, uint64_t filterGeneration, QFuture future) + { + BN_ASSERT(!m_jobActive); + m_jobActive = true; + m_jobKind = kind; + m_jobFilterGeneration = filterGeneration; + m_future = std::move(future); + m_watcher = std::make_unique>(); + QObject::connect( + m_watcher.get(), &QFutureWatcherBase::finished, m_watcher.get(), [this] { finishJob(); }); + if (kind == JobKind::Filter && m_hooks.filteringChanged) + m_hooks.filteringChanged(true); + else if (kind == JobKind::Sort && m_hooks.sortingChanged) + m_hooks.sortingChanged(true); + m_watcher->setFuture(m_future); + } + + // Whether the filter changed after the current or just-finished job captured its generation. + bool filterChangedDuringJob() const { return m_jobFilterGeneration != m_filterGeneration.load(); } + + // Called on the owning thread when the registered job's future finishes. + virtual void commitJob(JobKind kind) = 0; + + ModelHooks m_hooks; + // Bumped when the filtered set becomes stale: on a filter-predicate change, and on teardown. + // Filter workers watch it to drop a superseded filter pass. Sort workers watch it too, because a + // filter change invalidates the display snapshot they are reordering, and commitSortJob reads it + // (via filterChangedDuringJob) to recognize that case and re-filter before re-sorting. + std::atomic m_filterGeneration = 0; + +private: + void finishJob() + { + // This is invoked by the watcher's finished signal so the watcher cannot be deleted + // directly here. + m_watcher.release()->deleteLater(); + m_jobActive = false; + commitJob(m_jobKind); + } + + QThread* m_owningThread = QThread::currentThread(); + uint64_t m_jobFilterGeneration = 0; + bool m_jobActive = false; + JobKind m_jobKind = JobKind::Filter; + std::unique_ptr> m_watcher; + QFuture m_future; +}; + +/*! Append-only row storage for table models that filters and sorts rows on the worker pool. + + Filtering and sorting run as QtConcurrent jobs. Changing the filter abandons a running job at + its next check and re-runs with the latest predicate, so rapid filter changes stay responsive. + Appends and sorts requested while a job is running are deferred and applied when the job + commits, allowing the job to read the rows without locking. + + All methods must be called on the thread the object was created on (the GUI thread for table + models): the job state machine and row vectors are single-thread-confined, with the worker + threads communicating back only through the watcher's queued finished signal, and reading only + the atomic generation counters and the unmutated rows. The filter predicate and comparator are + called concurrently from worker threads and must be thread-safe. + + \ingroup filter +*/ +template +class BackgroundSortFilterRows : public BackgroundSortFilterRowsBase +{ +public: + using Predicate = std::function; + using PredicateFactory = std::function; + using Comparator = std::function; + // Display entries are indices into the master rows. 32 bits suffices: a QAbstractItemModel + // addresses rows with int, so it can never display more than INT_MAX of them. + using Index = uint32_t; + + explicit BackgroundSortFilterRows(ModelHooks hooks) : BackgroundSortFilterRowsBase(std::move(hooks)) {} + + ~BackgroundSortFilterRows() override + { + // The workers read the row storage, so they must be stopped before it is destroyed. + shutdown(); + } + + // Number of rows a view should currently display. Either all rows, or the matches of the + // committed filter. + size_t displayCount() const { return m_display.size(); } + + // The row at display position `index`, resolving through the filter/sort index. + const Row& displayAt(size_t index) const { return m_rows[m_display[index]]; } + + // Count of all rows, ignoring any filter, including rows whose append is deferred. + size_t totalCount() const { return m_rows.size() + m_pendingRows.size(); } + + // Drop all rows and free their storage, returning to the empty state and abandoning any + // running job. The filter and sort settings are retained, so rows appended afterward are + // filtered as before. + void clear() + { + assertOwningThread(); + abandonJob(); + m_hooks.beginResetModel(); + // Assigning fresh vectors releases the capacity rather than retaining it like clear(). + m_rows = std::vector{}; + m_display = std::vector{}; + m_pendingRows = std::vector{}; + m_pendingSort.reset(); + m_maintainSortOnAppend = false; + m_hooks.endResetModel(); + } + + void append(std::vector rows) { appendRows(std::move(rows), true); } + + void setFilter(Predicate filter) + { + if (filter) + setFilterFactory([filter] { return filter; }); + else + setFilterFactory(nullptr); + } + + // Like `setFilter`, but the factory is invoked once per work chunk on the worker thread to + // create that chunk's predicate. Use this when the predicate holds state that is expensive + // to share across threads, e.g. a QRegularExpression, whose internal mutex serializes + // concurrent matches on a shared instance. + void setFilterFactory(PredicateFactory factory) + { + assertOwningThread(); + m_filterFactory = std::move(factory); + m_filter = m_filterFactory ? m_filterFactory() : Predicate(); + m_filterGeneration.fetch_add(1); + // An active job notices the generation change, finishes early, and re-applies on completion. + if (!busy()) + applyCurrentFilter(); + } + + void sort(Comparator comparator) + { + assertOwningThread(); + m_activeSort = comparator; + // Bump the sort generation so a sort already running notices and bails out promptly rather + // than sorting rows whose result this newer request will discard. + m_sortGeneration.fetch_add(1); + if (busy()) + { + m_pendingSort = std::move(comparator); + return; + } + startSortJob(std::move(comparator)); + } + +protected: + void commitJob(JobKind kind) override + { + if (kind == JobKind::Sort) + commitSortJob(); + else + commitFilterJob(); + } + +private: + void appendRows(std::vector rows, bool resortAfterAppend) + { + assertOwningThread(); + if (rows.empty()) + return; + + if (busy()) + { + m_pendingRows.insert(m_pendingRows.end(), std::make_move_iterator(rows.begin()), + std::make_move_iterator(rows.end())); + return; + } + + // Collect the master indices of the new rows that the filter accepts. An unset filter + // accepts all of them. + const Index base = static_cast(m_rows.size()); + std::vector newDisplay; + for (size_t i = 0; i < rows.size(); i++) + if (!m_filter || m_filter(rows[i])) + newDisplay.push_back(base + static_cast(i)); + + if (!newDisplay.empty()) + { + const int oldCount = static_cast(m_display.size()); + m_hooks.beginInsertRows(oldCount, oldCount + static_cast(newDisplay.size()) - 1); + } + + // Retain every row for later filter changes, even those not currently displayed. + m_rows.insert( + m_rows.end(), std::make_move_iterator(rows.begin()), std::make_move_iterator(rows.end())); + m_display.insert(m_display.end(), newDisplay.begin(), newDisplay.end()); + + if (!newDisplay.empty()) + m_hooks.endInsertRows(); + + if (resortAfterAppend && !newDisplay.empty() && m_maintainSortOnAppend && m_activeSort) + startSortJob(m_activeSort); + } + + // Parallel merge sort of `display` (indices into `rows`) by `comparator`. Returns the sorted + // indices, or nullopt if `filterGeneration` stopped matching `currentFilterGeneration` (the + // filter changed) or `sortGeneration` stopped matching `currentSortGeneration` (a newer sort was + // requested) mid-sort, meaning the result should be discarded. + static std::optional> parallelSort(std::vector display, + const std::vector& rows, const Comparator& comparator, uint64_t filterGeneration, + const std::atomic& currentFilterGeneration, uint64_t sortGeneration, + const std::atomic& currentSortGeneration) + { + if (display.size() < 2) + return display; + + struct Abandoned{}; + std::atomic abandoned = false; + // Wraps the comparator to compare rows by index and bail out promptly if the filter or + // sort changes mid-job. + const auto guarded = [&](size_t& comparisons) { + return [&](Index a, Index b) { + if ((++comparisons & 0xfff) == 0 + && (currentFilterGeneration.load(std::memory_order_relaxed) != filterGeneration + || currentSortGeneration.load(std::memory_order_relaxed) != sortGeneration)) + throw Abandoned {}; + return comparator(rows[a], rows[b]); + }; + }; + + // Sort chunks of the indices in parallel, then merge adjacent chunks pairwise, with each + // merge round also running in parallel. A chunk is the half-open range [first, second). + const size_t threadCount = + std::clamp(QThreadPool::globalInstance()->maxThreadCount(), 1, 64); + const size_t chunkSize = std::max(1, (display.size() + threadCount - 1) / threadCount); + using Range = std::pair; + std::vector chunks; + chunks.reserve((display.size() + chunkSize - 1) / chunkSize); + for (size_t begin = 0; begin < display.size(); begin += chunkSize) + chunks.emplace_back(begin, std::min(begin + chunkSize, display.size())); + + QtConcurrent::blockingMap(chunks, [&](const Range& chunk) { + size_t comparisons = 0; + try + { + std::sort(display.begin() + chunk.first, display.begin() + chunk.second, guarded(comparisons)); + } + catch (const Abandoned&) + { + abandoned = true; + } + }); + + while (chunks.size() > 1 && !abandoned) + { + // Pair up adjacent chunks. Each pair (i, i + 1) is merged about their shared boundary + // chunks[i].second. An odd final chunk is already sorted and carries forward unchanged. + std::vector pairs; + std::vector mergedChunks; + pairs.reserve(chunks.size() / 2); + mergedChunks.reserve((chunks.size() + 1) / 2); + for (size_t i = 0; i < chunks.size(); i += 2) + { + if (i + 1 < chunks.size()) + { + pairs.push_back(i); + mergedChunks.emplace_back(chunks[i].first, chunks[i + 1].second); + } + else + { + mergedChunks.push_back(chunks[i]); + } + } + QtConcurrent::blockingMap(pairs, [&](size_t i) { + size_t comparisons = 0; + try + { + std::inplace_merge(display.begin() + chunks[i].first, display.begin() + chunks[i].second, + display.begin() + chunks[i + 1].second, guarded(comparisons)); + } + catch (const Abandoned&) + { + abandoned = true; + } + }); + chunks = std::move(mergedChunks); + } + + if (abandoned) + return std::nullopt; + return display; + } + + void startSortJob(Comparator comparator) + { + const uint64_t filterGeneration = m_filterGeneration.load(); + const uint64_t sortGeneration = m_sortGeneration.load(); + m_sortResult = std::make_shared>(); + auto result = m_sortResult; + + // The display indices are not mutated while the job is active (appends and sorts are + // deferred), so the worker sorts a copy and reads m_rows without locking. An abandoned sort + // leaves the shared result empty, which commitSortJob never reads. + auto future = QtConcurrent::run( + [this, filterGeneration, sortGeneration, comparator, result, display = m_display]() mutable { + if (auto sorted = parallelSort(std::move(display), m_rows, comparator, filterGeneration, + m_filterGeneration, sortGeneration, m_sortGeneration)) + *result = std::move(*sorted); + }); + beginJob(JobKind::Sort, filterGeneration, std::move(future)); + } + + void commitSortJob() + { + auto result = std::move(m_sortResult); + + // The filter changed while sorting. Re-apply it and queue the sort to run afterwards. + if (filterChangedDuringJob()) + { + if (m_hooks.sortingChanged) + m_hooks.sortingChanged(false); + applyCurrentFilter(); + return; + } + + // A newer sort was requested while this one ran. Skip straight to it. + if (m_pendingSort) + { + auto next = std::move(*m_pendingSort); + m_pendingSort.reset(); + drainPendingRows(); + startSortJob(std::move(next)); + return; + } + + // Rows appended while this sort was running were not part of the worker's display snapshot. + // Add them and rerun the active sort over the full display set. + if (!m_pendingRows.empty()) + { + drainPendingRows(); + if (m_activeSort) + startSortJob(m_activeSort); + else if (m_hooks.sortingChanged) + m_hooks.sortingChanged(false); + return; + } + + m_hooks.beginResetModel(); + m_display = std::move(*result); + // Only start maintaining the sort across future appends once a sort has committed over + // actual rows. This keeps the initial bulk load cheap: the sort indicator is set on the + // empty model and rows stream in arrival order without re-sorting each batch, until the + // caller issues the final sort over the complete content. + m_maintainSortOnAppend = m_activeSort && !m_rows.empty(); + m_hooks.endResetModel(); + + if (m_hooks.sortingChanged) + m_hooks.sortingChanged(false); + } + + void applyCurrentFilter() + { + drainPendingRows(); + + if (!m_filter) + { + m_hooks.beginResetModel(); + // With no filter the display is every row, in master order. + m_display.resize(m_rows.size()); + std::iota(m_display.begin(), m_display.end(), Index{0}); + m_hooks.endResetModel(); + if (m_hooks.filteringChanged) + m_hooks.filteringChanged(false); + applySortAfterDisplayChange(); + return; + } + + const uint64_t filterGeneration = m_filterGeneration.load(); + PredicateFactory factory = m_filterFactory; + + constexpr size_t chunkSize = 1024 * 1024; + std::vector> chunks; + for (size_t begin = 0; begin < m_rows.size(); begin += chunkSize) + chunks.emplace_back(begin, std::min(begin + chunkSize, m_rows.size())); + + m_filterResult = QtConcurrent::mapped(chunks, + [this, filterGeneration, factory](const std::pair& chunk) { + std::vector matches; + if (m_filterGeneration.load() != filterGeneration) + return matches; + // Materialize the chunk's own predicate so predicate state is never shared across + // worker threads. + const Predicate filter = factory(); + for (size_t i = chunk.first; i < chunk.second; i++) + { + // Bail out promptly if the filter changes mid-chunk. + if ((i & 0xfff) == 0 && m_filterGeneration.load() != filterGeneration) + return matches; + if (filter(m_rows[i])) + matches.push_back(static_cast(i)); + } + return matches; + }); + beginJob(JobKind::Filter, filterGeneration, m_filterResult); + } + + void commitFilterJob() + { + auto result = std::move(m_filterResult); + + // The filter changed while the job ran. Run it again with the current predicate. + if (filterChangedDuringJob()) + { + applyCurrentFilter(); + return; + } + + std::vector matches; + for (const auto& chunkMatches : result.results()) + matches.insert(matches.end(), chunkMatches.begin(), chunkMatches.end()); + + m_hooks.beginResetModel(); + m_display = std::move(matches); + m_hooks.endResetModel(); + + drainPendingRows(); + if (m_hooks.filteringChanged) + m_hooks.filteringChanged(false); + applySortAfterDisplayChange(); + } + + void drainPendingRows() + { + if (m_pendingRows.empty()) + return; + auto pending = std::move(m_pendingRows); + m_pendingRows = {}; + appendRows(std::move(pending), false); + } + + void applyPendingSort() + { + if (!m_pendingSort) + return; + auto comparator = std::move(*m_pendingSort); + m_pendingSort.reset(); + sort(std::move(comparator)); + } + + void applySortAfterDisplayChange() + { + if (m_pendingSort) + { + applyPendingSort(); + return; + } + if (m_activeSort) + startSortJob(m_activeSort); + } + + PredicateFactory m_filterFactory; + // Materialized from m_filterFactory for matching appended rows on the main thread. + Predicate m_filter; + + // The master copy of every row, append-only and never reordered. + std::vector m_rows; + // Indices into m_rows that are currently displayed, in display order. This is the only thing + // filtering and sorting rearrange, so neither copies the rows. + std::vector m_display; + + // The sort job's output indices, read by its commit handler. Shared with the worker so the + // future's own result store, which would be a second copy, is avoided. + std::shared_ptr> m_sortResult; + // The filter job's future, whose per-chunk results its commit handler concatenates. + QFuture> m_filterResult; + + Comparator m_activeSort; + // Bumped on every sort request and watched only by sort workers, so a running sort bails out + // when a newer comparator arrives. Kept separate from m_filterGeneration so commitSortJob can + // tell a superseded sort (skip straight to the new comparator) apart from a filter change (which + // must re-filter first); one shared counter would force a full re-filter on every column re-sort. + std::atomic m_sortGeneration = 0; + std::vector m_pendingRows; + std::optional m_pendingSort; + // Set once a sort commits over a non-empty row set, after which appended rows re-run the sort to + // keep the display ordered. Left false during the initial bulk load so streamed rows append in + // arrival order rather than triggering a sort per batch. + bool m_maintainSortOnAppend = false; +}; From a60490764521673cde689ecf59621299f6f1d126 Mon Sep 17 00:00:00 2001 From: Mark Rowe Date: Fri, 12 Jun 2026 14:48:33 -0700 Subject: [PATCH 2/3] [DSC] Add a Strings table to the shared cache triage view The table displays strings from every image in the shared cache. Double-clicking on a string loads its corresponding image or region and navigates to it. The entire shared cache is scanned asynchronously on the worker pool to discover strings when the Strings tab is first selected. Table filtering and sorting is performed on background threads due to the sheer amount of data this table contains (over 15 millions of rows). Data is discarded shortly after the strings table is hidden. --- view/sharedcache/api/python/sharedcache.py | 68 +++ .../api/python/sharedcache_enums.py | 7 + view/sharedcache/api/sharedcache.cpp | 87 +++ view/sharedcache/api/sharedcacheapi.h | 36 ++ view/sharedcache/api/sharedcachecore.h | 37 ++ view/sharedcache/core/CacheStringScanner.cpp | 209 +++++++ view/sharedcache/core/CacheStringScanner.h | 103 ++++ .../core/SharedCacheController.cpp | 5 + view/sharedcache/core/SharedCacheController.h | 4 + view/sharedcache/core/ffi.cpp | 71 +++ view/sharedcache/ui/CMakeLists.txt | 4 +- view/sharedcache/ui/addresstext.h | 14 + view/sharedcache/ui/dsctriage.cpp | 274 ++++++++- view/sharedcache/ui/dsctriage.h | 20 +- view/sharedcache/ui/stringstable.cpp | 333 +++++++++++ view/sharedcache/ui/stringstable.h | 68 +++ view/sharedcache/ui/triagetable.cpp | 530 ++++++++++++++++++ view/sharedcache/ui/triagetable.h | 351 ++++++++++++ 18 files changed, 2204 insertions(+), 17 deletions(-) create mode 100644 view/sharedcache/core/CacheStringScanner.cpp create mode 100644 view/sharedcache/core/CacheStringScanner.h create mode 100644 view/sharedcache/ui/addresstext.h create mode 100644 view/sharedcache/ui/stringstable.cpp create mode 100644 view/sharedcache/ui/stringstable.h create mode 100644 view/sharedcache/ui/triagetable.cpp create mode 100644 view/sharedcache/ui/triagetable.h diff --git a/view/sharedcache/api/python/sharedcache.py b/view/sharedcache/api/python/sharedcache.py index cac4050e4..3f2897644 100644 --- a/view/sharedcache/api/python/sharedcache.py +++ b/view/sharedcache/api/python/sharedcache.py @@ -92,6 +92,71 @@ def image_to_api(image: CacheImage) -> sccore.BNSharedCacheImage: regionStarts=core_region_starts ) +@dataclasses.dataclass +class CacheString: + string_type: sccore.StringTypeEnum + address: int + raw_length: int + text: str + region_start: int + image_start: int + + def __str__(self): + return repr(self) + + def __repr__(self): + return f"" + + +def string_from_api(string: sccore.BNSharedCacheString) -> CacheString: + return CacheString( + string_type=string.stringType, + address=string.address, + raw_length=string.rawLength, + text=string.text, + region_start=string.regionStart, + image_start=string.imageStart + ) + + +class CacheStringScanner: + def __init__(self, handle: sccore.BNSharedCacheStringScannerHandle): + self.handle = handle + + def __del__(self): + if self.handle is not None: + sccore.BNFreeSharedCacheStringScanner(self.handle) + + def start(self) -> bool: + return sccore.BNSharedCacheStringScannerStart(self.handle) + + @property + def is_complete(self) -> bool: + return sccore.BNSharedCacheStringScannerIsComplete(self.handle) + + @property + def progress(self) -> (int, int): + current = ctypes.c_ulonglong() + total = ctypes.c_ulonglong() + sccore.BNSharedCacheStringScannerGetProgress(self.handle, current, total) + return current.value, total.value + + @property + def string_count(self) -> int: + return sccore.BNSharedCacheStringScannerGetStringCount(self.handle) + + def take_strings(self, max_count: int = 0xffffffffffffffff) -> [CacheString]: + count = ctypes.c_ulonglong() + value = sccore.BNSharedCacheStringScannerTakeStrings(self.handle, max_count, count) + if value is None: + return [] + result = [] + for i in range(count.value): + result.append(string_from_api(value[i])) + sccore.BNSharedCacheFreeStringList(value, count) + return result + + def symbol_from_api(symbol: sccore.BNSharedCacheSymbol) -> CacheSymbol: return CacheSymbol( symbol_type=symbol.symbolType, @@ -291,6 +356,9 @@ def symbols(self) -> [CacheSymbol]: sccore.BNSharedCacheFreeSymbolList(value, count) return result + def create_string_scanner(self) -> CacheStringScanner: + return CacheStringScanner(sccore.BNSharedCacheControllerCreateStringScanner(self.handle)) + def _get_shared_cache(instance: binaryninja.PythonScriptingInstance): if instance.interpreter.active_view is None: diff --git a/view/sharedcache/api/python/sharedcache_enums.py b/view/sharedcache/api/python/sharedcache_enums.py index d772adda3..1dd767d52 100644 --- a/view/sharedcache/api/python/sharedcache_enums.py +++ b/view/sharedcache/api/python/sharedcache_enums.py @@ -26,6 +26,13 @@ class SharedCacheRegionType(enum.IntEnum): SharedCacheRegionTypeNonImage = 3 +class StringType(enum.IntEnum): + AsciiString = 0 + Utf16String = 1 + Utf32String = 2 + Utf8String = 3 + + class SymbolBinding(enum.IntEnum): NoBinding = 0 LocalBinding = 1 diff --git a/view/sharedcache/api/sharedcache.cpp b/view/sharedcache/api/sharedcache.cpp index d90f27994..1c239dceb 100644 --- a/view/sharedcache/api/sharedcache.cpp +++ b/view/sharedcache/api/sharedcache.cpp @@ -111,6 +111,19 @@ CacheSymbol SymbolFromApi(BNSharedCacheSymbol apiSymbol) return symbol; } +CacheString StringFromApi(const BNSharedCacheString& apiString) +{ + CacheString string; + string.type = apiString.stringType; + string.address = apiString.address; + string.rawLength = apiString.rawLength; + string.text = apiString.text; + string.regionStart = apiString.regionStart; + if (apiString.imageStart != 0) + string.imageStart = apiString.imageStart; + return string; +} + std::string SharedCacheAPI::GetRegionTypeAsString(const BNSharedCacheRegionType &type) { switch (type) @@ -359,3 +372,77 @@ std::vector SharedCacheController::GetSymbols() const BNSharedCacheFreeSymbolList(symbols, count); return result; } + +CacheStringScanner::CacheStringScanner(BNSharedCacheStringScanner* scanner) : m_object(scanner) {} + + +CacheStringScanner::~CacheStringScanner() +{ + if (m_object) + BNFreeSharedCacheStringScanner(m_object); +} + + +CacheStringScanner::CacheStringScanner(CacheStringScanner&& other) noexcept : m_object(other.m_object) +{ + other.m_object = nullptr; +} + + +CacheStringScanner& CacheStringScanner::operator=(CacheStringScanner&& other) noexcept +{ + if (this != &other) + { + if (m_object) + BNFreeSharedCacheStringScanner(m_object); + m_object = other.m_object; + other.m_object = nullptr; + } + return *this; +} + + +bool CacheStringScanner::Start() +{ + return BNSharedCacheStringScannerStart(m_object); +} + + +bool CacheStringScanner::IsComplete() const +{ + return BNSharedCacheStringScannerIsComplete(m_object); +} + + +std::pair CacheStringScanner::GetProgress() const +{ + uint64_t current = 0; + uint64_t total = 0; + BNSharedCacheStringScannerGetProgress(m_object, ¤t, &total); + return {current, total}; +} + + +uint64_t CacheStringScanner::GetStringCount() const +{ + return BNSharedCacheStringScannerGetStringCount(m_object); +} + + +std::vector CacheStringScanner::TakeStrings(uint64_t maxCount) +{ + size_t count; + BNSharedCacheString* strings = BNSharedCacheStringScannerTakeStrings(m_object, maxCount, &count); + std::vector result; + result.reserve(count); + for (size_t i = 0; i < count; i++) + result.emplace_back(StringFromApi(strings[i])); + BNSharedCacheFreeStringList(strings, count); + return result; +} + + +std::unique_ptr SharedCacheController::CreateStringScanner() const +{ + return std::make_unique(BNSharedCacheControllerCreateStringScanner(m_object)); +} diff --git a/view/sharedcache/api/sharedcacheapi.h b/view/sharedcache/api/sharedcacheapi.h index 070b10f63..e92cd9936 100644 --- a/view/sharedcache/api/sharedcacheapi.h +++ b/view/sharedcache/api/sharedcacheapi.h @@ -3,6 +3,8 @@ #include #include "sharedcachecore.h" +#include + template class DSCRefCountObject { void AddRefInternal() { m_refs.fetch_add(1); } @@ -296,6 +298,38 @@ namespace SharedCacheAPI { std::string GetSymbolTypeAsString(const BNSymbolType& type); + struct CacheString + { + BNStringType type; + uint64_t address; + // Length of the string in the cache, in bytes. + size_t rawLength; + // UTF-8 display text, truncated. + std::string text; + uint64_t regionStart; + std::optional imageStart; + }; + + class CacheStringScanner + { + BNSharedCacheStringScanner* m_object = nullptr; + + public: + explicit CacheStringScanner(BNSharedCacheStringScanner* scanner); + ~CacheStringScanner(); + + CacheStringScanner(const CacheStringScanner&) = delete; + CacheStringScanner& operator=(const CacheStringScanner&) = delete; + CacheStringScanner(CacheStringScanner&& other) noexcept; + CacheStringScanner& operator=(CacheStringScanner&& other) noexcept; + + bool Start(); + bool IsComplete() const; + std::pair GetProgress() const; + uint64_t GetStringCount() const; + std::vector TakeStrings(uint64_t maxCount); + }; + class SharedCacheController : public DSCCoreRefCountObject { public: explicit SharedCacheController(BNSharedCacheController* controller); @@ -330,5 +364,7 @@ namespace SharedCacheAPI { std::vector GetImages() const; std::vector GetLoadedImages() const; std::vector GetSymbols() const; + + std::unique_ptr CreateStringScanner() const; }; } diff --git a/view/sharedcache/api/sharedcachecore.h b/view/sharedcache/api/sharedcachecore.h index f8e9c48bc..fcfb8d062 100644 --- a/view/sharedcache/api/sharedcachecore.h +++ b/view/sharedcache/api/sharedcachecore.h @@ -61,10 +61,19 @@ extern "C" GlobalBinding = 2, WeakBinding = 3, }; + + enum BNStringType : uint8_t + { + AsciiString = 0, + Utf16String = 1, + Utf32String = 2, + Utf8String = 3, + }; #endif typedef struct BNBinaryView BNBinaryView; typedef struct BNSharedCacheController BNSharedCacheController; + typedef struct BNSharedCacheStringScanner BNSharedCacheStringScanner; typedef enum BNSharedCacheEntryType { SharedCacheEntryTypePrimary, @@ -119,6 +128,18 @@ extern "C" char* name; } BNSharedCacheSymbol; + typedef struct BNSharedCacheString { + BNStringType stringType; + uint64_t address; + // Length of the string in the cache, in bytes. + size_t rawLength; + // UTF-8 display text, truncated. + char* text; + uint64_t regionStart; + // NOTE: If not associated with an image this will be zero. + uint64_t imageStart; + } BNSharedCacheString; + SHAREDCACHE_FFI_API BNSharedCacheController* BNGetSharedCacheController(BNBinaryView* data); SHAREDCACHE_FFI_API BNSharedCacheController* BNNewSharedCacheControllerReference(BNSharedCacheController* controller); @@ -166,6 +187,22 @@ extern "C" SHAREDCACHE_FFI_API void BNSharedCacheFreeEntry(BNSharedCacheEntry entry); SHAREDCACHE_FFI_API void BNSharedCacheFreeEntryList(BNSharedCacheEntry* entries, size_t count); + SHAREDCACHE_FFI_API BNSharedCacheStringScanner* BNSharedCacheControllerCreateStringScanner( + BNSharedCacheController* controller); + SHAREDCACHE_FFI_API void BNFreeSharedCacheStringScanner(BNSharedCacheStringScanner* scanner); + SHAREDCACHE_FFI_API bool BNSharedCacheStringScannerStart(BNSharedCacheStringScanner* scanner); + SHAREDCACHE_FFI_API bool BNSharedCacheStringScannerIsComplete(BNSharedCacheStringScanner* scanner); + SHAREDCACHE_FFI_API void BNSharedCacheStringScannerGetProgress( + BNSharedCacheStringScanner* scanner, uint64_t* current, uint64_t* total); + + SHAREDCACHE_FFI_API uint64_t BNSharedCacheStringScannerGetStringCount(BNSharedCacheStringScanner* scanner); + // Removes and returns up to maxCount of the queued scan results. + SHAREDCACHE_FFI_API BNSharedCacheString* BNSharedCacheStringScannerTakeStrings( + BNSharedCacheStringScanner* scanner, uint64_t maxCount, size_t* count); + + SHAREDCACHE_FFI_API void BNSharedCacheFreeString(BNSharedCacheString string); + SHAREDCACHE_FFI_API void BNSharedCacheFreeStringList(BNSharedCacheString* strings, size_t count); + #ifdef __cplusplus } diff --git a/view/sharedcache/core/CacheStringScanner.cpp b/view/sharedcache/core/CacheStringScanner.cpp new file mode 100644 index 000000000..1d07c585b --- /dev/null +++ b/view/sharedcache/core/CacheStringScanner.cpp @@ -0,0 +1,209 @@ +#include "CacheStringScanner.h" + +#include "binaryninjaapi.h" + +using namespace BinaryNinja; +using namespace BinaryNinja::DSC; + +namespace { + +constexpr size_t kChunkSize = 1024 * 1024; + +// Builds the UTF-8 display text for a string found at `data`, truncated to +// `CacheStringScanner::kMaxDisplayTextLength` bytes of output. +std::string DisplayTextForString(const uint8_t* data, const BNStringReference& ref) +{ + constexpr size_t maxLength = CacheStringScanner::kMaxDisplayTextLength; + switch (ref.type) + { + case AsciiString: + case Utf8String: + return std::string(reinterpret_cast(data), std::min(ref.length, maxLength)); + case Utf16String: + { + char* converted = BNUnicodeUTF16ToUTF8(data, std::min(ref.length, maxLength * 2)); + std::string text(converted); + BNFreeString(converted); + if (text.size() > maxLength) + text.resize(maxLength); + return text; + } + case Utf32String: + { + std::string text; + for (size_t offset = 0; offset + 4 <= ref.length && text.size() < maxLength; offset += 4) + { + char* converted = BNUnicodeUTF32ToUTF8(data + offset); + text += converted; + BNFreeString(converted); + } + if (text.size() > maxLength) + text.resize(maxLength); + return text; + } + } + return {}; +} + +} // namespace + +CacheStringScanner::CacheStringScanner(SharedCache& cache, std::regex regionFilter, Ref logger) + : m_cache(cache), m_regionFilter(std::move(regionFilter)), m_logger(std::move(logger)) +{} + +CacheStringScanner::~CacheStringScanner() +{ + // Tell any outstanding scan jobs to bail out. We cannot block waiting for them to + // drain as it would risk a deadlock during exit. + if (m_state) + m_state->abort = true; +} + +bool CacheStringScanner::Start() +{ + if (m_started.exchange(true)) + return false; + + auto state = std::make_shared(); + state->vm = m_cache.GetVirtualMemory(); + state->logger = m_logger; + state->detector.emplace(StringDetectionParameters::FromSettings(Settings::Instance())); + + uint64_t totalBytes = 0; + for (const auto& [range, region] : m_cache.GetRegions()) + { + // Stub islands hold only trampoline code, and the filtered regions (LINKEDIT by default) + // hold symbol tables whose name strings are already presented by the symbols UI. + if (region.type == CacheRegionType::StubIsland) + continue; + if (std::regex_match(region.name, m_regionFilter)) + continue; + state->regions.push_back(region); + totalBytes += region.size; + } + state->bytesTotal = totalBytes; + m_state = state; + + if (state->regions.empty()) + { + state->complete = true; + return true; + } + + { + std::lock_guard lock(state->completionMutex); + state->remainingRegions = state->regions.size(); + } + for (size_t i = 0; i < state->regions.size(); i++) + { + // Scanning starts only when the user opens the Strings tab and is watching it fill, so it + // runs at priority above the normal-priority background analysis of off-screen code. + WorkerPriorityEnqueue([state, i] { + ScanRegion(*state, state->regions[i]); + FinishRegion(*state); + }, "Scanning shared cache strings"); + } + return true; +} + +void CacheStringScanner::FinishRegion(ScanState& state) +{ + std::lock_guard lock(state.completionMutex); + if (--state.remainingRegions == 0) + state.complete = true; +} + +void CacheStringScanner::GetProgress(uint64_t& current, uint64_t& total) const +{ + if (!m_state) + { + current = 0; + total = 0; + return; + } + current = m_state->bytesScanned.load(); + total = m_state->bytesTotal.load(); +} + +size_t CacheStringScanner::GetStringCount() const +{ + if (!m_state) + return 0; + std::lock_guard lock(m_state->resultsMutex); + return m_state->producedCount; +} + +std::vector CacheStringScanner::TakeStrings(size_t maxCount) +{ + if (!m_state) + return {}; + auto& strings = m_state->strings; + std::lock_guard lock(m_state->resultsMutex); + const size_t count = std::min(maxCount, strings.size()); + const size_t first = strings.size() - count; + std::vector result( + std::make_move_iterator(strings.begin() + first), std::make_move_iterator(strings.end())); + strings.erase(strings.begin() + first, strings.end()); + + // Shrink the buffer if it both proportionally and substantially larger than the live data. + static constexpr size_t kMinReclaimableSlack = 1 << 20; + if (strings.capacity() > 2 * strings.size() + kMinReclaimableSlack) + strings.shrink_to_fit(); + + return result; +} + +void CacheStringScanner::ScanRegion(ScanState& state, const CacheRegion& region) +{ + const uint64_t imageStart = region.imageStart.value_or(0); + const uint64_t end = region.start + region.size; + + BNStringReference lastFound {}; + for (uint64_t cur = region.start; cur < end; ) + { + if (state.abort) + return; + + const size_t blockLen = static_cast(std::min(kChunkSize, end - cur)); + const size_t dataLen = static_cast(std::min(blockLen + BN_MAX_STRING_LENGTH, end - cur)); + + std::span data; + try + { + data = state.vm->ReadSpan(cur, dataLen); + } + catch (std::exception& e) + { + // This happens if we have not mapped in all the relevant entries. + state.logger->LogErrorF("Failed to read region {:#x} while scanning for strings: {}", region.start, e.what()); + state.bytesScanned += end - cur; + return; + } + + const auto refs = state.detector->DetectStrings(data.data(), dataLen, blockLen, cur, &lastFound); + + std::vector batch; + batch.reserve(refs.size()); + for (const auto& ref : refs) + { + CacheString str; + str.type = ref.type; + str.address = ref.start; + str.rawLength = ref.length; + str.text = DisplayTextForString(data.data() + (ref.start - cur), ref); + str.regionStart = region.start; + str.imageStart = imageStart; + batch.push_back(std::move(str)); + } + + { + std::lock_guard lock(state.resultsMutex); + state.producedCount += batch.size(); + state.strings.insert(state.strings.end(), std::make_move_iterator(batch.begin()), + std::make_move_iterator(batch.end())); + } + + state.bytesScanned += blockLen; + cur += blockLen; + } +} diff --git a/view/sharedcache/core/CacheStringScanner.h b/view/sharedcache/core/CacheStringScanner.h new file mode 100644 index 000000000..2945497a4 --- /dev/null +++ b/view/sharedcache/core/CacheStringScanner.h @@ -0,0 +1,103 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "binaryninjaapi.h" +#include "SharedCache.h" + +namespace BinaryNinja::DSC { + + // A string found in the cache by `CacheStringScanner`. + struct CacheString + { + BNStringType type; + uint64_t address; + // Length of the string in the cache, in bytes. + size_t rawLength; + // UTF-8 display text, truncated to `CacheStringScanner::kMaxDisplayTextLength` bytes. + std::string text; + // Start address of the region containing the string. + uint64_t regionStart; + // Header address of the image owning the region, or 0 if the region is not part of an image. + uint64_t imageStart; + }; + + // Scans all mapped cache regions for strings using the same string detection as the core + // strings analysis. Each region is scanned as a separate worker pool job. Results queue in job + // completion order. Consumers drain them with `TakeStrings(maxCount)`, so the scanner only + // holds strings that have not yet been handed off. Sort once the scan completes. + class CacheStringScanner + { + public: + static constexpr size_t kMaxDisplayTextLength = 256; + + CacheStringScanner(SharedCache& cache, std::regex regionFilter, Ref logger); + ~CacheStringScanner(); + + CacheStringScanner(const CacheStringScanner&) = delete; + CacheStringScanner& operator=(const CacheStringScanner&) = delete; + + // Enqueues a scan job for each cache region. Idempotent. + // Returns false if the scan was already started. + bool Start(); + + bool IsScanStarted() const { return m_started.load(); } + bool IsScanComplete() const { return m_state && m_state->complete.load(); } + + // Progress in bytes scanned out of total bytes scheduled for scanning. + void GetProgress(uint64_t& current, uint64_t& total) const; + + // Total number of strings detected so far, including those already taken. + size_t GetStringCount() const; + + // Removes and returns up to `maxCount` of the queued strings. Result order is unspecified. + std::vector TakeStrings(size_t maxCount); + + private: + // Everything the scan jobs read and write, owned jointly by the scanner and every queued + // job so that the scanner itself can safely be destroyed while jobs are still queued or + // running. + struct ScanState + { + std::shared_ptr vm; + // The regions to scan, copied out of the cache so the jobs do not reference it. + std::vector regions; + Ref logger; + std::optional detector; + + std::atomic complete = false; + std::atomic abort = false; + + std::mutex completionMutex; + size_t remainingRegions = 0; + + std::atomic bytesScanned = 0; + std::atomic bytesTotal = 0; + + std::mutex resultsMutex; + // Detected strings not yet taken. TakeStrings drains from the back, which is O(1) per + // element. Order does not matter as the consumer sorts the full set once scanning + // completes. + std::vector strings; + // Total detected, including taken strings. Not reduced by TakeStrings. + uint64_t producedCount = 0; + }; + + static void ScanRegion(ScanState& state, const CacheRegion& region); + static void FinishRegion(ScanState& state); + + // Used only by `Start`, which copies the scan inputs into `ScanState`. + SharedCache& m_cache; + std::regex m_regionFilter; + Ref m_logger; + + std::atomic m_started = false; + std::shared_ptr m_state; + }; + +} // namespace BinaryNinja::DSC diff --git a/view/sharedcache/core/SharedCacheController.cpp b/view/sharedcache/core/SharedCacheController.cpp index 8ed011bd3..8f543ff85 100644 --- a/view/sharedcache/core/SharedCacheController.cpp +++ b/view/sharedcache/core/SharedCacheController.cpp @@ -316,3 +316,8 @@ void SharedCacheController::ProcessObjCForLoadedImages(BinaryView& view) } } } + +std::unique_ptr SharedCacheController::CreateStringScanner() +{ + return std::make_unique(m_cache, m_regionFilter, m_logger); +} diff --git a/view/sharedcache/core/SharedCacheController.h b/view/sharedcache/core/SharedCacheController.h index c468bee5e..07c5ce788 100644 --- a/view/sharedcache/core/SharedCacheController.h +++ b/view/sharedcache/core/SharedCacheController.h @@ -1,8 +1,10 @@ #pragma once +#include #include #include +#include "CacheStringScanner.h" #include "SharedCache.h" #include "refcountobject.h" #include "ffi_global.h" @@ -66,5 +68,7 @@ namespace BinaryNinja::DSC { // Re-run the ObjC processor for loaded images to restore Objective-C metadata. void ProcessObjCForLoadedImages(BinaryView& view); + + std::unique_ptr CreateStringScanner(); }; } // namespace BinaryNinja::DSC diff --git a/view/sharedcache/core/ffi.cpp b/view/sharedcache/core/ffi.cpp index 7ad10f9bb..b916972d3 100644 --- a/view/sharedcache/core/ffi.cpp +++ b/view/sharedcache/core/ffi.cpp @@ -4,6 +4,12 @@ using namespace BinaryNinja; using namespace BinaryNinja::DSC; +struct BNSharedCacheStringScanner +{ + DSCRef controller; + std::unique_ptr scanner; +}; + BNSharedCacheImage ImageToApi(const CacheImage& image) { BNSharedCacheImage apiImage; @@ -105,6 +111,18 @@ CacheSymbol SymbolFromApi(const BNSharedCacheSymbol& apiSymbol) return symbol; } +BNSharedCacheString StringToApi(const CacheString& string) +{ + BNSharedCacheString apiString; + apiString.stringType = string.type; + apiString.address = string.address; + apiString.rawLength = string.rawLength; + apiString.text = BNAllocStringWithLength(string.text.c_str(), string.text.size()); + apiString.regionStart = string.regionStart; + apiString.imageStart = string.imageStart; + return apiString; +} + BNSharedCacheEntryType EntryTypeToApi(const CacheEntryType& entryType) { switch (entryType) @@ -431,4 +449,57 @@ extern "C" BNSharedCacheFreeEntry(entries[i]); delete[] entries; } + + BNSharedCacheStringScanner* BNSharedCacheControllerCreateStringScanner(BNSharedCacheController* controller) + { + return new BNSharedCacheStringScanner {controller->object, controller->object->CreateStringScanner()}; + } + + void BNFreeSharedCacheStringScanner(BNSharedCacheStringScanner* scanner) + { + delete scanner; + } + + bool BNSharedCacheStringScannerStart(BNSharedCacheStringScanner* scanner) + { + return scanner->scanner->Start(); + } + + bool BNSharedCacheStringScannerIsComplete(BNSharedCacheStringScanner* scanner) + { + return scanner->scanner->IsScanComplete(); + } + + void BNSharedCacheStringScannerGetProgress(BNSharedCacheStringScanner* scanner, uint64_t* current, uint64_t* total) + { + scanner->scanner->GetProgress(*current, *total); + } + + uint64_t BNSharedCacheStringScannerGetStringCount(BNSharedCacheStringScanner* scanner) + { + return scanner->scanner->GetStringCount(); + } + + BNSharedCacheString* BNSharedCacheStringScannerTakeStrings( + BNSharedCacheStringScanner* scanner, uint64_t maxCount, size_t* count) + { + const auto strings = scanner->scanner->TakeStrings(maxCount); + *count = strings.size(); + BNSharedCacheString* apiStrings = new BNSharedCacheString[*count]; + for (size_t i = 0; i < *count; i++) + apiStrings[i] = StringToApi(strings[i]); + return apiStrings; + } + + void BNSharedCacheFreeString(BNSharedCacheString string) + { + BNFreeString(string.text); + } + + void BNSharedCacheFreeStringList(BNSharedCacheString* strings, size_t count) + { + for (size_t i = 0; i < count; i++) + BNSharedCacheFreeString(strings[i]); + delete[] strings; + } }; diff --git a/view/sharedcache/ui/CMakeLists.txt b/view/sharedcache/ui/CMakeLists.txt index a0450722a..96c24db95 100644 --- a/view/sharedcache/ui/CMakeLists.txt +++ b/view/sharedcache/ui/CMakeLists.txt @@ -4,7 +4,7 @@ project(sharedcacheui) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) -find_package(Qt6 COMPONENTS Core Gui Widgets REQUIRED) +find_package(Qt6 COMPONENTS Core Concurrent Gui Widgets REQUIRED) file(GLOB SOURCES CONFIGURE_DEPENDS *.cpp *.h) list(FILTER SOURCES EXCLUDE REGEX moc_.*) @@ -93,6 +93,6 @@ get_recursive_include_dirs(sharedcacheapi INCLUDES) target_include_directories(sharedcacheui PRIVATE ${INCLUDES}) -target_link_libraries(sharedcacheui sharedcacheapi sharedcache binaryninjaui Qt6::Core Qt6::Gui Qt6::Widgets) +target_link_libraries(sharedcacheui sharedcacheapi sharedcache binaryninjaui Qt6::Core Qt6::Concurrent Qt6::Gui Qt6::Widgets) diff --git a/view/sharedcache/ui/addresstext.h b/view/sharedcache/ui/addresstext.h new file mode 100644 index 000000000..2b339603b --- /dev/null +++ b/view/sharedcache/ui/addresstext.h @@ -0,0 +1,14 @@ +#pragma once + +#include +#include + +// Renders an address the way the triage tables' Address columns display it: lowercase hex, +// zero-padded to `width` digits. +inline std::string AddressText(uint64_t address, uint32_t width) +{ + std::string text(width, '0'); + for (size_t i = text.size(); address != 0 && i > 0; address >>= 4) + text[--i] = "0123456789abcdef"[address & 0xf]; + return text; +} diff --git a/view/sharedcache/ui/dsctriage.cpp b/view/sharedcache/ui/dsctriage.cpp index 1e7ca336a..f40000f38 100644 --- a/view/sharedcache/ui/dsctriage.cpp +++ b/view/sharedcache/ui/dsctriage.cpp @@ -1,8 +1,13 @@ +#include #include +#include #include +#include +#include #include #include "dsctriage.h" #include "globalarea.h" +#include "stringstable.h" #include "symboltable.h" #include "ui/fontsettings.h" @@ -52,8 +57,15 @@ DSCTriageView::DSCTriageView(QWidget* parent, BinaryViewRef data) : QWidget(pare QWidget* defaultWidget = initImageTable(); initSymbolTable(); + initStringsTab(); initCacheInfoTables(); + // The string scan is expensive, so the panel starts its load only once the Strings tab is + // first shown, and clears its large result set after the tab leaves the screen. + connect(m_triageTabs, &SplitTabWidget::currentChanged, this, [this](QWidget* widget) { + m_stringsPanel->setCurrentTabWidget(widget); + }); + m_layout = new QVBoxLayout(this); m_layout->addWidget(m_triageTabs); setLayout(m_layout); @@ -73,7 +85,8 @@ DSCTriageView::~DSCTriageView() } -void DSCTriageView::loadImagesWithAddr(const std::vector& addresses, bool includeDependencies) { +void DSCTriageView::loadImagesWithAddr(const std::vector& addresses, bool includeDependencies, + std::optional navigateTo) { auto controller = SharedCacheController::GetController(*m_data); if (!controller) return; @@ -119,21 +132,24 @@ void DSCTriageView::loadImagesWithAddr(const std::vector& addresses, b // Apply the images in a future than update the triage view and run analysis. QPointer> watcher = new QFutureWatcher(this); - connect(watcher, &QFutureWatcher::finished, this, [watcher, this]() { + connect(watcher, &QFutureWatcher::finished, this, [watcher, navigateTo, this]() { if (watcher) { auto loadedImages = watcher->result(); - if (loadedImages.empty()) - return; - - // Update the triage to display the images as loaded. - for (const auto& image : loadedImages) - setImageLoaded(image.headerAddress); + if (!loadedImages.empty()) + { + // Update the triage to display the images as loaded. + for (const auto& image : loadedImages) + setImageLoaded(image.headerAddress); + + // Run analysis. + this->m_data->AddAnalysisOption("linearsweep"); + this->m_data->AddAnalysisOption("pointersweep"); + this->m_data->UpdateAnalysis(); + } - // Run analysis. - this->m_data->AddAnalysisOption("linearsweep"); - this->m_data->AddAnalysisOption("pointersweep"); - this->m_data->UpdateAnalysis(); + if (navigateTo) + navigateToAddress(*navigateTo); } }); QFuture future = QtConcurrent::run([this, controller, images, imageLoadTask]() { @@ -270,6 +286,9 @@ QWidget* DSCTriageView::initImageTable() connect(loadImageFilterEdit, &FilterEdit::textChanged, [this, loadImageFilterEdit](const QString& filter) { m_imageTable->setFilter(filter.toStdString(), loadImageFilterEdit->getFilterOptions()); }); + connect(loadImageFilterEdit, &FilterEdit::optionsChanged, [this, loadImageFilterEdit](FilterOptions options) { + m_imageTable->setFilter(loadImageFilterEdit->text().toStdString(), options); + }); connect(m_imageTable, &FilterableTableView::activated, this, [=, this](const QModelIndex& index) { auto addr = m_imageModel->item(index.row(), 0)->text().toULongLong(nullptr, 16); @@ -324,6 +343,9 @@ void DSCTriageView::initSymbolTable() connect(symbolFilterEdit, &FilterEdit::textChanged, [this, symbolFilterEdit](const QString& filter) { m_symbolTable->setFilter(filter.toStdString(), symbolFilterEdit->getFilterOptions()); }); + connect(symbolFilterEdit, &FilterEdit::optionsChanged, [this, symbolFilterEdit](FilterOptions options) { + m_symbolTable->setFilter(symbolFilterEdit->text().toStdString(), options); + }); auto loadSymbolImageButton = new QPushButton(); connect(loadSymbolImageButton, &QPushButton::clicked, [this](bool) { @@ -366,7 +388,6 @@ void DSCTriageView::initSymbolTable() connect(m_symbolTable, &SymbolTableView::activated, this, [=, this](const QModelIndex& index){ auto symbol = m_symbolTable->getSymbolAtRow(index.row()); - auto dialog = new QMessageBox(this); auto controller = SharedCacheController::GetController(*this->m_data); if (!controller) @@ -376,13 +397,20 @@ void DSCTriageView::initSymbolTable() if (!image.has_value()) return; + if (controller->IsImageLoaded(*image)) + { + navigateToAddress(symbol.address); + return; + } + + auto dialog = new QMessageBox(this); dialog->setText("Load " + QString::fromStdString(image->name) + "?"); dialog->setStandardButtons(QMessageBox::Yes | QMessageBox::No); connect(dialog, &QMessageBox::buttonClicked, this, [=, this](QAbstractButton* button) { if (button == dialog->button(QMessageBox::Yes)) - loadImagesWithAddr({image->headerAddress}); + loadImagesWithAddr({image->headerAddress}, false, symbol.address); }); dialog->exec(); @@ -393,6 +421,218 @@ void DSCTriageView::initSymbolTable() } +void DSCTriageView::promptToLoadImage(const std::string& imageName, uint64_t address, uint64_t navigateTo) +{ + auto dialog = new QMessageBox(this); + dialog->setText("Load " + QString::fromStdString(imageName) + "?"); + auto loadButton = dialog->addButton("Load Image", QMessageBox::AcceptRole); + dialog->addButton(QMessageBox::Cancel); + dialog->setDefaultButton(loadButton); + + connect(dialog, &QMessageBox::buttonClicked, this, [=, this](QAbstractButton* button) + { + if (button == loadButton) + loadImagesWithAddr({address}, false, navigateTo); + }); + + dialog->exec(); +} + + +void DSCTriageView::initStringsTab() +{ + m_stringsTable = new StringsTableView(this); + m_stringsPanel = new TriageTablePanel(this, m_stringsTable, "Filter strings", "strings"); + m_stringsPanel->setLoader([this] { return startStringScan(); }); + m_stringsPanel->setClearHandler([this] { + m_stringsPollTimer->stop(); + m_stringScanner.reset(); + }); + // Strings hidden by the visibility toggles are not part of the searched population, so they + // do not appear in the count unless a text filter narrows it. + m_stringsPanel->setBaselineCount( + [this] { return m_stringsTable->stringsModel()->baselineStringCount(); }); + + m_stringsPanel->addFilterToggle(":/icons/images/folder.png", "Match Image Names", + [this](bool checked) { m_stringsTable->stringsModel()->setMatchImageNames(checked); }); + // Strings in regions that belong to no image (dyld data and other non-image regions) are + // rarely of interest, so they are hidden unless this is toggled on. + m_stringsPanel->addFilterToggle(":/icons/images/stack.png", "Show Non-Image Strings", + [this](bool checked) { m_stringsTable->stringsModel()->setShowNonImageStrings(checked); }); + + auto loadStringImageButton = m_stringsPanel->addSelectionButton("Load Image"); + connect(loadStringImageButton, &QPushButton::clicked, [this](bool) { + auto selected = m_stringsTable->selectionModel()->selectedRows(); + std::vector imageAddresses; + for (const auto& row : selected) + { + const auto string = m_stringsTable->getStringAtRow(row.row()); + if (string.imageStart) + imageAddresses.push_back(string.address); + else + loadStringRegion(string, std::nullopt); + } + loadImagesWithAddr(imageAddresses); + }); + + connect(m_stringsTable, &StringsTableView::activated, this, [this](const QModelIndex& index) { + const auto string = m_stringsTable->getStringAtRow(index.row()); + auto controller = SharedCacheController::GetController(*this->m_data); + if (!controller) + return; + + // Strings outside any image (e.g. the coalesced selector pool) load just their region. + if (!string.imageStart) + { + loadStringRegion(string, string.address); + return; + } + + auto image = controller->GetImageAt(*string.imageStart); + if (!image.has_value()) + return; + + if (controller->IsImageLoaded(*image)) + { + navigateToAddress(string.address); + return; + } + + promptToLoadImage(image->name, string.address, string.address); + }); + + m_stringsPollTimer = new QTimer(this); + m_stringsPollTimer->setInterval(250); + connect(m_stringsPollTimer, &QTimer::timeout, this, &DSCTriageView::pollStringScan); + + m_triageTabs->addTab(m_stringsPanel, "Strings"); + m_triageTabs->setCanCloseTab(m_stringsPanel, false); +} + + +void DSCTriageView::showEvent(QShowEvent* event) +{ + QWidget::showEvent(event); + m_stringsPanel->setViewVisible(true); +} + + +void DSCTriageView::hideEvent(QHideEvent* event) +{ + QWidget::hideEvent(event); + m_stringsPanel->setViewVisible(false); +} + + +bool DSCTriageView::startStringScan() +{ + // The controller is not available until view init has finished. Retry on the next activation. + auto controller = SharedCacheController::GetController(*m_data); + if (!controller) + return false; + + m_stringsTable->setNameSources(*controller); + m_stringScanner = controller->CreateStringScanner(); + m_stringScanner->Start(); + m_stringsPanel->statusLabel()->setText("Scanning…"); + m_stringsPollTimer->start(); + return true; +} + + +void DSCTriageView::pollStringScan() +{ + if (!m_stringScanner) + return; + + auto model = m_stringsTable->stringsModel(); + constexpr uint64_t maxBatchSize = 250000; + constexpr qint64 maxIngestMilliseconds = 150; + // Ingest batches until the time budget is spent, then yield so the UI stays responsive. + QElapsedTimer ingestTimer; + ingestTimer.start(); + do + { + auto batch = m_stringScanner->TakeStrings(maxBatchSize); + if (batch.empty()) + break; + model->appendStrings(std::move(batch)); + } while (ingestTimer.elapsed() < maxIngestMilliseconds); + + const bool scanComplete = m_stringScanner->IsComplete(); + const QLocale locale; + if (scanComplete && model->totalRowCount() == m_stringScanner->GetStringCount()) + { + m_stringsPollTimer->stop(); + m_stringScanner.reset(); + m_stringsPanel->finishLoad(); + } + else if (scanComplete) + { + // The scan has finished but the table is still ingesting batched results. + const QString totalStrings = locale.toString(static_cast(m_stringScanner->GetStringCount())); + const QString fetchedStrings = + locale.toString(static_cast(model->totalRowCount())).rightJustified(totalStrings.size()); + m_stringsPanel->statusLabel()->setText(QString("Loading… %1 / %2 strings").arg(fetchedStrings, totalStrings)); + } + else + { + const auto [current, total] = m_stringScanner->GetProgress(); + const QString totalMB = locale.toString(static_cast(total / (1024 * 1024))); + // Pad the growing values so the label width stays stable while scanning. + const QString currentMB = + locale.toString(static_cast(current / (1024 * 1024))).rightJustified(totalMB.size()); + const QString stringCount = + locale.toString(static_cast(model->totalRowCount())).rightJustified(10); + m_stringsPanel->statusLabel()->setText( + QString("Scanning… %1 / %2 MB — %3 strings").arg(currentMB, totalMB, stringCount)); + } +} + + +void DSCTriageView::loadStringRegion(const CacheString& string, std::optional navigateTo) +{ + auto controller = SharedCacheController::GetController(*m_data); + if (!controller) + return; + + auto region = controller->GetRegionAt(string.regionStart); + if (!region.has_value()) + return; + + QPointer> watcher = new QFutureWatcher(this); + connect(watcher, &QFutureWatcher::finished, this, [watcher, navigateTo, this]() { + if (!watcher) + return; + if (watcher->result()) + m_data->UpdateAnalysis(); + if (navigateTo) + navigateToAddress(*navigateTo); + }); + QFuture future = QtConcurrent::run([this, controller, region]() { + return controller->ApplyRegion(*this->m_data, *region); + }); + watcher->setFuture(future); + connect(this, &QObject::destroyed, this, [watcher]() { + if (watcher && watcher->isRunning()) { + watcher->cancel(); + watcher->waitForFinished(); + } + }); +} + + +void DSCTriageView::navigateToAddress(uint64_t address) +{ + ViewFrame* frame = ViewFrame::viewFrameForWidget(this); + if (!frame) + return; + // Navigating the frame's current view would hit DSCTriageView::navigate, which is not + // navigable. Switch to the linear view of the cache instead. + frame->navigate("Linear:" + frame->getCurrentDataType(), address, true, true); +} + + void DSCTriageView::initCacheInfoTables() { auto cacheInfoWidget = new QWidget; @@ -457,6 +697,9 @@ void DSCTriageView::initCacheInfoTables() connect(mappingFilterEdit, &FilterEdit::textChanged, [this, mappingFilterEdit](const QString& filter) { m_mappingTable->setFilter(filter.toStdString(), mappingFilterEdit->getFilterOptions()); }); + connect(mappingFilterEdit, &FilterEdit::optionsChanged, [this, mappingFilterEdit](FilterOptions options) { + m_mappingTable->setFilter(mappingFilterEdit->text().toStdString(), options); + }); auto mappingHeaderLayout = new QHBoxLayout; mappingHeaderLayout->addWidget(mappingLabel); @@ -470,6 +713,9 @@ void DSCTriageView::initCacheInfoTables() connect(regionFilterEdit, &FilterEdit::textChanged, [this, regionFilterEdit](const QString& filter) { m_regionTable->setFilter(filter.toStdString(), regionFilterEdit->getFilterOptions()); }); + connect(regionFilterEdit, &FilterEdit::optionsChanged, [this, regionFilterEdit](FilterOptions options) { + m_regionTable->setFilter(regionFilterEdit->text().toStdString(), options); + }); auto regionHeaderLayout = new QHBoxLayout; regionHeaderLayout->addWidget(regionLabel); diff --git a/view/sharedcache/ui/dsctriage.h b/view/sharedcache/ui/dsctriage.h index 03eea6be1..b312c9575 100644 --- a/view/sharedcache/ui/dsctriage.h +++ b/view/sharedcache/ui/dsctriage.h @@ -8,7 +8,9 @@ #include #include #include +#include #include "filter.h" +#include "stringstable.h" #include "symboltable.h" #include "ui/fontsettings.h" #include "uicontext.h" @@ -192,6 +194,11 @@ class DSCTriageView : public QWidget, public View, public UIContextNotification SymbolTableView* m_symbolTable; + StringsTableView* m_stringsTable; + TriageTablePanel* m_stringsPanel; + QTimer* m_stringsPollTimer; + std::unique_ptr m_stringScanner; + FilterableTableView* m_regionTable; FilterableTableView* m_mappingTable; @@ -212,11 +219,22 @@ class DSCTriageView : public QWidget, public View, public UIContextNotification void OnAfterOpenFile(UIContext* context, FileContext* file, ViewFrame* frame) override; void RefreshData(); +protected: + void showEvent(QShowEvent* event) override; + void hideEvent(QHideEvent* event) override; + private: - void loadImagesWithAddr(const std::vector& addresses, bool includeDependencies = false); + void loadImagesWithAddr(const std::vector& addresses, bool includeDependencies = false, + std::optional navigateTo = std::nullopt); void setImageLoaded(uint64_t imageHeaderAddr); + void navigateToAddress(uint64_t address); QWidget* initImageTable(); void initSymbolTable(); + void initStringsTab(); + bool startStringScan(); + void pollStringScan(); + void promptToLoadImage(const std::string& imageName, uint64_t address, uint64_t navigateTo); + void loadStringRegion(const SharedCacheAPI::CacheString& string, std::optional navigateTo); void initCacheInfoTables(); }; diff --git a/view/sharedcache/ui/stringstable.cpp b/view/sharedcache/ui/stringstable.cpp new file mode 100644 index 000000000..767f06109 --- /dev/null +++ b/view/sharedcache/ui/stringstable.cpp @@ -0,0 +1,333 @@ +#include "stringstable.h" + +#include +#include +#include + +#include "addresstext.h" +#include "theme.h" + +using namespace SharedCacheAPI; + +namespace { + +enum StringsTableColumn +{ + StringsTableAddressColumn, + StringsTableTypeColumn, + StringsTableLengthColumn, + StringsTableImageColumn, + StringsTableStringColumn, + StringsTableColumnCount, +}; + +QString StringTypeAsString(BNStringType type) +{ + switch (type) + { + case AsciiString: + return QStringLiteral("ASCII"); + case Utf8String: + return QStringLiteral("UTF-8"); + case Utf16String: + return QStringLiteral("UTF-16"); + case Utf32String: + return QStringLiteral("UTF-32"); + default: + return QStringLiteral("Unknown"); + } +} + +QString DisplayTextForString(const CacheString& string) +{ + QString text = QString::fromUtf8(string.text.c_str(), string.text.size()); + text.replace('\n', QStringLiteral("\\n")); + text.replace('\r', QStringLiteral("\\r")); + text.replace('\t', QStringLiteral("\\t")); + return text; +} + + +QString ImageNameForString(const ImageNameLookup::State& names, const CacheString& string) +{ + return ImageNameLookup::displayName(names, string.imageStart, string.regionStart); +} + +} // namespace + + +StringsTableModel::StringsTableModel(QWidget* parent) : TriageTableRowsModel(parent) +{ + // Establish the initial row visibility. Non-image strings are hidden by default. + applyFilter(); +} + + +int StringsTableModel::columnCount(const QModelIndex& parent) const +{ + Q_UNUSED(parent); + return StringsTableColumnCount; +} + + +QVariant StringsTableModel::data(const QModelIndex& index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + switch (role) + { + case Qt::DisplayRole: + { + const auto& string = stringAt(index.row()); + + switch (index.column()) + { + case StringsTableAddressColumn: + return QString::fromStdString(AddressText(string.address, m_addressWidth)); + case StringsTableTypeColumn: + return StringTypeAsString(string.type); + case StringsTableLengthColumn: + return QString::number(string.rawLength); + case StringsTableImageColumn: + return ImageNameForString(*m_names.snapshot(), string); + case StringsTableStringColumn: + return DisplayTextForString(string); + default: + return QVariant(); + } + } + case Qt::ForegroundRole: + switch (index.column()) + { + case StringsTableAddressColumn: + return getThemeColor(AddressColor); + case StringsTableTypeColumn: + return getThemeColor(TypeNameColor); + case StringsTableLengthColumn: + return getThemeColor(NumberColor); + case StringsTableStringColumn: + return getThemeColor(StringColor); + default: + return QVariant(); + } + case Qt::ToolTipRole: + { + if (index.column() != StringsTableImageColumn) + return QVariant(); + const auto& string = stringAt(index.row()); + if (QString tooltip = m_names.tooltip(string.imageStart, string.regionStart); !tooltip.isEmpty()) + return tooltip; + return QVariant(); + } + case Qt::FontRole: + return m_font; + default: + return QVariant(); + } +} + + +QVariant StringsTableModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (role != Qt::DisplayRole || orientation != Qt::Horizontal) + return QVariant(); + + switch (section) + { + case StringsTableAddressColumn: + return QString("Address"); + case StringsTableTypeColumn: + return QString("Type"); + case StringsTableLengthColumn: + return QString("Length"); + case StringsTableImageColumn: + return QString("Image"); + case StringsTableStringColumn: + return QString("String"); + default: + return QVariant(); + } +} + + +void StringsTableModel::sort(int column, Qt::SortOrder order) +{ + Comparator comparator; + + switch (column) + { + case StringsTableAddressColumn: + comparator = [](const CacheString& a, const CacheString& b) { + return a.address < b.address; + }; + break; + case StringsTableTypeColumn: + comparator = [](const CacheString& a, const CacheString& b) { + return a.type < b.type; + }; + break; + case StringsTableLengthColumn: + comparator = [](const CacheString& a, const CacheString& b) { + return a.rawLength < b.rawLength; + }; + break; + case StringsTableImageColumn: + { + // Rank the known image and region names once so that row comparisons are integer-only + // instead of repeated string comparisons. + auto namesSnapshot = m_names.snapshot(); + struct NameEntry + { + const QString* name; + bool isImage; + uint64_t key; + }; + std::vector entries; + entries.reserve(namesSnapshot->imageNames.size() + namesSnapshot->regionNames.size()); + for (const auto& [address, name] : namesSnapshot->imageNames) + entries.push_back({&name, true, address}); + for (const auto& [start, name] : namesSnapshot->regionNames) + entries.push_back({&name, false, start}); + std::sort(entries.begin(), entries.end(), + [](const NameEntry& a, const NameEntry& b) { return *a.name < *b.name; }); + + struct NameRanks + { + std::unordered_map imageRanks; + std::unordered_map regionRanks; + }; + auto ranks = std::make_shared(); + int rank = -1; + const QString* previousName = nullptr; + for (const auto& entry : entries) + { + // Identical names share a rank, matching equality under name comparison. + if (!previousName || *entry.name != *previousName) + { + rank++; + previousName = entry.name; + } + (entry.isImage ? ranks->imageRanks : ranks->regionRanks)[entry.key] = rank; + } + + comparator = [ranks](const CacheString& a, const CacheString& b) { + // Mirrors the Image column's name resolution. Unknown names rank first like + // the empty string. + const auto rankOf = [&ranks](const CacheString& string) { + if (string.imageStart) + { + if (auto it = ranks->imageRanks.find(*string.imageStart); it != ranks->imageRanks.end()) + return it->second; + } + if (auto it = ranks->regionRanks.find(string.regionStart); it != ranks->regionRanks.end()) + return it->second; + return -1; + }; + return rankOf(a) < rankOf(b); + }; + break; + } + case StringsTableStringColumn: + comparator = [](const CacheString& a, const CacheString& b) { + return a.text < b.text; + }; + break; + default: + return; + } + + sortRows(std::move(comparator), order); +} + + +void StringsTableModel::setNameSources(const SharedCacheController& controller) +{ + m_names.build(controller); + m_addressWidth = BNGetAddressRenderedWidth(m_names.maxAddress()); +} + + +void StringsTableModel::appendStrings(std::vector strings) +{ + m_imageStringCount += std::ranges::count_if( + strings, [](const CacheString& string) { return string.imageStart.has_value(); }); + appendRows(std::move(strings)); +} + + +void StringsTableModel::clearRows() +{ + m_imageStringCount = 0; + TriageTableRowsModel::clearRows(); +} + + +void StringsTableModel::setShowNonImageStrings(bool show) +{ + if (m_showNonImageStrings == show) + return; + m_showNonImageStrings = show; + applyFilter(); +} + + +bool StringsTableModel::rowsEquivalent(const CacheString& a, const CacheString& b) const +{ + return a.address == b.address; +} + + +void StringsTableModel::applyFilter() +{ + if (m_filterText.empty() && m_showNonImageStrings) + { + m_rows.setFilter(nullptr); + return; + } + + const auto snapshot = filterSnapshot(); + const uint32_t addressWidth = m_addressWidth; + const auto names = m_names.snapshot(); + + m_rows.setFilterFactory( + [snapshot, addressWidth, names, showNonImageStrings = m_showNonImageStrings]() -> Predicate { + FilterParams params = MakeFilterParams(snapshot); + return [params = std::move(params), addressWidth, names, showNonImageStrings](const CacheString& string) { + if (!showNonImageStrings && !string.imageStart) + return false; + if (params.text.empty()) + return true; + QString imageName; + if (params.matchImageNames) + imageName = ImageNameForString(*names, string); + return MatchesText(params, string.text, string.address, addressWidth, imageName); + }; + }); +} + + +StringsTableView::StringsTableView(QWidget* parent) : TriageTableView(parent) +{ + m_model = new StringsTableModel(this); + setTriageModel(m_model, StringsTableAddressColumn); + applyDefaultColumnWidths(); +} + + +void StringsTableView::setNameSources(const SharedCacheController& controller) +{ + m_model->setNameSources(controller); + applyDefaultColumnWidths(); +} + + +void StringsTableView::applyDefaultColumnWidths() +{ + fitColumn(StringsTableAddressColumn, {QString(m_model->addressWidth(), QChar('0'))}); + fitColumn(StringsTableTypeColumn, + {StringTypeAsString(AsciiString), StringTypeAsString(Utf8String), StringTypeAsString(Utf16String), + StringTypeAsString(Utf32String)}); + fitColumn(StringsTableLengthColumn, {}); + fitColumn(StringsTableImageColumn, {m_model->names().widestImageName()}); +} diff --git a/view/sharedcache/ui/stringstable.h b/view/sharedcache/ui/stringstable.h new file mode 100644 index 000000000..ceaa4bdfa --- /dev/null +++ b/view/sharedcache/ui/stringstable.h @@ -0,0 +1,68 @@ +#pragma once + +#include "triagetable.h" + + +class StringsTableModel : public TriageTableRowsModel +{ + ImageNameLookup m_names; + bool m_showNonImageStrings = false; + size_t m_imageStringCount = 0; + +protected: + void applyFilter() override; + bool rowsEquivalent( + const SharedCacheAPI::CacheString& a, const SharedCacheAPI::CacheString& b) const override; + +public: + explicit StringsTableModel(QWidget* parent); + + int columnCount(const QModelIndex& parent) const override; + QVariant data(const QModelIndex& index, int role) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + void sort(int column, Qt::SortOrder order) override; + + // Build the Image column lookup tables. + void setNameSources(const SharedCacheAPI::SharedCacheController& controller); + + const ImageNameLookup& names() const { return m_names; } + + // Append a batch of scan results, filtering only the new rows. + void appendStrings(std::vector strings); + + void clearRows() override; + + // Whether strings in regions not belonging to any image are shown. + void setShowNonImageStrings(bool show); + + // Count of strings eligible for display under the current visibility options, ignoring any + // text filter. + size_t baselineStringCount() const + { + return m_showNonImageStrings ? totalRowCount() : m_imageStringCount; + } + + const SharedCacheAPI::CacheString& stringAt(int row) const { return rowAt(row); } +}; + + +class StringsTableView : public TriageTableView +{ + StringsTableModel* m_model; + +public: + explicit StringsTableView(QWidget* parent); + + StringsTableModel* stringsModel() const { return m_model; } + + // Build the Image column lookup tables and refit the default column widths. + void setNameSources(const SharedCacheAPI::SharedCacheController& controller); + + SharedCacheAPI::CacheString getStringAtRow(int row) const + { + return m_model->stringAt(row); + } + +protected: + void applyDefaultColumnWidths() override; +}; diff --git a/view/sharedcache/ui/triagetable.cpp b/view/sharedcache/ui/triagetable.cpp new file mode 100644 index 000000000..d70c60820 --- /dev/null +++ b/view/sharedcache/ui/triagetable.cpp @@ -0,0 +1,530 @@ +#include "triagetable.h" + +#include +#include + +#include + +#include "addresstext.h" +#include "theme.h" +#include "ui/fontsettings.h" + +using namespace SharedCacheAPI; + + +TriageTableModel::TriageTableModel(QWidget* parent) : QAbstractTableModel(parent) +{ + // TODO: Need to implement updating this font if it is changed by the user + m_font = getMonospaceFont(parent); +} + + +TriageTableModel::FilterParams TriageTableModel::MakeFilterParams(const FilterSnapshot& snapshot) +{ + return {snapshot.text, snapshot.options, + QRegularExpression(QString::fromStdString(snapshot.text), + snapshot.options.testFlag(CaseSensitiveOption) ? QRegularExpression::NoPatternOption + : QRegularExpression::CaseInsensitiveOption), + snapshot.matchImageNames}; +} + + +bool TriageTableModel::MatchesText(const FilterParams& params, const std::string& text, + uint64_t address, uint32_t addressWidth, const QString& imageName) +{ + if (params.options.testFlag(UseRegexOption)) + { + if (params.regex.match(QString::fromUtf8(text.c_str(), text.size())).hasMatch()) + return true; + if (params.regex.match(QString::fromStdString(AddressText(address, addressWidth))).hasMatch()) + return true; + return params.matchImageNames && params.regex.match(imageName).hasMatch(); + } + + const bool caseSensitive = params.options.testFlag(CaseSensitiveOption); + const auto contains = [&](const std::string& haystack) { + if (caseSensitive) + return haystack.find(params.text) != std::string::npos; + const auto lower = [](char c) { + return std::tolower(static_cast(c)); + }; + auto it = std::search(haystack.begin(), haystack.end(), params.text.begin(), params.text.end(), + [lower](char c1, char c2) { return lower(c1) == lower(c2); }); + return it != haystack.end(); + }; + + if (contains(text)) + return true; + if (contains(AddressText(address, addressWidth))) + return true; + return params.matchImageNames && contains(imageName.toStdString()); +} + + +void ImageNameLookup::build(const SharedCacheController& controller) +{ + auto state = std::make_shared(); + for (const auto& image : controller.GetImages()) + { + const auto lastSlash = image.name.find_last_of('/'); + const auto baseName = lastSlash == std::string::npos ? image.name : image.name.substr(lastSlash + 1); + state->imageNames[image.headerAddress] = QString::fromStdString(baseName); + state->imagePaths[image.headerAddress] = QString::fromStdString(image.name); + } + for (const auto& region : controller.GetRegions()) + { + state->regionNames[region.start] = QString::fromStdString(region.name); + state->maxAddress = std::max(state->maxAddress, region.start + region.size); + } + m_state = std::move(state); +} + + +QString ImageNameLookup::displayName(const State& state, std::optional imageStart, + uint64_t regionStart) +{ + if (imageStart) + { + if (auto it = state.imageNames.find(*imageStart); it != state.imageNames.end()) + return it->second; + } + if (auto it = state.regionNames.find(regionStart); it != state.regionNames.end()) + return it->second; + return {}; +} + + +QString ImageNameLookup::tooltip(const State& state, std::optional imageStart, + uint64_t regionStart) +{ + if (imageStart) + { + if (auto it = state.imagePaths.find(*imageStart); it != state.imagePaths.end()) + return it->second; + } + if (auto it = state.regionNames.find(regionStart); it != state.regionNames.end()) + return it->second; + return {}; +} + + +QString ImageNameLookup::displayName(std::optional imageStart, uint64_t regionStart) const +{ + return displayName(*m_state, imageStart, regionStart); +} + + +QString ImageNameLookup::tooltip(std::optional imageStart, uint64_t regionStart) const +{ + return tooltip(*m_state, imageStart, regionStart); +} + + +QString ImageNameLookup::widestImageName() const +{ + QString widest; + for (const auto& [_, name] : m_state->imageNames) + if (name.size() > widest.size()) + widest = name; + return widest; +} + + +void TriageTableModel::setFilter(const std::string& text, FilterOptions options) +{ + m_filterText = text; + m_filterOptions = options; + applyFilter(); +} + + +void TriageTableModel::setMatchImageNames(bool match) +{ + if (m_matchImageNames == match) + return; + m_matchImageNames = match; + if (!m_filterText.empty()) + applyFilter(); +} + + +TriageTableView::TriageTableView(QWidget* parent) : QTableView(parent) +{ + setFont(getMonospaceFont(this)); + setWordWrap(false); + verticalHeader()->setVisible(false); + + // Match the dense row sizing of the regular strings and symbols views. + const int charHeight = (int)(QFontMetricsF(getMonospaceFont(this)).height() + getExtraFontSpacing()); + verticalHeader()->setDefaultSectionSize(charHeight); + verticalHeader()->setSectionResizeMode(QHeaderView::Fixed); + horizontalHeader()->setStretchLastSection(true); + // With row selection, the default section highlighting bolds every column header whenever a + // row is selected. + horizontalHeader()->setHighlightSections(false); + setShowGrid(false); + + setEditTriggers(QAbstractItemView::NoEditTriggers); + setSelectionBehavior(QAbstractItemView::SelectRows); + setSelectionMode(QAbstractItemView::SingleSelection); +} + + +void TriageTableView::setTriageModel(TriageTableModel* model, int sortColumn) +{ + m_model = model; + setModel(model); + + sortByColumn(sortColumn, Qt::AscendingOrder); + setSortingEnabled(true); + + m_busyOverlay = new QLabel(viewport()); + m_busyOverlay->setAlignment(Qt::AlignCenter); + m_busyOverlay->setAutoFillBackground(true); + m_busyOverlay->setContentsMargins(16, 8, 16, 8); + m_busyOverlay->hide(); + const auto updateOverlay = [this](bool) { + if (m_model->isSorting()) + m_busyOverlay->setText(QStringLiteral("Sorting…")); + else if (m_model->isFiltering()) + m_busyOverlay->setText(QStringLiteral("Filtering…")); + m_busyOverlay->setVisible(m_model->isSorting() || m_model->isFiltering()); + positionBusyOverlay(); + }; + connect(m_model, &TriageTableModel::filteringChanged, this, updateOverlay); + connect(m_model, &TriageTableModel::sortingChanged, this, updateOverlay); +} + + +void TriageTableView::fitColumn(int column, const std::vector& contents) +{ + const QFontMetricsF metrics(getMonospaceFont(this)); + // Room for the header's sort indicator. + const int padding = (int)metrics.horizontalAdvance(QStringLiteral(" ")); + qreal width = metrics.horizontalAdvance( + m_model->headerData(column, Qt::Horizontal, Qt::DisplayRole).toString()); + for (const auto& content : contents) + width = std::max(width, metrics.horizontalAdvance(content)); + setColumnWidth(column, (int)width + padding); +} + + +void TriageTableView::savePosition() +{ + const auto current = selectionModel()->currentIndex(); + const auto top = indexAt(QPoint(0, 0)); + m_model->saveRowIdentities(current.isValid() ? current.row() : -1, top.isValid() ? top.row() : -1); +} + + +void TriageTableView::restorePosition() +{ + const auto [selectedRow, topRow] = m_model->takeSavedRows(); + if (selectedRow >= 0) + selectionModel()->setCurrentIndex(m_model->index(selectedRow, 0), + QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); + if (topRow >= 0) + scrollTo(m_model->index(topRow, 0), QAbstractItemView::PositionAtTop); +} + + +void TriageTableView::setFilter(const std::string& filter, FilterOptions options) +{ + m_model->setFilter(filter, options); +} + + +void TriageTableView::scrollToFirstItem() +{ + if (model()->rowCount() > 0) + { + QModelIndex top = indexAt(rect().topLeft()); + if (top.isValid()) + scrollTo(top); + } +} + + +void TriageTableView::scrollToCurrentItem() +{ + QModelIndex currentIndex = selectionModel()->currentIndex(); + if (currentIndex.isValid()) + scrollTo(currentIndex); +} + + +void TriageTableView::ensureSelection() +{ + QModelIndex current = selectionModel()->currentIndex(); + if (current.isValid() || model()->rowCount() == 0) + return; + + if (auto top = indexAt(rect().topLeft()); top.isValid()) + { + selectionModel()->select(top, QItemSelectionModel::ClearAndSelect); + setCurrentIndex(top); + } +} + + +void TriageTableView::activateSelection() +{ + ensureSelection(); + if (auto current = selectionModel()->currentIndex(); current.isValid()) + emit activated(current); +} + + +void TriageTableView::resizeEvent(QResizeEvent* event) +{ + QTableView::resizeEvent(event); + if (m_busyOverlay) + positionBusyOverlay(); +} + + +void TriageTableView::positionBusyOverlay() +{ + m_busyOverlay->adjustSize(); + m_busyOverlay->move((viewport()->width() - m_busyOverlay->width()) / 2, + (viewport()->height() - m_busyOverlay->height()) / 2); +} + + +TriageTablePanel::TriageTablePanel(QWidget* parent, TriageTableView* table, + const QString& filterPlaceholder, QString rowNoun) + : QWidget(parent), m_table(table), m_rowNoun(std::move(rowNoun)) +{ + m_filterEdit = new FilterEdit(table); + m_filterEdit->setPlaceholderText(filterPlaceholder); + m_filterEdit->showRegexToggle(true); + connect(m_filterEdit, &FilterEdit::textChanged, this, &TriageTablePanel::applyFilter); + connect(m_filterEdit, &FilterEdit::optionsChanged, this, &TriageTablePanel::applyFilter); + + m_statusLabel = new QLabel(this); + m_statusLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + // Tabular numbers keep the status text from shifting as its numbers change. + QFont statusFont = m_statusLabel->font(); + statusFont.setFeature("tnum", 1); + m_statusLabel->setFont(statusFont); + + m_footerLayout = new QHBoxLayout; + m_footerLayout->addWidget(m_statusLabel); + m_footerLayout->setAlignment(Qt::AlignLeft); + + auto layout = new QVBoxLayout(this); + layout->addWidget(m_filterEdit); + layout->addWidget(m_table); + layout->addLayout(m_footerLayout); + + // Frees the loaded content once the tab has been hidden for this long. + constexpr int clearDelayMs = 60 * 1000; + m_clearTimer = new QTimer(this); + m_clearTimer->setInterval(clearDelayMs); + m_clearTimer->setSingleShot(true); + connect(m_clearTimer, &QTimer::timeout, this, &TriageTablePanel::clearContent); + + const auto model = m_table->triageModel(); + connect(model, &TriageTableModel::filteringChanged, this, [this](bool) { updateStatusLabel(); }); + connect(model, &TriageTableModel::sortingChanged, this, [this](bool active) { + updateStatusLabel(); + if (!active) + restoreWhenIdle(); + }); +} + + +void TriageTablePanel::applyFilter() +{ + const QString text = m_filterEdit->text(); + const FilterOptions options = m_filterEdit->getFilterOptions(); + if (options.testFlag(UseRegexOption)) + { + const QRegularExpression regex(text); + if (!regex.isValid()) + { + m_filterEdit->setRegexValidationError(regex.errorString()); + return; + } + } + + m_filterEdit->setRegexValidationError({}); + m_table->setFilter(text.toStdString(), options); +} + + +void TriageTablePanel::addFooterWidget(QWidget* widget) +{ + m_footerLayout->insertWidget(m_footerLayout->count() - 1, widget); +} + + +QAction* TriageTablePanel::addFilterToggle(const QString& iconPath, const QString& toolTip, + std::function onToggled) +{ + QPixmap offPixmap; + pixmapForBWMaskIcon(iconPath, &offPixmap); + QPixmap onPixmap; + pixmapForBWMaskIcon(iconPath, &onPixmap, m_filterEdit->palette().color(QPalette::Highlight), "filterOn"); + auto action = m_filterEdit->addAction(QIcon(offPixmap), QLineEdit::TrailingPosition); + action->setCheckable(true); + action->setToolTip(toolTip); + connect(action, &QAction::toggled, this, + [action, offIcon = QIcon(offPixmap), onIcon = QIcon(onPixmap), + onToggled = std::move(onToggled)](bool checked) { + action->setIcon(checked ? onIcon : offIcon); + onToggled(checked); + }); + return action; +} + + +QPushButton* TriageTablePanel::addSelectionButton(const QString& text) +{ + auto button = new QPushButton(text); + button->setEnabled(false); + const auto update = [this, button] { + button->setEnabled(m_table->selectionModel()->hasSelection()); + }; + connect(m_table->selectionModel(), &QItemSelectionModel::selectionChanged, button, update); + connect(m_table->triageModel(), &QAbstractItemModel::modelReset, button, update); + addFooterWidget(button); + return button; +} + + +void TriageTablePanel::setLoader(std::function loader) +{ + m_loader = std::move(loader); +} + + +void TriageTablePanel::setClearHandler(std::function handler) +{ + m_clearHandler = std::move(handler); +} + + +void TriageTablePanel::setBaselineCount(std::function baselineCount) +{ + m_baselineCount = std::move(baselineCount); +} + + +void TriageTablePanel::setViewVisible(bool visible) +{ + m_viewVisible = visible; + updateActive(); +} + + +void TriageTablePanel::setCurrentTabWidget(QWidget* current) +{ + m_tabCurrent = (current == this); + updateActive(); +} + + +void TriageTablePanel::updateActive() +{ + // The content is on screen only when the triage view is visible and this tab is current. + if (m_viewVisible && m_tabCurrent) + { + m_clearTimer->stop(); + if (!m_loadStarted && m_loader) + { + m_loadStarted = m_loader(); + // The loader owns the status label, for its progress text, until the load finishes. + m_loaderOwnsStatus = m_loadStarted; + } + } + else if (m_loadStarted && !m_clearTimer->isActive()) + { + // Begin the countdown from when the content first went off screen. Later transitions + // while still off screen must not extend it. + m_clearTimer->start(); + } +} + + +void TriageTablePanel::clearContent() +{ + if (!m_loadStarted) + return; + + // Clearing abandons any running sort, whose end signal must not consume the saved position. + m_restorePending = false; + if (m_clearHandler) + m_clearHandler(); + m_table->savePosition(); + m_table->triageModel()->clearRows(); + m_loadStarted = false; + m_loaderOwnsStatus = false; + m_statusLabel->setText(""); +} + + +void TriageTablePanel::resetContent() +{ + clearContent(); + updateActive(); +} + + +void TriageTablePanel::finishLoad() +{ + // Restore the saved position once the sort over the now complete content commits. + m_restorePending = true; + m_table->sortByColumn(m_table->horizontalHeader()->sortIndicatorSection(), + m_table->horizontalHeader()->sortIndicatorOrder()); + m_loaderOwnsStatus = false; + updateStatusLabel(); +} + + +void TriageTablePanel::updateStatusLabel() +{ + if (!m_loadStarted || m_loaderOwnsStatus) + return; + + const auto model = m_table->triageModel(); + if (model->isSorting()) + { + m_statusLabel->setText(QStringLiteral("Sorting…")); + return; + } + if (model->isFiltering()) + { + m_statusLabel->setText(QStringLiteral("Filtering…")); + return; + } + + const QLocale locale; + const auto shown = static_cast(model->rowCount(QModelIndex())); + if (!model->hasTextFilter()) + { + m_statusLabel->setText(QString("%1 %2").arg(locale.toString(shown), m_rowNoun)); + return; + } + const auto baseline = + static_cast(m_baselineCount ? m_baselineCount() : model->totalRowCount()); + m_statusLabel->setText( + QString("%1 / %2 %3").arg(locale.toString(shown), locale.toString(baseline), m_rowNoun)); +} + + +void TriageTablePanel::restoreWhenIdle() +{ + if (!m_restorePending) + return; + // A sort's end signal also fires when the sort is abandoned for a filter change or superseded + // by a newer sort, with the follow-up job starting just after, so the check is deferred one + // event-loop turn. Follow-up chains always end with a committed sort. + QTimer::singleShot(0, this, [this] { + const auto model = m_table->triageModel(); + if (!m_restorePending || model->isSorting() || model->isFiltering()) + return; + m_restorePending = false; + m_table->restorePosition(); + }); +} diff --git a/view/sharedcache/ui/triagetable.h b/view/sharedcache/ui/triagetable.h new file mode 100644 index 000000000..47575e3f5 --- /dev/null +++ b/view/sharedcache/ui/triagetable.h @@ -0,0 +1,351 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include "backgroundsortfilterrows.h" +#include "filter.h" + +/*! Name lookups for a table's Image column, keyed by image header address and region start + address. +*/ +class ImageNameLookup +{ +public: + struct State + { + std::map imageNames; + std::map regionNames; + // Full image paths for the Image column tooltip. + std::map imagePaths; + uint64_t maxAddress = 0; + }; + +private: + std::shared_ptr m_state = std::make_shared(); + +public: + // Build the lookups from the controller's images and regions. + void build(const SharedCacheAPI::SharedCacheController& controller); + + std::shared_ptr snapshot() const { return m_state; } + + static QString displayName( + const State& state, std::optional imageStart, uint64_t regionStart); + static QString tooltip(const State& state, std::optional imageStart, uint64_t regionStart); + + // The Image column display name: the image's base name, or the region's name for rows + // outside any image. + QString displayName(std::optional imageStart, uint64_t regionStart) const; + + // The Image column tooltip: the image's full path, or the region's name for rows outside + // any image. + QString tooltip(std::optional imageStart, uint64_t regionStart) const; + + // The longest image name, for sizing the Image column. + QString widestImageName() const; + + // The end of the highest region, for sizing zero-padded address text. + uint64_t maxAddress() const { return m_state->maxAddress; } +}; + + +/*! Shared portion of the triage view's flat table models: the filter state and the text matching + common to the tables. Row storage lives in the typed subclass `TriageTableRowsModel`. +*/ +class TriageTableModel : public QAbstractTableModel +{ +Q_OBJECT +protected: + struct FilterParams + { + std::string text; + FilterOptions options; + QRegularExpression regex; + bool matchImageNames; + }; + + // The filter state, captured on the GUI thread for the worker-thread predicate factories. + struct FilterSnapshot + { + std::string text; + FilterOptions options; + bool matchImageNames; + }; + + QFont m_font; + + // Rendered width of the highest cache address, for zero-padded address display. + uint32_t m_addressWidth = 16; + + std::string m_filterText; + FilterOptions m_filterOptions; + bool m_matchImageNames = false; + + explicit TriageTableModel(QWidget* parent); + + FilterSnapshot filterSnapshot() const { return {m_filterText, m_filterOptions, m_matchImageNames}; } + + // Materialize a snapshot's matching state. Call once per work chunk on the worker threads: + // QRegularExpression::match takes the instance's mutex, so a shared instance serializes them. + static FilterParams MakeFilterParams(const FilterSnapshot& snapshot); + + // Whether the filter matches the row's primary text, its rendered address, or its image name. + // Pass an empty image name when image name matching is disabled. + static bool MatchesText(const FilterParams& params, const std::string& text, uint64_t address, + uint32_t addressWidth, const QString& imageName); + + // Re-apply the filter state to the rows. + virtual void applyFilter() = 0; + +public: + uint32_t addressWidth() const { return m_addressWidth; } + + void setFilter(const std::string& text, FilterOptions options); + + // Whether the filter also matches against the Image column. Re-applies any active filter. + void setMatchImageNames(bool match); + + bool hasTextFilter() const { return !m_filterText.empty(); } + + virtual bool isFiltering() const = 0; + virtual bool isSorting() const = 0; + + // Count of all rows fetched so far, ignoring any filter. + virtual size_t totalRowCount() const = 0; + + // Drop all rows, freeing their storage, while retaining the filter settings. + virtual void clearRows() = 0; + + // Capture the identities of the given display rows for a `takeSavedRows` lookup after a + // content reload. Pass -1 to leave a slot empty. + virtual void saveRowIdentities(int selectedRow, int topRow) = 0; + // Consume the identities captured by `saveRowIdentities`, returning their current display + // rows as {selected, top}, with -1 where a row is no longer displayed. + virtual std::pair takeSavedRows() = 0; + +Q_SIGNALS: + // Emitted when background filtering starts and finishes. + void filteringChanged(bool active); + // Emitted when background sorting starts and finishes. + void sortingChanged(bool active); +}; + + +/*! Typed row storage for triage table models: rows sorted and filtered on the worker pool, plus + the row identity bookkeeping that restores positions across a content reload. +*/ +template +class TriageTableRowsModel : public TriageTableModel +{ +public: + using Predicate = typename BackgroundSortFilterRows::Predicate; + using PredicateFactory = typename BackgroundSortFilterRows::PredicateFactory; + using Comparator = typename BackgroundSortFilterRows::Comparator; + + int rowCount(const QModelIndex& parent) const override + { + Q_UNUSED(parent); + return static_cast(m_rows.displayCount()); + } + + const Row& rowAt(int row) const { return m_rows.displayAt(row); } + + void appendRows(std::vector rows) { m_rows.append(std::move(rows)); } + + bool isFiltering() const override { return m_rows.filtering(); } + bool isSorting() const override { return m_rows.sorting(); } + + size_t totalRowCount() const override { return m_rows.totalCount(); } + + void clearRows() override { m_rows.clear(); } + + void saveRowIdentities(int selectedRow, int topRow) override + { + m_savedSelection = selectedRow >= 0 ? std::optional(rowAt(selectedRow)) : std::nullopt; + m_savedTopRow = topRow >= 0 ? std::optional(rowAt(topRow)) : std::nullopt; + } + + std::pair takeSavedRows() override + { + int selectedRow = -1; + int topRow = -1; + const int count = static_cast(m_rows.displayCount()); + for (int row = 0; row < count && (selectedRow < 0 || topRow < 0); row++) + { + const Row& candidate = m_rows.displayAt(row); + if (selectedRow < 0 && m_savedSelection && rowsEquivalent(candidate, *m_savedSelection)) + selectedRow = row; + if (topRow < 0 && m_savedTopRow && rowsEquivalent(candidate, *m_savedTopRow)) + topRow = row; + } + m_savedSelection.reset(); + m_savedTopRow.reset(); + return {selectedRow, topRow}; + } + +protected: + explicit TriageTableRowsModel(QWidget* parent) : + TriageTableModel(parent), + m_rows({ + std::bind_front(&TriageTableRowsModel::beginResetModel, this), + std::bind_front(&TriageTableRowsModel::endResetModel, this), + std::bind_front(&TriageTableRowsModel::beginInsertRows, this, QModelIndex()), + std::bind_front(&TriageTableRowsModel::endInsertRows, this), + std::bind_front(&TriageTableRowsModel::filteringChanged, this), + std::bind_front(&TriageTableRowsModel::sortingChanged, this), + }) + {} + + // Whether two rows are the same row, for position save and restore. + virtual bool rowsEquivalent(const Row& a, const Row& b) const = 0; + + // Sort the display rows with `comparator`, reversed for descending order. + void sortRows(Comparator comparator, Qt::SortOrder order) + { + if (order == Qt::DescendingOrder) + m_rows.sort([comparator](const Row& a, const Row& b) { return comparator(b, a); }); + else + m_rows.sort(std::move(comparator)); + } + + // This base member is destroyed after derived model members, so predicates and comparators + // passed to it must capture worker-safe snapshots rather than reading derived model state. + BackgroundSortFilterRows m_rows; + // Rows captured by `saveRowIdentities` for restoring positions across a content reload. + std::optional m_savedSelection; + std::optional m_savedTopRow; +}; + + +/*! Shared behavior for the triage view's tables: dense monospace styling, filter target + plumbing, the busy overlay, and position save and restore across content reloads. +*/ +class TriageTableView : public QTableView, public FilterTarget +{ +Q_OBJECT + TriageTableModel* m_model = nullptr; + QLabel* m_busyOverlay = nullptr; + + void positionBusyOverlay(); + +public: + TriageTableModel* triageModel() const { return m_model; } + + // Capture the identities of the selected row and the first visible row ahead of a content + // reload. + void savePosition(); + // Re-select and re-scroll to the rows captured by `savePosition`, where still displayed. + void restorePosition(); + + void setFilter(const std::string& filter, FilterOptions options) override; + + void scrollToFirstItem() override; + void scrollToCurrentItem() override; + void ensureSelection() override; + void activateSelection() override; + +protected: + explicit TriageTableView(QWidget* parent); + + // Attach the model, set the column the table initially sorts by, and finish the setup that + // depends on the model. Must be called exactly once from the derived constructor. + void setTriageModel(TriageTableModel* model, int sortColumn); + + // Fit the column to the widest of its header and `contents` in the monospace font, plus room + // for the header's sort indicator. The column remains user-resizable. + void fitColumn(int column, const std::vector& contents); + + // Fit each fixed-content column to its possible contents. Called when the name sources + // change. Only the last column stretches. + virtual void applyDefaultColumnWidths() = 0; + + void resizeEvent(QResizeEvent* event) override; +}; + + +/*! A triage tab hosting an expensive table: a filter field above the table and a footer with a + status label. The content loads when the tab first comes on screen, is cleared after the tab + has been off screen for a delay, and the table's position is saved and restored across the + reload. +*/ +class TriageTablePanel : public QWidget +{ +Q_OBJECT + TriageTableView* m_table; + FilterEdit* m_filterEdit; + QHBoxLayout* m_footerLayout; + QLabel* m_statusLabel; + QTimer* m_clearTimer; + QString m_rowNoun; + + std::function m_loader; + std::function m_clearHandler; + std::function m_baselineCount; + + bool m_loadStarted = false; + bool m_viewVisible = false; + bool m_tabCurrent = false; + bool m_restorePending = false; + bool m_loaderOwnsStatus = false; + + void updateActive(); + void applyFilter(); + void updateStatusLabel(); + void restoreWhenIdle(); + +public: + // `rowNoun` names the rows in the footer's counts, e.g. "symbols". + TriageTablePanel(QWidget* parent, TriageTableView* table, const QString& filterPlaceholder, + QString rowNoun); + + TriageTableView* table() const { return m_table; } + FilterEdit* filterEdit() const { return m_filterEdit; } + // The loader owns the status label, for its progress text, until `finishLoad`. + QLabel* statusLabel() const { return m_statusLabel; } + + // Insert a widget ahead of the status label in the footer. + void addFooterWidget(QWidget* widget); + + // Add a checkable icon action to the filter field, highlighting the icon while checked. + QAction* addFilterToggle(const QString& iconPath, const QString& toolTip, + std::function onToggled); + + // Add a footer button that is enabled only while the table has a selection. + QPushButton* addSelectionButton(const QString& text); + + // Starts loading the table's content when the tab first comes on screen. Returns false if + // loading cannot begin yet, retrying on the next activation. + void setLoader(std::function loader); + // Invoked before the rows are cleared, to stop and free the owner's loader state. + void setClearHandler(std::function handler); + // Count of rows eligible for display ignoring any text filter, the denominator of the + // footer's filtered count. Defaults to the model's total row count. + void setBaselineCount(std::function baselineCount); + + // Track whether the triage view is on screen and which triage tab is current. + void setViewVisible(bool visible); + void setCurrentTabWidget(QWidget* current); + + // Call when the load has delivered every row: applies the selected sort, restores the saved + // position once it commits, and returns the status label to the panel. + void finishLoad(); + + // Discard the content immediately and reload it if the tab is on screen. + void resetContent(); + + // Free the content. The filter widgets keep their state and the position is saved, so a + // reload reproduces the same view. + void clearContent(); +}; From 6f9f9f9bdba8295cab37024a459d49b202cf91b9 Mon Sep 17 00:00:00 2001 From: Mark Rowe Date: Fri, 12 Jun 2026 15:34:03 -0700 Subject: [PATCH 3/3] [DSC] Refactor the Symbols table to use TriageTablePanel This gives it asynchronous loading, filtering, and sorting, along with lazy loading and releasing the table data after the view is hidden. --- view/sharedcache/api/sharedcache.cpp | 3 + view/sharedcache/ui/dsctriage.cpp | 119 +++++---- view/sharedcache/ui/dsctriage.h | 5 + view/sharedcache/ui/symboltable.cpp | 346 +++++++++++++++------------ view/sharedcache/ui/symboltable.h | 104 +++----- 5 files changed, 293 insertions(+), 284 deletions(-) diff --git a/view/sharedcache/api/sharedcache.cpp b/view/sharedcache/api/sharedcache.cpp index 1c239dceb..1367fa2fa 100644 --- a/view/sharedcache/api/sharedcache.cpp +++ b/view/sharedcache/api/sharedcache.cpp @@ -53,6 +53,9 @@ CacheRegion RegionFromApi(BNSharedCacheRegion apiRegion) region.size = apiRegion.size; region.flags = apiRegion.flags; region.type = apiRegion.regionType; + // A zeroed imageStart means the region is not associated with an image. + if (apiRegion.imageStart != 0) + region.imageStart = apiRegion.imageStart; return region; } diff --git a/view/sharedcache/ui/dsctriage.cpp b/view/sharedcache/ui/dsctriage.cpp index f40000f38..c61b3d743 100644 --- a/view/sharedcache/ui/dsctriage.cpp +++ b/view/sharedcache/ui/dsctriage.cpp @@ -60,10 +60,11 @@ DSCTriageView::DSCTriageView(QWidget* parent, BinaryViewRef data) : QWidget(pare initStringsTab(); initCacheInfoTables(); - // The string scan is expensive, so the panel starts its load only once the Strings tab is - // first shown, and clears its large result set after the tab leaves the screen. + // The string scan and symbol fetch are expensive, so each panel starts its load only once its + // tab is first shown, and clears its large result set after the tab leaves the screen. connect(m_triageTabs, &SplitTabWidget::currentChanged, this, [this](QWidget* widget) { m_stringsPanel->setCurrentTabWidget(widget); + m_symbolsPanel->setCurrentTabWidget(widget); }); m_layout = new QVBoxLayout(this); @@ -334,59 +335,31 @@ QWidget* DSCTriageView::initImageTable() void DSCTriageView::initSymbolTable() { m_symbolTable = new SymbolTableView(this); - - // Apply custom column styling - m_symbolTable->setItemDelegateForColumn(0, new AddressColorDelegate(m_symbolTable)); - - auto symbolFilterEdit = new FilterEdit(m_symbolTable); - symbolFilterEdit->setPlaceholderText("Filter symbols"); - connect(symbolFilterEdit, &FilterEdit::textChanged, [this, symbolFilterEdit](const QString& filter) { - m_symbolTable->setFilter(filter.toStdString(), symbolFilterEdit->getFilterOptions()); - }); - connect(symbolFilterEdit, &FilterEdit::optionsChanged, [this, symbolFilterEdit](FilterOptions options) { - m_symbolTable->setFilter(symbolFilterEdit->text().toStdString(), options); + m_symbolsPanel = new TriageTablePanel(this, m_symbolTable, "Filter symbols", "symbols"); + m_symbolsPanel->setLoader([this] { return startSymbolLoad(); }); + m_symbolsPanel->setClearHandler([this] { + // Discard an in-flight symbol fetch rather than letting its results repopulate the + // cleared table. + if (m_symbolsWatcher) + { + m_symbolsWatcher->disconnect(); + m_symbolsWatcher->deleteLater(); + } }); - auto loadSymbolImageButton = new QPushButton(); + m_symbolsPanel->addFilterToggle(":/icons/images/folder.png", "Match Image Names", + [this](bool checked) { m_symbolTable->symbolsModel()->setMatchImageNames(checked); }); + + auto loadSymbolImageButton = m_symbolsPanel->addSelectionButton("Load Image"); connect(loadSymbolImageButton, &QPushButton::clicked, [this](bool) { auto selected = m_symbolTable->selectionModel()->selectedRows(); std::vector addresses; for (const auto& row : selected) - addresses.push_back(row.data().toString().toULongLong(nullptr, 16)); + addresses.push_back(m_symbolTable->getSymbolAtRow(row.row()).address); loadImagesWithAddr(addresses); }); - loadSymbolImageButton->setText("Load Image"); - - // Shows the current selected rows image name. - auto currentImageLabel = new QLabel(this); - currentImageLabel->setText(""); - currentImageLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - connect(m_symbolTable->selectionModel(), &QItemSelectionModel::currentRowChanged, this, [this, currentImageLabel](const QModelIndex ¤t, const QModelIndex &) { - auto symbol = m_symbolTable->getSymbolAtRow(current.row()); - auto controller = SharedCacheController::GetController(*this->m_data); - if (!controller) - return; - auto image = controller->GetImageContaining(symbol.address); - if (image) - currentImageLabel->setText("Image: " + QString::fromStdString(image->name)); - else - currentImageLabel->setText(""); - }); - - auto symbolFooterLayout = new QHBoxLayout; - symbolFooterLayout->addWidget(loadSymbolImageButton); - symbolFooterLayout->addWidget(currentImageLabel); - symbolFooterLayout->setAlignment(Qt::AlignLeft); - - auto symbolLayout = new QVBoxLayout; - symbolLayout->addWidget(symbolFilterEdit); - symbolLayout->addWidget(m_symbolTable); - symbolLayout->addLayout(symbolFooterLayout); - auto symbolWidget = new QWidget; - symbolWidget->setLayout(symbolLayout); - - connect(m_symbolTable, &SymbolTableView::activated, this, [=, this](const QModelIndex& index){ + connect(m_symbolTable, &SymbolTableView::activated, this, [this](const QModelIndex& index){ auto symbol = m_symbolTable->getSymbolAtRow(index.row()); auto controller = SharedCacheController::GetController(*this->m_data); @@ -403,21 +376,43 @@ void DSCTriageView::initSymbolTable() return; } - auto dialog = new QMessageBox(this); - dialog->setText("Load " + QString::fromStdString(image->name) + "?"); - dialog->setStandardButtons(QMessageBox::Yes | QMessageBox::No); + promptToLoadImage(image->name, image->headerAddress, symbol.address); + }); - connect(dialog, &QMessageBox::buttonClicked, this, [=, this](QAbstractButton* button) - { - if (button == dialog->button(QMessageBox::Yes)) - loadImagesWithAddr({image->headerAddress}, false, symbol.address); - }); + m_triageTabs->addTab(m_symbolsPanel, "Symbols"); + m_triageTabs->setCanCloseTab(m_symbolsPanel, false); +} - dialog->exec(); - }); - m_triageTabs->addTab(symbolWidget, "Symbols"); - m_triageTabs->setCanCloseTab(symbolWidget, false); +bool DSCTriageView::startSymbolLoad() +{ + // The controller is not available until view init has finished. Retry on the next activation. + auto controller = SharedCacheController::GetController(*m_data); + if (!controller) + return false; + + m_symbolTable->setNameSources(*controller); + m_symbolsPanel->statusLabel()->setText("Loading…"); + + typedef std::vector SymbolList; + QPointer> watcher = new QFutureWatcher(this); + m_symbolsWatcher = watcher; + connect(watcher, &QFutureWatcher::finished, this, [watcher, this]() { + if (!watcher) + return; + m_symbolTable->symbolsModel()->appendRows(watcher->result()); + m_symbolsPanel->finishLoad(); + watcher->deleteLater(); + }); + QFuture future = QtConcurrent::run([controller]() { return controller->GetSymbols(); }); + watcher->setFuture(future); + connect(this, &QObject::destroyed, this, [watcher]() { + if (watcher && watcher->isRunning()) { + watcher->cancel(); + watcher->waitForFinished(); + } + }); + return true; } @@ -514,6 +509,7 @@ void DSCTriageView::showEvent(QShowEvent* event) { QWidget::showEvent(event); m_stringsPanel->setViewVisible(true); + m_symbolsPanel->setViewVisible(true); } @@ -521,6 +517,7 @@ void DSCTriageView::hideEvent(QHideEvent* event) { QWidget::hideEvent(event); m_stringsPanel->setViewVisible(false); + m_symbolsPanel->setViewVisible(false); } @@ -839,6 +836,8 @@ void DSCTriageView::RefreshData() // TODO: This should use `QSortFilterProxyModel`, but that's a bigger change. m_mappingTable->setSortingEnabled(true); - - m_symbolTable->populateSymbols(*m_data); + // Symbols and strings are loaded lazily when their tabs are shown. + // Discard any loaded content so the reload picks up the refreshed cache information. + m_symbolsPanel->resetContent(); + m_stringsPanel->resetContent(); } diff --git a/view/sharedcache/ui/dsctriage.h b/view/sharedcache/ui/dsctriage.h index b312c9575..8f9bd0800 100644 --- a/view/sharedcache/ui/dsctriage.h +++ b/view/sharedcache/ui/dsctriage.h @@ -1,6 +1,8 @@ +#include #include #include #include +#include #include #include #include @@ -193,6 +195,8 @@ class DSCTriageView : public QWidget, public View, public UIContextNotification QStandardItemModel* m_imageModel; SymbolTableView* m_symbolTable; + TriageTablePanel* m_symbolsPanel; + QPointer>> m_symbolsWatcher; StringsTableView* m_stringsTable; TriageTablePanel* m_stringsPanel; @@ -230,6 +234,7 @@ class DSCTriageView : public QWidget, public View, public UIContextNotification void navigateToAddress(uint64_t address); QWidget* initImageTable(); void initSymbolTable(); + bool startSymbolLoad(); void initStringsTab(); bool startStringScan(); void pollStringScan(); diff --git a/view/sharedcache/ui/symboltable.cpp b/view/sharedcache/ui/symboltable.cpp index 2a6cb467c..53c4c29db 100644 --- a/view/sharedcache/ui/symboltable.cpp +++ b/view/sharedcache/ui/symboltable.cpp @@ -1,61 +1,129 @@ -#include #include "symboltable.h" -#include +#include +#include +#include -#include "ui/fontsettings.h" +#include "addresstext.h" +#include "theme.h" -#include "binaryninjaapi.h" - -using namespace BinaryNinja; using namespace SharedCacheAPI; +namespace { -SymbolTableModel::SymbolTableModel(SymbolTableView* parent) : - QAbstractTableModel(parent), m_parent(parent), m_displaySymbols(&m_symbols) +enum SymbolsTableColumn +{ + SymbolsTableAddressColumn, + SymbolsTableTypeColumn, + SymbolsTableImageColumn, + SymbolsTableNameColumn, + SymbolsTableColumnCount, +}; + +QString SymbolTypeAsString(BNSymbolType type) { - // TODO: Need to implement updating this font if it is changed by the user - m_font = getMonospaceFont(parent); + const std::string name = GetSymbolTypeAsString(type); + return QString::fromUtf8(name.c_str(), name.size()); } -int SymbolTableModel::rowCount(const QModelIndex& parent) const { - Q_UNUSED(parent); - return static_cast(m_displaySymbols->size()); +const SymbolTableModel::AddressRange* RangeContaining( + const std::vector& ranges, uint64_t address) +{ + auto it = std::upper_bound(ranges.begin(), ranges.end(), address, + [](uint64_t address, const SymbolTableModel::AddressRange& range) { + return address < range.start; + }); + if (it == ranges.begin()) + return nullptr; + --it; + if (address >= it->end) + return nullptr; + return &*it; } -int SymbolTableModel::columnCount(const QModelIndex& parent) const { +QString ImageNameForSymbol(const ImageNameLookup::State& names, + const std::vector& ranges, const CacheSymbol& symbol) +{ + const auto* range = RangeContaining(ranges, symbol.address); + if (!range) + return {}; + return ImageNameLookup::displayName(names, range->imageStart, range->start); +} + +} // namespace + + +SymbolTableModel::SymbolTableModel(QWidget* parent) : TriageTableRowsModel(parent) +{ +} + + +int SymbolTableModel::columnCount(const QModelIndex& parent) const +{ Q_UNUSED(parent); - // We have 3 columns: Address, Type, Name - return 3; + return SymbolsTableColumnCount; } -QVariant SymbolTableModel::data(const QModelIndex& index, int role) const { - if (!index.isValid() || (role != Qt::DisplayRole && role != Qt::FontRole)) { +QVariant SymbolTableModel::data(const QModelIndex& index, int role) const +{ + if (!index.isValid()) return QVariant(); - } switch (role) { case Qt::DisplayRole: { - auto symbol = symbolAt(index.row()); - auto symbolType = GetSymbolTypeAsString(symbol.type); + const auto& symbol = symbolAt(index.row()); switch (index.column()) { - case 0: // Address column - return QString("0x%1").arg(symbol.address, 0, 16); // Display address as hexadecimal - case 1: // Type column - return QString::fromUtf8(symbolType.c_str(), symbolType.size()); - case 2: // Name column + case SymbolsTableAddressColumn: + return QString::fromStdString(AddressText(symbol.address, m_addressWidth)); + case SymbolsTableTypeColumn: + return SymbolTypeAsString(symbol.type); + case SymbolsTableImageColumn: + return ImageNameForSymbol(*m_names.snapshot(), *m_ranges, symbol); + case SymbolsTableNameColumn: return QString::fromUtf8(symbol.name.c_str(), symbol.name.size()); default: return QVariant(); } } + case Qt::ForegroundRole: + switch (index.column()) + { + case SymbolsTableAddressColumn: + return getThemeColor(AddressColor); + case SymbolsTableTypeColumn: + return getThemeColor(TypeNameColor); + case SymbolsTableNameColumn: + switch (symbolAt(index.row()).type) + { + case FunctionSymbol: + return getThemeColor(CodeSymbolColor); + case DataSymbol: + return getThemeColor(DataSymbolColor); + default: + return QVariant(); + } + default: + return QVariant(); + } + case Qt::ToolTipRole: + { + if (index.column() != SymbolsTableImageColumn) + return QVariant(); + const auto& symbol = symbolAt(index.row()); + const auto* range = RangeContaining(*m_ranges, symbol.address); + if (!range) + return QVariant(); + if (QString tooltip = m_names.tooltip(range->imageStart, range->start); !tooltip.isEmpty()) + return tooltip; + return QVariant(); + } case Qt::FontRole: return m_font; default: @@ -64,17 +132,20 @@ QVariant SymbolTableModel::data(const QModelIndex& index, int role) const { } -QVariant SymbolTableModel::headerData(int section, Qt::Orientation orientation, int role) const { - if (role != Qt::DisplayRole || orientation != Qt::Horizontal) { +QVariant SymbolTableModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (role != Qt::DisplayRole || orientation != Qt::Horizontal) return QVariant(); - } - switch (section) { - case 0: + switch (section) + { + case SymbolsTableAddressColumn: return QString("Address"); - case 1: + case SymbolsTableTypeColumn: return QString("Type"); - case 2: + case SymbolsTableImageColumn: + return QString("Image"); + case SymbolsTableNameColumn: return QString("Name"); default: return QVariant(); @@ -84,168 +155,139 @@ QVariant SymbolTableModel::headerData(int section, Qt::Orientation orientation, void SymbolTableModel::sort(int column, Qt::SortOrder order) { - beginResetModel(); - - std::function comparator; + Comparator comparator; switch (column) { - case 0: // Address column + case SymbolsTableAddressColumn: comparator = [](const CacheSymbol& a, const CacheSymbol& b) { return a.address < b.address; }; break; - case 1: // Type column + case SymbolsTableTypeColumn: comparator = [](const CacheSymbol& a, const CacheSymbol& b) { - return GetSymbolTypeAsString(a.type) < GetSymbolTypeAsString(b.type); + return a.type < b.type; + }; + break; + case SymbolsTableImageColumn: + { + // Rank the region ranges by their displayed name once so that row comparisons are + // integer-only instead of repeated string comparisons. + auto ranges = m_ranges; + auto namesSnapshot = m_names.snapshot(); + std::vector names(ranges->size()); + for (size_t i = 0; i < ranges->size(); i++) + names[i] = ImageNameLookup::displayName( + *namesSnapshot, (*ranges)[i].imageStart, (*ranges)[i].start); + std::vector byName(ranges->size()); + std::iota(byName.begin(), byName.end(), 0); + std::sort(byName.begin(), byName.end(), + [&names](size_t a, size_t b) { return names[a] < names[b]; }); + + auto ranks = std::make_shared>(ranges->size(), -1); + int rank = -1; + const QString* previousName = nullptr; + for (size_t index : byName) + { + // Identical names share a rank, matching equality under name comparison. + if (!previousName || names[index] != *previousName) + { + rank++; + previousName = &names[index]; + } + (*ranks)[index] = rank; + } + + comparator = [ranges, ranks](const CacheSymbol& a, const CacheSymbol& b) { + // Mirrors the Image column's name resolution. Symbols outside every range rank + // first like the empty string. + const auto rankOf = [ranges, &ranks](const CacheSymbol& symbol) { + const auto* range = RangeContaining(*ranges, symbol.address); + if (!range) + return -1; + return (*ranks)[range - ranges->data()]; + }; + return rankOf(a) < rankOf(b); }; break; - case 2: // Name column + } + case SymbolsTableNameColumn: comparator = [](const CacheSymbol& a, const CacheSymbol& b) { return a.name < b.name; }; break; default: - endResetModel(); return; } - if (order == Qt::DescendingOrder) - { - std::sort(m_displaySymbols->begin(), m_displaySymbols->end(), - [&comparator](const CacheSymbol& a, const CacheSymbol& b) { return comparator(b, a); }); - } - else - { - std::sort(m_displaySymbols->begin(), m_displaySymbols->end(), comparator); - } - - endResetModel(); + sortRows(std::move(comparator), order); } -void SymbolTableModel::updateSymbols(std::vector symbols) +void SymbolTableModel::setNameSources(const SharedCacheController& controller) { - m_symbols = std::move(symbols); - setFilter(m_filter, m_filterOptions); + m_names.build(controller); + m_addressWidth = BNGetAddressRenderedWidth(m_names.maxAddress()); + + auto ranges = std::make_shared>(); + for (const auto& region : controller.GetRegions()) + ranges->push_back({region.start, region.start + region.size, region.imageStart}); + std::sort(ranges->begin(), ranges->end(), + [](const AddressRange& a, const AddressRange& b) { return a.start < b.start; }); + m_ranges = std::move(ranges); } -const CacheSymbol& SymbolTableModel::symbolAt(int row) const +bool SymbolTableModel::rowsEquivalent(const CacheSymbol& a, const CacheSymbol& b) const { - return m_displaySymbols->at(row); + return a.address == b.address && a.name == b.name; } -void SymbolTableModel::setFilter(const std::string& text, FilterOptions options) +void SymbolTableModel::applyFilter() { - m_filter = text; - m_filterOptions = options; - bool caseSensitive = options.testFlag(CaseSensitiveOption); - beginResetModel(); - // Skip filtering if no filter applied. - if (m_filter.empty()) + if (m_filterText.empty()) { - m_filteredSymbols = {}; - m_displaySymbols = &m_symbols; - } - else - { - // Clear the filtered symbols while preserving the capacity - m_filteredSymbols.clear(); - - for (const auto& symbol : m_symbols) - { - const std::string& symbolName = symbol.name; - bool match; - if (caseSensitive) - { - match = (symbolName.find(m_filter) != std::string::npos); - } - else - { - auto it = std::search( - symbolName.begin(), symbolName.end(), - m_filter.begin(), m_filter.end(), - [](char c1, char c2) { return std::tolower(c1) == std::tolower(c2); } - ); - - match = (it != symbolName.end()); - } - - if (match) - { - m_filteredSymbols.push_back(symbol); - } - } - - // If the filtered vector is using less than 25% of its capacity, - // shrink it to reduce memory usage. - if (m_filteredSymbols.size() < m_filteredSymbols.capacity() / 4) - m_filteredSymbols.shrink_to_fit(); - - m_displaySymbols = &m_filteredSymbols; + m_rows.setFilter(nullptr); + return; } - endResetModel(); + const auto snapshot = filterSnapshot(); + const uint32_t addressWidth = m_addressWidth; + const auto names = m_names.snapshot(); + const auto ranges = m_ranges; + + m_rows.setFilterFactory([snapshot, addressWidth, names, ranges]() -> Predicate { + FilterParams params = MakeFilterParams(snapshot); + return [params = std::move(params), addressWidth, names, ranges](const CacheSymbol& symbol) { + QString imageName; + if (params.matchImageNames) + imageName = ImageNameForSymbol(*names, *ranges, symbol); + return MatchesText(params, symbol.name, symbol.address, addressWidth, imageName); + }; + }); } - - -SymbolTableView::SymbolTableView(QWidget* parent) : - QTableView(parent), m_model(new SymbolTableModel(this)) +SymbolTableView::SymbolTableView(QWidget* parent) : TriageTableView(parent) { - // Set up the filter model - setModel(m_model); - - // Configure view settings - horizontalHeader()->setSectionResizeMode(0, QHeaderView::Fixed); - horizontalHeader()->setSectionResizeMode(1, QHeaderView::Fixed); - horizontalHeader()->setSectionResizeMode(2, QHeaderView::Stretch); - setEditTriggers(QAbstractItemView::NoEditTriggers); - setSelectionBehavior(QAbstractItemView::SelectRows); - setSelectionMode(QAbstractItemView::SingleSelection); - verticalHeader()->setVisible(false); - - sortByColumn(0, Qt::AscendingOrder); - setSortingEnabled(true); + m_model = new SymbolTableModel(this); + setTriageModel(m_model, SymbolsTableAddressColumn); + applyDefaultColumnWidths(); } -SymbolTableView::~SymbolTableView() = default; -void SymbolTableView::populateSymbols(BinaryView &view) +void SymbolTableView::setNameSources(const SharedCacheController& controller) { - if (auto controller = SharedCacheController::GetController(view)) { - typedef std::vector SymbolList; - // Retrieve the symbols from the controller in a future than pass that to the model. - QPointer> watcher = new QFutureWatcher(this); - connect(watcher, &QFutureWatcher::finished, this, [watcher, this]() { - if (watcher) - { - auto symbols = watcher->result(); - m_model->updateSymbols(std::move(symbols)); - - // Reapply the current sort after repopulating the model - // TODO: The model should use `QSortFilterProxyModel`, but that's a bigger change. - setSortingEnabled(true); - } - }); - QFuture future = QtConcurrent::run([controller]() { - return controller->GetSymbols(); - }); - watcher->setFuture(future); - connect(this, &QObject::destroyed, this, [watcher]() { - if (watcher && watcher->isRunning()) { - watcher->cancel(); - watcher->waitForFinished(); - } - }); - } + m_model->setNameSources(controller); + applyDefaultColumnWidths(); } -void SymbolTableView::setFilter(const std::string& filter, FilterOptions options) +void SymbolTableView::applyDefaultColumnWidths() { - m_model->setFilter(filter, options); + fitColumn(SymbolsTableAddressColumn, {QString(m_model->addressWidth(), QChar('0'))}); + fitColumn(SymbolsTableTypeColumn, + {SymbolTypeAsString(FunctionSymbol), SymbolTypeAsString(DataSymbol), QStringLiteral("Unknown")}); + fitColumn(SymbolsTableImageColumn, {m_model->names().widestImageName()}); } diff --git a/view/sharedcache/ui/symboltable.h b/view/sharedcache/ui/symboltable.h index bcaef2946..ff23035d6 100644 --- a/view/sharedcache/ui/symboltable.h +++ b/view/sharedcache/ui/symboltable.h @@ -1,106 +1,66 @@ #pragma once -#include -#include "viewframe.h" +#include "triagetable.h" -#include -#include -#include "filter.h" +#include -#ifndef BINARYNINJA_DSCSYMBOLTABLE_H -#define BINARYNINJA_DSCSYMBOLTABLE_H -class SymbolTableView; - - -class SymbolTableModel : public QAbstractTableModel +class SymbolTableModel : public TriageTableRowsModel { -Q_OBJECT - SymbolTableView* m_parent; - QFont m_font; - std::string m_filter; - FilterOptions m_filterOptions; +public: + // A region's address range, for resolving a symbol's address to the image or region + // containing it. + struct AddressRange + { + uint64_t start; + uint64_t end; + std::optional imageStart; + }; - std::vector m_symbols; - std::vector m_filteredSymbols; +private: + // Region address ranges sorted by start address, for resolving symbol addresses. + std::shared_ptr> m_ranges = std::make_shared>(); + ImageNameLookup m_names; - // A pointer to either m_symbols or m_filteredSymbols, depending on whether a filter is applied. - std::vector *m_displaySymbols = nullptr; +protected: + void applyFilter() override; + bool rowsEquivalent( + const SharedCacheAPI::CacheSymbol& a, const SharedCacheAPI::CacheSymbol& b) const override; public: - explicit SymbolTableModel(SymbolTableView* parent); + explicit SymbolTableModel(QWidget* parent); - int rowCount(const QModelIndex& parent) const override; int columnCount(const QModelIndex& parent) const override; QVariant data(const QModelIndex& index, int role) const override; QVariant headerData(int section, Qt::Orientation orientation, int role) const override; void sort(int column, Qt::SortOrder order) override; - void updateSymbols(std::vector symbols); - void setFilter(const std::string& text, FilterOptions options); + // Build the Image column lookup tables and the address resolution ranges. + void setNameSources(const SharedCacheAPI::SharedCacheController& controller); + + const ImageNameLookup& names() const { return m_names; } - const SharedCacheAPI::CacheSymbol& symbolAt(int row) const; + const SharedCacheAPI::CacheSymbol& symbolAt(int row) const { return rowAt(row); } }; -class SymbolTableView : public QTableView, public FilterTarget +class SymbolTableView : public TriageTableView { -Q_OBJECT - friend class SymbolTableModel; - SymbolTableModel* m_model; public: explicit SymbolTableView(QWidget* parent); - ~SymbolTableView() override; - - // Call this to populate the symbols from the given view. - void populateSymbols(BinaryNinja::BinaryView& view); - - void scrollToFirstItem() override - { - if (model()->rowCount() > 0) { - QModelIndex top = indexAt(rect().topLeft()); - if (top.isValid()) - scrollTo(top); - } - } - void scrollToCurrentItem() override - { - QModelIndex currentIndex = selectionModel()->currentIndex(); - if (currentIndex.isValid()) - scrollTo(currentIndex); - } - - void ensureSelection() override - { - QModelIndex current = selectionModel()->currentIndex(); - if (current.isValid() || model()->rowCount() == 0) - return; - - if (auto top = indexAt(rect().topLeft()); top.isValid()) - { - selectionModel()->select(top, QItemSelectionModel::ClearAndSelect); - setCurrentIndex(top); - } - } + SymbolTableModel* symbolsModel() const { return m_model; } - - void activateSelection() override - { - ensureSelection(); - if (auto current = selectionModel()->currentIndex(); current.isValid()) - emit activated(current); - } + // Build the Image column lookup tables and refit the default column widths. + void setNameSources(const SharedCacheAPI::SharedCacheController& controller); SharedCacheAPI::CacheSymbol getSymbolAtRow(int row) const { return m_model->symbolAt(row); } - void setFilter(const std::string& filter, FilterOptions options) override; +protected: + void applyDefaultColumnWidths() override; }; - - -#endif // BINARYNINJA_DSCSYMBOLTABLE_H