ESPHome 2026.5.0-dev
Loading...
Searching...
No Matches
proto.cpp
Go to the documentation of this file.
1#include "proto.h"
2#include <cinttypes>
3#include <cstring>
5#include "esphome/core/log.h"
6
7namespace esphome::api {
8
9static const char *const TAG = "api.proto";
10
11uint32_t ProtoSize::varint_slow(uint32_t value) { return varint_wide(value); }
12
14 do {
15 this->debug_check_bounds_(1);
16 *this->pos_++ = static_cast<uint8_t>(value | 0x80);
17 value >>= 7;
18 } while (value > 0x7F);
19 this->debug_check_bounds_(1);
20 *this->pos_++ = static_cast<uint8_t>(value);
21}
22
24 // Multi-byte varint: first byte already checked to have high bit set
25 uint32_t result32 = buffer[0] & 0x7F;
26#ifdef USE_API_VARINT64
27 uint32_t limit = std::min(len, uint32_t(4));
28#else
29 uint32_t limit = std::min(len, uint32_t(5));
30#endif
31 for (uint32_t i = 1; i < limit; i++) {
32 uint8_t val = buffer[i];
33 result32 |= uint32_t(val & 0x7F) << (i * 7);
34 if ((val & 0x80) == 0) {
35 return {result32, i + 1};
36 }
37 }
38#ifdef USE_API_VARINT64
39 return parse_wide(buffer, len, result32);
40#else
41 return {0, PROTO_VARINT_PARSE_FAILED};
42#endif
43}
44
45#ifdef USE_API_VARINT64
47 uint64_t result64 = result32;
48 uint32_t limit = std::min(len, uint32_t(10));
49 for (uint32_t i = 4; i < limit; i++) {
50 uint8_t val = buffer[i];
51 result64 |= uint64_t(val & 0x7F) << (i * 7);
52 if ((val & 0x80) == 0) {
53 return {result64, i + 1};
54 }
55 }
56 return {0, PROTO_VARINT_PARSE_FAILED};
57}
58#endif
59
60uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size_t length, uint32_t target_field_id) {
61 uint32_t count = 0;
62 const uint8_t *ptr = buffer;
63 const uint8_t *end = buffer + length;
64
65 while (ptr < end) {
66 // Parse field header (tag) - ptr < end guarantees len >= 1
67 auto res = ProtoVarInt::parse_non_empty(ptr, end - ptr);
68 if (!res.has_value()) {
69 break; // Invalid data, stop counting
70 }
71
72 uint32_t tag = static_cast<uint32_t>(res.value);
73 uint32_t field_type = tag & WIRE_TYPE_MASK;
74 uint32_t field_id = tag >> 3;
75 ptr += res.consumed;
76
77 // Count if this is the target field
78 if (field_id == target_field_id) {
79 count++;
80 }
81
82 // Skip field data based on wire type
83 switch (field_type) {
84 case WIRE_TYPE_VARINT: { // VarInt - parse and skip
85 res = ProtoVarInt::parse(ptr, end - ptr);
86 if (!res.has_value()) {
87 return count; // Invalid data, return what we have
88 }
89 ptr += res.consumed;
90 break;
91 }
92 case WIRE_TYPE_LENGTH_DELIMITED: { // Length-delimited - parse length and skip data
93 res = ProtoVarInt::parse(ptr, end - ptr);
94 if (!res.has_value()) {
95 return count;
96 }
97 uint32_t field_length = static_cast<uint32_t>(res.value);
98 ptr += res.consumed;
99 if (field_length > static_cast<size_t>(end - ptr)) {
100 return count; // Out of bounds
101 }
102 ptr += field_length;
103 break;
104 }
105 case WIRE_TYPE_FIXED32: { // 32-bit - skip 4 bytes
106 if (end - ptr < 4) {
107 return count;
108 }
109 ptr += 4;
110 break;
111 }
112 default:
113 // Unknown wire type, can't continue
114 return count;
115 }
116 }
117
118 return count;
119}
120
121// Single-pass encode for repeated submessage elements (non-template core).
122// Writes field tag, reserves 1 byte for length varint, encodes the submessage body,
123// then backpatches the actual length. For the common case (body < 128 bytes), this is
124// just a single byte write with no memmove — all current repeated submessage types
125// (BLE advertisements at ~47B, GATT descriptors at ~24B, service args, etc.) take
126// this fast path.
127//
128// The memmove fallback for body >= 128 bytes exists only for correctness (e.g., a GATT
129// characteristic with many descriptors). It is safe because calculate_size() already
130// reserved space for the full multi-byte varint — the shift fills that reserved space:
131//
132// calculate_size() allocates per element: tag + varint_size(body) + body_size
133//
134// After encode, before memmove (1 byte reserved, body written):
135// [tag][__][body ..... body][??]
136// ^ ^-- unused byte (v2 space from calculate_size)
137// len_pos
138//
139// After memmove(body_start+1, body_start, body_size):
140// [tag][__][__][body ..... body]
141// ^ ^-- body shifted forward, fills v2 space exactly
142// len_pos
143//
144// After writing 2-byte varint at len_pos:
145// [tag][v1][v2][body ..... body]
146// ^-- pos_ = element end, within buffer
147void ProtoWriteBuffer::encode_sub_message(uint32_t field_id, const void *value,
148 uint8_t *(*encode_fn)(const void *,
149 ProtoWriteBuffer &PROTO_ENCODE_DEBUG_PARAM)) {
150 this->encode_field_raw(field_id, 2);
151 // Reserve 1 byte for length varint (optimistic: submessage < 128 bytes)
152 uint8_t *len_pos = this->pos_;
153 this->debug_check_bounds_(1);
154 this->pos_++;
155 uint8_t *body_start = this->pos_;
156 this->pos_ = encode_fn(value, *this PROTO_ENCODE_DEBUG_INIT(this->buffer_));
157 uint32_t body_size = static_cast<uint32_t>(this->pos_ - body_start);
158 if (body_size < 128) [[likely]] {
159 // Common case: 1-byte varint, just backpatch
160 *len_pos = static_cast<uint8_t>(body_size);
161 return;
162 }
163 // Compute extra bytes needed for varint beyond the 1 already reserved
164 uint8_t extra = ProtoSize::varint(body_size) - 1;
165 // Shift body forward to make room for the extra varint bytes
166 this->debug_check_bounds_(extra);
167 std::memmove(body_start + extra, body_start, body_size);
168 uint8_t *end = this->pos_ + extra;
169 // Write the full varint at len_pos
170 this->pos_ = len_pos;
171 this->encode_varint_raw(body_size);
172 this->pos_ = end;
173}
174
175// Non-template core for encode_optional_sub_message.
176void ProtoWriteBuffer::encode_optional_sub_message(uint32_t field_id, uint32_t nested_size, const void *value,
177 uint8_t *(*encode_fn)(const void *,
178 ProtoWriteBuffer &PROTO_ENCODE_DEBUG_PARAM)) {
179 if (nested_size == 0)
180 return;
181 this->encode_field_raw(field_id, 2);
182 this->encode_varint_raw(nested_size);
183#ifdef ESPHOME_DEBUG_API
184 uint8_t *start = this->pos_;
185 this->pos_ = encode_fn(value, *this PROTO_ENCODE_DEBUG_INIT(this->buffer_));
186 if (static_cast<uint32_t>(this->pos_ - start) != nested_size)
187 this->debug_check_encode_size_(field_id, nested_size, this->pos_ - start);
188#else
189 this->pos_ = encode_fn(value, *this PROTO_ENCODE_DEBUG_INIT(this->buffer_));
190#endif
191}
192
193#ifdef ESPHOME_DEBUG_API
194void proto_check_bounds_failed(const uint8_t *pos, size_t bytes, const uint8_t *end, const char *caller) {
195 ESP_LOGE(TAG, "Proto encode bounds check failed in %s: need %zu bytes, %td available", caller, bytes, end - pos);
196 abort();
197}
198void ProtoWriteBuffer::debug_check_bounds_(size_t bytes, const char *caller) {
199 if (this->pos_ + bytes > this->buffer_->data() + this->buffer_->size()) {
200 ESP_LOGE(TAG, "ProtoWriteBuffer bounds check failed in %s: bytes=%zu offset=%td buf_size=%zu", caller, bytes,
201 this->pos_ - this->buffer_->data(), this->buffer_->size());
202 abort();
203 }
204}
205void ProtoWriteBuffer::debug_check_encode_size_(uint32_t field_id, uint32_t expected, ptrdiff_t actual) {
206 ESP_LOGE(TAG, "encode_message: size mismatch for field %" PRIu32 ": calculated=%" PRIu32 " actual=%td", field_id,
207 expected, actual);
208 abort();
209}
210
211#endif
212
213void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
214 const uint8_t *ptr = buffer;
215 const uint8_t *end = buffer + length;
216
217 while (ptr < end) {
218 // Parse field header - ptr < end guarantees len >= 1
219 auto res = ProtoVarInt::parse_non_empty(ptr, end - ptr);
220 if (!res.has_value()) {
221 ESP_LOGV(TAG, "Invalid field start at offset %ld", (long) (ptr - buffer));
222 return;
223 }
224
225 uint32_t tag = static_cast<uint32_t>(res.value);
226 uint32_t field_type = tag & WIRE_TYPE_MASK;
227 uint32_t field_id = tag >> 3;
228 ptr += res.consumed;
229
230 switch (field_type) {
231 case WIRE_TYPE_VARINT: { // VarInt
232 res = ProtoVarInt::parse(ptr, end - ptr);
233 if (!res.has_value()) {
234 ESP_LOGV(TAG, "Invalid VarInt at offset %ld", (long) (ptr - buffer));
235 return;
236 }
237 if (!this->decode_varint(field_id, res.value)) {
238 ESP_LOGV(TAG, "Cannot decode VarInt field %" PRIu32 " with value %" PRIu64 "!", field_id,
239 static_cast<uint64_t>(res.value));
240 }
241 ptr += res.consumed;
242 break;
243 }
244 case WIRE_TYPE_LENGTH_DELIMITED: { // Length-delimited
245 res = ProtoVarInt::parse(ptr, end - ptr);
246 if (!res.has_value()) {
247 ESP_LOGV(TAG, "Invalid Length Delimited at offset %ld", (long) (ptr - buffer));
248 return;
249 }
250 uint32_t field_length = static_cast<uint32_t>(res.value);
251 ptr += res.consumed;
252 if (field_length > static_cast<size_t>(end - ptr)) {
253 ESP_LOGV(TAG, "Out-of-bounds Length Delimited at offset %ld", (long) (ptr - buffer));
254 return;
255 }
256 if (!this->decode_length(field_id, ProtoLengthDelimited(ptr, field_length))) {
257 ESP_LOGV(TAG, "Cannot decode Length Delimited field %" PRIu32 "!", field_id);
258 }
259 ptr += field_length;
260 break;
261 }
262 case WIRE_TYPE_FIXED32: { // 32-bit
263 if (end - ptr < 4) {
264 ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer));
265 return;
266 }
268#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
269 // Protobuf fixed32 is little-endian — direct load on LE platforms
270 memcpy(&val, ptr, 4);
271#else
272 val = encode_uint32(ptr[3], ptr[2], ptr[1], ptr[0]);
273#endif
274 if (!this->decode_32bit(field_id, Proto32Bit(val))) {
275 ESP_LOGV(TAG, "Cannot decode 32-bit field %" PRIu32 " with value %" PRIu32 "!", field_id, val);
276 }
277 ptr += 4;
278 break;
279 }
280 default:
281 ESP_LOGV(TAG, "Invalid field type %" PRIu32 " at offset %ld", field_type, (long) (ptr - buffer));
282 return;
283 }
284 }
285}
286
287} // namespace esphome::api
virtual bool decode_32bit(uint32_t field_id, Proto32Bit value)
Definition proto.h:641
virtual bool decode_varint(uint32_t field_id, proto_varint_value_t value)
Definition proto.h:639
void decode(const uint8_t *buffer, size_t length)
Definition proto.cpp:213
virtual bool decode_length(uint32_t field_id, ProtoLengthDelimited value)
Definition proto.h:640
static uint32_t count_repeated_field(const uint8_t *buffer, size_t length, uint32_t target_field_id)
Count occurrences of a repeated field in a protobuf buffer.
Definition proto.cpp:60
static constexpr uint32_t ESPHOME_ALWAYS_INLINE varint(uint32_t value)
Calculates the size in bytes needed to encode a uint32_t value as a varint.
Definition proto.h:671
static ProtoVarIntResult ESPHOME_ALWAYS_INLINE parse_non_empty(const uint8_t *buffer, uint32_t len)
Parse a varint from buffer.
Definition proto.h:140
static ProtoVarIntResult parse_wide(const uint8_t *buffer, uint32_t len, uint32_t result32) __attribute__((noinline))
Continue parsing varint bytes 4-9 with 64-bit arithmetic.
Definition proto.cpp:46
static ProtoVarIntResult parse_slow(const uint8_t *buffer, uint32_t len) __attribute__((noinline))
Definition proto.cpp:23
static ProtoVarIntResult ESPHOME_ALWAYS_INLINE parse(const uint8_t *buffer, uint32_t len)
Parse a varint from buffer (safe for empty buffers).
Definition proto.h:153
void debug_check_encode_size_(uint32_t field_id, uint32_t expected, ptrdiff_t actual)
Definition proto.cpp:205
void encode_varint_raw_slow_(uint32_t value) __attribute__((noinline))
Definition proto.cpp:13
void encode_field_raw(uint32_t field_id, uint32_t type)
Encode a field key (tag/wire type combination).
Definition proto.h:255
void encode_optional_sub_message(uint32_t field_id, const T &value)
Encode an optional singular submessage field — skips if empty.
Definition proto.h:860
void encode_sub_message(uint32_t field_id, const T &value)
Single-pass encode for repeated submessage elements.
Definition proto.h:855
void debug_check_bounds_(size_t bytes, const char *caller=__builtin_FUNCTION())
Definition proto.cpp:198
void ESPHOME_ALWAYS_INLINE encode_varint_raw(uint32_t value)
Definition proto.h:235
mopeka_std_values val[3]
constexpr uint8_t WIRE_TYPE_VARINT
Definition proto.h:23
constexpr uint8_t WIRE_TYPE_MASK
Definition proto.h:26
constexpr uint32_t PROTO_VARINT_PARSE_FAILED
Sentinel value for consumed field indicating parse failure.
Definition proto.h:124
constexpr uint8_t WIRE_TYPE_LENGTH_DELIMITED
Definition proto.h:24
constexpr uint8_t WIRE_TYPE_FIXED32
Definition proto.h:25
void proto_check_bounds_failed(const uint8_t *pos, size_t bytes, const uint8_t *end, const char *caller)
Definition proto.cpp:194
const char * tag
Definition log.h:74
std::string size_t len
Definition helpers.h:1045
size_t size_t pos
Definition helpers.h:1082
constexpr uint32_t encode_uint32(uint8_t byte1, uint8_t byte2, uint8_t byte3, uint8_t byte4)
Encode a 32-bit value given four bytes in most to least significant byte order.
Definition helpers.h:889
static void uint32_t
Result of parsing a varint: value + number of bytes consumed.
Definition proto.h:128
uint8_t end[39]
Definition sun_gtil2.cpp:17
uint16_t length
Definition tt21100.cpp:0