mod error;
mod spec;
use spec::*;
pub mod provider;
pub use error::GitUrlParseError;
use core::str;
use std::fmt;
use url::Url;
use getset::{CopyGetters, Getters, Setters};
#[cfg(feature = "log")]
use log::debug;
use nom::Finish;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub(crate) enum GitUrlParseHint {
#[default]
Unknown,
Sshlike,
Filelike,
Httplike,
}
#[derive(Clone, CopyGetters, Getters, Debug, Default, Setters, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[getset(set = "pub(crate)")]
pub struct GitUrl {
scheme: Option<String>,
user: Option<String>,
password: Option<String>,
host: Option<String>,
#[getset(get_copy = "pub")]
port: Option<u16>,
path: String,
#[getset(get_copy = "pub")]
print_scheme: bool,
#[getset(get_copy = "pub")]
hint: GitUrlParseHint,
}
impl GitUrl {
pub fn scheme(&self) -> Option<&str> {
if let Some(s) = &self.scheme {
Some(&s[..])
} else {
None
}
}
pub fn user(&self) -> Option<&str> {
if let Some(u) = &self.user {
Some(&u[..])
} else {
None
}
}
pub fn password(&self) -> Option<&str> {
if let Some(p) = &self.password {
Some(&p[..])
} else {
None
}
}
pub fn host(&self) -> Option<&str> {
if let Some(h) = &self.host {
Some(&h[..])
} else {
None
}
}
pub fn path(&self) -> &str {
&self.path[..]
}
fn display(&self) -> String {
self.build_string(false)
}
#[cfg(feature = "url")]
fn url_compat_display(&self) -> String {
self.build_string(true)
}
fn build_string(&self, url_compat: bool) -> String {
let scheme = if self.print_scheme() || url_compat {
if let Some(scheme) = self.scheme() {
format!("{scheme}://")
} else {
String::new()
}
} else {
String::new()
};
let auth_info = match (self.user(), self.password()) {
(Some(user), Some(password)) => format!("{user}:{password}@"),
(Some(user), None) => format!("{user}@",),
(None, Some(password)) => format!("{password}@"),
(None, None) => String::new(),
};
let host = match &self.host() {
Some(host) => host.to_string(),
None => String::new(),
};
let (port, path) = match (self.hint(), self.port(), self.path()) {
(GitUrlParseHint::Httplike, Some(port), path) => {
(format!(":{port}"), format!("/{path}"))
}
(GitUrlParseHint::Httplike, None, path) => (String::new(), path.to_string()),
(GitUrlParseHint::Sshlike, Some(port), path) => {
(format!(":{port}"), format!("/{path}"))
}
(GitUrlParseHint::Sshlike, None, path) => {
if url_compat {
(String::new(), format!("/{path}"))
} else {
(String::new(), format!(":{path}"))
}
}
(GitUrlParseHint::Filelike, None, path) => (String::new(), path.to_string()),
_ => (String::new(), String::new()),
};
let git_url_str = format!("{scheme}{auth_info}{host}{port}{path}");
git_url_str
}
pub fn trim_auth(&self) -> GitUrl {
let mut new_giturl = self.clone();
new_giturl.set_user(None);
new_giturl.set_password(None);
#[cfg(feature = "log")]
debug!("{new_giturl:?}");
new_giturl
}
pub fn parse(input: &str) -> Result<Self, GitUrlParseError> {
let git_url = Self::parse_to_git_url(input)?;
git_url.is_valid()?;
Ok(git_url)
}
fn parse_to_git_url(input: &str) -> Result<Self, GitUrlParseError> {
let mut git_url_result = GitUrl::default();
if input.contains('\0') {
return Err(GitUrlParseError::FoundNullBytes);
}
let (_input, url_spec_parser) = UrlSpecParser::parse(input).finish().unwrap_or_default();
let scheme = url_spec_parser.scheme();
let user = url_spec_parser.hier_part().authority().userinfo().user();
let password = url_spec_parser.hier_part().authority().userinfo().token();
let host = url_spec_parser.hier_part().authority().host();
let port = url_spec_parser.hier_part().authority().port();
let path = url_spec_parser.hier_part().path();
git_url_result.set_scheme(scheme.clone());
git_url_result.set_user(user.clone());
git_url_result.set_password(password.clone());
git_url_result.set_host(host.clone());
git_url_result.set_port(*port);
git_url_result.set_path(path.clone());
let print_scheme = scheme.is_some();
let hint = if let Some(scheme) = scheme.as_ref() {
if scheme.contains("ssh") {
GitUrlParseHint::Sshlike
} else {
match scheme.to_lowercase().as_str() {
"file" => GitUrlParseHint::Filelike,
_ => GitUrlParseHint::Httplike,
}
}
} else if user.is_none()
&& password.is_none()
&& host.is_none()
&& port.is_none()
&& !path.is_empty()
{
GitUrlParseHint::Filelike
} else if user.is_some() && password.is_some() {
GitUrlParseHint::Httplike
} else if path.starts_with(':') {
GitUrlParseHint::Sshlike
} else {
GitUrlParseHint::Unknown
};
if hint == GitUrlParseHint::Sshlike {
git_url_result.set_scheme(Some("ssh".to_string()));
git_url_result.set_path(path[1..].to_string());
}
if hint == GitUrlParseHint::Filelike {
git_url_result.set_scheme(Some("file".to_string()));
}
git_url_result.set_print_scheme(print_scheme);
git_url_result.set_hint(hint);
git_url_result.is_valid()?;
Ok(git_url_result)
}
#[cfg(feature = "url")]
pub fn parse_to_url(input: &str) -> Result<Url, GitUrlParseError> {
let git_url = Self::parse_to_git_url(input)?;
Ok(Url::try_from(git_url)?)
}
pub fn provider_info<T>(&self) -> Result<T, GitUrlParseError>
where
T: provider::GitProvider<GitUrl, GitUrlParseError>,
{
T::from_git_url(self)
}
fn is_valid(&self) -> Result<(), GitUrlParseError> {
#[cfg(feature = "log")]
debug!("Validating parsing results {self:#?}");
if self.path().is_empty() {
return Err(GitUrlParseError::InvalidPathEmpty);
}
if self.hint() != GitUrlParseHint::Sshlike && self.path.starts_with(':') {
#[cfg(feature = "log")]
{
debug!("{:?}", self.hint());
debug!("{:?}", self.path());
debug!("Only sshlike url path starts with ':'");
debug!("path starts with ':'? {}", self.path.starts_with(':'));
}
return Err(GitUrlParseError::InvalidPortNumber);
}
if self.hint() != GitUrlParseHint::Httplike && self.password().is_some() {
#[cfg(feature = "log")]
{
debug!("{:?}", self.hint());
debug!(
"password support only for httplike url: {:?}",
self.password()
);
}
return Err(GitUrlParseError::InvalidPasswordUnsupported);
}
if self.hint() == GitUrlParseHint::Filelike
&& (self.user().is_some()
|| self.password().is_some()
|| self.host().is_some()
|| self.port().is_some()
|| self.path().is_empty())
{
#[cfg(feature = "log")]
{
debug!(
"Only scheme and path expected to have values set for filelike urls {:?}",
self
);
}
return Err(GitUrlParseError::InvalidFilePattern);
}
#[cfg(feature = "url")]
{
let _u: Url = self.try_into()?;
}
Ok(())
}
}
impl fmt::Display for GitUrl {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let git_url_str = self.display();
write!(f, "{git_url_str}",)
}
}
#[cfg(feature = "url")]
impl TryFrom<&GitUrl> for Url {
type Error = url::ParseError;
fn try_from(value: &GitUrl) -> Result<Self, Self::Error> {
Url::parse(&value.url_compat_display())
}
}
#[cfg(feature = "url")]
impl TryFrom<GitUrl> for Url {
type Error = url::ParseError;
fn try_from(value: GitUrl) -> Result<Self, Self::Error> {
Url::parse(&value.url_compat_display())
}
}
#[cfg(feature = "url")]
impl TryFrom<&Url> for GitUrl {
type Error = GitUrlParseError;
fn try_from(value: &Url) -> Result<Self, Self::Error> {
GitUrl::parse(value.as_str())
}
}
#[cfg(feature = "url")]
impl TryFrom<Url> for GitUrl {
type Error = GitUrlParseError;
fn try_from(value: Url) -> Result<Self, Self::Error> {
GitUrl::parse(value.as_str())
}
}