use anyhow::Result;
use std::io::Write;
use std::process::{Command, Stdio};
use std::sync::{Mutex, OnceLock};
fn password_cell() -> &'static Mutex<Option<String>> {
static CELL: OnceLock<Mutex<Option<String>>> = OnceLock::new();
CELL.get_or_init(|| Mutex::new(None))
}
pub fn cache_sudo_password(password: String) {
if let Ok(mut guard) = password_cell().lock() {
*guard = Some(password);
}
}
pub fn clear_cached_sudo_password() {
if let Ok(mut guard) = password_cell().lock() {
guard.take();
}
}
pub fn cached_sudo_password() -> Option<String> {
password_cell().lock().ok().and_then(|guard| guard.clone())
}
pub fn authenticate_sudo(password: &str) -> Result<bool> {
let mut child = Command::new("sudo")
.arg("-S")
.arg("-v")
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
if let Some(mut stdin) = child.stdin.take() {
writeln!(stdin, "{}", password)?;
}
let status = child.wait()?;
let success = status.success();
if success {
cache_sudo_password(password.to_string());
}
Ok(success)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[cfg(unix)]
fn authenticate_sudo_with_wrong_password_does_not_panic() {
let _ = authenticate_sudo("definitely-not-the-real-password-12345");
}
#[test]
fn cached_sudo_password_round_trips_and_clears() {
clear_cached_sudo_password();
assert_eq!(cached_sudo_password(), None);
cache_sudo_password("hunter2".to_string());
assert_eq!(cached_sudo_password().as_deref(), Some("hunter2"));
cache_sudo_password("new-password".to_string());
assert_eq!(cached_sudo_password().as_deref(), Some("new-password"));
clear_cached_sudo_password();
assert_eq!(cached_sudo_password(), None);
clear_cached_sudo_password();
assert_eq!(cached_sudo_password(), None);
}
}