use std::sync::Arc;
use rvoip_sip_core::types::headers::HeaderName;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RedactionDecision {
Keep,
Redact(String),
Drop,
}
pub trait TraceRedactor: Send + Sync + std::fmt::Debug {
fn redact(&self, header: &HeaderName, value: &str) -> RedactionDecision;
}
#[derive(Clone, Debug, Default)]
pub struct PassthroughRedactor;
impl TraceRedactor for PassthroughRedactor {
fn redact(&self, _header: &HeaderName, _value: &str) -> RedactionDecision {
RedactionDecision::Keep
}
}
pub fn apply_redaction(
redactor: Option<&Arc<dyn TraceRedactor>>,
header: &HeaderName,
value: &str,
) -> Option<String> {
match redactor {
None => Some(value.to_string()),
Some(r) => match r.redact(header, value) {
RedactionDecision::Keep => Some(value.to_string()),
RedactionDecision::Redact(replacement) => Some(replacement),
RedactionDecision::Drop => None,
},
}
}
pub fn apply_message_redactor(redactor: &dyn TraceRedactor, raw: &str) -> String {
let mut out = String::with_capacity(raw.len());
let mut in_headers = true;
for line in raw.split_inclusive('\n') {
let trimmed = line.trim_end_matches('\n').trim_end_matches('\r');
if !in_headers {
out.push_str(line);
continue;
}
if trimmed.is_empty() {
out.push_str(line);
in_headers = false;
continue;
}
let bytes = trimmed.as_bytes();
if matches!(bytes.first(), Some(b' ' | b'\t')) {
out.push_str(line);
continue;
}
let Some(colon) = trimmed.find(':') else {
out.push_str(line);
continue;
};
let name = trimmed[..colon].trim();
let value = trimmed[colon + 1..].trim();
let header_name = name
.parse::<HeaderName>()
.unwrap_or_else(|_| HeaderName::Other(name.to_string()));
match redactor.redact(&header_name, value) {
RedactionDecision::Keep => out.push_str(line),
RedactionDecision::Redact(replacement) => {
out.push_str(name);
out.push_str(": ");
out.push_str(&replacement);
if line.ends_with("\r\n") {
out.push_str("\r\n");
} else {
out.push('\n');
}
}
RedactionDecision::Drop => {
}
}
}
out
}