diff --git a/CMakeLists.txt b/CMakeLists.txt index bb83d6e..1cc8ccb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -73,6 +73,8 @@ add_executable(Reclass src/titlebar.cpp src/mcp/mcp_bridge.h src/mcp/mcp_bridge.cpp + src/addressparser.h + src/addressparser.cpp src/disasm.h src/disasm.cpp third_party/fadec/decode.c @@ -154,17 +156,17 @@ if(BUILD_TESTING) # ── Headless tests (Qt::Core only — safe for CI without a display) ── - add_executable(test_core tests/test_core.cpp src/format.cpp src/compose.cpp) + add_executable(test_core tests/test_core.cpp src/format.cpp src/compose.cpp src/addressparser.cpp) target_include_directories(test_core PRIVATE src) target_link_libraries(test_core PRIVATE ${QT}::Core ${QT}::Test) add_test(NAME test_core COMMAND test_core) - add_executable(test_format tests/test_format.cpp src/format.cpp) + add_executable(test_format tests/test_format.cpp src/format.cpp src/addressparser.cpp) target_include_directories(test_format PRIVATE src) target_link_libraries(test_format PRIVATE ${QT}::Core ${QT}::Test) add_test(NAME test_format COMMAND test_format) - add_executable(test_compose tests/test_compose.cpp src/compose.cpp src/format.cpp) + add_executable(test_compose tests/test_compose.cpp src/compose.cpp src/format.cpp src/addressparser.cpp) target_include_directories(test_compose PRIVATE src) target_link_libraries(test_compose PRIVATE ${QT}::Core ${QT}::Test) add_test(NAME test_compose COMMAND test_compose) @@ -180,42 +182,47 @@ if(BUILD_TESTING) add_test(NAME test_command_row COMMAND test_command_row) add_executable(test_generator tests/test_generator.cpp - src/generator.cpp src/compose.cpp src/format.cpp) + src/generator.cpp src/compose.cpp src/format.cpp src/addressparser.cpp) target_include_directories(test_generator PRIVATE src) target_link_libraries(test_generator PRIVATE ${QT}::Core ${QT}::Test) add_test(NAME test_generator COMMAND test_generator) add_executable(test_import_xml tests/test_import_xml.cpp - src/import_reclass_xml.cpp src/format.cpp src/compose.cpp) + src/import_reclass_xml.cpp src/format.cpp src/compose.cpp src/addressparser.cpp) target_include_directories(test_import_xml PRIVATE src) target_link_libraries(test_import_xml PRIVATE ${QT}::Core ${QT}::Test) add_test(NAME test_import_xml COMMAND test_import_xml) add_executable(test_import_source tests/test_import_source.cpp - src/import_source.cpp src/format.cpp src/compose.cpp) + src/import_source.cpp src/format.cpp src/compose.cpp src/addressparser.cpp) target_include_directories(test_import_source PRIVATE src) target_link_libraries(test_import_source PRIVATE ${QT}::Core ${QT}::Test) add_test(NAME test_import_source COMMAND test_import_source) add_executable(test_export_xml tests/test_export_xml.cpp - src/export_reclass_xml.cpp src/import_reclass_xml.cpp src/format.cpp src/compose.cpp) + src/export_reclass_xml.cpp src/import_reclass_xml.cpp src/format.cpp src/compose.cpp src/addressparser.cpp) target_include_directories(test_export_xml PRIVATE src) target_link_libraries(test_export_xml PRIVATE ${QT}::Core ${QT}::Test) add_test(NAME test_export_xml COMMAND test_export_xml) add_executable(test_disasm tests/test_disasm.cpp - src/disasm.cpp src/compose.cpp src/format.cpp + src/disasm.cpp src/compose.cpp src/format.cpp src/addressparser.cpp third_party/fadec/decode.c third_party/fadec/format.c) target_include_directories(test_disasm PRIVATE src third_party/fadec) target_link_libraries(test_disasm PRIVATE ${QT}::Core ${QT}::Test) add_test(NAME test_disasm COMMAND test_disasm) + add_executable(test_addressparser tests/test_addressparser.cpp src/addressparser.cpp) + target_include_directories(test_addressparser PRIVATE src) + target_link_libraries(test_addressparser PRIVATE ${QT}::Core ${QT}::Test) + add_test(NAME test_addressparser COMMAND test_addressparser) + # ── UI tests (require Qt::Widgets / QScintilla / display — skip on headless CI) ── option(BUILD_UI_TESTS "Build tests that require a display (Qt Widgets)" ON) if(BUILD_UI_TESTS) add_executable(test_controller tests/test_controller.cpp - src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp + src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp src/typeselectorpopup.cpp src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS}) @@ -229,7 +236,7 @@ if(BUILD_TESTING) add_test(NAME test_controller COMMAND test_controller) add_executable(test_validation tests/test_validation.cpp - src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp + src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp src/typeselectorpopup.cpp src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS}) @@ -243,7 +250,7 @@ if(BUILD_TESTING) add_test(NAME test_validation COMMAND test_validation) add_executable(test_context_menu tests/test_context_menu.cpp - src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp + src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp src/typeselectorpopup.cpp src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS}) @@ -257,7 +264,7 @@ if(BUILD_TESTING) add_test(NAME test_context_menu COMMAND test_context_menu) add_executable(test_source_management tests/test_source_management.cpp - src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp + src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp src/typeselectorpopup.cpp src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS}) @@ -271,7 +278,7 @@ if(BUILD_TESTING) add_test(NAME test_source_management COMMAND test_source_management) add_executable(test_editor tests/test_editor.cpp - src/editor.cpp src/compose.cpp src/format.cpp + src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/providerregistry.cpp src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS}) target_include_directories(test_editor PRIVATE src third_party/fadec) @@ -281,7 +288,7 @@ if(BUILD_TESTING) add_test(NAME test_editor COMMAND test_editor) add_executable(test_rendered_view tests/test_rendered_view.cpp - src/generator.cpp src/compose.cpp src/format.cpp) + src/generator.cpp src/compose.cpp src/format.cpp src/addressparser.cpp) target_include_directories(test_rendered_view PRIVATE src) target_link_libraries(test_rendered_view PRIVATE ${QT}::Widgets ${QT}::PrintSupport ${QT}::Test @@ -289,7 +296,7 @@ if(BUILD_TESTING) add_test(NAME test_rendered_view COMMAND test_rendered_view) add_executable(test_new_features tests/test_new_features.cpp - src/generator.cpp src/compose.cpp src/format.cpp src/controller.cpp + src/generator.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp src/editor.cpp src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp src/typeselectorpopup.cpp src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS}) @@ -303,7 +310,7 @@ if(BUILD_TESTING) add_test(NAME test_new_features COMMAND test_new_features) add_executable(test_type_selector tests/test_type_selector.cpp - src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp + src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp src/typeselectorpopup.cpp src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS}) @@ -317,7 +324,7 @@ if(BUILD_TESTING) add_test(NAME test_type_selector COMMAND test_type_selector) add_executable(test_type_visibility tests/test_type_visibility.cpp - src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp + src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp src/typeselectorpopup.cpp src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS}) diff --git a/plugins/ProcessMemory/ProcessMemoryPlugin.cpp b/plugins/ProcessMemory/ProcessMemoryPlugin.cpp index a7da356..2a14a90 100644 --- a/plugins/ProcessMemory/ProcessMemoryPlugin.cpp +++ b/plugins/ProcessMemory/ProcessMemoryPlugin.cpp @@ -284,6 +284,15 @@ void ProcessMemoryProvider::cacheModules() #endif // platform +uint64_t ProcessMemoryProvider::symbolToAddress(const QString& name) const +{ + for (const auto& mod : m_modules) { + if (mod.name.compare(name, Qt::CaseInsensitive) == 0) + return mod.base; + } + return 0; +} + ProcessMemoryProvider::~ProcessMemoryProvider() { #ifdef _WIN32 diff --git a/plugins/ProcessMemory/ProcessMemoryPlugin.h b/plugins/ProcessMemory/ProcessMemoryPlugin.h index 1089936..e8c1a49 100644 --- a/plugins/ProcessMemory/ProcessMemoryPlugin.h +++ b/plugins/ProcessMemory/ProcessMemoryPlugin.h @@ -24,6 +24,7 @@ public: QString name() const override { return m_processName; } QString kind() const override { return QStringLiteral("LocalProcess"); } QString getSymbol(uint64_t addr) const override; + uint64_t symbolToAddress(const QString& name) const override; bool isLive() const override { return true; } uint64_t base() const override { return m_base; } diff --git a/plugins/RcNetPluginCompatLayer/RcNetCompatProvider.cpp b/plugins/RcNetPluginCompatLayer/RcNetCompatProvider.cpp index ad1ace4..3ab256e 100644 --- a/plugins/RcNetPluginCompatLayer/RcNetCompatProvider.cpp +++ b/plugins/RcNetPluginCompatLayer/RcNetCompatProvider.cpp @@ -74,6 +74,15 @@ QString RcNetCompatProvider::getSymbol(uint64_t addr) const return {}; } +uint64_t RcNetCompatProvider::symbolToAddress(const QString& name) const +{ + for (const auto& mod : m_modules) { + if (mod.name.compare(name, Qt::CaseInsensitive) == 0) + return mod.base; + } + return 0; +} + // -- Module enumeration --------------------------------------------------- namespace { diff --git a/plugins/RcNetPluginCompatLayer/RcNetCompatProvider.h b/plugins/RcNetPluginCompatLayer/RcNetCompatProvider.h index fdba8c0..e76d608 100644 --- a/plugins/RcNetPluginCompatLayer/RcNetCompatProvider.h +++ b/plugins/RcNetPluginCompatLayer/RcNetCompatProvider.h @@ -28,6 +28,7 @@ public: bool isLive() const override { return true; } uint64_t base() const override { return m_base; } QString getSymbol(uint64_t addr) const override; + uint64_t symbolToAddress(const QString& name) const override; struct ModuleInfo { QString name; diff --git a/src/addressparser.cpp b/src/addressparser.cpp new file mode 100644 index 0000000..98c6f84 --- /dev/null +++ b/src/addressparser.cpp @@ -0,0 +1,300 @@ +#include "addressparser.h" + +namespace rcx { + +// ── Address Expression Parser ────────────────────────────────────────── +// +// Parses expressions like: +// "7FF66CCE0000" → plain hex address +// "0x100 + 0x200" → arithmetic on hex values +// " + 0xDE" → module base + offset +// "[ + 0xDE] - AB" → dereference pointer, then subtract +// "7ff6`6cce0000" → WinDbg-style backtick separator (stripped before parsing) +// +// Grammar (standard operator precedence: *, / bind tighter than +, -): +// +// expr = term (('+' | '-') term)* +// term = unary (('*' | '/') unary)* +// unary = '-' unary | atom +// atom = '[' expr ']' -- read pointer at address (dereference) +// | '<' moduleName '>' -- resolve module base address +// | '(' expr ')' -- grouping +// | hexLiteral -- hex number, optional 0x prefix +// +// All numeric literals are hexadecimal (base 16). +// Module names and pointer reads are resolved via optional callbacks. +// Without callbacks, modules and dereferences evaluate to 0 (syntax-check mode). + +class ExpressionParser { +public: + ExpressionParser(const QString& input, const AddressParserCallbacks* callbacks) + : m_input(input), m_callbacks(callbacks) {} + + AddressParseResult parse() { + skipSpaces(); + if (atEnd()) + return error("empty expression"); + + uint64_t value = 0; + if (!parseExpression(value)) + return error(m_error); + + skipSpaces(); + if (!atEnd()) + return error(QStringLiteral("unexpected '%1'").arg(m_input[m_pos])); + + return {true, value, {}, -1}; + } + +private: + const QString& m_input; + const AddressParserCallbacks* m_callbacks; + int m_pos = 0; + QString m_error; + int m_errorPos = 0; + + // ── Helpers ── + + bool atEnd() const { return m_pos >= m_input.size(); } + + QChar peek() const { return atEnd() ? QChar('\0') : m_input[m_pos]; } + + void advance() { m_pos++; } + + void skipSpaces() { + while (!atEnd() && m_input[m_pos].isSpace()) + m_pos++; + } + + AddressParseResult error(const QString& msg) const { + return {false, 0, msg, m_errorPos}; + } + + bool fail(const QString& msg) { + m_error = msg; + m_errorPos = m_pos; + return false; + } + + bool expect(QChar ch) { + skipSpaces(); + if (peek() != ch) + return fail(QStringLiteral("expected '%1'").arg(ch)); + advance(); + return true; + } + + static bool isHexDigit(QChar ch) { + return (ch >= '0' && ch <= '9') + || (ch >= 'a' && ch <= 'f') + || (ch >= 'A' && ch <= 'F'); + } + + // ── Recursive descent parsing ── + + // expr = term (('+' | '-') term)* + bool parseExpression(uint64_t& result) { + if (!parseTerm(result)) + return false; + + for (;;) { + skipSpaces(); + QChar op = peek(); + if (op != '+' && op != '-') + break; + advance(); + + uint64_t rhs = 0; + if (!parseTerm(rhs)) + return false; + + result = (op == '+') ? result + rhs : result - rhs; + } + return true; + } + + // term = unary (('*' | '/') unary)* + bool parseTerm(uint64_t& result) { + if (!parseUnary(result)) + return false; + + for (;;) { + skipSpaces(); + QChar op = peek(); + if (op != '*' && op != '/') + break; + advance(); + + uint64_t rhs = 0; + if (!parseUnary(rhs)) + return false; + + if (op == '*') { + result *= rhs; + } else { + if (rhs == 0) + return fail("division by zero"); + result /= rhs; + } + } + return true; + } + + // unary = '-' unary | atom + bool parseUnary(uint64_t& result) { + skipSpaces(); + if (peek() == '-') { + advance(); + uint64_t inner = 0; + if (!parseUnary(inner)) + return false; + result = static_cast(-static_cast(inner)); + return true; + } + return parseAtom(result); + } + + // atom = '[' expr ']' | '<' name '>' | '(' expr ')' | hexLiteral + bool parseAtom(uint64_t& result) { + skipSpaces(); + if (atEnd()) + return fail("unexpected end of expression"); + + QChar ch = peek(); + + if (ch == '[') return parseDereference(result); + if (ch == '<') return parseModuleName(result); + if (ch == '(') return parseGrouping(result); + return parseHexNumber(result); + } + + // '[' expr ']' — read the pointer value at the computed address + bool parseDereference(uint64_t& result) { + advance(); // skip '[' + + uint64_t address = 0; + if (!parseExpression(address)) + return false; + if (!expect(']')) + return false; + + // Without a callback, just return 0 (syntax-check mode) + if (!m_callbacks || !m_callbacks->readPointer) { + result = 0; + return true; + } + + bool ok = false; + result = m_callbacks->readPointer(address, &ok); + if (!ok) + return fail(QStringLiteral("failed to read memory at 0x%1").arg(address, 0, 16)); + return true; + } + + // '<' moduleName '>' — resolve a module's base address (e.g. ) + bool parseModuleName(uint64_t& result) { + advance(); // skip '<' + + int nameStart = m_pos; + while (!atEnd() && peek() != '>') + advance(); + if (atEnd()) + return fail("expected '>'"); + + QString name = m_input.mid(nameStart, m_pos - nameStart).trimmed(); + advance(); // skip '>' + + if (name.isEmpty()) + return fail("empty module name"); + + // Without a callback, just return 0 (syntax-check mode) + if (!m_callbacks || !m_callbacks->resolveModule) { + result = 0; + return true; + } + + bool ok = false; + result = m_callbacks->resolveModule(name, &ok); + if (!ok) + return fail(QStringLiteral("module '%1' not found").arg(name)); + return true; + } + + // '(' expr ')' — parenthesized sub-expression for grouping + bool parseGrouping(uint64_t& result) { + advance(); // skip '(' + if (!parseExpression(result)) + return false; + return expect(')'); + } + + // Hex number with optional "0x" prefix. All literals are base-16. + bool parseHexNumber(uint64_t& result) { + skipSpaces(); + if (atEnd()) + return fail("unexpected end of expression"); + + int start = m_pos; + + // Skip optional 0x/0X prefix + if (m_pos + 1 < m_input.size() + && m_input[m_pos] == '0' + && (m_input[m_pos + 1] == 'x' || m_input[m_pos + 1] == 'X')) + m_pos += 2; + + // Consume hex digits + int digitsStart = m_pos; + while (!atEnd() && isHexDigit(peek())) + advance(); + + if (m_pos == digitsStart) { + m_errorPos = start; + return fail("expected hex number"); + } + + QString digits = m_input.mid(digitsStart, m_pos - digitsStart); + bool ok = false; + result = digits.toULongLong(&ok, 16); + if (!ok) { + m_errorPos = start; + return fail("invalid hex number"); + } + return true; + } +}; + +// ── Public API ───────────────────────────────────────────────────────── + +AddressParseResult AddressParser::evaluate(const QString& formula, int ptrSize, + const AddressParserCallbacks* cb) +{ + Q_UNUSED(ptrSize); + + // WinDbg displays 64-bit addresses with backtick separators for readability, + // e.g. "00007ff6`1a2b3c4d". Strip them so users can paste directly. + // Also remove ' in case user uses it + QString cleaned = formula; + cleaned.remove('`'); + cleaned.remove('\''); + + ExpressionParser parser(cleaned, cb); + return parser.parse(); +} + +QString AddressParser::validate(const QString& formula) +{ + QString cleaned = formula; + cleaned.remove('`'); + cleaned.remove('\''); + cleaned = cleaned.trimmed(); + if (cleaned.isEmpty()) + return QStringLiteral("empty"); + + // Parse with no callbacks — modules and dereferences succeed but return 0. + // This checks syntax only. + ExpressionParser parser(cleaned, nullptr); + auto result = parser.parse(); + return result.ok ? QString() : result.error; +} + +} // namespace rcx diff --git a/src/addressparser.h b/src/addressparser.h new file mode 100644 index 0000000..a733d7a --- /dev/null +++ b/src/addressparser.h @@ -0,0 +1,27 @@ +#pragma once +#include +#include +#include + +namespace rcx { + +struct AddressParseResult { + bool ok; + uint64_t value; + QString error; + int errorPos; +}; + +struct AddressParserCallbacks { + std::function resolveModule; + std::function readPointer; +}; + +class AddressParser { +public: + static AddressParseResult evaluate(const QString& formula, int ptrSize = 8, + const AddressParserCallbacks* cb = nullptr); + static QString validate(const QString& formula); +}; + +} // namespace rcx diff --git a/src/controller.cpp b/src/controller.cpp index eb19fae..848dcb2 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -1,4 +1,5 @@ #include "controller.h" +#include "addressparser.h" #include "typeselectorpopup.h" #include "providerregistry.h" #include "themes/thememanager.h" @@ -328,53 +329,29 @@ void RcxController::connectEditor(RcxEditor* editor) { s.remove('`'); // WinDbg backtick separators (e.g. 7ff6`6cce0000) s.remove('\n'); s.remove('\r'); - // Support simple equations: 0x10+0x4, 0x100-0x10, etc. - uint64_t newBase = 0; - bool ok = true; - int pos = 0; - bool firstTerm = true; - bool adding = true; - while (pos < s.size() && ok) { - // Skip whitespace - while (pos < s.size() && s[pos].isSpace()) pos++; - if (pos >= s.size()) break; - - // Check for +/- operator (except first term) - if (!firstTerm) { - if (s[pos] == '+') { adding = true; pos++; } - else if (s[pos] == '-') { adding = false; pos++; } - else { ok = false; break; } - while (pos < s.size() && s[pos].isSpace()) pos++; - } - - // Parse hex number (with or without 0x prefix) - int start = pos; - bool hasPrefix = (pos + 1 < s.size() && - s[pos] == '0' && (s[pos+1] == 'x' || s[pos+1] == 'X')); - if (hasPrefix) pos += 2; - - int numStart = pos; - while (pos < s.size() && (s[pos].isDigit() || - (s[pos] >= 'a' && s[pos] <= 'f') || - (s[pos] >= 'A' && s[pos] <= 'F'))) pos++; - - if (pos == numStart) { ok = false; break; } - - QString numStr = s.mid(numStart, pos - numStart); - uint64_t val = numStr.toULongLong(&ok, 16); - if (!ok) break; - - if (adding) newBase += val; - else newBase -= val; - - firstTerm = false; + AddressParserCallbacks cbs; + if (m_doc->provider) { + auto* prov = m_doc->provider.get(); + cbs.resolveModule = [prov](const QString& name, bool* ok) -> uint64_t { + uint64_t base = prov->symbolToAddress(name); + *ok = (base != 0); + return base; + }; + cbs.readPointer = [prov](uint64_t addr, bool* ok) -> uint64_t { + uint64_t val = 0; + *ok = prov->read(addr, &val, 8); + return val; + }; } - - if (ok && newBase != m_doc->tree.baseAddress) { + auto result = AddressParser::evaluate(s, 8, &cbs); + if (result.ok && result.value != m_doc->tree.baseAddress) { uint64_t oldBase = m_doc->tree.baseAddress; + QString oldFormula = m_doc->tree.baseAddressFormula; + // Store formula if input uses module/deref syntax, otherwise clear + QString newFormula = (s.contains('<') || s.contains('[')) ? s : QString(); m_doc->undoStack.push(new RcxCommand(this, - cmd::ChangeBase{oldBase, newBase})); + cmd::ChangeBase{oldBase, result.value, oldFormula, newFormula})); } break; } @@ -982,6 +959,7 @@ void RcxController::applyCommand(const Command& command, bool isUndo) { clearHistoryForAdjs(c.offAdjs); } else if constexpr (std::is_same_v) { tree.baseAddress = isUndo ? c.oldBase : c.newBase; + tree.baseAddressFormula = isUndo ? c.oldFormula : c.newFormula; resetSnapshot(); } else if constexpr (std::is_same_v) { const QByteArray& bytes = isUndo ? c.oldBytes : c.newBytes; @@ -1779,30 +1757,15 @@ void RcxController::updateCommandRow() { .arg(provName); } - // -- Symbol for selected node (getSymbol integration) -- - QString sym; - if (m_selIds.size() == 1) { - uint64_t sid = *m_selIds.begin(); - int idx = m_doc->tree.indexOfId(sid & ~kFooterIdBit); - if (idx >= 0) { - const auto& node = m_doc->tree.nodes[idx]; - uint64_t addr = m_doc->tree.baseAddress + m_doc->tree.computeOffset(idx); - sym = m_doc->provider->getSymbol(addr); - } - } + QString addr; + if (!m_doc->tree.baseAddressFormula.isEmpty()) + addr = m_doc->tree.baseAddressFormula; + else + addr = QStringLiteral("0x") + + QString::number(m_doc->tree.baseAddress, 16).toUpper(); - QString addr = QStringLiteral("0x") + - QString::number(m_doc->tree.baseAddress, 16).toUpper(); - - // Build the row. If we have a symbol, append it after the address. - QString row; - if (sym.isEmpty()) { - row = QStringLiteral("%1 \u00B7 %2") - .arg(elide(src, 40), elide(addr, 24)); - } else { - row = QStringLiteral("%1 \u00B7 %2 %3") - .arg(elide(src, 40), elide(addr, 24), elide(sym, 40)); - } + QString row = QStringLiteral("%1 \u00B7 %2") + .arg(elide(src, 40), elide(addr, 24)); // Build row 2: root class type + name (uses current view root) QString row2; @@ -2341,6 +2304,26 @@ void RcxController::attachViaPlugin(const QString& providerIdentifier, const QSt m_doc->dataPath.clear(); if (m_doc->tree.baseAddress == 0) m_doc->tree.baseAddress = newBase; + + // Re-evaluate stored formula against the new provider + if (!m_doc->tree.baseAddressFormula.isEmpty()) { + AddressParserCallbacks cbs; + auto* prov = m_doc->provider.get(); + cbs.resolveModule = [prov](const QString& name, bool* ok) -> uint64_t { + uint64_t base = prov->symbolToAddress(name); + *ok = (base != 0); + return base; + }; + cbs.readPointer = [prov](uint64_t addr, bool* ok) -> uint64_t { + uint64_t val = 0; + *ok = prov->read(addr, &val, 8); + return val; + }; + auto result = AddressParser::evaluate(m_doc->tree.baseAddressFormula, 8, &cbs); + if (result.ok) + m_doc->tree.baseAddress = result.value; + } + resetSnapshot(); emit m_doc->documentChanged(); refresh(); @@ -2351,8 +2334,10 @@ void RcxController::switchToSavedSource(int idx) { if (idx == m_activeSourceIdx) return; // Save current source's base address before switching - if (m_activeSourceIdx >= 0 && m_activeSourceIdx < m_savedSources.size()) + if (m_activeSourceIdx >= 0 && m_activeSourceIdx < m_savedSources.size()) { m_savedSources[m_activeSourceIdx].baseAddress = m_doc->tree.baseAddress; + m_savedSources[m_activeSourceIdx].baseAddressFormula = m_doc->tree.baseAddressFormula; + } m_activeSourceIdx = idx; const auto& entry = m_savedSources[idx]; @@ -2360,9 +2345,12 @@ void RcxController::switchToSavedSource(int idx) { if (entry.kind == QStringLiteral("File")) { m_doc->loadData(entry.filePath); m_doc->tree.baseAddress = entry.baseAddress; + m_doc->tree.baseAddressFormula = entry.baseAddressFormula; refresh(); } else if (!entry.providerTarget.isEmpty()) { // Plugin-based provider (e.g. "processmemory" with target "pid:name") + // Restore formula before attach so it can be re-evaluated against the new provider + m_doc->tree.baseAddressFormula = entry.baseAddressFormula; attachViaPlugin(entry.kind, entry.providerTarget); } } diff --git a/src/controller.h b/src/controller.h index c636299..4d864e9 100644 --- a/src/controller.h +++ b/src/controller.h @@ -70,6 +70,7 @@ struct SavedSourceEntry { QString filePath; // for File sources QString providerTarget; // for plugin providers (e.g. "pid:name") uint64_t baseAddress = 0; + QString baseAddressFormula; }; // ── Controller ── diff --git a/src/core.h b/src/core.h index 750a544..7996e00 100644 --- a/src/core.h +++ b/src/core.h @@ -267,6 +267,7 @@ struct Node { struct NodeTree { QVector nodes; uint64_t baseAddress = 0x00400000; + QString baseAddressFormula; // e.g. " + 0x100" uint64_t m_nextId = 1; mutable QHash m_idCache; @@ -400,6 +401,8 @@ struct NodeTree { QJsonObject toJson() const { QJsonObject o; o["baseAddress"] = QString::number(baseAddress, 16); + if (!baseAddressFormula.isEmpty()) + o["baseAddressFormula"] = baseAddressFormula; o["nextId"] = QString::number(m_nextId); QJsonArray arr; for (const auto& n : nodes) arr.append(n.toJson()); @@ -410,6 +413,7 @@ struct NodeTree { static NodeTree fromJson(const QJsonObject& o) { NodeTree t; t.baseAddress = o["baseAddress"].toString("400000").toULongLong(nullptr, 16); + t.baseAddressFormula = o["baseAddressFormula"].toString(); t.m_nextId = o["nextId"].toString("1").toULongLong(); QJsonArray arr = o["nodes"].toArray(); for (const auto& v : arr) { @@ -541,7 +545,7 @@ namespace cmd { struct Insert { Node node; QVector offAdjs; }; struct Remove { uint64_t nodeId; QVector subtree; QVector offAdjs; }; - struct ChangeBase { uint64_t oldBase, newBase; }; + struct ChangeBase { uint64_t oldBase, newBase; QString oldFormula, newFormula; }; struct WriteBytes { uint64_t addr; QByteArray oldBytes, newBytes; }; struct ChangeArrayMeta { uint64_t nodeId; NodeKind oldElementKind, newElementKind; @@ -663,8 +667,11 @@ inline ColumnSpan commandRowAddrSpan(const QString& lineText) { int tag = lineText.indexOf(QStringLiteral(" \u00B7")); if (tag < 0) return {}; int start = tag + 3; // after " · " - int end = start; - while (end < lineText.size() && !lineText[end].isSpace()) end++; + // Scan to next " · " separator (or end of line) to support formulas with spaces + int nextSep = lineText.indexOf(QStringLiteral(" \u00B7"), start); + int end = (nextSep >= 0) ? nextSep : lineText.size(); + // Trim trailing whitespace + while (end > start && lineText[end - 1].isSpace()) end--; if (end <= start) return {}; return {start, end, true}; } diff --git a/src/format.cpp b/src/format.cpp index 19326e4..2b72385 100644 --- a/src/format.cpp +++ b/src/format.cpp @@ -1,4 +1,5 @@ #include "core.h" +#include "addressparser.h" #include #include #include @@ -664,43 +665,13 @@ QString validateValue(NodeKind kind, const QString& text) { return QStringLiteral("invalid"); } -// ── Base address validation (supports simple +/- equations) ── +// ── Base address validation (delegates to AddressParser) ── QString validateBaseAddress(const QString& text) { QString s = text.trimmed(); if (s.isEmpty()) return QStringLiteral("empty"); - - int pos = 0; - bool firstTerm = true; - - while (pos < s.size()) { - // Skip whitespace - while (pos < s.size() && s[pos].isSpace()) pos++; - if (pos >= s.size()) break; - - // Check for +/- operator (except first term) - if (!firstTerm) { - if (s[pos] == '+' || s[pos] == '-') pos++; - else return QStringLiteral("invalid '%1'").arg(s[pos]); - while (pos < s.size() && s[pos].isSpace()) pos++; - } - - // Skip 0x prefix if present - if (pos + 1 < s.size() && s[pos] == '0' && (s[pos+1] == 'x' || s[pos+1] == 'X')) - pos += 2; - - // Must have at least one hex digit - int numStart = pos; - while (pos < s.size() && (s[pos].isDigit() || - (s[pos] >= 'a' && s[pos] <= 'f') || - (s[pos] >= 'A' && s[pos] <= 'F'))) pos++; - - if (pos == numStart) return QStringLiteral("invalid"); - - firstTerm = false; - } - - return {}; + //s.remove('`'); + return AddressParser::validate(s); } } // namespace rcx::fmt diff --git a/src/providers/provider.h b/src/providers/provider.h index aaed39d..3a8271e 100644 --- a/src/providers/provider.h +++ b/src/providers/provider.h @@ -47,6 +47,13 @@ public: return {}; } + // Resolve a module/symbol name to its address (reverse of getSymbol). + // Returns 0 if the name is not found. + virtual uint64_t symbolToAddress(const QString& name) const { + Q_UNUSED(name); + return 0; + } + // --- Derived convenience (non-virtual, never override) --- bool isValid() const { return size() > 0; } diff --git a/src/providers/snapshot_provider.h b/src/providers/snapshot_provider.h index 74c23d9..b0727c2 100644 --- a/src/providers/snapshot_provider.h +++ b/src/providers/snapshot_provider.h @@ -67,6 +67,9 @@ public: QString getSymbol(uint64_t addr) const override { return m_real ? m_real->getSymbol(addr) : QString(); } + uint64_t symbolToAddress(const QString& n) const override { + return m_real ? m_real->symbolToAddress(n) : 0; + } bool write(uint64_t addr, const void* buf, int len) override { if (!m_real) return false; diff --git a/tests/test_addressparser.cpp b/tests/test_addressparser.cpp new file mode 100644 index 0000000..da9d275 --- /dev/null +++ b/tests/test_addressparser.cpp @@ -0,0 +1,219 @@ +#include "addressparser.h" +#include + +using rcx::AddressParser; +using rcx::AddressParserCallbacks; +using rcx::AddressParseResult; + +class TestAddressParser : public QObject { + Q_OBJECT + +private slots: + // -- Hex literals -- + + void bareHex() { auto r = AddressParser::evaluate("AB"); QVERIFY(r.ok); QCOMPARE(r.value, 0xABULL); } + void prefixedHex() { auto r = AddressParser::evaluate("0x1F4"); QVERIFY(r.ok); QCOMPARE(r.value, 0x1F4ULL); } + void zeroLiteral() { auto r = AddressParser::evaluate("0"); QVERIFY(r.ok); QCOMPARE(r.value, 0ULL); } + void large64bit() { auto r = AddressParser::evaluate("7FF66CCE0000");QVERIFY(r.ok); QCOMPARE(r.value, 0x7FF66CCE0000ULL); } + + // -- Arithmetic -- + + void addition() { + auto r = AddressParser::evaluate("0x100 + 0x200"); + QVERIFY(r.ok); QCOMPARE(r.value, 0x300ULL); + } + void subtraction() { + auto r = AddressParser::evaluate("0x300 - 0x100"); + QVERIFY(r.ok); QCOMPARE(r.value, 0x200ULL); + } + void multiplication() { + auto r = AddressParser::evaluate("0x10 * 4"); + QVERIFY(r.ok); QCOMPARE(r.value, 0x40ULL); + } + void division() { + auto r = AddressParser::evaluate("0x100 / 2"); + QVERIFY(r.ok); QCOMPARE(r.value, 0x80ULL); + } + void precedence() { + // 0x10 + 2*3 = 0x10 + 6 = 0x16 + auto r = AddressParser::evaluate("0x10 + 2 * 3"); + QVERIFY(r.ok); QCOMPARE(r.value, 0x16ULL); + } + void parentheses() { + // (0x10 + 2) * 3 = 0x12 * 3 = 0x36 + auto r = AddressParser::evaluate("(0x10 + 2) * 3"); + QVERIFY(r.ok); QCOMPARE(r.value, 0x36ULL); + } + + // -- Unary minus -- + + void unaryMinus() { + auto r = AddressParser::evaluate("-0x10 + 0x20"); + QVERIFY(r.ok); QCOMPARE(r.value, 0x10ULL); + } + + // -- Module resolution -- + + void moduleResolve() { + AddressParserCallbacks cbs; + cbs.resolveModule = [](const QString& name, bool* ok) -> uint64_t { + *ok = (name == "Program.exe"); + return *ok ? 0x140000000ULL : 0; + }; + auto r = AddressParser::evaluate(" + 0x123", 8, &cbs); + QVERIFY(r.ok); + QCOMPARE(r.value, 0x140000123ULL); + } + + void moduleNotFound() { + AddressParserCallbacks cbs; + cbs.resolveModule = [](const QString&, bool* ok) -> uint64_t { + *ok = false; + return 0; + }; + auto r = AddressParser::evaluate("", 8, &cbs); + QVERIFY(!r.ok); + QVERIFY(r.error.contains("not found")); + } + + // -- Dereference -- + + void derefSimple() { + AddressParserCallbacks cbs; + cbs.readPointer = [](uint64_t addr, bool* ok) -> uint64_t { + *ok = (addr == 0x1000); + return *ok ? 0xDEADBEEFULL : 0; + }; + auto r = AddressParser::evaluate("[0x1000]", 8, &cbs); + QVERIFY(r.ok); + QCOMPARE(r.value, 0xDEADBEEFULL); + } + + void derefNested() { + AddressParserCallbacks cbs; + cbs.resolveModule = [](const QString& name, bool* ok) -> uint64_t { + *ok = (name == "mod"); + return *ok ? 0x400000ULL : 0; + }; + cbs.readPointer = [](uint64_t addr, bool* ok) -> uint64_t { + *ok = true; + if (addr == 0x400100) return 0x500000; + if (addr == 0x900000) return 0xABCDEF; + return 0; + }; + // [ + [ + 0x100]] = [0x400000 + [0x400000+0x100]] + // inner deref: [0x400100] = 0x500000 + // outer: [0x400000 + 0x500000] = [0x900000] = 0xABCDEF + auto r = AddressParser::evaluate("[ + [ + 0x100]]", 8, &cbs); + QVERIFY(r.ok); + QCOMPARE(r.value, 0xABCDEFULL); + } + + void derefReadFailure() { + AddressParserCallbacks cbs; + cbs.readPointer = [](uint64_t, bool* ok) -> uint64_t { + *ok = false; + return 0; + }; + auto r = AddressParser::evaluate("[0x1000]", 8, &cbs); + QVERIFY(!r.ok); + QVERIFY(r.error.contains("failed to read")); + } + + // -- Complex expression from plan -- + + void complexExpr() { + AddressParserCallbacks cbs; + cbs.resolveModule = [](const QString& name, bool* ok) -> uint64_t { + *ok = (name == "Program.exe"); + return *ok ? 0x140000000ULL : 0; + }; + cbs.readPointer = [](uint64_t addr, bool* ok) -> uint64_t { + *ok = true; + if (addr == 0x1400000DEULL) return 0x500000; + return 0; + }; + // [ + 0xDE] - AB = [0x1400000DE] - 0xAB = 0x500000 - 0xAB = 0x4FFF55 + auto r = AddressParser::evaluate("[ + 0xDE] - AB", 8, &cbs); + QVERIFY(r.ok); + QCOMPARE(r.value, 0x4FFF55ULL); + } + + // -- Errors -- + + void emptyInput() { + auto r = AddressParser::evaluate(""); + QVERIFY(!r.ok); + } + void unmatchedBracket() { + auto r = AddressParser::evaluate("[0x1000"); + QVERIFY(!r.ok); + QVERIFY(r.error.contains("']'")); + } + void unmatchedAngle() { + auto r = AddressParser::evaluate("'")); + } + void divisionByZero() { + auto r = AddressParser::evaluate("0x100 / 0"); + QVERIFY(!r.ok); + QVERIFY(r.error.contains("division by zero")); + } + void trailingGarbage() { + auto r = AddressParser::evaluate("0x100 xyz"); + QVERIFY(!r.ok); + QVERIFY(r.error.contains("unexpected")); + } + void trailingOperator() { + auto r = AddressParser::evaluate("0x100 +"); + QVERIFY(!r.ok); + } + + // -- Validation -- + + void validateValid() { + QCOMPARE(AddressParser::validate("0x100 + 0x200"), QString()); + QCOMPARE(AddressParser::validate(" + [0x100]"), QString()); + } + void validateInvalid() { + QVERIFY(!AddressParser::validate("").isEmpty()); + QVERIFY(!AddressParser::validate("[0x100").isEmpty()); + QVERIFY(!AddressParser::validate("0x100 xyz").isEmpty()); + } + + // -- Backtick stripping -- + + void backtickStripping() { + auto r = AddressParser::evaluate("7ff6`6cce0000"); + QVERIFY(r.ok); + QCOMPARE(r.value, 0x7FF66CCE0000ULL); + } + + // -- Whitespace tolerance -- + + void whitespace() { + auto r = AddressParser::evaluate(" 0x100 + 0x200 "); + QVERIFY(r.ok); + QCOMPARE(r.value, 0x300ULL); + } + + // -- Legacy compat: simple hex -- + + void simpleHexAddress() { + auto r = AddressParser::evaluate("140000000"); + QVERIFY(r.ok); + QCOMPARE(r.value, 0x140000000ULL); + } + + // -- Multiple additions -- + + void multipleAdditions() { + auto r = AddressParser::evaluate("0x100 + 0x200 + 0x300"); + QVERIFY(r.ok); + QCOMPARE(r.value, 0x600ULL); + } +}; + +QTEST_GUILESS_MAIN(TestAddressParser) +#include "test_addressparser.moc"