1 use std
::{borrow::Cow, convert::Infallible}
;
4 use bstr
::{BStr, BString, ByteSlice}
;
8 /// The Error returned by [`parse()`]
9 #[derive(Debug, thiserror::Error)]
10 #[allow(missing_docs)]
12 #[error("Could not decode URL as UTF8")]
13 Utf8(#[from] std::str::Utf8Error),
15 Url(#[from] url::ParseError),
16 #[error("URLs need to specify the path to the repository")]
17 MissingResourceLocation
,
18 #[error("file URLs require an absolute or relative path to the repository")]
19 MissingRepositoryPath
,
20 #[error("\"{url}\" is not a valid local path")]
21 NotALocalFile { url: BString }
,
22 #[error("Relative URLs are not permitted: {url:?}")]
23 RelativeUrl { url: String }
,
26 impl From
<Infallible
> for Error
{
27 fn from(_
: Infallible
) -> Self {
28 unreachable
!("Cannot actually happen, but it seems there can't be a blanket impl for this")
32 fn str_to_protocol(s
: &str) -> Scheme
{
36 fn guess_protocol(url
: &[u8]) -> Option
<&str> {
37 match url
.find_byte(b'
:'
) {
39 if url
[..colon_pos
].find_byteset(b
"@.").is_some() {
42 url
.get(colon_pos
+ 1..).and_then(|from_colon
| {
43 (from_colon
.contains(&b'
/'
) || from_colon
.contains(&b'
\\'
)).then_some("file")
52 /// Extract the path part from an SCP-like URL `[user@]host.xz:path/to/repo.git/`
53 fn extract_scp_path(url
: &str) -> Option
<&str> {
54 url
.splitn(2, '
:'
).last()
57 fn sanitize_for_protocol
<'a
>(protocol
: &str, url
: &'a
str) -> Cow
<'a
, str> {
59 "ssh" => url
.replacen('
:'
, "/", 1).into(),
64 fn has_no_explicit_protocol(url
: &[u8]) -> bool
{
65 url
.find(b
"://").is_none()
68 fn to_owned_url(url
: url
::Url
) -> Result
<crate::Url
, Error
> {
70 serialize_alternative_form
: false,
71 scheme
: str_to_protocol(url
.scheme()),
72 user
: if url
.username().is_empty() {
75 Some(url
.username().into())
77 host
: url
.host_str().map(Into
::into
),
79 path
: url
.path().into(),
83 /// Parse the given `bytes` as git url.
87 /// We cannot and should never have to deal with UTF-16 encoded windows strings, so bytes input is acceptable.
88 /// For file-paths, we don't expect UTF8 encoding either.
89 pub fn parse(input
: &BStr
) -> Result
<crate::Url
, Error
> {
90 let guessed_protocol
= guess_protocol(input
).ok_or_else(|| Error
::NotALocalFile { url: input.into() }
)?
;
91 let path_without_file_protocol
= input
.strip_prefix(b
"file://");
92 if path_without_file_protocol
.is_some() || (has_no_explicit_protocol(input
) && guessed_protocol
== "file") {
93 let path
: BString
= path_without_file_protocol
94 .map(|stripped_path
| {
97 if stripped_path
.starts_with(b
"/") {
102 let path
= url
::Url
::parse(url
).ok()?
.to_file_path().ok()?
;
103 path
.is_absolute().then(|| gix_path
::into_bstr(path
).into_owned())
105 .unwrap_or_else(|| stripped_path
.into())
115 .unwrap_or_else(|| input
.into());
117 return Err(Error
::MissingRepositoryPath
);
119 let input_starts_with_file_protocol
= input
.starts_with(b
"file://");
120 if input_starts_with_file_protocol
{
121 let wanted
= cfg
!(windows
).then(|| &[b'
\\'
, b'
/'
] as &[_
]).unwrap_or(&[b'
/'
]);
122 if !wanted
.iter().any(|w
| path
.contains(w
)) {
123 return Err(Error
::MissingRepositoryPath
);
126 return Ok(crate::Url
{
127 scheme
: Scheme
::File
,
129 serialize_alternative_form
: !input_starts_with_file_protocol
,
134 let url_str
= std
::str::from_utf8(input
)?
;
135 let (mut url
, mut scp_path
) = match url
::Url
::parse(url_str
) {
136 Ok(url
) => (url
, None
),
137 Err(url
::ParseError
::RelativeUrlWithoutBase
) => {
138 // happens with bare paths as well as scp like paths. The latter contain a ':' past the host portion,
139 // which we are trying to detect.
141 url
::Url
::parse(&format
!(
144 sanitize_for_protocol(guessed_protocol
, url_str
)
146 extract_scp_path(url_str
),
149 Err(err
) => return Err(err
.into()),
151 // SCP like URLs without user parse as 'something' with the scheme being the 'host'. Hosts always have dots.
152 if url
.scheme().find('
.'
).is_some() {
153 // try again with prefixed protocol
154 url
= url
::Url
::parse(&format
!("ssh://{}", sanitize_for_protocol("ssh", url_str
)))?
;
155 scp_path
= extract_scp_path(url_str
);
157 if url
.path().is_empty() && ["ssh", "git"].contains(&url
.scheme()) {
158 return Err(Error
::MissingResourceLocation
);
160 if url
.cannot_be_a_base() {
161 return Err(Error
::RelativeUrl { url: url.into() }
);
164 let mut url
= to_owned_url(url
)?
;
165 if let Some(path
) = scp_path
{
166 url
.path
= path
.into();
167 url
.serialize_alternative_form
= true;