diff --git a/src/compose.cpp b/src/compose.cpp index 9ba195e..ca29cfa 100644 --- a/src/compose.cpp +++ b/src/compose.cpp @@ -18,6 +18,7 @@ struct ComposeState { int currentLine = 0; int typeW = kColType; // global type column width (fallback) int nameW = kColName; // global name column width (fallback) + bool baseEmitted = false; // only first root struct shows base address // Precomputed for O(1) lookups QHash> childMap; @@ -206,7 +207,8 @@ void composeParent(ComposeState& state, const NodeTree& tree, lm.foldCollapsed = node.collapsed; lm.foldLevel = computeFoldLevel(depth, true); lm.markerMask = (1u << M_STRUCT_BG); - lm.isRootHeader = (node.parentId == 0 && node.kind == NodeKind::Struct); + lm.isRootHeader = (node.parentId == 0 && node.kind == NodeKind::Struct && !state.baseEmitted); + if (lm.isRootHeader) state.baseEmitted = true; QString headerText; if (node.kind == NodeKind::Array) { @@ -350,7 +352,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov) { } maxTypeLen = qMax(maxTypeLen, typeName.size()); } - state.typeW = qBound(kMinTypeW, maxTypeLen + 1, kMaxTypeW); + state.typeW = qBound(kMinTypeW, maxTypeLen, kMaxTypeW); // Compute effective name column width from longest name int maxNameLen = kMinNameW; @@ -361,7 +363,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov) { if (node.kind == NodeKind::Struct || node.kind == NodeKind::Array) continue; maxNameLen = qMax(maxNameLen, node.name.size()); } - state.nameW = qBound(kMinNameW, maxNameLen + 1, kMaxNameW); + state.nameW = qBound(kMinNameW, maxNameLen, kMaxNameW); // Pre-compute per-scope widths (each container gets widths based on direct children only) for (int i = 0; i < tree.nodes.size(); i++) { @@ -375,24 +377,43 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov) { for (int childIdx : state.childMap.value(container.id)) { const Node& child = tree.nodes[childIdx]; + // Skip containers - their headers don't use columnar layout + if (child.kind == NodeKind::Struct || child.kind == NodeKind::Array) + continue; + // Type width - QString childTypeName; - if (child.kind == NodeKind::Array) { - childTypeName = fmt::arrayTypeName(child.elementKind, child.arrayLen); - } else { - childTypeName = fmt::typeNameRaw(child.kind); - } + QString childTypeName = fmt::typeNameRaw(child.kind); scopeMaxType = qMax(scopeMaxType, childTypeName.size()); - // Name width (skip hex/padding and containers) - if (!isHexPreview(child.kind) && - child.kind != NodeKind::Struct && child.kind != NodeKind::Array) { + // Name width (skip hex/padding) + if (!isHexPreview(child.kind)) { scopeMaxName = qMax(scopeMaxName, child.name.size()); } } - state.scopeTypeW[container.id] = qBound(kMinTypeW, scopeMaxType + 1, kMaxTypeW); - state.scopeNameW[container.id] = qBound(kMinNameW, scopeMaxName + 1, kMaxNameW); + state.scopeTypeW[container.id] = qBound(kMinTypeW, scopeMaxType, kMaxTypeW); + state.scopeNameW[container.id] = qBound(kMinNameW, scopeMaxName, kMaxNameW); + } + + // Compute scope widths for root level (parentId == 0) + { + int rootMaxType = kMinTypeW; + int rootMaxName = kMinNameW; + for (int childIdx : state.childMap.value(0)) { + const Node& child = tree.nodes[childIdx]; + + // Skip containers - their headers don't use columnar layout + if (child.kind == NodeKind::Struct || child.kind == NodeKind::Array) + continue; + + QString childTypeName = fmt::typeNameRaw(child.kind); + rootMaxType = qMax(rootMaxType, childTypeName.size()); + if (!isHexPreview(child.kind)) { + rootMaxName = qMax(rootMaxName, child.name.size()); + } + } + state.scopeTypeW[0] = qBound(kMinTypeW, rootMaxType, kMaxTypeW); + state.scopeNameW[0] = qBound(kMinNameW, rootMaxName, kMaxNameW); } QVector roots = state.childMap.value(0); diff --git a/src/controller.cpp b/src/controller.cpp index d4bbc87..5cbf9e1 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -151,13 +151,12 @@ void RcxController::connectEditor(RcxEditor* editor) { bool typeOk; NodeKind elemKind = kindFromTypeName(elemTypeName, &typeOk); if (typeOk && nodeIdx < m_doc->tree.nodes.size()) { - Node& node = m_doc->tree.nodes[nodeIdx]; + const Node& node = m_doc->tree.nodes[nodeIdx]; if (node.kind == NodeKind::Array) { - // Update element kind and count (no undo for now) - node.elementKind = elemKind; - node.arrayLen = newCount; - if (node.viewIndex >= newCount) - node.viewIndex = qMax(0, newCount - 1); + m_doc->undoStack.push(new RcxCommand(this, + cmd::ChangeArrayMeta{node.id, + node.elementKind, elemKind, + node.arrayLen, newCount})); } } } @@ -446,6 +445,14 @@ void RcxController::applyCommand(const Command& command, bool isUndo) { } else if constexpr (std::is_same_v) { const QByteArray& bytes = isUndo ? c.oldBytes : c.newBytes; m_doc->provider->writeBytes(c.addr, bytes); + } else if constexpr (std::is_same_v) { + int idx = tree.indexOfId(c.nodeId); + if (idx >= 0) { + tree.nodes[idx].elementKind = isUndo ? c.oldElementKind : c.newElementKind; + tree.nodes[idx].arrayLen = isUndo ? c.oldArrayLen : c.newArrayLen; + if (tree.nodes[idx].viewIndex >= tree.nodes[idx].arrayLen) + tree.nodes[idx].viewIndex = qMax(0, tree.nodes[idx].arrayLen - 1); + } } }, command); diff --git a/src/core.h b/src/core.h index 477cfba..8c7078d 100644 --- a/src/core.h +++ b/src/core.h @@ -382,6 +382,12 @@ struct NodeTree { int structSpan(uint64_t structId, const QHash>* childMap = nullptr) const { + int idx = indexOfId(structId); + if (idx < 0) return 0; + + const Node& node = nodes[idx]; + int declaredSize = node.byteSize(); + int maxEnd = 0; QVector kids = childMap ? childMap->value(structId) : childrenOf(structId); for (int ci : kids) { @@ -391,7 +397,8 @@ struct NodeTree { int end = c.offset + sz; if (end > maxEnd) maxEnd = end; } - return maxEnd; + + return qMax(declaredSize, maxEnd); } // Batch selection normalizers @@ -480,11 +487,15 @@ namespace cmd { QVector offAdjs; }; struct ChangeBase { uint64_t oldBase, newBase; }; struct WriteBytes { uint64_t addr; QByteArray oldBytes, newBytes; }; + struct ChangeArrayMeta { uint64_t nodeId; + NodeKind oldElementKind, newElementKind; + int oldArrayLen, newArrayLen; }; } using Command = std::variant< cmd::ChangeKind, cmd::Rename, cmd::Collapse, - cmd::Insert, cmd::Remove, cmd::ChangeBase, cmd::WriteBytes + cmd::Insert, cmd::Remove, cmd::ChangeBase, cmd::WriteBytes, + cmd::ChangeArrayMeta >; // ── Column spans (for inline editing) ── @@ -504,7 +515,7 @@ inline constexpr int kColName = 22; inline constexpr int kColValue = 32; inline constexpr int kColComment = 28; // "// Enter=Save Esc=Cancel" fits inline constexpr int kColBaseAddr = 12; // "0x" + up to 10 hex digits (40-bit address) -inline constexpr int kSepWidth = 2; +inline constexpr int kSepWidth = 1; inline constexpr int kMinTypeW = 8; // Minimum type column width (fits "uint64_t") inline constexpr int kMaxTypeW = 14; // Maximum type column width (fits "uint64_t[999]") inline constexpr int kMinNameW = 8; // Minimum name column width (matches ASCII preview) @@ -541,7 +552,7 @@ inline ColumnSpan valueSpanFor(const LineMeta& lm, int /*lineLength*/, int typeW if (lm.isContinuation) { int prefixW = isHexPad ? (typeW + kSepWidth + 8 + kSepWidth) - : (typeW + nameW + 4); + : (typeW + nameW + 2 * kSepWidth); int start = ind + prefixW; return {start, start + valWidth, true}; } @@ -564,7 +575,7 @@ inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength, int typeW = if (lm.isContinuation) { int prefixW = isHexPad ? (typeW + kSepWidth + 8 + kSepWidth) - : (typeW + nameW + 4); + : (typeW + nameW + 2 * kSepWidth); start = ind + prefixW + valWidth; } else { start = isHexPad @@ -575,13 +586,13 @@ inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength, int typeW = } // Base address span (only valid for root struct headers) -// Line format: " - struct Name { base: 0x00400000" +// Line format: " - struct Name { // base: 0x00400000" inline ColumnSpan baseAddressSpanFor(const LineMeta& lm, const QString& lineText) { if (lm.lineKind != LineKind::Header || !lm.isRootHeader) return {}; - // Find "base: " after the opening brace - int baseIdx = lineText.indexOf(QStringLiteral("base: ")); + // Find "// base: " after the opening brace + int baseIdx = lineText.indexOf(QStringLiteral("// base: ")); if (baseIdx < 0) return {}; - int startPos = baseIdx + 6; // after "base: " + int startPos = baseIdx + 9; // after "// base: " // Value goes to end of line int endPos = lineText.size(); while (endPos > startPos && lineText[endPos-1].isSpace()) @@ -590,10 +601,10 @@ inline ColumnSpan baseAddressSpanFor(const LineMeta& lm, const QString& lineText return {startPos, endPos, true}; } -// Full "base: 0x..." span for coloring (includes "base: " prefix) +// Full "// base: 0x..." span for coloring (includes "// base: " prefix) inline ColumnSpan baseAddressFullSpanFor(const LineMeta& lm, const QString& lineText) { if (lm.lineKind != LineKind::Header || !lm.isRootHeader) return {}; - int baseIdx = lineText.indexOf(QStringLiteral("base: ")); + int baseIdx = lineText.indexOf(QStringLiteral("// base: ")); if (baseIdx < 0) return {}; int endPos = lineText.size(); while (endPos > baseIdx && lineText[endPos-1].isSpace()) diff --git a/src/editor.cpp b/src/editor.cpp index fd37b71..e8fcdb3 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -140,7 +140,7 @@ void RcxEditor::setupScintilla() { m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE, IND_BASE_ADDR, 17 /*INDIC_TEXTFORE*/); m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE, - IND_BASE_ADDR, QColor("#6a9955")); + IND_BASE_ADDR, QColor("#5a8248")); // Hover span indicator — blue text like a link m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE, @@ -517,10 +517,8 @@ void RcxEditor::applyBaseAddressColoring(const QVector& meta) { QString lineText = getLineText(m_sci, i); ColumnSpan span = baseAddressFullSpanFor(lm, lineText); if (!span.valid) continue; - long lineStart = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE, - (unsigned long)i); - long posA = lineStart + span.start; - long posB = lineStart + span.end; + long posA = posFromCol(m_sci, i, span.start); + long posB = posFromCol(m_sci, i, span.end); if (posB > posA) m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, posA, posB - posA); } @@ -1381,11 +1379,9 @@ void RcxEditor::setEditComment(const QString& comment) { QString formatted = QStringLiteral("//") + comment; QString padded = formatted.leftJustified(availWidth, ' ').left(availWidth); - // Use direct position calculation from line start - long lineStart = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE, - (unsigned long)m_editState.line); - long posA = lineStart + startCol; - long posB = lineStart + endCol; + // Use UTF-8 safe column-to-position conversion + long posA = posFromCol(m_sci, m_editState.line, startCol); + long posB = posFromCol(m_sci, m_editState.line, endCol); QByteArray utf8 = padded.toUtf8(); m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETSTART, posA); diff --git a/src/format.cpp b/src/format.cpp index b462ba3..8f08e50 100644 --- a/src/format.cpp +++ b/src/format.cpp @@ -10,7 +10,7 @@ static constexpr int COL_TYPE = kColType; static constexpr int COL_NAME = kColName; static constexpr int COL_VALUE = kColValue; static constexpr int COL_COMMENT = 28; // "// Enter=Save Esc=Cancel" fits -static const QString SEP = QStringLiteral(" "); +static const QString SEP = QStringLiteral(" "); static QString fit(QString s, int w) { if (w <= 0) return {}; @@ -94,6 +94,8 @@ QString indent(int depth) { QString fmtOffsetMargin(int64_t relativeOffset, bool isContinuation) { if (isContinuation) return QStringLiteral(" \u00B7"); + if (relativeOffset < 0) + return QStringLiteral("-0x") + QString::number(-relativeOffset, 16).toUpper(); return QStringLiteral("+0x") + QString::number(relativeOffset, 16).toUpper(); } @@ -109,7 +111,7 @@ QString fmtStructHeaderWithBase(const Node& node, int depth, uint64_t baseAddres QString header = indent(depth) + typeName(node.kind).trimmed() + QStringLiteral(" ") + node.name + QStringLiteral(" { "); QString baseHex = QStringLiteral("0x") + QString::number(baseAddress, 16).toUpper(); - return header + QStringLiteral("base: ") + baseHex; + return header + QStringLiteral("// base: ") + baseHex; } QString fmtStructFooter(const Node& /*node*/, int depth, int /*totalSize*/) { @@ -255,7 +257,7 @@ QString fmtNodeLine(const Node& node, const Provider& prov, QString type = typeName(node.kind, colType); QString name = fit(node.name, colName); // Blank prefix for continuation lines (same width as type+sep+name+sep) - const int prefixW = colType + colName + 4; // 2 seps × 2 chars + const int prefixW = colType + colName + 2 * kSepWidth; // Comment suffix (padded or empty) QString cmtSuffix = comment.isEmpty() ? QString(COL_COMMENT, ' ') @@ -384,13 +386,62 @@ QByteArray parseValue(NodeKind kind, const QString& text, bool* ok) { switch (kind) { case NodeKind::Hex8: return parseHexBytes(stripHex(s), 1, ok); - case NodeKind::Hex16: return parseHexBytes(stripHex(s), 2, ok); - case NodeKind::Hex32: return parseHexBytes(stripHex(s), 4, ok); - case NodeKind::Hex64: return parseHexBytes(stripHex(s), 8, ok); - case NodeKind::Int8: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; int val = stripHex(s).toInt(ok,b); return parseIntChecked(val, ok); } - case NodeKind::Int16: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; int val = stripHex(s).toInt(ok,b); return parseIntChecked(val, ok); } - case NodeKind::Int32: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; int val = stripHex(s).toInt(ok,b); return *ok ? toBytes(val) : QByteArray{}; } - case NodeKind::Int64: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; qlonglong val = stripHex(s).toLongLong(ok,b); return *ok ? toBytes(val) : QByteArray{}; } + case NodeKind::Hex16: { + uint val = stripHex(s).toUInt(ok, 16); + if (*ok && val > 0xFFFF) *ok = false; + return *ok ? toBytes(static_cast(val)) : QByteArray{}; + } + case NodeKind::Hex32: { + uint val = stripHex(s).toUInt(ok, 16); + return *ok ? toBytes(val) : QByteArray{}; + } + case NodeKind::Hex64: { + qulonglong val = stripHex(s).toULongLong(ok, 16); + return *ok ? toBytes(val) : QByteArray{}; + } + case NodeKind::Int8: { + bool isHex = s.startsWith("0x", Qt::CaseInsensitive); + if (isHex) { + uint val = stripHex(s).toUInt(ok, 16); + if (*ok && val > 0xFF) *ok = false; + return *ok ? toBytes(static_cast(val)) : QByteArray{}; + } else { + int val = s.toInt(ok, 10); + return parseIntChecked(val, ok); + } + } + case NodeKind::Int16: { + bool isHex = s.startsWith("0x", Qt::CaseInsensitive); + if (isHex) { + uint val = stripHex(s).toUInt(ok, 16); + if (*ok && val > 0xFFFF) *ok = false; + return *ok ? toBytes(static_cast(val)) : QByteArray{}; + } else { + int val = s.toInt(ok, 10); + return parseIntChecked(val, ok); + } + } + case NodeKind::Int32: { + bool isHex = s.startsWith("0x", Qt::CaseInsensitive); + if (isHex) { + qulonglong val = stripHex(s).toULongLong(ok, 16); + if (*ok && val > 0xFFFFFFFFULL) *ok = false; + return *ok ? toBytes(static_cast(val)) : QByteArray{}; + } else { + int val = s.toInt(ok, 10); + return *ok ? toBytes(val) : QByteArray{}; + } + } + case NodeKind::Int64: { + bool isHex = s.startsWith("0x", Qt::CaseInsensitive); + if (isHex) { + qulonglong val = stripHex(s).toULongLong(ok, 16); + return *ok ? toBytes(static_cast(val)) : QByteArray{}; + } else { + qlonglong val = s.toLongLong(ok, 10); + return *ok ? toBytes(val) : QByteArray{}; + } + } case NodeKind::UInt8: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; uint val = stripHex(s).toUInt(ok,b); return parseIntChecked(val, ok); } case NodeKind::UInt16: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; uint val = stripHex(s).toUInt(ok,b); return parseIntChecked(val, ok); } case NodeKind::UInt32: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; uint val = stripHex(s).toUInt(ok,b); return *ok ? toBytes(val) : QByteArray{}; } diff --git a/tests/test_core.cpp b/tests/test_core.cpp index fdb4e68..fe539dc 100644 --- a/tests/test_core.cpp +++ b/tests/test_core.cpp @@ -330,13 +330,13 @@ private slots: auto ns = rcx::nameSpanFor(lm); QVERIFY(ns.valid); - QCOMPARE(ns.start, 22); // 6 + 14 + 2 - QCOMPARE(ns.end, 44); // 22 + 22 (kColName) + QCOMPARE(ns.start, 21); // 6 + 14 + 1 (kSepWidth) + QCOMPARE(ns.end, 43); // 21 + 22 (kColName) auto vs = rcx::valueSpanFor(lm, 100); QVERIFY(vs.valid); - QCOMPARE(vs.start, 46); // 22 + 22 + 2 - QCOMPARE(vs.end, 78); // 46 + 32 (kColValue) + QCOMPARE(vs.start, 44); // 21 + 22 + 1 (kSepWidth) + QCOMPARE(vs.end, 76); // 44 + 32 (kColValue) } void testColumnSpan_continuation() { @@ -351,8 +351,8 @@ private slots: auto vs = rcx::valueSpanFor(lm, 100); QVERIFY(vs.valid); - QCOMPARE(vs.start, 6 + 14 + 22 + 4); // kFoldCol+indent + kColType(14) + kColName(22) + 4 - QCOMPARE(vs.end, 46 + 32); // start + kColValue + QCOMPARE(vs.start, 6 + 14 + 22 + 2); // kFoldCol+indent + kColType(14) + kColName(22) + 2*kSepWidth + QCOMPARE(vs.end, 44 + 32); // start + kColValue } void testColumnSpan_headerFooter() { @@ -386,13 +386,13 @@ private slots: auto ns = rcx::nameSpanFor(lm); QVERIFY(ns.valid); - QCOMPARE(ns.start, 19); // 3 + 14 + 2 - QCOMPARE(ns.end, 41); // 19 + 22 (kColName) + QCOMPARE(ns.start, 18); // 3 + 14 + 1 (kSepWidth) + QCOMPARE(ns.end, 40); // 18 + 22 (kColName) auto vs = rcx::valueSpanFor(lm, 100); QVERIFY(vs.valid); - QCOMPARE(vs.start, 43); // 19 + 22 + 2 - QCOMPARE(vs.end, 75); // 43 + 32 (kColValue) + QCOMPARE(vs.start, 41); // 18 + 22 + 1 (kSepWidth) + QCOMPARE(vs.end, 73); // 41 + 32 (kColValue) } void testNodeIdJsonRoundTrip() { @@ -474,6 +474,38 @@ private slots: empty.parentId = 0; int ei = tree3.addNode(empty); QCOMPARE(tree3.structSpan(tree3.nodes[ei].id), 0); + + // Primitive array (no children) should return its declared size + NodeTree tree4; + Node arr; + arr.kind = NodeKind::Array; + arr.name = "data"; + arr.parentId = 0; + arr.arrayLen = 16; + arr.elementKind = NodeKind::UInt32; // 16 * 4 = 64 bytes + int ai = tree4.addNode(arr); + QCOMPARE(tree4.structSpan(tree4.nodes[ai].id), 64); + + // Struct containing primitive array - span includes array size + NodeTree tree5; + Node container; + container.kind = NodeKind::Struct; + container.name = "Container"; + container.parentId = 0; + int ci = tree5.addNode(container); + uint64_t containerId = tree5.nodes[ci].id; + + Node arr2; + arr2.kind = NodeKind::Array; + arr2.name = "items"; + arr2.parentId = containerId; + arr2.offset = 8; + arr2.arrayLen = 10; + arr2.elementKind = NodeKind::UInt64; // 10 * 8 = 80 bytes + tree5.addNode(arr2); + + // Container span = array offset (8) + array size (80) = 88 + QCOMPARE(tree5.structSpan(containerId), 88); } void testNormalizePreferAncestors() { using namespace rcx; diff --git a/tests/test_format.cpp b/tests/test_format.cpp index 9fcc259..9efc2b6 100644 --- a/tests/test_format.cpp +++ b/tests/test_format.cpp @@ -94,14 +94,14 @@ private slots: void testParseValueHex32() { bool ok; - // Hex parsing produces raw bytes (no endian conversion) + // Hex parsing produces native-endian bytes (matches display which reads native-endian) QByteArray b = fmt::parseValue(NodeKind::Hex32, "DEADBEEF", &ok); QVERIFY(ok); QCOMPARE(b.size(), 4); - QCOMPARE((uint8_t)b[0], (uint8_t)0xDE); - QCOMPARE((uint8_t)b[1], (uint8_t)0xAD); - QCOMPARE((uint8_t)b[2], (uint8_t)0xBE); - QCOMPARE((uint8_t)b[3], (uint8_t)0xEF); + // Value 0xDEADBEEF stored as native-endian (little-endian on x86) + uint32_t v; + memcpy(&v, b.data(), 4); + QCOMPARE(v, (uint32_t)0xDEADBEEF); } void testParseValueBool() { @@ -122,13 +122,12 @@ private slots: void testParseValueHex0xPrefix() { bool ok; - // Hex32 with 0x prefix should work (raw bytes, no endian conversion) + // Hex32 with 0x prefix should work (native-endian, matches display) QByteArray b = fmt::parseValue(NodeKind::Hex32, "0xDEADBEEF", &ok); QVERIFY(ok); - QCOMPARE((uint8_t)b[0], (uint8_t)0xDE); - QCOMPARE((uint8_t)b[1], (uint8_t)0xAD); - QCOMPARE((uint8_t)b[2], (uint8_t)0xBE); - QCOMPARE((uint8_t)b[3], (uint8_t)0xEF); + uint32_t v32; + memcpy(&v32, b.data(), 4); + QCOMPARE(v32, (uint32_t)0xDEADBEEF); // Pointer64 with 0x prefix b = fmt::parseValue(NodeKind::Pointer64, "0x0000000000400000", &ok); @@ -177,6 +176,44 @@ private slots: QVERIFY(!ok); } + void testSignedHexRoundTrip() { + bool ok; + // Int8: 0xFF should parse as -1 (two's complement) + QByteArray b = fmt::parseValue(NodeKind::Int8, "0xFF", &ok); + QVERIFY(ok); + int8_t sv8; + memcpy(&sv8, b.data(), 1); + QCOMPARE(sv8, (int8_t)-1); + + // Int8: 0x80 should parse as -128 + b = fmt::parseValue(NodeKind::Int8, "0x80", &ok); + QVERIFY(ok); + memcpy(&sv8, b.data(), 1); + QCOMPARE(sv8, (int8_t)-128); + + // Int16: 0xFFFF should parse as -1 + b = fmt::parseValue(NodeKind::Int16, "0xFFFF", &ok); + QVERIFY(ok); + int16_t sv16; + memcpy(&sv16, b.data(), 2); + QCOMPARE(sv16, (int16_t)-1); + + // Int32: 0xFFFFFFFF should parse as -1 + b = fmt::parseValue(NodeKind::Int32, "0xFFFFFFFF", &ok); + QVERIFY(ok); + int32_t sv32; + memcpy(&sv32, b.data(), 4); + QCOMPARE(sv32, (int32_t)-1); + + // Int8: 0x1FF should fail (exceeds byte range) + fmt::parseValue(NodeKind::Int8, "0x1FF", &ok); + QVERIFY(!ok); + + // Int16: 0x1FFFF should fail (exceeds 16-bit range) + fmt::parseValue(NodeKind::Int16, "0x1FFFF", &ok); + QVERIFY(!ok); + } + void testReadValueBoundsCheck() { // Vec2 subLine=2 (out of bounds) should return "?" QByteArray data(16, '\0');