wireforge 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.
- wireforge/__init__.py +3 -0
- wireforge/_data/cmake/WireForgeConfig.cmake +29 -0
- wireforge/_data/cmake/WireForgeConfigVersion.cmake +15 -0
- wireforge/_data/cmake/WireForgeGenerate.cmake +64 -0
- wireforge/_data/runtime/LICENSE +21 -0
- wireforge/_data/runtime/include/wireforge/array_access.hpp +35 -0
- wireforge/_data/runtime/include/wireforge/bit_utils.hpp +226 -0
- wireforge/_data/runtime/include/wireforge/byte_io.hpp +387 -0
- wireforge/_data/runtime/include/wireforge/checksum.hpp +201 -0
- wireforge/_data/runtime/include/wireforge/codec_support.hpp +412 -0
- wireforge/_data/runtime/include/wireforge/config.hpp +41 -0
- wireforge/_data/runtime/include/wireforge/delegate.hpp +101 -0
- wireforge/_data/runtime/include/wireforge/dispatcher.hpp +339 -0
- wireforge/_data/runtime/include/wireforge/expr_support.hpp +38 -0
- wireforge/_data/runtime/include/wireforge/frame_decoder.hpp +262 -0
- wireforge/_data/runtime/include/wireforge/frame_layout.hpp +20 -0
- wireforge/_data/runtime/include/wireforge/link.hpp +223 -0
- wireforge/_data/runtime/include/wireforge/lock.hpp +146 -0
- wireforge/_data/runtime/include/wireforge/ring_buffer.hpp +226 -0
- wireforge/_data/runtime/include/wireforge/span.hpp +116 -0
- wireforge/_data/runtime/include/wireforge/static_vector.hpp +264 -0
- wireforge/_data/runtime/include/wireforge/status.hpp +75 -0
- wireforge/_data/runtime/include/wireforge/transport.hpp +95 -0
- wireforge/_data/runtime/include/wireforge/validation.hpp +153 -0
- wireforge/_data/runtime/include/wireforge/version.hpp +22 -0
- wireforge/_data/runtime/requirements.yaml +438 -0
- wireforge/_data/schema/deviations.schema.json +47 -0
- wireforge/_data/schema/fragment.schema.json +79 -0
- wireforge/_data/schema/llr_ids.schema.json +25 -0
- wireforge/_data/schema/project.schema.json +1240 -0
- wireforge/_data/schema/runtime_requirements.schema.json +34 -0
- wireforge/_data/ui/assets/index-6Pomu6U4.js +44 -0
- wireforge/_data/ui/assets/index-GVdDyUAG.css +1 -0
- wireforge/_data/ui/index.html +13 -0
- wireforge/bundle.py +177 -0
- wireforge/ci_check.py +155 -0
- wireforge/cli.py +617 -0
- wireforge/codec.py +897 -0
- wireforge/compat.py +536 -0
- wireforge/coverage_points.py +64 -0
- wireforge/cpp.py +273 -0
- wireforge/cpp_codec.py +440 -0
- wireforge/cpp_interface.py +874 -0
- wireforge/cpp_tests.py +1993 -0
- wireforge/differential.py +586 -0
- wireforge/docs_markdown.py +72 -0
- wireforge/expr.py +598 -0
- wireforge/findings.py +187 -0
- wireforge/fmt.py +351 -0
- wireforge/generate.py +644 -0
- wireforge/golden.py +253 -0
- wireforge/hlr_import.py +112 -0
- wireforge/impact.py +249 -0
- wireforge/importers.py +402 -0
- wireforge/issues.py +27 -0
- wireforge/layout.py +681 -0
- wireforge/lifecycle.py +1266 -0
- wireforge/lifecycle_results.py +205 -0
- wireforge/lifecycle_rules.py +78 -0
- wireforge/lifecycle_runtime.py +375 -0
- wireforge/llr.py +693 -0
- wireforge/llr_ids.py +104 -0
- wireforge/loader.py +422 -0
- wireforge/mcp_server.py +357 -0
- wireforge/model.py +322 -0
- wireforge/plain.py +15 -0
- wireforge/py.typed +0 -0
- wireforge/reqif.py +289 -0
- wireforge/resources.py +43 -0
- wireforge/results.py +377 -0
- wireforge/runtime_reqs.py +147 -0
- wireforge/schema.py +151 -0
- wireforge/server/__init__.py +1 -0
- wireforge/server/app.py +619 -0
- wireforge/server/oidc.py +99 -0
- wireforge/server/storage.py +146 -0
- wireforge/service.py +1354 -0
- wireforge/templates/command/project.yaml +38 -0
- wireforge/templates/raw/project.yaml +28 -0
- wireforge/templates/telemetry/project.yaml +51 -0
- wireforge/test_cases.py +475 -0
- wireforge/test_project.py +478 -0
- wireforge/trace.py +442 -0
- wireforge/validate.py +1241 -0
- wireforge/vcs.py +44 -0
- wireforge/vectors.py +222 -0
- wireforge/versioning.py +137 -0
- wireforge/wireshark.py +792 -0
- wireforge/wireshark_runtime.lua +725 -0
- wireforge-0.1.0.dist-info/METADATA +91 -0
- wireforge-0.1.0.dist-info/RECORD +96 -0
- wireforge-0.1.0.dist-info/WHEEL +5 -0
- wireforge-0.1.0.dist-info/entry_points.txt +2 -0
- wireforge-0.1.0.dist-info/licenses/LICENSE +98 -0
- wireforge-0.1.0.dist-info/licenses/runtime/LICENSE +21 -0
- wireforge-0.1.0.dist-info/top_level.txt +1 -0
wireforge/wireshark.py
ADDED
|
@@ -0,0 +1,792 @@
|
|
|
1
|
+
"""Wireshark dissector generator: one self-contained Lua plugin per project.
|
|
2
|
+
|
|
3
|
+
The plugin registers one protocol per interface. Its upper part is a data table describing every
|
|
4
|
+
profile's wire layout (header, trailer, framing, messages with fields fully expanded, bit
|
|
5
|
+
positions, ranges and checksums); its lower part is a fixed interpreter
|
|
6
|
+
(`wireshark_runtime.lua`) that walks those tables the same way the reference codec walks the
|
|
7
|
+
model. Profiles become a protocol preference, so one file serves every profile.
|
|
8
|
+
|
|
9
|
+
Each field gets a display filter name `<protocol>.<Message>.<path>` (header and trailer fields
|
|
10
|
+
`<protocol>.header.<name>` and `<protocol>.trailer.<name>`). Decode problems are reported as
|
|
11
|
+
expert info whose text starts with the runtime status name (`[ChecksumMismatch] ...`).
|
|
12
|
+
|
|
13
|
+
The plugin uses only the Lua API of Wireshark 3.6 and later and runs on Lua 5.2 to 5.4.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import math
|
|
20
|
+
import re
|
|
21
|
+
import sys
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from wireforge import bundle, expr, versioning
|
|
27
|
+
from wireforge.codec import CRC32, ReferenceCodec, Settings
|
|
28
|
+
from wireforge.expr import ExprError
|
|
29
|
+
from wireforge.issues import Issue
|
|
30
|
+
from wireforge.loader import load_project
|
|
31
|
+
from wireforge.model import (
|
|
32
|
+
BUILTIN_BITS,
|
|
33
|
+
FLOAT_TYPES,
|
|
34
|
+
SIGNED_TYPES,
|
|
35
|
+
ChecksumUse,
|
|
36
|
+
Field,
|
|
37
|
+
Interface,
|
|
38
|
+
Occurs,
|
|
39
|
+
Project,
|
|
40
|
+
Struct,
|
|
41
|
+
Value,
|
|
42
|
+
)
|
|
43
|
+
from wireforge.validate import Validator, Variant
|
|
44
|
+
|
|
45
|
+
BYTE_BITS = 8
|
|
46
|
+
NUMBER_BYTES = 4
|
|
47
|
+
WIDE_BYTES = 8
|
|
48
|
+
WORD_BITS = 32
|
|
49
|
+
PRINTABLE = range(0x20, 0x7F)
|
|
50
|
+
RUNTIME = Path(__file__).with_name("wireshark_runtime.lua")
|
|
51
|
+
PROTOCOL_NAME = re.compile(r"^[a-z][a-z0-9_]*$")
|
|
52
|
+
LINE_WIDTH = 100
|
|
53
|
+
INDENT = " "
|
|
54
|
+
|
|
55
|
+
LuaValue = Any
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class WiresharkError(Exception):
|
|
59
|
+
"""The project cannot be expressed as a dissector (the message says why)."""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class FieldSpec:
|
|
64
|
+
"""One registered ProtoField."""
|
|
65
|
+
|
|
66
|
+
abbr: str
|
|
67
|
+
label: str
|
|
68
|
+
ftype: str
|
|
69
|
+
base: str = "NONE"
|
|
70
|
+
mask: int | None = None
|
|
71
|
+
enum: str | None = None
|
|
72
|
+
description: str = ""
|
|
73
|
+
|
|
74
|
+
def lua(self) -> dict[str, LuaValue]:
|
|
75
|
+
"""Return the table the runtime turns into a ProtoField."""
|
|
76
|
+
spec: dict[str, LuaValue] = {"abbr": self.abbr, "label": self.label, "type": self.ftype}
|
|
77
|
+
if self.base != "NONE":
|
|
78
|
+
spec["base"] = self.base
|
|
79
|
+
if self.mask is not None:
|
|
80
|
+
spec["mask"] = self.mask
|
|
81
|
+
if self.enum is not None:
|
|
82
|
+
spec["enum"] = self.enum
|
|
83
|
+
if self.description:
|
|
84
|
+
spec["description"] = self.description
|
|
85
|
+
return spec
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
class Registry:
|
|
90
|
+
"""ProtoFields of one protocol, shared by every profile."""
|
|
91
|
+
|
|
92
|
+
protocol: str
|
|
93
|
+
specs: dict[str, FieldSpec] = field(default_factory=dict)
|
|
94
|
+
|
|
95
|
+
def add(self, spec: FieldSpec) -> str:
|
|
96
|
+
"""Register `spec` (once) and return its abbreviation."""
|
|
97
|
+
known = self.specs.get(spec.abbr)
|
|
98
|
+
if known is None:
|
|
99
|
+
self.specs[spec.abbr] = spec
|
|
100
|
+
elif known != spec:
|
|
101
|
+
message = (
|
|
102
|
+
f"two different fields would both be named {spec.abbr} (a name clash, or a field "
|
|
103
|
+
"that differs between profiles)"
|
|
104
|
+
)
|
|
105
|
+
raise WiresharkError(message)
|
|
106
|
+
return spec.abbr
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def protocol_name(project: Project, interface: Interface) -> str:
|
|
110
|
+
"""Return the protocol (display filter) name of an interface: lower case, `_` separated."""
|
|
111
|
+
text = f"{project.name}_{interface.name}".lower()
|
|
112
|
+
text = re.sub(r"[^a-z0-9_]", "_", text)
|
|
113
|
+
return text if PROTOCOL_NAME.match(text) else f"wf_{text}"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _int_type(type_name: str, width: int) -> str:
|
|
117
|
+
prefix = "int" if type_name in SIGNED_TYPES else "uint"
|
|
118
|
+
return f"{prefix}{width * BYTE_BITS}"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _pair(value: int, *, signed: bool) -> dict[str, LuaValue]:
|
|
122
|
+
"""Split a 64-bit value into two 32-bit halves (the runtime builds an Int64 or UInt64 of it)."""
|
|
123
|
+
bits = value % (1 << 64)
|
|
124
|
+
pair: dict[str, LuaValue] = {"low": bits & 0xFFFFFFFF, "high": bits >> WORD_BITS}
|
|
125
|
+
if signed:
|
|
126
|
+
pair["signed"] = True
|
|
127
|
+
return pair
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class _Builder:
|
|
131
|
+
"""Builds the layout tables of one profile."""
|
|
132
|
+
|
|
133
|
+
def __init__(self, project: Project, validator: Validator, variant: Variant) -> None:
|
|
134
|
+
self.project = project
|
|
135
|
+
self.validator = validator
|
|
136
|
+
self.variant = variant
|
|
137
|
+
self.codec = ReferenceCodec(project, variant.name or None)
|
|
138
|
+
|
|
139
|
+
# --- model helpers --------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
def static(self, raw: Value, what: str) -> int:
|
|
142
|
+
value = self.validator.static_value(raw, self.variant)
|
|
143
|
+
if value is None:
|
|
144
|
+
message = f"{what}: cannot evaluate {raw!r}{self.variant.label()}"
|
|
145
|
+
raise WiresharkError(message)
|
|
146
|
+
return value
|
|
147
|
+
|
|
148
|
+
def holds(self, condition: str | None) -> bool:
|
|
149
|
+
if condition is None:
|
|
150
|
+
return True
|
|
151
|
+
try:
|
|
152
|
+
return expr.evaluate(expr.parse_condition(condition), self.variant.scope) != 0
|
|
153
|
+
except ExprError:
|
|
154
|
+
return True
|
|
155
|
+
|
|
156
|
+
def active(self, fields: tuple[Field, ...]) -> tuple[Field, ...]:
|
|
157
|
+
return tuple(item for item in fields if self.validator.present(item, self.variant))
|
|
158
|
+
|
|
159
|
+
def pure_bits(self, type_name: str) -> Struct | None:
|
|
160
|
+
item = self.project.types.get(type_name)
|
|
161
|
+
if item is None:
|
|
162
|
+
return None
|
|
163
|
+
active = self.active(item.fields)
|
|
164
|
+
return item if active and all(inner.bits is not None for inner in active) else None
|
|
165
|
+
|
|
166
|
+
def in_group(self, item: Field) -> bool:
|
|
167
|
+
if item.occurs is not None:
|
|
168
|
+
return False
|
|
169
|
+
return item.bits is not None or self.pure_bits(item.type) is not None
|
|
170
|
+
|
|
171
|
+
def base_type(self, item: Field) -> str:
|
|
172
|
+
enum = self.project.enums.get(item.type)
|
|
173
|
+
return enum.type if enum is not None else item.type
|
|
174
|
+
|
|
175
|
+
def scalar_width(self, item: Field) -> int:
|
|
176
|
+
user = self.project.user_types.get(item.type)
|
|
177
|
+
if user is not None:
|
|
178
|
+
return user.size_bytes
|
|
179
|
+
return BUILTIN_BITS[self.base_type(item)] // BYTE_BITS
|
|
180
|
+
|
|
181
|
+
def fixed_size(self, fields: tuple[Field, ...], what: str) -> int:
|
|
182
|
+
"""Size of fields without variable-size arrays (headers, trailers, fill elements)."""
|
|
183
|
+
total_bits = 0
|
|
184
|
+
for item in self.active(fields):
|
|
185
|
+
if self.in_group(item):
|
|
186
|
+
inner = self.pure_bits(item.type) if item.bits is None else None
|
|
187
|
+
members = self.active(inner.fields) if inner is not None else (item,)
|
|
188
|
+
total_bits += sum(member.bits or 0 for member in members)
|
|
189
|
+
continue
|
|
190
|
+
occurs = item.occurs
|
|
191
|
+
if occurs is not None and (occurs.count is not None or occurs.fill):
|
|
192
|
+
message = f"{what}: {item.name} has a variable size"
|
|
193
|
+
raise WiresharkError(message)
|
|
194
|
+
count = 1 if occurs is None else self.static(occurs.max, item.name)
|
|
195
|
+
inner_struct = self.project.types.get(item.type)
|
|
196
|
+
element = (
|
|
197
|
+
self.fixed_size(inner_struct.fields, what) * BYTE_BITS
|
|
198
|
+
if inner_struct is not None
|
|
199
|
+
else self.scalar_width(item) * BYTE_BITS
|
|
200
|
+
)
|
|
201
|
+
total_bits += count * element
|
|
202
|
+
return total_bits // BYTE_BITS
|
|
203
|
+
|
|
204
|
+
# --- expressions ----------------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
def expression(self, raw: Value, names: set[str]) -> LuaValue:
|
|
207
|
+
"""Count expression as a runtime tree; parts without field names are folded."""
|
|
208
|
+
if isinstance(raw, float):
|
|
209
|
+
message = f"count {raw!r} is not an integer"
|
|
210
|
+
raise WiresharkError(message)
|
|
211
|
+
return self.node(expr.as_expression(raw), names)
|
|
212
|
+
|
|
213
|
+
def node(self, node: expr.Node, names: set[str]) -> LuaValue:
|
|
214
|
+
if not expr.referenced_names(node) & names:
|
|
215
|
+
try:
|
|
216
|
+
return expr.evaluate(node, self.variant.scope)
|
|
217
|
+
except ExprError as error:
|
|
218
|
+
message = f"cannot evaluate a count expression{self.variant.label()}: {error}"
|
|
219
|
+
raise WiresharkError(message) from error
|
|
220
|
+
if isinstance(node, expr.Name):
|
|
221
|
+
return ["field", node.name]
|
|
222
|
+
if isinstance(node, expr.Unary):
|
|
223
|
+
return ["unary", node.op, self.node(node.operand, names)]
|
|
224
|
+
if isinstance(node, expr.Binary):
|
|
225
|
+
return ["binary", node.op, self.node(node.left, names), self.node(node.right, names)]
|
|
226
|
+
if isinstance(node, expr.Ternary):
|
|
227
|
+
parts = (node.condition, node.then, node.otherwise)
|
|
228
|
+
return ["if", *(self.node(part, names) for part in parts)]
|
|
229
|
+
if isinstance(node, expr.Call):
|
|
230
|
+
return ["call", node.function, *(self.node(arg, names) for arg in node.args)]
|
|
231
|
+
message = f"unsupported count expression node {expr.describe(node)}"
|
|
232
|
+
raise WiresharkError(message)
|
|
233
|
+
|
|
234
|
+
# --- values ---------------------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
def bound(self, raw: Value | None) -> float | None:
|
|
237
|
+
if raw is None:
|
|
238
|
+
return None
|
|
239
|
+
if isinstance(raw, float):
|
|
240
|
+
return raw
|
|
241
|
+
return self.validator.static_value(raw, self.variant)
|
|
242
|
+
|
|
243
|
+
def number(self, value: float, *, big: bool, signed: bool) -> LuaValue:
|
|
244
|
+
if big and isinstance(value, int):
|
|
245
|
+
return _pair(value, signed=signed)
|
|
246
|
+
return value
|
|
247
|
+
|
|
248
|
+
def checks(self, item: Field, entry: dict[str, LuaValue], *, big: bool) -> None:
|
|
249
|
+
"""Add range, enum and constant checks of a scalar or bit-field."""
|
|
250
|
+
signed = self.base_type(item) in SIGNED_TYPES
|
|
251
|
+
low, high = self.codec.int_range(item) if item.type not in FLOAT_TYPES else (None, None)
|
|
252
|
+
lower, upper = self.bound(item.min), self.bound(item.max)
|
|
253
|
+
if lower is not None and (low is None or lower > low):
|
|
254
|
+
entry["min"] = self.number(lower, big=big, signed=signed)
|
|
255
|
+
if upper is not None and (high is None or upper < high):
|
|
256
|
+
entry["max"] = self.number(upper, big=big, signed=signed)
|
|
257
|
+
if item.type in self.project.enums:
|
|
258
|
+
entry["enum"] = item.type
|
|
259
|
+
if item.const is not None:
|
|
260
|
+
const: object = item.const
|
|
261
|
+
enum = self.project.enums.get(item.type)
|
|
262
|
+
if enum is not None and isinstance(const, str):
|
|
263
|
+
const = enum.values[const]
|
|
264
|
+
if isinstance(const, bool):
|
|
265
|
+
const = int(const)
|
|
266
|
+
if isinstance(const, (int, float)):
|
|
267
|
+
entry["const"] = self.number(const, big=big, signed=signed)
|
|
268
|
+
if item.reserved:
|
|
269
|
+
entry["reserved"] = True
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
class _Layout:
|
|
273
|
+
"""Expands the fields of one message, header or trailer into runtime tables."""
|
|
274
|
+
|
|
275
|
+
def __init__(
|
|
276
|
+
self, builder: _Builder, registry: Registry, prefix: str, settings: Settings
|
|
277
|
+
) -> None:
|
|
278
|
+
self.builder = builder
|
|
279
|
+
self.registry = registry
|
|
280
|
+
self.prefix = prefix
|
|
281
|
+
self.settings = settings
|
|
282
|
+
|
|
283
|
+
def label(self, item: Field) -> str:
|
|
284
|
+
return f"{item.name} [{item.units}]" if item.units else item.name
|
|
285
|
+
|
|
286
|
+
def describe(self, item: Field) -> str:
|
|
287
|
+
return " ".join((item.description or "").split())
|
|
288
|
+
|
|
289
|
+
def base(self, item: Field) -> str:
|
|
290
|
+
if item.type in self.builder.project.enums:
|
|
291
|
+
return "DEC"
|
|
292
|
+
hexadecimal = item.role in {"sync", "message_id"} or item.checksum is not None
|
|
293
|
+
if hexadecimal or item.const is not None or item.reserved:
|
|
294
|
+
return "HEX"
|
|
295
|
+
return "DEC"
|
|
296
|
+
|
|
297
|
+
def struct(self, fields: tuple[Field, ...], path: str) -> list[dict[str, LuaValue]]:
|
|
298
|
+
builder = self.builder
|
|
299
|
+
active = builder.active(fields)
|
|
300
|
+
names = {item.name for item in active}
|
|
301
|
+
result: list[dict[str, LuaValue]] = []
|
|
302
|
+
group: list[Field] = []
|
|
303
|
+
for item in [*active, None]:
|
|
304
|
+
if item is not None and builder.in_group(item):
|
|
305
|
+
group.append(item)
|
|
306
|
+
continue
|
|
307
|
+
if group:
|
|
308
|
+
result.append(self.group(group, path))
|
|
309
|
+
group = []
|
|
310
|
+
if item is not None:
|
|
311
|
+
result.append(self.field(item, path, names))
|
|
312
|
+
return result
|
|
313
|
+
|
|
314
|
+
def field(self, item: Field, path: str, names: set[str]) -> dict[str, LuaValue]:
|
|
315
|
+
builder = self.builder
|
|
316
|
+
abbr = f"{self.prefix}.{path}{item.name}"
|
|
317
|
+
entry: dict[str, LuaValue] = {"name": item.name}
|
|
318
|
+
if item.occurs is not None:
|
|
319
|
+
entry["occurs"] = self.occurs(item, item.occurs, names)
|
|
320
|
+
inner = builder.project.types.get(item.type)
|
|
321
|
+
if inner is not None:
|
|
322
|
+
entry["kind"] = "struct"
|
|
323
|
+
spec = FieldSpec(abbr, item.name, "none", description=self.describe(item))
|
|
324
|
+
entry["abbr"] = self.registry.add(spec)
|
|
325
|
+
entry["fields"] = self.struct(inner.fields, f"{path}{item.name}.")
|
|
326
|
+
return entry
|
|
327
|
+
user = builder.project.user_types.get(item.type)
|
|
328
|
+
if user is not None:
|
|
329
|
+
entry["kind"] = "bytes"
|
|
330
|
+
entry["width"] = user.size_bytes
|
|
331
|
+
spec = FieldSpec(abbr, self.label(item), "bytes", description=self.describe(item))
|
|
332
|
+
entry["abbr"] = self.registry.add(spec)
|
|
333
|
+
return entry
|
|
334
|
+
entry["kind"] = "scalar"
|
|
335
|
+
entry["type"] = item.type
|
|
336
|
+
width = builder.scalar_width(item)
|
|
337
|
+
entry["width"] = width
|
|
338
|
+
if item.type in FLOAT_TYPES:
|
|
339
|
+
ftype = "float" if item.type == "f32" else "double"
|
|
340
|
+
elif item.type == "bool":
|
|
341
|
+
ftype = "bool"
|
|
342
|
+
elif item.type == "char":
|
|
343
|
+
ftype = "string"
|
|
344
|
+
else:
|
|
345
|
+
ftype = _int_type(builder.base_type(item), width)
|
|
346
|
+
entry["signed"] = builder.base_type(item) in SIGNED_TYPES
|
|
347
|
+
spec = FieldSpec(
|
|
348
|
+
abbr,
|
|
349
|
+
self.label(item),
|
|
350
|
+
ftype,
|
|
351
|
+
base=self.base(item) if ftype.startswith(("uint", "int")) else "NONE",
|
|
352
|
+
enum=item.type if item.type in builder.project.enums and width < WIDE_BYTES else None,
|
|
353
|
+
description=self.describe(item),
|
|
354
|
+
)
|
|
355
|
+
entry["abbr"] = self.registry.add(spec)
|
|
356
|
+
builder.checks(item, entry, big=width == WIDE_BYTES)
|
|
357
|
+
if item.checksum is not None:
|
|
358
|
+
entry["checksum"] = self.checksum(item.checksum)
|
|
359
|
+
return entry
|
|
360
|
+
|
|
361
|
+
def occurs(self, item: Field, occurs: Occurs, names: set[str]) -> dict[str, LuaValue]:
|
|
362
|
+
builder = self.builder
|
|
363
|
+
capacity = builder.static(occurs.max, item.name)
|
|
364
|
+
result: dict[str, LuaValue] = {"max": capacity}
|
|
365
|
+
dynamic = occurs.count is not None or occurs.fill
|
|
366
|
+
result["min"] = builder.static(occurs.min, item.name) if occurs.min is not None else 0
|
|
367
|
+
if not dynamic:
|
|
368
|
+
result["min"] = capacity
|
|
369
|
+
if occurs.fill:
|
|
370
|
+
result["fill"] = True
|
|
371
|
+
inner = builder.project.types.get(item.type)
|
|
372
|
+
result["element"] = (
|
|
373
|
+
builder.fixed_size(inner.fields, item.name)
|
|
374
|
+
if inner is not None
|
|
375
|
+
else builder.scalar_width(item)
|
|
376
|
+
)
|
|
377
|
+
elif occurs.count is not None:
|
|
378
|
+
result["count"] = builder.expression(occurs.count, names)
|
|
379
|
+
if dynamic:
|
|
380
|
+
result["dynamic"] = True
|
|
381
|
+
return result
|
|
382
|
+
|
|
383
|
+
def checksum(self, use: ChecksumUse) -> dict[str, LuaValue]:
|
|
384
|
+
result: dict[str, LuaValue] = {"use": use.use}
|
|
385
|
+
if use.scope is not None:
|
|
386
|
+
result["scope"] = use.scope
|
|
387
|
+
else:
|
|
388
|
+
result["first"] = use.first
|
|
389
|
+
result["last"] = use.last
|
|
390
|
+
return result
|
|
391
|
+
|
|
392
|
+
def leaves(self, group: list[Field], path: str) -> list[tuple[Field, str, str | None]]:
|
|
393
|
+
"""Group members as (leaf, abbreviation path, owning nested struct field name)."""
|
|
394
|
+
result: list[tuple[Field, str, str | None]] = []
|
|
395
|
+
for item in group:
|
|
396
|
+
inner = self.builder.pure_bits(item.type) if item.bits is None else None
|
|
397
|
+
if inner is None:
|
|
398
|
+
result.append((item, f"{path}{item.name}", None))
|
|
399
|
+
else:
|
|
400
|
+
result.extend(
|
|
401
|
+
(member, f"{path}{item.name}.{member.name}", item.name)
|
|
402
|
+
for member in self.builder.active(inner.fields)
|
|
403
|
+
)
|
|
404
|
+
return result
|
|
405
|
+
|
|
406
|
+
def group(self, group: list[Field], path: str) -> dict[str, LuaValue]:
|
|
407
|
+
leaves = self.leaves(group, path)
|
|
408
|
+
total = sum(leaf.bits or 0 for leaf, _, _ in leaves)
|
|
409
|
+
width = total // BYTE_BITS
|
|
410
|
+
wide = width > NUMBER_BYTES
|
|
411
|
+
used = 0
|
|
412
|
+
parts: list[dict[str, LuaValue]] = []
|
|
413
|
+
owners: dict[str, dict[str, LuaValue]] = {}
|
|
414
|
+
for leaf, leaf_path, owner in leaves:
|
|
415
|
+
bits = leaf.bits or 0
|
|
416
|
+
shift = used if self.settings.bit_order == "lsb_first" else total - used - bits
|
|
417
|
+
used += bits
|
|
418
|
+
entry = self.leaf(leaf, leaf_path, bits, shift, width)
|
|
419
|
+
if owner is None:
|
|
420
|
+
parts.append(entry)
|
|
421
|
+
continue
|
|
422
|
+
if owner not in owners:
|
|
423
|
+
abbr = f"{self.prefix}.{path}{owner}"
|
|
424
|
+
owner_field = next(item for item in group if item.name == owner)
|
|
425
|
+
spec = FieldSpec(abbr, owner, "none", description=self.describe(owner_field))
|
|
426
|
+
owners[owner] = {"name": owner, "abbr": self.registry.add(spec), "leaves": []}
|
|
427
|
+
parts.append(owners[owner])
|
|
428
|
+
owners[owner]["leaves"].append(entry)
|
|
429
|
+
result: dict[str, LuaValue] = {"kind": "group", "width": width, "parts": parts}
|
|
430
|
+
if wide:
|
|
431
|
+
result["wide"] = True
|
|
432
|
+
return result
|
|
433
|
+
|
|
434
|
+
def leaf(
|
|
435
|
+
self, leaf: Field, path: str, bits: int, shift: int, width: int
|
|
436
|
+
) -> dict[str, LuaValue]:
|
|
437
|
+
builder = self.builder
|
|
438
|
+
wide = width > NUMBER_BYTES
|
|
439
|
+
signed = builder.base_type(leaf) in SIGNED_TYPES
|
|
440
|
+
entry: dict[str, LuaValue] = {
|
|
441
|
+
"name": leaf.name,
|
|
442
|
+
"type": leaf.type,
|
|
443
|
+
"bits": bits,
|
|
444
|
+
"shift": shift,
|
|
445
|
+
}
|
|
446
|
+
if signed:
|
|
447
|
+
entry["signed"] = True
|
|
448
|
+
mask = None if wide else ((1 << bits) - 1) << shift
|
|
449
|
+
if leaf.type == "bool":
|
|
450
|
+
ftype = "bool"
|
|
451
|
+
base = "NONE" if wide else str(width * BYTE_BITS)
|
|
452
|
+
else:
|
|
453
|
+
ftype = ("int" if signed else "uint") + ("64" if wide else str(width * BYTE_BITS))
|
|
454
|
+
base = self.base(leaf)
|
|
455
|
+
spec = FieldSpec(
|
|
456
|
+
f"{self.prefix}.{path}",
|
|
457
|
+
self.label(leaf),
|
|
458
|
+
ftype,
|
|
459
|
+
base=base,
|
|
460
|
+
mask=mask,
|
|
461
|
+
enum=leaf.type if leaf.type in builder.project.enums and not wide else None,
|
|
462
|
+
description=self.describe(leaf),
|
|
463
|
+
)
|
|
464
|
+
entry["abbr"] = self.registry.add(spec)
|
|
465
|
+
builder.checks(leaf, entry, big=wide)
|
|
466
|
+
return entry
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
class _InterfaceBuilder:
|
|
470
|
+
"""Builds the protocol of one interface for every profile."""
|
|
471
|
+
|
|
472
|
+
def __init__(self, project: Project, interface: Interface, validator: Validator) -> None:
|
|
473
|
+
self.project = project
|
|
474
|
+
self.interface = interface
|
|
475
|
+
self.validator = validator
|
|
476
|
+
self.registry = Registry(protocol_name(project, interface))
|
|
477
|
+
|
|
478
|
+
def build(self, variants: list[Variant]) -> dict[str, LuaValue]:
|
|
479
|
+
name = self.registry.protocol
|
|
480
|
+
registry = self.registry
|
|
481
|
+
registry.add(FieldSpec(f"{name}.header", "Header", "none"))
|
|
482
|
+
registry.add(FieldSpec(f"{name}.trailer", "Trailer", "none"))
|
|
483
|
+
registry.add(FieldSpec(f"{name}.payload", "Payload", "bytes"))
|
|
484
|
+
registry.add(FieldSpec(f"{name}.skipped", "Skipped bytes", "bytes"))
|
|
485
|
+
layouts = [self.layout(variant) for variant in variants]
|
|
486
|
+
interface = self.interface
|
|
487
|
+
version = f" {self.project.version}" if self.project.version else ""
|
|
488
|
+
description = (
|
|
489
|
+
f"{self.project.name}{version} {interface.name} {interface.version} (WireForge)"
|
|
490
|
+
)
|
|
491
|
+
return {
|
|
492
|
+
"name": name,
|
|
493
|
+
"interface": interface.name,
|
|
494
|
+
"description": description,
|
|
495
|
+
"profiles": [variant.name or "default" for variant in variants],
|
|
496
|
+
"fields": [spec.lua() for spec in registry.specs.values()],
|
|
497
|
+
"layouts": layouts,
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
def layout(self, variant: Variant) -> dict[str, LuaValue]:
|
|
501
|
+
builder = _Builder(self.project, self.validator, variant)
|
|
502
|
+
interface = self.interface
|
|
503
|
+
codec = builder.codec
|
|
504
|
+
settings = codec.interface_settings(interface)
|
|
505
|
+
name = self.registry.protocol
|
|
506
|
+
header_fields = builder.active(interface.header or ())
|
|
507
|
+
trailer_fields = builder.active(interface.trailer)
|
|
508
|
+
frame_layout = _Layout(builder, self.registry, f"{name}.header", settings)
|
|
509
|
+
header = frame_layout.struct(header_fields, "")
|
|
510
|
+
trailer = _Layout(builder, self.registry, f"{name}.trailer", settings).struct(
|
|
511
|
+
trailer_fields, ""
|
|
512
|
+
)
|
|
513
|
+
framing = interface.framing
|
|
514
|
+
result: dict[str, LuaValue] = {
|
|
515
|
+
"framing": framing.kind,
|
|
516
|
+
"header_size": builder.fixed_size(header_fields, f"{interface.name} header"),
|
|
517
|
+
"trailer_size": builder.fixed_size(trailer_fields, f"{interface.name} trailer"),
|
|
518
|
+
"little": settings.endian == "little",
|
|
519
|
+
"check": settings.check_decode,
|
|
520
|
+
"header": header,
|
|
521
|
+
"trailer": trailer,
|
|
522
|
+
"length_counts": framing.length_counts,
|
|
523
|
+
}
|
|
524
|
+
if settings.allow_trailing:
|
|
525
|
+
result["allow_trailing"] = True
|
|
526
|
+
if framing.resync and framing.kind != "none":
|
|
527
|
+
result["resync"] = True
|
|
528
|
+
if framing.kind == "fixed" and framing.frame_size is not None:
|
|
529
|
+
result["frame_size"] = builder.static(framing.frame_size, "frame_size")
|
|
530
|
+
if framing.max_frame_size is not None:
|
|
531
|
+
result["max_frame_size"] = builder.static(framing.max_frame_size, "max_frame_size")
|
|
532
|
+
self.roles(builder, header_fields, result)
|
|
533
|
+
result["messages"] = self.messages(builder)
|
|
534
|
+
return result
|
|
535
|
+
|
|
536
|
+
def roles(
|
|
537
|
+
self, builder: _Builder, header: tuple[Field, ...], result: dict[str, LuaValue]
|
|
538
|
+
) -> None:
|
|
539
|
+
"""Locate the sync, message id and length fields of the header."""
|
|
540
|
+
offset = 0
|
|
541
|
+
sync: list[dict[str, LuaValue]] = []
|
|
542
|
+
little = result["little"]
|
|
543
|
+
for item in header:
|
|
544
|
+
size = builder.fixed_size((item,), f"{self.interface.name} header")
|
|
545
|
+
if item.role == "sync" and isinstance(item.const, int):
|
|
546
|
+
data = item.const.to_bytes(size, "little" if little else "big")
|
|
547
|
+
sync.append({"offset": offset, "bytes": data})
|
|
548
|
+
elif item.role == "message_id":
|
|
549
|
+
result["id_field"] = item.name
|
|
550
|
+
elif item.role == "payload_length":
|
|
551
|
+
result["length_field"] = {"name": item.name, "offset": offset, "width": size}
|
|
552
|
+
offset += size
|
|
553
|
+
if sync:
|
|
554
|
+
result["sync"] = sync
|
|
555
|
+
|
|
556
|
+
def messages(self, builder: _Builder) -> list[dict[str, LuaValue]]:
|
|
557
|
+
interface = self.interface
|
|
558
|
+
name = self.registry.protocol
|
|
559
|
+
codec = builder.codec
|
|
560
|
+
ids = codec.message_ids(interface.name)
|
|
561
|
+
result = []
|
|
562
|
+
for entry in interface.messages:
|
|
563
|
+
message = self.project.messages[entry.name]
|
|
564
|
+
if not builder.holds(entry.condition) or not builder.holds(message.condition):
|
|
565
|
+
continue
|
|
566
|
+
settings = codec.message_settings(message.name, interface.name)
|
|
567
|
+
prefix = f"{name}.{message.name}"
|
|
568
|
+
description = " ".join((message.description or "").split())
|
|
569
|
+
spec = FieldSpec(prefix, message.name, "none", description=description)
|
|
570
|
+
fields = _Layout(builder, self.registry, prefix, settings).struct(message.fields, "")
|
|
571
|
+
item: dict[str, LuaValue] = {
|
|
572
|
+
"name": message.name,
|
|
573
|
+
"abbr": self.registry.add(spec),
|
|
574
|
+
"little": settings.endian == "little",
|
|
575
|
+
"check": settings.check_decode,
|
|
576
|
+
"fields": fields,
|
|
577
|
+
}
|
|
578
|
+
identifier = ids.get(message.name)
|
|
579
|
+
if identifier is not None:
|
|
580
|
+
item["id"] = identifier
|
|
581
|
+
result.append(item)
|
|
582
|
+
return result
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def _checksums(project: Project) -> dict[str, LuaValue]:
|
|
586
|
+
result: dict[str, LuaValue] = {}
|
|
587
|
+
for name, item in sorted(project.checksums.items()):
|
|
588
|
+
if item.algo == "custom":
|
|
589
|
+
result[name] = {"algo": "custom", "impl": item.impl or ""}
|
|
590
|
+
continue
|
|
591
|
+
if item.algo == "crc32":
|
|
592
|
+
width, poly, init, refin, refout, xorout = CRC32
|
|
593
|
+
else:
|
|
594
|
+
width, poly, init = item.width or 0, item.poly or 0, item.init
|
|
595
|
+
refin, refout, xorout = item.refin, item.refout, item.xorout
|
|
596
|
+
wide = width > WORD_BITS
|
|
597
|
+
result[name] = {
|
|
598
|
+
"algo": "crc",
|
|
599
|
+
"width": width,
|
|
600
|
+
"poly": _pair(poly, signed=False) if wide else poly,
|
|
601
|
+
"init": _pair(init, signed=False) if wide else init,
|
|
602
|
+
"refin": refin,
|
|
603
|
+
"refout": refout,
|
|
604
|
+
"xorout": _pair(xorout, signed=False) if wide else xorout,
|
|
605
|
+
}
|
|
606
|
+
return result
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def model(project: Project) -> dict[str, LuaValue]:
|
|
610
|
+
"""Return the data table of the plugin for a valid project."""
|
|
611
|
+
validator = Validator(project)
|
|
612
|
+
by_name = {variant.name: variant for variant in validator.variants}
|
|
613
|
+
names = list(project.profiles) or [""]
|
|
614
|
+
variants = [by_name[name] for name in names]
|
|
615
|
+
protocols = []
|
|
616
|
+
seen: dict[str, str] = {}
|
|
617
|
+
for interface_name in sorted(project.interfaces):
|
|
618
|
+
interface = project.interfaces[interface_name]
|
|
619
|
+
builder = _InterfaceBuilder(project, interface, validator)
|
|
620
|
+
protocol = builder.registry.protocol
|
|
621
|
+
if protocol in seen:
|
|
622
|
+
message = (
|
|
623
|
+
f"interfaces {seen[protocol]} and {interface_name} map to the same protocol "
|
|
624
|
+
f"name {protocol}"
|
|
625
|
+
)
|
|
626
|
+
raise WiresharkError(message)
|
|
627
|
+
seen[protocol] = interface_name
|
|
628
|
+
protocols.append(builder.build(variants))
|
|
629
|
+
enums = {
|
|
630
|
+
name: {value: member for member, value in item.values.items()}
|
|
631
|
+
for name, item in sorted(project.enums.items())
|
|
632
|
+
}
|
|
633
|
+
return {
|
|
634
|
+
"project": project.name,
|
|
635
|
+
"enums": enums,
|
|
636
|
+
"checksums": _checksums(project),
|
|
637
|
+
"protocols": protocols,
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
# --- Lua rendering ------------------------------------------------------------------------------
|
|
642
|
+
|
|
643
|
+
_LUA_NAME = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
644
|
+
_LUA_KEYWORDS = frozenset(
|
|
645
|
+
(
|
|
646
|
+
*("and", "break", "do", "else", "elseif", "end", "false", "for", "function", "goto"),
|
|
647
|
+
*("if", "in", "local", "nil", "not", "or", "repeat", "return", "then", "true"),
|
|
648
|
+
*("until", "while"),
|
|
649
|
+
)
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def _string(text: str | bytes) -> str:
|
|
654
|
+
data = text.encode("utf-8") if isinstance(text, str) else text
|
|
655
|
+
out = []
|
|
656
|
+
for byte in data:
|
|
657
|
+
char = chr(byte)
|
|
658
|
+
if char in {'"', "\\"}:
|
|
659
|
+
out.append("\\" + char)
|
|
660
|
+
elif byte in PRINTABLE:
|
|
661
|
+
out.append(char)
|
|
662
|
+
else:
|
|
663
|
+
out.append(f"\\{byte:03d}")
|
|
664
|
+
return '"' + "".join(out) + '"'
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
def _key(key: object) -> str:
|
|
668
|
+
if isinstance(key, str):
|
|
669
|
+
if _LUA_NAME.match(key) and key not in _LUA_KEYWORDS:
|
|
670
|
+
return key
|
|
671
|
+
return f"[{_string(key)}]"
|
|
672
|
+
return f"[{_scalar(key)}]"
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def _scalar(value: object) -> str:
|
|
676
|
+
if isinstance(value, bool):
|
|
677
|
+
return "true" if value else "false"
|
|
678
|
+
if isinstance(value, int):
|
|
679
|
+
return str(value)
|
|
680
|
+
if isinstance(value, float):
|
|
681
|
+
if math.isinf(value):
|
|
682
|
+
return "math.huge" if value > 0 else "-math.huge"
|
|
683
|
+
return repr(value)
|
|
684
|
+
if isinstance(value, (str, bytes)):
|
|
685
|
+
return _string(value)
|
|
686
|
+
message = f"cannot render {value!r} as Lua"
|
|
687
|
+
raise TypeError(message)
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def _inline(value: object) -> str:
|
|
691
|
+
if isinstance(value, dict):
|
|
692
|
+
items = ", ".join(f"{_key(key)} = {_inline(item)}" for key, item in value.items())
|
|
693
|
+
return "{ " + items + " }" if items else "{}"
|
|
694
|
+
if isinstance(value, list):
|
|
695
|
+
items = ", ".join(_inline(item) for item in value)
|
|
696
|
+
return "{ " + items + " }" if items else "{}"
|
|
697
|
+
return _scalar(value)
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
def lua_table(value: object, depth: int = 0) -> str:
|
|
701
|
+
"""Render plain data as a Lua table constructor, one entry per line when it is long."""
|
|
702
|
+
text = _inline(value)
|
|
703
|
+
if len(text) + len(INDENT) * depth <= LINE_WIDTH or not isinstance(value, (dict, list)):
|
|
704
|
+
return text
|
|
705
|
+
inner = INDENT * (depth + 1)
|
|
706
|
+
if isinstance(value, dict):
|
|
707
|
+
lines = [
|
|
708
|
+
f"{inner}{_key(key)} = {lua_table(item, depth + 1)}," for key, item in value.items()
|
|
709
|
+
]
|
|
710
|
+
else:
|
|
711
|
+
lines = [f"{inner}{lua_table(item, depth + 1)}," for item in value]
|
|
712
|
+
return "{\n" + "\n".join(lines) + "\n" + INDENT * depth + "}"
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def render(project: Project, content_hash: str) -> str:
|
|
716
|
+
"""Return the Lua plugin of a valid project."""
|
|
717
|
+
data = model(project)
|
|
718
|
+
version = f" {project.version}" if project.version else ""
|
|
719
|
+
names = ", ".join(protocol["name"] for protocol in data["protocols"]) or "(none)"
|
|
720
|
+
header = [
|
|
721
|
+
f"-- Wireshark dissector for {project.name}{version}.",
|
|
722
|
+
(
|
|
723
|
+
f"-- Generated by WireForge {versioning.TOOL_VERSION} (file format "
|
|
724
|
+
f"{versioning.FORMAT_MAJOR}.{versioning.FORMAT_MINOR}), model {content_hash}."
|
|
725
|
+
),
|
|
726
|
+
"-- Do not edit: regenerate with `wireforge wireshark`.",
|
|
727
|
+
"--",
|
|
728
|
+
f"-- Protocols: {names}.",
|
|
729
|
+
"-- Install: copy this file into the Wireshark personal Lua plugins folder",
|
|
730
|
+
"-- (Help > About Wireshark > Folders), or run `tshark -X lua_script:<this file>`.",
|
|
731
|
+
"-- Bind a protocol in Edit > Preferences > Protocols (UDP port, TCP port, user DLT)",
|
|
732
|
+
"-- or with Decode As. Custom checksums are verified when another plugin sets",
|
|
733
|
+
'-- `_G.wireforge_checksums["<impl>"] = function(bytes) return <number> end`',
|
|
734
|
+
"-- (`bytes` is a Lua string of the covered bytes).",
|
|
735
|
+
"",
|
|
736
|
+
f"local MODEL = {lua_table(data)}",
|
|
737
|
+
"",
|
|
738
|
+
]
|
|
739
|
+
return "\n".join(header) + RUNTIME.read_text(encoding="utf-8")
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
@dataclass(frozen=True)
|
|
743
|
+
class Plugin:
|
|
744
|
+
"""Result of `build`: the plugin text, or the problems that prevent it."""
|
|
745
|
+
|
|
746
|
+
issues: list[Issue]
|
|
747
|
+
text: str = ""
|
|
748
|
+
project: str = ""
|
|
749
|
+
protocols: tuple[str, ...] = ()
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def build(location: Path) -> Plugin:
|
|
753
|
+
"""Validate the project at `location` and return its plugin."""
|
|
754
|
+
try:
|
|
755
|
+
content_hash = bundle.build_bundle(location)["bundle"]["content_hash"]
|
|
756
|
+
except bundle.BundleError as error:
|
|
757
|
+
return Plugin(error.issues or [Issue(str(location), None, "", str(error))])
|
|
758
|
+
result = load_project(location)
|
|
759
|
+
if result.project is None:
|
|
760
|
+
return Plugin(result.issues)
|
|
761
|
+
project = result.project
|
|
762
|
+
try:
|
|
763
|
+
text = render(project, content_hash)
|
|
764
|
+
except WiresharkError as error:
|
|
765
|
+
return Plugin([Issue(str(location), None, "/interfaces", str(error))])
|
|
766
|
+
names = tuple(sorted(protocol_name(project, item) for item in project.interfaces.values()))
|
|
767
|
+
return Plugin([], text, project.name, names)
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
def generate(location: Path, output: Path) -> Plugin:
|
|
771
|
+
"""Write the plugin of the project at `location` to `output` (nothing on problems)."""
|
|
772
|
+
plugin = build(location)
|
|
773
|
+
if not plugin.issues:
|
|
774
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
775
|
+
output.write_text(plugin.text, encoding="utf-8")
|
|
776
|
+
return plugin
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
def main(argv: list[str] | None = None) -> int:
|
|
780
|
+
"""Command line: python -m wireforge.wireshark PROJECT -o FILE.lua."""
|
|
781
|
+
parser = argparse.ArgumentParser(prog="python -m wireforge.wireshark")
|
|
782
|
+
parser.add_argument("project", type=Path)
|
|
783
|
+
parser.add_argument("-o", "--output", type=Path, required=True)
|
|
784
|
+
args = parser.parse_args(argv)
|
|
785
|
+
issues = generate(args.project, args.output).issues
|
|
786
|
+
for issue in issues:
|
|
787
|
+
sys.stdout.write(f"{issue}\n")
|
|
788
|
+
return 1 if issues else 0
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
if __name__ == "__main__":
|
|
792
|
+
sys.exit(main())
|