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