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.
This commit is contained in:
2026-07-27 22:33:13 +01:00
parent 665f846dc9
commit a4d08e9998
2 changed files with 16 additions and 1 deletions
+1
View File
@@ -61,6 +61,7 @@ Verified against GNU coreutils 9.7 with a 154-case differential test ([tests/par
| | Why | | | 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`. | | `\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. | | `--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. | | Error messages quote with `'ASCII'` | GNU uses `'…'` in a UTF-8 locale. Windows consoles are frequently cp437/cp1252, where those bytes render as mojibake. |
+15 -1
View File
@@ -5,7 +5,7 @@
use std::env; use std::env;
use std::fs::File; use std::fs::File;
use std::io::{self, BufWriter, ErrorKind, Read, Write}; use std::io::{self, BufWriter, ErrorKind, IsTerminal, Read, Write};
use std::process::ExitCode; use std::process::ExitCode;
const PROGRAM: &str = "base64"; const PROGRAM: &str = "base64";
@@ -56,6 +56,10 @@ fn main() -> ExitCode {
eprintln!("Try '{PROGRAM} --help' for more information."); eprintln!("Try '{PROGRAM} --help' for more information.");
ExitCode::FAILURE ExitCode::FAILURE
} }
Err(Fail::NoInput) => {
eprintln!("{HELP}");
ExitCode::FAILURE
}
Err(Fail::Error(msg)) => { Err(Fail::Error(msg)) => {
eprintln!("{PROGRAM}: {msg}"); eprintln!("{PROGRAM}: {msg}");
ExitCode::FAILURE ExitCode::FAILURE
@@ -68,6 +72,9 @@ enum Fail {
Usage(String), Usage(String),
/// Runtime problem: message only. /// Runtime problem: message only.
Error(String), Error(String),
/// Bare invocation at a console: show the usage text rather than sit on
/// stdin looking hung.
NoInput,
/// Downstream closed the pipe (`base64 f | head`); nothing to report. /// Downstream closed the pipe (`base64 f | head`); nothing to report.
Quiet, Quiet,
} }
@@ -88,6 +95,13 @@ fn run() -> Result<(), Fail> {
} }
}; };
// GNU reads stdin when given no FILE, which at an interactive prompt is
// indistinguishable from a hang. Only the implicit case is caught: an
// explicit `-` is someone asking for stdin on purpose.
if opts.file.is_none() && io::stdin().is_terminal() {
return Err(Fail::NoInput);
}
// Reading is buffered by the encode/decode loops, which pull in large // Reading is buffered by the encode/decode loops, which pull in large
// chunks; stdout is wrapped so small writes do not hit the OS each time. // chunks; stdout is wrapped so small writes do not hit the OS each time.
let stdout = io::stdout(); let stdout = io::stdout();