redacted 0.2.0

Wrappers to control debug formatting of potentially sensitive byte arrays
Documentation
#![warn(clippy::all, clippy::pedantic, rust_2018_idioms, missing_docs)]
#![allow(clippy::expl_impl_clone_on_copy)]
#![doc = include_str!("../README.md")]

use std::{
    cmp::Ordering,
    fmt::{Debug, Display},
    hash::Hash,
    marker::PhantomData,
    ops::{Deref, DerefMut},
};

#[cfg(feature = "serde_support")]
use serde::Deserialize;
#[cfg(feature = "serde_support")]
use serde_bytes::Serialize;
#[cfg(feature = "zeroize")]
use zeroize::Zeroize;

pub mod formatter;

pub use formatter::{Formatter, RedactContents};

/// Convenience wrapper for fully redacting the contents of the value
pub type FullyRedacted<T> = Redacted<T, RedactContents>;

/// [`Deref`]ing wrapper for types that replaces debug outputs with redacted versions
pub struct Redacted<T, F: Formatter> {
    /// The item being wrapped
    item: T,
    /// Phantom for the formatter type
    _formatter: PhantomData<F>,
}

impl<T, F: Formatter> Redacted<T, F> {
    /// Creates a new `Redacted` from the given value
    pub fn new(item: T) -> Self {
        Self {
            item,
            _formatter: PhantomData,
        }
    }
    /// Converts this `Redacted` into its inner type
    pub fn into_inner(self) -> T {
        self.item
    }
}

impl<T: AsRef<[u8]>, F: Formatter> Debug for Redacted<T, F> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        F::debug_fmt(self.item.as_ref(), std::any::type_name::<T>(), f)
    }
}

impl<T: AsRef<[u8]>, F: Formatter> Display for Redacted<T, F> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        F::display_fmt(self.item.as_ref(), std::any::type_name::<T>(), f)
    }
}

impl<T, F: Formatter> Deref for Redacted<T, F> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        &self.item
    }
}

impl<T, F: Formatter> DerefMut for Redacted<T, F> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.item
    }
}

impl<T, F: Formatter> AsRef<T> for Redacted<T, F> {
    fn as_ref(&self) -> &T {
        &self.item
    }
}

impl<T, F: Formatter> AsMut<T> for Redacted<T, F> {
    fn as_mut(&mut self) -> &mut T {
        &mut self.item
    }
}

impl<T, F: Formatter> From<T> for Redacted<T, F> {
    fn from(item: T) -> Self {
        Self::new(item)
    }
}

// Passthrough implementations
impl<T: Default, F: Formatter> Default for Redacted<T, F> {
    fn default() -> Self {
        Self {
            item: Default::default(),
            _formatter: PhantomData,
        }
    }
}
impl<T: Clone, F: Formatter> Clone for Redacted<T, F> {
    fn clone(&self) -> Self {
        Self {
            item: self.item.clone(),
            _formatter: PhantomData,
        }
    }
}

impl<T: Copy, F: Formatter> Copy for Redacted<T, F> {}

impl<T: AsRef<[u8]>, F: Formatter> AsRef<[u8]> for Redacted<T, F> {
    fn as_ref(&self) -> &[u8] {
        self.item.as_ref()
    }
}

impl<T: AsMut<[u8]>, F: Formatter> AsMut<[u8]> for Redacted<T, F> {
    fn as_mut(&mut self) -> &mut [u8] {
        self.item.as_mut()
    }
}

impl<T: PartialEq, F: Formatter> PartialEq for Redacted<T, F> {
    fn eq(&self, other: &Self) -> bool {
        self.item == other.item
    }
}

impl<T: Eq, F: Formatter> Eq for Redacted<T, F> {}

impl<T: Hash, F: Formatter> Hash for Redacted<T, F> {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        self.item.hash(state);
    }
}

impl<T: PartialOrd, F: Formatter> PartialOrd for Redacted<T, F> {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        self.item.partial_cmp(&other.item)
    }
}

