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 };
279 normalize_event(&mut event, &normalization_config);
280
281 RelayStr::from_string(event.to_json()?)
282}
283
284#[unsafe(no_mangle)]
286#[relay_ffi::catch_unwind]
287pub unsafe extern "C" fn relay_translate_legacy_python_json(event: *mut RelayStr) -> bool {
288 let data = unsafe { slice::from_raw_parts_mut((*event).data as *mut u8, (*event).len) };
289 json_forensics::translate_slice(data);
290 true
291}
292
293#[unsafe(no_mangle)]
295#[relay_ffi::catch_unwind]
296pub unsafe extern "C" fn relay_validate_pii_selector(value: *const RelayStr) -> RelayStr {
297 let value = unsafe { (*value).as_str() };
298 match value.parse::<SelectorSpec>() {
299 Ok(_) => RelayStr::new(""),
300 Err(err) => match err {
301 InvalidSelectorError::ParseError(_) => {
302 RelayStr::from_string(format!("invalid syntax near {value:?}"))
305 }
306 err => RelayStr::from_string(err.to_string()),
307 },
308 }
309}
310
311#[unsafe(no_mangle)]
313#[relay_ffi::catch_unwind]
314pub unsafe extern "C" fn relay_validate_pii_config(value: *const RelayStr) -> RelayStr {
315 match serde_json::from_str::<PiiConfig>(unsafe { (*value).as_str() }) {
316 Ok(config) => match config.compiled().force_compile() {
317 Ok(_) => RelayStr::new(""),
318 Err(PiiConfigError::RegexError(source)) => RelayStr::from_string(source.to_string()),
319 },
320 Err(e) => RelayStr::from_string(e.to_string()),
321 }
322}
323
324#[unsafe(no_mangle)]
326#[relay_ffi::catch_unwind]
327pub unsafe extern "C" fn relay_convert_datascrubbing_config(config: *const RelayStr) -> RelayStr {
328 let config: DataScrubbingConfig = serde_json::from_str(unsafe { (*config).as_str() })?;
329 match config.pii_config() {
330 Some(config) => RelayStr::from_string(serde_json::to_string(config)?),
331 None => RelayStr::new("{}"),
332 }
333}
334
335#[unsafe(no_mangle)]
337#[relay_ffi::catch_unwind]
338pub unsafe extern "C" fn relay_pii_strip_event(
339 config: *const RelayStr,
340 event: *const RelayStr,
341) -> RelayStr {
342 let config = serde_json::from_str::<PiiConfig>(unsafe { (*config).as_str() })?;
343 let mut processor = PiiProcessor::new(config.compiled());
344
345 let mut event = Annotated::<Event>::from_json(unsafe { (*event).as_str() })?;
346 process_value(&mut event, &mut processor, ProcessingState::root())?;
347
348 RelayStr::from_string(event.to_json()?)
349}
350
351#[unsafe(no_mangle)]
354#[relay_ffi::catch_unwind]
355pub unsafe extern "C" fn relay_pii_selector_suggestions_from_event(
356 event: *const RelayStr,
357) -> RelayStr {
358 let mut event = Annotated::<Event>::from_json(unsafe { (*event).as_str() })?;
359 let rv = selector_suggestions_from_value(&mut event);
360 RelayStr::from_string(serde_json::to_string(&rv)?)
361}
362
363#[unsafe(no_mangle)]
365#[relay_ffi::catch_unwind]
366#[allow(clippy::diverging_sub_expression)]
367pub unsafe extern "C" fn relay_test_panic() -> () {
368 panic!("this is a test panic")
369}
370
371#[unsafe(no_mangle)]
373#[relay_ffi::catch_unwind]
374pub unsafe extern "C" fn relay_parse_release(value: *const RelayStr) -> RelayStr {
375 let release = sentry_release_parser::Release::parse(unsafe { (*value).as_str() })?;
376 RelayStr::from_string(serde_json::to_string(&release)?)
377}
378
379#[unsafe(no_mangle)]
381#[relay_ffi::catch_unwind]
382pub unsafe extern "C" fn relay_compare_versions(a: *const RelayStr, b: *const RelayStr) -> i32 {
383 let ver_a = sentry_release_parser::Version::parse(unsafe { (*a).as_str() })?;
384 let ver_b = sentry_release_parser::Version::parse(unsafe { (*b).as_str() })?;
385 match ver_a.cmp(&ver_b) {
386 Ordering::Less => -1,
387 Ordering::Equal => 0,
388 Ordering::Greater => 1,
389 }
390}
391
392#[unsafe(no_mangle)]
394#[relay_ffi::catch_unwind]
395pub unsafe extern "C" fn relay_compare_versions_semver_precedence(
396 a: *const RelayStr,
397 b: *const RelayStr,
398) -> i32 {
399 let ver_a = sentry_release_parser::Version::parse(unsafe { (*a).as_str() })?;
400 let ver_b = sentry_release_parser::Version::parse(unsafe { (*b).as_str() })?;
401 match ver_a.as_semver1().cmp_precedence(&ver_b.as_semver1()) {
402 Ordering::Less => -1,
403 Ordering::Equal => 0,
404 Ordering::Greater => 1,
405 }
406}
407
408#[unsafe(no_mangle)]
412#[relay_ffi::catch_unwind]
413pub unsafe extern "C" fn relay_validate_rule_condition(value: *const RelayStr) -> RelayStr {
414 let ret_val = match serde_json::from_str::<RuleCondition>(unsafe { (*value).as_str() }) {
415 Ok(condition) => {
416 if condition.supported() {
417 "".to_owned()
418 } else {
419 "unsupported condition".to_owned()
420 }
421 }
422 Err(e) => e.to_string(),
423 };
424 RelayStr::from_string(ret_val)
425}
426
427#[unsafe(no_mangle)]
431#[relay_ffi::catch_unwind]
432pub unsafe extern "C" fn relay_validate_sampling_configuration(value: *const RelayStr) -> RelayStr {
433 match serde_json::from_str::<SamplingConfig>(unsafe { (*value).as_str() }) {
434 Ok(config) => {
435 for rule in config.rules {
436 if !rule.condition.supported() {
437 return Ok(RelayStr::new("unsupported sampling rule"));
438 }
439 }
440 RelayStr::default()
441 }
442 Err(e) => RelayStr::from_string(e.to_string()),
443 }
444}
445
446#[unsafe(no_mangle)]
448#[relay_ffi::catch_unwind]
449pub unsafe extern "C" fn relay_normalize_project_config(value: *const RelayStr) -> RelayStr {
450 let value = unsafe { (*value).as_str() };
451 match normalize_json::<ProjectConfig>(value) {
452 Ok(normalized) => RelayStr::from_string(normalized),
453 Err(e) => RelayStr::from_string(e.to_string()),
454 }
455}
456
457#[unsafe(no_mangle)]
459#[relay_ffi::catch_unwind]
460pub unsafe extern "C" fn relay_normalize_global_config(value: *const RelayStr) -> RelayStr {
461 let value = unsafe { (*value).as_str() };
462 match normalize_json::<GlobalConfig>(value) {
463 Ok(normalized) => RelayStr::from_string(normalized),
464 Err(e) => RelayStr::from_string(e.to_string()),
465 }
466}
467
468fn normalize_json<'de, S>(value: &'de str) -> serde_json::Result<String>
470where
471 S: serde::Serialize + serde::Deserialize<'de>,
472{
473 let deserialized: S = serde_json::from_str(value)?;
474 let serialized = serde_json::to_value(&deserialized)?.to_string();
475 Ok(serialized)
476}
477
478#[cfg(test)]
479mod tests {
480 use super::*;
481
482 #[test]
483 fn pii_config_validation_invalid_regex() {
484 let config = r#"
485 {
486 "rules": {
487 "strip-fields": {
488 "type": "redact_pair",
489 "keyPattern": "(not valid regex",
490 "redaction": {
491 "method": "replace",
492 "text": "[Filtered]"
493 }
494 }
495 },
496 "applications": {
497 "*.everything": ["strip-fields"]
498 }
499 }
500 "#;
501 unsafe {
502 assert_eq!(
503 relay_validate_pii_config(&RelayStr::from(config)).as_str(),
504 "regex parse error:\n (not valid regex\n ^\nerror: unclosed group"
505 );
506 }
507 }
508
509 #[test]
510 fn pii_config_validation_valid_regex() {
511 let config = r#"
512 {
513 "rules": {
514 "strip-fields": {
515 "type": "redact_pair",
516 "keyPattern": "(\\w+)?+",
517 "redaction": {
518 "method": "replace",
519 "text": "[Filtered]"
520 }
521 }
522 },
523 "applications": {
524 "*.everything": ["strip-fields"]
525 }
526 }
527 "#;
528 unsafe {
529 assert_eq!(
530 relay_validate_pii_config(&RelayStr::from(config)).as_str(),
531 ""
532 );
533 }
534 }
535
536 #[test]
537 fn test_compare_versions_semver_precedence() {
538 unsafe {
539 assert_eq!(
541 relay_compare_versions_semver_precedence(
542 &RelayStr::from("1.0.0+200"),
543 &RelayStr::from("1.0.0+100")
544 ),
545 0
546 );
547 assert_eq!(
548 relay_compare_versions_semver_precedence(
549 &RelayStr::from("1.0.0+abc"),
550 &RelayStr::from("1.0.0+xyz")
551 ),
552 0
553 );
554
555 assert_eq!(
557 relay_compare_versions_semver_precedence(
558 &RelayStr::from("2.0.0"),
559 &RelayStr::from("1.0.0")
560 ),
561 1
562 );
563 assert_eq!(
564 relay_compare_versions_semver_precedence(
565 &RelayStr::from("1.0.0"),
566 &RelayStr::from("1.0.0-rc1")
567 ),
568 1
569 );
570 }
571 }
572}