Skip to main content

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, command::Feature};
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            let mut features = 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            // Echo the server's object format in every v2 command.
161            // A stateless transport like HTTP sends each command as its own request, so without this,
162            // the server assumes SHA1 and aborts any command against a SHA-256 repository.
163            if matches!(version, gix_transport::Protocol::V2) {
164                if let Some(object_format) = server_capabilities
165                    .capability("object-format")
166                    .and_then(|c| c.value())
167                    .and_then(|value| value.to_str().ok())
168                {
169                    features.push(("object-format", Some(object_format.to_owned().into())));
170                }
171            }
172            features
173        }
174        /// Return an error if the given `arguments` and `features` don't match what's statically known.
175        pub fn validate_argument_prefixes(
176            &self,
177            version: gix_transport::Protocol,
178            server: &Capabilities,
179            arguments: &[BString],
180            features: &[Feature],
181        ) -> Result<(), validate_argument_prefixes::Error> {
182            use validate_argument_prefixes::Error;
183            let allowed = self.all_argument_prefixes();
184            for arg in arguments {
185                if allowed.iter().any(|allowed| arg.starts_with(allowed.as_bytes())) {
186                    continue;
187                }
188                return Err(Error::UnsupportedArgument {
189                    command: self.as_str(),
190                    argument: arg.clone(),
191                });
192            }
193            match version {
194                gix_transport::Protocol::V0 | gix_transport::Protocol::V1 => {
195                    for (feature, _) in features {
196                        if server
197                            .iter()
198                            .any(|c| feature.starts_with(c.name().to_str_lossy().as_ref()))
199                        {
200                            continue;
201                        }
202                        return Err(Error::UnsupportedCapability {
203                            command: self.as_str(),
204                            feature: feature.to_string(),
205                        });
206                    }
207                }
208                gix_transport::Protocol::V2 => {
209                    let allowed = server
210                        .iter()
211                        .find_map(|c| {
212                            if c.name() == self.as_str() {
213                                c.values().map(|v| v.map(ToString::to_string).collect::<Vec<_>>())
214                            } else {
215                                None
216                            }
217                        })
218                        .unwrap_or_default();
219                    for (feature, _) in features {
220                        if allowed.iter().any(|allowed| feature == allowed) {
221                            continue;
222                        }
223                        match *feature {
224                            "agent" | "object-format" => {}
225                            _ => {
226                                return Err(Error::UnsupportedCapability {
227                                    command: self.as_str(),
228                                    feature: feature.to_string(),
229                                });
230                            }
231                        }
232                    }
233                }
234            }
235            Ok(())
236        }
237    }
238
239    ///
240    pub mod validate_argument_prefixes {
241        use bstr::BString;
242
243        /// The error returned by [Command::validate_argument_prefixes()](super::Command::validate_argument_prefixes()).
244        #[derive(Debug, thiserror::Error)]
245        #[allow(missing_docs)]
246        pub enum Error {
247            #[error("{command}: argument {argument} is not known or allowed")]
248            UnsupportedArgument { command: &'static str, argument: BString },
249            #[error("{command}: capability {feature} is not supported")]
250            UnsupportedCapability { command: &'static str, feature: String },
251        }
252    }
253}
254#[cfg(any(test, feature = "async-client", feature = "blocking-client"))]
255pub use with_io::validate_argument_prefixes;