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. /// /// ### Deviation /// /// As a *shortcoming*, we are unable to parse `V1` as emitted from `git-upload-pack` without a `git-daemon` or server, /// as it will not emit any capabilities for some reason. Only `V2` and `V0` work in that context. #[derive(Debug, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Capabilities { data: BString, value_sep: u8, } /// This implementation yields exactly those minimal capabilities that are required for `gix` to work, nothing more and nothing less. /// /// This is a bit of a hack just get tests with Protocol V0 to work, which is a good way to enforce stateful transports. /// Of course, V1 would also do that but when calling `git-upload-pack` directly, it advertises so badly that this is easier to implement. impl Default for Capabilities { fn default() -> Self { Capabilities::from_lines("version 2\nmulti_ack_detailed\nside-band-64k\n".into()) .expect("valid format, known at compile time") } } /// 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(ByteSlice::as_bstr) } /// Returns the values of a capability if its [`value()`][Capability::value()] is space separated. pub fn values(&self) -> Option> { self.value().map(|v| v.split(|b| *b == b' ').map(ByteSlice::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 { 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 { 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> { self.iter().find(|c| c.name() == name.as_bytes().as_bstr()) } /// Returns an iterator over all capabilities. pub fn iter(&self) -> impl Iterator> { 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 { 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>, /// 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( rd: &mut gix_packetline::StreamingPeekableIter, ) -> Result, 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); Ok(match rd.peek_line() { Some(line) => { let line = line??.as_text().ok_or(client::Error::ExpectedLine("text"))?; let version = Capabilities::extract_protocol(line)?; match version { Protocol::V0 => unreachable!("already handled in `None` case"), Protocol::V1 => { let (capabilities, delimiter_position) = Capabilities::from_bytes(line.0)?; rd.peek_buffer_replace_and_truncate(delimiter_position, b'\n'); Outcome { capabilities, refs: Some(Box::new(rd.as_read())), protocol: Protocol::V1, } } Protocol::V2 => 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, }, } } None => Outcome { capabilities: Capabilities::default(), refs: Some(Box::new(rd.as_read())), protocol: Protocol::V0, }, }) } } } #[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>, /// 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( rd: &mut gix_packetline::StreamingPeekableIter, ) -> Result, 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); Ok(match rd.peek_line().await { Some(line) => { let line = line??.as_text().ok_or(client::Error::ExpectedLine("text"))?; let version = Capabilities::extract_protocol(line)?; match version { Protocol::V0 => unreachable!("already handled in `None` case"), Protocol::V1 => { let (capabilities, delimiter_position) = Capabilities::from_bytes(line.0)?; rd.peek_buffer_replace_and_truncate(delimiter_position, b'\n'); Outcome { capabilities, refs: Some(Box::new(rd.as_read())), protocol: Protocol::V1, } } Protocol::V2 => 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, }, } } None => Outcome { capabilities: Capabilities::default(), refs: Some(Box::new(rd.as_read())), protocol: Protocol::V0, }, }) } } }