microfmt
Zero-allocation C++ formatting for constrained environments
Loading...
Searching...
No Matches
gdb_stream.hpp
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2026 Jarosław Pelczar <jarek@jpelczar.com>
2// SPDX-License-Identifier: BSD-2-Clause
3
4#pragma once
5
6#include "../microfmt.hpp"
7#include "../reloco.hpp"
8
9namespace microfmt::gdb {
10
17public:
18 explicit constexpr gdb_packet_writer(span<char> buffer RELOCO_LIFETIMEBOUND RELOCO_LIFETIME_CAPTURE_BY_THIS) noexcept
19 : m_buf(buffer), m_pos(0), m_checksum(0), m_finalized(false) {
20 // Start packet with '$'
21 if (m_buf.size() > 0) {
22 m_buf[0] = '$';
23 m_pos = 1;
24 }
25 }
26
30 [[nodiscard]] sink as_sink() noexcept RELOCO_LIFETIMEBOUND {
31 return sink{this, [](void *ctx, string_view sv) noexcept {
32 auto *self = static_cast<gdb_packet_writer *>(ctx);
33 if (self->m_finalized)
34 return;
35
36 for (char c : sv) {
37 if (self->m_pos < self->m_buf.size()) {
38 self->m_buf[self->m_pos++] = c;
39 self->m_checksum += static_cast<uint8_t>(c);
40 }
41 }
42 }};
43 }
44
49 [[nodiscard]] string_view finalize() noexcept RELOCO_LIFETIMEBOUND {
50 if (m_finalized) {
51 return string_view(m_buf.data(), m_total_len);
52 }
53
54 // We need 4 bytes for `#XX`
55 if (m_pos + 3 > m_buf.size()) {
56 return string_view{}; // Overflow guard
57 }
58
59 m_buf[m_pos++] = '#';
60
61 // Format 8-bit checksum as 2 hex digits
62 m_buf[m_pos++] = microfmt::detail::hex_digits_lower[(m_checksum >> 4) & 0x0F];
63 m_buf[m_pos++] = microfmt::detail::hex_digits_lower[m_checksum & 0x0F];
64
65 m_total_len = m_pos;
66 m_finalized = true;
67 return string_view(m_buf.data(), m_total_len);
68 }
69
70 [[nodiscard]] constexpr bool is_finalized() const noexcept { return m_finalized; }
71
72private:
73 span<char> m_buf;
74 size_t m_pos{0};
75 size_t m_total_len{0};
76 uint8_t m_checksum{0};
77 bool m_finalized{false};
78};
79
87public:
91 enum class status : uint8_t {
92 in_progress, // Packet framing or payload is still being received
93 ready, // A complete packet was received and checksum validated successfully
94 error_overflow, // Payload exceeded the capacity of the scratch buffer
95 error_checksum, // Computed checksum did not match the trailing packet checksum
96 error_format // Malformed packet framing or invalid hex characters
97 };
98
103 explicit constexpr gdb_streaming_decoder(
104 span<char> payload_scratch RELOCO_LIFETIMEBOUND RELOCO_LIFETIME_CAPTURE_BY_THIS) noexcept
105 : m_buf(payload_scratch) {
106 reset();
107 }
108
112 constexpr void reset() noexcept {
113 m_pos = 0;
114 m_running_sum = 0;
115 m_expected_checksum = 0;
116 m_nibble_count = 0;
117 m_escaped = false;
118 m_state = state::waiting_for_start;
119 }
120
127 // decodes raw bytes of a target address space; casts, pointers and offsets are bounded by the explicit lengths
128 // checked here.
129 RELOCO_BEGIN_UNSAFE_CODE
130 constexpr status feed(char c) noexcept {
131 switch (m_state) {
132 case state::waiting_for_start:
133 if (c == '$') {
134 m_pos = 0;
135 m_running_sum = 0;
136 m_escaped = false;
137 m_state = state::reading_payload;
138 }
139 // Discard background noise, ACKs ('+'), or NAKs ('-') outside packets
140 return status::in_progress;
141
142 case state::reading_payload:
143 if (c == '#') {
144 m_state = state::reading_checksum_1;
145 m_nibble_count = 0;
146 m_expected_checksum = 0;
147 return status::in_progress;
148 }
149 if (c == '$') {
150 // Restart packet if a new start marker appears unexpectedly
151 reset();
152 m_state = state::reading_payload;
153 return status::in_progress;
154 }
155
156 // GDB checksum accumulates raw transmitted bytes (including escape markers)
157 m_running_sum += static_cast<uint8_t>(c);
158
159 if (m_escaped) {
160 const char decoded = static_cast<char>(c ^ 0x20);
161 if (m_pos >= m_buf.size()) {
162 m_state = state::waiting_for_start;
163 return status::error_overflow;
164 }
165 m_buf[m_pos++] = decoded;
166 m_escaped = false;
167 } else if (c == '}') {
168 m_escaped = true;
169 } else {
170 if (m_pos >= m_buf.size()) {
171 m_state = state::waiting_for_start;
172 return status::error_overflow;
173 }
174 m_buf[m_pos++] = c;
175 }
176 return status::in_progress;
177
178 case state::reading_checksum_1:
179 case state::reading_checksum_2: {
180 uint8_t nibble = 0;
181 if (!parse_hex_nibble(c, nibble)) {
182 m_state = state::waiting_for_start;
183 return status::error_format;
184 }
185 m_expected_checksum = static_cast<uint8_t>((m_expected_checksum << 4) | nibble);
186
187 if (m_state == state::reading_checksum_1) {
188 m_state = state::reading_checksum_2;
189 } else {
190 // Second checksum nibble received — validate
191 m_state = state::waiting_for_start;
192 if (m_running_sum != m_expected_checksum) {
193 return status::error_checksum;
194 }
195 return status::ready;
196 }
197 return status::in_progress;
198 }
199 }
200
201 return status::in_progress;
202 }
204
209 [[nodiscard]] constexpr string_view payload() const noexcept RELOCO_LIFETIMEBOUND {
210 return string_view(m_buf.data(), m_pos);
211 }
212
213private:
214 enum class state : uint8_t { waiting_for_start, reading_payload, reading_checksum_1, reading_checksum_2 };
215
216 span<char> m_buf;
217 size_t m_pos{0};
218 uint8_t m_running_sum{0};
219 uint8_t m_expected_checksum{0};
220 uint8_t m_nibble_count{0};
221 bool m_escaped{false};
222 state m_state{state::waiting_for_start};
223
224 // decodes raw bytes of a target address space; casts, pointers and offsets are bounded by the explicit lengths
225 // checked here.
226 RELOCO_BEGIN_UNSAFE_CODE
227 [[nodiscard]] static constexpr bool parse_hex_nibble(char c, uint8_t &out_val) noexcept {
228 if (c >= '0' && c <= '9') {
229 out_val = static_cast<uint8_t>(c - '0');
230 return true;
231 }
232 if (c >= 'a' && c <= 'f') {
233 out_val = static_cast<uint8_t>(c - 'a' + 10);
234 return true;
235 }
236 if (c >= 'A' && c <= 'F') {
237 out_val = static_cast<uint8_t>(c - 'A' + 10);
238 return true;
239 }
240 return false;
241 }
243};
244
245} // namespace microfmt::gdb
Zero-allocation GDB Remote Serial Protocol packet builder sink.
Definition gdb_stream.hpp:16
string_view finalize() noexcept RELOCO_LIFETIMEBOUND
Finalizes the packet by appending #XX checksum.
Definition gdb_stream.hpp:49
sink as_sink() noexcept RELOCO_LIFETIMEBOUND
Creates a type-erased sink adapter for formatting.
Definition gdb_stream.hpp:30
constexpr gdb_packet_writer(span< char > buffer RELOCO_LIFETIMEBOUND RELOCO_LIFETIME_CAPTURE_BY_THIS) noexcept
Definition gdb_stream.hpp:18
constexpr bool is_finalized() const noexcept
Definition gdb_stream.hpp:70
Stateful streaming decoder for incoming GDB Remote Serial Protocol packets.
Definition gdb_stream.hpp:86
constexpr gdb_streaming_decoder(span< char > payload_scratch RELOCO_LIFETIMEBOUND RELOCO_LIFETIME_CAPTURE_BY_THIS) noexcept
Constructs a streaming decoder over a caller-provided scratch span.
Definition gdb_stream.hpp:103
RELOCO_BEGIN_UNSAFE_CODE constexpr status feed(char c) noexcept
Feeds a single incoming character into the stateful decoder.
Definition gdb_stream.hpp:130
status
Status returned after feeding a byte into the decoder.
Definition gdb_stream.hpp:91
constexpr void reset() noexcept
Resets the decoder state machine back to waiting for a new packet start ('$').
Definition gdb_stream.hpp:112
RELOCO_END_UNSAFE_CODE constexpr string_view payload() const noexcept RELOCO_LIFETIMEBOUND
Returns a non-owning string view over the fully unescaped packet payload. Valid only when feed() retu...
Definition gdb_stream.hpp:209
#define MICROFMT_API_CLASS
Definition compat.hpp:181
RELOCO_BEGIN_UNSAFE_CODE constexpr bool parse_hex_nibble(char c, uint8_t &val) noexcept
Definition gdb_decoders.hpp:21
Definition gdb_decoders.hpp:15
reloco::sink sink
Type-erased, zero-allocation output sink.
Definition microfmt.hpp:46
RELOCO_END_UNSAFE_CODE
Definition remote_object.hpp:132