| use std::fmt; |
| use std::ops::Deref; |
|
|
| use gix_url::Scheme; |
| use gix_url::Url; |
| use schemars::JsonSchema; |
| use serde::Deserialize; |
| use serde::Serialize; |
| use ts_rs::TS; |
|
|
| |
| #[derive( |
| Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema, TS, |
| )] |
| #[serde(try_from = "String", into = "String")] |
| #[schemars(with = "String")] |
| #[ts(type = "string")] |
| pub struct SanitizedGitUrl(String); |
|
|
| impl SanitizedGitUrl { |
| pub fn as_str(&self) -> &str { |
| self.0.as_str() |
| } |
| } |
|
|
| impl TryFrom<&str> for SanitizedGitUrl { |
| type Error = &'static str; |
|
|
| fn try_from(value: &str) -> Result<Self, Self::Error> { |
| |
| |
| let mut address = value; |
| while let Some((transport, nested_address)) = address.split_once("::") |
| && !transport.is_empty() |
| && transport.chars().all(|character| { |
| character.is_ascii_alphanumeric() || matches!(character, '+' | '-' | '.') |
| }) |
| { |
| |
| |
| if address.len() == value.len() && nested_address.chars().any(char::is_whitespace) { |
| return Err("invalid git remote URL"); |
| } |
| address = nested_address; |
| } |
| let helper_prefix = &value[..value.len() - address.len()]; |
| let original = value; |
| let value = address; |
|
|
| |
| |
| let url = match Url::try_from(value) { |
| Ok(url) => url, |
| Err(_) => { |
| |
| |
| let (user, host_and_path) = |
| value.split_once('@').ok_or("invalid git remote URL")?; |
| let (host, path) = host_and_path |
| .strip_prefix('[') |
| .and_then(|host_and_path| host_and_path.split_once("]:")) |
| .ok_or("invalid git remote URL")?; |
| if user.is_empty() || path.is_empty() { |
| return Err("invalid git remote URL"); |
| } |
| |
| let ssh_url = format!("ssh://{user}@[{host}]/"); |
| Url::try_from(ssh_url.as_str()).map_err(|_| "invalid git remote URL")? |
| } |
| }; |
|
|
| |
| |
| let preserve_ssh_git_user = url.scheme == Scheme::Ssh && url.user() == Some("git"); |
|
|
| |
| |
| |
| if let Some((scheme, authority_and_path)) = value.split_once("://") { |
| let authority_end = authority_and_path |
| .find('/') |
| .unwrap_or(authority_and_path.len()); |
| let (authority, path) = authority_and_path.split_at(authority_end); |
| let Some((_, host)) = authority.rsplit_once('@') else { |
| return Ok(Self(original.to_owned())); |
| }; |
| if preserve_ssh_git_user && url.password().is_none() { |
| return Ok(Self(original.to_owned())); |
| } |
| let user = if preserve_ssh_git_user { "git@" } else { "" }; |
| return Ok(Self(format!( |
| "{helper_prefix}{scheme}://{user}{host}{path}" |
| ))); |
| } |
|
|
| |
| |
| if url.password().is_none() && (url.user().is_none() || preserve_ssh_git_user) { |
| return Ok(Self(original.to_owned())); |
| } |
|
|
| |
| |
| let authority_end = value.find(':').unwrap_or(value.len()); |
| let user_end = value[..authority_end] |
| .rfind('@') |
| .ok_or("invalid git remote URL")?; |
| Ok(Self(format!("{helper_prefix}{}", &value[user_end + 1..]))) |
| } |
| } |
|
|
| impl TryFrom<String> for SanitizedGitUrl { |
| type Error = &'static str; |
|
|
| fn try_from(value: String) -> Result<Self, Self::Error> { |
| Self::try_from(value.as_str()) |
| } |
| } |
|
|
| impl From<SanitizedGitUrl> for String { |
| fn from(value: SanitizedGitUrl) -> Self { |
| value.0 |
| } |
| } |
|
|
| impl AsRef<str> for SanitizedGitUrl { |
| fn as_ref(&self) -> &str { |
| self.as_str() |
| } |
| } |
|
|
| impl Deref for SanitizedGitUrl { |
| type Target = str; |
|
|
| fn deref(&self) -> &Self::Target { |
| self.as_str() |
| } |
| } |
|
|
| impl fmt::Display for SanitizedGitUrl { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| f.write_str(self.as_str()) |
| } |
| } |
|
|
| pub(crate) fn deserialize_optional_sanitized_git_url<'de, D>( |
| deserializer: D, |
| ) -> Result<Option<SanitizedGitUrl>, D::Error> |
| where |
| D: serde::Deserializer<'de>, |
| { |
| |
| Ok(Option::<String>::deserialize(deserializer)? |
| .and_then(|value| SanitizedGitUrl::try_from(value).ok())) |
| } |
|
|
| #[cfg(test)] |
| #[path = "sanitized_git_url_tests.rs"] |
| mod tests; |
|
|