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::Feature, Command};
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 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 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 pub mod validate_argument_prefixes {
228 use bstr::BString;
229
230 #[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;