netsuke-build 0.1.0-beta1

A YAML-powered Ninja/Jinja hybrid build system.
//! Argument redaction helpers for the Ninja runner.
//! Provides the `CommandArg` wrapper used by doctests and logging.

#[derive(Debug, Clone)]
pub struct CommandArg(String);
impl CommandArg {
    #[must_use]
    pub const fn new(arg: String) -> Self {
        Self(arg)
    }
    #[must_use]
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

fn is_sensitive_key(key: &str) -> bool {
    const SENSITIVE_KEYS: [&str; 7] = [
        "password",
        "token",
        "secret",
        "api_key",
        "apikey",
        "auth",
        "authorization",
    ];
    SENSITIVE_KEYS
        .iter()
        .any(|candidate| key.eq_ignore_ascii_case(candidate))
}

/// Determine whether the argument should be redacted.
///
/// An argument is sensitive when it is a `key=value` pair whose key matches a
/// known sensitive keyword such as `password`, `token`, or `secret`.
///
/// # Examples
/// ```ignore
/// use netsuke::runner::process::redaction::{CommandArg, is_sensitive_arg};
/// assert!(is_sensitive_arg(&CommandArg::new("password=123".into())));
/// assert!(!is_sensitive_arg(&CommandArg::new("file=readme".into())));
/// ```
#[must_use]
pub fn is_sensitive_arg(arg: &CommandArg) -> bool {
    arg.as_str()
        .split_once('=')
        .is_some_and(|(key, _)| is_sensitive_key(key.trim()))
}

/// Redact sensitive information in a single argument.
///
/// Sensitive values are replaced with `***REDACTED***`, preserving keys.
///
/// # Examples
/// ```ignore
/// use netsuke::runner::process::redaction::{CommandArg, redact_argument};
/// let arg = CommandArg::new("token=abc".into());
/// assert_eq!(redact_argument(&arg).as_str(), "token=***REDACTED***");
/// let arg = CommandArg::new("path=/tmp".into());
/// assert_eq!(redact_argument(&arg).as_str(), "path=/tmp");
/// ```
#[must_use]
pub fn redact_argument(arg: &CommandArg) -> CommandArg {
    if is_sensitive_arg(arg) {
        if let Some((key, _)) = arg.as_str().split_once('=') {
            let trimmed = key.trim();
            return CommandArg::new(format!("{trimmed}=***REDACTED***"));
        }
        return CommandArg::new(String::from("***REDACTED***"));
    }
    arg.clone()
}

/// Redact sensitive information from all `args`.
///
/// # Examples
/// ```ignore
/// use netsuke::runner::process::redaction::{CommandArg, redact_sensitive_args};
/// let args = vec![
///     CommandArg::new("ninja".into()),
///     CommandArg::new("token=abc".into()),
/// ];
/// let redacted = redact_sensitive_args(&args);
/// assert_eq!(redacted[1].as_str(), "token=***REDACTED***");
/// ```
#[must_use]
pub fn redact_sensitive_args(args: &[CommandArg]) -> Vec<CommandArg> {
    args.iter().map(redact_argument).collect()
}

#[cfg(test)]
mod tests {
    //! Unit tests for sensitive environment variable redaction.

    use super::*;

    #[test]
    fn is_sensitive_arg_only_flags_known_keys() {
        let token = CommandArg::new(String::from("token=abc"));
        assert!(is_sensitive_arg(&token));

        let positional = CommandArg::new(String::from("secrets.yml"));
        assert!(!is_sensitive_arg(&positional));

        let path_arg = CommandArg::new(String::from("path=/tmp/secrets.yml"));
        assert!(!is_sensitive_arg(&path_arg));

        let spaced = CommandArg::new(String::from("  PASSWORD = value "));
        assert!(is_sensitive_arg(&spaced));
    }

    #[test]
    fn redact_argument_preserves_non_sensitive_pairs() {
        let redacted = redact_argument(&CommandArg::new(String::from("auth = token123")));
        assert_eq!(redacted.as_str(), "auth=***REDACTED***");

        let untouched = redact_argument(&CommandArg::new(String::from("path=/var/secrets")));
        assert_eq!(untouched.as_str(), "path=/var/secrets");

        let positional = redact_argument(&CommandArg::new(String::from("secret")));
        assert_eq!(positional.as_str(), "secret");
    }
}