//! V2 command abstraction to validate invocations and arguments, like a database of what we know about them. use std::borrow::Cow; use super::Command; /// A key value pair of values known at compile time. pub type Feature = (&'static str, Option<Cow<'static, str>>); 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::Feature, Command}; 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", ], }, } } /// Compute initial arguments based on the given `features`. They are typically provided by the `default_features(…)` method. /// Only useful for V2 pub(crate) fn initial_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> { 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(|f| f.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![], } } /// Panics if the given arguments and features don't match what's statically known. It's considered a bug in the delegate. pub(crate) fn validate_argument_prefixes_or_panic( &self, version: gix_transport::Protocol, server: &Capabilities, arguments: &[BString], features: &[Feature], ) { let allowed = self.all_argument_prefixes(); for arg in arguments { if allowed.iter().any(|allowed| arg.starts_with(allowed.as_bytes())) { continue; } panic!("{}: argument {} is not known or allowed", self.as_str(), arg); } 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; } panic!("{}: capability {} is not supported", self.as_str(), feature); } } gix_transport::Protocol::V2 => { let allowed = server .iter() .find_map(|c| { if c.name() == self.as_str().as_bytes().as_bstr() { c.values().map(|v| v.map(|f| f.to_string()).collect::<Vec<_>>()) } else { None } }) .unwrap_or_default(); for (feature, _) in features { if allowed.iter().any(|allowed| feature == allowed) { continue; } match *feature { "agent" => {} _ => panic!("{}: V2 feature/capability {} is not supported", self.as_str(), feature), } } } } } } } #[cfg(test)] mod tests;