koek_redact/
lib.rs

1// Redaction library with built-in redaction operations and flexibility to add more.
2//
3// By default, all types that implement Display are redactable. Redacted values are typically obscured
4// entirely, although some types may be partially obscured where limited redaction is feasible (e.g. IP addresses).
5
6#![feature(min_specialization)]
7
8use std::{
9    fmt::Display,
10    net::{IpAddr, Ipv4Addr},
11};
12
13pub trait Redact {
14    // TODO: For the typical case we could avoid the string allocation if we returned &str or a more flexible type.
15    fn redacted(&self) -> String;
16}
17
18// Anything that implements Display is extended to be redactable.
19impl<T: Display> Redact for T {
20    default fn redacted(&self) -> String {
21        String::from(DEFAULT_REDACTED_VALUE)
22    }
23}
24
25// IP addresses have their own special default rules.
26impl Redact for IpAddr {
27    fn redacted(&self) -> String {
28        match self {
29            IpAddr::V4(x) => x.redacted(),
30            IpAddr::V6(x) => x.redacted(),
31        }
32    }
33}
34
35impl Redact for Ipv4Addr {
36    fn redacted(&self) -> String {
37        // IPv4 addresses are redacted by removing the last octet.
38        // 1.2.3.4 -> 1.2.3.xxx
39        let octets = self.octets();
40        format!("{}.{}.{}.xxx", octets[0], octets[1], octets[2])
41    }
42}
43
44const DEFAULT_REDACTED_VALUE: &str = "***";
45
46#[cfg(test)]
47mod tests {
48    use std::net::Ipv6Addr;
49
50    use super::*;
51
52    #[test]
53    fn can_redact_string() {
54        let value = String::from("qax qex qqx");
55        let redacted = value.redacted();
56
57        assert_ne!(value, redacted);
58        assert_eq!(DEFAULT_REDACTED_VALUE, redacted);
59    }
60
61    #[test]
62    fn can_redact_integer() {
63        let value = 1234;
64        let redacted = value.redacted();
65
66        assert_ne!(value.to_string(), redacted);
67        assert_eq!(DEFAULT_REDACTED_VALUE, redacted);
68    }
69
70    #[test]
71    fn can_redact_float() {
72        let value = 12.34;
73        let redacted = value.redacted();
74
75        assert_ne!(value.to_string(), redacted);
76        assert_eq!(DEFAULT_REDACTED_VALUE, redacted);
77    }
78
79    #[test]
80    fn can_redact_ipv4() {
81        let value = Ipv4Addr::new(1, 2, 3, 4);
82        let redacted = value.redacted();
83
84        assert_ne!(value.to_string(), redacted);
85        assert_eq!("1.2.3.xxx", redacted);
86    }
87
88    #[test]
89    fn can_redact_ipv6() {
90        let value = Ipv6Addr::new(1, 2, 3, 4, 5, 6, 7, 8);
91        let redacted = value.redacted();
92
93        assert_ne!(value.to_string(), redacted);
94        assert_eq!(DEFAULT_REDACTED_VALUE, redacted);
95    }
96
97    #[test]
98    fn can_redact_ip() {
99        let value = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4));
100        let redacted = value.redacted();
101
102        assert_ne!(value.to_string(), redacted);
103        assert_eq!("1.2.3.xxx", redacted);
104    }
105
106    struct CustomSecretStructViaDisplay {
107        tell_noone: String,
108    }
109
110    impl Display for CustomSecretStructViaDisplay {
111        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112            f.write_str(&self.tell_noone)
113        }
114    }
115
116    #[test]
117    fn can_redact_custom_struct_via_display() {
118        let value = CustomSecretStructViaDisplay {
119            tell_noone: "the secret value".to_owned(),
120        };
121        let redacted = value.redacted();
122
123        assert_ne!(value.to_string(), redacted);
124        assert_eq!(DEFAULT_REDACTED_VALUE, redacted);
125    }
126
127    struct CustomSecretStructViaCustomLogic {
128        first_name: String,
129        last_name: String,
130    }
131
132    impl Display for CustomSecretStructViaCustomLogic {
133        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134            f.write_fmt(format_args!("{} {}", &self.first_name, &self.last_name))
135        }
136    }
137
138    impl Redact for CustomSecretStructViaCustomLogic {
139        fn redacted(&self) -> String {
140            format!("{}. {}.", &self.first_name[0..1], &self.last_name[0..1])
141        }
142    }
143
144    #[test]
145    fn can_redact_custom_struct_via_custom_logic() {
146        let value = CustomSecretStructViaCustomLogic {
147            first_name: "Firstname".to_owned(),
148            last_name: "Lastname".to_owned(),
149        };
150        let redacted = value.redacted();
151
152        assert_ne!(value.to_string(), redacted);
153        assert_eq!("F. L.", redacted);
154    }
155}