From f1957b43e9c94530fd84f517459c7c3fcda3ab9c Mon Sep 17 00:00:00 2001 From: William Venner <14863743+WilliamVenner@users.noreply.github.com> Date: Thu, 15 Dec 2022 13:50:12 +0000 Subject: [PATCH 1/5] Internal code refactor + optimisations --- src/private.rs | 157 +++++++++--------- veil-macros/src/enums.rs | 33 ++-- veil-macros/src/flags.rs | 323 +++++++++++++++++++++++-------------- veil-macros/src/fmt.rs | 46 +++--- veil-macros/src/lib.rs | 76 ++------- veil-macros/src/redact.rs | 68 ++++++++ veil-macros/src/structs.rs | 11 +- 7 files changed, 421 insertions(+), 293 deletions(-) create mode 100644 veil-macros/src/redact.rs diff --git a/src/private.rs b/src/private.rs index ee4d1f77..7d76ae93 100644 --- a/src/private.rs +++ b/src/private.rs @@ -1,26 +1,12 @@ -use std::fmt::{Debug, Display}; +use std::fmt::{Debug, Display, Write}; -#[repr(transparent)] -pub struct DisplayDebug(String); -impl std::fmt::Debug for DisplayDebug { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(self.0.as_str()) - } -} -impl AsRef for DisplayDebug { - #[inline(always)] - fn as_ref(&self) -> &str { - self.0.as_str() - } -} - -pub struct RedactFlags { +pub enum RedactSpecialization { /// Whether the type we're redacting is an Option or not. Poor man's specialization! This is detected /// by the proc macro reading the path to the type, so it's not perfect. /// /// This could be improved & rid of in a number of different ways in the future: /// - /// * Once specialization is stabilized, we can use a trait to override redacting behaviour for some types, + /// * Once specialization is stabilized, we can use a trait to override redacting behavior for some types, /// one of which would be Option. /// /// * Once std::ptr::metadata and friends are stabilized, we could use it to unsafely cast the dyn Debug pointer @@ -28,8 +14,11 @@ pub struct RedactFlags { /// /// * Once trait upcasting is stabilized, we could use it to upcast the dyn Debug pointer to a dyn Any and then /// downcast it to a concrete Option and redact it directly. - pub is_option: bool, + Option, +} +#[derive(Clone, Copy)] +pub struct RedactFlags { /// Whether to only partially redact the data. /// /// Incompatible with `fixed`. @@ -52,14 +41,14 @@ impl RedactFlags { /// Maximum number of characters to expose at the beginning and end of a partial redact. const MAX_PARTIAL_EXPOSE: usize = 3; - fn redact_partial(&self, str: &str, redacted: &mut String) { + pub(crate) fn redact_partial(&self, fmt: &mut std::fmt::Formatter, str: &str) -> std::fmt::Result { let count = str.chars().filter(|char| char.is_alphanumeric()).count(); if count < Self::MIN_PARTIAL_CHARS { for char in str.chars() { if char.is_alphanumeric() { - redacted.push(self.redact_char); + fmt.write_char(self.redact_char)?; } else { - redacted.push(char); + fmt.write_char(char)?; } } } else { @@ -72,40 +61,43 @@ impl RedactFlags { if char.is_alphanumeric() { if prefix_gas > 0 { prefix_gas -= 1; - redacted.push(char); + fmt.write_char(char)?; } else if middle_gas > 0 { middle_gas -= 1; - redacted.push(self.redact_char); + fmt.write_char(self.redact_char)?; } else { - redacted.push(char); + fmt.write_char(char)?; } } else { - redacted.push(char); + fmt.write_char(char)?; } } } + Ok(()) } - fn redact_full(&self, str: &str, redacted: &mut String) { + pub(crate) fn redact_full(&self, fmt: &mut std::fmt::Formatter, str: &str) -> std::fmt::Result { for char in str.chars() { if char.is_whitespace() || !char.is_alphanumeric() { - redacted.push(char); + fmt.write_char(char)?; } else { - redacted.push(self.redact_char); + fmt.write_char(self.redact_char)?; } } + Ok(()) } - fn redact_fixed(&self, width: usize, redacted: &mut String) { - redacted.reserve_exact(width); + pub(crate) fn redact_fixed(fmt: &mut std::fmt::Formatter, width: usize, char: char) -> std::fmt::Result { + let mut buf = String::with_capacity(width); for _ in 0..width { - redacted.push(self.redact_char); + buf.push(char); } + fmt.write_str(&buf) } } pub enum RedactionTarget<'a> { - /// Redact the output of the type's [`std::fmt::Debug`] implementation. + /// Redact the output of the type's [`Debug`] implementation. Debug { this: &'a dyn Debug, @@ -113,59 +105,74 @@ pub enum RedactionTarget<'a> { alternate: bool, }, - /// Redact the output of the type's [`std::fmt::Display`] implementation. + /// Redact the output of the type's [`Display`] implementation. Display(&'a dyn Display), } - -pub fn redact(this: RedactionTarget, flags: RedactFlags) -> DisplayDebug { - let mut redacted = String::new(); - - let to_redactable_string = || match this { - RedactionTarget::Debug { this, alternate: false } => format!("{:?}", this), - RedactionTarget::Debug { this, alternate: true } => format!("{:#?}", this), - RedactionTarget::Display(this) => this.to_string(), - }; - +impl RedactionTarget<'_> { + /// Pass through directly to the formatter. #[cfg(feature = "toggle")] - if crate::toggle::get_redaction_behavior().is_plaintext() { - return DisplayDebug(to_redactable_string()); + pub(crate) fn passthrough(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + RedactionTarget::Debug { this, .. } => std::fmt::Debug::fmt(this, fmt), + RedactionTarget::Display(this) => std::fmt::Display::fmt(this, fmt), + } } +} +impl ToString for RedactionTarget<'_> { + fn to_string(&self) -> String { + match self { + RedactionTarget::Debug { this, alternate: false } => format!("{:?}", this), + RedactionTarget::Debug { this, alternate: true } => format!("{:#?}", this), + RedactionTarget::Display(this) => this.to_string(), + } + } +} - (|| { - if flags.fixed > 0 { - flags.redact_fixed(flags.fixed as usize, &mut redacted); - return; +pub struct RedactionFormatter<'a> { + pub this: RedactionTarget<'a>, + pub flags: RedactFlags, + pub specialization: Option, +} +impl std::fmt::Debug for RedactionFormatter<'_> { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + #[cfg(feature = "toggle")] + if crate::toggle::get_redaction_behavior().is_plaintext() { + return self.this.passthrough(fmt); } - let redactable_string = to_redactable_string(); - - redacted.reserve(redactable_string.len()); - - // Specialize for Option - if flags.is_option { - if redactable_string == "None" { - // We don't need to do any redacting - // https://prima.slack.com/archives/C03URH9N43U/p1661423554871499 - } else if let Some(inner) = redactable_string - .strip_prefix("Some(") - .and_then(|inner| inner.strip_suffix(')')) - { - redacted.push_str("Some("); - flags.redact_partial(inner, &mut redacted); - redacted.push(')'); - } else { - // This should never happen, but just in case... - flags.redact_full(&redactable_string, &mut redacted); + if self.flags.fixed > 0 { + return RedactFlags::redact_fixed(fmt, self.flags.fixed as usize, self.flags.redact_char); + } + + let redactable_string = self.this.to_string(); + + #[allow(clippy::single_match)] + match self.specialization { + Some(RedactSpecialization::Option) => { + if redactable_string == "None" { + // We don't need to do any redacting + // https://prima.slack.com/archives/C03URH9N43U/p1661423554871499 + return fmt.write_str("None"); + } else if let Some(inner) = redactable_string + .strip_prefix("Some(") + .and_then(|inner| inner.strip_suffix(')')) + { + fmt.write_str("Some(")?; + self.flags.redact_partial(fmt, inner)?; + return fmt.write_char(')'); + } else { + // This should never happen, but just in case... + return self.flags.redact_full(fmt, &redactable_string); + } } - return; + + _ => {} } - if flags.partial { - flags.redact_partial(&redactable_string, &mut redacted); + if self.flags.partial { + self.flags.redact_partial(fmt, &redactable_string) } else { - flags.redact_full(&redactable_string, &mut redacted); + self.flags.redact_full(fmt, &redactable_string) } - })(); - - DisplayDebug(redacted) + } } diff --git a/veil-macros/src/enums.rs b/veil-macros/src/enums.rs index 188adfac..12d1f473 100644 --- a/veil-macros/src/enums.rs +++ b/veil-macros/src/enums.rs @@ -1,7 +1,6 @@ use crate::{ - flags::FieldFlags, - fmt::{self, FormatData}, - UnusedDiagnostic, + flags::{ExtractFlags, FieldFlags, FieldFlagsParse}, + fmt::{self, FormatData}, redact::UnusedDiagnostic, }; use proc_macro::TokenStream; use quote::ToTokens; @@ -21,7 +20,7 @@ pub(super) fn derive_redact( unused: &mut UnusedDiagnostic, ) -> Result { // Parse #[redact(all, variant, ...)] from the enum attributes, if present. - let top_level_flags = match FieldFlags::extract::<1>(&attrs, false)? { + let top_level_flags = match FieldFlags::extract::<1>("Redact", &attrs, FieldFlagsParse { skip_allowed: false })? { [Some(flags)] => { if !flags.all || !flags.variant { return Err(syn::Error::new( @@ -41,7 +40,13 @@ pub(super) fn derive_redact( // Collect each variant's flags let mut variant_flags = Vec::with_capacity(e.variants.len()); for variant in &e.variants { - let mut flags = match FieldFlags::extract::<2>(&variant.attrs, top_level_flags.is_some())? { + let mut flags = match FieldFlags::extract::<2>( + "Redact", + &variant.attrs, + FieldFlagsParse { + skip_allowed: top_level_flags.is_some(), + }, + )? { [None, None] => EnumVariantFieldFlags::default(), [Some(flags), None] => { @@ -163,7 +168,17 @@ pub(super) fn derive_redact( // Variant name redacting let variant_name = variant.ident.to_string(); let variant_name = if let Some(flags) = &flags.variant_flags { - fmt::generate_redact_call(quote! { &#variant_name }, false, flags, unused) + // The variant name must always be formatted with the Display impl. + let flags = FieldFlags { + display: true, + ..*flags + }; + + // Generate the RedactionFormatter expression for the variant name + let redact = fmt::generate_redact_call(quote! { &#variant_name }, false, &flags, unused); + + // Because the other side is expecting a &str, we need to convert the RedactionFormatter to a String (and then to a &str) + quote! { format!("{:?}", #redact).as_str() } } else { variant_name.into_token_stream() }; @@ -182,7 +197,7 @@ pub(super) fn derive_redact( "unit structs do not need redacting as they contain no data", )); } else { - quote! { write!(f, "{:?}", #variant_name)? } + quote! { fmt.write_str(#variant_name)? } } } }); @@ -191,9 +206,9 @@ pub(super) fn derive_redact( let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); Ok(quote! { impl #impl_generics ::std::fmt::Debug for #name_ident #ty_generics #where_clause { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + fn fmt(&self, fmt: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { #[allow(unused)] // Suppresses unused warning with `#[redact(display)]` - let alternate = f.alternate(); + let alternate = fmt.alternate(); match self { #(Self::#variant_idents #variant_destructures => { #variant_bodies; },)* diff --git a/veil-macros/src/flags.rs b/veil-macros/src/flags.rs index 7686484c..5304762e 100644 --- a/veil-macros/src/flags.rs +++ b/veil-macros/src/flags.rs @@ -1,47 +1,48 @@ use std::num::NonZeroU8; use syn::spanned::Spanned; -#[derive(Clone, Copy, PartialEq, Eq)] -pub struct FieldFlags { - /// Whether to blanket redact everything (fields, variants) - pub all: bool, +pub struct FieldFlagsParse { + pub skip_allowed: bool, +} - /// Redacts the name of this enum variant, if applicable. - pub variant: bool, +pub enum TryParseMeta { + Consumed, + Unrecognised(syn::Meta), + Err(syn::Error), +} - /// Whether the field is partially or fully redacted. - /// - /// Incompatible with `fixed`. - pub partial: bool, +pub trait ExtractFlags: Sized + Copy + Default { + type Options; - /// The character to use for redacting. Defaults to `*`. - pub redact_char: char, + fn try_parse_meta(&mut self, meta: syn::Meta) -> TryParseMeta; - /// Whether to redact with a fixed width, ignoring the length of the data. - /// - /// Incompatible with `partial`. - pub fixed: Option, - - /// Whether to skip redaction. - /// - /// Only allowed if this field is affected by a `#[redact(all)]` attribute. - /// - /// Fields are not redacted by default unless their parent is marked as `#[redact(all)]`, and this flag turns off that redaction for this specific field. - pub skip: bool, + fn parse_meta( + &mut self, + derive_name: &'static str, + attr: &syn::Attribute, + meta: syn::Meta, + ) -> Result<(), syn::Error> { + match self.try_parse_meta(meta) { + TryParseMeta::Consumed => Ok(()), + TryParseMeta::Err(err) => Err(err), + TryParseMeta::Unrecognised(meta) => match meta { + // Anything we don't expect + syn::Meta::List(_) => Err(syn::Error::new_spanned( + attr, + format!("unexpected list for `{}` attribute", derive_name), + )), + _ => Err(syn::Error::new_spanned( + attr, + format!("unknown modifier for `{}` attribute", derive_name), + )), + }, + } + } - /// Whether to use the type's [`std::fmt::Display`] implementation instead of [`std::fmt::Debug`]. - pub display: bool, -} -impl FieldFlags { - /// Returns a list of `FieldFlags` parsed from an attribute. - /// - /// `AMOUNT` is the maximum number of attributes that should be parsed. - /// - /// `skip_allowed` should be `true` if `#[redact(all)]` is present and this field is affected by it. - /// Otherwise, `#[redact(skip)]` is not allowed. - pub fn extract( + fn extract( + derive_name: &'static str, attrs: &[syn::Attribute], - skip_allowed: bool, + options: Self::Options, ) -> Result<[Option; AMOUNT], syn::Error> { let mut extracted = [None; AMOUNT]; let mut head = 0; @@ -54,27 +55,8 @@ impl FieldFlags { )); } - if let Some(flags) = Self::parse(attr)? { - if flags.skip { - if !skip_allowed { - return Err(syn::Error::new(attr.span(), "`#[redact(skip)]` is not allowed here")); - } - - // It doesn't make sense for `skip` to be present with any other flags. - // We'll throw an error if it is. - let valid_skip_flags = FieldFlags { - skip: true, - variant: flags.variant, - ..Default::default() - }; - if flags != valid_skip_flags { - return Err(syn::Error::new( - attr.span(), - "`#[redact(skip)]` should not have any other modifiers present", - )); - } - } - + if let Some(flags) = Self::parse(derive_name, attr)? { + flags.validate(attr, &options)?; extracted[head] = Some(flags); head += 1; } @@ -83,8 +65,8 @@ impl FieldFlags { Ok(extracted) } - fn parse(attr: &syn::Attribute) -> Result, syn::Error> { - let mut flags = FieldFlags::default(); + fn parse(derive_name: &'static str, attr: &syn::Attribute) -> Result, syn::Error> { + let mut flags = Self::default(); // The modifiers could be a single value or a list, so we need to handle both cases. let modifiers = match attr.parse_meta()? { @@ -103,89 +85,87 @@ impl FieldFlags { // Now we can finally process each modifier. for meta in modifiers { - match meta { - // #[redact(all)] - syn::Meta::Path(path) if path.is_ident("all") => { - flags.all = true; - } - - // #[redact(skip)] - syn::Meta::Path(path) if path.is_ident("skip") => { - flags.skip = true; - } - - // #[redact(partial)] - syn::Meta::Path(path) if path.is_ident("partial") => { - flags.partial = true; - } - - // #[redact(variant)] - syn::Meta::Path(path) if path.is_ident("variant") => { - flags.variant = true; - } + flags.parse_meta(derive_name, attr, meta)?; + } - // #[redact(display)] - syn::Meta::Path(path) if path.is_ident("display") => { - flags.display = true; - } + Ok(Some(flags)) + } - // #[redact(with = 'X')] - syn::Meta::NameValue(kv) if kv.path.is_ident("with") => match kv.lit { - syn::Lit::Char(with) => flags.redact_char = with.value(), - _ => return Err(syn::Error::new_spanned(kv.lit, "expected a character literal")), - }, - - // #[redact(fixed = u8)] - syn::Meta::NameValue(kv) if kv.path.is_ident("fixed") => match kv.lit { - syn::Lit::Int(int) => { - flags.fixed = Some(NonZeroU8::new(int.base10_parse::()?).ok_or_else(|| { - syn::Error::new_spanned(int, "fixed redacting width must be greater than zero") - })?) - } - _ => return Err(syn::Error::new_spanned(kv.lit, "expected a character literal")), - }, + fn validate(&self, _attr: &syn::Attribute, _options: &Self::Options) -> Result<(), syn::Error> { + Ok(()) + } +} - // Anything we don't expect - syn::Meta::List(_) => { - return Err(syn::Error::new_spanned(attr, "unexpected list for `Redact` attribute")) - } - _ => return Err(syn::Error::new_spanned(attr, "unknown modifier for `Redact` attribute")), - } - } +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct RedactFlags { + /// Whether the field is partially or fully redacted. + /// + /// Incompatible with `fixed`. + pub partial: bool, - if flags.partial && flags.fixed.is_some() { - return Err(syn::Error::new_spanned( - attr, - "`#[redact(partial)]` and `#[redact(fixed = ...)]` are incompatible", - )); - } + /// The character to use for redacting. Defaults to `*`. + pub redact_char: char, - Ok(Some(flags)) - } + /// Whether to redact with a fixed width, ignoring the length of the data. + /// + /// Incompatible with `partial`. + pub fixed: Option, } -impl Default for FieldFlags { +impl Default for RedactFlags { fn default() -> Self { Self { partial: false, fixed: None, redact_char: '*', - variant: false, - all: false, - skip: false, - display: false, } } } -impl quote::ToTokens for FieldFlags { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - assert!(!self.skip, "internal error: skip flag should not be set here"); +impl ExtractFlags for RedactFlags { + type Options = (); + + fn try_parse_meta(&mut self, meta: syn::Meta) -> TryParseMeta { + match meta { + // #[redact(partial)] + syn::Meta::Path(path) if path.is_ident("partial") => { + self.partial = true; + } + + // #[redact(with = 'X')] + syn::Meta::NameValue(kv) if kv.path.is_ident("with") => match kv.lit { + syn::Lit::Char(with) => self.redact_char = with.value(), + _ => return TryParseMeta::Err(syn::Error::new_spanned(kv.lit, "expected a character literal")), + }, + + // #[redact(fixed = u8)] + syn::Meta::NameValue(kv) if kv.path.is_ident("fixed") => match kv.lit { + syn::Lit::Int(int) => { + self.fixed = Some( + match int.base10_parse::().and_then(|int| { + NonZeroU8::new(int).ok_or_else(|| { + syn::Error::new_spanned(int, "fixed redacting width must be greater than zero") + }) + }) { + Ok(fixed) => fixed, + Err(err) => return TryParseMeta::Err(err), + }, + ) + } + _ => return TryParseMeta::Err(syn::Error::new_spanned(kv.lit, "expected a character literal")), + }, + _ => return TryParseMeta::Unrecognised(meta), + } + TryParseMeta::Consumed + } +} +impl quote::ToTokens for RedactFlags { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { let Self { partial, redact_char, fixed, .. - } = *self; + } = self; let fixed = fixed.map(|fixed| fixed.get()).unwrap_or(0); @@ -196,3 +176,104 @@ impl quote::ToTokens for FieldFlags { }); } } + +#[derive(Clone, Copy, PartialEq, Eq, Default)] +pub struct FieldFlags { + /// Whether to blanket redact everything (fields, variants) + pub all: bool, + + /// Redacts the name of this enum variant, if applicable. + pub variant: bool, + + /// Whether to skip redaction. + /// + /// Only allowed if this field is affected by a `#[redact(all)]` attribute. + /// + /// Fields are not redacted by default unless their parent is marked as `#[redact(all)]`, and this flag turns off that redaction for this specific field. + pub skip: bool, + + /// Whether to use the type's [`Display`] implementation instead of [`Debug`]. + pub display: bool, + + /// Flags that modify the redaction behavior. + pub redact: RedactFlags, +} +impl ExtractFlags for FieldFlags { + type Options = FieldFlagsParse; + + fn try_parse_meta(&mut self, meta: syn::Meta) -> TryParseMeta { + // First try to parse the redaction flags. + let meta = match self.redact.try_parse_meta(meta) { + // This was a redaction flag, so we don't need to do anything else. + // OR + // This was an error, so we need to propagate it. + result @ (TryParseMeta::Consumed | TryParseMeta::Err(_)) => return result, + + // This was not a redaction flag, so we need to continue processing. + TryParseMeta::Unrecognised(meta) => meta, + }; + + match meta { + // #[redact(all)] + syn::Meta::Path(path) if path.is_ident("all") => { + self.all = true; + } + + // #[redact(skip)] + syn::Meta::Path(path) if path.is_ident("skip") => { + self.skip = true; + } + + // #[redact(variant)] + syn::Meta::Path(path) if path.is_ident("variant") => { + self.variant = true; + } + + // #[redact(display)] + syn::Meta::Path(path) if path.is_ident("display") => { + self.display = true; + } + + _ => return TryParseMeta::Unrecognised(meta), + } + + TryParseMeta::Consumed + } + + fn validate(&self, attr: &syn::Attribute, options: &Self::Options) -> Result<(), syn::Error> { + if self.skip { + if !options.skip_allowed { + return Err(syn::Error::new(attr.span(), "`#[redact(skip)]` is not allowed here")); + } + + // It doesn't make sense for `skip` to be present with any other flags. + // We'll throw an error if it is. + let valid_skip_flags = FieldFlags { + skip: true, + variant: self.variant, + ..Default::default() + }; + if self != &valid_skip_flags { + return Err(syn::Error::new( + attr.span(), + "`#[redact(skip)]` should not have any other modifiers present", + )); + } + } + + if self.redact.partial && self.redact.fixed.is_some() { + return Err(syn::Error::new_spanned( + attr, + "`#[redact(partial)]` and `#[redact(fixed = ...)]` are incompatible", + )); + } + + Ok(()) + } +} +impl quote::ToTokens for FieldFlags { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + assert!(!self.skip, "internal error: skip flag should not be set here"); + self.redact.to_tokens(tokens) + } +} diff --git a/veil-macros/src/fmt.rs b/veil-macros/src/fmt.rs index f2ec0ea6..2f1ad79f 100644 --- a/veil-macros/src/fmt.rs +++ b/veil-macros/src/fmt.rs @@ -1,4 +1,6 @@ -use crate::{flags::FieldFlags, UnusedDiagnostic}; +use crate::{ + flags::{ExtractFlags, FieldFlags, FieldFlagsParse}, redact::UnusedDiagnostic, +}; use quote::ToTokens; use syn::spanned::Spanned; @@ -74,7 +76,13 @@ impl FormatData<'_> { // Parse field flags from attributes on this field let field_flags = match field.attrs.len() { 0 => all_fields_flags, - 1 => match FieldFlags::extract::<1>(&field.attrs, all_fields_flags.is_some())? { + 1 => match FieldFlags::extract::<1>( + "Redact", + &field.attrs, + FieldFlagsParse { + skip_allowed: all_fields_flags.is_some(), + }, + )? { [Some(flags)] => { if flags.variant { return Err(syn::Error::new( @@ -120,7 +128,7 @@ impl FormatData<'_> { let field_names = named.iter().map(|field| field.ident.as_ref().unwrap().to_string()); quote! { - f.debug_struct(&#name.as_ref()) + fmt.debug_struct(#name) #( .field(#field_names, &#field_bodies) )* @@ -130,7 +138,7 @@ impl FormatData<'_> { Self::FieldsUnnamed(syn::FieldsUnnamed { .. }) => { quote! { - f.debug_tuple(&#name.as_ref()) + fmt.debug_tuple(#name) #( .field(&#field_bodies) )* @@ -152,27 +160,29 @@ pub(crate) fn generate_redact_call( // This is the one place where we actually track whether the derive macro had any effect! Nice. unused.redacted_something(); + let specialization = if is_option { + quote! { ::std::option::Option::Some(::veil::private::RedactSpecialization::Option) } + } else { + quote! { ::std::option::Option::None } + }; + if field_flags.display { // std::fmt::Display quote! { - ::veil::private::redact( - ::veil::private::RedactionTarget::Display(#field_accessor), - ::veil::private::RedactFlags { - is_option: #is_option, - #field_flags - } - ) + &::veil::private::RedactionFormatter { + this: ::veil::private::RedactionTarget::Display(#field_accessor), + flags: ::veil::private::RedactFlags { #field_flags }, + specialization: #specialization + } } } else { // std::fmt::Debug quote! { - ::veil::private::redact( - ::veil::private::RedactionTarget::Debug { this: #field_accessor, alternate }, - ::veil::private::RedactFlags { - is_option: #is_option, - #field_flags - } - ) + &::veil::private::RedactionFormatter { + this: ::veil::private::RedactionTarget::Debug { this: #field_accessor, alternate }, + flags: ::veil::private::RedactFlags { #field_flags }, + specialization: #specialization + } } } } else { diff --git a/veil-macros/src/lib.rs b/veil-macros/src/lib.rs index ba813769..f33996bd 100644 --- a/veil-macros/src/lib.rs +++ b/veil-macros/src/lib.rs @@ -1,87 +1,31 @@ +//! Macros for `veil` + +#![deny(missing_docs)] +#![deny(rustdoc::broken_intra_doc_links)] + #[macro_use] extern crate quote; mod enums; mod flags; mod fmt; +mod redact; mod sanitize; mod structs; use proc_macro::TokenStream; -use sanitize::DeriveAttributeFilter; -use syn::spanned::Spanned; - -/// Keep track of whether we actually redact anything. -/// -/// By default fields are not redacted. One must add #[redact(...)] to them. -/// -/// We should throw an error if no fields are redacted, because the user should derive Debug instead. -/// -/// This should also be aware of #[redact(skip)] - we shouldn't let users bypass this check via that. -struct UnusedDiagnostic(bool); -impl UnusedDiagnostic { - #[inline(always)] - /// We redacted something! Don't throw an error saying the derive was unused. - pub(crate) fn redacted_something(&mut self) { - self.0 = false; - } - - #[inline(always)] - #[must_use] - fn should_throw_err(self) -> bool { - self.0 - } -} -impl Default for UnusedDiagnostic { - #[inline(always)] - fn default() -> Self { - Self(true) - } -} #[proc_macro_derive(Redact, attributes(redact))] -/// Implements [`std::fmt::Debug`] for a struct or enum variant, with certain fields redacted. +/// Implements [`Debug`] for a struct or enum, with certain fields redacted. /// -/// See the [crate level documentation](index.html) for more information. +/// See the [crate level documentation](index.html) for flags and modifiers. pub fn derive_redact(item: TokenStream) -> TokenStream { - let mut item = syn::parse_macro_input!(item as syn::DeriveInput); - - // Remove all non-veil attributes to avoid conflicting with other - // derive proc macro attributes. - item.retain_veil_attrs(); - - // Unfortunately this is somewhat complex to implement at this stage of the macro "pipeline", - // so we'll pass around a mutable reference to this variable, and set it to false if we redact anything. - // TBH this kind of smells, but I can't think of a better way to do it. - let mut unused = UnusedDiagnostic::default(); - - let item_span = item.span(); - - let result = match item.data { - syn::Data::Struct(s) => structs::derive_redact(s, item.generics, item.attrs, item.ident, &mut unused), - syn::Data::Enum(e) => enums::derive_redact(e, item.generics, item.attrs, item.ident, &mut unused), - syn::Data::Union(_) => Err(syn::Error::new(item_span, "this trait cannot be derived for unions")), - }; - - let result = result.and_then(|tokens| { - if unused.should_throw_err() { - Err(syn::Error::new( - item_span, - "`#[derive(Redact)]` does nothing by default, you must specify at least one field to redact. You should `#[derive(Debug)]` instead if this is intentional", - )) - } else { - Ok(tokens) - } - }); - - match result { - Ok(tokens) => tokens, - Err(err) => err.into_compile_error().into(), - } + redact::derive(item) } #[doc(hidden)] #[proc_macro] +/// Used by the `versioning::test_macros_version` test. pub fn __private_version(_: TokenStream) -> TokenStream { format!("{:?}", env!("CARGO_PKG_VERSION")).parse().unwrap() } diff --git a/veil-macros/src/redact.rs b/veil-macros/src/redact.rs new file mode 100644 index 00000000..6e27f8d6 --- /dev/null +++ b/veil-macros/src/redact.rs @@ -0,0 +1,68 @@ +use crate::{enums, sanitize::DeriveAttributeFilter, structs}; +use proc_macro::TokenStream; +use syn::spanned::Spanned; + +/// Keep track of whether we actually redact anything. +/// +/// By default fields are not redacted. One must add `#[redact(...)]` to them. +/// +/// We should throw an error if no fields are redacted, because the user should derive Debug instead. +/// +/// This should also be aware of `#[redact(skip)]` - we shouldn't let users bypass this check via that. +pub struct UnusedDiagnostic(bool); +impl UnusedDiagnostic { + #[inline(always)] + /// We redacted something! Don't throw an error saying the derive was unused. + pub(crate) fn redacted_something(&mut self) { + self.0 = false; + } + + #[inline(always)] + #[must_use] + fn should_throw_err(self) -> bool { + self.0 + } +} +impl Default for UnusedDiagnostic { + #[inline(always)] + fn default() -> Self { + Self(true) + } +} + +fn try_derive(mut item: syn::DeriveInput) -> Result { + // Remove all non-veil attributes to avoid conflicting with other + // derive proc macro attributes. + item.retain_veil_attrs(); + + let item_span = item.span(); + + // Unfortunately this is somewhat complex to implement at this stage of the macro "pipeline", + // so we'll pass around a mutable reference to this variable, and set it to false if we redact anything. + // TBH this kind of smells, but I can't think of a better way to do it. + let mut unused = UnusedDiagnostic::default(); + + let tokens = match item.data { + syn::Data::Struct(s) => structs::derive_redact(s, item.generics, item.attrs, item.ident, &mut unused)?, + syn::Data::Enum(e) => enums::derive_redact(e, item.generics, item.attrs, item.ident, &mut unused)?, + syn::Data::Union(_) => return Err(syn::Error::new(item_span, "this trait cannot be derived for unions")), + }; + + if unused.should_throw_err() { + return Err(syn::Error::new( + item_span, + "`#[derive(Redact)]` does nothing by default, you must specify at least one field to redact. You should `#[derive(Debug)]` instead if this is intentional", + )); + } + + Ok(tokens) +} + +pub fn derive(item: TokenStream) -> TokenStream { + let item = syn::parse_macro_input!(item as syn::DeriveInput); + + match try_derive(item) { + Ok(tokens) => tokens, + Err(err) => err.into_compile_error().into(), + } +} diff --git a/veil-macros/src/structs.rs b/veil-macros/src/structs.rs index 68359048..dd90ff66 100644 --- a/veil-macros/src/structs.rs +++ b/veil-macros/src/structs.rs @@ -1,4 +1,7 @@ -use crate::{flags::FieldFlags, fmt::FormatData, UnusedDiagnostic}; +use crate::{ + flags::{ExtractFlags, FieldFlags, FieldFlagsParse}, + fmt::FormatData, redact::UnusedDiagnostic, +}; use proc_macro::TokenStream; use quote::ToTokens; use syn::spanned::Spanned; @@ -13,7 +16,7 @@ pub(super) fn derive_redact( // Parse #[redact(all, variant, ...)] from the enum attributes, if present. let top_level_flags = match attrs.len() { 0 => None, - 1 => match FieldFlags::extract::<1>(&attrs, false)? { + 1 => match FieldFlags::extract::<1>("Redact", &attrs, FieldFlagsParse { skip_allowed: false })? { [Some(flags)] => { if flags.variant { return Err(syn::Error::new( @@ -61,9 +64,9 @@ pub(super) fn derive_redact( let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); Ok(quote! { impl #impl_generics ::std::fmt::Debug for #name_ident #ty_generics #where_clause { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + fn fmt(&self, fmt: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { #[allow(unused)] // Suppresses unused warning with `#[redact(display)]` - let alternate = f.alternate(); + let alternate = fmt.alternate(); #impl_debug; From b62864e192f7847b491b2e05e064976157aaa654 Mon Sep 17 00:00:00 2001 From: William Venner <14863743+WilliamVenner@users.noreply.github.com> Date: Thu, 15 Dec 2022 13:53:38 +0000 Subject: [PATCH 2/5] Format --- veil-macros/src/enums.rs | 3 ++- veil-macros/src/fmt.rs | 3 ++- veil-macros/src/structs.rs | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/veil-macros/src/enums.rs b/veil-macros/src/enums.rs index 12d1f473..21db339b 100644 --- a/veil-macros/src/enums.rs +++ b/veil-macros/src/enums.rs @@ -1,6 +1,7 @@ use crate::{ flags::{ExtractFlags, FieldFlags, FieldFlagsParse}, - fmt::{self, FormatData}, redact::UnusedDiagnostic, + fmt::{self, FormatData}, + redact::UnusedDiagnostic, }; use proc_macro::TokenStream; use quote::ToTokens; diff --git a/veil-macros/src/fmt.rs b/veil-macros/src/fmt.rs index 2f1ad79f..f9ce3d1a 100644 --- a/veil-macros/src/fmt.rs +++ b/veil-macros/src/fmt.rs @@ -1,5 +1,6 @@ use crate::{ - flags::{ExtractFlags, FieldFlags, FieldFlagsParse}, redact::UnusedDiagnostic, + flags::{ExtractFlags, FieldFlags, FieldFlagsParse}, + redact::UnusedDiagnostic, }; use quote::ToTokens; use syn::spanned::Spanned; diff --git a/veil-macros/src/structs.rs b/veil-macros/src/structs.rs index dd90ff66..9be8af41 100644 --- a/veil-macros/src/structs.rs +++ b/veil-macros/src/structs.rs @@ -1,6 +1,7 @@ use crate::{ flags::{ExtractFlags, FieldFlags, FieldFlagsParse}, - fmt::FormatData, redact::UnusedDiagnostic, + fmt::FormatData, + redact::UnusedDiagnostic, }; use proc_macro::TokenStream; use quote::ToTokens; From be0ba70bc92a7b2da77a37e85c3d4e833bb97d54 Mon Sep 17 00:00:00 2001 From: William Date: Thu, 15 Dec 2022 15:06:10 +0000 Subject: [PATCH 3/5] Compile time error if a specialisation isn't handled Co-authored-by: MaeIsBad <26093674+MaeIsBad@users.noreply.github.com> --- src/private.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/private.rs b/src/private.rs index 7d76ae93..10e4e253 100644 --- a/src/private.rs +++ b/src/private.rs @@ -166,7 +166,7 @@ impl std::fmt::Debug for RedactionFormatter<'_> { } } - _ => {} + None => {} } if self.flags.partial { From 02e712bee197323d16e76d21b57084657688f310 Mon Sep 17 00:00:00 2001 From: William Venner <14863743+WilliamVenner@users.noreply.github.com> Date: Thu, 15 Dec 2022 15:11:10 +0000 Subject: [PATCH 4/5] `str` -> `to_redact` more descriptive name --- src/private.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/private.rs b/src/private.rs index 10e4e253..2f2545a9 100644 --- a/src/private.rs +++ b/src/private.rs @@ -41,10 +41,10 @@ impl RedactFlags { /// Maximum number of characters to expose at the beginning and end of a partial redact. const MAX_PARTIAL_EXPOSE: usize = 3; - pub(crate) fn redact_partial(&self, fmt: &mut std::fmt::Formatter, str: &str) -> std::fmt::Result { - let count = str.chars().filter(|char| char.is_alphanumeric()).count(); + pub(crate) fn redact_partial(&self, fmt: &mut std::fmt::Formatter, to_redact: &str) -> std::fmt::Result { + let count = to_redact.chars().filter(|char| char.is_alphanumeric()).count(); if count < Self::MIN_PARTIAL_CHARS { - for char in str.chars() { + for char in to_redact.chars() { if char.is_alphanumeric() { fmt.write_char(self.redact_char)?; } else { @@ -57,7 +57,7 @@ impl RedactFlags { let mut prefix_gas = redact_count; let mut middle_gas = count - redact_count - redact_count; - for char in str.chars() { + for char in to_redact.chars() { if char.is_alphanumeric() { if prefix_gas > 0 { prefix_gas -= 1; @@ -76,8 +76,8 @@ impl RedactFlags { Ok(()) } - pub(crate) fn redact_full(&self, fmt: &mut std::fmt::Formatter, str: &str) -> std::fmt::Result { - for char in str.chars() { + pub(crate) fn redact_full(&self, fmt: &mut std::fmt::Formatter, to_redact: &str) -> std::fmt::Result { + for char in to_redact.chars() { if char.is_whitespace() || !char.is_alphanumeric() { fmt.write_char(char)?; } else { From 570c79234a762ae78c9fed2c076299ea4278c874 Mon Sep 17 00:00:00 2001 From: William Venner <14863743+WilliamVenner@users.noreply.github.com> Date: Thu, 15 Dec 2022 15:30:24 +0000 Subject: [PATCH 5/5] Use an enum for much cleaner handling of redaction length modifier --- src/private.rs | 34 +++++--- veil-macros/src/flags.rs | 80 ++++++++++++------- veil-tests/src/compile_tests/fail.rs | 4 +- .../fail/redact_incompatible_flags.rs | 31 +++++++ .../fail/redact_incompatible_flags.stderr | 23 ++++++ .../fail/redact_partial_fixed.rs | 16 ---- .../fail/redact_partial_fixed.stderr | 11 --- 7 files changed, 126 insertions(+), 73 deletions(-) create mode 100644 veil-tests/src/compile_tests/fail/redact_incompatible_flags.rs create mode 100644 veil-tests/src/compile_tests/fail/redact_incompatible_flags.stderr delete mode 100644 veil-tests/src/compile_tests/fail/redact_partial_fixed.rs delete mode 100644 veil-tests/src/compile_tests/fail/redact_partial_fixed.stderr diff --git a/src/private.rs b/src/private.rs index 2f2545a9..6656d838 100644 --- a/src/private.rs +++ b/src/private.rs @@ -1,4 +1,7 @@ -use std::fmt::{Debug, Display, Write}; +use std::{ + fmt::{Debug, Display, Write}, + num::NonZeroU8, +}; pub enum RedactSpecialization { /// Whether the type we're redacting is an Option or not. Poor man's specialization! This is detected @@ -17,20 +20,25 @@ pub enum RedactSpecialization { Option, } +#[derive(Clone, Copy)] +pub enum RedactionLength { + /// Redact the entire data. + Full, + + /// Redact a portion of the data. + Partial, + + /// Whether to redact with a fixed width, ignoring the length of the data. + Fixed(NonZeroU8), +} + #[derive(Clone, Copy)] pub struct RedactFlags { - /// Whether to only partially redact the data. - /// - /// Incompatible with `fixed`. - pub partial: bool, + /// How much of the data to redact. + pub redact_length: RedactionLength, /// What character to use for redacting. pub redact_char: char, - - /// Whether to redact with a fixed width, ignoring the length of the data. - /// - /// Incompatible with `partial`. - pub fixed: u8, } impl RedactFlags { /// How many characters must a word be for it to be partially redacted? @@ -140,8 +148,8 @@ impl std::fmt::Debug for RedactionFormatter<'_> { return self.this.passthrough(fmt); } - if self.flags.fixed > 0 { - return RedactFlags::redact_fixed(fmt, self.flags.fixed as usize, self.flags.redact_char); + if let RedactionLength::Fixed(n) = &self.flags.redact_length { + return RedactFlags::redact_fixed(fmt, n.get() as usize, self.flags.redact_char); } let redactable_string = self.this.to_string(); @@ -169,7 +177,7 @@ impl std::fmt::Debug for RedactionFormatter<'_> { None => {} } - if self.flags.partial { + if let RedactionLength::Partial = &self.flags.redact_length { self.flags.redact_partial(fmt, &redactable_string) } else { self.flags.redact_full(fmt, &redactable_string) diff --git a/veil-macros/src/flags.rs b/veil-macros/src/flags.rs index 5304762e..2d6e6b7e 100644 --- a/veil-macros/src/flags.rs +++ b/veil-macros/src/flags.rs @@ -96,26 +96,42 @@ pub trait ExtractFlags: Sized + Copy + Default { } } +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum RedactionLength { + /// Redact the entire data. + Full, + + /// Redact a portion of the data. + Partial, + + /// Whether to redact with a fixed width, ignoring the length of the data. + Fixed(NonZeroU8), +} +impl quote::ToTokens for RedactionLength { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + match self { + RedactionLength::Full => quote! { ::veil::private::RedactionLength::Full }.to_tokens(tokens), + RedactionLength::Partial => quote! { ::veil::private::RedactionLength::Partial }.to_tokens(tokens), + RedactionLength::Fixed(n) => { + let n = n.get(); + quote! { ::veil::private::RedactionLength::Fixed(::core::num::NonZeroU8::new(#n).unwrap()) } + .to_tokens(tokens) + } + } + } +} + #[derive(Clone, Copy, PartialEq, Eq)] pub struct RedactFlags { - /// Whether the field is partially or fully redacted. - /// - /// Incompatible with `fixed`. - pub partial: bool, + pub redact_length: RedactionLength, /// The character to use for redacting. Defaults to `*`. pub redact_char: char, - - /// Whether to redact with a fixed width, ignoring the length of the data. - /// - /// Incompatible with `partial`. - pub fixed: Option, } impl Default for RedactFlags { fn default() -> Self { Self { - partial: false, - fixed: None, + redact_length: RedactionLength::Full, redact_char: '*', } } @@ -127,7 +143,13 @@ impl ExtractFlags for RedactFlags { match meta { // #[redact(partial)] syn::Meta::Path(path) if path.is_ident("partial") => { - self.partial = true; + if self.redact_length != RedactionLength::Full { + return TryParseMeta::Err(syn::Error::new_spanned( + path, + "`partial` clashes with an existing redaction length flag", + )); + } + self.redact_length = RedactionLength::Partial; } // #[redact(with = 'X')] @@ -137,9 +159,15 @@ impl ExtractFlags for RedactFlags { }, // #[redact(fixed = u8)] - syn::Meta::NameValue(kv) if kv.path.is_ident("fixed") => match kv.lit { - syn::Lit::Int(int) => { - self.fixed = Some( + syn::Meta::NameValue(kv) if kv.path.is_ident("fixed") => { + if self.redact_length != RedactionLength::Full { + return TryParseMeta::Err(syn::Error::new_spanned( + kv.path, + "`fixed` clashes with an existing redaction length flag", + )); + } + if let syn::Lit::Int(int) = kv.lit { + self.redact_length = RedactionLength::Fixed( match int.base10_parse::().and_then(|int| { NonZeroU8::new(int).ok_or_else(|| { syn::Error::new_spanned(int, "fixed redacting width must be greater than zero") @@ -149,9 +177,10 @@ impl ExtractFlags for RedactFlags { Err(err) => return TryParseMeta::Err(err), }, ) + } else { + return TryParseMeta::Err(syn::Error::new_spanned(kv.lit, "expected a character literal")); } - _ => return TryParseMeta::Err(syn::Error::new_spanned(kv.lit, "expected a character literal")), - }, + } _ => return TryParseMeta::Unrecognised(meta), } @@ -161,18 +190,14 @@ impl ExtractFlags for RedactFlags { impl quote::ToTokens for RedactFlags { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { let Self { - partial, + redact_length, redact_char, - fixed, .. } = self; - let fixed = fixed.map(|fixed| fixed.get()).unwrap_or(0); - tokens.extend(quote! { - partial: #partial, - redact_char: #redact_char, - fixed: #fixed, + redact_length: #redact_length, + redact_char: #redact_char }); } } @@ -261,13 +286,6 @@ impl ExtractFlags for FieldFlags { } } - if self.redact.partial && self.redact.fixed.is_some() { - return Err(syn::Error::new_spanned( - attr, - "`#[redact(partial)]` and `#[redact(fixed = ...)]` are incompatible", - )); - } - Ok(()) } } diff --git a/veil-tests/src/compile_tests/fail.rs b/veil-tests/src/compile_tests/fail.rs index eae311a7..e90e725d 100644 --- a/veil-tests/src/compile_tests/fail.rs +++ b/veil-tests/src/compile_tests/fail.rs @@ -10,7 +10,6 @@ fail_tests! { redact_all_on_field, redact_all_variant_on_variant, redact_enum_without_variant, - redact_partial_fixed, redact_too_many, redact_unused, redact_variant_on_field, @@ -19,5 +18,6 @@ fail_tests! { redact_units, redact_skip, redact_missing_all, - redact_display_enum_variant + redact_display_enum_variant, + redact_incompatible_flags } diff --git a/veil-tests/src/compile_tests/fail/redact_incompatible_flags.rs b/veil-tests/src/compile_tests/fail/redact_incompatible_flags.rs new file mode 100644 index 00000000..87bb196e --- /dev/null +++ b/veil-tests/src/compile_tests/fail/redact_incompatible_flags.rs @@ -0,0 +1,31 @@ +fn main() {} + +#[derive(veil::Redact)] +struct Foo { + #[redact(partial, fixed = 3)] + bar: String +} + +#[derive(veil::Redact)] +struct Bar { + #[redact(fixed = 3, partial)] + baz: String +} + +#[derive(veil::Redact)] +enum Baz { + #[redact(variant, fixed = 3, partial)] + Foo { + #[redact(fixed = 3, partial)] + bar: String, + } +} + +#[derive(veil::Redact)] +enum Qux { + #[redact(variant, partial, fixed = 3)] + Foo { + #[redact(partial, fixed = 3)] + bar: String, + } +} diff --git a/veil-tests/src/compile_tests/fail/redact_incompatible_flags.stderr b/veil-tests/src/compile_tests/fail/redact_incompatible_flags.stderr new file mode 100644 index 00000000..b71295c8 --- /dev/null +++ b/veil-tests/src/compile_tests/fail/redact_incompatible_flags.stderr @@ -0,0 +1,23 @@ +error: `fixed` clashes with an existing redaction length flag + --> src/compile_tests/fail/redact_incompatible_flags.rs:5:23 + | +5 | #[redact(partial, fixed = 3)] + | ^^^^^ + +error: `partial` clashes with an existing redaction length flag + --> src/compile_tests/fail/redact_incompatible_flags.rs:11:25 + | +11 | #[redact(fixed = 3, partial)] + | ^^^^^^^ + +error: `partial` clashes with an existing redaction length flag + --> src/compile_tests/fail/redact_incompatible_flags.rs:17:34 + | +17 | #[redact(variant, fixed = 3, partial)] + | ^^^^^^^ + +error: `fixed` clashes with an existing redaction length flag + --> src/compile_tests/fail/redact_incompatible_flags.rs:26:32 + | +26 | #[redact(variant, partial, fixed = 3)] + | ^^^^^ diff --git a/veil-tests/src/compile_tests/fail/redact_partial_fixed.rs b/veil-tests/src/compile_tests/fail/redact_partial_fixed.rs deleted file mode 100644 index 973a24c9..00000000 --- a/veil-tests/src/compile_tests/fail/redact_partial_fixed.rs +++ /dev/null @@ -1,16 +0,0 @@ -fn main() {} - -#[derive(veil::Redact)] -struct Foo { - #[redact(fixed = 3, partial)] - bar: String, -} - -#[derive(veil::Redact)] -enum Fooe { - #[redact(variant, fixed = 3, partial)] - Bar { - #[redact(fixed = 3, partial)] - baz: String, - } -} diff --git a/veil-tests/src/compile_tests/fail/redact_partial_fixed.stderr b/veil-tests/src/compile_tests/fail/redact_partial_fixed.stderr deleted file mode 100644 index ce1bfd36..00000000 --- a/veil-tests/src/compile_tests/fail/redact_partial_fixed.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: `#[redact(partial)]` and `#[redact(fixed = ...)]` are incompatible - --> src/compile_tests/fail/redact_partial_fixed.rs:5:5 - | -5 | #[redact(fixed = 3, partial)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -error: `#[redact(partial)]` and `#[redact(fixed = ...)]` are incompatible - --> src/compile_tests/fail/redact_partial_fixed.rs:11:5 - | -11 | #[redact(variant, fixed = 3, partial)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^