gix_protocol/
command.rs

1//! V2 command abstraction to validate invocations and arguments, like a database of what we know about them.
2use std::borrow::Cow;
3
4use super::Command;
5
6/// A key value pair of values known at compile time.
7pub type Feature = (&'static str, Option<Cow<'static, str>>);
8
9impl Command {
10    /// Produce the name of the command as known by the server side.
11    pub fn as_str(&self) -> &'static str {
12        match self {
13            Command::LsRefs => "ls-refs",
14            Command::Fetch => "fetch",
15        }
16    }
17}
18
19#[cfg(any(test, feature = "async-client", feature = "blocking-client"))]
20mod with_io {
21    use bstr::{BString, ByteSlice};
22    use gix_transport::client::Capabilities;
23
24    use crate::{command::Feature, Command};
25
26    impl Command {
27        /// Only V2
28        fn all_argument_prefixes(&self) -> &'static [&'static str] {
29            match self {
30                Command::LsRefs => &["symrefs", "peel", "ref-prefix ", "unborn"],
31                Command::Fetch => &[
32                    "want ", // hex oid
33                    "have ", // hex oid
34                    "done",
35                    "thin-pack",
36                    "no-progress",
37                    "include-tag",
38                    "ofs-delta",
39                    // Shallow feature/capability
40                    "shallow ", // hex oid
41                    "deepen ",  // commit depth
42                    "deepen-relative",
43                    "deepen-since ", // time-stamp
44                    "deepen-not ",   // rev
45                    // filter feature/capability
46                    "filter ", // filter-spec
47                    // ref-in-want feature
48                    "want-ref ", // ref path
49                    // sideband-all feature
50                    "sideband-all",
51                    // packfile-uris feature
52                    "packfile-uris ", // protocols
53                    // wait-for-done feature
54                    "wait-for-done",
55                ],
56            }
57        }
58
59        fn all_features(&self, version: gix_transport::Protocol) -> &'static [&'static str] {
60            match self {
61                Command::LsRefs => &[],
62                Command::Fetch => match version {
63                    gix_transport::Protocol::V0 | gix_transport::Protocol::V1 => &[
64                        "multi_ack",
65                        "thin-pack",
66                        "side-band",
67                        "side-band-64k",
68                        "ofs-delta",
69                        "shallow",
70                        "deepen-since",
71                        "deepen-not",
72                        "deepen-relative",
73                        "no-progress",
74                        "include-tag",
75                        "multi_ack_detailed",
76                        "allow-tip-sha1-in-want",
77                        "allow-reachable-sha1-in-want",
78                        "no-done",
79                        "filter",
80                    ],
81                    gix_transport::Protocol::V2 => &[
82                        "shallow",
83                        "filter",
84                        "ref-in-want",
85                        "sideband-all",
86                        "packfile-uris",
87                        "wait-for-done",
88                    ],
89                },
90            }
91        }
92
93        /// Provide the initial arguments based on the given `features`.
94        /// They are typically provided by the [`Self::default_features`] method.
95        /// Only useful for V2, and based on heuristics/experimentation.
96        pub fn initial_v2_arguments(&self, features: &[Feature]) -> Vec<BString> {
97            match self {
98                Command::Fetch => ["thin-pack", "ofs-delta"]
99                    .iter()
100                    .map(|s| s.as_bytes().as_bstr().to_owned())
101                    .chain(
102                        [
103                            "sideband-all",
104                            /* "packfile-uris" */ // packfile-uris must be configurable and can't just be used. Some servers advertise it and reject it later.
105                        ]
106                        .iter()
107                        .filter(|f| features.iter().any(|(sf, _)| sf == *f))
108                        .map(|f| f.as_bytes().as_bstr().to_owned()),
109                    )
110                    .collect(),
111                Command::LsRefs => vec![b"symrefs".as_bstr().to_owned(), b"peel".as_bstr().to_owned()],
112            }
113        }
114
115        /// Turns on all modern features for V1 and all supported features for V2, returning them as a vector of features.
116        /// Note that this is the basis for any fetch operation as these features fulfil basic requirements and reasonably up-to-date servers.
117        pub fn default_features(
118            &self,
119            version: gix_transport::Protocol,
120            server_capabilities: &Capabilities,
121        ) -> Vec<Feature> {
122            match self {
123                Command::Fetch => match version {
124                    gix_transport::Protocol::V0 | gix_transport::Protocol::V1 => {
125                        let has_multi_ack_detailed = server_capabilities.contains("multi_ack_detailed");
126                        let has_sideband_64k = server_capabilities.contains("side-band-64k");
127                        self.all_features(version)
128                            .iter()
129                            .copied()
130                            .filter(|feature| match *feature {
131                                "side-band" if has_sideband_64k => false,
132                                "multi_ack" if has_multi_ack_detailed => false,
133                                "no-progress" => false,
134                                feature => server_capabilities.contains(feature),
135                            })
136                            .map(|s| (s, None))
137                            .collect()
138                    }
139                    gix_transport::Protocol::V2 => {
140                        let supported_features: Vec<_> = server_capabilities
141                            .iter()
142                            .find_map(|c| {
143                                if c.name() == Command::Fetch.as_str() {
144                                    c.values().map(|v| v.map(ToOwned::to_owned).collect())
145                                } else {
146                                    None
147                                }
148                            })
149                            .unwrap_or_default();
150                        self.all_features(version)
151                            .iter()
152                            .copied()
153                            .filter(|feature| supported_features.iter().any(|supported| supported == feature))
154                            .map(|s| (s, None))
155                            .collect()
156                    }
157                },
158                Command::LsRefs => vec![],
159            }
160        }
161        /// Return an error if the given `arguments` and `features` don't match what's statically known.
162        pub fn validate_argument_prefixes(
163            &self,
164            version: gix_transport::Protocol,
165            server: &Capabilities,
166            arguments: &[BString],
167            features: &[Feature],
168        ) -> Result<(), validate_argument_prefixes::Error> {
169            use validate_argument_prefixes::Error;
170            let allowed = self.all_argument_prefixes();
171            for arg in arguments {
172                if allowed.iter().any(|allowed| arg.starts_with(allowed.as_bytes())) {
173                    continue;
174                }
175                return Err(Error::UnsupportedArgument {
176                    command: self.as_str(),
177                    argument: arg.clone(),
178                });
179            }
180            match version {
181                gix_transport::Protocol::V0 | gix_transport::Protocol::V1 => {
182                    for (feature, _) in features {
183                        if server
184                            .iter()
185                            .any(|c| feature.starts_with(c.name().to_str_lossy().as_ref()))
186                        {
187                            continue;
188                        }
189                        return Err(Error::UnsupportedCapability {
190                            command: self.as_str(),
191                            feature: feature.to_string(),
192                        });
193                    }
194                }
195                gix_transport::Protocol::V2 => {
196                    let allowed = server
197                        .iter()
198                        .find_map(|c| {
199                            if c.name() == self.as_str() {
200                                c.values().map(|v| v.map(ToString::to_string).collect::<Vec<_>>())
201                            } else {
202                                None
203                            }
204                        })
205                        .unwrap_or_default();
206                    for (feature, _) in features {
207                        if allowed.iter().any(|allowed| feature == allowed) {
208                            continue;
209                        }
210                        match *feature {
211                            "agent" => {}
212                            _ => {
213                                return Err(Error::UnsupportedCapability {
214                                    command: self.as_str(),
215                                    feature: feature.to_string(),
216                                })
217                            }
218                        }
219                    }
220                }
221            }
222            Ok(())
223        }
224    }
225
226    ///
227    pub mod validate_argument_prefixes {
228        use bstr::BString;
229
230        /// The error returned by [Command::validate_argument_prefixes()](super::Command::validate_argument_prefixes()).
231        #[derive(Debug, thiserror::Error)]
232        #[allow(missing_docs)]
233        pub enum Error {
234            #[error("{command}: argument {argument} is not known or allowed")]
235            UnsupportedArgument { command: &'static str, argument: BString },
236            #[error("{command}: capability {feature} is not supported")]
237            UnsupportedCapability { command: &'static str, feature: String },
238        }
239    }
240}
241#[cfg(any(test, feature = "async-client", feature = "blocking-client"))]
242pub use with_io::validate_argument_prefixes;