// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. //! Interactive terminal prompts: non-echoing password entry and destructive- //! action confirmation. //! //! Both degrade deliberately when stdin is not a terminal (a pipe, a systemd //! unit, CI). A password can still be supplied on a single stdin line for //! scripting; a confirmation, by contrast, refuses rather than guess, so a //! non-interactive destructive command must pass `--yes` explicitly. use std::io::{IsTerminal, Write}; /// Resolve a password to use, in three ways depending on how the command was /// invoked: /// /// * `explicit` (from `--pass`) is used verbatim when present; /// * otherwise, on a terminal, the user is prompted twice with no echo and the /// two entries must match; /// * otherwise (piped stdin) a single line is read, so /// `printf '%s' pw | fabrica user passwd alice` works. /// /// An empty password is rejected in every case. /// /// # Errors /// /// Returns an error if the prompts cannot be read, the two interactive entries /// differ, or the resolved password is empty. pub(crate) fn resolve_password(explicit: Option) -> anyhow::Result { let password = match explicit { Some(pass) => pass, None if std::io::stdin().is_terminal() => { let first = rpassword::prompt_password("New password: ")?; let second = rpassword::prompt_password("Confirm password: ")?; if first != second { anyhow::bail!("passwords did not match"); } first } None => { let mut line = String::new(); std::io::stdin().read_line(&mut line)?; line.trim_end_matches(['\r', '\n']).to_string() } }; if password.is_empty() { anyhow::bail!("password must not be empty"); } Ok(password) } /// Ask the user to confirm a destructive action. /// /// `assume_yes` (from `--yes`) short-circuits to `true`. Otherwise a terminal is /// prompted `[y/N]` and only `y`/`yes` (any case) confirms. When stdin is not a /// terminal and `--yes` was not given, the action is refused rather than assumed. /// /// # Errors /// /// Returns an error if stdin is not a terminal and `--yes` was not passed, or if /// the prompt cannot be read. pub(crate) fn confirm(question: &str, assume_yes: bool) -> anyhow::Result { if assume_yes { return Ok(true); } if !std::io::stdin().is_terminal() { anyhow::bail!( "refusing to proceed without confirmation; re-run with --yes for non-interactive use" ); } eprint!("{question} [y/N] "); std::io::stderr().flush()?; let mut line = String::new(); std::io::stdin().read_line(&mut line)?; let answer = line.trim().to_ascii_lowercase(); Ok(answer == "y" || answer == "yes") }