gix_transport/client/
capabilities.rs

1use bstr::{BStr, BString, ByteSlice};
2
3#[cfg(any(feature = "blocking-client", feature = "async-client"))]
4use crate::client;
5use crate::Protocol;
6
7/// The error used in [`Capabilities::from_bytes()`] and [`Capabilities::from_lines()`].
8#[derive(Debug, thiserror::Error)]
9#[allow(missing_docs)]
10pub enum Error {
11    #[error("Capabilities were missing entirely as there was no 0 byte")]
12    MissingDelimitingNullByte,
13    #[error("there was not a single capability behind the delimiter")]
14    NoCapabilities,
15    #[error("a version line was expected, but none was retrieved")]
16    MissingVersionLine,
17    #[error("expected 'version X', got {0:?}")]
18    MalformattedVersionLine(BString),
19    #[error("Got unsupported version {actual:?}, expected {}", *desired as u8)]
20    UnsupportedVersion { desired: Protocol, actual: BString },
21    #[error("An IO error occurred while reading V2 lines")]
22    Io(#[from] std::io::Error),
23}
24
25/// A structure to represent multiple [capabilities](Capability) or features supported by the server.
26///
27/// ### Deviation
28///
29/// As a *shortcoming*, we are unable to parse `V1` as emitted from `git-upload-pack` without a `git-daemon` or server,
30/// as it will not emit any capabilities for some reason. Only `V2` and `V0` work in that context.
31#[derive(Debug, Clone)]
32#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
33pub struct Capabilities {
34    data: BString,
35    value_sep: u8,
36}
37
38/// This implementation yields exactly those minimal capabilities that are required for `gix` to work, nothing more and nothing less.
39///
40/// This is a bit of a hack just get tests with Protocol V0 to work, which is a good way to enforce stateful transports.
41/// Of course, V1 would also do that but when calling `git-upload-pack` directly, it advertises so badly that this is easier to implement.
42impl Default for Capabilities {
43    fn default() -> Self {
44        Capabilities::from_lines("version 2\nmulti_ack_detailed\nside-band-64k\n".into())
45            .expect("valid format, known at compile time")
46    }
47}
48
49/// The name of a single capability.
50pub struct Capability<'a>(&'a BStr);
51
52impl<'a> Capability<'a> {
53    /// Returns the name of the capability.
54    ///
55    /// Most capabilities only consist of a name, making them appear like a feature toggle.
56    pub fn name(&self) -> &'a BStr {
57        self.0
58            .splitn(2, |b| *b == b'=')
59            .next()
60            .expect("there is always a single item")
61            .as_bstr()
62    }
63    /// Returns the value associated with the capability.
64    ///
65    /// Note that the caller must know whether a single or multiple values are expected, in which
66    /// case [`values()`](Capability::values()) should be called.
67    pub fn value(&self) -> Option<&'a BStr> {
68        self.0.splitn(2, |b| *b == b'=').nth(1).map(ByteSlice::as_bstr)
69    }
70    /// Returns the values of a capability if its [`value()`](Capability::value()) is space separated.
71    pub fn values(&self) -> Option<impl Iterator<Item = &'a BStr>> {
72        self.value().map(|v| v.split(|b| *b == b' ').map(ByteSlice::as_bstr))
73    }
74    /// Returns true if its space-separated [`value()`](Capability::value()) contains the given `want`ed capability.
75    pub fn supports(&self, want: impl Into<&'a BStr>) -> Option<bool> {
76        let want = want.into();
77        self.values().map(|mut iter| iter.any(|v| v == want))
78    }
79}
80
81impl Capabilities {
82    /// Parse capabilities from the given `bytes`.
83    ///
84    /// Useful in case they are encoded within a `ref` behind a null byte.
85    pub fn from_bytes(bytes: &[u8]) -> Result<(Capabilities, usize), Error> {
86        let delimiter_pos = bytes.find_byte(0).ok_or(Error::MissingDelimitingNullByte)?;
87        if delimiter_pos + 1 == bytes.len() {
88            return Err(Error::NoCapabilities);
89        }
90        let capabilities = &bytes[delimiter_pos + 1..];
91        Ok((
92            Capabilities {
93                data: capabilities.as_bstr().to_owned(),
94                value_sep: b' ',
95            },
96            delimiter_pos,
97        ))
98    }
99
100    /// Parse capabilities from the given a `lines_buf` which is expected to be all newline separated lines
101    /// from the server.
102    ///
103    /// Useful for parsing capabilities from a data sent from a server, and to avoid having to deal with
104    /// blocking and async traits for as long as possible. There is no value in parsing a few bytes
105    /// in a non-blocking fashion.
106    pub fn from_lines(lines_buf: BString) -> Result<Capabilities, Error> {
107        let mut lines = <_ as bstr::ByteSlice>::lines(lines_buf.as_slice().trim());
108        let version_line = lines.next().ok_or(Error::MissingVersionLine)?;
109        let (name, value) = version_line.split_at(
110            version_line
111                .find(b" ")
112                .ok_or_else(|| Error::MalformattedVersionLine(version_line.to_owned().into()))?,
113        );
114        if name != b"version" {
115            return Err(Error::MalformattedVersionLine(version_line.to_owned().into()));
116        }
117        if value != b" 2" {
118            return Err(Error::UnsupportedVersion {
119                desired: Protocol::V2,
120                actual: value.to_owned().into(),
121            });
122        }
123        Ok(Capabilities {
124            value_sep: b'\n',
125            data: lines.as_bytes().into(),
126        })
127    }
128
129    /// Returns true of the given `feature` is mentioned in this list of capabilities.
130    pub fn contains(&self, feature: &str) -> bool {
131        self.capability(feature).is_some()
132    }
133
134    /// Returns the capability with `name`.
135    pub fn capability(&self, name: &str) -> Option<Capability<'_>> {
136        self.iter().find(|c| c.name() == name.as_bytes().as_bstr())
137    }
138
139    /// Returns an iterator over all capabilities.
140    pub fn iter(&self) -> impl Iterator<Item = Capability<'_>> {
141        self.data
142            .split(move |b| *b == self.value_sep)
143            .map(|c| Capability(c.as_bstr()))
144    }
145}
146
147/// internal use
148#[cfg(any(feature = "blocking-client", feature = "async-client"))]
149impl Capabilities {
150    fn extract_protocol(capabilities_or_version: gix_packetline::TextRef<'_>) -> Result<Protocol, client::Error> {
151        let line = capabilities_or_version.as_bstr();
152        let version = if line.starts_with_str("version ") {
153            if line.len() != "version X".len() {
154                return Err(client::Error::UnsupportedProtocolVersion(line.as_bstr().into()));
155            }
156            match line {
157                line if line.ends_with_str("1") => Protocol::V1,
158                line if line.ends_with_str("2") => Protocol::V2,
159                _ => return Err(client::Error::UnsupportedProtocolVersion(line.as_bstr().into())),
160            }
161        } else {
162            Protocol::V1
163        };
164        Ok(version)
165    }
166}
167
168///
169#[cfg(feature = "blocking-client")]
170pub mod blocking_recv {
171    use std::io;
172
173    use bstr::ByteVec;
174
175    use crate::{
176        client::{self, blocking_io::ReadlineBufRead, Capabilities},
177        packetline::blocking_io::StreamingPeekableIter,
178        Protocol,
179    };
180
181    /// The information provided by the server upon first connection.
182    pub struct Handshake<'a> {
183        /// The [`Capabilities`] the remote advertised.
184        pub capabilities: Capabilities,
185        /// The remote refs as a [`io::BufRead`].
186        ///
187        /// This is `Some` only when protocol v1 is used. The [`io::BufRead`] must be exhausted by
188        /// the caller.
189        pub refs: Option<Box<dyn ReadlineBufRead + 'a>>,
190        /// The [`Protocol`] the remote advertised.
191        pub protocol: Protocol,
192    }
193
194    impl Handshake<'_> {
195        /// Read the capabilities and version advertisement from the given packetline reader.
196        ///
197        /// If [`Protocol::V1`] was requested, or the remote decided to downgrade, the remote refs
198        /// advertisement will also be included in the [`Handshake`].
199        pub fn from_lines_with_version_detection<T: io::Read>(
200            rd: &mut StreamingPeekableIter<T>,
201        ) -> Result<Handshake<'_>, client::Error> {
202            // NOTE that this is vitally important - it is turned on and stays on for all following requests so
203            // we automatically abort if the server sends an ERR line anywhere.
204            // We are sure this can't clash with binary data when sent due to the way the PACK
205            // format looks like, thus there is no binary blob that could ever look like an ERR line by accident.
206            rd.fail_on_err_lines(true);
207
208            Ok(match rd.peek_line() {
209                Some(line) => {
210                    let line = line??.as_text().ok_or(client::Error::ExpectedLine("text"))?;
211                    let version = Capabilities::extract_protocol(line)?;
212                    match version {
213                        Protocol::V0 => unreachable!("already handled in `None` case"),
214                        Protocol::V1 => {
215                            let (capabilities, delimiter_position) = Capabilities::from_bytes(line.0)?;
216                            rd.peek_buffer_replace_and_truncate(delimiter_position, b'\n');
217                            Handshake {
218                                capabilities,
219                                refs: Some(Box::new(rd.as_read())),
220                                protocol: Protocol::V1,
221                            }
222                        }
223                        Protocol::V2 => Handshake {
224                            capabilities: {
225                                let mut rd = rd.as_read();
226                                let mut buf = Vec::new();
227                                while let Some(line) = rd.read_data_line() {
228                                    let line = line??;
229                                    match line.as_bstr() {
230                                        Some(line) => {
231                                            buf.push_str(line);
232                                            if buf.last() != Some(&b'\n') {
233                                                buf.push(b'\n');
234                                            }
235                                        }
236                                        None => break,
237                                    }
238                                }
239                                Capabilities::from_lines(buf.into())?
240                            },
241                            refs: None,
242                            protocol: Protocol::V2,
243                        },
244                    }
245                }
246                None => Handshake {
247                    capabilities: Capabilities::default(),
248                    refs: Some(Box::new(rd.as_read())),
249                    protocol: Protocol::V0,
250                },
251            })
252        }
253    }
254}
255
256///
257#[cfg(feature = "async-client")]
258#[allow(missing_docs)]
259pub mod async_recv {
260    use bstr::ByteVec;
261    use futures_io::AsyncRead;
262
263    use crate::{
264        client::{self, async_io::ReadlineBufRead, Capabilities},
265        packetline::async_io::StreamingPeekableIter,
266        Protocol,
267    };
268
269    /// The information provided by the server upon first connection.
270    pub struct Handshake<'a> {
271        /// The [`Capabilities`] the remote advertised.
272        pub capabilities: Capabilities,
273        /// The remote refs as an [`AsyncBufRead`].
274        ///
275        /// This is `Some` only when protocol v1 is used. The [`AsyncRead`] must be exhausted by
276        /// the caller.
277        pub refs: Option<Box<dyn ReadlineBufRead + Unpin + 'a>>,
278        /// The [`Protocol`] the remote advertised.
279        pub protocol: Protocol,
280    }
281
282    impl Handshake<'_> {
283        /// Read the capabilities and version advertisement from the given packetline reader.
284        ///
285        /// If [`Protocol::V1`] was requested, or the remote decided to downgrade, the remote refs
286        /// advertisement will also be included in the [`Handshake`].
287        pub async fn from_lines_with_version_detection<T: AsyncRead + Unpin>(
288            rd: &mut StreamingPeekableIter<T>,
289        ) -> Result<Handshake<'_>, client::Error> {
290            // NOTE that this is vitally important - it is turned on and stays on for all following requests so
291            // we automatically abort if the server sends an ERR line anywhere.
292            // We are sure this can't clash with binary data when sent due to the way the PACK
293            // format looks like, thus there is no binary blob that could ever look like an ERR line by accident.
294            rd.fail_on_err_lines(true);
295
296            Ok(match rd.peek_line().await {
297                Some(line) => {
298                    let line = line??.as_text().ok_or(client::Error::ExpectedLine("text"))?;
299                    let version = Capabilities::extract_protocol(line)?;
300                    match version {
301                        Protocol::V0 => unreachable!("already handled in `None` case"),
302                        Protocol::V1 => {
303                            let (capabilities, delimiter_position) = Capabilities::from_bytes(line.0)?;
304                            rd.peek_buffer_replace_and_truncate(delimiter_position, b'\n');
305                            Handshake {
306                                capabilities,
307                                refs: Some(Box::new(rd.as_read())),
308                                protocol: Protocol::V1,
309                            }
310                        }
311                        Protocol::V2 => Handshake {
312                            capabilities: {
313                                let mut rd = rd.as_read();
314                                let mut buf = Vec::new();
315                                while let Some(line) = rd.read_data_line().await {
316                                    let line = line??;
317                                    match line.as_bstr() {
318                                        Some(line) => {
319                                            buf.push_str(line);
320                                            if buf.last() != Some(&b'\n') {
321                                                buf.push(b'\n');
322                                            }
323                                        }
324                                        None => break,
325                                    }
326                                }
327                                Capabilities::from_lines(buf.into())?
328                            },
329                            refs: None,
330                            protocol: Protocol::V2,
331                        },
332                    }
333                }
334                None => Handshake {
335                    capabilities: Capabilities::default(),
336                    refs: Some(Box::new(rd.as_read())),
337                    protocol: Protocol::V0,
338                },
339            })
340        }
341    }
342}