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<F> Display for Label<'_, 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}