//! A library implementing a URL for use in git with access to its special capabilities. //! ## Feature Flags #![cfg_attr( feature = "document-features", cfg_attr(doc, doc = ::document_features::document_features!()) )] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] #![deny(rust_2018_idioms, missing_docs)] #![forbid(unsafe_code)] use bstr::{BStr, BString}; /// pub mod parse; #[doc(inline)] pub use parse::parse; /// pub mod expand_path; #[doc(inline)] pub use expand_path::expand_path; mod scheme; pub use scheme::Scheme; /// A URL with support for specialized git related capabilities. /// /// Additionally there is support for [deserialization][Url::from_bytes()] and serialization /// (_see the `Display::fmt()` implementation_). /// /// # Deviation /// /// Note that we do not support passing the password using the URL as it's likely leading to accidents. #[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. user: Option, /// The password associated with a user. password: Option, /// The host to which to connect. Localhost is implied if `None`. host: Option, /// When serializing, use the alternative forms as it was parsed as such. 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, /// The path portion of the URL, usually the location of the git repository. pub path: bstr::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, password: Option, host: Option, port: Option, path: BString, serialize_alternative_form: bool, ) -> Result { parse( Url { scheme, user, password, host, port, path, serialize_alternative_form, } .to_bstring() .as_ref(), ) } } /// Modification impl Url { /// Set the given `user`, with `None` unsetting it. Returns the previous value. pub fn set_user(&mut self, user: Option) -> Option { let prev = self.user.take(); self.user = user; 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. 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. pub fn canonicalize(&mut self) -> Result<(), gix_path::realpath::Error> { if self.scheme == Scheme::File { let path = gix_path::from_bstr(self.path.as_ref()); let abs_path = gix_path::realpath(path)?; self.path = gix_path::into_bstr(abs_path).into_owned(); } Ok(()) } } /// Access impl Url { /// Returns the user mentioned in the url, if present. pub fn user(&self) -> Option<&str> { self.user.as_deref() } /// Returns the password mentioned in the url, if present. pub fn password(&self) -> Option<&str> { self.password.as_deref() } /// Returns the host mentioned in the url, if present. pub fn host(&self) -> Option<&str> { self.host.as_deref() } /// Returns true if the path portion of the url is `/`. pub fn path_is_root(&self) -> bool { self.path == "/" } /// Returns 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 { self.port.or_else(|| { use Scheme::*; Some(match self.scheme { Http => 80, Https => 443, Ssh => 22, Git => 9418, File | Ext(_) => return None, }) }) } } /// Transformation impl Url { /// Turn a file url like `file://relative` into `file:///root/relative`, hence it assures the url's path component is absolute. pub fn canonicalized(&self) -> Result { let mut res = self.clone(); res.canonicalize()?; Ok(res) } } /// Serialization impl Url { /// Write this URL losslessly to `out`, ready to be parsed again. pub fn write_to(&self, mut out: impl std::io::Write) -> std::io::Result<()> { if !(self.serialize_alternative_form && (self.scheme == Scheme::File || self.scheme == Scheme::Ssh)) { out.write_all(self.scheme.as_str().as_bytes())?; out.write_all(b"://")?; } match (&self.user, &self.host) { (Some(user), Some(host)) => { out.write_all(user.as_bytes())?; if let Some(password) = &self.password { out.write_all(&[b':'])?; out.write_all(password.as_bytes())?; } out.write_all(&[b'@'])?; out.write_all(host.as_bytes())?; } (None, Some(host)) => { out.write_all(host.as_bytes())?; } (None, None) => {} (Some(_user), None) => unreachable!("BUG: should not be possible to have a user but no host"), }; if let Some(port) = &self.port { write!(&mut out, ":{port}")?; } if self.serialize_alternative_form && self.scheme == Scheme::Ssh { out.write_all(b":")?; } out.write_all(&self.path)?; Ok(()) } /// Transform ourselves into a binary string, losslessly, or fail if the URL is malformed due to host or user parts being incorrect. pub fn to_bstring(&self) -> bstr::BString { let mut buf = Vec::with_capacity( (5 + 3) + self.user.as_ref().map(|n| n.len()).unwrap_or_default() + 1 + self.host.as_ref().map(|h| h.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() } } /// Deserialization impl Url { /// Parse a URL from `bytes` pub fn from_bytes(bytes: &BStr) -> Result { parse(bytes) } } mod impls;