253 lines · 10.7 KB · Rust
RawDownload
//! V2 command abstraction to validate invocations and arguments, like a database of what we know about them.use super::Command; /// A feature name known at compile time and its optional owned value.pub type Feature = (&'static str, Option<String>); impl Command {    /// Produce the name of the command as known by the server side.    pub fn as_str(&self) -> &'static str {        match self {            Command::LsRefs => "ls-refs",            Command::Fetch => "fetch",        }    }} #[cfg(any(test, feature = "async-client", feature = "blocking-client"))]mod with_io {    use bstr::{BString, ByteSlice};    use gix_transport::client::Capabilities;     use crate::{Command, command::Feature};     impl Command {        /// Only V2        fn all_argument_prefixes(&self) -> &'static [&'static str] {            match self {                Command::LsRefs => &["symrefs", "peel", "ref-prefix ", "unborn"],                Command::Fetch => &[                    "want ", // hex oid                    "have ", // hex oid                    "done",                    "thin-pack",                    "no-progress",                    "include-tag",                    "ofs-delta",                    // Shallow feature/capability                    "shallow ", // hex oid                    "deepen ",  // commit depth                    "deepen-relative",                    "deepen-since ", // time-stamp                    "deepen-not ",   // rev                    // filter feature/capability                    "filter ", // filter-spec                    // ref-in-want feature                    "want-ref ", // ref path                    // sideband-all feature                    "sideband-all",                    // packfile-uris feature                    "packfile-uris ", // protocols                    // wait-for-done feature                    "wait-for-done",                ],            }        }         fn all_features(&self, version: gix_transport::Protocol) -> &'static [&'static str] {            match self {                Command::LsRefs => &[],                Command::Fetch => match version {                    gix_transport::Protocol::V0 | gix_transport::Protocol::V1 => &[                        "multi_ack",                        "thin-pack",                        "side-band",                        "side-band-64k",                        "ofs-delta",                        "shallow",                        "deepen-since",                        "deepen-not",                        "deepen-relative",                        "no-progress",                        "include-tag",                        "multi_ack_detailed",                        "allow-tip-sha1-in-want",                        "allow-reachable-sha1-in-want",                        "no-done",                        "filter",                    ],                    gix_transport::Protocol::V2 => &[                        "shallow",                        "filter",                        "ref-in-want",                        "sideband-all",                        "packfile-uris",                        "wait-for-done",                    ],                },            }        }         /// Provide the initial arguments based on the given `features`.        /// They are typically provided by the [`Self::default_features`] method.        /// Only useful for V2, and based on heuristics/experimentation.        pub fn initial_v2_arguments(&self, features: &[Feature]) -> Vec<BString> {            match self {                Command::Fetch => ["thin-pack", "ofs-delta"]                    .iter()                    .map(|s| s.as_bytes().as_bstr().to_owned())                    .chain(                        [                            "sideband-all",                            /* "packfile-uris" */ // packfile-uris must be configurable and can't just be used. Some servers advertise it and reject it later.                        ]                        .iter()                        .filter(|f| features.iter().any(|(sf, _)| sf == *f))                        .map(|f| f.as_bytes().as_bstr().to_owned()),                    )                    .collect(),                Command::LsRefs => vec![b"symrefs".as_bstr().to_owned(), b"peel".as_bstr().to_owned()],            }        }         /// Turns on all modern features for V1 and all supported features for V2, returning them as a vector of features.        /// Note that this is the basis for any fetch operation as these features fulfil basic requirements and reasonably up-to-date servers.        pub fn default_features(            &self,            version: gix_transport::Protocol,            server_capabilities: &Capabilities,        ) -> Vec<Feature> {            let mut features = match self {                Command::Fetch => match version {                    gix_transport::Protocol::V0 | gix_transport::Protocol::V1 => {                        let has_multi_ack_detailed = server_capabilities.contains("multi_ack_detailed");                        let has_sideband_64k = server_capabilities.contains("side-band-64k");                        self.all_features(version)                            .iter()                            .copied()                            .filter(|feature| match *feature {                                "side-band" if has_sideband_64k => false,                                "multi_ack" if has_multi_ack_detailed => false,                                "no-progress" => false,                                feature => server_capabilities.contains(feature),                            })                            .map(|s| (s, None))                            .collect()                    }                    gix_transport::Protocol::V2 => {                        let supported_features: Vec<_> = server_capabilities                            .iter()                            .find_map(|c| {                                if c.name() == Command::Fetch.as_str() {                                    c.values().map(|v| v.map(ToOwned::to_owned).collect())                                } else {                                    None                                }                            })                            .unwrap_or_default();                        self.all_features(version)                            .iter()                            .copied()                            .filter(|feature| supported_features.iter().any(|supported| supported == feature))                            .map(|s| (s, None))                            .collect()                    }                },                Command::LsRefs => vec![],            };            // Echo the server's object format in every v2 command.            // A stateless transport like HTTP sends each command as its own request, so without this,            // the server assumes SHA1 and aborts any command against a SHA-256 repository.            if matches!(version, gix_transport::Protocol::V2) {                if let Some(object_format) = server_capabilities                    .capability("object-format")                    .and_then(|c| c.value())                    .and_then(|value| value.to_str().ok())                {                    features.push(("object-format", Some(object_format.to_owned())));                }            }            features        }        /// Return an error if the given `arguments` and `features` don't match what's statically known.        pub fn validate_argument_prefixes(            &self,            version: gix_transport::Protocol,            server: &Capabilities,            arguments: &[BString],            features: &[Feature],        ) -> Result<(), validate_argument_prefixes::Error> {            use validate_argument_prefixes::Error;            let allowed = self.all_argument_prefixes();            for arg in arguments {                if allowed.iter().any(|allowed| arg.starts_with(allowed.as_bytes())) {                    continue;                }                return Err(Error::UnsupportedArgument {                    command: self.as_str(),                    argument: arg.clone(),                });            }            match version {                gix_transport::Protocol::V0 | gix_transport::Protocol::V1 => {                    for (feature, _) in features {                        if server                            .iter()                            .any(|c| feature.starts_with(c.name().to_str_lossy().as_ref()))                        {                            continue;                        }                        return Err(Error::UnsupportedCapability {                            command: self.as_str(),                            feature: feature.to_string(),                        });                    }                }                gix_transport::Protocol::V2 => {                    let allowed = server                        .iter()                        .find_map(|c| {                            if c.name() == self.as_str() {                                c.values().map(|v| v.map(ToString::to_string).collect::<Vec<_>>())                            } else {                                None                            }                        })                        .unwrap_or_default();                    for (feature, _) in features {                        if allowed.iter().any(|allowed| feature == allowed) {                            continue;                        }                        match *feature {                            "agent" | "object-format" => {}                            _ => {                                return Err(Error::UnsupportedCapability {                                    command: self.as_str(),                                    feature: feature.to_string(),                                });                            }                        }                    }                }            }            Ok(())        }    }     ///    pub mod validate_argument_prefixes {        use bstr::BString;         /// The error returned by [Command::validate_argument_prefixes()](super::Command::validate_argument_prefixes()).        #[derive(Debug, thiserror::Error)]        #[expect(missing_docs)]        pub enum Error {            #[error("{command}: argument {argument} is not known or allowed")]            UnsupportedArgument { command: &'static str, argument: BString },            #[error("{command}: capability {feature} is not supported")]            UnsupportedCapability { command: &'static str, feature: String },        }    }}#[cfg(any(test, feature = "async-client", feature = "blocking-client"))]pub use with_io::validate_argument_prefixes;