diff --git a/CMakeLists.txt b/CMakeLists.txt index 4203a00..c0925d6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,6 +20,8 @@ add_executable(ReclassX src/controller.cpp src/compose.cpp src/format.cpp + src/generator.h + src/generator.cpp src/processpicker.h src/processpicker.cpp src/processpicker.ui @@ -58,6 +60,7 @@ foreach(_f \"${CMAKE_SOURCE_DIR}/src/controller.cpp\" \"${CMAKE_SOURCE_DIR}/src/compose.cpp\" \"${CMAKE_SOURCE_DIR}/src/format.cpp\" + \"${CMAKE_SOURCE_DIR}/src/generator.cpp\" \"${CMAKE_SOURCE_DIR}/src/main.cpp\") file(READ \${_f} _content) file(APPEND \${_out} \${_content}) @@ -125,4 +128,19 @@ if(BUILD_TESTING) Qt6::Widgets Qt6::PrintSupport Qt6::Test QScintilla::QScintilla dbghelp psapi) 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/processpicker.cpp src/processpicker.ui) + target_include_directories(test_validation PRIVATE src) + target_link_libraries(test_validation PRIVATE + Qt6::Widgets Qt6::PrintSupport Qt6::Test + QScintilla::QScintilla dbghelp psapi) + add_test(NAME test_validation COMMAND test_validation) + + add_executable(test_generator tests/test_generator.cpp + src/generator.cpp src/compose.cpp src/format.cpp) + target_include_directories(test_generator PRIVATE src) + target_link_libraries(test_generator PRIVATE Qt6::Core Qt6::Test) + add_test(NAME test_generator COMMAND test_generator) endif() diff --git a/src/compose.cpp b/src/compose.cpp index 3888bb0..43fc9d9 100644 --- a/src/compose.cpp +++ b/src/compose.cpp @@ -401,8 +401,8 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov) { // Include struct/array names - they now use columnar layout too int maxNameLen = kMinNameW; for (const Node& node : tree.nodes) { - // Skip hex/padding (they show ASCII preview, not name column) - if (isHexPreview(node.kind)) continue; + // Skip padding (it shows ASCII preview, not name column) + if (node.kind == NodeKind::Padding) continue; maxNameLen = qMax(maxNameLen, (int)node.name.size()); } state.nameW = qBound(kMinNameW, maxNameLen, kMaxNameW); @@ -420,8 +420,8 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov) { const Node& child = tree.nodes[childIdx]; scopeMaxType = qMax(scopeMaxType, (int)nodeTypeName(child).size()); - // Name width (skip hex/padding, but include containers) - if (!isHexPreview(child.kind)) { + // Name width (skip padding, but include hex and containers) + if (child.kind != NodeKind::Padding) { scopeMaxName = qMax(scopeMaxName, (int)child.name.size()); } } @@ -439,8 +439,8 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov) { const Node& child = tree.nodes[childIdx]; rootMaxType = qMax(rootMaxType, (int)nodeTypeName(child).size()); - // Name width (skip hex/padding, include containers) - if (!isHexPreview(child.kind)) { + // Name width (skip padding, include hex and containers) + if (child.kind != NodeKind::Padding) { rootMaxName = qMax(rootMaxName, (int)child.name.size()); } } diff --git a/src/controller.cpp b/src/controller.cpp index 05e66fc..09eabf7 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -172,8 +172,8 @@ void RcxController::connectEditor(RcxEditor* editor) { case EditTarget::Name: { if (text.isEmpty()) break; const Node& node = m_doc->tree.nodes[nodeIdx]; - // ASCII edit on Hex/Padding nodes - if (isHexPreview(node.kind)) { + // ASCII edit on Padding nodes + if (node.kind == NodeKind::Padding) { setNodeValue(nodeIdx, subLine, text, /*isAscii=*/true); } else { renameNode(nodeIdx, text); diff --git a/src/core.h b/src/core.h index 119717c..0988036 100644 --- a/src/core.h +++ b/src/core.h @@ -486,8 +486,8 @@ inline ColumnSpan nameSpanFor(const LineMeta& lm, int typeW = kColType, int name int ind = kFoldCol + lm.depth * 3; int start = ind + typeW + kSepWidth; - // Hex/Padding: ASCII preview takes the name column position (8 chars) - if (isHexPreview(lm.nodeKind)) + // Padding: ASCII preview takes the name column position (8 chars) + if (lm.nodeKind == NodeKind::Padding) return {start, start + 8, true}; return {start, start + nameW, true}; @@ -498,12 +498,12 @@ inline ColumnSpan valueSpanFor(const LineMeta& lm, int /*lineLength*/, int typeW lm.lineKind == LineKind::ArrayElementSeparator) return {}; int ind = kFoldCol + lm.depth * 3; - // Hex/Padding layout: [Type][sep][ASCII(8)][sep][hex bytes(23)] - bool isHexPad = isHexPreview(lm.nodeKind); - int valWidth = isHexPad ? 23 : kColValue; // hex bytes or value column + // Padding layout: [Type][sep][ASCII(8)][sep][hex bytes(23)] + bool isPad = (lm.nodeKind == NodeKind::Padding); + int valWidth = isPad ? 23 : kColValue; if (lm.isContinuation) { - int prefixW = isHexPad + int prefixW = isPad ? (typeW + kSepWidth + 8 + kSepWidth) : (typeW + nameW + 2 * kSepWidth); int start = ind + prefixW; @@ -511,7 +511,7 @@ inline ColumnSpan valueSpanFor(const LineMeta& lm, int /*lineLength*/, int typeW } if (lm.lineKind != LineKind::Field) return {}; - int start = isHexPad + int start = isPad ? (ind + typeW + kSepWidth + 8 + kSepWidth) : (ind + typeW + kSepWidth + nameW + kSepWidth); return {start, start + valWidth, true}; @@ -521,17 +521,17 @@ inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength, int typeW = if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer) return {}; int ind = kFoldCol + lm.depth * 3; - bool isHexPad = isHexPreview(lm.nodeKind); - int valWidth = isHexPad ? 23 : kColValue; + bool isPad = (lm.nodeKind == NodeKind::Padding); + int valWidth = isPad ? 23 : kColValue; int start; if (lm.isContinuation) { - int prefixW = isHexPad + int prefixW = isPad ? (typeW + kSepWidth + 8 + kSepWidth) : (typeW + nameW + 2 * kSepWidth); start = ind + prefixW + valWidth; } else { - start = isHexPad + start = isPad ? (ind + typeW + kSepWidth + 8 + kSepWidth + valWidth) : (ind + typeW + kSepWidth + nameW + kSepWidth + valWidth); } diff --git a/src/editor.cpp b/src/editor.cpp index a4ae9ee..cbebe17 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -404,7 +404,7 @@ void RcxEditor::fillIndicatorCols(int indic, int line, int colA, int colB) { void RcxEditor::applyHexDimming(const QVector& meta) { m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_HEX_DIM); for (int i = 0; i < meta.size(); i++) { - if (isHexPreview(meta[i].nodeKind)) { + if (meta[i].nodeKind == NodeKind::Padding) { long pos, len; lineRangeNoEol(m_sci, i, pos, len); if (len > 0) m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, pos, len); @@ -1678,7 +1678,7 @@ void RcxEditor::applyHoverCursor() { bool inHexDataArea = false; uint64_t hoverNodeId = 0; if (hoverLine >= 0 && hoverLine < m_meta.size() - && isHexPreview(m_meta[hoverLine].nodeKind)) { + && m_meta[hoverLine].nodeKind == NodeKind::Padding) { hoverNodeId = m_meta[hoverLine].nodeId; if (hoverNodeId != 0 && h.col >= 0) { int ind = kFoldCol + m_meta[hoverLine].depth * 3; diff --git a/src/format.cpp b/src/format.cpp index 8c7ac02..8d0d71c 100644 --- a/src/format.cpp +++ b/src/format.cpp @@ -328,27 +328,18 @@ QString fmtNodeLine(const Node& node, const Provider& prov, return ind + QString(prefixW, ' ') + val + cmtSuffix; } - // Hex nodes and Padding: ASCII preview + hex bytes (compact) - if (isHexPreview(node.kind)) { - if (node.kind == NodeKind::Padding) { - const int totalSz = qMax(1, node.arrayLen); - const int lineOff = subLine * 8; - const int lineBytes = qMin(8, totalSz - lineOff); - QByteArray b = prov.isReadable(addr + lineOff, lineBytes) - ? prov.readBytes(addr + lineOff, lineBytes) : QByteArray(lineBytes, '\0'); - QString ascii = bytesToAscii(b, lineBytes); - QString hex = bytesToHex(b, lineBytes).leftJustified(23, ' '); // 8*3-1 - if (subLine == 0) - return ind + type + SEP + ascii + SEP + hex + cmtSuffix; - return ind + QString(colType + (int)SEP.size(), ' ') + ascii + SEP + hex + cmtSuffix; - } - // Hex8..Hex64: single line, ASCII padded to 8 chars so hex column aligns - 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(8, ' '); - QString hex = bytesToHex(b, sz).leftJustified(23, ' '); - return ind + type + SEP + ascii + SEP + hex + cmtSuffix; + // Padding: ASCII preview + hex bytes (compact, multi-line) + if (node.kind == NodeKind::Padding) { + const int totalSz = qMax(1, node.arrayLen); + const int lineOff = subLine * 8; + const int lineBytes = qMin(8, totalSz - lineOff); + QByteArray b = prov.isReadable(addr + lineOff, lineBytes) + ? prov.readBytes(addr + lineOff, lineBytes) : QByteArray(lineBytes, '\0'); + QString ascii = bytesToAscii(b, lineBytes); + QString hex = bytesToHex(b, lineBytes).leftJustified(23, ' '); // 8*3-1 + if (subLine == 0) + return ind + type + SEP + ascii + SEP + hex + cmtSuffix; + return ind + QString(colType + (int)SEP.size(), ' ') + ascii + SEP + hex + cmtSuffix; } QString val = fit(readValue(node, prov, addr, subLine), COL_VALUE); diff --git a/src/generator.cpp b/src/generator.cpp new file mode 100644 index 0000000..661223b --- /dev/null +++ b/src/generator.cpp @@ -0,0 +1,369 @@ +#include "generator.h" +#include +#include +#include +#include + +namespace rcx { + +namespace { + +// ── Identifier sanitisation ── + +static QString sanitizeIdent(const QString& name) { + if (name.isEmpty()) return QStringLiteral("unnamed"); + QString out; + out.reserve(name.size()); + for (QChar c : name) { + if (c.isLetterOrNumber() || c == '_') out += c; + else out += '_'; + } + if (!out[0].isLetter() && out[0] != '_') + out.prepend('_'); + return out; +} + +// ── C type name for a primitive NodeKind ── + +static QString cTypeName(NodeKind kind) { + switch (kind) { + case NodeKind::Hex8: return QStringLiteral("uint8_t"); + case NodeKind::Hex16: return QStringLiteral("uint16_t"); + case NodeKind::Hex32: return QStringLiteral("uint32_t"); + case NodeKind::Hex64: return QStringLiteral("uint64_t"); + case NodeKind::Int8: return QStringLiteral("int8_t"); + case NodeKind::Int16: return QStringLiteral("int16_t"); + case NodeKind::Int32: return QStringLiteral("int32_t"); + case NodeKind::Int64: return QStringLiteral("int64_t"); + case NodeKind::UInt8: return QStringLiteral("uint8_t"); + case NodeKind::UInt16: return QStringLiteral("uint16_t"); + case NodeKind::UInt32: return QStringLiteral("uint32_t"); + case NodeKind::UInt64: return QStringLiteral("uint64_t"); + case NodeKind::Float: return QStringLiteral("float"); + case NodeKind::Double: return QStringLiteral("double"); + case NodeKind::Bool: return QStringLiteral("bool"); + case NodeKind::Pointer32: return QStringLiteral("uint32_t"); + case NodeKind::Pointer64: return QStringLiteral("uint64_t"); + case NodeKind::Vec2: return QStringLiteral("float"); + case NodeKind::Vec3: return QStringLiteral("float"); + case NodeKind::Vec4: return QStringLiteral("float"); + case NodeKind::Mat4x4: return QStringLiteral("float"); + case NodeKind::UTF8: return QStringLiteral("char"); + case NodeKind::UTF16: return QStringLiteral("wchar_t"); + case NodeKind::Padding: return QStringLiteral("uint8_t"); + default: return QStringLiteral("uint8_t"); + } +} + +// ── Generator context ── + +struct GenContext { + const NodeTree& tree; + QHash> childMap; + QSet emittedTypeNames; // struct type names already emitted + QSet emittedIds; // struct node IDs already emitted + QSet visiting; // cycle guard + QSet forwardDeclared; // forward-declared type IDs + QString output; + int padCounter = 0; + + QString uniquePadName() { + return QStringLiteral("_pad%1").arg(padCounter++, 4, 16, QChar('0')); + } + + // Resolve the canonical type name for a struct/array node + QString structName(const Node& n) const { + if (!n.structTypeName.isEmpty()) return sanitizeIdent(n.structTypeName); + if (!n.name.isEmpty()) return sanitizeIdent(n.name); + return QStringLiteral("anon_%1").arg(n.id, 0, 16); + } +}; + +// Forward declarations +static void emitStruct(GenContext& ctx, uint64_t structId); + +// ── Emit a single field declaration ── + +static QString emitField(GenContext& ctx, const Node& node) { + const NodeTree& tree = ctx.tree; + QString name = sanitizeIdent(node.name.isEmpty() + ? QStringLiteral("field_%1").arg(node.offset, 2, 16, QChar('0')) + : node.name); + + switch (node.kind) { + case NodeKind::Vec2: + return QStringLiteral(" float %1[2];").arg(name); + case NodeKind::Vec3: + return QStringLiteral(" float %1[3];").arg(name); + case NodeKind::Vec4: + return QStringLiteral(" float %1[4];").arg(name); + case NodeKind::Mat4x4: + return QStringLiteral(" float %1[4][4];").arg(name); + case NodeKind::UTF8: + return QStringLiteral(" char %1[%2];").arg(name).arg(node.strLen); + case NodeKind::UTF16: + return QStringLiteral(" wchar_t %1[%2];").arg(name).arg(node.strLen); + case NodeKind::Padding: + return QStringLiteral(" uint8_t %1[%2];").arg(name).arg(qMax(1, node.arrayLen)); + case NodeKind::Pointer32: { + if (node.refId != 0) { + int refIdx = tree.indexOfId(node.refId); + if (refIdx >= 0) { + QString target = ctx.structName(tree.nodes[refIdx]); + return QStringLiteral(" uint32_t %1; // -> %2*").arg(name, target); + } + } + return QStringLiteral(" uint32_t %1;").arg(name); + } + case NodeKind::Pointer64: { + if (node.refId != 0) { + int refIdx = tree.indexOfId(node.refId); + if (refIdx >= 0) { + QString target = ctx.structName(tree.nodes[refIdx]); + return QStringLiteral(" %1* %2;").arg(target, name); + } + } + return QStringLiteral(" void* %1;").arg(name); + } + default: + return QStringLiteral(" %1 %2;").arg(cTypeName(node.kind), name); + } +} + +// ── Emit struct body (fields + padding) ── + +static void emitStructBody(GenContext& ctx, uint64_t structId) { + const NodeTree& tree = ctx.tree; + int idx = tree.indexOfId(structId); + if (idx < 0) return; + + int structSize = tree.structSpan(structId, &ctx.childMap); + + QVector children = ctx.childMap.value(structId); + std::sort(children.begin(), children.end(), [&](int a, int b) { + return tree.nodes[a].offset < tree.nodes[b].offset; + }); + + int cursor = 0; + + for (int ci : children) { + const Node& child = tree.nodes[ci]; + int childSize; + if (child.kind == NodeKind::Struct || child.kind == NodeKind::Array) + childSize = tree.structSpan(child.id, &ctx.childMap); + else + childSize = child.byteSize(); + + // Gap before this field + if (child.offset > cursor) { + int gap = child.offset - cursor; + ctx.output += QStringLiteral(" uint8_t %1[0x%2];\n") + .arg(ctx.uniquePadName()) + .arg(QString::number(gap, 16).toUpper()); + } else if (child.offset < cursor) { + // Overlap + ctx.output += QStringLiteral(" // WARNING: overlap at offset 0x%1 (previous field ends at 0x%2)\n") + .arg(QString::number(child.offset, 16).toUpper()) + .arg(QString::number(cursor, 16).toUpper()); + } + + // Emit the field + if (child.kind == NodeKind::Struct) { + // Ensure the nested struct type is emitted first + emitStruct(ctx, child.id); + QString typeName = ctx.structName(child); + QString fieldName = sanitizeIdent(child.name); + ctx.output += QStringLiteral(" %1 %2;\n").arg(typeName, fieldName); + } else if (child.kind == NodeKind::Array) { + // Check if array has struct element children + QVector arrayKids = ctx.childMap.value(child.id); + bool hasStructChild = false; + QString elemTypeName; + + for (int ak : arrayKids) { + if (tree.nodes[ak].kind == NodeKind::Struct) { + hasStructChild = true; + emitStruct(ctx, tree.nodes[ak].id); + elemTypeName = ctx.structName(tree.nodes[ak]); + break; + } + } + + QString fieldName = sanitizeIdent(child.name); + if (hasStructChild && !elemTypeName.isEmpty()) { + ctx.output += QStringLiteral(" %1 %2[%3];\n") + .arg(elemTypeName, fieldName).arg(child.arrayLen); + } else { + ctx.output += QStringLiteral(" %1 %2[%3];\n") + .arg(cTypeName(child.elementKind), fieldName).arg(child.arrayLen); + } + } else { + ctx.output += emitField(ctx, child) + QStringLiteral("\n"); + } + + int childEnd = child.offset + childSize; + if (childEnd > cursor) cursor = childEnd; + } + + // Tail padding + if (cursor < structSize) { + int gap = structSize - cursor; + ctx.output += QStringLiteral(" uint8_t %1[0x%2];\n") + .arg(ctx.uniquePadName()) + .arg(QString::number(gap, 16).toUpper()); + } +} + +// ── Emit a complete struct definition ── + +static void emitStruct(GenContext& ctx, uint64_t structId) { + if (ctx.emittedIds.contains(structId)) return; + if (ctx.visiting.contains(structId)) return; // cycle + ctx.visiting.insert(structId); + + int idx = ctx.tree.indexOfId(structId); + if (idx < 0) { ctx.visiting.remove(structId); return; } + + const Node& node = ctx.tree.nodes[idx]; + if (node.kind != NodeKind::Struct && node.kind != NodeKind::Array) { + ctx.visiting.remove(structId); + return; + } + + // For arrays, we don't emit a top-level struct — the array itself + // is a field inside its parent. But we do emit struct element types. + if (node.kind == NodeKind::Array) { + QVector kids = ctx.childMap.value(structId); + for (int ki : kids) { + if (ctx.tree.nodes[ki].kind == NodeKind::Struct) + emitStruct(ctx, ctx.tree.nodes[ki].id); + } + ctx.visiting.remove(structId); + return; + } + + // Emit nested struct types first (dependency order) + QVector children = ctx.childMap.value(structId); + for (int ci : children) { + const Node& child = ctx.tree.nodes[ci]; + if (child.kind == NodeKind::Struct) + emitStruct(ctx, child.id); + else if (child.kind == NodeKind::Array) { + QVector arrayKids = ctx.childMap.value(child.id); + for (int ak : arrayKids) { + if (ctx.tree.nodes[ak].kind == NodeKind::Struct) + emitStruct(ctx, ctx.tree.nodes[ak].id); + } + } + // Forward-declare pointer target types if they're outside this subtree + if (child.kind == NodeKind::Pointer64 && child.refId != 0) { + int refIdx = ctx.tree.indexOfId(child.refId); + if (refIdx >= 0 && !ctx.emittedIds.contains(child.refId) + && !ctx.forwardDeclared.contains(child.refId)) { + QString fwdName = ctx.structName(ctx.tree.nodes[refIdx]); + ctx.output += QStringLiteral("struct %1;\n").arg(fwdName); + ctx.forwardDeclared.insert(child.refId); + } + } + } + + ctx.emittedIds.insert(structId); + + QString typeName = ctx.structName(node); + int structSize = ctx.tree.structSpan(structId, &ctx.childMap); + + ctx.output += QStringLiteral("#pragma pack(push, 1)\n"); + ctx.output += QStringLiteral("struct %1 {\n").arg(typeName); + + emitStructBody(ctx, structId); + + ctx.output += QStringLiteral("};\n"); + ctx.output += QStringLiteral("#pragma pack(pop)\n"); + ctx.output += QStringLiteral("static_assert(sizeof(%1) == 0x%2, \"Size mismatch for %1\");\n\n") + .arg(typeName) + .arg(QString::number(structSize, 16).toUpper()); + + ctx.visiting.remove(structId); +} + +// ── Build the child map used by all generators ── + +static QHash> buildChildMap(const NodeTree& tree) { + QHash> map; + for (int i = 0; i < tree.nodes.size(); i++) + map[tree.nodes[i].parentId].append(i); + return map; +} + +// ── Path breadcrumb for header comment ── + +static QString nodePath(const NodeTree& tree, uint64_t nodeId) { + QStringList parts; + QSet seen; + uint64_t cur = nodeId; + while (cur != 0 && !seen.contains(cur)) { + seen.insert(cur); + int idx = tree.indexOfId(cur); + if (idx < 0) break; + const Node& n = tree.nodes[idx]; + parts << (n.name.isEmpty() ? QStringLiteral("") : n.name); + cur = n.parentId; + } + std::reverse(parts.begin(), parts.end()); + return parts.join(QStringLiteral(" > ")); +} + +} // anonymous namespace + +// ── Public API ── + +QString renderCpp(const NodeTree& tree, uint64_t rootStructId) { + int idx = tree.indexOfId(rootStructId); + if (idx < 0) return {}; + + const Node& root = tree.nodes[idx]; + if (root.kind != NodeKind::Struct) return {}; + + GenContext ctx{tree, buildChildMap(tree), {}, {}, {}, {}, {}, 0}; + int rootSize = tree.structSpan(rootStructId, &ctx.childMap); + QString typeName = ctx.structName(root); + + ctx.output += QStringLiteral("// Generated by ReclassX\n"); + ctx.output += QStringLiteral("// Rendered from: %1 (id=0x%2, size=0x%3)\n\n") + .arg(nodePath(tree, rootStructId)) + .arg(QString::number(rootStructId, 16).toUpper()) + .arg(QString::number(rootSize, 16).toUpper()); + ctx.output += QStringLiteral("#pragma once\n"); + ctx.output += QStringLiteral("#include \n\n"); + + emitStruct(ctx, rootStructId); + + return ctx.output; +} + +QString renderCppAll(const NodeTree& tree) { + GenContext ctx{tree, buildChildMap(tree), {}, {}, {}, {}, {}, 0}; + + ctx.output += QStringLiteral("// Generated by ReclassX\n"); + ctx.output += QStringLiteral("// Full SDK export\n\n"); + ctx.output += QStringLiteral("#pragma once\n"); + ctx.output += QStringLiteral("#include \n\n"); + + QVector roots = ctx.childMap.value(0); + std::sort(roots.begin(), roots.end(), [&](int a, int b) { + return tree.nodes[a].offset < tree.nodes[b].offset; + }); + + for (int ri : roots) { + if (tree.nodes[ri].kind == NodeKind::Struct) + emitStruct(ctx, tree.nodes[ri].id); + } + + return ctx.output; +} + +QString renderNull(const NodeTree&, uint64_t) { + return {}; +} + +} // namespace rcx diff --git a/src/generator.h b/src/generator.h new file mode 100644 index 0000000..6928855 --- /dev/null +++ b/src/generator.h @@ -0,0 +1,18 @@ +#pragma once +#include "core.h" +#include +#include + +namespace rcx { + +// Generate C++ struct definitions for a single root struct and all +// nested/referenced types reachable from it. +QString renderCpp(const NodeTree& tree, uint64_t rootStructId); + +// Generate C++ struct definitions for every root-level struct (full SDK). +QString renderCppAll(const NodeTree& tree); + +// Null generator placeholder (returns empty string). +QString renderNull(const NodeTree& tree, uint64_t rootStructId); + +} // namespace rcx diff --git a/src/main.cpp b/src/main.cpp index 5d0c705..db78307 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,4 +1,5 @@ #include "controller.h" +#include "generator.h" #include #include #include @@ -8,6 +9,8 @@ #include #include #include +#include +#include #include #include #include @@ -21,6 +24,8 @@ #include #include #include +#include +#include #ifdef _WIN32 #include @@ -122,18 +127,29 @@ private slots: void redo(); void about(); void setEditorFont(const QString& fontName); + void exportCpp(); private: + enum ViewMode { VM_Reclass, VM_Rendered }; + QMdiArea* m_mdiArea; QLabel* m_statusLabel; struct TabState { - RcxDocument* doc; - RcxController* ctrl; - QSplitter* splitter; + RcxDocument* doc; + RcxController* ctrl; + QSplitter* splitter; + QStackedWidget* stack = nullptr; + QPointer rendered; + ViewMode viewMode = VM_Reclass; + uint64_t lastRenderedRootId = 0; + int lastRenderedFirstLine = 0; }; QMap m_tabs; + QAction* m_actViewReclass = nullptr; + QAction* m_actViewRendered = nullptr; + void createMenus(); void createStatusBar(); QIcon makeIcon(const QString& svgPath); @@ -142,6 +158,12 @@ private: TabState* activeTab(); QMdiSubWindow* createTab(RcxDocument* doc); void updateWindowTitle(); + + void setViewMode(ViewMode mode); + void updateRenderedView(TabState& tab); + void syncRenderMenuState(); + uint64_t findRootStructForNode(const NodeTree& tree, uint64_t nodeId) const; + void setupRenderedSci(QsciScintilla* sci); }; MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) { @@ -158,7 +180,10 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) { createStatusBar(); connect(m_mdiArea, &QMdiArea::subWindowActivated, - this, [this](QMdiSubWindow*) { updateWindowTitle(); }); + this, [this](QMdiSubWindow*) { + updateWindowTitle(); + syncRenderMenuState(); + }); } QIcon MainWindow::makeIcon(const QString& svgPath) { @@ -191,6 +216,8 @@ void MainWindow::createMenus() { file->addSeparator(); file->addAction(makeIcon(":/vsicons/file-binary.svg"), "Load &Binary...", this, &MainWindow::loadBinary); file->addSeparator(); + file->addAction(makeIcon(":/vsicons/export.svg"), "Export &C++ Header...", this, &MainWindow::exportCpp); + file->addSeparator(); file->addAction(makeIcon(":/vsicons/close.svg"), "E&xit", QKeySequence(Qt::Key_Close), this, &QMainWindow::close); // Edit @@ -220,6 +247,10 @@ void MainWindow::createMenus() { connect(actConsolas, &QAction::triggered, this, [this]() { setEditorFont("Consolas"); }); connect(actIosevka, &QAction::triggered, this, [this]() { setEditorFont("Iosevka"); }); + view->addSeparator(); + m_actViewRendered = view->addAction(makeIcon(":/vsicons/code.svg"), "&C/C++", this, [this]() { setViewMode(VM_Rendered); }); + m_actViewReclass = view->addAction(makeIcon(":/vsicons/eye.svg"), "&Reclass View", this, [this]() { setViewMode(VM_Reclass); }); + // Node auto* node = menuBar()->addMenu("&Node"); node->addAction(makeIcon(":/vsicons/add.svg"), "&Add Field", QKeySequence(Qt::Key_Insert), this, &MainWindow::addNode); @@ -240,17 +271,27 @@ void MainWindow::createStatusBar() { } QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) { + // QStackedWidget wraps [0] splitter (Reclass view) and [1] rendered QsciScintilla + auto* stack = new QStackedWidget; auto* splitter = new QSplitter(Qt::Horizontal); auto* ctrl = new RcxController(doc, splitter); ctrl->addSplitEditor(splitter); - auto* sub = m_mdiArea->addSubWindow(splitter); + stack->addWidget(splitter); // index 0 = Reclass view + + auto* renderedSci = new QsciScintilla; + setupRenderedSci(renderedSci); + stack->addWidget(renderedSci); // index 1 = Rendered view + stack->setCurrentIndex(0); + + auto* sub = m_mdiArea->addSubWindow(stack); sub->setWindowTitle(doc->filePath.isEmpty() ? "Untitled" : QFileInfo(doc->filePath).fileName()); sub->setAttribute(Qt::WA_DeleteOnClose); sub->showMaximized(); - m_tabs[sub] = { doc, ctrl, splitter }; + m_tabs[sub] = { doc, ctrl, splitter, stack, renderedSci, + VM_Reclass, 0, 0 }; connect(sub, &QObject::destroyed, this, [this, sub]() { auto it = m_tabs.find(sub); @@ -261,18 +302,29 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) { }); connect(ctrl, &RcxController::nodeSelected, - this, [this, ctrl](int nodeIdx) { + this, [this, ctrl, sub](int nodeIdx) { if (nodeIdx >= 0 && nodeIdx < ctrl->document()->tree.nodes.size()) { auto& node = ctrl->document()->tree.nodes[nodeIdx]; - m_statusLabel->setText( - QString("%1 %2 offset: 0x%3 size: %4 bytes") - .arg(kindToString(node.kind)) - .arg(node.name) - .arg(node.offset, 4, 16, QChar('0')) - .arg(node.byteSize())); + auto it = m_tabs.find(sub); + if (it != m_tabs.end() && it->viewMode == VM_Rendered) + m_statusLabel->setText( + QString("Rendered: %1 %2") + .arg(kindToString(node.kind)) + .arg(node.name)); + else + m_statusLabel->setText( + QString("%1 %2 offset: 0x%3 size: %4 bytes") + .arg(kindToString(node.kind)) + .arg(node.name) + .arg(node.offset, 4, 16, QChar('0')) + .arg(node.byteSize())); } else { m_statusLabel->setText("Ready"); } + // Update rendered view on selection change + auto it = m_tabs.find(sub); + if (it != m_tabs.end()) + updateRenderedView(*it); }); connect(ctrl, &RcxController::selectionChanged, this, [this](int count) { @@ -282,6 +334,26 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) { m_statusLabel->setText(QString("%1 nodes selected").arg(count)); }); + // Update rendered view on document changes and undo/redo + connect(doc, &RcxDocument::documentChanged, + this, [this, sub]() { + auto it = m_tabs.find(sub); + if (it != m_tabs.end()) + QTimer::singleShot(0, this, [this, sub]() { + auto it2 = m_tabs.find(sub); + if (it2 != m_tabs.end()) updateRenderedView(*it2); + }); + }); + connect(&doc->undoStack, &QUndoStack::indexChanged, + this, [this, sub](int) { + auto it = m_tabs.find(sub); + if (it != m_tabs.end()) + QTimer::singleShot(0, this, [this, sub]() { + auto it2 = m_tabs.find(sub); + if (it2 != m_tabs.end()) updateRenderedView(*it2); + }); + }); + ctrl->refresh(); return sub; } @@ -289,314 +361,23 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) { void MainWindow::newFile() { auto* doc = new RcxDocument(this); - // ══════════════════════════════════════════════════════════════════════════ - // _PEB64 Demo — Process Environment Block + stub structs - // Buffer covers PEB (0x7D0) + _PEB_LDR_DATA (0x800) + _RTL_USER_PROCESS_PARAMETERS (0x900) - // ══════════════════════════════════════════════════════════════════════════ + QByteArray data(16, '\0'); + doc->loadData(data); + doc->tree.baseAddress = 0x00400000; - QByteArray pebData(0x940, '\0'); - char* d = pebData.data(); + Node root; + root.kind = NodeKind::Struct; + root.name = "Entity"; + root.structTypeName = "Entity"; + root.parentId = 0; + root.offset = 0; + int ri = doc->tree.addNode(root); + uint64_t rootId = doc->tree.nodes[ri].id; - auto w8 = [&](int off, uint8_t v) { d[off] = (char)v; }; - auto w16 = [&](int off, uint16_t v) { memcpy(d+off, &v, 2); }; - auto w32 = [&](int off, uint32_t v) { memcpy(d+off, &v, 4); }; - auto w64 = [&](int off, uint64_t v) { memcpy(d+off, &v, 8); }; - - w8 (0x002, 1); // BeingDebugged - w8 (0x003, 0x04); // BitField - w64(0x008, 0xFFFFFFFFFFFFFFFFULL); // Mutant (-1) - w64(0x010, 0x00007FF6DE120000ULL); // ImageBaseAddress - w64(0x018, 0x000000D87B5E5800ULL); // Ldr (baseAddress + 0x800) - w64(0x020, 0x000000D87B5E5900ULL); // ProcessParameters (baseAddress + 0x900) - w64(0x030, 0x000001A4C3D40000ULL); // ProcessHeap - w64(0x038, 0x00007FFE3B8D4260ULL); // FastPebLock - w32(0x050, 0x01); // CrossProcessFlags - w64(0x058, 0x00007FFE3B720000ULL); // KernelCallbackTable - w64(0x068, 0x00007FFE3E570000ULL); // ApiSetMap - w64(0x078, 0x00007FFE3B8D3F50ULL); // TlsBitmap - w32(0x080, 0x00000003); // TlsBitmapBits[0] - w64(0x088, 0x00007FFE38800000ULL); // ReadOnlySharedMemoryBase - w64(0x090, 0x00007FFE38820000ULL); // SharedData - w64(0x0A0, 0x00007FFE3B8D1000ULL); // AnsiCodePageData - w64(0x0A8, 0x00007FFE3B8D2040ULL); // OemCodePageData - w64(0x0B0, 0x00007FFE3B8CE020ULL); // UnicodeCaseTableData - w32(0x0B8, 8); // NumberOfProcessors - w32(0x0BC, 0x70); // NtGlobalFlag - w64(0x0C0, 0xFFFFFFFF7C91E000ULL); // CriticalSectionTimeout - w64(0x0C8, 0x0000000000100000ULL); // HeapSegmentReserve - w64(0x0D0, 0x0000000000002000ULL); // HeapSegmentCommit - w32(0x0E8, 4); // NumberOfHeaps - w32(0x0EC, 16); // MaximumNumberOfHeaps - w64(0x0F0, 0x000001A4C3D40688ULL); // ProcessHeaps - w64(0x0F8, 0x00007FFE388B0000ULL); // GdiSharedHandleTable - w64(0x110, 0x00007FFE3B8D42E8ULL); // LoaderLock - w32(0x118, 10); // OSMajorVersion - w16(0x120, 19045); // OSBuildNumber - w32(0x124, 2); // OSPlatformId - w32(0x128, 3); // ImageSubsystem (CUI) - w32(0x12C, 10); // ImageSubsystemMajorVersion - w64(0x138, 0x00000000000000FFULL); // ActiveProcessAffinityMask - w64(0x238, 0x00007FFE3B8D3F70ULL); // TlsExpansionBitmap - w32(0x2C0, 1); // SessionId - w64(0x2F8, 0x000001A4C3E21000ULL); // ActivationContextData - w64(0x308, 0x00007FFE38840000ULL); // SystemDefaultActivationContextData - w64(0x318, 0x0000000000002000ULL); // MinimumStackCommit - w16(0x34C, 1252); // ActiveCodePage - w16(0x34E, 437); // OemCodePage - w64(0x358, 0x000001A4C3E30000ULL); // WerRegistrationData - w64(0x380, 0x00007FFE38890000ULL); // CsrServerReadOnlySharedMemoryBase - w64(0x390, 0x000000D87B5E5390ULL); // TppWorkerpList.Flink (self) - w64(0x398, 0x000000D87B5E5390ULL); // TppWorkerpList.Blink (self) - w64(0x7B8, 0x00007FFE38860000ULL); // LeapSecondData - - // ── _PEB_LDR_DATA at offset 0x800 ── - w32(0x800, 0x48); // Length - w8 (0x804, 0x01); // Initialized - w64(0x808, 0x0000000000000000ULL); // SsHandle - w64(0x810, 0x000001A4C3D40100ULL); // InLoadOrderModuleList.Flink - w64(0x818, 0x000001A4C3D40200ULL); // InLoadOrderModuleList.Blink - w64(0x820, 0x000001A4C3D40110ULL); // InMemoryOrderModuleList.Flink - w64(0x828, 0x000001A4C3D40210ULL); // InMemoryOrderModuleList.Blink - - // ── _RTL_USER_PROCESS_PARAMETERS at offset 0x900 ── - w32(0x900, 0x07B0); // MaximumLength - w32(0x904, 0x07B0); // Length - w32(0x908, 0x0001); // Flags (NORMALIZED) - w32(0x90C, 0x0000); // DebugFlags - w64(0x910, 0x0000000000000044ULL); // ConsoleHandle - w32(0x918, 0x0000); // ConsoleFlags - w64(0x920, 0x0000000000000008ULL); // StandardInput - w64(0x928, 0x000000000000000CULL); // StandardOutput - w64(0x930, 0x0000000000000010ULL); // StandardError - - doc->loadData(pebData); - doc->tree.baseAddress = 0x000000D87B5E5000ULL; - - // ══════════════════════════════════════════════════════════════════════════ - // Build _PEB64 Node Tree (0x7D0 bytes, unions mapped to first member) - // ══════════════════════════════════════════════════════════════════════════ - - auto addField = [&](uint64_t parent, int offset, NodeKind kind, const QString& name) -> uint64_t { - Node n; n.kind = kind; n.name = name; - n.parentId = parent; n.offset = offset; - int idx = doc->tree.addNode(n); - return doc->tree.nodes[idx].id; - }; - auto addPad = [&](uint64_t parent, int offset, int len, const QString& name) { - Node n; n.kind = NodeKind::Padding; n.name = name; - n.parentId = parent; n.offset = offset; n.arrayLen = len; - doc->tree.addNode(n); - }; - auto addStruct = [&](uint64_t parent, int offset, const QString& typeName, const QString& name, bool collapse = true) -> uint64_t { - Node n; n.kind = NodeKind::Struct; - n.structTypeName = typeName; n.name = name; - n.parentId = parent; n.offset = offset; n.collapsed = collapse; - int idx = doc->tree.addNode(n); - return doc->tree.nodes[idx].id; - }; - auto addArray = [&](uint64_t parent, int offset, const QString& name, int count, NodeKind elemKind) { - Node n; n.kind = NodeKind::Array; n.name = name; - n.parentId = parent; n.offset = offset; - n.arrayLen = count; n.elementKind = elemKind; - n.collapsed = true; - int idx = doc->tree.addNode(n); - uint64_t arrId = doc->tree.nodes[idx].id; - int elemSz = sizeForKind(elemKind); - if (elemSz > 0) { - for (int i = 0; i < count; i++) { - Node e; e.kind = elemKind; - e.name = QStringLiteral("[%1]").arg(i); - e.parentId = arrId; e.offset = i * elemSz; - doc->tree.addNode(e); - } - } - }; - - // Root struct (not collapsed so fields are visible on open) - uint64_t peb = addStruct(0, 0, "_PEB64", "Peb", false); - - // 0x000 – 0x007 - addField(peb, 0x000, NodeKind::UInt8, "InheritedAddressSpace"); - addField(peb, 0x001, NodeKind::UInt8, "ReadImageFileExecOptions"); - addField(peb, 0x002, NodeKind::UInt8, "BeingDebugged"); - addField(peb, 0x003, NodeKind::UInt8, "BitField"); - addPad (peb, 0x004, 4, "Padding0"); - - // 0x008 – 0x04F - addField(peb, 0x008, NodeKind::Pointer64, "Mutant"); - addField(peb, 0x010, NodeKind::Pointer64, "ImageBaseAddress"); - uint64_t ldrPtrId = addField(peb, 0x018, NodeKind::Pointer64, "Ldr"); - uint64_t ppPtrId = addField(peb, 0x020, NodeKind::Pointer64, "ProcessParameters"); - addField(peb, 0x028, NodeKind::Pointer64, "SubSystemData"); - addField(peb, 0x030, NodeKind::Pointer64, "ProcessHeap"); - addField(peb, 0x038, NodeKind::Pointer64, "FastPebLock"); - addField(peb, 0x040, NodeKind::Pointer64, "AtlThunkSListPtr"); - addField(peb, 0x048, NodeKind::Pointer64, "IFEOKey"); - - // 0x050 – 0x07F - addField(peb, 0x050, NodeKind::UInt32, "CrossProcessFlags"); - addPad (peb, 0x054, 4, "Padding1"); - addField(peb, 0x058, NodeKind::Pointer64, "KernelCallbackTable"); - addField(peb, 0x060, NodeKind::UInt32, "SystemReserved"); - addField(peb, 0x064, NodeKind::UInt32, "AtlThunkSListPtr32"); - addField(peb, 0x068, NodeKind::Pointer64, "ApiSetMap"); - addField(peb, 0x070, NodeKind::UInt32, "TlsExpansionCounter"); - addPad (peb, 0x074, 4, "Padding2"); - addField(peb, 0x078, NodeKind::Pointer64, "TlsBitmap"); - addArray(peb, 0x080, "TlsBitmapBits", 2, NodeKind::UInt32); - - // 0x088 – 0x0BF - addField(peb, 0x088, NodeKind::Pointer64, "ReadOnlySharedMemoryBase"); - addField(peb, 0x090, NodeKind::Pointer64, "SharedData"); - addField(peb, 0x098, NodeKind::Pointer64, "ReadOnlyStaticServerData"); - addField(peb, 0x0A0, NodeKind::Pointer64, "AnsiCodePageData"); - addField(peb, 0x0A8, NodeKind::Pointer64, "OemCodePageData"); - addField(peb, 0x0B0, NodeKind::Pointer64, "UnicodeCaseTableData"); - addField(peb, 0x0B8, NodeKind::UInt32, "NumberOfProcessors"); - addField(peb, 0x0BC, NodeKind::Hex32, "NtGlobalFlag"); - - // 0x0C0 – 0x0EF - addField(peb, 0x0C0, NodeKind::UInt64, "CriticalSectionTimeout"); - addField(peb, 0x0C8, NodeKind::UInt64, "HeapSegmentReserve"); - addField(peb, 0x0D0, NodeKind::UInt64, "HeapSegmentCommit"); - addField(peb, 0x0D8, NodeKind::UInt64, "HeapDeCommitTotalFreeThreshold"); - addField(peb, 0x0E0, NodeKind::UInt64, "HeapDeCommitFreeBlockThreshold"); - addField(peb, 0x0E8, NodeKind::UInt32, "NumberOfHeaps"); - addField(peb, 0x0EC, NodeKind::UInt32, "MaximumNumberOfHeaps"); - - // 0x0F0 – 0x13F - addField(peb, 0x0F0, NodeKind::Pointer64, "ProcessHeaps"); - addField(peb, 0x0F8, NodeKind::Pointer64, "GdiSharedHandleTable"); - addField(peb, 0x100, NodeKind::Pointer64, "ProcessStarterHelper"); - addField(peb, 0x108, NodeKind::UInt32, "GdiDCAttributeList"); - addPad (peb, 0x10C, 4, "Padding3"); - addField(peb, 0x110, NodeKind::Pointer64, "LoaderLock"); - addField(peb, 0x118, NodeKind::UInt32, "OSMajorVersion"); - addField(peb, 0x11C, NodeKind::UInt32, "OSMinorVersion"); - addField(peb, 0x120, NodeKind::UInt16, "OSBuildNumber"); - addField(peb, 0x122, NodeKind::UInt16, "OSCSDVersion"); - addField(peb, 0x124, NodeKind::UInt32, "OSPlatformId"); - addField(peb, 0x128, NodeKind::UInt32, "ImageSubsystem"); - addField(peb, 0x12C, NodeKind::UInt32, "ImageSubsystemMajorVersion"); - addField(peb, 0x130, NodeKind::UInt32, "ImageSubsystemMinorVersion"); - addPad (peb, 0x134, 4, "Padding4"); - addField(peb, 0x138, NodeKind::UInt64, "ActiveProcessAffinityMask"); - - // 0x140 – 0x22F - addArray(peb, 0x140, "GdiHandleBuffer", 60, NodeKind::UInt32); - - // 0x230 – 0x2BF - addField(peb, 0x230, NodeKind::Pointer64, "PostProcessInitRoutine"); - addField(peb, 0x238, NodeKind::Pointer64, "TlsExpansionBitmap"); - addArray(peb, 0x240, "TlsExpansionBitmapBits", 32, NodeKind::UInt32); - - // 0x2C0 – 0x2E7 - addField(peb, 0x2C0, NodeKind::UInt32, "SessionId"); - addPad (peb, 0x2C4, 4, "Padding5"); - addField(peb, 0x2C8, NodeKind::UInt64, "AppCompatFlags"); - addField(peb, 0x2D0, NodeKind::UInt64, "AppCompatFlagsUser"); - addField(peb, 0x2D8, NodeKind::Pointer64, "pShimData"); - addField(peb, 0x2E0, NodeKind::Pointer64, "AppCompatInfo"); - - // 0x2E8 – 0x2F7: _STRING64 CSDVersion - { - uint64_t sid = addStruct(peb, 0x2E8, "_STRING64", "CSDVersion"); - addField(sid, 0, NodeKind::UInt16, "Length"); - addField(sid, 2, NodeKind::UInt16, "MaximumLength"); - addPad (sid, 4, 4, "Pad"); - addField(sid, 8, NodeKind::Pointer64, "Buffer"); - } - - // 0x2F8 – 0x31F - addField(peb, 0x2F8, NodeKind::Pointer64, "ActivationContextData"); - addField(peb, 0x300, NodeKind::Pointer64, "ProcessAssemblyStorageMap"); - addField(peb, 0x308, NodeKind::Pointer64, "SystemDefaultActivationContextData"); - addField(peb, 0x310, NodeKind::Pointer64, "SystemAssemblyStorageMap"); - addField(peb, 0x318, NodeKind::UInt64, "MinimumStackCommit"); - - // 0x320 – 0x34B - addArray(peb, 0x320, "SparePointers", 2, NodeKind::UInt64); - addField(peb, 0x330, NodeKind::Pointer64, "PatchLoaderData"); - addField(peb, 0x338, NodeKind::Pointer64, "ChpeV2ProcessInfo"); - addField(peb, 0x340, NodeKind::UInt32, "AppModelFeatureState"); - addArray(peb, 0x344, "SpareUlongs", 2, NodeKind::UInt32); - addField(peb, 0x34C, NodeKind::UInt16, "ActiveCodePage"); - addField(peb, 0x34E, NodeKind::UInt16, "OemCodePage"); - addField(peb, 0x350, NodeKind::UInt16, "UseCaseMapping"); - addField(peb, 0x352, NodeKind::UInt16, "UnusedNlsField"); - - // 0x354 – 0x37F - addPad (peb, 0x354, 4, "Pad354"); - addField(peb, 0x358, NodeKind::Pointer64, "WerRegistrationData"); - addField(peb, 0x360, NodeKind::Pointer64, "WerShipAssertPtr"); - addField(peb, 0x368, NodeKind::Pointer64, "EcCodeBitMap"); - addField(peb, 0x370, NodeKind::Pointer64, "pImageHeaderHash"); - addField(peb, 0x378, NodeKind::UInt32, "TracingFlags"); - addPad (peb, 0x37C, 4, "Padding6"); - - // 0x380 – 0x39F - addField(peb, 0x380, NodeKind::Pointer64, "CsrServerReadOnlySharedMemoryBase"); - addField(peb, 0x388, NodeKind::UInt64, "TppWorkerpListLock"); - - // LIST_ENTRY64 TppWorkerpList - { - uint64_t sid = addStruct(peb, 0x390, "LIST_ENTRY64", "TppWorkerpList"); - addField(sid, 0, NodeKind::Pointer64, "Flink"); - addField(sid, 8, NodeKind::Pointer64, "Blink"); - } - - // 0x3A0 – 0x79F - addArray(peb, 0x3A0, "WaitOnAddressHashTable", 128, NodeKind::UInt64); - - // 0x7A0 – 0x7CF - addField(peb, 0x7A0, NodeKind::Pointer64, "TelemetryCoverageHeader"); - addField(peb, 0x7A8, NodeKind::UInt32, "CloudFileFlags"); - addField(peb, 0x7AC, NodeKind::UInt32, "CloudFileDiagFlags"); - addField(peb, 0x7B0, NodeKind::Int8, "PlaceholderCompatibilityMode"); - addArray(peb, 0x7B1, "PlaceholderCompatibilityModeReserved", 7, NodeKind::Int8); - addField(peb, 0x7B8, NodeKind::Pointer64, "LeapSecondData"); - addField(peb, 0x7C0, NodeKind::UInt32, "LeapSecondFlags"); - addField(peb, 0x7C4, NodeKind::UInt32, "NtGlobalFlag2"); - addField(peb, 0x7C8, NodeKind::UInt64, "ExtendedFeatureDisableMask"); - - // ── Stub structs for pointer deref demo ── - // _PEB_LDR_DATA (Ldr target) - uint64_t ldrData = addStruct(0, 0x800, "_PEB_LDR_DATA", "LdrData"); - addField(ldrData, 0x00, NodeKind::UInt32, "Length"); - addField(ldrData, 0x04, NodeKind::UInt8, "Initialized"); - addPad (ldrData, 0x05, 3, "Pad"); - addField(ldrData, 0x08, NodeKind::Pointer64, "SsHandle"); - { - uint64_t le = addStruct(ldrData, 0x10, "LIST_ENTRY64", "InLoadOrderModuleList"); - addField(le, 0, NodeKind::Pointer64, "Flink"); - addField(le, 8, NodeKind::Pointer64, "Blink"); - } - { - uint64_t le = addStruct(ldrData, 0x20, "LIST_ENTRY64", "InMemoryOrderModuleList"); - addField(le, 0, NodeKind::Pointer64, "Flink"); - addField(le, 8, NodeKind::Pointer64, "Blink"); - } - - // _RTL_USER_PROCESS_PARAMETERS (ProcessParameters target) - uint64_t procParams = addStruct(0, 0x900, "_RTL_USER_PROCESS_PARAMETERS", "ProcessParams"); - addField(procParams, 0x00, NodeKind::UInt32, "MaximumLength"); - addField(procParams, 0x04, NodeKind::UInt32, "Length"); - addField(procParams, 0x08, NodeKind::UInt32, "Flags"); - addField(procParams, 0x0C, NodeKind::UInt32, "DebugFlags"); - addField(procParams, 0x10, NodeKind::Pointer64, "ConsoleHandle"); - addField(procParams, 0x18, NodeKind::UInt32, "ConsoleFlags"); - addPad (procParams, 0x1C, 4, "Pad"); - addField(procParams, 0x20, NodeKind::Pointer64, "StandardInput"); - addField(procParams, 0x28, NodeKind::Pointer64, "StandardOutput"); - addField(procParams, 0x30, NodeKind::Pointer64, "StandardError"); - - // Wire up pointer refIds - { - int li = doc->tree.indexOfId(ldrPtrId); - if (li >= 0) doc->tree.nodes[li].refId = ldrData; - int pi = doc->tree.indexOfId(ppPtrId); - if (pi >= 0) doc->tree.nodes[pi].refId = procParams; - } + { Node n; n.kind = NodeKind::Int32; n.name = "health"; n.parentId = rootId; n.offset = 0; doc->tree.addNode(n); } + { Node n; n.kind = NodeKind::Int32; n.name = "armor"; n.parentId = rootId; n.offset = 4; doc->tree.addNode(n); } + { Node n; n.kind = NodeKind::Float; n.name = "speed"; n.parentId = rootId; n.offset = 8; doc->tree.addNode(n); } + { Node n; n.kind = NodeKind::Hex32; n.name = "flags"; n.parentId = rootId; n.offset = 12; doc->tree.addNode(n); } createTab(doc); } @@ -735,9 +516,20 @@ void MainWindow::about() { void MainWindow::setEditorFont(const QString& fontName) { QSettings settings("ReclassX", "ReclassX"); settings.setValue("font", fontName); - // Notify all controllers to refresh fonts for (auto& state : m_tabs) { state.ctrl->setEditorFont(fontName); + // Also update the rendered view font + if (state.rendered) { + QFont f(fontName, 12); + f.setFixedPitch(true); + state.rendered->setFont(f); + if (auto* lex = state.rendered->lexer()) { + lex->setFont(f); + for (int i = 0; i <= 127; i++) + lex->setFont(f, i); + } + state.rendered->setMarginsFont(f); + } } } @@ -768,6 +560,170 @@ void MainWindow::updateWindowTitle() { } } +// ── Rendered view setup ── + +void MainWindow::setupRenderedSci(QsciScintilla* sci) { + QSettings settings("ReclassX", "ReclassX"); + QString fontName = settings.value("font", "Consolas").toString(); + QFont f(fontName, 12); + f.setFixedPitch(true); + + sci->setFont(f); + sci->setReadOnly(true); + sci->setWrapMode(QsciScintilla::WrapNone); + sci->setCaretLineVisible(false); + sci->setPaper(QColor("#1e1e1e")); + sci->setColor(QColor("#d4d4d4")); + sci->setTabWidth(4); + sci->setIndentationsUseTabs(false); + sci->setCaretForegroundColor(QColor("#d4d4d4")); + sci->SendScintilla(QsciScintillaBase::SCI_SETEXTRAASCENT, (long)2); + sci->SendScintilla(QsciScintillaBase::SCI_SETEXTRADESCENT, (long)2); + + // Line number margin + sci->setMarginType(0, QsciScintilla::NumberMargin); + sci->setMarginWidth(0, "00000"); + sci->setMarginsBackgroundColor(QColor("#252526")); + sci->setMarginsForegroundColor(QColor("#858585")); + sci->setMarginsFont(f); + + // Hide other margins + sci->setMarginWidth(1, 0); + sci->setMarginWidth(2, 0); + + // C++ lexer for syntax highlighting + auto* lexer = new QsciLexerCPP(sci); + lexer->setFont(f); + lexer->setColor(QColor("#569cd6"), QsciLexerCPP::Keyword); + lexer->setColor(QColor("#569cd6"), QsciLexerCPP::KeywordSet2); + lexer->setColor(QColor("#b5cea8"), QsciLexerCPP::Number); + lexer->setColor(QColor("#ce9178"), QsciLexerCPP::DoubleQuotedString); + lexer->setColor(QColor("#ce9178"), QsciLexerCPP::SingleQuotedString); + lexer->setColor(QColor("#6a9955"), QsciLexerCPP::Comment); + lexer->setColor(QColor("#6a9955"), QsciLexerCPP::CommentLine); + lexer->setColor(QColor("#6a9955"), QsciLexerCPP::CommentDoc); + lexer->setColor(QColor("#d4d4d4"), QsciLexerCPP::Default); + lexer->setColor(QColor("#d4d4d4"), QsciLexerCPP::Identifier); + lexer->setColor(QColor("#c586c0"), QsciLexerCPP::PreProcessor); + lexer->setColor(QColor("#d4d4d4"), QsciLexerCPP::Operator); + for (int i = 0; i <= 127; i++) { + lexer->setPaper(QColor("#1e1e1e"), i); + lexer->setFont(f, i); + } + sci->setLexer(lexer); + sci->setBraceMatching(QsciScintilla::NoBraceMatch); +} + +// ── View mode / generator switching ── + +void MainWindow::setViewMode(ViewMode mode) { + auto* tab = activeTab(); + if (!tab) return; + tab->viewMode = mode; + if (tab->stack) { + tab->stack->setCurrentIndex(mode == VM_Rendered ? 1 : 0); + } + if (mode == VM_Rendered) { + updateRenderedView(*tab); + } + syncRenderMenuState(); +} + +void MainWindow::syncRenderMenuState() { + auto* tab = activeTab(); + bool rendered = tab && tab->viewMode == VM_Rendered; + if (m_actViewRendered) m_actViewRendered->setEnabled(!rendered); + if (m_actViewReclass) m_actViewReclass->setEnabled(rendered); +} + +// ── Find the root-level struct ancestor for a node ── + +uint64_t MainWindow::findRootStructForNode(const NodeTree& tree, uint64_t nodeId) const { + QSet visited; + uint64_t cur = nodeId; + uint64_t lastStruct = 0; + while (cur != 0 && !visited.contains(cur)) { + visited.insert(cur); + int idx = tree.indexOfId(cur); + if (idx < 0) break; + const Node& n = tree.nodes[idx]; + if (n.kind == NodeKind::Struct) + lastStruct = n.id; + if (n.parentId == 0) + return (n.kind == NodeKind::Struct) ? n.id : lastStruct; + cur = n.parentId; + } + return lastStruct; +} + +// ── Update the rendered view for a tab ── + +void MainWindow::updateRenderedView(TabState& tab) { + if (tab.viewMode != VM_Rendered) return; + if (!tab.rendered) return; + + // Determine which struct to render based on selection + uint64_t rootId = 0; + QSet selIds = tab.ctrl->selectedIds(); + if (selIds.size() >= 1) { + uint64_t selId = *selIds.begin(); + selId &= ~kFooterIdBit; + rootId = findRootStructForNode(tab.doc->tree, selId); + } + + // Generate text + QString text; + if (rootId != 0) + text = renderCpp(tab.doc->tree, rootId); + else + text = renderCppAll(tab.doc->tree); + + // Scroll restoration: save if same root, reset if different + int restoreLine = 0; + if (rootId != 0 && rootId == tab.lastRenderedRootId) { + restoreLine = (int)tab.rendered->SendScintilla( + QsciScintillaBase::SCI_GETFIRSTVISIBLELINE); + } + tab.lastRenderedRootId = rootId; + + // Set text + tab.rendered->setReadOnly(false); + tab.rendered->setText(text); + tab.rendered->setReadOnly(true); + + // Update margin width for line count + int lineCount = tab.rendered->lines(); + QString marginStr = QString(QString::number(lineCount).size() + 2, '0'); + tab.rendered->setMarginWidth(0, marginStr); + + // Restore scroll + if (restoreLine > 0) { + tab.rendered->SendScintilla(QsciScintillaBase::SCI_SETFIRSTVISIBLELINE, + (unsigned long)restoreLine); + } +} + +// ── Export C++ header to file ── + +void MainWindow::exportCpp() { + auto* tab = activeTab(); + if (!tab) return; + + QString path = QFileDialog::getSaveFileName(this, + "Export C++ Header", {}, "C++ Header (*.h);;All Files (*)"); + if (path.isEmpty()) return; + + QString text = renderCppAll(tab->doc->tree); + QFile file(path); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { + QMessageBox::warning(this, "Export Failed", + "Could not write to: " + path); + return; + } + file.write(text.toUtf8()); + m_statusLabel->setText("Exported to " + QFileInfo(path).fileName()); +} + } // namespace rcx // ── Entry point ── diff --git a/src/resources.qrc b/src/resources.qrc index b520065..195de70 100644 --- a/src/resources.qrc +++ b/src/resources.qrc @@ -27,5 +27,9 @@ vsicons/files.svg vsicons/extensions.svg vsicons/question.svg + vsicons/eye.svg + vsicons/code.svg + vsicons/export.svg + vsicons/preview.svg diff --git a/tests/test_generator.cpp b/tests/test_generator.cpp new file mode 100644 index 0000000..451d8e8 --- /dev/null +++ b/tests/test_generator.cpp @@ -0,0 +1,671 @@ +#include +#include +#include +#include "core.h" +#include "generator.h" + +class TestGenerator : public QObject { + Q_OBJECT + +private: + // Helper: build a simple struct with a few fields + rcx::NodeTree makeSimpleStruct() { + rcx::NodeTree tree; + rcx::Node root; + root.kind = rcx::NodeKind::Struct; + root.name = "Player"; + root.structTypeName = "Player"; + root.parentId = 0; + root.offset = 0; + int ri = tree.addNode(root); + uint64_t rootId = tree.nodes[ri].id; + + rcx::Node f1; + f1.kind = rcx::NodeKind::Int32; + f1.name = "health"; + f1.parentId = rootId; + f1.offset = 0; + tree.addNode(f1); + + rcx::Node f2; + f2.kind = rcx::NodeKind::Float; + f2.name = "speed"; + f2.parentId = rootId; + f2.offset = 4; + tree.addNode(f2); + + rcx::Node f3; + f3.kind = rcx::NodeKind::UInt64; + f3.name = "id"; + f3.parentId = rootId; + f3.offset = 8; + tree.addNode(f3); + + return tree; + } + +private slots: + + // ── Basic struct generation ── + + void testSimpleStruct() { + auto tree = makeSimpleStruct(); + uint64_t rootId = tree.nodes[0].id; + QString result = rcx::renderCpp(tree, rootId); + + // Header + QVERIFY(result.contains("Generated by ReclassX")); + QVERIFY(result.contains("#pragma once")); + QVERIFY(result.contains("#include ")); + + // Struct definition + QVERIFY(result.contains("#pragma pack(push, 1)")); + QVERIFY(result.contains("struct Player {")); + QVERIFY(result.contains("int32_t health;")); + QVERIFY(result.contains("float speed;")); + QVERIFY(result.contains("uint64_t id;")); + QVERIFY(result.contains("};")); + QVERIFY(result.contains("#pragma pack(pop)")); + + // static_assert - struct is 16 bytes (0+4 + 4+4 + 8+8 = 16) + QVERIFY(result.contains("static_assert(sizeof(Player) == 0x10")); + } + + // ── Padding gap detection ── + + void testPaddingGaps() { + rcx::NodeTree tree; + rcx::Node root; + root.kind = rcx::NodeKind::Struct; + root.name = "GappyStruct"; + root.structTypeName = "GappyStruct"; + root.parentId = 0; + int ri = tree.addNode(root); + uint64_t rootId = tree.nodes[ri].id; + + // Field at offset 0, size 4 + rcx::Node f1; + f1.kind = rcx::NodeKind::UInt32; + f1.name = "a"; + f1.parentId = rootId; + f1.offset = 0; + tree.addNode(f1); + + // Field at offset 8, size 4 (gap of 4 bytes at offset 4) + rcx::Node f2; + f2.kind = rcx::NodeKind::UInt32; + f2.name = "b"; + f2.parentId = rootId; + f2.offset = 8; + tree.addNode(f2); + + QString result = rcx::renderCpp(tree, rootId); + + // Should contain a padding field between a and b + QVERIFY(result.contains("uint8_t _pad")); + QVERIFY(result.contains("[0x4]")); + QVERIFY(result.contains("uint32_t a;")); + QVERIFY(result.contains("uint32_t b;")); + } + + // ── Tail padding ── + + void testTailPadding() { + rcx::NodeTree tree; + rcx::Node root; + root.kind = rcx::NodeKind::Struct; + root.name = "TailPad"; + root.structTypeName = "TailPad"; + root.parentId = 0; + int ri = tree.addNode(root); + uint64_t rootId = tree.nodes[ri].id; + + // Only field at offset 0, size 1 + rcx::Node f1; + f1.kind = rcx::NodeKind::UInt8; + f1.name = "flag"; + f1.parentId = rootId; + f1.offset = 0; + tree.addNode(f1); + + // Add another field at offset 16 to make struct bigger + rcx::Node f2; + f2.kind = rcx::NodeKind::UInt8; + f2.name = "end"; + f2.parentId = rootId; + f2.offset = 16; + tree.addNode(f2); + + QString result = rcx::renderCpp(tree, rootId); + + // Gap between offset 1 and 16 = 15 bytes padding + QVERIFY(result.contains("[0xF]")); + // Total size = 17 + QVERIFY(result.contains("static_assert(sizeof(TailPad) == 0x11")); + } + + // ── Overlap warning ── + + void testOverlapWarning() { + rcx::NodeTree tree; + rcx::Node root; + root.kind = rcx::NodeKind::Struct; + root.name = "OverlapStruct"; + root.structTypeName = "OverlapStruct"; + root.parentId = 0; + int ri = tree.addNode(root); + uint64_t rootId = tree.nodes[ri].id; + + // Two fields that overlap: both at offset 0, size 8 and size 4 + rcx::Node f1; + f1.kind = rcx::NodeKind::UInt64; + f1.name = "wide"; + f1.parentId = rootId; + f1.offset = 0; + tree.addNode(f1); + + rcx::Node f2; + f2.kind = rcx::NodeKind::UInt32; + f2.name = "narrow"; + f2.parentId = rootId; + f2.offset = 4; // starts at 4, but wide ends at 8 => overlap + tree.addNode(f2); + + QString result = rcx::renderCpp(tree, rootId); + + // Should contain overlap warning + QVERIFY(result.contains("WARNING: overlap")); + } + + // ── Nested struct ── + + void testNestedStruct() { + rcx::NodeTree tree; + + // Outer struct + rcx::Node outer; + outer.kind = rcx::NodeKind::Struct; + outer.name = "Outer"; + outer.structTypeName = "Outer"; + outer.parentId = 0; + int oi = tree.addNode(outer); + uint64_t outerId = tree.nodes[oi].id; + + // Inner struct as child + rcx::Node inner; + inner.kind = rcx::NodeKind::Struct; + inner.name = "pos"; + inner.structTypeName = "Vec2f"; + inner.parentId = outerId; + inner.offset = 0; + int ii = tree.addNode(inner); + uint64_t innerId = tree.nodes[ii].id; + + // Inner fields + rcx::Node ix; + ix.kind = rcx::NodeKind::Float; + ix.name = "x"; + ix.parentId = innerId; + ix.offset = 0; + tree.addNode(ix); + + rcx::Node iy; + iy.kind = rcx::NodeKind::Float; + iy.name = "y"; + iy.parentId = innerId; + iy.offset = 4; + tree.addNode(iy); + + // Another field in outer after inner + rcx::Node f2; + f2.kind = rcx::NodeKind::Int32; + f2.name = "score"; + f2.parentId = outerId; + f2.offset = 8; + tree.addNode(f2); + + QString result = rcx::renderCpp(tree, outerId); + + // Inner struct should be defined before outer + int innerPos = result.indexOf("struct Vec2f {"); + int outerPos = result.indexOf("struct Outer {"); + QVERIFY(innerPos >= 0); + QVERIFY(outerPos >= 0); + QVERIFY(innerPos < outerPos); + + // Inner struct fields + QVERIFY(result.contains("float x;")); + QVERIFY(result.contains("float y;")); + QVERIFY(result.contains("static_assert(sizeof(Vec2f) == 0x8")); + + // Outer struct uses inner type + QVERIFY(result.contains("Vec2f pos;")); + QVERIFY(result.contains("int32_t score;")); + } + + // ── Primitive array ── + + void testPrimitiveArray() { + rcx::NodeTree tree; + rcx::Node root; + root.kind = rcx::NodeKind::Struct; + root.name = "WithArray"; + root.structTypeName = "WithArray"; + root.parentId = 0; + int ri = tree.addNode(root); + uint64_t rootId = tree.nodes[ri].id; + + rcx::Node arr; + arr.kind = rcx::NodeKind::Array; + arr.name = "data"; + arr.parentId = rootId; + arr.offset = 0; + arr.arrayLen = 16; + arr.elementKind = rcx::NodeKind::UInt32; + tree.addNode(arr); + + QString result = rcx::renderCpp(tree, rootId); + QVERIFY(result.contains("uint32_t data[16];")); + } + + // ── Pointer fields ── + + void testPointerFields() { + rcx::NodeTree tree; + + // Target struct (separate root) + rcx::Node target; + target.kind = rcx::NodeKind::Struct; + target.name = "Target"; + target.structTypeName = "TargetData"; + target.parentId = 0; + target.offset = 0x100; + int ti = tree.addNode(target); + uint64_t targetId = tree.nodes[ti].id; + + rcx::Node tf; + tf.kind = rcx::NodeKind::UInt32; + tf.name = "value"; + tf.parentId = targetId; + tf.offset = 0; + tree.addNode(tf); + + // Main struct with pointers + rcx::Node main; + main.kind = rcx::NodeKind::Struct; + main.name = "Main"; + main.structTypeName = "MainStruct"; + main.parentId = 0; + int mi = tree.addNode(main); + uint64_t mainId = tree.nodes[mi].id; + + // ptr64 with reference + rcx::Node p64; + p64.kind = rcx::NodeKind::Pointer64; + p64.name = "pTarget"; + p64.parentId = mainId; + p64.offset = 0; + p64.refId = targetId; + tree.addNode(p64); + + // ptr64 without reference + rcx::Node p64n; + p64n.kind = rcx::NodeKind::Pointer64; + p64n.name = "pVoid"; + p64n.parentId = mainId; + p64n.offset = 8; + tree.addNode(p64n); + + // ptr32 with reference + rcx::Node p32; + p32.kind = rcx::NodeKind::Pointer32; + p32.name = "pTarget32"; + p32.parentId = mainId; + p32.offset = 16; + p32.refId = targetId; + tree.addNode(p32); + + QString result = rcx::renderCpp(tree, mainId); + + // ptr64 with target → real C++ pointer + QVERIFY(result.contains("TargetData* pTarget;")); + // ptr64 without target → void* + QVERIFY(result.contains("void* pVoid;")); + // ptr32 with target → uint32_t with comment + QVERIFY(result.contains("uint32_t pTarget32;")); + QVERIFY(result.contains("-> TargetData*")); + // Forward declaration for TargetData + QVERIFY(result.contains("struct TargetData;")); + } + + // ── Vector and matrix types ── + + void testVectorTypes() { + rcx::NodeTree tree; + rcx::Node root; + root.kind = rcx::NodeKind::Struct; + root.name = "Vectors"; + root.structTypeName = "Vectors"; + root.parentId = 0; + int ri = tree.addNode(root); + uint64_t rootId = tree.nodes[ri].id; + + rcx::Node v2; + v2.kind = rcx::NodeKind::Vec2; + v2.name = "pos2d"; + v2.parentId = rootId; + v2.offset = 0; + tree.addNode(v2); + + rcx::Node v3; + v3.kind = rcx::NodeKind::Vec3; + v3.name = "pos3d"; + v3.parentId = rootId; + v3.offset = 8; + tree.addNode(v3); + + rcx::Node v4; + v4.kind = rcx::NodeKind::Vec4; + v4.name = "color"; + v4.parentId = rootId; + v4.offset = 20; + tree.addNode(v4); + + rcx::Node mat; + mat.kind = rcx::NodeKind::Mat4x4; + mat.name = "transform"; + mat.parentId = rootId; + mat.offset = 36; + tree.addNode(mat); + + QString result = rcx::renderCpp(tree, rootId); + + QVERIFY(result.contains("float pos2d[2];")); + QVERIFY(result.contains("float pos3d[3];")); + QVERIFY(result.contains("float color[4];")); + QVERIFY(result.contains("float transform[4][4];")); + } + + // ── String types ── + + void testStringTypes() { + rcx::NodeTree tree; + rcx::Node root; + root.kind = rcx::NodeKind::Struct; + root.name = "Strings"; + root.structTypeName = "Strings"; + root.parentId = 0; + int ri = tree.addNode(root); + uint64_t rootId = tree.nodes[ri].id; + + rcx::Node utf8; + utf8.kind = rcx::NodeKind::UTF8; + utf8.name = "name"; + utf8.parentId = rootId; + utf8.offset = 0; + utf8.strLen = 64; + tree.addNode(utf8); + + rcx::Node utf16; + utf16.kind = rcx::NodeKind::UTF16; + utf16.name = "wname"; + utf16.parentId = rootId; + utf16.offset = 64; + utf16.strLen = 32; + tree.addNode(utf16); + + QString result = rcx::renderCpp(tree, rootId); + + QVERIFY(result.contains("char name[64];")); + QVERIFY(result.contains("wchar_t wname[32];")); + } + + // ── Padding node ── + + void testPaddingNode() { + rcx::NodeTree tree; + rcx::Node root; + root.kind = rcx::NodeKind::Struct; + root.name = "PadTest"; + root.structTypeName = "PadTest"; + root.parentId = 0; + int ri = tree.addNode(root); + uint64_t rootId = tree.nodes[ri].id; + + rcx::Node pad; + pad.kind = rcx::NodeKind::Padding; + pad.name = "reserved"; + pad.parentId = rootId; + pad.offset = 0; + pad.arrayLen = 16; + tree.addNode(pad); + + QString result = rcx::renderCpp(tree, rootId); + QVERIFY(result.contains("uint8_t reserved[16];")); + } + + // ── Full SDK export (multiple root structs) ── + + void testFullSdkExport() { + rcx::NodeTree tree; + + // Struct A at offset 0 + rcx::Node a; + a.kind = rcx::NodeKind::Struct; + a.name = "StructA"; + a.structTypeName = "StructA"; + a.parentId = 0; + a.offset = 0; + int ai = tree.addNode(a); + uint64_t aId = tree.nodes[ai].id; + + rcx::Node af; + af.kind = rcx::NodeKind::UInt32; + af.name = "valueA"; + af.parentId = aId; + af.offset = 0; + tree.addNode(af); + + // Struct B at offset 0x100 + rcx::Node b; + b.kind = rcx::NodeKind::Struct; + b.name = "StructB"; + b.structTypeName = "StructB"; + b.parentId = 0; + b.offset = 0x100; + int bi = tree.addNode(b); + uint64_t bId = tree.nodes[bi].id; + + rcx::Node bf; + bf.kind = rcx::NodeKind::UInt64; + bf.name = "valueB"; + bf.parentId = bId; + bf.offset = 0; + tree.addNode(bf); + + QString result = rcx::renderCppAll(tree); + + QVERIFY(result.contains("Full SDK export")); + QVERIFY(result.contains("struct StructA {")); + QVERIFY(result.contains("struct StructB {")); + QVERIFY(result.contains("uint32_t valueA;")); + QVERIFY(result.contains("uint64_t valueB;")); + QVERIFY(result.contains("static_assert(sizeof(StructA) == 0x4")); + QVERIFY(result.contains("static_assert(sizeof(StructB) == 0x8")); + } + + // ── Null generator ── + + void testNullGenerator() { + auto tree = makeSimpleStruct(); + QString result = rcx::renderNull(tree, tree.nodes[0].id); + QVERIFY(result.isEmpty()); + } + + // ── Invalid root ID ── + + void testInvalidRootId() { + auto tree = makeSimpleStruct(); + QString result = rcx::renderCpp(tree, 9999); + QVERIFY(result.isEmpty()); + } + + // ── Non-struct root ── + + void testNonStructRoot() { + rcx::NodeTree tree; + rcx::Node n; + n.kind = rcx::NodeKind::UInt32; + n.name = "scalar"; + n.parentId = 0; + tree.addNode(n); + + QString result = rcx::renderCpp(tree, tree.nodes[0].id); + QVERIFY(result.isEmpty()); + } + + // ── Empty struct ── + + void testEmptyStruct() { + rcx::NodeTree tree; + rcx::Node root; + root.kind = rcx::NodeKind::Struct; + root.name = "Empty"; + root.structTypeName = "Empty"; + root.parentId = 0; + tree.addNode(root); + + QString result = rcx::renderCpp(tree, tree.nodes[0].id); + + QVERIFY(result.contains("struct Empty {")); + QVERIFY(result.contains("};")); + QVERIFY(result.contains("static_assert(sizeof(Empty) == 0x0")); + } + + // ── Name sanitization ── + + void testNameSanitization() { + rcx::NodeTree tree; + rcx::Node root; + root.kind = rcx::NodeKind::Struct; + root.name = "my struct-name"; + root.structTypeName = "my struct-name"; + root.parentId = 0; + int ri = tree.addNode(root); + uint64_t rootId = tree.nodes[ri].id; + + rcx::Node f; + f.kind = rcx::NodeKind::UInt32; + f.name = "field with spaces"; + f.parentId = rootId; + f.offset = 0; + tree.addNode(f); + + QString result = rcx::renderCpp(tree, rootId); + + // Spaces and dashes should be replaced with underscores + QVERIFY(result.contains("struct my_struct_name {")); + QVERIFY(result.contains("uint32_t field_with_spaces;")); + } + + // ── Export produces valid file content ── + + void testExportToFile() { + auto tree = makeSimpleStruct(); + uint64_t rootId = tree.nodes[0].id; + QString text = rcx::renderCpp(tree, rootId); + + QTemporaryFile tmpFile; + tmpFile.setAutoRemove(true); + QVERIFY(tmpFile.open()); + tmpFile.write(text.toUtf8()); + tmpFile.close(); + + // Read back and verify + QVERIFY(tmpFile.open()); + QByteArray readBack = tmpFile.readAll(); + tmpFile.close(); + + QString readStr = QString::fromUtf8(readBack); + QVERIFY(readStr.contains("#pragma once")); + QVERIFY(readStr.contains("struct Player {")); + QVERIFY(readStr.contains("static_assert")); + } + + // ── Full SDK with no structs (only primitives) ── + + void testFullSdkNoStructs() { + rcx::NodeTree tree; + rcx::Node n; + n.kind = rcx::NodeKind::UInt32; + n.name = "scalar"; + n.parentId = 0; + tree.addNode(n); + + QString result = rcx::renderCppAll(tree); + + // Header present but no struct definitions + QVERIFY(result.contains("#pragma once")); + QVERIFY(!result.contains("struct ")); + } + + // ── Deeply nested structs ── + + void testDeeplyNested() { + rcx::NodeTree tree; + + // A > B > C, each containing one field + rcx::Node a; + a.kind = rcx::NodeKind::Struct; + a.name = "A"; + a.structTypeName = "TypeA"; + a.parentId = 0; + int ai = tree.addNode(a); + uint64_t aId = tree.nodes[ai].id; + + rcx::Node b; + b.kind = rcx::NodeKind::Struct; + b.name = "b"; + b.structTypeName = "TypeB"; + b.parentId = aId; + b.offset = 0; + int bi = tree.addNode(b); + uint64_t bId = tree.nodes[bi].id; + + rcx::Node c; + c.kind = rcx::NodeKind::Struct; + c.name = "c"; + c.structTypeName = "TypeC"; + c.parentId = bId; + c.offset = 0; + int ci = tree.addNode(c); + uint64_t cId = tree.nodes[ci].id; + + rcx::Node leaf; + leaf.kind = rcx::NodeKind::UInt8; + leaf.name = "val"; + leaf.parentId = cId; + leaf.offset = 0; + tree.addNode(leaf); + + QString result = rcx::renderCpp(tree, aId); + + // TypeC defined first, then TypeB, then TypeA + int cPos = result.indexOf("struct TypeC {"); + int bPos = result.indexOf("struct TypeB {"); + int aPos = result.indexOf("struct TypeA {"); + QVERIFY(cPos >= 0); + QVERIFY(bPos >= 0); + QVERIFY(aPos >= 0); + QVERIFY(cPos < bPos); + QVERIFY(bPos < aPos); + + // TypeA contains TypeB, TypeB contains TypeC + QVERIFY(result.contains("TypeB b;")); + QVERIFY(result.contains("TypeC c;")); + QVERIFY(result.contains("uint8_t val;")); + } +}; + +QTEST_MAIN(TestGenerator) +#include "test_generator.moc" diff --git a/tests/test_validation.cpp b/tests/test_validation.cpp new file mode 100644 index 0000000..951db2f --- /dev/null +++ b/tests/test_validation.cpp @@ -0,0 +1,1160 @@ +// Stress tests for editor/controller validation: +// – Invalid values, boundary values, excessive inputs +// – Ensures no crashes and data integrity after rejected edits +// Skips: ASCII/byte preview editing (under discussion) + +#include +#include +#include +#include +#include +#include "controller.h" +#include "core.h" + +using namespace rcx; + +// ── Fixture: small tree with diverse field types ── + +static void buildValidationTree(NodeTree& tree) { + tree.baseAddress = 0x1000; + + Node root; + root.kind = NodeKind::Struct; + root.structTypeName = "TestStruct"; + root.name = "root"; + root.parentId = 0; + root.offset = 0; + int ri = tree.addNode(root); + uint64_t rootId = tree.nodes[ri].id; + + auto field = [&](int off, NodeKind k, const char* name) { + Node n; + n.kind = k; n.name = name; + n.parentId = rootId; n.offset = off; + tree.addNode(n); + }; + auto fieldArr = [&](int off, NodeKind ek, int count, const char* name) { + Node n; + n.kind = NodeKind::Array; n.name = name; + n.parentId = rootId; n.offset = off; + n.arrayLen = count; n.elementKind = ek; + tree.addNode(n); + }; + + field(0, NodeKind::Int8, "field_i8"); + field(1, NodeKind::UInt8, "field_u8"); + field(2, NodeKind::Int16, "field_i16"); + field(4, NodeKind::UInt16, "field_u16"); + field(6, NodeKind::Int32, "field_i32"); + field(10, NodeKind::UInt32, "field_u32"); + field(14, NodeKind::Int64, "field_i64"); + field(22, NodeKind::UInt64, "field_u64"); + field(30, NodeKind::Float, "field_float"); + field(34, NodeKind::Double, "field_dbl"); + field(42, NodeKind::Bool, "field_bool"); + field(43, NodeKind::Hex8, "field_h8"); + field(44, NodeKind::Hex16, "field_h16"); + field(46, NodeKind::Hex32, "field_h32"); + field(50, NodeKind::Hex64, "field_h64"); + field(58, NodeKind::Pointer64, "field_ptr"); + field(66, NodeKind::Padding, "pad0"); + tree.nodes.last().arrayLen = 6; + fieldArr(72, NodeKind::UInt32, 4, "field_arr"); +} + +static QByteArray makeValidationBuffer() { + QByteArray data(256, '\0'); + // i8 = -5 + data[0] = (char)(int8_t)-5; + // u8 = 0x42 + data[1] = 0x42; + // i16 = -1000 + int16_t i16v = -1000; + memcpy(data.data() + 2, &i16v, 2); + // u16 = 60000 + uint16_t u16v = 60000; + memcpy(data.data() + 4, &u16v, 2); + // i32 = -100000 + int32_t i32v = -100000; + memcpy(data.data() + 6, &i32v, 4); + // u32 = 0xDEADBEEF + uint32_t u32v = 0xDEADBEEF; + memcpy(data.data() + 10, &u32v, 4); + // i64 = -1 + int64_t i64v = -1; + memcpy(data.data() + 14, &i64v, 8); + // u64 = UINT64_MAX + uint64_t u64v = ~0ULL; + memcpy(data.data() + 22, &u64v, 8); + // float = 3.14f + float fv = 3.14f; + memcpy(data.data() + 30, &fv, 4); + // double = 2.718 + double dv = 2.718; + memcpy(data.data() + 34, &dv, 8); + // bool = 1 + data[42] = 1; + // hex8 = 0xAB + data[43] = (char)0xAB; + // hex16 = 0xCAFE + uint16_t h16 = 0xCAFE; + memcpy(data.data() + 44, &h16, 2); + // hex32 = 0xBAADF00D + uint32_t h32 = 0xBAADF00D; + memcpy(data.data() + 46, &h32, 4); + // hex64 = 0xDEADC0DEDEADBEEF + uint64_t h64 = 0xDEADC0DEDEADBEEFULL; + memcpy(data.data() + 50, &h64, 8); + // pointer = 0x7FFE3B8D4260 + uint64_t ptr = 0x00007FFE3B8D4260ULL; + memcpy(data.data() + 58, &ptr, 8); + return data; +} + +// ── Helper: find node index by name ── + +static int findNode(const NodeTree& tree, const char* name) { + for (int i = 0; i < tree.nodes.size(); i++) + if (tree.nodes[i].name == name) return i; + return -1; +} + +// ══════════════════════════════════════════════════════════════════════ +// Part 1: Pure unit tests – fmt::parseValue / fmt::validateValue +// These are mixed into TestValidationController so they all run under +// one QTEST_MAIN. The init()/cleanup() create GUI fixtures but the +// pure parsing tests simply don't use them. +// ══════════════════════════════════════════════════════════════════════ + +// (forward-declared — tests are added as slots of TestValidationController below) + +// ══════════════════════════════════════════════════════════════════════ +// Part 2: Controller-level stress tests (requires GUI) +// Tests that invalid inputs through the controller API don't corrupt data. +// ══════════════════════════════════════════════════════════════════════ + +class TestValidationController : public QObject { + Q_OBJECT +private: + RcxDocument* m_doc = nullptr; + RcxController* m_ctrl = nullptr; + QSplitter* m_splitter = nullptr; + RcxEditor* m_editor = nullptr; + + QByteArray snapshotProvider() { + return m_doc->provider->readBytes(m_doc->tree.baseAddress, + m_doc->provider->isReadable(m_doc->tree.baseAddress, 256) ? 256 : 0); + } + +private slots: + + void init() { + m_doc = new RcxDocument(); + buildValidationTree(m_doc->tree); + m_doc->provider = std::make_unique(makeValidationBuffer()); + + m_splitter = new QSplitter(); + m_ctrl = new RcxController(m_doc, nullptr); + m_editor = m_ctrl->addSplitEditor(m_splitter); + + m_splitter->resize(800, 600); + m_splitter->show(); + QVERIFY(QTest::qWaitForWindowExposed(m_splitter)); + QApplication::processEvents(); + } + + void cleanup() { + delete m_ctrl; m_ctrl = nullptr; m_editor = nullptr; + delete m_splitter; m_splitter = nullptr; + delete m_doc; m_doc = nullptr; + } + + // ════════════════════════════════════════════════════════ + // Pure parsing/validation tests (no GUI interaction) + // ════════════════════════════════════════════════════════ + + // ── Integer overflow: values that exceed type max ── + + void testInt8Overflow() { + bool ok; + // Max int8 = 127, min = -128 + fmt::parseValue(NodeKind::Int8, "128", &ok); + QVERIFY2(!ok, "128 overflows int8"); + + fmt::parseValue(NodeKind::Int8, "-129", &ok); + QVERIFY2(!ok, "-129 underflows int8"); + + fmt::parseValue(NodeKind::Int8, "127", &ok); + QVERIFY(ok); + + fmt::parseValue(NodeKind::Int8, "-128", &ok); + QVERIFY(ok); + + // Hex overflow: 0x100 > 0xFF + fmt::parseValue(NodeKind::Int8, "0x100", &ok); + QVERIFY2(!ok, "0x100 overflows int8 hex"); + + fmt::parseValue(NodeKind::Int8, "0xFF", &ok); + QVERIFY(ok); + } + + void testUInt8Overflow() { + bool ok; + fmt::parseValue(NodeKind::UInt8, "256", &ok); + QVERIFY2(!ok, "256 overflows uint8"); + + fmt::parseValue(NodeKind::UInt8, "255", &ok); + QVERIFY(ok); + + fmt::parseValue(NodeKind::UInt8, "0", &ok); + QVERIFY(ok); + + // Negative should fail for unsigned + fmt::parseValue(NodeKind::UInt8, "-1", &ok); + QVERIFY2(!ok, "Negative should fail for uint8"); + } + + void testInt16Overflow() { + bool ok; + fmt::parseValue(NodeKind::Int16, "32768", &ok); + QVERIFY2(!ok, "32768 overflows int16"); + + fmt::parseValue(NodeKind::Int16, "-32769", &ok); + QVERIFY2(!ok, "-32769 underflows int16"); + + fmt::parseValue(NodeKind::Int16, "32767", &ok); + QVERIFY(ok); + + fmt::parseValue(NodeKind::Int16, "-32768", &ok); + QVERIFY(ok); + + fmt::parseValue(NodeKind::Int16, "0x10000", &ok); + QVERIFY2(!ok, "0x10000 overflows int16 hex"); + } + + void testUInt16Overflow() { + bool ok; + fmt::parseValue(NodeKind::UInt16, "65536", &ok); + QVERIFY2(!ok, "65536 overflows uint16"); + + fmt::parseValue(NodeKind::UInt16, "65535", &ok); + QVERIFY(ok); + } + + void testInt32Overflow() { + bool ok; + // 2147483647 is INT32_MAX + fmt::parseValue(NodeKind::Int32, "2147483647", &ok); + QVERIFY(ok); + + // 2147483648 overflows signed int32 in decimal + // Note: toInt returns false for overflow + fmt::parseValue(NodeKind::Int32, "2147483648", &ok); + QVERIFY2(!ok, "2147483648 overflows int32 decimal"); + + fmt::parseValue(NodeKind::Int32, "0xFFFFFFFF", &ok); + QVERIFY(ok); // hex path allows up to 0xFFFFFFFF + + fmt::parseValue(NodeKind::Int32, "0x100000000", &ok); + QVERIFY2(!ok, "0x100000000 overflows int32 hex"); + } + + void testUInt32Overflow() { + bool ok; + fmt::parseValue(NodeKind::UInt32, "4294967295", &ok); + QVERIFY(ok); + + fmt::parseValue(NodeKind::UInt32, "4294967296", &ok); + QVERIFY2(!ok, "4294967296 overflows uint32"); + } + + void testUInt64Max() { + bool ok; + // UINT64_MAX = 18446744073709551615 + fmt::parseValue(NodeKind::UInt64, "18446744073709551615", &ok); + QVERIFY(ok); + + // Beyond UINT64_MAX should fail to parse + fmt::parseValue(NodeKind::UInt64, "18446744073709551616", &ok); + QVERIFY2(!ok, "UINT64_MAX+1 should fail"); + + fmt::parseValue(NodeKind::UInt64, "0xFFFFFFFFFFFFFFFF", &ok); + QVERIFY(ok); + } + + // ── Invalid characters in numeric fields ── + + void testInvalidCharsInIntegers() { + bool ok; + fmt::parseValue(NodeKind::Int32, "12abc", &ok); + QVERIFY(!ok); + + fmt::parseValue(NodeKind::UInt32, "hello", &ok); + QVERIFY(!ok); + + fmt::parseValue(NodeKind::Int8, "3.14", &ok); + QVERIFY(!ok); // Not a valid integer + + fmt::parseValue(NodeKind::UInt16, "", &ok); + QVERIFY(!ok); // Empty string fails for non-string types + } + + void testInvalidCharsInHex() { + bool ok; + fmt::parseValue(NodeKind::Hex32, "GHIJKL", &ok); + QVERIFY(!ok); + + fmt::parseValue(NodeKind::Hex64, "0xZZZZ", &ok); + QVERIFY(!ok); + + fmt::parseValue(NodeKind::Hex8, "XY", &ok); + QVERIFY(!ok); + } + + // ── Hex wrong byte count ── + + void testHexWrongByteCount() { + bool ok; + // Hex32 expects 4 bytes when space-separated + fmt::parseValue(NodeKind::Hex32, "AA BB CC DD EE", &ok); + QVERIFY2(!ok, "5 bytes should fail for Hex32"); + + fmt::parseValue(NodeKind::Hex32, "AA BB", &ok); + QVERIFY2(!ok, "2 bytes should fail for Hex32"); + + // Correct: 4 bytes + fmt::parseValue(NodeKind::Hex32, "AA BB CC DD", &ok); + QVERIFY(ok); + + // Hex64 expects 8 bytes + fmt::parseValue(NodeKind::Hex64, "AA BB CC DD", &ok); + QVERIFY2(!ok, "4 bytes should fail for Hex64"); + + fmt::parseValue(NodeKind::Hex64, "AA BB CC DD EE FF 00 11", &ok); + QVERIFY(ok); + } + + // ── Float/Double edge cases ── + + void testFloatEdgeCases() { + bool ok; + // Valid floats + fmt::parseValue(NodeKind::Float, "0", &ok); + QVERIFY(ok); + + fmt::parseValue(NodeKind::Float, "-0.0", &ok); + QVERIFY(ok); + + fmt::parseValue(NodeKind::Float, "1e38", &ok); + QVERIFY(ok); + + // EU comma separator (converted to dot internally) + fmt::parseValue(NodeKind::Float, "3,14", &ok); + QVERIFY(ok); + + // Junk + fmt::parseValue(NodeKind::Float, "not_a_number", &ok); + QVERIFY(!ok); + + fmt::parseValue(NodeKind::Float, "", &ok); + QVERIFY(!ok); + } + + void testDoubleEdgeCases() { + bool ok; + fmt::parseValue(NodeKind::Double, "1.7976931348623157e+308", &ok); + QVERIFY(ok); + + fmt::parseValue(NodeKind::Double, "abc", &ok); + QVERIFY(!ok); + + fmt::parseValue(NodeKind::Double, "1,5", &ok); + QVERIFY(ok); // EU comma + } + + // ── Bool: only "true"/"false"/"0"/"1" are valid ── + + void testBoolInvalid() { + bool ok; + fmt::parseValue(NodeKind::Bool, "true", &ok); + QVERIFY(ok); + + fmt::parseValue(NodeKind::Bool, "false", &ok); + QVERIFY(ok); + + fmt::parseValue(NodeKind::Bool, "1", &ok); + QVERIFY(ok); + + fmt::parseValue(NodeKind::Bool, "0", &ok); + QVERIFY(ok); + + // Invalid: "yes", "no", "2", random text + fmt::parseValue(NodeKind::Bool, "yes", &ok); + QVERIFY2(!ok, "'yes' is not valid bool"); + + fmt::parseValue(NodeKind::Bool, "no", &ok); + QVERIFY2(!ok, "'no' is not valid bool"); + + fmt::parseValue(NodeKind::Bool, "2", &ok); + QVERIFY2(!ok, "'2' is not valid bool"); + + fmt::parseValue(NodeKind::Bool, "TRUE", &ok); + QVERIFY2(!ok, "'TRUE' (uppercase) is not valid bool"); + + fmt::parseValue(NodeKind::Bool, "", &ok); + QVERIFY(!ok); + } + + // ── Pointer: hex-only parsing ── + + void testPointerInvalid() { + bool ok; + // Valid + fmt::parseValue(NodeKind::Pointer64, "0x7FFE3B8D4260", &ok); + QVERIFY(ok); + + fmt::parseValue(NodeKind::Pointer64, "7FFE3B8D4260", &ok); + QVERIFY(ok); + + // Invalid chars + fmt::parseValue(NodeKind::Pointer64, "0xGGGG", &ok); + QVERIFY(!ok); + + // Pointer32 overflow + fmt::parseValue(NodeKind::Pointer32, "0x100000000", &ok); + QVERIFY2(!ok, "0x100000000 overflows ptr32"); + + fmt::parseValue(NodeKind::Pointer32, "0xFFFFFFFF", &ok); + QVERIFY(ok); + } + + // ── validateValue: error message testing ── + + void testValidateValueMessages() { + // Hex kind with non-hex chars → character-level error + QString err = fmt::validateValue(NodeKind::Hex32, "GGGG"); + QVERIFY(!err.isEmpty()); + QVERIFY(err.contains("invalid hex")); + + // Int kind overflow → "too large" message + err = fmt::validateValue(NodeKind::UInt8, "999"); + QVERIFY(!err.isEmpty()); + QVERIFY(err.contains("too large")); + + // Decimal with non-digit + err = fmt::validateValue(NodeKind::UInt32, "12!3"); + QVERIFY(!err.isEmpty()); + QVERIFY(err.contains("invalid")); + + // Signed integer with leading minus accepted + err = fmt::validateValue(NodeKind::Int32, "-42"); + QVERIFY2(err.isEmpty(), qPrintable("Negative int32 should be valid: " + err)); + + // Unsigned with minus → invalid + err = fmt::validateValue(NodeKind::UInt32, "-1"); + QVERIFY(!err.isEmpty()); + + // Float junk + err = fmt::validateValue(NodeKind::Float, "abc"); + QVERIFY(!err.isEmpty()); + QVERIFY(err.contains("invalid number")); + + // Empty is valid (special case) + err = fmt::validateValue(NodeKind::UInt32, ""); + QVERIFY(err.isEmpty()); + + // Spaces only trimmed to empty → valid + err = fmt::validateValue(NodeKind::UInt32, " "); + QVERIFY(err.isEmpty()); + } + + // ── validateBaseAddress: equation syntax ── + + void testValidateBaseAddressEdgeCases() { + // Valid cases + QVERIFY(fmt::validateBaseAddress("0x1000").isEmpty()); + QVERIFY(fmt::validateBaseAddress("1000").isEmpty()); + QVERIFY(fmt::validateBaseAddress("0x1000 + 0x100").isEmpty()); + QVERIFY(fmt::validateBaseAddress("0x2000 - 0x10").isEmpty()); + QVERIFY(fmt::validateBaseAddress("0x400+0x200-0x100").isEmpty()); + QVERIFY(fmt::validateBaseAddress(" 0xDEAD ").isEmpty()); + + // Invalid cases + QVERIFY(!fmt::validateBaseAddress("").isEmpty()); // empty + QVERIFY(!fmt::validateBaseAddress(" ").isEmpty()); // whitespace only - no hex digits + QVERIFY(!fmt::validateBaseAddress("0xGGGG").isEmpty()); + QVERIFY(!fmt::validateBaseAddress("0x1000 * 2").isEmpty()); // multiplication not supported + QVERIFY(!fmt::validateBaseAddress("0x1000 ++ 0x100").isEmpty()); // double operator + QVERIFY(!fmt::validateBaseAddress("hello").isEmpty()); + } + + // ── Extremely long strings ── + + void testExtremelyLongInput() { + bool ok; + // 10000-char string of hex digits + QString longHex = QString("F").repeated(10000); + fmt::parseValue(NodeKind::Hex32, longHex, &ok); + // Should either fail or succeed gracefully (no crash) + // For Hex32 continuous mode, this is a valid huge hex number that overflows uint32 + Q_UNUSED(ok); // Just testing it doesn't crash + + // Long garbage + QString longJunk = QString("@#$%^&*").repeated(1000); + fmt::parseValue(NodeKind::Int32, longJunk, &ok); + QVERIFY(!ok); + + // Very long decimal number + QString longDec = QString("9").repeated(100); + fmt::parseValue(NodeKind::UInt64, longDec, &ok); + QVERIFY(!ok); // Way beyond UINT64_MAX + + // Extremely long hex for parseValue + fmt::parseValue(NodeKind::Hex64, "0x" + QString("F").repeated(200), &ok); + // No crash is the test + } + + // ── Special/weird characters ── + + void testSpecialCharacters() { + bool ok; + fmt::parseValue(NodeKind::Int32, "\0", &ok); + QVERIFY(!ok); + + fmt::parseValue(NodeKind::Int32, "\t42\n", &ok); + // trimmed internally — may or may not parse; just don't crash + Q_UNUSED(ok); + + fmt::parseValue(NodeKind::UInt32, " 42 ", &ok); + QVERIFY(ok); // Leading/trailing whitespace should be trimmed + + // Unicode characters + fmt::parseValue(NodeKind::UInt32, QString::fromUtf8("\xC3\xA9"), &ok); // é + QVERIFY(!ok); + } + + // ── Container kinds: parseValue should fail gracefully ── + + void testContainerKindParseValue() { + bool ok; + fmt::parseValue(NodeKind::Struct, "anything", &ok); + QVERIFY(!ok); + + fmt::parseValue(NodeKind::Array, "42", &ok); + QVERIFY(!ok); + } + + // ════════════════════════════════════════════════════════ + // Controller-level stress tests (uses GUI fixtures) + // ════════════════════════════════════════════════════════ + + // ── setNodeValue rejects overflowing values without changing data ── + + void testRejectOverflowInt8() { + int idx = findNode(m_doc->tree, "field_i8"); + QVERIFY(idx >= 0); + uint64_t addr = m_doc->tree.computeOffset(idx); + QByteArray before = m_doc->provider->readBytes(addr, 1); + + m_ctrl->setNodeValue(idx, 0, "999"); + QApplication::processEvents(); + + QByteArray after = m_doc->provider->readBytes(addr, 1); + QCOMPARE(after, before); // Data unchanged + QCOMPARE(m_doc->undoStack.count(), 0); // No command pushed + } + + void testRejectOverflowUInt8() { + int idx = findNode(m_doc->tree, "field_u8"); + QVERIFY(idx >= 0); + uint64_t addr = m_doc->tree.computeOffset(idx); + QByteArray before = m_doc->provider->readBytes(addr, 1); + + m_ctrl->setNodeValue(idx, 0, "256"); + QApplication::processEvents(); + + QByteArray after = m_doc->provider->readBytes(addr, 1); + QCOMPARE(after, before); + QCOMPARE(m_doc->undoStack.count(), 0); + } + + void testRejectOverflowUInt16() { + int idx = findNode(m_doc->tree, "field_u16"); + QVERIFY(idx >= 0); + uint64_t addr = m_doc->tree.computeOffset(idx); + QByteArray before = m_doc->provider->readBytes(addr, 2); + + m_ctrl->setNodeValue(idx, 0, "70000"); + QApplication::processEvents(); + + QByteArray after = m_doc->provider->readBytes(addr, 2); + QCOMPARE(after, before); + QCOMPARE(m_doc->undoStack.count(), 0); + } + + void testRejectOverflowUInt32() { + int idx = findNode(m_doc->tree, "field_u32"); + QVERIFY(idx >= 0); + uint64_t addr = m_doc->tree.computeOffset(idx); + QByteArray before = m_doc->provider->readBytes(addr, 4); + + m_ctrl->setNodeValue(idx, 0, "4294967296"); + QApplication::processEvents(); + + QByteArray after = m_doc->provider->readBytes(addr, 4); + QCOMPARE(after, before); + QCOMPARE(m_doc->undoStack.count(), 0); + } + + // ── setNodeValue rejects garbage text ── + + void testRejectGarbageText() { + int idx = findNode(m_doc->tree, "field_u32"); + QVERIFY(idx >= 0); + uint64_t addr = m_doc->tree.computeOffset(idx); + QByteArray before = m_doc->provider->readBytes(addr, 4); + + // Various garbage inputs + const char* junk[] = { + "hello", "!@#$%", "", " ", "0xGGGG", "3.14", + "true", "null", "NaN", "inf", "\t\n\r" + }; + for (const char* s : junk) { + m_ctrl->setNodeValue(idx, 0, s); + QApplication::processEvents(); + } + + QByteArray after = m_doc->provider->readBytes(addr, 4); + QCOMPARE(after, before); + QCOMPARE(m_doc->undoStack.count(), 0); + } + + void testRejectGarbageFloat() { + int idx = findNode(m_doc->tree, "field_float"); + QVERIFY(idx >= 0); + uint64_t addr = m_doc->tree.computeOffset(idx); + QByteArray before = m_doc->provider->readBytes(addr, 4); + + m_ctrl->setNodeValue(idx, 0, "not_a_number"); + m_ctrl->setNodeValue(idx, 0, ""); + m_ctrl->setNodeValue(idx, 0, "0xDEAD"); // hex not valid for float + QApplication::processEvents(); + + QByteArray after = m_doc->provider->readBytes(addr, 4); + QCOMPARE(after, before); + QCOMPARE(m_doc->undoStack.count(), 0); + } + + void testRejectGarbageBool() { + int idx = findNode(m_doc->tree, "field_bool"); + QVERIFY(idx >= 0); + uint64_t addr = m_doc->tree.computeOffset(idx); + QByteArray before = m_doc->provider->readBytes(addr, 1); + + m_ctrl->setNodeValue(idx, 0, "yes"); + m_ctrl->setNodeValue(idx, 0, "2"); + m_ctrl->setNodeValue(idx, 0, "TRUE"); + m_ctrl->setNodeValue(idx, 0, "maybe"); + QApplication::processEvents(); + + QByteArray after = m_doc->provider->readBytes(addr, 1); + QCOMPARE(after, before); + QCOMPARE(m_doc->undoStack.count(), 0); + } + + // ── setNodeValue on invalid node indices ── + + void testOutOfBoundsNodeIndex() { + QByteArray before = m_doc->provider->readBytes(m_doc->tree.baseAddress, 256); + + m_ctrl->setNodeValue(-1, 0, "42"); + m_ctrl->setNodeValue(-100, 0, "42"); + m_ctrl->setNodeValue(99999, 0, "42"); + m_ctrl->setNodeValue(INT_MAX, 0, "42"); + QApplication::processEvents(); + + QByteArray after = m_doc->provider->readBytes(m_doc->tree.baseAddress, 256); + QCOMPARE(after, before); + QCOMPARE(m_doc->undoStack.count(), 0); + } + + // ── renameNode with edge cases ── + + void testRenameNodeEdgeCases() { + int idx = findNode(m_doc->tree, "field_u32"); + QVERIFY(idx >= 0); + + // Empty name is allowed at controller level + m_ctrl->renameNode(idx, ""); + QApplication::processEvents(); + QCOMPARE(m_doc->tree.nodes[idx].name, QString("")); + m_doc->undoStack.undo(); + QCOMPARE(m_doc->tree.nodes[idx].name, QString("field_u32")); + + // Very long name (1000 chars) + QString longName = QString("a").repeated(1000); + m_ctrl->renameNode(idx, longName); + QApplication::processEvents(); + QCOMPARE(m_doc->tree.nodes[idx].name, longName); + m_doc->undoStack.undo(); + + // Special characters + m_ctrl->renameNode(idx, "field with spaces & \"chars\""); + QApplication::processEvents(); + QCOMPARE(m_doc->tree.nodes[idx].name, + QString("field with spaces & \"chars\"")); + m_doc->undoStack.undo(); + + // Out of bounds indices + m_ctrl->renameNode(-1, "bad"); + m_ctrl->renameNode(99999, "bad"); + QApplication::processEvents(); + // Should not crash; undo stack not affected + } + + // ── changeNodeKind with invalid indices ── + + void testChangeKindOutOfBounds() { + int origCount = m_doc->tree.nodes.size(); + + m_ctrl->changeNodeKind(-1, NodeKind::Float); + m_ctrl->changeNodeKind(99999, NodeKind::Float); + QApplication::processEvents(); + + QCOMPARE(m_doc->tree.nodes.size(), origCount); + QCOMPARE(m_doc->undoStack.count(), 0); + } + + // ── changeNodeKind size transitions: shrink inserts padding ── + + void testChangeKindShrinkInsertsPadding() { + int idx = findNode(m_doc->tree, "field_u32"); + QVERIFY(idx >= 0); + QCOMPARE(m_doc->tree.nodes[idx].kind, NodeKind::UInt32); // 4 bytes + + int origCount = m_doc->tree.nodes.size(); + m_ctrl->changeNodeKind(idx, NodeKind::UInt8); // 4 → 1 byte = 3 gap + QApplication::processEvents(); + + QCOMPARE(m_doc->tree.nodes[idx].kind, NodeKind::UInt8); + // Should have inserted padding nodes (Hex16 + Hex8 = 3 bytes, or similar) + QVERIFY(m_doc->tree.nodes.size() > origCount); + + // Undo restores everything + m_doc->undoStack.undo(); + QApplication::processEvents(); + QCOMPARE(m_doc->tree.nodes[idx].kind, NodeKind::UInt32); + QCOMPARE(m_doc->tree.nodes.size(), origCount); + } + + // ── insertNode / removeNode boundary conditions ── + + void testInsertNodeWithInvalidParent() { + int origCount = m_doc->tree.nodes.size(); + + // Non-existent parent ID — insertNode doesn't validate parent existence, + // so it will add a node with an orphan parentId. Verify no crash. + m_ctrl->insertNode(0xDEADBEEF, 0, NodeKind::UInt32, "orphan"); + QApplication::processEvents(); + + // The node was added (the tree accepts orphan parentId) + QCOMPARE(m_doc->tree.nodes.size(), origCount + 1); + + // Undo cleans up + m_doc->undoStack.undo(); + QApplication::processEvents(); + QCOMPARE(m_doc->tree.nodes.size(), origCount); + } + + void testRemoveNodeOutOfBounds() { + int origCount = m_doc->tree.nodes.size(); + + m_ctrl->removeNode(-1); + m_ctrl->removeNode(99999); + QApplication::processEvents(); + + QCOMPARE(m_doc->tree.nodes.size(), origCount); + QCOMPARE(m_doc->undoStack.count(), 0); + } + + // ── Array element count: boundary validation ── + + void testArrayCountBoundaries() { + int idx = findNode(m_doc->tree, "field_arr"); + QVERIFY(idx >= 0); + QCOMPARE(m_doc->tree.nodes[idx].kind, NodeKind::Array); + int origLen = m_doc->tree.nodes[idx].arrayLen; + + // Simulate EditTarget::ArrayElementCount through the controller API + // The controller validates: ok && newLen > 0 && newLen <= 100000 + + // Zero count — should be rejected (> 0 check) + m_doc->undoStack.clear(); + { + bool ok; + int newLen = QString("0").toInt(&ok); + // Controller logic: ok && newLen > 0 → false + QVERIFY(ok && newLen == 0); // toInt succeeds, but newLen is 0 + // This should NOT push a command + } + + // Negative count + { + bool ok; + int newLen = QString("-5").toInt(&ok); + QVERIFY(ok && newLen < 0); // toInt succeeds, but negative + } + + // Just above max: 100001 + { + bool ok; + int newLen = QString("100001").toInt(&ok); + QVERIFY(ok && newLen > 100000); + } + + // At max: 100000 (should be accepted) + { + bool ok; + int newLen = QString("100000").toInt(&ok); + QVERIFY(ok && newLen > 0 && newLen <= 100000); + } + + // Non-numeric text + { + bool ok; + QString("hello").toInt(&ok); + QVERIFY(!ok); + } + + // Verify actual array length is unchanged + QCOMPARE(m_doc->tree.nodes[idx].arrayLen, origLen); + } + + // ── Hex values: space-separated with wrong count ── + + void testHexWrongByteCountAtController() { + int idx = findNode(m_doc->tree, "field_h32"); + QVERIFY(idx >= 0); + uint64_t addr = m_doc->tree.computeOffset(idx); + QByteArray before = m_doc->provider->readBytes(addr, 4); + + // 5 bytes for a 4-byte field + m_ctrl->setNodeValue(idx, 0, "AA BB CC DD EE"); + QApplication::processEvents(); + + QByteArray after = m_doc->provider->readBytes(addr, 4); + QCOMPARE(after, before); + QCOMPARE(m_doc->undoStack.count(), 0); + } + + // ── Valid writes followed by undo: verify round-trip integrity ── + + void testValueWriteUndoIntegrity() { + // Write valid values to multiple fields, undo all, verify original data + int i8idx = findNode(m_doc->tree, "field_i8"); + int u32idx = findNode(m_doc->tree, "field_u32"); + int fltidx = findNode(m_doc->tree, "field_float"); + QVERIFY(i8idx >= 0 && u32idx >= 0 && fltidx >= 0); + + // Snapshot original provider + QByteArray origData = m_doc->provider->readBytes( + m_doc->tree.baseAddress, 256); + + // Write three valid values + m_ctrl->setNodeValue(i8idx, 0, "42"); + m_ctrl->setNodeValue(u32idx, 0, "12345"); + m_ctrl->setNodeValue(fltidx, 0, "2.5"); + QApplication::processEvents(); + + QCOMPARE(m_doc->undoStack.count(), 3); + + // Undo all three + m_doc->undoStack.undo(); + m_doc->undoStack.undo(); + m_doc->undoStack.undo(); + QApplication::processEvents(); + + QByteArray afterUndo = m_doc->provider->readBytes( + m_doc->tree.baseAddress, 256); + QCOMPARE(afterUndo, origData); + } + + // ── toggleCollapse on out-of-bounds index ── + + void testToggleCollapseOutOfBounds() { + m_ctrl->toggleCollapse(-1); + m_ctrl->toggleCollapse(99999); + QApplication::processEvents(); + QCOMPARE(m_doc->undoStack.count(), 0); + } + + // ── Rapid fire: many rejected writes don't accumulate undo history ── + + void testRapidFireRejectedWrites() { + int idx = findNode(m_doc->tree, "field_u8"); + QVERIFY(idx >= 0); + + for (int i = 0; i < 100; i++) + m_ctrl->setNodeValue(idx, 0, "9999"); // overflow + QApplication::processEvents(); + + QCOMPARE(m_doc->undoStack.count(), 0); + } + + // ── Duplicate nodes: verify they get unique IDs ── + + void testDuplicateNodeGetsUniqueId() { + int idx = findNode(m_doc->tree, "field_u32"); + QVERIFY(idx >= 0); + int origCount = m_doc->tree.nodes.size(); + + m_ctrl->duplicateNode(idx); + QApplication::processEvents(); + + // duplicateNode appends "_copy" to the name + QCOMPARE(m_doc->tree.nodes.size(), origCount + 1); + + int copyIdx = findNode(m_doc->tree, "field_u32_copy"); + QVERIFY2(copyIdx >= 0, "Duplicate node should exist with '_copy' suffix"); + + // Verify all IDs are unique + QSet ids; + for (const auto& n : m_doc->tree.nodes) { + QVERIFY2(!ids.contains(n.id), + qPrintable(QString("Duplicate ID found: %1").arg(n.id))); + ids.insert(n.id); + } + + m_doc->undoStack.undo(); + QApplication::processEvents(); + QCOMPARE(m_doc->tree.nodes.size(), origCount); + } + + // ── Batch remove with invalid indices in the mix ── + + void testBatchRemoveWithInvalidIndices() { + int origCount = m_doc->tree.nodes.size(); + int validIdx = findNode(m_doc->tree, "field_u8"); + QVERIFY(validIdx >= 0); + + // Mix of valid and invalid indices — batchRemoveNodes filters internally + QVector indices = {validIdx, -1, 99999}; + m_ctrl->batchRemoveNodes(indices); + QApplication::processEvents(); + + // At least the valid node should have been removed + QVERIFY(m_doc->tree.nodes.size() < origCount); + + // Undo restores + m_doc->undoStack.undo(); + QApplication::processEvents(); + QCOMPARE(m_doc->tree.nodes.size(), origCount); + } + + // ── Batch change kind with invalid indices ── + + void testBatchChangeKindWithInvalidIndices() { + int validIdx = findNode(m_doc->tree, "field_i32"); + QVERIFY(validIdx >= 0); + NodeKind origKind = m_doc->tree.nodes[validIdx].kind; + + // Mix of valid and invalid + QVector indices = {-1, validIdx, 99999}; + m_ctrl->batchChangeKind(indices, NodeKind::Float); + QApplication::processEvents(); + + // Valid node should have changed + QCOMPARE(m_doc->tree.nodes[validIdx].kind, NodeKind::Float); + + m_doc->undoStack.undo(); + QApplication::processEvents(); + QCOMPARE(m_doc->tree.nodes[validIdx].kind, origKind); + } + + // ── Editor: inline edit rejected on out-of-range lines ── + + void testInlineEditOutOfRangeLines() { + m_ctrl->refresh(); + QApplication::processEvents(); + + // Try to edit a line that doesn't exist + QVERIFY(!m_editor->beginInlineEdit(EditTarget::Name, 99999)); + QVERIFY(!m_editor->isEditing()); + + QVERIFY(!m_editor->beginInlineEdit(EditTarget::Value, -1)); + QVERIFY(!m_editor->isEditing()); + } + + // ── Editor: padding value edit blocked, name/type still work ── + + void testPaddingEditRestrictions() { + m_ctrl->refresh(); + QApplication::processEvents(); + + ComposeResult result = m_doc->compose(); + m_editor->applyDocument(result); + QApplication::processEvents(); + + // Find padding line + int padLine = -1; + for (int i = 0; i < result.meta.size(); i++) { + if (result.meta[i].nodeKind == NodeKind::Padding && + result.meta[i].lineKind == LineKind::Field) { + padLine = i; + break; + } + } + QVERIFY(padLine >= 0); + + // Value edit rejected + QVERIFY(!m_editor->beginInlineEdit(EditTarget::Value, padLine)); + + // Type edit accepted + bool ok = m_editor->beginInlineEdit(EditTarget::Type, padLine); + QVERIFY(ok); + m_editor->cancelInlineEdit(); + QApplication::processEvents(); + } + + // ── Editor: struct header rejects value edit ── + + void testStructHeaderRejectsValueEdit() { + m_ctrl->refresh(); + QApplication::processEvents(); + + ComposeResult result = m_doc->compose(); + m_editor->applyDocument(result); + QApplication::processEvents(); + + // Find header line + int headerLine = -1; + for (int i = 0; i < result.meta.size(); i++) { + if (result.meta[i].lineKind == LineKind::Header) { + headerLine = i; + break; + } + } + QVERIFY(headerLine >= 0); + + QVERIFY(!m_editor->beginInlineEdit(EditTarget::Value, headerLine)); + QVERIFY(!m_editor->isEditing()); + + // But Name and Type should work + bool ok = m_editor->beginInlineEdit(EditTarget::Name, headerLine); + QVERIFY(ok); + m_editor->cancelInlineEdit(); + } + + // ── Base address: invalid equation syntax ── + + void testBaseAddressInvalidEquation() { + uint64_t origBase = m_doc->tree.baseAddress; + + m_ctrl->refresh(); + QApplication::processEvents(); + + // These are processed through the inlineEditCommitted handler, + // but we can test the parsing logic directly: + // The controller silently ignores invalid base address text + + // Test the validation function directly + QVERIFY(!fmt::validateBaseAddress("0x1000 ** 2").isEmpty()); + QVERIFY(!fmt::validateBaseAddress("0x1000 / 2").isEmpty()); + QVERIFY(!fmt::validateBaseAddress("abc xyz").isEmpty()); + + // Original base should be unchanged + QCOMPARE(m_doc->tree.baseAddress, origBase); + } + + // ── Pointer64 value: accepts hex, rejects garbage ── + + void testPointerValueValidation() { + int idx = findNode(m_doc->tree, "field_ptr"); + QVERIFY(idx >= 0); + uint64_t addr = m_doc->tree.computeOffset(idx); + QByteArray before = m_doc->provider->readBytes(addr, 8); + + // Garbage + m_ctrl->setNodeValue(idx, 0, "not_a_pointer"); + m_ctrl->setNodeValue(idx, 0, ""); + m_ctrl->setNodeValue(idx, 0, "0xZZZZ"); + QApplication::processEvents(); + + QByteArray after = m_doc->provider->readBytes(addr, 8); + QCOMPARE(after, before); + QCOMPARE(m_doc->undoStack.count(), 0); + + // Valid hex write + m_ctrl->setNodeValue(idx, 0, "0xDEADBEEFCAFEBABE"); + QApplication::processEvents(); + + QByteArray written = m_doc->provider->readBytes(addr, 8); + uint64_t writtenVal; + memcpy(&writtenVal, written.data(), 8); + QCOMPARE(writtenVal, (uint64_t)0xDEADBEEFCAFEBABEULL); + + m_doc->undoStack.undo(); + QApplication::processEvents(); + QByteArray restored = m_doc->provider->readBytes(addr, 8); + QCOMPARE(restored, before); + } + + // ── Hex64 space-separated: exact 8 bytes accepted, other counts rejected ── + + void testHex64SpaceSeparatedBoundary() { + int idx = findNode(m_doc->tree, "field_h64"); + QVERIFY(idx >= 0); + uint64_t addr = m_doc->tree.computeOffset(idx); + QByteArray before = m_doc->provider->readBytes(addr, 8); + + // 7 bytes — reject + m_ctrl->setNodeValue(idx, 0, "AA BB CC DD EE FF 00"); + QApplication::processEvents(); + QCOMPARE(m_doc->provider->readBytes(addr, 8), before); + + // 9 bytes — reject + m_ctrl->setNodeValue(idx, 0, "AA BB CC DD EE FF 00 11 22"); + QApplication::processEvents(); + QCOMPARE(m_doc->provider->readBytes(addr, 8), before); + + QCOMPARE(m_doc->undoStack.count(), 0); + + // 8 bytes — accept + m_ctrl->setNodeValue(idx, 0, "01 02 03 04 05 06 07 08"); + QApplication::processEvents(); + QCOMPARE(m_doc->undoStack.count(), 1); + + QByteArray written = m_doc->provider->readBytes(addr, 8); + QCOMPARE((uint8_t)written[0], (uint8_t)0x01); + QCOMPARE((uint8_t)written[7], (uint8_t)0x08); + + m_doc->undoStack.undo(); + } + + // ── Multiple undos past the beginning don't crash ── + + void testExcessiveUndos() { + int idx = findNode(m_doc->tree, "field_u32"); + QVERIFY(idx >= 0); + + m_ctrl->setNodeValue(idx, 0, "42"); + QApplication::processEvents(); + QCOMPARE(m_doc->undoStack.count(), 1); + + // Undo once (valid) + m_doc->undoStack.undo(); + // Undo 50 more times (all no-ops, should not crash) + for (int i = 0; i < 50; i++) + m_doc->undoStack.undo(); + QApplication::processEvents(); + + // Redo 50 times past the end + m_doc->undoStack.redo(); + for (int i = 0; i < 50; i++) + m_doc->undoStack.redo(); + QApplication::processEvents(); + } +}; + +QTEST_MAIN(TestValidationController) +#include "test_validation.moc"