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