relay_event_normalization/
schema.rs

1use relay_event_schema::processor::{
2    ProcessValue, ProcessingAction, ProcessingResult, ProcessingState, Processor,
3};
4use relay_protocol::{Array, Empty, Error, ErrorKind, Meta, Object};
5
6/// Validates constraints such as empty strings or arrays and invalid characters.
7pub struct SchemaProcessor;
8
9impl Processor for SchemaProcessor {
10    fn process_string(
11        &mut self,
12        value: &mut String,
13        meta: &mut Meta,
14        state: &ProcessingState<'_>,
15    ) -> ProcessingResult {
16        value_trim_whitespace(value, meta, state);
17        verify_value_nonempty_string(value, meta, state)?;
18        verify_value_characters(value, meta, state)?;
19        Ok(())
20    }
21
22    fn process_array<T>(
23        &mut self,
24        value: &mut Array<T>,
25        meta: &mut Meta,
26        state: &ProcessingState<'_>,
27    ) -> ProcessingResult
28    where
29        T: ProcessValue,
30    {
31        value.process_child_values(self, state)?;
32        verify_value_nonempty(value, meta, state)?;
33        Ok(())
34    }
35
36    fn process_object<T>(
37        &mut self,
38        value: &mut Object<T>,
39        meta: &mut Meta,
40        state: &ProcessingState<'_>,
41    ) -> ProcessingResult
42    where
43        T: ProcessValue,
44    {
45        value.process_child_values(self, state)?;
46        verify_value_nonempty(value, meta, state)?;
47        Ok(())
48    }
49
50    fn before_process<T: ProcessValue>(
51        &mut self,
52        value: Option<&T>,
53        meta: &mut Meta,
54        state: &ProcessingState<'_>,
55    ) -> ProcessingResult {
56        if value.is_none() && state.attrs().required && !meta.has_errors() {
57            meta.add_error(ErrorKind::MissingAttribute);
58        }
59
60        Ok(())
61    }
62}
63
64fn value_trim_whitespace(value: &mut String, _meta: &mut Meta, state: &ProcessingState<'_>) {
65    if state.attrs().trim_whitespace {
66        let new_value = value.trim().to_owned();
67        value.clear();
68        value.push_str(&new_value);
69    }
70}
71
72fn verify_value_nonempty<T>(
73    value: &T,
74    meta: &mut Meta,
75    state: &ProcessingState<'_>,
76) -> ProcessingResult
77where
78    T: Empty,
79{
80    if state.attrs().nonempty && value.is_empty() {
81        meta.add_error(Error::nonempty());
82        Err(ProcessingAction::DeleteValueHard)
83    } else {
84        Ok(())
85    }
86}
87
88fn verify_value_nonempty_string<T>(
89    value: &T,
90    meta: &mut Meta,
91    state: &ProcessingState<'_>,
92) -> ProcessingResult
93where
94    T: Empty,
95{
96    if state.attrs().nonempty && value.is_empty() {
97        meta.add_error(Error::nonempty_string());
98        Err(ProcessingAction::DeleteValueHard)
99    } else {
100        Ok(())
101    }
102}
103
104fn verify_value_characters(
105    value: &str,
106    meta: &mut Meta,
107    state: &ProcessingState<'_>,
108) -> ProcessingResult {
109    if let Some(ref character_set) = state.attrs().characters {
110        for c in value.chars() {
111            if !(character_set.char_is_valid)(c) {
112                meta.add_error(Error::invalid(format!("invalid character {c:?}")));
113                return Err(ProcessingAction::DeleteValueSoft);
114            }
115        }
116    }
117
118    Ok(())
119}
120
121#[cfg(test)]
122mod tests {
123    use relay_event_schema::processor;
124    use relay_event_schema::protocol::{
125        CError, ClientSdkInfo, Event, MachException, Mechanism, MechanismMeta, PosixSignal,
126        RawStacktrace, User,
127    };
128    use relay_protocol::Annotated;
129    use similar_asserts::assert_eq;
130
131    use super::*;
132
133    // TODO(ja): Enable this test
134    // fn assert_nonempty_base<T>(expected_error: &str)
135    // where
136    //     T: Default + PartialEq + ProcessValue,
137    // {
138    //     #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
139    //     struct Foo<T> {
140    //         #[metastructure(required = "true", nonempty = "true")]
141    //         bar: Annotated<T>,
142    //         bar2: Annotated<T>,
143    //     }
144
145    //     let mut wrapper = Annotated::new(Foo {
146    //         bar: Annotated::new(T::default()),
147    //         bar2: Annotated::new(T::default()),
148    //     });
149    //     process_value(&mut wrapper, &mut SchemaProcessor, ProcessingState::root()).unwrap();
150
151    //     assert_eq!(
152    //         wrapper,
153    //         Annotated::new(Foo {
154    //             bar: Annotated::from_error(Error::expected(expected_error), None),
155    //             bar2: Annotated::new(T::default())
156    //         })
157    //     );
158    // }
159
160    // #[test]
161    // fn test_nonempty_string() {
162    //     assert_nonempty_base::<String>("a non-empty string");
163    // }
164
165    // #[test]
166    // fn test_nonempty_array() {
167    //     assert_nonempty_base::<Array<u64>>("a non-empty value");
168    // }
169
170    // #[test]
171    // fn test_nonempty_object() {
172    //     assert_nonempty_base::<Object<u64>>("a non-empty value");
173    // }
174
175    #[test]
176    fn test_invalid_email() {
177        let mut user = Annotated::new(User {
178            email: Annotated::new("bananabread".to_owned()),
179            ..Default::default()
180        });
181
182        let expected = user.clone();
183        processor::process_value(&mut user, &mut SchemaProcessor, ProcessingState::root()).unwrap();
184
185        assert_eq!(user, expected);
186    }
187
188    #[test]
189    fn test_client_sdk_missing_attribute() {
190        let mut info = Annotated::new(ClientSdkInfo {
191            name: Annotated::new("sentry.rust".to_string()),
192            ..Default::default()
193        });
194
195        processor::process_value(&mut info, &mut SchemaProcessor, ProcessingState::root()).unwrap();
196
197        let expected = Annotated::new(ClientSdkInfo {
198            name: Annotated::new("sentry.rust".to_string()),
199            version: Annotated::from_error(ErrorKind::MissingAttribute, None),
200            ..Default::default()
201        });
202
203        assert_eq!(info, expected);
204    }
205
206    #[test]
207    fn test_mechanism_missing_attributes() {
208        let mut mechanism = Annotated::new(Mechanism {
209            ty: Annotated::new("mytype".to_string()),
210            meta: Annotated::new(MechanismMeta {
211                errno: Annotated::new(CError {
212                    name: Annotated::new("ENOENT".to_string()),
213                    ..Default::default()
214                }),
215                mach_exception: Annotated::new(MachException {
216                    name: Annotated::new("EXC_BAD_ACCESS".to_string()),
217                    ..Default::default()
218                }),
219                signal: Annotated::new(PosixSignal {
220                    name: Annotated::new("SIGSEGV".to_string()),
221                    ..Default::default()
222                }),
223                ..Default::default()
224            }),
225            ..Default::default()
226        });
227
228        processor::process_value(
229            &mut mechanism,
230            &mut SchemaProcessor,
231            ProcessingState::root(),
232        )
233        .unwrap();
234
235        let expected = Annotated::new(Mechanism {
236            ty: Annotated::new("mytype".to_string()),
237            meta: Annotated::new(MechanismMeta {
238                errno: Annotated::new(CError {
239                    number: Annotated::empty(),
240                    name: Annotated::new("ENOENT".to_string()),
241                }),
242                mach_exception: Annotated::new(MachException {
243                    ty: Annotated::empty(),
244                    code: Annotated::empty(),
245                    subcode: Annotated::empty(),
246                    name: Annotated::new("EXC_BAD_ACCESS".to_string()),
247                }),
248                signal: Annotated::new(PosixSignal {
249                    number: Annotated::empty(),
250                    code: Annotated::empty(),
251                    name: Annotated::new("SIGSEGV".to_string()),
252                    code_name: Annotated::empty(),
253                }),
254                ..Default::default()
255            }),
256            ..Default::default()
257        });
258
259        assert_eq!(mechanism, expected);
260    }
261
262    #[test]
263    fn test_stacktrace_missing_attribute() {
264        let mut stack = Annotated::new(RawStacktrace::default());
265
266        processor::process_value(&mut stack, &mut SchemaProcessor, ProcessingState::root())
267            .unwrap();
268
269        let expected = Annotated::new(RawStacktrace {
270            frames: Annotated::from_error(ErrorKind::MissingAttribute, None),
271            ..Default::default()
272        });
273
274        assert_eq!(stack, expected);
275    }
276
277    #[test]
278    fn test_newlines_release() {
279        let mut event = Annotated::new(Event {
280            release: Annotated::new("42\n".to_string().into()),
281            ..Default::default()
282        });
283
284        processor::process_value(&mut event, &mut SchemaProcessor, ProcessingState::root())
285            .unwrap();
286
287        let expected = Annotated::new(Event {
288            release: Annotated::new("42".to_string().into()),
289            ..Default::default()
290        });
291
292        assert_eq!(expected, event);
293    }
294}