1#![warn(
13 anonymous_parameters,
14 bare_trait_objects,
15 elided_lifetimes_in_paths,
16 missing_copy_implementations,
17 rust_2018_idioms,
18 trivial_casts,
19 trivial_numeric_casts,
20 unreachable_pub,
21 unsafe_code,
22 unused_extern_crates,
23 unused_import_braces
24)]
25#![warn(
27 clippy::all,
28 clippy::cargo,
29 clippy::dbg_macro,
30 clippy::float_cmp_const,
31 clippy::get_unwrap,
32 clippy::mem_forget,
33 clippy::nursery,
34 clippy::pedantic,
35 clippy::todo,
36 clippy::unwrap_used,
37 clippy::uninlined_format_args
38)]
39#![allow(
41 clippy::default_trait_access,
42 clippy::doc_markdown,
43 clippy::if_not_else,
44 clippy::module_name_repetitions,
45 clippy::multiple_crate_versions,
46 clippy::must_use_candidate,
47 clippy::needless_pass_by_value,
48 clippy::needless_ifs,
49 clippy::use_self,
50 clippy::cargo_common_metadata,
51 clippy::missing_errors_doc,
52 clippy::enum_glob_use,
53 clippy::struct_excessive_bools,
54 clippy::missing_const_for_fn,
55 clippy::redundant_pub_crate,
56 clippy::result_large_err,
57 clippy::future_not_send,
58 clippy::option_if_let_else,
59 clippy::from_over_into,
60 clippy::manual_inspect
61)]
62#![cfg_attr(test, allow(clippy::non_ascii_literal, clippy::unwrap_used))]
64
65#[allow(unused_extern_crates)]
66extern crate proc_macro;
67
68#[cfg(feature = "slog")]
69use proc_macro2::Span;
70use proc_macro2::{Ident, TokenStream};
71use proc_macro_crate::{crate_name, FoundCrate};
72use quote::{format_ident, quote};
73#[cfg(feature = "slog")]
74use syn::parse_quote;
75use syn::{parse_macro_input, spanned::Spanned, Data, DeriveInput, Result};
76
77mod container;
78mod derive_enum;
79mod derive_struct;
80mod generics;
81mod redacted_display;
82mod strategy;
83mod transform;
84mod types;
85use container::{parse_container_options, ContainerOptions};
86use derive_enum::derive_enum;
87use derive_struct::derive_struct;
88use generics::{
89 add_classified_value_bounds, add_clone_bounds, add_container_bounds, add_debug_bounds,
90 add_display_bounds, add_redacted_display_bounds,
91};
92use redacted_display::derive_redacted_display;
93
94#[proc_macro_derive(Sensitive, attributes(sensitive))]
128pub fn derive_sensitive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
129 let input = parse_macro_input!(input as DeriveInput);
130 match expand(input, SlogMode::RedactedJson) {
131 Ok(tokens) => tokens.into(),
132 Err(err) => err.into_compile_error().into(),
133 }
134}
135
136#[proc_macro_derive(SensitiveError, attributes(sensitive, error))]
150pub fn derive_sensitive_error(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
151 let input = parse_macro_input!(input as DeriveInput);
152 match expand(input, SlogMode::RedactedDisplayString) {
153 Ok(tokens) => tokens.into(),
154 Err(err) => err.into_compile_error().into(),
155 }
156}
157
158fn crate_root() -> proc_macro2::TokenStream {
163 match crate_name("redaction") {
164 Ok(FoundCrate::Itself) => quote! { crate },
165 Ok(FoundCrate::Name(name)) => {
166 let ident = format_ident!("{}", name);
167 quote! { ::#ident }
168 }
169 Err(_) => quote! { ::redaction },
170 }
171}
172
173#[cfg(feature = "slog")]
179fn slog_crate() -> Result<proc_macro2::TokenStream> {
180 match crate_name("slog") {
181 Ok(FoundCrate::Itself) => Ok(quote! { crate }),
182 Ok(FoundCrate::Name(name)) => {
183 let ident = format_ident!("{}", name);
184 Ok(quote! { ::#ident })
185 }
186 Err(_) => {
187 let env_value = std::env::var("REDACTION_SLOG_CRATE").map_err(|_| {
188 syn::Error::new(
189 Span::call_site(),
190 "slog support is enabled, but no top-level `slog` crate was found. \
191Set the REDACTION_SLOG_CRATE env var to a path (e.g., `my_log::slog`) or add \
192`slog` as a direct dependency.",
193 )
194 })?;
195 let path = syn::parse_str::<syn::Path>(&env_value).map_err(|_| {
196 syn::Error::new(
197 Span::call_site(),
198 format!("REDACTION_SLOG_CRATE must be a valid Rust path (got `{env_value}`)"),
199 )
200 })?;
201 Ok(quote! { #path })
202 }
203 }
204}
205
206fn crate_path(item: &str) -> proc_macro2::TokenStream {
207 let root = crate_root();
208 let item_ident = syn::parse_str::<syn::Path>(item).expect("redaction crate path should parse");
209 quote! { #root::#item_ident }
210}
211
212struct DeriveOutput {
213 redaction_body: TokenStream,
214 used_generics: Vec<Ident>,
215 classified_generics: Vec<Ident>,
216 debug_redacted_body: TokenStream,
217 debug_redacted_generics: Vec<Ident>,
218 debug_unredacted_body: TokenStream,
219 debug_unredacted_generics: Vec<Ident>,
220 redacted_display_body: Option<TokenStream>,
221 redacted_display_generics: Vec<Ident>,
222 redacted_display_debug_generics: Vec<Ident>,
223 redacted_display_clone_generics: Vec<Ident>,
224 redacted_display_nested_generics: Vec<Ident>,
225}
226
227enum SlogMode {
228 RedactedJson,
229 RedactedDisplayString,
230}
231
232#[allow(clippy::too_many_lines)]
233fn expand(input: DeriveInput, slog_mode: SlogMode) -> Result<TokenStream> {
234 let DeriveInput {
235 ident,
236 generics,
237 data,
238 attrs,
239 ..
240 } = input;
241
242 let ContainerOptions { skip_debug } = parse_container_options(&attrs)?;
243
244 let crate_root = crate_root();
245
246 let redacted_display_output = if matches!(slog_mode, SlogMode::RedactedDisplayString) {
247 Some(derive_redacted_display(&ident, &data, &attrs, &generics)?)
248 } else {
249 None
250 };
251
252 let derive_output = match &data {
253 Data::Struct(data) => {
254 let output = derive_struct(&ident, data.clone(), &generics)?;
255 DeriveOutput {
256 redaction_body: output.redaction_body,
257 used_generics: output.used_generics,
258 classified_generics: output.classified_generics,
259 debug_redacted_body: output.debug_redacted_body,
260 debug_redacted_generics: output.debug_redacted_generics,
261 debug_unredacted_body: output.debug_unredacted_body,
262 debug_unredacted_generics: output.debug_unredacted_generics,
263 redacted_display_body: redacted_display_output
264 .as_ref()
265 .map(|output| output.body.clone()),
266 redacted_display_generics: redacted_display_output
267 .as_ref()
268 .map(|output| output.display_generics.clone())
269 .unwrap_or_default(),
270 redacted_display_debug_generics: redacted_display_output
271 .as_ref()
272 .map(|output| output.debug_generics.clone())
273 .unwrap_or_default(),
274 redacted_display_clone_generics: redacted_display_output
275 .as_ref()
276 .map(|output| output.clone_generics.clone())
277 .unwrap_or_default(),
278 redacted_display_nested_generics: redacted_display_output
279 .as_ref()
280 .map(|output| output.nested_generics.clone())
281 .unwrap_or_default(),
282 }
283 }
284 Data::Enum(data) => {
285 let output = derive_enum(&ident, data.clone(), &generics)?;
286 DeriveOutput {
287 redaction_body: output.redaction_body,
288 used_generics: output.used_generics,
289 classified_generics: output.classified_generics,
290 debug_redacted_body: output.debug_redacted_body,
291 debug_redacted_generics: output.debug_redacted_generics,
292 debug_unredacted_body: output.debug_unredacted_body,
293 debug_unredacted_generics: output.debug_unredacted_generics,
294 redacted_display_body: redacted_display_output
295 .as_ref()
296 .map(|output| output.body.clone()),
297 redacted_display_generics: redacted_display_output
298 .as_ref()
299 .map(|output| output.display_generics.clone())
300 .unwrap_or_default(),
301 redacted_display_debug_generics: redacted_display_output
302 .as_ref()
303 .map(|output| output.debug_generics.clone())
304 .unwrap_or_default(),
305 redacted_display_clone_generics: redacted_display_output
306 .as_ref()
307 .map(|output| output.clone_generics.clone())
308 .unwrap_or_default(),
309 redacted_display_nested_generics: redacted_display_output
310 .as_ref()
311 .map(|output| output.nested_generics.clone())
312 .unwrap_or_default(),
313 }
314 }
315 Data::Union(u) => {
316 return Err(syn::Error::new(
317 u.union_token.span(),
318 "`Sensitive` cannot be derived for unions",
319 ));
320 }
321 };
322
323 let classify_generics = add_container_bounds(generics.clone(), &derive_output.used_generics);
324 let classify_generics =
325 add_classified_value_bounds(classify_generics, &derive_output.classified_generics);
326 let (impl_generics, ty_generics, where_clause) = classify_generics.split_for_impl();
327 let debug_redacted_generics =
328 add_debug_bounds(generics.clone(), &derive_output.debug_redacted_generics);
329 let (debug_redacted_impl_generics, debug_redacted_ty_generics, debug_redacted_where_clause) =
330 debug_redacted_generics.split_for_impl();
331 let debug_unredacted_generics =
332 add_debug_bounds(generics.clone(), &derive_output.debug_unredacted_generics);
333 let (
334 debug_unredacted_impl_generics,
335 debug_unredacted_ty_generics,
336 debug_unredacted_where_clause,
337 ) = debug_unredacted_generics.split_for_impl();
338 let redaction_body = &derive_output.redaction_body;
339 let debug_redacted_body = &derive_output.debug_redacted_body;
340 let debug_unredacted_body = &derive_output.debug_unredacted_body;
341 let debug_impl = if skip_debug {
342 quote! {}
343 } else {
344 quote! {
345 #[cfg(any(test, feature = "testing"))]
346 impl #debug_unredacted_impl_generics ::core::fmt::Debug for #ident #debug_unredacted_ty_generics #debug_unredacted_where_clause {
347 fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
348 #debug_unredacted_body
349 }
350 }
351
352 #[cfg(not(any(test, feature = "testing")))]
353 #[allow(unused_variables)]
354 impl #debug_redacted_impl_generics ::core::fmt::Debug for #ident #debug_redacted_ty_generics #debug_redacted_where_clause {
355 fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
356 #debug_redacted_body
357 }
358 }
359 }
360 };
361
362 let redacted_display_body = derive_output.redacted_display_body.as_ref();
363 let redacted_display_impl = if matches!(slog_mode, SlogMode::RedactedDisplayString) {
364 let redacted_display_generics =
365 add_display_bounds(generics.clone(), &derive_output.redacted_display_generics);
366 let redacted_display_generics = add_debug_bounds(
367 redacted_display_generics,
368 &derive_output.redacted_display_debug_generics,
369 );
370 let redacted_display_generics = add_clone_bounds(
371 redacted_display_generics,
372 &derive_output.redacted_display_clone_generics,
373 );
374 let redacted_display_generics = add_redacted_display_bounds(
375 redacted_display_generics,
376 &derive_output.redacted_display_nested_generics,
377 );
378 let (display_impl_generics, display_ty_generics, display_where_clause) =
379 redacted_display_generics.split_for_impl();
380 let redacted_display_body = redacted_display_body
381 .cloned()
382 .unwrap_or_else(TokenStream::new);
383 quote! {
384 #[cfg(feature = "slog")]
385 impl #display_impl_generics #crate_root::slog::RedactedDisplay for #ident #display_ty_generics #display_where_clause {
386 fn fmt_redacted(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
387 #redacted_display_body
388 }
389 }
390 }
391 } else {
392 quote! {}
393 };
394
395 #[cfg(feature = "slog")]
398 let slog_impl = {
399 let slog_crate = slog_crate()?;
400 let mut slog_generics = generics;
401 let slog_where_clause = slog_generics.make_where_clause();
402 let self_ty: syn::Type = parse_quote!(#ident #ty_generics);
403 match slog_mode {
404 SlogMode::RedactedJson => {
405 slog_where_clause
406 .predicates
407 .push(parse_quote!(#self_ty: ::core::clone::Clone));
408 slog_where_clause
411 .predicates
412 .push(parse_quote!(#self_ty: ::serde::Serialize));
413 slog_where_clause
414 .predicates
415 .push(parse_quote!(#self_ty: #crate_root::slog::IntoRedactedJson));
416 let (slog_impl_generics, slog_ty_generics, slog_where_clause) =
417 slog_generics.split_for_impl();
418 quote! {
419 impl #slog_impl_generics #slog_crate::Value for #ident #slog_ty_generics #slog_where_clause {
420 fn serialize(
421 &self,
422 _record: &#slog_crate::Record<'_>,
423 key: #slog_crate::Key,
424 serializer: &mut dyn #slog_crate::Serializer,
425 ) -> #slog_crate::Result {
426 let redacted = #crate_root::slog::IntoRedactedJson::into_redacted_json(self.clone());
427 #slog_crate::Value::serialize(&redacted, _record, key, serializer)
428 }
429 }
430 }
431 }
432 SlogMode::RedactedDisplayString => {
433 slog_where_clause
434 .predicates
435 .push(parse_quote!(#self_ty: #crate_root::slog::RedactedDisplay));
436 let (slog_impl_generics, slog_ty_generics, slog_where_clause) =
437 slog_generics.split_for_impl();
438 quote! {
439 impl #slog_impl_generics #slog_crate::Value for #ident #slog_ty_generics #slog_where_clause {
440 fn serialize(
441 &self,
442 _record: &#slog_crate::Record<'_>,
443 key: #slog_crate::Key,
444 serializer: &mut dyn #slog_crate::Serializer,
445 ) -> #slog_crate::Result {
446 let redacted = #crate_root::slog::RedactedDisplay::redacted_display(self);
447 serializer.emit_arguments(key, &format_args!("{}", redacted))
448 }
449 }
450 }
451 }
452 }
453 };
454
455 #[cfg(not(feature = "slog"))]
456 let slog_impl = quote! {};
457
458 let trait_impl = quote! {
459 impl #impl_generics #crate_root::SensitiveType for #ident #ty_generics #where_clause {
460 fn redact_with<M: #crate_root::RedactionMapper>(self, mapper: &M) -> Self {
461 use #crate_root::SensitiveType as _;
462 #redaction_body
463 }
464 }
465
466 #debug_impl
467
468 #redacted_display_impl
469
470 #slog_impl
471
472 };
475 Ok(trait_impl)
476}