1use crate::config::ConfigSet;
7use crate::error::Error;
8use thiserror::Error;
9
10#[derive(Clone, Debug, Error, PartialEq, Eq)]
12pub enum ProtocolError {
13 #[error("protocol '{protocol}' is not allowed by GIT_ALLOW_PROTOCOL")]
15 NotAllowedByEnvironment {
16 protocol: String,
18 },
19 #[error("protocol '{protocol}' is not allowed")]
21 NotAllowed {
22 protocol: String,
24 },
25 #[error("unknown protocol.allow value '{value}' for protocol '{protocol}'")]
27 UnknownAllowValue {
28 protocol: String,
30 value: String,
32 },
33}
34
35#[derive(Clone, Debug, Default)]
37pub struct ProtocolPolicyInputs {
38 pub git_allow_protocol: Option<String>,
40 pub git_protocol_from_user: Option<String>,
42 pub specific_allow: Option<String>,
44 pub blanket_allow: Option<String>,
46}
47
48pub fn check_protocol_allowed_with(
57 protocol: &str,
58 inputs: &ProtocolPolicyInputs,
59) -> Result<(), ProtocolError> {
60 if let Some(val) = inputs.git_allow_protocol.as_deref() {
61 let allowed: Vec<&str> = val
62 .split([':', ','])
63 .map(str::trim)
64 .filter(|s| !s.is_empty())
65 .collect();
66 if allowed.contains(&protocol) {
67 return Ok(());
68 }
69 return Err(ProtocolError::NotAllowedByEnvironment {
70 protocol: protocol.to_owned(),
71 });
72 }
73
74 if let Some(ref val) = inputs.specific_allow {
75 return check_allow_value(protocol, val, inputs.git_protocol_from_user.as_deref());
76 }
77
78 if let Some(ref val) = inputs.blanket_allow {
79 return check_allow_value(protocol, val, inputs.git_protocol_from_user.as_deref());
80 }
81
82 match protocol.to_ascii_lowercase().as_str() {
83 "http" | "https" | "git" | "ssh" => Ok(()),
84 "ext" => Err(ProtocolError::NotAllowed {
85 protocol: protocol.to_owned(),
86 }),
87 _ => check_allow_value(protocol, "user", inputs.git_protocol_from_user.as_deref()),
88 }
89}
90
91fn check_allow_value(
92 protocol: &str,
93 value: &str,
94 git_protocol_from_user: Option<&str>,
95) -> Result<(), ProtocolError> {
96 match value.to_lowercase().as_str() {
97 "always" => Ok(()),
98 "never" => Err(ProtocolError::NotAllowed {
99 protocol: protocol.to_owned(),
100 }),
101 "user" => {
102 if protocol_from_user(git_protocol_from_user) {
103 Ok(())
104 } else {
105 Err(ProtocolError::NotAllowed {
106 protocol: protocol.to_owned(),
107 })
108 }
109 }
110 other => Err(ProtocolError::UnknownAllowValue {
111 protocol: protocol.to_owned(),
112 value: other.to_owned(),
113 }),
114 }
115}
116
117#[must_use]
119pub fn protocol_from_user(raw: Option<&str>) -> bool {
120 match raw {
121 None => true,
122 Some(v) => {
123 let v = v.trim().to_ascii_lowercase();
124 v.is_empty() || !matches!(v.as_str(), "0" | "false" | "no" | "off")
125 }
126 }
127}
128
129#[derive(Clone, Debug, Default)]
131pub struct ClientProtocolVersionInputs {
132 pub config_param_version: Option<String>,
134 pub repo_config_version: Option<String>,
136 pub git_test_protocol_version: Option<String>,
138}
139
140#[must_use]
142pub fn parse_protocol_version_digit(s: &str) -> Option<u8> {
143 match s.trim() {
144 "0" => Some(0),
145 "1" => Some(1),
146 "2" => Some(2),
147 _ => None,
148 }
149}
150
151pub fn try_protocol_version_from_config_value(raw: &str) -> Result<u8, Error> {
155 parse_protocol_version_digit(raw)
156 .ok_or_else(|| Error::Config(format!("bad protocol version '{raw}'").into()))
157}
158
159pub fn try_effective_client_protocol_version_from_inputs(
165 inputs: &ClientProtocolVersionInputs,
166) -> Result<u8, Error> {
167 if let Some(v) = inputs.config_param_version.as_deref() {
168 return try_protocol_version_from_config_value(v);
169 }
170 if let Some(v) = inputs.repo_config_version.as_deref() {
171 return try_protocol_version_from_config_value(v);
172 }
173 if let Some(raw) = inputs
174 .git_test_protocol_version
175 .as_deref()
176 .filter(|s| !s.is_empty())
177 {
178 return try_protocol_version_from_config_value(raw);
179 }
180 Ok(2)
181}
182
183pub fn try_effective_client_protocol_version_from_config(config: &ConfigSet) -> Result<u8, Error> {
188 try_effective_client_protocol_version_from_inputs(&ClientProtocolVersionInputs {
189 repo_config_version: config.get("protocol.version"),
190 ..Default::default()
191 })
192}
193
194#[must_use]
199pub fn client_git_protocol_header_value(version: u8) -> Option<String> {
200 match version {
201 0 => None,
202 1 => Some("version=1".to_string()),
203 _ => Some("version=2".to_string()),
204 }
205}
206
207pub fn client_git_protocol_header_from_config(config: &ConfigSet) -> Result<Option<String>, Error> {
213 Ok(client_git_protocol_header_value(
214 try_effective_client_protocol_version_from_config(config)?,
215 ))
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221
222 #[test]
223 fn client_git_protocol_header_matches_version() {
224 assert_eq!(client_git_protocol_header_value(0), None);
225 assert_eq!(
226 client_git_protocol_header_value(1).as_deref(),
227 Some("version=1")
228 );
229 assert_eq!(
230 client_git_protocol_header_value(2).as_deref(),
231 Some("version=2")
232 );
233 }
234
235 #[test]
236 fn protocol_version_from_config_defaults_to_v2_header() {
237 let config = ConfigSet::new();
238 assert_eq!(
239 client_git_protocol_header_from_config(&config)
240 .expect("header")
241 .as_deref(),
242 Some("version=2")
243 );
244 }
245
246 #[test]
247 fn protocol_version_from_config_honors_protocol_version_key() {
248 let mut config = ConfigSet::new();
249 config
250 .add_command_override("protocol.version", "1")
251 .expect("override");
252 assert_eq!(
253 client_git_protocol_header_from_config(&config)
254 .expect("header")
255 .as_deref(),
256 Some("version=1")
257 );
258 }
259
260 #[test]
261 fn protocol_version_rejects_invalid_config_value() {
262 let mut config = ConfigSet::new();
263 config
264 .add_command_override("protocol.version", "9")
265 .expect("override");
266 let err = client_git_protocol_header_from_config(&config).expect_err("invalid");
267 assert!(
268 matches!(err, Error::Config(_)),
269 "expected ConfigError, got {err:?}"
270 );
271 }
272
273 #[test]
274 fn protocol_version_inputs_honor_git_test_without_env_read_in_config_helper() {
275 let inputs = ClientProtocolVersionInputs {
276 git_test_protocol_version: Some("0".to_owned()),
277 ..Default::default()
278 };
279 assert_eq!(
280 try_effective_client_protocol_version_from_inputs(&inputs).expect("v0"),
281 0
282 );
283 let config = ConfigSet::new();
284 assert_eq!(
285 try_effective_client_protocol_version_from_config(&config).expect("default"),
286 2,
287 "config helper must not read GIT_TEST_PROTOCOL_VERSION from the environment"
288 );
289 }
290}