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}