Skip to content
Merged
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
66 changes: 63 additions & 3 deletions emrg/tools/bash_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -1969,10 +1969,15 @@ def is_separator(index: int) -> bool:
elif word in _COMPRESSOR_VERBS:
# `gzip f` rewrites f in place; the read spellings (`gzip -c f`,
# `gzip -t f`, `gzip -l f`) leave it alone and must stay allowed.
# This gate is why the family is not simply in the set above.
# This gate is why the family is not simply in the set above. A bare
# `-` operand is the family's stdin/stdout spelling and is dropped
# one operand at a time, because `gzip - f` really does compress `f`
# — see `_without_the_stream_operand`.
if not _compressor_operand_is_a_read(tokens, i):
targets.extend(
_positional_args(tokens, i, _COMPRESSOR_OPTIONS_WITH_VALUE)
_without_the_stream_operand(
_positional_args(tokens, i, _COMPRESSOR_OPTIONS_WITH_VALUE)
)
)
elif word in _LZ4_VERBS:
# `lz4 f` writes `f.lz4` beside the operand rather than rewriting it,
Expand Down Expand Up @@ -2214,6 +2219,46 @@ def _compressor_operand_is_a_read(tokens: list[str], i: int) -> bool:
return False


def _without_the_stream_operand(targets: list[str]) -> list[str]:
"""``targets`` without the bare ``-`` — the operand that is not a path.

A bare ``-`` is the convention for *standard input, standard output*: the
program reads the stream and writes the stream, so no file is opened under
that name and naming it turns a pure read into a refusal. This walk refuses
in the direction its own record treats as the costlier one — an empty target
list is a hole it can be argued out of, a refusal is a command a reader
cannot run — so the operand is dropped rather than guessed at.

Measured on this host 2026-09-19, one **fresh** directory per row with the
input present and the listing read back off disk afterwards:

gzip - xz - bzip2 - zstd - compress - lz4 - rc=0, no file created
gzip -9 - gzip -- - xz -9 - bzip2 -9 - rc=0, no file created
zstd -19 - lz4 -9 - rc=0, no file created
gzip -d - rc=1 (`unexpected end of
file`), no file created
gzip - f gzip f - xz - f zstd - f `f` IS compressed —
`gzip - f` writes
`f.gz` and removes `f`

The last row is why this is applied **per operand** and not per run, and why
`_compressor_operand_is_a_read` is left alone: that gate answers once for the
whole run, so teaching it the bare ``-`` would have made `gzip - f` name
nothing at all — a hole, in exchange for nothing, since the operand beside
the dash is a real path and must still be named. Dropping the token keeps
``['f']`` for that row and ``[]`` for `gzip -`.

The drop is deliberately not made in ``_positional_args``, where it would
reach every verb. A bare ``-`` is a *path* to the other writers, measured in
the same geometry: `touch -`, `truncate -s0 -`, `mv src.txt -` and
`cp src.txt -` each create the file named ``-`` (and `chmod 777 -` and `rm -`
look one up), so a global drop would open exactly the hole the everyday
writers are named to close. It is the compressor family's own spelling, and
it is dropped where that is measured.
"""
return [t for t in targets if t != "-"]


def _lz4_letters(args: list[str]) -> set[str]:
"""The short-option letters of an ``lz4`` run, read cluster by cluster.

Expand Down Expand Up @@ -2261,14 +2306,29 @@ def _lz4_write_targets(tokens: list[str], i: int) -> list[str]:
* otherwise the *last* operand is written — exactly the explicit destination
of `lz4 f out.lz4`, and for the single-operand form the operand itself,
whose directory is where the derived sibling lands.

A bare ``-`` operand is the stream, in either position, and is never named;
which operand it is takes the third bullet with it. Measured in one fresh
directory per row with only `f` present and the listing read back off disk
(2026-09-19): `lz4 -`, `lz4 - -`, `lz4 -t -` and **`lz4 f -`** all create no
file — the last one names stdout as its destination, so nothing is written
beside `f` either — while `lz4 -m f -` really does derive `f.lz4`, which is
why the multi-input branch drops the token and keeps `f`. See
`_without_the_stream_operand`.
"""
args = _args_after_command(tokens, i)
letters = _lz4_letters(args)
if letters & _LZ4_READ_LETTERS or any(t in _LZ4_READ_LONG for t in args):
return []
operands = _positional_args(tokens, i, _LZ4_OPTIONS_WITH_VALUE)
if letters & _LZ4_MULTI_LETTERS or any(t in _LZ4_MULTI_LONG for t in args):
return operands
return _without_the_stream_operand(operands)
if operands and operands[-1] == "-":
# `lz4 f -` names stdout as its destination instead of writing `f.lz4`
# beside the operand, so nothing on disk is written under *any* operand
# and the last-operand rule has to answer with nothing rather than with
# the operand it would otherwise fall back on (`f`, which it only reads).
return []
return operands[-1:]


