1#![allow(clippy::cast_ptr_alignment)]
3#![deny(unused_must_use)]
4#![allow(clippy::derive_partial_eq_without_eq)]
5
6use std::cmp::Ordering;
7use std::ffi::CStr;
8use std::os::raw::c_char;
9use std::slice;
10use std::sync::OnceLock;
11
12use chrono::{DateTime, Utc};
13use relay_dynamic_config::{GlobalConfig, ProjectConfig};
14use relay_event_normalization::{
15 BreakdownsConfig, ClientHints, EventValidationConfig, GeoIpLookup, NormalizationConfig,
16 RawUserAgentInfo, normalize_event, validate_event,
17};
18use relay_event_schema::processor::{ProcessingState, process_value, split_chunks};
19use relay_event_schema::protocol::{Event, IpAddr, VALID_PLATFORMS};
20use relay_pii::{
21 DataScrubbingConfig, InvalidSelectorError, PiiConfig, PiiConfigError, PiiProcessor,
22 SelectorSpec, selector_suggestions_from_value,
23};
24use relay_protocol::{Annotated, Remark, RuleCondition};
25use relay_sampling::SamplingConfig;
26use serde::{Deserialize, Serialize};
27use uuid::Uuid;
28
29use crate::core::RelayStr;
30
31#[derive(Serialize, Deserialize, Debug, Default)]
33#[serde(default)]
34pub struct StoreNormalizer {
35 pub project_id: Option<u64>,
37
38 pub client_ip: Option<IpAddr>,
43
44 pub client: Option<String>,
46
47 pub key_id: Option<String>,
51
52 pub protocol_version: Option<String>,
56
57 pub grouping_config: Option<serde_json::Value>,
62
63 pub user_agent: Option<String>,
71
72 pub client_hints: ClientHints<String>,
78
79 pub received_at: Option<DateTime<Utc>>,
83
84 pub sent_at: Option<DateTime<Utc>>,
89
90 pub max_secs_in_future: Option<i64>,
94
95 pub max_secs_in_past: Option<i64>,
99
100 pub enable_trimming: Option<bool>,
104
105 pub is_renormalize: Option<bool>,
114
115 pub remove_other: Option<bool>,
117
118 pub normalize_user_agent: Option<bool>,
120
121 pub breakdowns: Option<BreakdownsConfig>,
123
124 pub client_sample_rate: Option<f64>,
128
129 pub replay_id: Option<Uuid>,
133
134 pub normalize_spans: bool,
139}
140
141impl StoreNormalizer {
142 fn this(&self) -> &Self {
144 self
145 }
146}
147
148pub struct RelayGeoIpLookup;
150
151pub struct RelayStoreNormalizer;
153
154#[unsafe(no_mangle)]
156#[relay_ffi::catch_unwind]
157pub unsafe extern "C" fn relay_split_chunks(
158 string: *const RelayStr,
159 remarks: *const RelayStr,
160) -> RelayStr {
161 let remarks: Vec<Remark> = serde_json::from_str(unsafe { (*remarks).as_str() })?;
162 let chunks = split_chunks(unsafe { (*string).as_str() }, &remarks);
163 let json = serde_json::to_string(&chunks)?;
164 json.into()
165}
166
167#[unsafe(no_mangle)]
169#[relay_ffi::catch_unwind]
170pub unsafe extern "C" fn relay_geoip_lookup_new(path: *const c_char) -> *mut RelayGeoIpLookup {
171 let path = unsafe { CStr::from_ptr(path) }.to_string_lossy();
172 let lookup = GeoIpLookup::open(path.as_ref())?;
173 Box::into_raw(Box::new(lookup)) as *mut RelayGeoIpLookup
174}
175
176#[unsafe(no_mangle)]
178#[relay_ffi::catch_unwind]
179pub unsafe extern "C" fn relay_geoip_lookup_free(lookup: *mut RelayGeoIpLookup) {
180 if !lookup.is_null() {
181 let lookup = lookup as *mut GeoIpLookup;
182 let _dropped = unsafe { Box::from_raw(lookup) };
183 }
184}
185
186#[unsafe(no_mangle)]
188#[relay_ffi::catch_unwind]
189pub unsafe extern "C" fn relay_valid_platforms(size_out: *mut usize) -> *const RelayStr {
190 static VALID_PLATFORM_STRS: OnceLock<Vec<RelayStr>> = OnceLock::new();
191 let platforms = VALID_PLATFORM_STRS
192 .get_or_init(|| VALID_PLATFORMS.iter().map(|s| RelayStr::new(s)).collect());
193
194 if let Some(size_out) = unsafe { size_out.as_mut() } {
195 *size_out = platforms.len();
196 }
197
198 platforms.as_ptr()
199}
200
201#[unsafe(no_mangle)]
203#[relay_ffi::catch_unwind]
204pub unsafe extern "C" fn relay_store_normalizer_new(
205 config: *const RelayStr,
206 _geoip_lookup: *const RelayGeoIpLookup,
207) -> *mut RelayStoreNormalizer {
208 let normalizer: StoreNormalizer = serde_json::from_str(unsafe { (*config).as_str() })?;
209 Box::into_raw(Box::new(normalizer)) as *mut RelayStoreNormalizer
210}
211
212#[unsafe(no_mangle)]
214#[relay_ffi::catch_unwind]
215pub unsafe extern "C" fn relay_store_normalizer_free(normalizer: *mut RelayStoreNormalizer) {
216 if !normalizer.is_null() {
217 let normalizer = normalizer as *mut StoreNormalizer;
218 let _dropped = unsafe { Box::from_raw(normalizer) };
219 }
220}
221
222#[unsafe(no_mangle)]
224#[relay_ffi::catch_unwind]
225pub unsafe extern "C" fn relay_store_normalizer_normalize_event(
226 normalizer: *mut RelayStoreNormalizer,
227 event: *const RelayStr,
228) -> RelayStr {
229 let normalizer = normalizer as *mut StoreNormalizer;
230 let config = unsafe { (*normalizer).this() };
231 let mut event = Annotated::<Event>::from_json(unsafe { (*event).as_str() })?;
232
233 let event_validation_config = EventValidationConfig {
234 received_at: config.received_at,
235 max_secs_in_past: config.max_secs_in_past,
236 max_secs_in_future: config.max_secs_in_future,
237 transaction_timestamp_range: None, is_validated: config.is_renormalize.unwrap_or(false),
239 };
240 validate_event(&mut event, &event_validation_config)?;
241
242 let is_renormalize = config.is_renormalize.unwrap_or(false);
243
244 let normalization_config = NormalizationConfig {
245 project_id: config.project_id,
246 client: config.client.clone(),
247 protocol_version: config.protocol_version.clone(),
248 key_id: config.key_id.clone(),
249 grouping_config: config.grouping_config.clone(),
250 client_ip: config.client_ip.as_ref(),
251 infer_ip_address: false, client_sample_rate: config.client_sample_rate,
253 user_agent: RawUserAgentInfo {
254 user_agent: config.user_agent.as_deref(),
255 client_hints: config.client_hints.as_deref(),
256 },
257 max_name_and_unit_len: None,
258 breakdowns_config: None, normalize_user_agent: config.normalize_user_agent,
260 transaction_name_config: Default::default(), is_renormalize,
262 remove_other: config.remove_other.unwrap_or(!is_renormalize),
263 emit_event_errors: !is_renormalize,
264 enrich_spans: false,
265 max_tag_value_length: usize::MAX,
266 span_description_rules: None,
267 performance_score: None,
268 geoip_lookup: None, ai_model_metadata: None, enable_trimming: config.enable_trimming.unwrap_or_default(),
271 measurements: None,
272 normalize_spans: config.normalize_spans,
273 replay_id: config.replay_id,
274 span_allowed_hosts: &[], span_op_defaults: Default::default(), performance_issues_spans: Default::default(),
277 derive_trace_id: Default::default(),
278 dsc: None,
279 };
280 normalize_event(&mut event, &normalization_config);
281
282 RelayStr::from_string(event.to_json()?)
283}
284
285#[unsafe(no_mangle)]
287#[relay_ffi::catch_unwind]
288pub unsafe extern "C" fn relay_translate_legacy_python_json(event: *mut RelayStr) -> bool {
289 let data = unsafe { slice::from_raw_parts_mut((*event).data as *mut u8, (*event).len) };
290 json_forensics::translate_slice(data);
291 true
292}
293
294#[unsafe(no_mangle)]
296#[relay_ffi::catch_unwind]
297pub unsafe extern "C" fn relay_validate_pii_selector(value: *const RelayStr) -> RelayStr {
298 let value = unsafe { (*value).as_str() };
299 match value.parse::<SelectorSpec>() {
300 Ok(_) => RelayStr::new(""),
301 Err(err) => match err {
302 InvalidSelectorError::ParseError(_) => {
303 RelayStr::from_string(format!("invalid syntax near {value:?}"))
306 }
307 err => RelayStr::from_string(err.to_string()),
308 },
309 }
310}
311
312#[unsafe(no_mangle)]
314#[relay_ffi::catch_unwind]
315pub unsafe extern "C" fn relay_validate_pii_config(value: *const RelayStr) -> RelayStr {
316 match serde_json::from_str::<PiiConfig>(unsafe { (*value).as_str() }) {
317 Ok(config) => match config.compiled().force_compile() {
318 Ok(_) => RelayStr::new(""),
319 Err(PiiConfigError::RegexError(source)) => RelayStr::from_string(source.to_string()),
320 },
321 Err(e) => RelayStr::from_string(e.to_string()),
322 }
323}
324
325#[unsafe(no_mangle)]
327#[relay_ffi::catch_unwind]
328pub unsafe extern "C" fn relay_convert_datascrubbing_config(config: *const RelayStr) -> RelayStr {
329 let config: DataScrubbingConfig = serde_json::from_str(unsafe { (*config).as_str() })?;
330 match config.pii_config() {
331 Some(config) => RelayStr::from_string(serde_json::to_string(config)?),
332 None => RelayStr::new("{}"),
333 }
334}
335
336#[unsafe(no_mangle)]
338#[relay_ffi::catch_unwind]
339pub unsafe extern "C" fn relay_pii_strip_event(
340 config: *const RelayStr,
341 event: *const RelayStr,
342) -> RelayStr {
343 let config = serde_json::from_str::<PiiConfig>(unsafe { (*config).as_str() })?;
344 let mut processor = PiiProcessor::new(config.compiled());
345
346 let mut event = Annotated::<Event>::from_json(unsafe { (*event).as_str() })?;
347 process_value(&mut event, &mut processor, ProcessingState::root())?;
348
349 RelayStr::from_string(event.to_json()?)
350}
351
352#[unsafe(no_mangle)]
355#[relay_ffi::catch_unwind]
356pub unsafe extern "C" fn relay_pii_selector_suggestions_from_event(
357 event: *const RelayStr,
358) -> RelayStr {
359 let mut event = Annotated::<Event>::from_json(unsafe { (*event).as_str() })?;
360 let rv = selector_suggestions_from_value(&mut event);
361 RelayStr::from_string(serde_json::to_string(&rv)?)
362}
363
364#[unsafe(no_mangle)]
366#[relay_ffi::catch_unwind]
367#[allow(clippy::diverging_sub_expression)]
368pub unsafe extern "C" fn relay_test_panic() -> () {
369 panic!("this is a test panic")
370}
371
372#[unsafe(no_mangle)]
374#[relay_ffi::catch_unwind]
375pub unsafe extern "C" fn relay_parse_release(value: *const RelayStr) -> RelayStr {
376 let release = sentry_release_parser::Release::parse(unsafe { (*value).as_str() })?;
377 RelayStr::from_string(serde_json::to_string(&release)?)
378}
379
380#[unsafe(no_mangle)]
382#[relay_ffi::catch_unwind]
383pub unsafe extern "C" fn relay_compare_versions(a: *const RelayStr, b: *const RelayStr) -> i32 {
384 let ver_a = sentry_release_parser::Version::parse(unsafe { (*a).as_str() })?;
385 let ver_b = sentry_release_parser::Version::parse(unsafe { (*b).as_str() })?;
386 match ver_a.cmp(&ver_b) {
387 Ordering::Less => -1,
388 Ordering::Equal => 0,
389 Ordering::Greater => 1,
390 }
391}
392
393#[unsafe(no_mangle)]
395#[relay_ffi::catch_unwind]
396pub unsafe extern "C" fn relay_compare_versions_semver_precedence(
397 a: *const RelayStr,
398 b: *const RelayStr,
399) -> i32 {
400 let ver_a = sentry_release_parser::Version::parse(unsafe { (*a).as_str() })?;
401 let ver_b = sentry_release_parser::Version::parse(unsafe { (*b).as_str() })?;
402 match ver_a.as_semver1().cmp_precedence(&ver_b.as_semver1()) {
403 Ordering::Less => -1,
404 Ordering::Equal => 0,
405 Ordering::Greater => 1,
406 }
407}
408
409#[unsafe(no_mangle)]
413#[relay_ffi::catch_unwind]
414pub unsafe extern "C" fn relay_validate_rule_condition(value: *const RelayStr) -> RelayStr {
415 let ret_val = match serde_json::from_str::<RuleCondition>(unsafe { (*value).as_str() }) {
416 Ok(condition) => {
417 if condition.supported() {
418 "".to_owned()
419 } else {
420 "unsupported condition".to_owned()
421 }
422 }
423 Err(e) => e.to_string(),
424 };
425 RelayStr::from_string(ret_val)
426}
427
428#[unsafe(no_mangle)]
432#[relay_ffi::catch_unwind]
433pub unsafe extern "C" fn relay_validate_sampling_configuration(value: *const RelayStr) -> RelayStr {
434 match serde_json::from_str::<SamplingConfig>(unsafe { (*value).as_str() }) {
435 Ok(config) => {
436 for rule in config.rules {
437 if !rule.condition.supported() {
438 return Ok(RelayStr::new("unsupported sampling rule"));
439 }
440 }
441 RelayStr::default()
442 }
443 Err(e) => RelayStr::from_string(e.to_string()),
444 }
445}
446
447#[unsafe(no_mangle)]
449#[relay_ffi::catch_unwind]
450pub unsafe extern "C" fn relay_normalize_project_config(value: *const RelayStr) -> RelayStr {
451 let value = unsafe { (*value).as_str() };
452 match normalize_json::<ProjectConfig>(value) {
453 Ok(normalized) => RelayStr::from_string(normalized),
454 Err(e) => RelayStr::from_string(e.to_string()),
455 }
456}
457
458#[unsafe(no_mangle)]
460#[relay_ffi::catch_unwind]
461pub unsafe extern "C" fn relay_normalize_global_config(value: *const RelayStr) -> RelayStr {
462 let value = unsafe { (*value).as_str() };
463 match normalize_json::<GlobalConfig>(value) {
464 Ok(normalized) => RelayStr::from_string(normalized),
465 Err(e) => RelayStr::from_string(e.to_string()),
466 }
467}
468
469fn normalize_json<'de, S>(value: &'de str) -> serde_json::Result<String>
471where
472 S: serde::Serialize + serde::Deserialize<'de>,
473{
474 let deserialized: S = serde_json::from_str(value)?;
475 let serialized = serde_json::to_value(&deserialized)?.to_string();
476 Ok(serialized)
477}
478
479#[cfg(test)]
480mod tests {
481 use super::*;
482
483 #[test]
484 fn pii_config_validation_invalid_regex() {
485 let config = r#"
486 {
487 "rules": {
488 "strip-fields": {
489 "type": "redact_pair",
490 "keyPattern": "(not valid regex",
491 "redaction": {
492 "method": "replace",
493 "text": "[Filtered]"
494 }
495 }
496 },
497 "applications": {
498 "*.everything": ["strip-fields"]
499 }
500 }
501 "#;
502 unsafe {
503 assert_eq!(
504 relay_validate_pii_config(&RelayStr::from(config)).as_str(),
505 "regex parse error:\n (not valid regex\n ^\nerror: unclosed group"
506 );
507 }
508 }
509
510 #[test]
511 fn pii_config_validation_valid_regex() {
512 let config = r#"
513 {
514 "rules": {
515 "strip-fields": {
516 "type": "redact_pair",
517 "keyPattern": "(\\w+)?+",
518 "redaction": {
519 "method": "replace",
520 "text": "[Filtered]"
521 }
522 }
523 },
524 "applications": {
525 "*.everything": ["strip-fields"]
526 }
527 }
528 "#;
529 unsafe {
530 assert_eq!(
531 relay_validate_pii_config(&RelayStr::from(config)).as_str(),
532 ""
533 );
534 }
535 }
536
537 #[test]
538 fn test_compare_versions_semver_precedence() {
539 unsafe {
540 assert_eq!(
542 relay_compare_versions_semver_precedence(
543 &RelayStr::from("1.0.0+200"),
544 &RelayStr::from("1.0.0+100")
545 ),
546 0
547 );
548 assert_eq!(
549 relay_compare_versions_semver_precedence(
550 &RelayStr::from("1.0.0+abc"),
551 &RelayStr::from("1.0.0+xyz")
552 ),
553 0
554 );
555
556 assert_eq!(
558 relay_compare_versions_semver_precedence(
559 &RelayStr::from("2.0.0"),
560 &RelayStr::from("1.0.0")
561 ),
562 1
563 );
564 assert_eq!(
565 relay_compare_versions_semver_precedence(
566 &RelayStr::from("1.0.0"),
567 &RelayStr::from("1.0.0-rc1")
568 ),
569 1
570 );
571 }
572 }
573}