Files
Tools/base64-rs/README.md
T
LyAhn a4d08e9998 fix(base64-rs): print usage instead of blocking on console stdin
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.
2026-07-27 22:33:13 +01:00

4.8 KiB
Raw Blame History

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. 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

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.