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}