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 #[serde(alias = "redactPair")]
198 RedactPair(RedactPairRule),
199 Multiple(MultipleRule),
201 Alias(AliasRule),
203 Unknown(String),
205}
206
207#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
209pub struct RuleSpec {
210 #[serde(flatten)]
212 pub ty: RuleType,
213
214 #[serde(default)]
216 pub redaction: Redaction,
217}
218
219#[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)]
221#[serde(rename_all = "camelCase")]
222pub struct Vars {
223 #[serde(default, skip_serializing_if = "Option::is_none")]
225 pub hash_key: Option<String>,
226}
227
228impl Vars {
229 fn is_empty(&self) -> bool {
230 self.hash_key.is_none()
231 }
232}
233
234#[derive(Serialize, Deserialize, Debug, Default, Clone)]
236pub struct PiiConfig {
237 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
239 pub rules: BTreeMap<String, RuleSpec>,
240
241 #[serde(default, skip_serializing_if = "Vars::is_empty")]
243 pub vars: Vars,
244
245 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
247 pub applications: BTreeMap<SelectorSpec, Vec<String>>,
248
249 #[serde(skip)]
253 pub(super) compiled: OnceLock<CompiledPiiConfig>,
254}
255
256impl PartialEq for PiiConfig {
257 fn eq(&self, other: &PiiConfig) -> bool {
258 let PiiConfig {
261 rules,
262 vars,
263 applications,
264 compiled: _compiled,
265 } = &self;
266
267 rules == &other.rules && vars == &other.vars && applications == &other.applications
268 }
269}
270
271impl PiiConfig {
272 pub fn compiled(&self) -> &CompiledPiiConfig {
277 self.compiled.get_or_init(|| self.compiled_uncached())
278 }
279
280 #[inline]
282 pub fn compiled_uncached(&self) -> CompiledPiiConfig {
283 CompiledPiiConfig::new(self)
284 }
285}