Compare commits

..
Author SHA1 Message Date
J. Nick Koston 5ac2fa8811 [api] Print the dropped action's service with its length
The field can now start as a null StringRef, so the log passes the size and an empty
literal instead of a pointer that may be null.
2026-09-08 04:42:58 +02:00
J. Nick Koston e715696bd7 [core] Bound StringRef's JSON conversion by the view length 2026-09-08 04:15:24 +02:00
J. Nick Koston 82d399400e [core] Convert a null StringRef to an empty JSON string and pin null against null 2026-09-08 03:44:24 +02:00
J. Nick Koston a7bf937001 [core] Keep StringRef::str() inline
The std::string range constructor reads nothing for a zero length, so the guard only
pushed str() out of line; the memcmp guards stay.
2026-09-08 03:22:05 +02:00
J. Nick Koston 63b5331e47 [core] Let StringRef carry a null pointer at zero length
The generated api messages start their encode only string fields that way. starts_with,
compare and str() no longer hand a null pointer to memcmp or the std::string range
constructor when there is nothing to compare or copy, the class comment states the
contract, and gtest cases pin every member on a null, empty view.
2026-09-08 03:14:44 +02:00
J. Nick Koston 4c72948575 [api] Say why the pointer buffer base keeps its constructor 2026-09-08 00:24:14 +02:00
J. Nick Koston 77d850cbe2 [api] Drop the unused array_size on the pointer buffer base 2026-09-08 00:07:54 +02:00
J. Nick Koston a97e4ebc1c [api] Pass needs_decode at both bytes buffer sites and guard the second unconditional copy path 2026-09-07 23:46:15 +02:00
J. Nick Koston d09f642eb2 [api] Name the null default invariant once and thread needs_decode through the pointer buffer base
The dead size parameter goes, the string type reads the inherited flag, the forced short
string path asserts against it, and the generated declarations say why the pointer may be
null.
2026-09-07 23:23:19 +02:00
J. Nick Koston 144cd419ad [api] Test which string fields get the null default 2026-09-07 23:11:47 +02:00
J. Nick Koston 7910c372cd [api] Default response only string fields to a null StringRef
A StringRef field that is only ever encoded is skipped when empty before its pointer is
read, and the dump helper checks empty() first, so pointing it at "" buys nothing while
costing one store per field in every message constructor. Fields that are decoded or force
encoded keep the empty string default.
2026-09-07 23:04:33 +02:00
6 changed files with 151 additions and 59 deletions
+47 -47
View File
@@ -377,7 +377,7 @@ class InfoResponseProtoMessage : public ProtoMessage {
StringRef name{};
bool disabled_by_default{false};
#ifdef USE_ENTITY_ICON
StringRef icon{};
StringRef icon{nullptr, 0}; // null until set, encode only
#endif
enums::EntityCategory entity_category{};
#ifdef USE_DEVICES
@@ -549,7 +549,7 @@ class DeviceInfo final : public ProtoMessage {
#ifdef USE_SERIAL_PROXY
class SerialProxyInfo final : public ProtoMessage {
public:
StringRef name{};
StringRef name{nullptr, 0}; // null until set, encode only
enums::SerialProxyPortType port_type{};
uint32_t configured_line_states{0};
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
@@ -744,7 +744,7 @@ class ListEntitiesBinarySensorResponse final : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("list_entities_binary_sensor_response"); }
#endif
StringRef device_class{};
StringRef device_class{nullptr, 0}; // null until set, encode only
bool is_status_binary_sensor{false};
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -791,7 +791,7 @@ class ListEntitiesCoverResponse final : public InfoResponseProtoMessage {
bool assumed_state{false};
bool supports_position{false};
bool supports_tilt{false};
StringRef device_class{};
StringRef device_class{nullptr, 0}; // null until set, encode only
bool supports_stop{false};
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -883,7 +883,7 @@ class FanStateResponse final : public StateResponseProtoMessage {
bool oscillating{false};
enums::FanDirection direction{};
int32_t speed_level{0};
StringRef preset_mode{};
StringRef preset_mode{nullptr, 0}; // null until set, encode only
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer PROTO_ENCODE_DEBUG_ARG);
@@ -963,7 +963,7 @@ class LightStateResponse final : public StateResponseProtoMessage {
float color_temperature{0.0f};
float cold_white{0.0f};
float warm_white{0.0f};
StringRef effect{};
StringRef effect{nullptr, 0}; // null until set, encode only
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer PROTO_ENCODE_DEBUG_ARG);
@@ -1025,10 +1025,10 @@ class ListEntitiesSensorResponse final : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("list_entities_sensor_response"); }
#endif
StringRef unit_of_measurement{};
StringRef unit_of_measurement{nullptr, 0}; // null until set, encode only
int32_t accuracy_decimals{0};
bool force_update{false};
StringRef device_class{};
StringRef device_class{nullptr, 0}; // null until set, encode only
enums::SensorStateClass state_class{};
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -1073,7 +1073,7 @@ class ListEntitiesSwitchResponse final : public InfoResponseProtoMessage {
const LogString *message_name() const override { return LOG_STR("list_entities_switch_response"); }
#endif
bool assumed_state{false};
StringRef device_class{};
StringRef device_class{nullptr, 0}; // null until set, encode only
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer PROTO_ENCODE_DEBUG_ARG);
@@ -1130,7 +1130,7 @@ class ListEntitiesTextSensorResponse final : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("list_entities_text_sensor_response"); }
#endif
StringRef device_class{};
StringRef device_class{nullptr, 0}; // null until set, encode only
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer PROTO_ENCODE_DEBUG_ARG);
@@ -1150,7 +1150,7 @@ class TextSensorStateResponse final : public StateResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("text_sensor_state_response"); }
#endif
StringRef state{};
StringRef state{nullptr, 0}; // null until set, encode only
bool missing_state{false};
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -1248,8 +1248,8 @@ class NoiseEncryptionSetKeyResponse final : public ProtoMessage {
#ifdef USE_API_HOMEASSISTANT_SERVICES
class HomeassistantServiceMap final : public ProtoMessage {
public:
StringRef key{};
StringRef value{};
StringRef key{nullptr, 0}; // null until set, encode only
StringRef value{nullptr, 0}; // null until set, encode only
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer PROTO_ENCODE_DEBUG_ARG);
@@ -1269,7 +1269,7 @@ class HomeassistantActionRequest final : public ProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("homeassistant_action_request"); }
#endif
StringRef service{};
StringRef service{nullptr, 0}; // null until set, encode only
FixedVector<HomeassistantServiceMap> data{};
FixedVector<HomeassistantServiceMap> data_template{};
FixedVector<HomeassistantServiceMap> variables{};
@@ -1281,7 +1281,7 @@ class HomeassistantActionRequest final : public ProtoMessage {
bool wants_response{false};
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
StringRef response_template{};
StringRef response_template{nullptr, 0}; // null until set, encode only
#endif
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -1327,8 +1327,8 @@ class SubscribeHomeAssistantStateResponse final : public ProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("subscribe_home_assistant_state_response"); }
#endif
StringRef entity_id{};
StringRef attribute{};
StringRef entity_id{nullptr, 0}; // null until set, encode only
StringRef attribute{nullptr, 0}; // null until set, encode only
bool once{false};
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -1421,13 +1421,13 @@ class GetTimeResponse final : public ProtoDecodableMessage {
#ifdef USE_API_USER_DEFINED_ACTIONS
class ListEntitiesServicesArgument final : public ProtoMessage {
public:
StringRef name{};
StringRef name{nullptr, 0}; // null until set, encode only
enums::ServiceArgType type{};
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
StringRef description{};
StringRef description{nullptr, 0}; // null until set, encode only
#endif
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
StringRef example{};
StringRef example{nullptr, 0}; // null until set, encode only
#endif
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -1448,12 +1448,12 @@ class ListEntitiesServicesResponse final : public ProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("list_entities_services_response"); }
#endif
StringRef name{};
StringRef name{nullptr, 0}; // null until set, encode only
uint32_t key{0};
FixedVector<ListEntitiesServicesArgument> args{};
enums::SupportsResponseType supports_response{};
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
StringRef description{};
StringRef description{nullptr, 0}; // null until set, encode only
#endif
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -1520,7 +1520,7 @@ class ExecuteServiceResponse final : public ProtoMessage {
#endif
uint32_t call_id{0};
bool success{false};
StringRef error_message{};
StringRef error_message{nullptr, 0}; // null until set, encode only
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
const uint8_t *response_data{nullptr};
uint16_t response_data_len{0};
@@ -1655,9 +1655,9 @@ class ClimateStateResponse final : public StateResponseProtoMessage {
enums::ClimateAction action{};
enums::ClimateFanMode fan_mode{};
enums::ClimateSwingMode swing_mode{};
StringRef custom_fan_mode{};
StringRef custom_fan_mode{nullptr, 0}; // null until set, encode only
enums::ClimatePreset preset{};
StringRef custom_preset{};
StringRef custom_preset{nullptr, 0}; // null until set, encode only
float current_humidity{0.0f};
float target_humidity{0.0f};
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
@@ -1790,9 +1790,9 @@ class ListEntitiesNumberResponse final : public InfoResponseProtoMessage {
float min_value{0.0f};
float max_value{0.0f};
float step{0.0f};
StringRef unit_of_measurement{};
StringRef unit_of_measurement{nullptr, 0}; // null until set, encode only
enums::NumberMode mode{};
StringRef device_class{};
StringRef device_class{nullptr, 0}; // null until set, encode only
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer PROTO_ENCODE_DEBUG_ARG);
@@ -1870,7 +1870,7 @@ class SelectStateResponse final : public StateResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("select_state_response"); }
#endif
StringRef state{};
StringRef state{nullptr, 0}; // null until set, encode only
bool missing_state{false};
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -1977,7 +1977,7 @@ class ListEntitiesLockResponse final : public InfoResponseProtoMessage {
bool assumed_state{false};
bool supports_open{false};
bool requires_code{false};
StringRef code_format{};
StringRef code_format{nullptr, 0}; // null until set, encode only
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer PROTO_ENCODE_DEBUG_ARG);
@@ -2036,7 +2036,7 @@ class ListEntitiesButtonResponse final : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("list_entities_button_response"); }
#endif
StringRef device_class{};
StringRef device_class{nullptr, 0}; // null until set, encode only
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer PROTO_ENCODE_DEBUG_ARG);
@@ -2067,7 +2067,7 @@ class ButtonCommandRequest final : public CommandProtoMessage {
#ifdef USE_MEDIA_PLAYER
class MediaPlayerSupportedFormat final : public ProtoMessage {
public:
StringRef format{};
StringRef format{nullptr, 0}; // null until set, encode only
uint32_t sample_rate{0};
uint32_t num_channels{0};
enums::MediaPlayerFormatPurpose purpose{};
@@ -2727,10 +2727,10 @@ class VoiceAssistantRequest final : public ProtoMessage {
const LogString *message_name() const override { return LOG_STR("voice_assistant_request"); }
#endif
bool start{false};
StringRef conversation_id{};
StringRef conversation_id{nullptr, 0}; // null until set, encode only
uint32_t flags{0};
VoiceAssistantAudioSettings audio_settings{};
StringRef wake_word_phrase{};
StringRef wake_word_phrase{nullptr, 0}; // null until set, encode only
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer PROTO_ENCODE_DEBUG_ARG);
@@ -2871,8 +2871,8 @@ class VoiceAssistantAnnounceFinished final : public ProtoMessage {
};
class VoiceAssistantWakeWord final : public ProtoMessage {
public:
StringRef id{};
StringRef wake_word{};
StringRef id{nullptr, 0}; // null until set, encode only
StringRef wake_word{nullptr, 0}; // null until set, encode only
std::vector<std::string> trained_languages{};
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -3025,7 +3025,7 @@ class ListEntitiesTextResponse final : public InfoResponseProtoMessage {
#endif
uint32_t min_length{0};
uint32_t max_length{0};
StringRef pattern{};
StringRef pattern{nullptr, 0}; // null until set, encode only
enums::TextMode mode{};
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -3046,7 +3046,7 @@ class TextStateResponse final : public StateResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("text_state_response"); }
#endif
StringRef state{};
StringRef state{nullptr, 0}; // null until set, encode only
bool missing_state{false};
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -3206,7 +3206,7 @@ class ListEntitiesEventResponse final : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("list_entities_event_response"); }
#endif
StringRef device_class{};
StringRef device_class{nullptr, 0}; // null until set, encode only
const FixedVector<const char *> *event_types{};
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -3227,7 +3227,7 @@ class EventResponse final : public StateResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("event_response"); }
#endif
StringRef event_type{};
StringRef event_type{nullptr, 0}; // null until set, encode only
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer PROTO_ENCODE_DEBUG_ARG);
@@ -3249,7 +3249,7 @@ class ListEntitiesValveResponse final : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("list_entities_valve_response"); }
#endif
StringRef device_class{};
StringRef device_class{nullptr, 0}; // null until set, encode only
bool assumed_state{false};
bool supports_position{false};
bool supports_stop{false};
@@ -3369,7 +3369,7 @@ class ListEntitiesUpdateResponse final : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("list_entities_update_response"); }
#endif
StringRef device_class{};
StringRef device_class{nullptr, 0}; // null until set, encode only
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer PROTO_ENCODE_DEBUG_ARG);
@@ -3393,11 +3393,11 @@ class UpdateStateResponse final : public StateResponseProtoMessage {
bool in_progress{false};
bool has_progress{false};
float progress{0.0f};
StringRef current_version{};
StringRef latest_version{};
StringRef title{};
StringRef release_summary{};
StringRef release_url{};
StringRef current_version{nullptr, 0}; // null until set, encode only
StringRef latest_version{nullptr, 0}; // null until set, encode only
StringRef title{nullptr, 0}; // null until set, encode only
StringRef release_summary{nullptr, 0}; // null until set, encode only
StringRef release_url{nullptr, 0}; // null until set, encode only
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer PROTO_ENCODE_DEBUG_ARG);
@@ -3735,7 +3735,7 @@ class SerialProxyRequestResponse final : public ProtoMessage {
uint32_t instance{0};
enums::SerialProxyRequestType type{};
enums::SerialProxyStatus status{};
StringRef error_message{};
StringRef error_message{nullptr, 0}; // null until set, encode only
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer PROTO_ENCODE_DEBUG_ARG);
+3 -2
View File
@@ -433,8 +433,9 @@ void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call
// Home Assistant subscribes to actions shortly *after* authenticating, so actions
// fired right at connection time (on_client_connected, on_time_sync, ...) can
// arrive before the subscription and are lost - warn instead of failing silently.
ESP_LOGW(TAG, "Home Assistant %s '%s' dropped; %s",
call.is_event ? LOG_STR_LITERAL("event") : LOG_STR_LITERAL("action"), call.service.c_str(),
ESP_LOGW(TAG, "Home Assistant %s '%.*s' dropped; %s",
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)")
: LOG_STR_LITERAL("no client connected"));
}
+16 -4
View File
@@ -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
* 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>
*
* 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 {
public:
@@ -78,7 +82,7 @@ class StringRef {
/// True if the view begins with the given prefix (std::string::starts_with-like)
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 std::string &prefix) const { return this->starts_with(StringRef(prefix)); }
@@ -92,14 +96,15 @@ class StringRef {
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_); }
operator std::string() const { return str(); }
/// Compare (compatible with std::string::compare)
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)
return result;
if (len_ < other.len_)
@@ -258,7 +263,14 @@ inline double stod(const StringRef &str, size_t *pos = nullptr) {
#ifdef USE_JSON
// 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
} // namespace esphome
+24 -6
View File
@@ -566,17 +566,17 @@ def create_field_type_info(
# For messages that decode (SOURCE_CLIENT or SOURCE_BOTH), use pointer
# for zero-copy access to the receive buffer
if needs_decode:
return PointerToBytesBufferType(field, None)
return PointerToBytesBufferType(field, needs_decode)
# For SOURCE_SERVER (encode only), explicit annotation is still needed
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)
# Special handling for string fields - use StringRef for zero-copy
if field.type == 9:
return PointerToStringBufferType(field, None)
return PointerToStringBufferType(field, needs_decode)
validate_field_type(field.type, field.name)
if field.type == 11:
@@ -1134,11 +1134,12 @@ class PointerToBufferTypeBase(TypeInfo):
def can_use_dump_field(cls) -> bool:
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__(
self, field: descriptor.FieldDescriptorProto, size: int | None = None
self, field: descriptor.FieldDescriptorProto, needs_decode: bool
) -> None:
super().__init__(field)
self.array_size = 0
super().__init__(field, needs_decode)
@property
def wire_type(self) -> WireType:
@@ -1219,14 +1220,28 @@ class PointerToStringBufferType(PointerToBufferTypeBase):
def can_use_dump_field(cls) -> bool:
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
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}{{}};"]
@property
def encode_content(self) -> str:
max_len = self.max_data_length
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()
if tag < 128:
return _encode_call(
@@ -1236,6 +1251,9 @@ class PointerToStringBufferType(PointerToBufferTypeBase):
f"this->{self.field_name}.c_str()",
f"this->{self.field_name}.size()",
):
assert not self._starts_null, (
"unconditional copy of a field that may start null"
)
return result
return _encode_call(
"encode_string",
+43
View File
@@ -59,4 +59,47 @@ TEST(StringRefStartsWith, RefOverloadComparesOnlyTheViewedLength) {
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
@@ -199,6 +199,24 @@ def _decode_case(field_type: int, number: int, *, repeated: bool = False) -> str
).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"),
[