use crate::util::give_me_a_formatter;
use std::{
fmt::{Debug, Display, Write},
num::NonZeroU8,
};
pub enum RedactSpecialization {
Option,
}
#[derive(Clone, Copy)]
pub enum RedactionLength {
Full,
Partial,
Fixed(NonZeroU8),
}
#[derive(Clone, Copy)]
pub struct RedactFlags {
pub redact_length: RedactionLength,
pub redact_char: char,
}
impl RedactFlags {
const MIN_PARTIAL_CHARS: usize = 5;
const MAX_PARTIAL_EXPOSE: usize = 3;
pub(crate) fn redact_partial(&self, fmt: &mut std::fmt::Formatter, to_redact: &str) -> std::fmt::Result {
let count = to_redact.chars().filter(|char| char.is_alphanumeric()).count();
if count < Self::MIN_PARTIAL_CHARS {
for char in to_redact.chars() {
if char.is_alphanumeric() {
fmt.write_char(self.redact_char)?;
} else {
fmt.write_char(char)?;
}
}
} else {
let redact_count = (count / 3).min(Self::MAX_PARTIAL_EXPOSE);
let mut prefix_gas = redact_count;
let mut middle_gas = count - redact_count - redact_count;
for char in to_redact.chars() {
if char.is_alphanumeric() {
if prefix_gas > 0 {
prefix_gas -= 1;
fmt.write_char(char)?;
} else if middle_gas > 0 {
middle_gas -= 1;
fmt.write_char(self.redact_char)?;
} else {
fmt.write_char(char)?;
}
} else {
fmt.write_char(char)?;
}
}
}
Ok(())
}
pub(crate) fn redact_full(&self, fmt: &mut std::fmt::Formatter, to_redact: &str) -> std::fmt::Result {
for char in to_redact.chars() {
if char.is_whitespace() || !char.is_alphanumeric() {
fmt.write_char(char)?;
} else {
fmt.write_char(self.redact_char)?;
}
}
Ok(())
}
pub(crate) fn redact_fixed(fmt: &mut std::fmt::Formatter, width: usize, char: char) -> std::fmt::Result {
let mut buf = String::with_capacity(width);
for _ in 0..width {
buf.push(char);
}
fmt.write_str(&buf)
}
}
pub enum RedactionTarget<'a> {
Debug {
this: &'a dyn Debug,
alternate: bool,
},
Display(&'a dyn Display),
}
impl RedactionTarget<'_> {
#[cfg(feature = "toggle")]
pub(crate) fn passthrough(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
RedactionTarget::Debug { this, .. } => std::fmt::Debug::fmt(this, fmt),
RedactionTarget::Display(this) => std::fmt::Display::fmt(this, fmt),
}
}
}
impl ToString for RedactionTarget<'_> {
fn to_string(&self) -> String {
match self {
RedactionTarget::Debug { this, alternate: false } => format!("{:?}", this),
RedactionTarget::Debug { this, alternate: true } => format!("{:#?}", this),
RedactionTarget::Display(this) => this.to_string(),
}
}
}
pub struct RedactionFormatter<'a> {
pub this: RedactionTarget<'a>,
pub flags: RedactFlags,
pub specialization: Option<RedactSpecialization>,
}
impl std::fmt::Debug for RedactionFormatter<'_> {
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
#[cfg(feature = "toggle")]
if crate::toggle::get_redaction_behavior().is_plaintext() {
return self.this.passthrough(fmt);
}
if let RedactionLength::Fixed(n) = &self.flags.redact_length {
return RedactFlags::redact_fixed(fmt, n.get() as usize, self.flags.redact_char);
}
let redactable_string = self.this.to_string();
#[allow(clippy::single_match)]
match self.specialization {
Some(RedactSpecialization::Option) => {
if redactable_string == "None" {
return fmt.write_str("None");
} else if let Some(inner) = redactable_string
.strip_prefix("Some(")
.and_then(|inner| inner.strip_suffix(')'))
{
fmt.write_str("Some(")?;
self.flags.redact_partial(fmt, inner)?;
return fmt.write_char(')');
} else {
return self.flags.redact_full(fmt, &redactable_string);
}
}
None => {}
}
if let RedactionLength::Partial = &self.flags.redact_length {
self.flags.redact_partial(fmt, &redactable_string)
} else {
self.flags.redact_full(fmt, &redactable_string)
}
}
}
pub fn derived_redactable(this: &dyn Display, flags: RedactFlags) -> String {
give_me_a_formatter(|fmt| {
std::fmt::Debug::fmt(
&RedactionFormatter {
this: RedactionTarget::Display(this),
flags,
specialization: None,
},
fmt,
)
})
.to_string()
}