#![allow(clippy::cast_ptr_alignment)]
#![deny(unused_must_use)]
#![allow(clippy::derive_partial_eq_without_eq)]
use std::cmp::Ordering;
use std::ffi::CStr;
use std::os::raw::c_char;
use std::slice;
use std::sync::OnceLock;
use chrono::{DateTime, Utc};
use relay_cardinality::CardinalityLimit;
use relay_dynamic_config::{normalize_json, GlobalConfig, ProjectConfig};
use relay_event_normalization::{
normalize_event, validate_event, BreakdownsConfig, ClientHints, EventValidationConfig,
GeoIpLookup, NormalizationConfig, RawUserAgentInfo,
};
use relay_event_schema::processor::{process_value, split_chunks, ProcessingState};
use relay_event_schema::protocol::{Event, IpAddr, VALID_PLATFORMS};
use relay_pii::{
selector_suggestions_from_value, DataScrubbingConfig, InvalidSelectorError, PiiConfig,
PiiConfigError, PiiProcessor, SelectorSpec,
};
use relay_protocol::{Annotated, Remark, RuleCondition};
use relay_sampling::SamplingConfig;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::core::RelayStr;
#[derive(Serialize, Deserialize, Debug, Default)]
#[serde(default)]
pub struct StoreNormalizer {
pub project_id: Option<u64>,
pub client_ip: Option<IpAddr>,
pub client: Option<String>,
pub key_id: Option<String>,
pub protocol_version: Option<String>,
pub grouping_config: Option<serde_json::Value>,
pub user_agent: Option<String>,
pub client_hints: ClientHints<String>,
pub received_at: Option<DateTime<Utc>>,
pub sent_at: Option<DateTime<Utc>>,
pub max_secs_in_future: Option<i64>,
pub max_secs_in_past: Option<i64>,
pub enable_trimming: Option<bool>,
pub is_renormalize: Option<bool>,
pub remove_other: Option<bool>,
pub normalize_user_agent: Option<bool>,
pub breakdowns: Option<BreakdownsConfig>,
pub client_sample_rate: Option<f64>,
pub replay_id: Option<Uuid>,
pub normalize_spans: bool,
}
impl StoreNormalizer {
fn this(&self) -> &Self {
self
}
}
pub struct RelayGeoIpLookup;
pub struct RelayStoreNormalizer;
#[no_mangle]
#[relay_ffi::catch_unwind]
pub unsafe extern "C" fn relay_split_chunks(
string: *const RelayStr,
remarks: *const RelayStr,
) -> RelayStr {
let remarks: Vec<Remark> = serde_json::from_str((*remarks).as_str())?;
let chunks = split_chunks((*string).as_str(), &remarks);
let json = serde_json::to_string(&chunks)?;
json.into()
}
#[no_mangle]
#[relay_ffi::catch_unwind]
pub unsafe extern "C" fn relay_geoip_lookup_new(path: *const c_char) -> *mut RelayGeoIpLookup {
let path = CStr::from_ptr(path).to_string_lossy();
let lookup = GeoIpLookup::open(path.as_ref())?;
Box::into_raw(Box::new(lookup)) as *mut RelayGeoIpLookup
}
#[no_mangle]
#[relay_ffi::catch_unwind]
pub unsafe extern "C" fn relay_geoip_lookup_free(lookup: *mut RelayGeoIpLookup) {
if !lookup.is_null() {
let lookup = lookup as *mut GeoIpLookup;
let _dropped = Box::from_raw(lookup);
}
}
#[no_mangle]
#[relay_ffi::catch_unwind]
pub unsafe extern "C" fn relay_valid_platforms(size_out: *mut usize) -> *const RelayStr {
static VALID_PLATFORM_STRS: OnceLock<Vec<RelayStr>> = OnceLock::new();
let platforms = VALID_PLATFORM_STRS
.get_or_init(|| VALID_PLATFORMS.iter().map(|s| RelayStr::new(s)).collect());
if let Some(size_out) = size_out.as_mut() {
*size_out = platforms.len();
}
platforms.as_ptr()
}
#[no_mangle]
#[relay_ffi::catch_unwind]
pub unsafe extern "C" fn relay_store_normalizer_new(
config: *const RelayStr,
_geoip_lookup: *const RelayGeoIpLookup,
) -> *mut RelayStoreNormalizer {
let normalizer: StoreNormalizer = serde_json::from_str((*config).as_str())?;
Box::into_raw(Box::new(normalizer)) as *mut RelayStoreNormalizer
}
#[no_mangle]
#[relay_ffi::catch_unwind]
pub unsafe extern "C" fn relay_store_normalizer_free(normalizer: *mut RelayStoreNormalizer) {
if !normalizer.is_null() {
let normalizer = normalizer as *mut StoreNormalizer;
let _dropped = Box::from_raw(normalizer);
}
}
#[no_mangle]
#[relay_ffi::catch_unwind]
pub unsafe extern "C" fn relay_store_normalizer_normalize_event(
normalizer: *mut RelayStoreNormalizer,
event: *const RelayStr,
) -> RelayStr {
let normalizer = normalizer as *mut StoreNormalizer;
let config = (*normalizer).this();
let mut event = Annotated::<Event>::from_json((*event).as_str())?;
let event_validation_config = EventValidationConfig {
received_at: config.received_at,
max_secs_in_past: config.max_secs_in_past,
max_secs_in_future: config.max_secs_in_future,
transaction_timestamp_range: None, is_validated: config.is_renormalize.unwrap_or(false),
};
validate_event(&mut event, &event_validation_config)?;
let is_renormalize = config.is_renormalize.unwrap_or(false);
let normalization_config = NormalizationConfig {
project_id: config.project_id,
client: config.client.clone(),
protocol_version: config.protocol_version.clone(),
key_id: config.key_id.clone(),
grouping_config: config.grouping_config.clone(),
client_ip: config.client_ip.as_ref(),
client_sample_rate: config.client_sample_rate,
user_agent: RawUserAgentInfo {
user_agent: config.user_agent.as_deref(),
client_hints: config.client_hints.as_deref(),
},
max_name_and_unit_len: None,
breakdowns_config: None, normalize_user_agent: config.normalize_user_agent,
transaction_name_config: Default::default(), is_renormalize,
remove_other: config.remove_other.unwrap_or(!is_renormalize),
emit_event_errors: !is_renormalize,
device_class_synthesis_config: false, enrich_spans: false,
max_tag_value_length: usize::MAX,
span_description_rules: None,
performance_score: None,
geoip_lookup: None, ai_model_costs: None, enable_trimming: config.enable_trimming.unwrap_or_default(),
measurements: None,
normalize_spans: config.normalize_spans,
replay_id: config.replay_id,
span_allowed_hosts: &[], span_op_defaults: Default::default(), };
normalize_event(&mut event, &normalization_config);
RelayStr::from_string(event.to_json()?)
}
#[no_mangle]
#[relay_ffi::catch_unwind]
pub unsafe extern "C" fn relay_translate_legacy_python_json(event: *mut RelayStr) -> bool {
let data = slice::from_raw_parts_mut((*event).data as *mut u8, (*event).len);
json_forensics::translate_slice(data);
true
}
#[no_mangle]
#[relay_ffi::catch_unwind]
pub unsafe extern "C" fn relay_validate_pii_selector(value: *const RelayStr) -> RelayStr {
let value = (*value).as_str();
match value.parse::<SelectorSpec>() {
Ok(_) => RelayStr::new(""),
Err(err) => match err {
InvalidSelectorError::ParseError(_) => {
RelayStr::from_string(format!("invalid syntax near {value:?}"))
}
err => RelayStr::from_string(err.to_string()),
},
}
}
#[no_mangle]
#[relay_ffi::catch_unwind]
pub unsafe extern "C" fn relay_validate_pii_config(value: *const RelayStr) -> RelayStr {
match serde_json::from_str::<PiiConfig>((*value).as_str()) {
Ok(config) => match config.compiled().force_compile() {
Ok(_) => RelayStr::new(""),
Err(PiiConfigError::RegexError(source)) => RelayStr::from_string(source.to_string()),
},
Err(e) => RelayStr::from_string(e.to_string()),
}
}
#[no_mangle]
#[relay_ffi::catch_unwind]
pub unsafe extern "C" fn relay_convert_datascrubbing_config(config: *const RelayStr) -> RelayStr {
let config: DataScrubbingConfig = serde_json::from_str((*config).as_str())?;
match config.pii_config() {
Ok(Some(config)) => RelayStr::from_string(serde_json::to_string(config)?),
Ok(None) => RelayStr::new("{}"),
Err(e) => RelayStr::from_string(e.to_string()),
}
}
#[no_mangle]
#[relay_ffi::catch_unwind]
pub unsafe extern "C" fn relay_pii_strip_event(
config: *const RelayStr,
event: *const RelayStr,
) -> RelayStr {
let config = serde_json::from_str::<PiiConfig>((*config).as_str())?;
let mut processor = PiiProcessor::new(config.compiled());
let mut event = Annotated::<Event>::from_json((*event).as_str())?;
process_value(&mut event, &mut processor, ProcessingState::root())?;
RelayStr::from_string(event.to_json()?)
}
#[no_mangle]
#[relay_ffi::catch_unwind]
pub unsafe extern "C" fn relay_pii_selector_suggestions_from_event(
event: *const RelayStr,
) -> RelayStr {
let mut event = Annotated::<Event>::from_json((*event).as_str())?;
let rv = selector_suggestions_from_value(&mut event);
RelayStr::from_string(serde_json::to_string(&rv)?)
}
#[no_mangle]
#[relay_ffi::catch_unwind]
#[allow(clippy::diverging_sub_expression)]
pub unsafe extern "C" fn relay_test_panic() -> () {
panic!("this is a test panic")
}
#[no_mangle]
#[relay_ffi::catch_unwind]
pub unsafe extern "C" fn relay_parse_release(value: *const RelayStr) -> RelayStr {
let release = sentry_release_parser::Release::parse((*value).as_str())?;
RelayStr::from_string(serde_json::to_string(&release)?)
}
#[no_mangle]
#[relay_ffi::catch_unwind]
pub unsafe extern "C" fn relay_compare_versions(a: *const RelayStr, b: *const RelayStr) -> i32 {
let ver_a = sentry_release_parser::Version::parse((*a).as_str())?;
let ver_b = sentry_release_parser::Version::parse((*b).as_str())?;
match ver_a.cmp(&ver_b) {
Ordering::Less => -1,
Ordering::Equal => 0,
Ordering::Greater => 1,
}
}
#[no_mangle]
#[relay_ffi::catch_unwind]
pub unsafe extern "C" fn relay_validate_rule_condition(value: *const RelayStr) -> RelayStr {
let ret_val = match serde_json::from_str::<RuleCondition>((*value).as_str()) {
Ok(condition) => {
if condition.supported() {
"".to_string()
} else {
"unsupported condition".to_string()
}
}
Err(e) => e.to_string(),
};
RelayStr::from_string(ret_val)
}
#[no_mangle]
#[relay_ffi::catch_unwind]
pub unsafe extern "C" fn relay_validate_sampling_configuration(value: *const RelayStr) -> RelayStr {
match serde_json::from_str::<SamplingConfig>((*value).as_str()) {
Ok(config) => {
for rule in config.rules {
if !rule.condition.supported() {
return Ok(RelayStr::new("unsupported sampling rule"));
}
}
RelayStr::default()
}
Err(e) => RelayStr::from_string(e.to_string()),
}
}
#[no_mangle]
#[relay_ffi::catch_unwind]
pub unsafe extern "C" fn relay_normalize_project_config(value: *const RelayStr) -> RelayStr {
let value = (*value).as_str();
match normalize_json::<ProjectConfig>(value) {
Ok(normalized) => RelayStr::from_string(normalized),
Err(e) => RelayStr::from_string(e.to_string()),
}
}
#[no_mangle]
#[relay_ffi::catch_unwind]
pub unsafe extern "C" fn normalize_cardinality_limit_config(value: *const RelayStr) -> RelayStr {
let value = (*value).as_str();
match normalize_json::<CardinalityLimit>(value) {
Ok(normalized) => RelayStr::from_string(normalized),
Err(e) => RelayStr::from_string(e.to_string()),
}
}
#[no_mangle]
#[relay_ffi::catch_unwind]
pub unsafe extern "C" fn relay_normalize_global_config(value: *const RelayStr) -> RelayStr {
let value = (*value).as_str();
match normalize_json::<GlobalConfig>(value) {
Ok(normalized) => RelayStr::from_string(normalized),
Err(e) => RelayStr::from_string(e.to_string()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pii_config_validation_invalid_regex() {
let config = r#"
{
"rules": {
"strip-fields": {
"type": "redact_pair",
"keyPattern": "(not valid regex",
"redaction": {
"method": "replace",
"text": "[Filtered]"
}
}
},
"applications": {
"*.everything": ["strip-fields"]
}
}
"#;
assert_eq!(
unsafe { relay_validate_pii_config(&RelayStr::from(config)).as_str() },
"regex parse error:\n (not valid regex\n ^\nerror: unclosed group"
);
}
#[test]
fn pii_config_validation_valid_regex() {
let config = r#"
{
"rules": {
"strip-fields": {
"type": "redact_pair",
"keyPattern": "(\\w+)?+",
"redaction": {
"method": "replace",
"text": "[Filtered]"
}
}
},
"applications": {
"*.everything": ["strip-fields"]
}
}
"#;
assert_eq!(
unsafe { relay_validate_pii_config(&RelayStr::from(config)).as_str() },
""
);
}
}