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
@@ -0,0 +1,339 @@
1
+ /**
2
+ * @file dispatcher.hpp
3
+ * @brief Routes received frames or payloads to message handlers.
4
+ */
5
+ #ifndef WIREFORGE_DISPATCHER_HPP
6
+ #define WIREFORGE_DISPATCHER_HPP
7
+
8
+ #include "wireforge/array_access.hpp"
9
+ #include "wireforge/byte_io.hpp"
10
+ #include "wireforge/delegate.hpp"
11
+ #include "wireforge/frame_layout.hpp"
12
+ #include "wireforge/lock.hpp"
13
+ #include "wireforge/span.hpp"
14
+ #include "wireforge/status.hpp"
15
+
16
+ #include <algorithm>
17
+ #include <array>
18
+ #include <atomic>
19
+ #include <cstddef>
20
+ #include <cstdint>
21
+ #include <iterator>
22
+
23
+ namespace wireforge {
24
+
25
+ /**
26
+ * @brief A received message before decoding.
27
+ *
28
+ * Both spans point into the caller's buffer (for a Link: its receive frame buffer) and are valid
29
+ * only while the handler runs.
30
+ */
31
+ struct RawMessage {
32
+ Span<const std::uint8_t> payload; ///< Bytes between header and trailer.
33
+ Span<const std::uint8_t> frame; ///< The whole frame: header, payload and trailer.
34
+ };
35
+
36
+ /// @brief Handler of one message type; receives the raw message and returns the decode result.
37
+ using MessageHandler = Delegate<Status(const RawMessage&)>;
38
+
39
+ /// @brief Handler for message ids without a registered handler; receives the id and the raw
40
+ /// message.
41
+ using FallbackHandler = Delegate<Status(std::uint64_t, const RawMessage&)>;
42
+
43
+ /// @brief Where the message id and the payload are inside a frame.
44
+ struct HeaderLayout {
45
+ std::size_t header_size; ///< Header bytes; the payload starts right after them.
46
+ std::size_t trailer_size; ///< Trailer bytes at the end of the frame.
47
+ FieldLocation message_id; ///< Message id field inside the header.
48
+ Endian endian; ///< Byte order of the message id field.
49
+ };
50
+
51
+ /// @brief Statistics of a dispatcher; readable from any thread.
52
+ struct DispatchCounters {
53
+ RelaxedCounter dispatched; ///< Payloads passed to a registered handler.
54
+ RelaxedCounter fallback; ///< Payloads passed to the fallback handler.
55
+ RelaxedCounter unknown_ids; ///< Payloads dropped because no handler matched.
56
+ };
57
+
58
+ /**
59
+ * @brief Header-based dispatcher with a fixed-capacity handler table.
60
+ *
61
+ * Usage: register handlers with add() (and optionally set_fallback()), then call start(). After
62
+ * start() the table is read-only: dispatch() and dispatch_frame() take no lock and may be called
63
+ * from several threads at once, while add() and set_fallback() fail with Status::InvalidState.
64
+ * Registration calls are serialized by lock policy @p L.
65
+ * @tparam N Maximum number of message types.
66
+ * @tparam L Lock policy protecting registration.
67
+ */
68
+ template <std::size_t N, typename L = DefaultLock> class Dispatcher {
69
+ static_assert(N > 0U, "a dispatcher needs room for at least one handler");
70
+ static_assert(is_lock_policy_v<L>, "L must satisfy the lock policy contract");
71
+
72
+ public:
73
+ /// @brief Creates an empty dispatcher for frames laid out as @p layout.
74
+ explicit Dispatcher(const HeaderLayout& layout) noexcept : layout_(layout) {}
75
+
76
+ /**
77
+ * @brief Registers @p handler for message @p message_id.
78
+ * @return Status::InvalidState after start(), for an empty handler or an id registered twice;
79
+ * Status::CountExceedsMax when the table is full.
80
+ */
81
+ Status add(std::uint64_t message_id, MessageHandler handler) noexcept {
82
+ const ScopedLock<L> guard(lock_);
83
+ if (started_.load(std::memory_order_relaxed) || !handler || find(message_id) != nullptr) {
84
+ return Status::InvalidState;
85
+ }
86
+ if (count_ == N) {
87
+ return Status::CountExceedsMax;
88
+ }
89
+ array_at(entries_, count_) = Entry{message_id, handler};
90
+ ++count_;
91
+ return Status::Ok;
92
+ }
93
+
94
+ /**
95
+ * @brief Sets the handler for ids without a registered handler.
96
+ * @return Status::InvalidState after start().
97
+ */
98
+ Status set_fallback(FallbackHandler handler) noexcept {
99
+ const ScopedLock<L> guard(lock_);
100
+ if (started_.load(std::memory_order_relaxed)) {
101
+ return Status::InvalidState;
102
+ }
103
+ fallback_ = handler;
104
+ return Status::Ok;
105
+ }
106
+
107
+ /**
108
+ * @brief Freezes the table; dispatching is possible afterwards.
109
+ * @return Status::InvalidState when already started.
110
+ */
111
+ Status start() noexcept {
112
+ const ScopedLock<L> guard(lock_);
113
+ if (started_.load(std::memory_order_relaxed)) {
114
+ return Status::InvalidState;
115
+ }
116
+ sort_entries();
117
+ started_.store(true, std::memory_order_release);
118
+ return Status::Ok;
119
+ }
120
+
121
+ /// @brief true after start().
122
+ [[nodiscard]] bool started() const noexcept { return started_.load(std::memory_order_acquire); }
123
+
124
+ /// @brief Number of registered handlers.
125
+ [[nodiscard]] std::size_t size() const noexcept {
126
+ const ScopedLock<L> guard(lock_);
127
+ return count_;
128
+ }
129
+
130
+ /**
131
+ * @brief Passes @p payload to the handler of message @p message_id.
132
+ * @return The handler's result; Status::UnknownMessageId when no handler (and no fallback)
133
+ * matches; Status::InvalidState before start().
134
+ */
135
+ Status dispatch(std::uint64_t message_id, Span<const std::uint8_t> payload) const noexcept {
136
+ return dispatch(message_id, RawMessage{payload, payload});
137
+ }
138
+
139
+ /**
140
+ * @brief Passes @p message to the handler of message @p message_id.
141
+ * @return The handler's result; Status::UnknownMessageId when no handler (and no fallback)
142
+ * matches; Status::InvalidState before start().
143
+ */
144
+ Status dispatch(std::uint64_t message_id, const RawMessage& message) const noexcept {
145
+ if (!started()) {
146
+ return Status::InvalidState;
147
+ }
148
+ const Entry* entry = find(message_id);
149
+ if (entry != nullptr) {
150
+ counters_.dispatched.increment();
151
+ return entry->handler(message);
152
+ }
153
+ if (fallback_) {
154
+ counters_.fallback.increment();
155
+ return fallback_(message_id, message);
156
+ }
157
+ counters_.unknown_ids.increment();
158
+ return Status::UnknownMessageId;
159
+ }
160
+
161
+ /**
162
+ * @brief Reads the message id from a complete frame and dispatches its payload.
163
+ * @param frame Header, payload and trailer, as delivered by FrameDecoder.
164
+ * @return Status::Truncated when @p frame is shorter than header and trailer,
165
+ * Status::InvalidState when the message id field is outside the header; otherwise as
166
+ * dispatch().
167
+ */
168
+ Status dispatch_frame(Span<const std::uint8_t> frame) const noexcept {
169
+ const FieldLocation& field = layout_.message_id;
170
+ if (!valid_width(field.width) || field.offset > layout_.header_size ||
171
+ layout_.header_size - field.offset < field.width) {
172
+ return Status::InvalidState;
173
+ }
174
+ const std::size_t overhead = layout_.header_size + layout_.trailer_size;
175
+ if (frame.size() < overhead) {
176
+ return Status::Truncated;
177
+ }
178
+ const std::uint64_t message_id =
179
+ load_unsigned(frame.subspan(field.offset), field.width, layout_.endian);
180
+ return dispatch(
181
+ message_id,
182
+ RawMessage{frame.subspan(layout_.header_size, frame.size() - overhead), frame});
183
+ }
184
+
185
+ /// @brief Statistics since construction.
186
+ [[nodiscard]] const DispatchCounters& counters() const noexcept { return counters_; }
187
+
188
+ private:
189
+ struct Entry {
190
+ std::uint64_t message_id = 0U;
191
+ MessageHandler handler;
192
+ };
193
+
194
+ // Before start(): linear search over the unsorted entries; afterwards: binary search.
195
+ [[nodiscard]] const Entry* find(std::uint64_t message_id) const noexcept {
196
+ if (!started_.load(std::memory_order_acquire)) {
197
+ for (std::size_t index = 0U; index < count_; ++index) {
198
+ if (array_at(entries_, index).message_id == message_id) {
199
+ return &array_at(entries_, index);
200
+ }
201
+ }
202
+ return nullptr;
203
+ }
204
+ std::size_t low = 0U;
205
+ std::size_t high = count_;
206
+ while (low < high) {
207
+ const std::size_t middle = low + ((high - low) / 2U);
208
+ if (array_at(entries_, middle).message_id < message_id) {
209
+ low = middle + 1U;
210
+ } else {
211
+ high = middle;
212
+ }
213
+ }
214
+ return low < count_ && array_at(entries_, low).message_id == message_id
215
+ ? &array_at(entries_, low)
216
+ : nullptr;
217
+ }
218
+
219
+ // Insertion sort by message id: each entry is rotated into place behind the smaller ids.
220
+ void sort_entries() noexcept {
221
+ for (std::size_t index = 1U; index < count_; ++index) {
222
+ const std::uint64_t moving_id = array_at(entries_, index).message_id;
223
+ std::size_t position = index;
224
+ while (position > 0U && array_at(entries_, position - 1U).message_id > moving_id) {
225
+ --position;
226
+ }
227
+ const auto first = entries_.begin();
228
+ std::rotate(std::next(first, static_cast<std::ptrdiff_t>(position)),
229
+ std::next(first, static_cast<std::ptrdiff_t>(index)),
230
+ std::next(first, static_cast<std::ptrdiff_t>(index + 1U)));
231
+ }
232
+ }
233
+
234
+ HeaderLayout layout_;
235
+ std::array<Entry, N> entries_{};
236
+ std::size_t count_ = 0U;
237
+ FallbackHandler fallback_;
238
+ std::atomic<bool> started_{false};
239
+ mutable L lock_;
240
+ mutable DispatchCounters counters_;
241
+ };
242
+
243
+ /**
244
+ * @brief Dispatcher for interfaces without a header: every payload goes to one handler.
245
+ *
246
+ * Same life cycle as Dispatcher: set_handler(), then start(), then dispatch() from any thread.
247
+ * @tparam L Lock policy protecting registration.
248
+ */
249
+ template <typename L = DefaultLock> class RawDispatcher {
250
+ static_assert(is_lock_policy_v<L>, "L must satisfy the lock policy contract");
251
+
252
+ public:
253
+ /**
254
+ * @brief Creates a dispatcher for frames with @p header_size header bytes before and
255
+ * @p trailer_size trailer bytes after the payload (e.g. a checksum).
256
+ */
257
+ explicit RawDispatcher(std::size_t trailer_size = 0U, std::size_t header_size = 0U) noexcept
258
+ : trailer_size_(trailer_size), header_size_(header_size) {}
259
+
260
+ /**
261
+ * @brief Sets the handler of all payloads.
262
+ * @return Status::InvalidState after start() or for an empty handler.
263
+ */
264
+ Status set_handler(MessageHandler handler) noexcept {
265
+ const ScopedLock<L> guard(lock_);
266
+ if (started_.load(std::memory_order_relaxed) || !handler) {
267
+ return Status::InvalidState;
268
+ }
269
+ handler_ = handler;
270
+ return Status::Ok;
271
+ }
272
+
273
+ /**
274
+ * @brief Freezes the handler; dispatching is possible afterwards.
275
+ * @return Status::InvalidState when already started.
276
+ */
277
+ Status start() noexcept {
278
+ const ScopedLock<L> guard(lock_);
279
+ if (started_.load(std::memory_order_relaxed)) {
280
+ return Status::InvalidState;
281
+ }
282
+ started_.store(true, std::memory_order_release);
283
+ return Status::Ok;
284
+ }
285
+
286
+ /**
287
+ * @brief Passes @p payload to the handler.
288
+ * @return The handler's result; Status::UnknownMessageId without a handler;
289
+ * Status::InvalidState before start().
290
+ */
291
+ Status dispatch(Span<const std::uint8_t> payload) const noexcept {
292
+ return dispatch(RawMessage{payload, payload});
293
+ }
294
+
295
+ /**
296
+ * @brief Passes @p message to the handler.
297
+ * @return The handler's result; Status::UnknownMessageId without a handler;
298
+ * Status::InvalidState before start().
299
+ */
300
+ Status dispatch(const RawMessage& message) const noexcept {
301
+ if (!started_.load(std::memory_order_acquire)) {
302
+ return Status::InvalidState;
303
+ }
304
+ if (!handler_) {
305
+ counters_.unknown_ids.increment();
306
+ return Status::UnknownMessageId;
307
+ }
308
+ counters_.dispatched.increment();
309
+ return handler_(message);
310
+ }
311
+
312
+ /**
313
+ * @brief Dispatches a complete frame: everything between header and trailer is the payload.
314
+ * @return Status::Truncated when @p frame is shorter than header and trailer; otherwise as
315
+ * dispatch().
316
+ */
317
+ Status dispatch_frame(Span<const std::uint8_t> frame) const noexcept {
318
+ if (frame.size() < header_size_ + trailer_size_) {
319
+ return Status::Truncated;
320
+ }
321
+ return dispatch(RawMessage{
322
+ frame.subspan(header_size_, frame.size() - header_size_ - trailer_size_), frame});
323
+ }
324
+
325
+ /// @brief Statistics since construction.
326
+ [[nodiscard]] const DispatchCounters& counters() const noexcept { return counters_; }
327
+
328
+ private:
329
+ std::size_t trailer_size_;
330
+ std::size_t header_size_;
331
+ MessageHandler handler_;
332
+ std::atomic<bool> started_{false};
333
+ mutable L lock_;
334
+ mutable DispatchCounters counters_;
335
+ };
336
+
337
+ } // namespace wireforge
338
+
339
+ #endif // WIREFORGE_DISPATCHER_HPP
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @file expr_support.hpp
3
+ * @brief Integer helper functions used by generated size and count expressions.
4
+ *
5
+ * All operands are `std::int64_t`. In constant expressions an overflow or a division by zero is a
6
+ * compile error.
7
+ */
8
+ #ifndef WIREFORGE_EXPR_SUPPORT_HPP
9
+ #define WIREFORGE_EXPR_SUPPORT_HPP
10
+
11
+ #include <cstdint>
12
+
13
+ namespace wireforge::expr {
14
+
15
+ /// @brief Smaller of @p lhs and @p rhs.
16
+ [[nodiscard]] constexpr std::int64_t min(std::int64_t lhs, std::int64_t rhs) noexcept {
17
+ return rhs < lhs ? rhs : lhs;
18
+ }
19
+
20
+ /// @brief Larger of @p lhs and @p rhs.
21
+ [[nodiscard]] constexpr std::int64_t max(std::int64_t lhs, std::int64_t rhs) noexcept {
22
+ return lhs < rhs ? rhs : lhs;
23
+ }
24
+
25
+ /**
26
+ * @brief Quotient rounded toward positive infinity.
27
+ * @param lhs Dividend.
28
+ * @param rhs Divisor, must not be 0.
29
+ */
30
+ [[nodiscard]] constexpr std::int64_t ceil_div(std::int64_t lhs, std::int64_t rhs) noexcept {
31
+ const std::int64_t quotient = lhs / rhs;
32
+ const bool exact = quotient * rhs == lhs;
33
+ return exact || ((lhs < 0) != (rhs < 0)) ? quotient : quotient + 1;
34
+ }
35
+
36
+ } // namespace wireforge::expr
37
+
38
+ #endif // WIREFORGE_EXPR_SUPPORT_HPP
@@ -0,0 +1,262 @@
1
+ /**
2
+ * @file frame_decoder.hpp
3
+ * @brief Extracts complete frames from a byte ring: sync + length, fixed size and length-prefixed
4
+ * framing, optional checksum verification and resynchronisation.
5
+ */
6
+ #ifndef WIREFORGE_FRAME_DECODER_HPP
7
+ #define WIREFORGE_FRAME_DECODER_HPP
8
+
9
+ #include "wireforge/byte_io.hpp"
10
+ #include "wireforge/checksum.hpp"
11
+ #include "wireforge/frame_layout.hpp"
12
+ #include "wireforge/lock.hpp"
13
+ #include "wireforge/span.hpp"
14
+ #include "wireforge/status.hpp"
15
+
16
+ #include <array>
17
+ #include <cstddef>
18
+ #include <cstdint>
19
+ #include <type_traits>
20
+
21
+ namespace wireforge {
22
+
23
+ /// @brief How frame boundaries are found in the byte stream.
24
+ enum class FramingKind : std::uint8_t {
25
+ Fixed, ///< Every frame has the same size (an optional sync field is still checked).
26
+ SyncLength, ///< A sync field marks the frame start; a length field gives its size.
27
+ LengthPrefixed, ///< A length field at the frame start gives its size; no sync field.
28
+ };
29
+
30
+ /// @brief What the length field of a frame counts.
31
+ enum class LengthCounts : std::uint8_t {
32
+ Payload, ///< Payload bytes only.
33
+ PayloadAndTrailer, ///< Payload and trailer bytes.
34
+ Frame, ///< The whole frame, header and trailer included.
35
+ };
36
+
37
+ /// @brief Which bytes a frame checksum covers.
38
+ enum class ChecksumCoverage : std::uint8_t {
39
+ HeaderAndPayload, ///< From the first header byte to the end of the payload.
40
+ Payload, ///< The payload only.
41
+ };
42
+
43
+ /**
44
+ * @brief Layout of the frames of one interface.
45
+ *
46
+ * Sync and length fields are whole bytes inside the header and use the frame byte order. When the
47
+ * decoder's checksum type is not NoChecksum, the checksum is a trailer field of the checksum's
48
+ * size.
49
+ */
50
+ struct FrameFormat {
51
+ FramingKind kind; ///< Framing method.
52
+ Endian endian; ///< Byte order of the sync, length and checksum fields.
53
+ std::size_t header_size; ///< Header bytes.
54
+ std::size_t trailer_size; ///< Trailer bytes.
55
+ std::size_t max_frame_size; ///< Largest frame; the frame size for FramingKind::Fixed.
56
+ FieldLocation sync; ///< Sync field; required for FramingKind::SyncLength.
57
+ std::uint64_t sync_value; ///< Expected value of the sync field.
58
+ FieldLocation length; ///< Length field; required unless the framing is fixed.
59
+ LengthCounts length_counts; ///< Meaning of the length field.
60
+ std::size_t checksum_offset; ///< Offset of the checksum inside the trailer.
61
+ ChecksumCoverage checksum_covers; ///< Bytes covered by the checksum.
62
+ bool resync; ///< On errors skip one byte and rescan; otherwise drop all buffered bytes.
63
+ };
64
+
65
+ /// @brief Checksum type meaning "frames carry no checksum".
66
+ struct NoChecksum {};
67
+
68
+ /// @brief Statistics of a FrameDecoder; readable from any thread.
69
+ struct FrameCounters {
70
+ RelaxedCounter frames; ///< Frames delivered.
71
+ RelaxedCounter discarded_bytes; ///< Bytes dropped while searching for a frame start.
72
+ RelaxedCounter length_errors; ///< Frames rejected because of their length field.
73
+ RelaxedCounter checksum_errors; ///< Frames rejected because of their checksum.
74
+ RelaxedCounter overflow_errors; ///< Frames that did not fit the output buffer.
75
+ };
76
+
77
+ /**
78
+ * @brief Finds frames in a byte ring and copies each complete frame to a caller buffer.
79
+ *
80
+ * Incomplete frames stay in the ring until more bytes arrive, so the result does not depend on how
81
+ * the stream was chunked. The ring must provide `size()`, `capacity()`,
82
+ * `peek(Span<std::uint8_t>, std::size_t)` and `consume(std::size_t)` (SpscByteRing,
83
+ * LockedByteRing). Use one decoder per ring, called from the ring's consumer thread.
84
+ * @tparam Checksum Checksum algorithm of the trailer, or NoChecksum.
85
+ */
86
+ template <typename Checksum = NoChecksum> class FrameDecoder {
87
+ static constexpr bool kHasChecksum = !std::is_same_v<Checksum, NoChecksum>;
88
+ static_assert(!kHasChecksum || is_checksum_v<Checksum>,
89
+ "Checksum must be NoChecksum or satisfy the checksum contract");
90
+
91
+ public:
92
+ /// @brief Creates a decoder for @p format; check valid() before use.
93
+ explicit constexpr FrameDecoder(const FrameFormat& format) noexcept : format_(format) {}
94
+
95
+ /// @brief true when the format is consistent (field positions, sizes, checksum placement).
96
+ [[nodiscard]] constexpr bool valid() const noexcept {
97
+ const FrameFormat& fmt = format_;
98
+ const std::size_t overhead = fmt.header_size + fmt.trailer_size;
99
+ const bool sized = fmt.max_frame_size >= overhead && fmt.max_frame_size > 0U;
100
+ const bool sync_ok =
101
+ field_ok(fmt.sync) && (fmt.kind != FramingKind::SyncLength || fmt.sync.width != 0U);
102
+ const bool length_ok =
103
+ field_ok(fmt.length) && (fmt.kind == FramingKind::Fixed || fmt.length.width != 0U);
104
+ return sized && sync_ok && length_ok && checksum_ok();
105
+ }
106
+
107
+ /**
108
+ * @brief Extracts the next frame from @p ring.
109
+ * @param ring Byte source; bytes of delivered and rejected frames are consumed.
110
+ * @param out Destination of the frame (header, payload and trailer).
111
+ * @param[out] frame_size Size of the delivered frame; 0 unless Status::Ok is returned.
112
+ * @return Status::Ok when a frame was copied to @p out;
113
+ * Status::Truncated when more bytes are needed (call again after the next write);
114
+ * Status::InvalidLength, Status::ChecksumMismatch or Status::BufferTooSmall when a
115
+ * frame was rejected (call again to continue with the following bytes);
116
+ * Status::InvalidState when the format is not valid().
117
+ */
118
+ template <typename Ring>
119
+ Status next(Ring& ring, Span<std::uint8_t> out, std::size_t& frame_size) noexcept {
120
+ frame_size = 0U;
121
+ if (!valid()) {
122
+ return Status::InvalidState;
123
+ }
124
+ for (;;) {
125
+ if (ring.size() < format_.header_size || ring.size() == 0U) {
126
+ return Status::Truncated;
127
+ }
128
+ if (!sync_matches(ring)) {
129
+ counters_.discarded_bytes.add(ring.consume(1U));
130
+ continue;
131
+ }
132
+ std::size_t total = 0U;
133
+ Status status = frame_length(ring, total);
134
+ if (status == Status::Ok && ring.capacity() < total) {
135
+ status = Status::InvalidLength;
136
+ }
137
+ if (status != Status::Ok) {
138
+ counters_.length_errors.increment();
139
+ return reject(ring, status);
140
+ }
141
+ if (ring.size() < total) {
142
+ return Status::Truncated;
143
+ }
144
+ if (out.size() < total) {
145
+ counters_.overflow_errors.increment();
146
+ return reject(ring, Status::BufferTooSmall);
147
+ }
148
+ const Span<std::uint8_t> frame = out.first(total);
149
+ static_cast<void>(ring.peek(frame, 0U));
150
+ if (!checksum_matches(Span<const std::uint8_t>(frame))) {
151
+ counters_.checksum_errors.increment();
152
+ return reject(ring, Status::ChecksumMismatch);
153
+ }
154
+ static_cast<void>(ring.consume(total));
155
+ counters_.frames.increment();
156
+ frame_size = total;
157
+ return Status::Ok;
158
+ }
159
+ }
160
+
161
+ /// @brief Statistics since construction or the last reset_counters().
162
+ [[nodiscard]] const FrameCounters& counters() const noexcept { return counters_; }
163
+
164
+ /// @brief Sets every counter to zero.
165
+ void reset_counters() noexcept {
166
+ counters_.frames.reset();
167
+ counters_.discarded_bytes.reset();
168
+ counters_.length_errors.reset();
169
+ counters_.checksum_errors.reset();
170
+ counters_.overflow_errors.reset();
171
+ }
172
+
173
+ /// @brief The format this decoder was created with.
174
+ [[nodiscard]] constexpr const FrameFormat& format() const noexcept { return format_; }
175
+
176
+ private:
177
+ [[nodiscard]] constexpr bool field_ok(const FieldLocation& field) const noexcept {
178
+ return field.width == 0U ||
179
+ (valid_width(field.width) && field.offset <= format_.header_size &&
180
+ format_.header_size - field.offset >= field.width);
181
+ }
182
+
183
+ [[nodiscard]] constexpr bool checksum_ok() const noexcept {
184
+ if constexpr (kHasChecksum) {
185
+ return format_.checksum_offset <= format_.trailer_size &&
186
+ format_.trailer_size - format_.checksum_offset >= Checksum::kSize;
187
+ } else {
188
+ return true;
189
+ }
190
+ }
191
+
192
+ template <typename Ring>
193
+ [[nodiscard]] std::uint64_t peek_field(const Ring& ring,
194
+ const FieldLocation& field) const noexcept {
195
+ std::array<std::uint8_t, kMaxWidth> bytes{};
196
+ const Span<std::uint8_t> view = Span<std::uint8_t>(bytes).first(field.width);
197
+ static_cast<void>(ring.peek(view, field.offset));
198
+ return load_unsigned(Span<const std::uint8_t>(view), field.width, format_.endian);
199
+ }
200
+
201
+ template <typename Ring> [[nodiscard]] bool sync_matches(const Ring& ring) const noexcept {
202
+ if (format_.sync.width == 0U) {
203
+ return true;
204
+ }
205
+ return peek_field(ring, format_.sync) == format_.sync_value;
206
+ }
207
+
208
+ template <typename Ring>
209
+ Status frame_length(const Ring& ring, std::size_t& total) const noexcept {
210
+ const std::size_t overhead = format_.header_size + format_.trailer_size;
211
+ if (format_.kind == FramingKind::Fixed) {
212
+ total = format_.max_frame_size;
213
+ return Status::Ok;
214
+ }
215
+ const std::uint64_t length = peek_field(ring, format_.length);
216
+ if (length > format_.max_frame_size) {
217
+ return Status::InvalidLength;
218
+ }
219
+ const auto value = static_cast<std::size_t>(length);
220
+ // An if chain: a switch over every LengthCounts value needs a default label that
221
+ // -Wcovered-switch-default rejects.
222
+ if (format_.length_counts == LengthCounts::Payload) {
223
+ total = overhead + value;
224
+ } else if (format_.length_counts == LengthCounts::PayloadAndTrailer) {
225
+ total = format_.header_size + value;
226
+ } else {
227
+ total = value;
228
+ }
229
+ return total >= overhead && total <= format_.max_frame_size ? Status::Ok
230
+ : Status::InvalidLength;
231
+ }
232
+
233
+ [[nodiscard]] bool checksum_matches(Span<const std::uint8_t> frame) const noexcept {
234
+ if constexpr (kHasChecksum) {
235
+ const std::size_t payload_end = frame.size() - format_.trailer_size;
236
+ const std::size_t start =
237
+ format_.checksum_covers == ChecksumCoverage::Payload ? format_.header_size : 0U;
238
+ const std::uint64_t expected =
239
+ compute_checksum<Checksum>(frame.subspan(start, payload_end - start));
240
+ const std::uint64_t received =
241
+ load_unsigned(frame.subspan(payload_end + format_.checksum_offset), Checksum::kSize,
242
+ format_.endian);
243
+ return expected == received;
244
+ } else {
245
+ static_cast<void>(frame);
246
+ return true;
247
+ }
248
+ }
249
+
250
+ template <typename Ring> Status reject(Ring& ring, Status status) noexcept {
251
+ const std::size_t dropped = format_.resync ? ring.consume(1U) : ring.consume(ring.size());
252
+ counters_.discarded_bytes.add(dropped);
253
+ return status;
254
+ }
255
+
256
+ FrameFormat format_;
257
+ FrameCounters counters_;
258
+ };
259
+
260
+ } // namespace wireforge
261
+
262
+ #endif // WIREFORGE_FRAME_DECODER_HPP
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @file frame_layout.hpp
3
+ * @brief Positions of fields inside frame headers and trailers.
4
+ */
5
+ #ifndef WIREFORGE_FRAME_LAYOUT_HPP
6
+ #define WIREFORGE_FRAME_LAYOUT_HPP
7
+
8
+ #include <cstddef>
9
+
10
+ namespace wireforge {
11
+
12
+ /// @brief Position of a header or trailer field; a width of 0 means "no such field".
13
+ struct FieldLocation {
14
+ std::size_t offset; ///< Byte offset inside the header (or trailer).
15
+ std::size_t width; ///< Width in bytes, 0..8.
16
+ };
17
+
18
+ } // namespace wireforge
19
+
20
+ #endif // WIREFORGE_FRAME_LAYOUT_HPP