[core] Add cv.ByteLength validator, switch all proto-backed length checks to byte length

Add cv.ByteLength() validator that checks UTF-8 byte length instead
of character count. This ensures multibyte characters don't cause
the encoded string to exceed the proto max_data_length limit.

Switch all length validators that feed into proto fields with
max_data_length annotations to use byte length:
- Entity names, friendly names, area names, device names
- Icons, device classes, units of measurement
- Board names, project name/version
This commit is contained in:
J. Nick Koston
2026-04-06 18:33:04 -10:00
parent 52d76f8db0
commit cfcf9216f3
10 changed files with 49 additions and 24 deletions
+5 -5
View File
@@ -246,7 +246,7 @@ AREA_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_ID): cv.declare_id(Area),
cv.Required(CONF_NAME): cv.All(
cv.string_no_slash, cv.Length(max=FRIENDLY_NAME_MAX_LEN)
cv.string_no_slash, cv.ByteLength(max=FRIENDLY_NAME_MAX_LEN)
),
}
)
@@ -255,7 +255,7 @@ DEVICE_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_ID): cv.declare_id(Device),
cv.Required(CONF_NAME): cv.All(
cv.string_no_slash, cv.Length(max=FRIENDLY_NAME_MAX_LEN)
cv.string_no_slash, cv.ByteLength(max=FRIENDLY_NAME_MAX_LEN)
),
cv.Optional(CONF_AREA_ID): cv.use_id(Area),
}
@@ -272,7 +272,7 @@ CONFIG_SCHEMA = cv.All(
cv.Required(CONF_NAME): cv.valid_name,
# Keep max=120 in sync with OBJECT_ID_MAX_LEN in esphome/core/entity_base.h
cv.Optional(CONF_FRIENDLY_NAME, ""): cv.All(
cv.string_no_slash, cv.Length(max=FRIENDLY_NAME_MAX_LEN)
cv.string_no_slash, cv.ByteLength(max=FRIENDLY_NAME_MAX_LEN)
),
cv.Optional(CONF_AREA): validate_area_config,
cv.Optional(CONF_COMMENT): cv.All(cv.string, cv.Length(max=255)),
@@ -314,10 +314,10 @@ CONFIG_SCHEMA = cv.All(
cv.Required(CONF_NAME): cv.All(
cv.string_strict,
valid_project_name,
cv.Length(max=PROJECT_MAX_LENGTH),
cv.ByteLength(max=PROJECT_MAX_LENGTH),
),
cv.Required(CONF_VERSION): cv.All(
cv.string_strict, cv.Length(max=PROJECT_MAX_LENGTH)
cv.string_strict, cv.ByteLength(max=PROJECT_MAX_LENGTH)
),
cv.Optional(CONF_ON_UPDATE): automation.validate_automation(
{
+9 -6
View File
@@ -193,9 +193,10 @@ def _register_string(
def register_device_class(value: str) -> int:
"""Register a device_class string and return its 1-based index."""
if value and len(value) > DEVICE_CLASS_MAX_LENGTH:
byte_len = len(value.encode("utf-8")) if value else 0
if byte_len > DEVICE_CLASS_MAX_LENGTH:
raise ValueError(
f"Device class string too long ({len(value)} chars, max {DEVICE_CLASS_MAX_LENGTH}): '{value}'"
f"Device class string too long ({byte_len} bytes, max {DEVICE_CLASS_MAX_LENGTH}): '{value}'"
)
return _register_string(
value, _get_pool().device_classes, _MAX_DEVICE_CLASSES, "device_class"
@@ -204,9 +205,10 @@ def register_device_class(value: str) -> int:
def register_unit_of_measurement(value: str) -> int:
"""Register a unit_of_measurement string and return its 1-based index."""
if value and len(value) > UNIT_OF_MEASUREMENT_MAX_LENGTH:
byte_len = len(value.encode("utf-8")) if value else 0
if byte_len > UNIT_OF_MEASUREMENT_MAX_LENGTH:
raise ValueError(
f"Unit of measurement string too long ({len(value)} chars, "
f"Unit of measurement string too long ({byte_len} bytes, "
f"max {UNIT_OF_MEASUREMENT_MAX_LENGTH}): '{value}'"
)
return _register_string(value, _get_pool().units, _MAX_UNITS, "unit_of_measurement")
@@ -214,9 +216,10 @@ def register_unit_of_measurement(value: str) -> int:
def register_icon(value: str) -> int:
"""Register an icon string and return its 1-based index."""
if value and len(value) > ICON_MAX_LENGTH:
byte_len = len(value.encode("utf-8")) if value else 0
if byte_len > ICON_MAX_LENGTH:
raise ValueError(
f"Icon string too long ({len(value)} chars, max {ICON_MAX_LENGTH}): '{value}'"
f"Icon string too long ({byte_len} bytes, max {ICON_MAX_LENGTH}): '{value}'"
)
return _register_string(value, _get_pool().icons, _MAX_ICONS, "icon")