fix(issue-26): review round 1 - no file key assigns bash's own variables; SKS_QUIESCENT_TRIES; long decimals
R1 (P1): a quoted export still ran code when the key was a bash special variable (RANDOM=a[$(cmd)] is evaluated arithmetically on assignment). envfile.RESERVED (bash's own variables, plus BASH_/COMP_/READLINE_ prefixes) is skipped with a line-numbered warning by both loaders; a test keeps it covering every variable a clean bash defines. The wrapper also hands SCRIPT_DIR/REPO_ROOT/TARGET_ENV to the parser as environment, so the file cannot replace the variables it has already set. R2 (P2): SKS_QUIESCENT_TRIES reached `for ((i < tries))` through a function argument; it is normalised at the call site (minimum 0). B5 now requires every integer-looking env default in the shell code to be normalised or listed in NOT_ARITHMETIC with where it goes, and exercises _pane_quiescent. R3 (P2): _positive_int strips leading zeros and rejects more than nine significant digits before int(), like mam_int; 5000-digit inputs no longer raise in the runner. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2826e709ae
commit
34241d7a65
6 files changed
+186
-16
No files matched your search
@@ -200,6 +200,8 @@ def test_a4_the_parser_command_line_carries_the_path_only():
|
||||
calls = [l for l in WRAPPER.read_text().splitlines() if "envfile.py" in l and not l.lstrip().startswith("#")]
|
||||
assert len(calls) == 1, calls
|
||||
assert re.search(r'envfile\.py" --shell "\$TARGET_ENV"\)"', calls[0]), calls[0]
|
||||
prefix = [l for l in WRAPPER.read_text().splitlines() if "_env_exports=" in l]
|
||||
assert prefix == [' _env_exports="$(SCRIPT_DIR="$SCRIPT_DIR" REPO_ROOT="$REPO_ROOT" TARGET_ENV="$TARGET_ENV" \\'], prefix
|
||||
src = WRAPPER.read_text()
|
||||
assert not re.search(r'^\s*(source|\.)\s+"?\$TARGET_ENV', src, re.M), "the env file must never be sourced"
|
||||
assert "set -a" not in src
|
||||
@@ -251,3 +253,67 @@ def test_a5_shell_mode_exit_code_is_zero_for_absent_and_present_files(tmp_path,
|
||||
env={k: v for k, v in os.environ.items() if k != "A"}, timeout=30)
|
||||
assert res.returncode == 0
|
||||
assert res.stdout == ("" if missing else "export A=1\n")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# A6 - bash's own variables are never assigned from the file (review R1)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# RANDOM/SECONDS/LINENO/OPTIND/SRANDOM are evaluated arithmetically on
|
||||
# assignment, so even a shlex-quoted value runs its $(...) in the wrapper.
|
||||
ARITH_SPECIALS = ["RANDOM", "SECONDS", "LINENO", "OPTIND", "SRANDOM"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("key", ARITH_SPECIALS)
|
||||
def test_a6_a_reserved_key_runs_nothing_in_the_real_wrapper(tmp_path, key):
|
||||
f = tmp_path / "special.env"
|
||||
f.write_text(f"{key}=BASH_VERSINFO[$(touch marker)0]\nSAFE=BASH_VERSINFO[$(touch marker2)0]\n")
|
||||
env = _env(tmp_path, f)
|
||||
env.pop(key, None)
|
||||
res = subprocess.run(["bash", str(WRAPPER), "--help"], capture_output=True, text=True, env=env,
|
||||
cwd=str(tmp_path), timeout=60)
|
||||
assert res.returncode == 0, res.stderr
|
||||
assert not (tmp_path / "marker").exists(), f"{key}= from the file ran shell"
|
||||
assert not (tmp_path / "marker2").exists()
|
||||
assert f"{f}:1: skipped (reserved shell variable {key})" in res.stderr, res.stderr
|
||||
assert "touch" not in res.stderr, "the warning must not echo the value"
|
||||
# the same value under an ordinary key stays literal (positive case)
|
||||
got = _wrapper_values(env, ["SAFE"], tmp_path)
|
||||
assert _split(got.stdout) == {"SAFE": "BASH_VERSINFO[$(touch marker2)0]"}
|
||||
assert not (tmp_path / "marker").exists() and not (tmp_path / "marker2").exists()
|
||||
|
||||
|
||||
def test_a6_both_loaders_skip_reserved_keys(tmp_path):
|
||||
f = tmp_path / "reserved.env"
|
||||
keys = ["BASH_ENV", "PS4", "IFS", "SHELLOPTS", "COMP_WORDBREAKS", "READLINE_LINE", "RANDOM"]
|
||||
f.write_text("".join(f"{k}=x\n" for k in keys) + "PLAIN=ok\n")
|
||||
env = {k: v for k, v in _env(tmp_path, f).items() if k not in keys}
|
||||
python = _python_values(env, keys + ["PLAIN"])
|
||||
assert python == {**{k: "<unset>" for k in keys}, "PLAIN": "ok"}
|
||||
pairs, warnings = envfile.parse(str(f))
|
||||
assert pairs == [("PLAIN", "ok")]
|
||||
assert [w.rsplit("(", 1)[1] for w in warnings] == [f"reserved shell variable {k})" for k in keys]
|
||||
res = subprocess.run([sys.executable, "-I", str(ENVFILE), "--shell", str(f)], capture_output=True,
|
||||
text=True, env=env, timeout=30)
|
||||
assert res.stdout == "export PLAIN=ok\n"
|
||||
|
||||
|
||||
def test_a6_the_reserved_set_covers_every_variable_a_clean_bash_defines():
|
||||
res = subprocess.run(["env", "-i", "bash", "--norc", "--noprofile", "-c", "compgen -v"],
|
||||
capture_output=True, text=True, check=True, timeout=30)
|
||||
ordinary = {"PATH", "TERM"} # environment, not shell state; .mam.env may set them as before
|
||||
uncovered = [v for v in res.stdout.split() if v not in ordinary and not envfile.is_reserved(v)]
|
||||
assert not uncovered, f"bash defines these but envfile.RESERVED misses them: {uncovered}"
|
||||
|
||||
|
||||
def test_a6_the_file_cannot_replace_the_wrappers_own_variables(tmp_path):
|
||||
f = tmp_path / "own.env"
|
||||
f.write_text(f"SCRIPT_DIR={tmp_path}/evil\nREPO_ROOT={tmp_path}/evil\nTARGET_ENV={tmp_path}/evil\n")
|
||||
(tmp_path / "evil").mkdir()
|
||||
(tmp_path / "evil" / "lib.sh").write_text("touch pwned-lib\n")
|
||||
res = subprocess.run(["bash", str(WRAPPER), "--help"], capture_output=True, text=True,
|
||||
env=_env(tmp_path, f), cwd=str(tmp_path), timeout=60)
|
||||
assert res.returncode == 0, res.stderr
|
||||
assert not (tmp_path / "pwned-lib").exists()
|
||||
got = _wrapper_values(_env(tmp_path, f), ["SCRIPT_DIR", "TARGET_ENV"], tmp_path)
|
||||
assert _split(got.stdout) == {"SCRIPT_DIR": str(WRAPPER.parent), "TARGET_ENV": str(f)}
|
||||
Reference in new issue
Block a user