rusty-sponge 0.2.0

Soak up stdin and write it atomically to a file — a Rust port of moreutils `sponge` with strict-compat mode, configurable spill-to-tempfile, and a typed library API.
Documentation
# Fixtures — moreutils `sponge` byte-equality reference

This directory holds the moreutils `sponge` reference fixtures used by the
snapshot tests under `tests/compat_default.rs` and `tests/compat_strict.rs`.
The fixture-capture protocol mirrors the one promoted to the umbrella SAD
during the `rusty-ts` (00001) feature.

## Capture protocol (mandatory)

All fixtures MUST be captured under a pinned environment:

| Variable | Value | Why |
|----------|-------|-----|
| `LC_ALL` | `C.UTF-8` | Locale stability; sponge does not emit timestamps or locale-sensitive output, but pinning prevents future drift if moreutils ever adds locale-dependent error text. |
| moreutils source | `madx/moreutils@<COMMIT_HASH>` — pinned at capture time | `sponge.c` evolves; byte-equal fidelity is meaningless without a pinned reference. |
| Capture platform | WSL2 Ubuntu 24.04 LTS | Matches the Linux CI runner toolchain; `apt install moreutils` provides a vetted distro build of `sponge`. |

The capture script `scripts/capture-sponge.sh` asserts these values at
startup and refuses to run otherwise (HINT-001 lesson: fixtures captured
under wrong env corrupt byte-equality assertions silently).

## Layout

```
fixtures/
├── README.md                  # this file
├── inputs/                    # input bytes piped to moreutils sponge
│   ├── happy_path/
│   ├── existing_target/
│   ├── missing_target/
│   ├── append_existing/
│   ├── append_missing/
│   ├── symlink_target/        # Unix-only
│   └── ...
├── moreutils_outputs/         # captured target-file bytes after sponge runs
│   ├── happy_path/
│   ├── existing_target/
│   ├── missing_target/
│   └── ...
└── scripts/
    └── capture-sponge.sh      # the capture driver
```

## Recapture policy

**Do not regenerate inputs and outputs in the same commit.** Per HINT-001
(promoted from `rusty-ts` iter-5 lessons): if you change an input, the
moreutils output drift is no longer being verified — you're just asserting
that today's run equals today's run. Capture inputs once and freeze them;
regenerate outputs only when the pinned upstream commit advances.

## Pinned upstream version

**moreutils package**: `0.69-1` (Ubuntu 24.04 LTS distro build, captured 2026-05-22 via WSL2 Ubuntu).

The upstream `madx/moreutils` repository tags do not perfectly match Debian package versions; the captured bytes are the source of truth for byte-equality. Re-capture is only triggered when the Ubuntu package advances and the team has decided to bump the pin.

## Captured fixture categories (v0.1.0 baseline)

| Category | Inputs | Outputs | Notes |
|---|---|---|---|
| `happy_path/` | 4 | 4 | empty / small text / binary bytes / 1 MiB ascii |
| `existing_target/` | 2 | 1 | preexisting target + replacement stdin |
| `missing_target/` | 1 | 1 | target created from scratch |
| `append_existing/` | 2 | 1 | `-a` mode prepending existing file contents |
| `append_missing/` | 1 | 1 | `-a` mode against non-existent target |
| `stdout_passthrough/` | 3 | 3 | no file argument → write to stdout |
| `symlink_target/` | 2 | 2 | Unix-only: symlink + linked-file content |
| `strict_h/` | 0 | 3 | `sponge -h` → usage banner to **stdout**, exit 0 |
| `strict_help/` | 0 | 3 | `sponge --help` → getopt error + usage, exit 0 |
| `strict_unknown_option/` | 0 | 9 | `-x`, `-X`, `--some-flag` (per-char getopt iteration) |
| `target_is_directory/` | 0 | 3 | exit 1, stderr `error opening output file: Is a directory` |

## Critical moreutils behaviors discovered at capture time

These shaped the implementation and Strict-mode parity surface:

1. **`sponge` exits 0 for nearly everything**, including `-h`, `--help`, and unknown-option errors. The only non-zero exit observed is when the target is a directory (`exit 1`).
2. **Usage banner goes to stdout, not stderr.** `sponge -h` writes `sponge [-a] <file>: soak up all input from stdin and write it to <file>\n` (72 bytes) to stdout.
3. **`--some-flag` produces per-character getopt errors.** moreutils sponge's POSIX `getopt` strips the leading `--` and iterates through `s`, `o`, `m`, `e`, `-`, `f`, `l`, `g` — emitting `sponge: invalid option -- 'X'` for each (note `a` is missing because `-a` is a valid flag). Total nine error lines for one input. **Strict mode implementation note**: replicating this byte-for-byte in clap is non-trivial; the implementation may need a custom argv pre-parser for Strict mode, or document this as a single-error divergence.
4. **Long flags (`--`) are not recognized at all.** Anything starting with `--` is decomposed character-by-character. There is no concept of long-form options in moreutils sponge.
5. **`error opening output file: <reason>`** is the canonical IO-error format from moreutils sponge. The `<reason>` is the libc `strerror()` of the failed `open(2)` call.

## Recapture policy

**Do not regenerate inputs and outputs in the same commit.** Per HINT-001 (promoted from `rusty-ts` iter-5 lessons): if you change an input, the moreutils output drift is no longer being verified — you're just asserting that today's run equals today's run. Capture inputs once and freeze them; regenerate outputs only when the moreutils package version pin advances.

## Layout

```
fixtures/
├── README.md                       # this file
├── inputs/
│   ├── happy_path/                 # 4 input files (empty / small / binary / large 1 MiB)
│   ├── existing_target/            # .in + .preexisting per case
│   ├── missing_target/             # .in only (target created at runtime)
│   ├── append_existing/            # .in + .preexisting
│   ├── append_missing/             # .in only
│   ├── stdout_passthrough/         # .in only (no file arg)
│   └── symlink_target/             # .in + .linkdest (the linked file's content)
├── moreutils_outputs/
│   ├── happy_path/                 # .target per case
│   ├── existing_target/            # .target per case
│   ├── missing_target/             # .target per case
│   ├── append_existing/            # .target per case
│   ├── append_missing/             # .target per case
│   ├── stdout_passthrough/         # .stdout per case
│   ├── symlink_target/             # symlink + linkdest
│   ├── strict_h/                   # .stdout + .stderr + .exit per case
│   ├── strict_help/                # .stdout + .stderr + .exit per case
│   ├── strict_unknown_option/      # .stdout + .stderr + .exit per case
│   └── target_is_directory/        # .stdout + .stderr + .exit per case
└── scripts/
    └── capture-sponge.sh           # the capture driver (asserts pinned env)
```