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/// Type alias for Results with boxed errors to avoid large error variants
58type BoxedResult<T> = std::result::Result<T, Box<facet_macros_parse::Error>>;
59
60/// Derives `std::fmt::Debug` with automatic redaction for sensitive fields.
61///
62/// # Supported Types
63/// - Named structs
64/// - Tuple structs
65/// - Unit structs
66/// - Enums (all variant types: unit, tuple, struct)
67/// - Generic types (with lifetimes and type parameters)
68/// - Nested structures
69///
70/// # Requirements
71/// - Must also derive or implement `Facet`
72/// - Sensitive fields marked with `#[facet(sensitive)]`
73/// - For generic types, type parameters must implement `Debug`
74///
75/// # Performance
76///
77/// There should be minimal runtime overhead:
78/// - **One** `Self::SHAPE` const access per `Debug::fmt` call
79/// - **One** `field.is_sensitive()` check per field (O(1) bitflag check)
80/// - No heap allocations beyond standard `Debug` formatting
81/// - Zero cost for non-sensitive fields (formatted normally)
82///
83/// Benchmarks show <1% overhead compared to hand-written Debug implementations,
84/// and of course a lot less tedium writing code.
85///
86/// # Security
87///
88/// Fails safe when metadata is unavailable:
89/// - **Structs**: Redacts all fields with `"[REDACTED:NO_METADATA]"`
90/// - **Enums**: Outputs only the type name, no variant or field data
91///
92/// # Varied examples
93///
94/// Basic struct with mixed sensitive/non-sensitive fields:
95/// ```ignore
96/// use facet::Facet;
97/// use safe_debug::SafeDebug;
98///
99/// #[derive(Facet, SafeDebug)]
100/// struct PatientRecord {
101///     id: String,              // public field
102///     #[facet(sensitive)]
103///     ssn: String,             // redacted as [REDACTED]
104///     #[facet(sensitive)]
105///     medical_history: String, // redacted as [REDACTED]
106/// }
107/// ```
108///
109/// Enums with sensitive fields in specific variants:
110/// ```ignore
111/// #[derive(Facet, SafeDebug)]
112/// enum ApiResponse {
113///     Success { code: u16, data: String },
114///     Error {
115///         code: u16,
116///         #[facet(sensitive)]
117///         error_details: String,  // only redacted in Error variant
118///     },
119///     Pending(u64),
120/// }
121/// ```
122///
123/// Generic types (automatically adds required trait bounds):
124/// ```ignore
125/// #[derive(Facet, SafeDebug)]
126/// struct Container<T> {
127///     id: u32,
128///     #[facet(sensitive)]
129///     secret: T,  // T will be redacted
130/// }
131/// // Expands to: impl<T: Debug + Facet> Debug for Container<T>
132/// ```
133///
134/// Types with lifetimes:
135/// ```ignore
136/// #[derive(Facet, SafeDebug)]
137/// struct BorrowedData<'a> {
138///     public: &'a str,
139///     #[facet(sensitive)]
140///     token: &'a str,
141/// }
142/// ```
143#[proc_macro_derive(SafeDebug)]
144pub fn derive_safe_debug(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
145    let input = TokenStream::from(input);
146
147    match derive_facet_debug_impl(&input) {
148        Ok(output) => output.into(),
149        Err(e) => {
150            let error = format!(
151                "SafeDebug derive error: {}\n\n\
152                 Help: SafeDebug requires the Facet trait to be derived or implemented.\n\
153                 Make sure you have `#[derive(Facet, SafeDebug)]` on your type.\n\
154                 \n\
155                 For types with generics, ensure all type parameters implement Debug.\n\
156                 Example: `struct Foo<T: Debug>`",
157                e
158            );
159            quote! {
160                compile_error!(#error);
161            }
162            .into()
163        }
164    }
165}
166
167fn derive_facet_debug_impl(input: &TokenStream) -> BoxedResult<TokenStream> {
168    let mut iter = input.to_token_iter();
169
170    // Parse the input as an ADT (struct or enum)
171    let adt: AdtDecl = iter.parse().map_err(Box::new)?;
172
173    match adt {
174        AdtDecl::Struct(s) => derive_for_struct(s),
175        AdtDecl::Enum(e) => derive_for_enum(e),
176    }
177}
178
179/// Extracts trait bounds for generic type parameters.
180///
181/// This macro generates trait bounds (`T: Debug + Facet`) for all type
182/// parameters in the generics list. Lifetime parameters are skipped (they don't
183/// need bounds).
184///
185/// # Why String Parsing?
186///
187/// Because I'm lazy? A better answer: We use string parsing instead of AST
188/// traversal because `facet-macros-parse` provides generics as an opaque
189/// `TokenStream`. While this is less elegant than structural parsing, it's
190/// reliable for the common cases (simple type parameters, lifetimes,
191/// and basic bounds). Complex const generics or exotic syntax might not be
192/// handled perfectly. If you encounter this, please file a with an example and
193/// I'll find a different implementation for you.
194///
195/// # Usage
196///
197/// ```ignore
198/// let trait_bounds = extract_type_param_bounds!(parsed.generics.as_ref());
199/// ```
200///
201/// # Returns
202///
203/// A `Vec` of quoted token streams, each representing a bound like:
204/// `T: ::std::fmt::Debug + for<'__facet> ::facet::Facet<'__facet>`
205///
206/// The HRTB (Higher-Rank Trait Bound) `for<'__facet>` allows the Facet trait
207/// bound to work with any lifetime, since Facet has a lifetime parameter.
208macro_rules! extract_type_param_bounds {
209    ($generics:expr) => {{
210        if let Some(ref g) = $generics {
211            let params_str = g.params.to_token_stream().to_string();
212
213            params_str
214                .split(',')
215                .filter_map(|param| {
216                    let param = param.trim();
217                    // Skip empty or lifetime parameters
218                    if param.is_empty() || param.starts_with('\'') {
219                        return None;
220                    }
221
222                    // Extract the identifier (first token before : or whitespace)
223                    let ident_name = param
224                        .split(|c: char| c.is_whitespace() || c == ':')
225                        .find(|s| !s.is_empty())?;
226
227                    // Create identifier and bound - need both Debug and Facet
228                    let ident = quote::format_ident!("{}", ident_name);
229                    Some(quote! { #ident: ::std::fmt::Debug + for<'__facet> ::facet::Facet<'__facet> })
230                })
231                .collect::<Vec<_>>()
232        } else {
233            vec![]
234        }
235    }};
236}
237
238/// Generates the Debug implementation for a struct (named, tuple, or unit).
239///
240/// This function handles all three struct kinds and generates code that:
241/// 1. Accesses the Facet-provided Shape metadata
242/// 2. Checks each field's sensitivity flag
243/// 3. Redacts sensitive fields, shows others
244/// 4. Falls back to redacting everything if metadata is unavailable
245///
246/// # Security Philosophy
247///
248/// This takes a fail-safe approach: if reflection fails, ALL fields are
249/// redacted to prevent accidental data leakage.
250fn derive_for_struct(parsed: Struct) -> BoxedResult<TokenStream> {
251    let struct_name = &parsed.name;
252
253    // Extract generics - convert to TokenStream
254    let generics = if let Some(ref g) = parsed.generics {
255        // Convert the generic params to a token stream manually
256        let params_ts = g.params.to_token_stream();
257        quote! { < #params_ts > }
258    } else {
259        quote! {}
260    };
261
262    // Extract where clause from the struct kind and track if it exists
263    let (has_existing_where, existing_where_clause) = match &parsed.kind {
264        StructKind::Struct { clauses, .. }
265        | StructKind::TupleStruct { clauses, .. }
266        | StructKind::UnitStruct { clauses, .. } => {
267            if let Some(w) = clauses {
268                let clauses_ts = w.to_token_stream();
269                (true, clauses_ts)
270            } else {
271                (false, quote! {})
272            }
273        }
274    };
275
276    // Build trait bounds for generic type parameters
277    let trait_bounds = extract_type_param_bounds!(parsed.generics);
278
279    // Combine existing where clause with trait bounds
280    let where_clause = if !trait_bounds.is_empty() {
281        if has_existing_where {
282            // Existing where clause already has "where", just append bounds
283            quote! { #existing_where_clause, #(#trait_bounds),* }
284        } else {
285            // No existing where clause, create one
286            quote! { where #(#trait_bounds),* }
287        }
288    } else {
289        // No bounds to add, use existing or empty
290        if has_existing_where {
291            quote! { #existing_where_clause }
292        } else {
293            quote! {}
294        }
295    };
296
297    // Generate field formatting code based on struct kind
298    let format_fields = match parsed.kind {
299        StructKind::Struct { ref fields, .. } => {
300            // Generate field checks (when metadata is available)
301            let field_checks: Vec<_> = fields
302                .content
303                .iter()
304                .enumerate()
305                .map(|(idx, field)| {
306                    let field_name = &field.value.name;
307                    let field_name_str = field_name.to_string();
308
309                    quote! {
310                        if struct_type.fields[#idx].is_sensitive() {
311                            debug_struct.field(#field_name_str, &"[REDACTED]");
312                        } else {
313                            debug_struct.field(#field_name_str, &self.#field_name);
314                        }
315                    }
316                })
317                .collect();
318
319            // Generate fallback (when metadata is unavailable) - redact for safety
320            let fallback_fields: Vec<_> = fields
321                .content
322                .iter()
323                .map(|field| {
324                    let field_name = &field.value.name;
325                    let field_name_str = field_name.to_string();
326
327                    quote! {
328                        debug_struct.field(#field_name_str, &"[REDACTED:NO_METADATA]");
329                    }
330                })
331                .collect();
332
333            quote! {
334                let mut debug_struct = f.debug_struct(stringify!(#struct_name));
335
336                // Hoist Shape access - check once for all fields (performance optimization)
337                let shape = Self::SHAPE;
338                if let ::facet::Type::User(::facet::UserType::Struct(ref struct_type)) = shape.ty {
339                    // Normal path: We have metadata, so check each field's sensitivity
340                    #(#field_checks)*
341                } else {
342                    // Security fallback: metadata unavailable. This is unlikely, but we're being
343                    // super-paranoid because we're worried about personal data.
344                    // We redact ALL fields to prevent accidental data leakage even in unexpected
345                    // error cases.
346                    #(#fallback_fields)*
347                }
348
349                debug_struct.finish()
350            }
351        }
352        StructKind::TupleStruct { ref fields, .. } => {
353            // Generate field checks (when metadata is available)
354            let field_checks: Vec<_> = fields
355                .content
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.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) -> BoxedResult<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        .iter()
471        .enumerate()
472        .map(|(variant_idx, variant_like)| {
473            let variant = &variant_like.value.variant;
474
475            match variant {
476                EnumVariantData::Unit(unit_variant) => {
477                    let variant_name = &unit_variant.name;
478                    // Unit variant: MyEnum::Variant
479                    quote! {
480                        #enum_name::#variant_name => {
481                            write!(f, concat!(stringify!(#enum_name), "::", stringify!(#variant_name)))
482                        }
483                    }
484                }
485                EnumVariantData::Tuple(tuple_variant) => {
486                    let variant_name = &tuple_variant.name;
487                    let field_count = tuple_variant.fields.content.len();
488
489                    // Generate field bindings: _field_0, _field_1, _field_2, ...
490                    let bindings: Vec<_> = (0..field_count).map(|i| quote::format_ident!("_field_{}", i)).collect();
491
492                    // Generate field checks with sensitivity
493                    let field_checks: Vec<_> = bindings
494                        .iter()
495                        .enumerate()
496                        .map(|(field_idx, binding)| {
497                            quote! {
498                                if enum_type.variants[#variant_idx].data.fields[#field_idx].is_sensitive() {
499                                    debug_tuple.field(&"[REDACTED]");
500                                } else {
501                                    debug_tuple.field(#binding);
502                                }
503                            }
504                        })
505                        .collect();
506
507                    quote! {
508                        #enum_name::#variant_name(#(#bindings),*) => {
509                            let mut debug_tuple = f.debug_tuple(
510                                concat!(stringify!(#enum_name), "::", stringify!(#variant_name))
511                            );
512                            #(#field_checks)*
513                            debug_tuple.finish()
514                        }
515                    }
516                }
517                EnumVariantData::Struct(struct_variant) => {
518                    let variant_name = &struct_variant.name;
519                    let fields = &struct_variant.fields.content;
520
521                    // Generate field bindings: ref field1, ref field2, ...
522                    let field_names: Vec<_> = fields.iter().map(|field| &field.value.name).collect();
523
524                    // Generate field checks with sensitivity
525                    let field_checks: Vec<_> = field_names
526                        .iter()
527                        .enumerate()
528                        .map(|(field_idx, name)| {
529                            let name_str = name.to_string();
530                            quote! {
531                                if enum_type.variants[#variant_idx].data.fields[#field_idx].is_sensitive() {
532                                    debug_struct.field(#name_str, &"[REDACTED]");
533                                } else {
534                                    debug_struct.field(#name_str, #name);
535                                }
536                            }
537                        })
538                        .collect();
539
540                    quote! {
541                        #enum_name::#variant_name { #(#field_names),* } => {
542                            let mut debug_struct = f.debug_struct(
543                                concat!(stringify!(#enum_name), "::", stringify!(#variant_name))
544                            );
545                            #(#field_checks)*
546                            debug_struct.finish()
547                        }
548                    }
549                }
550            }
551        })
552        .collect();
553
554    let impl_block = quote! {
555        impl #generics ::std::fmt::Debug for #enum_name #generics #where_clause {
556            fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
557                // Hoist Shape access - check once at method entry (performance optimization)
558                let shape = Self::SHAPE;
559                if let ::facet::Type::User(::facet::UserType::Enum(ref enum_type)) = shape.ty {
560                    // Normal path: We have metadata, so check field sensitivity for each variant
561                    match self {
562                        #(#match_arms),*
563                    }
564                } else {
565                    // Security fallback: Metadata unavailable (should never happen in normal use).
566                    // Fail-conservative by writing ONLY the type name with no variant or field data.
567                    // Repeat previous comments about why we're doing this.
568                    write!(f, "{}", stringify!(#enum_name))
569                }
570            }
571        }
572    };
573
574    Ok(impl_block)
575}