fix(cli): warn once per path for UTF-32 .env refuse-to-mangle
Hot-reload and multi-entry load_hermes_dotenv can hit the same UTF-32 file repeatedly; gate the refuse-to-mangle warning on a module-level seen-set (house style: _WARNED_KEYS sibling) so logs are not spammed.
This commit is contained in:
parent
7d597cc5d4
commit
90d3ba5be9
2 changed files with 44 additions and 6 deletions
|
|
@ -23,6 +23,12 @@ _CREDENTIAL_SUFFIXES = ("_API_KEY", "_TOKEN", "_SECRET", "_KEY")
|
|||
# tests) don't spam the same warning multiple times.
|
||||
_WARNED_KEYS: set[str] = set()
|
||||
|
||||
# Paths we've already emitted a UTF-32 refuse-to-mangle warning for.
|
||||
# load_hermes_dotenv can call _sanitize_env_file_if_needed multiple times
|
||||
# for the same file (user env + project env + hot-reload); once per path
|
||||
# is enough.
|
||||
_WARNED_UTF32_PATHS: set[str] = set()
|
||||
|
||||
# Map of env-var name → source label ("bitwarden", etc.) for credentials
|
||||
# that were injected by an external secret source during load_hermes_dotenv().
|
||||
# Used by setup / `hermes model` flows to label detected credentials so
|
||||
|
|
@ -208,13 +214,16 @@ def _sanitize_env_file_if_needed(path: Path) -> None:
|
|||
if raw.startswith(codecs.BOM_UTF32_LE) or raw.startswith(codecs.BOM_UTF32_BE):
|
||||
# Lazy import keeps the module import block identical to #65124's
|
||||
# codecs/io additions so the two PRs auto-merge either order.
|
||||
import logging
|
||||
path_key = str(path.resolve())
|
||||
if path_key not in _WARNED_UTF32_PATHS:
|
||||
_WARNED_UTF32_PATHS.add(path_key)
|
||||
import logging
|
||||
|
||||
logging.getLogger(__name__).warning(
|
||||
"Skipping .env sanitize for %s: UTF-32 BOM detected; "
|
||||
"leaving file untouched to avoid corruption",
|
||||
path,
|
||||
)
|
||||
logging.getLogger(__name__).warning(
|
||||
"Skipping .env sanitize for %s: UTF-32 BOM detected; "
|
||||
"leaving file untouched to avoid corruption",
|
||||
path,
|
||||
)
|
||||
return
|
||||
if raw.startswith(codecs.BOM_UTF16_LE) or raw.startswith(codecs.BOM_UTF16_BE):
|
||||
# "utf-16" uses the BOM to select endianness and strips it.
|
||||
|
|
|
|||
|
|
@ -276,6 +276,35 @@ def test_utf32_be_bom_leaves_file_untouched(tmp_path, caplog):
|
|||
assert any("UTF-32" in r.message for r in caplog.records)
|
||||
|
||||
|
||||
def test_utf32_warning_fires_once_per_path(tmp_path, caplog, monkeypatch):
|
||||
"""Three sanitize calls on the same UTF-32 file → exactly one warning.
|
||||
|
||||
Matches house style for warn-once (module-level seen-set, same class as
|
||||
``_WARNED_KEYS``): hot-reload / multi-entry load must not spam logs.
|
||||
"""
|
||||
import logging
|
||||
|
||||
import hermes_cli.env_loader as env_loader
|
||||
from hermes_cli.env_loader import _sanitize_env_file_if_needed
|
||||
|
||||
# Isolate process-level seen-set so other tests' paths don't leak in.
|
||||
monkeypatch.setattr(env_loader, "_WARNED_UTF32_PATHS", set())
|
||||
|
||||
env_file = tmp_path / ".env"
|
||||
content = "HERMES_TEST_KEY=hello_utf32\nSECOND_KEY=world\n"
|
||||
raw = codecs.BOM_UTF32_LE + content.encode("utf-32-le")
|
||||
env_file.write_bytes(raw)
|
||||
|
||||
with caplog.at_level(logging.WARNING, logger="hermes_cli.env_loader"):
|
||||
_sanitize_env_file_if_needed(env_file)
|
||||
_sanitize_env_file_if_needed(env_file)
|
||||
_sanitize_env_file_if_needed(env_file)
|
||||
|
||||
utf32_warnings = [r for r in caplog.records if "UTF-32" in r.message]
|
||||
assert len(utf32_warnings) == 1
|
||||
assert env_file.read_bytes() == raw
|
||||
|
||||
|
||||
def test_leading_replacement_char_does_not_rewrite(tmp_path):
|
||||
"""errors=replace FFFD-on-first-line guard: do not persist mangling.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue