Files
imhex/plugins/builtin/source/content/views/view_find.cpp
iTrooz 17c2dfcbd0 chore: apply more light lints (#2575)
<!--
Please provide as much information as possible about what your PR aims
to do.
PRs with no description will most likely be closed until more
information is provided.
If you're planing on changing fundamental behaviour or add big new
features, please open a GitHub Issue first before starting to work on
it.
If it's not something big and you still want to contact us about it,
feel free to do so !
-->

### Problem description
<!-- Describe the bug that you fixed/feature request that you
implemented, or link to an existing issue describing it -->

### Implementation description
<!-- Explain what you did to correct the problem -->

### Screenshots
<!-- If your change is visual, take a screenshot showing it. Ideally,
make before/after sceenshots -->

### Additional things
<!-- Anything else you would like to say -->
2025-12-20 15:59:48 +01:00

1315 lines
59 KiB
C++

#include "content/views/view_find.hpp"
#include <algorithm>
#include <hex/api/achievement_manager.hpp>
#include <hex/api/imhex_api/hex_editor.hpp>
#include <hex/api/events/events_interaction.hpp>
#include <hex/api/content_registry/user_interface.hpp>
#include <hex/trace/stacktrace.hpp>
#include <hex/providers/buffered_reader.hpp>
#include <fonts/vscode_icons.hpp>
#include <imgui_internal.h>
#include <array>
#include <string>
#include <utility>
#include <barrier>
#include <boost/regex.hpp>
#include <content/helpers/constants.hpp>
#include <hex/helpers/crypto.hpp>
#include <hex/helpers/default_paths.hpp>
#include <toasts/toast_notification.hpp>
namespace hex::plugin::builtin {
ViewFind::ViewFind() : View::Window("hex.builtin.view.find.name", ICON_VS_SEARCH) {
const static auto HighlightColor = [] { return (ImGuiExt::GetCustomColorU32(ImGuiCustomCol_FindHighlight) & 0x00FFFFFF) | 0x70000000; };
ImHexApi::HexEditor::addBackgroundHighlightingProvider([this](u64 address, const u8* data, size_t size, bool) -> std::optional<color_t> {
std::ignore = data;
std::ignore = size;
if (m_searchTask.isRunning())
return { };
if (!m_occurrenceTree->overlapping({ .start=address, .end=address }).empty())
return HighlightColor();
else
return std::nullopt;
});
ImHexApi::HexEditor::addTooltipProvider([this](u64 address, const u8* data, size_t size) {
std::ignore = data;
std::ignore = size;
if (m_searchTask.isRunning())
return;
auto occurrences = m_occurrenceTree->overlapping({ .start=address, .end=address + size });
if (occurrences.empty())
return;
ImGui::BeginTooltip();
for (const auto &occurrence : occurrences) {
ImGui::PushID(&occurrence);
if (ImGui::BeginTable("##tooltips", 1, ImGuiTableFlags_RowBg | ImGuiTableFlags_NoClip)) {
ImGui::TableNextRow();
ImGui::TableNextColumn();
{
auto region = occurrence.value->region;
const auto value = this->decodeValue(ImHexApi::Provider::get(), *occurrence.value, 256);
ImGui::ColorButton("##color", ImColor(HighlightColor()), ImGuiColorEditFlags_AlphaOpaque);
ImGui::SameLine(0, 10);
ImGuiExt::TextFormatted("{} ", value);
if (ImGui::GetIO().KeyShift) {
ImGui::Indent();
if (ImGui::BeginTable("##extra_info", 2, ImGuiTableFlags_RowBg | ImGuiTableFlags_NoClip)) {
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGuiExt::TextFormatted("{}: ", "hex.ui.common.region"_lang);
ImGui::TableNextColumn();
ImGuiExt::TextFormatted("[ 0x{:08X} - 0x{:08X} ]", region.getStartAddress(), region.getEndAddress());
auto demangledValue = trace::demangle(value);
if (value != demangledValue) {
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGuiExt::TextFormatted("{}: ", "hex.builtin.view.find.demangled"_lang);
ImGui::TableNextColumn();
ImGuiExt::TextFormatted("{}", demangledValue);
}
ImGui::EndTable();
}
ImGui::Unindent();
}
}
ImGui::PushStyleColor(ImGuiCol_TableRowBg, HighlightColor());
ImGui::PushStyleColor(ImGuiCol_TableRowBgAlt, HighlightColor());
ImGui::EndTable();
ImGui::PopStyleColor(2);
}
ImGui::PopID();
}
ImGui::EndTooltip();
});
ShortcutManager::addShortcut(this, CTRLCMD + Keys::A, "hex.builtin.view.find.shortcut.select_all", [this] {
if (m_filterTask.isRunning())
return;
if (m_searchTask.isRunning())
return;
for (auto &occurrence : *m_sortedOccurrences)
occurrence.selected = true;
});
/* Find Selection */
ContentRegistry::UserInterface::addMenuItem({ "hex.builtin.menu.edit", "hex.builtin.menu.edit.find.find_selection" }, ICON_VS_SEARCH_SPARKLE, 1950, CTRLCMD + SHIFT + Keys::F, [&] {
auto selection = ImHexApi::HexEditor::getSelection();
if (!selection.has_value())
return;
std::string sequence;
{
std::vector<u8> buffer(selection->getSize());
selection->getProvider()->read(selection->getStartAddress(), buffer.data(), buffer.size());
sequence = hex::crypt::encode16(buffer);
}
m_searchSettings.mode = SearchSettings::Mode::BinaryPattern;
m_searchSettings.region = { .address=selection->getProvider()->getBaseAddress(), .size=selection->getProvider()->getActualSize() };
m_searchSettings.binaryPattern = {
.input = sequence,
.pattern = hex::BinaryPattern(sequence),
.alignment = 1
};
this->runSearch();
this->bringToFront();
}, []{ return ImHexApi::Provider::isValid() && ImHexApi::HexEditor::isSelectionValid(); },
ContentRegistry::Views::getViewByName("hex.builtin.view.hex_editor.name"));
}
template<typename Type, typename StorageType>
static std::tuple<bool, std::variant<u64, i64, float, double>, size_t> parseNumericValue(const std::string &string) {
static_assert(sizeof(StorageType) >= sizeof(Type));
StorageType value;
std::size_t processed = 0;
try {
if constexpr (std::floating_point<Type>)
value = std::stod(string, &processed);
else if constexpr (std::signed_integral<Type>)
value = std::stoll(string, &processed, 0);
else
value = std::stoull(string, &processed, 0);
} catch (std::exception &) {
return { false, { }, 0 };
}
if (processed != string.size())
return { false, { }, 0 };
if (value < std::numeric_limits<Type>::lowest() || value > std::numeric_limits<Type>::max())
return { false, { }, 0 };
return { true, value, sizeof(Type) };
}
std::tuple<bool, std::variant<u64, i64, float, double>, size_t> ViewFind::parseNumericValueInput(const std::string &input, SearchSettings::Value::Type type) {
switch (type) {
using enum SearchSettings::Value::Type;
case U8: return parseNumericValue<u8, u64>(input);
case U16: return parseNumericValue<u16, u64>(input);
case U32: return parseNumericValue<u32, u64>(input);
case U64: return parseNumericValue<u64, u64>(input);
case I8: return parseNumericValue<i8, i64>(input);
case I16: return parseNumericValue<i16, i64>(input);
case I32: return parseNumericValue<i32, i64>(input);
case I64: return parseNumericValue<i64, i64>(input);
case F32: return parseNumericValue<float, float>(input);
case F64: return parseNumericValue<double, double>(input);
default: return { false, { }, 0 };
}
}
template<typename T>
static std::string formatBytes(const std::vector<u8> &bytes, std::endian endian) {
if (bytes.size() > sizeof(T))
return { };
T value = 0x00;
std::memcpy(&value, bytes.data(), bytes.size());
value = hex::changeEndianness(value, bytes.size(), endian);
if (std::signed_integral<T>)
value = T(hex::signExtend(bytes.size() * 8, value));
return fmt::format("{}", value);
}
std::vector<hex::ContentRegistry::DataFormatter::impl::FindOccurrence> ViewFind::searchStrings(Task &task, prv::Provider *provider, hex::Region searchRegion, const SearchSettings::Strings &settings) {
using enum SearchSettings::StringType;
std::vector<Occurrence> results;
if (settings.type == ASCII_UTF16BE || settings.type == ASCII_UTF16LE) {
auto newSettings = settings;
newSettings.type = ASCII;
auto asciiResults = searchStrings(task, provider, searchRegion, newSettings);
std::ranges::copy(asciiResults, std::back_inserter(results));
if (settings.type == ASCII_UTF16BE) {
newSettings.type = UTF16BE;
auto utf16Results = searchStrings(task, provider, searchRegion, newSettings);
std::ranges::copy(utf16Results, std::back_inserter(results));
} else if (settings.type == ASCII_UTF16LE) {
newSettings.type = UTF16LE;
auto utf16Results = searchStrings(task, provider, searchRegion, newSettings);
std::ranges::copy(utf16Results, std::back_inserter(results));
}
return results;
}
auto reader = prv::ProviderReader(provider);
reader.seek(searchRegion.getStartAddress());
reader.setEndAddress(searchRegion.getEndAddress());
const auto [decodeType, endian] = [&]() -> std::pair<Occurrence::DecodeType, std::endian> {
if (settings.type == ASCII)
return { Occurrence::DecodeType::ASCII, std::endian::native };
if (settings.type == UTF8)
return { Occurrence::DecodeType::UTF8, std::endian::native };
else if (settings.type == SearchSettings::StringType::UTF16BE)
return { Occurrence::DecodeType::UTF16, std::endian::big };
else if (settings.type == SearchSettings::StringType::UTF16LE)
return { Occurrence::DecodeType::UTF16, std::endian::little };
else
return { Occurrence::DecodeType::Binary, std::endian::native };
}();
const auto validAscii = [&](u8 byte) {
return
(settings.lowerCaseLetters && std::islower(byte)) ||
(settings.upperCaseLetters && std::isupper(byte)) ||
(settings.numbers && std::isdigit(byte)) ||
(settings.spaces && std::isspace(byte) && byte != '\r' && byte != '\n') ||
(settings.underscores && byte == '_') ||
(settings.symbols && std::ispunct(byte) && !std::isspace(byte)) ||
(settings.lineFeeds && (byte == '\r' || byte == '\n'));
};
i64 countedCharacters = 0;
u64 startAddress = reader.begin().getAddress();
u64 endAddress = reader.end().getAddress();
u64 progress = 0;
i8 remainingCharacters = 0;
for (const u8 byte : reader) {
bool validChar = false;
if (settings.type == ASCII) {
validChar = validAscii(byte);
} else if (settings.type == UTF16LE) {
// Check if second byte of UTF-16 encoded string is 0x00
if (countedCharacters % 2 == 1)
validChar = byte == 0x00;
else
validChar = validAscii(byte);
} else if (settings.type == UTF16BE) {
// Check if first byte of UTF-16 encoded string is 0x00
if (countedCharacters % 2 == 0)
validChar = byte == 0x00;
else
validChar = validAscii(byte);
} else if (settings.type == UTF8) {
if (remainingCharacters > 0) {
// Expect continuation byte
if ((byte & 0b1100'0000) == 0b1000'0000) {
validChar = true;
remainingCharacters -= 1;
} else {
validChar = false; // broken sequence
remainingCharacters = 0;
}
} else {
// New character
if (byte <= 0x7F) {
// ASCII
validChar = validAscii(byte);
remainingCharacters = 0;
} else if ((byte & 0b1110'0000) == 0b1100'0000) {
// 2-byte start (U+80..U+7FF)
validChar = (byte >= 0xC2); // exclude overlongs (0xC0, 0xC1)
remainingCharacters = 1;
} else if ((byte & 0b1111'0000) == 0b1110'0000) {
// 3-byte start (U+800..U+FFFF)
validChar = byte != 0xE0 && byte != 0xED;
// E0 must be followed by >= 0xA0, ED must be <= 0x9F (avoid surrogates)
remainingCharacters = 2;
} else if ((byte & 0b1111'1000) == 0b1111'0000) {
// 4-byte start (U+10000..U+10FFFF)
validChar = (byte <= 0xF4); // reject > U+10FFFF
remainingCharacters = 3;
} else {
validChar = false;
remainingCharacters = 0;
}
}
}
task.update(progress);
if (validChar)
countedCharacters++;
if (!validChar || startAddress + countedCharacters == endAddress) {
if (countedCharacters >= settings.minLength) {
if (!settings.nullTermination || byte == 0x00) {
results.push_back(Occurrence { Region { .address=startAddress, .size=size_t(countedCharacters) }, endian, decodeType, false, {} });
}
}
startAddress += countedCharacters + 1;
countedCharacters = 0;
progress = startAddress - searchRegion.getStartAddress();
}
}
return results;
}
std::vector<hex::ContentRegistry::DataFormatter::impl::FindOccurrence> ViewFind::searchSequence(Task &task, prv::Provider *provider, hex::Region searchRegion, const SearchSettings::Sequence &settings) {
std::vector<Occurrence> results;
auto reader = prv::ProviderReader(provider);
reader.seek(searchRegion.getStartAddress());
reader.setEndAddress(searchRegion.getEndAddress());
auto input = hex::decodeByteString(settings.sequence);
if (input.empty())
return { };
std::vector<u8> bytes;
auto decodeType = Occurrence::DecodeType::Binary;
std::endian endian;
switch (settings.type) {
default:
case SearchSettings::StringType::ASCII:
bytes = input;
decodeType = Occurrence::DecodeType::ASCII;
endian = std::endian::native;
break;
case SearchSettings::StringType::UTF16LE: {
auto wString = hex::utf8ToUtf16({ input.begin(), input.end() });
bytes.resize(wString.size() * 2);
std::memcpy(bytes.data(), wString.data(), bytes.size());
decodeType = Occurrence::DecodeType::UTF16;
endian = std::endian::little;
break;
}
case SearchSettings::StringType::UTF16BE: {
auto wString = hex::utf8ToUtf16({ input.begin(), input.end() });
bytes.resize(wString.size() * 2);
std::memcpy(bytes.data(), wString.data(), bytes.size());
decodeType = Occurrence::DecodeType::UTF16;
endian = std::endian::big;
for (size_t i = 0; i < bytes.size(); i += 2)
std::swap(bytes[i], bytes[i + 1]);
break;
}
}
auto occurrence = reader.begin();
u64 progress = 0;
auto searchPredicate = [&]() -> bool(*)(u8, u8) {
if (!settings.ignoreCase)
return [](u8 left, u8 right) -> bool {
return left == right;
};
else
return [](u8 left, u8 right) -> bool {
if (std::isupper(left))
left = std::tolower(left);
if (std::isupper(right))
right = std::tolower(right);
return left == right;
};
}();
while (true) {
task.update(progress);
occurrence = std::search(reader.begin(), reader.end(), std::default_searcher(bytes.begin(), bytes.end(), searchPredicate));
if (occurrence == reader.end())
break;
auto address = occurrence.getAddress();
reader.seek(address + 1);
results.push_back(Occurrence{ Region { .address=address, .size=bytes.size() }, endian, decodeType, false, {} });
progress = address - searchRegion.getStartAddress();
}
return results;
}
std::vector<hex::ContentRegistry::DataFormatter::impl::FindOccurrence> ViewFind::searchRegex(Task &task, prv::Provider *provider, hex::Region searchRegion, const SearchSettings::Regex &settings) {
auto stringOccurrences = searchStrings(task, provider, searchRegion, SearchSettings::Strings {
.minLength = settings.minLength,
.nullTermination = settings.nullTermination,
.type = settings.type,
.lowerCaseLetters = true,
.upperCaseLetters = true,
.numbers = true,
.underscores = true,
.symbols = true,
.spaces = true,
.lineFeeds = true
});
std::vector<Occurrence> result;
boost::regex regex(settings.pattern);
for (const auto &occurrence : stringOccurrences) {
std::string string(occurrence.region.getSize(), '\x00');
provider->read(occurrence.region.getStartAddress(), string.data(), occurrence.region.getSize());
task.update();
if (settings.fullMatch) {
if (boost::regex_match(string, regex))
result.push_back(occurrence);
} else {
if (boost::regex_search(string, regex))
result.push_back(occurrence);
}
}
return result;
}
std::vector<hex::ContentRegistry::DataFormatter::impl::FindOccurrence> ViewFind::searchBinaryPattern(Task &task, prv::Provider *provider, hex::Region searchRegion, const SearchSettings::BinaryPattern &settings) {
std::vector<Occurrence> results;
auto reader = prv::ProviderReader(provider);
reader.seek(searchRegion.getStartAddress());
reader.setEndAddress(searchRegion.getEndAddress());
const size_t patternSize = settings.pattern.getSize();
if (settings.alignment == 1) {
u32 matchedBytes = 0;
for (auto it = reader.begin(); it < reader.end(); it += 1) {
auto byte = *it;
task.update(it.getAddress());
if (settings.pattern.matchesByte(byte, matchedBytes)) {
matchedBytes++;
if (matchedBytes == settings.pattern.getSize()) {
auto occurrenceAddress = it.getAddress() - (patternSize - 1);
results.push_back(Occurrence { Region { .address=occurrenceAddress, .size=patternSize }, std::endian::native, Occurrence::DecodeType::Binary, false, {} });
it.setAddress(occurrenceAddress);
matchedBytes = 0;
}
} else {
if (matchedBytes > 0)
it -= matchedBytes;
matchedBytes = 0;
}
}
} else {
std::vector<u8> data(patternSize);
for (u64 address = searchRegion.getStartAddress(); address < searchRegion.getEndAddress(); address += settings.alignment) {
reader.read(address, data.data(), data.size());
task.update(address);
bool match = true;
for (u32 i = 0; i < patternSize; i++) {
if (!settings.pattern.matchesByte(data[i], i)) {
match = false;
break;
}
}
if (match)
results.push_back(Occurrence { Region { .address=address, .size=patternSize }, std::endian::native, Occurrence::DecodeType::Binary, false, {} });
}
}
return results;
}
template<typename T> T convert_signed_integer( T value, size_t size ) {
switch (size) {
case 1: return static_cast<T>(static_cast<i8>(value));
case 2: return static_cast<T>(static_cast<i16>(value));
case 4: return static_cast<T>(static_cast<i32>(value));
case 8: return static_cast<T>(static_cast<i64>(value));
default: return value;
}
}
std::vector<hex::ContentRegistry::DataFormatter::impl::FindOccurrence> ViewFind::searchValue(Task &task, prv::Provider *provider, Region searchRegion, const SearchSettings::Value &settings) {
std::vector<Occurrence> results;
auto reader = prv::ProviderReader(provider);
reader.seek(searchRegion.getStartAddress());
reader.setEndAddress(searchRegion.getEndAddress());
auto inputMin = settings.inputMin;
auto inputMax = settings.inputMax;
if (inputMax.empty())
inputMax = inputMin;
const auto [validMin, min, sizeMin] = parseNumericValueInput(inputMin, settings.type);
const auto [validMax, max, sizeMax] = parseNumericValueInput(inputMax, settings.type);
if (!validMin || !validMax || sizeMin != sizeMax)
return { };
const auto size = sizeMin;
const auto advance = settings.aligned ? size : 1;
for (u64 address = searchRegion.getStartAddress(); address < searchRegion.getEndAddress(); address += advance) {
task.update(address);
auto result = std::visit([&]<typename T>(T) {
using DecayedType = std::remove_cvref_t<std::decay_t<T>>;
auto minValue = std::get<DecayedType>(min);
auto maxValue = std::get<DecayedType>(max);
DecayedType value = 0;
reader.read(address, reinterpret_cast<u8*>(&value), size);
value = hex::changeEndianness(value, size, settings.endian);
if constexpr (std::signed_integral<DecayedType>)
value = convert_signed_integer<DecayedType>(value, size);
return value >= minValue && value <= maxValue;
}, min);
if (result) {
Occurrence::DecodeType decodeType = [&]{
switch (settings.type) {
using enum SearchSettings::Value::Type;
using enum Occurrence::DecodeType;
case U8:
case U16:
case U32:
case U64:
return Unsigned;
case I8:
case I16:
case I32:
case I64:
return Signed;
case F32:
return Float;
case F64:
return Double;
default:
return Binary;
}
}();
results.push_back(Occurrence { Region { .address=address, .size=size }, settings.endian, decodeType, false, {} });
}
}
return results;
}
std::vector<ViewFind::Occurrence> ViewFind::searchConstants(Task &task, prv::Provider* provider, Region searchRegion, const SearchSettings::Constants &settings) {
std::vector<Occurrence> results;
std::vector<ConstantGroup> constantGroups;
for (const auto &path : paths::Constants.read()) {
for (const auto &entry : std::fs::directory_iterator(path)) {
try {
constantGroups.emplace_back(entry.path());
} catch (const std::exception &e) {
ui::ToastError::open(fmt::format("Failed to load constant group from {}: {}", wolv::util::toUTF8String(entry.path()), e.what()));
}
}
}
auto reader = prv::ProviderReader(provider);
reader.seek(searchRegion.getStartAddress());
reader.setEndAddress(searchRegion.getEndAddress());
u64 constantCount = 0;
for (const auto &group : constantGroups) {
constantCount += group.getConstants().size();
}
task.setMaxValue(constantCount * searchRegion.getSize());
u64 progress = 0;
for (const auto &group : constantGroups) {
for (const auto &constant : group.getConstants()) {
const auto &pattern = constant.value;
const size_t patternSize = pattern.getSize();
if (settings.alignment == 1) {
u32 matchedBytes = 0;
for (auto it = reader.begin(); it < reader.end(); it += 1) {
auto byte = *it;
task.update(progress + it.getAddress());
if (pattern.matchesByte(byte, matchedBytes)) {
matchedBytes++;
if (matchedBytes == pattern.getSize()) {
auto occurrenceAddress = it.getAddress() - (patternSize - 1);
results.push_back(Occurrence {
Region { .address=occurrenceAddress, .size=patternSize },
std::endian::native,
Occurrence::DecodeType::ASCII,
false,
fmt::format("[{}] {}", group.getName(), constant.name)
});
it.setAddress(occurrenceAddress);
matchedBytes = 0;
}
} else {
if (matchedBytes > 0)
it -= matchedBytes;
matchedBytes = 0;
}
}
} else {
std::vector<u8> data(patternSize);
for (u64 address = searchRegion.getStartAddress(); address < searchRegion.getEndAddress(); address += settings.alignment) {
reader.read(address, data.data(), data.size());
task.update(address);
bool match = true;
for (u32 i = 0; i < patternSize; i++) {
if (!pattern.matchesByte(data[i], i)) {
match = false;
break;
}
}
if (match)
results.push_back(Occurrence {
Region { .address=address, .size=patternSize },
std::endian::native,
Occurrence::DecodeType::ASCII,
false,
fmt::format("[] {}", group.getName(), constant.name)
});
}
}
progress += searchRegion.getSize();
}
}
return results;
}
void ViewFind::runSearch() {
Region searchRegion = m_searchSettings.region;
if (m_searchSettings.mode == SearchSettings::Mode::Strings) {
AchievementManager::unlockAchievement("hex.builtin.achievement.find", "hex.builtin.achievement.find.find_strings.name");
} else if (m_searchSettings.mode == SearchSettings::Mode::Sequence) {
AchievementManager::unlockAchievement("hex.builtin.achievement.find", "hex.builtin.achievement.find.find_specific_string.name");
} else if (m_searchSettings.mode == SearchSettings::Mode::Value) {
if (m_searchSettings.value.inputMin == "250" && m_searchSettings.value.inputMax == "1000")
AchievementManager::unlockAchievement("hex.builtin.achievement.find", "hex.builtin.achievement.find.find_numeric.name");
}
m_occurrenceTree->clear();
EventHighlightingChanged::post();
m_searchTask = TaskManager::createTask("hex.builtin.view.find.searching", searchRegion.getSize(), [this, settings = m_searchSettings, searchRegion](auto &task) {
auto provider = ImHexApi::Provider::get();
switch (settings.mode) {
using enum SearchSettings::Mode;
case Strings:
m_foundOccurrences.get(provider) = searchStrings(task, provider, searchRegion, settings.strings);
break;
case Sequence:
m_foundOccurrences.get(provider) = searchSequence(task, provider, searchRegion, settings.bytes);
break;
case Regex:
m_foundOccurrences.get(provider) = searchRegex(task, provider, searchRegion, settings.regex);
break;
case BinaryPattern:
m_foundOccurrences.get(provider) = searchBinaryPattern(task, provider, searchRegion, settings.binaryPattern);
break;
case Value:
m_foundOccurrences.get(provider) = searchValue(task, provider, searchRegion, settings.value);
break;
case Constants:
m_foundOccurrences.get(provider) = searchConstants(task, provider, searchRegion, settings.constants);
break;
}
m_sortedOccurrences.get(provider).clear();
m_lastSelectedOccurrence = nullptr;
for (const auto &occurrence : m_foundOccurrences.get(provider))
m_occurrenceTree->insert({ .start=occurrence.region.getStartAddress(), .end=occurrence.region.getEndAddress() }, occurrence);
TaskManager::doLater([this, provider] {
EventHighlightingChanged::post();
m_settingsCollapsed.get(provider) = !m_foundOccurrences->empty();
});
});
m_decodeSettings = m_searchSettings;
m_foundOccurrences->clear();
m_sortedOccurrences->clear();
m_occurrenceTree->clear();
m_lastSelectedOccurrence = nullptr;
EventHighlightingChanged::post();
}
std::string ViewFind::decodeValue(prv::Provider *provider, const Occurrence &occurrence, size_t maxBytes) const {
std::vector<u8> bytes(std::min<size_t>(occurrence.region.getSize(), maxBytes));
provider->read(occurrence.region.getStartAddress(), bytes.data(), bytes.size());
std::string result;
switch (m_decodeSettings.mode) {
using enum SearchSettings::Mode;
case Value:
case Strings:
case Sequence:
case Regex:
{
switch (occurrence.decodeType) {
using enum Occurrence::DecodeType;
case Binary:
case ASCII:
result = hex::encodeByteString(bytes);
break;
case UTF8:
result = std::string(bytes.begin(), bytes.end());
result = wolv::util::replaceStrings(result, "\n", "");
result = wolv::util::replaceStrings(result, "\r", "");
break;
case UTF16:
for (size_t i = occurrence.endian == std::endian::little ? 0 : 1; i < bytes.size(); i += 2)
result += hex::encodeByteString({ bytes[i] });
break;
case Unsigned:
result = formatBytes<u64>(bytes, occurrence.endian);
break;
case Signed:
result = formatBytes<i64>(bytes, occurrence.endian);
break;
case Float:
result = formatBytes<float>(bytes, occurrence.endian);
break;
case Double:
result = formatBytes<double>(bytes, occurrence.endian);
break;
}
}
break;
case BinaryPattern:
result = hex::encodeByteString(bytes);
break;
case Constants:
result = occurrence.string;
break;
}
if (occurrence.region.getSize() > maxBytes)
result += "...";
return result;
}
void ViewFind::drawContextMenu(Occurrence &target, const std::string &value) {
if (ImGui::IsMouseClicked(ImGuiMouseButton_Right) && ImGui::IsItemHovered()) {
ImGui::OpenPopup("FindContextMenu");
target.selected = true;
m_replaceBuffer.clear();
}
if (ImGui::BeginPopup("FindContextMenu")) {
if (ImGui::MenuItemEx("hex.builtin.view.find.context.copy"_lang, ICON_VS_COPY))
ImGui::SetClipboardText(value.c_str());
if (ImGui::MenuItemEx("hex.builtin.view.find.context.copy_demangle"_lang, ICON_VS_FILES))
ImGui::SetClipboardText(trace::demangle(value).c_str());
if (ImGui::BeginMenuEx("hex.builtin.view.find.context.replace"_lang, ICON_VS_REPLACE)) {
if (ImGui::BeginTabBar("##replace_tabs")) {
if (ImGui::BeginTabItem("hex.builtin.view.find.context.replace.hex"_lang)) {
ImGuiExt::InputTextIcon("##replace_input", ICON_VS_SYMBOL_NAMESPACE, m_replaceBuffer);
ImGui::BeginDisabled(m_replaceBuffer.empty());
if (ImGui::Button("hex.builtin.view.find.context.replace"_lang)) {
auto provider = ImHexApi::Provider::get();
auto bytes = parseHexString(m_replaceBuffer);
for (const auto &occurrence : *m_sortedOccurrences) {
if (occurrence.selected) {
size_t size = std::min<size_t>(occurrence.region.size, bytes.size());
provider->write(occurrence.region.getStartAddress(), bytes.data(), size);
}
}
}
ImGui::EndDisabled();
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("hex.builtin.view.find.context.replace.ascii"_lang)) {
ImGuiExt::InputTextIcon("##replace_input", ICON_VS_SYMBOL_KEY, m_replaceBuffer);
ImGui::BeginDisabled(m_replaceBuffer.empty());
if (ImGui::Button("hex.builtin.view.find.context.replace"_lang)) {
auto provider = ImHexApi::Provider::get();
auto bytes = decodeByteString(m_replaceBuffer);
for (const auto &occurrence : *m_sortedOccurrences) {
if (occurrence.selected) {
size_t size = std::min<size_t>(occurrence.region.size, bytes.size());
provider->write(occurrence.region.getStartAddress(), bytes.data(), size);
}
}
}
ImGui::EndDisabled();
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
ImGui::EndMenu();
}
ImGui::EndPopup();
}
}
void ViewFind::drawContent() {
auto provider = ImHexApi::Provider::get();
ImGui::BeginDisabled(m_searchTask.isRunning());
{
auto &collapsed = m_settingsCollapsed.get(provider);
ImGui::SetNextWindowScroll(ImVec2(0, 0));
if (ImGuiExt::BeginSubWindow("hex.ui.common.settings"_lang, &collapsed, collapsed ? ImVec2(0, 1) : ImVec2(0, 0))) {
ui::regionSelectionPicker(&m_searchSettings.region, provider, &m_searchSettings.range, false, true);
ImGui::SameLine();
ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical);
ImGui::SameLine();
ImGui::BeginGroup();
if (ImGui::BeginTabBar("SearchMethods")) {
const std::array<std::string, 6> StringTypes = {
"hex.ui.common.encoding.ascii"_lang,
"hex.ui.common.encoding.utf8"_lang,
"hex.ui.common.encoding.utf16le"_lang,
"hex.ui.common.encoding.utf16be"_lang,
fmt::format("{} + {}", "hex.ui.common.encoding.ascii"_lang, "hex.ui.common.encoding.utf16le"_lang),
fmt::format("{} + {}", "hex.ui.common.encoding.ascii"_lang, "hex.ui.common.encoding.utf16be"_lang)
};
auto &mode = m_searchSettings.mode;
if (ImGui::BeginTabItem("hex.builtin.view.find.strings"_lang)) {
auto &settings = m_searchSettings.strings;
mode = SearchSettings::Mode::Strings;
ImGui::InputInt("hex.builtin.view.find.strings.min_length"_lang, &settings.minLength, 1, 1);
if (settings.minLength < 1)
settings.minLength = 1;
if (ImGui::BeginCombo("hex.ui.common.type"_lang, StringTypes[std::to_underlying(settings.type)].c_str())) {
for (size_t i = 0; i < StringTypes.size(); i++) {
auto type = static_cast<SearchSettings::StringType>(i);
if (ImGui::Selectable(StringTypes[i].c_str(), type == settings.type))
settings.type = type;
}
ImGui::EndCombo();
}
ImGui::SameLine();
if (ImGuiExt::DimmedIconButton(ICON_VS_SYMBOL_STRING, ImGui::GetStyleColorVec4(ImGuiCol_Text))) {
ImGui::OpenPopup("##match_settings");
}
ImGui::SetItemTooltip("%s", "hex.builtin.view.find.strings.match_settings"_lang.get());
ImGui::SetNextWindowPos(ImGui::GetCursorScreenPos(), ImGuiCond_Always);
if (ImGui::BeginPopup("##match_settings")) {
ImGui::Checkbox("hex.builtin.view.find.strings.null_term"_lang, &settings.nullTermination);
ImGuiExt::Header("hex.builtin.view.find.strings.chars"_lang);
ImGui::Checkbox(fmt::format("{} [a-z]", "hex.builtin.view.find.strings.lower_case"_lang.get()).c_str(), &settings.lowerCaseLetters);
ImGui::Checkbox(fmt::format("{} [A-Z]", "hex.builtin.view.find.strings.upper_case"_lang.get()).c_str(), &settings.upperCaseLetters);
ImGui::Checkbox(fmt::format("{} [0-9]", "hex.builtin.view.find.strings.numbers"_lang.get()).c_str(), &settings.numbers);
ImGui::Checkbox(fmt::format("{} [_]", "hex.builtin.view.find.strings.underscores"_lang.get()).c_str(), &settings.underscores);
ImGui::Checkbox(fmt::format("{} [!\"#$%...]", "hex.builtin.view.find.strings.symbols"_lang.get()).c_str(), &settings.symbols);
ImGui::Checkbox(fmt::format(R"({} [ \f\t\v])", "hex.builtin.view.find.strings.spaces"_lang.get()).c_str(), &settings.spaces);
ImGui::Checkbox(fmt::format("{} [\\r\\n]", "hex.builtin.view.find.strings.line_feeds"_lang.get()).c_str(), &settings.lineFeeds);
ImGui::EndPopup();
}
m_settingsValid = true;
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("hex.builtin.view.find.sequences"_lang)) {
auto &settings = m_searchSettings.bytes;
mode = SearchSettings::Mode::Sequence;
ImGuiExt::InputTextIconHint("hex.ui.common.value"_lang, ICON_VS_SYMBOL_KEY, "String", settings.sequence);
if (ImGui::BeginCombo("hex.ui.common.type"_lang, StringTypes[std::to_underlying(settings.type)].c_str())) {
for (size_t i = 0; i < StringTypes.size() - 2; i++) {
auto type = static_cast<SearchSettings::StringType>(i);
if (ImGui::Selectable(StringTypes[i].c_str(), type == settings.type))
settings.type = type;
}
ImGui::EndCombo();
}
ImGui::Checkbox("hex.builtin.view.find.sequences.ignore_case"_lang, &settings.ignoreCase);
m_settingsValid = !settings.sequence.empty() && !hex::decodeByteString(settings.sequence).empty();
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("hex.builtin.view.find.regex"_lang)) {
auto &settings = m_searchSettings.regex;
mode = SearchSettings::Mode::Regex;
ImGui::InputInt("hex.builtin.view.find.strings.min_length"_lang, &settings.minLength, 1, 1);
if (settings.minLength < 1)
settings.minLength = 1;
if (ImGui::BeginCombo("hex.ui.common.type"_lang, StringTypes[std::to_underlying(settings.type)].c_str())) {
for (size_t i = 0; i < StringTypes.size(); i++) {
auto type = static_cast<SearchSettings::StringType>(i);
if (ImGui::Selectable(StringTypes[i].c_str(), type == settings.type))
settings.type = type;
}
ImGui::EndCombo();
}
ImGui::Checkbox("hex.builtin.view.find.strings.null_term"_lang, &settings.nullTermination);
ImGui::NewLine();
ImGuiExt::InputTextIconHint("hex.builtin.view.find.regex.pattern"_lang, ICON_VS_REGEX, "[A-Za-z]{2}\\d{3}", settings.pattern);
try {
boost::regex regex(settings.pattern);
m_settingsValid = true;
} catch (const boost::regex_error &) {
m_settingsValid = false;
}
if (settings.pattern.empty())
m_settingsValid = false;
ImGui::Checkbox("hex.builtin.view.find.regex.full_match"_lang, &settings.fullMatch);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("hex.builtin.view.find.binary_pattern"_lang)) {
auto &settings = m_searchSettings.binaryPattern;
mode = SearchSettings::Mode::BinaryPattern;
if (ImGuiExt::InputTextIconHint("hex.builtin.view.find.binary_pattern"_lang, ICON_VS_SYMBOL_NAMESPACE, "AA BB ?? ?D \"XYZ\" u32be(1234)", settings.input)) {
settings.pattern = hex::BinaryPattern(settings.input);
}
constexpr static u32 min = 1, max = 0x1000;
ImGui::SliderScalar("hex.builtin.view.find.binary_pattern.alignment"_lang, ImGuiDataType_U32, &settings.alignment, &min, &max);
m_settingsValid = settings.pattern.isValid() && settings.alignment > 0;
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("hex.builtin.view.find.value"_lang)) {
auto &settings = m_searchSettings.value;
mode = SearchSettings::Mode::Value;
bool edited = false;
if (settings.range) {
if (ImGuiExt::InputTextIcon("hex.builtin.view.find.value.min"_lang, ICON_VS_SYMBOL_NUMERIC, settings.inputMin)) edited = true;
if (ImGuiExt::InputTextIcon("hex.builtin.view.find.value.max"_lang, ICON_VS_SYMBOL_NUMERIC, settings.inputMax)) edited = true;
} else {
if (ImGuiExt::InputTextIcon("hex.ui.common.value"_lang, ICON_VS_SYMBOL_NUMERIC, settings.inputMin)) {
edited = true;
settings.inputMax = settings.inputMin;
}
ImGui::BeginDisabled();
ImGuiExt::InputTextIcon("##placeholder_value", ICON_VS_SYMBOL_NUMERIC, settings.inputMax);
ImGui::EndDisabled();
}
if (ImGui::Checkbox("hex.builtin.view.find.value.range"_lang, &settings.range)) {
settings.inputMax = settings.inputMin;
}
ImGui::NewLine();
constexpr static std::array InputTypes = {
"hex.ui.common.type.u8"_lang,
"hex.ui.common.type.u16"_lang,
"hex.ui.common.type.u32"_lang,
"hex.ui.common.type.u64"_lang,
"hex.ui.common.type.i8"_lang,
"hex.ui.common.type.i16"_lang,
"hex.ui.common.type.i32"_lang,
"hex.ui.common.type.i64"_lang,
"hex.ui.common.type.f32"_lang,
"hex.ui.common.type.f64"_lang
};
if (ImGui::BeginCombo("hex.ui.common.type"_lang, InputTypes[std::to_underlying(settings.type)].get())) {
for (size_t i = 0; i < InputTypes.size(); i++) {
auto type = static_cast<SearchSettings::Value::Type>(i);
if (ImGui::Selectable(InputTypes[i].get(), type == settings.type)) {
settings.type = type;
edited = true;
}
}
ImGui::EndCombo();
}
{
int selection = [&] {
switch (settings.endian) {
default:
case std::endian::little: return 0;
case std::endian::big: return 1;
}
}();
std::array options = { "hex.ui.common.little"_lang, "hex.ui.common.big"_lang };
if (ImGui::SliderInt("hex.ui.common.endian"_lang, &selection, 0, options.size() - 1, options[selection], ImGuiSliderFlags_NoInput)) {
edited = true;
switch (selection) {
default:
case 0: settings.endian = std::endian::little; break;
case 1: settings.endian = std::endian::big; break;
}
}
}
ImGui::Checkbox("hex.builtin.view.find.value.aligned"_lang, &settings.aligned);
if (edited) {
auto [minValid, min, minSize] = parseNumericValueInput(settings.inputMin, settings.type);
auto [maxValid, max, maxSize] = parseNumericValueInput(settings.inputMax, settings.type);
m_settingsValid = minValid && maxValid && minSize == maxSize;
}
if (settings.inputMin.empty())
m_settingsValid = false;
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("hex.builtin.view.find.constants"_lang)) {
auto &settings = m_searchSettings.constants;
mode = SearchSettings::Mode::Constants;
constexpr static u32 min = 1, max = 0x1000;
ImGui::SliderScalar("hex.builtin.view.find.binary_pattern.alignment"_lang, ImGuiDataType_U32, &settings.alignment, &min, &max);
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
ImGui::EndGroup();
}
ImGuiExt::EndSubWindow();
ImGui::BeginDisabled(!m_settingsValid);
{
if (ImGuiExt::DimmedIconButton(ICON_VS_SEARCH, ImGui::GetStyleColorVec4(ImGuiCol_Text))) {
this->runSearch();
}
ImGui::SetItemTooltip("%s", "hex.builtin.view.find.search"_lang.get());
}
ImGui::EndDisabled();
ImGui::SameLine();
ImGui::BeginDisabled(m_foundOccurrences->empty());
{
if (ImGuiExt::DimmedIconButton(ICON_VS_SEARCH_STOP, ImGui::GetStyleColorVec4(ImGuiCol_Text))) {
m_foundOccurrences->clear();
m_sortedOccurrences->clear();
m_occurrenceTree->clear();
m_lastSelectedOccurrence = nullptr;
EventHighlightingChanged::post();
}
ImGui::SetItemTooltip("%s", "hex.builtin.view.find.search.reset"_lang.get());
}
ImGui::EndDisabled();
ImGui::SameLine();
ImGuiExt::TextFormatted("hex.builtin.view.find.search.entries"_lang, m_foundOccurrences->size());
}
ImGui::EndDisabled();
ImGui::Separator();
ImGui::NewLine();
auto &currOccurrences = *m_sortedOccurrences;
ImGui::PushItemWidth(-30_scaled);
auto prevFilterLength = m_currFilter->length();
if (ImGuiExt::InputTextIcon("##filter", ICON_VS_FILTER, *m_currFilter)) {
if (prevFilterLength > m_currFilter->length())
*m_sortedOccurrences = *m_foundOccurrences;
if (m_filterTask.isRunning())
m_filterTask.interrupt();
static std::mutex mutex;
std::scoped_lock lock(mutex);
if (!m_currFilter->empty()) {
m_filterTask = TaskManager::createTask("hex.builtin.task.filtering_data", currOccurrences.size(), [this, provider, &currOccurrences, filter = m_currFilter.get(provider)](Task &task) {
std::scoped_lock lock(mutex);
u64 progress = 0;
std::erase_if(currOccurrences, [this, provider, &task, &progress, &filter](const auto &region) {
task.update(progress);
progress += 1;
return !hex::containsIgnoreCase(this->decodeValue(provider, region), filter);
});
});
}
}
ImGui::PopItemWidth();
ImGui::SameLine();
const auto startPos = ImGui::GetCursorPos();
ImGui::BeginDisabled(m_sortedOccurrences->empty());
if (ImGuiExt::DimmedIconButton(ICON_VS_EXPORT, ImGui::GetStyleColorVec4(ImGuiCol_Text))) {
ImGui::OpenPopup("ExportResults");
}
ImGui::EndDisabled();
ImGui::SetNextWindowPos(ImGui::GetWindowPos() + ImVec2(startPos.x, ImGui::GetCursorPosY()));
if (ImGui::BeginPopup("ExportResults")) {
for (const auto &formatter : ContentRegistry::DataFormatter::impl::getFindExporterEntries()) {
const auto formatterName = formatter.unlocalizedName;
const auto name = toUpper(formatterName);
const auto &extension = formatter.fileExtension;
if (ImGui::MenuItem(name.c_str())) {
fs::openFileBrowser(fs::DialogMode::Save, { { name.c_str(), extension.c_str() } }, [&](const std::fs::path &path) {
wolv::io::File file(path, wolv::io::File::Mode::Create);
if (!file.isValid())
return;
auto result = formatter.callback(
m_sortedOccurrences.get(provider),
[&](Occurrence o){ return this->decodeValue(provider, o); });
file.writeVector(result);
file.close();
});
}
}
ImGui::EndPopup();
}
if (ImGui::BeginTable("##entries", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_Sortable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY, ImMax(ImGui::GetContentRegionAvail(), ImVec2(0, ImGui::GetTextLineHeightWithSpacing() * 5)))) {
ImGui::TableSetupScrollFreeze(0, 1);
ImGui::TableSetupColumn("hex.ui.common.offset"_lang, 0, -1, ImGui::GetID("offset"));
ImGui::TableSetupColumn("hex.ui.common.size"_lang, 0, -1, ImGui::GetID("size"));
ImGui::TableSetupColumn("hex.ui.common.value"_lang, ImGuiTableColumnFlags_WidthStretch, -1, ImGui::GetID("value"));
auto sortSpecs = ImGui::TableGetSortSpecs();
if (m_sortedOccurrences->empty() && !m_foundOccurrences->empty()) {
currOccurrences = *m_foundOccurrences;
sortSpecs->SpecsDirty = true;
}
if (sortSpecs->SpecsDirty) {
std::ranges::stable_sort(currOccurrences, [this, &sortSpecs, provider](const Occurrence &left, const Occurrence &right) -> bool {
if (sortSpecs->Specs->ColumnUserID == ImGui::GetID("offset")) {
if (sortSpecs->Specs->SortDirection == ImGuiSortDirection_Ascending)
return left.region.getStartAddress() < right.region.getStartAddress();
else
return left.region.getStartAddress() > right.region.getStartAddress();
} else if (sortSpecs->Specs->ColumnUserID == ImGui::GetID("size")) {
if (sortSpecs->Specs->SortDirection == ImGuiSortDirection_Ascending)
return left.region.getSize() < right.region.getSize();
else
return left.region.getSize() > right.region.getSize();
} else if (sortSpecs->Specs->ColumnUserID == ImGui::GetID("value")) {
if (sortSpecs->Specs->SortDirection == ImGuiSortDirection_Ascending)
return this->decodeValue(provider, left) < this->decodeValue(provider, right);
else
return this->decodeValue(provider, left) > this->decodeValue(provider, right);
}
return false;
});
sortSpecs->SpecsDirty = false;
}
ImGui::TableHeadersRow();
ImGuiListClipper clipper;
clipper.Begin(currOccurrences.size(), ImGui::GetTextLineHeightWithSpacing());
while (clipper.Step()) {
for (size_t i = clipper.DisplayStart; i < std::min<size_t>(clipper.DisplayEnd, currOccurrences.size()); i++) {
auto &foundItem = currOccurrences[i];
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGuiExt::TextFormatted("0x{:08X}", foundItem.region.getStartAddress());
ImGui::TableNextColumn();
ImGuiExt::TextFormatted("{}", hex::toByteString(foundItem.region.getSize()));
ImGui::TableNextColumn();
ImGui::PushID(i);
auto value = this->decodeValue(provider, foundItem, 256);
ImGuiExt::TextFormatted("{}", value);
ImGui::SameLine();
if (ImGui::Selectable("##line", foundItem.selected, ImGuiSelectableFlags_SpanAllColumns)) {
if (ImGui::GetIO().KeyShift && m_lastSelectedOccurrence != nullptr) {
for (auto start = std::min(&foundItem, m_lastSelectedOccurrence.get(provider)); start <= std::max(&foundItem, m_lastSelectedOccurrence.get(provider)); start += 1)
start->selected = true;
} else if (ImGui::GetIO().KeyCtrl) {
foundItem.selected = !foundItem.selected;
} else {
for (auto &occurrence : *m_sortedOccurrences)
occurrence.selected = false;
foundItem.selected = true;
ImHexApi::HexEditor::setSelection(foundItem.region.getStartAddress(), foundItem.region.getSize());
}
m_lastSelectedOccurrence = &foundItem;
}
drawContextMenu(foundItem, value);
ImGui::PopID();
}
}
clipper.End();
ImGui::EndTable();
}
}
void ViewFind::drawHelpText() {
ImGuiExt::TextFormattedWrapped("This view lets you search for all occurrences of a specific pattern in the opened data source.");
ImGui::NewLine();
ImGuiExt::TextFormattedWrapped(
"- Strings: Search for all strings matching the specified criteria.\n"
"- Sequences: Search for a specific string character sequence in various encodings.\n"
"- Regex: Search for all strings matching the specified regular expression.\n"
"- Binary Pattern: Search for a specific byte pattern with wildcards.\n"
"- Numeric Value: Search for numeric values within a specified range in various formats."
);
}
}