iri_string/
mask_password.rs

1//! Password masker.
2
3use core::fmt::{self, Write as _};
4use core::ops::Range;
5
6#[cfg(all(feature = "alloc", not(feature = "std")))]
7use alloc::borrow::ToOwned;
8#[cfg(feature = "alloc")]
9use alloc::collections::TryReserveError;
10#[cfg(all(feature = "alloc", not(feature = "std")))]
11use alloc::string::String;
12
13use crate::components::AuthorityComponents;
14#[cfg(feature = "alloc")]
15use crate::format::ToDedicatedString;
16use crate::spec::Spec;
17use crate::types::{RiAbsoluteStr, RiReferenceStr, RiRelativeStr, RiStr};
18#[cfg(feature = "alloc")]
19use crate::types::{RiAbsoluteString, RiReferenceString, RiRelativeString, RiString};
20
21/// Returns the range of the password to hide.
22pub(crate) fn password_range_to_hide<S: Spec>(iri: &RiReferenceStr<S>) -> Option<Range<usize>> {
23    /// Spec-agnostic internal implementation of `password_range_to_hide`.
24    fn inner(iri: &str, userinfo: &str) -> Option<Range<usize>> {
25        // Length (including `//`) before the `authority` compontent.
26        // 2: `"//".len()`.
27        let authority_start = 2 + iri
28            .find("//")
29            .expect("[validity] `authority` component must be prefixed with `//`");
30        let end = authority_start + userinfo.len();
31        let start = authority_start + userinfo.find(':').map_or_else(|| userinfo.len(), |v| v + 1);
32        Some(start..end)
33    }
34
35    let authority_components = AuthorityComponents::from_iri(iri)?;
36    let userinfo = authority_components.userinfo()?;
37    inner(iri.as_str(), userinfo)
38}
39
40/// Writes the URI with the password part replaced.
41fn write_with_masked_password<D>(
42    f: &mut fmt::Formatter<'_>,
43    s: &str,
44    pw_range: Range<usize>,
45    alt: &D,
46) -> fmt::Result
47where
48    D: ?Sized + fmt::Display,
49{
50    debug_assert!(
51        s.len() >= pw_range.end,
52        "[consistency] password range must be inside the IRI"
53    );
54
55    f.write_str(&s[..pw_range.start])?;
56    alt.fmt(f)?;
57    f.write_str(&s[pw_range.end..])?;
58    Ok(())
59}
60
61/// Writes an IRI with the password part trimmed.
62fn write_trim_password(f: &mut fmt::Formatter<'_>, s: &str, pw_range: Range<usize>) -> fmt::Result {
63    write_with_masked_password(f, s, pw_range, "")
64}
65
66/// A wrapper of an IRI string that masks the non-empty password when `Display`ed.
67///
68/// This is a retrun type of `mask_password` method of IRI string types (such as
69/// [`RiStr::mask_password`]).
70///
71/// # Examples
72///
73/// ```
74/// # use iri_string::validate::Error;
75/// # #[cfg(feature = "alloc")] {
76/// use iri_string::types::UriReferenceStr;
77///
78/// let iri = UriReferenceStr::new("http://user:password@example.com/path?query")?;
79/// let masked = iri.mask_password();
80/// assert_eq!(masked.to_string(), "http://user:@example.com/path?query");
81///
82/// assert_eq!(
83///     masked.replace_password("${password}").to_string(),
84///     "http://user:${password}@example.com/path?query"
85/// );
86/// # }
87/// # Ok::<_, Error>(())
88/// ```
89///
90/// [`RiStr::mask_password`]: `crate::types::RiStr::mask_password`
91#[derive(Clone, Copy)]
92pub struct PasswordMasked<'a, T: ?Sized> {
93    /// IRI reference.
94    iri_ref: &'a T,
95}
96
97impl<'a, T: ?Sized> PasswordMasked<'a, T> {
98    /// Creates a new `PasswordMasked` object.
99    #[inline]
100    #[must_use]
101    pub(crate) fn new(iri_ref: &'a T) -> Self {
102        Self { iri_ref }
103    }
104}
105
106/// Implements traits for `PasswordMasked`.
107macro_rules! impl_mask {
108    ($borrowed:ident, $owned:ident) => {
109        impl<'a, S: Spec> PasswordMasked<'a, $borrowed<S>> {
110            /// Replaces the password with the given arbitrary content.
111            ///
112            /// Note that the result might be invalid as an IRI since arbitrary string
113            /// can go to the place of the password.
114            ///
115            /// # Examples
116            ///
117            /// ```
118            /// # use iri_string::validate::Error;
119            /// # #[cfg(feature = "alloc")] {
120            /// use iri_string::format::ToDedicatedString;
121            /// use iri_string::types::IriReferenceStr;
122            ///
123            /// let iri = IriReferenceStr::new("http://user:password@example.com/path?query")?;
124            /// let masked = iri.mask_password();
125            ///
126            /// assert_eq!(
127            ///     masked.replace_password("${password}").to_string(),
128            ///     "http://user:${password}@example.com/path?query"
129            /// );
130            /// # }
131            /// # Ok::<_, Error>(())
132            /// ```
133            #[inline]
134            #[must_use]
135            pub fn replace_password<D>(&self, alt: D) -> PasswordReplaced<'a, $borrowed<S>, D>
136            where
137                D: fmt::Display,
138            {
139                PasswordReplaced::with_replacer(self.iri_ref, move |_| alt)
140            }
141
142            /// Replaces the password with the given arbitrary content.
143            ///
144            /// Note that the result might be invalid as an IRI since arbitrary string
145            /// can go to the place of the password.
146            ///
147            /// # Examples
148            ///
149            /// ```
150            /// # use iri_string::validate::Error;
151            /// # #[cfg(feature = "alloc")] {
152            /// use iri_string::format::ToDedicatedString;
153            /// use iri_string::types::IriReferenceStr;
154            ///
155            /// let iri = IriReferenceStr::new("http://user:password@example.com/path?query")?;
156            /// let masked = iri.mask_password();
157            ///
158            /// let replaced = masked
159            ///     .replace_password_with(|password| format!("{{{} chars}}", password.len()));
160            /// assert_eq!(
161            ///     replaced.to_string(),
162            ///     "http://user:{8 chars}@example.com/path?query"
163            /// );
164            /// # }
165            /// # Ok::<_, Error>(())
166            /// ```
167            #[inline]
168            #[must_use]
169            pub fn replace_password_with<F, D>(
170                &self,
171                replace: F,
172            ) -> PasswordReplaced<'a, $borrowed<S>, D>
173            where
174                F: FnOnce(&str) -> D,
175                D: fmt::Display,
176            {
177                PasswordReplaced::with_replacer(self.iri_ref, replace)
178            }
179        }
180
181        impl<S: Spec> fmt::Display for PasswordMasked<'_, $borrowed<S>> {
182            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183                match password_range_to_hide(self.iri_ref.as_ref()) {
184                    Some(pw_range) => write_trim_password(f, self.iri_ref.as_str(), pw_range),
185                    None => self.iri_ref.fmt(f),
186                }
187            }
188        }
189
190        impl<S: Spec> fmt::Debug for PasswordMasked<'_, $borrowed<S>> {
191            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
192                f.write_char('<')?;
193                fmt::Display::fmt(self, f)?;
194                f.write_char('>')
195            }
196        }
197
198        #[cfg(feature = "alloc")]
199        impl<S: Spec> ToDedicatedString for PasswordMasked<'_, $borrowed<S>> {
200            type Target = $owned<S>;
201
202            fn try_to_dedicated_string(&self) -> Result<Self::Target, TryReserveError> {
203                let pw_range = match password_range_to_hide(self.iri_ref.as_ref()) {
204                    Some(pw_range) => pw_range,
205                    None => return Ok(self.iri_ref.to_owned()),
206                };
207                let mut s = String::new();
208                let iri_ref = self.iri_ref.as_str();
209                s.try_reserve(iri_ref.len() - (pw_range.end - pw_range.start))?;
210                s.push_str(&iri_ref[..pw_range.start]);
211                s.push_str(&iri_ref[pw_range.end..]);
212                // SAFETY: IRI remains valid and type does not change if
213                // the password is trimmed.
214                let iri = unsafe { <$owned<S>>::new_maybe_unchecked(s) };
215                Ok(iri)
216            }
217        }
218    };
219}
220
221impl_mask!(RiReferenceStr, RiReferenceString);
222impl_mask!(RiStr, RiString);
223impl_mask!(RiAbsoluteStr, RiAbsoluteString);
224impl_mask!(RiRelativeStr, RiRelativeString);
225
226/// A wrapper of an IRI string that replaces the non-empty password when `Display`ed.
227///
228/// This is a retrun type of `mask_password` method of IRI string types (such as
229/// [`RiStr::mask_password`]).
230///
231/// Note that the result might be invalid as an IRI since arbitrary string can
232/// go to the place of the password.
233#[cfg_attr(
234    feature = "alloc",
235    doc = "Because of this, [`ToDedicatedString`] trait is not implemented for this type."
236)]
237///
238/// [`PasswordMasked::replace_password`]: `PasswordMasked::replace_password`
239pub struct PasswordReplaced<'a, T: ?Sized, D> {
240    /// IRI reference.
241    iri_ref: &'a T,
242    /// Password range and alternative content.
243    password: Option<(Range<usize>, D)>,
244}
245
246impl<'a, T, D> PasswordReplaced<'a, T, D>
247where
248    T: ?Sized,
249    D: fmt::Display,
250{
251    /// Creates a new `PasswordMasked` object.
252    ///
253    /// # Precondition
254    ///
255    /// The given string must be a valid IRI reference.
256    #[inline]
257    #[must_use]
258    pub(crate) fn with_replacer<S, F>(iri_ref: &'a T, replace: F) -> Self
259    where
260        S: Spec,
261        T: AsRef<RiReferenceStr<S>>,
262        F: FnOnce(&str) -> D,
263    {
264        let iri_ref_asref = iri_ref.as_ref();
265        let password = password_range_to_hide(iri_ref_asref)
266            .map(move |pw_range| (pw_range.clone(), replace(&iri_ref_asref.as_str()[pw_range])));
267        Self { iri_ref, password }
268    }
269}
270
271/// Implements traits for `PasswordReplaced`.
272macro_rules! impl_replace {
273    ($borrowed:ident, $owned:ident) => {
274        impl<S: Spec, D: fmt::Display> fmt::Display for PasswordReplaced<'_, $borrowed<S>, D> {
275            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
276                match &self.password {
277                    Some((pw_range, alt)) => {
278                        write_with_masked_password(f, self.iri_ref.as_str(), pw_range.clone(), alt)
279                    }
280                    None => self.iri_ref.fmt(f),
281                }
282            }
283        }
284
285        impl<S: Spec, D: fmt::Display> fmt::Debug for PasswordReplaced<'_, $borrowed<S>, D> {
286            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
287                f.write_char('<')?;
288                fmt::Display::fmt(self, f)?;
289                f.write_char('>')
290            }
291        }
292    };
293}
294
295impl_replace!(RiReferenceStr, RiReferenceString);
296impl_replace!(RiStr, RiString);
297impl_replace!(RiAbsoluteStr, RiAbsoluteString);
298impl_replace!(RiRelativeStr, RiRelativeString);