use std::{borrow::Cow, convert::Infallible}; pub use bstr;use bstr::{BStr, BString, ByteSlice}; use crate::Scheme; /// The Error returned by [`parse()`]#[derive(Debug, thiserror::Error)]#[allow(missing_docs)]pub enum Error { #[error("Could not decode URL as UTF8")] Utf8(#[from] std::str::Utf8Error), #[error(transparent)] Url(#[from] url::ParseError), #[error("urls require the path to the repository")] MissingResourceLocation, #[error("file urls require an absolute or relative path to the repository repository")] MissingRepositoryPath, #[error("\"{url}\" is not a valid local path")] NotALocalFile { url: BString }, #[error("Relative URLs are not permitted: {url:?}")] RelativeUrl { url: String },} impl From<Infallible> for Error { fn from(_: Infallible) -> Self { unreachable!("Cannot actually happen, but it seems there can't be a blanket impl for this") }} fn str_to_protocol(s: &str) -> Scheme { Scheme::from(s)} fn guess_protocol(url: &[u8]) -> Option<&str> { match url.find_byte(b':') { Some(colon_pos) => { if url[..colon_pos].find_byte(b'.').is_some() { "ssh" } else { url.get(colon_pos + 1..).and_then(|from_colon| { (from_colon.contains(&b'/') || from_colon.contains(&b'\\')).then(|| "file") })? } } None => "file", } .into()} fn sanitize_for_protocol<'a>(protocol: &str, url: &'a str) -> Cow<'a, str> { match protocol { "ssh" => url.replacen(':', "/", 1).into(), _ => url.into(), }} fn has_no_explicit_protocol(url: &[u8]) -> bool { url.find(b"://").is_none()} fn to_owned_url(url: url::Url) -> Result<crate::Url, Error> { Ok(crate::Url { serialize_alternative_form: false, scheme: str_to_protocol(url.scheme()), user: if url.username().is_empty() { None } else { Some(url.username().into()) }, host: url.host_str().map(Into::into), port: url.port(), path: url.path().into(), })} /// Parse the given `bytes` as git url.////// # Note////// We cannot and should never have to deal with UTF-16 encoded windows strings, so bytes input is acceptable./// For file-paths, we don't expect UTF8 encoding either.pub fn parse(input: &BStr) -> Result<crate::Url, Error> { let guessed_protocol = guess_protocol(input).ok_or_else(|| Error::NotALocalFile { url: input.into() })?; let path_without_file_protocol = input.strip_prefix(b"file://"); if path_without_file_protocol.is_some() || (has_no_explicit_protocol(input) && guessed_protocol == "file") { let path: BString = path_without_file_protocol .map(|stripped_path| { #[cfg(windows)] { if stripped_path.starts_with(b"/") { input .to_str() .ok() .and_then(|url| { let path = url::Url::parse(url).ok()?.to_file_path().ok()?; path.is_absolute().then(|| git_path::into_bstr(path).into_owned()) }) .unwrap_or_else(|| stripped_path.into()) } else { stripped_path.into() } } #[cfg(not(windows))] { stripped_path.into() } }) .unwrap_or_else(|| input.into()); if path.is_empty() { return Err(Error::MissingRepositoryPath); } let input_starts_with_file_protocol = input.starts_with(b"file://"); if input_starts_with_file_protocol { let wanted = cfg!(windows).then(|| &[b'\\', b'/'] as &[_]).unwrap_or(&[b'/']); if !wanted.iter().any(|w| path.contains(w)) { return Err(Error::MissingRepositoryPath); } } return Ok(crate::Url { scheme: Scheme::File, path, serialize_alternative_form: !input_starts_with_file_protocol, ..Default::default() }); } let url_str = std::str::from_utf8(input)?; let (mut url, mut sanitized_scp) = match url::Url::parse(url_str) { Ok(url) => (url, false), Err(url::ParseError::RelativeUrlWithoutBase) => { // happens with bare paths as well as scp like paths. The latter contain a ':' past the host portion, // which we are trying to detect. ( url::Url::parse(&format!( "{}://{}", guessed_protocol, sanitize_for_protocol(guessed_protocol, url_str) ))?, true, ) } Err(err) => return Err(err.into()), }; // SCP like URLs without user parse as 'something' with the scheme being the 'host'. Hosts always have dots. if url.scheme().find('.').is_some() { // try again with prefixed protocol url = url::Url::parse(&format!("ssh://{}", sanitize_for_protocol("ssh", url_str)))?; sanitized_scp = true; } if url.path().is_empty() && ["ssh", "git"].contains(&url.scheme()) { return Err(Error::MissingResourceLocation); } if url.cannot_be_a_base() { return Err(Error::RelativeUrl { url: url.into() }); } to_owned_url(url).map(|url| url.serialize_alternate_form(sanitized_scp))}
use std::{borrow::Cow, convert::Infallible};
pub use bstr;
use bstr::{BStr, BString, ByteSlice};
use crate::Scheme;
/// The Error returned by [`parse()`]
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
#[error("Could not decode URL as UTF8")]
Utf8(#[from] std::str::Utf8Error),
#[error(transparent)]
Url(#[from] url::ParseError),
#[error("urls require the path to the repository")]
MissingResourceLocation,
#[error("file urls require an absolute or relative path to the repository repository")]
MissingRepositoryPath,
#[error("\"{url}\" is not a valid local path")]
NotALocalFile { url: BString },
#[error("Relative URLs are not permitted: {url:?}")]
RelativeUrl { url: String },
}
impl From<Infallible> for Error {
fn from(_: Infallible) -> Self {
unreachable!("Cannot actually happen, but it seems there can't be a blanket impl for this")
}
}
fn str_to_protocol(s: &str) -> Scheme {
Scheme::from(s)
}
fn guess_protocol(url: &[u8]) -> Option<&str> {
match url.find_byte(b':') {
Some(colon_pos) => {
if url[..colon_pos].find_byte(b'.').is_some() {
"ssh"
} else {
url.get(colon_pos + 1..).and_then(|from_colon| {
(from_colon.contains(&b'/') || from_colon.contains(&b'\\')).then(|| "file")
})?
}
}
None => "file",
}
.into()
}
fn sanitize_for_protocol<'a>(protocol: &str, url: &'a str) -> Cow<'a, str> {
match protocol {
"ssh" => url.replacen(':', "/", 1).into(),
_ => url.into(),
}
}
fn has_no_explicit_protocol(url: &[u8]) -> bool {
url.find(b"://").is_none()
}
fn to_owned_url(url: url::Url) -> Result<crate::Url, Error> {
Ok(crate::Url {
serialize_alternative_form: false,
scheme: str_to_protocol(url.scheme()),
user: if url.username().is_empty() {
None
} else {
Some(url.username().into())
},
host: url.host_str().map(Into::into),
port: url.port(),
path: url.path().into(),
})
}
/// Parse the given `bytes` as git url.
///
/// # Note
///
/// We cannot and should never have to deal with UTF-16 encoded windows strings, so bytes input is acceptable.
/// For file-paths, we don't expect UTF8 encoding either.
pub fn parse(input: &BStr) -> Result<crate::Url, Error> {
let guessed_protocol = guess_protocol(input).ok_or_else(|| Error::NotALocalFile { url: input.into() })?;
let path_without_file_protocol = input.strip_prefix(b"file://");
if path_without_file_protocol.is_some() || (has_no_explicit_protocol(input) && guessed_protocol == "file") {
let path: BString = path_without_file_protocol
.map(|stripped_path| {
#[cfg(windows)]
{
if stripped_path.starts_with(b"/") {
input
.to_str()
.ok()
.and_then(|url| {
let path = url::Url::parse(url).ok()?.to_file_path().ok()?;
path.is_absolute().then(|| git_path::into_bstr(path).into_owned())
})
.unwrap_or_else(|| stripped_path.into())
} else {
stripped_path.into()
}
}
#[cfg(not(windows))]
{
stripped_path.into()
}
})
.unwrap_or_else(|| input.into());
if path.is_empty() {
return Err(Error::MissingRepositoryPath);
}
let input_starts_with_file_protocol = input.starts_with(b"file://");
if input_starts_with_file_protocol {
let wanted = cfg!(windows).then(|| &[b'\\', b'/'] as &[_]).unwrap_or(&[b'/']);
if !wanted.iter().any(|w| path.contains(w)) {
return Err(Error::MissingRepositoryPath);
}
}
return Ok(crate::Url {
scheme: Scheme::File,
path,
serialize_alternative_form: !input_starts_with_file_protocol,
..Default::default()
});
}
let url_str = std::str::from_utf8(input)?;
let (mut url, mut sanitized_scp) = match url::Url::parse(url_str) {
Ok(url) => (url, false),
Err(url::ParseError::RelativeUrlWithoutBase) => {
// happens with bare paths as well as scp like paths. The latter contain a ':' past the host portion,
// which we are trying to detect.
(
url::Url::parse(&format!(
"{}://{}",
guessed_protocol,
sanitize_for_protocol(guessed_protocol, url_str)
))?,
true,
)
}
Err(err) => return Err(err.into()),
};
// SCP like URLs without user parse as 'something' with the scheme being the 'host'. Hosts always have dots.
if url.scheme().find('.').is_some() {
// try again with prefixed protocol
url = url::Url::parse(&format!("ssh://{}", sanitize_for_protocol("ssh", url_str)))?;
sanitized_scp = true;
}
if url.path().is_empty() && ["ssh", "git"].contains(&url.scheme()) {
return Err(Error::MissingResourceLocation);
}
if url.cannot_be_a_base() {
return Err(Error::RelativeUrl { url: url.into() });
}
to_owned_url(url).map(|url| url.serialize_alternate_form(sanitized_scp))
}