use std::io::IsTerminal;
pub const PASSWORD_ENV: &str = "SOLIDB_PASSWORD";
pub fn resolve_password(
cli: Option<&str>,
file: Option<&str>,
prompt_for: Option<&str>,
) -> Result<Option<String>, String> {
if let Some(pw) = cli {
eprintln!(
"Warning: a password given on the command line is visible to other \
users in `ps` and is kept in shell history. Prefer {} or \
--password-file.",
PASSWORD_ENV
);
return Ok(Some(pw.to_string()));
}
if let Some(path) = file {
let contents = std::fs::read_to_string(path)
.map_err(|e| format!("Cannot read password file '{}': {}", path, e))?;
return Ok(Some(strip_line_ending(&contents).to_string()));
}
if let Ok(pw) = std::env::var(PASSWORD_ENV) {
if !pw.is_empty() {
return Ok(Some(pw));
}
}
if let Some(user) = prompt_for {
if std::io::stdin().is_terminal() || std::io::stderr().is_terminal() {
let pw = rpassword::prompt_password(format!("Password for {}: ", user))
.map_err(|e| format!("Cannot read password from terminal: {}", e))?;
return Ok(Some(pw));
}
}
Ok(None)
}
fn strip_line_ending(s: &str) -> &str {
s.strip_suffix("\r\n")
.or_else(|| s.strip_suffix('\n'))
.unwrap_or(s)
}
#[cfg(test)]
mod password_tests {
use super::*;
#[test]
fn strips_one_line_ending_only() {
assert_eq!(strip_line_ending("secret\n"), "secret");
assert_eq!(strip_line_ending("secret\r\n"), "secret");
assert_eq!(strip_line_ending("secret"), "secret");
assert_eq!(strip_line_ending(" sec ret \n\n"), " sec ret \n");
}
#[test]
fn cli_wins_then_file() {
assert_eq!(
resolve_password(Some("a"), None, None).unwrap(),
Some("a".to_string())
);
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("pw");
std::fs::write(&path, "from-file\n").unwrap();
assert_eq!(
resolve_password(None, Some(path.to_str().unwrap()), None).unwrap(),
Some("from-file".to_string())
);
assert!(resolve_password(None, Some("/nonexistent/solidb-pw"), None).is_err());
}
}