Fix 7 data bugs: hex endian, signed hex, array span, UTF-8 positions, array undo, offset sign

This commit is contained in:
sysadmin
2026-02-05 07:15:51 -07:00
parent 4d35db224e
commit a0d6b769b6
7 changed files with 226 additions and 71 deletions

View File

@@ -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<uint64_t, QVector<int>> 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<int> roots = state.childMap.value(0);

View File

@@ -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<T, cmd::WriteBytes>) {
const QByteArray& bytes = isUndo ? c.oldBytes : c.newBytes;
m_doc->provider->writeBytes(c.addr, bytes);
} else if constexpr (std::is_same_v<T, cmd::ChangeArrayMeta>) {
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);

View File

@@ -382,6 +382,12 @@ struct NodeTree {
int structSpan(uint64_t structId,
const QHash<uint64_t, QVector<int>>* 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<int> 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<OffsetAdj> 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())

View File

@@ -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<LineMeta>& 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);

View File

@@ -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<int8_t>(val, ok); }
case NodeKind::Int16: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; int val = stripHex(s).toInt(ok,b); return parseIntChecked<int16_t>(val, ok); }
case NodeKind::Int32: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; int val = stripHex(s).toInt(ok,b); return *ok ? toBytes<int32_t>(val) : QByteArray{}; }
case NodeKind::Int64: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; qlonglong val = stripHex(s).toLongLong(ok,b); return *ok ? toBytes<int64_t>(val) : QByteArray{}; }
case NodeKind::Hex16: {
uint val = stripHex(s).toUInt(ok, 16);
if (*ok && val > 0xFFFF) *ok = false;
return *ok ? toBytes<uint16_t>(static_cast<uint16_t>(val)) : QByteArray{};
}
case NodeKind::Hex32: {
uint val = stripHex(s).toUInt(ok, 16);
return *ok ? toBytes<uint32_t>(val) : QByteArray{};
}
case NodeKind::Hex64: {
qulonglong val = stripHex(s).toULongLong(ok, 16);
return *ok ? toBytes<uint64_t>(val) : QByteArray{};
}
case NodeKind::Int8: {
bool isHex = s.startsWith("0x", Qt::CaseInsensitive);
if (isHex) {
uint val = stripHex(s).toUInt(ok, 16);
if (*ok && val > 0xFF) *ok = false;
return *ok ? toBytes<int8_t>(static_cast<int8_t>(val)) : QByteArray{};
} else {
int val = s.toInt(ok, 10);
return parseIntChecked<int8_t>(val, ok);
}
}
case NodeKind::Int16: {
bool isHex = s.startsWith("0x", Qt::CaseInsensitive);
if (isHex) {
uint val = stripHex(s).toUInt(ok, 16);
if (*ok && val > 0xFFFF) *ok = false;
return *ok ? toBytes<int16_t>(static_cast<int16_t>(val)) : QByteArray{};
} else {
int val = s.toInt(ok, 10);
return parseIntChecked<int16_t>(val, ok);
}
}
case NodeKind::Int32: {
bool isHex = s.startsWith("0x", Qt::CaseInsensitive);
if (isHex) {
qulonglong val = stripHex(s).toULongLong(ok, 16);
if (*ok && val > 0xFFFFFFFFULL) *ok = false;
return *ok ? toBytes<int32_t>(static_cast<int32_t>(val)) : QByteArray{};
} else {
int val = s.toInt(ok, 10);
return *ok ? toBytes<int32_t>(val) : QByteArray{};
}
}
case NodeKind::Int64: {
bool isHex = s.startsWith("0x", Qt::CaseInsensitive);
if (isHex) {
qulonglong val = stripHex(s).toULongLong(ok, 16);
return *ok ? toBytes<int64_t>(static_cast<int64_t>(val)) : QByteArray{};
} else {
qlonglong val = s.toLongLong(ok, 10);
return *ok ? toBytes<int64_t>(val) : QByteArray{};
}
}
case NodeKind::UInt8: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; uint val = stripHex(s).toUInt(ok,b); return parseIntChecked<uint8_t>(val, ok); }
case NodeKind::UInt16: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; uint val = stripHex(s).toUInt(ok,b); return parseIntChecked<uint16_t>(val, ok); }
case NodeKind::UInt32: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; uint val = stripHex(s).toUInt(ok,b); return *ok ? toBytes<uint32_t>(val) : QByteArray{}; }

View File

@@ -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;

View File

@@ -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');