@h2loop/autosar 0.0.0-stage → 0.1.2
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.
- package/.hydron-plugin/plugin.json +3 -0
- package/README.md +53 -2
- package/agents/autosar-integrator.md +44 -0
- package/commands/autosar-trace.md +11 -0
- package/commands/autosar-validate.md +12 -0
- package/package.json +44 -4
- package/plugins/index.js +26 -0
- package/python/ecuc_engine/__init__.py +2 -0
- package/python/ecuc_engine/__main__.py +5 -0
- package/python/ecuc_engine/audit.py +452 -0
- package/python/ecuc_engine/cli.py +125 -0
- package/python/ecuc_engine/defs.py +330 -0
- package/python/ecuc_engine/diff.py +111 -0
- package/python/ecuc_engine/ecucjson.py +154 -0
- package/python/ecuc_engine/edit.py +429 -0
- package/python/ecuc_engine/model.py +332 -0
- package/python/ecuc_engine/rules/__init__.py +82 -0
- package/python/ecuc_engine/rules/com.py +107 -0
- package/python/ecuc_engine/rules/handles.py +42 -0
- package/python/ecuc_engine/validate.py +227 -0
- package/python/ecuc_engine/values.py +146 -0
- package/python/ecuc_engine/xmltree.py +141 -0
- package/python/pyproject.toml +13 -0
- package/skills/autosar/SKILL.md +93 -0
- package/skills/autosar/references/config-classes.md +23 -0
- package/skills/autosar/references/ecuc-model.md +42 -0
- package/skills/autosar/references/project-layout.md +29 -0
- package/skills/autosar/references/symbolic-names.md +25 -0
- package/skills/autosar/references/workflow.md +29 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
"""Deterministic audit of BSW API calls in integration C code against the ECUC configuration.
|
|
2
|
+
|
|
3
|
+
Finds calls that take a configuration handle (Com signals/groups, NvM blocks, Dem events, PduR
|
|
4
|
+
source PDUs; Rte_Write_/Rte_Read_ macros resolved through the generated RTE headers), resolves the
|
|
5
|
+
handle to its ECUC container via the symbolic-name rule (<Mod>Conf_<ContainerDef>_<ShortName>,
|
|
6
|
+
TPS_ECUC_02108) or a raw number via the module's handle-ID parameter, and checks:
|
|
7
|
+
|
|
8
|
+
H2L-API-001 raw numeric handle (use the symbolic name)
|
|
9
|
+
H2L-API-002 unknown handle (symbol or number matches no configured container)
|
|
10
|
+
H2L-API-003 direction mismatch (send on a RECEIVE I-PDU signal, receive on a SEND one)
|
|
11
|
+
H2L-API-004 C object width differs from the signal's ComSignalType / ComBitSize
|
|
12
|
+
H2L-API-005 NvM NULL_PTR without a permanent RAM block or explicit synchronization
|
|
13
|
+
|
|
14
|
+
Width is checked only when the C type is known (compound literal, or a declaration in the same
|
|
15
|
+
translation unit); anything else is listed under "unchecked", never guessed.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from typing import Optional
|
|
23
|
+
|
|
24
|
+
from .model import Elem, Project
|
|
25
|
+
from .values import parse_int
|
|
26
|
+
|
|
27
|
+
C_EXT = (".c", ".h")
|
|
28
|
+
SKIP_DIRS = {".git", "node_modules", ".hydron", "Output", "build", "Build"}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class Api:
|
|
33
|
+
module: str
|
|
34
|
+
defs: tuple # container def names the handle may refer to
|
|
35
|
+
handle_param: str
|
|
36
|
+
kind: str # send | receive | nvm | dem | pdur
|
|
37
|
+
data: Optional[int] # index of the data pointer argument
|
|
38
|
+
cite: str
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
APIS = {
|
|
42
|
+
"Com_SendSignal": Api("Com", ("ComSignal", "ComGroupSignal"), "ComHandleId", "send", 1, "SWS_Com_00197"),
|
|
43
|
+
"Com_ReceiveSignal": Api("Com", ("ComSignal", "ComGroupSignal"), "ComHandleId", "receive", 1, "SWS_Com_00198"),
|
|
44
|
+
"Com_SendSignalGroup": Api("Com", ("ComSignalGroup",), "ComHandleId", "send", None, "SWS_Com_00200"),
|
|
45
|
+
"Com_ReceiveSignalGroup": Api("Com", ("ComSignalGroup",), "ComHandleId", "receive", None, "SWS_Com_00201"),
|
|
46
|
+
"NvM_ReadBlock": Api("NvM", ("NvMBlockDescriptor",), "NvMNvramBlockIdentifier", "nvm", 1, "SWS_NvM_00454"),
|
|
47
|
+
"NvM_WriteBlock": Api("NvM", ("NvMBlockDescriptor",), "NvMNvramBlockIdentifier", "nvm", 1, "SWS_NvM_00455"),
|
|
48
|
+
"NvM_RestoreBlockDefaults": Api("NvM", ("NvMBlockDescriptor",), "NvMNvramBlockIdentifier", "nvm", 1, "SWS_NvM_00456"),
|
|
49
|
+
"Dem_SetEventStatus": Api("Dem", ("DemEventParameter",), "DemEventId", "dem", None, "SWS_Dem_00183"),
|
|
50
|
+
"Dem_ReportErrorStatus": Api("Dem", ("DemEventParameter",), "DemEventId", "dem", None, "SWS_Dem (Dem_ReportErrorStatus, pre-R4.2 releases)"),
|
|
51
|
+
}
|
|
52
|
+
PDUR = Api("PduR", ("PduRSrcPdu",), "PduRSourcePduHandleId", "pdur", None, "SWS_PduR_00406")
|
|
53
|
+
CALL = re.compile(r"\b(Com_(?:Send|Receive)Signal(?:Group)?|NvM_(?:ReadBlock|WriteBlock|RestoreBlockDefaults)|"
|
|
54
|
+
r"Dem_(?:SetEventStatus|ReportErrorStatus)|PduR_\w*Transmit|Rte_(?:Write|Read)_\w+)\s*\(")
|
|
55
|
+
NUM = re.compile(r"^\(?\s*(?:\(\s*\w+\s*\)\s*)?(0[xX][0-9a-fA-F]+|\d+)[uUlL]*\s*\)?$")
|
|
56
|
+
SYM = re.compile(r"^(\w+?)Conf_(\w+?)_(\w+)$")
|
|
57
|
+
NULLS = {"NULL_PTR", "NULL", "0", "((void*)0)", "(void*)0"}
|
|
58
|
+
TYPE_BITS = {"boolean": 8, "uint8": 8, "sint8": 8, "uint16": 16, "sint16": 16, "uint32": 32, "sint32": 32,
|
|
59
|
+
"uint64": 64, "sint64": 64, "float32": 32, "float64": 64, "char": 8, "int8_t": 8, "uint8_t": 8,
|
|
60
|
+
"int16_t": 16, "uint16_t": 16, "int32_t": 32, "uint32_t": 32, "int64_t": 64, "uint64_t": 64,
|
|
61
|
+
"float": 32, "double": 64, "bool": 8}
|
|
62
|
+
SIG_BITS = {"BOOLEAN": 8, "UINT8": 8, "SINT8": 8, "UINT16": 16, "SINT16": 16, "UINT32": 32, "SINT32": 32,
|
|
63
|
+
"UINT64": 64, "SINT64": 64, "FLOAT32": 32, "FLOAT64": 64}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def strip_comments(src: str) -> str:
|
|
67
|
+
"""Blank out comments and string literals, keeping offsets and newlines."""
|
|
68
|
+
out = list(src)
|
|
69
|
+
i, n = 0, len(src)
|
|
70
|
+
while i < n:
|
|
71
|
+
c = src[i]
|
|
72
|
+
if src.startswith("/*", i):
|
|
73
|
+
j = src.find("*/", i + 2)
|
|
74
|
+
j = n if j < 0 else j + 2
|
|
75
|
+
for k in range(i, j):
|
|
76
|
+
if out[k] != "\n":
|
|
77
|
+
out[k] = " "
|
|
78
|
+
i = j
|
|
79
|
+
elif src.startswith("//", i):
|
|
80
|
+
j = src.find("\n", i)
|
|
81
|
+
j = n if j < 0 else j
|
|
82
|
+
for k in range(i, j):
|
|
83
|
+
out[k] = " "
|
|
84
|
+
i = j
|
|
85
|
+
elif c in "\"'":
|
|
86
|
+
j = i + 1
|
|
87
|
+
while j < n and src[j] != c:
|
|
88
|
+
j += 2 if src[j] == "\\" else 1
|
|
89
|
+
for k in range(i + 1, min(j, n)):
|
|
90
|
+
if out[k] != "\n":
|
|
91
|
+
out[k] = " "
|
|
92
|
+
i = j + 1
|
|
93
|
+
else:
|
|
94
|
+
i += 1
|
|
95
|
+
return "".join(out)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def split_args(src: str, open_idx: int):
|
|
99
|
+
"""Args of the call whose '(' is at open_idx -> (list of arg strings, index after ')')."""
|
|
100
|
+
depth, args, cur, i = 0, [], [], open_idx
|
|
101
|
+
while i < len(src):
|
|
102
|
+
c = src[i]
|
|
103
|
+
if c in "([{":
|
|
104
|
+
depth += 1
|
|
105
|
+
if depth > 1:
|
|
106
|
+
cur.append(c)
|
|
107
|
+
elif c in ")]}":
|
|
108
|
+
depth -= 1
|
|
109
|
+
if depth == 0:
|
|
110
|
+
args.append("".join(cur).strip())
|
|
111
|
+
return [a for a in args if a != "" or len(args) > 1], i + 1
|
|
112
|
+
cur.append(c)
|
|
113
|
+
elif c == "," and depth == 1:
|
|
114
|
+
args.append("".join(cur).strip())
|
|
115
|
+
cur = []
|
|
116
|
+
else:
|
|
117
|
+
cur.append(c)
|
|
118
|
+
i += 1
|
|
119
|
+
return None, i
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def parse_defines(files: list) -> dict:
|
|
123
|
+
"""NAME -> (params|None, body, file, line) for every #define in the given headers."""
|
|
124
|
+
out: dict = {}
|
|
125
|
+
rx = re.compile(r"^[ \t]*#[ \t]*define[ \t]+(\w+)(\([^)]*\))?[ \t]*(.*)$")
|
|
126
|
+
for f in files:
|
|
127
|
+
try:
|
|
128
|
+
text = strip_comments(open(f, encoding="utf-8", errors="replace").read())
|
|
129
|
+
except OSError:
|
|
130
|
+
continue
|
|
131
|
+
lines = text.split("\n")
|
|
132
|
+
i = 0
|
|
133
|
+
while i < len(lines):
|
|
134
|
+
line = lines[i]
|
|
135
|
+
start = i
|
|
136
|
+
while line.endswith("\\") and i + 1 < len(lines):
|
|
137
|
+
i += 1
|
|
138
|
+
line = line[:-1] + " " + lines[i]
|
|
139
|
+
m = rx.match(line)
|
|
140
|
+
if m:
|
|
141
|
+
params = [p.strip() for p in m.group(2)[1:-1].split(",")] if m.group(2) else None
|
|
142
|
+
out.setdefault(m.group(1), (params, m.group(3).strip(), f, start + 1))
|
|
143
|
+
i += 1
|
|
144
|
+
return out
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _files(root: str, paths: list, exts=C_EXT, skip=SKIP_DIRS) -> list:
|
|
148
|
+
res = []
|
|
149
|
+
for p in paths:
|
|
150
|
+
full = p if os.path.isabs(p) else os.path.join(root, p)
|
|
151
|
+
if os.path.isfile(full):
|
|
152
|
+
res.append(full)
|
|
153
|
+
continue
|
|
154
|
+
for dp, dns, fns in os.walk(full):
|
|
155
|
+
dns[:] = sorted(d for d in dns if d not in skip and not d.startswith("."))
|
|
156
|
+
res.extend(os.path.join(dp, fn) for fn in sorted(fns) if fn.endswith(exts))
|
|
157
|
+
return res
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class Auditor:
|
|
161
|
+
def __init__(self, project: Project, defines: dict):
|
|
162
|
+
self.p = project
|
|
163
|
+
self.defines = defines
|
|
164
|
+
self.findings: list = []
|
|
165
|
+
self.unchecked: list = []
|
|
166
|
+
self.calls = 0
|
|
167
|
+
self._handles: dict = {}
|
|
168
|
+
|
|
169
|
+
# ---------- ECUC lookups ----------
|
|
170
|
+
def module(self, name: str) -> list:
|
|
171
|
+
return [m for m in self.p.modules if (m.def_ref or "").rsplit("/", 1)[-1] == name or m.name == name]
|
|
172
|
+
|
|
173
|
+
def containers(self, module: str, defnames: tuple) -> list:
|
|
174
|
+
out = []
|
|
175
|
+
for m in self.module(module):
|
|
176
|
+
stack = list(m.children)
|
|
177
|
+
while stack:
|
|
178
|
+
e = stack.pop()
|
|
179
|
+
stack.extend(e.children)
|
|
180
|
+
if (e.def_ref or "").rsplit("/", 1)[-1] in defnames:
|
|
181
|
+
out.append(e)
|
|
182
|
+
return out
|
|
183
|
+
|
|
184
|
+
def val(self, e: Elem, name: str) -> Optional[str]:
|
|
185
|
+
for g in self.p.find(e.path):
|
|
186
|
+
for pv in g.params:
|
|
187
|
+
if pv.name == name:
|
|
188
|
+
return pv.value
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
def by_handle(self, api: Api, num: int) -> list:
|
|
192
|
+
key = (api.module, api.defs)
|
|
193
|
+
if key not in self._handles:
|
|
194
|
+
idx: dict = {}
|
|
195
|
+
for e in self.containers(api.module, api.defs):
|
|
196
|
+
v = parse_int(self.val(e, api.handle_param) or "")
|
|
197
|
+
if v is not None:
|
|
198
|
+
idx.setdefault(v, []).append(e)
|
|
199
|
+
self._handles[key] = idx
|
|
200
|
+
return self._handles[key].get(num, [])
|
|
201
|
+
|
|
202
|
+
def by_symbol(self, api: Api, sym: str) -> Optional[Elem]:
|
|
203
|
+
m = SYM.match(sym)
|
|
204
|
+
if not m:
|
|
205
|
+
return None
|
|
206
|
+
mod, dname, short = m.groups()
|
|
207
|
+
if mod != api.module:
|
|
208
|
+
return None
|
|
209
|
+
for e in self.containers(api.module, (dname,)):
|
|
210
|
+
if e.name == short:
|
|
211
|
+
return e
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
def symbol_of(self, e: Elem, api: Api) -> str:
|
|
215
|
+
return f"{api.module}Conf_{(e.def_ref or '').rsplit('/', 1)[-1]}_{e.name}"
|
|
216
|
+
|
|
217
|
+
def ipdus_of(self, sig: Elem) -> list:
|
|
218
|
+
"""I-PDUs that reference this signal (or, for a group signal, its signal group)."""
|
|
219
|
+
targets = {sig.path}
|
|
220
|
+
if (sig.def_ref or "").endswith("/ComGroupSignal") and sig.parent is not None:
|
|
221
|
+
targets = {sig.parent.path}
|
|
222
|
+
out = []
|
|
223
|
+
for ip in self.containers("Com", ("ComIPdu",)):
|
|
224
|
+
for g in self.p.find(ip.path):
|
|
225
|
+
if any(pv.is_ref and pv.name in ("ComIPduSignalRef", "ComIPduSignalGroupRef") and (pv.value or "").strip() in targets
|
|
226
|
+
for pv in g.params):
|
|
227
|
+
out.append(ip)
|
|
228
|
+
break
|
|
229
|
+
return out
|
|
230
|
+
|
|
231
|
+
# ---------- findings ----------
|
|
232
|
+
def add(self, rule, sev, f, line, call, msg, cite, evidence, fix=None):
|
|
233
|
+
d = {"rule": rule, "severity": sev, "file": self.p.rel(f), "line": line, "call": call,
|
|
234
|
+
"message": msg, "cite": cite, "evidence": evidence}
|
|
235
|
+
if fix:
|
|
236
|
+
d["fix"] = fix
|
|
237
|
+
self.findings.append(d)
|
|
238
|
+
|
|
239
|
+
# ---------- per call ----------
|
|
240
|
+
def handle_value(self, expr: str):
|
|
241
|
+
"""-> ("num", int) | ("sym", name, resolved_int|None) | ("expr", text)."""
|
|
242
|
+
e = expr.strip()
|
|
243
|
+
m = NUM.match(e)
|
|
244
|
+
if m:
|
|
245
|
+
return ("num", int(m.group(1), 0))
|
|
246
|
+
while e.startswith("(") and e.endswith(")") and split_args("x" + e, 1)[1] == len(e) + 1:
|
|
247
|
+
e = e[1:-1].strip()
|
|
248
|
+
if re.fullmatch(r"\w+", e):
|
|
249
|
+
d = self.defines.get(e)
|
|
250
|
+
num = None
|
|
251
|
+
if d and d[0] is None:
|
|
252
|
+
mm = NUM.match(d[1])
|
|
253
|
+
num = int(mm.group(1), 0) if mm else None
|
|
254
|
+
return ("sym", e, num)
|
|
255
|
+
return ("expr", e)
|
|
256
|
+
|
|
257
|
+
def audit_call(self, f: str, line: int, name: str, args: list, unit: str, via: Optional[str] = None):
|
|
258
|
+
if name.startswith("Rte_"):
|
|
259
|
+
self.audit_rte(f, line, name, args, unit)
|
|
260
|
+
return
|
|
261
|
+
api = APIS.get(name) or (PDUR if name.startswith("PduR_") else None)
|
|
262
|
+
if api is None or not args:
|
|
263
|
+
return
|
|
264
|
+
self.calls += 1
|
|
265
|
+
call = f"{name}({', '.join(args)})" + (f" via {via}" if via else "")
|
|
266
|
+
h = self.handle_value(args[0])
|
|
267
|
+
target: Optional[Elem] = None
|
|
268
|
+
cite_sym = "TPS_ECUC_02108 symbolic name values"
|
|
269
|
+
if h[0] == "num":
|
|
270
|
+
hits = self.by_handle(api, h[1])
|
|
271
|
+
if not hits:
|
|
272
|
+
self.add("H2L-API-002", "error", f, line, call,
|
|
273
|
+
f"handle {args[0]} matches no {'/'.join(api.defs)} with {api.handle_param}={h[1]}",
|
|
274
|
+
f"{api.cite}; {cite_sym}", {"handleParam": api.handle_param, "value": h[1]})
|
|
275
|
+
return
|
|
276
|
+
target = hits[0]
|
|
277
|
+
sym = self.symbol_of(target, api)
|
|
278
|
+
self.add("H2L-API-001", "warning", f, line, call,
|
|
279
|
+
f"raw numeric handle {args[0]} = {target.path} ({api.handle_param}={h[1]}); use {sym} so the call "
|
|
280
|
+
"survives handle re-assignment", f"{cite_sym}; TPS_ECUConfiguration 3.4.1.1",
|
|
281
|
+
{"path": target.path, api.handle_param: h[1]}, fix=f"replace {args[0]} with {sym}")
|
|
282
|
+
elif h[0] == "sym":
|
|
283
|
+
target = self.by_symbol(api, h[1])
|
|
284
|
+
if target is None and h[2] is not None:
|
|
285
|
+
hits = self.by_handle(api, h[2])
|
|
286
|
+
target = hits[0] if hits else None
|
|
287
|
+
if target is None:
|
|
288
|
+
if SYM.match(h[1]) or h[2] is not None:
|
|
289
|
+
self.add("H2L-API-002", "error", f, line, call,
|
|
290
|
+
f"handle {h[1]} resolves to no configured {'/'.join(api.defs)} in {api.module}",
|
|
291
|
+
f"{api.cite}; {cite_sym}", {"symbol": h[1], "define": h[2]})
|
|
292
|
+
else:
|
|
293
|
+
self.unchecked.append({"file": self.p.rel(f), "line": line, "call": call,
|
|
294
|
+
"reason": f"handle {h[1]} is not a configured symbol or constant"})
|
|
295
|
+
return
|
|
296
|
+
else:
|
|
297
|
+
self.unchecked.append({"file": self.p.rel(f), "line": line, "call": call, "reason": "handle is an expression"})
|
|
298
|
+
return
|
|
299
|
+
if api.kind in ("send", "receive"):
|
|
300
|
+
self.check_direction(f, line, call, api, target)
|
|
301
|
+
if api.data is not None and len(args) > api.data:
|
|
302
|
+
self.check_width(f, line, call, target, args[api.data], unit)
|
|
303
|
+
if api.kind == "nvm" and api.data is not None and len(args) > api.data:
|
|
304
|
+
self.check_nvm_null(f, line, call, name, target, args[api.data])
|
|
305
|
+
|
|
306
|
+
def audit_rte(self, f, line, name, args, unit):
|
|
307
|
+
d = self.defines.get(name)
|
|
308
|
+
if d is None or d[0] is None:
|
|
309
|
+
self.unchecked.append({"file": self.p.rel(f), "line": line, "call": f"{name}(...)",
|
|
310
|
+
"reason": "RTE API not a macro in the generated headers (function-mapped)"})
|
|
311
|
+
return
|
|
312
|
+
params, body = d[0], d[1]
|
|
313
|
+
subst = body
|
|
314
|
+
for prm, arg in zip(params, args):
|
|
315
|
+
subst = re.sub(rf"\b{re.escape(prm)}\b", lambda _m, a=arg: f"({a})", subst)
|
|
316
|
+
clean = strip_comments(subst)
|
|
317
|
+
for m in CALL.finditer(clean):
|
|
318
|
+
inner, _ = split_args(clean, m.end() - 1)
|
|
319
|
+
if inner is not None and not m.group(1).startswith("Rte_"):
|
|
320
|
+
self.audit_call(f, line, m.group(1), inner, unit, via=f"{name} ({self.p.rel(d[2])}:{d[3]})")
|
|
321
|
+
|
|
322
|
+
def check_direction(self, f, line, call, api, sig):
|
|
323
|
+
ipdus = self.ipdus_of(sig)
|
|
324
|
+
dirs = [(ip, (self.val(ip, "ComIPduDirection") or "").strip()) for ip in ipdus]
|
|
325
|
+
want = "SEND" if api.kind == "send" else "RECEIVE"
|
|
326
|
+
bad = [(ip, d) for ip, d in dirs if d and d != want]
|
|
327
|
+
if bad and not any(d == want for _, d in dirs):
|
|
328
|
+
ip, d = bad[0]
|
|
329
|
+
self.add("H2L-API-003", "error", f, line, call,
|
|
330
|
+
f"{sig.name} belongs to I-PDU {ip.name} with ComIPduDirection={d}; {call.split('(')[0]} needs a "
|
|
331
|
+
f"{want} signal (Com returns COM_SERVICE_NOT_AVAILABLE / E_NOT_OK)",
|
|
332
|
+
f"ECUC_Com_00493 ComIPduDirection; {api.cite}",
|
|
333
|
+
{"signal": sig.path, "ipdu": ip.path, "ComIPduDirection": d})
|
|
334
|
+
|
|
335
|
+
def c_type(self, expr: str, unit: str):
|
|
336
|
+
"""Type of the object a data-pointer argument points to -> (type, array_len|None), or None if unknown."""
|
|
337
|
+
e = expr.strip()
|
|
338
|
+
while e.startswith("(") and e.endswith(")") and split_args("x" + e, 1)[1] == len(e) + 1:
|
|
339
|
+
e = e[1:-1].strip()
|
|
340
|
+
lit = re.fullmatch(r"&\s*\(\s*(\w+)\s*\)\s*\{.*\}", e, re.S) # &(uint8){ x }
|
|
341
|
+
if lit:
|
|
342
|
+
return lit.group(1), None
|
|
343
|
+
ref = re.fullmatch(r"&\s*(\w+)", e) # &var
|
|
344
|
+
elem = re.fullmatch(r"&\s*(\w+)\s*\[[^\]]*\]", e) # &buf[i]
|
|
345
|
+
bare = re.fullmatch(r"(\w+)", e) # buf (array decays) or a pointer variable
|
|
346
|
+
var = (ref or elem or bare).group(1) if (ref or elem or bare) else None
|
|
347
|
+
if var is None:
|
|
348
|
+
return None
|
|
349
|
+
decl = re.search(rf"\b(?:(?:static|volatile|const|extern)\s+)*(\w+)\s+(?:const\s+)?{re.escape(var)}"
|
|
350
|
+
rf"\s*(\[\s*(\w*)\s*\])?\s*[;=,)]", unit)
|
|
351
|
+
if not decl or decl.group(1) in ("return", "else", "case", "goto", "sizeof", "struct", "union", "enum"):
|
|
352
|
+
return None
|
|
353
|
+
is_array = bool(decl.group(2))
|
|
354
|
+
if ref:
|
|
355
|
+
return decl.group(1), (decl.group(3) if is_array else None)
|
|
356
|
+
if elem or (bare and is_array):
|
|
357
|
+
return decl.group(1), None
|
|
358
|
+
return None # bare non-array variable: a pointer whose pointee is not visible here
|
|
359
|
+
|
|
360
|
+
def check_width(self, f, line, call, sig, arg, unit):
|
|
361
|
+
typ = (self.val(sig, "ComSignalType") or "").strip()
|
|
362
|
+
if typ in ("UINT8_N", "UINT8_DYN"):
|
|
363
|
+
self.unchecked.append({"file": self.p.rel(f), "line": line, "call": call, "reason": f"array signal type {typ}"})
|
|
364
|
+
return
|
|
365
|
+
expect = SIG_BITS.get(typ)
|
|
366
|
+
ct = self.c_type(arg, unit)
|
|
367
|
+
if expect is None or ct is None or ct[0] not in TYPE_BITS:
|
|
368
|
+
self.unchecked.append({"file": self.p.rel(f), "line": line, "call": call,
|
|
369
|
+
"reason": f"width unknown (C type {ct[0] if ct else '?'}, ComSignalType {typ or '?'})"})
|
|
370
|
+
return
|
|
371
|
+
cbits = TYPE_BITS[ct[0]]
|
|
372
|
+
if ct[1]:
|
|
373
|
+
n = parse_int(ct[1])
|
|
374
|
+
cbits = cbits * n if n is not None else cbits
|
|
375
|
+
if cbits != expect:
|
|
376
|
+
bs = self.val(sig, "ComBitSize")
|
|
377
|
+
self.add("H2L-API-004", "error", f, line, call,
|
|
378
|
+
f"{arg} is a {ct[0]}{'[' + ct[1] + ']' if ct[1] else ''} ({cbits} bits) but {sig.name} is "
|
|
379
|
+
f"ComSignalType={typ} ({expect // 8}-byte object, ComBitSize={bs}); Com accesses {expect // 8} "
|
|
380
|
+
f"byte(s) at that address — wrong signal or wrong variable",
|
|
381
|
+
"SWS_Com_00197/00198 SignalDataPtr points to an object of the ComSignalType; ECUC_Com_00127 ComSignalType",
|
|
382
|
+
{"signal": sig.path, "ComSignalType": typ, "ComBitSize": bs, "cType": ct[0]})
|
|
383
|
+
|
|
384
|
+
def check_nvm_null(self, f, line, call, name, blk, arg):
|
|
385
|
+
if arg.replace(" ", "") not in NULLS:
|
|
386
|
+
return
|
|
387
|
+
ram = (self.val(blk, "NvMRamBlockDataAddress") or "").strip()
|
|
388
|
+
sync = (self.val(blk, "NvMBlockUseSyncMechanism") or "").strip().lower() in ("true", "1")
|
|
389
|
+
if ram or sync:
|
|
390
|
+
return # valid: SWS_NvM_00898/00900 (permanent RAM) or 00899/00901 (explicit sync)
|
|
391
|
+
self.add("H2L-API-005", "error", f, line, call,
|
|
392
|
+
f"{name} with NULL_PTR on {blk.name}, which has no permanent RAM block (NvMRamBlockDataAddress unset) and "
|
|
393
|
+
"no explicit synchronization; NvM rejects the request (DET parameter error)",
|
|
394
|
+
"SWS_NvM_00898/00899 (ReadBlock), SWS_NvM_00900/00901 (WriteBlock), SWS_NvM_00278/00280",
|
|
395
|
+
{"block": blk.path, "NvMRamBlockDataAddress": None, "NvMBlockUseSyncMechanism": sync})
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def audit(project: Project, paths: Optional[list] = None, headers: Optional[list] = None) -> dict:
|
|
399
|
+
root = project.root
|
|
400
|
+
cfg = {}
|
|
401
|
+
try:
|
|
402
|
+
import json
|
|
403
|
+
with open(os.path.join(root, ".hydron", "autosar.json")) as fh:
|
|
404
|
+
raw = json.load(fh).get("audit")
|
|
405
|
+
cfg = raw if isinstance(raw, dict) else ({"paths": raw} if isinstance(raw, list) else {})
|
|
406
|
+
except (OSError, ValueError):
|
|
407
|
+
pass
|
|
408
|
+
paths = paths or cfg.get("paths") or (["Source"] if os.path.isdir(os.path.join(root, "Source")) else ["."])
|
|
409
|
+
headers = headers or cfg.get("headers") or [d for d in ("Output",) if os.path.isdir(os.path.join(root, d))]
|
|
410
|
+
sources = [f for f in _files(root, paths) if f.endswith(".c")]
|
|
411
|
+
hdrs = _files(root, headers, exts=(".h",), skip={".git", "node_modules"}) + [f for f in _files(root, paths) if f.endswith(".h")]
|
|
412
|
+
a = Auditor(project, parse_defines(hdrs))
|
|
413
|
+
for f in sources:
|
|
414
|
+
try:
|
|
415
|
+
unit = strip_comments(open(f, encoding="utf-8", errors="replace").read())
|
|
416
|
+
except OSError:
|
|
417
|
+
continue
|
|
418
|
+
for m in CALL.finditer(unit):
|
|
419
|
+
ls = unit.rfind("\n", 0, m.start()) + 1
|
|
420
|
+
if unit[ls:m.start()].lstrip().startswith("#"):
|
|
421
|
+
continue # macro definition, not a call
|
|
422
|
+
before = unit[ls:m.start()].rstrip()
|
|
423
|
+
prev = re.search(r"(\w+)\s*$", before)
|
|
424
|
+
if prev and prev.group(1) not in ("return", "void") and not before.endswith(")"):
|
|
425
|
+
continue # a declaration/prototype ("Std_ReturnType Com_SendSignal(")
|
|
426
|
+
args, _ = split_args(unit, m.end() - 1)
|
|
427
|
+
if args is None:
|
|
428
|
+
continue
|
|
429
|
+
a.audit_call(f, unit.count("\n", 0, m.start()) + 1, m.group(1), args, unit)
|
|
430
|
+
a.findings.sort(key=lambda d: (d["file"], d["line"], d["rule"]))
|
|
431
|
+
summary: dict = {}
|
|
432
|
+
for d in a.findings:
|
|
433
|
+
summary[d["severity"]] = summary.get(d["severity"], 0) + 1
|
|
434
|
+
return {"scanned": {"sources": [project.rel(f) for f in sources], "headers": len(hdrs), "calls": a.calls},
|
|
435
|
+
"summary": summary, "findings": a.findings, "unchecked": a.unchecked}
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def signal_owners(project: Project, paths: list) -> dict:
|
|
439
|
+
"""AUTOSAR path of a ComSignal / ComGroupSignal -> [{ipdu, direction, pdu}] from the local ECUC."""
|
|
440
|
+
a = Auditor(project, {})
|
|
441
|
+
out: dict = {}
|
|
442
|
+
for p in paths:
|
|
443
|
+
g = project.find(p)
|
|
444
|
+
if not g:
|
|
445
|
+
out[p] = None # not in the local checkout
|
|
446
|
+
continue
|
|
447
|
+
sig = g[0]
|
|
448
|
+
if not (sig.def_ref or "").endswith(("/ComSignal", "/ComGroupSignal", "/ComSignalGroup")):
|
|
449
|
+
continue
|
|
450
|
+
out[p] = [{"ipdu": ip.path, "direction": (a.val(ip, "ComIPduDirection") or "").strip() or None,
|
|
451
|
+
"pdu": (a.val(ip, "ComPduIdRef") or "").strip() or None} for ip in a.ipdus_of(sig)]
|
|
452
|
+
return out
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""CLI: python -m ecuc_engine [--root DIR] <info|read|edit|validate|diff> ... --json
|
|
2
|
+
|
|
3
|
+
Always prints one JSON object on stdout. {"ok": false, "error": ...} on failure (exit code 0 for
|
|
4
|
+
handled errors, so callers parse stdout rather than the status).
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
import time
|
|
13
|
+
|
|
14
|
+
from . import ENGINE_VERSION
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _ops(arg: str):
|
|
18
|
+
if arg.startswith("@"):
|
|
19
|
+
with open(arg[1:]) as f:
|
|
20
|
+
arg = f.read()
|
|
21
|
+
if arg == "-":
|
|
22
|
+
arg = sys.stdin.read()
|
|
23
|
+
ops = json.loads(arg)
|
|
24
|
+
for _ in range(3): # tolerate JSON that was encoded more than once ("[{...}]" as a string)
|
|
25
|
+
if isinstance(ops, str):
|
|
26
|
+
ops = json.loads(ops)
|
|
27
|
+
if isinstance(ops, dict) and isinstance(ops.get("ops"), (list, str)):
|
|
28
|
+
return _ops(json.dumps(ops["ops"]))
|
|
29
|
+
return ops if isinstance(ops, list) else [ops]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def main(argv=None) -> int:
|
|
33
|
+
ap = argparse.ArgumentParser(prog="ecuc_engine")
|
|
34
|
+
ap.add_argument("--root", default=os.getcwd(), help="project root (default: cwd)")
|
|
35
|
+
ap.add_argument("--defs", action="append", help="param-def file/dir/glob (overrides discovery)")
|
|
36
|
+
ap.add_argument("--config", action="append", help="ECUC value file/dir/glob (overrides discovery)")
|
|
37
|
+
ap.add_argument("--json", action="store_true", help="JSON output (always on; accepted for clarity)")
|
|
38
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
39
|
+
sub.add_parser("info")
|
|
40
|
+
r = sub.add_parser("read")
|
|
41
|
+
r.add_argument("path", nargs="?")
|
|
42
|
+
r.add_argument("--depth", type=int, default=2)
|
|
43
|
+
r.add_argument("--schema", action="store_true")
|
|
44
|
+
e = sub.add_parser("edit")
|
|
45
|
+
e.add_argument("--ops", required=True, help="JSON op or list, @file, or - for stdin")
|
|
46
|
+
e.add_argument("--dry-run", action="store_true")
|
|
47
|
+
e.add_argument("--force", action="store_true")
|
|
48
|
+
e.add_argument("--no-validate", action="store_true")
|
|
49
|
+
v = sub.add_parser("validate")
|
|
50
|
+
v.add_argument("--file", action="append")
|
|
51
|
+
v.add_argument("--module")
|
|
52
|
+
v.add_argument("--max", type=int, default=0, help="cap the number of diagnostics returned (0 = all)")
|
|
53
|
+
au = sub.add_parser("audit")
|
|
54
|
+
au.add_argument("paths", nargs="*", help="C files/dirs to scan (default: audit.paths in .hydron/autosar.json, else Source/)")
|
|
55
|
+
au.add_argument("--headers", action="append", help="generated header dirs for symbols and RTE macros (default Output/)")
|
|
56
|
+
so = sub.add_parser("signal-owners")
|
|
57
|
+
so.add_argument("paths", nargs="+")
|
|
58
|
+
d = sub.add_parser("diff")
|
|
59
|
+
d.add_argument("a")
|
|
60
|
+
d.add_argument("b")
|
|
61
|
+
for p in (r, e, v, d, au, so): # allow --json after the subcommand too
|
|
62
|
+
p.add_argument("--json", action="store_true", help=argparse.SUPPRESS)
|
|
63
|
+
a = ap.parse_args(argv)
|
|
64
|
+
t0 = time.time()
|
|
65
|
+
try:
|
|
66
|
+
res = run(a)
|
|
67
|
+
except Exception as ex: # noqa: BLE001
|
|
68
|
+
res = {"ok": False, "error": f"{type(ex).__name__}: {ex}"}
|
|
69
|
+
res.setdefault("ok", "error" not in res)
|
|
70
|
+
res["engine"] = ENGINE_VERSION
|
|
71
|
+
res["ms"] = int((time.time() - t0) * 1000)
|
|
72
|
+
sys.stdout.write(json.dumps(res, ensure_ascii=False, indent=None if a.json else 1, default=str) + "\n")
|
|
73
|
+
return 0
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def run(a) -> dict:
|
|
77
|
+
if a.cmd == "edit":
|
|
78
|
+
try:
|
|
79
|
+
a.ops = _ops(a.ops)
|
|
80
|
+
except (ValueError, OSError) as ex:
|
|
81
|
+
return {"error": f"ops is not valid JSON: {ex}"}
|
|
82
|
+
if a.cmd == "diff":
|
|
83
|
+
from .diff import diff
|
|
84
|
+
for x in (a.a, a.b):
|
|
85
|
+
if not os.path.exists(x):
|
|
86
|
+
return {"error": f"{x} does not exist"}
|
|
87
|
+
root = os.path.abspath(a.root)
|
|
88
|
+
rel = lambda f: os.path.relpath(f, root) if not os.path.relpath(f, root).startswith("..") else f # noqa: E731
|
|
89
|
+
return diff(a.a, a.b, rel)
|
|
90
|
+
from .model import Project
|
|
91
|
+
project = Project(a.root, config_files=a.config, def_files=a.defs, load_input=a.cmd in ("validate", "edit"))
|
|
92
|
+
if a.cmd == "signal-owners":
|
|
93
|
+
from .audit import signal_owners
|
|
94
|
+
return {"owners": signal_owners(project, a.paths)}
|
|
95
|
+
if a.cmd == "audit":
|
|
96
|
+
from .audit import audit
|
|
97
|
+
res = audit(project, paths=a.paths or None, headers=a.headers)
|
|
98
|
+
if project.warnings:
|
|
99
|
+
res["warnings"] = project.warnings
|
|
100
|
+
return res
|
|
101
|
+
if a.cmd == "info":
|
|
102
|
+
s = project.sources
|
|
103
|
+
return {"root": s.root, "origin": s.origin, "config": [project.rel(f) for f in s.config],
|
|
104
|
+
"paramDefs": [project.rel(f) for f in s.defs], "input": [project.rel(f) for f in s.input][:200],
|
|
105
|
+
"defModules": len(project.defs.modules), "modules": sorted({m.name for m in project.modules}),
|
|
106
|
+
"warnings": project.warnings}
|
|
107
|
+
if a.cmd == "read":
|
|
108
|
+
from .ecucjson import read
|
|
109
|
+
return read(project, a.path, depth=a.depth, schema=a.schema)
|
|
110
|
+
if a.cmd == "edit":
|
|
111
|
+
from .edit import edit
|
|
112
|
+
res = edit(project, a.ops, dry_run=a.dry_run, force=a.force, run_validate=not a.no_validate)
|
|
113
|
+
if project.warnings:
|
|
114
|
+
res["warnings"] = project.warnings
|
|
115
|
+
return res
|
|
116
|
+
if a.cmd == "validate":
|
|
117
|
+
from .validate import validate
|
|
118
|
+
res = validate(project, files=a.file, module=a.module)
|
|
119
|
+
if a.max and len(res["diagnostics"]) > a.max:
|
|
120
|
+
res["truncated"] = len(res["diagnostics"]) - a.max
|
|
121
|
+
res["diagnostics"] = res["diagnostics"][: a.max]
|
|
122
|
+
if project.warnings:
|
|
123
|
+
res["warnings"] = project.warnings
|
|
124
|
+
return res
|
|
125
|
+
return {"error": f"unknown command {a.cmd}"}
|