1use std::borrow::Cow;
2use std::collections::{BTreeMap, BTreeSet};
3use std::sync::OnceLock;
4
5use regex::{Regex, RegexBuilder};
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7
8use crate::{CompiledPiiConfig, Redaction, SelectorSpec};
9
10const COMPILED_PATTERN_MAX_SIZE: usize = 262_144;
11
12#[allow(clippy::trivially_copy_pass_by_ref)]
14pub(crate) fn is_flag_default(flag: &bool) -> bool {
15 !*flag
16}
17
18#[derive(Clone, Debug, thiserror::Error)]
20pub enum PiiConfigError {
21 #[error("could not parse pattern")]
23 RegexError(#[source] regex::Error),
24}
25
26#[derive(Debug, Clone)]
31pub struct LazyPattern {
32 raw: Cow<'static, str>,
33 case_insensitive: bool,
34 pattern: OnceLock<Result<Regex, PiiConfigError>>,
35}
36
37impl PartialEq for LazyPattern {
38 fn eq(&self, other: &Self) -> bool {
39 self.raw.to_lowercase() == other.raw.to_lowercase()
40 }
41}
42
43impl LazyPattern {
44 pub fn new<S>(raw: S) -> Self
46 where
47 Cow<'static, str>: From<S>,
48 {
49 Self {
50 raw: raw.into(),
51 case_insensitive: false,
52 pattern: OnceLock::new(),
53 }
54 }
55
56 pub fn case_insensitive(mut self, value: bool) -> Self {
61 self.case_insensitive = value;
62 self.pattern.take();
63 self
64 }
65
66 pub fn compiled(&self) -> Result<&Regex, &PiiConfigError> {
68 self.pattern
69 .get_or_init(|| {
70 let regex_result = RegexBuilder::new(&self.raw)
71 .size_limit(COMPILED_PATTERN_MAX_SIZE)
72 .case_insensitive(self.case_insensitive)
73 .build()
74 .map_err(PiiConfigError::RegexError);
75
76 if let Err(ref error) = regex_result {
77 relay_log::error!(
78 error = error as &dyn std::error::Error,
79 "unable to compile pattern into regex"
80 );
81 }
82 regex_result
83 })
84 .as_ref()
85 }
86}
87
88impl From<&'static str> for LazyPattern {
89 fn from(pattern: &'static str) -> LazyPattern {
90 LazyPattern::new(pattern)
91 }
92}
93
94impl Serialize for LazyPattern {
95 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
96 serializer.serialize_str(&self.raw)
97 }
98}
99
100impl<'de> Deserialize<'de> for LazyPattern {
101 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
102 let raw = String::deserialize(deserializer)?;
103 Ok(LazyPattern::new(raw))
104 }
105}
106
107#[allow(clippy::unnecessary_wraps)]
108fn replace_groups_default() -> Option<BTreeSet<u8>> {
109 let mut set = BTreeSet::new();
110 set.insert(0);
111 Some(set)
112}
113
114#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
116#[serde(rename_all = "camelCase")]
117pub struct PatternRule {
118 pub pattern: LazyPattern,
120 #[serde(default = "replace_groups_default")]
122 pub replace_groups: Option<BTreeSet<u8>>,
123}
124
125#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
127#[serde(rename_all = "camelCase")]
128pub struct MultipleRule {
129 pub rules: Vec<String>,
131 #[serde(default, skip_serializing_if = "is_flag_default")]
133 pub hide_inner: bool,
134}
135
136#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
138#[serde(rename_all = "camelCase")]
139pub struct AliasRule {
140 pub rule: String,
142 #[serde(default, skip_serializing_if = "is_flag_default")]
144 pub hide_inner: bool,
145}
146
147#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
149#[serde(rename_all = "camelCase")]
150pub struct RedactPairRule {
151 pub key_pattern: LazyPattern,
160}
161
162#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
164#[serde(tag = "type", rename_all = "snake_case")]
165pub enum RuleType {
166 Anything,
168 Pattern(PatternRule),
170 Imei,
172 Mac,
174 Uuid,
176 Email,
178 Ip,
180 Creditcard,
182 Iban,
184 Userpath,
186 Pemkey,
188 UrlAuth,
190 UsSsn,
192 Bearer,
194 Password,
196 Cookies,
198 #[serde(alias = "redactPair")]
200 RedactPair(RedactPairRule),
201 Multiple(MultipleRule),
203 Alias(AliasRule),
205 Unknown(String),
207}
208
209#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
211pub struct RuleSpec {
212 #[serde(flatten)]
214 pub ty: RuleType,
215
216 #[serde(default)]
218 pub redaction: Redaction,
219}
220
221#[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)]
223#[serde(rename_all = "camelCase")]
224pub struct Vars {
225 #[serde(default, skip_serializing_if = "Option::is_none")]
227 pub hash_key: Option<String>,
228}
229
230impl Vars {
231 fn is_empty(&self) -> bool {
232 self.hash_key.is_none()
233 }
234}
235
236#[derive(Serialize, Deserialize, Debug, Default, Clone)]
238pub struct PiiConfig {
239 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
241 pub rules: BTreeMap<String, RuleSpec>,
242
243 #[serde(default, skip_serializing_if = "Vars::is_empty")]
245 pub vars: Vars,
246
247 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
249 pub applications: BTreeMap<SelectorSpec, Vec<String>>,
250
251 #[serde(skip)]
255 pub(super) compiled: OnceLock<CompiledPiiConfig>,
256}
257
258impl PartialEq for PiiConfig {
259 fn eq(&self, other: &PiiConfig) -> bool {
260 let PiiConfig {
263 rules,
264 vars,
265 applications,
266 compiled: _compiled,
267 } = &self;
268
269 rules == &other.rules && vars == &other.vars && applications == &other.applications
270 }
271}
272
273impl PiiConfig {
274 pub fn compiled(&self) -> &CompiledPiiConfig {
279 self.compiled.get_or_init(|| self.compiled_uncached())
280 }
281
282 #[inline]
284 pub fn compiled_uncached(&self) -> CompiledPiiConfig {
285 CompiledPiiConfig::new(self)
286 }
287}