diff --git a/src/lib.rs b/src/lib.rs
index 9dd90ff..6af9df9 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -6,7 +6,7 @@
//! The purpose of this macro is to allow for easy, configurable and efficient redaction of sensitive data in structs and enum variants.
//! This can be used to hide sensitive data in logs or anywhere where personal data should not be exposed or stored.
//!
-//! Redaction is unicode-aware. Only alphanumeric characters are redacted. Whitespace, symbols and other characters are left as-is.
+//! Redaction is unicode-aware by default. Unless opted out of, only alphanumeric characters are redacted. Whitespace, symbols and other characters are left as-is.
//!
//! # Controlling Redaction
//!
@@ -19,6 +19,7 @@
//! | **Modifier** | | **Effects** | | **Default** |
//! |--------------------------------|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---|-----------------------------------------------|
//! | `#[redact(partial)]` | | If the string is long enough, a small part of the
beginning and end will be exposed. If the string is too short to securely expose a portion of it, it will be redacted entirely. | | Disabled. The entire string will be redacted. |
+//! | `#[redact(all_utf8)]` | | Overrides the redaction behavior to redact all characters instead of just alphanumeric characters. | | Disabled. |
//! | `#[redact(with = 'X')]` | | Specifies the `char` the string will be redacted with. | | `'*'` |
//! | `#[redact(fixed = )]` | | If this modifier is present, the length and contents of
the string are completely ignored and the string will always
be redacted as a fixed number of redaction characters. | | Disabled. |
//! | `#[redact(display)]` | | Overrides the redaction behavior to use the type's [`Display`](std::fmt::Display) implementation instead of [`Debug`]. | | Disabled. |
@@ -40,7 +41,7 @@
//! ```rust
//! # use veil_macros::Redact;
//! #[derive(Redact)]
-//! #[redact(all, partial, with = 'X')]
+//! #[redact(all, partial, with = 'X', all_utf8)]
//! struct Foo {
//! redact_me: String,
//! also_redact_me: String,
@@ -56,10 +57,10 @@
//! # use veil_macros::Redact;
//! #[derive(Redact)]
//! struct Foo {
-//! #[redact(partial, with = 'X')]
+//! #[redact(partial, with = 'X', all_utf8)]
//! redact_me: String,
//!
-//! #[redact(partial, with = 'X')]
+//! #[redact(partial, with = 'X', all_utf8)]
//! also_redact_me: String,
//!
//! do_not_redact_me: String,
diff --git a/src/private.rs b/src/private.rs
index 73559ec..6734f54 100644
--- a/src/private.rs
+++ b/src/private.rs
@@ -35,6 +35,9 @@ pub enum RedactionLength {
#[derive(Clone, Copy)]
pub struct RedactFlags {
+ /// If we should skip non-alphanumeric characters when redacting
+ pub skip_non_alphanumeric: bool,
+
/// How much of the data to redact.
pub redact_length: RedactionLength,
@@ -54,7 +57,7 @@ impl RedactFlags {
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() {
+ if char.is_alphanumeric() && self.skip_non_alphanumeric {
fmt.write_char(self.redact_char)?;
} else {
fmt.write_char(char)?;
@@ -87,7 +90,7 @@ impl RedactFlags {
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() {
+ if (char.is_whitespace() || !char.is_alphanumeric()) && self.skip_non_alphanumeric {
fmt.write_char(char)?;
} else {
fmt.write_char(self.redact_char)?;
diff --git a/src/redactor.rs b/src/redactor.rs
index 3d3944b..3c49d78 100644
--- a/src/redactor.rs
+++ b/src/redactor.rs
@@ -162,6 +162,7 @@ impl Redactor {
pub struct RedactorBuilder {
redact_char: Option,
partial: bool,
+ skip_non_alphanumeric: bool,
}
impl RedactorBuilder {
/// Initialize a new redaction flag builder.
@@ -170,6 +171,7 @@ impl RedactorBuilder {
Self {
redact_char: None,
partial: false,
+ skip_non_alphanumeric: true,
}
}
@@ -191,26 +193,31 @@ impl RedactorBuilder {
self
}
+ /// Redact all utf8 characters, not just the alphanumeric ones.
+ ///
+ /// Equivalent to `#[redact(all_utf8)]` when deriving.
+ #[inline(always)]
+ pub const fn all_utf8(mut self) -> Self {
+ self.skip_non_alphanumeric = false;
+ self
+ }
+
/// Build the redaction flags.
///
/// Returns an error if the state of the builder is invalid.
/// The error will be optimised away by the compiler if the builder is valid at compile time, so it's safe and zero-cost to use `unwrap` on the result if you are constructing this at compile time.
#[inline(always)]
pub const fn build(self) -> Result {
- let mut flags = RedactFlags {
+ let flags = RedactFlags {
redact_length: if self.partial {
RedactionLength::Partial
} else {
RedactionLength::Full
},
-
- redact_char: '*',
+ skip_non_alphanumeric: self.skip_non_alphanumeric,
+ redact_char: if let Some(ch) = self.redact_char { ch } else { '*' },
};
- if let Some(char) = self.redact_char {
- flags.redact_char = char;
- }
-
Ok(Redactor(flags))
}
}
diff --git a/veil-macros/src/flags.rs b/veil-macros/src/flags.rs
index 7152e9d..11a4b79 100644
--- a/veil-macros/src/flags.rs
+++ b/veil-macros/src/flags.rs
@@ -105,12 +105,15 @@ pub struct RedactFlags {
/// The character to use for redacting. Defaults to `*`.
pub redact_char: char,
+
+ pub all_utf8: bool,
}
impl Default for RedactFlags {
fn default() -> Self {
Self {
redact_length: RedactionLength::Full,
redact_char: '*',
+ all_utf8: false,
}
}
}
@@ -138,6 +141,8 @@ impl ExtractFlags for RedactFlags {
NonZeroU8::new(int)
.ok_or_else(|| syn::Error::new_spanned(int, "fixed redacting width must be greater than zero"))
})?)
+ } else if meta.path.is_ident("all_utf8") {
+ self.all_utf8 = true;
} else {
return Ok(ParseMeta::Unrecognised);
}
@@ -149,12 +154,14 @@ impl quote::ToTokens for RedactFlags {
let Self {
redact_length,
redact_char,
+ all_utf8,
..
} = self;
tokens.extend(quote! {
redact_length: #redact_length,
- redact_char: #redact_char
+ redact_char: #redact_char,
+ skip_non_alphanumeric: !#all_utf8,
});
}
}
diff --git a/veil-tests/src/redaction_tests.rs b/veil-tests/src/redaction_tests.rs
index bd73a05..1694da0 100644
--- a/veil-tests/src/redaction_tests.rs
+++ b/veil-tests/src/redaction_tests.rs
@@ -10,6 +10,7 @@ pub const SENSITIVE_DATA: &[&str] = &[
"039845734895",
"10 Downing Street",
"SensitiveVariant",
+ "password123!@#",
];
const DEBUGGY_PHRASE: &str = "Hello \"William\"!\nAnd here's the newline...";
@@ -280,6 +281,40 @@ fn test_derive_redactable_modifiers() {
assert_eq!(buffer, "---");
}
+#[test]
+fn test_derive_redactable_all_utf8() {
+ #[derive(Redactable)]
+ #[redact(all_utf8)]
+ struct SensitiveString(String);
+ impl std::fmt::Display for SensitiveString {
+ fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.fmt(fmt)
+ }
+ }
+
+ #[derive(Redactable)]
+ struct SensitiveStringLeaky(String);
+ impl std::fmt::Display for SensitiveStringLeaky {
+ fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.fmt(fmt)
+ }
+ }
+
+ let sensitive = SensitiveString(SENSITIVE_DATA[5].to_string());
+ let sensitive_leaky = SensitiveStringLeaky(SENSITIVE_DATA[5].to_string());
+
+ assert_eq!(sensitive.redact(), "**************");
+ assert_eq!(sensitive_leaky.redact(), "***********!@#");
+
+ let mut buffer = String::new();
+ sensitive.redact_into(&mut buffer).unwrap();
+ assert_eq!(buffer, "**************");
+
+ buffer.clear();
+ sensitive_leaky.redact_into(&mut buffer).unwrap();
+ assert_eq!(buffer, "***********!@#");
+}
+
#[test]
fn test_derive_redactable_dyn() {
#[derive(Redactable)]
@@ -316,6 +351,7 @@ fn test_derive_redactable_dyn() {
fn test_enum_variant_names() {
#[derive(Debug)]
enum Control {
+ #[allow(dead_code)]
Foo(String),
Bar,
}