a4d08e9998
Running base64 with no FILE fell into the stdin branch and sat in a read loop. That is exactly what GNU does, but on Windows it presents as a dead terminal: the console echoes keystrokes so it looks like a prompt, output buffers into 32K chunks so nothing comes back as you type, and there is no hint that it is waiting for Ctrl+Z. Guard the implicit case only. With no FILE operand and stdin attached to a console, print the help text to stderr and exit 1. Pipes and '<' redirects are untouched, so pipelines and the parity harness see no change -- the harness always hands the child a stdin pipe via input=. An explicit '-' still reads the console for anyone who does want to type input by hand. Uses std::io::IsTerminal, so the crate stays dependency-free.
83 lines
4.8 KiB
Markdown
83 lines
4.8 KiB
Markdown
# 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 154-case differential test ([tests/parity.py](tests/parity.py)) 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`. |
|
||
| Running it with no FILE at an interactive console prints the usage text and exits `1` | GNU would read stdin, which looks exactly like a hung terminal — no prompt, no output, no hint that it wants Ctrl+Z. Only applies when stdin is a console: pipes and `<` redirects read stdin as usual, and `base64 -` still reads the console if you actually want to type input. |
|
||
| `--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. |
|
||
|
||
## Running the tests
|
||
|
||
```bash
|
||
cargo build --release
|
||
python3 tests/parity.py
|
||
```
|
||
|
||
Linux-only — it diffs against a real GNU `base64`, so it can't run on the Windows box this tool targets. Note that many distributions now ship **uutils** coreutils as `/usr/bin/base64`; it disagrees with GNU on most malformed-input cases, so the script hunts for a genuine GNU build and refuses to run against anything else. On Ubuntu with `coreutils-from-uutils` installed, the GNU one is `gnubase64`.
|
||
|
||
## 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.
|