mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Add AddressParser + tests, remove symbol from commandrow
This commit is contained in:
@@ -73,6 +73,8 @@ add_executable(Reclass
|
|||||||
src/titlebar.cpp
|
src/titlebar.cpp
|
||||||
src/mcp/mcp_bridge.h
|
src/mcp/mcp_bridge.h
|
||||||
src/mcp/mcp_bridge.cpp
|
src/mcp/mcp_bridge.cpp
|
||||||
|
src/addressparser.h
|
||||||
|
src/addressparser.cpp
|
||||||
src/disasm.h
|
src/disasm.h
|
||||||
src/disasm.cpp
|
src/disasm.cpp
|
||||||
third_party/fadec/decode.c
|
third_party/fadec/decode.c
|
||||||
@@ -154,17 +156,17 @@ if(BUILD_TESTING)
|
|||||||
|
|
||||||
# ── Headless tests (Qt::Core only — safe for CI without a display) ──
|
# ── 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_include_directories(test_core PRIVATE src)
|
||||||
target_link_libraries(test_core PRIVATE ${QT}::Core ${QT}::Test)
|
target_link_libraries(test_core PRIVATE ${QT}::Core ${QT}::Test)
|
||||||
add_test(NAME test_core COMMAND test_core)
|
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_include_directories(test_format PRIVATE src)
|
||||||
target_link_libraries(test_format PRIVATE ${QT}::Core ${QT}::Test)
|
target_link_libraries(test_format PRIVATE ${QT}::Core ${QT}::Test)
|
||||||
add_test(NAME test_format COMMAND test_format)
|
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_include_directories(test_compose PRIVATE src)
|
||||||
target_link_libraries(test_compose PRIVATE ${QT}::Core ${QT}::Test)
|
target_link_libraries(test_compose PRIVATE ${QT}::Core ${QT}::Test)
|
||||||
add_test(NAME test_compose COMMAND test_compose)
|
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_test(NAME test_command_row COMMAND test_command_row)
|
||||||
|
|
||||||
add_executable(test_generator tests/test_generator.cpp
|
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_include_directories(test_generator PRIVATE src)
|
||||||
target_link_libraries(test_generator PRIVATE ${QT}::Core ${QT}::Test)
|
target_link_libraries(test_generator PRIVATE ${QT}::Core ${QT}::Test)
|
||||||
add_test(NAME test_generator COMMAND test_generator)
|
add_test(NAME test_generator COMMAND test_generator)
|
||||||
|
|
||||||
add_executable(test_import_xml tests/test_import_xml.cpp
|
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_include_directories(test_import_xml PRIVATE src)
|
||||||
target_link_libraries(test_import_xml PRIVATE ${QT}::Core ${QT}::Test)
|
target_link_libraries(test_import_xml PRIVATE ${QT}::Core ${QT}::Test)
|
||||||
add_test(NAME test_import_xml COMMAND test_import_xml)
|
add_test(NAME test_import_xml COMMAND test_import_xml)
|
||||||
|
|
||||||
add_executable(test_import_source tests/test_import_source.cpp
|
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_include_directories(test_import_source PRIVATE src)
|
||||||
target_link_libraries(test_import_source PRIVATE ${QT}::Core ${QT}::Test)
|
target_link_libraries(test_import_source PRIVATE ${QT}::Core ${QT}::Test)
|
||||||
add_test(NAME test_import_source COMMAND test_import_source)
|
add_test(NAME test_import_source COMMAND test_import_source)
|
||||||
|
|
||||||
add_executable(test_export_xml tests/test_export_xml.cpp
|
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_include_directories(test_export_xml PRIVATE src)
|
||||||
target_link_libraries(test_export_xml PRIVATE ${QT}::Core ${QT}::Test)
|
target_link_libraries(test_export_xml PRIVATE ${QT}::Core ${QT}::Test)
|
||||||
add_test(NAME test_export_xml COMMAND test_export_xml)
|
add_test(NAME test_export_xml COMMAND test_export_xml)
|
||||||
|
|
||||||
add_executable(test_disasm tests/test_disasm.cpp
|
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)
|
third_party/fadec/decode.c third_party/fadec/format.c)
|
||||||
target_include_directories(test_disasm PRIVATE src third_party/fadec)
|
target_include_directories(test_disasm PRIVATE src third_party/fadec)
|
||||||
target_link_libraries(test_disasm PRIVATE ${QT}::Core ${QT}::Test)
|
target_link_libraries(test_disasm PRIVATE ${QT}::Core ${QT}::Test)
|
||||||
add_test(NAME test_disasm COMMAND test_disasm)
|
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) ──
|
# ── UI tests (require Qt::Widgets / QScintilla / display — skip on headless CI) ──
|
||||||
option(BUILD_UI_TESTS "Build tests that require a display (Qt Widgets)" ON)
|
option(BUILD_UI_TESTS "Build tests that require a display (Qt Widgets)" ON)
|
||||||
if(BUILD_UI_TESTS)
|
if(BUILD_UI_TESTS)
|
||||||
|
|
||||||
add_executable(test_controller tests/test_controller.cpp
|
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/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||||
src/typeselectorpopup.cpp
|
src/typeselectorpopup.cpp
|
||||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
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_test(NAME test_controller COMMAND test_controller)
|
||||||
|
|
||||||
add_executable(test_validation tests/test_validation.cpp
|
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/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||||
src/typeselectorpopup.cpp
|
src/typeselectorpopup.cpp
|
||||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
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_test(NAME test_validation COMMAND test_validation)
|
||||||
|
|
||||||
add_executable(test_context_menu tests/test_context_menu.cpp
|
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/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||||
src/typeselectorpopup.cpp
|
src/typeselectorpopup.cpp
|
||||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
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_test(NAME test_context_menu COMMAND test_context_menu)
|
||||||
|
|
||||||
add_executable(test_source_management tests/test_source_management.cpp
|
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/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||||
src/typeselectorpopup.cpp
|
src/typeselectorpopup.cpp
|
||||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
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_test(NAME test_source_management COMMAND test_source_management)
|
||||||
|
|
||||||
add_executable(test_editor tests/test_editor.cpp
|
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/providerregistry.cpp
|
||||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
||||||
target_include_directories(test_editor PRIVATE src third_party/fadec)
|
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_test(NAME test_editor COMMAND test_editor)
|
||||||
|
|
||||||
add_executable(test_rendered_view tests/test_rendered_view.cpp
|
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_include_directories(test_rendered_view PRIVATE src)
|
||||||
target_link_libraries(test_rendered_view PRIVATE
|
target_link_libraries(test_rendered_view PRIVATE
|
||||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Test
|
${QT}::Widgets ${QT}::PrintSupport ${QT}::Test
|
||||||
@@ -289,7 +296,7 @@ if(BUILD_TESTING)
|
|||||||
add_test(NAME test_rendered_view COMMAND test_rendered_view)
|
add_test(NAME test_rendered_view COMMAND test_rendered_view)
|
||||||
|
|
||||||
add_executable(test_new_features tests/test_new_features.cpp
|
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/editor.cpp src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||||
src/typeselectorpopup.cpp
|
src/typeselectorpopup.cpp
|
||||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
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_test(NAME test_new_features COMMAND test_new_features)
|
||||||
|
|
||||||
add_executable(test_type_selector tests/test_type_selector.cpp
|
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/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||||
src/typeselectorpopup.cpp
|
src/typeselectorpopup.cpp
|
||||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
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_test(NAME test_type_selector COMMAND test_type_selector)
|
||||||
|
|
||||||
add_executable(test_type_visibility tests/test_type_visibility.cpp
|
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/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||||
src/typeselectorpopup.cpp
|
src/typeselectorpopup.cpp
|
||||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
||||||
|
|||||||
@@ -284,6 +284,15 @@ void ProcessMemoryProvider::cacheModules()
|
|||||||
|
|
||||||
#endif // platform
|
#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()
|
ProcessMemoryProvider::~ProcessMemoryProvider()
|
||||||
{
|
{
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ public:
|
|||||||
QString name() const override { return m_processName; }
|
QString name() const override { return m_processName; }
|
||||||
QString kind() const override { return QStringLiteral("LocalProcess"); }
|
QString kind() const override { return QStringLiteral("LocalProcess"); }
|
||||||
QString getSymbol(uint64_t addr) const override;
|
QString getSymbol(uint64_t addr) const override;
|
||||||
|
uint64_t symbolToAddress(const QString& name) const override;
|
||||||
|
|
||||||
bool isLive() const override { return true; }
|
bool isLive() const override { return true; }
|
||||||
uint64_t base() const override { return m_base; }
|
uint64_t base() const override { return m_base; }
|
||||||
|
|||||||
@@ -74,6 +74,15 @@ QString RcNetCompatProvider::getSymbol(uint64_t addr) const
|
|||||||
return {};
|
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 ---------------------------------------------------
|
// -- Module enumeration ---------------------------------------------------
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ public:
|
|||||||
bool isLive() const override { return true; }
|
bool isLive() const override { return true; }
|
||||||
uint64_t base() const override { return m_base; }
|
uint64_t base() const override { return m_base; }
|
||||||
QString getSymbol(uint64_t addr) const override;
|
QString getSymbol(uint64_t addr) const override;
|
||||||
|
uint64_t symbolToAddress(const QString& name) const override;
|
||||||
|
|
||||||
struct ModuleInfo {
|
struct ModuleInfo {
|
||||||
QString name;
|
QString name;
|
||||||
|
|||||||
300
src/addressparser.cpp
Normal file
300
src/addressparser.cpp
Normal file
@@ -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
|
||||||
|
// "<Program.exe> + 0xDE" → module base + offset
|
||||||
|
// "[<Program.exe> + 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<uint64_t>(-static_cast<int64_t>(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. <Program.exe>)
|
||||||
|
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
|
||||||
27
src/addressparser.h
Normal file
27
src/addressparser.h
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <QString>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
namespace rcx {
|
||||||
|
|
||||||
|
struct AddressParseResult {
|
||||||
|
bool ok;
|
||||||
|
uint64_t value;
|
||||||
|
QString error;
|
||||||
|
int errorPos;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AddressParserCallbacks {
|
||||||
|
std::function<uint64_t(const QString& name, bool* ok)> resolveModule;
|
||||||
|
std::function<uint64_t(uint64_t addr, bool* ok)> 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
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "controller.h"
|
#include "controller.h"
|
||||||
|
#include "addressparser.h"
|
||||||
#include "typeselectorpopup.h"
|
#include "typeselectorpopup.h"
|
||||||
#include "providerregistry.h"
|
#include "providerregistry.h"
|
||||||
#include "themes/thememanager.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('`'); // WinDbg backtick separators (e.g. 7ff6`6cce0000)
|
||||||
s.remove('\n');
|
s.remove('\n');
|
||||||
s.remove('\r');
|
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) {
|
AddressParserCallbacks cbs;
|
||||||
// Skip whitespace
|
if (m_doc->provider) {
|
||||||
while (pos < s.size() && s[pos].isSpace()) pos++;
|
auto* prov = m_doc->provider.get();
|
||||||
if (pos >= s.size()) break;
|
cbs.resolveModule = [prov](const QString& name, bool* ok) -> uint64_t {
|
||||||
|
uint64_t base = prov->symbolToAddress(name);
|
||||||
// Check for +/- operator (except first term)
|
*ok = (base != 0);
|
||||||
if (!firstTerm) {
|
return base;
|
||||||
if (s[pos] == '+') { adding = true; pos++; }
|
};
|
||||||
else if (s[pos] == '-') { adding = false; pos++; }
|
cbs.readPointer = [prov](uint64_t addr, bool* ok) -> uint64_t {
|
||||||
else { ok = false; break; }
|
uint64_t val = 0;
|
||||||
while (pos < s.size() && s[pos].isSpace()) pos++;
|
*ok = prov->read(addr, &val, 8);
|
||||||
|
return val;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
auto result = AddressParser::evaluate(s, 8, &cbs);
|
||||||
// Parse hex number (with or without 0x prefix)
|
if (result.ok && result.value != m_doc->tree.baseAddress) {
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ok && newBase != m_doc->tree.baseAddress) {
|
|
||||||
uint64_t oldBase = 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,
|
m_doc->undoStack.push(new RcxCommand(this,
|
||||||
cmd::ChangeBase{oldBase, newBase}));
|
cmd::ChangeBase{oldBase, result.value, oldFormula, newFormula}));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -982,6 +959,7 @@ void RcxController::applyCommand(const Command& command, bool isUndo) {
|
|||||||
clearHistoryForAdjs(c.offAdjs);
|
clearHistoryForAdjs(c.offAdjs);
|
||||||
} else if constexpr (std::is_same_v<T, cmd::ChangeBase>) {
|
} else if constexpr (std::is_same_v<T, cmd::ChangeBase>) {
|
||||||
tree.baseAddress = isUndo ? c.oldBase : c.newBase;
|
tree.baseAddress = isUndo ? c.oldBase : c.newBase;
|
||||||
|
tree.baseAddressFormula = isUndo ? c.oldFormula : c.newFormula;
|
||||||
resetSnapshot();
|
resetSnapshot();
|
||||||
} else if constexpr (std::is_same_v<T, cmd::WriteBytes>) {
|
} else if constexpr (std::is_same_v<T, cmd::WriteBytes>) {
|
||||||
const QByteArray& bytes = isUndo ? c.oldBytes : c.newBytes;
|
const QByteArray& bytes = isUndo ? c.oldBytes : c.newBytes;
|
||||||
@@ -1779,30 +1757,15 @@ void RcxController::updateCommandRow() {
|
|||||||
.arg(provName);
|
.arg(provName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Symbol for selected node (getSymbol integration) --
|
QString addr;
|
||||||
QString sym;
|
if (!m_doc->tree.baseAddressFormula.isEmpty())
|
||||||
if (m_selIds.size() == 1) {
|
addr = m_doc->tree.baseAddressFormula;
|
||||||
uint64_t sid = *m_selIds.begin();
|
else
|
||||||
int idx = m_doc->tree.indexOfId(sid & ~kFooterIdBit);
|
addr = QStringLiteral("0x") +
|
||||||
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 = QStringLiteral("0x") +
|
|
||||||
QString::number(m_doc->tree.baseAddress, 16).toUpper();
|
QString::number(m_doc->tree.baseAddress, 16).toUpper();
|
||||||
|
|
||||||
// Build the row. If we have a symbol, append it after the address.
|
QString row = QStringLiteral("%1 \u00B7 %2")
|
||||||
QString row;
|
|
||||||
if (sym.isEmpty()) {
|
|
||||||
row = QStringLiteral("%1 \u00B7 %2")
|
|
||||||
.arg(elide(src, 40), elide(addr, 24));
|
.arg(elide(src, 40), elide(addr, 24));
|
||||||
} else {
|
|
||||||
row = QStringLiteral("%1 \u00B7 %2 %3")
|
|
||||||
.arg(elide(src, 40), elide(addr, 24), elide(sym, 40));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build row 2: root class type + name (uses current view root)
|
// Build row 2: root class type + name (uses current view root)
|
||||||
QString row2;
|
QString row2;
|
||||||
@@ -2341,6 +2304,26 @@ void RcxController::attachViaPlugin(const QString& providerIdentifier, const QSt
|
|||||||
m_doc->dataPath.clear();
|
m_doc->dataPath.clear();
|
||||||
if (m_doc->tree.baseAddress == 0)
|
if (m_doc->tree.baseAddress == 0)
|
||||||
m_doc->tree.baseAddress = newBase;
|
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();
|
resetSnapshot();
|
||||||
emit m_doc->documentChanged();
|
emit m_doc->documentChanged();
|
||||||
refresh();
|
refresh();
|
||||||
@@ -2351,8 +2334,10 @@ void RcxController::switchToSavedSource(int idx) {
|
|||||||
if (idx == m_activeSourceIdx) return;
|
if (idx == m_activeSourceIdx) return;
|
||||||
|
|
||||||
// Save current source's base address before switching
|
// 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].baseAddress = m_doc->tree.baseAddress;
|
||||||
|
m_savedSources[m_activeSourceIdx].baseAddressFormula = m_doc->tree.baseAddressFormula;
|
||||||
|
}
|
||||||
|
|
||||||
m_activeSourceIdx = idx;
|
m_activeSourceIdx = idx;
|
||||||
const auto& entry = m_savedSources[idx];
|
const auto& entry = m_savedSources[idx];
|
||||||
@@ -2360,9 +2345,12 @@ void RcxController::switchToSavedSource(int idx) {
|
|||||||
if (entry.kind == QStringLiteral("File")) {
|
if (entry.kind == QStringLiteral("File")) {
|
||||||
m_doc->loadData(entry.filePath);
|
m_doc->loadData(entry.filePath);
|
||||||
m_doc->tree.baseAddress = entry.baseAddress;
|
m_doc->tree.baseAddress = entry.baseAddress;
|
||||||
|
m_doc->tree.baseAddressFormula = entry.baseAddressFormula;
|
||||||
refresh();
|
refresh();
|
||||||
} else if (!entry.providerTarget.isEmpty()) {
|
} else if (!entry.providerTarget.isEmpty()) {
|
||||||
// Plugin-based provider (e.g. "processmemory" with target "pid:name")
|
// 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);
|
attachViaPlugin(entry.kind, entry.providerTarget);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ struct SavedSourceEntry {
|
|||||||
QString filePath; // for File sources
|
QString filePath; // for File sources
|
||||||
QString providerTarget; // for plugin providers (e.g. "pid:name")
|
QString providerTarget; // for plugin providers (e.g. "pid:name")
|
||||||
uint64_t baseAddress = 0;
|
uint64_t baseAddress = 0;
|
||||||
|
QString baseAddressFormula;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Controller ──
|
// ── Controller ──
|
||||||
|
|||||||
13
src/core.h
13
src/core.h
@@ -267,6 +267,7 @@ struct Node {
|
|||||||
struct NodeTree {
|
struct NodeTree {
|
||||||
QVector<Node> nodes;
|
QVector<Node> nodes;
|
||||||
uint64_t baseAddress = 0x00400000;
|
uint64_t baseAddress = 0x00400000;
|
||||||
|
QString baseAddressFormula; // e.g. "<ReClass.exe> + 0x100"
|
||||||
uint64_t m_nextId = 1;
|
uint64_t m_nextId = 1;
|
||||||
mutable QHash<uint64_t, int> m_idCache;
|
mutable QHash<uint64_t, int> m_idCache;
|
||||||
|
|
||||||
@@ -400,6 +401,8 @@ struct NodeTree {
|
|||||||
QJsonObject toJson() const {
|
QJsonObject toJson() const {
|
||||||
QJsonObject o;
|
QJsonObject o;
|
||||||
o["baseAddress"] = QString::number(baseAddress, 16);
|
o["baseAddress"] = QString::number(baseAddress, 16);
|
||||||
|
if (!baseAddressFormula.isEmpty())
|
||||||
|
o["baseAddressFormula"] = baseAddressFormula;
|
||||||
o["nextId"] = QString::number(m_nextId);
|
o["nextId"] = QString::number(m_nextId);
|
||||||
QJsonArray arr;
|
QJsonArray arr;
|
||||||
for (const auto& n : nodes) arr.append(n.toJson());
|
for (const auto& n : nodes) arr.append(n.toJson());
|
||||||
@@ -410,6 +413,7 @@ struct NodeTree {
|
|||||||
static NodeTree fromJson(const QJsonObject& o) {
|
static NodeTree fromJson(const QJsonObject& o) {
|
||||||
NodeTree t;
|
NodeTree t;
|
||||||
t.baseAddress = o["baseAddress"].toString("400000").toULongLong(nullptr, 16);
|
t.baseAddress = o["baseAddress"].toString("400000").toULongLong(nullptr, 16);
|
||||||
|
t.baseAddressFormula = o["baseAddressFormula"].toString();
|
||||||
t.m_nextId = o["nextId"].toString("1").toULongLong();
|
t.m_nextId = o["nextId"].toString("1").toULongLong();
|
||||||
QJsonArray arr = o["nodes"].toArray();
|
QJsonArray arr = o["nodes"].toArray();
|
||||||
for (const auto& v : arr) {
|
for (const auto& v : arr) {
|
||||||
@@ -541,7 +545,7 @@ namespace cmd {
|
|||||||
struct Insert { Node node; QVector<OffsetAdj> offAdjs; };
|
struct Insert { Node node; QVector<OffsetAdj> offAdjs; };
|
||||||
struct Remove { uint64_t nodeId; QVector<Node> subtree;
|
struct Remove { uint64_t nodeId; QVector<Node> subtree;
|
||||||
QVector<OffsetAdj> offAdjs; };
|
QVector<OffsetAdj> 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 WriteBytes { uint64_t addr; QByteArray oldBytes, newBytes; };
|
||||||
struct ChangeArrayMeta { uint64_t nodeId;
|
struct ChangeArrayMeta { uint64_t nodeId;
|
||||||
NodeKind oldElementKind, newElementKind;
|
NodeKind oldElementKind, newElementKind;
|
||||||
@@ -663,8 +667,11 @@ inline ColumnSpan commandRowAddrSpan(const QString& lineText) {
|
|||||||
int tag = lineText.indexOf(QStringLiteral(" \u00B7"));
|
int tag = lineText.indexOf(QStringLiteral(" \u00B7"));
|
||||||
if (tag < 0) return {};
|
if (tag < 0) return {};
|
||||||
int start = tag + 3; // after " · "
|
int start = tag + 3; // after " · "
|
||||||
int end = start;
|
// Scan to next " · " separator (or end of line) to support formulas with spaces
|
||||||
while (end < lineText.size() && !lineText[end].isSpace()) end++;
|
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 {};
|
if (end <= start) return {};
|
||||||
return {start, end, true};
|
return {start, end, true};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "core.h"
|
#include "core.h"
|
||||||
|
#include "addressparser.h"
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
@@ -664,43 +665,13 @@ QString validateValue(NodeKind kind, const QString& text) {
|
|||||||
return QStringLiteral("invalid");
|
return QStringLiteral("invalid");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Base address validation (supports simple +/- equations) ──
|
// ── Base address validation (delegates to AddressParser) ──
|
||||||
|
|
||||||
QString validateBaseAddress(const QString& text) {
|
QString validateBaseAddress(const QString& text) {
|
||||||
QString s = text.trimmed();
|
QString s = text.trimmed();
|
||||||
if (s.isEmpty()) return QStringLiteral("empty");
|
if (s.isEmpty()) return QStringLiteral("empty");
|
||||||
|
//s.remove('`');
|
||||||
int pos = 0;
|
return AddressParser::validate(s);
|
||||||
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 {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace rcx::fmt
|
} // namespace rcx::fmt
|
||||||
|
|||||||
@@ -47,6 +47,13 @@ public:
|
|||||||
return {};
|
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) ---
|
// --- Derived convenience (non-virtual, never override) ---
|
||||||
|
|
||||||
bool isValid() const { return size() > 0; }
|
bool isValid() const { return size() > 0; }
|
||||||
|
|||||||
@@ -67,6 +67,9 @@ public:
|
|||||||
QString getSymbol(uint64_t addr) const override {
|
QString getSymbol(uint64_t addr) const override {
|
||||||
return m_real ? m_real->getSymbol(addr) : QString();
|
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 {
|
bool write(uint64_t addr, const void* buf, int len) override {
|
||||||
if (!m_real) return false;
|
if (!m_real) return false;
|
||||||
|
|||||||
219
tests/test_addressparser.cpp
Normal file
219
tests/test_addressparser.cpp
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
#include "addressparser.h"
|
||||||
|
#include <QTest>
|
||||||
|
|
||||||
|
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("<Program.exe> + 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("<NoSuch.dll>", 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;
|
||||||
|
};
|
||||||
|
// [<mod> + [<mod> + 0x100]] = [0x400000 + [0x400000+0x100]]
|
||||||
|
// inner deref: [0x400100] = 0x500000
|
||||||
|
// outer: [0x400000 + 0x500000] = [0x900000] = 0xABCDEF
|
||||||
|
auto r = AddressParser::evaluate("[<mod> + [<mod> + 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;
|
||||||
|
};
|
||||||
|
// [<Program.exe> + 0xDE] - AB = [0x1400000DE] - 0xAB = 0x500000 - 0xAB = 0x4FFF55
|
||||||
|
auto r = AddressParser::evaluate("[<Program.exe> + 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("<Program.exe");
|
||||||
|
QVERIFY(!r.ok);
|
||||||
|
QVERIFY(r.error.contains("'>'"));
|
||||||
|
}
|
||||||
|
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("<Prog.exe> + [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"
|
||||||
Reference in New Issue
Block a user