Expand Down
54 changes: 54 additions & 0 deletions tests/test_bash_tool_compressor_enumeration.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,13 @@
# `-m` makes every operand an input, each deriving its own sibling.
("multiple inputs", f"lz4 -m {OUTSIDE}/f {OUTSIDE}/g",
(f"{OUTSIDE}/f", f"{OUTSIDE}/g")),
# …with one of the inputs a stream: measured on the host 2026-09-19 in a fresh
# directory holding only `f`, `lz4 -m f -` is rc=0 and **does** derive `f.lz4`,
# so the file must still be named while the bare `-` is not. Naming the token
# was the false block; dropping the whole run would have been the hole. The row
# carries that measured shape with an outside path, so both tiers have the file
# — rather than the stream token — to refuse.
("multiple inputs with a stream", f"lz4 -m - {OUTSIDE}/f", (f"{OUTSIDE}/f",)),
("recursive", f"lz4 -r {OUTSIDE}/f", (f"{OUTSIDE}/f",)),
("block size", f"lz4 -B4 {OUTSIDE}/f", (f"{OUTSIDE}/f",)),
("threads", f"lz4 -T4 {OUTSIDE}/f", (f"{OUTSIDE}/f",)),
Expand Down Expand Up @@ -109,6 +116,18 @@
# read, or "a value follows" would swallow real flags. Measured — `lz4 -Dcats
# -c f` is rc=0, 34 bytes on stdout and no file created.
("attached dictionary then -c", f"lz4 -D{OUTSIDE}/cats -c {OUTSIDE}/f"),
# A bare `-` is the stream, in either position. Measured on the host
# 2026-09-19, one fresh directory per row with only `f` present and the
# listing read back off disk: `lz4 -`, `lz4 -9 -` and `lz4 - -` are rc=0 and
# create no file, and `lz4 f -` is rc=0 with **nothing beside `f`** either —
# its last operand names stdout as the destination rather than deriving a
# sibling, so the last-operand rule has to answer with nothing instead of
# falling back on the operand it only reads. (`lz4 -t -` is rc=44 and creates
# nothing too, though it is spared by the read letters before this rule.)
("bare dash", "lz4 -"),
("bare dash with a level", "lz4 -9 -"),
("both operands the stream", "lz4 - -"),
("dash as the destination", "lz4 f -"),
)

# (row, command) — the compressors *not* on `_COMPRESSOR_VERBS` and not read by a
Expand Down Expand Up @@ -278,6 +297,41 @@ def test_the_multi_input_rule_is_what_spares_the_second_operand() -> None:
bash_tool._LZ4_MULTI_LETTERS, bash_tool._LZ4_MULTI_LONG = letters, longs


def test_the_stream_operand_is_what_spares_the_bare_dash_here_too() -> None:
"""The same drop as the family's, in this verb's multi-input branch.

With it removed that row names the bare `-` again and goes back to the
refusal master gave, i.e. the row is refused *because* the token is dropped.
The destination row (`lz4 f -`) is guarded by the last-operand branch's own
test rather than by the drop — the drop would leave `f` there, which is the
operand it only reads — so it is asserted in both states instead of flipped.
"""
drop = bash_tool._without_the_stream_operand
multi = "lz4 -m f -"
destination = "lz4 f -"

assert _extract_write_targets(multi) == ["f"]
assert _extract_write_targets(destination) == []
assert _check_sandbox(destination, "read-only", workdir="/workspace")[0] is True

try:
bash_tool._without_the_stream_operand = lambda targets: list(targets)
assert _check_sandbox(multi, "read-only", workdir="/workspace")[0] is False, (
"with the drop removed the stream operand must be named again — "
"otherwise the drop is not what spares it"
)
assert _extract_write_targets(destination) == [], (
"the destination row is not the drop's to spare: it must stay empty "
"with the drop removed too, or `f` would be named for a run that "
"writes to stdout"
)
finally:
bash_tool._without_the_stream_operand = drop

assert _extract_write_targets(multi) == ["f"]
assert _extract_write_targets(destination) == []


