mirror of
https://github.com/esphome/esphome.git
synced 2026-02-23 17:58:23 -07:00
On 32-bit platforms (ESP32 Xtensa), 64-bit shifts in varint parsing compile to __ashldi3 library calls. Since the vast majority of protobuf varint fields (message types, sizes, enum values, sensor readings) fit in 4 bytes, the 64-bit arithmetic is unnecessary overhead on the common path. Split parse() into two phases: - Bytes 0-3: uint32_t loop with native 32-bit shifts (0, 7, 14, 21) - Bytes 4-9: noinline parse_wide_() with uint64_t, only for BLE addresses and other 64-bit fields The code generator auto-detects which proto messages use int64/uint64/ sint64 fields and emits USE_API_VARINT64 conditionally. On non-BLE configs, parse_wide_() and the 64-bit accessors (as_uint64, as_int64, as_sint64) are compiled out entirely. Saves ~40 bytes flash on non-BLE configs. Benchmark shows 25-50% faster parsing for 1-4 byte varints (the common case).
160 lines
4.8 KiB
C++
160 lines
4.8 KiB
C++
#include "proto.h"
|
|
#include <cinttypes>
|
|
#include "esphome/core/helpers.h"
|
|
#include "esphome/core/log.h"
|
|
|
|
namespace esphome::api {
|
|
|
|
static const char *const TAG = "api.proto";
|
|
|
|
#ifdef USE_API_VARINT64
|
|
optional<ProtoVarInt> ProtoVarInt::parse_wide_(const uint8_t *buffer, uint32_t len, uint32_t *consumed,
|
|
uint32_t result32) {
|
|
uint64_t result64 = result32;
|
|
uint32_t limit = std::min(len, uint32_t(10));
|
|
for (uint32_t i = 4; i < limit; i++) {
|
|
uint8_t val = buffer[i];
|
|
result64 |= uint64_t(val & 0x7F) << (i * 7);
|
|
if ((val & 0x80) == 0) {
|
|
*consumed = i + 1;
|
|
return ProtoVarInt(result64);
|
|
}
|
|
}
|
|
return {};
|
|
}
|
|
#endif
|
|
|
|
uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size_t length, uint32_t target_field_id) {
|
|
uint32_t count = 0;
|
|
const uint8_t *ptr = buffer;
|
|
const uint8_t *end = buffer + length;
|
|
|
|
while (ptr < end) {
|
|
uint32_t consumed;
|
|
|
|
// Parse field header (tag)
|
|
auto res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
|
|
if (!res.has_value()) {
|
|
break; // Invalid data, stop counting
|
|
}
|
|
|
|
uint32_t tag = res->as_uint32();
|
|
uint32_t field_type = tag & WIRE_TYPE_MASK;
|
|
uint32_t field_id = tag >> 3;
|
|
ptr += consumed;
|
|
|
|
// Count if this is the target field
|
|
if (field_id == target_field_id) {
|
|
count++;
|
|
}
|
|
|
|
// Skip field data based on wire type
|
|
switch (field_type) {
|
|
case WIRE_TYPE_VARINT: { // VarInt - parse and skip
|
|
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
|
|
if (!res.has_value()) {
|
|
return count; // Invalid data, return what we have
|
|
}
|
|
ptr += consumed;
|
|
break;
|
|
}
|
|
case WIRE_TYPE_LENGTH_DELIMITED: { // Length-delimited - parse length and skip data
|
|
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
|
|
if (!res.has_value()) {
|
|
return count;
|
|
}
|
|
uint32_t field_length = res->as_uint32();
|
|
ptr += consumed;
|
|
if (field_length > static_cast<size_t>(end - ptr)) {
|
|
return count; // Out of bounds
|
|
}
|
|
ptr += field_length;
|
|
break;
|
|
}
|
|
case WIRE_TYPE_FIXED32: { // 32-bit - skip 4 bytes
|
|
if (end - ptr < 4) {
|
|
return count;
|
|
}
|
|
ptr += 4;
|
|
break;
|
|
}
|
|
default:
|
|
// Unknown wire type, can't continue
|
|
return count;
|
|
}
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
|
|
const uint8_t *ptr = buffer;
|
|
const uint8_t *end = buffer + length;
|
|
|
|
while (ptr < end) {
|
|
uint32_t consumed;
|
|
|
|
// Parse field header
|
|
auto res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
|
|
if (!res.has_value()) {
|
|
ESP_LOGV(TAG, "Invalid field start at offset %ld", (long) (ptr - buffer));
|
|
return;
|
|
}
|
|
|
|
uint32_t tag = res->as_uint32();
|
|
uint32_t field_type = tag & WIRE_TYPE_MASK;
|
|
uint32_t field_id = tag >> 3;
|
|
ptr += consumed;
|
|
|
|
switch (field_type) {
|
|
case WIRE_TYPE_VARINT: { // VarInt
|
|
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
|
|
if (!res.has_value()) {
|
|
ESP_LOGV(TAG, "Invalid VarInt at offset %ld", (long) (ptr - buffer));
|
|
return;
|
|
}
|
|
if (!this->decode_varint(field_id, *res)) {
|
|
ESP_LOGV(TAG, "Cannot decode VarInt field %" PRIu32 " with value %" PRIu32 "!", field_id, res->as_uint32());
|
|
}
|
|
ptr += consumed;
|
|
break;
|
|
}
|
|
case WIRE_TYPE_LENGTH_DELIMITED: { // Length-delimited
|
|
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
|
|
if (!res.has_value()) {
|
|
ESP_LOGV(TAG, "Invalid Length Delimited at offset %ld", (long) (ptr - buffer));
|
|
return;
|
|
}
|
|
uint32_t field_length = res->as_uint32();
|
|
ptr += 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 = encode_uint32(ptr[3], ptr[2], ptr[1], ptr[0]);
|
|
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 %u at offset %ld", field_type, (long) (ptr - buffer));
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace esphome::api
|