Skip to content

Commit 8f8f5c3

Browse files
committed
fix(env): PR #13 round2 gemini 指摘対応
- io_export._validate_options: DEST='-' (stdout) と --passphrase-stdin の 排他チェックを削除。stdin (passphrase) と stdout (bundle) は別ストリーム のため衝突しない (例: `echo pass | devbase env export - --passphrase-stdin > out`) - io_common.read_passphrase: stdin から読んだ行末を rstrip('\n') から rstrip('\r\n') に変更。Windows/WSL 由来 CRLF パイプで末尾 \r が残ると age 復号が無音で失敗するため。 - tests: 排他テストを併用許可テストに置換 + CRLF rstrip 回帰テスト追加 Refs: #13 (review)
1 parent c4c4bca commit 8f8f5c3

3 files changed

Lines changed: 41 additions & 10 deletions

File tree

lib/devbase/env/io_common.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ def read_passphrase(
4646
line = sys.stdin.readline()
4747
if not line:
4848
raise error_class("stdin からパスフレーズを読み取れませんでした")
49-
return line.rstrip('\n')
49+
# CRLF (Windows/WSL からのパイプ) を考慮して \r も剥がす。
50+
# パスフレーズ末尾に \r が残ると複合化が一致せず原因不明の失敗になる。
51+
return line.rstrip('\r\n')
5052
return None
5153

5254

lib/devbase/env/io_export.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,10 @@ def _sensitive_keys(entries: Sequence[_bundle.BundleEntry]) -> List[str]:
7575

7676

7777
def _validate_options(opts: ExportOptions) -> None:
78-
if opts.passphrase_stdin and opts.dest == '-':
79-
raise ExportError(
80-
"DEST='-' (stdout) と --passphrase-stdin は併用できません "
81-
"(stdin/stdout が衝突します)"
82-
)
78+
# NOTE: DEST='-' (stdout) と --passphrase-stdin の併用は許可する。
79+
# export は stdin (passphrase) と stdout (bundle) で別ストリームを使うため
80+
# `echo "pass" | devbase env export - --passphrase-stdin > out` は適法。
81+
# (import 側は両方 stdin なので併用不可。io_import._validate_options 参照)
8382
if opts.passphrase_env and opts.passphrase_stdin:
8483
raise ExportError("--passphrase-env と --passphrase-stdin は併用できません")
8584

tests/cli/test_env_export.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from __future__ import annotations
44

55
import io
6-
import os
76
from pathlib import Path
87

98
import pyrage
@@ -82,9 +81,27 @@ def test_export_force_unencrypted_writes_plaintext_tar_gz(fake_root, tmp_path, c
8281
assert dest.stat().st_mode & 0o777 == 0o600
8382

8483

85-
def test_export_rejects_stdout_with_passphrase_stdin(fake_root):
86-
with pytest.raises(ExportError, match="DEST='-'"):
87-
export(fake_root, ExportOptions(dest="-", passphrase_stdin=True))
84+
def test_export_allows_stdout_with_passphrase_stdin(
85+
fake_root, age_keys, monkeypatch, capsysbinary
86+
):
87+
"""DEST='-' (stdout) と --passphrase-stdin の併用は許可される。
88+
89+
stdin (passphrase) と stdout (bundle) は別ストリームのため衝突しない:
90+
echo "pass" | devbase env export - --passphrase-stdin > out.dbenv
91+
"""
92+
fake_stdin = io.StringIO("hunter2\n")
93+
monkeypatch.setattr(fake_stdin, "isatty", lambda: False, raising=False)
94+
monkeypatch.setattr("sys.stdin", fake_stdin)
95+
96+
rc = export(fake_root, ExportOptions(dest="-", passphrase_stdin=True))
97+
assert rc == 0
98+
99+
out_bytes = capsysbinary.readouterr().out
100+
assert len(out_bytes) > 0
101+
# age 暗号化ヘッダ (passphrase mode) — `age-encryption.org/v1` を含む
102+
decrypted = cipher.decrypt(out_bytes, passphrase="hunter2")
103+
manifest, members = bundle.unpack(decrypted)
104+
assert "env/global.env" in members
88105

89106

90107
def test_export_rejects_both_passphrase_env_and_stdin(fake_root):
@@ -130,6 +147,19 @@ def fail_getpass(*args, **kwargs):
130147
assert "passphrase" not in capsys.readouterr().err
131148

132149

150+
def test_read_passphrase_strips_crlf_from_pipe(monkeypatch):
151+
"""Windows/WSL 由来の CRLF パイプ入力でも末尾 \\r が混入しないこと。
152+
153+
`\\r` が残ると age 復号は無音で失敗するため、対称的に `rstrip('\\r\\n')` が必要。
154+
"""
155+
fake_stdin = io.StringIO("hunter2\r\n")
156+
monkeypatch.setattr(fake_stdin, "isatty", lambda: False, raising=False)
157+
monkeypatch.setattr("sys.stdin", fake_stdin)
158+
159+
pw = _read_passphrase(ExportOptions(passphrase_stdin=True))
160+
assert pw == "hunter2"
161+
162+
133163
def test_read_passphrase_tty_eof_raises_export_error(monkeypatch):
134164
"""tty で getpass が EOFError を投げた場合は ExportError に変換される"""
135165
fake_stdin = io.StringIO("")

0 commit comments

Comments
 (0)