use std::fmt;
use std::process::Command;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PrivilegeTool {
Sudo,
Doas,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum PrivilegeMode {
#[default]
Auto,
Sudo,
Doas,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[allow(clippy::struct_excessive_bools)]
pub struct PrivilegeCapabilities {
pub supports_stdin_password: bool,
pub supports_credential_refresh: bool,
pub supports_credential_invalidation: bool,
pub supports_askpass: bool,
}
impl PrivilegeTool {
#[must_use]
pub const fn binary_name(self) -> &'static str {
match self {
Self::Sudo => "sudo",
Self::Doas => "doas",
}
}
#[must_use]
pub const fn capabilities(self) -> PrivilegeCapabilities {
match self {
Self::Sudo => PrivilegeCapabilities {
supports_stdin_password: true,
supports_credential_refresh: true,
supports_credential_invalidation: true,
supports_askpass: true,
},
Self::Doas => PrivilegeCapabilities {
supports_stdin_password: false,
supports_credential_refresh: false,
supports_credential_invalidation: false,
supports_askpass: false,
},
}
}
#[must_use]
pub fn is_available(self) -> bool {
if is_integration_test_context()
&& let Ok(val) = std::env::var("PACSEA_TEST_PRIVILEGE_AVAILABLE")
{
if val == "none" {
return false;
}
return val.split(',').any(|t| t.trim() == self.binary_name());
}
which::which(self.binary_name()).is_ok()
}
pub fn check_passwordless(self) -> Result<bool, String> {
if is_integration_test_context()
&& let Ok(val) = std::env::var("PACSEA_TEST_SUDO_PASSWORDLESS")
{
tracing::debug!(
tool = self.binary_name(),
val = %val,
"Using test override for passwordless check"
);
return Ok(val == "1");
}
let status = Command::new(self.binary_name())
.args(["-n", "true"])
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map_err(|e| format!("Failed to check passwordless {}: {e}", self.binary_name()))?;
Ok(status.success())
}
}
impl fmt::Display for PrivilegeTool {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.binary_name())
}
}
impl PrivilegeMode {
#[must_use]
pub fn from_config_key(val: &str) -> Option<Self> {
match val.trim().to_ascii_lowercase().as_str() {
"auto" => Some(Self::Auto),
"sudo" => Some(Self::Sudo),
"doas" => Some(Self::Doas),
_ => None,
}
}
#[must_use]
pub const fn as_config_key(self) -> &'static str {
match self {
Self::Auto => "auto",
Self::Sudo => "sudo",
Self::Doas => "doas",
}
}
}
impl fmt::Display for PrivilegeMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_config_key())
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum AuthMode {
#[default]
Prompt,
PasswordlessOnly,
Interactive,
}
impl AuthMode {
#[must_use]
pub fn from_config_key(val: &str) -> Option<Self> {
match val.trim().to_ascii_lowercase().replace('-', "_").as_str() {
"prompt" => Some(Self::Prompt),
"passwordless_only" | "passwordless" => Some(Self::PasswordlessOnly),
"interactive" => Some(Self::Interactive),
_ => None,
}
}
#[must_use]
pub const fn as_config_key(self) -> &'static str {
match self {
Self::Prompt => "prompt",
Self::PasswordlessOnly => "passwordless_only",
Self::Interactive => "interactive",
}
}
#[must_use]
pub const fn always_skips_password_modal(self) -> bool {
matches!(self, Self::Interactive)
}
}
impl fmt::Display for AuthMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_config_key())
}
}
pub fn resolve_privilege_tool(mode: PrivilegeMode) -> Result<PrivilegeTool, String> {
match mode {
PrivilegeMode::Auto => {
let doas_ok = PrivilegeTool::Doas.is_available();
let sudo_ok = PrivilegeTool::Sudo.is_available();
tracing::debug!(
mode = %mode,
doas_available = doas_ok,
sudo_available = sudo_ok,
"Resolving privilege tool"
);
if doas_ok {
tracing::info!(
tool = "doas",
reason = "auto: doas preferred when available",
"Selected privilege tool"
);
Ok(PrivilegeTool::Doas)
} else if sudo_ok {
tracing::info!(
tool = "sudo",
reason = "auto: doas unavailable, falling back",
"Selected privilege tool"
);
Ok(PrivilegeTool::Sudo)
} else {
Err("Neither doas nor sudo found on $PATH. \
Install one to perform privileged operations: \
`pacman -S sudo` or `pacman -S opendoas`."
.to_string())
}
}
PrivilegeMode::Sudo => {
if PrivilegeTool::Sudo.is_available() {
tracing::info!(
tool = "sudo",
reason = "explicit config",
"Selected privilege tool"
);
Ok(PrivilegeTool::Sudo)
} else {
Err(
"sudo is not available on $PATH. Install sudo (`pacman -S sudo`) \
or change privilege_tool to 'auto' or 'doas' in settings.conf."
.to_string(),
)
}
}
PrivilegeMode::Doas => {
if PrivilegeTool::Doas.is_available() {
tracing::info!(
tool = "doas",
reason = "explicit config",
"Selected privilege tool"
);
Ok(PrivilegeTool::Doas)
} else {
Err(
"doas is not available on $PATH. Install opendoas (`pacman -S opendoas`) \
or change privilege_tool to 'auto' or 'sudo' in settings.conf."
.to_string(),
)
}
}
}
}
fn active_tool_for_mode(mode: PrivilegeMode) -> Result<PrivilegeTool, String> {
match resolve_privilege_tool(mode) {
Ok(tool) => Ok(tool),
Err(err) if mode == PrivilegeMode::Auto => {
tracing::warn!(
configured_mode = %mode,
error = %err,
fallback = "sudo",
"Privilege tool auto-resolution failed — falling back to sudo; privileged commands may fail if sudo is missing"
);
Ok(PrivilegeTool::Sudo)
}
Err(err) => Err(err),
}
}
#[must_use = "caller should handle missing explicit privilege tools"]
pub fn active_tool() -> Result<PrivilegeTool, String> {
let settings = crate::theme::settings();
active_tool_for_mode(settings.privilege_mode)
}
pub fn run_interactive_auth(tool: PrivilegeTool) -> Result<bool, String> {
if is_integration_test_context() {
tracing::debug!(tool = %tool, "Skipping interactive auth in integration test context");
return Ok(true);
}
let mut cmd = Command::new(tool.binary_name());
if tool.capabilities().supports_credential_refresh {
cmd.arg("-v");
} else {
cmd.arg("true");
}
let status = cmd.status().map_err(|e| {
format!(
"Failed to run {} for interactive authentication: {e}",
tool.binary_name()
)
})?;
Ok(status.success())
}
pub fn detect_pam_fingerprint(tool: PrivilegeTool) -> bool {
use std::fs;
let tool_pam_path = format!("/etc/pam.d/{}", tool.binary_name());
let pam_files = [
tool_pam_path.as_str(),
"/etc/pam.d/system-auth",
"/etc/pam.d/system-local-login",
];
for path in &pam_files {
if let Ok(contents) = fs::read_to_string(path)
&& contents.contains("pam_fprintd")
{
tracing::debug!(path = %path, "Detected pam_fprintd in PAM configuration");
return true;
}
}
false
}
pub fn detect_fprintd_enrolled() -> bool {
let username = std::env::var("USER").unwrap_or_default();
if username.is_empty() {
return false;
}
let output = Command::new("fprintd-list").arg(&username).output();
match output {
Ok(out) if out.status.success() => {
let stdout = String::from_utf8_lossy(&out.stdout);
let has_finger = stdout.lines().any(|line| {
let trimmed = line.trim().to_lowercase();
trimmed.contains("left-") || trimmed.contains("right-")
});
if has_finger {
tracing::debug!("fprintd-list reports enrolled fingerprint(s)");
}
has_finger
}
_ => false,
}
}
static FINGERPRINT_AVAILABLE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
#[must_use]
pub fn is_fingerprint_available() -> bool {
*FINGERPRINT_AVAILABLE.get_or_init(|| {
let Ok(tool) = active_tool() else {
return false;
};
let pam_configured = detect_pam_fingerprint(tool);
if !pam_configured {
tracing::debug!(tool = %tool, "No pam_fprintd in PAM config for tool");
return false;
}
let enrolled = detect_fprintd_enrolled();
if !enrolled {
tracing::debug!("pam_fprintd configured but no enrolled fingerprints found");
}
enrolled
})
}
#[must_use]
pub fn build_privilege_command(tool: PrivilegeTool, command: &str) -> String {
format!("{} {command}", tool.binary_name())
}
#[must_use]
pub fn build_password_pipe(tool: PrivilegeTool, password: &str, command: &str) -> Option<String> {
if !tool.capabilities().supports_stdin_password {
return None;
}
let escaped = crate::install::shell_single_quote(password);
Some(format!(
"printf '%s\\n' {escaped} | {} -S {command}",
tool.binary_name()
))
}
#[must_use]
pub fn build_credential_warmup(tool: PrivilegeTool, password: &str) -> Option<String> {
if !tool.capabilities().supports_credential_refresh {
return None;
}
let escaped = crate::install::shell_single_quote(password);
Some(format!(
"printf '%s\\n' {escaped} | {} -S -v 2>/dev/null",
tool.binary_name()
))
}
#[must_use]
pub fn build_credential_invalidation(tool: PrivilegeTool) -> Option<String> {
if !tool.capabilities().supports_credential_invalidation {
return None;
}
Some(format!("{} -k", tool.binary_name()))
}
pub fn validate_password(tool: PrivilegeTool, password: &str) -> Result<bool, String> {
if !tool.capabilities().supports_stdin_password {
return Err(format!(
"{tool} does not support password validation via stdin. \
Configure passwordless {tool} or switch to sudo in settings.conf."
));
}
let escaped = crate::install::shell_single_quote(password);
let bin = tool.binary_name();
let cmd = format!("{bin} -k ; printf '%s\\n' {escaped} | {bin} -S -v 2>&1");
let output = Command::new("sh")
.arg("-c")
.arg(&cmd)
.output()
.map_err(|e| format!("Failed to execute {bin} validation: {e}"))?;
Ok(output.status.success())
}
fn is_integration_test_context() -> bool {
if !std::env::var("PACSEA_INTEGRATION_TEST").is_ok_and(|v| v == "1") {
return false;
}
#[cfg(debug_assertions)]
{
true
}
#[cfg(not(debug_assertions))]
{
is_running_cargo_test_binary()
}
}
#[cfg(not(debug_assertions))]
fn is_running_cargo_test_binary() -> bool {
let Ok(exe) = std::env::current_exe() else {
return false;
};
exe.components()
.any(|component| component.as_os_str() == "deps")
}
#[must_use]
pub fn is_integration_test() -> bool {
is_integration_test_context()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tool_binary_name_sudo() {
assert_eq!(PrivilegeTool::Sudo.binary_name(), "sudo");
}
#[test]
fn tool_binary_name_doas() {
assert_eq!(PrivilegeTool::Doas.binary_name(), "doas");
}
#[test]
fn tool_display_matches_binary_name() {
assert_eq!(format!("{}", PrivilegeTool::Sudo), "sudo");
assert_eq!(format!("{}", PrivilegeTool::Doas), "doas");
}
#[test]
fn sudo_capabilities_all_enabled() {
let caps = PrivilegeTool::Sudo.capabilities();
assert!(caps.supports_stdin_password);
assert!(caps.supports_credential_refresh);
assert!(caps.supports_credential_invalidation);
assert!(caps.supports_askpass);
}
#[test]
fn doas_capabilities_all_disabled() {
let caps = PrivilegeTool::Doas.capabilities();
assert!(!caps.supports_stdin_password);
assert!(!caps.supports_credential_refresh);
assert!(!caps.supports_credential_invalidation);
assert!(!caps.supports_askpass);
}
#[test]
fn mode_from_config_key_valid() {
assert_eq!(
PrivilegeMode::from_config_key("auto"),
Some(PrivilegeMode::Auto)
);
assert_eq!(
PrivilegeMode::from_config_key("sudo"),
Some(PrivilegeMode::Sudo)
);
assert_eq!(
PrivilegeMode::from_config_key("doas"),
Some(PrivilegeMode::Doas)
);
}
#[test]
fn mode_from_config_key_case_insensitive() {
assert_eq!(
PrivilegeMode::from_config_key("AUTO"),
Some(PrivilegeMode::Auto)
);
assert_eq!(
PrivilegeMode::from_config_key("Sudo"),
Some(PrivilegeMode::Sudo)
);
assert_eq!(
PrivilegeMode::from_config_key("DOAS"),
Some(PrivilegeMode::Doas)
);
}
#[test]
fn mode_from_config_key_with_whitespace() {
assert_eq!(
PrivilegeMode::from_config_key(" auto "),
Some(PrivilegeMode::Auto)
);
}
#[test]
fn mode_from_config_key_invalid() {
assert_eq!(PrivilegeMode::from_config_key(""), None);
assert_eq!(PrivilegeMode::from_config_key("su"), None);
assert_eq!(PrivilegeMode::from_config_key("runas"), None);
}
#[test]
fn mode_as_config_key_roundtrip() {
for mode in [
PrivilegeMode::Auto,
PrivilegeMode::Sudo,
PrivilegeMode::Doas,
] {
let key = mode.as_config_key();
assert_eq!(PrivilegeMode::from_config_key(key), Some(mode));
}
}
#[test]
fn mode_default_is_auto() {
assert_eq!(PrivilegeMode::default(), PrivilegeMode::Auto);
}
#[test]
fn mode_display() {
assert_eq!(format!("{}", PrivilegeMode::Auto), "auto");
assert_eq!(format!("{}", PrivilegeMode::Sudo), "sudo");
assert_eq!(format!("{}", PrivilegeMode::Doas), "doas");
}
#[test]
fn auth_mode_from_config_key_valid() {
assert_eq!(AuthMode::from_config_key("prompt"), Some(AuthMode::Prompt));
assert_eq!(
AuthMode::from_config_key("passwordless_only"),
Some(AuthMode::PasswordlessOnly)
);
assert_eq!(
AuthMode::from_config_key("interactive"),
Some(AuthMode::Interactive)
);
}
#[test]
fn auth_mode_from_config_key_case_insensitive() {
assert_eq!(AuthMode::from_config_key("PROMPT"), Some(AuthMode::Prompt));
assert_eq!(
AuthMode::from_config_key("Interactive"),
Some(AuthMode::Interactive)
);
assert_eq!(
AuthMode::from_config_key("PASSWORDLESS_ONLY"),
Some(AuthMode::PasswordlessOnly)
);
}
#[test]
fn auth_mode_from_config_key_hyphen_alias() {
assert_eq!(
AuthMode::from_config_key("passwordless-only"),
Some(AuthMode::PasswordlessOnly)
);
}
#[test]
fn auth_mode_from_config_key_short_alias() {
assert_eq!(
AuthMode::from_config_key("passwordless"),
Some(AuthMode::PasswordlessOnly)
);
}
#[test]
fn auth_mode_from_config_key_with_whitespace() {
assert_eq!(
AuthMode::from_config_key(" interactive "),
Some(AuthMode::Interactive)
);
}
#[test]
fn auth_mode_from_config_key_invalid() {
assert_eq!(AuthMode::from_config_key(""), None);
assert_eq!(AuthMode::from_config_key("fingerprint"), None);
assert_eq!(AuthMode::from_config_key("password"), None);
}
#[test]
fn auth_mode_as_config_key_roundtrip() {
for mode in [
AuthMode::Prompt,
AuthMode::PasswordlessOnly,
AuthMode::Interactive,
] {
let key = mode.as_config_key();
assert_eq!(AuthMode::from_config_key(key), Some(mode));
}
}
#[test]
fn auth_mode_default_is_prompt() {
assert_eq!(AuthMode::default(), AuthMode::Prompt);
}
#[test]
fn auth_mode_display() {
assert_eq!(format!("{}", AuthMode::Prompt), "prompt");
assert_eq!(
format!("{}", AuthMode::PasswordlessOnly),
"passwordless_only"
);
assert_eq!(format!("{}", AuthMode::Interactive), "interactive");
}
#[test]
fn auth_mode_always_skips_password_modal() {
assert!(!AuthMode::Prompt.always_skips_password_modal());
assert!(!AuthMode::PasswordlessOnly.always_skips_password_modal());
assert!(AuthMode::Interactive.always_skips_password_modal());
}
#[test]
fn resolve_auto_prefers_doas_when_both_available() {
let _guard = crate::global_test_mutex_lock();
unsafe {
std::env::set_var("PACSEA_INTEGRATION_TEST", "1");
std::env::set_var("PACSEA_TEST_PRIVILEGE_AVAILABLE", "sudo,doas");
}
let result = resolve_privilege_tool(PrivilegeMode::Auto);
unsafe {
std::env::remove_var("PACSEA_TEST_PRIVILEGE_AVAILABLE");
std::env::remove_var("PACSEA_INTEGRATION_TEST");
}
assert_eq!(result, Ok(PrivilegeTool::Doas));
}
#[test]
fn resolve_auto_falls_back_to_sudo() {
let _guard = crate::global_test_mutex_lock();
unsafe {
std::env::set_var("PACSEA_INTEGRATION_TEST", "1");
std::env::set_var("PACSEA_TEST_PRIVILEGE_AVAILABLE", "sudo");
}
let result = resolve_privilege_tool(PrivilegeMode::Auto);
unsafe {
std::env::remove_var("PACSEA_TEST_PRIVILEGE_AVAILABLE");
std::env::remove_var("PACSEA_INTEGRATION_TEST");
}
assert_eq!(result, Ok(PrivilegeTool::Sudo));
}
#[test]
fn resolve_auto_fails_when_none_available() {
let _guard = crate::global_test_mutex_lock();
unsafe {
std::env::set_var("PACSEA_INTEGRATION_TEST", "1");
std::env::set_var("PACSEA_TEST_PRIVILEGE_AVAILABLE", "none");
}
let result = resolve_privilege_tool(PrivilegeMode::Auto);
unsafe {
std::env::remove_var("PACSEA_TEST_PRIVILEGE_AVAILABLE");
std::env::remove_var("PACSEA_INTEGRATION_TEST");
}
let err = result.expect_err("should fail when no tool available");
assert!(err.contains("Neither doas nor sudo found"));
}
#[test]
fn resolve_explicit_sudo_succeeds() {
let _guard = crate::global_test_mutex_lock();
unsafe {
std::env::set_var("PACSEA_INTEGRATION_TEST", "1");
std::env::set_var("PACSEA_TEST_PRIVILEGE_AVAILABLE", "sudo");
}
let result = resolve_privilege_tool(PrivilegeMode::Sudo);
unsafe {
std::env::remove_var("PACSEA_TEST_PRIVILEGE_AVAILABLE");
std::env::remove_var("PACSEA_INTEGRATION_TEST");
}
assert_eq!(result, Ok(PrivilegeTool::Sudo));
}
#[test]
fn resolve_explicit_sudo_fails_when_missing() {
let _guard = crate::global_test_mutex_lock();
unsafe {
std::env::set_var("PACSEA_INTEGRATION_TEST", "1");
std::env::set_var("PACSEA_TEST_PRIVILEGE_AVAILABLE", "doas");
}
let result = resolve_privilege_tool(PrivilegeMode::Sudo);
unsafe {
std::env::remove_var("PACSEA_TEST_PRIVILEGE_AVAILABLE");
std::env::remove_var("PACSEA_INTEGRATION_TEST");
}
assert!(result.is_err());
let err = result.expect_err("should fail when sudo unavailable");
assert!(err.contains("sudo is not available"));
}
#[test]
fn resolve_explicit_doas_succeeds() {
let _guard = crate::global_test_mutex_lock();
unsafe {
std::env::set_var("PACSEA_INTEGRATION_TEST", "1");
std::env::set_var("PACSEA_TEST_PRIVILEGE_AVAILABLE", "doas");
}
let result = resolve_privilege_tool(PrivilegeMode::Doas);
unsafe {
std::env::remove_var("PACSEA_TEST_PRIVILEGE_AVAILABLE");
std::env::remove_var("PACSEA_INTEGRATION_TEST");
}
assert_eq!(result, Ok(PrivilegeTool::Doas));
}
#[test]
fn resolve_explicit_doas_fails_when_missing() {
let _guard = crate::global_test_mutex_lock();
unsafe {
std::env::set_var("PACSEA_INTEGRATION_TEST", "1");
std::env::set_var("PACSEA_TEST_PRIVILEGE_AVAILABLE", "sudo");
}
let result = resolve_privilege_tool(PrivilegeMode::Doas);
unsafe {
std::env::remove_var("PACSEA_TEST_PRIVILEGE_AVAILABLE");
std::env::remove_var("PACSEA_INTEGRATION_TEST");
}
assert!(result.is_err());
let err = result.expect_err("should fail when doas unavailable");
assert!(err.contains("doas is not available"));
}
#[test]
fn build_privilege_command_sudo() {
let cmd = build_privilege_command(PrivilegeTool::Sudo, "pacman -S foo");
assert_eq!(cmd, "sudo pacman -S foo");
}
#[test]
fn build_privilege_command_doas() {
let cmd = build_privilege_command(PrivilegeTool::Doas, "pacman -S foo");
assert_eq!(cmd, "doas pacman -S foo");
}
#[test]
fn build_password_pipe_sudo_returns_some() {
let result = build_password_pipe(PrivilegeTool::Sudo, "secret", "pacman -S foo");
let cmd = result.expect("sudo should support password pipe");
assert!(cmd.contains("printf "));
assert!(cmd.contains("sudo -S pacman -S foo"));
}
#[test]
fn build_password_pipe_doas_returns_none() {
let result = build_password_pipe(PrivilegeTool::Doas, "secret", "pacman -S foo");
assert!(result.is_none());
}
#[test]
fn build_credential_warmup_sudo_returns_some() {
let result = build_credential_warmup(PrivilegeTool::Sudo, "secret");
let cmd = result.expect("sudo should support credential warmup");
assert!(cmd.contains("sudo -S -v"));
}
#[test]
fn build_credential_warmup_doas_returns_none() {
let result = build_credential_warmup(PrivilegeTool::Doas, "secret");
assert!(result.is_none());
}
#[test]
fn build_credential_invalidation_sudo_returns_some() {
let cmd = build_credential_invalidation(PrivilegeTool::Sudo)
.expect("sudo should support credential invalidation");
assert_eq!(cmd, "sudo -k");
}
#[test]
fn build_credential_invalidation_doas_returns_none() {
let result = build_credential_invalidation(PrivilegeTool::Doas);
assert!(result.is_none());
}
#[test]
fn validate_password_doas_returns_err() {
let result = validate_password(PrivilegeTool::Doas, "any");
let err = result.expect_err("doas should not support password validation");
assert!(err.contains("does not support"));
}
#[test]
fn is_available_test_override_none() {
let _guard = crate::global_test_mutex_lock();
unsafe {
std::env::set_var("PACSEA_INTEGRATION_TEST", "1");
std::env::set_var("PACSEA_TEST_PRIVILEGE_AVAILABLE", "none");
}
assert!(!PrivilegeTool::Sudo.is_available());
assert!(!PrivilegeTool::Doas.is_available());
unsafe {
std::env::remove_var("PACSEA_TEST_PRIVILEGE_AVAILABLE");
std::env::remove_var("PACSEA_INTEGRATION_TEST");
}
}
#[test]
fn is_available_test_override_sudo_only() {
let _guard = crate::global_test_mutex_lock();
unsafe {
std::env::set_var("PACSEA_INTEGRATION_TEST", "1");
std::env::set_var("PACSEA_TEST_PRIVILEGE_AVAILABLE", "sudo");
}
assert!(PrivilegeTool::Sudo.is_available());
assert!(!PrivilegeTool::Doas.is_available());
unsafe {
std::env::remove_var("PACSEA_TEST_PRIVILEGE_AVAILABLE");
std::env::remove_var("PACSEA_INTEGRATION_TEST");
}
}
#[test]
fn is_available_test_override_both() {
let _guard = crate::global_test_mutex_lock();
unsafe {
std::env::set_var("PACSEA_INTEGRATION_TEST", "1");
std::env::set_var("PACSEA_TEST_PRIVILEGE_AVAILABLE", "sudo,doas");
}
assert!(PrivilegeTool::Sudo.is_available());
assert!(PrivilegeTool::Doas.is_available());
unsafe {
std::env::remove_var("PACSEA_TEST_PRIVILEGE_AVAILABLE");
std::env::remove_var("PACSEA_INTEGRATION_TEST");
}
}
#[test]
fn is_integration_test_when_set() {
let _guard = crate::global_test_mutex_lock();
unsafe {
std::env::set_var("PACSEA_INTEGRATION_TEST", "1");
}
assert!(is_integration_test());
unsafe {
std::env::remove_var("PACSEA_INTEGRATION_TEST");
}
}
#[test]
fn is_integration_test_when_unset() {
let _guard = crate::global_test_mutex_lock();
unsafe {
std::env::remove_var("PACSEA_INTEGRATION_TEST");
}
assert!(!is_integration_test());
}
#[test]
fn is_integration_test_wrong_value() {
let _guard = crate::global_test_mutex_lock();
unsafe {
std::env::set_var("PACSEA_INTEGRATION_TEST", "0");
}
assert!(!is_integration_test());
unsafe {
std::env::remove_var("PACSEA_INTEGRATION_TEST");
}
}
#[test]
fn active_tool_returns_ok_with_sudo_or_doas() {
let tool = active_tool().expect("active_tool should resolve in this environment");
assert!(
tool == PrivilegeTool::Sudo || tool == PrivilegeTool::Doas,
"active_tool should return Sudo or Doas"
);
}
#[test]
fn active_tool_for_mode_explicit_doas_errors_when_missing() {
let _guard = crate::global_test_mutex_lock();
unsafe {
std::env::set_var("PACSEA_INTEGRATION_TEST", "1");
std::env::set_var("PACSEA_TEST_PRIVILEGE_AVAILABLE", "sudo");
}
let result = super::active_tool_for_mode(PrivilegeMode::Doas);
unsafe {
std::env::remove_var("PACSEA_TEST_PRIVILEGE_AVAILABLE");
std::env::remove_var("PACSEA_INTEGRATION_TEST");
}
let err = result.expect_err("explicit doas without doas on PATH should error");
assert!(err.contains("doas is not available"));
}
#[test]
fn active_tool_for_mode_explicit_sudo_errors_when_missing() {
let _guard = crate::global_test_mutex_lock();
unsafe {
std::env::set_var("PACSEA_INTEGRATION_TEST", "1");
std::env::set_var("PACSEA_TEST_PRIVILEGE_AVAILABLE", "doas");
}
let result = super::active_tool_for_mode(PrivilegeMode::Sudo);
unsafe {
std::env::remove_var("PACSEA_TEST_PRIVILEGE_AVAILABLE");
std::env::remove_var("PACSEA_INTEGRATION_TEST");
}
let err = result.expect_err("explicit sudo without sudo on PATH should error");
assert!(err.contains("sudo is not available"));
}
#[test]
fn active_tool_for_mode_auto_none_falls_back_to_sudo() {
let _guard = crate::global_test_mutex_lock();
unsafe {
std::env::set_var("PACSEA_INTEGRATION_TEST", "1");
std::env::set_var("PACSEA_TEST_PRIVILEGE_AVAILABLE", "none");
}
let result = super::active_tool_for_mode(PrivilegeMode::Auto);
unsafe {
std::env::remove_var("PACSEA_TEST_PRIVILEGE_AVAILABLE");
std::env::remove_var("PACSEA_INTEGRATION_TEST");
}
assert_eq!(
result,
Ok(PrivilegeTool::Sudo),
"Auto with no tools should still return Ok(Sudo) after fallback"
);
}
#[test]
fn build_password_pipe_uses_printf_not_echo() {
let result = build_password_pipe(PrivilegeTool::Sudo, "pw", "cmd");
let cmd = result.expect("sudo pipe should return Some");
assert!(cmd.starts_with("printf "), "should use printf, not echo");
assert!(cmd.contains("%s\\n"), "should use %s\\n format");
}
#[test]
fn build_password_pipe_escapes_special_chars() {
let result = build_password_pipe(PrivilegeTool::Sudo, "pa's$word", "pacman -S foo");
let cmd = result.expect("sudo pipe should return Some");
assert!(cmd.contains("sudo -S pacman -S foo"));
assert!(!cmd.contains("pa's$word"), "password must be shell-escaped");
}
#[test]
fn build_credential_warmup_uses_printf() {
let result = build_credential_warmup(PrivilegeTool::Sudo, "pw");
let cmd = result.expect("sudo warmup should return Some");
assert!(cmd.starts_with("printf "), "warmup should use printf");
assert!(cmd.contains("sudo -S -v"));
assert!(cmd.contains("2>/dev/null"));
}
#[test]
fn build_privilege_command_doas_format() {
let cmd = build_privilege_command(PrivilegeTool::Doas, "pacman -Syu --noconfirm");
assert_eq!(cmd, "doas pacman -Syu --noconfirm");
}
#[test]
fn doas_password_pipe_returns_none() {
assert!(build_password_pipe(PrivilegeTool::Doas, "pw", "cmd").is_none());
}
#[test]
fn doas_credential_warmup_returns_none() {
assert!(build_credential_warmup(PrivilegeTool::Doas, "pw").is_none());
}
#[test]
fn doas_credential_invalidation_returns_none() {
assert!(build_credential_invalidation(PrivilegeTool::Doas).is_none());
}
#[test]
fn check_passwordless_test_override_true() {
let _guard = crate::global_test_mutex_lock();
unsafe {
std::env::set_var("PACSEA_INTEGRATION_TEST", "1");
std::env::set_var("PACSEA_TEST_SUDO_PASSWORDLESS", "1");
}
let result = PrivilegeTool::Sudo.check_passwordless();
unsafe {
std::env::remove_var("PACSEA_TEST_SUDO_PASSWORDLESS");
std::env::remove_var("PACSEA_INTEGRATION_TEST");
}
assert_eq!(result, Ok(true));
}
#[test]
fn check_passwordless_test_override_false() {
let _guard = crate::global_test_mutex_lock();
unsafe {
std::env::set_var("PACSEA_INTEGRATION_TEST", "1");
std::env::set_var("PACSEA_TEST_SUDO_PASSWORDLESS", "0");
}
let result = PrivilegeTool::Sudo.check_passwordless();
unsafe {
std::env::remove_var("PACSEA_TEST_SUDO_PASSWORDLESS");
std::env::remove_var("PACSEA_INTEGRATION_TEST");
}
assert_eq!(result, Ok(false));
}
#[test]
fn check_passwordless_doas_test_override() {
let _guard = crate::global_test_mutex_lock();
unsafe {
std::env::set_var("PACSEA_INTEGRATION_TEST", "1");
std::env::set_var("PACSEA_TEST_SUDO_PASSWORDLESS", "1");
}
let result = PrivilegeTool::Doas.check_passwordless();
unsafe {
std::env::remove_var("PACSEA_TEST_SUDO_PASSWORDLESS");
std::env::remove_var("PACSEA_INTEGRATION_TEST");
}
assert_eq!(result, Ok(true));
}
#[test]
fn validate_password_doas_returns_unsupported() {
let result = validate_password(PrivilegeTool::Doas, "any");
let err = result.expect_err("doas should not support password validation");
assert!(
err.contains("does not support"),
"unexpected error message: {err}"
);
}
#[test]
fn both_tools_produce_distinct_commands() {
let sudo_cmd = build_privilege_command(PrivilegeTool::Sudo, "pacman -S foo");
let doas_cmd = build_privilege_command(PrivilegeTool::Doas, "pacman -S foo");
assert_ne!(sudo_cmd, doas_cmd, "sudo and doas commands must differ");
assert!(sudo_cmd.starts_with("sudo "));
assert!(doas_cmd.starts_with("doas "));
}
#[test]
fn capabilities_are_complementary() {
let sudo_caps = PrivilegeTool::Sudo.capabilities();
let doas_caps = PrivilegeTool::Doas.capabilities();
assert_ne!(
sudo_caps.supports_stdin_password, doas_caps.supports_stdin_password,
"sudo and doas should differ on stdin password support"
);
}
#[test]
fn detect_pam_fingerprint_sudo_does_not_panic() {
let _result = detect_pam_fingerprint(PrivilegeTool::Sudo);
}
#[test]
fn detect_pam_fingerprint_doas_does_not_panic() {
let _result = detect_pam_fingerprint(PrivilegeTool::Doas);
}
#[test]
fn detect_fprintd_enrolled_does_not_panic() {
let _result = detect_fprintd_enrolled();
}
#[test]
fn is_fingerprint_available_does_not_panic() {
let _result = is_fingerprint_available();
}
}