mz_ore/
url.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License in the LICENSE file at the
6// root of this repository, or online at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! URL utilities.
17
18use std::fmt;
19use std::ops::Deref;
20use std::str::FromStr;
21
22#[cfg(feature = "proptest")]
23use proptest::prelude::{Arbitrary, BoxedStrategy, Strategy};
24use serde::{Deserialize, Serialize};
25use url::Url;
26
27/// A URL that redacts its password when formatted.
28#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
29pub struct SensitiveUrl(pub Url);
30
31impl SensitiveUrl {
32    /// Converts into the underlying URL with the password redacted if it
33    /// exists.
34    pub fn into_redacted(mut self) -> Url {
35        if self.0.password().is_some() {
36            self.0.set_password(Some("<redacted>")).unwrap();
37        }
38        self.0
39    }
40
41    /// Formats as a string without redacting the password.
42    pub fn to_string_unredacted(&self) -> String {
43        self.0.to_string()
44    }
45}
46
47impl fmt::Display for SensitiveUrl {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        self.clone().into_redacted().fmt(f)
50    }
51}
52
53impl fmt::Debug for SensitiveUrl {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        self.clone().into_redacted().fmt(f)
56    }
57}
58
59impl FromStr for SensitiveUrl {
60    type Err = url::ParseError;
61
62    fn from_str(s: &str) -> Result<Self, Self::Err> {
63        Ok(Self(Url::from_str(s)?))
64    }
65}
66
67impl Deref for SensitiveUrl {
68    type Target = Url;
69
70    fn deref(&self) -> &Self::Target {
71        &self.0
72    }
73}
74
75#[cfg(feature = "cli")]
76#[derive(Clone, Debug)]
77/// clap parser for SensitiveUrl
78pub struct SensitiveUrlParser;
79
80#[cfg(feature = "cli")]
81impl clap::builder::TypedValueParser for SensitiveUrlParser {
82    type Value = SensitiveUrl;
83
84    fn parse_ref(
85        &self,
86        cmd: &clap::Command,
87        arg: Option<&clap::Arg>,
88        value: &std::ffi::OsStr,
89    ) -> Result<Self::Value, clap::Error> {
90        let s = value
91            .to_str()
92            .ok_or_else(|| clap::Error::new(clap::error::ErrorKind::InvalidUtf8).with_cmd(cmd))?;
93        let url = s.parse().map_err(|_e| {
94            let mut err = clap::Error::new(clap::error::ErrorKind::ValueValidation);
95            if let Some(arg) = arg {
96                err.insert(
97                    clap::error::ContextKind::InvalidArg,
98                    clap::error::ContextValue::String(arg.to_string()),
99                );
100            }
101            err.insert(
102                clap::error::ContextKind::InvalidValue,
103                clap::error::ContextValue::String("<redacted>".to_string()),
104            );
105            // TODO: waiting on https://github.com/clap-rs/clap/issues/5065
106            // to be resolved
107            // err.set_source(e);
108            err
109        })?;
110        Ok(SensitiveUrl(url))
111    }
112}
113
114#[cfg(feature = "cli")]
115impl clap::builder::ValueParserFactory for SensitiveUrl {
116    type Parser = SensitiveUrlParser;
117
118    fn value_parser() -> Self::Parser {
119        SensitiveUrlParser
120    }
121}
122
123#[cfg(feature = "proptest")]
124impl Arbitrary for SensitiveUrl {
125    type Parameters = ();
126    type Strategy = BoxedStrategy<Self>;
127
128    fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
129        proptest::sample::select(vec![
130            SensitiveUrl::from_str("http://user:pass@example.com").unwrap(),
131            SensitiveUrl::from_str("http://user@example.com").unwrap(),
132            SensitiveUrl::from_str("http://example.com").unwrap(),
133        ])
134        .boxed()
135    }
136}