1use std::borrow::Cow;
2use std::fmt;
3use std::ops::{Deref, RangeInclusive};
4
5use enumset::{EnumSet, EnumSetType};
6use relay_protocol::Annotated;
7
8use crate::processor::ProcessValue;
9
10#[derive(Debug)]
12pub struct UnknownValueTypeError;
13
14impl fmt::Display for UnknownValueTypeError {
15 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
16 write!(f, "unknown value type")
17 }
18}
19
20impl std::error::Error for UnknownValueTypeError {}
21
22#[derive(Debug, Ord, PartialOrd, EnumSetType)]
24pub enum ValueType {
25 String,
27 Binary,
28 Number,
29 Boolean,
30 DateTime,
31 Array,
32 Object,
33
34 Event,
36 Attachments,
37 Replay,
38
39 Exception,
41 Stacktrace,
42 Frame,
43 Request,
44 User,
45 LogEntry,
46 Message,
47 Thread,
48 Breadcrumb,
49 OurLog,
50 TraceMetric,
51 Span,
52 ClientSdkInfo,
53
54 Minidump,
56 HeapMemory,
57 StackMemory,
58}
59
60impl ValueType {
61 pub fn for_field<T: ProcessValue>(field: &Annotated<T>) -> EnumSet<Self> {
62 field
63 .value()
64 .map(ProcessValue::value_type)
65 .unwrap_or_else(EnumSet::empty)
66 }
67}
68
69relay_common::derive_fromstr_and_display!(ValueType, UnknownValueTypeError, {
70 ValueType::String => "string",
71 ValueType::Binary => "binary",
72 ValueType::Number => "number",
73 ValueType::Boolean => "boolean" | "bool",
74 ValueType::DateTime => "datetime",
75 ValueType::Array => "array" | "list",
76 ValueType::Object => "object",
77 ValueType::Event => "event",
78 ValueType::Attachments => "attachments",
79 ValueType::Replay => "replay",
80 ValueType::Exception => "error" | "exception",
81 ValueType::Stacktrace => "stack" | "stacktrace",
82 ValueType::Frame => "frame",
83 ValueType::Request => "http" | "request",
84 ValueType::User => "user",
85 ValueType::LogEntry => "logentry",
86 ValueType::Message => "message",
87 ValueType::Thread => "thread",
88 ValueType::Breadcrumb => "breadcrumb",
89 ValueType::OurLog => "log",
90 ValueType::TraceMetric => "trace_metric",
91
92 ValueType::Span => "span",
93 ValueType::ClientSdkInfo => "sdk",
94 ValueType::Minidump => "minidump",
95 ValueType::HeapMemory => "heap_memory",
96 ValueType::StackMemory => "stack_memory",
97});
98
99#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
101pub enum Pii {
102 True,
104 False,
106 Maybe,
109}
110
111#[derive(Debug, Clone, Copy)]
113pub enum PiiMode {
114 Static(Pii),
116 Dynamic(fn(&ProcessingState) -> Pii),
118}
119
120#[derive(Debug, Clone, Copy)]
124pub enum SizeMode {
125 Static(Option<usize>),
126 Dynamic(fn(&ProcessingState) -> Option<usize>),
127}
128
129#[derive(Debug, Clone, Copy)]
131pub enum Required {
132 False,
134 ValueOrMeta,
136 Value,
138}
139
140#[derive(Debug, Clone, Copy)]
142pub struct FieldAttrs {
143 pub name: Option<&'static str>,
145 pub required: Required,
147 pub nonempty: bool,
149 pub trim_whitespace: bool,
151 pub characters: Option<CharacterSet>,
153 pub max_chars: SizeMode,
155 pub max_chars_allowance: usize,
157 pub max_depth: Option<usize>,
159 pub max_bytes: SizeMode,
161 pub bytes_size: SizeMode,
169 pub pii: PiiMode,
171 pub retain: bool,
173 pub trim: bool,
175}
176
177#[derive(Clone, Copy)]
181pub struct CharacterSet {
182 pub char_is_valid: fn(char) -> bool,
184 pub ranges: &'static [RangeInclusive<char>],
186 pub is_negative: bool,
188}
189
190impl fmt::Debug for CharacterSet {
191 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
192 f.debug_struct("CharacterSet")
193 .field("ranges", &self.ranges)
194 .field("is_negative", &self.is_negative)
195 .finish()
196 }
197}
198
199impl FieldAttrs {
200 pub const fn new() -> Self {
202 FieldAttrs {
203 name: None,
204 required: Required::False,
205 nonempty: false,
206 trim_whitespace: false,
207 characters: None,
208 max_chars: SizeMode::Static(None),
209 max_chars_allowance: 0,
210 max_depth: None,
211 max_bytes: SizeMode::Static(None),
212 pii: PiiMode::Static(Pii::False),
213 retain: false,
214 trim: true,
215 bytes_size: SizeMode::Static(None),
216 }
217 }
218
219 pub const fn required(mut self, required: Required) -> Self {
221 self.required = required;
222 self
223 }
224
225 pub const fn nonempty(mut self, nonempty: bool) -> Self {
230 self.nonempty = nonempty;
231 self
232 }
233
234 pub const fn trim_whitespace(mut self, trim_whitespace: bool) -> Self {
236 self.trim_whitespace = trim_whitespace;
237 self
238 }
239
240 pub const fn pii(mut self, pii: Pii) -> Self {
242 self.pii = PiiMode::Static(pii);
243 self
244 }
245
246 pub const fn pii_dynamic(mut self, pii: fn(&ProcessingState) -> Pii) -> Self {
248 self.pii = PiiMode::Dynamic(pii);
249 self
250 }
251
252 pub const fn max_chars(mut self, max_chars: Option<usize>) -> Self {
254 self.max_chars = SizeMode::Static(max_chars);
255 self
256 }
257
258 pub const fn max_chars_dynamic(
260 mut self,
261 max_chars: fn(&ProcessingState) -> Option<usize>,
262 ) -> Self {
263 self.max_chars = SizeMode::Dynamic(max_chars);
264 self
265 }
266
267 pub const fn max_bytes(mut self, max_bytes: Option<usize>) -> Self {
269 self.max_bytes = SizeMode::Static(max_bytes);
270 self
271 }
272
273 pub const fn max_bytes_dynamic(
275 mut self,
276 max_bytes: fn(&ProcessingState) -> Option<usize>,
277 ) -> Self {
278 self.max_bytes = SizeMode::Dynamic(max_bytes);
279 self
280 }
281
282 pub const fn retain(mut self, retain: bool) -> Self {
284 self.retain = retain;
285 self
286 }
287}
288
289static DEFAULT_FIELD_ATTRS: FieldAttrs = FieldAttrs::new();
290static PII_TRUE_FIELD_ATTRS: FieldAttrs = FieldAttrs::new().pii(Pii::True);
291static PII_MAYBE_FIELD_ATTRS: FieldAttrs = FieldAttrs::new().pii(Pii::Maybe);
292
293impl Default for FieldAttrs {
294 fn default() -> Self {
295 Self::new()
296 }
297}
298
299#[derive(Debug, Clone, Eq, Ord, PartialOrd)]
300enum PathItem<'a> {
301 StaticKey(&'a str),
302 OwnedKey(String),
303 Index(usize),
304}
305
306impl<'a> PartialEq for PathItem<'a> {
307 fn eq(&self, other: &PathItem<'a>) -> bool {
308 self.key() == other.key() && self.index() == other.index()
309 }
310}
311
312impl PathItem<'_> {
313 #[inline]
315 pub fn key(&self) -> Option<&str> {
316 match self {
317 PathItem::StaticKey(s) => Some(s),
318 PathItem::OwnedKey(s) => Some(s.as_str()),
319 PathItem::Index(_) => None,
320 }
321 }
322
323 #[inline]
325 pub fn index(&self) -> Option<usize> {
326 match self {
327 PathItem::StaticKey(_) | PathItem::OwnedKey(_) => None,
328 PathItem::Index(idx) => Some(*idx),
329 }
330 }
331}
332
333impl fmt::Display for PathItem<'_> {
334 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
335 match self {
336 PathItem::StaticKey(s) => f.pad(s),
337 PathItem::OwnedKey(s) => f.pad(s.as_str()),
338 PathItem::Index(val) => write!(f, "{val}"),
339 }
340 }
341}
342
343#[derive(Debug, Clone)]
353enum BoxCow<'a, T> {
354 Borrowed(&'a T),
355 Owned(Box<T>),
356}
357
358impl<T> Deref for BoxCow<'_, T> {
359 type Target = T;
360
361 fn deref(&self) -> &Self::Target {
362 match self {
363 BoxCow::Borrowed(inner) => inner,
364 BoxCow::Owned(inner) => inner.deref(),
365 }
366 }
367}
368
369#[derive(Debug, Clone)]
373pub struct ProcessingStateBuilder {
374 attrs: Option<FieldAttrs>,
375 value_type: EnumSet<ValueType>,
376}
377
378impl ProcessingStateBuilder {
379 pub fn attrs<F: FnOnce(FieldAttrs) -> FieldAttrs>(mut self, f: F) -> Self {
381 let attrs = self.attrs.take().unwrap_or_default();
382 self.attrs = Some(f(attrs));
383 self
384 }
385
386 pub fn required(self, required: Required) -> Self {
388 self.attrs(|attrs| attrs.required(required))
389 }
390
391 pub fn nonempty(self, nonempty: bool) -> Self {
396 self.attrs(|attrs| attrs.nonempty(nonempty))
397 }
398
399 pub fn trim_whitespace(self, trim_whitespace: bool) -> Self {
401 self.attrs(|attrs| attrs.trim_whitespace(trim_whitespace))
402 }
403
404 pub fn pii(self, pii: Pii) -> Self {
406 self.attrs(|attrs| attrs.pii(pii))
407 }
408
409 pub fn pii_dynamic(self, pii: fn(&ProcessingState) -> Pii) -> Self {
411 self.attrs(|attrs| attrs.pii_dynamic(pii))
412 }
413
414 pub fn max_chars(self, max_chars: impl Into<Option<usize>>) -> Self {
416 self.attrs(|attrs| attrs.max_chars(max_chars.into()))
417 }
418
419 pub fn max_chars_dynamic(self, max_chars: fn(&ProcessingState) -> Option<usize>) -> Self {
421 self.attrs(|attrs| attrs.max_chars_dynamic(max_chars))
422 }
423
424 pub fn max_bytes(self, max_bytes: impl Into<Option<usize>>) -> Self {
426 self.attrs(|attrs| attrs.max_bytes(max_bytes.into()))
427 }
428
429 pub fn max_bytes_dynamic(self, max_bytes: fn(&ProcessingState) -> Option<usize>) -> Self {
431 self.attrs(|attrs| attrs.max_bytes_dynamic(max_bytes))
432 }
433
434 pub fn retain(self, retain: bool) -> Self {
436 self.attrs(|attrs| attrs.retain(retain))
437 }
438
439 pub fn value_type(mut self, value_type: EnumSet<ValueType>) -> Self {
441 self.value_type = value_type;
442 self
443 }
444
445 pub fn build(self) -> ProcessingState<'static> {
448 let Self { attrs, value_type } = self;
449 ProcessingState {
450 parent: None,
451 path_item: None,
452 attrs: attrs.map(Cow::Owned),
453 value_type,
454 depth: 0,
455 }
456 }
457}
458
459#[derive(Debug, Clone)]
468pub struct ProcessingState<'a> {
469 parent: Option<BoxCow<'a, ProcessingState<'a>>>,
475 path_item: Option<PathItem<'a>>,
476 attrs: Option<Cow<'a, FieldAttrs>>,
477 value_type: EnumSet<ValueType>,
478 depth: usize,
479}
480
481static ROOT_STATE: ProcessingState = ProcessingState {
482 parent: None,
483 path_item: None,
484 attrs: None,
485 value_type: enumset::enum_set!(),
486 depth: 0,
487};
488
489impl<'a> ProcessingState<'a> {
490 pub fn root() -> &'static ProcessingState<'static> {
492 &ROOT_STATE
493 }
494
495 pub fn new_root(
497 attrs: Option<Cow<'static, FieldAttrs>>,
498 value_type: impl IntoIterator<Item = ValueType>,
499 ) -> ProcessingState<'static> {
500 ProcessingState {
501 parent: None,
502 path_item: None,
503 attrs,
504 value_type: value_type.into_iter().collect(),
505 depth: 0,
506 }
507 }
508
509 pub fn root_builder() -> ProcessingStateBuilder {
522 ProcessingStateBuilder {
523 attrs: None,
524 value_type: EnumSet::empty(),
525 }
526 }
527
528 pub fn enter_borrowed(
530 &'a self,
531 key: &'a str,
532 attrs: Option<Cow<'a, FieldAttrs>>,
533 value_type: impl IntoIterator<Item = ValueType>,
534 ) -> Self {
535 ProcessingState {
536 parent: Some(BoxCow::Borrowed(self)),
537 path_item: Some(PathItem::StaticKey(key)),
538 attrs,
539 value_type: value_type.into_iter().collect(),
540 depth: self.depth + 1,
541 }
542 }
543
544 pub fn enter_owned(
548 self,
549 key: String,
550 attrs: Option<Cow<'a, FieldAttrs>>,
551 value_type: impl IntoIterator<Item = ValueType>,
552 ) -> Self {
553 let depth = self.depth + 1;
554 ProcessingState {
555 parent: Some(BoxCow::Owned(self.into())),
556 path_item: Some(PathItem::OwnedKey(key)),
557 attrs,
558 value_type: value_type.into_iter().collect(),
559 depth,
560 }
561 }
562
563 pub fn enter_index(
565 &'a self,
566 idx: usize,
567 attrs: Option<Cow<'a, FieldAttrs>>,
568 value_type: impl IntoIterator<Item = ValueType>,
569 ) -> Self {
570 ProcessingState {
571 parent: Some(BoxCow::Borrowed(self)),
572 path_item: Some(PathItem::Index(idx)),
573 attrs,
574 value_type: value_type.into_iter().collect(),
575 depth: self.depth + 1,
576 }
577 }
578
579 pub fn enter_nothing(&'a self, attrs: Option<Cow<'a, FieldAttrs>>) -> Self {
581 ProcessingState {
582 attrs,
583 path_item: None,
584 parent: Some(BoxCow::Borrowed(self)),
585 ..self.clone()
586 }
587 }
588
589 pub fn path(&'a self) -> Path<'a> {
591 Path(self)
592 }
593
594 pub fn value_type(&self) -> EnumSet<ValueType> {
595 self.value_type
596 }
597
598 pub fn attrs(&self) -> &FieldAttrs {
600 match self.attrs {
601 Some(ref cow) => cow,
602 None => &DEFAULT_FIELD_ATTRS,
603 }
604 }
605
606 pub fn inner_attrs(&self) -> Option<Cow<'_, FieldAttrs>> {
608 match self.attrs().pii {
609 PiiMode::Static(Pii::True) => Some(Cow::Borrowed(&PII_TRUE_FIELD_ATTRS)),
610 PiiMode::Static(Pii::False) => None,
611 PiiMode::Static(Pii::Maybe) => Some(Cow::Borrowed(&PII_MAYBE_FIELD_ATTRS)),
612 PiiMode::Dynamic(f) => Some(Cow::Owned(DEFAULT_FIELD_ATTRS.pii_dynamic(f))),
613 }
614 }
615
616 pub fn pii(&self) -> Pii {
622 match self.attrs().pii {
623 PiiMode::Static(pii) => pii,
624 PiiMode::Dynamic(pii_fn) => pii_fn(self),
625 }
626 }
627
628 pub fn max_bytes(&self) -> Option<usize> {
634 match self.attrs().max_bytes {
635 SizeMode::Static(n) => n,
636 SizeMode::Dynamic(max_bytes_fn) => max_bytes_fn(self),
637 }
638 }
639
640 pub fn bytes_size(&self) -> Option<usize> {
646 match self.attrs().bytes_size {
647 SizeMode::Static(n) => n,
648 SizeMode::Dynamic(bytes_size_fn) => bytes_size_fn(self),
649 }
650 }
651
652 pub fn max_chars(&self) -> Option<usize> {
658 match self.attrs().max_chars {
659 SizeMode::Static(n) => n,
660 SizeMode::Dynamic(max_chars_fn) => max_chars_fn(self),
661 }
662 }
663
664 pub fn iter(&'a self) -> ProcessingStateIter<'a> {
669 ProcessingStateIter {
670 state: Some(self),
671 size: self.depth,
672 }
673 }
674
675 #[expect(
680 clippy::result_large_err,
681 reason = "this method returns `self` in the error case"
682 )]
683 pub fn try_into_parent(self) -> Result<Option<Self>, Self> {
684 match self.parent {
685 Some(BoxCow::Borrowed(_)) => Err(self),
686 Some(BoxCow::Owned(parent)) => Ok(Some(*parent)),
687 None => Ok(None),
688 }
689 }
690
691 pub fn depth(&'a self) -> usize {
693 self.depth
694 }
695
696 pub fn entered_anything(&'a self) -> bool {
700 if let Some(parent) = &self.parent {
701 parent.depth() != self.depth()
702 } else {
703 true
704 }
705 }
706
707 pub fn keys(&self) -> impl Iterator<Item = &str> {
710 self.iter()
711 .filter_map(|state| state.path_item.as_ref())
712 .flat_map(|item| item.key())
713 }
714
715 #[inline]
718 fn path_item(&self) -> Option<&PathItem<'_>> {
719 for state in self.iter() {
720 if let Some(ref path_item) = state.path_item {
721 return Some(path_item);
722 }
723 }
724 None
725 }
726}
727
728pub struct ProcessingStateIter<'a> {
729 state: Option<&'a ProcessingState<'a>>,
730 size: usize,
731}
732
733impl<'a> Iterator for ProcessingStateIter<'a> {
734 type Item = &'a ProcessingState<'a>;
735
736 fn next(&mut self) -> Option<Self::Item> {
737 let current = self.state?;
738 self.state = current.parent.as_deref();
739 Some(current)
740 }
741
742 fn size_hint(&self) -> (usize, Option<usize>) {
743 (self.size, Some(self.size))
744 }
745}
746
747impl ExactSizeIterator for ProcessingStateIter<'_> {}
748
749impl Default for ProcessingState<'_> {
750 fn default() -> Self {
751 ProcessingState::root().clone()
752 }
753}
754
755#[derive(Debug)]
759pub struct Path<'a>(&'a ProcessingState<'a>);
760
761impl Path<'_> {
762 #[inline]
764 pub fn key(&self) -> Option<&str> {
765 PathItem::key(self.0.path_item()?)
766 }
767
768 #[inline]
770 pub fn index(&self) -> Option<usize> {
771 PathItem::index(self.0.path_item()?)
772 }
773
774 pub fn depth(&self) -> usize {
776 self.0.depth()
777 }
778
779 pub fn attrs(&self) -> &FieldAttrs {
781 self.0.attrs()
782 }
783
784 pub fn pii(&self) -> Pii {
786 self.0.pii()
787 }
788
789 pub fn iter(&self) -> ProcessingStateIter<'_> {
791 self.0.iter()
792 }
793}
794
795impl fmt::Display for Path<'_> {
796 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
797 let mut items = Vec::with_capacity(self.0.depth);
798 for state in self.0.iter() {
799 if let Some(ref path_item) = state.path_item {
800 items.push(path_item)
801 }
802 }
803
804 for (idx, item) in items.into_iter().rev().enumerate() {
805 if idx > 0 {
806 write!(f, ".")?;
807 }
808 write!(f, "{item}")?;
809 }
810 Ok(())
811 }
812}
813
814#[cfg(test)]
815mod tests {
816
817 use relay_protocol::{Annotated, Empty, FromValue, IntoValue, Object, SerializableAnnotated};
818
819 use crate::processor::attrs::ROOT_STATE;
820 use crate::processor::{Pii, ProcessValue, ProcessingState, Processor, process_value};
821
822 fn pii_from_item_name(state: &ProcessingState) -> Pii {
823 match state.path_item().and_then(|p| p.key()) {
824 Some("true_item") => Pii::True,
825 Some("false_item") => Pii::False,
826 _ => Pii::Maybe,
827 }
828 }
829
830 fn max_chars_from_item_name(state: &ProcessingState) -> Option<usize> {
831 match state.path_item().and_then(|p| p.key()) {
832 Some("short_item") => Some(10),
833 Some("long_item") => Some(20),
834 _ => None,
835 }
836 }
837
838 #[derive(Debug, Clone, Empty, IntoValue, FromValue, ProcessValue)]
839 #[metastructure(pii = "pii_from_item_name")]
840 struct TestValue(#[metastructure(max_chars = "max_chars_from_item_name")] String);
841
842 struct TestPiiProcessor;
843
844 impl Processor for TestPiiProcessor {
845 fn process_string(
846 &mut self,
847 value: &mut String,
848 _meta: &mut relay_protocol::Meta,
849 state: &ProcessingState<'_>,
850 ) -> crate::processor::ProcessingResult where {
851 match state.pii() {
852 Pii::True => *value = "true".to_owned(),
853 Pii::False => *value = "false".to_owned(),
854 Pii::Maybe => *value = "maybe".to_owned(),
855 }
856 Ok(())
857 }
858 }
859
860 struct TestTrimmingProcessor;
861
862 impl Processor for TestTrimmingProcessor {
863 fn process_string(
864 &mut self,
865 value: &mut String,
866 _meta: &mut relay_protocol::Meta,
867 state: &ProcessingState<'_>,
868 ) -> crate::processor::ProcessingResult where {
869 if let Some(n) = state.max_chars() {
870 value.truncate(n);
871 }
872 Ok(())
873 }
874 }
875
876 #[test]
877 fn test_dynamic_pii() {
878 let mut object: Annotated<Object<TestValue>> = Annotated::from_json(
879 r#"
880 {
881 "false_item": "replace me",
882 "other_item": "replace me",
883 "true_item": "replace me"
884 }
885 "#,
886 )
887 .unwrap();
888
889 process_value(&mut object, &mut TestPiiProcessor, &ROOT_STATE).unwrap();
890
891 insta::assert_json_snapshot!(SerializableAnnotated(&object), @r###"
892 {
893 "false_item": "false",
894 "other_item": "maybe",
895 "true_item": "true"
896 }
897 "###);
898 }
899
900 #[test]
901 fn test_dynamic_max_chars() {
902 let mut object: Annotated<Object<TestValue>> = Annotated::from_json(
903 r#"
904 {
905 "short_item": "Should be shortened to 10",
906 "long_item": "Should be shortened to 20",
907 "other_item": "Should not be shortened at all"
908 }
909 "#,
910 )
911 .unwrap();
912
913 process_value(&mut object, &mut TestTrimmingProcessor, &ROOT_STATE).unwrap();
914
915 insta::assert_json_snapshot!(SerializableAnnotated(&object), @r###"
916 {
917 "long_item": "Should be shortened ",
918 "other_item": "Should not be shortened at all",
919 "short_item": "Should be "
920 }
921 "###);
922 }
923}