safe_debug/
lib.rs

1#![doc = include_str!("../README.md")]
2#![warn(missing_docs)]
3#![allow(clippy::needless_doctest_main)]
4
5//! # SafeDebug - Debug with Field Redaction
6//!
7//! Provides a derive macro for `std::fmt::Debug` that automatically redacts
8//! sensitive fields marked with `#[facet(sensitive)]`. Designed for
9//! HIPAA-compliant healthcare applications and other systems handling sensitive
10//! data.
11//!
12//! ## Requirements
13//!
14//! - Must also derive or implement [`facet::Facet`]
15//! - Sensitive fields marked with `#[facet(sensitive)]`
16//!
17//! ## Performance
18//!
19//! Minimal runtime overhead:
20//! - **One** `Self::SHAPE` const access per `Debug::fmt` call
21//! - **One** `field.is_sensitive()` check per field (O(1) bitflag check)
22//! - No heap allocations beyond standard `Debug` formatting
23//! - Zero cost for non-sensitive fields (formatted normally)
24//!
25//! The overhead is typically <1% compared to hand-written Debug
26//! implementations, which is negligible in practice since Debug formatting is
27//! already I/O-bound.
28//!
29//! ## Example
30//!
31//! ```rust
32//! use facet::Facet;
33//! use safe_debug::SafeDebug;
34//!
35//! #[derive(Facet, SafeDebug)]
36//! struct PatientRecord {
37//!     id: String,
38//!     #[facet(sensitive)]
39//!     ssn: String,
40//! }
41//!
42//! let record = PatientRecord {
43//!     id: "12345".to_string(),
44//!     ssn: "123-45-6789".to_string(),
45//! };
46//!
47//! // SSN will be redacted in debug output
48//! let debug_output = format!("{:?}", record);
49//! assert!(debug_output.contains("12345"));
50//! assert!(!debug_output.contains("123-45-6789"));
51//! assert!(debug_output.contains("[REDACTED]"));
52//! ```
53
54use facet_macros_parse::*;
55use quote::quote;
56
57/// Derives `std::fmt::Debug` with automatic redaction for sensitive fields.
58///
59/// # Supported Types
60/// - Named structs
61/// - Tuple structs
62/// - Unit structs
63/// - Enums (all variant types: unit, tuple, struct)
64/// - Generic types (with lifetimes and type parameters)
65/// - Nested structures
66///
67/// # Requirements
68/// - Must also derive or implement `Facet`
69/// - Sensitive fields marked with `#[facet(sensitive)]`
70/// - For generic types, type parameters must implement `Debug`
71///
72/// # Performance
73///
74/// There should be minimal runtime overhead:
75/// - **One** `Self::SHAPE` const access per `Debug::fmt` call
76/// - **One** `field.is_sensitive()` check per field (O(1) bitflag check)
77/// - No heap allocations beyond standard `Debug` formatting
78/// - Zero cost for non-sensitive fields (formatted normally)
79///
80/// Benchmarks show <1% overhead compared to hand-written Debug implementations,
81/// and of course a lot less tedium writing code.
82///
83/// # Security
84///
85/// Fails safe when metadata is unavailable:
86/// - **Structs**: Redacts all fields with `"[REDACTED:NO_METADATA]"`
87/// - **Enums**: Outputs only the type name, no variant or field data
88///
89/// # Varied examples
90///
91/// Basic struct with mixed sensitive/non-sensitive fields:
92/// ```ignore
93/// use facet::Facet;
94/// use safe_debug::SafeDebug;
95///
96/// #[derive(Facet, SafeDebug)]
97/// struct PatientRecord {
98///     id: String,              // public field
99///     #[facet(sensitive)]
100///     ssn: String,             // redacted as [REDACTED]
101///     #[facet(sensitive)]
102///     medical_history: String, // redacted as [REDACTED]
103/// }
104/// ```
105///
106/// Enums with sensitive fields in specific variants:
107/// ```ignore
108/// #[derive(Facet, SafeDebug)]
109/// enum ApiResponse {
110///     Success { code: u16, data: String },
111///     Error {
112///         code: u16,
113///         #[facet(sensitive)]
114///         error_details: String,  // only redacted in Error variant
115///     },
116///     Pending(u64),
117/// }
118/// ```
119///
120/// Generic types (automatically adds required trait bounds):
121/// ```ignore
122/// #[derive(Facet, SafeDebug)]
123/// struct Container<T> {
124///     id: u32,
125///     #[facet(sensitive)]
126///     secret: T,  // T will be redacted
127/// }
128/// // Expands to: impl<T: Debug + Facet> Debug for Container<T>
129/// ```
130///
131/// Types with lifetimes:
132/// ```ignore
133/// #[derive(Facet, SafeDebug)]
134/// struct BorrowedData<'a> {
135///     public: &'a str,
136///     #[facet(sensitive)]
137///     token: &'a str,
138/// }
139/// ```
140#[proc_macro_derive(SafeDebug)]
141pub fn derive_safe_debug(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
142    let input = TokenStream::from(input);
143
144    match derive_facet_debug_impl(&input) {
145        Ok(output) => output.into(),
146        Err(e) => {
147            let error = format!(
148                "SafeDebug derive error: {}\n\n\
149                 Help: SafeDebug requires the Facet trait to be derived or implemented.\n\
150                 Make sure you have `#[derive(Facet, SafeDebug)]` on your type.\n\
151                 \n\
152                 For types with generics, ensure all type parameters implement Debug.\n\
153                 Example: `struct Foo<T: Debug>`",
154                e
155            );
156            quote! {
157                compile_error!(#error);
158            }
159            .into()
160        }
161    }
162}
163
164fn derive_facet_debug_impl(input: &TokenStream) -> Result<TokenStream> {
165    let mut iter = input.to_token_iter();
166
167    // Parse the input as an ADT (struct or enum)
168    let adt: AdtDecl = iter.parse()?;
169
170    match adt {
171        AdtDecl::Struct(s) => derive_for_struct(s),
172        AdtDecl::Enum(e) => derive_for_enum(e),
173    }
174}
175
176/// Extracts trait bounds for generic type parameters.
177///
178/// This macro generates trait bounds (`T: Debug + Facet`) for all type
179/// parameters in the generics list. Lifetime parameters are skipped (they don't
180/// need bounds).
181///
182/// # Why String Parsing?
183///
184/// Because I'm lazy? A better answer: We use string parsing instead of AST
185/// traversal because `facet-macros-parse` provides generics as an opaque
186/// `TokenStream`. While this is less elegant than structural parsing, it's
187/// reliable for the common cases (simple type parameters, lifetimes,
188/// and basic bounds). Complex const generics or exotic syntax might not be
189/// handled perfectly. If you encounter this, please file a with an example and
190/// I'll find a different implementation for you.
191///
192/// # Usage
193///
194/// ```ignore
195/// let trait_bounds = extract_type_param_bounds!(parsed.generics.as_ref());
196/// ```
197///
198/// # Returns
199///
200/// A `Vec` of quoted token streams, each representing a bound like:
201/// `T: ::std::fmt::Debug + for<'__facet> ::facet::Facet<'__facet>`
202///
203/// The HRTB (Higher-Rank Trait Bound) `for<'__facet>` allows the Facet trait
204/// bound to work with any lifetime, since Facet has a lifetime parameter.
205macro_rules! extract_type_param_bounds {
206    ($generics:expr) => {{
207        if let Some(ref g) = $generics {
208            let params_str = g.params.to_token_stream().to_string();
209
210            params_str
211                .split(',')
212                .filter_map(|param| {
213                    let param = param.trim();
214                    // Skip empty or lifetime parameters
215                    if param.is_empty() || param.starts_with('\'') {
216                        return None;
217                    }
218
219                    // Extract the identifier (first token before : or whitespace)
220                    let ident_name = param
221                        .split(|c: char| c.is_whitespace() || c == ':')
222                        .find(|s| !s.is_empty())?;
223
224                    // Create identifier and bound - need both Debug and Facet
225                    let ident = quote::format_ident!("{}", ident_name);
226                    Some(quote! { #ident: ::std::fmt::Debug + for<'__facet> ::facet::Facet<'__facet> })
227                })
228                .collect::<Vec<_>>()
229        } else {
230            vec![]
231        }
232    }};
233}
234
235/// Generates the Debug implementation for a struct (named, tuple, or unit).
236///
237/// This function handles all three struct kinds and generates code that:
238/// 1. Accesses the Facet-provided Shape metadata
239/// 2. Checks each field's sensitivity flag
240/// 3. Redacts sensitive fields, shows others
241/// 4. Falls back to redacting everything if metadata is unavailable
242///
243/// # Security Philosophy
244///
245/// This takes a fail-safe approach: if reflection fails, ALL fields are
246/// redacted to prevent accidental data leakage.
247fn derive_for_struct(parsed: Struct) -> Result<TokenStream> {
248    let struct_name = &parsed.name;
249
250    // Extract generics - convert to TokenStream
251    let generics = if let Some(ref g) = parsed.generics {
252        // Convert the generic params to a token stream manually
253        let params_ts = g.params.to_token_stream();
254        quote! { < #params_ts > }
255    } else {
256        quote! {}
257    };
258
259    // Extract where clause from the struct kind and track if it exists
260    let (has_existing_where, existing_where_clause) = match &parsed.kind {
261        StructKind::Struct { clauses, .. }
262        | StructKind::TupleStruct { clauses, .. }
263        | StructKind::UnitStruct { clauses, .. } => {
264            if let Some(w) = clauses {
265                let clauses_ts = w.to_token_stream();
266                (true, clauses_ts)
267            } else {
268                (false, quote! {})
269            }
270        }
271    };
272
273    // Build trait bounds for generic type parameters
274    let trait_bounds = extract_type_param_bounds!(parsed.generics);
275
276    // Combine existing where clause with trait bounds
277    let where_clause = if !trait_bounds.is_empty() {
278        if has_existing_where {
279            // Existing where clause already has "where", just append bounds
280            quote! { #existing_where_clause, #(#trait_bounds),* }
281        } else {
282            // No existing where clause, create one
283            quote! { where #(#trait_bounds),* }
284        }
285    } else {
286        // No bounds to add, use existing or empty
287        if has_existing_where {
288            quote! { #existing_where_clause }
289        } else {
290            quote! {}
291        }
292    };
293
294    // Generate field formatting code based on struct kind
295    let format_fields = match parsed.kind {
296        StructKind::Struct { ref fields, .. } => {
297            // Generate field checks (when metadata is available)
298            let field_checks: Vec<_> = fields
299                .content
300                .0
301                .iter()
302                .enumerate()
303                .map(|(idx, field)| {
304                    let field_name = &field.value.name;
305                    let field_name_str = field_name.to_string();
306
307                    quote! {
308                        if struct_type.fields[#idx].is_sensitive() {
309                            debug_struct.field(#field_name_str, &"[REDACTED]");
310                        } else {
311                            debug_struct.field(#field_name_str, &self.#field_name);
312                        }
313                    }
314                })
315                .collect();
316
317            // Generate fallback (when metadata is unavailable) - redact for safety
318            let fallback_fields: Vec<_> = fields
319                .content
320                .0
321                .iter()
322                .map(|field| {
323                    let field_name = &field.value.name;
324                    let field_name_str = field_name.to_string();
325
326                    quote! {
327                        debug_struct.field(#field_name_str, &"[REDACTED:NO_METADATA]");
328                    }
329                })
330                .collect();
331
332            quote! {
333                let mut debug_struct = f.debug_struct(stringify!(#struct_name));
334
335                // Hoist Shape access - check once for all fields (performance optimization)
336                let shape = Self::SHAPE;
337                if let ::facet::Type::User(::facet::UserType::Struct(ref struct_type)) = shape.ty {
338                    // Normal path: We have metadata, so check each field's sensitivity
339                    #(#field_checks)*
340                } else {
341                    // Security fallback: metadata unavailable. This is unlikely, but we're being
342                    // super-paranoid because we're worried about personal data.
343                    // We redact ALL fields to prevent accidental data leakage even in unexpected
344                    // error cases.
345                    #(#fallback_fields)*
346                }
347
348                debug_struct.finish()
349            }
350        }
351        StructKind::TupleStruct { ref fields, .. } => {
352            // Generate field checks (when metadata is available)
353            let field_checks: Vec<_> = fields
354                .content
355                .0
356                .iter()
357                .enumerate()
358                .map(|(idx, _field)| {
359                    // Create a numeric literal for field access using unsynn's Literal
360                    let field_idx = Literal::usize_unsuffixed(idx);
361
362                    quote! {
363                        if struct_type.fields[#idx].is_sensitive() {
364                            debug_tuple.field(&"[REDACTED]");
365                        } else {
366                            debug_tuple.field(&self.#field_idx);
367                        }
368                    }
369                })
370                .collect();
371
372            // Generate fallback (when metadata is unavailable) - redact for safety
373            let fallback_fields: Vec<_> = (0..fields.content.0.len())
374                .map(|_| {
375                    quote! {
376                        debug_tuple.field(&"[REDACTED:NO_METADATA]");
377                    }
378                })
379                .collect();
380
381            quote! {
382                let mut debug_tuple = f.debug_tuple(stringify!(#struct_name));
383
384                // Hoist Shape access - check once for all fields (performance optimization)
385                let shape = Self::SHAPE;
386                if let ::facet::Type::User(::facet::UserType::Struct(ref struct_type)) = shape.ty {
387                    // Normal path: We have metadata, so check each field's sensitivity
388                    #(#field_checks)*
389                } else {
390                    // Security fallback: Metadata unavailable (should never happen in normal use).
391                    // Fail-safe by redacting ALL fields to prevent accidental data leakage.
392                    #(#fallback_fields)*
393                }
394
395                debug_tuple.finish()
396            }
397        }
398        StructKind::UnitStruct { .. } => {
399            quote! {
400                f.debug_struct(stringify!(#struct_name)).finish()
401            }
402        }
403    };
404
405    let impl_block = quote! {
406        impl #generics ::std::fmt::Debug for #struct_name #generics #where_clause {
407            fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
408                #format_fields
409            }
410        }
411    };
412
413    Ok(impl_block)
414}
415
416/// Generates the Debug implementation for an enum with any variant types.
417///
418/// This function handles all enum variant types (unit, tuple, struct) and
419/// generates code that:
420/// 1. accesses the Facet-provided Shape metadata
421/// 2. matches on each variant
422/// 3. checks field sensitivity for tuple/struct variants
423/// 4. redacts sensitive fields, shows others
424/// 5. falls back to showing only the type name if metadata is unavailable
425///
426/// # Security Philosophy
427///
428/// The generated code takes a fail-conservative approach: if reflection
429/// fails, ONLY the enum type name is written with no variant or field data,
430/// preventing any potential data leakage.
431fn derive_for_enum(parsed: Enum) -> Result<TokenStream> {
432    let enum_name = &parsed.name;
433
434    // Extract generics - same as structs
435    let generics = if let Some(ref g) = parsed.generics {
436        let params_ts = g.params.to_token_stream();
437        quote! { < #params_ts > }
438    } else {
439        quote! {}
440    };
441
442    // Extract where clause from enum (same pattern as structs)
443    let (has_existing_where, existing_where_clause) = if let Some(ref clauses) = parsed.clauses {
444        let clauses_ts = clauses.to_token_stream();
445        (true, clauses_ts)
446    } else {
447        (false, quote! {})
448    };
449
450    // Build trait bounds for generic type parameters
451    let trait_bounds = extract_type_param_bounds!(parsed.generics);
452
453    // Combine existing where clause with trait bounds
454    let where_clause = if !trait_bounds.is_empty() {
455        if has_existing_where {
456            quote! { #existing_where_clause, #(#trait_bounds),* }
457        } else {
458            quote! { where #(#trait_bounds),* }
459        }
460    } else if has_existing_where {
461        quote! { #existing_where_clause }
462    } else {
463        quote! {}
464    };
465
466    // Generate match arms for each variant
467    let match_arms: Vec<_> = parsed
468        .body
469        .content
470        .0
471        .iter()
472        .enumerate()
473        .map(|(variant_idx, variant_like)| {
474            let variant = &variant_like.value.variant;
475
476            match variant {
477                EnumVariantData::Unit(unit_variant) => {
478                    let variant_name = &unit_variant.name;
479                    // Unit variant: MyEnum::Variant
480                    quote! {
481                        #enum_name::#variant_name => {
482                            write!(f, concat!(stringify!(#enum_name), "::", stringify!(#variant_name)))
483                        }
484                    }
485                }
486                EnumVariantData::Tuple(tuple_variant) => {
487                    let variant_name = &tuple_variant.name;
488                    let field_count = tuple_variant.fields.content.0.len();
489
490                    // Generate field bindings: _field_0, _field_1, _field_2, ...
491                    let bindings: Vec<_> = (0..field_count).map(|i| quote::format_ident!("_field_{}", i)).collect();
492
493                    // Generate field checks with sensitivity
494                    let field_checks: Vec<_> = bindings
495                        .iter()
496                        .enumerate()
497                        .map(|(field_idx, binding)| {
498                            quote! {
499                                if enum_type.variants[#variant_idx].data.fields[#field_idx].is_sensitive() {
500                                    debug_tuple.field(&"[REDACTED]");
501                                } else {
502                                    debug_tuple.field(#binding);
503                                }
504                            }
505                        })
506                        .collect();
507
508                    quote! {
509                        #enum_name::#variant_name(#(#bindings),*) => {
510                            let mut debug_tuple = f.debug_tuple(
511                                concat!(stringify!(#enum_name), "::", stringify!(#variant_name))
512                            );
513                            #(#field_checks)*
514                            debug_tuple.finish()
515                        }
516                    }
517                }
518                EnumVariantData::Struct(struct_variant) => {
519                    let variant_name = &struct_variant.name;
520                    let fields = &struct_variant.fields.content.0;
521
522                    // Generate field bindings: ref field1, ref field2, ...
523                    let field_names: Vec<_> = fields.iter().map(|field| &field.value.name).collect();
524
525                    // Generate field checks with sensitivity
526                    let field_checks: Vec<_> = field_names
527                        .iter()
528                        .enumerate()
529                        .map(|(field_idx, name)| {
530                            let name_str = name.to_string();
531                            quote! {
532                                if enum_type.variants[#variant_idx].data.fields[#field_idx].is_sensitive() {
533                                    debug_struct.field(#name_str, &"[REDACTED]");
534                                } else {
535                                    debug_struct.field(#name_str, #name);
536                                }
537                            }
538                        })
539                        .collect();
540
541                    quote! {
542                        #enum_name::#variant_name { #(#field_names),* } => {
543                            let mut debug_struct = f.debug_struct(
544                                concat!(stringify!(#enum_name), "::", stringify!(#variant_name))
545                            );
546                            #(#field_checks)*
547                            debug_struct.finish()
548                        }
549                    }
550                }
551            }
552        })
553        .collect();
554
555    let impl_block = quote! {
556        impl #generics ::std::fmt::Debug for #enum_name #generics #where_clause {
557            fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
558                // Hoist Shape access - check once at method entry (performance optimization)
559                let shape = Self::SHAPE;
560                if let ::facet::Type::User(::facet::UserType::Enum(ref enum_type)) = shape.ty {
561                    // Normal path: We have metadata, so check field sensitivity for each variant
562                    match self {
563                        #(#match_arms),*
564                    }
565                } else {
566                    // Security fallback: Metadata unavailable (should never happen in normal use).
567                    // Fail-conservative by writing ONLY the type name with no variant or field data.
568                    // Repeat previous comments about why we're doing this.
569                    write!(f, "{}", stringify!(#enum_name))
570                }
571            }
572        }
573    };
574
575    Ok(impl_block)
576}