Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions hermes_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -11416,15 +11416,23 @@ def _render_distribution_plan(plan) -> None:
env_path = plan.target_dir / ".env"
if env_path.is_file():
try:
for raw in env_path.read_text().splitlines():
# .env is written as UTF-8 everywhere in the codebase,
# but Path.read_text() defaults to the system locale
# (cp1251/GBK on Windows), which raises UnicodeDecodeError
# on any non-ASCII byte. Read as utf-8-sig so a
# Notepad-added BOM doesn't corrupt the first key either.
for raw in env_path.read_text(encoding="utf-8-sig").splitlines():
line = raw.strip()
if not line or line.startswith("#"):
continue
key = line.split("=", 1)[0].strip()
if key == er.name:
already = True
break
except OSError:
except (OSError, UnicodeDecodeError):
# UnicodeDecodeError is a ValueError, not an OSError, so
# the old guard let a mis-encoded .env abort the whole
# install preview. Skip the pre-check instead.
pass
status = "✓ set" if already else ("needs setting" if er.required else "—")
line = f" • {er.name} ({tag}, {status})"
Expand Down
72 changes: 72 additions & 0 deletions tests/hermes_cli/test_profile_install_env_encoding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""Regression: the distribution-install preview must read .env as UTF-8.

`_render_distribution_plan` inspects the target profile's `.env` to decide
whether a required env var is already set, so it doesn't nag the user. It
used `Path.read_text()` with no encoding, which:

1. defaults to the system locale (cp1251/GBK on Windows) and raises
`UnicodeDecodeError` on any non-ASCII byte — and the surrounding
`except OSError` does NOT catch that (it's a `ValueError`), so a
mis-encoded .env aborts the whole install preview; and
2. even on a UTF-8 locale, a Notepad-added BOM prefixes the first key,
so the very first env var is mis-detected as "needs setting".

The fix reads `utf-8-sig` and also catches `UnicodeDecodeError`.
"""

from types import SimpleNamespace

import pytest

from hermes_cli.main import _render_distribution_plan


def _make_plan(target_dir, env_requires):
manifest = SimpleNamespace(
name="demo",
version="1.0.0",
description="",
author="",
hermes_requires="",
env_requires=env_requires,
)
return SimpleNamespace(
manifest=manifest,
provenance="local",
target_dir=target_dir,
existing=False,
has_cron=False,
)


def test_bom_prefixed_env_first_key_is_detected_as_set(tmp_path, monkeypatch, capsys):
"""A required key on the first line of a BOM-prefixed .env reads as set."""
monkeypatch.delenv("FOO_TOKEN", raising=False)
profile = tmp_path / "profile"
profile.mkdir()
# utf-8-sig write == UTF-8 with a leading BOM, exactly what Notepad emits.
(profile / ".env").write_text("FOO_TOKEN=abc\nBAR=1\n", encoding="utf-8-sig")

plan = _make_plan(profile, [SimpleNamespace(name="FOO_TOKEN", required=True, description="")])
_render_distribution_plan(plan)

out = capsys.readouterr().out
foo_line = next(ln for ln in out.splitlines() if "FOO_TOKEN" in ln)
assert "✓ set" in foo_line, f"BOM must not hide the first key: {foo_line!r}"


def test_non_utf8_env_does_not_abort_the_preview(tmp_path, monkeypatch, capsys):
"""A .env with invalid UTF-8 bytes must not crash the install preview."""
monkeypatch.delenv("FOO_TOKEN", raising=False)
profile = tmp_path / "profile"
profile.mkdir()
# \xff is an invalid UTF-8 start byte -> UnicodeDecodeError on read.
(profile / ".env").write_bytes(b"FOO_TOKEN=\xff\xfe bad\n")

plan = _make_plan(profile, [SimpleNamespace(name="FOO_TOKEN", required=True, description="")])

# Must return normally (old `except OSError` let UnicodeDecodeError escape).
_render_distribution_plan(plan)

out = capsys.readouterr().out
assert any("FOO_TOKEN" in ln for ln in out.splitlines())
Loading