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 enum RedactionContent<'a> {
Asterisks,
Char(char),
Str(&'a str),
}
#[derive(Clone, Copy)]
pub struct RedactFlags {
pub redact_length: RedactionLength,
pub redact_content: RedactionContent<'static>,
}
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();
match &self.redact_content {
RedactionContent::Asterisks => self.redact_partial_with_char(fmt, to_redact, '*', count),
RedactionContent::Char(c) => self.redact_partial_with_char(fmt, to_redact, *c, count),
RedactionContent::Str(s) => fmt.write_str(s),
}
}
fn redact_partial_with_char(
&self,
fmt: &mut std::fmt::Formatter,
to_redact: &str,
redact_char: char,
count: usize,
) -> std::fmt::Result {
if count < Self::MIN_PARTIAL_CHARS {
for char in to_redact.chars() {
if char.is_alphanumeric() {
fmt.write_char(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(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 {
match &self.redact_content {
RedactionContent::Asterisks => self.redact_full_with_char(fmt, to_redact, '*'),
RedactionContent::Char(c) => self.redact_full_with_char(fmt, to_redact, *c),
RedactionContent::Str(s) => fmt.write_str(s),
}
}
fn redact_full_with_char(
&self,
fmt: &mut std::fmt::Formatter,
to_redact: &str,
redact_char: char,
) -> std::fmt::Result {
for char in to_redact.chars() {
if char.is_whitespace() || !char.is_alphanumeric() {
fmt.write_char(char)?;
} else {
fmt.write_char(redact_char)?;
}
}
Ok(())
}
fn redact_fixed_str(fmt: &mut std::fmt::Formatter, width: usize, s: &str) -> std::fmt::Result {
let mut buf = String::with_capacity(width);
for char in s.chars().take(width) {
buf.push(char);
}
fmt.write_str(&buf)
}
fn redact_fixed_char(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 Display for RedactionTarget<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
match self {
RedactionTarget::Debug { this, alternate: false } => write!(f, "{this:?}"),
RedactionTarget::Debug { this, alternate: true } => write!(f, "{this:#?}"),
RedactionTarget::Display(this) => write!(f, "{this}"),
}
}
}
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 {
match &self.flags.redact_content {
RedactionContent::Asterisks => {
return RedactFlags::redact_fixed_char(fmt, n.get() as usize, '*');
}
RedactionContent::Char(c) => {
return RedactFlags::redact_fixed_char(fmt, n.get() as usize, *c);
}
RedactionContent::Str(s) => {
return RedactFlags::redact_fixed_str(fmt, n.get() as usize, s);
}
}
}
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(")?;
if let RedactionLength::Partial = &self.flags.redact_length {
self.flags.redact_partial(fmt, inner)?;
} else {
self.flags.redact_full(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()
}