From 980438008c22399076109ebc93f5585497385c5d Mon Sep 17 00:00:00 2001 From: WerWolv Date: Mon, 1 Dec 2025 19:35:22 +0100 Subject: [PATCH] fix: Crashes and usability issues with the pattern tree filter --- lib/external/pattern_language | 2 +- plugins/ui/include/ui/pattern_drawer.hpp | 15 +- plugins/ui/source/ui/pattern_drawer.cpp | 193 ++++++++++++++---- .../content/pl_visualizers/digital_signal.cpp | 4 +- .../source/content/pl_visualizers/table.cpp | 2 +- 5 files changed, 165 insertions(+), 51 deletions(-) diff --git a/lib/external/pattern_language b/lib/external/pattern_language index 7b2a0523e..32394f789 160000 --- a/lib/external/pattern_language +++ b/lib/external/pattern_language @@ -1 +1 @@ -Subproject commit 7b2a0523ee8bb235cb1a8591eef969095e6c4fe2 +Subproject commit 32394f789a81a5401d905689a5ea52a6634005f5 diff --git a/plugins/ui/include/ui/pattern_drawer.hpp b/plugins/ui/include/ui/pattern_drawer.hpp index 103316e9b..9eae7ef6d 100644 --- a/plugins/ui/include/ui/pattern_drawer.hpp +++ b/plugins/ui/include/ui/pattern_drawer.hpp @@ -85,7 +85,7 @@ namespace hex::ui { void drawColorColumn(const pl::ptrn::Pattern& pattern); void drawCommentColumn(const pl::ptrn::Pattern& pattern); - bool beginPatternTable(const std::vector> &patterns, std::vector &sortedPatterns, float height) const; + bool beginPatternTable(const std::vector> &patterns, std::vector> &sortedPatterns, float height) const; bool createTreeNode(const pl::ptrn::Pattern& pattern, bool leaf = false); void createDefaultEntry(const pl::ptrn::Pattern &pattern); void closeTreeNode(bool inlined) const; @@ -93,23 +93,28 @@ namespace hex::ui { bool sortPatterns(const ImGuiTableSortSpecs* sortSpecs, const pl::ptrn::Pattern * left, const pl::ptrn::Pattern * right) const; [[nodiscard]] bool isEditingPattern(const pl::ptrn::Pattern& pattern) const; void resetEditing(); - void traversePatternTree(pl::ptrn::Pattern &pattern, std::vector &patternPath, const std::function &callback); + void traversePatternTree(const std::shared_ptr &pattern, std::vector &patternPath, const std::function&)> &callback); [[nodiscard]] std::string getDisplayName(const pl::ptrn::Pattern& pattern) const; [[nodiscard]] std::vector getPatternPath(const pl::ptrn::Pattern *pattern) const; struct Filter { std::vector path; + + bool inverted = false; + bool typeMatch = false; + std::strong_ordering operation = std::strong_ordering::equal; std::optional value; }; [[nodiscard]] static bool matchesFilter(const std::vector &filterPath, const std::vector &patternPath, bool fullMatch); [[nodiscard]] static std::optional parseRValueFilter(const std::string &filter); + [[nodiscard]] static std::optional parseComparison(const Filter &currFilter, std::string filterString); void updateFilter(); private: std::map m_displayEnd; - std::vector m_sortedPatterns; + std::vector> m_sortedPatterns; const pl::ptrn::Pattern *m_editingPattern = nullptr; u64 m_editingPatternOffset = 0; @@ -125,8 +130,8 @@ namespace hex::ui { std::set m_visualizedPatterns; std::string m_filterText; - Filter m_filter; - std::vector m_filteredPatterns; + std::optional m_filter; + std::vector> m_filteredPatterns; std::vector m_currPatternPath; std::map, std::shared_ptr> m_favorites; diff --git a/plugins/ui/source/ui/pattern_drawer.cpp b/plugins/ui/source/ui/pattern_drawer.cpp index 04bc3b620..f9f9c0737 100644 --- a/plugins/ui/source/ui/pattern_drawer.cpp +++ b/plugins/ui/source/ui/pattern_drawer.cpp @@ -32,6 +32,8 @@ #include #include #include +#include +#include #include @@ -197,6 +199,61 @@ namespace hex::ui { } + std::optional PatternDrawer::parseComparison(const Filter &currFilter, std::string filterString) { + auto result = currFilter; + + pl::core::Lexer lexer; + + filterString = wolv::util::trim(filterString); + if (filterString.empty()) + return std::nullopt; + else if (filterString.starts_with("===")) { + result.operation = std::strong_ordering::equal; + result.inverted = false; + result.typeMatch = true; + filterString = filterString.substr(3); + } else if (filterString.starts_with("==")) { + result.operation = std::strong_ordering::equal; + result.inverted = false; + filterString = filterString.substr(2); + } else if (filterString.starts_with("!=")) { + result.operation = std::strong_ordering::equal; + result.inverted = true; + filterString = filterString.substr(2); + } else if (filterString.starts_with(">=")) { + result.operation = std::strong_ordering::less; + result.inverted = true; + filterString = filterString.substr(2); + } else if (filterString.starts_with("<=")) { + result.operation = std::strong_ordering::greater; + result.inverted = true; + filterString = filterString.substr(2); + } else if (filterString.starts_with(">")) { + result.operation = std::strong_ordering::greater; + result.inverted = false; + filterString = filterString.substr(1); + } else if (filterString.starts_with("<")) { + result.operation = std::strong_ordering::less; + result.inverted = false; + filterString = filterString.substr(1); + } else { + return std::nullopt; + } + + pl::api::Source source(filterString); + auto tokens = lexer.lex(&source); + + if (!tokens.isOk() || tokens.unwrap().size() != 2) + return std::nullopt; + + auto literal = std::get_if(&tokens.unwrap().front().value); + if (literal == nullptr) + return std::nullopt; + result.value = *literal; + + return result; + } + std::optional PatternDrawer::parseRValueFilter(const std::string &filter) { Filter result; @@ -208,30 +265,30 @@ namespace hex::ui { for (size_t i = 0; i < filter.size(); i += 1) { char c = filter[i]; - if (i < filter.size() - 1 && c == '=' && filter[i + 1] == '=') { - pl::core::Lexer lexer; - - pl::api::Source source(filter.substr(i + 2)); - auto tokens = lexer.lex(&source); - - if (!tokens.isOk() || tokens.unwrap().size() != 2) + if (c == '.') { + if (result.path.back().empty()) return std::nullopt; - auto literal = std::get_if(&tokens.unwrap().front().value); - if (literal == nullptr) - return std::nullopt; - result.value = *literal; - - break; - } else if (c == '.') { result.path.emplace_back(); + } else if (c == '*') { + if (!result.path.back().empty()) + return std::nullopt; + + result.path.back() = "*"; } else if (c == '[') { result.path.emplace_back(); result.path.back() += c; + } else if (c == ']') { + result.path.back() += c; } else if (c == ' ') { // Skip whitespace - } else { + } else if (std::isalnum(c) || c == '_') { + if (result.path.back() == "*") + return std::nullopt; + result.path.back() += c; + } else { + return parseComparison(result, filter.substr(i)); } } @@ -241,7 +298,10 @@ namespace hex::ui { void PatternDrawer::updateFilter() { m_filteredPatterns.clear(); - if (m_filter.path.empty()) { + if (!m_filter.has_value()) + return; + + if (m_filter->path.empty()) { m_filteredPatterns = m_sortedPatterns; return; } @@ -251,13 +311,23 @@ namespace hex::ui { if (m_filteredPatterns.size() > m_maxFilterDisplayItems) break; - traversePatternTree(*pattern, treePath, [this, &treePath](auto &pattern) { + traversePatternTree(pattern, treePath, [this, &treePath](auto &pattern) { if (m_filteredPatterns.size() > m_maxFilterDisplayItems) return; - if (matchesFilter(m_filter.path, treePath, false)) { - if (!m_filter.value.has_value() || pattern.getValue() == m_filter.value) - m_filteredPatterns.push_back(&pattern); + if (matchesFilter(m_filter->path, treePath, false)) { + if (!m_filter->value.has_value()) { + m_filteredPatterns.push_back(pattern); + } else { + auto patternValue = pattern->getValue(); + if (!m_filter->inverted && (patternValue <=> m_filter->value) == m_filter->operation) { + if (!m_filter->typeMatch || (m_filter->value->index() == patternValue.index())) + m_filteredPatterns.push_back(pattern); + } else if (m_filter->inverted && (patternValue <=> m_filter->value) != m_filter->operation) { + if (!m_filter->typeMatch || (m_filter->value->index() == patternValue.index())) + m_filteredPatterns.push_back(pattern); + } + } } }); } @@ -608,7 +678,7 @@ namespace hex::ui { } int id = 1; - pattern.forEachEntry(0, pattern.getEntryCount(), [&] (u64, auto *field) { + pattern.forEachEntry(0, pattern.getEntryCount(), [&] (u64, const auto &field) { ImGui::PushID(id); this->draw(*field); ImGui::PopID(); @@ -804,7 +874,7 @@ namespace hex::ui { } int id = 1; - pattern.forEachEntry(0, pattern.getEntryCount(), [&](u64, auto *member){ + pattern.forEachEntry(0, pattern.getEntryCount(), [&](u64, const auto &member){ ImGui::PushID(id); this->draw(*member); ImGui::PopID(); @@ -849,7 +919,7 @@ namespace hex::ui { } int id = 1; - pattern.forEachEntry(0, pattern.getEntryCount(), [&](u64, auto *member) { + pattern.forEachEntry(0, pattern.getEntryCount(), [&](u64, const auto &member) { ImGui::PushID(id); this->draw(*member); ImGui::PopID(); @@ -1051,7 +1121,7 @@ namespace hex::ui { } int id = 1; - iterable.forEachEntry(i, endIndex, [&](u64, auto *entry){ + iterable.forEachEntry(i, endIndex, [&](u64, const auto &entry){ ImGui::PushID(id); this->draw(*entry); ImGui::PopID(); @@ -1100,7 +1170,7 @@ namespace hex::ui { return sortSpecs->Specs->SortDirection == ImGuiSortDirection_Ascending ? result == std::strong_ordering::less : result == std::strong_ordering::greater; } - bool PatternDrawer::beginPatternTable(const std::vector> &patterns, std::vector &sortedPatterns, float height) const { + bool PatternDrawer::beginPatternTable(const std::vector> &patterns, std::vector> &sortedPatterns, float height) const { if (!ImGui::BeginTable("##Patterntable", 9, ImGuiTableFlags_Borders | ImGuiTableFlags_Resizable | ImGuiTableFlags_Sortable | ImGuiTableFlags_Hideable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY, ImVec2(0, height))) { return false; } @@ -1128,13 +1198,10 @@ namespace hex::ui { } if (!m_favoritesUpdateTask.isRunning()) { - sortedPatterns.clear(); - std::transform(patterns.begin(), patterns.end(), std::back_inserter(sortedPatterns), [](const std::shared_ptr &pattern) { - return pattern.get(); - }); + sortedPatterns = patterns; - std::stable_sort(sortedPatterns.begin(), sortedPatterns.end(), [this, &sortSpecs](const pl::ptrn::Pattern *left, const pl::ptrn::Pattern *right) -> bool { - return this->sortPatterns(sortSpecs, left, right); + std::stable_sort(sortedPatterns.begin(), sortedPatterns.end(), [this, &sortSpecs](const std::shared_ptr &left, const std::shared_ptr &right) -> bool { + return this->sortPatterns(sortSpecs, left.get(), right.get()); }); for (auto &pattern : sortedPatterns) { @@ -1149,14 +1216,18 @@ namespace hex::ui { return true; } - void PatternDrawer::traversePatternTree(pl::ptrn::Pattern &pattern, std::vector &patternPath, const std::function &callback) { - patternPath.push_back(pattern.getVariableName()); + void PatternDrawer::traversePatternTree(const std::shared_ptr &pattern, std::vector &patternPath, const std::function&)> &callback) { + patternPath.push_back(pattern->getVariableName()); ON_SCOPE_EXIT { patternPath.pop_back(); }; callback(pattern); - if (auto iterable = dynamic_cast(&pattern); iterable != nullptr) { - iterable->forEachEntry(0, iterable->getEntryCount(), [&](u64, pl::ptrn::Pattern *entry) { - traversePatternTree(*entry, patternPath, callback); + if (auto iterable = dynamic_cast(pattern.get()); iterable != nullptr) { + // Don't index individual characters of strings + if (dynamic_cast(pattern.get()) || dynamic_cast(pattern.get())) + return; + + iterable->forEachEntry(0, iterable->getEntryCount(), [&](u64, const auto &entry) { + traversePatternTree(entry, patternPath, callback); }); } } @@ -1199,10 +1270,48 @@ namespace hex::ui { auto &style = ImGui::GetStyle(); ImGui::PushItemWidth(-(style.ItemSpacing.x * 2 + style.WindowPadding.x * 2 + ImGui::GetTextLineHeightWithSpacing() * 5 + 15_scaled)); - if (ImGuiExt::InputTextIcon("##Search", ICON_VS_FILTER, m_filterText)) { - m_filter = parseRValueFilter(m_filterText).value_or(Filter{ }); - updateFilter(); + + const bool filterError = !m_filterText.empty() && !m_filter.has_value(); + + if (filterError) { + ImGui::PushStyleColor(ImGuiCol_Border, ImGuiExt::GetCustomColorU32(ImGuiCustomCol_LoggerError)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1_scaled); } + + if (ImGuiExt::InputTextIcon("##Search", ICON_VS_FILTER, m_filterText)) { + auto newFilter = parseRValueFilter(m_filterText); + + if (m_filterText.empty()) { + m_filter.reset(); + updateFilter(); + } else if (!newFilter.has_value()) { + m_filter.reset(); + m_filteredPatterns.clear(); + } else { + m_filter = newFilter; + updateFilter(); + } + } + + TutorialManager::setLastItemInteractiveHelpPopup([] { + ImGuiExt::TextFormattedWrapped("{}", + "Allows filtering of the pattern tree using the following syntax:\n\n" + "- variable_name: Shows all patterns with this name\n" + "- container.var: Shows all patterns named var that are inside a pattern called container\n" + "- a.*.b: Searches all children of pattern a for a pattern named b\n" + "- a[10].b: Shows patterns named b inside the 10th array entry of a pattern named a\n" + "- x > 10: Shows all patterns named x that have a value greater than 10\n" + "- a.b == 100: Shows all patterns named b with a value of 100 that are inside a pattern named a\n" + "- a == \"Hello\": Shows all patterns named a whose value is the string \"Hello\"\n\n" + "If nothing was found, the tree will be empty. If there was a parsing error, the filter text field will be highlighted in red." + ); + }); + + if (filterError) { + ImGui::PopStyleColor(); + ImGui::PopStyleVar(); + } + ImGui::PopItemWidth(); ImGui::SameLine(); @@ -1322,7 +1431,7 @@ namespace hex::ui { m_showFavoriteStars = true; - for (auto &pattern : m_filter.path.empty() ? m_sortedPatterns : m_filteredPatterns) { + for (auto &pattern : m_filterText.empty() ? m_sortedPatterns : m_filteredPatterns) { ImGui::PushID(id); this->draw(*pattern); ImGui::PopID(); @@ -1372,14 +1481,14 @@ namespace hex::ui { continue; patternPath.clear(); - traversePatternTree(*pattern, patternPath, [&, this](const pl::ptrn::Pattern &currPattern) { + traversePatternTree(pattern, patternPath, [&, this](const std::shared_ptr &currPattern) { for (auto &[path, favoritePattern] : m_favorites) { if (updatedFavorites == m_favorites.size()) task.interrupt(); task.update(); if (matchesFilter(patternPath, path, true)) { - favoritePattern = currPattern.clone(); + favoritePattern = currPattern->clone(); updatedFavorites += 1; break; diff --git a/plugins/visualizers/source/content/pl_visualizers/digital_signal.cpp b/plugins/visualizers/source/content/pl_visualizers/digital_signal.cpp index c8110d386..c2d5f8c2f 100644 --- a/plugins/visualizers/source/content/pl_visualizers/digital_signal.cpp +++ b/plugins/visualizers/source/content/pl_visualizers/digital_signal.cpp @@ -29,9 +29,9 @@ namespace hex::plugin::visualizers { dataPoints.clear(); lastPoint = { 0, 0 }; - bitfield->forEachEntry(0, bitfield->getEntryCount(), [&](u64, pl::ptrn::Pattern *entry) { + bitfield->forEachEntry(0, bitfield->getEntryCount(), [&](u64, const auto &entry) { size_t bitSize; - if (const auto *bitfieldField = dynamic_cast(entry); bitfieldField != nullptr) + if (const auto *bitfieldField = dynamic_cast(entry.get()); bitfieldField != nullptr) bitSize = bitfieldField->getBitSize(); else bitSize = entry->getSize() * 8; diff --git a/plugins/visualizers/source/content/pl_visualizers/table.cpp b/plugins/visualizers/source/content/pl_visualizers/table.cpp index d199e8ca1..14cd5f273 100644 --- a/plugins/visualizers/source/content/pl_visualizers/table.cpp +++ b/plugins/visualizers/source/content/pl_visualizers/table.cpp @@ -33,7 +33,7 @@ namespace hex::plugin::visualizers { height = u64(arguments[2].toUnsigned()); auto iterable = dynamic_cast(pattern.get()); - iterable->forEachEntry(0, iterable->getEntryCount(), [&](u64, pl::ptrn::Pattern *entry) { + iterable->forEachEntry(0, iterable->getEntryCount(), [&](u64, const auto &entry) { tableContent.push_back(entry->toString()); }); }