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: git_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 bstr::ByteVec; use std::io; 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 git_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 futures_io::AsyncRead; use crate::{client, client::Capabilities, Protocol}; use bstr::ByteVec; /// 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 git_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, }), } } }}
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: git_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 bstr::ByteVec;
use std::io;
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 git_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 futures_io::AsyncRead;
use crate::{client, client::Capabilities, Protocol};
use bstr::ByteVec;
/// 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 git_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,
}),
}
}
}
}