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 pii: PiiMode,
152 pub retain: bool,
154 pub trim: bool,
156}
157
158#[derive(Clone, Copy)]
162pub struct CharacterSet {
163 pub char_is_valid: fn(char) -> bool,
165 pub ranges: &'static [RangeInclusive<char>],
167 pub is_negative: bool,
169}
170
171impl fmt::Debug for CharacterSet {
172 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173 f.debug_struct("CharacterSet")
174 .field("ranges", &self.ranges)
175 .field("is_negative", &self.is_negative)
176 .finish()
177 }
178}
179
180impl FieldAttrs {
181 pub const fn new() -> Self {
183 FieldAttrs {
184 name: None,
185 required: false,
186 nonempty: false,
187 trim_whitespace: false,
188 characters: None,
189 max_chars: SizeMode::Static(None),
190 max_chars_allowance: 0,
191 max_depth: None,
192 max_bytes: SizeMode::Static(None),
193 pii: PiiMode::Static(Pii::False),
194 retain: false,
195 trim: true,
196 }
197 }
198
199 pub const fn required(mut self, required: bool) -> Self {
201 self.required = required;
202 self
203 }
204
205 pub const fn nonempty(mut self, nonempty: bool) -> Self {
210 self.nonempty = nonempty;
211 self
212 }
213
214 pub const fn trim_whitespace(mut self, trim_whitespace: bool) -> Self {
216 self.trim_whitespace = trim_whitespace;
217 self
218 }
219
220 pub const fn pii(mut self, pii: Pii) -> Self {
222 self.pii = PiiMode::Static(pii);
223 self
224 }
225
226 pub const fn pii_dynamic(mut self, pii: fn(&ProcessingState) -> Pii) -> Self {
228 self.pii = PiiMode::Dynamic(pii);
229 self
230 }
231
232 pub const fn max_chars(mut self, max_chars: usize) -> Self {
234 self.max_chars = SizeMode::Static(Some(max_chars));
235 self
236 }
237
238 pub const fn max_chars_dynamic(
240 mut self,
241 max_chars: fn(&ProcessingState) -> Option<usize>,
242 ) -> Self {
243 self.max_chars = SizeMode::Dynamic(max_chars);
244 self
245 }
246
247 pub const fn retain(mut self, retain: bool) -> Self {
249 self.retain = retain;
250 self
251 }
252}
253
254static DEFAULT_FIELD_ATTRS: FieldAttrs = FieldAttrs::new();
255static PII_TRUE_FIELD_ATTRS: FieldAttrs = FieldAttrs::new().pii(Pii::True);
256static PII_MAYBE_FIELD_ATTRS: FieldAttrs = FieldAttrs::new().pii(Pii::Maybe);
257
258impl Default for FieldAttrs {
259 fn default() -> Self {
260 Self::new()
261 }
262}
263
264#[derive(Debug, Clone, Eq, Ord, PartialOrd)]
265enum PathItem<'a> {
266 StaticKey(&'a str),
267 OwnedKey(String),
268 Index(usize),
269}
270
271impl<'a> PartialEq for PathItem<'a> {
272 fn eq(&self, other: &PathItem<'a>) -> bool {
273 self.key() == other.key() && self.index() == other.index()
274 }
275}
276
277impl PathItem<'_> {
278 #[inline]
280 pub fn key(&self) -> Option<&str> {
281 match self {
282 PathItem::StaticKey(s) => Some(s),
283 PathItem::OwnedKey(s) => Some(s.as_str()),
284 PathItem::Index(_) => None,
285 }
286 }
287
288 #[inline]
290 pub fn index(&self) -> Option<usize> {
291 match self {
292 PathItem::StaticKey(_) | PathItem::OwnedKey(_) => None,
293 PathItem::Index(idx) => Some(*idx),
294 }
295 }
296}
297
298impl fmt::Display for PathItem<'_> {
299 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
300 match self {
301 PathItem::StaticKey(s) => f.pad(s),
302 PathItem::OwnedKey(s) => f.pad(s.as_str()),
303 PathItem::Index(val) => write!(f, "{val}"),
304 }
305 }
306}
307
308#[derive(Debug, Clone)]
318enum BoxCow<'a, T> {
319 Borrowed(&'a T),
320 Owned(Box<T>),
321}
322
323impl<T> Deref for BoxCow<'_, T> {
324 type Target = T;
325
326 fn deref(&self) -> &Self::Target {
327 match self {
328 BoxCow::Borrowed(inner) => inner,
329 BoxCow::Owned(inner) => inner.deref(),
330 }
331 }
332}
333
334#[derive(Debug, Clone)]
343pub struct ProcessingState<'a> {
344 parent: Option<BoxCow<'a, ProcessingState<'a>>>,
350 path_item: Option<PathItem<'a>>,
351 attrs: Option<Cow<'a, FieldAttrs>>,
352 value_type: EnumSet<ValueType>,
353 depth: usize,
354}
355
356static ROOT_STATE: ProcessingState = ProcessingState {
357 parent: None,
358 path_item: None,
359 attrs: None,
360 value_type: enumset::enum_set!(),
361 depth: 0,
362};
363
364impl<'a> ProcessingState<'a> {
365 pub fn root() -> &'static ProcessingState<'static> {
367 &ROOT_STATE
368 }
369
370 pub fn new_root(
372 attrs: Option<Cow<'static, FieldAttrs>>,
373 value_type: impl IntoIterator<Item = ValueType>,
374 ) -> ProcessingState<'static> {
375 ProcessingState {
376 parent: None,
377 path_item: None,
378 attrs,
379 value_type: value_type.into_iter().collect(),
380 depth: 0,
381 }
382 }
383
384 pub fn enter_borrowed(
386 &'a self,
387 key: &'a str,
388 attrs: Option<Cow<'a, FieldAttrs>>,
389 value_type: impl IntoIterator<Item = ValueType>,
390 ) -> Self {
391 ProcessingState {
392 parent: Some(BoxCow::Borrowed(self)),
393 path_item: Some(PathItem::StaticKey(key)),
394 attrs,
395 value_type: value_type.into_iter().collect(),
396 depth: self.depth + 1,
397 }
398 }
399
400 pub fn enter_owned(
404 self,
405 key: String,
406 attrs: Option<Cow<'a, FieldAttrs>>,
407 value_type: impl IntoIterator<Item = ValueType>,
408 ) -> Self {
409 let depth = self.depth + 1;
410 ProcessingState {
411 parent: Some(BoxCow::Owned(self.into())),
412 path_item: Some(PathItem::OwnedKey(key)),
413 attrs,
414 value_type: value_type.into_iter().collect(),
415 depth,
416 }
417 }
418
419 pub fn enter_index(
421 &'a self,
422 idx: usize,
423 attrs: Option<Cow<'a, FieldAttrs>>,
424 value_type: impl IntoIterator<Item = ValueType>,
425 ) -> Self {
426 ProcessingState {
427 parent: Some(BoxCow::Borrowed(self)),
428 path_item: Some(PathItem::Index(idx)),
429 attrs,
430 value_type: value_type.into_iter().collect(),
431 depth: self.depth + 1,
432 }
433 }
434
435 pub fn enter_nothing(&'a self, attrs: Option<Cow<'a, FieldAttrs>>) -> Self {
437 ProcessingState {
438 attrs,
439 path_item: None,
440 parent: Some(BoxCow::Borrowed(self)),
441 ..self.clone()
442 }
443 }
444
445 pub fn path(&'a self) -> Path<'a> {
447 Path(self)
448 }
449
450 pub fn value_type(&self) -> EnumSet<ValueType> {
451 self.value_type
452 }
453
454 pub fn attrs(&self) -> &FieldAttrs {
456 match self.attrs {
457 Some(ref cow) => cow,
458 None => &DEFAULT_FIELD_ATTRS,
459 }
460 }
461
462 pub fn inner_attrs(&self) -> Option<Cow<'_, FieldAttrs>> {
464 match self.pii() {
465 Pii::True => Some(Cow::Borrowed(&PII_TRUE_FIELD_ATTRS)),
466 Pii::False => None,
467 Pii::Maybe => Some(Cow::Borrowed(&PII_MAYBE_FIELD_ATTRS)),
468 }
469 }
470
471 pub fn pii(&self) -> Pii {
477 match self.attrs().pii {
478 PiiMode::Static(pii) => pii,
479 PiiMode::Dynamic(pii_fn) => pii_fn(self),
480 }
481 }
482
483 pub fn max_bytes(&self) -> Option<usize> {
489 match self.attrs().max_bytes {
490 SizeMode::Static(n) => n,
491 SizeMode::Dynamic(max_bytes_fn) => max_bytes_fn(self),
492 }
493 }
494
495 pub fn max_chars(&self) -> Option<usize> {
501 match self.attrs().max_chars {
502 SizeMode::Static(n) => n,
503 SizeMode::Dynamic(max_chars_fn) => max_chars_fn(self),
504 }
505 }
506
507 pub fn iter(&'a self) -> ProcessingStateIter<'a> {
512 ProcessingStateIter {
513 state: Some(self),
514 size: self.depth,
515 }
516 }
517
518 #[expect(
523 clippy::result_large_err,
524 reason = "this method returns `self` in the error case"
525 )]
526 pub fn try_into_parent(self) -> Result<Option<Self>, Self> {
527 match self.parent {
528 Some(BoxCow::Borrowed(_)) => Err(self),
529 Some(BoxCow::Owned(parent)) => Ok(Some(*parent)),
530 None => Ok(None),
531 }
532 }
533
534 pub fn depth(&'a self) -> usize {
536 self.depth
537 }
538
539 pub fn entered_anything(&'a self) -> bool {
543 if let Some(parent) = &self.parent {
544 parent.depth() != self.depth()
545 } else {
546 true
547 }
548 }
549
550 pub fn keys(&self) -> impl Iterator<Item = &str> {
553 self.iter()
554 .filter_map(|state| state.path_item.as_ref())
555 .flat_map(|item| item.key())
556 }
557
558 #[inline]
561 fn path_item(&self) -> Option<&PathItem<'_>> {
562 for state in self.iter() {
563 if let Some(ref path_item) = state.path_item {
564 return Some(path_item);
565 }
566 }
567 None
568 }
569}
570
571pub struct ProcessingStateIter<'a> {
572 state: Option<&'a ProcessingState<'a>>,
573 size: usize,
574}
575
576impl<'a> Iterator for ProcessingStateIter<'a> {
577 type Item = &'a ProcessingState<'a>;
578
579 fn next(&mut self) -> Option<Self::Item> {
580 let current = self.state?;
581 self.state = current.parent.as_deref();
582 Some(current)
583 }
584
585 fn size_hint(&self) -> (usize, Option<usize>) {
586 (self.size, Some(self.size))
587 }
588}
589
590impl ExactSizeIterator for ProcessingStateIter<'_> {}
591
592impl Default for ProcessingState<'_> {
593 fn default() -> Self {
594 ProcessingState::root().clone()
595 }
596}
597
598#[derive(Debug)]
602pub struct Path<'a>(&'a ProcessingState<'a>);
603
604impl Path<'_> {
605 #[inline]
607 pub fn key(&self) -> Option<&str> {
608 PathItem::key(self.0.path_item()?)
609 }
610
611 #[inline]
613 pub fn index(&self) -> Option<usize> {
614 PathItem::index(self.0.path_item()?)
615 }
616
617 pub fn depth(&self) -> usize {
619 self.0.depth()
620 }
621
622 pub fn attrs(&self) -> &FieldAttrs {
624 self.0.attrs()
625 }
626
627 pub fn pii(&self) -> Pii {
629 self.0.pii()
630 }
631
632 pub fn iter(&self) -> ProcessingStateIter<'_> {
634 self.0.iter()
635 }
636}
637
638impl fmt::Display for Path<'_> {
639 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
640 let mut items = Vec::with_capacity(self.0.depth);
641 for state in self.0.iter() {
642 if let Some(ref path_item) = state.path_item {
643 items.push(path_item)
644 }
645 }
646
647 for (idx, item) in items.into_iter().rev().enumerate() {
648 if idx > 0 {
649 write!(f, ".")?;
650 }
651 write!(f, "{item}")?;
652 }
653 Ok(())
654 }
655}
656
657#[cfg(test)]
658mod tests {
659
660 use relay_protocol::{Annotated, Empty, FromValue, IntoValue, Object, SerializableAnnotated};
661
662 use crate::processor::attrs::ROOT_STATE;
663 use crate::processor::{Pii, ProcessValue, ProcessingState, Processor, process_value};
664
665 fn pii_from_item_name(state: &ProcessingState) -> Pii {
666 match state.path_item().and_then(|p| p.key()) {
667 Some("true_item") => Pii::True,
668 Some("false_item") => Pii::False,
669 _ => Pii::Maybe,
670 }
671 }
672
673 fn max_chars_from_item_name(state: &ProcessingState) -> Option<usize> {
674 match state.path_item().and_then(|p| p.key()) {
675 Some("short_item") => Some(10),
676 Some("long_item") => Some(20),
677 _ => None,
678 }
679 }
680
681 #[derive(Debug, Clone, Empty, IntoValue, FromValue, ProcessValue)]
682 #[metastructure(pii = "pii_from_item_name")]
683 struct TestValue(#[metastructure(max_chars = "max_chars_from_item_name")] String);
684
685 struct TestPiiProcessor;
686
687 impl Processor for TestPiiProcessor {
688 fn process_string(
689 &mut self,
690 value: &mut String,
691 _meta: &mut relay_protocol::Meta,
692 state: &ProcessingState<'_>,
693 ) -> crate::processor::ProcessingResult where {
694 match state.pii() {
695 Pii::True => *value = "true".to_owned(),
696 Pii::False => *value = "false".to_owned(),
697 Pii::Maybe => *value = "maybe".to_owned(),
698 }
699 Ok(())
700 }
701 }
702
703 struct TestTrimmingProcessor;
704
705 impl Processor for TestTrimmingProcessor {
706 fn process_string(
707 &mut self,
708 value: &mut String,
709 _meta: &mut relay_protocol::Meta,
710 state: &ProcessingState<'_>,
711 ) -> crate::processor::ProcessingResult where {
712 if let Some(n) = state.max_chars() {
713 value.truncate(n);
714 }
715 Ok(())
716 }
717 }
718
719 #[test]
720 fn test_dynamic_pii() {
721 let mut object: Annotated<Object<TestValue>> = Annotated::from_json(
722 r#"
723 {
724 "false_item": "replace me",
725 "other_item": "replace me",
726 "true_item": "replace me"
727 }
728 "#,
729 )
730 .unwrap();
731
732 process_value(&mut object, &mut TestPiiProcessor, &ROOT_STATE).unwrap();
733
734 insta::assert_json_snapshot!(SerializableAnnotated(&object), @r###"
735 {
736 "false_item": "false",
737 "other_item": "maybe",
738 "true_item": "true"
739 }
740 "###);
741 }
742
743 #[test]
744 fn test_dynamic_max_chars() {
745 let mut object: Annotated<Object<TestValue>> = Annotated::from_json(
746 r#"
747 {
748 "short_item": "Should be shortened to 10",
749 "long_item": "Should be shortened to 20",
750 "other_item": "Should not be shortened at all"
751 }
752 "#,
753 )
754 .unwrap();
755
756 process_value(&mut object, &mut TestTrimmingProcessor, &ROOT_STATE).unwrap();
757
758 insta::assert_json_snapshot!(SerializableAnnotated(&object), @r###"
759 {
760 "long_item": "Should be shortened ",
761 "other_item": "Should not be shortened at all",
762 "short_item": "Should be "
763 }
764 "###);
765 }
766}