diff --git a/relay-event-derive/src/lib.rs b/relay-event-derive/src/lib.rs index 1c5c0ab47f2..960b56c2499 100644 --- a/relay-event-derive/src/lib.rs +++ b/relay-event-derive/src/lib.rs @@ -12,7 +12,7 @@ use proc_macro2::{Span, TokenStream}; use quote::{ToTokens, quote}; -use syn::meta::ParseNestedMeta; +use syn::parse::ParseStream; use syn::{ExprPath, Ident, Lit, LitBool, LitInt, LitStr}; use synstructure::decl_derive; @@ -284,8 +284,7 @@ fn parse_type_attributes(s: &synstructure::Structure<'_>) -> syn::Result()?; rv.trim = Some(s.value()); } else if ident == "pii" { - let s = meta.value()?.parse::()?; - rv.pii = parse_pii_value(s, &meta)?; + rv.pii = Some(meta.value()?.parse()?); } else { // Ignore other attributes used by `relay-protocol-derive`. if !meta.input.peek(syn::Token![,]) { @@ -308,19 +307,66 @@ enum Pii { Dynamic(ExprPath), } +impl syn::parse::Parse for Pii { + fn parse(input: ParseStream) -> syn::Result { + let head = input.fork(); + let value = input.parse::()?; + let pii = match value.value().as_str() { + "true" => Self::True, + "false" => Self::False, + "maybe" => Self::Maybe, + _ => Self::Dynamic(value.parse().map_err(|_| { + head.error("Expected one of `true`, `false`, `maybe`, or a function name") + })?), + }; + Ok(pii) + } +} + impl Pii { fn as_tokens(&self) -> TokenStream { match self { - Pii::True => quote!(::relay_event_schema::processor::PiiMode::Static( + Self::True => quote!(::relay_event_schema::processor::PiiMode::Static( ::relay_event_schema::processor::Pii::True )), - Pii::False => quote!(::relay_event_schema::processor::PiiMode::Static( + Self::False => quote!(::relay_event_schema::processor::PiiMode::Static( ::relay_event_schema::processor::Pii::False )), - Pii::Maybe => quote!(::relay_event_schema::processor::PiiMode::Static( + Self::Maybe => quote!(::relay_event_schema::processor::PiiMode::Static( ::relay_event_schema::processor::Pii::Maybe )), - Pii::Dynamic(fun) => quote!(::relay_event_schema::processor::PiiMode::Dynamic(#fun)), + Self::Dynamic(fun) => quote!(::relay_event_schema::processor::PiiMode::Dynamic(#fun)), + } + } +} + +#[derive(Clone, Debug)] +enum Size { + Static(usize), + Dynamic(ExprPath), +} + +impl syn::parse::Parse for Size { + fn parse(input: ParseStream) -> syn::Result { + if input.peek(LitInt) { + let lit = input.parse::()?; + return Ok(Self::Static(lit.base10_parse()?)); + } + + let head = input.fork(); + let path = input + .parse::()? + .parse() + .map_err(|_| head.error("Expected a function name"))?; + Ok(Self::Dynamic(path)) + } +} + +impl Size { + fn as_tokens(&self) -> TokenStream { + match self { + Self::Static(n) => quote!(::relay_event_schema::processor::SizeMode::Static(Some(#n))), + Self::Dynamic(fun) => quote!(::relay_event_schema::processor::SizeMode::Dynamic(#fun)), } } } @@ -337,10 +383,10 @@ struct FieldAttrs { pii: Option, retain: bool, characters: Option, - max_chars: Option, + max_chars: Option, max_chars_allowance: Option, max_depth: Option, - max_bytes: Option, + max_bytes: Option, trim: Option, } @@ -402,11 +448,11 @@ impl FieldAttrs { let retain = self.retain; let max_chars = if let Some(ref max_chars) = self.max_chars { - quote!(Some(#max_chars)) + max_chars.as_tokens() } else if let Some(ref parent_attrs) = inherit_from_field_attrs { quote!(#parent_attrs.max_chars) } else { - quote!(None) + quote!(::relay_event_schema::processor::SizeMode::Static(None)) }; let max_chars_allowance = if let Some(ref max_chars_allowance) = self.max_chars_allowance { @@ -426,11 +472,11 @@ impl FieldAttrs { }; let max_bytes = if let Some(ref max_bytes) = self.max_bytes { - quote!(Some(#max_bytes)) + max_bytes.as_tokens() } else if let Some(ref parent_attrs) = inherit_from_field_attrs { quote!(#parent_attrs.max_bytes) } else { - quote!(None) + quote!(::relay_event_schema::processor::SizeMode::Static(None)) }; let characters = if let Some(ref characters) = self.characters { @@ -513,8 +559,7 @@ fn parse_field_attributes( let s = meta.value()?.parse::()?; rv.characters = Some(parse_character_set(ident, &s.value())); } else if ident == "max_chars" { - let s = meta.value()?.parse::()?; - rv.max_chars = Some(quote!(#s)); + rv.max_chars = Some(meta.value()?.parse()?); } else if ident == "max_chars_allowance" { let s = meta.value()?.parse::()?; rv.max_chars_allowance = Some(quote!(#s)); @@ -522,11 +567,9 @@ fn parse_field_attributes( let s = meta.value()?.parse::()?; rv.max_depth = Some(quote!(#s)); } else if ident == "max_bytes" { - let s = meta.value()?.parse::()?; - rv.max_bytes = Some(quote!(#s)); + rv.max_bytes = Some(meta.value()?.parse()?); } else if ident == "pii" { - let s = meta.value()?.parse::()?; - rv.pii = parse_pii_value(s, &meta)?; + rv.pii = Some(meta.value()?.parse()?); } else if ident == "retain" { let s = meta.value()?.parse::()?; rv.retain = s.value(); @@ -600,14 +643,3 @@ fn parse_character_set(ident: &Ident, value: &str) -> TokenStream { } } } - -fn parse_pii_value(value: LitStr, meta: &ParseNestedMeta) -> syn::Result> { - Ok(Some(match value.value().as_str() { - "true" => Pii::True, - "false" => Pii::False, - "maybe" => Pii::Maybe, - _ => Pii::Dynamic(value.parse().map_err(|_| { - meta.error("Expected one of `true`, `false`, `maybe`, or a function name") - })?), - })) -} diff --git a/relay-event-normalization/src/trimming.rs b/relay-event-normalization/src/trimming.rs index fa304133f76..a381d4eaa4f 100644 --- a/relay-event-normalization/src/trimming.rs +++ b/relay-event-normalization/src/trimming.rs @@ -66,9 +66,9 @@ impl Processor for TrimmingProcessor { // If we encounter a max_bytes or max_depth attribute it // resets the size and depth that is permitted below it. // XXX(iker): test setting only one of the two attributes. - if state.attrs().max_bytes.is_some() || state.attrs().max_depth.is_some() { + if state.max_bytes().is_some() || state.attrs().max_depth.is_some() { self.size_state.push(SizeState { - size_remaining: state.attrs().max_bytes, + size_remaining: state.max_bytes(), encountered_at_depth: state.depth(), max_depth: state.attrs().max_depth, }); @@ -128,7 +128,7 @@ impl Processor for TrimmingProcessor { meta: &mut Meta, state: &ProcessingState<'_>, ) -> ProcessingResult { - if let Some(max_chars) = state.attrs().max_chars { + if let Some(max_chars) = state.max_chars() { trim_string(value, meta, max_chars, state.attrs().max_chars_allowance); } diff --git a/relay-event-schema/src/processor/attrs.rs b/relay-event-schema/src/processor/attrs.rs index 557bb17258e..a4883c74654 100644 --- a/relay-event-schema/src/processor/attrs.rs +++ b/relay-event-schema/src/processor/attrs.rs @@ -117,6 +117,15 @@ pub enum PiiMode { Dynamic(fn(&ProcessingState) -> Pii), } +/// A static or dynamic Option<`usize`> value. +/// +/// Used for the fields `max_chars` and `max_bytes`. +#[derive(Debug, Clone, Copy)] +pub enum SizeMode { + Static(Option), + Dynamic(fn(&ProcessingState) -> Option), +} + /// Meta information about a field. #[derive(Debug, Clone, Copy)] pub struct FieldAttrs { @@ -131,13 +140,13 @@ pub struct FieldAttrs { /// A set of allowed or denied character ranges for this string. pub characters: Option, /// The maximum char length of this field. - pub max_chars: Option, + pub max_chars: SizeMode, /// The extra char length allowance on top of max_chars. pub max_chars_allowance: usize, /// The maximum depth of this field. pub max_depth: Option, /// The maximum number of bytes of this field. - pub max_bytes: Option, + pub max_bytes: SizeMode, /// The type of PII on the field. pub pii: PiiMode, /// Whether additional properties should be retained during normalization. @@ -177,10 +186,10 @@ impl FieldAttrs { nonempty: false, trim_whitespace: false, characters: None, - max_chars: None, + max_chars: SizeMode::Static(None), max_chars_allowance: 0, max_depth: None, - max_bytes: None, + max_bytes: SizeMode::Static(None), pii: PiiMode::Static(Pii::False), retain: false, trim: true, @@ -222,7 +231,16 @@ impl FieldAttrs { /// Sets the maximum number of characters allowed in the field. pub const fn max_chars(mut self, max_chars: usize) -> Self { - self.max_chars = Some(max_chars); + self.max_chars = SizeMode::Static(Some(max_chars)); + self + } + + /// Sets the maximum number of characters allowed in the field dynamically based on the current state. + pub const fn max_chars_dynamic( + mut self, + max_chars: fn(&ProcessingState) -> Option, + ) -> Self { + self.max_chars = SizeMode::Dynamic(max_chars); self } @@ -462,6 +480,30 @@ impl<'a> ProcessingState<'a> { } } + /// Returns the max bytes for this state. + /// + /// If the state's `FieldAttrs` contain a fixed `max_bytes` value, + /// it is returned. If they contain a dynamic `max_bytes` value (a function), + /// it is applied to this state and the output returned. + pub fn max_bytes(&self) -> Option { + match self.attrs().max_bytes { + SizeMode::Static(n) => n, + SizeMode::Dynamic(max_bytes_fn) => max_bytes_fn(self), + } + } + + /// Returns the max chars for this state. + /// + /// If the state's `FieldAttrs` contain a fixed `max_chars` value, + /// it is returned. If they contain a dynamic `max_chars` value (a function), + /// it is applied to this state and the output returned. + pub fn max_chars(&self) -> Option { + match self.attrs().max_chars { + SizeMode::Static(n) => n, + SizeMode::Dynamic(max_chars_fn) => max_chars_fn(self), + } + } + /// Iterates through this state and all its ancestors up the hierarchy. /// /// This starts at the top of the stack of processing states and ends at the root. Thus @@ -628,13 +670,21 @@ mod tests { } } + fn max_chars_from_item_name(state: &ProcessingState) -> Option { + match state.path_item().and_then(|p| p.key()) { + Some("short_item") => Some(10), + Some("long_item") => Some(20), + _ => None, + } + } + #[derive(Debug, Clone, Empty, IntoValue, FromValue, ProcessValue)] #[metastructure(pii = "pii_from_item_name")] - struct TestValue(String); + struct TestValue(#[metastructure(max_chars = "max_chars_from_item_name")] String); - struct TestProcessor; + struct TestPiiProcessor; - impl Processor for TestProcessor { + impl Processor for TestPiiProcessor { fn process_string( &mut self, value: &mut String, @@ -650,6 +700,22 @@ mod tests { } } + struct TestTrimmingProcessor; + + impl Processor for TestTrimmingProcessor { + fn process_string( + &mut self, + value: &mut String, + _meta: &mut relay_protocol::Meta, + state: &ProcessingState<'_>, + ) -> crate::processor::ProcessingResult where { + if let Some(n) = state.max_chars() { + value.truncate(n); + } + Ok(()) + } + } + #[test] fn test_dynamic_pii() { let mut object: Annotated> = Annotated::from_json( @@ -663,7 +729,7 @@ mod tests { ) .unwrap(); - process_value(&mut object, &mut TestProcessor, &ROOT_STATE).unwrap(); + process_value(&mut object, &mut TestPiiProcessor, &ROOT_STATE).unwrap(); insta::assert_json_snapshot!(SerializableAnnotated(&object), @r###" { @@ -673,4 +739,28 @@ mod tests { } "###); } + + #[test] + fn test_dynamic_max_chars() { + let mut object: Annotated> = Annotated::from_json( + r#" + { + "short_item": "Should be shortened to 10", + "long_item": "Should be shortened to 20", + "other_item": "Should not be shortened at all" + } + "#, + ) + .unwrap(); + + process_value(&mut object, &mut TestTrimmingProcessor, &ROOT_STATE).unwrap(); + + insta::assert_json_snapshot!(SerializableAnnotated(&object), @r###" + { + "long_item": "Should be shortened ", + "other_item": "Should not be shortened at all", + "short_item": "Should be " + } + "###); + } }