Skip to content

Commit ebca00a

Browse files
committed
feat: add std::fmt::Display to Url type.
That way it's possible to have a more safe display implementation that tries to not spill obvious secrets, like the password of a URL.
1 parent e3c5a0f commit ebca00a

File tree

3 files changed

+42
-4
lines changed

3 files changed

+42
-4
lines changed

gix-url/src/impls.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,17 @@ impl<'a> TryFrom<std::borrow::Cow<'a, BStr>> for Url {
7878
Self::try_from(&*value)
7979
}
8080
}
81+
82+
impl std::fmt::Display for Url {
83+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84+
let mut storage;
85+
let to_print = if self.password.is_some() {
86+
storage = self.clone();
87+
storage.password = Some("<redacted>".into());
88+
&storage
89+
} else {
90+
self
91+
};
92+
to_print.to_bstring().fmt(f)
93+
}
94+
}

gix-url/src/lib.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,17 @@ pub fn expand_path(user: Option<&expand_path::ForUser>, path: &BStr) -> Result<P
5555

5656
/// A URL with support for specialized git related capabilities.
5757
///
58-
/// Additionally there is support for [deserialization](Url::from_bytes()) and serialization
59-
/// (_see the [`std::fmt::Display::fmt()`] implementation_).
58+
/// Additionally there is support for [deserialization](Url::from_bytes()) and [serialization](Url::to_bstring()).
6059
///
6160
/// # Security Warning
6261
///
63-
/// URLs may contain passwords and we serialize them when [formatting](std::fmt::Display) or
64-
/// [serializing losslessly](Url::to_bstring()).
62+
/// URLs may contain passwords and using standard [formatting](std::fmt::Display) will redact
63+
/// such password, whereas [lossless serialization](Url::to_bstring()) will contain all parts of the
64+
/// URL.
65+
/// **Beware that some URls still print secrets if they use them outside of the designated password fields.**
66+
///
67+
/// Also note that URLs that fail to parse are typically stored in [the resulting error](parse::Error) type
68+
/// and printed in full using its display implementation.
6569
#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
6670
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
6771
pub struct Url {

gix-url/tests/access/mod.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,23 @@ fn path_argument_safe() -> crate::Result {
7171
assert_eq!(url.path_argument_safe(), None);
7272
Ok(())
7373
}
74+
75+
#[test]
76+
fn display() {
77+
fn compare(input: &str, expected: &str, message: &str) {
78+
let url = gix_url::parse(input.into()).expect("input is valid url");
79+
assert_eq!(format!("{url}"), expected, "{message}");
80+
}
81+
82+
compare(
83+
"ssh://foo/-oProxyCommand=open$IFS-aCalculator",
84+
"ssh://foo/-oProxyCommand=open$IFS-aCalculator",
85+
"it round-trips with sane unicode and without password",
86+
);
87+
compare("/path/to/repo", "/path/to/repo", "same goes for simple paths");
88+
compare(
89+
"https://user:password@host/path",
90+
"https://user:<redacted>@host/path",
91+
"it visibly redacts passwords though",
92+
);
93+
}

0 commit comments

Comments
 (0)