Skip to main content

gix_refspec/
parse.rs

1/// The error returned by the [`parse()`][crate::parse()] function.
2#[derive(Debug, thiserror::Error)]
3#[allow(missing_docs)]
4pub enum Error {
5    #[error("Empty refspecs are invalid")]
6    Empty,
7    #[error("Negative refspecs cannot have destinations as they exclude sources")]
8    NegativeWithDestination,
9    #[error("Negative specs must not be empty")]
10    NegativeEmpty,
11    #[error("Negative specs must be object hashes")]
12    NegativeObjectHash,
13    #[error("Negative specs must be full ref names, starting with \"refs/\"")]
14    NegativePartialName,
15    #[error("Negative glob patterns are not allowed")]
16    NegativeGlobPattern,
17    #[error("Fetch destinations must be ref-names, like 'HEAD:refs/heads/branch'")]
18    InvalidFetchDestination,
19    #[error("Cannot push into an empty destination")]
20    PushToEmpty,
21    #[error("glob patterns may only involved a single '*' character, found {pattern:?}")]
22    PatternUnsupported { pattern: bstr::BString },
23    #[error("Both sides of the specification need a pattern, like 'a/*:b/*'")]
24    PatternUnbalanced,
25    #[error(transparent)]
26    ReferenceName(#[from] gix_validate::reference::name::Error),
27    #[error(transparent)]
28    RevSpec(#[from] gix_revision::spec::parse::Error),
29}
30
31/// Define how the parsed refspec should be used.
32#[derive(PartialOrd, Ord, PartialEq, Eq, Copy, Clone, Hash, Debug)]
33pub enum Operation {
34    /// The `src` side is local and the `dst` side is remote.
35    Push,
36    /// The `src` side is remote and the `dst` side is local.
37    Fetch,
38}
39
40pub(crate) mod function {
41    use crate::{
42        RefSpecRef,
43        parse::{Error, Operation},
44        types::Mode,
45    };
46    use bstr::{BStr, ByteSlice};
47    use gix_error::Exn;
48
49    /// Parse `spec` for use in `operation` and return it if it is valid.
50    pub fn parse(mut spec: &BStr, operation: Operation) -> Result<RefSpecRef<'_>, Error> {
51        fn fetch_head_only(mode: Mode) -> RefSpecRef<'static> {
52            RefSpecRef {
53                mode,
54                op: Operation::Fetch,
55                src: Some("HEAD".into()),
56                dst: None,
57            }
58        }
59
60        let mode = match spec.first() {
61            Some(&b'^') => {
62                spec = &spec[1..];
63                Mode::Negative
64            }
65            Some(&b'+') => {
66                spec = &spec[1..];
67                Mode::Force
68            }
69            Some(_) => Mode::Normal,
70            None => {
71                return match operation {
72                    Operation::Push => Err(Error::Empty),
73                    Operation::Fetch => Ok(fetch_head_only(Mode::Normal)),
74                };
75            }
76        };
77
78        let (mut src, dst) = match spec.find_byte(b':') {
79            Some(pos) => {
80                if mode == Mode::Negative {
81                    return Err(Error::NegativeWithDestination);
82                }
83
84                let (src, dst) = spec.split_at(pos);
85                let dst = &dst[1..];
86                let src = (!src.is_empty()).then(|| src.as_bstr());
87                let dst = (!dst.is_empty()).then(|| dst.as_bstr());
88                match (src, dst) {
89                    (None, None) => match operation {
90                        Operation::Push => (None, None),
91                        Operation::Fetch => (Some("HEAD".into()), None),
92                    },
93                    (None, Some(dst)) => match operation {
94                        Operation::Push => (None, Some(dst)),
95                        Operation::Fetch => (Some("HEAD".into()), Some(dst)),
96                    },
97                    (Some(src), None) => match operation {
98                        Operation::Push => return Err(Error::PushToEmpty),
99                        Operation::Fetch => (Some(src), None),
100                    },
101                    (Some(src), Some(dst)) => (Some(src), Some(dst)),
102                }
103            }
104            None => {
105                let src = (!spec.is_empty()).then_some(spec);
106                if Operation::Fetch == operation && mode != Mode::Negative && src.is_none() {
107                    return Ok(fetch_head_only(mode));
108                } else {
109                    (src, None)
110                }
111            }
112        };
113
114        if let Some(spec) = src.as_mut() {
115            if *spec == "@" {
116                *spec = "HEAD".into();
117            }
118        }
119        let is_one_sided = dst.is_none();
120        let (src, src_had_pattern) = validated(src, operation == Operation::Push && dst.is_some(), is_one_sided)?;
121        let (dst, dst_had_pattern) = validated(dst, false, false)?;
122        // For one-sided refspecs, we don't need to check for pattern balance
123        if !is_one_sided && mode != Mode::Negative && src_had_pattern != dst_had_pattern {
124            return Err(Error::PatternUnbalanced);
125        }
126
127        if mode == Mode::Negative {
128            match src {
129                Some(spec) => {
130                    if looks_like_object_hash(spec) {
131                        return Err(Error::NegativeObjectHash);
132                    } else if !spec.starts_with(b"refs/") && spec != "HEAD" {
133                        return Err(Error::NegativePartialName);
134                    } else if src_had_pattern {
135                        validate_negative_pattern(spec)?;
136                    }
137                }
138                None => return Err(Error::NegativeEmpty),
139            }
140        }
141
142        Ok(RefSpecRef {
143            op: operation,
144            mode,
145            src,
146            dst,
147        })
148    }
149
150    fn looks_like_object_hash(spec: &BStr) -> bool {
151        spec.len() >= gix_hash::Kind::shortest().len_in_hex() && spec.iter().all(u8::is_ascii_hexdigit)
152    }
153
154    fn validate_negative_pattern(spec: &BStr) -> Result<(), Error> {
155        if spec.iter().filter(|b| **b == b'*').take(2).count() > 1 {
156            return Err(Error::PatternUnsupported { pattern: spec.into() });
157        }
158
159        validate_partial_name_with_single_glob(spec)?;
160        Ok(())
161    }
162
163    fn validate_partial_name_with_single_glob(spec: &BStr) -> Result<(), Error> {
164        let mut buf = smallvec::SmallVec::<[u8; 256]>::with_capacity(spec.len());
165        buf.extend_from_slice(spec);
166        let glob_pos = buf.find_byte(b'*').expect("glob present");
167        buf[glob_pos] = b'a';
168        gix_validate::reference::name_partial(buf.as_bstr())?;
169        Ok(())
170    }
171
172    fn validated(
173        spec: Option<&BStr>,
174        allow_revspecs: bool,
175        is_one_sided: bool,
176    ) -> Result<(Option<&BStr>, bool), Error> {
177        match spec {
178            Some(spec) => {
179                let glob_count = spec.iter().filter(|b| **b == b'*').take(2).count();
180                if glob_count > 1 {
181                    // For one-sided refspecs, allow any number of globs without validation
182                    if !is_one_sided {
183                        return Err(Error::PatternUnsupported { pattern: spec.into() });
184                    }
185                }
186                // Check if there are any globs (one or more asterisks)
187                let has_globs = glob_count > 0;
188                if has_globs {
189                    // For one-sided refspecs, skip validation of glob patterns
190                    if !is_one_sided {
191                        validate_partial_name_with_single_glob(spec)?;
192                    }
193                } else {
194                    gix_validate::reference::name_partial(spec)
195                        .map_err(Error::from)
196                        .or_else(|err| {
197                            if allow_revspecs {
198                                gix_revision::spec::parse(spec, &mut super::revparse::Noop).map_err(Exn::into_inner)?;
199                                Ok(spec)
200                            } else {
201                                Err(err)
202                            }
203                        })?;
204                }
205                Ok((Some(spec), has_globs))
206            }
207            None => Ok((None, false)),
208        }
209    }
210}
211
212mod revparse {
213    use bstr::BStr;
214    use gix_error::Exn;
215    use gix_revision::spec::parse::delegate::{
216        Kind, Navigate, PeelTo, PrefixHint, ReflogLookup, Revision, SiblingBranch, Traversal,
217    };
218
219    pub(crate) struct Noop;
220
221    impl Revision for Noop {
222        fn find_ref(&mut self, _name: &BStr) -> Result<(), Exn> {
223            Ok(())
224        }
225
226        fn disambiguate_prefix(&mut self, _prefix: gix_hash::Prefix, _hint: Option<PrefixHint<'_>>) -> Result<(), Exn> {
227            Ok(())
228        }
229
230        fn reflog(&mut self, _query: ReflogLookup) -> Result<(), Exn> {
231            Ok(())
232        }
233
234        fn nth_checked_out_branch(&mut self, _branch_no: usize) -> Result<(), Exn> {
235            Ok(())
236        }
237
238        fn sibling_branch(&mut self, _kind: SiblingBranch) -> Result<(), Exn> {
239            Ok(())
240        }
241    }
242
243    impl Navigate for Noop {
244        fn traverse(&mut self, _kind: Traversal) -> Result<(), Exn> {
245            Ok(())
246        }
247
248        fn peel_until(&mut self, _kind: PeelTo<'_>) -> Result<(), Exn> {
249            Ok(())
250        }
251
252        fn find(&mut self, _regex: &BStr, _negated: bool) -> Result<(), Exn> {
253            Ok(())
254        }
255
256        fn index_lookup(&mut self, _path: &BStr, _stage: u8) -> Result<(), Exn> {
257            Ok(())
258        }
259    }
260
261    impl Kind for Noop {
262        fn kind(&mut self, _kind: gix_revision::spec::Kind) -> Result<(), Exn> {
263            Ok(())
264        }
265    }
266
267    impl gix_revision::spec::parse::Delegate for Noop {
268        fn done(&mut self) -> Result<(), Exn> {
269            Ok(())
270        }
271    }
272}