1use crate::check_ref_format::{check_refname_format, RefNameOptions};
17
18#[derive(Debug, Clone, Default, PartialEq, Eq)]
23pub struct RefspecItem {
24 pub force: bool,
26 pub negative: bool,
28 pub matching: bool,
30 pub pattern: bool,
32 pub exact_sha1: bool,
34 pub src: Option<String>,
36 pub dst: Option<String>,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum RefspecError {
43 Invalid(String),
45}
46
47impl std::fmt::Display for RefspecError {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 match self {
50 RefspecError::Invalid(s) => write!(f, "invalid refspec '{s}'"),
51 }
52 }
53}
54
55impl std::error::Error for RefspecError {}
56
57const SHA1_HEXSZ: usize = 40;
59
60fn is_exact_sha1_hex(s: &str) -> bool {
62 s.len() == SHA1_HEXSZ && s.bytes().all(|b| b.is_ascii_hexdigit())
63}
64
65fn refname_ok(name: &str, is_glob: bool) -> bool {
71 let opts = RefNameOptions {
72 allow_onelevel: true,
73 refspec_pattern: is_glob,
74 normalize: false,
75 };
76 check_refname_format(name, &opts).is_ok()
77}
78
79fn parse_refspec(refspec: &str, fetch: bool) -> Result<RefspecItem, RefspecError> {
84 let bytes = refspec.as_bytes();
85 let invalid = || RefspecError::Invalid(refspec.to_owned());
86
87 let mut item = RefspecItem::default();
88 let mut is_glob = false;
89
90 let mut lhs_start = 0usize;
92 if let Some(&first) = bytes.first() {
93 if first == b'+' {
94 item.force = true;
95 lhs_start = 1;
96 } else if first == b'^' {
97 item.negative = true;
98 lhs_start = 1;
99 }
100 }
101
102 let lhs = &refspec[lhs_start..];
103
104 let colon_pos = lhs.rfind(':');
106
107 if item.negative && colon_pos.is_some() {
109 return Err(invalid());
110 }
111
112 if !fetch && colon_pos == Some(0) && lhs.len() == 1 {
116 item.matching = true;
117 return Ok(item);
118 }
119
120 let (lhs_str, rhs_opt): (&str, Option<&str>) = match colon_pos {
122 Some(pos) => (&lhs[..pos], Some(&lhs[pos + 1..])),
123 None => (lhs, None),
124 };
125
126 if let Some(rhs) = rhs_opt {
127 let rlen = rhs.len();
128 is_glob = rlen >= 1 && rhs.contains('*');
129 item.dst = Some(rhs.to_owned());
130 } else {
131 item.dst = None;
132 }
133
134 let llen = lhs_str.len();
135 if llen >= 1 && lhs_str.contains('*') {
136 if (rhs_opt.is_some() && !is_glob) || (rhs_opt.is_none() && !item.negative && fetch) {
138 return Err(invalid());
139 }
140 is_glob = true;
141 } else if rhs_opt.is_some() && is_glob {
142 return Err(invalid());
144 }
145
146 item.pattern = is_glob;
147 if llen == 1 && lhs_str == "@" {
148 item.src = Some("HEAD".to_owned());
149 } else {
150 item.src = Some(lhs_str.to_owned());
151 }
152 let src = item.src.as_deref().unwrap_or("");
153
154 if item.negative {
155 if src.is_empty() {
157 return Err(invalid()); } else if is_exact_sha1_hex(src) {
159 return Err(invalid()); } else if refname_ok(src, is_glob) {
161 } else {
163 return Err(invalid());
164 }
165 return Ok(item);
166 }
167
168 if fetch {
169 if src.is_empty() {
171 } else if is_exact_sha1_hex(src) {
173 item.exact_sha1 = true; } else if refname_ok(src, is_glob) {
175 } else {
177 return Err(invalid());
178 }
179 match item.dst.as_deref() {
181 None => {} Some("") => {} Some(dst) => {
184 if !refname_ok(dst, is_glob) {
185 return Err(invalid());
186 }
187 }
188 }
189 } else {
190 if src.is_empty() {
193 } else if is_glob {
195 if !refname_ok(src, is_glob) {
196 return Err(invalid());
197 }
198 } else {
199 }
201 match item.dst.as_deref() {
203 None => {
204 if !refname_ok(src, is_glob) {
206 return Err(invalid());
207 }
208 }
209 Some("") => {
210 return Err(invalid());
212 }
213 Some(dst) => {
214 if !refname_ok(dst, is_glob) {
215 return Err(invalid());
216 }
217 }
218 }
219 }
220
221 Ok(item)
222}
223
224pub fn parse_fetch_refspec(refspec: &str) -> Result<RefspecItem, RefspecError> {
226 parse_refspec(refspec, true)
227}
228
229pub fn parse_push_refspec(refspec: &str) -> Result<RefspecItem, RefspecError> {
231 parse_refspec(refspec, false)
232}
233
234pub fn valid_fetch_refspec(refspec: &str) -> bool {
236 parse_refspec(refspec, true).is_ok()
237}
238
239pub fn valid_push_refspec(refspec: &str) -> bool {
241 parse_refspec(refspec, false).is_ok()
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247
248 fn fetch_valid(s: &str) {
251 assert!(valid_fetch_refspec(s), "expected fetch '{s}' to be valid");
252 }
253 fn fetch_invalid(s: &str) {
254 assert!(
255 !valid_fetch_refspec(s),
256 "expected fetch '{s}' to be invalid"
257 );
258 }
259 fn push_valid(s: &str) {
260 assert!(valid_push_refspec(s), "expected push '{s}' to be valid");
261 }
262 fn push_invalid(s: &str) {
263 assert!(!valid_push_refspec(s), "expected push '{s}' to be invalid");
264 }
265
266 #[test]
267 fn empty_and_colon() {
268 push_invalid("");
269 push_valid(":");
270 push_invalid("::");
271 push_valid("+:");
272 fetch_valid("");
273 fetch_valid(":");
274 fetch_invalid("::");
275 }
276
277 #[test]
278 fn glob_balance() {
279 push_valid("refs/heads/*:refs/remotes/frotz/*");
280 push_invalid("refs/heads/*:refs/remotes/frotz");
281 push_invalid("refs/heads:refs/remotes/frotz/*");
282 push_valid("refs/heads/main:refs/remotes/frotz/xyzzy");
283
284 fetch_valid("refs/heads/*:refs/remotes/frotz/*");
285 fetch_invalid("refs/heads/*:refs/remotes/frotz");
286 fetch_invalid("refs/heads:refs/remotes/frotz/*");
287 fetch_valid("refs/heads/main:refs/remotes/frotz/xyzzy");
288 fetch_invalid("refs/heads/main::refs/remotes/frotz/xyzzy");
289 fetch_invalid("refs/heads/maste :refs/remotes/frotz/xyzzy");
290 }
291
292 #[test]
293 fn rev_expressions() {
294 push_valid("main~1:refs/remotes/frotz/backup");
295 fetch_invalid("main~1:refs/remotes/frotz/backup");
296 push_valid("HEAD~4:refs/remotes/frotz/new");
297 fetch_invalid("HEAD~4:refs/remotes/frotz/new");
298 }
299
300 #[test]
301 fn bare_head_and_at() {
302 push_valid("HEAD");
303 fetch_valid("HEAD");
304 push_valid("@");
305 fetch_valid("@");
306 push_invalid("refs/heads/ nitfol");
307 fetch_invalid("refs/heads/ nitfol");
308 }
309
310 #[test]
311 fn head_colon() {
312 push_invalid("HEAD:");
313 fetch_valid("HEAD:");
314 push_invalid("refs/heads/ nitfol:");
315 fetch_invalid("refs/heads/ nitfol:");
316 }
317
318 #[test]
319 fn delete_specs() {
320 push_valid(":refs/remotes/frotz/deleteme");
321 fetch_valid(":refs/remotes/frotz/HEAD-to-me");
322 push_invalid(":refs/remotes/frotz/delete me");
323 fetch_invalid(":refs/remotes/frotz/HEAD to me");
324 }
325
326 #[test]
327 fn star_placements() {
328 fetch_valid("refs/heads/*/for-linus:refs/remotes/mine/*-blah");
329 push_valid("refs/heads/*/for-linus:refs/remotes/mine/*-blah");
330 fetch_valid("refs/heads*/for-linus:refs/remotes/mine/*");
331 push_valid("refs/heads*/for-linus:refs/remotes/mine/*");
332 fetch_invalid("refs/heads/*/*/for-linus:refs/remotes/mine/*");
333 push_invalid("refs/heads/*/*/for-linus:refs/remotes/mine/*");
334 fetch_invalid("refs/heads/*g*/for-linus:refs/remotes/mine/*");
335 push_invalid("refs/heads/*g*/for-linus:refs/remotes/mine/*");
336 fetch_valid("refs/heads/*/for-linus:refs/remotes/mine/*");
337 push_valid("refs/heads/*/for-linus:refs/remotes/mine/*");
338 }
339
340 #[test]
341 fn utf8_and_tab() {
342 fetch_valid("refs/heads/\u{00C4}");
343 fetch_invalid("refs/heads/\ttab");
344 }
345}