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.
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.
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
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:
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) 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.
aGVsbG8decodes tohello;aGVis rejected becauseVcarries stray bits. - Concatenated streams decode as one (
aGVsbG8=aGVsbG8=→hellohello). -w0emits no trailing newline; wrapped output always ends with one. Empty input produces no output at all.- Exit codes:
0success,1on 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
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.