gix_revision/spec/parse/
function.rs

1use std::{str::FromStr, time::SystemTime};
2
3use bstr::{BStr, BString, ByteSlice, ByteVec};
4
5use crate::{
6    spec,
7    spec::parse::{delegate, delegate::SiblingBranch, Delegate, Error},
8};
9
10/// Parse a git [`revspec`](https://git-scm.com/docs/git-rev-parse#_specifying_revisions) and call `delegate` for each token
11/// successfully parsed.
12///
13/// Note that the `delegate` is expected to maintain enough state to lookup revisions properly.
14/// Returns `Ok(())` if all of `input` was consumed, or the error if either the `revspec` syntax was incorrect or
15/// the `delegate` failed to perform the request.
16pub fn parse(mut input: &BStr, delegate: &mut impl Delegate) -> Result<(), Error> {
17    use delegate::{Kind, Revision};
18    let mut delegate = InterceptRev::new(delegate);
19    let mut prev_kind = None;
20    if let Some(b'^') = input.first() {
21        input = next(input).1;
22        let kind = spec::Kind::ExcludeReachable;
23        delegate.kind(kind).ok_or(Error::Delegate)?;
24        prev_kind = kind.into();
25    }
26
27    let mut found_revision;
28    (input, found_revision) = {
29        let rest = revision(input, &mut delegate)?;
30        (rest, rest != input)
31    };
32    if delegate.done {
33        return if input.is_empty() {
34            Ok(())
35        } else {
36            Err(Error::UnconsumedInput { input: input.into() })
37        };
38    }
39    if let Some((rest, kind)) = try_range(input) {
40        if let Some(prev_kind) = prev_kind {
41            return Err(Error::KindSetTwice { prev_kind, kind });
42        }
43        if !found_revision {
44            delegate.find_ref("HEAD".into()).ok_or(Error::Delegate)?;
45        }
46        delegate.kind(kind).ok_or(Error::Delegate)?;
47        (input, found_revision) = {
48            let remainder = revision(rest.as_bstr(), &mut delegate)?;
49            (remainder, remainder != rest)
50        };
51        if !found_revision {
52            delegate.find_ref("HEAD".into()).ok_or(Error::Delegate)?;
53        }
54    }
55
56    if input.is_empty() {
57        delegate.done();
58        Ok(())
59    } else {
60        Err(Error::UnconsumedInput { input: input.into() })
61    }
62}
63
64mod intercept {
65    use bstr::{BStr, BString};
66
67    use crate::spec::parse::{delegate, Delegate};
68
69    #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
70    pub(crate) enum PrefixHintOwned {
71        MustBeCommit,
72        DescribeAnchor { ref_name: BString, generation: usize },
73    }
74
75    impl PrefixHintOwned {
76        pub fn to_ref(&self) -> delegate::PrefixHint<'_> {
77            match self {
78                PrefixHintOwned::MustBeCommit => delegate::PrefixHint::MustBeCommit,
79                PrefixHintOwned::DescribeAnchor { ref_name, generation } => delegate::PrefixHint::DescribeAnchor {
80                    ref_name: ref_name.as_ref(),
81                    generation: *generation,
82                },
83            }
84        }
85    }
86
87    impl<'a> From<delegate::PrefixHint<'a>> for PrefixHintOwned {
88        fn from(v: delegate::PrefixHint<'a>) -> Self {
89            match v {
90                delegate::PrefixHint::MustBeCommit => PrefixHintOwned::MustBeCommit,
91                delegate::PrefixHint::DescribeAnchor { generation, ref_name } => PrefixHintOwned::DescribeAnchor {
92                    ref_name: ref_name.to_owned(),
93                    generation,
94                },
95            }
96        }
97    }
98
99    pub(crate) struct InterceptRev<'a, T> {
100        pub inner: &'a mut T,
101        pub last_ref: Option<BString>, // TODO: smallvec to save the unnecessary allocation? Can't keep ref due to lifetime constraints in traits
102        pub last_prefix: Option<(gix_hash::Prefix, Option<PrefixHintOwned>)>,
103        pub done: bool,
104    }
105
106    impl<'a, T> InterceptRev<'a, T>
107    where
108        T: Delegate,
109    {
110        pub fn new(delegate: &'a mut T) -> Self {
111            InterceptRev {
112                inner: delegate,
113                last_ref: None,
114                last_prefix: None,
115                done: false,
116            }
117        }
118    }
119
120    impl<T> Delegate for InterceptRev<'_, T>
121    where
122        T: Delegate,
123    {
124        fn done(&mut self) {
125            self.done = true;
126            self.inner.done();
127        }
128    }
129
130    impl<T> delegate::Revision for InterceptRev<'_, T>
131    where
132        T: Delegate,
133    {
134        fn find_ref(&mut self, name: &BStr) -> Option<()> {
135            self.last_ref = name.to_owned().into();
136            self.inner.find_ref(name)
137        }
138
139        fn disambiguate_prefix(
140            &mut self,
141            prefix: gix_hash::Prefix,
142            hint: Option<delegate::PrefixHint<'_>>,
143        ) -> Option<()> {
144            self.last_prefix = Some((prefix, hint.map(Into::into)));
145            self.inner.disambiguate_prefix(prefix, hint)
146        }
147
148        fn reflog(&mut self, query: delegate::ReflogLookup) -> Option<()> {
149            self.inner.reflog(query)
150        }
151
152        fn nth_checked_out_branch(&mut self, branch_no: usize) -> Option<()> {
153            self.inner.nth_checked_out_branch(branch_no)
154        }
155
156        fn sibling_branch(&mut self, kind: delegate::SiblingBranch) -> Option<()> {
157            self.inner.sibling_branch(kind)
158        }
159    }
160
161    impl<T> delegate::Navigate for InterceptRev<'_, T>
162    where
163        T: Delegate,
164    {
165        fn traverse(&mut self, kind: delegate::Traversal) -> Option<()> {
166            self.inner.traverse(kind)
167        }
168
169        fn peel_until(&mut self, kind: delegate::PeelTo<'_>) -> Option<()> {
170            self.inner.peel_until(kind)
171        }
172
173        fn find(&mut self, regex: &BStr, negated: bool) -> Option<()> {
174            self.inner.find(regex, negated)
175        }
176
177        fn index_lookup(&mut self, path: &BStr, stage: u8) -> Option<()> {
178            self.inner.index_lookup(path, stage)
179        }
180    }
181
182    impl<T> delegate::Kind for InterceptRev<'_, T>
183    where
184        T: Delegate,
185    {
186        fn kind(&mut self, kind: crate::spec::Kind) -> Option<()> {
187            self.inner.kind(kind)
188        }
189    }
190}
191use intercept::InterceptRev;
192
193fn try_set_prefix(delegate: &mut impl Delegate, hex_name: &BStr, hint: Option<delegate::PrefixHint<'_>>) -> Option<()> {
194    gix_hash::Prefix::from_hex(hex_name.to_str().expect("hexadecimal only"))
195        .ok()
196        .and_then(|prefix| delegate.disambiguate_prefix(prefix, hint))
197}
198
199fn long_describe_prefix(name: &BStr) -> Option<(&BStr, delegate::PrefixHint<'_>)> {
200    let mut iter = name.rsplit(|b| *b == b'-');
201    let candidate = iter.by_ref().find_map(|substr| {
202        if substr.first()? != &b'g' {
203            return None;
204        };
205        let rest = substr.get(1..)?;
206        rest.iter().all(u8::is_ascii_hexdigit).then(|| rest.as_bstr())
207    })?;
208
209    let candidate = iter.clone().any(|token| !token.is_empty()).then_some(candidate);
210    let hint = iter
211        .next()
212        .and_then(|gen| gen.to_str().ok().and_then(|gen| usize::from_str(gen).ok()))
213        .and_then(|generation| {
214            iter.next().map(|token| {
215                let last_token_len = token.len();
216                let first_token_ptr = iter.last().map_or(token.as_ptr(), <[_]>::as_ptr);
217                // SAFETY: both pointers are definitely part of the same object
218                #[allow(unsafe_code)]
219                let prior_tokens_len: usize = unsafe { token.as_ptr().offset_from(first_token_ptr) }
220                    .try_into()
221                    .expect("positive value");
222                delegate::PrefixHint::DescribeAnchor {
223                    ref_name: name[..prior_tokens_len + last_token_len].as_bstr(),
224                    generation,
225                }
226            })
227        })
228        .unwrap_or(delegate::PrefixHint::MustBeCommit);
229
230    candidate.map(|c| (c, hint))
231}
232
233fn short_describe_prefix(name: &BStr) -> Option<&BStr> {
234    let mut iter = name.split(|b| *b == b'-');
235    let candidate = iter
236        .next()
237        .and_then(|prefix| prefix.iter().all(u8::is_ascii_hexdigit).then(|| prefix.as_bstr()));
238    (iter.count() == 1).then_some(candidate).flatten()
239}
240
241type InsideParensRestConsumed<'a> = (std::borrow::Cow<'a, BStr>, &'a BStr, usize);
242fn parens(input: &[u8]) -> Result<Option<InsideParensRestConsumed<'_>>, Error> {
243    if input.first() != Some(&b'{') {
244        return Ok(None);
245    }
246    let mut open_braces = 0;
247    let mut ignore_next = false;
248    let mut skip_list = Vec::new();
249    for (idx, b) in input.iter().enumerate() {
250        match *b {
251            b'{' => {
252                if ignore_next {
253                    ignore_next = false;
254                } else {
255                    open_braces += 1;
256                }
257            }
258            b'}' => {
259                if ignore_next {
260                    ignore_next = false;
261                } else {
262                    open_braces -= 1;
263                }
264            }
265            b'\\' => {
266                skip_list.push(idx);
267                if ignore_next {
268                    skip_list.pop();
269                    ignore_next = false;
270                } else {
271                    ignore_next = true;
272                }
273            }
274            _ => {
275                if ignore_next {
276                    skip_list.pop();
277                };
278                ignore_next = false;
279            }
280        }
281        if open_braces == 0 {
282            let inner: std::borrow::Cow<'_, _> = if skip_list.is_empty() {
283                input[1..idx].as_bstr().into()
284            } else {
285                let mut from = 1;
286                let mut buf = BString::default();
287                for next in skip_list.into_iter() {
288                    buf.push_str(&input[from..next]);
289                    from = next + 1;
290                }
291                if let Some(rest) = input.get(from..idx) {
292                    buf.push_str(rest);
293                }
294                buf.into()
295            };
296            return Ok(Some((inner, input[idx + 1..].as_bstr(), idx + 1)));
297        }
298    }
299    Err(Error::UnclosedBracePair { input: input.into() })
300}
301
302fn try_parse<T: FromStr + PartialEq + Default>(input: &BStr) -> Result<Option<T>, Error> {
303    input
304        .to_str()
305        .ok()
306        .and_then(|n| {
307            n.parse().ok().map(|n| {
308                if n == T::default() && input[0] == b'-' {
309                    return Err(Error::NegativeZero { input: input.into() });
310                };
311                Ok(n)
312            })
313        })
314        .transpose()
315}
316
317fn revision<'a, T>(mut input: &'a BStr, delegate: &mut InterceptRev<'_, T>) -> Result<&'a BStr, Error>
318where
319    T: Delegate,
320{
321    use delegate::{Navigate, Revision};
322    fn consume_all(res: Option<()>) -> Result<&'static BStr, Error> {
323        res.ok_or(Error::Delegate).map(|_| "".into())
324    }
325    match input.as_bytes() {
326        [b':'] => return Err(Error::MissingColonSuffix),
327        [b':', b'/'] => return Err(Error::EmptyTopLevelRegex),
328        [b':', b'/', regex @ ..] => {
329            let (regex, negated) = parse_regex_prefix(regex.as_bstr())?;
330            if regex.is_empty() {
331                return Err(Error::UnconsumedInput { input: input.into() });
332            }
333            return consume_all(delegate.find(regex, negated));
334        }
335        [b':', b'0', b':', path @ ..] => return consume_all(delegate.index_lookup(path.as_bstr(), 0)),
336        [b':', b'1', b':', path @ ..] => return consume_all(delegate.index_lookup(path.as_bstr(), 1)),
337        [b':', b'2', b':', path @ ..] => return consume_all(delegate.index_lookup(path.as_bstr(), 2)),
338        [b':', path @ ..] => return consume_all(delegate.index_lookup(path.as_bstr(), 0)),
339        _ => {}
340    };
341
342    let mut sep_pos = None;
343    let mut consecutive_hex_chars = Some(0);
344    {
345        let mut cursor = input;
346        let mut ofs = 0;
347        const SEPARATORS: &[u8] = b"~^:.";
348        while let Some((pos, b)) = cursor.iter().enumerate().find(|(pos, b)| {
349            if **b == b'@' {
350                if cursor.len() == 1 {
351                    return true;
352                }
353                let next = cursor.get(pos + 1);
354                let next_next = cursor.get(pos + 2);
355                if *pos != 0 && (next, next_next) == (Some(&b'.'), Some(&b'.')) {
356                    return false;
357                }
358                next == Some(&b'{') || next.is_some_and(|b| SEPARATORS.contains(b))
359            } else if SEPARATORS.contains(b) {
360                true
361            } else {
362                if let Some(num) = consecutive_hex_chars.as_mut() {
363                    if b.is_ascii_hexdigit() {
364                        *num += 1;
365                    } else {
366                        consecutive_hex_chars = None;
367                    }
368                }
369                false
370            }
371        }) {
372            if *b != b'.' || cursor.get(pos + 1) == Some(&b'.') {
373                sep_pos = Some(ofs + pos);
374                break;
375            }
376            ofs += pos + 1;
377            cursor = &cursor[pos + 1..];
378        }
379    }
380
381    let name = &input[..sep_pos.unwrap_or(input.len())].as_bstr();
382    let mut sep = sep_pos.map(|pos| input[pos]);
383    let mut has_ref_or_implied_name = name.is_empty();
384    if name.is_empty() && sep == Some(b'@') && sep_pos.and_then(|pos| input.get(pos + 1)) != Some(&b'{') {
385        delegate.find_ref("HEAD".into()).ok_or(Error::Delegate)?;
386        sep_pos = sep_pos.map(|pos| pos + 1);
387        sep = match sep_pos.and_then(|pos| input.get(pos).copied()) {
388            None => return Ok("".into()),
389            Some(pos) => Some(pos),
390        };
391    } else {
392        (consecutive_hex_chars.unwrap_or(0) >= gix_hash::Prefix::MIN_HEX_LEN)
393            .then(|| try_set_prefix(delegate, name, None))
394            .flatten()
395            .or_else(|| {
396                let (prefix, hint) = long_describe_prefix(name)
397                    .map(|(c, h)| (c, Some(h)))
398                    .or_else(|| short_describe_prefix(name).map(|c| (c, None)))?;
399                try_set_prefix(delegate, prefix, hint)
400            })
401            .or_else(|| {
402                name.is_empty().then_some(()).or_else(|| {
403                    #[allow(clippy::let_unit_value)]
404                    {
405                        let res = delegate.find_ref(name)?;
406                        has_ref_or_implied_name = true;
407                        res.into()
408                    }
409                })
410            })
411            .ok_or(Error::Delegate)?;
412    }
413
414    input = {
415        if let Some(b'@') = sep {
416            let past_sep = input[sep_pos.map_or(input.len(), |pos| pos + 1)..].as_bstr();
417            let (nav, rest, _consumed) = parens(past_sep)?.ok_or_else(|| Error::AtNeedsCurlyBrackets {
418                input: input[sep_pos.unwrap_or(input.len())..].into(),
419            })?;
420            let nav = nav.as_ref();
421            if let Some(n) = try_parse::<isize>(nav)? {
422                if n < 0 {
423                    if name.is_empty() {
424                        delegate
425                            .nth_checked_out_branch(n.unsigned_abs())
426                            .ok_or(Error::Delegate)?;
427                    } else {
428                        return Err(Error::RefnameNeedsPositiveReflogEntries { nav: nav.into() });
429                    }
430                } else if has_ref_or_implied_name {
431                    delegate
432                        .reflog(if n >= 100000000 {
433                            let time = nav
434                                .to_str()
435                                .map_err(|_| Error::Time {
436                                    input: nav.into(),
437                                    source: None,
438                                })
439                                .and_then(|date| {
440                                    gix_date::parse(date, None).map_err(|err| Error::Time {
441                                        input: nav.into(),
442                                        source: err.into(),
443                                    })
444                                })?;
445                            delegate::ReflogLookup::Date(time)
446                        } else {
447                            delegate::ReflogLookup::Entry(n.try_into().expect("non-negative isize fits usize"))
448                        })
449                        .ok_or(Error::Delegate)?;
450                } else {
451                    return Err(Error::ReflogLookupNeedsRefName { name: (*name).into() });
452                }
453            } else if let Some(kind) = SiblingBranch::parse(nav) {
454                if has_ref_or_implied_name {
455                    delegate.sibling_branch(kind).ok_or(Error::Delegate)
456                } else {
457                    Err(Error::SiblingBranchNeedsBranchName { name: (*name).into() })
458                }?;
459            } else if has_ref_or_implied_name {
460                let time = nav
461                    .to_str()
462                    .map_err(|_| Error::Time {
463                        input: nav.into(),
464                        source: None,
465                    })
466                    .and_then(|date| {
467                        gix_date::parse(date, Some(SystemTime::now())).map_err(|err| Error::Time {
468                            input: nav.into(),
469                            source: err.into(),
470                        })
471                    })?;
472                delegate
473                    .reflog(delegate::ReflogLookup::Date(time))
474                    .ok_or(Error::Delegate)?;
475            } else {
476                return Err(Error::ReflogLookupNeedsRefName { name: (*name).into() });
477            }
478            rest
479        } else {
480            if sep_pos == Some(0) && sep == Some(b'~') {
481                return Err(Error::MissingTildeAnchor);
482            }
483            input[sep_pos.unwrap_or(input.len())..].as_bstr()
484        }
485    };
486
487    navigate(input, delegate)
488}
489
490fn navigate<'a, T>(input: &'a BStr, delegate: &mut InterceptRev<'_, T>) -> Result<&'a BStr, Error>
491where
492    T: Delegate,
493{
494    use delegate::{Kind, Navigate, Revision};
495    let mut cursor = 0;
496    while let Some(b) = input.get(cursor) {
497        cursor += 1;
498        match *b {
499            b'~' => {
500                let (number, consumed) = input
501                    .get(cursor..)
502                    .and_then(|past_sep| try_parse_usize(past_sep.as_bstr()).transpose())
503                    .transpose()?
504                    .unwrap_or((1, 0));
505                if number != 0 {
506                    delegate
507                        .traverse(delegate::Traversal::NthAncestor(number))
508                        .ok_or(Error::Delegate)?;
509                }
510                cursor += consumed;
511            }
512            b'^' => {
513                let past_sep = input.get(cursor..);
514                if let Some((number, negative, consumed)) = past_sep
515                    .and_then(|past_sep| try_parse_isize(past_sep.as_bstr()).transpose())
516                    .transpose()?
517                {
518                    if negative {
519                        delegate
520                            .traverse(delegate::Traversal::NthParent(
521                                number
522                                    .checked_mul(-1)
523                                    .ok_or_else(|| Error::InvalidNumber {
524                                        input: past_sep.expect("present").into(),
525                                    })?
526                                    .try_into()
527                                    .expect("non-negative"),
528                            ))
529                            .ok_or(Error::Delegate)?;
530                        delegate.kind(spec::Kind::RangeBetween).ok_or(Error::Delegate)?;
531                        if let Some((prefix, hint)) = delegate.last_prefix.take() {
532                            match hint {
533                                Some(hint) => delegate.disambiguate_prefix(prefix, hint.to_ref().into()),
534                                None => delegate.disambiguate_prefix(prefix, None),
535                            }
536                            .ok_or(Error::Delegate)?;
537                        } else if let Some(name) = delegate.last_ref.take() {
538                            delegate.find_ref(name.as_bstr()).ok_or(Error::Delegate)?;
539                        } else {
540                            return Err(Error::UnconsumedInput {
541                                input: input[cursor..].into(),
542                            });
543                        }
544                        delegate.done();
545                        cursor += consumed;
546                        return Ok(input[cursor..].as_bstr());
547                    } else if number == 0 {
548                        delegate.peel_until(delegate::PeelTo::ObjectKind(gix_object::Kind::Commit))
549                    } else {
550                        delegate.traverse(delegate::Traversal::NthParent(
551                            number.try_into().expect("positive number"),
552                        ))
553                    }
554                    .ok_or(Error::Delegate)?;
555                    cursor += consumed;
556                } else if let Some((kind, _rest, consumed)) =
557                    past_sep.and_then(|past_sep| parens(past_sep).transpose()).transpose()?
558                {
559                    cursor += consumed;
560                    let target = match kind.as_ref().as_bytes() {
561                        b"commit" => delegate::PeelTo::ObjectKind(gix_object::Kind::Commit),
562                        b"tag" => delegate::PeelTo::ObjectKind(gix_object::Kind::Tag),
563                        b"tree" => delegate::PeelTo::ObjectKind(gix_object::Kind::Tree),
564                        b"blob" => delegate::PeelTo::ObjectKind(gix_object::Kind::Blob),
565                        b"object" => delegate::PeelTo::ValidObject,
566                        b"" => delegate::PeelTo::RecursiveTagObject,
567                        regex if regex.starts_with(b"/") => {
568                            let (regex, negated) = parse_regex_prefix(regex[1..].as_bstr())?;
569                            if !regex.is_empty() {
570                                delegate.find(regex, negated).ok_or(Error::Delegate)?;
571                            }
572                            continue;
573                        }
574                        invalid => return Err(Error::InvalidObject { input: invalid.into() }),
575                    };
576                    delegate.peel_until(target).ok_or(Error::Delegate)?;
577                } else if past_sep.and_then(<[_]>::first) == Some(&b'!') {
578                    delegate
579                        .kind(spec::Kind::ExcludeReachableFromParents)
580                        .ok_or(Error::Delegate)?;
581                    delegate.done();
582                    return Ok(input[cursor + 1..].as_bstr());
583                } else if past_sep.and_then(<[_]>::first) == Some(&b'@') {
584                    delegate
585                        .kind(spec::Kind::IncludeReachableFromParents)
586                        .ok_or(Error::Delegate)?;
587                    delegate.done();
588                    return Ok(input[cursor + 1..].as_bstr());
589                } else {
590                    delegate
591                        .traverse(delegate::Traversal::NthParent(1))
592                        .ok_or(Error::Delegate)?;
593                }
594            }
595            b':' => {
596                delegate
597                    .peel_until(delegate::PeelTo::Path(input[cursor..].as_bstr()))
598                    .ok_or(Error::Delegate)?;
599                return Ok("".into());
600            }
601            _ => return Ok(input[cursor - 1..].as_bstr()),
602        }
603    }
604    Ok("".into())
605}
606
607fn parse_regex_prefix(regex: &BStr) -> Result<(&BStr, bool), Error> {
608    Ok(match regex.strip_prefix(b"!") {
609        Some(regex) if regex.first() == Some(&b'!') => (regex.as_bstr(), false),
610        Some(regex) if regex.first() == Some(&b'-') => (regex[1..].as_bstr(), true),
611        Some(_regex) => return Err(Error::UnspecifiedRegexModifier { regex: regex.into() }),
612        None => (regex, false),
613    })
614}
615
616fn try_parse_usize(input: &BStr) -> Result<Option<(usize, usize)>, Error> {
617    let mut bytes = input.iter().peekable();
618    if bytes.peek().filter(|&&&b| b == b'-' || b == b'+').is_some() {
619        return Err(Error::SignedNumber { input: input.into() });
620    }
621    let num_digits = bytes.take_while(|b| b.is_ascii_digit()).count();
622    if num_digits == 0 {
623        return Ok(None);
624    }
625    let input = &input[..num_digits];
626    let number = try_parse(input)?.ok_or_else(|| Error::InvalidNumber { input: input.into() })?;
627    Ok(Some((number, num_digits)))
628}
629
630fn try_parse_isize(input: &BStr) -> Result<Option<(isize, bool, usize)>, Error> {
631    let mut bytes = input.iter().peekable();
632    if bytes.peek().filter(|&&&b| b == b'+').is_some() {
633        return Err(Error::SignedNumber { input: input.into() });
634    }
635    let negative = bytes.peek() == Some(&&b'-');
636    let num_digits = bytes.take_while(|b| b.is_ascii_digit() || *b == &b'-').count();
637    if num_digits == 0 {
638        return Ok(None);
639    } else if num_digits == 1 && negative {
640        return Ok(Some((-1, negative, num_digits)));
641    }
642    let input = &input[..num_digits];
643    let number = try_parse(input)?.ok_or_else(|| Error::InvalidNumber { input: input.into() })?;
644    Ok(Some((number, negative, num_digits)))
645}
646
647fn try_range(input: &BStr) -> Option<(&[u8], spec::Kind)> {
648    input
649        .strip_prefix(b"...")
650        .map(|rest| (rest, spec::Kind::ReachableToMergeBase))
651        .or_else(|| input.strip_prefix(b"..").map(|rest| (rest, spec::Kind::RangeBetween)))
652}
653
654fn next(i: &BStr) -> (u8, &BStr) {
655    let b = i[0];
656    (b, i[1..].as_bstr())
657}