From f4724d77975fa4a141c1613084158efe4286fbc7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 8 Mar 2026 18:18:19 -1000 Subject: [PATCH] Split parse() into parse() and parse_non_empty() parse_non_empty() has no len==0 check (with debug assert) for callers that guarantee len >= 1 (e.g. after while (ptr < end)). parse() adds the len==0 guard and delegates to parse_non_empty() for callers where the buffer may be empty (e.g. value parse after tag advance in decode(), frame helper header parsing). Co-Authored-By: Claude Opus 4.6 --- .../components/api/api_frame_helper_plaintext.cpp | 3 ++- esphome/components/api/proto.cpp | 8 ++++---- esphome/components/api/proto.h | 12 ++++++++++-- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index 2fdcd87da5..2fd9196579 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -129,7 +129,8 @@ APIError APIPlaintextFrameHelper::try_read_frame_() { // Skip indicator byte at position 0 uint8_t varint_pos = 1; - auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos); + // rx_header_buf_pos_ >= 3 and varint_pos == 1, so len >= 2 + auto msg_size_varint = ProtoVarInt::parse_non_empty(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos); if (!msg_size_varint.has_value()) { // not enough data there yet continue; diff --git a/esphome/components/api/proto.cpp b/esphome/components/api/proto.cpp index 7e01185849..5dbeae7758 100644 --- a/esphome/components/api/proto.cpp +++ b/esphome/components/api/proto.cpp @@ -63,8 +63,8 @@ uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size const uint8_t *end = buffer + length; while (ptr < end) { - // Parse field header (tag) - auto res = ProtoVarInt::parse(ptr, end - ptr); + // Parse field header (tag) - ptr < end guarantees len >= 1 + auto res = ProtoVarInt::parse_non_empty(ptr, end - ptr); if (!res.has_value()) { break; // Invalid data, stop counting } @@ -208,8 +208,8 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) { const uint8_t *end = buffer + length; while (ptr < end) { - // Parse field header - auto res = ProtoVarInt::parse(ptr, end - ptr); + // Parse field header - ptr < end guarantees len >= 1 + auto res = ProtoVarInt::parse_non_empty(ptr, end - ptr); if (!res.has_value()) { ESP_LOGV(TAG, "Invalid field start at offset %ld", (long) (ptr - buffer)); return; diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index ebb0ecdd79..86746c949f 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -132,9 +132,9 @@ class ProtoVarInt { /// Parse a varint from buffer. Caller must ensure len >= 1. /// Returns result with consumed=0 on failure (truncated multi-byte varint). - static inline ProtoVarIntResult ESPHOME_ALWAYS_INLINE parse(const uint8_t *buffer, uint32_t len) { + static inline ProtoVarIntResult ESPHOME_ALWAYS_INLINE parse_non_empty(const uint8_t *buffer, uint32_t len) { #ifdef ESPHOME_DEBUG_API - assert(len > 0); // All callers guarantee len > 0 + assert(len > 0); #endif // Fast path: single-byte varints (0-127) are the most common case // (booleans, small enums, field tags, small message sizes/types). @@ -143,6 +143,14 @@ class ProtoVarInt { return parse_slow(buffer, len); } + /// Parse a varint from buffer (safe for empty buffers). + /// Returns result with consumed=0 on failure (empty buffer or truncated varint). + static inline ProtoVarIntResult ESPHOME_ALWAYS_INLINE parse(const uint8_t *buffer, uint32_t len) { + if (len == 0) + return {0, PROTO_VARINT_PARSE_FAILED}; + return parse_non_empty(buffer, len); + } + protected: // Slow path for multi-byte varints (>= 128), outlined to keep fast path small static ProtoVarIntResult parse_slow(const uint8_t *buffer, uint32_t len) __attribute__((noinline));