mirror of
https://github.com/esphome/esphome.git
synced 2026-08-31 10:06:03 +00:00
[api] Enforce the 16383 message ID cap with a generated constant and static_assert
This commit is contained in:
@@ -326,8 +326,10 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
bool is_marked_for_removal() const { return this->flags_.remove; }
|
||||
uint8_t get_log_subscription_level() const { return this->flags_.log_subscription; }
|
||||
|
||||
// Get client API version for feature detection
|
||||
bool client_supports_api_version(uint16_t major, uint16_t minor) const {
|
||||
// Get client API version for feature detection.
|
||||
// Stored versions saturate at 255 (see send_hello_response_), so requesting
|
||||
// a minimum above that can never match.
|
||||
bool client_supports_api_version(uint8_t major, uint8_t minor) const {
|
||||
return this->client_api_version_major_ > major ||
|
||||
(this->client_api_version_major_ == major && this->client_api_version_minor_ >= minor);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "api_pb2.h"
|
||||
#include "proto.h"
|
||||
#include <cstring>
|
||||
#include <cinttypes>
|
||||
@@ -252,6 +253,13 @@ ESPHOME_ALWAYS_INLINE static inline void encode_varint_16(uint16_t value, uint8_
|
||||
*p = static_cast<uint8_t>(value);
|
||||
}
|
||||
|
||||
// The generator rejects message IDs above MAX_MESSAGE_TYPE, so the type varint
|
||||
// can never outgrow the 2 bytes HEADER_PADDING budgets for it. Without this
|
||||
// bound, write_plaintext_header's header_offset would underflow for the first
|
||||
// message in a batch and the header write would land outside the buffer.
|
||||
static_assert(1 + 3 + ProtoSize::varint16(MAX_MESSAGE_TYPE) <= APIPlaintextFrameHelper::HEADER_PADDING,
|
||||
"HEADER_PADDING cannot fit the type varint of the largest message ID");
|
||||
|
||||
// Write plaintext header into pre-allocated padding before payload.
|
||||
// padding_size: bytes reserved before payload (HEADER_PADDING for first/single msg,
|
||||
// actual header size for contiguous batch messages).
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
// Upper bound on message IDs, enforced by the code generator: the plaintext
|
||||
// frame header budgets 2 varint bytes for the type (HEADER_PADDING).
|
||||
static constexpr uint16_t MAX_MESSAGE_TYPE = 16383;
|
||||
|
||||
namespace enums {
|
||||
|
||||
enum DisconnectReason : uint32_t {
|
||||
|
||||
@@ -475,6 +475,19 @@ TYPE_INFO: dict[int, TypeInfo] = {}
|
||||
# TYPE_DOUBLE = 1, TYPE_FIXED64 = 6, TYPE_SFIXED64 = 16, TYPE_SINT64 = 18
|
||||
UNSUPPORTED_TYPES = {1: "double", 6: "fixed64", 16: "sfixed64", 18: "sint64"}
|
||||
|
||||
# The plaintext frame header budgets 2 varint bytes for the message type
|
||||
# (APIPlaintextFrameHelper::HEADER_PADDING), which caps message IDs at 16383.
|
||||
MAX_MESSAGE_ID = 16383
|
||||
|
||||
|
||||
def validate_message_id(message_id: int, message_name: str) -> None:
|
||||
"""Reject message IDs whose plaintext type varint would not fit in 2 bytes."""
|
||||
if message_id > MAX_MESSAGE_ID:
|
||||
raise ValueError(
|
||||
f"Message ID {message_id} for {message_name} exceeds the plaintext "
|
||||
f"2-byte type varint maximum ({MAX_MESSAGE_ID})"
|
||||
)
|
||||
|
||||
|
||||
def validate_field_type(field_type: int, field_name: str = "") -> None:
|
||||
"""Validate that the field type is supported by ESPHome API.
|
||||
@@ -2511,13 +2524,7 @@ def build_message_type(
|
||||
|
||||
# Add MESSAGE_TYPE method if this is a service message
|
||||
if message_id is not None:
|
||||
# The plaintext frame header budgets 2 varint bytes for the type
|
||||
# (HEADER_PADDING), which caps message IDs at 16383
|
||||
if message_id > 16383:
|
||||
raise ValueError(
|
||||
f"Message ID {message_id} for {desc.name} exceeds the plaintext "
|
||||
"2-byte type varint maximum (16383)"
|
||||
)
|
||||
validate_message_id(message_id, desc.name)
|
||||
|
||||
# Add static constexpr for message type
|
||||
public_content.append(f"static constexpr uint16_t MESSAGE_TYPE = {message_id};")
|
||||
@@ -3176,8 +3183,12 @@ def main() -> None:
|
||||
#include "api_pb2_includes.h"
|
||||
"""
|
||||
|
||||
content += """
|
||||
namespace esphome::api {
|
||||
content += f"""
|
||||
namespace esphome::api {{
|
||||
|
||||
// Upper bound on message IDs, enforced by the code generator: the plaintext
|
||||
// frame header budgets 2 varint bytes for the type (HEADER_PADDING).
|
||||
static constexpr uint16_t MAX_MESSAGE_TYPE = {MAX_MESSAGE_ID};
|
||||
|
||||
"""
|
||||
|
||||
|
||||
@@ -15,7 +15,12 @@ import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parents[4] / "script" / "api_protobuf"))
|
||||
|
||||
from api_protobuf import _make_ifdef_line, get_varint64_ifdef # noqa: E402
|
||||
from api_protobuf import ( # noqa: E402
|
||||
MAX_MESSAGE_ID,
|
||||
_make_ifdef_line,
|
||||
get_varint64_ifdef,
|
||||
validate_message_id,
|
||||
)
|
||||
from google.protobuf import descriptor_pb2 # noqa: E402
|
||||
|
||||
|
||||
@@ -91,3 +96,14 @@ def test_make_ifdef_line_conjunction_and_negation() -> None:
|
||||
assert (
|
||||
_make_ifdef_line("USE_X && !USE_Y") == "#if defined(USE_X) && !defined(USE_Y)"
|
||||
)
|
||||
|
||||
|
||||
def test_message_id_at_maximum_is_accepted() -> None:
|
||||
# 16383 is the largest ID whose plaintext type varint fits the 2 bytes
|
||||
# budgeted in HEADER_PADDING.
|
||||
validate_message_id(MAX_MESSAGE_ID, "MaxMessage")
|
||||
|
||||
|
||||
def test_message_id_above_maximum_is_rejected() -> None:
|
||||
with pytest.raises(ValueError, match="exceeds the plaintext"):
|
||||
validate_message_id(MAX_MESSAGE_ID + 1, "TooBigMessage")
|
||||
|
||||
Reference in New Issue
Block a user