aws_smithy_http_server/instrumentation/sensitivity/uri/
label.rs1use std::fmt::{Debug, Display, Error, Formatter};
9
10use crate::instrumentation::{sensitivity::Sensitive, MakeFmt};
11
12#[allow(missing_debug_implementations)]
31#[derive(Clone)]
32pub struct Label<'a, F> {
33 path: &'a str,
34 label_marker: F,
35 greedy_label: Option<GreedyLabel>,
36}
37
38#[derive(Clone, Debug)]
49pub struct GreedyLabel {
50 segment_index: usize,
51 end_offset: usize,
52}
53
54impl GreedyLabel {
55 pub fn new(segment_index: usize, end_offset: usize) -> Self {
57 Self {
58 segment_index,
59 end_offset,
60 }
61 }
62}
63
64impl<'a, F> Label<'a, F> {
65 pub fn new(path: &'a str, label_marker: F, greedy_label: Option<GreedyLabel>) -> Self {
67 Self {
68 path,
69 label_marker,
70 greedy_label,
71 }
72 }
73}
74
75impl<'a, F> Display for Label<'a, F>
76where
77 F: Fn(usize) -> bool,
78{
79 #[inline]
80 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
81 if let Some(greedy_label) = &self.greedy_label {
82 #[allow(clippy::manual_try_fold)]
86 let (greedy_start, greedy_hit) = self
87 .path
88 .split('/')
89 .skip(1)
91 .take(greedy_label.segment_index + 1)
93 .enumerate()
94 .fold(Ok((0, false)), |acc, (index, segment)| {
95 acc.and_then(|(greedy_start, _)| {
96 if index == greedy_label.segment_index {
97 Ok((greedy_start, true))
99 } else {
100 if (self.label_marker)(index) {
102 write!(f, "/{}", Sensitive(segment))?;
103 } else {
104 write!(f, "/{}", segment)?;
105 }
106 let greedy_start = greedy_start + segment.len() + 1;
108 Ok((greedy_start, false))
109 }
110 })
111 })?;
112
113 if greedy_hit {
116 if let Some(end_index) = self.path.len().checked_sub(greedy_label.end_offset) {
117 if greedy_start < end_index {
118 let greedy_redaction = Sensitive(&self.path[greedy_start + 1..end_index]);
120 let remainder = &self.path[end_index..];
121 write!(f, "/{greedy_redaction}{remainder}")?;
122 } else {
123 write!(f, "{}", &self.path[greedy_start..])?;
126 }
127 }
128 } else {
129 }
131 } else {
132 for (index, segment) in self
134 .path
135 .split('/')
136 .skip(1)
138 .enumerate()
139 {
140 if (self.label_marker)(index) {
141 write!(f, "/{}", Sensitive(segment))?;
142 } else {
143 write!(f, "/{}", segment)?;
144 }
145 }
146 }
147
148 Ok(())
149 }
150}
151
152#[derive(Clone)]
154pub struct MakeLabel<F> {
155 pub(crate) label_marker: F,
156 pub(crate) greedy_label: Option<GreedyLabel>,
157}
158
159impl<F> Debug for MakeLabel<F> {
160 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
161 f.debug_struct("MakeLabel")
162 .field("greedy_label", &self.greedy_label)
163 .finish_non_exhaustive()
164 }
165}
166
167impl<'a, F> MakeFmt<&'a str> for MakeLabel<F>
168where
169 F: Clone,
170{
171 type Target = Label<'a, F>;
172
173 fn make(&self, path: &'a str) -> Self::Target {
174 Label::new(path, self.label_marker.clone(), self.greedy_label.clone())
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use http::Uri;
181
182 use crate::instrumentation::sensitivity::uri::{tests::EXAMPLES, GreedyLabel};
183
184 use super::Label;
185
186 #[test]
187 fn mark_none() {
188 let originals = EXAMPLES.into_iter().map(Uri::from_static);
189 for original in originals {
190 let expected = original.path().to_string();
191 let output = Label::new(original.path(), |_| false, None).to_string();
192 assert_eq!(output, expected, "original = {original}");
193 }
194 }
195
196 #[cfg(not(feature = "unredacted-logging"))]
197 const ALL_EXAMPLES: [&str; 19] = [
198 "g:h",
199 "http://a/{redacted}/{redacted}/{redacted}",
200 "http://a/{redacted}/{redacted}/{redacted}/{redacted}",
201 "http://a/{redacted}",
202 "http://a/{redacted}",
203 "http://a/{redacted}/{redacted}/{redacted}",
204 "http://a/{redacted}/{redacted}/{redacted}",
205 "http://a/{redacted}/{redacted}/{redacted}",
206 "http://a/{redacted}/{redacted}/{redacted}",
207 "http://a/{redacted}/{redacted}/{redacted}",
208 "http://a/{redacted}/{redacted}/{redacted}",
209 "http://a/{redacted}/{redacted}/{redacted}",
210 "http://a/{redacted}/{redacted}/{redacted}",
211 "http://a/{redacted}/{redacted}/{redacted}",
212 "http://a/{redacted}/{redacted}/{redacted}",
213 "http://a/{redacted}/{redacted}/{redacted}",
214 "http://a/{redacted}/{redacted}",
215 "http://a/{redacted}/{redacted}",
216 "http://a/{redacted}",
217 ];
218
219 #[cfg(feature = "unredacted-logging")]
220 pub const ALL_EXAMPLES: [&str; 19] = EXAMPLES;
221
222 #[test]
223 fn mark_all() {
224 let originals = EXAMPLES.into_iter().map(Uri::from_static);
225 let expecteds = ALL_EXAMPLES.into_iter().map(Uri::from_static);
226 for (original, expected) in originals.zip(expecteds) {
227 let output = Label::new(original.path(), |_| true, None).to_string();
228 assert_eq!(output, expected.path(), "original = {original}");
229 }
230 }
231
232 #[cfg(not(feature = "unredacted-logging"))]
233 pub const GREEDY_EXAMPLES: [&str; 19] = [
234 "g:h",
235 "http://a/b/{redacted}",
236 "http://a/b/{redacted}",
237 "http://a/g",
238 "http://g",
239 "http://a/b/{redacted}?y",
240 "http://a/b/{redacted}?y",
241 "http://a/b/{redacted}?q#s",
242 "http://a/b/{redacted}",
243 "http://a/b/{redacted}?y#s",
244 "http://a/b/{redacted}",
245 "http://a/b/{redacted}",
246 "http://a/b/{redacted}?y#s",
247 "http://a/b/{redacted}?q",
248 "http://a/b/{redacted}",
249 "http://a/b/{redacted}",
250 "http://a/b/{redacted}",
251 "http://a/b/{redacted}",
252 "http://a/",
253 ];
254
255 #[cfg(feature = "unredacted-logging")]
256 pub const GREEDY_EXAMPLES: [&str; 19] = EXAMPLES;
257
258 #[test]
259 fn greedy() {
260 let originals = EXAMPLES.into_iter().map(Uri::from_static);
261 let expecteds = GREEDY_EXAMPLES.into_iter().map(Uri::from_static);
262 for (original, expected) in originals.zip(expecteds) {
263 let output = Label::new(original.path(), |_| false, Some(GreedyLabel::new(1, 0))).to_string();
264 assert_eq!(output, expected.path(), "original = {original}");
265 }
266 }
267
268 #[cfg(not(feature = "unredacted-logging"))]
269 pub const GREEDY_EXAMPLES_OFFSET: [&str; 19] = [
270 "g:h",
271 "http://a/b/{redacted}g",
272 "http://a/b/{redacted}/",
273 "http://a/g",
274 "http://g",
275 "http://a/b/{redacted}p?y",
276 "http://a/b/{redacted}g?y",
277 "http://a/b/{redacted}p?q#s",
278 "http://a/b/{redacted}g",
279 "http://a/b/{redacted}g?y#s",
280 "http://a/b/{redacted}x",
281 "http://a/b/{redacted}x",
282 "http://a/b/{redacted}x?y#s",
283 "http://a/b/{redacted}p?q",
284 "http://a/b/{redacted}/",
285 "http://a/b/{redacted}/",
286 "http://a/b/",
287 "http://a/b/{redacted}g",
288 "http://a/",
289 ];
290
291 #[cfg(feature = "unredacted-logging")]
292 pub const GREEDY_EXAMPLES_OFFSET: [&str; 19] = EXAMPLES;
293
294 #[test]
295 fn greedy_offset_a() {
296 let originals = EXAMPLES.into_iter().map(Uri::from_static);
297 let expecteds = GREEDY_EXAMPLES_OFFSET.into_iter().map(Uri::from_static);
298 for (original, expected) in originals.zip(expecteds) {
299 let output = Label::new(original.path(), |_| false, Some(GreedyLabel::new(1, 1))).to_string();
300 assert_eq!(output, expected.path(), "original = {original}");
301 }
302 }
303
304 const EXTRA_EXAMPLES_UNREDACTED: [&str; 4] = [
305 "http://base/a/b/hello_world",
306 "http://base/a/b/c/hello_world",
307 "http://base/a",
308 "http://base/a/b/c",
309 ];
310
311 #[cfg(feature = "unredacted-logging")]
312 const EXTRA_EXAMPLES_REDACTED: [&str; 4] = EXTRA_EXAMPLES_UNREDACTED;
313 #[cfg(not(feature = "unredacted-logging"))]
314 const EXTRA_EXAMPLES_REDACTED: [&str; 4] = [
315 "http://base/a/b/{redacted}world",
316 "http://base/a/b/{redacted}world",
317 "http://base/a",
318 "http://base/a/b/c",
319 ];
320
321 #[test]
322 fn greedy_offset_b() {
323 let originals = EXTRA_EXAMPLES_UNREDACTED.into_iter().map(Uri::from_static);
324 let expecteds = EXTRA_EXAMPLES_REDACTED.into_iter().map(Uri::from_static);
325 for (original, expected) in originals.zip(expecteds) {
326 let output = Label::new(original.path(), |_| false, Some(GreedyLabel::new(2, 5))).to_string();
327 assert_eq!(output, expected.path(), "original = {original}");
328 }
329 }
330}