def test_the_attached_value_is_what_stops_the_letter_scan() -> None:
"""The stop is its own piece of the rule, so it is flipped on its own.

Expand Down
83 changes: 82 additions & 1 deletion tests/test_bash_tool_compressor_operands.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@
`zcat` idiom, so the letter is read inside a short cluster as well;
* the **`*cat` wrappers** (`zcat`, `bzcat`, `xzcat`, `zstdcat`) are not in the
family at all: they are `-dc` wrappers that write nothing, and a rule that
named their operand would refuse a pure read.
named their operand would refuse a pure read;
* the family's own **stream operand** — a bare `-` — is not a path, so it is
dropped from a write form one operand at a time, while the file beside it is
still named (`gzip - f` really compresses `f`; see
`test_the_stream_operand_is_what_spares_the_bare_dash`).

Nothing here executes a command. `_check_sandbox` is a pure predicate — it
`realpath`s a path and opens nothing — and `_extract_write_targets` only parses,
Expand Down Expand Up @@ -96,6 +100,14 @@
("uncompress", f"uncompress {OUTSIDE}/f.Z", (f"{OUTSIDE}/f.Z",)),
# Two operands: both are rewritten, so both are named.
("two operands", f"gzip {OUTSIDE}/a {OUTSIDE}/b", (f"{OUTSIDE}/a", f"{OUTSIDE}/b")),
# …and the drop below is **per operand**, not per run: measured on the host
# 2026-09-19 in a fresh directory holding only `f`, `gzip - f` is rc=0 and
# writes `f.gz` while removing `f`, so the file beside the stream must still
# be named. A run-level "this is a read form" answer for the bare `-` would
# have named nothing here, i.e. a hole in exchange for the false block. The
# row carries the measured shape with an outside path, so that both tiers have
# the file — rather than the stream token — to refuse.
("dash beside a file", f"gzip - {OUTSIDE}/f", (f"{OUTSIDE}/f",)),
)

# (row, command) — the spellings whose operand is a *read*. Each must name
Expand All @@ -118,6 +130,22 @@
# `compress` takes `-c` and only `-c` of the three letters.
("compress -c", f"compress -c {OUTSIDE}/f"),
("uncompress -c", f"uncompress -c {OUTSIDE}/f.Z"),
# A bare `-` is this family's own stdin/stdout spelling: the program reads the
# stream and writes the stream, so no file is opened under that name. Measured
# on the host 2026-09-19, one **fresh** directory per row with the input present
# and the listing read back off disk: every row below creates no file, and
# `gzip -- -` / `gzip -9 -` leave a file literally named `-` untouched as well.
# Before the drop each of them was refused — a false block, the direction this
# file's own record treats as the costlier one.
("bare dash", "gzip -"),
("bare dash with a level", "gzip -9 -"),
("bare dash after --", "gzip -- -"),
("bare dash, bzip2", "bzip2 -"),
("bare dash, xz", "xz -"),
("bare dash, zstd", "zstd -"),
("bare dash, compress", "compress -"),
("bare dash, uncompress", "uncompress -"),
("bare dash, decompress", "gzip -d -"),
)

# The wrappers, which are reads by construction and must not be in the family.
Expand Down Expand Up @@ -284,3 +312,56 @@ def test_the_read_gate_is_what_spares_the_read_forms() -> None:
)
finally:
bash_tool._compressor_operand_is_a_read = gate


def test_the_stream_operand_is_what_spares_the_bare_dash() -> None:
"""The drop is a second piece of code, so it gets its own arm.

Without it a bare `-` goes back to being named, and every row in the dash
block above returns to the refusal master gave. That is the whole claim: the
rows are refused *because* the token is dropped, not because something else
in the walk happens to spare them.
"""
drop = bash_tool._without_the_stream_operand
row = "gzip -"
beside = "gzip - f"

assert _extract_write_targets(row) == []
assert _check_sandbox(row, "read-only", workdir="/workspace")[0] is True
assert _check_sandbox(beside, "read-only", workdir="/workspace")[0] is False

try:
bash_tool._without_the_stream_operand = lambda targets: list(targets)
assert _check_sandbox(row, "read-only", workdir="/workspace")[0] is False, (
"with the drop removed the bare dash must return to the ALLOW master "
"gave — otherwise the drop is not what spares it"
)
# …and the row beside it must *not* move: it is refused for naming `f`,
# which is what makes the drop a per-operand rule rather than a per-run one.
assert _extract_write_targets(beside) == ["-", "f"]
finally:
bash_tool._without_the_stream_operand = drop

assert _extract_write_targets(row) == []
assert _extract_write_targets(beside) == ["f"]


def test_the_drop_is_the_compressors_alone() -> None:
"""The boundary the drop must not cross, measured in the same geometry.

A bare `-` is the compressors' stream operand. To the everyday writers it is
a **path**, and a file named ``-`` is exactly what they act on: measured on
this host 2026-09-19, one fresh directory per row with only `src.txt` present
(no dash file), `touch -`, `truncate -s0 -`, `mv src.txt -` and `cp src.txt -`
each *create* the file named ``-`` at rc=0, while `rm -` and `chmod 777 -` look
one up (rc=1, `-: No such file or directory`). Dropping the token inside
`_positional_args`, where every verb would inherit it, would therefore open
the hole those rows are named to close — this test pins the drop to the family
that measured it.
"""
for cmd in ("touch -", "truncate -s0 -", "mv src.txt -", "cp src.txt -"):
assert _extract_write_targets(cmd) == ["-"], cmd
assert _extract_write_targets(cmd) != [], (
f"{cmd}: an empty target list here is the everyday writers' hole"
)

Loading