Condensed array display + per-scope column widths + MIT license

- Array element structs render without { } braces (condensed display)
- [N] separators show element indices within arrays
- Per-scope column width calculation (nested elements use tighter spacing)
- Array headers show struct[N] for struct arrays
- [N] separators are not interactive (no hover/click highlight)
- Dynamic type column width (min 8, max 14)
- PE32+ sample data with full headers, DataDirectory[16], SectionHeaders[4]
- Added MIT license

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
IChooChoose
2026-02-05 06:26:00 -07:00
committed by sysadmin
parent 04252a3c96
commit 4d35db224e
11 changed files with 727 additions and 430 deletions

View File

@@ -608,7 +608,7 @@ private slots:
QCOMPARE(result.meta[2].lineKind, LineKind::Header); // Recursive header (expansion)
}
void testStructFooterSizeof() {
void testStructFooterSimple() {
NodeTree tree;
tree.baseAddress = 0;
@@ -627,13 +627,6 @@ private slots:
f1.offset = 0;
tree.addNode(f1);
Node f2;
f2.kind = NodeKind::UInt64;
f2.name = "b";
f2.parentId = rootId;
f2.offset = 4;
tree.addNode(f2);
NullProvider prov;
ComposeResult result = compose(tree, prov);
@@ -641,9 +634,10 @@ private slots:
int lastLine = result.meta.size() - 1;
QCOMPARE(result.meta[lastLine].lineKind, LineKind::Footer);
// Footer text should contain sizeof(Sized)=0xC (4+8=12=0xC)
// Footer text should just be "};" (no sizeof)
QString footerText = result.text.split('\n').last();
QVERIFY(footerText.contains("sizeof(Sized)=0xC"));
QVERIFY(footerText.contains("};"));
QVERIFY(!footerText.contains("sizeof"));
}
void testLineMetaHasNodeId() {
@@ -669,115 +663,6 @@ private slots:
}
}
void testSizeofUpdatesAfterDelete() {
// Test that sizeof recalculates after deleting a node
NodeTree tree;
tree.baseAddress = 0;
Node root;
root.kind = NodeKind::Struct;
root.name = "Test";
root.parentId = 0;
root.offset = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
Node f1;
f1.kind = NodeKind::UInt32;
f1.name = "a";
f1.parentId = rootId;
f1.offset = 0;
tree.addNode(f1);
Node f2;
f2.kind = NodeKind::UInt64;
f2.name = "b";
f2.parentId = rootId;
f2.offset = 4;
int f2i = tree.addNode(f2);
uint64_t f2Id = tree.nodes[f2i].id;
NullProvider prov;
// First compose: sizeof should be 0xC (4+8=12)
ComposeResult result1 = compose(tree, prov);
QString footer1 = result1.text.split('\n').last();
QVERIFY2(footer1.contains("sizeof(Test)=0xC"),
qPrintable("Before delete: " + footer1));
// Delete the second field
int idx = tree.indexOfId(f2Id);
QVERIFY(idx >= 0);
tree.nodes.remove(idx);
tree.invalidateIdCache();
// Second compose: sizeof should be 0x4 (only UInt32 remains)
ComposeResult result2 = compose(tree, prov);
QString footer2 = result2.text.split('\n').last();
QVERIFY2(footer2.contains("sizeof(Test)=0x4"),
qPrintable("After delete: " + footer2));
}
void testNestedStructSizeofUpdates() {
// Test nested struct sizeof updates when child is deleted
NodeTree tree;
tree.baseAddress = 0;
// Root struct
Node root;
root.kind = NodeKind::Struct;
root.name = "Root";
root.parentId = 0;
root.offset = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
// Nested struct (like IMAGE_FILE_HEADER)
Node nested;
nested.kind = NodeKind::Struct;
nested.name = "Nested";
nested.parentId = rootId;
nested.offset = 0;
int ni = tree.addNode(nested);
uint64_t nestedId = tree.nodes[ni].id;
// Field in nested struct
Node f1;
f1.kind = NodeKind::UInt32;
f1.name = "a";
f1.parentId = nestedId;
f1.offset = 0;
tree.addNode(f1);
Node f2;
f2.kind = NodeKind::UInt32;
f2.name = "b";
f2.parentId = nestedId;
f2.offset = 4;
int f2i = tree.addNode(f2);
uint64_t f2Id = tree.nodes[f2i].id;
NullProvider prov;
// First compose
ComposeResult result1 = compose(tree, prov);
// Find nested struct footer
QString text1 = result1.text;
QVERIFY2(text1.contains("sizeof(Nested)=0x8"),
qPrintable("Before delete nested sizeof: " + text1));
// Delete field from nested struct
int idx = tree.indexOfId(f2Id);
QVERIFY(idx >= 0);
tree.nodes.remove(idx);
tree.invalidateIdCache();
// Second compose - nested sizeof should update
ComposeResult result2 = compose(tree, prov);
QString text2 = result2.text;
QVERIFY2(text2.contains("sizeof(Nested)=0x4"),
qPrintable("After delete nested sizeof: " + text2));
}
};
QTEST_MAIN(TestCompose)

