rusty-sponge 0.1.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
# rusty-sponge Design Notes

This document is a maintainer-facing distillation of the implementation plan. The authoritative source-of-truth is `specs/00002-next-port-selection/plan.md` in the umbrella repo.

## Architecture overview

`rusty-sponge` is a single Cargo crate exposing a library API (`Sponge`, `SpongeBuilder`) and a CLI binary (`rusty-sponge`, plus an optional `sponge` alias gated behind the `sponge-alias` feature). The library is the canonical surface; the binary is a thin clap-derive wrapper around `rusty_sponge::run()`.

The CLI flow:

1. Parse argv via clap; resolve Default vs Strict mode (precedence: `--strict` > `RUSTY_SPONGE_STRICT=1` > `argv[0] == "sponge"` > Default).
2. Validate the target path (fail-fast on directory targets per FR-014).
3. Drain stdin into a `Buffer` enum — `InMemory(Vec<u8>)` up to the spill threshold (default 128 MiB, configurable via `RUSTY_SPONGE_SPILL_MB`), then `Spilled(BufWriter<NamedTempFile>)`.
4. Dispatch:
   - `Target::Stdout` → write buffer to stdout (FR-003).
   - `Target::File` on regular non-symlink → atomic path: write to sibling tempfile in target's parent dir, preserve mode bits (Unix) / read-only attr (Windows), `persist()` (which wraps `std::fs::rename`).
   - `Target::File` on symlink or reparse point → write-through path: `OpenOptions::write+truncate.open(path)` (FR-010), matching moreutils.
   - On atomic-rename failure (cross-volume, Windows shared-handle) → fall back to non-atomic copy + truncate-and-rewrite with Default-mode warning (FR-025).
5. Signal handler (SIGINT/SIGTERM/SIGHUP on Unix; console-control on Windows) cleans up the tempfile if it exists and the rename hasn't yet completed.

## Upstream Dependency Status

**E003 (umbrella reusable workflow) — NOT YET AVAILABLE.** As of 2026-05-22, the umbrella's reusable CI/release workflow does not exist. This repo therefore duplicates the inline workflows from `rusty-ts/.github/workflows/{ci,release}.yml` as a **pragmatic-path** approach. When E003 ships (`<umbrella>/.github/workflows/port-ci.yml@v1.0.0`), this repo's `.github/workflows/ci.yml` and `release.yml` will be replaced by ~20-line caller stubs.

**E004 (cargo-generate template) — NOT YET AVAILABLE.** This repo was scaffolded by hand mirroring the `rusty-ts` v0.1.0 layout. When E004 ships, future ports will scaffold via `cargo generate <umbrella>/templates/port` instead.

**Tracked in** `specs/project-plan.md` under Epics E003 and E004.

## Repo provenance

This repo was bootstrapped on 2026-05-22 as the **second** port in the Rusty portfolio (following `rusty-ts` v0.1.0). It mirrors the `rusty-ts` conventions:

- Dual MIT/Apache-2.0 license (LICENSE = MIT for GitHub sidebar; LICENSE-APACHE alongside).
- MSRV 1.85 (edition 2024 minimum — an upward deviation from the portfolio's rolling N-2 rule, documented in `README.md` and `CHANGELOG.md`).
- Inline CI/release workflows duplicated from `rusty-ts` until E003 ships.
- Fixture-capture protocol promoted from `rusty-ts` (pinned upstream commit, `LC_ALL=C.UTF-8`, capture script asserts pinned env, fixtures stored as binary in `.gitattributes`).
- Library-vs-binary error split: `thiserror` in lib, `anyhow` at the binary boundary.
- Drift-tested generated documentation: Compatibility Matrix is regenerated by an integration test, never by `build.rs`.

## Key Architecture Decisions (feature-local)

See `specs/00002-next-port-selection/plan.md` for the full AD-001…AD-016 table. Highlights:

- **AD-001/AD-014**: `tempfile` 3.x via `Builder::tempfile_in(parent)` + `NamedTempFile::persist()` for sibling-directory atomic rename.
- **AD-002**: `Buffer` enum with in-memory → spilled-tempfile transition at threshold.
- **AD-011**: 128 MiB compile-time spill default (static, not dynamic-RAM). Trades adaptivity for predictability.
- **AD-012/AD-013**: `signal-hook` on Unix (SIGINT/SIGTERM/SIGHUP); direct `windows-sys` `SetConsoleCtrlHandler` on Windows (covers `CTRL_C_EVENT` + `CTRL_BREAK_EVENT` + `CTRL_CLOSE_EVENT`).
- **AD-015**: `OpenOptions::write+truncate.open(path)` for symlink/reparse write-through — matches moreutils `fopen()` behavior.
- **AD-016**: `-a` append mode reads existing target into buffer before stdin (preserves atomic-safety on the rename path).

## Project-level baseline patterns inherited from rusty-ts

These patterns were promoted to `specs/sad.md` after `rusty-ts` shipped and apply to this port verbatim:

- Behavioral-parity snapshot capture protocol (pinned commit + LC_ALL=C.UTF-8 + capture-script env assertion).
- Library-vs-binary error split (`thiserror` lib, `anyhow` bin).
- Drift-tested generated documentation (no `build.rs`-driven file mutations).

This port adds two more baselines (promoted during `/sddp-specify` and `/sddp-plan`):

- **Atomic in-place file-replacement pattern**: sibling tempfile + `std::fs::rename` (Rust 1.81+ POSIX-semantics on Windows where available) + documented non-atomic copy fallback.
- **Signal-driven tempfile cleanup pattern**: async-signal-safe handler + main-thread polled atomic flag + cleanup-before-exit; uncatchable signals fall back to `tempfile`-crate `Drop`.