use std::borrow::Cow;
use std::collections::{BTreeMap, BTreeSet};
use std::sync::OnceLock;
use regex::{Regex, RegexBuilder};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::{CompiledPiiConfig, Redaction, SelectorSpec};
const COMPILED_PATTERN_MAX_SIZE: usize = 262_144;
#[allow(clippy::trivially_copy_pass_by_ref)]
pub(crate) fn is_flag_default(flag: &bool) -> bool {
!*flag
}
#[derive(Clone, Debug, thiserror::Error)]
pub enum PiiConfigError {
#[error("could not parse pattern")]
RegexError(#[source] regex::Error),
}
#[derive(Debug, Clone)]
pub struct LazyPattern {
raw: Cow<'static, str>,
case_insensitive: bool,
pattern: OnceLock<Result<Regex, PiiConfigError>>,
}
impl PartialEq for LazyPattern {
fn eq(&self, other: &Self) -> bool {
self.raw.to_lowercase() == other.raw.to_lowercase()
}
}
impl LazyPattern {
pub fn new<S>(raw: S) -> Self
where
Cow<'static, str>: From<S>,
{
Self {
raw: raw.into(),
case_insensitive: false,
pattern: OnceLock::new(),
}
}
pub fn case_insensitive(mut self, value: bool) -> Self {
self.case_insensitive = value;
self.pattern.take();
self
}
pub fn compiled(&self) -> Result<&Regex, &PiiConfigError> {
self.pattern
.get_or_init(|| {
let regex_result = RegexBuilder::new(&self.raw)
.size_limit(COMPILED_PATTERN_MAX_SIZE)
.case_insensitive(self.case_insensitive)
.build()
.map_err(PiiConfigError::RegexError);
if let Err(ref error) = regex_result {
relay_log::error!(
error = error as &dyn std::error::Error,
"unable to compile pattern into regex"
);
}
regex_result
})
.as_ref()
}
}
impl From<&'static str> for LazyPattern {
fn from(pattern: &'static str) -> LazyPattern {
LazyPattern::new(pattern)
}
}
impl Serialize for LazyPattern {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.raw)
}
}
impl<'de> Deserialize<'de> for LazyPattern {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let raw = String::deserialize(deserializer)?;
Ok(LazyPattern::new(raw))
}
}
#[allow(clippy::unnecessary_wraps)]
fn replace_groups_default() -> Option<BTreeSet<u8>> {
let mut set = BTreeSet::new();
set.insert(0);
Some(set)
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PatternRule {
pub pattern: LazyPattern,
#[serde(default = "replace_groups_default")]
pub replace_groups: Option<BTreeSet<u8>>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct MultipleRule {
pub rules: Vec<String>,
#[serde(default, skip_serializing_if = "is_flag_default")]
pub hide_inner: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct AliasRule {
pub rule: String,
#[serde(default, skip_serializing_if = "is_flag_default")]
pub hide_inner: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RedactPairRule {
pub key_pattern: LazyPattern,
}
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum RuleType {
Anything,
Pattern(PatternRule),
Imei,
Mac,
Uuid,
Email,
Ip,
Creditcard,
Iban,
Userpath,
Pemkey,
UrlAuth,
UsSsn,
Password,
#[serde(alias = "redactPair")]
RedactPair(RedactPairRule),
Multiple(MultipleRule),
Alias(AliasRule),
Unknown(String),
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct RuleSpec {
#[serde(flatten)]
pub ty: RuleType,
#[serde(default)]
pub redaction: Redaction,
}
#[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Vars {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hash_key: Option<String>,
}
impl Vars {
fn is_empty(&self) -> bool {
self.hash_key.is_none()
}
}
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct PiiConfig {
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub rules: BTreeMap<String, RuleSpec>,
#[serde(default, skip_serializing_if = "Vars::is_empty")]
pub vars: Vars,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub applications: BTreeMap<SelectorSpec, Vec<String>>,
#[serde(skip)]
pub(super) compiled: OnceLock<CompiledPiiConfig>,
}
impl PartialEq for PiiConfig {
fn eq(&self, other: &PiiConfig) -> bool {
let PiiConfig {
rules,
vars,
applications,
compiled: _compiled,
} = &self;
rules == &other.rules && vars == &other.vars && applications == &other.applications
}
}
impl PiiConfig {
pub fn compiled(&self) -> &CompiledPiiConfig {
self.compiled.get_or_init(|| self.compiled_uncached())
}
#[inline]
pub fn compiled_uncached(&self) -> CompiledPiiConfig {
CompiledPiiConfig::new(self)
}
}