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 struct FieldAttrs {
132 pub name: Option<&'static str>,
134 pub required: bool,
136 pub nonempty: bool,
138 pub trim_whitespace: bool,
140 pub characters: Option<CharacterSet>,
142 pub max_chars: SizeMode,
144 pub max_chars_allowance: usize,
146 pub max_depth: Option<usize>,
148 pub max_bytes: SizeMode,
150 pub bytes_size: SizeMode,
158 pub pii: PiiMode,
160 pub retain: bool,
162 pub trim: bool,
164}
165
166#[derive(Clone, Copy)]
170pub struct CharacterSet {
171 pub char_is_valid: fn(char) -> bool,
173 pub ranges: &'static [RangeInclusive<char>],
175 pub is_negative: bool,
177}
178
179impl fmt::Debug for CharacterSet {
180 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181 f.debug_struct("CharacterSet")
182 .field("ranges", &self.ranges)
183 .field("is_negative", &self.is_negative)
184 .finish()
185 }
186}
187
188impl FieldAttrs {
189 pub const fn new() -> Self {
191 FieldAttrs {
192 name: None,
193 required: false,
194 nonempty: false,
195 trim_whitespace: false,
196 characters: None,
197 max_chars: SizeMode::Static(None),
198 max_chars_allowance: 0,
199 max_depth: None,
200 max_bytes: SizeMode::Static(None),
201 pii: PiiMode::Static(Pii::False),
202 retain: false,
203 trim: true,
204 bytes_size: SizeMode::Static(None),
205 }
206 }
207
208 pub const fn required(mut self, required: bool) -> Self {
210 self.required = required;
211 self
212 }
213
214 pub const fn nonempty(mut self, nonempty: bool) -> Self {
219 self.nonempty = nonempty;
220 self
221 }
222
223 pub const fn trim_whitespace(mut self, trim_whitespace: bool) -> Self {
225 self.trim_whitespace = trim_whitespace;
226 self
227 }
228
229 pub const fn pii(mut self, pii: Pii) -> Self {
231 self.pii = PiiMode::Static(pii);
232 self
233 }
234
235 pub const fn pii_dynamic(mut self, pii: fn(&ProcessingState) -> Pii) -> Self {
237 self.pii = PiiMode::Dynamic(pii);
238 self
239 }
240
241 pub const fn max_chars(mut self, max_chars: Option<usize>) -> Self {
243 self.max_chars = SizeMode::Static(max_chars);
244 self
245 }
246
247 pub const fn max_chars_dynamic(
249 mut self,
250 max_chars: fn(&ProcessingState) -> Option<usize>,
251 ) -> Self {
252 self.max_chars = SizeMode::Dynamic(max_chars);
253 self
254 }
255
256 pub const fn max_bytes(mut self, max_bytes: Option<usize>) -> Self {
258 self.max_bytes = SizeMode::Static(max_bytes);
259 self
260 }
261
262 pub const fn max_bytes_dynamic(
264 mut self,
265 max_bytes: fn(&ProcessingState) -> Option<usize>,
266 ) -> Self {
267 self.max_bytes = SizeMode::Dynamic(max_bytes);
268 self
269 }
270
271 pub const fn retain(mut self, retain: bool) -> Self {
273 self.retain = retain;
274 self
275 }
276}
277
278static DEFAULT_FIELD_ATTRS: FieldAttrs = FieldAttrs::new();
279static PII_TRUE_FIELD_ATTRS: FieldAttrs = FieldAttrs::new().pii(Pii::True);
280static PII_MAYBE_FIELD_ATTRS: FieldAttrs = FieldAttrs::new().pii(Pii::Maybe);
281
282impl Default for FieldAttrs {
283 fn default() -> Self {
284 Self::new()
285 }
286}
287
288#[derive(Debug, Clone, Eq, Ord, PartialOrd)]
289enum PathItem<'a> {
290 StaticKey(&'a str),
291 OwnedKey(String),
292 Index(usize),
293}
294
295impl<'a> PartialEq for PathItem<'a> {
296 fn eq(&self, other: &PathItem<'a>) -> bool {
297 self.key() == other.key() && self.index() == other.index()
298 }
299}
300
301impl PathItem<'_> {
302 #[inline]
304 pub fn key(&self) -> Option<&str> {
305 match self {
306 PathItem::StaticKey(s) => Some(s),
307 PathItem::OwnedKey(s) => Some(s.as_str()),
308 PathItem::Index(_) => None,
309 }
310 }
311
312 #[inline]
314 pub fn index(&self) -> Option<usize> {
315 match self {
316 PathItem::StaticKey(_) | PathItem::OwnedKey(_) => None,
317 PathItem::Index(idx) => Some(*idx),
318 }
319 }
320}
321
322impl fmt::Display for PathItem<'_> {
323 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
324 match self {
325 PathItem::StaticKey(s) => f.pad(s),
326 PathItem::OwnedKey(s) => f.pad(s.as_str()),
327 PathItem::Index(val) => write!(f, "{val}"),
328 }
329 }
330}
331
332#[derive(Debug, Clone)]
342enum BoxCow<'a, T> {
343 Borrowed(&'a T),
344 Owned(Box<T>),
345}
346
347impl<T> Deref for BoxCow<'_, T> {
348 type Target = T;
349
350 fn deref(&self) -> &Self::Target {
351 match self {
352 BoxCow::Borrowed(inner) => inner,
353 BoxCow::Owned(inner) => inner.deref(),
354 }
355 }
356}
357
358#[derive(Debug, Clone)]
362pub struct ProcessingStateBuilder {
363 attrs: Option<FieldAttrs>,
364 value_type: EnumSet<ValueType>,
365}
366
367impl ProcessingStateBuilder {
368 pub fn attrs<F: FnOnce(FieldAttrs) -> FieldAttrs>(mut self, f: F) -> Self {
370 let attrs = self.attrs.take().unwrap_or_default();
371 self.attrs = Some(f(attrs));
372 self
373 }
374
375 pub fn required(self, required: bool) -> Self {
377 self.attrs(|attrs| attrs.required(required))
378 }
379
380 pub fn nonempty(self, nonempty: bool) -> Self {
385 self.attrs(|attrs| attrs.nonempty(nonempty))
386 }
387
388 pub fn trim_whitespace(self, trim_whitespace: bool) -> Self {
390 self.attrs(|attrs| attrs.trim_whitespace(trim_whitespace))
391 }
392
393 pub fn pii(self, pii: Pii) -> Self {
395 self.attrs(|attrs| attrs.pii(pii))
396 }
397
398 pub fn pii_dynamic(self, pii: fn(&ProcessingState) -> Pii) -> Self {
400 self.attrs(|attrs| attrs.pii_dynamic(pii))
401 }
402
403 pub fn max_chars(self, max_chars: impl Into<Option<usize>>) -> Self {
405 self.attrs(|attrs| attrs.max_chars(max_chars.into()))
406 }
407
408 pub fn max_chars_dynamic(self, max_chars: fn(&ProcessingState) -> Option<usize>) -> Self {
410 self.attrs(|attrs| attrs.max_chars_dynamic(max_chars))
411 }
412
413 pub fn max_bytes(self, max_bytes: impl Into<Option<usize>>) -> Self {
415 self.attrs(|attrs| attrs.max_bytes(max_bytes.into()))
416 }
417
418 pub fn max_bytes_dynamic(self, max_bytes: fn(&ProcessingState) -> Option<usize>) -> Self {
420 self.attrs(|attrs| attrs.max_bytes_dynamic(max_bytes))
421 }
422
423 pub fn retain(self, retain: bool) -> Self {
425 self.attrs(|attrs| attrs.retain(retain))
426 }
427
428 pub fn value_type(mut self, value_type: EnumSet<ValueType>) -> Self {
430 self.value_type = value_type;
431 self
432 }
433
434 pub fn build(self) -> ProcessingState<'static> {
437 let Self { attrs, value_type } = self;
438 ProcessingState {
439 parent: None,
440 path_item: None,
441 attrs: attrs.map(Cow::Owned),
442 value_type,
443 depth: 0,
444 }
445 }
446}
447
448#[derive(Debug, Clone)]
457pub struct ProcessingState<'a> {
458 parent: Option<BoxCow<'a, ProcessingState<'a>>>,
464 path_item: Option<PathItem<'a>>,
465 attrs: Option<Cow<'a, FieldAttrs>>,
466 value_type: EnumSet<ValueType>,
467 depth: usize,
468}
469
470static ROOT_STATE: ProcessingState = ProcessingState {
471 parent: None,
472 path_item: None,
473 attrs: None,
474 value_type: enumset::enum_set!(),
475 depth: 0,
476};
477
478impl<'a> ProcessingState<'a> {
479 pub fn root() -> &'static ProcessingState<'static> {
481 &ROOT_STATE
482 }
483
484 pub fn new_root(
486 attrs: Option<Cow<'static, FieldAttrs>>,
487 value_type: impl IntoIterator<Item = ValueType>,
488 ) -> ProcessingState<'static> {
489 ProcessingState {
490 parent: None,
491 path_item: None,
492 attrs,
493 value_type: value_type.into_iter().collect(),
494 depth: 0,
495 }
496 }
497
498 pub fn root_builder() -> ProcessingStateBuilder {
511 ProcessingStateBuilder {
512 attrs: None,
513 value_type: EnumSet::empty(),
514 }
515 }
516
517 pub fn enter_borrowed(
519 &'a self,
520 key: &'a str,
521 attrs: Option<Cow<'a, FieldAttrs>>,
522 value_type: impl IntoIterator<Item = ValueType>,
523 ) -> Self {
524 ProcessingState {
525 parent: Some(BoxCow::Borrowed(self)),
526 path_item: Some(PathItem::StaticKey(key)),
527 attrs,
528 value_type: value_type.into_iter().collect(),
529 depth: self.depth + 1,
530 }
531 }
532
533 pub fn enter_owned(
537 self,
538 key: String,
539 attrs: Option<Cow<'a, FieldAttrs>>,
540 value_type: impl IntoIterator<Item = ValueType>,
541 ) -> Self {
542 let depth = self.depth + 1;
543 ProcessingState {
544 parent: Some(BoxCow::Owned(self.into())),
545 path_item: Some(PathItem::OwnedKey(key)),
546 attrs,
547 value_type: value_type.into_iter().collect(),
548 depth,
549 }
550 }
551
552 pub fn enter_index(
554 &'a self,
555 idx: usize,
556 attrs: Option<Cow<'a, FieldAttrs>>,
557 value_type: impl IntoIterator<Item = ValueType>,
558 ) -> Self {
559 ProcessingState {
560 parent: Some(BoxCow::Borrowed(self)),
561 path_item: Some(PathItem::Index(idx)),
562 attrs,
563 value_type: value_type.into_iter().collect(),
564 depth: self.depth + 1,
565 }
566 }
567
568 pub fn enter_nothing(&'a self, attrs: Option<Cow<'a, FieldAttrs>>) -> Self {
570 ProcessingState {
571 attrs,
572 path_item: None,
573 parent: Some(BoxCow::Borrowed(self)),
574 ..self.clone()
575 }
576 }
577
578 pub fn path(&'a self) -> Path<'a> {
580 Path(self)
581 }
582
583 pub fn value_type(&self) -> EnumSet<ValueType> {
584 self.value_type
585 }
586
587 pub fn attrs(&self) -> &FieldAttrs {
589 match self.attrs {
590 Some(ref cow) => cow,
591 None => &DEFAULT_FIELD_ATTRS,
592 }
593 }
594
595 pub fn inner_attrs(&self) -> Option<Cow<'_, FieldAttrs>> {
597 match self.pii() {
598 Pii::True => Some(Cow::Borrowed(&PII_TRUE_FIELD_ATTRS)),
599 Pii::False => None,
600 Pii::Maybe => Some(Cow::Borrowed(&PII_MAYBE_FIELD_ATTRS)),
601 }
602 }
603
604 pub fn pii(&self) -> Pii {
610 match self.attrs().pii {
611 PiiMode::Static(pii) => pii,
612 PiiMode::Dynamic(pii_fn) => pii_fn(self),
613 }
614 }
615
616 pub fn max_bytes(&self) -> Option<usize> {
622 match self.attrs().max_bytes {
623 SizeMode::Static(n) => n,
624 SizeMode::Dynamic(max_bytes_fn) => max_bytes_fn(self),
625 }
626 }
627
628 pub fn bytes_size(&self) -> Option<usize> {
634 match self.attrs().bytes_size {
635 SizeMode::Static(n) => n,
636 SizeMode::Dynamic(bytes_size_fn) => bytes_size_fn(self),
637 }
638 }
639
640 pub fn max_chars(&self) -> Option<usize> {
646 match self.attrs().max_chars {
647 SizeMode::Static(n) => n,
648 SizeMode::Dynamic(max_chars_fn) => max_chars_fn(self),
649 }
650 }
651
652 pub fn iter(&'a self) -> ProcessingStateIter<'a> {
657 ProcessingStateIter {
658 state: Some(self),
659 size: self.depth,
660 }
661 }
662
663 #[expect(
668 clippy::result_large_err,
669 reason = "this method returns `self` in the error case"
670 )]
671 pub fn try_into_parent(self) -> Result<Option<Self>, Self> {
672 match self.parent {
673 Some(BoxCow::Borrowed(_)) => Err(self),
674 Some(BoxCow::Owned(parent)) => Ok(Some(*parent)),
675 None => Ok(None),
676 }
677 }
678
679 pub fn depth(&'a self) -> usize {
681 self.depth
682 }
683
684 pub fn entered_anything(&'a self) -> bool {
688 if let Some(parent) = &self.parent {
689 parent.depth() != self.depth()
690 } else {
691 true
692 }
693 }
694
695 pub fn keys(&self) -> impl Iterator<Item = &str> {
698 self.iter()
699 .filter_map(|state| state.path_item.as_ref())
700 .flat_map(|item| item.key())
701 }
702
703 #[inline]
706 fn path_item(&self) -> Option<&PathItem<'_>> {
707 for state in self.iter() {
708 if let Some(ref path_item) = state.path_item {
709 return Some(path_item);
710 }
711 }
712 None
713 }
714}
715
716pub struct ProcessingStateIter<'a> {
717 state: Option<&'a ProcessingState<'a>>,
718 size: usize,
719}
720
721impl<'a> Iterator for ProcessingStateIter<'a> {
722 type Item = &'a ProcessingState<'a>;
723
724 fn next(&mut self) -> Option<Self::Item> {
725 let current = self.state?;
726 self.state = current.parent.as_deref();
727 Some(current)
728 }
729
730 fn size_hint(&self) -> (usize, Option<usize>) {
731 (self.size, Some(self.size))
732 }
733}
734
735impl ExactSizeIterator for ProcessingStateIter<'_> {}
736
737impl Default for ProcessingState<'_> {
738 fn default() -> Self {
739 ProcessingState::root().clone()
740 }
741}
742
743#[derive(Debug)]
747pub struct Path<'a>(&'a ProcessingState<'a>);
748
749impl Path<'_> {
750 #[inline]
752 pub fn key(&self) -> Option<&str> {
753 PathItem::key(self.0.path_item()?)
754 }
755
756 #[inline]
758 pub fn index(&self) -> Option<usize> {
759 PathItem::index(self.0.path_item()?)
760 }
761
762 pub fn depth(&self) -> usize {
764 self.0.depth()
765 }
766
767 pub fn attrs(&self) -> &FieldAttrs {
769 self.0.attrs()
770 }
771
772 pub fn pii(&self) -> Pii {
774 self.0.pii()
775 }
776
777 pub fn iter(&self) -> ProcessingStateIter<'_> {
779 self.0.iter()
780 }
781}
782
783impl fmt::Display for Path<'_> {
784 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
785 let mut items = Vec::with_capacity(self.0.depth);
786 for state in self.0.iter() {
787 if let Some(ref path_item) = state.path_item {
788 items.push(path_item)
789 }
790 }
791
792 for (idx, item) in items.into_iter().rev().enumerate() {
793 if idx > 0 {
794 write!(f, ".")?;
795 }
796 write!(f, "{item}")?;
797 }
798 Ok(())
799 }
800}
801
802#[cfg(test)]
803mod tests {
804
805 use relay_protocol::{Annotated, Empty, FromValue, IntoValue, Object, SerializableAnnotated};
806
807 use crate::processor::attrs::ROOT_STATE;
808 use crate::processor::{Pii, ProcessValue, ProcessingState, Processor, process_value};
809
810 fn pii_from_item_name(state: &ProcessingState) -> Pii {
811 match state.path_item().and_then(|p| p.key()) {
812 Some("true_item") => Pii::True,
813 Some("false_item") => Pii::False,
814 _ => Pii::Maybe,
815 }
816 }
817
818 fn max_chars_from_item_name(state: &ProcessingState) -> Option<usize> {
819 match state.path_item().and_then(|p| p.key()) {
820 Some("short_item") => Some(10),
821 Some("long_item") => Some(20),
822 _ => None,
823 }
824 }
825
826 #[derive(Debug, Clone, Empty, IntoValue, FromValue, ProcessValue)]
827 #[metastructure(pii = "pii_from_item_name")]
828 struct TestValue(#[metastructure(max_chars = "max_chars_from_item_name")] String);
829
830 struct TestPiiProcessor;
831
832 impl Processor for TestPiiProcessor {
833 fn process_string(
834 &mut self,
835 value: &mut String,
836 _meta: &mut relay_protocol::Meta,
837 state: &ProcessingState<'_>,
838 ) -> crate::processor::ProcessingResult where {
839 match state.pii() {
840 Pii::True => *value = "true".to_owned(),
841 Pii::False => *value = "false".to_owned(),
842 Pii::Maybe => *value = "maybe".to_owned(),
843 }
844 Ok(())
845 }
846 }
847
848 struct TestTrimmingProcessor;
849
850 impl Processor for TestTrimmingProcessor {
851 fn process_string(
852 &mut self,
853 value: &mut String,
854 _meta: &mut relay_protocol::Meta,
855 state: &ProcessingState<'_>,
856 ) -> crate::processor::ProcessingResult where {
857 if let Some(n) = state.max_chars() {
858 value.truncate(n);
859 }
860 Ok(())
861 }
862 }
863
864 #[test]
865 fn test_dynamic_pii() {
866 let mut object: Annotated<Object<TestValue>> = Annotated::from_json(
867 r#"
868 {
869 "false_item": "replace me",
870 "other_item": "replace me",
871 "true_item": "replace me"
872 }
873 "#,
874 )
875 .unwrap();
876
877 process_value(&mut object, &mut TestPiiProcessor, &ROOT_STATE).unwrap();
878
879 insta::assert_json_snapshot!(SerializableAnnotated(&object), @r###"
880 {
881 "false_item": "false",
882 "other_item": "maybe",
883 "true_item": "true"
884 }
885 "###);
886 }
887
888 #[test]
889 fn test_dynamic_max_chars() {
890 let mut object: Annotated<Object<TestValue>> = Annotated::from_json(
891 r#"
892 {
893 "short_item": "Should be shortened to 10",
894 "long_item": "Should be shortened to 20",
895 "other_item": "Should not be shortened at all"
896 }
897 "#,
898 )
899 .unwrap();
900
901 process_value(&mut object, &mut TestTrimmingProcessor, &ROOT_STATE).unwrap();
902
903 insta::assert_json_snapshot!(SerializableAnnotated(&object), @r###"
904 {
905 "long_item": "Should be shortened ",
906 "other_item": "Should not be shortened at all",
907 "short_item": "Should be "
908 }
909 "###);
910 }
911}