From f8c91ae3fc5c0e2494f2b69c23247fef371aecbf Mon Sep 17 00:00:00 2001 From: Joshua King Date: Sun, 13 Sep 2026 18:24:53 -0400 Subject: [PATCH] fix: validate rollover time with ascii-bounded pattern --- custom_components/nestquest/options_flow.py | 14 ++++---------- tests/test_options_flow.py | 10 ++++++++++ 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/custom_components/nestquest/options_flow.py b/custom_components/nestquest/options_flow.py index d5c2b56..eeacafb 100644 --- a/custom_components/nestquest/options_flow.py +++ b/custom_components/nestquest/options_flow.py @@ -1,6 +1,7 @@ """Options flow for NestQuest post-setup settings.""" from __future__ import annotations +import re from typing import Any import voluptuous as vol @@ -18,20 +19,13 @@ from .const import ( MIN_HORIZON_DAYS = 1 MIN_PANEL_IDLE_TIMEOUT = 30 -_TIME_PART_LENGTH = 2 + +_TIME_PATTERN = re.compile(r"(?:[01]\d|2[0-3]):[0-5]\d") def _parse_time(value: str) -> bool: """Return True when value is a valid 24-hour HH:MM string.""" - parts = value.split(":") - if len(parts) != 2: - return False - hours, minutes = parts - if len(hours) != _TIME_PART_LENGTH or len(minutes) != _TIME_PART_LENGTH: - return False - if not hours.isdigit() or not minutes.isdigit(): - return False - return int(hours) <= 23 and int(minutes) <= 59 + return _TIME_PATTERN.fullmatch(value) is not None def _async_validate(user_input: dict[str, Any]) -> dict[str, str]: diff --git a/tests/test_options_flow.py b/tests/test_options_flow.py index f8ad99b..71d73c4 100644 --- a/tests/test_options_flow.py +++ b/tests/test_options_flow.py @@ -375,6 +375,16 @@ def test_options_flow_rejects_malformed_time_out_of_range() -> None: assert result["errors"] == {CONF_DAY_ROLLOVER_TIME: "invalid_time"} +def test_options_flow_rejects_pasted_unicode_superscript_time() -> None: + """Pasted Unicode digits like '²²:²²' yield a field-level error, no exception.""" + flow = _make_flow(_make_entry()) + result = _run( + flow.async_step_init({**VALID_INPUT, CONF_DAY_ROLLOVER_TIME: "²²:²²"}) + ) + assert result["type"] == "form" + assert result["errors"] == {CONF_DAY_ROLLOVER_TIME: "invalid_time"} + + @pytest.mark.parametrize("bad_timeout", ["abc", 0, -10, 10]) def test_options_flow_rejects_invalid_idle_timeout(bad_timeout) -> None: """Non-numeric or too-low idle timeouts yield a field-level error."""