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.
Files changed (96) hide show
  1. wireforge/__init__.py +3 -0
  2. wireforge/_data/cmake/WireForgeConfig.cmake +29 -0
  3. wireforge/_data/cmake/WireForgeConfigVersion.cmake +15 -0
  4. wireforge/_data/cmake/WireForgeGenerate.cmake +64 -0
  5. wireforge/_data/runtime/LICENSE +21 -0
  6. wireforge/_data/runtime/include/wireforge/array_access.hpp +35 -0
  7. wireforge/_data/runtime/include/wireforge/bit_utils.hpp +226 -0
  8. wireforge/_data/runtime/include/wireforge/byte_io.hpp +387 -0
  9. wireforge/_data/runtime/include/wireforge/checksum.hpp +201 -0
  10. wireforge/_data/runtime/include/wireforge/codec_support.hpp +412 -0
  11. wireforge/_data/runtime/include/wireforge/config.hpp +41 -0
  12. wireforge/_data/runtime/include/wireforge/delegate.hpp +101 -0
  13. wireforge/_data/runtime/include/wireforge/dispatcher.hpp +339 -0
  14. wireforge/_data/runtime/include/wireforge/expr_support.hpp +38 -0
  15. wireforge/_data/runtime/include/wireforge/frame_decoder.hpp +262 -0
  16. wireforge/_data/runtime/include/wireforge/frame_layout.hpp +20 -0
  17. wireforge/_data/runtime/include/wireforge/link.hpp +223 -0
  18. wireforge/_data/runtime/include/wireforge/lock.hpp +146 -0
  19. wireforge/_data/runtime/include/wireforge/ring_buffer.hpp +226 -0
  20. wireforge/_data/runtime/include/wireforge/span.hpp +116 -0
  21. wireforge/_data/runtime/include/wireforge/static_vector.hpp +264 -0
  22. wireforge/_data/runtime/include/wireforge/status.hpp +75 -0
  23. wireforge/_data/runtime/include/wireforge/transport.hpp +95 -0
  24. wireforge/_data/runtime/include/wireforge/validation.hpp +153 -0
  25. wireforge/_data/runtime/include/wireforge/version.hpp +22 -0
  26. wireforge/_data/runtime/requirements.yaml +438 -0
  27. wireforge/_data/schema/deviations.schema.json +47 -0
  28. wireforge/_data/schema/fragment.schema.json +79 -0
  29. wireforge/_data/schema/llr_ids.schema.json +25 -0
  30. wireforge/_data/schema/project.schema.json +1240 -0
  31. wireforge/_data/schema/runtime_requirements.schema.json +34 -0
  32. wireforge/_data/ui/assets/index-6Pomu6U4.js +44 -0
  33. wireforge/_data/ui/assets/index-GVdDyUAG.css +1 -0
  34. wireforge/_data/ui/index.html +13 -0
  35. wireforge/bundle.py +177 -0
  36. wireforge/ci_check.py +155 -0
  37. wireforge/cli.py +617 -0
  38. wireforge/codec.py +897 -0
  39. wireforge/compat.py +536 -0
  40. wireforge/coverage_points.py +64 -0
  41. wireforge/cpp.py +273 -0
  42. wireforge/cpp_codec.py +440 -0
  43. wireforge/cpp_interface.py +874 -0
  44. wireforge/cpp_tests.py +1993 -0
  45. wireforge/differential.py +586 -0
  46. wireforge/docs_markdown.py +72 -0
  47. wireforge/expr.py +598 -0
  48. wireforge/findings.py +187 -0
  49. wireforge/fmt.py +351 -0
  50. wireforge/generate.py +644 -0
  51. wireforge/golden.py +253 -0
  52. wireforge/hlr_import.py +112 -0
  53. wireforge/impact.py +249 -0
  54. wireforge/importers.py +402 -0
  55. wireforge/issues.py +27 -0
  56. wireforge/layout.py +681 -0
  57. wireforge/lifecycle.py +1266 -0
  58. wireforge/lifecycle_results.py +205 -0
  59. wireforge/lifecycle_rules.py +78 -0
  60. wireforge/lifecycle_runtime.py +375 -0
  61. wireforge/llr.py +693 -0
  62. wireforge/llr_ids.py +104 -0
  63. wireforge/loader.py +422 -0
  64. wireforge/mcp_server.py +357 -0
  65. wireforge/model.py +322 -0
  66. wireforge/plain.py +15 -0
  67. wireforge/py.typed +0 -0
  68. wireforge/reqif.py +289 -0
  69. wireforge/resources.py +43 -0
  70. wireforge/results.py +377 -0
  71. wireforge/runtime_reqs.py +147 -0
  72. wireforge/schema.py +151 -0
  73. wireforge/server/__init__.py +1 -0
  74. wireforge/server/app.py +619 -0
  75. wireforge/server/oidc.py +99 -0
  76. wireforge/server/storage.py +146 -0
  77. wireforge/service.py +1354 -0
  78. wireforge/templates/command/project.yaml +38 -0
  79. wireforge/templates/raw/project.yaml +28 -0
  80. wireforge/templates/telemetry/project.yaml +51 -0
  81. wireforge/test_cases.py +475 -0
  82. wireforge/test_project.py +478 -0
  83. wireforge/trace.py +442 -0
  84. wireforge/validate.py +1241 -0
  85. wireforge/vcs.py +44 -0
  86. wireforge/vectors.py +222 -0
  87. wireforge/versioning.py +137 -0
  88. wireforge/wireshark.py +792 -0
  89. wireforge/wireshark_runtime.lua +725 -0
  90. wireforge-0.1.0.dist-info/METADATA +91 -0
  91. wireforge-0.1.0.dist-info/RECORD +96 -0
  92. wireforge-0.1.0.dist-info/WHEEL +5 -0
  93. wireforge-0.1.0.dist-info/entry_points.txt +2 -0
  94. wireforge-0.1.0.dist-info/licenses/LICENSE +98 -0
  95. wireforge-0.1.0.dist-info/licenses/runtime/LICENSE +21 -0
  96. 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())