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))]
133pub fn derive_sensitive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
134 let input = parse_macro_input!(input as DeriveInput);
135 match expand(input, SlogMode::RedactedJson) {
136 Ok(tokens) => tokens.into(),
137 Err(err) => err.into_compile_error().into(),
138 }
139}
140
141#[proc_macro_derive(SensitiveError, attributes(sensitive, error))]
155pub fn derive_sensitive_error(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
156 let input = parse_macro_input!(input as DeriveInput);
157 match expand(input, SlogMode::RedactedDisplayString) {
158 Ok(tokens) => tokens.into(),
159 Err(err) => err.into_compile_error().into(),
160 }
161}
162
163fn crate_root() -> proc_macro2::TokenStream {
168 match crate_name("redaction") {
169 Ok(FoundCrate::Itself) => quote! { crate },
170 Ok(FoundCrate::Name(name)) => {
171 let ident = format_ident!("{}", name);
172 quote! { ::#ident }
173 }
174 Err(_) => quote! { ::redaction },
175 }
176}
177
178#[cfg(feature = "slog")]
184fn slog_crate() -> Result<proc_macro2::TokenStream> {
185 match crate_name("slog") {
186 Ok(FoundCrate::Itself) => Ok(quote! { crate }),
187 Ok(FoundCrate::Name(name)) => {
188 let ident = format_ident!("{}", name);
189 Ok(quote! { ::#ident })
190 }
191 Err(_) => {
192 let env_value = std::env::var("REDACTION_SLOG_CRATE").map_err(|_| {
193 syn::Error::new(
194 Span::call_site(),
195 "slog support is enabled, but no top-level `slog` crate was found. \
196Set the REDACTION_SLOG_CRATE env var to a path (e.g., `my_log::slog`) or add \
197`slog` as a direct dependency.",
198 )
199 })?;
200 let path = syn::parse_str::<syn::Path>(&env_value).map_err(|_| {
201 syn::Error::new(
202 Span::call_site(),
203 format!("REDACTION_SLOG_CRATE must be a valid Rust path (got `{env_value}`)"),
204 )
205 })?;
206 Ok(quote! { #path })
207 }
208 }
209}
210
211fn crate_path(item: &str) -> proc_macro2::TokenStream {
212 let root = crate_root();
213 let item_ident = syn::parse_str::<syn::Path>(item).expect("redaction crate path should parse");
214 quote! { #root::#item_ident }
215}
216
217struct DeriveOutput {
218 redaction_body: TokenStream,
219 used_generics: Vec<Ident>,
220 classified_generics: Vec<Ident>,
221 debug_redacted_body: TokenStream,
222 debug_redacted_generics: Vec<Ident>,
223 debug_unredacted_body: TokenStream,
224 debug_unredacted_generics: Vec<Ident>,
225 redacted_display_body: Option<TokenStream>,
226 redacted_display_generics: Vec<Ident>,
227 redacted_display_debug_generics: Vec<Ident>,
228 redacted_display_clone_generics: Vec<Ident>,
229 redacted_display_nested_generics: Vec<Ident>,
230}
231
232enum SlogMode {
233 RedactedJson,
234 RedactedDisplayString,
235}
236
237#[allow(clippy::too_many_lines)]
238fn expand(input: DeriveInput, slog_mode: SlogMode) -> Result<TokenStream> {
239 let DeriveInput {
240 ident,
241 generics,
242 data,
243 attrs,
244 ..
245 } = input;
246
247 let ContainerOptions { skip_debug } = parse_container_options(&attrs)?;
248
249 let crate_root = crate_root();
250
251 let redacted_display_output = if matches!(slog_mode, SlogMode::RedactedDisplayString) {
252 Some(derive_redacted_display(&ident, &data, &attrs, &generics)?)
253 } else {
254 None
255 };
256
257 let derive_output = match &data {
258 Data::Struct(data) => {
259 let output = derive_struct(&ident, data.clone(), &generics)?;
260 DeriveOutput {
261 redaction_body: output.redaction_body,
262 used_generics: output.used_generics,
263 classified_generics: output.classified_generics,
264 debug_redacted_body: output.debug_redacted_body,
265 debug_redacted_generics: output.debug_redacted_generics,
266 debug_unredacted_body: output.debug_unredacted_body,
267 debug_unredacted_generics: output.debug_unredacted_generics,
268 redacted_display_body: redacted_display_output
269 .as_ref()
270 .map(|output| output.body.clone()),
271 redacted_display_generics: redacted_display_output
272 .as_ref()
273 .map(|output| output.display_generics.clone())
274 .unwrap_or_default(),
275 redacted_display_debug_generics: redacted_display_output
276 .as_ref()
277 .map(|output| output.debug_generics.clone())
278 .unwrap_or_default(),
279 redacted_display_clone_generics: redacted_display_output
280 .as_ref()
281 .map(|output| output.clone_generics.clone())
282 .unwrap_or_default(),
283 redacted_display_nested_generics: redacted_display_output
284 .as_ref()
285 .map(|output| output.nested_generics.clone())
286 .unwrap_or_default(),
287 }
288 }
289 Data::Enum(data) => {
290 let output = derive_enum(&ident, data.clone(), &generics)?;
291 DeriveOutput {
292 redaction_body: output.redaction_body,
293 used_generics: output.used_generics,
294 classified_generics: output.classified_generics,
295 debug_redacted_body: output.debug_redacted_body,
296 debug_redacted_generics: output.debug_redacted_generics,
297 debug_unredacted_body: output.debug_unredacted_body,
298 debug_unredacted_generics: output.debug_unredacted_generics,
299 redacted_display_body: redacted_display_output
300 .as_ref()
301 .map(|output| output.body.clone()),
302 redacted_display_generics: redacted_display_output
303 .as_ref()
304 .map(|output| output.display_generics.clone())
305 .unwrap_or_default(),
306 redacted_display_debug_generics: redacted_display_output
307 .as_ref()
308 .map(|output| output.debug_generics.clone())
309 .unwrap_or_default(),
310 redacted_display_clone_generics: redacted_display_output
311 .as_ref()
312 .map(|output| output.clone_generics.clone())
313 .unwrap_or_default(),
314 redacted_display_nested_generics: redacted_display_output
315 .as_ref()
316 .map(|output| output.nested_generics.clone())
317 .unwrap_or_default(),
318 }
319 }
320 Data::Union(u) => {
321 return Err(syn::Error::new(
322 u.union_token.span(),
323 "`Sensitive` cannot be derived for unions",
324 ));
325 }
326 };
327
328 let classify_generics = add_container_bounds(generics.clone(), &derive_output.used_generics);
329 let classify_generics =
330 add_classified_value_bounds(classify_generics, &derive_output.classified_generics);
331 let (impl_generics, ty_generics, where_clause) = classify_generics.split_for_impl();
332 let debug_redacted_generics =
333 add_debug_bounds(generics.clone(), &derive_output.debug_redacted_generics);
334 let (debug_redacted_impl_generics, debug_redacted_ty_generics, debug_redacted_where_clause) =
335 debug_redacted_generics.split_for_impl();
336 let debug_unredacted_generics =
337 add_debug_bounds(generics.clone(), &derive_output.debug_unredacted_generics);
338 let (
339 debug_unredacted_impl_generics,
340 debug_unredacted_ty_generics,
341 debug_unredacted_where_clause,
342 ) = debug_unredacted_generics.split_for_impl();
343 let redaction_body = &derive_output.redaction_body;
344 let debug_redacted_body = &derive_output.debug_redacted_body;
345 let debug_unredacted_body = &derive_output.debug_unredacted_body;
346 let debug_impl = if skip_debug {
347 quote! {}
348 } else {
349 quote! {
350 #[cfg(any(test, feature = "testing"))]
351 impl #debug_unredacted_impl_generics ::core::fmt::Debug for #ident #debug_unredacted_ty_generics #debug_unredacted_where_clause {
352 fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
353 #debug_unredacted_body
354 }
355 }
356
357 #[cfg(not(any(test, feature = "testing")))]
358 #[allow(unused_variables)]
359 impl #debug_redacted_impl_generics ::core::fmt::Debug for #ident #debug_redacted_ty_generics #debug_redacted_where_clause {
360 fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
361 #debug_redacted_body
362 }
363 }
364 }
365 };
366
367 let redacted_display_body = derive_output.redacted_display_body.as_ref();
368 let redacted_display_impl = if matches!(slog_mode, SlogMode::RedactedDisplayString) {
369 let redacted_display_generics =
370 add_display_bounds(generics.clone(), &derive_output.redacted_display_generics);
371 let redacted_display_generics = add_debug_bounds(
372 redacted_display_generics,
373 &derive_output.redacted_display_debug_generics,
374 );
375 let redacted_display_generics = add_clone_bounds(
376 redacted_display_generics,
377 &derive_output.redacted_display_clone_generics,
378 );
379 let redacted_display_generics = add_redacted_display_bounds(
380 redacted_display_generics,
381 &derive_output.redacted_display_nested_generics,
382 );
383 let (display_impl_generics, display_ty_generics, display_where_clause) =
384 redacted_display_generics.split_for_impl();
385 let redacted_display_body = redacted_display_body
386 .cloned()
387 .unwrap_or_else(TokenStream::new);
388 quote! {
389 impl #display_impl_generics #crate_root::slog::RedactedDisplay for #ident #display_ty_generics #display_where_clause {
390 fn fmt_redacted(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
391 #redacted_display_body
392 }
393 }
394 }
395 } else {
396 quote! {}
397 };
398
399 #[cfg(feature = "slog")]
402 let slog_impl = {
403 let slog_crate = slog_crate()?;
404 let mut slog_generics = generics;
405 let slog_where_clause = slog_generics.make_where_clause();
406 let self_ty: syn::Type = parse_quote!(#ident #ty_generics);
407 match slog_mode {
408 SlogMode::RedactedJson => {
409 slog_where_clause
410 .predicates
411 .push(parse_quote!(#self_ty: ::core::clone::Clone));
412 slog_where_clause
415 .predicates
416 .push(parse_quote!(#self_ty: ::serde::Serialize));
417 slog_where_clause
418 .predicates
419 .push(parse_quote!(#self_ty: #crate_root::slog::IntoRedactedJson));
420 let (slog_impl_generics, slog_ty_generics, slog_where_clause) =
421 slog_generics.split_for_impl();
422 quote! {
423 impl #slog_impl_generics #slog_crate::Value for #ident #slog_ty_generics #slog_where_clause {
424 fn serialize(
425 &self,
426 _record: &#slog_crate::Record<'_>,
427 key: #slog_crate::Key,
428 serializer: &mut dyn #slog_crate::Serializer,
429 ) -> #slog_crate::Result {
430 let redacted = #crate_root::slog::IntoRedactedJson::into_redacted_json(self.clone());
431 #slog_crate::Value::serialize(&redacted, _record, key, serializer)
432 }
433 }
434 }
435 }
436 SlogMode::RedactedDisplayString => {
437 slog_where_clause
438 .predicates
439 .push(parse_quote!(#self_ty: #crate_root::slog::RedactedDisplay));
440 let (slog_impl_generics, slog_ty_generics, slog_where_clause) =
441 slog_generics.split_for_impl();
442 quote! {
443 impl #slog_impl_generics #slog_crate::Value for #ident #slog_ty_generics #slog_where_clause {
444 fn serialize(
445 &self,
446 _record: &#slog_crate::Record<'_>,
447 key: #slog_crate::Key,
448 serializer: &mut dyn #slog_crate::Serializer,
449 ) -> #slog_crate::Result {
450 let redacted = #crate_root::slog::RedactedDisplay::redacted_display(self);
451 serializer.emit_arguments(key, &format_args!("{}", redacted))
452 }
453 }
454 }
455 }
456 }
457 };
458
459 #[cfg(not(feature = "slog"))]
460 let slog_impl = quote! {};
461
462 let trait_impl = quote! {
463 impl #impl_generics #crate_root::SensitiveType for #ident #ty_generics #where_clause {
464 fn redact_with<M: #crate_root::RedactionMapper>(self, mapper: &M) -> Self {
465 use #crate_root::SensitiveType as _;
466 #redaction_body
467 }
468 }
469
470 #debug_impl
471
472 #redacted_display_impl
473
474 #slog_impl
475
476 };
479 Ok(trait_impl)
480}