feat(base64-rs): add GNU-compatible base64 command for Windows

Windows has no base64 command; certutil is file-only, adds PEM markers
and cannot be piped. This is a real base64.exe matching GNU coreutils:
same flags (-d, -i, -w), stdin/stdout streaming, exit codes and error
messages, so existing muscle memory and scripts carry over.

Verified against coreutils 9.7 with a 153-case differential test. The
subtle behaviours are reproduced: bytes are emitted incrementally so a
stream that goes bad still writes its valid prefix, unpadded tails are
accepted only when canonically encoded, concatenated streams decode as
one, and -w0 suppresses the trailing newline.

Deliberate deviation: '\r' is skipped when decoding alongside '\n', as
CRLF base64 files are routine on Windows and GNU rejects them.

Zero dependencies, so the build works offline.
This commit is contained in:
2026-07-27 21:36:01 +01:00
parent 3ee7d7a3ac
commit 5e6b4428bb
6 changed files with 556 additions and 0 deletions
+72
View File
@@ -0,0 +1,72 @@
# base64 for Windows
Windows has no `base64` command. `certutil -encode` is the usual substitute, but it only works on files, wraps output in `-----BEGIN CERTIFICATE-----` markers, and can't be piped. This is a real `base64.exe` that behaves like the one on a Linux box, so the commands and scripts you already know carry over.
```powershell
base64 image.png > image.b64
base64 -d image.b64 > image.png
type secrets.json | base64 -w0
base64 -di messy.txt > payload.bin
```
## Build and install
```powershell
cargo build --release
```
Copy `target\release\base64.exe` somewhere on your PATH — e.g. `%LOCALAPPDATA%\Microsoft\WindowsApps`, or a `C:\tools\bin` folder you've added yourself:
```powershell
mkdir C:\tools\bin
copy target\release\base64.exe C:\tools\bin\
[Environment]::SetEnvironmentVariable(
'Path',
[Environment]::GetEnvironmentVariable('Path','User') + ';C:\tools\bin',
'User')
```
Open a new terminal afterwards for the PATH change to take effect. No runtime dependencies — it's a single static binary, and the crate itself has zero dependencies, so the build works offline.
## Usage
```
Usage: base64 [OPTION]... [FILE]
Base64 encode or decode FILE, or standard input, to standard output.
With no FILE, or when FILE is -, read standard input.
-d, --decode decode data
-i, --ignore-garbage when decoding, ignore non-alphabet characters
-w, --wrap=COLS wrap encoded lines after COLS character (default 76).
Use 0 to disable line wrapping
--help display this help and exit
--version output version information and exit
```
Long options accept unambiguous abbreviations (`--dec`), short options cluster (`-di`), `-w` takes its value joined or separate (`-w0`, `-w 0`, `--wrap=0`), and `--` ends option parsing.
## Behaviour
Verified against GNU coreutils 9.7 with a 153-case differential test covering encoding at every wrap width, decode round-trips, malformed padding, truncated input, garbage handling, all the usage errors and file operands. Output, exit codes and error messages match, including the fiddly parts:
- **Partial output on failure.** Bytes are emitted as soon as enough bits are available, so a stream that goes bad halfway still writes the valid prefix before the error — same as coreutils.
- **Unpadded input is accepted** when the encoding is canonical, i.e. the leftover bits are zero. `aGVsbG8` decodes to `hello`; `aGV` is rejected because `V` carries stray bits.
- **Concatenated streams** decode as one (`aGVsbG8=aGVsbG8=``hellohello`).
- **`-w0` emits no trailing newline**; wrapped output always ends with one. Empty input produces no output at all.
- **Exit codes**: `0` success, `1` on any error.
### Deliberate differences
| | Why |
|---|---|
| `\r` is skipped when decoding, as `\n` already is | GNU rejects CRLF input as invalid. On Windows a base64 file will routinely have CRLF line endings, and erroring on those would make the tool useless for its main job. Everything else is still rejected unless you pass `-i`. |
| `--help` omits GNU's support URLs | This isn't GNU coreutils; pointing at their bug tracker would be wrong. |
| Error messages quote with `'ASCII'` | GNU uses `'…'` in a UTF-8 locale. Windows consoles are frequently cp437/cp1252, where those bytes render as mojibake. |
## Notes
- Roughly 2× slower than GNU on large inputs (100 MB encodes in ~0.35s vs ~0.18s). GNU uses hand-tuned routines; this is plain portable Rust and fast enough that the difference is invisible for normal use.
- Input is streamed, not buffered whole, so encoding a multi-gigabyte file doesn't blow up memory.
- Piping to a program that exits early (`base64 big.bin | head`) is handled quietly rather than reporting a broken pipe.
- Writing raw binary to a Windows console fails with a clear message telling you to redirect, because consoles take UTF-16 text and would corrupt the bytes. Redirecting to a file or piping is unaffected.