apollo-redaction 0.2.0

Redaction utility for sensitive data in error messages, debug logs, etc.
Documentation
use crate::RedactionStrategy;
use std::fmt;

/// Redact IPv4 and IPv6 addresses.
///
/// ## IPv4
/// The first two octets are shown.
/// ```rust
/// use apollo_redaction::Redacted;
/// use apollo_redaction::strategy::IpRedactor;
///
/// let ip = Redacted::<_, IpRedactor>::new("192.168.1.100");
/// assert_eq!(
///     format!("private ip is {ip}"),
///     "private ip is 192.168.*.*",
/// );
/// ```
///
/// ## IPv6
/// The first four groups are shown.
/// ```rust
/// use apollo_redaction::Redacted;
/// use apollo_redaction::strategy::IpRedactor;
///
/// let ip = Redacted::<_, IpRedactor>::new("4fd3:694a:d3f4:c465:9b17:18af:e6ae:a243");
/// assert_eq!(
///     format!("private ip is {ip}"),
///     "private ip is 4fd3:694a:d3f4:c465:****:****:****:****",
/// );
/// ```
#[derive(Default, Clone)]
pub struct IpRedactor;

impl<T: AsRef<str>> RedactionStrategy<T> for IpRedactor {
    fn display(&self, value: &T, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        redact_ip(value.as_ref(), f)
    }
}

fn redact_ip(ip: &str, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    // For IPs: show first two octets/groups, redact the rest
    // Handle both IPv4 (dot-separated) and IPv6 (colon-separated)
    if ip.contains(':') {
        // IPv6 or SocketAddrV6 - show first 4 groups
        let parts: Vec<&str> = ip.split(':').collect();
        if parts.len() >= 4 {
            write!(
                f,
                "{}:{}:{}:{}:****:****:****:****",
                parts[0], parts[1], parts[2], parts[3]
            )
        } else {
            write!(f, "****:****:****:****:****:****:****:****")
        }
    } else {
        // IPv4 or SocketAddrV4 - show first two octets
        let parts: Vec<&str> = ip.split('.').collect();
        if parts.len() == 4 {
            write!(f, "{}.{}.*.*", parts[0], parts[1])
        } else {
            // Fallback for malformed input
            write!(f, "[REDACTED: invalid IP]")
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn display_ipv4() {
        let my_secret = "192.168.1.100";
        let redacted = crate::Redacted::<_, IpRedactor>::new(my_secret);

        assert_eq!(
            format!("private ip is {redacted}"),
            "private ip is 192.168.*.*",
        );
    }

    #[test]
    fn display_ipv6() {
        let my_secret = "4fd3:694a:d3f4:c465:9b17:18af:e6ae:a243";
        let redacted = crate::Redacted::<_, IpRedactor>::new(my_secret);

        assert_eq!(
            format!("private ip is {redacted}"),
            "private ip is 4fd3:694a:d3f4:c465:****:****:****:****",
        );
    }

    #[test]
    fn display_invalid() {
        let my_secret = "this is not an ip address";
        let redacted = crate::Redacted::<_, IpRedactor>::new(my_secret);

        assert_eq!(
            format!("private ip is {redacted}"),
            "private ip is [REDACTED: invalid IP]",
            "must not leak secret if it's malformed"
        );
    }
}