mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
feat: type selector overhaul, fuzzy search, address parser, value tracking
Redesign type selector popup with fuzzy subsequence matching, per-category icons, field summary tooltips, compact chips, and pointer target primitives. Add address expression parser with arithmetic and register support. Enable track value changes by default.
This commit is contained in:
@@ -10,20 +10,28 @@ namespace rcx {
|
||||
// "<Program.exe> + 0xDE" → module base + offset
|
||||
// "[<Program.exe> + 0xDE] - AB" → dereference pointer, then subtract
|
||||
// "7ff6`6cce0000" → WinDbg-style backtick separator (stripped before parsing)
|
||||
// "base + e_lfanew" → C/C++ style identifier resolution
|
||||
// "0xFF & 0x0F" → bitwise AND
|
||||
// "1 << 4" → shift left
|
||||
//
|
||||
// Grammar (standard operator precedence: *, / bind tighter than +, -):
|
||||
// Grammar (C operator precedence):
|
||||
//
|
||||
// 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
|
||||
// bitwiseOr = bitwiseXor ('|' bitwiseXor)*
|
||||
// bitwiseXor = bitwiseAnd ('^' bitwiseAnd)*
|
||||
// bitwiseAnd = shift ('&' shift)*
|
||||
// shift = expr (('<<' | '>>') expr)*
|
||||
// expr = term (('+' | '-') term)*
|
||||
// term = unary (('*' | '/') unary)*
|
||||
// unary = '-' unary | '~' unary | atom
|
||||
// atom = '[' bitwiseOr ']' -- read pointer at address (dereference)
|
||||
// | '<' moduleName '>' -- resolve module base address
|
||||
// | '(' bitwiseOr ')' -- grouping
|
||||
// | identifier -- C/C++ name resolved via callback
|
||||
// | 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).
|
||||
// Identifiers: [a-zA-Z_][a-zA-Z0-9_]* containing at least one non-hex char.
|
||||
// Pure hex-digit words (e.g. "DEAD") are treated as hex literals.
|
||||
|
||||
class ExpressionParser {
|
||||
public:
|
||||
@@ -36,7 +44,7 @@ public:
|
||||
return error("empty expression");
|
||||
|
||||
uint64_t value = 0;
|
||||
if (!parseExpression(value))
|
||||
if (!parseBitwiseOr(value))
|
||||
return error(m_error);
|
||||
|
||||
skipSpaces();
|
||||
@@ -90,8 +98,89 @@ private:
|
||||
|| (ch >= 'A' && ch <= 'F');
|
||||
}
|
||||
|
||||
static bool isIdentStart(QChar ch) {
|
||||
return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_';
|
||||
}
|
||||
|
||||
static bool isIdentChar(QChar ch) {
|
||||
return isIdentStart(ch) || (ch >= '0' && ch <= '9');
|
||||
}
|
||||
|
||||
// ── Recursive descent parsing ──
|
||||
|
||||
// bitwiseOr = bitwiseXor ('|' bitwiseXor)*
|
||||
bool parseBitwiseOr(uint64_t& result) {
|
||||
if (!parseBitwiseXor(result))
|
||||
return false;
|
||||
for (;;) {
|
||||
skipSpaces();
|
||||
if (peek() != '|')
|
||||
break;
|
||||
advance();
|
||||
uint64_t rhs = 0;
|
||||
if (!parseBitwiseXor(rhs))
|
||||
return false;
|
||||
result |= rhs;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// bitwiseXor = bitwiseAnd ('^' bitwiseAnd)*
|
||||
bool parseBitwiseXor(uint64_t& result) {
|
||||
if (!parseBitwiseAnd(result))
|
||||
return false;
|
||||
for (;;) {
|
||||
skipSpaces();
|
||||
if (peek() != '^')
|
||||
break;
|
||||
advance();
|
||||
uint64_t rhs = 0;
|
||||
if (!parseBitwiseAnd(rhs))
|
||||
return false;
|
||||
result ^= rhs;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// bitwiseAnd = shift ('&' shift)*
|
||||
bool parseBitwiseAnd(uint64_t& result) {
|
||||
if (!parseShift(result))
|
||||
return false;
|
||||
for (;;) {
|
||||
skipSpaces();
|
||||
if (peek() != '&')
|
||||
break;
|
||||
advance();
|
||||
uint64_t rhs = 0;
|
||||
if (!parseShift(rhs))
|
||||
return false;
|
||||
result &= rhs;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// shift = expr (('<<' | '>>') expr)*
|
||||
bool parseShift(uint64_t& result) {
|
||||
if (!parseExpression(result))
|
||||
return false;
|
||||
for (;;) {
|
||||
skipSpaces();
|
||||
QChar c = peek();
|
||||
if (c != '<' && c != '>')
|
||||
break;
|
||||
// Must be << or >> (not < or > alone)
|
||||
if (m_pos + 1 >= m_input.size() || m_input[m_pos + 1] != c)
|
||||
break;
|
||||
bool isLeft = (c == '<');
|
||||
advance(); advance(); // skip << or >>
|
||||
uint64_t rhs = 0;
|
||||
if (!parseExpression(rhs))
|
||||
return false;
|
||||
result = isLeft ? (result << rhs) : (result >> rhs);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// expr = term (('+' | '-') term)*
|
||||
bool parseExpression(uint64_t& result) {
|
||||
if (!parseTerm(result))
|
||||
@@ -140,7 +229,7 @@ private:
|
||||
return true;
|
||||
}
|
||||
|
||||
// unary = '-' unary | atom
|
||||
// unary = '-' unary | '~' unary | atom
|
||||
bool parseUnary(uint64_t& result) {
|
||||
skipSpaces();
|
||||
if (peek() == '-') {
|
||||
@@ -151,10 +240,18 @@ private:
|
||||
result = static_cast<uint64_t>(-static_cast<int64_t>(inner));
|
||||
return true;
|
||||
}
|
||||
if (peek() == '~') {
|
||||
advance();
|
||||
uint64_t inner = 0;
|
||||
if (!parseUnary(inner))
|
||||
return false;
|
||||
result = ~inner;
|
||||
return true;
|
||||
}
|
||||
return parseAtom(result);
|
||||
}
|
||||
|
||||
// atom = '[' expr ']' | '<' name '>' | '(' expr ')' | hexLiteral
|
||||
// atom = '[' bitwiseOr ']' | '<' name '>' | '(' bitwiseOr ')' | identifier | hexLiteral
|
||||
bool parseAtom(uint64_t& result) {
|
||||
skipSpaces();
|
||||
if (atEnd())
|
||||
@@ -165,15 +262,55 @@ private:
|
||||
if (ch == '[') return parseDereference(result);
|
||||
if (ch == '<') return parseModuleName(result);
|
||||
if (ch == '(') return parseGrouping(result);
|
||||
|
||||
// Try identifier before hex — identifiers start with [a-zA-Z_]
|
||||
if (isIdentStart(ch))
|
||||
return parseIdentifierOrHex(result);
|
||||
|
||||
return parseHexNumber(result);
|
||||
}
|
||||
|
||||
// '[' expr ']' — read the pointer value at the computed address
|
||||
// Identifier or hex literal disambiguation.
|
||||
// Scan [a-zA-Z_][a-zA-Z0-9_]*. If it contains any non-hex char → identifier.
|
||||
// Otherwise → backtrack and parse as hex number.
|
||||
bool parseIdentifierOrHex(uint64_t& result) {
|
||||
int start = m_pos;
|
||||
bool hasNonHex = false;
|
||||
|
||||
// Scan full token
|
||||
while (!atEnd() && isIdentChar(peek())) {
|
||||
if (!isHexDigit(peek()))
|
||||
hasNonHex = true;
|
||||
advance();
|
||||
}
|
||||
|
||||
QString token = m_input.mid(start, m_pos - start);
|
||||
|
||||
if (!hasNonHex) {
|
||||
// Pure hex digits (e.g. "DEAD") — backtrack, parse as hex
|
||||
m_pos = start;
|
||||
return parseHexNumber(result);
|
||||
}
|
||||
|
||||
// It's an identifier — resolve via callback
|
||||
if (!m_callbacks || !m_callbacks->resolveIdentifier) {
|
||||
result = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
result = m_callbacks->resolveIdentifier(token, &ok);
|
||||
if (!ok)
|
||||
return fail(QStringLiteral("unknown identifier '%1'").arg(token));
|
||||
return true;
|
||||
}
|
||||
|
||||
// '[' bitwiseOr ']' — read the pointer value at the computed address
|
||||
bool parseDereference(uint64_t& result) {
|
||||
advance(); // skip '['
|
||||
|
||||
uint64_t address = 0;
|
||||
if (!parseExpression(address))
|
||||
if (!parseBitwiseOr(address))
|
||||
return false;
|
||||
if (!expect(']'))
|
||||
return false;
|
||||
@@ -220,10 +357,10 @@ private:
|
||||
return true;
|
||||
}
|
||||
|
||||
// '(' expr ')' — parenthesized sub-expression for grouping
|
||||
// '(' bitwiseOr ')' — parenthesized sub-expression for grouping
|
||||
bool parseGrouping(uint64_t& result) {
|
||||
advance(); // skip '('
|
||||
if (!parseExpression(result))
|
||||
if (!parseBitwiseOr(result))
|
||||
return false;
|
||||
return expect(')');
|
||||
}
|
||||
@@ -290,7 +427,7 @@ QString AddressParser::validate(const QString& formula)
|
||||
if (cleaned.isEmpty())
|
||||
return QStringLiteral("empty");
|
||||
|
||||
// Parse with no callbacks — modules and dereferences succeed but return 0.
|
||||
// Parse with no callbacks — modules, dereferences, identifiers succeed but return 0.
|
||||
// This checks syntax only.
|
||||
ExpressionParser parser(cleaned, nullptr);
|
||||
auto result = parser.parse();
|
||||
|
||||
Reference in New Issue
Block a user