//! 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".into()).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".into()).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}; ///pub mod expand_path; mod scheme;pub use scheme::Scheme;mod impls; ///pub mod parse; /// Minimal URL parser to replace the `url` crate dependencymod simple_url; /// Parse the given `bytes` as a [git url](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<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), }} /// Expand `path` for the given `user`, which can be obtained by [`parse()`], resolving the home directories/// of `user` 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////// Due to the mutability of this type, it's possible that the URL serializes to something invalid/// when fields are modified directly. URLs should always be parsed to this type from string or byte/// parameters, but never be accepted as an instance of this type and then reconstructed, to maintain/// validity guarantees.////// # 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 lossless serialization, 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 password, whereas [lossless serialization](Url::to_bstring()) will contain all parts of the/// URL./// **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, serde::Deserialize))]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. Cannot be serialized in alternative form (will panic in debug builds). pub password: Option<String>, /// The host to which to connect. Localhost is implied if `None`. /// /// IPv6 addresses are stored *without* brackets for SSH schemes, but *with* brackets for other schemes. /// Brackets are automatically added during serialization when needed (e.g., when a port is specified with an IPv6 host). pub host: Option<String>, /// When serializing, use the alternative forms as it was parsed as such. /// /// Alternative forms include SCP-like syntax (`user@host:path`) and bare file paths. /// When `true`, password and port cannot be serialized (will panic in debug builds). pub serialize_alternative_form: bool, /// The port to use when connecting to a host. If `None`, standard ports depending on `scheme` will be used. pub port: Option<u16>, /// The path portion of the URL, usually the location of the git repository. /// /// Paths are stored in decoded form: percent-encoded characters are decoded during parsing /// and re-encoded during canonical serialization (e.g., `%20` becomes a space in this field). /// /// 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 `/` /// /// 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. /// /// If this value is ever going to be passed to a command-line application, call [Self::path_argument_safe()] instead. pub path: BString,} /// Instantiationimpl Url { /// Create a new instance from the given parts, including a password, which will be validated by parsing them back. 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(), ) }} /// 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 { /// Enable alternate serialization for this url, e.g. `file:///path` becomes `/path`. /// /// This is automatically set correctly for parsed URLs, but can be set here for urls /// created by constructor. /// /// Setting `use_alternate_form` to `false` forces canonical serialization. For SCP-like SSH URLs this /// can be lossy: `user@host:path/repo` and `user@host:/path/repo` both serialize as /// `ssh://user@host/path/repo`, even though Git treats their repository paths as /// `path/repo` and `/path/repo` respectively when invoking the remote service. pub fn serialize_alternate_form(mut self, use_alternate_form: bool) -> Self { self.serialize_alternative_form = use_alternate_form; self } /// Turn a file url like `file://relative` into `file:///root/relative`, hence it assures the url's path component is absolute, /// using `current_dir` if needed to achieve that. 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 host 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 host 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, } } /// Return the path of this URL *if* it can't be mistaken for a command-line argument. /// Note that it always begins with a slash, which is ignored for this comparison. /// /// Use this method instead of accessing [Self::path] directly if the path is going to be passed to a /// command-line application, unless it is certain that the leading `/` will always be included. 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())) } /// 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, File | Ext(_) => return None, }) }) }} fn looks_like_command_line_option(b: &[u8]) -> bool { b.first() == Some(&b'-')} /// Transformationimpl Url { /// Turn a file URL like `file://relative` into `file:///root/relative`, hence it assures the URL's path component is absolute, using /// `current_dir` if necessary. 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) }} /// Serializationimpl Url { /// Write this URL losslessly to `out`, ready to be parsed again. pub fn write_to(&self, out: &mut dyn std::io::Write) -> std::io::Result<()> { // Since alternative form doesn't employ any escape syntax, password and // port number cannot be encoded. 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> { /// Characters that must be percent-encoded in the userinfo component of a URL. /// /// According to RFC 3986, userinfo can contain: /// - unreserved characters: `A-Z a-z 0-9 - . _ ~` /// - percent-encoded characters /// - sub-delims: `! $ & ' ( ) * + , ; =` /// - `:` /// /// This encode-set encodes everything else, particularly `@` (userinfo delimiter), /// `/` `?` `#` (path/query/fragment delimiters), and various other special characters. 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}")?; } // For SSH and Git URLs, add leading '/' if path doesn't start with '/' // This handles paths like "~repo" which serialize as "/~repo" in URL form 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) { // We intentionally do not encode '?' and '#': ParsedUrl keeps them in `path`, // and encoding would change routed endpoints for already parsed URLs. 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();Lines 1–500 of 598. 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".into()).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".into()).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}; /// pub mod expand_path; mod scheme; pub use scheme::Scheme; mod impls; /// pub mod parse; /// Minimal URL parser to replace the `url` crate dependency mod simple_url; /// Parse the given `bytes` as a [git url](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<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), } } /// Expand `path` for the given `user`, which can be obtained by [`parse()`], resolving the home directories /// of `user` 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 /// /// Due to the mutability of this type, it's possible that the URL serializes to something invalid /// when fields are modified directly. URLs should always be parsed to this type from string or byte /// parameters, but never be accepted as an instance of this type and then reconstructed, to maintain /// validity guarantees. /// /// # 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 lossless serialization, 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 password, whereas [lossless serialization](Url::to_bstring()) will contain all parts of the /// URL. /// **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, serde::Deserialize))] 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. Cannot be serialized in alternative form (will panic in debug builds). pub password: Option<String>, /// The host to which to connect. Localhost is implied if `None`. /// /// IPv6 addresses are stored *without* brackets for SSH schemes, but *with* brackets for other schemes. /// Brackets are automatically added during serialization when needed (e.g., when a port is specified with an IPv6 host). pub host: Option<String>, /// When serializing, use the alternative forms as it was parsed as such. /// /// Alternative forms include SCP-like syntax (`user@host:path`) and bare file paths. /// When `true`, password and port cannot be serialized (will panic in debug builds). pub serialize_alternative_form: bool, /// The port to use when connecting to a host. If `None`, standard ports depending on `scheme` will be used. pub port: Option<u16>, /// The path portion of the URL, usually the location of the git repository. /// /// Paths are stored in decoded form: percent-encoded characters are decoded during parsing /// and re-encoded during canonical serialization (e.g., `%20` becomes a space in this field). /// /// 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 `/` /// /// 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. /// /// If this value is ever going to be passed to a command-line application, call [Self::path_argument_safe()] instead. pub path: BString, } /// Instantiation impl Url { /// Create a new instance from the given parts, including a password, which will be validated by parsing them back. 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(), ) } } /// 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 { /// Enable alternate serialization for this url, e.g. `file:///path` becomes `/path`. /// /// This is automatically set correctly for parsed URLs, but can be set here for urls /// created by constructor. /// /// Setting `use_alternate_form` to `false` forces canonical serialization. For SCP-like SSH URLs this /// can be lossy: `user@host:path/repo` and `user@host:/path/repo` both serialize as /// `ssh://user@host/path/repo`, even though Git treats their repository paths as /// `path/repo` and `/path/repo` respectively when invoking the remote service. pub fn serialize_alternate_form(mut self, use_alternate_form: bool) -> Self { self.serialize_alternative_form = use_alternate_form; self } /// Turn a file url like `file://relative` into `file:///root/relative`, hence it assures the url's path component is absolute, /// using `current_dir` if needed to achieve that. 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 host 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 host 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, } } /// Return the path of this URL *if* it can't be mistaken for a command-line argument. /// Note that it always begins with a slash, which is ignored for this comparison. /// /// Use this method instead of accessing [Self::path] directly if the path is going to be passed to a /// command-line application, unless it is certain that the leading `/` will always be included. 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())) } /// 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, File | Ext(_) => return None, }) }) } } fn looks_like_command_line_option(b: &[u8]) -> bool { b.first() == Some(&b'-') } /// Transformation impl Url { /// Turn a file URL like `file://relative` into `file:///root/relative`, hence it assures the URL's path component is absolute, using /// `current_dir` if necessary. 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) } } /// Serialization impl Url { /// Write this URL losslessly to `out`, ready to be parsed again. pub fn write_to(&self, out: &mut dyn std::io::Write) -> std::io::Result<()> { // Since alternative form doesn't employ any escape syntax, password and // port number cannot be encoded. 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> { /// Characters that must be percent-encoded in the userinfo component of a URL. /// /// According to RFC 3986, userinfo can contain: /// - unreserved characters: `A-Z a-z 0-9 - . _ ~` /// - percent-encoded characters /// - sub-delims: `! $ & ' ( ) * + , ; =` /// - `:` /// /// This encode-set encodes everything else, particularly `@` (userinfo delimiter), /// `/` `?` `#` (path/query/fragment delimiters), and various other special characters. 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}")?; } // For SSH and Git URLs, add leading '/' if path doesn't start with '/' // This handles paths like "~repo" which serialize as "/~repo" in URL form 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) { // We intentionally do not encode '?' and '#': ParsedUrl keeps them in `path`, // and encoding would change routed endpoints for already parsed URLs. 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();