1use std::borrow::Cow;
3
4use super::Command;
5
6pub type Feature = (&'static str, Option<Cow<'static, str>>);
8
9impl Command {
10 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 fn all_argument_prefixes(&self) -> &'static [&'static str] {
29 match self {
30 Command::LsRefs => &["symrefs", "peel", "ref-prefix ", "unborn"],
31 Command::Fetch => &[
32 "want ", "have ", "done",
35 "thin-pack",
36 "no-progress",
37 "include-tag",
38 "ofs-delta",
39 "shallow ", "deepen ", "deepen-relative",
43 "deepen-since ", "deepen-not ", "filter ", "want-ref ", "sideband-all",
51 "packfile-uris ", "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 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 ]
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 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 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 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 pub mod validate_argument_prefixes {
241 use bstr::BString;
242
243 #[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;