use std::net::{
Ipv4Addr,
Ipv6Addr,
};
use regex::Regex;
use crate::redactor::Redactor;
pub fn url_credentials_redactor() -> Option<Redactor> {
Regex::new(r"(?P<protocol>https?|ftp)://([^:]+):([^@]+)@")
.ok()
.map(|re| {
Redactor::regex_with_capture(
re,
"${protocol}://••••:••••@".to_string(),
)
})
}
pub fn email_redactor() -> Option<Redactor> {
Regex::new(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b")
.ok()
.map(|regex| Redactor::regex(regex, Some("•••@•••".to_owned())))
}
pub fn mac_address_redactor() -> Option<Redactor> {
Regex::new(
r"(?i:\b[0-9a-f]{2}(?::[0-9a-f]{2}){5}\b|\b[0-9a-f]{2}(?:-[0-9a-f]{2}){5}\b)",
)
.ok()
.map(|re| Redactor::regex(re, Some("••:••:••:••:••:••".to_string())))
}
pub fn ipv4_redactor() -> Option<Redactor> {
Regex::new(r"\b(?:\d{1,3}\.){3}\d{1,3}\b")
.ok()
.map(|regex| {
Redactor::validated(
regex,
is_public_ipv4,
Some("••.••.••.••".to_owned()),
)
})
}
fn is_public_ipv4(s: &str) -> bool {
if let Ok(addr) = s.parse::<Ipv4Addr>() {
!(addr.is_private()
|| addr.is_loopback()
|| addr.is_link_local()
|| addr.is_unspecified()
|| addr.is_broadcast())
} else {
false
}
}
fn is_public_ipv6(s: &str) -> bool {
if let Ok(addr) = s.parse::<Ipv6Addr>() {
!(addr.is_loopback()
|| addr.is_unicast_link_local()
|| addr.is_unique_local()
|| addr.is_unspecified()
|| addr.is_multicast())
} else {
false
}
}
pub fn ipv6_redactor() -> Option<Redactor> {
let pattern = r"\b[0-9a-fA-F:]+:[0-9a-fA-F:]*[0-9a-fA-F]\b";
Regex::new(pattern).ok().map(|re| {
Redactor::validated(
re,
is_public_ipv6,
Some("••:••:••:••:••:••:••:••".to_owned()),
)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_url_credentials_redactor() {
let redactor = url_credentials_redactor().unwrap();
assert_eq!(
redactor.redact("visit https://user:password123@example.com"),
"visit https://••••:••••@example.com"
);
assert_eq!(
redactor.redact("no creds here: http://example.com"),
"no creds here: http://example.com"
);
}
#[test]
fn test_mac_address_redactor() {
let redactor = mac_address_redactor().unwrap();
assert_eq!(
redactor.redact("My MAC is 00:1A:2B:3C:4D:5E."),
"My MAC is ••:••:••:••:••:••."
);
assert_eq!(
redactor.redact("Another is 01-23-45-67-89-AB."),
"Another is ••:••:••:••:••:••."
);
assert_eq!(
redactor.redact("2026-04-25:13:00:49 INFO artificial event"),
"2026-04-25:13:00:49 INFO artificial event"
);
}
#[test]
fn test_ipv6_redactor_validated() {
let redactor = ipv6_redactor().unwrap();
assert_eq!(
redactor.redact("The address is fe80::aaa:8888:ffff:9999"),
"The address is fe80::aaa:8888:ffff:9999"
);
assert_eq!(
redactor.redact("2001:0db8:85a3:0000:0000:8a2e:0370:7334"),
"••:••:••:••:••:••:••:••"
);
assert_eq!(
redactor.redact("This is a MAC: 00:1A:2B:3C:4D:5E"),
"This is a MAC: 00:1A:2B:3C:4D:5E"
);
}
#[test]
fn test_ipv6_does_not_redact_rust_paths_or_unspecified() {
let redactor = ipv6_redactor().unwrap();
assert_eq!(
redactor.redact("use crate::redactor::Redactor;"),
"use crate::redactor::Redactor;"
);
assert_eq!(redactor.redact("::"), "::");
}
#[test]
fn test_email_redactor() {
let redactor = email_redactor().unwrap();
assert_eq!(
redactor.redact("email: test@example.com"),
"email: •••@•••"
);
}
#[test]
fn test_ipv4_redactor() {
let redactor = ipv4_redactor().unwrap();
assert_eq!(redactor.redact("IP: 192.168.1.1"), "IP: 192.168.1.1");
assert_eq!(redactor.redact("DNS: 8.8.8.8"), "DNS: ••.••.••.••");
}
}