relay_event_schema/processor/
chunks.rs1use std::borrow::Cow;
26use std::fmt;
27
28use relay_protocol::{Meta, Remark, RemarkType};
29use serde::{Deserialize, Serialize};
30
31#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
33#[serde(tag = "type", rename_all = "lowercase")]
34pub enum Chunk<'a> {
35 Text {
37 text: Cow<'a, str>,
39 },
40 Redaction {
42 text: Cow<'a, str>,
44 rule_id: Cow<'a, str>,
46 #[serde(rename = "remark")]
48 ty: RemarkType,
49 },
50}
51
52impl Chunk<'_> {
53 pub fn as_str(&self) -> &str {
55 match self {
56 Chunk::Text { text } => text,
57 Chunk::Redaction { text, .. } => text,
58 }
59 }
60
61 pub fn len(&self) -> usize {
63 self.as_str().len()
64 }
65
66 pub fn count(&self) -> usize {
68 bytecount::num_chars(self.as_str().as_bytes())
69 }
70
71 pub fn is_empty(&self) -> bool {
73 self.len() == 0
74 }
75}
76
77impl fmt::Display for Chunk<'_> {
78 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79 write!(f, "{}", self.as_str())
80 }
81}
82
83pub fn split_chunks<'a, I>(text: &'a str, remarks: I) -> Vec<Chunk<'a>>
85where
86 I: IntoIterator<Item = &'a Remark>,
87{
88 let mut rv = vec![];
89 let mut pos = 0;
90
91 for remark in remarks {
92 let Some((from, to)) = remark.range().cloned() else {
93 continue;
94 };
95
96 if from > pos {
97 if let Some(piece) = text.get(pos..from) {
98 rv.push(Chunk::Text {
99 text: Cow::Borrowed(piece),
100 });
101 } else {
102 break;
103 }
104 pos = from;
105 } else if from < pos {
106 break;
108 }
109 if let Some(piece) = text.get(from..to) {
110 rv.push(Chunk::Redaction {
111 text: Cow::Borrowed(piece),
112 rule_id: remark.rule_id().into(),
113 ty: remark.ty(),
114 });
115 } else {
116 break;
117 }
118 pos = to;
119 }
120
121 if pos < text.len()
122 && let Some(piece) = text.get(pos..)
123 {
124 rv.push(Chunk::Text {
125 text: Cow::Borrowed(piece),
126 });
127 }
128
129 rv
130}
131
132pub fn join_chunks<'a, I>(chunks: I) -> (String, Vec<Remark>)
134where
135 I: IntoIterator<Item = Chunk<'a>>,
136{
137 let mut rv = String::new();
138 let mut remarks = vec![];
139 let mut pos = 0;
140
141 for chunk in chunks {
142 let new_pos = pos + chunk.len();
143 rv.push_str(chunk.as_str());
144
145 match chunk {
146 Chunk::Redaction { rule_id, ty, .. } => {
147 remarks.push(Remark::with_range(ty, rule_id.clone(), (pos, new_pos)))
148 }
149 Chunk::Text { .. } => {
150 }
152 }
153
154 pos = new_pos;
155 }
156
157 (rv, remarks)
158}
159
160pub fn process_chunked_value<F>(value: &mut String, meta: &mut Meta, f: F)
163where
164 F: FnOnce(Vec<Chunk>) -> Vec<Chunk>,
165{
166 let chunks = split_chunks(value, meta.iter_remarks());
167 let (new_value, remarks) = join_chunks(f(chunks));
168
169 if new_value != *value {
170 meta.clear_remarks();
171 for remark in remarks.into_iter() {
172 meta.add_remark(remark);
173 }
174 meta.set_original_length(Some(bytecount::num_chars(value.as_bytes())));
175 *value = new_value;
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use similar_asserts::assert_eq;
182
183 use super::*;
184
185 #[test]
186 fn test_chunk_split() {
187 let remarks = vec![Remark::with_range(
188 RemarkType::Masked,
189 "@email:strip",
190 (33, 47),
191 )];
192
193 let text = "Hello Peter, my email address is ****@*****.com. See you";
194
195 let chunks = vec![
196 Chunk::Text {
197 text: "Hello Peter, my email address is ".into(),
198 },
199 Chunk::Redaction {
200 ty: RemarkType::Masked,
201 text: "****@*****.com".into(),
202 rule_id: "@email:strip".into(),
203 },
204 Chunk::Text {
205 text: ". See you".into(),
206 },
207 ];
208
209 assert_eq!(split_chunks(text, &remarks), chunks);
210 assert_eq!(join_chunks(chunks), (text.into(), remarks));
211 }
212}