diff options
Diffstat (limited to 'vendor/gix-transport/src/client/capabilities.rs')
-rw-r--r-- | vendor/gix-transport/src/client/capabilities.rs | 307 |
1 files changed, 307 insertions, 0 deletions
diff --git a/vendor/gix-transport/src/client/capabilities.rs b/vendor/gix-transport/src/client/capabilities.rs new file mode 100644 index 000000000..4c10dc100 --- /dev/null +++ b/vendor/gix-transport/src/client/capabilities.rs @@ -0,0 +1,307 @@ +use bstr::{BStr, BString, ByteSlice}; + +#[cfg(any(feature = "blocking-client", feature = "async-client"))] +use crate::client; +use crate::Protocol; + +/// The error used in [`Capabilities::from_bytes()`] and [`Capabilities::from_lines()`]. +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum Error { + #[error("Capabilities were missing entirely as there was no 0 byte")] + MissingDelimitingNullByte, + #[error("there was not a single capability behind the delimiter")] + NoCapabilities, + #[error("a version line was expected, but none was retrieved")] + MissingVersionLine, + #[error("expected 'version X', got {0:?}")] + MalformattedVersionLine(BString), + #[error("Got unsupported version {actual:?}, expected {}", *desired as u8)] + UnsupportedVersion { desired: Protocol, actual: BString }, + #[error("An IO error occurred while reading V2 lines")] + Io(#[from] std::io::Error), +} + +/// A structure to represent multiple [capabilities][Capability] or features supported by the server. +#[derive(Debug, Clone, Default)] +#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] +pub struct Capabilities { + data: BString, + value_sep: u8, +} + +/// The name of a single capability. +pub struct Capability<'a>(&'a BStr); + +impl<'a> Capability<'a> { + /// Returns the name of the capability. + /// + /// Most capabilities only consist of a name, making them appear like a feature toggle. + pub fn name(&self) -> &'a BStr { + self.0 + .splitn(2, |b| *b == b'=') + .next() + .expect("there is always a single item") + .as_bstr() + } + /// Returns the value associated with the capability. + /// + /// Note that the caller must know whether a single or multiple values are expected, in which + /// case [`values()`][Capability::values()] should be called. + pub fn value(&self) -> Option<&'a BStr> { + self.0.splitn(2, |b| *b == b'=').nth(1).map(|s| s.as_bstr()) + } + /// Returns the values of a capability if its [`value()`][Capability::value()] is space separated. + pub fn values(&self) -> Option<impl Iterator<Item = &'a BStr>> { + self.value().map(|v| v.split(|b| *b == b' ').map(|s| s.as_bstr())) + } + /// Returns true if its space-separated [`value()`][Capability::value()] contains the given `want`ed capability. + pub fn supports(&self, want: impl Into<&'a BStr>) -> Option<bool> { + let want = want.into(); + self.values().map(|mut iter| iter.any(|v| v == want)) + } +} + +impl Capabilities { + /// Parse capabilities from the given `bytes`. + /// + /// Useful in case they are encoded within a `ref` behind a null byte. + pub fn from_bytes(bytes: &[u8]) -> Result<(Capabilities, usize), Error> { + let delimiter_pos = bytes.find_byte(0).ok_or(Error::MissingDelimitingNullByte)?; + if delimiter_pos + 1 == bytes.len() { + return Err(Error::NoCapabilities); + } + let capabilities = &bytes[delimiter_pos + 1..]; + Ok(( + Capabilities { + data: capabilities.as_bstr().to_owned(), + value_sep: b' ', + }, + delimiter_pos, + )) + } + + /// Parse capabilities from the given a `lines_buf` which is expected to be all newline separated lines + /// from the server. + /// + /// Useful for parsing capabilities from a data sent from a server, and to avoid having to deal with + /// blocking and async traits for as long as possible. There is no value in parsing a few bytes + /// in a non-blocking fashion. + pub fn from_lines(lines_buf: BString) -> Result<Capabilities, Error> { + let mut lines = <_ as bstr::ByteSlice>::lines(lines_buf.as_slice().trim()); + let version_line = lines.next().ok_or(Error::MissingVersionLine)?; + let (name, value) = version_line.split_at( + version_line + .find(b" ") + .ok_or_else(|| Error::MalformattedVersionLine(version_line.to_owned().into()))?, + ); + if name != b"version" { + return Err(Error::MalformattedVersionLine(version_line.to_owned().into())); + } + if value != b" 2" { + return Err(Error::UnsupportedVersion { + desired: Protocol::V2, + actual: value.to_owned().into(), + }); + } + Ok(Capabilities { + value_sep: b'\n', + data: lines.as_bytes().into(), + }) + } + + /// Returns true of the given `feature` is mentioned in this list of capabilities. + pub fn contains(&self, feature: &str) -> bool { + self.capability(feature).is_some() + } + + /// Returns the capability with `name`. + pub fn capability(&self, name: &str) -> Option<Capability<'_>> { + self.iter().find(|c| c.name() == name.as_bytes().as_bstr()) + } + + /// Returns an iterator over all capabilities. + pub fn iter(&self) -> impl Iterator<Item = Capability<'_>> { + self.data + .split(move |b| *b == self.value_sep) + .map(|c| Capability(c.as_bstr())) + } +} + +/// internal use +#[cfg(any(feature = "blocking-client", feature = "async-client"))] +impl Capabilities { + fn extract_protocol(capabilities_or_version: gix_packetline::TextRef<'_>) -> Result<Protocol, client::Error> { + let line = capabilities_or_version.as_bstr(); + let version = if line.starts_with_str("version ") { + if line.len() != "version X".len() { + return Err(client::Error::UnsupportedProtocolVersion(line.as_bstr().into())); + } + match line { + line if line.ends_with_str("1") => Protocol::V1, + line if line.ends_with_str("2") => Protocol::V2, + _ => return Err(client::Error::UnsupportedProtocolVersion(line.as_bstr().into())), + } + } else { + Protocol::V1 + }; + Ok(version) + } +} + +#[cfg(feature = "blocking-client")] +/// +pub mod recv { + use std::io; + + use bstr::ByteVec; + + use crate::{client, client::Capabilities, Protocol}; + + /// Success outcome of [`Capabilities::from_lines_with_version_detection`]. + pub struct Outcome<'a> { + /// The [`Capabilities`] the remote advertised. + pub capabilities: Capabilities, + /// The remote refs as a [`io::BufRead`]. + /// + /// This is `Some` only when protocol v1 is used. The [`io::BufRead`] must be exhausted by + /// the caller. + pub refs: Option<Box<dyn crate::client::ReadlineBufRead + 'a>>, + /// The [`Protocol`] the remote advertised. + pub protocol: Protocol, + } + + impl Capabilities { + /// Read the capabilities and version advertisement from the given packetline reader. + /// + /// If [`Protocol::V1`] was requested, or the remote decided to downgrade, the remote refs + /// advertisement will also be included in the [`Outcome`]. + pub fn from_lines_with_version_detection<T: io::Read>( + rd: &mut gix_packetline::StreamingPeekableIter<T>, + ) -> Result<Outcome<'_>, client::Error> { + // NOTE that this is vitally important - it is turned on and stays on for all following requests so + // we automatically abort if the server sends an ERR line anywhere. + // We are sure this can't clash with binary data when sent due to the way the PACK + // format looks like, thus there is no binary blob that could ever look like an ERR line by accident. + rd.fail_on_err_lines(true); + + let line = rd + .peek_line() + .ok_or(client::Error::ExpectedLine("capabilities or version"))???; + let line = line.as_text().ok_or(client::Error::ExpectedLine("text"))?; + + let version = Capabilities::extract_protocol(line)?; + match version { + Protocol::V1 => { + let (capabilities, delimiter_position) = Capabilities::from_bytes(line.0)?; + rd.peek_buffer_replace_and_truncate(delimiter_position, b'\n'); + Ok(Outcome { + capabilities, + refs: Some(Box::new(rd.as_read())), + protocol: Protocol::V1, + }) + } + Protocol::V2 => Ok(Outcome { + capabilities: { + let mut rd = rd.as_read(); + let mut buf = Vec::new(); + while let Some(line) = rd.read_data_line() { + let line = line??; + match line.as_bstr() { + Some(line) => { + buf.push_str(line); + if buf.last() != Some(&b'\n') { + buf.push(b'\n'); + } + } + None => break, + } + } + Capabilities::from_lines(buf.into())? + }, + refs: None, + protocol: Protocol::V2, + }), + } + } + } +} + +#[cfg(feature = "async-client")] +#[allow(missing_docs)] +/// +pub mod recv { + use bstr::ByteVec; + use futures_io::AsyncRead; + + use crate::{client, client::Capabilities, Protocol}; + + /// Success outcome of [`Capabilities::from_lines_with_version_detection`]. + pub struct Outcome<'a> { + /// The [`Capabilities`] the remote advertised. + pub capabilities: Capabilities, + /// The remote refs as an [`AsyncBufRead`]. + /// + /// This is `Some` only when protocol v1 is used. The [`AsyncBufRead`] must be exhausted by + /// the caller. + pub refs: Option<Box<dyn crate::client::ReadlineBufRead + Unpin + 'a>>, + /// The [`Protocol`] the remote advertised. + pub protocol: Protocol, + } + + impl Capabilities { + /// Read the capabilities and version advertisement from the given packetline reader. + /// + /// If [`Protocol::V1`] was requested, or the remote decided to downgrade, the remote refs + /// advertisement will also be included in the [`Outcome`]. + pub async fn from_lines_with_version_detection<T: AsyncRead + Unpin>( + rd: &mut gix_packetline::StreamingPeekableIter<T>, + ) -> Result<Outcome<'_>, client::Error> { + // NOTE that this is vitally important - it is turned on and stays on for all following requests so + // we automatically abort if the server sends an ERR line anywhere. + // We are sure this can't clash with binary data when sent due to the way the PACK + // format looks like, thus there is no binary blob that could ever look like an ERR line by accident. + rd.fail_on_err_lines(true); + + let line = rd + .peek_line() + .await + .ok_or(client::Error::ExpectedLine("capabilities or version"))???; + let line = line.as_text().ok_or(client::Error::ExpectedLine("text"))?; + + let version = Capabilities::extract_protocol(line)?; + match version { + Protocol::V1 => { + let (capabilities, delimiter_position) = Capabilities::from_bytes(line.0)?; + rd.peek_buffer_replace_and_truncate(delimiter_position, b'\n'); + Ok(Outcome { + capabilities, + refs: Some(Box::new(rd.as_read())), + protocol: Protocol::V1, + }) + } + Protocol::V2 => Ok(Outcome { + capabilities: { + let mut rd = rd.as_read(); + let mut buf = Vec::new(); + while let Some(line) = rd.read_data_line().await { + let line = line??; + match line.as_bstr() { + Some(line) => { + buf.push_str(line); + if buf.last() != Some(&b'\n') { + buf.push(b'\n'); + } + } + None => break, + } + } + Capabilities::from_lines(buf.into())? + }, + refs: None, + protocol: Protocol::V2, + }), + } + } + } +} |