mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
- Enum inline editing: name/value commit handling, auto-sort by value - Bitfield support in PDB import with proper container nodes - Per-member hover/selection highlighting (kMemberBit encoding) - Context menu fixes for enum/bitfield member lines - MCP pagination (limit/offset), includeMembers param, tree.search tool - MCP status bar activity indicator for tool calls - PDB anonymous type inlining: inline <unnamed-tag> types as children - Skip anonymous pointer targets to prevent root orphans - Enum import diagnostics for debugging missing enums
730 lines
28 KiB
C++
730 lines
28 KiB
C++
#include "core.h"
|
|
#include "addressparser.h"
|
|
#include <cmath>
|
|
#include <cstring>
|
|
#include <limits>
|
|
|
|
namespace rcx::fmt {
|
|
|
|
// ── Column layout ──
|
|
// COL_TYPE and COL_NAME use shared constants from core.h (kColType, kColName)
|
|
static constexpr int COL_TYPE = kColType;
|
|
static constexpr int COL_NAME = kColName;
|
|
static constexpr int COL_VALUE = kColValue;
|
|
static constexpr int COL_COMMENT = 28; // "// Enter=Save Esc=Cancel" fits
|
|
static const QString SEP = QStringLiteral(" ");
|
|
|
|
static QString fit(QString s, int w) {
|
|
if (w <= 0) return {};
|
|
if (s.size() > w) {
|
|
if (w >= 2) s = s.left(w - 1) + QChar(0x2026); // ellipsis
|
|
else s = s.left(w);
|
|
}
|
|
return s.leftJustified(w, ' ');
|
|
}
|
|
|
|
// Like fit() but overflows instead of truncating: if s exceeds w, return full string
|
|
static QString fitOverflow(const QString& s, int w) {
|
|
if (w <= 0) return {};
|
|
if (s.size() <= w)
|
|
return s.leftJustified(w, ' ');
|
|
return s;
|
|
}
|
|
|
|
// ── Type name ──
|
|
|
|
// Override seam: injectable type-name provider
|
|
static TypeNameFn g_typeNameFn = nullptr;
|
|
|
|
void setTypeNameProvider(TypeNameFn fn) { g_typeNameFn = fn; }
|
|
|
|
// Unpadded type name for width calculation
|
|
QString typeNameRaw(NodeKind kind) {
|
|
if (g_typeNameFn) return g_typeNameFn(kind);
|
|
auto* m = kindMeta(kind);
|
|
return m ? QString::fromLatin1(m->typeName) : QStringLiteral("???");
|
|
}
|
|
|
|
QString typeName(NodeKind kind, int colType) {
|
|
if (g_typeNameFn) return fit(g_typeNameFn(kind), colType);
|
|
auto* m = kindMeta(kind);
|
|
return fit(m ? QString::fromLatin1(m->typeName) : QStringLiteral("???"), colType);
|
|
}
|
|
|
|
// Array type string: "uint32_t[16]" or "Material[2]"
|
|
QString arrayTypeName(NodeKind elemKind, int count, const QString& structName) {
|
|
QString elem;
|
|
if (elemKind == NodeKind::Struct && !structName.isEmpty())
|
|
elem = structName;
|
|
else {
|
|
auto* m = kindMeta(elemKind);
|
|
elem = m ? QString::fromLatin1(m->typeName) : QStringLiteral("???");
|
|
}
|
|
return elem + QStringLiteral("[") + QString::number(count) + QStringLiteral("]");
|
|
}
|
|
|
|
// Pointer type string: "void*" or "StructName*"
|
|
QString pointerTypeName(NodeKind kind, const QString& targetName) {
|
|
Q_UNUSED(kind);
|
|
QString target = targetName.isEmpty() ? QStringLiteral("void") : targetName;
|
|
return target + QStringLiteral("*");
|
|
}
|
|
|
|
// ── Value formatting ──
|
|
|
|
static QString hexVal(uint64_t v) {
|
|
return QStringLiteral("0x") + QString::number(v, 16);
|
|
}
|
|
|
|
static QString rawHex(uint64_t v, int digits) {
|
|
return QString::number(v, 16).rightJustified(digits, '0');
|
|
}
|
|
|
|
QString fmtInt8(int8_t v) { return hexVal((uint8_t)v); }
|
|
QString fmtInt16(int16_t v) { return hexVal((uint16_t)v); }
|
|
QString fmtInt32(int32_t v) { return hexVal((uint32_t)v); }
|
|
QString fmtInt64(int64_t v) { return hexVal((uint64_t)v); }
|
|
QString fmtUInt8(uint8_t v) { return hexVal(v); }
|
|
QString fmtUInt16(uint16_t v) { return hexVal(v); }
|
|
QString fmtUInt32(uint32_t v) { return hexVal(v); }
|
|
QString fmtUInt64(uint64_t v) { return hexVal(v); }
|
|
|
|
QString fmtFloat(float v) {
|
|
if (std::isnan(v)) return QStringLiteral("NaN");
|
|
if (std::isinf(v)) return v > 0 ? QStringLiteral("inff") : QStringLiteral("-inff");
|
|
|
|
// 6 significant digits — covers full single-precision range
|
|
QString s = QString::number(v, 'g', 6);
|
|
|
|
// If 'g' chose scientific notation, reformat as plain decimal
|
|
if (s.contains('e') || s.contains('E')) {
|
|
s = QString::number(v, 'f', 8);
|
|
if (s.contains('.')) {
|
|
int i = s.size() - 1;
|
|
while (i > 0 && s[i] == '0') i--;
|
|
if (s[i] == '.') i++; // keep at least one decimal digit
|
|
s.truncate(i + 1);
|
|
}
|
|
}
|
|
|
|
if (!s.contains('.'))
|
|
s += QStringLiteral(".f");
|
|
else
|
|
s += QLatin1Char('f');
|
|
return s;
|
|
}
|
|
QString fmtDouble(double v) {
|
|
QString s = QString::number(v, 'g', 6);
|
|
if (!s.contains('.') && !s.contains('e') && !s.contains('E'))
|
|
s += QStringLiteral(".0");
|
|
return s;
|
|
}
|
|
QString fmtBool(uint8_t v) { return v ? QStringLiteral("true") : QStringLiteral("false"); }
|
|
|
|
QString fmtPointer32(uint32_t v) { return hexVal(v); }
|
|
QString fmtPointer64(uint64_t v) { return hexVal(v); }
|
|
|
|
// ── Indentation ──
|
|
|
|
QString indent(int depth) {
|
|
return QString(depth * 3, ' ');
|
|
}
|
|
|
|
// ── Offset margin ──
|
|
|
|
QString fmtOffsetMargin(uint64_t absoluteOffset, bool isContinuation, int hexDigits) {
|
|
if (isContinuation) return QStringLiteral(" \u00B7 ");
|
|
return QString::number(absoluteOffset, 16).toUpper()
|
|
.rightJustified(hexDigits, '0') + QChar(' ');
|
|
}
|
|
|
|
// ── Struct type name (for width calculation) ──
|
|
|
|
QString structTypeName(const Node& node) {
|
|
// Named types: just the type name (e.g. "_LIST_ENTRY")
|
|
// Anonymous: just the keyword (e.g. "union", "struct")
|
|
if (!node.structTypeName.isEmpty())
|
|
return node.structTypeName;
|
|
return node.resolvedClassKeyword();
|
|
}
|
|
|
|
// ── Struct header / footer ──
|
|
|
|
QString fmtStructHeader(const Node& node, int depth, bool collapsed, int colType, int colName, bool compact) {
|
|
// Columnar format: <type> <name> { (or no brace when collapsed)
|
|
QString ind = indent(depth);
|
|
QString rawType = structTypeName(node);
|
|
QString suffix = collapsed ? QString() : QStringLiteral("{");
|
|
if (node.name.isEmpty()) {
|
|
// Anonymous struct/union: "union {" with no column padding
|
|
return ind + rawType + SEP + suffix;
|
|
}
|
|
QString type = compact ? fitOverflow(rawType, colType) : fit(rawType, colType);
|
|
return ind + type + SEP + node.name + SEP + suffix;
|
|
}
|
|
|
|
QString fmtStructFooter(const Node& /*node*/, int depth, int /*totalSize*/) {
|
|
return indent(depth) + QStringLiteral("};");
|
|
}
|
|
|
|
// ── Array header ──
|
|
// Columnar format: <type[count]> <name> { (or no brace when collapsed)
|
|
QString fmtArrayHeader(const Node& node, int depth, int /*viewIdx*/, bool collapsed, int colType, int colName, const QString& elemStructName, bool compact) {
|
|
QString ind = indent(depth);
|
|
QString rawType = arrayTypeName(node.elementKind, node.arrayLen, elemStructName);
|
|
QString type = compact ? fitOverflow(rawType, colType) : fit(rawType, colType);
|
|
QString suffix = collapsed ? QString() : QStringLiteral("{");
|
|
return ind + type + SEP + node.name + SEP + suffix;
|
|
}
|
|
|
|
// ── Pointer header (merged pointer + struct header) ──
|
|
|
|
QString fmtPointerHeader(const Node& node, int depth, bool collapsed,
|
|
const Provider& prov, uint64_t addr,
|
|
const QString& ptrTypeName, int colType, int colName,
|
|
bool compact) {
|
|
QString ind = indent(depth);
|
|
bool overflow = compact && ptrTypeName.size() > colType;
|
|
QString type = compact ? fitOverflow(ptrTypeName, colType) : fit(ptrTypeName, colType);
|
|
if (collapsed) {
|
|
if (overflow) {
|
|
// Overflow: no column padding
|
|
return ind + type + SEP + node.name + SEP + readValue(node, prov, addr, 0);
|
|
}
|
|
// Collapsed: show pointer value instead of brace (name padded for value alignment)
|
|
QString name = fit(node.name, colName);
|
|
QString val = fit(readValue(node, prov, addr, 0), COL_VALUE);
|
|
return ind + type + SEP + name + SEP + val;
|
|
}
|
|
return ind + type + SEP + node.name + SEP + QStringLiteral("{");
|
|
}
|
|
|
|
// ── Hex / ASCII preview ──
|
|
|
|
static inline bool isAsciiPrintable(uint8_t c) { return c >= 0x20 && c <= 0x7E; }
|
|
|
|
// Escape control characters for display
|
|
static QString sanitizeString(const QString& s) {
|
|
QString out;
|
|
out.reserve(s.size() + 8);
|
|
for (QChar c : s) {
|
|
if (c == '\n') out += QStringLiteral("\\n");
|
|
else if (c == '\r') out += QStringLiteral("\\r");
|
|
else if (c == '\t') out += QStringLiteral("\\t");
|
|
else if (c == '\\') out += QStringLiteral("\\\\");
|
|
else if (c < QChar(0x20)) out += QStringLiteral("\\x") + QString::number(c.unicode(), 16);
|
|
else out += c;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
static QString bytesToAscii(const QByteArray& b, int slot) {
|
|
QString out;
|
|
out.reserve(slot);
|
|
for (int i = 0; i < slot; ++i) {
|
|
uint8_t c = (i < b.size()) ? (uint8_t)b[i] : 0;
|
|
out += isAsciiPrintable(c) ? QChar(c) : QChar('.');
|
|
}
|
|
return out;
|
|
}
|
|
|
|
static QString bytesToHex(const QByteArray& b, int slot) {
|
|
QString out;
|
|
out.reserve(slot * 3);
|
|
for (int i = 0; i < slot; ++i) {
|
|
uint8_t c = (i < b.size()) ? (uint8_t)b[i] : 0;
|
|
out += QString::asprintf("%02X", (unsigned)c);
|
|
if (i + 1 < slot) out += ' ';
|
|
}
|
|
return out;
|
|
}
|
|
|
|
static QString fmtAsciiAndBytes(const Provider& prov, uint64_t addr,
|
|
int sizeBytes, int slotBytes = 8) {
|
|
const int slot = qMax(slotBytes, sizeBytes);
|
|
QByteArray b = prov.isReadable(addr, slot)
|
|
? prov.readBytes(addr, slot)
|
|
: QByteArray(slot, '\0');
|
|
return bytesToAscii(b, slot) + QStringLiteral(" ") + bytesToHex(b, slot);
|
|
}
|
|
|
|
// ── Single value from provider (unified) ──
|
|
|
|
enum class ValueMode { Display, Editable };
|
|
|
|
static QString readValueImpl(const Node& node, const Provider& prov,
|
|
uint64_t addr, int subLine, ValueMode mode) {
|
|
const bool display = (mode == ValueMode::Display);
|
|
switch (node.kind) {
|
|
case NodeKind::Hex8: return display ? hexVal(prov.readU8(addr)) : rawHex(prov.readU8(addr), 2);
|
|
case NodeKind::Hex16: return display ? hexVal(prov.readU16(addr)) : rawHex(prov.readU16(addr), 4);
|
|
case NodeKind::Hex32: return display ? hexVal(prov.readU32(addr)) : rawHex(prov.readU32(addr), 8);
|
|
case NodeKind::Hex64: return display ? hexVal(prov.readU64(addr)) : rawHex(prov.readU64(addr), 16);
|
|
case NodeKind::Int8: return fmtInt8((int8_t)prov.readU8(addr));
|
|
case NodeKind::Int16: return fmtInt16((int16_t)prov.readU16(addr));
|
|
case NodeKind::Int32: return fmtInt32((int32_t)prov.readU32(addr));
|
|
case NodeKind::Int64: return fmtInt64((int64_t)prov.readU64(addr));
|
|
case NodeKind::UInt8: return fmtUInt8(prov.readU8(addr));
|
|
case NodeKind::UInt16: return fmtUInt16(prov.readU16(addr));
|
|
case NodeKind::UInt32: return fmtUInt32(prov.readU32(addr));
|
|
case NodeKind::UInt64: return fmtUInt64(prov.readU64(addr));
|
|
case NodeKind::Float: { auto s = fmtFloat(prov.readF32(addr)); return display ? s : s.trimmed(); }
|
|
case NodeKind::Double: { auto s = fmtDouble(prov.readF64(addr)); return display ? s : s.trimmed(); }
|
|
case NodeKind::Bool: return fmtBool(prov.readU8(addr));
|
|
case NodeKind::Pointer32: {
|
|
uint32_t val = prov.readU32(addr);
|
|
if (!display) return rawHex(val, 8);
|
|
QString s = fmtPointer32(val);
|
|
QString sym = prov.getSymbol((uint64_t)val);
|
|
if (!sym.isEmpty()) s += QStringLiteral(" // ") + sym;
|
|
return s;
|
|
}
|
|
case NodeKind::Pointer64: {
|
|
uint64_t val = prov.readU64(addr);
|
|
// Primitive pointer: dereference and show target value
|
|
// (hex/ptr/fnptr targets fall through to plain void* display)
|
|
if (node.ptrDepth > 0 && isValidPrimitivePtrTarget(node.elementKind) && val != 0) {
|
|
uint64_t target = val;
|
|
for (int d = 1; d < node.ptrDepth && target != 0; d++)
|
|
target = prov.isReadable(target, 8) ? prov.readU64(target) : 0;
|
|
if (target != 0 && prov.isReadable(target, sizeForKind(node.elementKind))) {
|
|
// Create a temporary node of the target kind to format the value
|
|
Node tmp;
|
|
tmp.kind = node.elementKind;
|
|
tmp.strLen = node.strLen;
|
|
QString derefVal = readValueImpl(tmp, prov, target, 0, mode);
|
|
if (display) {
|
|
QString arrow = QStringLiteral("-> ");
|
|
QString sym = prov.getSymbol(val);
|
|
if (!sym.isEmpty())
|
|
return arrow + derefVal + QStringLiteral(" // ") + sym;
|
|
return arrow + derefVal;
|
|
}
|
|
return derefVal;
|
|
}
|
|
if (!display) return rawHex(val, 16);
|
|
return fmtPointer64(val);
|
|
}
|
|
if (!display) return rawHex(val, 16);
|
|
QString s = fmtPointer64(val);
|
|
QString sym = prov.getSymbol(val);
|
|
if (!sym.isEmpty()) s += QStringLiteral(" // ") + sym;
|
|
return s;
|
|
}
|
|
case NodeKind::FuncPtr32: {
|
|
uint32_t val = prov.readU32(addr);
|
|
if (!display) return rawHex(val, 8);
|
|
QString s = fmtPointer32(val);
|
|
QString sym = prov.getSymbol((uint64_t)val);
|
|
if (!sym.isEmpty()) s += QStringLiteral(" // ") + sym;
|
|
return s;
|
|
}
|
|
case NodeKind::FuncPtr64: {
|
|
uint64_t val = prov.readU64(addr);
|
|
if (!display) return rawHex(val, 16);
|
|
QString s = fmtPointer64(val);
|
|
QString sym = prov.getSymbol(val);
|
|
if (!sym.isEmpty()) s += QStringLiteral(" // ") + sym;
|
|
return s;
|
|
}
|
|
case NodeKind::Vec2:
|
|
case NodeKind::Vec3:
|
|
case NodeKind::Vec4: {
|
|
int count = sizeForKind(node.kind) / 4;
|
|
QStringList parts;
|
|
for (int i = 0; i < count; i++)
|
|
parts << fmtFloat(prov.readF32(addr + i * 4)).trimmed();
|
|
return parts.join(QStringLiteral(", "));
|
|
}
|
|
case NodeKind::Mat4x4: {
|
|
if (!display) return {}; // not editable as single value
|
|
if (subLine < 0 || subLine >= 4) return QStringLiteral("?");
|
|
QString line = QStringLiteral("row%1 [").arg(subLine);
|
|
for (int c = 0; c < 4; c++) {
|
|
if (c > 0) line += QStringLiteral(", ");
|
|
line += fmtFloat(prov.readF32(addr + (subLine * 4 + c) * 4)).trimmed();
|
|
}
|
|
line += QStringLiteral("]");
|
|
return line;
|
|
}
|
|
case NodeKind::UTF8: {
|
|
QByteArray bytes = prov.readBytes(addr, node.strLen);
|
|
int end = bytes.indexOf('\0');
|
|
if (end >= 0) bytes.truncate(end);
|
|
QString s = QString::fromUtf8(bytes);
|
|
if (display) s = sanitizeString(s);
|
|
return display ? (QStringLiteral("\"") + s + QStringLiteral("\"")) : s;
|
|
}
|
|
case NodeKind::UTF16: {
|
|
QByteArray bytes = prov.readBytes(addr, node.strLen * 2);
|
|
QString s = QString::fromUtf16(reinterpret_cast<const char16_t*>(bytes.data()),
|
|
bytes.size() / 2);
|
|
int end = s.indexOf(QChar(0));
|
|
if (end >= 0) s.truncate(end);
|
|
if (display) s = sanitizeString(s);
|
|
return display ? (QStringLiteral("L\"") + s + QStringLiteral("\"")) : s;
|
|
}
|
|
default:
|
|
return {};
|
|
}
|
|
}
|
|
|
|
QString readValue(const Node& node, const Provider& prov,
|
|
uint64_t addr, int subLine) {
|
|
return readValueImpl(node, prov, addr, subLine, ValueMode::Display);
|
|
}
|
|
|
|
// ── Full node line ──
|
|
|
|
QString fmtNodeLine(const Node& node, const Provider& prov,
|
|
uint64_t addr, int depth, int subLine,
|
|
const QString& comment, int colType, int colName,
|
|
const QString& typeOverride, bool compact) {
|
|
QString ind = indent(depth);
|
|
|
|
// Compute raw type string for overflow detection
|
|
QString rawType = typeOverride.isEmpty() ? typeNameRaw(node.kind) : typeOverride;
|
|
bool overflow = compact && rawType.size() > colType;
|
|
|
|
QString type = overflow ? fitOverflow(rawType, colType)
|
|
: (typeOverride.isEmpty() ? typeName(node.kind, colType)
|
|
: fit(typeOverride, colType));
|
|
QString name = overflow ? node.name : fit(node.name, colName);
|
|
|
|
// Effective column width for this line (accounts for overflow, clamped to hard max)
|
|
int effectiveColType = overflow ? rawType.size() : colType;
|
|
// Blank prefix for continuation lines (same width as type+sep+name+sep)
|
|
const int prefixW = effectiveColType + (overflow ? name.size() : colName) + 2 * kSepWidth;
|
|
|
|
// Comment suffix (only present when a comment is provided; no trailing padding)
|
|
QString cmtSuffix = comment.isEmpty() ? QString()
|
|
: fit(comment, COL_COMMENT);
|
|
|
|
// Mat4x4: subLine 0..3 = rows — no truncation so large floats always display fully
|
|
if (node.kind == NodeKind::Mat4x4) {
|
|
QString val = readValue(node, prov, addr, subLine);
|
|
if (subLine == 0) return ind + type + SEP + name + SEP + val + cmtSuffix;
|
|
return ind + QString(prefixW, ' ') + val + cmtSuffix;
|
|
}
|
|
|
|
// Hex nodes: hex byte preview (ASCII padded to colName to align with value column)
|
|
if (isHexPreview(node.kind)) {
|
|
const int sz = sizeForKind(node.kind);
|
|
QByteArray b = prov.isReadable(addr, sz)
|
|
? prov.readBytes(addr, sz) : QByteArray(sz, '\0');
|
|
QString ascii = bytesToAscii(b, sz).leftJustified(colName, ' ');
|
|
QString hex = bytesToHex(b, sz).leftJustified(23, ' ');
|
|
return ind + type + SEP + ascii + SEP + hex + cmtSuffix;
|
|
}
|
|
|
|
QString val = overflow ? readValue(node, prov, addr, subLine)
|
|
: fit(readValue(node, prov, addr, subLine), COL_VALUE);
|
|
return ind + type + SEP + name + SEP + val + cmtSuffix;
|
|
}
|
|
|
|
// ── Editable value (parse-friendly form for edit dialog) ──
|
|
|
|
QString editableValue(const Node& node, const Provider& prov,
|
|
uint64_t addr, int subLine) {
|
|
return readValueImpl(node, prov, addr, subLine, ValueMode::Editable);
|
|
}
|
|
|
|
// ── Value parsing (text → bytes) ──
|
|
|
|
template<class T>
|
|
static QByteArray toBytes(T v) {
|
|
QByteArray b(sizeof(T), Qt::Uninitialized);
|
|
memcpy(b.data(), &v, sizeof(T));
|
|
return b;
|
|
}
|
|
|
|
static QString stripHex(const QString& s) {
|
|
if (s.startsWith(QStringLiteral("0x"), Qt::CaseInsensitive))
|
|
return s.mid(2);
|
|
return s;
|
|
}
|
|
|
|
// Parse ASCII text into raw byte array (each char becomes a byte)
|
|
QByteArray parseAsciiValue(const QString& text, int expectedSize, bool* ok) {
|
|
*ok = false;
|
|
if (text.size() != expectedSize) return {};
|
|
QByteArray result(expectedSize, Qt::Uninitialized);
|
|
for (int i = 0; i < expectedSize; i++) {
|
|
uint c = text[i].unicode();
|
|
if (c > 255) return {}; // Non-Latin1 character
|
|
result[i] = (char)c;
|
|
}
|
|
*ok = true;
|
|
return result;
|
|
}
|
|
|
|
// Parse space-separated hex byte string into raw byte array (no endian conversion)
|
|
static QByteArray parseHexBytes(const QString& s, int expectedSize, bool* ok) {
|
|
QString clean = s;
|
|
clean.remove(' ');
|
|
if (clean.size() != expectedSize * 2) { *ok = false; return {}; }
|
|
QByteArray result(expectedSize, Qt::Uninitialized);
|
|
for (int i = 0; i < expectedSize; i++) {
|
|
bool byteOk;
|
|
uint byte = clean.mid(i * 2, 2).toUInt(&byteOk, 16);
|
|
if (!byteOk) { *ok = false; return {}; }
|
|
result[i] = (char)byte;
|
|
}
|
|
*ok = true;
|
|
return result;
|
|
}
|
|
|
|
// Range-checked narrowing: sets *ok = false if parsed value doesn't fit in T
|
|
template<class T, class ParseT>
|
|
static QByteArray parseIntChecked(ParseT val, bool* ok) {
|
|
if (*ok) {
|
|
using L = std::numeric_limits<T>;
|
|
if constexpr (std::is_signed_v<T>) {
|
|
if (val < (ParseT)L::min() || val > (ParseT)L::max()) *ok = false;
|
|
} else {
|
|
if (val > (ParseT)L::max()) *ok = false;
|
|
}
|
|
}
|
|
return *ok ? toBytes<T>(static_cast<T>(val)) : QByteArray{};
|
|
}
|
|
|
|
QByteArray parseValue(NodeKind kind, const QString& text, bool* ok) {
|
|
*ok = false;
|
|
QString s = text.trimmed();
|
|
|
|
// Allow empty for string types (will produce zero-length content, caller pads)
|
|
if (s.isEmpty()) {
|
|
if (kind == NodeKind::UTF8 || kind == NodeKind::UTF16) {
|
|
*ok = true;
|
|
return {};
|
|
}
|
|
return {};
|
|
}
|
|
|
|
switch (kind) {
|
|
case NodeKind::Hex8: return parseHexBytes(stripHex(s), 1, ok);
|
|
case NodeKind::Hex16: {
|
|
QString cleaned = stripHex(s);
|
|
// Space-separated bytes → raw byte order (display order preserved)
|
|
if (cleaned.contains(' '))
|
|
return parseHexBytes(cleaned, 2, ok);
|
|
// Single value → native-endian
|
|
uint val = cleaned.toUInt(ok, 16);
|
|
if (*ok && val > 0xFFFF) *ok = false;
|
|
return *ok ? toBytes<uint16_t>(static_cast<uint16_t>(val)) : QByteArray{};
|
|
}
|
|
case NodeKind::Hex32: {
|
|
QString cleaned = stripHex(s);
|
|
// Space-separated bytes → raw byte order (display order preserved)
|
|
if (cleaned.contains(' '))
|
|
return parseHexBytes(cleaned, 4, ok);
|
|
// Single value → native-endian
|
|
uint val = cleaned.toUInt(ok, 16);
|
|
return *ok ? toBytes<uint32_t>(val) : QByteArray{};
|
|
}
|
|
case NodeKind::Hex64: {
|
|
QString cleaned = stripHex(s);
|
|
// Space-separated bytes → raw byte order (display order preserved)
|
|
if (cleaned.contains(' '))
|
|
return parseHexBytes(cleaned, 8, ok);
|
|
// Single value → native-endian
|
|
qulonglong val = cleaned.toULongLong(ok, 16);
|
|
return *ok ? toBytes<uint64_t>(val) : QByteArray{};
|
|
}
|
|
case NodeKind::Int8: {
|
|
bool isHex = s.startsWith("0x", Qt::CaseInsensitive);
|
|
if (isHex) {
|
|
uint val = stripHex(s).toUInt(ok, 16);
|
|
if (*ok && val > 0xFF) *ok = false;
|
|
return *ok ? toBytes<int8_t>(static_cast<int8_t>(val)) : QByteArray{};
|
|
} else {
|
|
int val = s.toInt(ok, 10);
|
|
return parseIntChecked<int8_t>(val, ok);
|
|
}
|
|
}
|
|
case NodeKind::Int16: {
|
|
bool isHex = s.startsWith("0x", Qt::CaseInsensitive);
|
|
if (isHex) {
|
|
uint val = stripHex(s).toUInt(ok, 16);
|
|
if (*ok && val > 0xFFFF) *ok = false;
|
|
return *ok ? toBytes<int16_t>(static_cast<int16_t>(val)) : QByteArray{};
|
|
} else {
|
|
int val = s.toInt(ok, 10);
|
|
return parseIntChecked<int16_t>(val, ok);
|
|
}
|
|
}
|
|
case NodeKind::Int32: {
|
|
bool isHex = s.startsWith("0x", Qt::CaseInsensitive);
|
|
if (isHex) {
|
|
qulonglong val = stripHex(s).toULongLong(ok, 16);
|
|
if (*ok && val > 0xFFFFFFFFULL) *ok = false;
|
|
return *ok ? toBytes<int32_t>(static_cast<int32_t>(val)) : QByteArray{};
|
|
} else {
|
|
int val = s.toInt(ok, 10);
|
|
return *ok ? toBytes<int32_t>(val) : QByteArray{};
|
|
}
|
|
}
|
|
case NodeKind::Int64: {
|
|
bool isHex = s.startsWith("0x", Qt::CaseInsensitive);
|
|
if (isHex) {
|
|
qulonglong val = stripHex(s).toULongLong(ok, 16);
|
|
return *ok ? toBytes<int64_t>(static_cast<int64_t>(val)) : QByteArray{};
|
|
} else {
|
|
qlonglong val = s.toLongLong(ok, 10);
|
|
return *ok ? toBytes<int64_t>(val) : QByteArray{};
|
|
}
|
|
}
|
|
case NodeKind::UInt8: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; uint val = stripHex(s).toUInt(ok,b); return parseIntChecked<uint8_t>(val, ok); }
|
|
case NodeKind::UInt16: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; uint val = stripHex(s).toUInt(ok,b); return parseIntChecked<uint16_t>(val, ok); }
|
|
case NodeKind::UInt32: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; qulonglong val = stripHex(s).toULongLong(ok,b); return parseIntChecked<uint32_t>(val, ok); }
|
|
case NodeKind::UInt64: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; qulonglong val = stripHex(s).toULongLong(ok,b); return *ok ? toBytes<uint64_t>(val) : QByteArray{}; }
|
|
case NodeKind::Float: {
|
|
QString n = s.trimmed();
|
|
if (n.endsWith('f', Qt::CaseInsensitive)) n.chop(1);
|
|
n.replace(',', '.');
|
|
float val = n.toFloat(ok);
|
|
return *ok ? toBytes<float>(val) : QByteArray{};
|
|
}
|
|
case NodeKind::Double: {
|
|
QString n = s.trimmed();
|
|
n.replace(',', '.');
|
|
double val = n.toDouble(ok);
|
|
return *ok ? toBytes<double>(val) : QByteArray{};
|
|
}
|
|
case NodeKind::Bool: {
|
|
if (s == QStringLiteral("true") || s == QStringLiteral("1")) {
|
|
*ok = true; return toBytes<uint8_t>(1);
|
|
}
|
|
if (s == QStringLiteral("false") || s == QStringLiteral("0")) {
|
|
*ok = true; return toBytes<uint8_t>(0);
|
|
}
|
|
return {}; // unknown token → ok stays false
|
|
}
|
|
case NodeKind::Pointer32: {
|
|
uint val = stripHex(s).toUInt(ok, 16);
|
|
return *ok ? toBytes<uint32_t>(val) : QByteArray{};
|
|
}
|
|
case NodeKind::Pointer64: {
|
|
qulonglong val = stripHex(s).toULongLong(ok, 16);
|
|
return *ok ? toBytes<uint64_t>(val) : QByteArray{};
|
|
}
|
|
case NodeKind::FuncPtr32: {
|
|
uint val = stripHex(s).toUInt(ok, 16);
|
|
return *ok ? toBytes<uint32_t>(val) : QByteArray{};
|
|
}
|
|
case NodeKind::FuncPtr64: {
|
|
qulonglong val = stripHex(s).toULongLong(ok, 16);
|
|
return *ok ? toBytes<uint64_t>(val) : QByteArray{};
|
|
}
|
|
case NodeKind::UTF8: {
|
|
*ok = true;
|
|
if (s.startsWith('"') && s.endsWith('"'))
|
|
s = s.mid(1, s.size() - 2);
|
|
return s.toUtf8();
|
|
}
|
|
case NodeKind::UTF16: {
|
|
*ok = true;
|
|
if (s.startsWith(QStringLiteral("L\""))) s = s.mid(2);
|
|
else if (s.startsWith('"')) s = s.mid(1);
|
|
if (s.endsWith('"')) s.chop(1);
|
|
QByteArray b(s.size() * 2, Qt::Uninitialized);
|
|
memcpy(b.data(), s.utf16(), s.size() * 2);
|
|
return b;
|
|
}
|
|
default:
|
|
return {};
|
|
}
|
|
}
|
|
|
|
// ── Value validation (returns error message or empty string if valid) ──
|
|
|
|
QString validateValue(NodeKind kind, const QString& text) {
|
|
QString s = text.trimmed();
|
|
if (s.isEmpty()) return {};
|
|
|
|
// For integer/hex types, validate character set first
|
|
bool isHexKind = (kind >= NodeKind::Hex8 && kind <= NodeKind::Hex64)
|
|
|| kind == NodeKind::Pointer32 || kind == NodeKind::Pointer64
|
|
|| kind == NodeKind::FuncPtr32 || kind == NodeKind::FuncPtr64;
|
|
bool isIntKind = (kind >= NodeKind::Int8 && kind <= NodeKind::UInt64);
|
|
|
|
if (isHexKind || isIntKind) {
|
|
bool hasHexPrefix = s.startsWith("0x", Qt::CaseInsensitive);
|
|
QString digits = hasHexPrefix ? s.mid(2) : s;
|
|
|
|
if (hasHexPrefix || isHexKind) {
|
|
// Hex mode: only 0-9, a-f, A-F
|
|
for (QChar c : digits) {
|
|
if (!c.isDigit() && !(c >= 'a' && c <= 'f') && !(c >= 'A' && c <= 'F'))
|
|
return QStringLiteral("invalid hex '%1'").arg(c);
|
|
}
|
|
} else {
|
|
// Decimal mode: only digits (and leading minus for signed)
|
|
int start = 0;
|
|
bool isSigned = (kind >= NodeKind::Int8 && kind <= NodeKind::Int64);
|
|
if (isSigned && !digits.isEmpty() && digits[0] == '-') start = 1;
|
|
for (int i = start; i < digits.size(); i++) {
|
|
if (!digits[i].isDigit())
|
|
return QStringLiteral("invalid '%1'").arg(digits[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Then do the actual parse for range checking
|
|
bool ok;
|
|
parseValue(kind, text, &ok);
|
|
if (ok) return {};
|
|
|
|
// Type-appropriate error messages
|
|
bool isFloatKind = (kind == NodeKind::Float || kind == NodeKind::Double);
|
|
if (isFloatKind)
|
|
return QStringLiteral("invalid number");
|
|
|
|
// Return byte-capacity max based on type size
|
|
const auto* m = kindMeta(kind);
|
|
if (m && m->size > 0 && m->size <= 8) {
|
|
uint64_t maxVal = (m->size == 8) ? ~0ULL : ((1ULL << (m->size * 8)) - 1);
|
|
return QStringLiteral("too large! max=0x%1").arg(maxVal, m->size * 2, 16, QChar('0'));
|
|
}
|
|
return QStringLiteral("invalid");
|
|
}
|
|
|
|
// ── Base address validation (delegates to AddressParser) ──
|
|
|
|
QString validateBaseAddress(const QString& text) {
|
|
QString s = text.trimmed();
|
|
if (s.isEmpty()) return QStringLiteral("empty");
|
|
//s.remove('`');
|
|
return AddressParser::validate(s);
|
|
}
|
|
|
|
QString fmtEnumMember(const QString& name, int64_t value, int depth, int nameW) {
|
|
QString ind = indent(depth);
|
|
return ind + name.leftJustified(nameW) + QStringLiteral(" = ") + QString::number(value);
|
|
}
|
|
|
|
// ── Bitfield member formatting ──
|
|
|
|
uint64_t extractBits(const Provider& prov, uint64_t addr,
|
|
NodeKind containerKind,
|
|
uint8_t bitOffset, uint8_t bitWidth) {
|
|
uint64_t container = 0;
|
|
switch (containerKind) {
|
|
case NodeKind::Hex8: container = prov.readU8(addr); break;
|
|
case NodeKind::Hex16: container = prov.readU16(addr); break;
|
|
case NodeKind::Hex32: container = prov.readU32(addr); break;
|
|
default: container = prov.readU64(addr); break;
|
|
}
|
|
if (bitWidth >= 64) return container >> bitOffset;
|
|
return (container >> bitOffset) & ((1ULL << bitWidth) - 1);
|
|
}
|
|
|
|
QString fmtBitfieldMember(const QString& name, uint8_t bitWidth,
|
|
uint64_t value, int depth, int nameW) {
|
|
QString ind = indent(depth);
|
|
return ind + name.leftJustified(nameW)
|
|
+ QStringLiteral(" : %1 = %2").arg(bitWidth).arg(value);
|
|
}
|
|
|
|
} // namespace rcx::fmt
|