fabrica

hanna/fabrica

2992 bytes
Raw
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Interactive terminal prompts: non-echoing password entry and destructive-
6//! action confirmation.
7//!
8//! Both degrade deliberately when stdin is not a terminal (a pipe, a systemd
9//! unit, CI). A password can still be supplied on a single stdin line for
10//! scripting; a confirmation, by contrast, refuses rather than guess, so a
11//! non-interactive destructive command must pass `--yes` explicitly.
12
13use std::io::{IsTerminal, Write};
14
15/// Resolve a password to use, in three ways depending on how the command was
16/// invoked:
17///
18/// * `explicit` (from `--pass`) is used verbatim when present;
19/// * otherwise, on a terminal, the user is prompted twice with no echo and the
20/// two entries must match;
21/// * otherwise (piped stdin) a single line is read, so
22/// `printf '%s' pw | fabrica user passwd alice` works.
23///
24/// An empty password is rejected in every case.
25///
26/// # Errors
27///
28/// Returns an error if the prompts cannot be read, the two interactive entries
29/// differ, or the resolved password is empty.
30pub(crate) fn resolve_password(explicit: Option<String>) -> anyhow::Result<String> {
31 let password = match explicit {
32 Some(pass) => pass,
33 None if std::io::stdin().is_terminal() => {
34 let first = rpassword::prompt_password("New password: ")?;
35 let second = rpassword::prompt_password("Confirm password: ")?;
36 if first != second {
37 anyhow::bail!("passwords did not match");
38 }
39 first
40 }
41 None => {
42 let mut line = String::new();
43 std::io::stdin().read_line(&mut line)?;
44 line.trim_end_matches(['\r', '\n']).to_string()
45 }
46 };
47 if password.is_empty() {
48 anyhow::bail!("password must not be empty");
49 }
50 Ok(password)
51}
52
53/// Ask the user to confirm a destructive action.
54///
55/// `assume_yes` (from `--yes`) short-circuits to `true`. Otherwise a terminal is
56/// prompted `[y/N]` and only `y`/`yes` (any case) confirms. When stdin is not a
57/// terminal and `--yes` was not given, the action is refused rather than assumed.
58///
59/// # Errors
60///
61/// Returns an error if stdin is not a terminal and `--yes` was not passed, or if
62/// the prompt cannot be read.
63pub(crate) fn confirm(question: &str, assume_yes: bool) -> anyhow::Result<bool> {
64 if assume_yes {
65 return Ok(true);
66 }
67 if !std::io::stdin().is_terminal() {
68 anyhow::bail!(
69 "refusing to proceed without confirmation; re-run with --yes for non-interactive use"
70 );
71 }
72 eprint!("{question} [y/N] ");
73 std::io::stderr().flush()?;
74 let mut line = String::new();
75 std::io::stdin().read_line(&mut line)?;
76 let answer = line.trim().to_ascii_lowercase();
77 Ok(answer == "y" || answer == "yes")
78}