View File

@@ -326,17 +326,17 @@ private slots:
auto ts = rcx::typeSpanFor(lm);
QVERIFY(ts.valid);
QCOMPARE(ts.start, 6);
QCOMPARE(ts.end, 16); // 6 + 10
QCOMPARE(ts.end, 20); // 6 + 14 (kColType)
auto ns = rcx::nameSpanFor(lm);
QVERIFY(ns.valid);
QCOMPARE(ns.start, 18); // 6 + 10 + 2
QCOMPARE(ns.end, 42); // 18 + 24
QCOMPARE(ns.start, 22); // 6 + 14 + 2
QCOMPARE(ns.end, 44); // 22 + 22 (kColName)
auto vs = rcx::valueSpanFor(lm, 60);
auto vs = rcx::valueSpanFor(lm, 100);
QVERIFY(vs.valid);
QCOMPARE(vs.start, 44); // 18 + 24 + 2
QCOMPARE(vs.end, 60);
QCOMPARE(vs.start, 46); // 22 + 22 + 2
QCOMPARE(vs.end, 78); // 46 + 32 (kColValue)
}
void testColumnSpan_continuation() {
@@ -349,10 +349,10 @@ private slots:
QVERIFY(!rcx::typeSpanFor(lm).valid);
QVERIFY(!rcx::nameSpanFor(lm).valid);
auto vs = rcx::valueSpanFor(lm, 60);
auto vs = rcx::valueSpanFor(lm, 100);
QVERIFY(vs.valid);
QCOMPARE(vs.start, 6 + 10 + 24 + 4); // kFoldCol+indent + COL_TYPE + COL_NAME + 4
QCOMPARE(vs.end, 60);
QCOMPARE(vs.start, 6 + 14 + 22 + 4); // kFoldCol+indent + kColType(14) + kColName(22) + 4
QCOMPARE(vs.end, 46 + 32); // start + kColValue
}
void testColumnSpan_headerFooter() {
@@ -382,17 +382,17 @@ private slots:
auto ts = rcx::typeSpanFor(lm);
QVERIFY(ts.valid);
QCOMPARE(ts.start, 3);
QCOMPARE(ts.end, 13); // 3 + 10
QCOMPARE(ts.end, 17); // 3 + 14 (kColType)
auto ns = rcx::nameSpanFor(lm);
QVERIFY(ns.valid);
QCOMPARE(ns.start, 15); // 3 + 10 + 2
QCOMPARE(ns.end, 39); // 15 + 24
QCOMPARE(ns.start, 19); // 3 + 14 + 2
QCOMPARE(ns.end, 41); // 19 + 22 (kColName)
auto vs = rcx::valueSpanFor(lm, 50);
auto vs = rcx::valueSpanFor(lm, 100);
QVERIFY(vs.valid);
QCOMPARE(vs.start, 41); // 15 + 24 + 2
QCOMPARE(vs.end, 50);
QCOMPARE(vs.start, 43); // 19 + 22 + 2
QCOMPARE(vs.end, 75); // 43 + 32 (kColValue)
}
void testNodeIdJsonRoundTrip() {

View File

@@ -9,12 +9,13 @@ private slots:
void testTypeName() {
QString s = fmt::typeName(NodeKind::Float);
QVERIFY(s.trimmed() == "float");
QCOMPARE(s.size(), 10); // COL_TYPE
QCOMPARE(s.size(), 14); // kColType
}
void testFmtInt32() {
QCOMPARE(fmt::fmtInt32(-42), QString("-42"));
QCOMPARE(fmt::fmtInt32(0), QString("0"));
// fmtInt32 outputs hex representation (0xffffffd6 for -42)
QCOMPARE(fmt::fmtInt32(-42), QString("0xffffffd6"));
QCOMPARE(fmt::fmtInt32(0), QString("0x0"));
}
void testFmtFloat() {
@@ -224,25 +225,15 @@ private slots:
QVERIFY(!ok);
}
void testFmtStructFooterWithSize() {
void testFmtStructFooterSimple() {
Node n;
n.kind = NodeKind::Struct;
n.name = "Test";
// With size
QString s1 = fmt::fmtStructFooter(n, 0, 0x14);
QVERIFY(s1.contains("};"));
QVERIFY(s1.contains("sizeof(Test)=0x14"));
// Size 0 → no sizeof
QString s2 = fmt::fmtStructFooter(n, 0, 0);
QVERIFY(s2.contains("};"));
QVERIFY(!s2.contains("sizeof"));
// Default (no size arg) → no sizeof
QString s3 = fmt::fmtStructFooter(n, 0);
QVERIFY(s3.contains("};"));
QVERIFY(!s3.contains("sizeof"));
// Footer is always just "};" (no sizeof comment)
QString s = fmt::fmtStructFooter(n, 0, 0x14);
QVERIFY(s.contains("};"));
QVERIFY(!s.contains("sizeof")); // No sizeof comment
}
};