diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 0c340c55cc..e37b45fe71 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -213,8 +213,7 @@ bool ESP32BLE::ble_setup_() { if (this->name_.has_value()) { name = this->name_.value(); if (App.is_name_add_mac_suffix_enabled()) { - name += "-"; - name += get_mac_address().substr(6); + name = make_name_with_suffix(name, '-', get_mac_address().substr(6)); } } else { name = App.get_name(); diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 28043dd969..5f28d6db25 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -691,7 +691,7 @@ void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ std::string EthernetComponent::get_use_address() const { if (this->use_address_.empty()) { - return App.get_name() + ".local"; + return make_name_with_suffix(App.get_name(), '.', "local"); } return this->use_address_; } diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 7ab6efd1a1..3642ddb38e 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -29,7 +29,7 @@ static const char *const TAG = "mqtt"; MQTTClientComponent::MQTTClientComponent() { global_mqtt_client = this; - this->credentials_.client_id = App.get_name() + "-" + get_mac_address(); + this->credentials_.client_id = make_name_with_suffix(App.get_name(), '-', get_mac_address()); } // Connection diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 2e083d4c68..ec8687e927 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -267,7 +267,7 @@ network::IPAddress WiFiComponent::get_dns_address(int num) { } std::string WiFiComponent::get_use_address() const { if (this->use_address_.empty()) { - return App.get_name() + ".local"; + return make_name_with_suffix(App.get_name(), '.', "local"); } return this->use_address_; } diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 7aaba886e3..2746f574ba 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1195,6 +1195,13 @@ def validate_bytes(value): def hostname(value): + """Validate that the value is a valid hostname. + + Maximum length is 63 characters per RFC 1035. + + Note: If this limit is changed, update MAX_NAME_WITH_SUFFIX_SIZE in + esphome/core/application.h to accommodate the new maximum length. + """ value = string(value) if re.match(r"^[a-z0-9-]{1,63}$", value, re.IGNORECASE) is not None: return value diff --git a/esphome/core/application.h b/esphome/core/application.h index 1f22499051..47e902a191 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -103,8 +103,10 @@ class Application { this->name_add_mac_suffix_ = name_add_mac_suffix; if (name_add_mac_suffix) { const std::string mac_suffix = get_mac_address().substr(6); - this->name_ = name + "-" + mac_suffix; - this->friendly_name_ = friendly_name.empty() ? "" : friendly_name + " " + mac_suffix; + this->name_ = make_name_with_suffix(name, '-', mac_suffix); + if (!friendly_name.empty()) { + this->friendly_name_ = make_name_with_suffix(friendly_name, ' ', mac_suffix); + } } else { this->name_ = name; this->friendly_name_ = friendly_name; diff --git a/esphome/core/config.py b/esphome/core/config.py index 7bf7f82a8b..8a5876dbcf 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -200,7 +200,7 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.Required(CONF_NAME): cv.valid_name, - cv.Optional(CONF_FRIENDLY_NAME, ""): cv.string, + cv.Optional(CONF_FRIENDLY_NAME, ""): cv.All(cv.string, cv.Length(max=120)), cv.Optional(CONF_AREA): validate_area_config, cv.Optional(CONF_COMMENT): cv.string, cv.Required(CONF_BUILD_PATH): cv.string, diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index d4f6809776..82dbeddf22 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -235,6 +235,28 @@ std::string str_sprintf(const char *fmt, ...) { return str; } +// Maximum size for name with suffix: 120 (max friendly name) + 1 (separator) + 6 (MAC suffix) + 1 (null term) +static constexpr size_t MAX_NAME_WITH_SUFFIX_SIZE = 128; + +std::string make_name_with_suffix(const std::string &name, char sep, const std::string &suffix) { + char buffer[MAX_NAME_WITH_SUFFIX_SIZE]; + size_t name_len = name.size(); + size_t suffix_len = suffix.size(); + size_t total_len = name_len + 1 + suffix_len; + + // Silently truncate if needed: prioritize keeping the full suffix + if (total_len >= MAX_NAME_WITH_SUFFIX_SIZE) { + name_len = MAX_NAME_WITH_SUFFIX_SIZE - suffix_len - 2; // -2 for separator and null terminator + total_len = name_len + 1 + suffix_len; + } + + memcpy(buffer, name.c_str(), name_len); + buffer[name_len] = sep; + memcpy(buffer + name_len + 1, suffix.c_str(), suffix_len); + buffer[total_len] = '\0'; + return std::string(buffer, total_len); +} + // Parsing & formatting size_t parse_hex(const char *str, size_t length, uint8_t *data, size_t count) { diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index e06f2d15ef..f64c14aa85 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -306,6 +306,15 @@ std::string __attribute__((format(printf, 1, 3))) str_snprintf(const char *fmt, /// sprintf-like function returning std::string. std::string __attribute__((format(printf, 1, 2))) str_sprintf(const char *fmt, ...); +/// Concatenate a name with a separator and suffix using an efficient stack-based approach. +/// This avoids multiple heap allocations during string construction. +/// Maximum name length supported is 120 characters for friendly names. +/// @param name The base name string +/// @param sep The separator character (e.g., '-', ' ', or '.') +/// @param suffix The suffix to append (e.g., MAC address suffix or ".local") +/// @return The concatenated string: name + sep + suffix +std::string make_name_with_suffix(const std::string &name, char sep, const std::string &suffix); + ///@} /// @name Parsing & formatting