diff --git a/esphome/ota_key_edit.py b/esphome/ota_key_edit.py index 8465ee05b62..5dcdf07c0d3 100644 --- a/esphome/ota_key_edit.py +++ b/esphome/ota_key_edit.py @@ -67,7 +67,6 @@ _INDENT_RE = re.compile(r"\s*") _KEY_FIELD_RE = re.compile(rf"^\s*{CONF_KEY}\s*:") _BARE_BLOCK_RE = re.compile(rf"^\s*{CONF_ENCRYPTION}\s*:\s*(#.*)?$") _YAML_SUFFIXES = (".yaml", ".yml") -_COMMENT_RE = re.compile(r"(?:^|\s)#.*$") _INCLUDE_DIR_RE = re.compile(r"!include_dir_\w+\s+(?P[^\s#]+)") # An include or package file whose path holds a substitution _SUBSTITUTED_INCLUDE_RE = re.compile( @@ -100,8 +99,18 @@ def _indent(text: str) -> str: def _without_comment(text: str) -> str: - """The line up to its comment, which starts at a `#` after whitespace.""" - return _COMMENT_RE.sub("", text) + """The line up to its comment: a `#` after whitespace that is not inside + a quoted scalar, so `!include "shared # ota.yaml"` keeps its name.""" + quote = "" + for i, char in enumerate(text): + if quote: + if char == quote: + quote = "" + elif char in "\"'": + quote = char + elif char == "#" and (i == 0 or text[i - 1].isspace()): + return text[:i] + return text def _file_use_re(file_name: str) -> re.Pattern[str]: diff --git a/tests/unit_tests/test_ota_key_edit.py b/tests/unit_tests/test_ota_key_edit.py index 74022a9de8f..3a4845b44f8 100644 --- a/tests/unit_tests/test_ota_key_edit.py +++ b/tests/unit_tests/test_ota_key_edit.py @@ -729,6 +729,28 @@ def test_shared_secret_scan_covers_yml_and_skips_unreadable_files( assert [e.shared_with for e in edits] == [[tmp_path.resolve() / "other.yml"]] +def test_shared_include_with_a_hash_in_its_quoted_name_is_reported( + tmp_path: Path, +) -> None: + """A `#` inside a quoted include path is part of the name, not a comment.""" + (tmp_path / "shared # ota.yaml").write_bytes( + f'encryption:\n key: "{OLD_KEY}"\n'.encode() + ) + (tmp_path / "other.yaml").write_bytes( + b'api: !include "shared # ota.yaml" # the device next door\n' + ) + _setup( + tmp_path, + "esphome:\n name: test\n\napi: !include 'shared # ota.yaml'\n\n" + "ota:\n - platform: esphome\n encryption:\n", + ) + edits = locate_key_edits(OLD_KEY, NEW_KEY) + with_sharers(edits) + assert [(e.path.name, e.shared_with) for e in edits] == [ + ("shared # ota.yaml", [tmp_path.resolve() / "other.yaml"]) + ] + + def test_literal_key_in_a_shared_include_is_reported(tmp_path: Path) -> None: """Another configuration including the file that holds the key is listed, like a shared secret."""