git_url/
lib.rs

1//! A library implementing a URL for use in git with access to its special capabilities.
2//! ## Feature Flags
3#![cfg_attr(
4    feature = "document-features",
5    cfg_attr(doc, doc = ::document_features::document_features!())
6)]
7#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
8#![deny(rust_2018_idioms, missing_docs)]
9#![forbid(unsafe_code)]
10
11use bstr::{BStr, BString};
12
13///
14pub mod parse;
15#[doc(inline)]
16pub use parse::parse;
17
18///
19pub mod expand_path;
20#[doc(inline)]
21pub use expand_path::expand_path;
22
23mod scheme;
24pub use scheme::Scheme;
25
26/// A URL with support for specialized git related capabilities.
27///
28/// Additionally there is support for [deserialization][Url::from_bytes()] and serialization
29/// (_see the `Display::fmt()` implementation_).
30///
31/// # Deviation
32///
33/// Note that we do not support passing the password using the URL as it's likely leading to accidents.
34#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
35#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
36pub struct Url {
37    /// The URL scheme.
38    pub scheme: Scheme,
39    /// The user to impersonate on the remote.
40    user: Option<String>,
41    /// The host to which to connect. Localhost is implied if `None`.
42    host: Option<String>,
43    /// When serializing, use the alternative forms as it was parsed as such.
44    serialize_alternative_form: bool,
45    /// The port to use when connecting to a host. If `None`, standard ports depending on `scheme` will be used.
46    pub port: Option<u16>,
47    /// The path portion of the URL, usually the location of the git repository.
48    pub path: bstr::BString,
49}
50
51/// Instantiation
52impl Url {
53    /// Create a new instance from the given parts, which will be validated by parsing them back.
54    pub fn from_parts(
55        scheme: Scheme,
56        user: Option<String>,
57        host: Option<String>,
58        port: Option<u16>,
59        path: BString,
60    ) -> Result<Self, parse::Error> {
61        parse(
62            Url {
63                scheme,
64                user,
65                host,
66                port,
67                path,
68                serialize_alternative_form: false,
69            }
70            .to_bstring()
71            .as_ref(),
72        )
73    }
74
75    /// Create a new instance from the given parts, which will be validated by parsing them back from its alternative form.
76    pub fn from_parts_as_alternative_form(
77        scheme: Scheme,
78        user: Option<String>,
79        host: Option<String>,
80        port: Option<u16>,
81        path: BString,
82    ) -> Result<Self, parse::Error> {
83        parse(
84            Url {
85                scheme,
86                user,
87                host,
88                port,
89                path,
90                serialize_alternative_form: true,
91            }
92            .to_bstring()
93            .as_ref(),
94        )
95    }
96}
97
98/// Modification
99impl Url {
100    /// Set the given `user`, with `None` unsetting it. Returns the previous value.
101    pub fn set_user(&mut self, user: Option<String>) -> Option<String> {
102        let prev = self.user.take();
103        self.user = user;
104        prev
105    }
106}
107
108/// Builder
109impl Url {
110    /// Enable alternate serialization for this url, e.g. `file:///path` becomes `/path`.
111    ///
112    /// This is automatically set correctly for parsed URLs, but can be set here for urls
113    /// created by constructor.
114    pub fn serialize_alternate_form(mut self, use_alternate_form: bool) -> Self {
115        self.serialize_alternative_form = use_alternate_form;
116        self
117    }
118
119    /// Turn a file url like `file://relative` into `file:///root/relative`, hence it assures the url's path component is absolute.
120    pub fn canonicalize(&mut self) -> Result<(), git_path::realpath::Error> {
121        if self.scheme == Scheme::File {
122            let path = git_path::from_bstr(self.path.as_ref());
123            let abs_path = git_path::realpath(path)?;
124            self.path = git_path::into_bstr(abs_path).into_owned();
125        }
126        Ok(())
127    }
128}
129
130/// Access
131impl Url {
132    /// Returns the user mentioned in the url, if present.
133    pub fn user(&self) -> Option<&str> {
134        self.user.as_deref()
135    }
136    /// Returns the host mentioned in the url, if present.
137    pub fn host(&self) -> Option<&str> {
138        self.host.as_deref()
139    }
140    /// Returns true if the path portion of the url is `/`.
141    pub fn path_is_root(&self) -> bool {
142        self.path == "/"
143    }
144    /// Returns the actual or default port for use according to the url scheme.
145    /// Note that there may be no default port either.
146    pub fn port_or_default(&self) -> Option<u16> {
147        self.port.or_else(|| {
148            use Scheme::*;
149            Some(match self.scheme {
150                Http => 80,
151                Https => 443,
152                Ssh => 22,
153                Git => 9418,
154                File | Ext(_) => return None,
155            })
156        })
157    }
158}
159
160/// Transformation
161impl Url {
162    /// Turn a file url like `file://relative` into `file:///root/relative`, hence it assures the url's path component is absolute.
163    pub fn canonicalized(&self) -> Result<Self, git_path::realpath::Error> {
164        let mut res = self.clone();
165        res.canonicalize()?;
166        Ok(res)
167    }
168}
169
170/// Serialization
171impl Url {
172    /// Write this URL losslessly to `out`, ready to be parsed again.
173    pub fn write_to(&self, mut out: impl std::io::Write) -> std::io::Result<()> {
174        if !(self.serialize_alternative_form && (self.scheme == Scheme::File || self.scheme == Scheme::Ssh)) {
175            out.write_all(self.scheme.as_str().as_bytes())?;
176            out.write_all(b"://")?;
177        }
178        match (&self.user, &self.host) {
179            (Some(user), Some(host)) => {
180                out.write_all(user.as_bytes())?;
181                out.write_all(&[b'@'])?;
182                out.write_all(host.as_bytes())?;
183            }
184            (None, Some(host)) => {
185                out.write_all(host.as_bytes())?;
186            }
187            (None, None) => {}
188            (Some(_user), None) => unreachable!("BUG: should not be possible to have a user but no host"),
189        };
190        if let Some(port) = &self.port {
191            write!(&mut out, ":{port}")?;
192        }
193        if self.serialize_alternative_form && self.scheme == Scheme::Ssh {
194            out.write_all(b":")?;
195        }
196        out.write_all(&self.path)?;
197        Ok(())
198    }
199
200    /// Transform ourselves into a binary string, losslessly, or fail if the URL is malformed due to host or user parts being incorrect.
201    pub fn to_bstring(&self) -> bstr::BString {
202        let mut buf = Vec::with_capacity(
203            (5 + 3)
204                + self.user.as_ref().map(|n| n.len()).unwrap_or_default()
205                + 1
206                + self.host.as_ref().map(|h| h.len()).unwrap_or_default()
207                + self.port.map(|_| 5).unwrap_or_default()
208                + self.path.len(),
209        );
210        self.write_to(&mut buf).expect("io cannot fail in memory");
211        buf.into()
212    }
213}
214
215/// Deserialization
216impl Url {
217    /// Parse a URL from `bytes`
218    pub fn from_bytes(bytes: &BStr) -> Result<Self, parse::Error> {
219        parse(bytes)
220    }
221}
222
223mod impls;