printwell-cli 0.1.11

Command-line tool for HTML to PDF conversion
Documentation
//! Password handling utilities for secure credential input.

use anyhow::{Context, Result};

/// Resolve a password from various sources.
///
/// Priority order:
/// 1. Direct password (if provided)
/// 2. Password file (if provided)
/// 3. Environment variable (if provided)
///
/// # Security Note
/// Using `--password` directly exposes the password in process listings.
/// Prefer `--password-file` or `--password-env` for better security.
#[allow(dead_code)]
pub fn resolve_password(
    direct: Option<&str>,
    file_path: Option<&str>,
    env_var: Option<&str>,
    field_name: &str,
) -> Result<String> {
    // Direct password takes precedence
    if let Some(pwd) = direct {
        return Ok(pwd.to_string());
    }

    // Try password file
    if let Some(path) = file_path {
        let content = std::fs::read_to_string(path)
            .with_context(|| format!("Failed to read {field_name} from file: {path}"))?;
        // Trim whitespace/newlines from file content
        return Ok(content.trim().to_string());
    }

    // Try environment variable
    if let Some(var_name) = env_var {
        let value = std::env::var(var_name).with_context(|| {
            format!("Environment variable '{var_name}' not set for {field_name}")
        })?;
        return Ok(value);
    }

    anyhow::bail!("{field_name} is required. Use --password, --password-file, or --password-env")
}

/// Resolve an optional password from various sources.
///
/// Returns None if no source is provided, otherwise resolves like `resolve_password`.
#[allow(dead_code)]
pub fn resolve_optional_password(
    direct: Option<&str>,
    file_path: Option<&str>,
    env_var: Option<&str>,
    field_name: &str,
) -> Result<Option<String>> {
    if direct.is_none() && file_path.is_none() && env_var.is_none() {
        return Ok(None);
    }

    resolve_password(direct, file_path, env_var, field_name).map(Some)
}

#[cfg(test)]
#[allow(unsafe_code)] // set_var/remove_var are unsafe in Rust 2024
mod tests {
    use super::*;
    use std::io::Write;

    #[test]
    fn test_resolve_direct_password() {
        let result = resolve_password(Some("secret"), None, None, "test");
        assert_eq!(result.unwrap(), "secret");
    }

    #[test]
    fn test_resolve_password_file() {
        let mut temp = tempfile::NamedTempFile::new().unwrap();
        writeln!(temp, "file_secret").unwrap();

        let result = resolve_password(None, Some(temp.path().to_str().unwrap()), None, "test");
        assert_eq!(result.unwrap(), "file_secret");
    }

    #[test]
    fn test_resolve_password_env() {
        // SAFETY: This is a test-only env var with a unique name, no other threads access it
        unsafe {
            std::env::set_var("TEST_PASSWORD_12345", "env_secret");
        }
        let result = resolve_password(None, None, Some("TEST_PASSWORD_12345"), "test");
        assert_eq!(result.unwrap(), "env_secret");
        // SAFETY: Cleaning up test env var
        unsafe {
            std::env::remove_var("TEST_PASSWORD_12345");
        }
    }

    #[test]
    fn test_resolve_password_priority() {
        // Direct takes precedence over file
        let mut temp = tempfile::NamedTempFile::new().unwrap();
        writeln!(temp, "file_secret").unwrap();

        let result = resolve_password(
            Some("direct"),
            Some(temp.path().to_str().unwrap()),
            None,
            "test",
        );
        assert_eq!(result.unwrap(), "direct");
    }

    #[test]
    fn test_resolve_password_missing() {
        let result = resolve_password(None, None, None, "certificate password");
        assert!(result.is_err());
        assert!(
            result
                .unwrap_err()
                .to_string()
                .contains("certificate password")
        );
    }

    #[test]
    fn test_resolve_optional_password_none() {
        let result = resolve_optional_password(None, None, None, "test");
        assert!(result.unwrap().is_none());
    }

    #[test]
    fn test_resolve_optional_password_some() {
        let result = resolve_optional_password(Some("secret"), None, None, "test");
        assert_eq!(result.unwrap(), Some("secret".to_string()));
    }
}