#![cfg_attr(
all(doc, feature = "document-features"),
doc = ::document_features::document_features!()
)]
#![cfg_attr(all(doc, feature = "document-features"), feature(doc_cfg))]
#![deny(rust_2018_idioms, missing_docs)]
#![forbid(unsafe_code)]
use std::{borrow::Cow, path::PathBuf};
use bstr::{BStr, BString};
pub mod expand_path;
mod scheme;
pub use scheme::Scheme;
mod impls;
pub mod parse;
mod simple_url;
pub fn parse(input: &BStr) -> Result<Url, parse::Error> {
use parse::InputScheme;
match parse::find_scheme(input) {
InputScheme::Local => parse::local(input),
InputScheme::Url { protocol_end } if input[..protocol_end].eq_ignore_ascii_case(b"file") => {
parse::file_url(input, protocol_end)
}
InputScheme::Url { protocol_end } => parse::url(input, protocol_end),
InputScheme::Scp { colon } => parse::scp(input, colon),
}
}
pub fn expand_path(user: Option<&expand_path::ForUser>, path: &BStr) -> Result<PathBuf, expand_path::Error> {
expand_path::with(user, path, |user| match user {
expand_path::ForUser::Current => gix_path::env::home_dir(),
expand_path::ForUser::Name(user) => {
gix_path::env::home_dir().and_then(|home| home.parent().map(|home_dirs| home_dirs.join(user.to_string())))
}
})
}
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum ArgumentSafety<'a> {
Absent,
Usable(&'a str),
Dangerous(&'a str),
}
#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Url {
pub scheme: Scheme,
pub user: Option<String>,
pub password: Option<String>,
pub host: Option<String>,
pub serialize_alternative_form: bool,
pub port: Option<u16>,
pub path: BString,
}
impl Url {
pub fn from_parts(
scheme: Scheme,
user: Option<String>,
password: Option<String>,
host: Option<String>,
port: Option<u16>,
path: BString,
serialize_alternative_form: bool,
) -> Result<Self, parse::Error> {
parse(
Url {
scheme,
user,
password,
host,
port,
path,
serialize_alternative_form,
}
.to_bstring()
.as_ref(),
)
}
}
impl Url {
pub fn set_user(&mut self, user: Option<String>) -> Option<String> {
let prev = self.user.take();
self.user = user;
prev
}
pub fn set_password(&mut self, password: Option<String>) -> Option<String> {
let prev = self.password.take();
self.password = password;
prev
}
}
impl Url {
pub fn serialize_alternate_form(mut self, use_alternate_form: bool) -> Self {
self.serialize_alternative_form = use_alternate_form;
self
}
pub fn canonicalize(&mut self, current_dir: &std::path::Path) -> Result<(), gix_path::realpath::Error> {
if self.scheme == Scheme::File {
let path = gix_path::from_bstr(Cow::Borrowed(self.path.as_ref()));
let abs_path = gix_path::realpath_opts(path.as_ref(), current_dir, gix_path::realpath::MAX_SYMLINKS)?;
self.path = gix_path::into_bstr(abs_path).into_owned();
}
Ok(())
}
}
impl Url {
pub fn user(&self) -> Option<&str> {
self.user.as_deref()
}
pub fn user_as_argument(&self) -> ArgumentSafety<'_> {
match self.user() {
Some(user) if looks_like_command_line_option(user.as_bytes()) => ArgumentSafety::Dangerous(user),
Some(user) => ArgumentSafety::Usable(user),
None => ArgumentSafety::Absent,
}
}
pub fn user_argument_safe(&self) -> Option<&str> {
match self.user_as_argument() {
ArgumentSafety::Usable(user) => Some(user),
_ => None,
}
}
pub fn password(&self) -> Option<&str> {
self.password.as_deref()
}
pub fn host(&self) -> Option<&str> {
self.host.as_deref()
}
pub fn host_as_argument(&self) -> ArgumentSafety<'_> {
match self.host() {
Some(host) if looks_like_command_line_option(host.as_bytes()) => ArgumentSafety::Dangerous(host),
Some(host) => ArgumentSafety::Usable(host),
None => ArgumentSafety::Absent,
}
}
pub fn host_argument_safe(&self) -> Option<&str> {
match self.host_as_argument() {
ArgumentSafety::Usable(host) => Some(host),
_ => None,
}
}
pub fn path_argument_safe(&self) -> Option<&BStr> {
self.path
.get(1..)
.and_then(|truncated| (!looks_like_command_line_option(truncated)).then_some(self.path.as_ref()))
}
pub fn path_is_root(&self) -> bool {
self.path == "/"
}
pub fn port_or_default(&self) -> Option<u16> {
self.port.or_else(|| {
use Scheme::*;
Some(match self.scheme {
Http => 80,
Https => 443,
Ssh => 22,
Git => 9418,
File | Ext(_) => return None,
})
})
}
}
fn looks_like_command_line_option(b: &[u8]) -> bool {
b.first() == Some(&b'-')
}
impl Url {
pub fn canonicalized(&self, current_dir: &std::path::Path) -> Result<Self, gix_path::realpath::Error> {
let mut res = self.clone();
res.canonicalize(current_dir)?;
Ok(res)
}
}
impl Url {
pub fn write_to(&self, out: &mut dyn std::io::Write) -> std::io::Result<()> {
if self.serialize_alternative_form
&& (self.scheme == Scheme::File || self.scheme == Scheme::Ssh)
&& self.password.is_none()
&& self.port.is_none()
{
self.write_alternative_form_to(out)
} else {
self.write_canonical_form_to(out)
}
}
fn write_canonical_form_to(&self, out: &mut dyn std::io::Write) -> std::io::Result<()> {
fn percent_encode(s: &str) -> Cow<'_, str> {
const USERINFO_ENCODE_SET: &percent_encoding::AsciiSet = &percent_encoding::CONTROLS
.add(b' ')
.add(b'"')
.add(b'#')
.add(b'%')
.add(b'/')
.add(b'<')
.add(b'>')
.add(b'?')
.add(b'@')
.add(b'[')
.add(b'\\')
.add(b']')
.add(b'^')
.add(b'`')
.add(b'{')
.add(b'|')
.add(b'}');
percent_encoding::utf8_percent_encode(s, USERINFO_ENCODE_SET).into()
}
out.write_all(self.scheme.as_str().as_bytes())?;
out.write_all(b"://")?;
let needs_brackets = self.port.is_some() && self.host_needs_brackets();
match (&self.user, &self.host) {
(Some(user), Some(host)) => {
out.write_all(percent_encode(user).as_bytes())?;
if let Some(password) = &self.password {
out.write_all(b":")?;
out.write_all(percent_encode(password).as_bytes())?;
}
out.write_all(b"@")?;
if needs_brackets {
out.write_all(b"[")?;
}
out.write_all(host.as_bytes())?;
if needs_brackets {
out.write_all(b"]")?;
}
}
(None, Some(host)) => {
if needs_brackets {
out.write_all(b"[")?;
}
out.write_all(host.as_bytes())?;
if needs_brackets {
out.write_all(b"]")?;
}
}
(None, None) => {}
(Some(_user), None) => {
return Err(std::io::Error::other(
"Invalid URL structure: user specified without host",
));
}
}
if let Some(port) = &self.port {
write!(out, ":{port}")?;
}
if matches!(self.scheme, Scheme::Ssh | Scheme::Git) && !self.path.starts_with(b"/") {
out.write_all(b"/")?;
}
if matches!(self.scheme, Scheme::Http | Scheme::Https) {
const PATH_ENCODE_SET: &percent_encoding::AsciiSet = &percent_encoding::CONTROLS
.add(b' ')
.add(b'"')
.add(b'%')
.add(b'<')
.add(b'>')
.add(b'`')
.add(b'{')
.add(b'}');
write!(
out,
"{}",
percent_encoding::percent_encode(self.path.as_ref(), PATH_ENCODE_SET)
)?;
} else {
out.write_all(&self.path)?;
}
Ok(())
}
fn host_needs_brackets(&self) -> bool {
fn is_ipv6(h: &str) -> bool {
h.contains(':') && !h.starts_with('[')
}
self.host.as_ref().is_some_and(|h| is_ipv6(h))
}
fn write_alternative_form_to(&self, out: &mut dyn std::io::Write) -> std::io::Result<()> {
let needs_brackets = self.host_needs_brackets();
match (&self.user, &self.host) {
(Some(user), Some(host)) => {
out.write_all(user.as_bytes())?;
out.write_all(b"@")?;
if needs_brackets {
out.write_all(b"[")?;
}
out.write_all(host.as_bytes())?;
if needs_brackets {
out.write_all(b"]")?;
}
}
(None, Some(host)) => {
if needs_brackets {
out.write_all(b"[")?;
}
out.write_all(host.as_bytes())?;
if needs_brackets {
out.write_all(b"]")?;
}
}
(None, None) => {}
(Some(_user), None) => {
return Err(std::io::Error::other(
"Invalid URL structure: user specified without host",
));
}
}
if self.scheme == Scheme::Ssh {
out.write_all(b":")?;
}
out.write_all(&self.path)?;
Ok(())
}
pub fn to_bstring(&self) -> BString {
let mut buf = Vec::with_capacity(
(5 + 3)
+ self.user.as_ref().map(String::len).unwrap_or_default()
+ 1
+ self.host.as_ref().map(String::len).unwrap_or_default()
+ self.port.map(|_| 5).unwrap_or_default()
+ self.path.len(),
);
self.write_to(&mut buf).expect("io cannot fail in memory");
buf.into()
}
}
impl Url {
pub fn from_bytes(bytes: &BStr) -> Result<Self, parse::Error> {
parse(bytes)
}
}
#[doc(hidden)]
pub mod testing {
use bstr::BString;
use crate::{Scheme, Url};
pub trait TestUrlExtension {
fn from_parts_unchecked(
scheme: Scheme,
user: Option<String>,
password: Option<String>,
host: Option<String>,
port: Option<u16>,
path: BString,
serialize_alternative_form: bool,
) -> Url {
Url {
scheme,
user,
password,
host,
port,
path,
serialize_alternative_form,
}
}
}
impl TestUrlExtension for Url {}
}