struct2ui 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- struct2ui/__init__.py +4 -0
- struct2ui/editor.py +1171 -0
- struct2ui/exporters/__init__.py +15 -0
- struct2ui/exporters/bin_emitter.py +254 -0
- struct2ui/exporters/c_emitter.py +233 -0
- struct2ui/exporters/c_parser.py +204 -0
- struct2ui/exporters/elf_verifier.py +341 -0
- struct2ui/exporters/json_format.py +137 -0
- struct2ui/icons/c2j.png +0 -0
- struct2ui/icons/elf.png +0 -0
- struct2ui/icons/export_notes_24dp_000000_FILL0_wght400_GRAD0_opsz24.png +0 -0
- struct2ui/icons/flowchart_24dp_000000_FILL0_wght400_GRAD0_opsz24.png +0 -0
- struct2ui/icons/refresh_24dp_000000_FILL0_wght400_GRAD0_opsz24.png +0 -0
- struct2ui/icons/report_24dp_000000_FILL0_wght400_GRAD0_opsz24.png +0 -0
- struct2ui/icons/save_24dp_000000_FILL0_wght400_GRAD0_opsz24.png +0 -0
- struct2ui/icons/save_as_24dp_000000_FILL0_wght400_GRAD0_opsz24.png +0 -0
- struct2ui/icons/settings_24dp_000000_FILL0_wght400_GRAD0_opsz24.png +0 -0
- struct2ui/icons/widgets_24dp_000000_FILL0_wght400_GRAD0_opsz24.png +0 -0
- struct2ui/schema.py +1118 -0
- struct2ui/ui/__init__.py +36 -0
- struct2ui/ui/renderers.py +304 -0
- struct2ui/ui/tables.py +207 -0
- struct2ui/ui/widgets.py +907 -0
- struct2ui-0.1.0.dist-info/METADATA +167 -0
- struct2ui-0.1.0.dist-info/RECORD +28 -0
- struct2ui-0.1.0.dist-info/WHEEL +5 -0
- struct2ui-0.1.0.dist-info/licenses/LICENSE +21 -0
- struct2ui-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""C source -> cfg_t-style schema dict (the reverse of the JSON->UI flow).
|
|
2
|
+
|
|
3
|
+
Pure data, no Qt. Single responsibility: scan a .c/.h file with simple regex
|
|
4
|
+
lexing and produce a dict shaped exactly like the existing cfg_t/*.json files
|
|
5
|
+
(see cfg_t/a_cfg_t.json for the target shape).
|
|
6
|
+
|
|
7
|
+
Supported constructs (deliberately limited; the project restricts macro use):
|
|
8
|
+
- simple `#define NAME VALUE` -> top-level int/float const
|
|
9
|
+
- `typedef enum { A=0, B, } name_t;` -> {"type":"enum","items":{...}}
|
|
10
|
+
- `typedef struct { ... } name_t;` -> {"type":"struct","items":[...]}
|
|
11
|
+
|
|
12
|
+
Inside a struct, each member line `type name;`, `type name[N];` or
|
|
13
|
+
`type name[N][M];` becomes a field spec. char[N] keeps `count` so the schema
|
|
14
|
+
layer recognises it as a string; other arrays keep `count` (or a `count` list
|
|
15
|
+
for multi-dimensional arrays, matching how shapes flatten elsewhere).
|
|
16
|
+
|
|
17
|
+
Anything the regex cannot classify is collected as a warning so the caller can
|
|
18
|
+
surface it instead of silently dropping members.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import re
|
|
24
|
+
from typing import Any, Dict, List, Tuple
|
|
25
|
+
|
|
26
|
+
# Strip // line comments and /* ... */ block comments before lexing.
|
|
27
|
+
_LINE_COMMENT = re.compile(r'//[^\n]*')
|
|
28
|
+
_BLOCK_COMMENT = re.compile(r'/\*.*?\*/', re.DOTALL)
|
|
29
|
+
|
|
30
|
+
# #define NAME VALUE (only simple object-like macros; function-like macros,
|
|
31
|
+
# i.e. `#define F(x) ...`, are skipped).
|
|
32
|
+
_DEFINE = re.compile(
|
|
33
|
+
r'^[ \t]*#[ \t]*define[ \t]+([A-Za-z_]\w*)[ \t]+(.+?)[ \t]*$',
|
|
34
|
+
re.MULTILINE,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# typedef enum { body } name ;
|
|
38
|
+
_ENUM = re.compile(
|
|
39
|
+
r'typedef\s+enum\s*(?:[A-Za-z_]\w*\s*)?\{(?P<body>.*?)\}\s*'
|
|
40
|
+
r'(?P<name>[A-Za-z_]\w*)\s*;',
|
|
41
|
+
re.DOTALL,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# typedef struct { body } name ;
|
|
45
|
+
_STRUCT = re.compile(
|
|
46
|
+
r'typedef\s+struct\s*(?:[A-Za-z_]\w*\s*)?\{(?P<body>.*?)\}\s*'
|
|
47
|
+
r'(?P<name>[A-Za-z_]\w*)\s*;',
|
|
48
|
+
re.DOTALL,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# One struct member: optional `const`, a type (one or more words to allow
|
|
52
|
+
# `unsigned int`), a name, optional [..] dimensions, trailing `;`.
|
|
53
|
+
_MEMBER = re.compile(
|
|
54
|
+
r'^\s*(?:const\s+)?'
|
|
55
|
+
r'(?P<type>(?:struct\s+)?[A-Za-z_]\w*(?:\s+[A-Za-z_]\w*)*?)\s+'
|
|
56
|
+
r'(?P<name>[A-Za-z_]\w*)\s*'
|
|
57
|
+
r'(?P<dims>(?:\[[^\]]*\])*)\s*;\s*$'
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
_DIM = re.compile(r'\[([^\]]*)\]')
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class CParseResult:
|
|
64
|
+
"""Container for one parse pass: the schema dict plus collected warnings."""
|
|
65
|
+
|
|
66
|
+
def __init__(self) -> None:
|
|
67
|
+
self.schema: Dict[str, Any] = {}
|
|
68
|
+
self.consts: Dict[str, Any] = {}
|
|
69
|
+
self.warnings: List[str] = []
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def has_content(self) -> bool:
|
|
73
|
+
return bool(self.schema) or bool(self.consts)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _strip_comments(text: str) -> str:
|
|
77
|
+
text = _BLOCK_COMMENT.sub(' ', text)
|
|
78
|
+
text = _LINE_COMMENT.sub('', text)
|
|
79
|
+
return text
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _parse_define_value(raw: str) -> Any:
|
|
83
|
+
"""Best-effort numeric value of a simple #define. Returns None if it is
|
|
84
|
+
not a plain int/float (e.g. an expression or another macro)."""
|
|
85
|
+
token = raw.strip()
|
|
86
|
+
# Allow a single set of wrapping parentheses: `#define N (40)`.
|
|
87
|
+
if token.startswith('(') and token.endswith(')'):
|
|
88
|
+
token = token[1:-1].strip()
|
|
89
|
+
# Drop integer suffixes like 40U, 40UL, 1.0f.
|
|
90
|
+
int_match = re.fullmatch(r'[+-]?\d+[uUlL]*', token)
|
|
91
|
+
if int_match:
|
|
92
|
+
return int(re.sub(r'[uUlL]+$', '', token))
|
|
93
|
+
float_match = re.fullmatch(r'[+-]?\d*\.\d+[fF]?|[+-]?\d+\.\d*[fF]?', token)
|
|
94
|
+
if float_match:
|
|
95
|
+
return float(token.rstrip('fF'))
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _parse_defines(text: str, result: CParseResult) -> None:
|
|
100
|
+
for name, raw in _DEFINE.findall(text):
|
|
101
|
+
# Skip function-like macros: `#define F (x)` would have raw "(x)" but
|
|
102
|
+
# the real giveaway is `NAME(` with no space, which the regex above
|
|
103
|
+
# already excludes by requiring whitespace before the value. A value
|
|
104
|
+
# we cannot reduce to a number is recorded as a warning.
|
|
105
|
+
value = _parse_define_value(raw)
|
|
106
|
+
if value is None:
|
|
107
|
+
result.warnings.append(
|
|
108
|
+
f"#define {name}: value '{raw}' is not a plain number; skipped")
|
|
109
|
+
continue
|
|
110
|
+
result.consts[name] = value
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _parse_enums(text: str, result: CParseResult) -> None:
|
|
114
|
+
for m in _ENUM.finditer(text):
|
|
115
|
+
name = m.group('name')
|
|
116
|
+
items: Dict[str, int] = {}
|
|
117
|
+
next_value = 0
|
|
118
|
+
for entry in m.group('body').split(','):
|
|
119
|
+
entry = entry.strip()
|
|
120
|
+
if not entry:
|
|
121
|
+
continue
|
|
122
|
+
if '=' in entry:
|
|
123
|
+
key, _, val = entry.partition('=')
|
|
124
|
+
key = key.strip()
|
|
125
|
+
val = val.strip()
|
|
126
|
+
parsed = _parse_define_value(val)
|
|
127
|
+
if parsed is None or not isinstance(parsed, int):
|
|
128
|
+
parsed = result.consts.get(val)
|
|
129
|
+
if not isinstance(parsed, int):
|
|
130
|
+
result.warnings.append(
|
|
131
|
+
f"enum {name}: cannot resolve value of '{key} = {val}'")
|
|
132
|
+
next_value += 1
|
|
133
|
+
continue
|
|
134
|
+
next_value = parsed
|
|
135
|
+
else:
|
|
136
|
+
key = entry
|
|
137
|
+
items[key] = next_value
|
|
138
|
+
next_value += 1
|
|
139
|
+
result.schema[name] = {'type': 'enum', 'items': items}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _parse_member(line: str) -> Tuple[Dict[str, Any], str]:
|
|
143
|
+
"""Parse one struct member line. Returns (field_spec, '') on success or
|
|
144
|
+
({}, reason) on failure."""
|
|
145
|
+
m = _MEMBER.match(line)
|
|
146
|
+
if not m:
|
|
147
|
+
return {}, f"unrecognized member: '{line.strip()}'"
|
|
148
|
+
ctype = re.sub(r'\s+', ' ', m.group('type')).strip()
|
|
149
|
+
fname = m.group('name')
|
|
150
|
+
dims = _DIM.findall(m.group('dims') or '')
|
|
151
|
+
|
|
152
|
+
field: Dict[str, Any] = {'name': fname, 'type': ctype}
|
|
153
|
+
if dims:
|
|
154
|
+
counts: List[Any] = []
|
|
155
|
+
for d in dims:
|
|
156
|
+
d = d.strip()
|
|
157
|
+
num = _parse_define_value(d)
|
|
158
|
+
counts.append(num if num is not None else d)
|
|
159
|
+
field['count'] = counts[0] if len(counts) == 1 else counts
|
|
160
|
+
return field, ''
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _parse_structs(text: str, result: CParseResult) -> None:
|
|
164
|
+
for m in _STRUCT.finditer(text):
|
|
165
|
+
name = m.group('name')
|
|
166
|
+
body = m.group('body')
|
|
167
|
+
items: List[Dict[str, Any]] = []
|
|
168
|
+
# Split on ';' so members spanning whitespace/newlines stay together.
|
|
169
|
+
for chunk in body.split(';'):
|
|
170
|
+
chunk = chunk.strip()
|
|
171
|
+
if not chunk:
|
|
172
|
+
continue
|
|
173
|
+
field, reason = _parse_member(chunk + ';')
|
|
174
|
+
if reason:
|
|
175
|
+
result.warnings.append(f"struct {name}: {reason}")
|
|
176
|
+
continue
|
|
177
|
+
items.append(field)
|
|
178
|
+
result.schema[name] = {'type': 'struct', 'items': items}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def parse_c_source(text: str) -> CParseResult:
|
|
182
|
+
"""Parse C source text into a cfg_t-style schema result."""
|
|
183
|
+
result = CParseResult()
|
|
184
|
+
clean = _strip_comments(text)
|
|
185
|
+
_parse_defines(clean, result)
|
|
186
|
+
_parse_enums(clean, result)
|
|
187
|
+
_parse_structs(clean, result)
|
|
188
|
+
return result
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def build_schema_dict(result: CParseResult) -> Dict[str, Any]:
|
|
192
|
+
"""Merge consts + enums + structs into one ordered cfg_t-style dict,
|
|
193
|
+
matching the layout of the hand-written cfg_t/*.json files (consts and
|
|
194
|
+
enums first, then structs)."""
|
|
195
|
+
out: Dict[str, Any] = {}
|
|
196
|
+
for k, v in result.consts.items():
|
|
197
|
+
out[k] = v
|
|
198
|
+
for k, v in result.schema.items():
|
|
199
|
+
if v.get('type') == 'enum':
|
|
200
|
+
out[k] = v
|
|
201
|
+
for k, v in result.schema.items():
|
|
202
|
+
if v.get('type') == 'struct':
|
|
203
|
+
out[k] = v
|
|
204
|
+
return out
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""ELF/DWARF layout verifier: compares the schema-derived C-ABI layout of
|
|
2
|
+
each pipeline cfg struct against the ground-truth layout recorded in an ELF's
|
|
3
|
+
DWARF debug info.
|
|
4
|
+
|
|
5
|
+
Strictest policy (all mismatches are errors):
|
|
6
|
+
- member name + order must match exactly
|
|
7
|
+
- each member's byte offset must match
|
|
8
|
+
- each member's byte size must match
|
|
9
|
+
- the struct's total byte size must match
|
|
10
|
+
|
|
11
|
+
Type names are matched against DWARF as-is: a stage's `type` (e.g. "a_cfg_t")
|
|
12
|
+
is looked up among the ELF's struct/typedef names verbatim. A type not found
|
|
13
|
+
in the ELF is an error.
|
|
14
|
+
|
|
15
|
+
No Qt here; the verifier returns a LoadReport so the UI can render it with the
|
|
16
|
+
same panels used for cfg_t / pipeline issues.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
23
|
+
|
|
24
|
+
from ..schema import (
|
|
25
|
+
Field, ScalarField, EnumField, StructField, ArrayField,
|
|
26
|
+
SchemaRegistry, LoadReport,
|
|
27
|
+
)
|
|
28
|
+
from .bin_emitter import _alignof, _sizeof, merge_abi
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# --------------------------------------------------------------------------- #
|
|
32
|
+
# DWARF model
|
|
33
|
+
# --------------------------------------------------------------------------- #
|
|
34
|
+
|
|
35
|
+
class DwarfMember:
|
|
36
|
+
__slots__ = ('name', 'offset', 'size')
|
|
37
|
+
|
|
38
|
+
def __init__(self, name: str, offset: int, size: int) -> None:
|
|
39
|
+
self.name = name
|
|
40
|
+
self.offset = offset
|
|
41
|
+
self.size = size
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class DwarfStruct:
|
|
45
|
+
__slots__ = ('name', 'byte_size', 'members')
|
|
46
|
+
|
|
47
|
+
def __init__(self, name: Optional[str], byte_size: int,
|
|
48
|
+
members: List[DwarfMember]) -> None:
|
|
49
|
+
self.name = name
|
|
50
|
+
self.byte_size = byte_size
|
|
51
|
+
self.members = members
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ElfLayout:
|
|
55
|
+
"""A lookup of type-name -> DwarfStruct parsed from one ELF file."""
|
|
56
|
+
|
|
57
|
+
def __init__(self, structs: Dict[str, DwarfStruct]) -> None:
|
|
58
|
+
self._structs = structs
|
|
59
|
+
|
|
60
|
+
def get(self, type_name: str) -> Optional[DwarfStruct]:
|
|
61
|
+
return self._structs.get(type_name)
|
|
62
|
+
|
|
63
|
+
def __contains__(self, type_name: str) -> bool:
|
|
64
|
+
return type_name in self._structs
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# --------------------------------------------------------------------------- #
|
|
68
|
+
# DWARF parsing
|
|
69
|
+
# --------------------------------------------------------------------------- #
|
|
70
|
+
|
|
71
|
+
def load_elf_layout(elf_path: str,
|
|
72
|
+
wanted: Optional[set] = None) -> ElfLayout:
|
|
73
|
+
"""Parse the ELF's DWARF and index named structs (and typedefs that point
|
|
74
|
+
at structs) by name. Raises on parse failure; callers turn that into a
|
|
75
|
+
LoadReport error.
|
|
76
|
+
|
|
77
|
+
Only top-level DIEs of each compilation unit are scanned, which is where
|
|
78
|
+
cfg structs / typedefs live; this avoids walking every nested DIE in a
|
|
79
|
+
large firmware ELF (orders of magnitude faster). When `wanted` is given,
|
|
80
|
+
iteration stops as soon as every wanted name has been indexed.
|
|
81
|
+
"""
|
|
82
|
+
from elftools.elf.elffile import ELFFile # lazy: optional dependency
|
|
83
|
+
|
|
84
|
+
structs: Dict[str, DwarfStruct] = {}
|
|
85
|
+
with open(elf_path, 'rb') as f:
|
|
86
|
+
elf = ELFFile(f)
|
|
87
|
+
if not elf.has_dwarf_info():
|
|
88
|
+
raise ValueError('ELF has no DWARF debug info')
|
|
89
|
+
dwarf = elf.get_dwarf_info()
|
|
90
|
+
for cu in dwarf.iter_CUs():
|
|
91
|
+
top = cu.get_top_DIE()
|
|
92
|
+
for die in top.iter_children():
|
|
93
|
+
name = _die_name(die)
|
|
94
|
+
if not name:
|
|
95
|
+
continue
|
|
96
|
+
if die.tag == 'DW_TAG_structure_type':
|
|
97
|
+
structs.setdefault(name, _parse_struct(cu, die))
|
|
98
|
+
elif die.tag == 'DW_TAG_typedef':
|
|
99
|
+
target = _resolve_to_struct(cu, die)
|
|
100
|
+
if target is not None:
|
|
101
|
+
structs.setdefault(name, _parse_struct(cu, target))
|
|
102
|
+
if wanted and wanted.issubset(structs.keys()):
|
|
103
|
+
break
|
|
104
|
+
return ElfLayout(structs)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _die_name(die) -> Optional[str]:
|
|
108
|
+
attr = die.attributes.get('DW_AT_name')
|
|
109
|
+
if attr is None:
|
|
110
|
+
return None
|
|
111
|
+
val = attr.value
|
|
112
|
+
return val.decode(errors='replace') if isinstance(val, bytes) else str(val)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _deref_type(cu, die):
|
|
116
|
+
attr = die.attributes.get('DW_AT_type')
|
|
117
|
+
if attr is None:
|
|
118
|
+
return None
|
|
119
|
+
return cu.get_DIE_from_refaddr(attr.value + cu.cu_offset)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _strip_cv_typedef(cu, die):
|
|
123
|
+
"""Follow typedef / const / volatile wrappers to the underlying type."""
|
|
124
|
+
seen = 0
|
|
125
|
+
while die is not None and seen < 32:
|
|
126
|
+
if die.tag in ('DW_TAG_typedef', 'DW_TAG_const_type',
|
|
127
|
+
'DW_TAG_volatile_type'):
|
|
128
|
+
die = _deref_type(cu, die)
|
|
129
|
+
seen += 1
|
|
130
|
+
continue
|
|
131
|
+
return die
|
|
132
|
+
return die
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _resolve_to_struct(cu, die):
|
|
136
|
+
target = _strip_cv_typedef(cu, _deref_type(cu, die))
|
|
137
|
+
if target is not None and target.tag == 'DW_TAG_structure_type':
|
|
138
|
+
return target
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _parse_struct(cu, die) -> DwarfStruct:
|
|
143
|
+
byte_size = _attr_int(die, 'DW_AT_byte_size', 0)
|
|
144
|
+
members: List[DwarfMember] = []
|
|
145
|
+
for child in die.iter_children():
|
|
146
|
+
if child.tag != 'DW_TAG_member':
|
|
147
|
+
continue
|
|
148
|
+
name = _die_name(child) or '<anon>'
|
|
149
|
+
offset = _attr_int(child, 'DW_AT_data_member_location', 0)
|
|
150
|
+
size = _member_byte_size(cu, child)
|
|
151
|
+
members.append(DwarfMember(name, offset, size))
|
|
152
|
+
return DwarfStruct(_die_name(die), byte_size, members)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _attr_int(die, key: str, default: int) -> int:
|
|
156
|
+
attr = die.attributes.get(key)
|
|
157
|
+
if attr is None:
|
|
158
|
+
return default
|
|
159
|
+
val = attr.value
|
|
160
|
+
if isinstance(val, list): # location expression form; ignore, rare here
|
|
161
|
+
return default
|
|
162
|
+
return int(val)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _member_byte_size(cu, member) -> int:
|
|
166
|
+
"""Byte size of a member, following its type chain.
|
|
167
|
+
|
|
168
|
+
For an array, multiply the element size by every dimension's count so the
|
|
169
|
+
result is the full in-struct footprint (matching the schema side, which
|
|
170
|
+
reports the whole array as one member).
|
|
171
|
+
"""
|
|
172
|
+
type_die = _deref_type(cu, member)
|
|
173
|
+
return _type_byte_size(cu, type_die)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _type_byte_size(cu, die) -> int:
|
|
177
|
+
seen = 0
|
|
178
|
+
while die is not None and seen < 64:
|
|
179
|
+
size_attr = die.attributes.get('DW_AT_byte_size')
|
|
180
|
+
if die.tag == 'DW_TAG_array_type':
|
|
181
|
+
elem = _strip_cv_typedef(cu, _deref_type(cu, die))
|
|
182
|
+
elem_size = _type_byte_size(cu, elem)
|
|
183
|
+
count = 1
|
|
184
|
+
for sub in die.iter_children():
|
|
185
|
+
if sub.tag == 'DW_TAG_subrange_type':
|
|
186
|
+
ub = sub.attributes.get('DW_AT_upper_bound')
|
|
187
|
+
cnt = sub.attributes.get('DW_AT_count')
|
|
188
|
+
if cnt is not None:
|
|
189
|
+
count *= int(cnt.value)
|
|
190
|
+
elif ub is not None:
|
|
191
|
+
count *= int(ub.value) + 1
|
|
192
|
+
return elem_size * count
|
|
193
|
+
if size_attr is not None:
|
|
194
|
+
return int(size_attr.value)
|
|
195
|
+
nxt = _deref_type(cu, die)
|
|
196
|
+
if nxt is None:
|
|
197
|
+
return 0
|
|
198
|
+
die = nxt
|
|
199
|
+
seen += 1
|
|
200
|
+
return 0
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# --------------------------------------------------------------------------- #
|
|
204
|
+
# Schema-side layout (mirrors the C-ABI offsets the bin emitter would use)
|
|
205
|
+
# --------------------------------------------------------------------------- #
|
|
206
|
+
|
|
207
|
+
class SchemaMember:
|
|
208
|
+
__slots__ = ('name', 'offset', 'size')
|
|
209
|
+
|
|
210
|
+
def __init__(self, name: str, offset: int, size: int) -> None:
|
|
211
|
+
self.name = name
|
|
212
|
+
self.offset = offset
|
|
213
|
+
self.size = size
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def schema_struct_layout(node: StructField,
|
|
217
|
+
abi: Dict[str, Any]) -> Tuple[int, List[SchemaMember]]:
|
|
218
|
+
"""Compute (total_size, members) for a StructField using the same C-ABI
|
|
219
|
+
rules as the .bin emitter (natural alignment, pack cap, tail padding).
|
|
220
|
+
"""
|
|
221
|
+
members: List[SchemaMember] = []
|
|
222
|
+
offset = 0
|
|
223
|
+
for child in node.children:
|
|
224
|
+
align = _alignof(child, abi)
|
|
225
|
+
rem = offset % align
|
|
226
|
+
if rem:
|
|
227
|
+
offset += align - rem
|
|
228
|
+
size = _sizeof(child, abi)
|
|
229
|
+
members.append(SchemaMember(child.name, offset, size))
|
|
230
|
+
offset += size
|
|
231
|
+
struct_align = _alignof(node, abi)
|
|
232
|
+
rem = offset % struct_align
|
|
233
|
+
if rem:
|
|
234
|
+
offset += struct_align - rem
|
|
235
|
+
return offset, members
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# --------------------------------------------------------------------------- #
|
|
239
|
+
# Comparison
|
|
240
|
+
# --------------------------------------------------------------------------- #
|
|
241
|
+
|
|
242
|
+
def verify_sections(sections, registry: SchemaRegistry, elf_path: str,
|
|
243
|
+
abi: Optional[Dict[str, Any]] = None) -> LoadReport:
|
|
244
|
+
"""Verify every section's cfg struct against the ELF DWARF layout.
|
|
245
|
+
|
|
246
|
+
`sections` is a sequence of (label, type_name, values); only label and
|
|
247
|
+
type_name are used. Each distinct type_name is verified once. Returns a
|
|
248
|
+
LoadReport whose `file` is the ELF path so the UI panels stay uniform.
|
|
249
|
+
"""
|
|
250
|
+
report = LoadReport()
|
|
251
|
+
if not elf_path:
|
|
252
|
+
return report
|
|
253
|
+
|
|
254
|
+
elf_label = os.path.basename(elf_path) or elf_path
|
|
255
|
+
if not os.path.exists(elf_path):
|
|
256
|
+
report.error(elf_label, '', f'ELF file not found: {elf_path}')
|
|
257
|
+
return report
|
|
258
|
+
|
|
259
|
+
wanted = {t for _l, t, _v in sections if t}
|
|
260
|
+
try:
|
|
261
|
+
layout = load_elf_layout(elf_path, wanted)
|
|
262
|
+
except ImportError:
|
|
263
|
+
report.error(
|
|
264
|
+
elf_label, '',
|
|
265
|
+
'pyelftools is required for ELF verification (pip install pyelftools)')
|
|
266
|
+
return report
|
|
267
|
+
except Exception as exc: # noqa: BLE001 - surface any parse failure
|
|
268
|
+
report.error(elf_label, '', f'failed to read DWARF: {exc}')
|
|
269
|
+
return report
|
|
270
|
+
|
|
271
|
+
abi = abi if abi is not None else merge_abi(None)
|
|
272
|
+
|
|
273
|
+
seen: set = set()
|
|
274
|
+
for _label, type_name, _values in sections:
|
|
275
|
+
if not type_name or type_name in seen:
|
|
276
|
+
continue
|
|
277
|
+
seen.add(type_name)
|
|
278
|
+
_verify_type(report, registry, layout, type_name, elf_label, abi)
|
|
279
|
+
return report
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _verify_type(report: LoadReport, registry: SchemaRegistry,
|
|
283
|
+
layout: ElfLayout, type_name: str, elf_label: str,
|
|
284
|
+
abi: Dict[str, Any]) -> None:
|
|
285
|
+
dwarf = layout.get(type_name)
|
|
286
|
+
if dwarf is None:
|
|
287
|
+
report.error(
|
|
288
|
+
elf_label, type_name,
|
|
289
|
+
f"type '{type_name}' not found in ELF DWARF")
|
|
290
|
+
return
|
|
291
|
+
|
|
292
|
+
root = registry.build(type_name)
|
|
293
|
+
total, members = schema_struct_layout(root, abi)
|
|
294
|
+
|
|
295
|
+
# Total struct size.
|
|
296
|
+
if total != dwarf.byte_size:
|
|
297
|
+
report.error(
|
|
298
|
+
elf_label, type_name,
|
|
299
|
+
f"struct size mismatch: schema {total} bytes, "
|
|
300
|
+
f"ELF {dwarf.byte_size} bytes")
|
|
301
|
+
|
|
302
|
+
# Member count / name+order, walked position by position so we can report
|
|
303
|
+
# the first divergence precisely without masking later members.
|
|
304
|
+
n = max(len(members), len(dwarf.members))
|
|
305
|
+
for i in range(n):
|
|
306
|
+
sm = members[i] if i < len(members) else None
|
|
307
|
+
dm = dwarf.members[i] if i < len(dwarf.members) else None
|
|
308
|
+
loc = f'{type_name}[{i}]'
|
|
309
|
+
|
|
310
|
+
if sm is None:
|
|
311
|
+
report.error(
|
|
312
|
+
elf_label, loc,
|
|
313
|
+
f"ELF has extra member '{dm.name}' "
|
|
314
|
+
f"(offset {dm.offset}, size {dm.size}) "
|
|
315
|
+
f"not present in schema")
|
|
316
|
+
continue
|
|
317
|
+
if dm is None:
|
|
318
|
+
report.error(
|
|
319
|
+
elf_label, f'{type_name} -> {sm.name}',
|
|
320
|
+
f"schema member '{sm.name}' has no matching member in ELF")
|
|
321
|
+
continue
|
|
322
|
+
|
|
323
|
+
if sm.name != dm.name:
|
|
324
|
+
report.error(
|
|
325
|
+
elf_label, loc,
|
|
326
|
+
f"member name/order mismatch: schema '{sm.name}', "
|
|
327
|
+
f"ELF '{dm.name}'")
|
|
328
|
+
# Names diverged: positional offset/size comparison would be
|
|
329
|
+
# noise, so move on to the next position.
|
|
330
|
+
continue
|
|
331
|
+
|
|
332
|
+
member_loc = f'{type_name} -> {sm.name}'
|
|
333
|
+
if sm.offset != dm.offset:
|
|
334
|
+
report.error(
|
|
335
|
+
elf_label, member_loc,
|
|
336
|
+
f"offset mismatch: schema {sm.offset}, ELF {dm.offset}")
|
|
337
|
+
if sm.size != dm.size:
|
|
338
|
+
report.error(
|
|
339
|
+
elf_label, member_loc,
|
|
340
|
+
f"size mismatch: schema {sm.size} bytes, "
|
|
341
|
+
f"ELF {dm.size} bytes")
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Pretty-print a pipeline document as JSON with .c-style compaction.
|
|
2
|
+
|
|
3
|
+
Structural heuristic (no schema needed):
|
|
4
|
+
- A list of scalars -> single line: [1, 2, 3]
|
|
5
|
+
- A list of dicts -> one dict per line, each dict on a single line
|
|
6
|
+
- Anything else (dict, or -> normal indented expansion
|
|
7
|
+
mixed/nested lists)
|
|
8
|
+
|
|
9
|
+
The dict-per-line rule mirrors how table widgets render struct arrays in the
|
|
10
|
+
exported .c, keeping the saved JSON readable for the same data.
|
|
11
|
+
|
|
12
|
+
Optional schema-aware multi-line wrapping:
|
|
13
|
+
Callers may pass `wrap_resolver`, a function (path) -> int that, given the
|
|
14
|
+
JSON path to a scalar list (a tuple of dict keys / list indices from the
|
|
15
|
+
document root), returns how many values to place per line (0 = single line).
|
|
16
|
+
This lets `widget=multiline` arrays save as line-broken blocks while the
|
|
17
|
+
printer itself stays schema-agnostic.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
from typing import Any, Callable, Optional, Tuple
|
|
24
|
+
|
|
25
|
+
INDENT = ' '
|
|
26
|
+
|
|
27
|
+
# Wrap scalar lists longer than this many elements onto multiple lines, even
|
|
28
|
+
# without an explicit widget=multiline hint. Keeps saved JSON readable.
|
|
29
|
+
MAX_ROW = 64
|
|
30
|
+
|
|
31
|
+
WrapResolver = Callable[[Tuple[Any, ...]], int]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def dumps(data: Any, wrap_resolver: Optional[WrapResolver] = None) -> str:
|
|
35
|
+
return _emit(data, 0, (), wrap_resolver) + '\n'
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _emit(value: Any, level: int, path: Tuple[Any, ...],
|
|
39
|
+
wrap: Optional[WrapResolver]) -> str:
|
|
40
|
+
if isinstance(value, dict):
|
|
41
|
+
return _emit_dict(value, level, path, wrap)
|
|
42
|
+
if isinstance(value, list):
|
|
43
|
+
return _emit_list(value, level, path, wrap)
|
|
44
|
+
return _scalar(value)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _emit_dict(obj: dict, level: int, path: Tuple[Any, ...],
|
|
48
|
+
wrap: Optional[WrapResolver]) -> str:
|
|
49
|
+
if not obj:
|
|
50
|
+
return '{}'
|
|
51
|
+
pad = INDENT * (level + 1)
|
|
52
|
+
close = INDENT * level
|
|
53
|
+
lines = [
|
|
54
|
+
f'{pad}{json.dumps(k, ensure_ascii=False)}: '
|
|
55
|
+
f'{_emit(v, level + 1, path + (k,), wrap)}'
|
|
56
|
+
for k, v in obj.items()
|
|
57
|
+
]
|
|
58
|
+
return '{\n' + ',\n'.join(lines) + f'\n{close}}}'
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _emit_list(arr: list, level: int, path: Tuple[Any, ...],
|
|
62
|
+
wrap: Optional[WrapResolver]) -> str:
|
|
63
|
+
if not arr:
|
|
64
|
+
return '[]'
|
|
65
|
+
if all(_is_scalar(x) for x in arr):
|
|
66
|
+
row_size = wrap(path) if wrap is not None else 0
|
|
67
|
+
if not (row_size and row_size > 0) and len(arr) > MAX_ROW:
|
|
68
|
+
row_size = MAX_ROW
|
|
69
|
+
if row_size and row_size > 0 and len(arr) > row_size:
|
|
70
|
+
return _emit_wrapped_scalars(arr, level, row_size)
|
|
71
|
+
return '[' + ', '.join(_scalar(x) for x in arr) + ']'
|
|
72
|
+
if all(isinstance(x, dict) for x in arr):
|
|
73
|
+
pad = INDENT * (level + 1)
|
|
74
|
+
close = INDENT * level
|
|
75
|
+
rows = _compact_dicts_aligned(arr, pad)
|
|
76
|
+
return '[\n' + ',\n'.join(rows) + f'\n{close}]'
|
|
77
|
+
pad = INDENT * (level + 1)
|
|
78
|
+
close = INDENT * level
|
|
79
|
+
rows = [pad + _emit(x, level + 1, path + (i,), wrap)
|
|
80
|
+
for i, x in enumerate(arr)]
|
|
81
|
+
return '[\n' + ',\n'.join(rows) + f'\n{close}]'
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _emit_wrapped_scalars(arr: list, level: int, row_size: int) -> str:
|
|
85
|
+
pad = INDENT * (level + 1)
|
|
86
|
+
close = INDENT * level
|
|
87
|
+
cells = [_scalar(x) for x in arr]
|
|
88
|
+
grid = [cells[i:i + row_size] for i in range(0, len(cells), row_size)]
|
|
89
|
+
widths = [max((len(row[j]) for row in grid if j < len(row)), default=0)
|
|
90
|
+
for j in range(row_size)]
|
|
91
|
+
rows = []
|
|
92
|
+
for row in grid:
|
|
93
|
+
padded = [c.ljust(widths[j]) for j, c in enumerate(row)]
|
|
94
|
+
rows.append(pad + ', '.join(padded).rstrip())
|
|
95
|
+
return '[\n' + ',\n'.join(rows) + f'\n{close}]'
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _compact_dicts_aligned(arr: list, pad: str) -> list:
|
|
99
|
+
"""One compact dict per line. When every dict has the same keys in the
|
|
100
|
+
same order (i.e. a table), pad each column so values line up vertically.
|
|
101
|
+
"""
|
|
102
|
+
keyseqs = [tuple(x.keys()) for x in arr]
|
|
103
|
+
uniform = len(arr) > 1 and len(set(keyseqs)) == 1
|
|
104
|
+
if not uniform:
|
|
105
|
+
return [pad + _compact_dict(x) for x in arr]
|
|
106
|
+
|
|
107
|
+
keys = keyseqs[0]
|
|
108
|
+
cols = [[f'{json.dumps(k, ensure_ascii=False)}: {_compact(x[k])}' for k in keys]
|
|
109
|
+
for x in arr]
|
|
110
|
+
widths = [max(len(row[j]) for row in cols) for j in range(len(keys))]
|
|
111
|
+
rows = []
|
|
112
|
+
for row in cols:
|
|
113
|
+
cells = [c.ljust(widths[j]) for j, c in enumerate(row)]
|
|
114
|
+
rows.append(pad + '{' + ', '.join(cells).rstrip() + '}')
|
|
115
|
+
return rows
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _compact_dict(obj: dict) -> str:
|
|
119
|
+
parts = [f'{json.dumps(k, ensure_ascii=False)}: {_compact(v)}'
|
|
120
|
+
for k, v in obj.items()]
|
|
121
|
+
return '{' + ', '.join(parts) + '}'
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _compact(value: Any) -> str:
|
|
125
|
+
if isinstance(value, dict):
|
|
126
|
+
return _compact_dict(value)
|
|
127
|
+
if isinstance(value, list):
|
|
128
|
+
return '[' + ', '.join(_compact(x) for x in value) + ']'
|
|
129
|
+
return _scalar(value)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _is_scalar(value: Any) -> bool:
|
|
133
|
+
return not isinstance(value, (dict, list))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _scalar(value: Any) -> str:
|
|
137
|
+
return json.dumps(value, ensure_ascii=False)
|
struct2ui/icons/c2j.png
ADDED
|
Binary file
|
struct2ui/icons/elf.png
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|