git-transport 0.4.0

A WIP crate of the gitoxide project dedicated to implementing the git transport layer
Documentation
use crate::Protocol;
use bstr::{BStr, BString, ByteSlice};
use quick_error::quick_error;
use std::io;

quick_error! {
    #[derive(Debug)]
    pub enum Error {
        MissingDelimitingNullByte {
            display("Capabilities were missing entirely as there was no 0 byte")
        }
        NoCapabilities {
            display("there was not a single capability behind the delimiter")
        }
        MissingVersionLine {
            display("a version line was expected, but none was retrieved")
        }
        MalformattedVersionLine(actual: String) {
            display("expected 'version X', got '{}'", actual)
        }
        UnsupportedVersion(wanted: Protocol, got: String) {
            display("Got unsupported version '{}', expected '{}'", got, *wanted as usize)
        }
        Io(err: io::Error) {
            display("An IO error occurred while reading V2 lines")
            from()
            source(err)
        }
    }
}

#[derive(Debug, Clone)]
pub struct Capabilities {
    data: BString,
    value_sep: u8,
}
pub struct Capability<'a>(&'a BStr);

impl<'a> Capability<'a> {
    pub fn name(&self) -> &BStr {
        self.0
            .splitn(2, |b| *b == b'=')
            .next()
            .expect("there is always a single item")
            .as_bstr()
    }
    pub fn value(&self) -> Option<&BStr> {
        self.0.splitn(2, |b| *b == b'=').nth(1).map(|s| s.as_bstr())
    }
    pub fn values(&self) -> Option<impl Iterator<Item = &BStr>> {
        self.value().map(|v| v.split(|b| *b == b' ').map(|s| s.as_bstr()))
    }
}

impl Capabilities {
    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,
        ))
    }
    pub fn from_lines(read: impl io::BufRead) -> Result<Capabilities, Error> {
        let mut lines = read.lines();
        let version_line = lines.next().ok_or(Error::MissingVersionLine)??;
        let (name, value) = version_line.split_at(
            version_line
                .find(' ')
                .ok_or_else(|| Error::MalformattedVersionLine(version_line.clone()))?,
        );
        if name != "version" {
            return Err(Error::MalformattedVersionLine(version_line));
        }
        if value != " 2" {
            return Err(Error::UnsupportedVersion(Protocol::V2, value.to_owned()));
        }
        Ok(Capabilities {
            value_sep: b'\n',
            data: lines
                .inspect(|l| {
                    if let Ok(l) = l {
                        assert!(
                            !l.contains('\n'),
                            "newlines are not expected in keys or values, got '{}'",
                            l
                        )
                    }
                })
                .collect::<Result<Vec<_>, _>>()?
                .join("\n")
                .into(),
        })
    }

    pub fn contains(&self, feature: &str) -> bool {
        self.capability(feature).is_some()
    }

    pub fn capability(&self, name: &str) -> Option<Capability<'_>> {
        self.iter().find(|c| c.name() == name.as_bytes().as_bstr())
    }

    pub fn iter(&self) -> impl Iterator<Item = Capability<'_>> {
        self.data
            .split(move |b| *b == self.value_sep)
            .map(|c| Capability(c.as_bstr()))
    }
}

pub(crate) mod recv {
    use crate::{client, client::Capabilities, Protocol};
    use bstr::ByteSlice;
    use std::io;

    pub struct Outcome<'a> {
        pub capabilities: Capabilities,
        pub refs: Option<Box<dyn io::BufRead + 'a>>,
        pub protocol: Protocol,
    }

    pub fn v1_or_v2_as_detected<T: io::Read>(
        rd: &mut git_packetline::Provider<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 capabilities_or_version = rd
            .peek_line()
            .ok_or(client::Error::ExpectedLine("capabilities or version"))???;
        let first_line = capabilities_or_version
            .to_text()
            .ok_or(client::Error::ExpectedLine("text"))?;

        let version = if first_line.as_bstr().starts_with_str("version ") {
            if first_line.as_bstr().ends_with_str(" 1") {
                Protocol::V1
            } else {
                Protocol::V2
            }
        } else {
            Protocol::V1
        };
        match version {
            Protocol::V1 => {
                let (capabilities, delimiter_position) = Capabilities::from_bytes(first_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: Capabilities::from_lines(rd.as_read())?,
                refs: None,
                protocol: Protocol::V2,
            }),
        }
    }
}