impl<T: Ord, F: Formatter> Ord for Redacted<T, F> {
    fn cmp(&self, other: &Self) -> Ordering {
        self.item.cmp(&other.item)
    }
}

#[cfg(feature = "serde_support")]
impl<T: Serialize, F: Formatter> Serialize for Redacted<T, F> {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        self.item.serialize(serializer)
    }
}

#[cfg(feature = "serde_support")]
impl<'de, T: Deserialize<'de>, F: Formatter> Deserialize<'de> for Redacted<T, F> {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let item: T = Deserialize::deserialize(deserializer)?;
        Ok(Redacted {
            item,
            _formatter: PhantomData,
        })
    }
}

#[cfg(feature = "zeroize")]
impl<T: Zeroize, F: Formatter> Zeroize for Redacted<T, F> {
    fn zeroize(&mut self) {
        self.item.zeroize();
    }
}

#[cfg(test)]
mod tests {
    use std::borrow::Cow;

    use super::*;
    // Smoke test with vecs
    #[test]
    fn smoke_vec() {
        let test = Redacted::<_, RedactContents>::new(vec![0_u8; 32]);
        let debug_sample = format!("{:?}", test);
        let display_sample = format!("{}", test);
        assert_eq!(&debug_sample, "[32 BYTES REDACTED]");
        assert_eq!(&display_sample, "[32 BYTES REDACTED]");
        let test = Redacted::<_, RedactContents>::new(vec![0_u8; 64]);
        let debug_sample = format!("{:?}", test);
        let display_sample = format!("{}", test);
        assert_eq!(&debug_sample, "[64 BYTES REDACTED]");
        assert_eq!(&display_sample, "[64 BYTES REDACTED]");
    }
    // Smoke test with arrays
    #[test]
    fn smoke_array() {
        let test = Redacted::<_, RedactContents>::new([0_u8; 32]);
        let debug_sample = format!("{:?}", test);
        let display_sample = format!("{}", test);
        assert_eq!(&debug_sample, "[32 BYTES REDACTED]");
        assert_eq!(&display_sample, "[32 BYTES REDACTED]");
        let test = Redacted::<_, RedactContents>::new([0_u8; 64]);
        let debug_sample = format!("{:?}", test);
        let display_sample = format!("{}", test);
        assert_eq!(&debug_sample, "[64 BYTES REDACTED]");
        assert_eq!(&display_sample, "[64 BYTES REDACTED]");
    }
    // Smoke test with slices
    #[test]
    fn smoke_slice() {
        let array = [0_u8; 32];
        let test = Redacted::<&[u8], RedactContents>::new(&array);
        let debug_sample = format!("{:?}", test);
        let display_sample = format!("{}", test);
        assert_eq!(&debug_sample, "[32 BYTES REDACTED]");
        assert_eq!(&display_sample, "[32 BYTES REDACTED]");
        let array = [0_u8; 64];
        let test = Redacted::<&[u8], RedactContents>::new(&array);
        let debug_sample = format!("{:?}", test);
        let display_sample = format!("{}", test);
        assert_eq!(&debug_sample, "[64 BYTES REDACTED]");
        assert_eq!(&display_sample, "[64 BYTES REDACTED]");
    }
    // Smoke test with cows
    #[test]
    fn smoke_cow() {
        let cow: Cow<'_, [u8]> = vec![0_u8; 32].into();
        let test = Redacted::<_, RedactContents>::new(cow);
        let debug_sample = format!("{:?}", test);
        let display_sample = format!("{}", test);
        assert_eq!(&debug_sample, "[32 BYTES REDACTED]");
        assert_eq!(&display_sample, "[32 BYTES REDACTED]");
        let cow: Cow<'_, [u8]> = vec![0_u8; 64].into();
        let test = Redacted::<_, RedactContents>::new(cow);
        let debug_sample = format!("{:?}", test);
        let display_sample = format!("{}", test);
        assert_eq!(&debug_sample, "[64 BYTES REDACTED]");
        assert_eq!(&display_sample, "[64 BYTES REDACTED]");
    }
}