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,
153}
154
155#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
157#[serde(tag = "type", rename_all = "snake_case")]
158pub enum RuleType {
159 Anything,
161 Pattern(PatternRule),
163 Imei,
165 Mac,
167 Uuid,
169 Email,
171 Ip,
173 Creditcard,
175 Iban,
177 Userpath,
179 Pemkey,
181 UrlAuth,
183 UsSsn,
185 Bearer,
187 Password,
189 #[serde(alias = "redactPair")]
191 RedactPair(RedactPairRule),
192 Multiple(MultipleRule),
194 Alias(AliasRule),
196 Unknown(String),
198}
199
200#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
202pub struct RuleSpec {
203 #[serde(flatten)]
205 pub ty: RuleType,
206
207 #[serde(default)]
209 pub redaction: Redaction,
210}
211
212#[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)]
214#[serde(rename_all = "camelCase")]
215pub struct Vars {
216 #[serde(default, skip_serializing_if = "Option::is_none")]
218 pub hash_key: Option<String>,
219}
220
221impl Vars {
222 fn is_empty(&self) -> bool {
223 self.hash_key.is_none()
224 }
225}
226
227#[derive(Serialize, Deserialize, Debug, Default, Clone)]
229pub struct PiiConfig {
230 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
232 pub rules: BTreeMap<String, RuleSpec>,
233
234 #[serde(default, skip_serializing_if = "Vars::is_empty")]
236 pub vars: Vars,
237
238 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
240 pub applications: BTreeMap<SelectorSpec, Vec<String>>,
241
242 #[serde(skip)]
246 pub(super) compiled: OnceLock<CompiledPiiConfig>,
247}
248
249impl PartialEq for PiiConfig {
250 fn eq(&self, other: &PiiConfig) -> bool {
251 let PiiConfig {
254 rules,
255 vars,
256 applications,
257 compiled: _compiled,
258 } = &self;
259
260 rules == &other.rules && vars == &other.vars && applications == &other.applications
261 }
262}
263
264impl PiiConfig {
265 pub fn compiled(&self) -> &CompiledPiiConfig {
270 self.compiled.get_or_init(|| self.compiled_uncached())
271 }
272
273 #[inline]
275 pub fn compiled_uncached(&self) -> CompiledPiiConfig {
276 CompiledPiiConfig::new(self)
277 }
278}