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_cardinality::CardinalityLimit;
14use relay_dynamic_config::{GlobalConfig, ProjectConfig};
15use relay_event_normalization::{
16 BreakdownsConfig, ClientHints, EventValidationConfig, GeoIpLookup, NormalizationConfig,
17 RawUserAgentInfo, normalize_event, validate_event,
18};
19use relay_event_schema::processor::{ProcessingState, process_value, split_chunks};
20use relay_event_schema::protocol::{Event, IpAddr, VALID_PLATFORMS};
21use relay_pii::{
22 DataScrubbingConfig, InvalidSelectorError, PiiConfig, PiiConfigError, PiiProcessor,
23 SelectorSpec, selector_suggestions_from_value,
24};
25use relay_protocol::{Annotated, Remark, RuleCondition};
26use relay_sampling::SamplingConfig;
27use serde::{Deserialize, Serialize};
28use uuid::Uuid;
29
30use crate::core::RelayStr;
31
32#[derive(Serialize, Deserialize, Debug, Default)]
34#[serde(default)]
35pub struct StoreNormalizer {
36 pub project_id: Option<u64>,
38
39 pub client_ip: Option<IpAddr>,
44
45 pub client: Option<String>,
47
48 pub key_id: Option<String>,
52
53 pub protocol_version: Option<String>,
57
58 pub grouping_config: Option<serde_json::Value>,
63
64 pub user_agent: Option<String>,
72
73 pub client_hints: ClientHints<String>,
79
80 pub received_at: Option<DateTime<Utc>>,
84
85 pub sent_at: Option<DateTime<Utc>>,
90
91 pub max_secs_in_future: Option<i64>,
95
96 pub max_secs_in_past: Option<i64>,
100
101 pub enable_trimming: Option<bool>,
105
106 pub is_renormalize: Option<bool>,
115
116 pub remove_other: Option<bool>,
118
119 pub normalize_user_agent: Option<bool>,
121
122 pub breakdowns: Option<BreakdownsConfig>,
124
125 pub client_sample_rate: Option<f64>,
129
130 pub replay_id: Option<Uuid>,
134
135 pub normalize_spans: bool,
140}
141
142impl StoreNormalizer {
143 fn this(&self) -> &Self {
145 self
146 }
147}
148
149pub struct RelayGeoIpLookup;
151
152pub struct RelayStoreNormalizer;
154
155#[unsafe(no_mangle)]
157#[relay_ffi::catch_unwind]
158pub unsafe extern "C" fn relay_split_chunks(
159 string: *const RelayStr,
160 remarks: *const RelayStr,
161) -> RelayStr {
162 let remarks: Vec<Remark> = serde_json::from_str(unsafe { (*remarks).as_str() })?;
163 let chunks = split_chunks(unsafe { (*string).as_str() }, &remarks);
164 let json = serde_json::to_string(&chunks)?;
165 json.into()
166}
167
168#[unsafe(no_mangle)]
170#[relay_ffi::catch_unwind]
171pub unsafe extern "C" fn relay_geoip_lookup_new(path: *const c_char) -> *mut RelayGeoIpLookup {
172 let path = unsafe { CStr::from_ptr(path) }.to_string_lossy();
173 let lookup = GeoIpLookup::open(path.as_ref())?;
174 Box::into_raw(Box::new(lookup)) as *mut RelayGeoIpLookup
175}
176
177#[unsafe(no_mangle)]
179#[relay_ffi::catch_unwind]
180pub unsafe extern "C" fn relay_geoip_lookup_free(lookup: *mut RelayGeoIpLookup) {
181 if !lookup.is_null() {
182 let lookup = lookup as *mut GeoIpLookup;
183 let _dropped = unsafe { Box::from_raw(lookup) };
184 }
185}
186
187#[unsafe(no_mangle)]
189#[relay_ffi::catch_unwind]
190pub unsafe extern "C" fn relay_valid_platforms(size_out: *mut usize) -> *const RelayStr {
191 static VALID_PLATFORM_STRS: OnceLock<Vec<RelayStr>> = OnceLock::new();
192 let platforms = VALID_PLATFORM_STRS
193 .get_or_init(|| VALID_PLATFORMS.iter().map(|s| RelayStr::new(s)).collect());
194
195 if let Some(size_out) = unsafe { size_out.as_mut() } {
196 *size_out = platforms.len();
197 }
198
199 platforms.as_ptr()
200}
201
202#[unsafe(no_mangle)]
204#[relay_ffi::catch_unwind]
205pub unsafe extern "C" fn relay_store_normalizer_new(
206 config: *const RelayStr,
207 _geoip_lookup: *const RelayGeoIpLookup,
208) -> *mut RelayStoreNormalizer {
209 let normalizer: StoreNormalizer = serde_json::from_str(unsafe { (*config).as_str() })?;
210 Box::into_raw(Box::new(normalizer)) as *mut RelayStoreNormalizer
211}
212
213#[unsafe(no_mangle)]
215#[relay_ffi::catch_unwind]
216pub unsafe extern "C" fn relay_store_normalizer_free(normalizer: *mut RelayStoreNormalizer) {
217 if !normalizer.is_null() {
218 let normalizer = normalizer as *mut StoreNormalizer;
219 let _dropped = unsafe { Box::from_raw(normalizer) };
220 }
221}
222
223#[unsafe(no_mangle)]
225#[relay_ffi::catch_unwind]
226pub unsafe extern "C" fn relay_store_normalizer_normalize_event(
227 normalizer: *mut RelayStoreNormalizer,
228 event: *const RelayStr,
229) -> RelayStr {
230 let normalizer = normalizer as *mut StoreNormalizer;
231 let config = unsafe { (*normalizer).this() };
232 let mut event = Annotated::<Event>::from_json(unsafe { (*event).as_str() })?;
233
234 let event_validation_config = EventValidationConfig {
235 received_at: config.received_at,
236 max_secs_in_past: config.max_secs_in_past,
237 max_secs_in_future: config.max_secs_in_future,
238 transaction_timestamp_range: None, is_validated: config.is_renormalize.unwrap_or(false),
240 };
241 validate_event(&mut event, &event_validation_config)?;
242
243 let is_renormalize = config.is_renormalize.unwrap_or(false);
244
245 let normalization_config = NormalizationConfig {
246 project_id: config.project_id,
247 client: config.client.clone(),
248 protocol_version: config.protocol_version.clone(),
249 key_id: config.key_id.clone(),
250 grouping_config: config.grouping_config.clone(),
251 client_ip: config.client_ip.as_ref(),
252 infer_ip_address: false, client_sample_rate: config.client_sample_rate,
254 user_agent: RawUserAgentInfo {
255 user_agent: config.user_agent.as_deref(),
256 client_hints: config.client_hints.as_deref(),
257 },
258 max_name_and_unit_len: None,
259 breakdowns_config: None, normalize_user_agent: config.normalize_user_agent,
261 transaction_name_config: Default::default(), is_renormalize,
263 remove_other: config.remove_other.unwrap_or(!is_renormalize),
264 emit_event_errors: !is_renormalize,
265 device_class_synthesis_config: false, enrich_spans: false,
267 max_tag_value_length: usize::MAX,
268 span_description_rules: None,
269 performance_score: None,
270 geoip_lookup: None, ai_model_costs: None, enable_trimming: config.enable_trimming.unwrap_or_default(),
273 measurements: None,
274 normalize_spans: config.normalize_spans,
275 replay_id: config.replay_id,
276 span_allowed_hosts: &[], span_op_defaults: Default::default(), performance_issues_spans: Default::default(),
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 normalize_cardinality_limit_config(value: *const RelayStr) -> RelayStr {
462 let value = unsafe { (*value).as_str() };
463 match normalize_json::<CardinalityLimit>(value) {
464 Ok(normalized) => RelayStr::from_string(normalized),
465 Err(e) => RelayStr::from_string(e.to_string()),
466 }
467}
468
469#[unsafe(no_mangle)]
471#[relay_ffi::catch_unwind]
472pub unsafe extern "C" fn relay_normalize_global_config(value: *const RelayStr) -> RelayStr {
473 let value = unsafe { (*value).as_str() };
474 match normalize_json::<GlobalConfig>(value) {
475 Ok(normalized) => RelayStr::from_string(normalized),
476 Err(e) => RelayStr::from_string(e.to_string()),
477 }
478}
479
480fn normalize_json<'de, S>(value: &'de str) -> serde_json::Result<String>
482where
483 S: serde::Serialize + serde::Deserialize<'de>,
484{
485 let deserialized: S = serde_json::from_str(value)?;
486 let serialized = serde_json::to_value(&deserialized)?.to_string();
487 Ok(serialized)
488}
489
490#[cfg(test)]
491mod tests {
492 use super::*;
493
494 #[test]
495 fn pii_config_validation_invalid_regex() {
496 let config = r#"
497 {
498 "rules": {
499 "strip-fields": {
500 "type": "redact_pair",
501 "keyPattern": "(not valid regex",
502 "redaction": {
503 "method": "replace",
504 "text": "[Filtered]"
505 }
506 }
507 },
508 "applications": {
509 "*.everything": ["strip-fields"]
510 }
511 }
512 "#;
513 unsafe {
514 assert_eq!(
515 relay_validate_pii_config(&RelayStr::from(config)).as_str(),
516 "regex parse error:\n (not valid regex\n ^\nerror: unclosed group"
517 );
518 }
519 }
520
521 #[test]
522 fn pii_config_validation_valid_regex() {
523 let config = r#"
524 {
525 "rules": {
526 "strip-fields": {
527 "type": "redact_pair",
528 "keyPattern": "(\\w+)?+",
529 "redaction": {
530 "method": "replace",
531 "text": "[Filtered]"
532 }
533 }
534 },
535 "applications": {
536 "*.everything": ["strip-fields"]
537 }
538 }
539 "#;
540 unsafe {
541 assert_eq!(
542 relay_validate_pii_config(&RelayStr::from(config)).as_str(),
543 ""
544 );
545 }
546 }
547
548 #[test]
549 fn test_compare_versions_semver_precedence() {
550 unsafe {
551 assert_eq!(
553 relay_compare_versions_semver_precedence(
554 &RelayStr::from("1.0.0+200"),
555 &RelayStr::from("1.0.0+100")
556 ),
557 0
558 );
559 assert_eq!(
560 relay_compare_versions_semver_precedence(
561 &RelayStr::from("1.0.0+abc"),
562 &RelayStr::from("1.0.0+xyz")
563 ),
564 0
565 );
566
567 assert_eq!(
569 relay_compare_versions_semver_precedence(
570 &RelayStr::from("2.0.0"),
571 &RelayStr::from("1.0.0")
572 ),
573 1
574 );
575 assert_eq!(
576 relay_compare_versions_semver_precedence(
577 &RelayStr::from("1.0.0"),
578 &RelayStr::from("1.0.0-rc1")
579 ),
580 1
581 );
582 }
583 }
584}