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 trim(mut self, trim: bool) -> Self {
254 self.trim = trim;
255 self
256 }
257
258 pub const fn max_chars(mut self, max_chars: Option<usize>) -> Self {
260 self.max_chars = SizeMode::Static(max_chars);
261 self
262 }
263
264 pub const fn max_chars_dynamic(
266 mut self,
267 max_chars: fn(&ProcessingState) -> Option<usize>,
268 ) -> Self {
269 self.max_chars = SizeMode::Dynamic(max_chars);
270 self
271 }
272
273 pub const fn max_bytes(mut self, max_bytes: Option<usize>) -> Self {
275 self.max_bytes = SizeMode::Static(max_bytes);
276 self
277 }
278
279 pub const fn max_bytes_dynamic(
281 mut self,
282 max_bytes: fn(&ProcessingState) -> Option<usize>,
283 ) -> Self {
284 self.max_bytes = SizeMode::Dynamic(max_bytes);
285 self
286 }
287
288 pub const fn retain(mut self, retain: bool) -> Self {
290 self.retain = retain;
291 self
292 }
293}
294
295static DEFAULT_FIELD_ATTRS: FieldAttrs = FieldAttrs::new();
296
297impl Default for FieldAttrs {
298 fn default() -> Self {
299 Self::new()
300 }
301}
302
303#[derive(Debug, Clone, Eq, Ord, PartialOrd)]
304enum PathItem<'a> {
305 StaticKey(&'a str),
306 OwnedKey(String),
307 Index(usize),
308}
309
310impl<'a> PartialEq for PathItem<'a> {
311 fn eq(&self, other: &PathItem<'a>) -> bool {
312 self.key() == other.key() && self.index() == other.index()
313 }
314}
315
316impl PathItem<'_> {
317 #[inline]
319 pub fn key(&self) -> Option<&str> {
320 match self {
321 PathItem::StaticKey(s) => Some(s),
322 PathItem::OwnedKey(s) => Some(s.as_str()),
323 PathItem::Index(_) => None,
324 }
325 }
326
327 #[inline]
329 pub fn index(&self) -> Option<usize> {
330 match self {
331 PathItem::StaticKey(_) | PathItem::OwnedKey(_) => None,
332 PathItem::Index(idx) => Some(*idx),
333 }
334 }
335}
336
337impl fmt::Display for PathItem<'_> {
338 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
339 match self {
340 PathItem::StaticKey(s) => f.pad(s),
341 PathItem::OwnedKey(s) => f.pad(s.as_str()),
342 PathItem::Index(val) => write!(f, "{val}"),
343 }
344 }
345}
346
347#[derive(Debug, Clone)]
357enum BoxCow<'a, T> {
358 Borrowed(&'a T),
359 Owned(Box<T>),
360}
361
362impl<T> Deref for BoxCow<'_, T> {
363 type Target = T;
364
365 fn deref(&self) -> &Self::Target {
366 match self {
367 BoxCow::Borrowed(inner) => inner,
368 BoxCow::Owned(inner) => inner.deref(),
369 }
370 }
371}
372
373#[derive(Debug, Clone)]
377pub struct ProcessingStateBuilder {
378 attrs: Option<FieldAttrs>,
379 value_type: EnumSet<ValueType>,
380}
381
382impl ProcessingStateBuilder {
383 pub fn attrs<F: FnOnce(FieldAttrs) -> FieldAttrs>(mut self, f: F) -> Self {
385 let attrs = self.attrs.take().unwrap_or_default();
386 self.attrs = Some(f(attrs));
387 self
388 }
389
390 pub fn required(self, required: Required) -> Self {
392 self.attrs(|attrs| attrs.required(required))
393 }
394
395 pub fn nonempty(self, nonempty: bool) -> Self {
400 self.attrs(|attrs| attrs.nonempty(nonempty))
401 }
402
403 pub fn trim_whitespace(self, trim_whitespace: bool) -> Self {
405 self.attrs(|attrs| attrs.trim_whitespace(trim_whitespace))
406 }
407
408 pub fn pii(self, pii: Pii) -> Self {
410 self.attrs(|attrs| attrs.pii(pii))
411 }
412
413 pub fn pii_dynamic(self, pii: fn(&ProcessingState) -> Pii) -> Self {
415 self.attrs(|attrs| attrs.pii_dynamic(pii))
416 }
417
418 pub fn max_chars(self, max_chars: impl Into<Option<usize>>) -> Self {
420 self.attrs(|attrs| attrs.max_chars(max_chars.into()))
421 }
422
423 pub fn max_chars_dynamic(self, max_chars: fn(&ProcessingState) -> Option<usize>) -> Self {
425 self.attrs(|attrs| attrs.max_chars_dynamic(max_chars))
426 }
427
428 pub fn max_bytes(self, max_bytes: impl Into<Option<usize>>) -> Self {
430 self.attrs(|attrs| attrs.max_bytes(max_bytes.into()))
431 }
432
433 pub fn max_bytes_dynamic(self, max_bytes: fn(&ProcessingState) -> Option<usize>) -> Self {
435 self.attrs(|attrs| attrs.max_bytes_dynamic(max_bytes))
436 }
437
438 pub fn retain(self, retain: bool) -> Self {
440 self.attrs(|attrs| attrs.retain(retain))
441 }
442
443 pub fn value_type(mut self, value_type: EnumSet<ValueType>) -> Self {
445 self.value_type = value_type;
446 self
447 }
448
449 pub fn build(self) -> ProcessingState<'static> {
452 let Self { attrs, value_type } = self;
453 ProcessingState {
454 parent: None,
455 path_item: None,
456 attrs: attrs.map(Cow::Owned),
457 value_type,
458 depth: 0,
459 }
460 }
461}
462
463#[derive(Debug, Clone)]
472pub struct ProcessingState<'a> {
473 parent: Option<BoxCow<'a, ProcessingState<'a>>>,
479 path_item: Option<PathItem<'a>>,
480 attrs: Option<Cow<'a, FieldAttrs>>,
481 value_type: EnumSet<ValueType>,
482 depth: usize,
483}
484
485static ROOT_STATE: ProcessingState = ProcessingState {
486 parent: None,
487 path_item: None,
488 attrs: None,
489 value_type: enumset::enum_set!(),
490 depth: 0,
491};
492
493impl<'a> ProcessingState<'a> {
494 pub fn root() -> &'static ProcessingState<'static> {
496 &ROOT_STATE
497 }
498
499 pub fn new_root(
501 attrs: Option<Cow<'static, FieldAttrs>>,
502 value_type: impl IntoIterator<Item = ValueType>,
503 ) -> ProcessingState<'static> {
504 ProcessingState {
505 parent: None,
506 path_item: None,
507 attrs,
508 value_type: value_type.into_iter().collect(),
509 depth: 0,
510 }
511 }
512
513 pub fn root_builder() -> ProcessingStateBuilder {
526 ProcessingStateBuilder {
527 attrs: None,
528 value_type: EnumSet::empty(),
529 }
530 }
531
532 pub fn enter_borrowed(
534 &'a self,
535 key: &'a str,
536 attrs: Option<Cow<'a, FieldAttrs>>,
537 value_type: impl IntoIterator<Item = ValueType>,
538 ) -> Self {
539 ProcessingState {
540 parent: Some(BoxCow::Borrowed(self)),
541 path_item: Some(PathItem::StaticKey(key)),
542 attrs,
543 value_type: value_type.into_iter().collect(),
544 depth: self.depth + 1,
545 }
546 }
547
548 pub fn enter_owned(
552 self,
553 key: String,
554 attrs: Option<Cow<'a, FieldAttrs>>,
555 value_type: impl IntoIterator<Item = ValueType>,
556 ) -> Self {
557 let depth = self.depth + 1;
558 ProcessingState {
559 parent: Some(BoxCow::Owned(self.into())),
560 path_item: Some(PathItem::OwnedKey(key)),
561 attrs,
562 value_type: value_type.into_iter().collect(),
563 depth,
564 }
565 }
566
567 pub fn enter_index(
569 &'a self,
570 idx: usize,
571 attrs: Option<Cow<'a, FieldAttrs>>,
572 value_type: impl IntoIterator<Item = ValueType>,
573 ) -> Self {
574 ProcessingState {
575 parent: Some(BoxCow::Borrowed(self)),
576 path_item: Some(PathItem::Index(idx)),
577 attrs,
578 value_type: value_type.into_iter().collect(),
579 depth: self.depth + 1,
580 }
581 }
582
583 pub fn enter_nothing(&'a self, attrs: Option<Cow<'a, FieldAttrs>>) -> Self {
585 ProcessingState {
586 attrs,
587 path_item: None,
588 parent: Some(BoxCow::Borrowed(self)),
589 ..self.clone()
590 }
591 }
592
593 pub fn path(&'a self) -> Path<'a> {
595 Path(self)
596 }
597
598 pub fn value_type(&self) -> EnumSet<ValueType> {
599 self.value_type
600 }
601
602 pub fn attrs(&self) -> &FieldAttrs {
604 match self.attrs {
605 Some(ref cow) => cow,
606 None => &DEFAULT_FIELD_ATTRS,
607 }
608 }
609
610 pub fn inner_attrs(&self) -> Option<Cow<'_, FieldAttrs>> {
612 let current_attrs = self.attrs();
613 match (current_attrs.pii, current_attrs.trim) {
614 (PiiMode::Static(Pii::False), true) => None,
616 (PiiMode::Static(Pii::True), true) => {
617 static ATTRS: FieldAttrs = FieldAttrs::new().pii(Pii::True).trim(true);
618 Some(Cow::Borrowed(&ATTRS))
619 }
620 (PiiMode::Static(Pii::Maybe), true) => {
621 static ATTRS: FieldAttrs = FieldAttrs::new().pii(Pii::Maybe).trim(true);
622 Some(Cow::Borrowed(&ATTRS))
623 }
624 (PiiMode::Static(Pii::True), false) => {
625 static ATTRS: FieldAttrs = FieldAttrs::new().pii(Pii::True).trim(false);
626 Some(Cow::Borrowed(&ATTRS))
627 }
628 (PiiMode::Static(Pii::False), false) => {
629 static ATTRS: FieldAttrs = FieldAttrs::new().pii(Pii::False).trim(false);
630 Some(Cow::Borrowed(&ATTRS))
631 }
632 (PiiMode::Static(Pii::Maybe), false) => {
633 static ATTRS: FieldAttrs = FieldAttrs::new().pii(Pii::Maybe).trim(false);
634 Some(Cow::Borrowed(&ATTRS))
635 }
636 (PiiMode::Dynamic(f), trim) => {
637 Some(Cow::Owned(FieldAttrs::new().pii_dynamic(f).trim(trim)))
638 }
639 }
640 }
641
642 pub fn pii(&self) -> Pii {
648 match self.attrs().pii {
649 PiiMode::Static(pii) => pii,
650 PiiMode::Dynamic(pii_fn) => pii_fn(self),
651 }
652 }
653
654 pub fn max_bytes(&self) -> Option<usize> {
660 match self.attrs().max_bytes {
661 SizeMode::Static(n) => n,
662 SizeMode::Dynamic(max_bytes_fn) => max_bytes_fn(self),
663 }
664 }
665
666 pub fn bytes_size(&self) -> Option<usize> {
672 match self.attrs().bytes_size {
673 SizeMode::Static(n) => n,
674 SizeMode::Dynamic(bytes_size_fn) => bytes_size_fn(self),
675 }
676 }
677
678 pub fn max_chars(&self) -> Option<usize> {
684 match self.attrs().max_chars {
685 SizeMode::Static(n) => n,
686 SizeMode::Dynamic(max_chars_fn) => max_chars_fn(self),
687 }
688 }
689
690 pub fn iter(&'a self) -> ProcessingStateIter<'a> {
695 ProcessingStateIter {
696 state: Some(self),
697 size: self.depth,
698 }
699 }
700
701 #[expect(
706 clippy::result_large_err,
707 reason = "this method returns `self` in the error case"
708 )]
709 pub fn try_into_parent(self) -> Result<Option<Self>, Self> {
710 match self.parent {
711 Some(BoxCow::Borrowed(_)) => Err(self),
712 Some(BoxCow::Owned(parent)) => Ok(Some(*parent)),
713 None => Ok(None),
714 }
715 }
716
717 pub fn depth(&'a self) -> usize {
719 self.depth
720 }
721
722 pub fn entered_anything(&'a self) -> bool {
726 if let Some(parent) = &self.parent {
727 parent.depth() != self.depth()
728 } else {
729 true
730 }
731 }
732
733 pub fn keys(&self) -> impl Iterator<Item = &str> {
736 self.iter()
737 .filter_map(|state| state.path_item.as_ref())
738 .flat_map(|item| item.key())
739 }
740
741 #[inline]
744 fn path_item(&self) -> Option<&PathItem<'_>> {
745 for state in self.iter() {
746 if let Some(ref path_item) = state.path_item {
747 return Some(path_item);
748 }
749 }
750 None
751 }
752}
753
754pub struct ProcessingStateIter<'a> {
755 state: Option<&'a ProcessingState<'a>>,
756 size: usize,
757}
758
759impl<'a> Iterator for ProcessingStateIter<'a> {
760 type Item = &'a ProcessingState<'a>;
761
762 fn next(&mut self) -> Option<Self::Item> {
763 let current = self.state?;
764 self.state = current.parent.as_deref();
765 Some(current)
766 }
767
768 fn size_hint(&self) -> (usize, Option<usize>) {
769 (self.size, Some(self.size))
770 }
771}
772
773impl ExactSizeIterator for ProcessingStateIter<'_> {}
774
775impl Default for ProcessingState<'_> {
776 fn default() -> Self {
777 ProcessingState::root().clone()
778 }
779}
780
781#[derive(Debug)]
785pub struct Path<'a>(&'a ProcessingState<'a>);
786
787impl Path<'_> {
788 #[inline]
790 pub fn key(&self) -> Option<&str> {
791 PathItem::key(self.0.path_item()?)
792 }
793
794 #[inline]
796 pub fn index(&self) -> Option<usize> {
797 PathItem::index(self.0.path_item()?)
798 }
799
800 pub fn depth(&self) -> usize {
802 self.0.depth()
803 }
804
805 pub fn attrs(&self) -> &FieldAttrs {
807 self.0.attrs()
808 }
809
810 pub fn pii(&self) -> Pii {
812 self.0.pii()
813 }
814
815 pub fn iter(&self) -> ProcessingStateIter<'_> {
817 self.0.iter()
818 }
819}
820
821impl fmt::Display for Path<'_> {
822 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
823 let mut items = Vec::with_capacity(self.0.depth);
824 for state in self.0.iter() {
825 if let Some(ref path_item) = state.path_item {
826 items.push(path_item)
827 }
828 }
829
830 for (idx, item) in items.into_iter().rev().enumerate() {
831 if idx > 0 {
832 write!(f, ".")?;
833 }
834 write!(f, "{item}")?;
835 }
836 Ok(())
837 }
838}
839
840#[cfg(test)]
841mod tests {
842
843 use relay_protocol::{Annotated, Empty, FromValue, IntoValue, Object, SerializableAnnotated};
844
845 use crate::processor::attrs::ROOT_STATE;
846 use crate::processor::{Pii, ProcessValue, ProcessingState, Processor, process_value};
847
848 fn pii_from_item_name(state: &ProcessingState) -> Pii {
849 match state.path_item().and_then(|p| p.key()) {
850 Some("true_item") => Pii::True,
851 Some("false_item") => Pii::False,
852 _ => Pii::Maybe,
853 }
854 }
855
856 fn max_chars_from_item_name(state: &ProcessingState) -> Option<usize> {
857 match state.path_item().and_then(|p| p.key()) {
858 Some("short_item") => Some(10),
859 Some("long_item") => Some(20),
860 _ => None,
861 }
862 }
863
864 #[derive(Debug, Clone, Empty, IntoValue, FromValue, ProcessValue)]
865 #[metastructure(pii = "pii_from_item_name")]
866 struct TestValue(#[metastructure(max_chars = "max_chars_from_item_name")] String);
867
868 struct TestPiiProcessor;
869
870 impl Processor for TestPiiProcessor {
871 fn process_string(
872 &mut self,
873 value: &mut String,
874 _meta: &mut relay_protocol::Meta,
875 state: &ProcessingState<'_>,
876 ) -> crate::processor::ProcessingResult where {
877 match state.pii() {
878 Pii::True => *value = "true".to_owned(),
879 Pii::False => *value = "false".to_owned(),
880 Pii::Maybe => *value = "maybe".to_owned(),
881 }
882 Ok(())
883 }
884 }
885
886 struct TestTrimmingProcessor;
887
888 impl Processor for TestTrimmingProcessor {
889 fn process_string(
890 &mut self,
891 value: &mut String,
892 _meta: &mut relay_protocol::Meta,
893 state: &ProcessingState<'_>,
894 ) -> crate::processor::ProcessingResult where {
895 if let Some(n) = state.max_chars() {
896 value.truncate(n);
897 }
898 Ok(())
899 }
900 }
901
902 #[test]
903 fn test_dynamic_pii() {
904 let mut object: Annotated<Object<TestValue>> = Annotated::from_json(
905 r#"
906 {
907 "false_item": "replace me",
908 "other_item": "replace me",
909 "true_item": "replace me"
910 }
911 "#,
912 )
913 .unwrap();
914
915 process_value(&mut object, &mut TestPiiProcessor, &ROOT_STATE).unwrap();
916
917 insta::assert_json_snapshot!(SerializableAnnotated(&object), @r###"
918 {
919 "false_item": "false",
920 "other_item": "maybe",
921 "true_item": "true"
922 }
923 "###);
924 }
925
926 #[test]
927 fn test_dynamic_max_chars() {
928 let mut object: Annotated<Object<TestValue>> = Annotated::from_json(
929 r#"
930 {
931 "short_item": "Should be shortened to 10",
932 "long_item": "Should be shortened to 20",
933 "other_item": "Should not be shortened at all"
934 }
935 "#,
936 )
937 .unwrap();
938
939 process_value(&mut object, &mut TestTrimmingProcessor, &ROOT_STATE).unwrap();
940
941 insta::assert_json_snapshot!(SerializableAnnotated(&object), @r###"
942 {
943 "long_item": "Should be shortened ",
944 "other_item": "Should not be shortened at all",
945 "short_item": "Should be "
946 }
947 "###);
948 }
949}