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;