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:
@@ -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.
|
||||
Reference in New Issue
Block a user