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 <noreply@anthropic.com>
This commit is contained in:
J. Nick Koston
2026-03-08 18:18:19 -10:00
parent 7185c66779
commit f4724d7797
3 changed files with 16 additions and 7 deletions
@@ -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;
+4 -4
View File
@@ -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;
+10 -2
View File
@@ -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));