mirror of
https://github.com/esphome/esphome.git
synced 2026-09-08 22:08:49 +00:00
Compare commits
40
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3dea55368f | ||
|
|
76100fec58 | ||
|
|
4736550677 | ||
|
|
2d86ffec7e | ||
|
|
4509e5d388 | ||
|
|
0e3554f6f8 | ||
|
|
5ac2fa8811 | ||
|
|
e715696bd7 | ||
|
|
82d399400e | ||
|
|
a7bf937001 | ||
|
|
63b5331e47 | ||
|
|
4c72948575 | ||
|
|
77d850cbe2 | ||
|
|
a97e4ebc1c | ||
|
|
d09f642eb2 | ||
|
|
144cd419ad | ||
|
|
7910c372cd | ||
|
|
c2a4981e25 | ||
|
|
097d487261 | ||
|
|
89082c7b2d | ||
|
|
fb3befe377 | ||
|
|
4a3a787756 | ||
|
|
5aec93d7e8 | ||
|
|
a79cd1550c | ||
|
|
6d23e4c842 | ||
|
|
cbdcfe640c | ||
|
|
2bc20d8721 | ||
|
|
2dbb3e055b | ||
|
|
7549309ad3 | ||
|
|
e600180417 | ||
|
|
0641dae9d2 | ||
|
|
bcf812d62b | ||
|
|
b0ce7f58f3 | ||
|
|
455e1d6374 | ||
|
|
3af1d50bce | ||
|
|
842f354a05 | ||
|
|
af9b59d4bd | ||
|
|
55fc5a10de | ||
|
|
79927b918b | ||
|
|
1b070629bc |
@@ -345,11 +345,7 @@ class APIConnection final : public APIServerConnectionBase {
|
|||||||
/// Returns false as soon as the TCP buffer is full. Marked nodiscard so we
|
/// Returns false as soon as the TCP buffer is full. Marked nodiscard so we
|
||||||
/// have no silent failures: every caller must handle (or log) a refusal.
|
/// have no silent failures: every caller must handle (or log) a refusal.
|
||||||
template<typename T> [[nodiscard]] bool send_message(const T &msg) {
|
template<typename T> [[nodiscard]] bool send_message(const T &msg) {
|
||||||
if constexpr (T::ESTIMATED_SIZE == 0) {
|
return this->send_message_(T::calc_size_msg(&msg), T::MESSAGE_TYPE, &T::encode_msg, &msg);
|
||||||
return this->send_message_(0, T::MESSAGE_TYPE, &encode_msg_noop, &msg);
|
|
||||||
} else {
|
|
||||||
return this->send_message_(msg.calculate_size(), T::MESSAGE_TYPE, &proto_encode_msg<T>, &msg);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clear the shared write buffer and reserve space for the first message.
|
/// Clear the shared write buffer and reserve space for the first message.
|
||||||
@@ -405,16 +401,6 @@ class APIConnection final : public APIServerConnectionBase {
|
|||||||
void process_state_subscriptions_();
|
void process_state_subscriptions_();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Size thunk — converts void* back to concrete type for direct calculate_size() call
|
|
||||||
template<typename T> static uint32_t calc_size(const void *msg) {
|
|
||||||
return static_cast<const T *>(msg)->calculate_size();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shared no-op encode thunk for empty messages (ESTIMATED_SIZE == 0)
|
|
||||||
static uint8_t *encode_msg_noop(const void *, ProtoWriteBuffer &buf PROTO_ENCODE_DEBUG_PARAM) {
|
|
||||||
return buf.get_pos();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Non-template buffer management for send_message
|
// Non-template buffer management for send_message
|
||||||
bool send_message_(uint32_t payload_size, uint16_t message_type, MessageEncodeFn encode_fn, const void *msg);
|
bool send_message_(uint32_t payload_size, uint16_t message_type, MessageEncodeFn encode_fn, const void *msg);
|
||||||
|
|
||||||
@@ -433,11 +419,7 @@ class APIConnection final : public APIServerConnectionBase {
|
|||||||
// Hot paths (state/info) go through fill_and_encode_entity_state/info instead.
|
// Hot paths (state/info) go through fill_and_encode_entity_state/info instead.
|
||||||
// batch_message_type_ is already set by dispatch_message_ before reaching here.
|
// batch_message_type_ is already set by dispatch_message_ before reaching here.
|
||||||
template<typename T> static uint16_t encode_message_to_buffer(T &msg, APIConnection *conn, uint32_t remaining_size) {
|
template<typename T> static uint16_t encode_message_to_buffer(T &msg, APIConnection *conn, uint32_t remaining_size) {
|
||||||
if constexpr (T::ESTIMATED_SIZE == 0) {
|
return encode_to_buffer_slow(T::calc_size_msg(&msg), &T::encode_msg, &msg, conn, remaining_size);
|
||||||
return encode_to_buffer_slow(0, &encode_msg_noop, &msg, conn, remaining_size);
|
|
||||||
} else {
|
|
||||||
return encode_to_buffer_slow(msg.calculate_size(), &proto_encode_msg<T>, &msg, conn, remaining_size);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Non-template core — fills state fields and encodes
|
// Non-template core — fills state fields and encodes
|
||||||
@@ -449,7 +431,7 @@ class APIConnection final : public APIServerConnectionBase {
|
|||||||
template<typename T>
|
template<typename T>
|
||||||
static uint16_t fill_and_encode_entity_state(EntityBase *entity, T &msg, APIConnection *conn,
|
static uint16_t fill_and_encode_entity_state(EntityBase *entity, T &msg, APIConnection *conn,
|
||||||
uint32_t remaining_size) {
|
uint32_t remaining_size) {
|
||||||
return fill_and_encode_entity_state(entity, msg, &calc_size<T>, &proto_encode_msg<T>, conn, remaining_size);
|
return fill_and_encode_entity_state(entity, msg, &T::calc_size_msg, &T::encode_msg, conn, remaining_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Non-template core — fills info fields, allocates buffers, and encodes
|
// Non-template core — fills info fields, allocates buffers, and encodes
|
||||||
@@ -461,7 +443,7 @@ class APIConnection final : public APIServerConnectionBase {
|
|||||||
template<typename T>
|
template<typename T>
|
||||||
static uint16_t fill_and_encode_entity_info(EntityBase *entity, T &msg, APIConnection *conn,
|
static uint16_t fill_and_encode_entity_info(EntityBase *entity, T &msg, APIConnection *conn,
|
||||||
uint32_t remaining_size) {
|
uint32_t remaining_size) {
|
||||||
return fill_and_encode_entity_info(entity, msg, &calc_size<T>, &proto_encode_msg<T>, conn, remaining_size);
|
return fill_and_encode_entity_info(entity, msg, &T::calc_size_msg, &T::encode_msg, conn, remaining_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Non-template core — fills device_class, then delegates to fill_and_encode_entity_info
|
// Non-template core — fills device_class, then delegates to fill_and_encode_entity_info
|
||||||
@@ -475,8 +457,8 @@ class APIConnection final : public APIServerConnectionBase {
|
|||||||
static uint16_t fill_and_encode_entity_info_with_device_class(EntityBase *entity, T &msg,
|
static uint16_t fill_and_encode_entity_info_with_device_class(EntityBase *entity, T &msg,
|
||||||
StringRef &device_class_field, APIConnection *conn,
|
StringRef &device_class_field, APIConnection *conn,
|
||||||
uint32_t remaining_size) {
|
uint32_t remaining_size) {
|
||||||
return fill_and_encode_entity_info_with_device_class(entity, msg, device_class_field, &calc_size<T>,
|
return fill_and_encode_entity_info_with_device_class(entity, msg, device_class_field, &T::calc_size_msg,
|
||||||
&proto_encode_msg<T>, conn, remaining_size);
|
&T::encode_msg, conn, remaining_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef USE_VOICE_ASSISTANT
|
#ifdef USE_VOICE_ASSISTANT
|
||||||
|
|||||||
+2532
-2587
File diff suppressed because it is too large
Load Diff
+871
-351
File diff suppressed because it is too large
Load Diff
@@ -433,8 +433,9 @@ void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call
|
|||||||
// Home Assistant subscribes to actions shortly *after* authenticating, so actions
|
// Home Assistant subscribes to actions shortly *after* authenticating, so actions
|
||||||
// fired right at connection time (on_client_connected, on_time_sync, ...) can
|
// fired right at connection time (on_client_connected, on_time_sync, ...) can
|
||||||
// arrive before the subscription and are lost - warn instead of failing silently.
|
// arrive before the subscription and are lost - warn instead of failing silently.
|
||||||
ESP_LOGW(TAG, "Home Assistant %s '%s' dropped; %s",
|
ESP_LOGW(TAG, "Home Assistant %s '%.*s' dropped; %s",
|
||||||
call.is_event ? LOG_STR_LITERAL("event") : LOG_STR_LITERAL("action"), call.service.c_str(),
|
call.is_event ? LOG_STR_LITERAL("event") : LOG_STR_LITERAL("action"),
|
||||||
|
static_cast<int>(call.service.size()), call.service.empty() ? "" : call.service.c_str(),
|
||||||
this->is_connected() ? LOG_STR_LITERAL("client has not subscribed to actions (yet)")
|
this->is_connected() ? LOG_STR_LITERAL("client has not subscribed to actions (yet)")
|
||||||
: LOG_STR_LITERAL("no client connected"));
|
: LOG_STR_LITERAL("no client connected"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -210,77 +210,78 @@ void ProtoWriteBuffer::debug_check_encode_size_(uint32_t field_id, uint32_t expe
|
|||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
|
void ProtoDecodableMessage::decode_fields(void *msg, const uint8_t *buffer, size_t length, DecodeFieldFn field) {
|
||||||
const uint8_t *ptr = buffer;
|
const uint8_t *ptr = buffer;
|
||||||
const uint8_t *end = buffer + length;
|
const uint8_t *end = buffer + length;
|
||||||
|
|
||||||
while (ptr < end) {
|
// Single-byte varints dominate, so that case advances the cursor inline.
|
||||||
// Parse field header - ptr < end guarantees len >= 1
|
auto read_varint = [&](proto_varint_value_t &value) ESPHOME_ALWAYS_INLINE {
|
||||||
|
if (ptr == end)
|
||||||
|
return false;
|
||||||
|
if (*ptr < 0x80) [[likely]] {
|
||||||
|
value = *ptr++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
auto res = ProtoVarInt::parse_non_empty(ptr, end - ptr);
|
auto res = ProtoVarInt::parse_non_empty(ptr, end - ptr);
|
||||||
if (!res.has_value()) {
|
if (!res.has_value())
|
||||||
|
return false;
|
||||||
|
value = res.value;
|
||||||
|
ptr += res.consumed;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
while (ptr < end) {
|
||||||
|
proto_varint_value_t tag_value;
|
||||||
|
if (!read_varint(tag_value)) {
|
||||||
ESP_LOGV(TAG, "Invalid field start at offset %ld", (long) (ptr - buffer));
|
ESP_LOGV(TAG, "Invalid field start at offset %ld", (long) (ptr - buffer));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t tag = static_cast<uint32_t>(res.value);
|
uint32_t tag = static_cast<uint32_t>(tag_value);
|
||||||
uint32_t field_type = tag & WIRE_TYPE_MASK;
|
uint32_t field_type = tag & WIRE_TYPE_MASK;
|
||||||
uint32_t field_id = tag >> 3;
|
// Length-delimited fields move this past the length prefix
|
||||||
ptr += res.consumed;
|
const uint8_t *data = ptr;
|
||||||
|
proto_varint_value_t scalar;
|
||||||
|
|
||||||
switch (field_type) {
|
if (field_type == WIRE_TYPE_VARINT) [[likely]] {
|
||||||
case WIRE_TYPE_VARINT: { // VarInt
|
if (!read_varint(scalar)) {
|
||||||
res = ProtoVarInt::parse(ptr, end - ptr);
|
ESP_LOGV(TAG, "Invalid VarInt at offset %ld", (long) (ptr - buffer));
|
||||||
if (!res.has_value()) {
|
|
||||||
ESP_LOGV(TAG, "Invalid VarInt at offset %ld", (long) (ptr - buffer));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!this->decode_varint(field_id, res.value)) {
|
|
||||||
ESP_LOGV(TAG, "Cannot decode VarInt field %" PRIu32 " with value %" PRIu64 "!", field_id,
|
|
||||||
static_cast<uint64_t>(res.value));
|
|
||||||
}
|
|
||||||
ptr += res.consumed;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case WIRE_TYPE_LENGTH_DELIMITED: { // Length-delimited
|
|
||||||
res = ProtoVarInt::parse(ptr, end - ptr);
|
|
||||||
if (!res.has_value()) {
|
|
||||||
ESP_LOGV(TAG, "Invalid Length Delimited at offset %ld", (long) (ptr - buffer));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
uint32_t field_length = static_cast<uint32_t>(res.value);
|
|
||||||
ptr += res.consumed;
|
|
||||||
if (field_length > static_cast<size_t>(end - ptr)) {
|
|
||||||
ESP_LOGV(TAG, "Out-of-bounds Length Delimited at offset %ld", (long) (ptr - buffer));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!this->decode_length(field_id, ProtoLengthDelimited(ptr, field_length))) {
|
|
||||||
ESP_LOGV(TAG, "Cannot decode Length Delimited field %" PRIu32 "!", field_id);
|
|
||||||
}
|
|
||||||
ptr += field_length;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case WIRE_TYPE_FIXED32: { // 32-bit
|
|
||||||
if (end - ptr < 4) {
|
|
||||||
ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
uint32_t val;
|
|
||||||
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
|
||||||
// Protobuf fixed32 is little-endian — direct load on LE platforms
|
|
||||||
memcpy(&val, ptr, 4);
|
|
||||||
#else
|
|
||||||
val = encode_uint32(ptr[3], ptr[2], ptr[1], ptr[0]);
|
|
||||||
#endif
|
|
||||||
if (!this->decode_32bit(field_id, Proto32Bit(val))) {
|
|
||||||
ESP_LOGV(TAG, "Cannot decode 32-bit field %" PRIu32 " with value %" PRIu32 "!", field_id, val);
|
|
||||||
}
|
|
||||||
ptr += 4;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
ESP_LOGV(TAG, "Invalid field type %" PRIu32 " at offset %ld", field_type, (long) (ptr - buffer));
|
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch (field_type) {
|
||||||
|
case WIRE_TYPE_LENGTH_DELIMITED: {
|
||||||
|
proto_varint_value_t length_value;
|
||||||
|
if (!read_varint(length_value)) {
|
||||||
|
ESP_LOGV(TAG, "Invalid Length Delimited at offset %ld", (long) (ptr - buffer));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uint32_t field_length = static_cast<uint32_t>(length_value);
|
||||||
|
if (field_length > static_cast<size_t>(end - ptr)) {
|
||||||
|
ESP_LOGV(TAG, "Out-of-bounds Length Delimited at offset %ld", (long) (ptr - buffer));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
data = ptr;
|
||||||
|
scalar = field_length;
|
||||||
|
ptr += field_length;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case WIRE_TYPE_FIXED32: {
|
||||||
|
if (end - ptr < 4) {
|
||||||
|
ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Byte loads instead of memcpy: ESP-IDF passes -fno-builtin-memcpy, which made this a call
|
||||||
|
scalar = encode_uint32(ptr[3], ptr[2], ptr[1], ptr[0]);
|
||||||
|
ptr += 4;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
ESP_LOGV(TAG, "Invalid field type %" PRIu32 " at offset %ld", field_type, (long) (ptr - buffer));
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
field(msg, tag, data, scalar);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <type_traits>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
|
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
|
||||||
@@ -170,40 +171,43 @@ class ProtoVarInt {
|
|||||||
class ProtoMessage;
|
class ProtoMessage;
|
||||||
class ProtoSize;
|
class ProtoSize;
|
||||||
|
|
||||||
class ProtoLengthDelimited {
|
/// Case label for decode_field(): the wire tag of a field, so a field that arrives with another wire
|
||||||
|
/// type matches no case.
|
||||||
|
constexpr uint32_t proto_tag(uint32_t field_id, uint32_t wire_type) { return (field_id << 3) | wire_type; }
|
||||||
|
|
||||||
|
/// One decoded field: the payload pointer and a scalar holding the varint or fixed32 value, or the
|
||||||
|
/// length of a length-delimited field. The wire type in the tag says which applies; accessors do not check.
|
||||||
|
class ProtoFieldValue {
|
||||||
public:
|
public:
|
||||||
explicit ProtoLengthDelimited(const uint8_t *value, size_t length) : value_(value), length_(length) {}
|
ProtoFieldValue(const uint8_t *data, proto_varint_value_t scalar) : data_(data), scalar_(scalar) {}
|
||||||
std::string as_string() const { return std::string(reinterpret_cast<const char *>(this->value_), this->length_); }
|
|
||||||
|
|
||||||
// Direct access to raw data without string allocation
|
proto_varint_value_t as_varint() const { return this->scalar_; }
|
||||||
const uint8_t *data() const { return this->value_; }
|
// A bool is sent as 0 or 1, so the low word is enough and saves a second compare with 64 bit varints
|
||||||
size_t size() const { return this->length_; }
|
bool as_bool() const { return static_cast<uint32_t>(this->scalar_) != 0; }
|
||||||
|
|
||||||
/// Decode the length-delimited data into a message instance.
|
// Length-delimited accessors
|
||||||
|
const uint8_t *data() const { return this->data_; }
|
||||||
|
size_t size() const { return static_cast<size_t>(this->scalar_); }
|
||||||
|
std::string as_string() const { return std::string(reinterpret_cast<const char *>(this->data_), this->size()); }
|
||||||
|
/// Decode the length-delimited payload into a message instance.
|
||||||
/// Template preserves concrete type so decode() resolves statically.
|
/// Template preserves concrete type so decode() resolves statically.
|
||||||
template<typename T> void decode_to_message(T &msg) const;
|
template<typename T> void decode_to_message(T &msg) const { msg.decode(this->data_, this->size()); }
|
||||||
|
|
||||||
protected:
|
// Fixed32 accessors
|
||||||
const uint8_t *const value_;
|
uint32_t as_fixed32() const { return static_cast<uint32_t>(this->scalar_); }
|
||||||
const size_t length_;
|
int32_t as_sfixed32() const { return static_cast<int32_t>(this->as_fixed32()); }
|
||||||
};
|
|
||||||
|
|
||||||
class Proto32Bit {
|
|
||||||
public:
|
|
||||||
explicit Proto32Bit(uint32_t value) : value_(value) {}
|
|
||||||
uint32_t as_fixed32() const { return this->value_; }
|
|
||||||
int32_t as_sfixed32() const { return static_cast<int32_t>(this->value_); }
|
|
||||||
float as_float() const {
|
float as_float() const {
|
||||||
union {
|
union {
|
||||||
uint32_t raw;
|
uint32_t raw;
|
||||||
float value;
|
float value;
|
||||||
} s{};
|
} s{};
|
||||||
s.raw = this->value_;
|
s.raw = this->as_fixed32();
|
||||||
return s.value;
|
return s.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected:
|
private:
|
||||||
const uint32_t value_;
|
const uint8_t *data_;
|
||||||
|
proto_varint_value_t scalar_;
|
||||||
};
|
};
|
||||||
|
|
||||||
// NOTE: Proto64Bit class removed - wire type 1 (64-bit fixed) not supported
|
// NOTE: Proto64Bit class removed - wire type 1 (64-bit fixed) not supported
|
||||||
@@ -252,7 +256,7 @@ class ProtoWriteBuffer {
|
|||||||
*
|
*
|
||||||
* Following https://protobuf.dev/programming-guides/encoding/#structure
|
* Following https://protobuf.dev/programming-guides/encoding/#structure
|
||||||
*/
|
*/
|
||||||
void encode_field_raw(uint32_t field_id, uint32_t type) { this->encode_varint_raw((field_id << 3) | type); }
|
void encode_field_raw(uint32_t field_id, uint32_t type) { this->encode_varint_raw(proto_tag(field_id, type)); }
|
||||||
/// Single-pass encode for repeated submessage elements.
|
/// Single-pass encode for repeated submessage elements.
|
||||||
/// Thin template wrapper; all buffer work is in the non-template core.
|
/// Thin template wrapper; all buffer work is in the non-template core.
|
||||||
template<typename T> void encode_sub_message(uint32_t field_id, const T &value);
|
template<typename T> void encode_sub_message(uint32_t field_id, const T &value);
|
||||||
@@ -382,7 +386,7 @@ class ProtoEncode {
|
|||||||
}
|
}
|
||||||
[[nodiscard]] static inline uint8_t *ESPHOME_ALWAYS_INLINE
|
[[nodiscard]] static inline uint8_t *ESPHOME_ALWAYS_INLINE
|
||||||
encode_field_raw(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, uint32_t type) {
|
encode_field_raw(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, uint32_t type) {
|
||||||
return encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, (field_id << 3) | type);
|
return encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, proto_tag(field_id, type));
|
||||||
}
|
}
|
||||||
/// Write a single precomputed tag byte. Tag must be < 128.
|
/// Write a single precomputed tag byte. Tag must be < 128.
|
||||||
[[nodiscard]] static inline uint8_t *ESPHOME_ALWAYS_INLINE
|
[[nodiscard]] static inline uint8_t *ESPHOME_ALWAYS_INLINE
|
||||||
@@ -692,11 +696,12 @@ class DumpBuffer {
|
|||||||
|
|
||||||
class ProtoMessage {
|
class ProtoMessage {
|
||||||
public:
|
public:
|
||||||
// Non-virtual defaults for messages with no fields.
|
// Non-virtual defaults for messages with no fields; generated classes hide all four. The
|
||||||
// Concrete message classes hide these with their own implementations.
|
// static encode_msg/calc_size_msg take const void * so &T::encode_msg needs no thunk.
|
||||||
// All call sites use templates to preserve the concrete type, so virtual
|
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) {
|
||||||
// dispatch is not needed. This eliminates per-message vtable entries for
|
return buffer.get_pos();
|
||||||
// encode/calculate_size, saving ~1.3 KB of flash across all message types.
|
}
|
||||||
|
static uint32_t calc_size_msg(const void *self) { return 0; }
|
||||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { return buffer.get_pos(); }
|
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { return buffer.get_pos(); }
|
||||||
uint32_t calculate_size() const { return 0; }
|
uint32_t calculate_size() const { return 0; }
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
@@ -716,7 +721,13 @@ class ProtoMessage {
|
|||||||
// Base class for messages that support decoding
|
// Base class for messages that support decoding
|
||||||
class ProtoDecodableMessage : public ProtoMessage {
|
class ProtoDecodableMessage : public ProtoMessage {
|
||||||
public:
|
public:
|
||||||
void decode(const uint8_t *buffer, size_t length);
|
/// Stores one decoded field into \p msg; generated per message type. \p scalar is the varint or
|
||||||
|
/// fixed32 value, or the length of the length-delimited payload at \p data. An unknown field or
|
||||||
|
/// wrong wire type matches no case and is skipped.
|
||||||
|
using DecodeFieldFn = void (*)(void *msg, uint32_t tag, const uint8_t *data, proto_varint_value_t scalar);
|
||||||
|
/// Walk \p buffer and hand every field to \p field. The generated decode() passes the message's
|
||||||
|
/// own decode_field, so decodable messages carry no vtable.
|
||||||
|
static void decode_fields(void *msg, const uint8_t *buffer, size_t length, DecodeFieldFn field);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Count occurrences of a repeated field in a protobuf buffer.
|
* Count occurrences of a repeated field in a protobuf buffer.
|
||||||
@@ -728,14 +739,15 @@ class ProtoDecodableMessage : public ProtoMessage {
|
|||||||
* @return Number of times the field appears in the buffer
|
* @return Number of times the field appears in the buffer
|
||||||
*/
|
*/
|
||||||
static uint32_t count_repeated_field(const uint8_t *buffer, size_t length, uint32_t target_field_id);
|
static uint32_t count_repeated_field(const uint8_t *buffer, size_t length, uint32_t target_field_id);
|
||||||
|
// The destructor stays accessible on purpose: the generated messages are aggregates that brace
|
||||||
protected:
|
// initialise sub message members, which copies a base temporary. That trades away the compile time
|
||||||
~ProtoDecodableMessage() = default;
|
// guard against deleting through this type; messages are stack locals and never owned through a base
|
||||||
virtual bool decode_varint(uint32_t field_id, proto_varint_value_t value) { return false; }
|
// pointer. ProtoMessage keeps its guard for the dump builds.
|
||||||
virtual bool decode_length(uint32_t field_id, ProtoLengthDelimited value) { return false; }
|
|
||||||
virtual bool decode_32bit(uint32_t field_id, Proto32Bit value) { return false; }
|
|
||||||
// NOTE: decode_64bit removed - wire type 1 not supported
|
|
||||||
};
|
};
|
||||||
|
#ifndef HAS_PROTO_MESSAGE_DUMP
|
||||||
|
// decode() passes decode_field explicitly, so nothing here may add a vtable
|
||||||
|
static_assert(!std::is_polymorphic_v<ProtoDecodableMessage>, "decodable messages carry no vtable");
|
||||||
|
#endif
|
||||||
|
|
||||||
class ProtoSize {
|
class ProtoSize {
|
||||||
public:
|
public:
|
||||||
@@ -860,7 +872,7 @@ class ProtoSize {
|
|||||||
* @return The number of bytes needed to encode the field ID and wire type
|
* @return The number of bytes needed to encode the field ID and wire type
|
||||||
*/
|
*/
|
||||||
static constexpr uint32_t field(uint32_t field_id, uint32_t type) {
|
static constexpr uint32_t field(uint32_t field_id, uint32_t type) {
|
||||||
uint32_t tag = (field_id << 3) | (type & WIRE_TYPE_MASK);
|
uint32_t tag = proto_tag(field_id, type & WIRE_TYPE_MASK);
|
||||||
return varint(tag);
|
return varint(tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -944,24 +956,14 @@ class ProtoSize {
|
|||||||
|
|
||||||
// Implementation of methods that depend on ProtoSize being fully defined
|
// Implementation of methods that depend on ProtoSize being fully defined
|
||||||
|
|
||||||
// Encode thunk — converts void* back to concrete type for direct encode() call
|
|
||||||
template<typename T> uint8_t *proto_encode_msg(const void *msg, ProtoWriteBuffer &buf PROTO_ENCODE_DEBUG_PARAM) {
|
|
||||||
return static_cast<const T *>(msg)->encode(buf PROTO_ENCODE_DEBUG_ARG);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Thin template wrapper; delegates to non-template core in proto.cpp.
|
// Thin template wrapper; delegates to non-template core in proto.cpp.
|
||||||
template<typename T> inline void ProtoWriteBuffer::encode_sub_message(uint32_t field_id, const T &value) {
|
template<typename T> inline void ProtoWriteBuffer::encode_sub_message(uint32_t field_id, const T &value) {
|
||||||
this->encode_sub_message(field_id, &value, &proto_encode_msg<T>);
|
this->encode_sub_message(field_id, &value, &T::encode_msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Thin template wrapper; delegates to non-template core.
|
// Thin template wrapper; delegates to non-template core.
|
||||||
template<typename T> inline void ProtoWriteBuffer::encode_optional_sub_message(uint32_t field_id, const T &value) {
|
template<typename T> inline void ProtoWriteBuffer::encode_optional_sub_message(uint32_t field_id, const T &value) {
|
||||||
this->encode_optional_sub_message(field_id, value.calculate_size(), &value, &proto_encode_msg<T>);
|
this->encode_optional_sub_message(field_id, T::calc_size_msg(&value), &value, &T::encode_msg);
|
||||||
}
|
|
||||||
|
|
||||||
// Template decode_to_message - preserves concrete type so decode() resolves statically
|
|
||||||
template<typename T> void ProtoLengthDelimited::decode_to_message(T &msg) const {
|
|
||||||
msg.decode(this->value_, this->length_);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
template<typename T> const char *proto_enum_to_string(T value);
|
template<typename T> const char *proto_enum_to_string(T value);
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ namespace esphome {
|
|||||||
* pointer. When it is default constructed, it has empty string. You can freely copy or move around this struct, but
|
* pointer. When it is default constructed, it has empty string. You can freely copy or move around this struct, but
|
||||||
* never free its pointer. str() function can be used to export the content as std::string. StringRef is adopted from
|
* never free its pointer. str() function can be used to export the content as std::string. StringRef is adopted from
|
||||||
* <https://github.com/nghttp2/nghttp2/blob/29cbf8b83ff78faf405d1086b16adc09a8772eca/src/template.h#L376>
|
* <https://github.com/nghttp2/nghttp2/blob/29cbf8b83ff78faf405d1086b16adc09a8772eca/src/template.h#L376>
|
||||||
|
*
|
||||||
|
* A StringRef may carry a null pointer while its length is zero (the generated api messages start their encode only
|
||||||
|
* string fields that way). Every member treats that as the empty string; only c_str() hands the null pointer on, so
|
||||||
|
* callers that print or copy through c_str() must check empty() first.
|
||||||
*/
|
*/
|
||||||
class StringRef {
|
class StringRef {
|
||||||
public:
|
public:
|
||||||
@@ -78,7 +82,7 @@ class StringRef {
|
|||||||
|
|
||||||
/// True if the view begins with the given prefix (std::string::starts_with-like)
|
/// True if the view begins with the given prefix (std::string::starts_with-like)
|
||||||
bool starts_with(const StringRef &prefix) const {
|
bool starts_with(const StringRef &prefix) const {
|
||||||
return len_ >= prefix.len_ && std::memcmp(base_, prefix.base_, prefix.len_) == 0;
|
return len_ >= prefix.len_ && (prefix.len_ == 0 || std::memcmp(base_, prefix.base_, prefix.len_) == 0);
|
||||||
}
|
}
|
||||||
bool starts_with(const char *prefix) const { return this->starts_with(StringRef(prefix)); }
|
bool starts_with(const char *prefix) const { return this->starts_with(StringRef(prefix)); }
|
||||||
bool starts_with(const std::string &prefix) const { return this->starts_with(StringRef(prefix)); }
|
bool starts_with(const std::string &prefix) const { return this->starts_with(StringRef(prefix)); }
|
||||||
@@ -92,14 +96,15 @@ class StringRef {
|
|||||||
return actual;
|
return actual;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string str() const { return std::string(base_, len_); }
|
std::string str() const { return std::string(base_, len_); } // fine for {nullptr, 0}: nothing is read
|
||||||
const uint8_t *byte() const { return reinterpret_cast<const uint8_t *>(base_); }
|
const uint8_t *byte() const { return reinterpret_cast<const uint8_t *>(base_); }
|
||||||
|
|
||||||
operator std::string() const { return str(); }
|
operator std::string() const { return str(); }
|
||||||
|
|
||||||
/// Compare (compatible with std::string::compare)
|
/// Compare (compatible with std::string::compare)
|
||||||
int compare(const StringRef &other) const {
|
int compare(const StringRef &other) const {
|
||||||
int result = std::memcmp(base_, other.base_, std::min(len_, other.len_));
|
size_type common = std::min(len_, other.len_);
|
||||||
|
int result = common == 0 ? 0 : std::memcmp(base_, other.base_, common);
|
||||||
if (result != 0)
|
if (result != 0)
|
||||||
return result;
|
return result;
|
||||||
if (len_ < other.len_)
|
if (len_ < other.len_)
|
||||||
@@ -258,7 +263,14 @@ inline double stod(const StringRef &str, size_t *pos = nullptr) {
|
|||||||
|
|
||||||
#ifdef USE_JSON
|
#ifdef USE_JSON
|
||||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||||
inline void convertToJson(const StringRef &src, JsonVariant dst) { dst.set(src.c_str()); }
|
inline void convertToJson(const StringRef &src, JsonVariant dst) {
|
||||||
|
// Bounded by the view length; a null, empty view becomes "" rather than JSON null
|
||||||
|
if (src.empty()) {
|
||||||
|
dst.set("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dst.set(JsonString(src.c_str(), src.size()));
|
||||||
|
}
|
||||||
#endif // USE_JSON
|
#endif // USE_JSON
|
||||||
|
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
|||||||
+193
-268
@@ -28,6 +28,11 @@ class WireType(IntEnum):
|
|||||||
END_GROUP = 4 # groups (deprecated)
|
END_GROUP = 4 # groups (deprecated)
|
||||||
FIXED32 = 5 # fixed32, sfixed32, float
|
FIXED32 = 5 # fixed32, sfixed32, float
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cpp_name(self) -> str:
|
||||||
|
"""The matching constant in proto.h."""
|
||||||
|
return f"WIRE_TYPE_{self.name}"
|
||||||
|
|
||||||
|
|
||||||
# Generate with
|
# Generate with
|
||||||
# protoc --python_out=script/api_protobuf -I esphome/components/api/ api_options.proto
|
# protoc --python_out=script/api_protobuf -I esphome/components/api/ api_options.proto
|
||||||
@@ -126,11 +131,6 @@ def camel_to_snake(name: str) -> str:
|
|||||||
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
|
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
|
||||||
|
|
||||||
|
|
||||||
def force_str(force: bool) -> str:
|
|
||||||
"""Convert a boolean force value to string format for C++ code."""
|
|
||||||
return str(force).lower()
|
|
||||||
|
|
||||||
|
|
||||||
def _encode_call(func: str, *args: str, force: bool = False) -> str:
|
def _encode_call(func: str, *args: str, force: bool = False) -> str:
|
||||||
"""Emit one ProtoEncode call; every helper takes the cursor and returns it advanced."""
|
"""Emit one ProtoEncode call; every helper takes the cursor and returns it advanced."""
|
||||||
suffix = "_force" if force else ""
|
suffix = "_force" if force else ""
|
||||||
@@ -229,41 +229,23 @@ class TypeInfo(ABC):
|
|||||||
def class_member(self) -> str:
|
def class_member(self) -> str:
|
||||||
return f"{self.cpp_type} {self.field_name}{{{self.default_value}}};"
|
return f"{self.cpp_type} {self.field_name}{{{self.default_value}}};"
|
||||||
|
|
||||||
@property
|
def decode_case(self, body: str) -> str:
|
||||||
def decode_varint_content(self) -> str:
|
"""Emit one decode_field() case, keyed on the field's wire tag."""
|
||||||
content = self.decode_varint
|
return f"case proto_tag({self.number}, {self.wire_type.cpp_name}):\n" + indent(
|
||||||
if content is None:
|
f"{body}\nbreak;"
|
||||||
return None
|
)
|
||||||
return f"case {self.number}: this->{self.field_name} = {content}; break;"
|
|
||||||
|
|
||||||
decode_varint = None
|
# Expression that reads this field from `value`; None when the type is never decoded.
|
||||||
|
decode_expr: str | None = None
|
||||||
|
|
||||||
|
def _decode_store(self, expr: str) -> str:
|
||||||
|
return f"this->{self.field_name} = {expr};"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def decode_length_content(self) -> str:
|
def decode_content(self) -> str | None:
|
||||||
content = self.decode_length
|
"""The decode_field() case for this field, or None when it is never decoded."""
|
||||||
if content is None:
|
expr = self.decode_expr
|
||||||
return None
|
return None if expr is None else self.decode_case(self._decode_store(expr))
|
||||||
return f"case {self.number}: this->{self.field_name} = {content}; break;"
|
|
||||||
|
|
||||||
decode_length = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def decode_32bit_content(self) -> str:
|
|
||||||
content = self.decode_32bit
|
|
||||||
if content is None:
|
|
||||||
return None
|
|
||||||
return f"case {self.number}: this->{self.field_name} = {content}; break;"
|
|
||||||
|
|
||||||
decode_32bit = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def decode_64bit_content(self) -> str:
|
|
||||||
content = self.decode_64bit
|
|
||||||
if content is None:
|
|
||||||
return None
|
|
||||||
return f"case {self.number}: this->{self.field_name} = {content}; break;"
|
|
||||||
|
|
||||||
decode_64bit = None
|
|
||||||
|
|
||||||
# Mapping from encode_func to raw encode expression template.
|
# Mapping from encode_func to raw encode expression template.
|
||||||
# When a forced field has a single-byte tag, the code generator emits
|
# When a forced field has a single-byte tag, the code generator emits
|
||||||
@@ -339,11 +321,12 @@ class TypeInfo(ABC):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def _encode_fixed32_with_precomputed_tag(self, value_expr: str) -> str | None:
|
def _encode_fixed32_with_precomputed_tag(self, value: str) -> str | None:
|
||||||
"""Single-byte tag fixed32 write, or None for multi-byte tags."""
|
"""Single-byte tag fixed32 write, or None for other types and multi-byte tags."""
|
||||||
tag = self.calculate_tag()
|
tag = self.calculate_tag()
|
||||||
if tag >= 128:
|
if self.fixed32_value_template is None or tag >= 128:
|
||||||
return None
|
return None
|
||||||
|
value_expr = self.fixed32_value_template.format(value=value)
|
||||||
if self.force:
|
if self.force:
|
||||||
return _encode_call("write_tag_and_fixed32", str(tag), value_expr)
|
return _encode_call("write_tag_and_fixed32", str(tag), value_expr)
|
||||||
return (
|
return (
|
||||||
@@ -357,14 +340,14 @@ class TypeInfo(ABC):
|
|||||||
value = f"this->{self.field_name}"
|
value = f"this->{self.field_name}"
|
||||||
if result := self._encode_with_precomputed_tag(value):
|
if result := self._encode_with_precomputed_tag(value):
|
||||||
return result
|
return result
|
||||||
if self.fixed32_value_template is not None and (
|
if result := self._encode_fixed32_with_precomputed_tag(value):
|
||||||
result := self._encode_fixed32_with_precomputed_tag(
|
|
||||||
self.fixed32_value_template.format(value=value)
|
|
||||||
)
|
|
||||||
):
|
|
||||||
return result
|
return result
|
||||||
return _encode_call(self.encode_func, str(self.number), value, force=self.force)
|
return _encode_call(self.encode_func, str(self.number), value, force=self.force)
|
||||||
|
|
||||||
|
def encode_element(self, number: int, element: str) -> str:
|
||||||
|
"""Encode one element of a repeated field; elements are always written."""
|
||||||
|
return _encode_call(self.encode_func, str(number), element, force=True)
|
||||||
|
|
||||||
encode_func = None
|
encode_func = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -583,17 +566,17 @@ def create_field_type_info(
|
|||||||
# For messages that decode (SOURCE_CLIENT or SOURCE_BOTH), use pointer
|
# For messages that decode (SOURCE_CLIENT or SOURCE_BOTH), use pointer
|
||||||
# for zero-copy access to the receive buffer
|
# for zero-copy access to the receive buffer
|
||||||
if needs_decode:
|
if needs_decode:
|
||||||
return PointerToBytesBufferType(field, None)
|
return PointerToBytesBufferType(field, needs_decode)
|
||||||
|
|
||||||
# For SOURCE_SERVER (encode only), explicit annotation is still needed
|
# For SOURCE_SERVER (encode only), explicit annotation is still needed
|
||||||
if get_field_opt(field, pb.pointer_to_buffer, False):
|
if get_field_opt(field, pb.pointer_to_buffer, False):
|
||||||
return PointerToBytesBufferType(field, None)
|
return PointerToBytesBufferType(field, needs_decode)
|
||||||
|
|
||||||
return BytesType(field, needs_decode, needs_encode)
|
return BytesType(field, needs_decode, needs_encode)
|
||||||
|
|
||||||
# Special handling for string fields - use StringRef for zero-copy
|
# Special handling for string fields - use StringRef for zero-copy
|
||||||
if field.type == 9:
|
if field.type == 9:
|
||||||
return PointerToStringBufferType(field, None)
|
return PointerToStringBufferType(field, needs_decode)
|
||||||
|
|
||||||
validate_field_type(field.type, field.name)
|
validate_field_type(field.type, field.name)
|
||||||
if field.type == 11:
|
if field.type == 11:
|
||||||
@@ -638,7 +621,6 @@ class DoubleType(FixedSizeTypeMixin, TypeInfo):
|
|||||||
# Unsupported but defined for completeness
|
# Unsupported but defined for completeness
|
||||||
cpp_type = "double"
|
cpp_type = "double"
|
||||||
default_value = "0.0"
|
default_value = "0.0"
|
||||||
decode_64bit = "value.as_double()"
|
|
||||||
encode_func = "encode_double"
|
encode_func = "encode_double"
|
||||||
wire_type = WireType.FIXED64 # Uses wire type 1 according to protobuf spec
|
wire_type = WireType.FIXED64 # Uses wire type 1 according to protobuf spec
|
||||||
|
|
||||||
@@ -664,7 +646,7 @@ class DoubleType(FixedSizeTypeMixin, TypeInfo):
|
|||||||
class FloatType(FixedSizeTypeMixin, TypeInfo):
|
class FloatType(FixedSizeTypeMixin, TypeInfo):
|
||||||
cpp_type = "float"
|
cpp_type = "float"
|
||||||
default_value = "0.0f"
|
default_value = "0.0f"
|
||||||
decode_32bit = "value.as_float()"
|
decode_expr = "value.as_float()"
|
||||||
encode_func = "encode_float"
|
encode_func = "encode_float"
|
||||||
wire_type = WireType.FIXED32 # Uses wire type 5
|
wire_type = WireType.FIXED32 # Uses wire type 5
|
||||||
|
|
||||||
@@ -693,7 +675,7 @@ class Int64Type(VarintTypeMixin, TypeInfo):
|
|||||||
cpp_type = "int64_t"
|
cpp_type = "int64_t"
|
||||||
_varint_max_bits = 64
|
_varint_max_bits = 64
|
||||||
default_value = "0"
|
default_value = "0"
|
||||||
decode_varint = "static_cast<int64_t>(value)"
|
decode_expr = "static_cast<int64_t>(value.as_varint())"
|
||||||
encode_func = "encode_int64"
|
encode_func = "encode_int64"
|
||||||
wire_type = WireType.VARINT # Uses wire type 0
|
wire_type = WireType.VARINT # Uses wire type 0
|
||||||
|
|
||||||
@@ -714,7 +696,7 @@ class UInt64Type(VarintTypeMixin, TypeInfo):
|
|||||||
cpp_type = "uint64_t"
|
cpp_type = "uint64_t"
|
||||||
_varint_max_bits = 64
|
_varint_max_bits = 64
|
||||||
default_value = "0"
|
default_value = "0"
|
||||||
decode_varint = "value"
|
decode_expr = "value.as_varint()"
|
||||||
encode_func = "encode_uint64"
|
encode_func = "encode_uint64"
|
||||||
wire_type = WireType.VARINT # Uses wire type 0
|
wire_type = WireType.VARINT # Uses wire type 0
|
||||||
|
|
||||||
@@ -749,7 +731,7 @@ class Int32Type(VarintTypeMixin, TypeInfo):
|
|||||||
cpp_type = "int32_t"
|
cpp_type = "int32_t"
|
||||||
_varint_max_bits = 64 # int32 is sign-extended to 64 bits in protobuf
|
_varint_max_bits = 64 # int32 is sign-extended to 64 bits in protobuf
|
||||||
default_value = "0"
|
default_value = "0"
|
||||||
decode_varint = "static_cast<int32_t>(value)"
|
decode_expr = "static_cast<int32_t>(value.as_varint())"
|
||||||
encode_func = "encode_int32"
|
encode_func = "encode_int32"
|
||||||
wire_type = WireType.VARINT # Uses wire type 0
|
wire_type = WireType.VARINT # Uses wire type 0
|
||||||
|
|
||||||
@@ -769,7 +751,6 @@ class Int32Type(VarintTypeMixin, TypeInfo):
|
|||||||
class Fixed64Type(FixedSizeTypeMixin, TypeInfo):
|
class Fixed64Type(FixedSizeTypeMixin, TypeInfo):
|
||||||
cpp_type = "uint64_t"
|
cpp_type = "uint64_t"
|
||||||
default_value = "0"
|
default_value = "0"
|
||||||
decode_64bit = "value.as_fixed64()"
|
|
||||||
encode_func = "encode_fixed64"
|
encode_func = "encode_fixed64"
|
||||||
wire_type = WireType.FIXED64 # Uses wire type 1
|
wire_type = WireType.FIXED64 # Uses wire type 1
|
||||||
|
|
||||||
@@ -795,7 +776,7 @@ class Fixed64Type(FixedSizeTypeMixin, TypeInfo):
|
|||||||
class Fixed32Type(FixedSizeTypeMixin, TypeInfo):
|
class Fixed32Type(FixedSizeTypeMixin, TypeInfo):
|
||||||
cpp_type = "uint32_t"
|
cpp_type = "uint32_t"
|
||||||
default_value = "0"
|
default_value = "0"
|
||||||
decode_32bit = "value.as_fixed32()"
|
decode_expr = "value.as_fixed32()"
|
||||||
encode_func = "encode_fixed32"
|
encode_func = "encode_fixed32"
|
||||||
wire_type = WireType.FIXED32 # Uses wire type 5
|
wire_type = WireType.FIXED32 # Uses wire type 5
|
||||||
|
|
||||||
@@ -824,7 +805,7 @@ class BoolType(VarintTypeMixin, TypeInfo):
|
|||||||
_varint_max_bits = 1
|
_varint_max_bits = 1
|
||||||
cpp_type = "bool"
|
cpp_type = "bool"
|
||||||
default_value = "false"
|
default_value = "false"
|
||||||
decode_varint = "value != 0"
|
decode_expr = "value.as_bool()"
|
||||||
encode_func = "encode_bool"
|
encode_func = "encode_bool"
|
||||||
wire_type = WireType.VARINT # Uses wire type 0
|
wire_type = WireType.VARINT # Uses wire type 0
|
||||||
|
|
||||||
@@ -844,7 +825,7 @@ class StringType(TypeInfo):
|
|||||||
default_value = ""
|
default_value = ""
|
||||||
reference_type = "std::string &"
|
reference_type = "std::string &"
|
||||||
const_reference_type = "const std::string &"
|
const_reference_type = "const std::string &"
|
||||||
decode_length = "value.as_string()"
|
decode_expr = "value.as_string()"
|
||||||
encode_func = "encode_string"
|
encode_func = "encode_string"
|
||||||
wire_type = WireType.LENGTH_DELIMITED # Uses wire type 2
|
wire_type = WireType.LENGTH_DELIMITED # Uses wire type 2
|
||||||
|
|
||||||
@@ -959,6 +940,9 @@ class MessageType(TypeInfo):
|
|||||||
def can_use_dump_field(cls) -> bool:
|
def can_use_dump_field(cls) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def encode_element(self, number: int, element: str) -> str:
|
||||||
|
return _encode_call("encode_sub_message", "buffer", str(number), element)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cpp_type(self) -> str:
|
def cpp_type(self) -> str:
|
||||||
return self._field.type_name[1:]
|
return self._field.type_name[1:]
|
||||||
@@ -985,14 +969,6 @@ class MessageType(TypeInfo):
|
|||||||
self.encode_func, "buffer", str(self.number), f"this->{self.field_name}"
|
self.encode_func, "buffer", str(self.number), f"this->{self.field_name}"
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
|
||||||
def decode_length(self) -> str:
|
|
||||||
# Override to return None for message types because we can't use template-based
|
|
||||||
# decoding when the specific message type isn't known at compile time.
|
|
||||||
# Instead, we use the non-template decode_to_message() method which allows
|
|
||||||
# runtime polymorphism through virtual function calls.
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def public_content(self) -> list[str]:
|
def public_content(self) -> list[str]:
|
||||||
content = [self.class_member]
|
content = [self.class_member]
|
||||||
@@ -1008,19 +984,14 @@ class MessageType(TypeInfo):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def decode_length_content(self) -> str:
|
def decode_content(self) -> str:
|
||||||
# Custom decode that doesn't use templates
|
body = f"value.decode_to_message(this->{self.field_name});"
|
||||||
if self._track_presence:
|
if self._track_presence:
|
||||||
# decode_to_message() cannot report failure, so setting the flag
|
# decode_to_message() cannot report failure, so setting the flag
|
||||||
# afterwards only documents intent; a status-returning decode could
|
# afterwards only documents intent; a status-returning decode could
|
||||||
# gate it for real without touching callers.
|
# gate it for real without touching callers.
|
||||||
return (
|
body += f"\nthis->has_{self.name} = true;"
|
||||||
f"case {self.number}:\n"
|
return self.decode_case(body)
|
||||||
f" value.decode_to_message(this->{self.field_name});\n"
|
|
||||||
f" this->has_{self.name} = true;\n"
|
|
||||||
f" break;"
|
|
||||||
)
|
|
||||||
return f"case {self.number}: value.decode_to_message(this->{self.field_name}); break;"
|
|
||||||
|
|
||||||
def dump(self, name: str) -> str:
|
def dump(self, name: str) -> str:
|
||||||
return f"{name}.dump_to(out);"
|
return f"{name}.dump_to(out);"
|
||||||
@@ -1059,7 +1030,7 @@ class BytesType(TypeInfo):
|
|||||||
reference_type = "std::string &"
|
reference_type = "std::string &"
|
||||||
const_reference_type = "const std::string &"
|
const_reference_type = "const std::string &"
|
||||||
encode_func = "encode_bytes"
|
encode_func = "encode_bytes"
|
||||||
decode_length = "value.as_string()"
|
decode_expr = "value.as_string()"
|
||||||
wire_type = WireType.LENGTH_DELIMITED # Uses wire type 2
|
wire_type = WireType.LENGTH_DELIMITED # Uses wire type 2
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -1163,16 +1134,12 @@ class PointerToBufferTypeBase(TypeInfo):
|
|||||||
def can_use_dump_field(cls) -> bool:
|
def can_use_dump_field(cls) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Only here to make needs_decode required: the null string default keys off it, so a call
|
||||||
|
# site must not fall back on the base class default
|
||||||
def __init__(
|
def __init__(
|
||||||
self, field: descriptor.FieldDescriptorProto, size: int | None = None
|
self, field: descriptor.FieldDescriptorProto, needs_decode: bool
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(field)
|
super().__init__(field, needs_decode)
|
||||||
self.array_size = 0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def decode_length(self) -> str | None:
|
|
||||||
# This is handled in decode_length_content
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def wire_type(self) -> WireType:
|
def wire_type(self) -> WireType:
|
||||||
@@ -1215,12 +1182,11 @@ class PointerToBytesBufferType(PointerToBufferTypeBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def decode_length_content(self) -> str | None:
|
def decode_content(self) -> str:
|
||||||
return f"""case {self.number}: {{
|
return self.decode_case(
|
||||||
this->{self.field_name} = value.data();
|
f"this->{self.field_name} = value.data();\n"
|
||||||
this->{self.field_name}_len = value.size();
|
f"this->{self.field_name}_len = value.size();",
|
||||||
break;
|
)
|
||||||
}}"""
|
|
||||||
|
|
||||||
def dump(self, name: str) -> str:
|
def dump(self, name: str) -> str:
|
||||||
return (
|
return (
|
||||||
@@ -1254,14 +1220,28 @@ class PointerToStringBufferType(PointerToBufferTypeBase):
|
|||||||
def can_use_dump_field(cls) -> bool:
|
def can_use_dump_field(cls) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _starts_null(self) -> bool:
|
||||||
|
"""A field that is only encoded, and skipped when empty, never has its pointer read
|
||||||
|
before it is set, so it can default to a null StringRef and the message constructs as
|
||||||
|
one zero fill. Any encode path that copies unconditionally must check this."""
|
||||||
|
return not self._needs_decode and not self.force
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def public_content(self) -> list[str]:
|
def public_content(self) -> list[str]:
|
||||||
|
if self._starts_null:
|
||||||
|
return [
|
||||||
|
f"StringRef {self.field_name}{{nullptr, 0}}; // null until set, encode only"
|
||||||
|
]
|
||||||
return [f"StringRef {self.field_name}{{}};"]
|
return [f"StringRef {self.field_name}{{}};"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def encode_content(self) -> str:
|
def encode_content(self) -> str:
|
||||||
max_len = self.max_data_length
|
max_len = self.max_data_length
|
||||||
if max_len is not None and max_len < 128 and self.force:
|
if max_len is not None and max_len < 128 and self.force:
|
||||||
|
assert not self._starts_null, (
|
||||||
|
"unconditional copy of a field that may start null"
|
||||||
|
)
|
||||||
tag = self.calculate_tag()
|
tag = self.calculate_tag()
|
||||||
if tag < 128:
|
if tag < 128:
|
||||||
return _encode_call(
|
return _encode_call(
|
||||||
@@ -1271,6 +1251,9 @@ class PointerToStringBufferType(PointerToBufferTypeBase):
|
|||||||
f"this->{self.field_name}.c_str()",
|
f"this->{self.field_name}.c_str()",
|
||||||
f"this->{self.field_name}.size()",
|
f"this->{self.field_name}.size()",
|
||||||
):
|
):
|
||||||
|
assert not self._starts_null, (
|
||||||
|
"unconditional copy of a field that may start null"
|
||||||
|
)
|
||||||
return result
|
return result
|
||||||
return _encode_call(
|
return _encode_call(
|
||||||
"encode_string",
|
"encode_string",
|
||||||
@@ -1280,11 +1263,10 @@ class PointerToStringBufferType(PointerToBufferTypeBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def decode_length_content(self) -> str | None:
|
def decode_content(self) -> str:
|
||||||
return f"""case {self.number}: {{
|
return self.decode_case(
|
||||||
this->{self.field_name} = StringRef(reinterpret_cast<const char *>(value.data()), value.size());
|
f"this->{self.field_name} = StringRef(value.data(), value.size());",
|
||||||
break;
|
)
|
||||||
}}"""
|
|
||||||
|
|
||||||
def dump(self, name: str) -> str:
|
def dump(self, name: str) -> str:
|
||||||
# Not used since we use dump_field, but required by abstract base class
|
# Not used since we use dump_field, but required by abstract base class
|
||||||
@@ -1353,14 +1335,13 @@ class PackedBufferTypeInfo(TypeInfo):
|
|||||||
]
|
]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def decode_length_content(self) -> str:
|
def decode_content(self) -> str:
|
||||||
"""Store pointer to buffer and calculate count of packed varints."""
|
"""Store pointer to buffer and calculate count of packed varints."""
|
||||||
return f"""case {self.number}: {{
|
return self.decode_case(
|
||||||
this->{self.field_name}_data_ = value.data();
|
f"this->{self.field_name}_data_ = value.data();\n"
|
||||||
this->{self.field_name}_length_ = value.size();
|
f"this->{self.field_name}_length_ = value.size();\n"
|
||||||
this->{self.field_name}_count_ = count_packed_varints(value.data(), value.size());
|
f"this->{self.field_name}_count_ = count_packed_varints(value.data(), value.size());",
|
||||||
break;
|
)
|
||||||
}}"""
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def encode_content(self) -> str:
|
def encode_content(self) -> str:
|
||||||
@@ -1445,17 +1426,11 @@ class FixedArrayBytesType(TypeInfo):
|
|||||||
]
|
]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def decode_length_content(self) -> str:
|
def decode_content(self) -> str:
|
||||||
o = f"case {self.number}: {{\n"
|
return self.decode_case(
|
||||||
o += " const std::string &data_str = value.as_string();\n"
|
f"this->{self.field_name}_len = std::min<size_t>(value.size(), {self.array_size});\n"
|
||||||
o += f" this->{self.field_name}_len = data_str.size();\n"
|
f"memcpy(this->{self.field_name}, value.data(), this->{self.field_name}_len);",
|
||||||
o += f" if (this->{self.field_name}_len > {self.array_size}) {{\n"
|
)
|
||||||
o += f" this->{self.field_name}_len = {self.array_size};\n"
|
|
||||||
o += " }\n"
|
|
||||||
o += f" memcpy(this->{self.field_name}, data_str.data(), this->{self.field_name}_len);\n"
|
|
||||||
o += " break;\n"
|
|
||||||
o += "}"
|
|
||||||
return o
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def encode_content(self) -> str:
|
def encode_content(self) -> str:
|
||||||
@@ -1518,7 +1493,7 @@ class UInt32Type(VarintTypeMixin, TypeInfo):
|
|||||||
cpp_type = "uint32_t"
|
cpp_type = "uint32_t"
|
||||||
_varint_max_bits = 32
|
_varint_max_bits = 32
|
||||||
default_value = "0"
|
default_value = "0"
|
||||||
decode_varint = "value"
|
decode_expr = "value.as_varint()"
|
||||||
encode_func = "encode_uint32"
|
encode_func = "encode_uint32"
|
||||||
wire_type = WireType.VARINT # Uses wire type 0
|
wire_type = WireType.VARINT # Uses wire type 0
|
||||||
|
|
||||||
@@ -1541,13 +1516,21 @@ class UInt32Type(VarintTypeMixin, TypeInfo):
|
|||||||
class EnumType(VarintTypeMixin, TypeInfo):
|
class EnumType(VarintTypeMixin, TypeInfo):
|
||||||
_varint_max_bits = 32
|
_varint_max_bits = 32
|
||||||
|
|
||||||
|
def encode_element(self, number: int, element: str) -> str:
|
||||||
|
return _encode_call(
|
||||||
|
self.encode_func,
|
||||||
|
str(number),
|
||||||
|
f"static_cast<uint32_t>({element})",
|
||||||
|
force=True,
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cpp_type(self) -> str:
|
def cpp_type(self) -> str:
|
||||||
return f"enums::{self._field.type_name[1:]}"
|
return f"enums::{self._field.type_name[1:]}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def decode_varint(self) -> str:
|
def decode_expr(self) -> str:
|
||||||
return f"static_cast<{self.cpp_type}>(value)"
|
return f"static_cast<{self.cpp_type}>(value.as_varint())"
|
||||||
|
|
||||||
default_value = ""
|
default_value = ""
|
||||||
wire_type = WireType.VARINT # Uses wire type 0
|
wire_type = WireType.VARINT # Uses wire type 0
|
||||||
@@ -1594,7 +1577,7 @@ class EnumType(VarintTypeMixin, TypeInfo):
|
|||||||
class SFixed32Type(FixedSizeTypeMixin, TypeInfo):
|
class SFixed32Type(FixedSizeTypeMixin, TypeInfo):
|
||||||
cpp_type = "int32_t"
|
cpp_type = "int32_t"
|
||||||
default_value = "0"
|
default_value = "0"
|
||||||
decode_32bit = "value.as_sfixed32()"
|
decode_expr = "value.as_sfixed32()"
|
||||||
encode_func = "encode_sfixed32"
|
encode_func = "encode_sfixed32"
|
||||||
wire_type = WireType.FIXED32 # Uses wire type 5
|
wire_type = WireType.FIXED32 # Uses wire type 5
|
||||||
|
|
||||||
@@ -1620,7 +1603,6 @@ class SFixed32Type(FixedSizeTypeMixin, TypeInfo):
|
|||||||
class SFixed64Type(FixedSizeTypeMixin, TypeInfo):
|
class SFixed64Type(FixedSizeTypeMixin, TypeInfo):
|
||||||
cpp_type = "int64_t"
|
cpp_type = "int64_t"
|
||||||
default_value = "0"
|
default_value = "0"
|
||||||
decode_64bit = "value.as_sfixed64()"
|
|
||||||
encode_func = "encode_sfixed64"
|
encode_func = "encode_sfixed64"
|
||||||
wire_type = WireType.FIXED64 # Uses wire type 1
|
wire_type = WireType.FIXED64 # Uses wire type 1
|
||||||
|
|
||||||
@@ -1647,7 +1629,7 @@ class SInt32Type(VarintTypeMixin, TypeInfo):
|
|||||||
cpp_type = "int32_t"
|
cpp_type = "int32_t"
|
||||||
_varint_max_bits = 32 # zigzag encoding keeps it 32-bit
|
_varint_max_bits = 32 # zigzag encoding keeps it 32-bit
|
||||||
default_value = "0"
|
default_value = "0"
|
||||||
decode_varint = "decode_zigzag32(static_cast<uint32_t>(value))"
|
decode_expr = "decode_zigzag32(static_cast<uint32_t>(value.as_varint()))"
|
||||||
encode_func = "encode_sint32"
|
encode_func = "encode_sint32"
|
||||||
wire_type = WireType.VARINT # Uses wire type 0
|
wire_type = WireType.VARINT # Uses wire type 0
|
||||||
|
|
||||||
@@ -1668,7 +1650,7 @@ class SInt64Type(VarintTypeMixin, TypeInfo):
|
|||||||
cpp_type = "int64_t"
|
cpp_type = "int64_t"
|
||||||
_varint_max_bits = 64
|
_varint_max_bits = 64
|
||||||
default_value = "0"
|
default_value = "0"
|
||||||
decode_varint = "decode_zigzag64(value)"
|
decode_expr = "decode_zigzag64(value.as_varint())"
|
||||||
encode_func = "encode_sint64"
|
encode_func = "encode_sint64"
|
||||||
wire_type = WireType.VARINT # Uses wire type 0
|
wire_type = WireType.VARINT # Uses wire type 0
|
||||||
|
|
||||||
@@ -1821,23 +1803,11 @@ class FixedArrayRepeatedType(TypeInfo):
|
|||||||
|
|
||||||
def _encode_element(self, element: str) -> str:
|
def _encode_element(self, element: str) -> str:
|
||||||
"""Helper to generate encode statement for a single element."""
|
"""Helper to generate encode statement for a single element."""
|
||||||
if isinstance(self._ti, EnumType):
|
if isinstance(self._ti, MessageType) and _is_inline_encode(self._ti.cpp_type):
|
||||||
return _encode_call(
|
return _generate_inline_encode_block(
|
||||||
self._ti.encode_func,
|
self.number, self._ti.cpp_type, element
|
||||||
str(self.number),
|
|
||||||
f"static_cast<uint32_t>({element})",
|
|
||||||
force=True,
|
|
||||||
)
|
)
|
||||||
# Repeated message elements use encode_sub_message (force=true is default)
|
return self._ti.encode_element(self.number, element)
|
||||||
if isinstance(self._ti, MessageType):
|
|
||||||
if _is_inline_encode(self._ti.cpp_type):
|
|
||||||
return _generate_inline_encode_block(
|
|
||||||
self.number, self._ti.cpp_type, element
|
|
||||||
)
|
|
||||||
return _encode_call(
|
|
||||||
"encode_sub_message", "buffer", str(self.number), element
|
|
||||||
)
|
|
||||||
return _encode_call(self._ti.encode_func, str(self.number), element, force=True)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cpp_type(self) -> str:
|
def cpp_type(self) -> str:
|
||||||
@@ -2131,55 +2101,23 @@ class RepeatedTypeInfo(TypeInfo):
|
|||||||
return self._ti.wire_type
|
return self._ti.wire_type
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def decode_varint_content(self) -> str:
|
def decode_expr(self) -> str | None:
|
||||||
# Pointer fields don't support decoding
|
return self._ti.decode_expr
|
||||||
if self._use_pointer:
|
|
||||||
return None
|
def _decode_store(self, expr: str) -> str:
|
||||||
content = self._ti.decode_varint
|
return f"this->{self.field_name}.push_back({expr});"
|
||||||
if content is None:
|
|
||||||
return None
|
|
||||||
return (
|
|
||||||
f"case {self.number}: this->{self.field_name}.push_back({content}); break;"
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def decode_length_content(self) -> str:
|
def decode_content(self) -> str | None:
|
||||||
# Pointer fields don't support decoding
|
# Pointer fields don't support decoding
|
||||||
if self._use_pointer:
|
if self._use_pointer:
|
||||||
return None
|
return None
|
||||||
content = self._ti.decode_length
|
if isinstance(self._ti, MessageType):
|
||||||
if content is None and isinstance(self._ti, MessageType):
|
return self.decode_case(
|
||||||
# Special handling for non-template message decoding
|
f"this->{self.field_name}.emplace_back();\n"
|
||||||
return f"case {self.number}: this->{self.field_name}.emplace_back(); value.decode_to_message(this->{self.field_name}.back()); break;"
|
f"value.decode_to_message(this->{self.field_name}.back());"
|
||||||
if content is None:
|
)
|
||||||
return None
|
return super().decode_content
|
||||||
return (
|
|
||||||
f"case {self.number}: this->{self.field_name}.push_back({content}); break;"
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def decode_32bit_content(self) -> str:
|
|
||||||
# Pointer fields don't support decoding
|
|
||||||
if self._use_pointer:
|
|
||||||
return None
|
|
||||||
content = self._ti.decode_32bit
|
|
||||||
if content is None:
|
|
||||||
return None
|
|
||||||
return (
|
|
||||||
f"case {self.number}: this->{self.field_name}.push_back({content}); break;"
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def decode_64bit_content(self) -> str:
|
|
||||||
# Pointer fields don't support decoding
|
|
||||||
if self._use_pointer:
|
|
||||||
return None
|
|
||||||
content = self._ti.decode_64bit
|
|
||||||
if content is None:
|
|
||||||
return None
|
|
||||||
return (
|
|
||||||
f"case {self.number}: this->{self.field_name}.push_back({content}); break;"
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _ti_is_bool(self) -> bool:
|
def _ti_is_bool(self) -> bool:
|
||||||
@@ -2187,20 +2125,7 @@ class RepeatedTypeInfo(TypeInfo):
|
|||||||
return isinstance(self._ti, BoolType)
|
return isinstance(self._ti, BoolType)
|
||||||
|
|
||||||
def _encode_element_call(self, element: str) -> str:
|
def _encode_element_call(self, element: str) -> str:
|
||||||
"""Helper to generate encode call for a single element."""
|
return self._ti.encode_element(self.number, element)
|
||||||
if isinstance(self._ti, EnumType):
|
|
||||||
return _encode_call(
|
|
||||||
self._ti.encode_func,
|
|
||||||
str(self.number),
|
|
||||||
f"static_cast<uint32_t>({element})",
|
|
||||||
force=True,
|
|
||||||
)
|
|
||||||
# Repeated message elements use encode_sub_message (force=true is default)
|
|
||||||
if isinstance(self._ti, MessageType):
|
|
||||||
return _encode_call(
|
|
||||||
"encode_sub_message", "buffer", str(self.number), element
|
|
||||||
)
|
|
||||||
return _encode_call(self._ti.encode_func, str(self.number), element, force=True)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def encode_content(self) -> str:
|
def encode_content(self) -> str:
|
||||||
@@ -2595,10 +2520,7 @@ def build_message_type(
|
|||||||
) -> tuple[str, str, str]:
|
) -> tuple[str, str, str]:
|
||||||
public_content: list[str] = []
|
public_content: list[str] = []
|
||||||
protected_content: list[str] = []
|
protected_content: list[str] = []
|
||||||
decode_varint: list[str] = []
|
decode: list[str] = []
|
||||||
decode_length: list[str] = []
|
|
||||||
decode_32bit: list[str] = []
|
|
||||||
decode_64bit: list[str] = []
|
|
||||||
encode: list[str] = []
|
encode: list[str] = []
|
||||||
dump: list[str] = []
|
dump: list[str] = []
|
||||||
size_calc: list[str] = []
|
size_calc: list[str] = []
|
||||||
@@ -2614,8 +2536,8 @@ def build_message_type(
|
|||||||
|
|
||||||
# Get source direction to determine if we need decode/encode methods
|
# Get source direction to determine if we need decode/encode methods
|
||||||
source = message_source_map[desc.name]
|
source = message_source_map[desc.name]
|
||||||
needs_decode = source in (SOURCE_BOTH, SOURCE_CLIENT)
|
needs_decode = message_needs_decode(source)
|
||||||
needs_encode = source in (SOURCE_BOTH, SOURCE_SERVER)
|
needs_encode = message_needs_encode(source)
|
||||||
|
|
||||||
# Add MESSAGE_TYPE method if this is a service message
|
# Add MESSAGE_TYPE method if this is a service message
|
||||||
if message_id is not None:
|
if message_id is not None:
|
||||||
@@ -2727,22 +2649,8 @@ def build_message_type(
|
|||||||
if field.options.HasExtension(pb.field_ifdef):
|
if field.options.HasExtension(pb.field_ifdef):
|
||||||
field_ifdef = field.options.Extensions[pb.field_ifdef]
|
field_ifdef = field.options.Extensions[pb.field_ifdef]
|
||||||
|
|
||||||
if ti.decode_varint_content:
|
if case := ti.decode_content:
|
||||||
decode_varint.extend(
|
decode.extend(wrap_with_ifdef(case, field_ifdef))
|
||||||
wrap_with_ifdef(ti.decode_varint_content, field_ifdef)
|
|
||||||
)
|
|
||||||
if ti.decode_length_content:
|
|
||||||
decode_length.extend(
|
|
||||||
wrap_with_ifdef(ti.decode_length_content, field_ifdef)
|
|
||||||
)
|
|
||||||
if ti.decode_32bit_content:
|
|
||||||
decode_32bit.extend(
|
|
||||||
wrap_with_ifdef(ti.decode_32bit_content, field_ifdef)
|
|
||||||
)
|
|
||||||
if ti.decode_64bit_content:
|
|
||||||
decode_64bit.extend(
|
|
||||||
wrap_with_ifdef(ti.decode_64bit_content, field_ifdef)
|
|
||||||
)
|
|
||||||
if ti.dump_content:
|
if ti.dump_content:
|
||||||
# Check for field_ifdef option for dump as well
|
# Check for field_ifdef option for dump as well
|
||||||
field_ifdef = None
|
field_ifdef = None
|
||||||
@@ -2752,50 +2660,23 @@ def build_message_type(
|
|||||||
dump.extend(wrap_with_ifdef(ti.dump_content, field_ifdef))
|
dump.extend(wrap_with_ifdef(ti.dump_content, field_ifdef))
|
||||||
|
|
||||||
cpp = ""
|
cpp = ""
|
||||||
if decode_varint:
|
if decode:
|
||||||
o = f"bool {desc.name}::decode_varint(uint32_t field_id, proto_varint_value_t value) {{\n"
|
o = f"void {desc.name}::decode_field(void *self, uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) {{\n"
|
||||||
o += " switch (field_id) {\n"
|
o += f" auto &msg = *static_cast<{desc.name} *>(self);\n"
|
||||||
o += indent("\n".join(decode_varint), " ") + "\n"
|
o += " const ProtoFieldValue value(data, scalar);\n"
|
||||||
o += " default: return false;\n"
|
o += " switch (tag) {\n"
|
||||||
|
o += indent("\n".join(decode), " ").replace("this->", "msg.") + "\n"
|
||||||
o += " }\n"
|
o += " }\n"
|
||||||
o += " return true;\n"
|
|
||||||
o += "}\n"
|
o += "}\n"
|
||||||
cpp += o
|
cpp += o
|
||||||
prot = "bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;"
|
prot = "static void decode_field(void *self, uint32_t tag, const uint8_t *data, proto_varint_value_t scalar);"
|
||||||
protected_content.insert(0, prot)
|
|
||||||
if decode_length:
|
|
||||||
o = f"bool {desc.name}::decode_length(uint32_t field_id, ProtoLengthDelimited value) {{\n"
|
|
||||||
o += " switch (field_id) {\n"
|
|
||||||
o += indent("\n".join(decode_length), " ") + "\n"
|
|
||||||
o += " default: return false;\n"
|
|
||||||
o += " }\n"
|
|
||||||
o += " return true;\n"
|
|
||||||
o += "}\n"
|
|
||||||
cpp += o
|
|
||||||
prot = "bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;"
|
|
||||||
protected_content.insert(0, prot)
|
|
||||||
if decode_32bit:
|
|
||||||
o = f"bool {desc.name}::decode_32bit(uint32_t field_id, Proto32Bit value) {{\n"
|
|
||||||
o += " switch (field_id) {\n"
|
|
||||||
o += indent("\n".join(decode_32bit), " ") + "\n"
|
|
||||||
o += " default: return false;\n"
|
|
||||||
o += " }\n"
|
|
||||||
o += " return true;\n"
|
|
||||||
o += "}\n"
|
|
||||||
cpp += o
|
|
||||||
prot = "bool decode_32bit(uint32_t field_id, Proto32Bit value) override;"
|
|
||||||
protected_content.insert(0, prot)
|
|
||||||
if decode_64bit:
|
|
||||||
o = f"bool {desc.name}::decode_64bit(uint32_t field_id, Proto64Bit value) {{\n"
|
|
||||||
o += " switch (field_id) {\n"
|
|
||||||
o += indent("\n".join(decode_64bit), " ") + "\n"
|
|
||||||
o += " default: return false;\n"
|
|
||||||
o += " }\n"
|
|
||||||
o += " return true;\n"
|
|
||||||
o += "}\n"
|
|
||||||
cpp += o
|
|
||||||
prot = "bool decode_64bit(uint32_t field_id, Proto64Bit value) override;"
|
|
||||||
protected_content.insert(0, prot)
|
protected_content.insert(0, prot)
|
||||||
|
if not fixed_vector_fields:
|
||||||
|
public_content.append(
|
||||||
|
"void decode(const uint8_t *buffer, size_t length) {\n"
|
||||||
|
" ProtoDecodableMessage::decode_fields(this, buffer, length, &decode_field);\n"
|
||||||
|
"}"
|
||||||
|
)
|
||||||
|
|
||||||
# Generate custom decode() override for messages with FixedVector fields
|
# Generate custom decode() override for messages with FixedVector fields
|
||||||
if fixed_vector_fields:
|
if fixed_vector_fields:
|
||||||
@@ -2805,8 +2686,8 @@ def build_message_type(
|
|||||||
for field_name, field_number in fixed_vector_fields:
|
for field_name, field_number in fixed_vector_fields:
|
||||||
o += f" uint32_t count_{field_name} = ProtoDecodableMessage::count_repeated_field(buffer, length, {field_number});\n"
|
o += f" uint32_t count_{field_name} = ProtoDecodableMessage::count_repeated_field(buffer, length, {field_number});\n"
|
||||||
o += f" this->{field_name}.init(count_{field_name});\n"
|
o += f" this->{field_name}.init(count_{field_name});\n"
|
||||||
# Call parent decode to populate the fields
|
# Then the shared loop fills them
|
||||||
o += " ProtoDecodableMessage::decode(buffer, length);\n"
|
o += " ProtoDecodableMessage::decode_fields(this, buffer, length, &decode_field);\n"
|
||||||
o += "}\n"
|
o += "}\n"
|
||||||
cpp += o
|
cpp += o
|
||||||
# Generate the decode() declaration in header (public method)
|
# Generate the decode() declaration in header (public method)
|
||||||
@@ -2841,28 +2722,36 @@ def build_message_type(
|
|||||||
)
|
)
|
||||||
for line in encode
|
for line in encode
|
||||||
]
|
]
|
||||||
o = f"{speed_attr}uint8_t *{desc.name}::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {{\n"
|
o = f"{speed_attr}uint8_t *{desc.name}::encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) {{\n"
|
||||||
|
o += f" const auto &msg = *static_cast<const {desc.name} *>(self);\n"
|
||||||
o += " uint8_t *__restrict__ pos = buffer.get_pos();\n"
|
o += " uint8_t *__restrict__ pos = buffer.get_pos();\n"
|
||||||
o += indent("\n".join(encode_debug)) + "\n"
|
o += indent("\n".join(encode_debug)).replace("this->", "msg.") + "\n"
|
||||||
o += " return pos;\n"
|
o += " return pos;\n"
|
||||||
o += "}\n"
|
o += "}\n"
|
||||||
cpp += o
|
cpp += o
|
||||||
prot = (
|
public_content.append(
|
||||||
"uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;"
|
"static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);"
|
||||||
|
)
|
||||||
|
public_content.append(
|
||||||
|
"uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {\n"
|
||||||
|
" return encode_msg(this, buffer PROTO_ENCODE_DEBUG_ARG);\n"
|
||||||
|
"}"
|
||||||
)
|
)
|
||||||
public_content.append(prot)
|
|
||||||
# If no fields to encode or message doesn't need encoding, the default implementation in ProtoMessage will be used
|
# If no fields to encode or message doesn't need encoding, the default implementation in ProtoMessage will be used
|
||||||
|
|
||||||
# Add calculate_size method only if this message needs encoding and has fields
|
# Add calculate_size method only if this message needs encoding and has fields
|
||||||
if needs_encode and size_calc and not is_inline_only:
|
if needs_encode and size_calc and not is_inline_only:
|
||||||
o = f"{speed_attr}uint32_t {desc.name}::calculate_size() const {{\n"
|
o = f"{speed_attr}uint32_t {desc.name}::calc_size_msg(const void *self) {{\n"
|
||||||
|
o += f" const auto &msg = *static_cast<const {desc.name} *>(self);\n"
|
||||||
o += " uint32_t size = 0;\n"
|
o += " uint32_t size = 0;\n"
|
||||||
o += indent("\n".join(size_calc)) + "\n"
|
o += indent("\n".join(size_calc)).replace("this->", "msg.") + "\n"
|
||||||
o += " return size;\n"
|
o += " return size;\n"
|
||||||
o += "}\n"
|
o += "}\n"
|
||||||
cpp += o
|
cpp += o
|
||||||
prot = "uint32_t calculate_size() const;"
|
public_content.append("static uint32_t calc_size_msg(const void *self);")
|
||||||
public_content.append(prot)
|
public_content.append(
|
||||||
|
"uint32_t calculate_size() const { return calc_size_msg(this); }"
|
||||||
|
)
|
||||||
# If no fields to calculate size for or message doesn't need encoding, the default implementation in ProtoMessage will be used
|
# If no fields to calculate size for or message doesn't need encoding, the default implementation in ProtoMessage will be used
|
||||||
|
|
||||||
# dump_to method declaration in header
|
# dump_to method declaration in header
|
||||||
@@ -2957,6 +2846,23 @@ def get_field_opt(
|
|||||||
return field.options.Extensions[opt]
|
return field.options.Extensions[opt]
|
||||||
|
|
||||||
|
|
||||||
|
def message_needs_decode(source: int) -> bool:
|
||||||
|
return source in (SOURCE_BOTH, SOURCE_CLIENT)
|
||||||
|
|
||||||
|
|
||||||
|
def message_needs_encode(source: int) -> bool:
|
||||||
|
return source in (SOURCE_BOTH, SOURCE_SERVER)
|
||||||
|
|
||||||
|
|
||||||
|
def is_decodable_class(desc: descriptor.DescriptorProto, source: int) -> bool:
|
||||||
|
"""Whether the generated class derives from ProtoDecodableMessage: decoded, and either on a
|
||||||
|
decodable base class or with at least one live field."""
|
||||||
|
return message_needs_decode(source) and (
|
||||||
|
get_base_class(desc) is not None
|
||||||
|
or any(not field.options.deprecated for field in desc.field)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_base_class(desc: descriptor.DescriptorProto) -> str | None:
|
def get_base_class(desc: descriptor.DescriptorProto) -> str | None:
|
||||||
"""Get the base_class option from a message descriptor."""
|
"""Get the base_class option from a message descriptor."""
|
||||||
if not desc.options.HasExtension(pb.base_class):
|
if not desc.options.HasExtension(pb.base_class):
|
||||||
@@ -3058,11 +2964,11 @@ def build_base_class(
|
|||||||
|
|
||||||
# Determine if any message using this base class needs decoding/encoding
|
# Determine if any message using this base class needs decoding/encoding
|
||||||
needs_decode = any(
|
needs_decode = any(
|
||||||
message_source_map.get(msg.name, SOURCE_BOTH) in (SOURCE_BOTH, SOURCE_CLIENT)
|
message_needs_decode(message_source_map.get(msg.name, SOURCE_BOTH))
|
||||||
for msg in messages
|
for msg in messages
|
||||||
)
|
)
|
||||||
needs_encode = any(
|
needs_encode = any(
|
||||||
message_source_map.get(msg.name, SOURCE_BOTH) in (SOURCE_BOTH, SOURCE_SERVER)
|
message_needs_encode(message_source_map.get(msg.name, SOURCE_BOTH))
|
||||||
for msg in messages
|
for msg in messages
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -3496,6 +3402,7 @@ static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint
|
|||||||
|
|
||||||
# Generate message types with base class information
|
# Generate message types with base class information
|
||||||
# Simple grouping by ifdef
|
# Simple grouping by ifdef
|
||||||
|
decodable_messages: list[tuple[str, str | None]] = []
|
||||||
current_ifdef = None
|
current_ifdef = None
|
||||||
|
|
||||||
for m in mt:
|
for m in mt:
|
||||||
@@ -3512,6 +3419,8 @@ static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
s, c, dc = build_message_type(m, base_class_fields, message_source_map)
|
s, c, dc = build_message_type(m, base_class_fields, message_source_map)
|
||||||
|
if is_decodable_class(m, message_source_map[m.name]):
|
||||||
|
decodable_messages.append((m.name, message_ifdef_map.get(m.name)))
|
||||||
msg_ifdef = message_ifdef_map.get(m.name)
|
msg_ifdef = message_ifdef_map.get(m.name)
|
||||||
|
|
||||||
# Handle ifdef changes
|
# Handle ifdef changes
|
||||||
@@ -3538,6 +3447,22 @@ static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint
|
|||||||
cpp += "#endif\n"
|
cpp += "#endif\n"
|
||||||
dump_cpp += "#endif\n"
|
dump_cpp += "#endif\n"
|
||||||
|
|
||||||
|
# decode() passes decode_field explicitly, so without the dump virtuals no decodable message
|
||||||
|
# may carry a vtable; a build at any level below VERY_VERBOSE proves it
|
||||||
|
cpp += "#ifndef HAS_PROTO_MESSAGE_DUMP\n"
|
||||||
|
assert_ifdef = None
|
||||||
|
for name, msg_ifdef in decodable_messages:
|
||||||
|
if msg_ifdef != assert_ifdef:
|
||||||
|
if assert_ifdef is not None:
|
||||||
|
cpp += "#endif\n"
|
||||||
|
if msg_ifdef is not None:
|
||||||
|
cpp += _make_ifdef_line(msg_ifdef) + "\n"
|
||||||
|
assert_ifdef = msg_ifdef
|
||||||
|
cpp += f'static_assert(!std::is_polymorphic_v<{name}>, "decodable messages carry no vtable");\n'
|
||||||
|
if assert_ifdef is not None:
|
||||||
|
cpp += "#endif\n"
|
||||||
|
cpp += "#endif\n"
|
||||||
|
|
||||||
content += """\
|
content += """\
|
||||||
|
|
||||||
} // namespace esphome::api
|
} // namespace esphome::api
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ static APIBuffer build_infrared_rf_transmit_wire() {
|
|||||||
std::memcpy(bytes + len, packed, packed_len);
|
std::memcpy(bytes + len, packed, packed_len);
|
||||||
len += packed_len;
|
len += packed_len;
|
||||||
// field 6: modulation = 1 (non-zero so it's actually emitted and exercises
|
// field 6: modulation = 1 (non-zero so it's actually emitted and exercises
|
||||||
// decode_varint for this field, matching the documented layout above).
|
// decode_field for this field, matching the documented layout above).
|
||||||
put_byte(0x30);
|
put_byte(0x30);
|
||||||
put_varint(1);
|
put_varint(1);
|
||||||
|
|
||||||
|
|||||||
@@ -59,4 +59,47 @@ TEST(StringRefStartsWith, RefOverloadComparesOnlyTheViewedLength) {
|
|||||||
EXPECT_TRUE(ref.starts_with(prefix));
|
EXPECT_TRUE(ref.starts_with(prefix));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The generated api messages start their encode only string fields as a null pointer with zero
|
||||||
|
// length; every member must treat that exactly like the default constructed empty string.
|
||||||
|
TEST(StringRefNullEmpty, BehavesAsEmptyString) {
|
||||||
|
const StringRef null_empty{nullptr, 0};
|
||||||
|
const StringRef empty;
|
||||||
|
EXPECT_TRUE(null_empty.empty());
|
||||||
|
EXPECT_EQ(null_empty.size(), 0u);
|
||||||
|
EXPECT_EQ(null_empty.c_str(), nullptr);
|
||||||
|
EXPECT_TRUE(null_empty == empty);
|
||||||
|
EXPECT_TRUE(null_empty == "");
|
||||||
|
EXPECT_TRUE(null_empty == std::string());
|
||||||
|
EXPECT_EQ(null_empty.compare(empty), 0);
|
||||||
|
EXPECT_EQ(null_empty.compare(""), 0);
|
||||||
|
EXPECT_LT(null_empty.compare("a"), 0);
|
||||||
|
EXPECT_TRUE(null_empty.starts_with(""));
|
||||||
|
EXPECT_FALSE(null_empty.starts_with("a"));
|
||||||
|
EXPECT_EQ(null_empty.str(), std::string());
|
||||||
|
EXPECT_EQ(null_empty.substr(0), std::string());
|
||||||
|
EXPECT_EQ(null_empty.find('a'), std::string::npos);
|
||||||
|
EXPECT_EQ(null_empty.find("a"), std::string::npos);
|
||||||
|
char buf[4] = "xyz";
|
||||||
|
EXPECT_EQ(null_empty.copy(buf, sizeof(buf)), 0u);
|
||||||
|
EXPECT_EQ(null_empty.begin(), null_empty.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(StringRefNullEmpty, ComparesAgainstText) {
|
||||||
|
const StringRef null_empty{nullptr, 0};
|
||||||
|
const StringRef text("abc", 3);
|
||||||
|
EXPECT_FALSE(null_empty == text);
|
||||||
|
EXPECT_FALSE(text == null_empty);
|
||||||
|
EXPECT_LT(null_empty.compare(text), 0);
|
||||||
|
EXPECT_GT(text.compare(null_empty), 0);
|
||||||
|
EXPECT_TRUE(text.starts_with(null_empty));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(StringRefNullEmpty, TwoNullViewsAreEqual) {
|
||||||
|
const StringRef a{nullptr, 0};
|
||||||
|
const StringRef b{nullptr, 0};
|
||||||
|
EXPECT_TRUE(a == b);
|
||||||
|
EXPECT_EQ(a.compare(b), 0);
|
||||||
|
EXPECT_TRUE(a.starts_with(b));
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace esphome::core::testing
|
} // namespace esphome::core::testing
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
esphome:
|
||||||
|
name: api-decode-wire-types-test
|
||||||
|
host:
|
||||||
|
api:
|
||||||
|
logger:
|
||||||
|
level: DEBUG
|
||||||
|
|
||||||
|
switch:
|
||||||
|
- platform: template
|
||||||
|
name: "Wire Switch"
|
||||||
|
optimistic: true
|
||||||
|
|
||||||
|
output:
|
||||||
|
- platform: template
|
||||||
|
id: wire_dim
|
||||||
|
type: float
|
||||||
|
write_action:
|
||||||
|
- lambda: ""
|
||||||
|
|
||||||
|
light:
|
||||||
|
- platform: monochromatic
|
||||||
|
name: "Wire Light"
|
||||||
|
output: wire_dim
|
||||||
|
default_transition_length: 0s
|
||||||
|
effects:
|
||||||
|
- pulse:
|
||||||
|
name: Pulse
|
||||||
|
|
||||||
|
text:
|
||||||
|
- platform: template
|
||||||
|
name: "Wire Text"
|
||||||
|
optimistic: true
|
||||||
|
mode: text
|
||||||
|
min_length: 0
|
||||||
|
max_length: 255
|
||||||
|
|
||||||
|
number:
|
||||||
|
- platform: template
|
||||||
|
name: "Wire Number"
|
||||||
|
optimistic: true
|
||||||
|
min_value: -1000
|
||||||
|
max_value: 1000
|
||||||
|
step: 0.5
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
esphome:
|
||||||
|
name: api-empty-message-test
|
||||||
|
host:
|
||||||
|
api:
|
||||||
|
logger:
|
||||||
|
level: DEBUG
|
||||||
|
|
||||||
|
switch:
|
||||||
|
- platform: template
|
||||||
|
name: "Empty Message Switch"
|
||||||
|
optimistic: true
|
||||||
@@ -125,11 +125,12 @@ class RawApiClient:
|
|||||||
await self.read_until_frame(MESSAGE_TYPE_OF[api_pb2.HelloResponse])
|
await self.read_until_frame(MESSAGE_TYPE_OF[api_pb2.HelloResponse])
|
||||||
|
|
||||||
async def send_message(self, msg: message.Message) -> None:
|
async def send_message(self, msg: message.Message) -> None:
|
||||||
|
await self.send_raw(MESSAGE_TYPE_OF[type(msg)], msg.SerializeToString())
|
||||||
|
|
||||||
|
async def send_raw(self, msg_type: int, payload: bytes) -> None:
|
||||||
|
"""Send a frame with a hand built payload, for shapes protobuf will not serialize."""
|
||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
await loop.sock_sendall(
|
await loop.sock_sendall(self._sock, encode_frame(msg_type, payload))
|
||||||
self._sock,
|
|
||||||
encode_frame(MESSAGE_TYPE_OF[type(msg)], msg.SerializeToString()),
|
|
||||||
)
|
|
||||||
|
|
||||||
async def read_until_frame(self, msg_type: int, timeout: float = 10.0) -> None:
|
async def read_until_frame(self, msg_type: int, timeout: float = 10.0) -> None:
|
||||||
"""Read until at least one frame of msg_type has been received."""
|
"""Read until at least one frame of msg_type has been received."""
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
"""decode_field() must take fields that match their declared wire type, drop the ones that do
|
||||||
|
not, skip unknown fields, and handle two byte tags, varints and length prefixes."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
import struct
|
||||||
|
|
||||||
|
from aioesphomeapi import (
|
||||||
|
EntityState,
|
||||||
|
LightState,
|
||||||
|
NumberState,
|
||||||
|
SwitchState,
|
||||||
|
TextState,
|
||||||
|
api_pb2,
|
||||||
|
)
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from .raw_api_client import MESSAGE_TYPE_OF, RawApiClient, encode_varint
|
||||||
|
from .state_utils import InitialStateHelper, StateWaiter, require_entity
|
||||||
|
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||||
|
|
||||||
|
SWITCH_COMMAND = MESSAGE_TYPE_OF[api_pb2.SwitchCommandRequest]
|
||||||
|
WIRE_VARINT, WIRE_LENGTH, WIRE_FIXED32 = 0, 2, 5
|
||||||
|
|
||||||
|
|
||||||
|
def tag(field: int, wire_type: int) -> bytes:
|
||||||
|
return encode_varint((field << 3) | wire_type)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_api_decode_wire_types(
|
||||||
|
yaml_config: str,
|
||||||
|
run_compiled: RunCompiledFunction,
|
||||||
|
api_client_connected: APIClientConnectedFactory,
|
||||||
|
unused_tcp_port: int,
|
||||||
|
) -> None:
|
||||||
|
async with (
|
||||||
|
run_compiled(yaml_config),
|
||||||
|
api_client_connected() as client,
|
||||||
|
RawApiClient(unused_tcp_port) as raw,
|
||||||
|
):
|
||||||
|
entities, _ = await client.list_entities_services()
|
||||||
|
switch = require_entity(entities, "wire_switch")
|
||||||
|
light = require_entity(entities, "wire_light")
|
||||||
|
text = require_entity(entities, "wire_text")
|
||||||
|
number = require_entity(entities, "wire_number")
|
||||||
|
key = tag(1, WIRE_FIXED32) + struct.pack("<I", switch.key)
|
||||||
|
on, off = tag(2, WIRE_VARINT) + b"\x01", tag(2, WIRE_VARINT) + b"\x00"
|
||||||
|
|
||||||
|
switch_states: list[bool] = []
|
||||||
|
waiter = StateWaiter()
|
||||||
|
|
||||||
|
def on_state(state: EntityState) -> None:
|
||||||
|
if isinstance(state, SwitchState) and state.key == switch.key:
|
||||||
|
switch_states.append(state.state)
|
||||||
|
waiter.on_state(state)
|
||||||
|
|
||||||
|
def switch_is(value: bool) -> Callable[[EntityState], bool]:
|
||||||
|
return lambda s: (
|
||||||
|
isinstance(s, SwitchState) and s.key == switch.key and s.state is value
|
||||||
|
)
|
||||||
|
|
||||||
|
def number_is(value: float) -> Callable[[EntityState], bool]:
|
||||||
|
return lambda s: (
|
||||||
|
isinstance(s, NumberState) and s.key == number.key and s.state == value
|
||||||
|
)
|
||||||
|
|
||||||
|
initial = InitialStateHelper(entities)
|
||||||
|
client.subscribe_states(initial.on_state_wrapper(on_state))
|
||||||
|
await initial.wait_for_initial_states()
|
||||||
|
await raw.connect()
|
||||||
|
|
||||||
|
# A well formed command: fixed32 key, varint state
|
||||||
|
await raw.send_raw(SWITCH_COMMAND, key + on)
|
||||||
|
await waiter.expect(switch_is(True))
|
||||||
|
await raw.send_raw(SWITCH_COMMAND, key + off)
|
||||||
|
await waiter.expect(switch_is(False))
|
||||||
|
|
||||||
|
# The same field with the wrong wire type is dropped, and a varint key never matches an
|
||||||
|
# entity; each of these would turn the switch on if the payload were read as a varint
|
||||||
|
seen = len(switch_states)
|
||||||
|
await raw.send_raw(SWITCH_COMMAND, key + tag(2, WIRE_LENGTH) + b"\x01\x01")
|
||||||
|
await raw.send_raw(
|
||||||
|
SWITCH_COMMAND, key + tag(2, WIRE_FIXED32) + b"\x01\x00\x00\x00"
|
||||||
|
)
|
||||||
|
await raw.send_raw(
|
||||||
|
SWITCH_COMMAND, tag(1, WIRE_VARINT) + encode_varint(switch.key) + on
|
||||||
|
)
|
||||||
|
# Ordered on the raw socket itself: this frame cannot be parsed before the bad ones, so
|
||||||
|
# the only switch state since the marker must be the one it produces
|
||||||
|
await raw.send_raw(SWITCH_COMMAND, key + on)
|
||||||
|
await waiter.expect(switch_is(True), label="switch on after wrong wire types")
|
||||||
|
assert switch_states[seen:] == [True]
|
||||||
|
await raw.send_raw(SWITCH_COMMAND, key + off)
|
||||||
|
await waiter.expect(switch_is(False))
|
||||||
|
|
||||||
|
# Truncated bodies stop the decode loop without taking the connection down: a tag with its
|
||||||
|
# continuation bit set and nothing after it, a length prefix past the end of the payload,
|
||||||
|
# and a fixed32 with two of its four bytes
|
||||||
|
seen = len(switch_states)
|
||||||
|
await raw.send_raw(SWITCH_COMMAND, key + b"\x80")
|
||||||
|
await raw.send_raw(SWITCH_COMMAND, key + tag(2, WIRE_LENGTH) + b"\x7f" + b"ab")
|
||||||
|
await raw.send_raw(SWITCH_COMMAND, tag(1, WIRE_FIXED32) + b"\x01\x02")
|
||||||
|
await raw.send_raw(SWITCH_COMMAND, key + on)
|
||||||
|
await waiter.expect(switch_is(True), label="switch on after truncated frames")
|
||||||
|
assert switch_states[seen:] == [True]
|
||||||
|
await raw.send_raw(SWITCH_COMMAND, key + off)
|
||||||
|
await waiter.expect(switch_is(False))
|
||||||
|
|
||||||
|
# A negative number goes through the fixed32 float path of a normal client
|
||||||
|
client.number_command(number.key, -77.5)
|
||||||
|
await waiter.expect(number_is(-77.5))
|
||||||
|
|
||||||
|
# An unknown field ahead of the known ones is skipped; field 200 needs a two byte tag
|
||||||
|
await raw.send_raw(
|
||||||
|
SWITCH_COMMAND, tag(200, WIRE_VARINT) + encode_varint(300) + key + on
|
||||||
|
)
|
||||||
|
await waiter.expect(switch_is(True))
|
||||||
|
|
||||||
|
# Two byte tags (effect fields 18 and 19) and a two byte varint (300 ms transition)
|
||||||
|
client.light_command(
|
||||||
|
light.key, state=True, brightness=0.5, transition_length=0.3, effect="Pulse"
|
||||||
|
)
|
||||||
|
await waiter.expect(
|
||||||
|
lambda s: (
|
||||||
|
isinstance(s, LightState) and s.key == light.key and s.effect == "Pulse"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
client.light_command(light.key, effect="None", state=False)
|
||||||
|
await waiter.expect(
|
||||||
|
lambda s: isinstance(s, LightState) and s.key == light.key and not s.state
|
||||||
|
)
|
||||||
|
|
||||||
|
# A string whose length prefix needs two varint bytes
|
||||||
|
long_text = "w" * 200
|
||||||
|
client.text_command(text.key, long_text)
|
||||||
|
await waiter.expect(
|
||||||
|
lambda s: (
|
||||||
|
isinstance(s, TextState) and s.key == text.key and s.state == long_text
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
"""Messages without fields go through the shared ProtoMessage entry points on both directions."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from aioesphomeapi import api_pb2
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from .raw_api_client import MESSAGE_TYPE_OF, RawApiClient
|
||||||
|
from .types import RunCompiledFunction
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_api_empty_message_roundtrip(
|
||||||
|
yaml_config: str,
|
||||||
|
run_compiled: RunCompiledFunction,
|
||||||
|
unused_tcp_port: int,
|
||||||
|
) -> None:
|
||||||
|
async with run_compiled(yaml_config), RawApiClient(unused_tcp_port) as client:
|
||||||
|
await client.connect()
|
||||||
|
|
||||||
|
# Field free request and reply on the plain send path
|
||||||
|
await client.send_message(api_pb2.PingRequest())
|
||||||
|
await client.read_until_frame(MESSAGE_TYPE_OF[api_pb2.PingResponse])
|
||||||
|
|
||||||
|
# Field free request answered by a message with fields, and a list that ends with
|
||||||
|
# the field free ListEntitiesDoneResponse through the batching path
|
||||||
|
await client.send_message(api_pb2.DeviceInfoRequest())
|
||||||
|
await client.read_until_frame(MESSAGE_TYPE_OF[api_pb2.DeviceInfoResponse])
|
||||||
|
await client.send_message(api_pb2.ListEntitiesRequest())
|
||||||
|
await client.read_until_frame(MESSAGE_TYPE_OF[api_pb2.ListEntitiesDoneResponse])
|
||||||
|
assert (
|
||||||
|
client.frame_counts[MESSAGE_TYPE_OF[api_pb2.ListEntitiesSwitchResponse]]
|
||||||
|
== 1
|
||||||
|
)
|
||||||
|
|
||||||
|
await client.send_message(api_pb2.DisconnectRequest())
|
||||||
|
await client.read_until_frame(MESSAGE_TYPE_OF[api_pb2.DisconnectResponse])
|
||||||
@@ -194,17 +194,17 @@ def test_superseded_device_info_fields_still_declared_in_header() -> None:
|
|||||||
|
|
||||||
def test_superseded_device_info_fields_still_encoded_and_sized() -> None:
|
def test_superseded_device_info_fields_still_encoded_and_sized() -> None:
|
||||||
"""Each superseded field must still be touched by DeviceInfoResponse's
|
"""Each superseded field must still be touched by DeviceInfoResponse's
|
||||||
generated encode() and calculate_size(), i.e. it is still put on the wire.
|
generated encode_msg() and calc_size_msg(), i.e. it is still put on the wire.
|
||||||
"""
|
"""
|
||||||
encode_body = _extract_function_body(CPP_TEXT, "DeviceInfoResponse::encode")
|
encode_body = _extract_function_body(CPP_TEXT, "DeviceInfoResponse::encode_msg")
|
||||||
size_body = _extract_function_body(CPP_TEXT, "DeviceInfoResponse::calculate_size")
|
size_body = _extract_function_body(CPP_TEXT, "DeviceInfoResponse::calc_size_msg")
|
||||||
for field_name in SUPERSEDED_FIELDS:
|
for field_name in SUPERSEDED_FIELDS:
|
||||||
assert f"this->{field_name}" in encode_body, (
|
assert f"msg.{field_name}" in encode_body, (
|
||||||
f"DeviceInfoResponse::encode() no longer references {field_name}. "
|
f"DeviceInfoResponse::encode_msg() no longer references {field_name}. "
|
||||||
f"{DEPRECATED_FIELD_TRAP}"
|
f"{DEPRECATED_FIELD_TRAP}"
|
||||||
)
|
)
|
||||||
assert f"this->{field_name}" in size_body, (
|
assert f"msg.{field_name}" in size_body, (
|
||||||
f"DeviceInfoResponse::calculate_size() no longer references "
|
f"DeviceInfoResponse::calc_size_msg() no longer references "
|
||||||
f"{field_name}. {DEPRECATED_FIELD_TRAP}"
|
f"{field_name}. {DEPRECATED_FIELD_TRAP}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ sys.path.insert(0, str(Path(__file__).parents[4] / "script" / "api_protobuf"))
|
|||||||
import aioesphomeapi.api_options_pb2 as pb # noqa: E402
|
import aioesphomeapi.api_options_pb2 as pb # noqa: E402
|
||||||
from api_protobuf import ( # noqa: E402
|
from api_protobuf import ( # noqa: E402
|
||||||
MAX_MESSAGE_ID,
|
MAX_MESSAGE_ID,
|
||||||
|
SOURCE_CLIENT,
|
||||||
_make_ifdef_line,
|
_make_ifdef_line,
|
||||||
|
build_message_type,
|
||||||
create_field_type_info,
|
create_field_type_info,
|
||||||
get_varint64_ifdef,
|
get_varint64_ifdef,
|
||||||
validate_message_id,
|
validate_message_id,
|
||||||
@@ -36,12 +38,15 @@ def _file_with_messages(
|
|||||||
file_desc = descriptor_pb2.FileDescriptorProto(name="test.proto")
|
file_desc = descriptor_pb2.FileDescriptorProto(name="test.proto")
|
||||||
for name, field_type, deprecated in messages:
|
for name, field_type, deprecated in messages:
|
||||||
msg = file_desc.message_type.add(name=name)
|
msg = file_desc.message_type.add(name=name)
|
||||||
field = msg.field.add(name="value", number=1, type=field_type)
|
field = msg.field.add()
|
||||||
|
field.CopyFrom(_field(field_type))
|
||||||
field.options.deprecated = deprecated
|
field.options.deprecated = deprecated
|
||||||
return file_desc
|
return file_desc
|
||||||
|
|
||||||
|
|
||||||
UINT64 = descriptor_pb2.FieldDescriptorProto.TYPE_UINT64
|
UINT64 = descriptor_pb2.FieldDescriptorProto.TYPE_UINT64
|
||||||
|
MESSAGE = descriptor_pb2.FieldDescriptorProto.TYPE_MESSAGE
|
||||||
|
DOUBLE = descriptor_pb2.FieldDescriptorProto.TYPE_DOUBLE
|
||||||
INT64 = descriptor_pb2.FieldDescriptorProto.TYPE_INT64
|
INT64 = descriptor_pb2.FieldDescriptorProto.TYPE_INT64
|
||||||
SINT64 = descriptor_pb2.FieldDescriptorProto.TYPE_SINT64
|
SINT64 = descriptor_pb2.FieldDescriptorProto.TYPE_SINT64
|
||||||
UINT32 = descriptor_pb2.FieldDescriptorProto.TYPE_UINT32
|
UINT32 = descriptor_pb2.FieldDescriptorProto.TYPE_UINT32
|
||||||
@@ -182,3 +187,156 @@ def test_multi_byte_tag_fixed32_falls_back_to_the_generic_helper(
|
|||||||
content = _encode_field(field_type, number=16)
|
content = _encode_field(field_type, number=16)
|
||||||
assert "write_tag_and_fixed32" not in content, content
|
assert "write_tag_and_fixed32" not in content, content
|
||||||
assert content.startswith("pos = ProtoEncode::encode_"), content
|
assert content.startswith("pos = ProtoEncode::encode_"), content
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_case(field_type: int, number: int, *, repeated: bool = False) -> str:
|
||||||
|
"""Return the decode_field() case the generator emits for one decoded field."""
|
||||||
|
field = _field(field_type, number, repeated=repeated)
|
||||||
|
if field_type == MESSAGE:
|
||||||
|
field.type_name = ".Sub"
|
||||||
|
return create_field_type_info(
|
||||||
|
field, needs_decode=True, needs_encode=False
|
||||||
|
).decode_content
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("needs_decode", "force", "member"),
|
||||||
|
[
|
||||||
|
(False, False, "StringRef value{nullptr, 0}; // null until set, encode only"),
|
||||||
|
(True, False, "StringRef value{};"),
|
||||||
|
(False, True, "StringRef value{};"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_string_fields_default_to_null_only_when_never_read(
|
||||||
|
needs_decode: bool, force: bool, member: str
|
||||||
|
) -> None:
|
||||||
|
"""Only a string that is neither decoded nor force encoded may start as a null StringRef."""
|
||||||
|
ti = create_field_type_info(
|
||||||
|
_field(STRING, force=force), needs_decode=needs_decode, needs_encode=True
|
||||||
|
)
|
||||||
|
assert ti.public_content == [member]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("field_type", "number", "wire_type", "accessor"),
|
||||||
|
[
|
||||||
|
(UINT32, 2, "WIRE_TYPE_VARINT", "value.as_varint()"),
|
||||||
|
(BOOL, 3, "WIRE_TYPE_VARINT", "value.as_bool()"),
|
||||||
|
(STRING, 1, "WIRE_TYPE_LENGTH_DELIMITED", "value.data()"),
|
||||||
|
(FLOAT, 4, "WIRE_TYPE_FIXED32", "value.as_float()"),
|
||||||
|
(FIXED32, 5, "WIRE_TYPE_FIXED32", "value.as_fixed32()"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_decode_cases_carry_field_number_and_wire_type(
|
||||||
|
field_type: int, number: int, wire_type: str, accessor: str
|
||||||
|
) -> None:
|
||||||
|
"""Each decoded field yields one case keyed on its number and declared wire type."""
|
||||||
|
case = _decode_case(field_type, number)
|
||||||
|
lines = case.splitlines()
|
||||||
|
assert lines[0] == f"case proto_tag({number}, {wire_type}):", case
|
||||||
|
assert accessor in lines[1], case
|
||||||
|
assert lines[-1].strip() == "break;", case
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("field_type", "repeated", "wire_type", "store"),
|
||||||
|
[
|
||||||
|
(UINT32, True, "WIRE_TYPE_VARINT", "this->value.push_back(value.as_varint());"),
|
||||||
|
(
|
||||||
|
STRING,
|
||||||
|
True,
|
||||||
|
"WIRE_TYPE_LENGTH_DELIMITED",
|
||||||
|
"this->value.push_back(value.as_string());",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
MESSAGE,
|
||||||
|
False,
|
||||||
|
"WIRE_TYPE_LENGTH_DELIMITED",
|
||||||
|
"value.decode_to_message(this->value);",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
MESSAGE,
|
||||||
|
True,
|
||||||
|
"WIRE_TYPE_LENGTH_DELIMITED",
|
||||||
|
"value.decode_to_message(this->value.back());",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_repeated_and_message_fields_decode_through_the_same_case_shape(
|
||||||
|
field_type: int, repeated: bool, wire_type: str, store: str
|
||||||
|
) -> None:
|
||||||
|
"""Repeated and sub message fields land in the one switch with their own store."""
|
||||||
|
case = _decode_case(field_type, 7, repeated=repeated)
|
||||||
|
lines = case.splitlines()
|
||||||
|
assert lines[0] == f"case proto_tag(7, {wire_type}):", case
|
||||||
|
assert store in case, case
|
||||||
|
if field_type == MESSAGE and repeated:
|
||||||
|
assert "this->value.emplace_back();" in case, case
|
||||||
|
assert lines[-1].strip() == "break;", case
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_fixed64_field_fails_at_generation_time() -> None:
|
||||||
|
"""The decode loop has no 64 bit wire type path, so such a field must never reach it silently."""
|
||||||
|
desc = descriptor_pb2.DescriptorProto(name="Wide")
|
||||||
|
desc.field.add(name="ratio", number=1, type=DOUBLE)
|
||||||
|
with pytest.raises(
|
||||||
|
ValueError, match="64-bit type 'double' .*ratio.* not supported"
|
||||||
|
):
|
||||||
|
build_message_type(desc, {}, {"Wide": SOURCE_CLIENT})
|
||||||
|
|
||||||
|
|
||||||
|
def test_message_decodes_through_one_static_decode_field() -> None:
|
||||||
|
"""All wire types of a decoded message land in one static decode_field() switch that the
|
||||||
|
inline decode() hands to the shared loop."""
|
||||||
|
desc = descriptor_pb2.DescriptorProto(name="Mixed")
|
||||||
|
desc.field.add(name="name", number=1, type=STRING)
|
||||||
|
desc.field.add(name="count", number=2, type=UINT32)
|
||||||
|
desc.field.add(name="level", number=3, type=FLOAT)
|
||||||
|
header, cpp, _ = build_message_type(desc, {}, {"Mixed": SOURCE_CLIENT})
|
||||||
|
decl = "static void decode_field(void *self, uint32_t tag, const uint8_t *data, proto_varint_value_t scalar);"
|
||||||
|
assert header.count(decl) == 1
|
||||||
|
assert (
|
||||||
|
cpp.count(
|
||||||
|
"void Mixed::decode_field(void *self, uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) {"
|
||||||
|
)
|
||||||
|
== 1
|
||||||
|
)
|
||||||
|
assert "switch (tag) {" in cpp
|
||||||
|
assert "const ProtoFieldValue value(data, scalar);" in cpp
|
||||||
|
for number, wire_type in (
|
||||||
|
(1, "WIRE_TYPE_LENGTH_DELIMITED"),
|
||||||
|
(2, "WIRE_TYPE_VARINT"),
|
||||||
|
(3, "WIRE_TYPE_FIXED32"),
|
||||||
|
):
|
||||||
|
assert f"case proto_tag({number}, {wire_type}):" in cpp, cpp
|
||||||
|
# The static body works on the cast message, never on this
|
||||||
|
assert "auto &msg = *static_cast<Mixed *>(self);" in cpp
|
||||||
|
assert "this->" not in cpp
|
||||||
|
assert (
|
||||||
|
header.count(
|
||||||
|
"ProtoDecodableMessage::decode_fields(this, buffer, length, &decode_field);"
|
||||||
|
)
|
||||||
|
== 1
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_fixed_vector_message_keeps_its_own_decode() -> None:
|
||||||
|
"""A message that sizes a FixedVector first decodes through its own decode(), not the inline one."""
|
||||||
|
desc = descriptor_pb2.DescriptorProto(name="Sized")
|
||||||
|
field = desc.field.add(name="values", number=1, type=UINT32)
|
||||||
|
field.label = descriptor_pb2.FieldDescriptorProto.LABEL_REPEATED
|
||||||
|
field.options.Extensions[pb.fixed_vector] = True
|
||||||
|
header, cpp, _ = build_message_type(desc, {}, {"Sized": SOURCE_CLIENT})
|
||||||
|
assert (
|
||||||
|
"ProtoDecodableMessage::decode_fields(this, buffer, length, &decode_field);"
|
||||||
|
not in header
|
||||||
|
)
|
||||||
|
assert header.count("void decode(const uint8_t *buffer, size_t length);") == 1
|
||||||
|
assert "void Sized::decode(const uint8_t *buffer, size_t length) {" in cpp
|
||||||
|
assert "ProtoDecodableMessage::count_repeated_field(buffer, length, 1)" in cpp
|
||||||
|
assert (
|
||||||
|
cpp.count(
|
||||||
|
"ProtoDecodableMessage::decode_fields(this, buffer, length, &decode_field);"
|
||||||
|
)
|
||||||
|
== 1
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user