//! A library implementing a URL for use in git with access to its special capabilities.//!//! ## Examples//!//! ```//! let mut url = gix_url::parse("ssh://git@example.com/gitoxide").unwrap();//! assert_eq!(url.user(), Some("git"));//! assert_eq!(url.host(), Some("example.com"));//! assert_eq!(url.to_bstring(), "ssh://git@example.com/gitoxide");//!//! assert_eq!(url.set_user(Some("byron".into())), Some("git".into()));//! assert_eq!(url.user_argument_safe(), Some("byron"));//! assert_eq!(url.to_bstring(), "ssh://byron@example.com/gitoxide");//!//! let suspicious = gix_url::parse("ssh://-Fconfig@host/repo").unwrap();//! assert_eq!(suspicious.user_argument_safe(), None, "The user isn't returned as it looks like an argument");//! ```//! ## Feature Flags#![cfg_attr( all(doc, feature = "document-features"), doc = ::document_features::document_features!())]#![cfg_attr(all(doc, feature = "document-features"), feature(doc_cfg))]#![deny(missing_docs)]#![forbid(unsafe_code)] use std::{borrow::Cow, path::PathBuf}; use bstr::{BStr, BString};use gix_utils::AsBStr; const HTTP_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'}'); /// User-home expansion for repository paths.pub mod expand_path; mod scheme;pub use scheme::Scheme;mod impls; /// Parsing errors and input classifications.pub mod parse; /// Minimal URL parser to replace the `url` crate dependencymod simple_url; /// Parse a Git remote location from `input`.////// This accepts standard URLs, SCP-like SSH locations, remote-helper locations and local paths. URL and SCP-like/// inputs must be UTF-8; remote-helper addresses and local paths retain arbitrary bytes.////// Locations of the `<helper>::<address>` form described in/// [`gitremote-helpers`](https://git-scm.com/docs/gitremote-helpers) are recognized before any URL/// syntax, so an address may itself contain `://`. They are represented as/// [`Scheme::Helper`] holding the helper name, with the address kept verbatim in [`Url::path`] as only the helper/// program can interpret it. The command-executing `ext` helper is represented as [`Scheme::Ext`] in both spellings;/// `ext://<address>` retains the entire URL as its command. Other unknown URL transports in the/// `<helper>://<address>` form are represented as [`Scheme::HelperUrl`].////// # Deviation////// Unlike Git, this rejects textual and overflowing ports in SSH and Git URLs. Git treats such port text as part of/// the hostname, which hides the malformed port and causes a less useful hostname-resolution error later.////// Also unlike Git, an empty remote-helper name as in `::address` is not accepted, as the `git-remote-` program it/// would name cannot meaningfully exist.pub fn parse(input: impl AsBStr) -> Result<Url, parse::Error> { use parse::InputScheme; let input = input.as_bstr(); match parse::find_scheme(input) { InputScheme::RemoteHelper { helper_end } => Ok(parse::remote_helper(input, helper_end)), InputScheme::Local => parse::local(input), InputScheme::Url { protocol_end } if input[..protocol_end] == *b"file" => parse::file_url(input, protocol_end), InputScheme::Url { protocol_end } => parse::url(input, protocol_end), InputScheme::Scp { colon } => parse::scp(input, colon), }} /// Expand `path` for the given `user`, which can be obtained from [`expand_path::parse()`], resolving the home/// directory automatically.////// If more precise control of the resolution mechanism is needed, then use the [expand_path::with()] function.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()))) } })} /// Classification of a portion of a URL by whether it is *syntactically* safe to pass as an argument to a command-line program.////// Various parts of URLs can be specified to begin with `-`. If they are used as options to a command-line application/// such as an SSH client, they will be treated as options rather than as non-option arguments as the developer intended./// This is a security risk, because URLs are not always trusted and can often be composed or influenced by an attacker./// See <https://secure.phabricator.com/T12961> for details.////// # Security Warning////// This type only expresses known *syntactic* risk. It does not cover other risks, such as passing a personal access/// token as a username rather than a password in an application that logs usernames.#[derive(Debug, PartialEq, Eq, Copy, Clone)]pub enum ArgumentSafety<'a> { /// May be safe. There is nothing to pass, so there is nothing dangerous. Absent, /// May be safe. The argument does not begin with a `-` and so will not be confused as an option. Usable(&'a str), /// Dangerous! Begins with `-` and could be treated as an option. Use the value in error messages only. Dangerous(&'a str),} /// A URL with support for specialized git related capabilities.////// Additionally, there is support for [deserialization](Url::from_bytes()) and [serialization](Url::to_bstring()).////// # Mutability Warning////// Public fields can be modified into combinations that do not serialize or parse. Use [`parse()`] or/// [`Url::from_parts()`] at trust boundaries; do not assume an accepted `Url` remains valid without revalidation.////// # Serialization////// This type does not implement `Into<String>`, `From<Url> for String` because URLs/// can contain non-UTF-8 sequences in the path component when parsed from raw bytes./// Use [to_bstring()](Url::to_bstring()) for complete serialization, including non-UTF-8 path bytes, or use the/// [`Display`](std::fmt::Display) trait for a UTF-8 representation that redacts passwords for safe logging.////// When the `serde` feature is enabled, this type implements `serde::Serialize` and `serde::Deserialize`,/// which will serialize *all* fields, including the password.////// # Security Warning////// URLs may contain passwords and using standard [formatting](std::fmt::Display) will redact such passwords,/// whereas [`Url::to_bstring()`] includes all URL parts./// **Beware that some URLs still print secrets if they use them outside of the designated password fields.**////// Also note that URLs that fail to parse are typically stored in [the resulting error](parse::Error) type/// and printed in full using its display implementation.#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]#[cfg_attr(feature = "serde", derive(serde::Serialize))]pub struct Url { /// The URL scheme. pub scheme: Scheme, /// The user to impersonate on the remote. /// /// Stored in decoded form: percent-encoded characters are decoded during parsing. /// Re-encoded during canonical serialization, but written as-is in alternative form. pub user: Option<String>, /// The password associated with a user. /// /// Stored in decoded form: percent-encoded characters are decoded during parsing. /// Re-encoded during canonical serialization. Its presence makes serialization use canonical rather than alternative /// form because SCP-like and local-path syntax cannot represent a password. pub password: Option<String>, /// The host to which to connect, or `None` for locations without a host, such as local paths. /// /// Brackets are stripped from parsed SSH hosts and otherwise preserved as parsed. Serialization adds brackets to /// unbracketed colon-containing hosts when needed to disambiguate a port or scoped IPv6 address. /// DNS-like ASCII hosts are lowercased. Non-HTTP hosts are percent-decoded for Git compatibility, while HTTP and /// HTTPS host escapes remain encoded. pub host: Option<String>, /// Request alternative serialization, generally because the location was parsed in that form. /// /// Alternative forms include SCP-like syntax (`user@host:path`), bare file paths, and the `<helper>::<address>` /// syntax of [`gitremote-helpers`](https://git-scm.com/docs/gitremote-helpers). /// It is used only for SSH or file locations without a password or port. [`Scheme::Helper`] and [`Scheme::Ext`] /// always use remote-helper form, and SCP-like form is also retained when canonical SSH form would change a /// relative repository path. pub serialize_alternative_form: bool, /// The explicit port, if parsed or assigned. /// /// Git accepts port zero in SSH and Git URLs, so this field may contain `0`. Textual and overflowing ports are /// rejected unlike Git; see the deviation documented on [`parse()`]. Use [`Self::port_or_default()`] to obtain a /// scheme default when this is `None`. pub port: Option<u16>, /// The path portion of the URL, usually the location of the git repository. /// /// Percent-encoded characters are decoded during parsing (e.g., `%20` becomes a space in this field). An unchanged /// parsed path may retain its original encoded spelling for serialization. Constructed or modified HTTP paths are /// percent-encoded during canonical serialization; other schemes write the path bytes as stored. /// /// Path normalization during parsing: /// - SSH/Git schemes: Leading `/~` is stripped (e.g., `/~repo` becomes `~repo`) /// - SSH/Git schemes: Empty paths are rejected as errors /// - HTTP/HTTPS schemes: Empty paths are normalized to `/` /// /// This type has no separate query or fragment fields. For HTTP and HTTPS, `?`, `#`, and everything after them are /// stored in this field. For other URL schemes, Git treats `?` and `#` before the first slash as authority text. /// /// For locations in the `<helper>::<address>` form of /// [`gitremote-helpers`](https://git-scm.com/docs/gitremote-helpers), this holds the address verbatim, /// uninterpreted and possibly empty, as only the helper program can make sense of it. /// /// During serialization, SSH/Git URLs prepend `/` to paths not starting with `/`. /// /// # Security Warning /// /// URLs allow paths to start with `-` which makes it possible to mask command-line arguments as path which then leads to /// the invocation of programs from an attacker controlled URL. See <https://secure.phabricator.com/T12961> for details. /// /// For a slash-prefixed path that will be passed intact to a command-line application, call /// [`Self::path_argument_safe()`]. Other path forms require validation appropriate to how they will be passed. pub path: BString, /// The original parsed path when it contains percent escapes. /// /// This lets serialization retain the encoded spelling while [`Self::path`] remains decoded. It is reused only /// while decoding it still produces the public path; constructing or mutating the public path encodes percent signs. #[cfg_attr(feature = "serde", serde(default))] pub(crate) path_with_percent_escapes: Option<BString>,} #[cfg(feature = "serde")]impl<'de> serde::Deserialize<'de> for Url { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: serde::Deserializer<'de>, { #[derive(serde::Deserialize)] struct Fields { scheme: Scheme, user: Option<String>, password: Option<String>, host: Option<String>, serialize_alternative_form: bool, port: Option<u16>, path: BString, #[serde(default)] path_with_percent_escapes: Option<BString>, } let mut fields = Fields::deserialize(deserializer)?; if fields.path_with_percent_escapes.as_ref() == Some(&fields.path) { fields.path_with_percent_escapes = Some(encode_legacy_http_path(&fields.path)); fields.path = percent_encoding::percent_decode(&fields.path) .collect::<Vec<_>>() .into(); } Ok(Url { scheme: fields.scheme, user: fields.user, password: fields.password, host: fields.host, serialize_alternative_form: fields.serialize_alternative_form, port: fields.port, path: fields.path, path_with_percent_escapes: fields.path_with_percent_escapes, }) }} #[cfg(feature = "serde")]fn encode_legacy_http_path(path: &[u8]) -> BString { let mut out = Vec::with_capacity(path.len()); let mut start = 0; let mut pos = 0; while pos + 2 < path.len() { if path[pos] == b'%' && path[pos + 1].is_ascii_hexdigit() && path[pos + 2].is_ascii_hexdigit() { out.extend( percent_encoding::percent_encode(&path[start..pos], HTTP_PATH_ENCODE_SET) .to_string() .bytes(), ); out.extend_from_slice(&path[pos..pos + 3]); pos += 3; start = pos; } else { pos += 1; } } out.extend( percent_encoding::percent_encode(&path[start..], HTTP_PATH_ENCODE_SET) .to_string() .bytes(), ); out.into()} /// Instantiationimpl Url { /// Create an instance from the given parts and validate it by serializing and parsing it back. /// /// For HTTP and HTTPS, `path` is decoded data: literal percent signs are encoded during serialization, and an empty /// path is normalized to `/`. Other schemes interpret `path` according to their serialized syntax. /// `serialize_alternative_form` merely requests alternative form; passwords, ports, and unsupported schemes force /// canonical URL serialization. /// /// # Panics /// /// Panics if the supplied parts cannot be serialized before validation, such as a user without a host. 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> { if let Scheme::Helper(name) = &scheme && !parse::is_valid_remote_helper_name(name.as_bytes()) { return Err(parse::Error::InvalidRemoteHelperName { name: name.clone() }); } let is_http = matches!(scheme, Scheme::Http | Scheme::Https); let mut parsed = parse( Url { scheme, user, password, host, port, path: path.clone(), serialize_alternative_form, path_with_percent_escapes: None, } .to_bstring(), )?; if is_http { // Preserve the caller's path as decoded data, except for an empty path normalized to `/` above. In // particular, percent escapes supplied through `from_parts()` are literal text and must be encoded. if !path.is_empty() { parsed.path = path; } parsed.path_with_percent_escapes = None; } Ok(parsed) }} /// Modificationimpl Url { /// Set the given `user`, or unset it with `None`. Return the previous value. pub fn set_user(&mut self, user: Option<String>) -> Option<String> { let prev = self.user.take(); self.user = user; prev } /// Set the given `password`, or unset it with `None`. Return the previous value. pub fn set_password(&mut self, password: Option<String>) -> Option<String> { let prev = self.password.take(); self.password = password; prev }} /// Builderimpl Url { /// Request alternative serialization, e.g. `file:///path` becomes `/path`. /// /// Parsed URLs set this automatically. Alternative form is used only for SSH or file locations without a password /// or port; all other values serialize in canonical URL form. /// /// Setting `use_alternate_form` to `false` requests canonical, URL-like, serialization. SCP-like form is retained if /// canonical SSH form would change a relative repository path. Remote-helper form is always retained because URL /// form would change the address Git passes to the helper. pub fn with_request_alternate_form(mut self, use_alternate_form: bool) -> Self { self.serialize_alternative_form = use_alternate_form; self } /// Resolve the path of a file location against `current_dir` and normalize it in place. /// /// Other schemes are unchanged. 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(()) }} /// Accessimpl Url { /// Return the username mentioned in the URL, if present. /// /// # Security Warning /// /// URLs allow usernames to start with `-` which makes it possible to mask command-line arguments as username which then leads to /// the invocation of programs from an attacker controlled URL. See <https://secure.phabricator.com/T12961> for details. /// /// If this value is ever going to be passed to a command-line application, call [Self::user_argument_safe()] instead. pub fn user(&self) -> Option<&str> { self.user.as_deref() } /// Classify the username of this URL by whether it is safe to pass as a command-line argument. /// /// Use this method instead of [Self::user()] if the username is going to be passed to a command-line application. /// If the unsafe and absent cases need not be distinguished, [Self::user_argument_safe()] may also be used. 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, } } /// Return the username of this URL if present *and* if it can't be mistaken for a command-line argument. /// /// Use this method or [Self::user_as_argument()] instead of [Self::user()] if the username is going to be /// passed to a command-line application. Prefer [Self::user_as_argument()] unless the unsafe and absent /// cases need not be distinguished from each other. pub fn user_argument_safe(&self) -> Option<&str> { match self.user_as_argument() { ArgumentSafety::Usable(user) => Some(user), _ => None, } } /// Return the password mentioned in the url, if present. pub fn password(&self) -> Option<&str> { self.password.as_deref() } /// Return the host mentioned in the URL, if present. /// /// # Security Warning /// /// URLs allow hosts to start with `-` which makes it possible to mask command-line arguments as host which then leads to /// the invocation of programs from an attacker controlled URL. See <https://secure.phabricator.com/T12961> for details. /// /// If this value is ever going to be passed to a command-line application, call [Self::host_as_argument()] /// or [Self::host_argument_safe()] instead. pub fn host(&self) -> Option<&str> { self.host.as_deref() } /// Classify the host of this URL by whether it is safe to pass as a command-line argument. /// /// Use this method instead of [Self::host()] if the host is going to be passed to a command-line application. /// If the unsafe and absent cases need not be distinguished, [Self::host_argument_safe()] may also be used. 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, } } /// Return the host of this URL if present *and* if it can't be mistaken for a command-line argument. /// /// Use this method or [Self::host_as_argument()] instead of [Self::host()] if the host is going to be /// passed to a command-line application. Prefer [Self::host_as_argument()] unless the unsafe and absent /// cases need not be distinguished from each other. pub fn host_argument_safe(&self) -> Option<&str> { match self.host_as_argument() { ArgumentSafety::Usable(host) => Some(host), _ => None, } } fn path_with_percent_escapes(&self) -> Option<&BStr> { let encoded = self.path_with_percent_escapes.as_ref()?; percent_encoding::percent_decode(encoded) .eq(self.path.iter().copied()) .then_some(encoded.as_ref()) } /// Return the original percent-escaped path if [Self::path] wasn't changed in the meantime, or [`Self::path`] otherwise. pub fn original_path(&self) -> &BStr { self.path_with_percent_escapes().unwrap_or(self.path.as_ref()) } /// Return a slash-prefixed path if the bytes after the slash can't be mistaken for a command-line argument. /// /// The leading slash must be present and must be passed to the command. Empty and non-slash-prefixed paths return /// `None`; validate those according to how they will be passed. pub fn path_argument_safe(&self) -> Option<&BStr> { self.path .strip_prefix(b"/") .and_then(|truncated| (!looks_like_command_line_option(truncated)).then_some(self.path.as_ref())) } /// Return true if the path portion of the URL is `/`. pub fn path_is_root(&self) -> bool { self.path == "/" } /// Return the actual or default port for use according to the URL scheme. /// Note that there may be no default port either. 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,Lines 1–500 of 814. Load more
//! A library implementing a URL for use in git with access to its special capabilities. //! //! ## Examples //! //! ``` //! let mut url = gix_url::parse("ssh://git@example.com/gitoxide").unwrap(); //! assert_eq!(url.user(), Some("git")); //! assert_eq!(url.host(), Some("example.com")); //! assert_eq!(url.to_bstring(), "ssh://git@example.com/gitoxide"); //! //! assert_eq!(url.set_user(Some("byron".into())), Some("git".into())); //! assert_eq!(url.user_argument_safe(), Some("byron")); //! assert_eq!(url.to_bstring(), "ssh://byron@example.com/gitoxide"); //! //! let suspicious = gix_url::parse("ssh://-Fconfig@host/repo").unwrap(); //! assert_eq!(suspicious.user_argument_safe(), None, "The user isn't returned as it looks like an argument"); //! ``` //! ## Feature Flags #![cfg_attr( all(doc, feature = "document-features"), doc = ::document_features::document_features!() )] #![cfg_attr(all(doc, feature = "document-features"), feature(doc_cfg))] #![deny(missing_docs)] #![forbid(unsafe_code)] use std::{borrow::Cow, path::PathBuf}; use bstr::{BStr, BString}; use gix_utils::AsBStr; const HTTP_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'}'); /// User-home expansion for repository paths. pub mod expand_path; mod scheme; pub use scheme::Scheme; mod impls; /// Parsing errors and input classifications. pub mod parse; /// Minimal URL parser to replace the `url` crate dependency mod simple_url; /// Parse a Git remote location from `input`. /// /// This accepts standard URLs, SCP-like SSH locations, remote-helper locations and local paths. URL and SCP-like /// inputs must be UTF-8; remote-helper addresses and local paths retain arbitrary bytes. /// /// Locations of the `<helper>::<address>` form described in /// [`gitremote-helpers`](https://git-scm.com/docs/gitremote-helpers) are recognized before any URL /// syntax, so an address may itself contain `://`. They are represented as /// [`Scheme::Helper`] holding the helper name, with the address kept verbatim in [`Url::path`] as only the helper /// program can interpret it. The command-executing `ext` helper is represented as [`Scheme::Ext`] in both spellings; /// `ext://<address>` retains the entire URL as its command. Other unknown URL transports in the /// `<helper>://<address>` form are represented as [`Scheme::HelperUrl`]. /// /// # Deviation /// /// Unlike Git, this rejects textual and overflowing ports in SSH and Git URLs. Git treats such port text as part of /// the hostname, which hides the malformed port and causes a less useful hostname-resolution error later. /// /// Also unlike Git, an empty remote-helper name as in `::address` is not accepted, as the `git-remote-` program it /// would name cannot meaningfully exist. pub fn parse(input: impl AsBStr) -> Result<Url, parse::Error> { use parse::InputScheme; let input = input.as_bstr(); match parse::find_scheme(input) { InputScheme::RemoteHelper { helper_end } => Ok(parse::remote_helper(input, helper_end)), InputScheme::Local => parse::local(input), InputScheme::Url { protocol_end } if input[..protocol_end] == *b"file" => parse::file_url(input, protocol_end), InputScheme::Url { protocol_end } => parse::url(input, protocol_end), InputScheme::Scp { colon } => parse::scp(input, colon), } } /// Expand `path` for the given `user`, which can be obtained from [`expand_path::parse()`], resolving the home /// directory automatically. /// /// If more precise control of the resolution mechanism is needed, then use the [expand_path::with()] function. 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()))) } }) } /// Classification of a portion of a URL by whether it is *syntactically* safe to pass as an argument to a command-line program. /// /// Various parts of URLs can be specified to begin with `-`. If they are used as options to a command-line application /// such as an SSH client, they will be treated as options rather than as non-option arguments as the developer intended. /// This is a security risk, because URLs are not always trusted and can often be composed or influenced by an attacker. /// See <https://secure.phabricator.com/T12961> for details. /// /// # Security Warning /// /// This type only expresses known *syntactic* risk. It does not cover other risks, such as passing a personal access /// token as a username rather than a password in an application that logs usernames. #[derive(Debug, PartialEq, Eq, Copy, Clone)] pub enum ArgumentSafety<'a> { /// May be safe. There is nothing to pass, so there is nothing dangerous. Absent, /// May be safe. The argument does not begin with a `-` and so will not be confused as an option. Usable(&'a str), /// Dangerous! Begins with `-` and could be treated as an option. Use the value in error messages only. Dangerous(&'a str), } /// A URL with support for specialized git related capabilities. /// /// Additionally, there is support for [deserialization](Url::from_bytes()) and [serialization](Url::to_bstring()). /// /// # Mutability Warning /// /// Public fields can be modified into combinations that do not serialize or parse. Use [`parse()`] or /// [`Url::from_parts()`] at trust boundaries; do not assume an accepted `Url` remains valid without revalidation. /// /// # Serialization /// /// This type does not implement `Into<String>`, `From<Url> for String` because URLs /// can contain non-UTF-8 sequences in the path component when parsed from raw bytes. /// Use [to_bstring()](Url::to_bstring()) for complete serialization, including non-UTF-8 path bytes, or use the /// [`Display`](std::fmt::Display) trait for a UTF-8 representation that redacts passwords for safe logging. /// /// When the `serde` feature is enabled, this type implements `serde::Serialize` and `serde::Deserialize`, /// which will serialize *all* fields, including the password. /// /// # Security Warning /// /// URLs may contain passwords and using standard [formatting](std::fmt::Display) will redact such passwords, /// whereas [`Url::to_bstring()`] includes all URL parts. /// **Beware that some URLs still print secrets if they use them outside of the designated password fields.** /// /// Also note that URLs that fail to parse are typically stored in [the resulting error](parse::Error) type /// and printed in full using its display implementation. #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize))] pub struct Url { /// The URL scheme. pub scheme: Scheme, /// The user to impersonate on the remote. /// /// Stored in decoded form: percent-encoded characters are decoded during parsing. /// Re-encoded during canonical serialization, but written as-is in alternative form. pub user: Option<String>, /// The password associated with a user. /// /// Stored in decoded form: percent-encoded characters are decoded during parsing. /// Re-encoded during canonical serialization. Its presence makes serialization use canonical rather than alternative /// form because SCP-like and local-path syntax cannot represent a password. pub password: Option<String>, /// The host to which to connect, or `None` for locations without a host, such as local paths. /// /// Brackets are stripped from parsed SSH hosts and otherwise preserved as parsed. Serialization adds brackets to /// unbracketed colon-containing hosts when needed to disambiguate a port or scoped IPv6 address. /// DNS-like ASCII hosts are lowercased. Non-HTTP hosts are percent-decoded for Git compatibility, while HTTP and /// HTTPS host escapes remain encoded. pub host: Option<String>, /// Request alternative serialization, generally because the location was parsed in that form. /// /// Alternative forms include SCP-like syntax (`user@host:path`), bare file paths, and the `<helper>::<address>` /// syntax of [`gitremote-helpers`](https://git-scm.com/docs/gitremote-helpers). /// It is used only for SSH or file locations without a password or port. [`Scheme::Helper`] and [`Scheme::Ext`] /// always use remote-helper form, and SCP-like form is also retained when canonical SSH form would change a /// relative repository path. pub serialize_alternative_form: bool, /// The explicit port, if parsed or assigned. /// /// Git accepts port zero in SSH and Git URLs, so this field may contain `0`. Textual and overflowing ports are /// rejected unlike Git; see the deviation documented on [`parse()`]. Use [`Self::port_or_default()`] to obtain a /// scheme default when this is `None`. pub port: Option<u16>, /// The path portion of the URL, usually the location of the git repository. /// /// Percent-encoded characters are decoded during parsing (e.g., `%20` becomes a space in this field). An unchanged /// parsed path may retain its original encoded spelling for serialization. Constructed or modified HTTP paths are /// percent-encoded during canonical serialization; other schemes write the path bytes as stored. /// /// Path normalization during parsing: /// - SSH/Git schemes: Leading `/~` is stripped (e.g., `/~repo` becomes `~repo`) /// - SSH/Git schemes: Empty paths are rejected as errors /// - HTTP/HTTPS schemes: Empty paths are normalized to `/` /// /// This type has no separate query or fragment fields. For HTTP and HTTPS, `?`, `#`, and everything after them are /// stored in this field. For other URL schemes, Git treats `?` and `#` before the first slash as authority text. /// /// For locations in the `<helper>::<address>` form of /// [`gitremote-helpers`](https://git-scm.com/docs/gitremote-helpers), this holds the address verbatim, /// uninterpreted and possibly empty, as only the helper program can make sense of it. /// /// During serialization, SSH/Git URLs prepend `/` to paths not starting with `/`. /// /// # Security Warning /// /// URLs allow paths to start with `-` which makes it possible to mask command-line arguments as path which then leads to /// the invocation of programs from an attacker controlled URL. See <https://secure.phabricator.com/T12961> for details. /// /// For a slash-prefixed path that will be passed intact to a command-line application, call /// [`Self::path_argument_safe()`]. Other path forms require validation appropriate to how they will be passed. pub path: BString, /// The original parsed path when it contains percent escapes. /// /// This lets serialization retain the encoded spelling while [`Self::path`] remains decoded. It is reused only /// while decoding it still produces the public path; constructing or mutating the public path encodes percent signs. #[cfg_attr(feature = "serde", serde(default))] pub(crate) path_with_percent_escapes: Option<BString>, } #[cfg(feature = "serde")] impl<'de> serde::Deserialize<'de> for Url { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: serde::Deserializer<'de>, { #[derive(serde::Deserialize)] struct Fields { scheme: Scheme, user: Option<String>, password: Option<String>, host: Option<String>, serialize_alternative_form: bool, port: Option<u16>, path: BString, #[serde(default)] path_with_percent_escapes: Option<BString>, } let mut fields = Fields::deserialize(deserializer)?; if fields.path_with_percent_escapes.as_ref() == Some(&fields.path) { fields.path_with_percent_escapes = Some(encode_legacy_http_path(&fields.path)); fields.path = percent_encoding::percent_decode(&fields.path) .collect::<Vec<_>>() .into(); } Ok(Url { scheme: fields.scheme, user: fields.user, password: fields.password, host: fields.host, serialize_alternative_form: fields.serialize_alternative_form, port: fields.port, path: fields.path, path_with_percent_escapes: fields.path_with_percent_escapes, }) } } #[cfg(feature = "serde")] fn encode_legacy_http_path(path: &[u8]) -> BString { let mut out = Vec::with_capacity(path.len()); let mut start = 0; let mut pos = 0; while pos + 2 < path.len() { if path[pos] == b'%' && path[pos + 1].is_ascii_hexdigit() && path[pos + 2].is_ascii_hexdigit() { out.extend( percent_encoding::percent_encode(&path[start..pos], HTTP_PATH_ENCODE_SET) .to_string() .bytes(), ); out.extend_from_slice(&path[pos..pos + 3]); pos += 3; start = pos; } else { pos += 1; } } out.extend( percent_encoding::percent_encode(&path[start..], HTTP_PATH_ENCODE_SET) .to_string() .bytes(), ); out.into() } /// Instantiation impl Url { /// Create an instance from the given parts and validate it by serializing and parsing it back. /// /// For HTTP and HTTPS, `path` is decoded data: literal percent signs are encoded during serialization, and an empty /// path is normalized to `/`. Other schemes interpret `path` according to their serialized syntax. /// `serialize_alternative_form` merely requests alternative form; passwords, ports, and unsupported schemes force /// canonical URL serialization. /// /// # Panics /// /// Panics if the supplied parts cannot be serialized before validation, such as a user without a host. 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> { if let Scheme::Helper(name) = &scheme && !parse::is_valid_remote_helper_name(name.as_bytes()) { return Err(parse::Error::InvalidRemoteHelperName { name: name.clone() }); } let is_http = matches!(scheme, Scheme::Http | Scheme::Https); let mut parsed = parse( Url { scheme, user, password, host, port, path: path.clone(), serialize_alternative_form, path_with_percent_escapes: None, } .to_bstring(), )?; if is_http { // Preserve the caller's path as decoded data, except for an empty path normalized to `/` above. In // particular, percent escapes supplied through `from_parts()` are literal text and must be encoded. if !path.is_empty() { parsed.path = path; } parsed.path_with_percent_escapes = None; } Ok(parsed) } } /// Modification impl Url { /// Set the given `user`, or unset it with `None`. Return the previous value. pub fn set_user(&mut self, user: Option<String>) -> Option<String> { let prev = self.user.take(); self.user = user; prev } /// Set the given `password`, or unset it with `None`. Return the previous value. pub fn set_password(&mut self, password: Option<String>) -> Option<String> { let prev = self.password.take(); self.password = password; prev } } /// Builder impl Url { /// Request alternative serialization, e.g. `file:///path` becomes `/path`. /// /// Parsed URLs set this automatically. Alternative form is used only for SSH or file locations without a password /// or port; all other values serialize in canonical URL form. /// /// Setting `use_alternate_form` to `false` requests canonical, URL-like, serialization. SCP-like form is retained if /// canonical SSH form would change a relative repository path. Remote-helper form is always retained because URL /// form would change the address Git passes to the helper. pub fn with_request_alternate_form(mut self, use_alternate_form: bool) -> Self { self.serialize_alternative_form = use_alternate_form; self } /// Resolve the path of a file location against `current_dir` and normalize it in place. /// /// Other schemes are unchanged. 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(()) } } /// Access impl Url { /// Return the username mentioned in the URL, if present. /// /// # Security Warning /// /// URLs allow usernames to start with `-` which makes it possible to mask command-line arguments as username which then leads to /// the invocation of programs from an attacker controlled URL. See <https://secure.phabricator.com/T12961> for details. /// /// If this value is ever going to be passed to a command-line application, call [Self::user_argument_safe()] instead. pub fn user(&self) -> Option<&str> { self.user.as_deref() } /// Classify the username of this URL by whether it is safe to pass as a command-line argument. /// /// Use this method instead of [Self::user()] if the username is going to be passed to a command-line application. /// If the unsafe and absent cases need not be distinguished, [Self::user_argument_safe()] may also be used. 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, } } /// Return the username of this URL if present *and* if it can't be mistaken for a command-line argument. /// /// Use this method or [Self::user_as_argument()] instead of [Self::user()] if the username is going to be /// passed to a command-line application. Prefer [Self::user_as_argument()] unless the unsafe and absent /// cases need not be distinguished from each other. pub fn user_argument_safe(&self) -> Option<&str> { match self.user_as_argument() { ArgumentSafety::Usable(user) => Some(user), _ => None, } } /// Return the password mentioned in the url, if present. pub fn password(&self) -> Option<&str> { self.password.as_deref() } /// Return the host mentioned in the URL, if present. /// /// # Security Warning /// /// URLs allow hosts to start with `-` which makes it possible to mask command-line arguments as host which then leads to /// the invocation of programs from an attacker controlled URL. See <https://secure.phabricator.com/T12961> for details. /// /// If this value is ever going to be passed to a command-line application, call [Self::host_as_argument()] /// or [Self::host_argument_safe()] instead. pub fn host(&self) -> Option<&str> { self.host.as_deref() } /// Classify the host of this URL by whether it is safe to pass as a command-line argument. /// /// Use this method instead of [Self::host()] if the host is going to be passed to a command-line application. /// If the unsafe and absent cases need not be distinguished, [Self::host_argument_safe()] may also be used. 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, } } /// Return the host of this URL if present *and* if it can't be mistaken for a command-line argument. /// /// Use this method or [Self::host_as_argument()] instead of [Self::host()] if the host is going to be /// passed to a command-line application. Prefer [Self::host_as_argument()] unless the unsafe and absent /// cases need not be distinguished from each other. pub fn host_argument_safe(&self) -> Option<&str> { match self.host_as_argument() { ArgumentSafety::Usable(host) => Some(host), _ => None, } } fn path_with_percent_escapes(&self) -> Option<&BStr> { let encoded = self.path_with_percent_escapes.as_ref()?; percent_encoding::percent_decode(encoded) .eq(self.path.iter().copied()) .then_some(encoded.as_ref()) } /// Return the original percent-escaped path if [Self::path] wasn't changed in the meantime, or [`Self::path`] otherwise. pub fn original_path(&self) -> &BStr { self.path_with_percent_escapes().unwrap_or(self.path.as_ref()) } /// Return a slash-prefixed path if the bytes after the slash can't be mistaken for a command-line argument. /// /// The leading slash must be present and must be passed to the command. Empty and non-slash-prefixed paths return /// `None`; validate those according to how they will be passed. pub fn path_argument_safe(&self) -> Option<&BStr> { self.path .strip_prefix(b"/") .and_then(|truncated| (!looks_like_command_line_option(truncated)).then_some(self.path.as_ref())) } /// Return true if the path portion of the URL is `/`. pub fn path_is_root(&self) -> bool { self.path == "/" } /// Return the actual or default port for use according to the URL scheme. /// Note that there may be no default port either. 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,