Skip to main content

relay_pattern/
wildmatch.rs

1use std::num::NonZeroUsize;
2
3use smallvec::SmallVec;
4
5use crate::{Literal, Options, Ranges, Token, Tokens};
6
7/// Matches [`Tokens`] against a `haystack` with the provided [`Options`].
8///
9/// This implementation is largely based on the algorithm described by [Kirk J Krauss]
10/// and combining the two loops into a single one and other small modifications to take advantage
11/// of the already pre-processed [`Tokens`] structure and its invariants.
12///
13/// [Kirk J Krauss]: http://developforperformance.com/MatchingWildcards_AnImprovedAlgorithmForBigData.html
14pub fn is_match(haystack: &str, tokens: &Tokens, options: Options) -> bool {
15    match options.case_insensitive {
16        false => is_match_impl::<CaseSensitive>(haystack, tokens.as_slice()),
17        true => is_match_impl::<CaseInsensitive>(haystack, tokens.as_slice()),
18    }
19}
20
21#[inline(always)]
22fn is_match_impl<'a, M>(haystack: &'a str, tokens: &'a [Token]) -> bool
23where
24    M: Matcher,
25{
26    // Empty glob never matches.
27    if tokens.is_empty() {
28        return false;
29    }
30
31    // Stack of matching attempts, the top of the stack is the attempt which is
32    // currently being matched.
33    //
34    // Each alternation pushes a new frame on the stack, containing a list of branches
35    // needing to be matched.
36    let mut frames: SmallVec<[Frame<'a>; 5]> = smallvec::smallvec![Frame::root(haystack, tokens)];
37
38    // Whether the last evaluated token matched.
39    let mut matched = true;
40
41    loop {
42        let Some(frame) = frames.last_mut() else {
43            // All alternation branches exhausted -> no match.
44            return false;
45        };
46
47        // Matches the current attempt against the haystack, including wildcard backtracking.
48        //
49        // The loop:
50        //  - Returns `true` if a match was found.
51        //  - Breaks with a new frame, when an alternate is found.
52        //  - Breaks with `None` if the current alternate does not match.
53        let new_frame = loop {
54            if !matched {
55                if frame.t_revert == 0 {
56                    // No backtracking possible, no wildcard was encountered
57                    // in the current attempt.
58                    break None;
59                }
60                frame.h_current = frame.h_revert;
61                frame.t_next = frame.t_revert;
62
63                // Backtrack to the previous location +1 character.
64                match n_chars_to_bytes(NonZeroUsize::MIN, frame.h_current) {
65                    Some(n) => frame.h_current = &frame.h_current[n..],
66                    // The haystack is exhausted.
67                    None => break None,
68                }
69
70                if !frame.skip_to_next_token::<M>() {
71                    break None;
72                }
73            }
74
75            if frame.t_next == frame.stream.len() {
76                if frame.h_current.is_empty() {
77                    // All tokens and the entire haystack are consumed -> match.
78                    return true;
79                }
80                // There is haystack remaining, only backtracking can consume more of it.
81                matched = false;
82                continue;
83            }
84
85            let token = frame.stream.get(frame.t_next);
86            frame.t_next += 1;
87
88            matched = match token {
89                Token::Literal(literal) => match M::is_prefix(frame.h_current, literal) {
90                    Some(n) => {
91                        frame.h_current = &frame.h_current[n..];
92                        true
93                    }
94                    // The literal does not match, but it may match after backtracking.
95                    // TODO: possible optimization: if the literal cannot possibly match
96                    // anymore because it is too long for the remaining haystack, we can
97                    // immediately give up on the current attempt here.
98                    None => false,
99                },
100                Token::Any(n) => match n_chars_to_bytes(*n, frame.h_current) {
101                    Some(n) => {
102                        frame.h_current = &frame.h_current[n..];
103                        true
104                    }
105                    // Not enough characters in the haystack remaining and backtracking
106                    // only shrinks the haystack, there cannot be any other possible
107                    // match in the current attempt.
108                    None => break None,
109                },
110                Token::Wildcard => {
111                    // `ab*c*` matches `abcd`.
112                    if frame.t_next == frame.stream.len() {
113                        return true;
114                    }
115
116                    frame.t_revert = frame.t_next;
117
118                    if !frame.skip_to_next_token::<M>() {
119                        break None;
120                    }
121                    true
122                }
123                Token::Class { negated, ranges } => match frame.h_current.chars().next() {
124                    Some(next) if M::ranges_match(next, *negated, ranges) => {
125                        frame.h_current = &frame.h_current[next.len_utf8()..];
126                        true
127                    }
128                    _ => false,
129                },
130                // The parent frame is already in the correct state to continue matching
131                // after the alternation, all it takes is a new frame for the alternation.
132                Token::Alternates(alternates) => {
133                    break Some(frame.new_branch(alternates.as_slice(), false));
134                }
135                Token::Optional(optional) => {
136                    break Some(frame.new_branch(std::slice::from_ref(optional), true));
137                }
138            };
139        };
140
141        match new_frame {
142            // An alternation was just entered, match its first branch.
143            Some(mut new_frame) => {
144                if new_frame.enter_next_alternate() {
145                    frames.push(new_frame);
146                    matched = true;
147                } else {
148                    // An alternation without any branches, there is nothing to match,
149                    // continue in the current frame.
150                    //
151                    // The parser never produces empty alternations, but they are
152                    // gracefully handled here like an alternation which did not match.
153                    matched = new_frame.optional;
154                }
155            }
156            // The current alternate can no longer match, continue with the next alternate.
157            None if frame.enter_next_alternate() => {
158                matched = true;
159            }
160            // The current frame is exhausted, all alternates did not match.
161            None => {
162                // All branches failed, continue with the parent and search for alternative
163                // matches.
164                //
165                // If the current frame is marked optional, we did match and the parent does
166                // not need to try to match alternates or backtrack.
167                matched = frame.optional;
168                frames.pop();
169            }
170        }
171    }
172}
173
174/// Bundles necessary matchers for [`is_match_impl`].
175trait Matcher {
176    /// Returns the length of the `needle` in the `haystack` if the `needle` is a prefix of `haystack`.
177    fn is_prefix(haystack: &str, needle: &Literal) -> Option<usize>;
178    /// Searches for the `needle` in the `haystack` and returns the index of the start of the match
179    /// and the length of match or `None` if the `needle` is not contained in the `haystack`.
180    fn find(haystack: &str, needle: &Literal) -> Option<(usize, usize)>;
181    /// Returns `true` if the char `c` is contained within `ranges`.
182    fn ranges_match(c: char, negated: bool, ranges: &Ranges) -> bool;
183    /// Searches for the first char in the `haystack` that is contained in one of the `ranges`.
184    ///
185    /// Returns the offset in bytes and matching `char` if the range is contained within the
186    /// `haystack`.
187    #[inline(always)]
188    fn ranges_find(haystack: &str, negated: bool, ranges: &Ranges) -> Option<(usize, char)> {
189        // TODO: possibly optimize range finding.
190        // TODO: possibly optimize with `memchr{1,2,3}` for short ranges.
191        haystack
192            .char_indices()
193            .find(|&(_, c)| Self::ranges_match(c, negated, ranges))
194    }
195}
196
197/// A case sensitive [`Matcher`].
198struct CaseSensitive;
199
200impl Matcher for CaseSensitive {
201    #[inline(always)]
202    fn is_prefix(haystack: &str, needle: &Literal) -> Option<usize> {
203        let needle = needle.as_case_converted_bytes();
204        memchr::arch::all::is_prefix(haystack.as_bytes(), needle).then_some(needle.len())
205    }
206
207    #[inline(always)]
208    fn find(haystack: &str, needle: &Literal) -> Option<(usize, usize)> {
209        let needle = needle.as_case_converted_bytes();
210        memchr::memmem::find(haystack.as_bytes(), needle).map(|offset| (offset, needle.len()))
211    }
212
213    #[inline(always)]
214    fn ranges_match(c: char, negated: bool, ranges: &Ranges) -> bool {
215        ranges.contains(c) ^ negated
216    }
217}
218
219/// A case insensitive [`Matcher`].
220struct CaseInsensitive;
221
222impl Matcher for CaseInsensitive {
223    #[inline(always)]
224    fn is_prefix(haystack: &str, needle: &Literal) -> Option<usize> {
225        // We can safely assume `needle` is already full lowercase. This transformation is done on
226        // token creation based on the options.
227        //
228        // The haystack cannot be converted to full lowercase to not break class matches on
229        // uppercase unicode characters which would produce multiple lowercase characters.
230        //
231        // TODO: benchmark if allocation free is better/faster.
232        let needle = needle.as_case_converted_bytes();
233        let lower_haystack = haystack.to_lowercase();
234
235        memchr::arch::all::is_prefix(lower_haystack.as_bytes(), needle)
236            .then(|| recover_offset_len(haystack, 0, needle.len()).1)
237    }
238
239    #[inline(always)]
240    fn find(haystack: &str, needle: &Literal) -> Option<(usize, usize)> {
241        // TODO: implement manual lowercase which remembers if there were 'special' unicode
242        // conversion involved, if not, there is no recovery necessary.
243        // TODO: benchmark if a lut from offset -> original offset makes sense.
244        // TODO: benchmark allocation free and search with proper case insensitive search.
245        let needle = needle.as_case_converted_bytes();
246        let lower_haystack = haystack.to_lowercase();
247
248        let offset = memchr::memmem::find(lower_haystack.as_bytes(), needle)?;
249
250        // `offset` now points into the lowercase converted string, but this may not match the
251        // offset in the original string. Time to recover the index.
252        Some(recover_offset_len(haystack, offset, offset + needle.len()))
253    }
254
255    #[inline(always)]
256    fn ranges_match(c: char, negated: bool, ranges: &Ranges) -> bool {
257        let matches = exactly_one(c.to_lowercase()).is_some_and(|c| ranges.contains(c))
258            || exactly_one(c.to_uppercase()).is_some_and(|c| ranges.contains(c));
259        matches ^ negated
260    }
261}
262
263/// Efficiently skips to the next possible match after a wildcard.
264///
265/// Returns `None` if there is no match and the matching can be aborted.
266/// Otherwise returns the amount of tokens consumed, the new save point to backtrack to
267/// and the remaining haystack.
268#[inline(always)]
269fn skip_to_token<'a, M>(next: &Token, haystack: &'a str) -> Option<(bool, &'a str, &'a str)>
270where
271    M: Matcher,
272{
273    // TODO: optimize other cases like:
274    //  - `[Any(n), Literal(_), ..]` (skip + literal find)
275    //  - `[Any(n)]` (minimum remaining length)
276    Some(match next {
277        Token::Literal(literal) => {
278            match M::find(haystack, literal) {
279                // We cannot use `offset + literal.len()` as the revert position
280                // to not discard overlapping matches.
281                Some((offset, len)) => (true, &haystack[offset..], &haystack[offset + len..]),
282                // The literal does not exist in the remaining slice.
283                // No backtracking necessary, we won't ever find it.
284                None => return None,
285            }
286        }
287        Token::Class { negated, ranges } => {
288            match M::ranges_find(haystack, *negated, ranges) {
289                Some((offset, c)) => (
290                    true,
291                    &haystack[offset..],
292                    &haystack[offset + c.len_utf8()..],
293                ),
294                // None of the remaining characters matches this class.
295                // No backtracking necessary, we won't ever find it.
296                None => return None,
297            }
298        }
299        _ => {
300            // We didn't consume and match the token, revert to the previous state and
301            // let the generic matching with slower backtracking handle the token.
302            (false, haystack, haystack)
303        }
304    })
305}
306
307/// Calculates a byte offset of the next `n` chars in the string `s`.
308///
309/// Returns `None` if the string is too short.
310#[inline(always)]
311fn n_chars_to_bytes(n: NonZeroUsize, s: &str) -> Option<usize> {
312    // Fast path check, if there are less bytes than characters.
313    if n.get() > s.len() {
314        return None;
315    }
316    s.char_indices()
317        .nth(n.get() - 1)
318        .map(|(i, c)| i + c.len_utf8())
319}
320
321/// Returns `Some` if `iter` contains exactly one element.
322#[inline(always)]
323fn exactly_one<T>(mut iter: impl Iterator<Item = T>) -> Option<T> {
324    let item = iter.next()?;
325    match iter.next() {
326        Some(_) => None,
327        None => Some(item),
328    }
329}
330
331/// Recovers offset and length from a case insensitive search in `haystack` using a lowecase
332/// haystack and a lowercase needle.
333///
334/// `lower_offset` is the offset of the match in the lowercase haystack.
335/// `lower_end` is the end offset of the match in the lowercase haystack.
336///
337/// Returns the recovered offset and length.
338#[inline(always)]
339fn recover_offset_len(
340    haystack: &str,
341    lower_offset: usize,
342    lower_offset_end: usize,
343) -> (usize, usize) {
344    haystack
345        .chars()
346        .try_fold((0, 0, 0), |(lower, h_offset, h_len), c| {
347            let lower = lower + c.to_lowercase().map(|c| c.len_utf8()).sum::<usize>();
348
349            if lower <= lower_offset {
350                Ok((lower, h_offset + c.len_utf8(), 0))
351            } else if lower <= lower_offset_end {
352                Ok((lower, h_offset, h_len + c.len_utf8()))
353            } else {
354                Err((h_offset, h_len))
355            }
356        })
357        .map_or_else(|e| e, |(_, offset, len)| (offset, len))
358}
359
360/// The stream of tokens which is currently being matched against the haystack.
361#[derive(Default, Clone, Copy, Debug)]
362struct TokenStream<'a> {
363    /// The tokens of the currently active alternation branch.
364    ///
365    /// Empty, if no alternate is being evaluated.
366    alternate: &'a [Token],
367    /// The remaining tokens of the original pattern to match against.
368    tokens: &'a [Token],
369}
370
371impl<'a> TokenStream<'a> {
372    /// Total amount of tokens in the stream.
373    #[inline(always)]
374    fn len(&self) -> usize {
375        self.alternate.len() + self.tokens.len()
376    }
377
378    /// Returns the token at position `index`.
379    ///
380    /// Panics if `index` is out of bounds.
381    #[inline(always)]
382    fn get(&self, index: usize) -> &'a Token {
383        match self.alternate.get(index) {
384            Some(token) => token,
385            None => &self.tokens[index - self.alternate.len()],
386        }
387    }
388
389    /// Returns the remaining tokens of the original pattern starting at `t_next`.
390    fn base_suffix(&self, t_next: usize) -> &'a [Token] {
391        match t_next.checked_sub(self.alternate.len()) {
392            Some(offset) => &self.tokens[offset..],
393            // This would only happen if `t_next` points into the alternates,
394            // which is not possible since we do not allow nesting alternates.
395            None => unreachable!("No nested alternates"),
396        }
397    }
398}
399
400/// A single matching attempt.
401///
402/// It consists of two parts:
403/// 1. Current matching information
404/// 2. Potential alternates to match
405///
406/// If the current match (1) is exhausted and does not match the haystack,
407/// another alternate is queried from (2) and replaces the failed match
408/// in (1).
409struct Frame<'a> {
410    /// The stream of tokens which is currently being matched.
411    stream: TokenStream<'a>,
412    /// Remainder of the haystack which still needs to be matched.
413    h_current: &'a str,
414    /// Saved haystack position for backtracking.
415    h_revert: &'a str,
416    /// The next token position in `stream` which needs to be evaluated.
417    t_next: usize,
418    /// Revert index for `stream`. In case of backtracking we backtrack to this index.
419    ///
420    /// If `t_revert` is zero, it means there is no currently saved backtracking position.
421    t_revert: usize,
422
423    /// The haystack at the position the alternation token was encountered and needs
424    /// to be matched against.
425    haystack: &'a str,
426    /// The alternations which still need to be tried.
427    ///
428    /// Empty for the root frame.
429    alternates: std::slice::Iter<'a, Tokens>,
430    /// Whether the alternates are optional.
431    ///
432    /// If a frame is optional, none of its alternates need to match for the entire frame
433    /// considered matching.
434    optional: bool,
435    /// The tokens of the original pattern following the alternation token.
436    ///
437    /// Every alternation branch is followed by these tokens.
438    base: &'a [Token],
439}
440
441impl<'a> Frame<'a> {
442    /// Creates the root frame matching the full pattern against the full haystack.
443    fn root(haystack: &'a str, tokens: &'a [Token]) -> Self {
444        Self {
445            stream: TokenStream {
446                alternate: &[],
447                tokens,
448            },
449            h_current: haystack,
450            h_revert: haystack,
451            t_next: 0,
452            t_revert: 0,
453            alternates: Default::default(),
454            optional: false,
455            haystack,
456            base: &[],
457        }
458    }
459
460    /// Creates a child frame starting at the current location with a list of `alternates` to evaluate.
461    ///
462    /// The parent's `t_next` must already point past the alternation token.
463    /// The working state is initialized when the first branch is entered.
464    fn new_branch(&self, alternates: &'a [Tokens], optional: bool) -> Self {
465        Self {
466            stream: TokenStream::default(),
467            h_current: self.h_current,
468            h_revert: self.h_current,
469            t_next: 0,
470            t_revert: 0,
471            alternates: alternates.iter(),
472            optional,
473            haystack: self.h_current,
474            base: self.stream.base_suffix(self.t_next),
475        }
476    }
477
478    /// Enters the next branch of the alternation and resets the working state.
479    ///
480    /// Returns `false` if all alternates are already exhausted.
481    fn enter_next_alternate(&mut self) -> bool {
482        let Some(branch) = self.alternates.next() else {
483            return false;
484        };
485
486        self.stream = TokenStream {
487            alternate: branch.as_slice(),
488            tokens: self.base,
489        };
490        self.h_current = self.haystack;
491        self.h_revert = self.haystack;
492        self.t_next = 0;
493        self.t_revert = 0;
494
495        true
496    }
497
498    /// Efficiently skips to the next possible match after a wildcard.
499    ///
500    /// Like [`skip_to_token`] but advances the frame state.
501    ///
502    /// Returns `false` if no match is possible and matching can be aborted.
503    #[inline(always)]
504    fn skip_to_next_token<M: Matcher>(&mut self) -> bool {
505        let next = self.stream.get(self.t_next);
506
507        match skip_to_token::<M>(next, self.h_current) {
508            Some((consumed, revert, remaining)) => {
509                self.t_next += consumed as usize;
510                self.h_revert = revert;
511                self.h_current = remaining;
512                true
513            }
514            None => false,
515        }
516    }
517}
518
519// Just some tests, full test suite is run on globs not tokens.
520#[cfg(test)]
521mod tests {
522    use super::*;
523    use crate::{Range, Ranges};
524
525    fn literal(s: &str) -> Literal {
526        Literal::new(s.to_owned(), Default::default())
527    }
528
529    fn literal_ci(s: &str) -> Literal {
530        Literal::new(
531            s.to_owned(),
532            Options {
533                case_insensitive: true,
534            },
535        )
536    }
537
538    fn range(start: char, end: char) -> Ranges {
539        Ranges::Single(Range { start, end })
540    }
541
542    #[test]
543    fn test_exactly_one() {
544        assert_eq!(exactly_one([].into_iter()), None::<i32>);
545        assert_eq!(exactly_one([1].into_iter()), Some(1));
546        assert_eq!(exactly_one([1, 2].into_iter()), None);
547        assert_eq!(exactly_one([1, 2, 3].into_iter()), None);
548    }
549
550    #[test]
551    fn test_literal() {
552        let mut tokens = Tokens::default();
553        tokens.push(Token::Literal(literal("abc")));
554        assert!(is_match("abc", &tokens, Default::default()));
555        assert!(!is_match("abcd", &tokens, Default::default()));
556        assert!(!is_match("bc", &tokens, Default::default()));
557    }
558
559    #[test]
560    fn test_class() {
561        let mut tokens = Tokens::default();
562        tokens.push(Token::Literal(literal("a")));
563        tokens.push(Token::Class {
564            negated: false,
565            ranges: Ranges::Single(Range::single('b')),
566        });
567        tokens.push(Token::Literal(literal("c")));
568        assert!(is_match("abc", &tokens, Default::default()));
569        assert!(!is_match("aac", &tokens, Default::default()));
570        assert!(!is_match("abbc", &tokens, Default::default()));
571    }
572
573    #[test]
574    fn test_class_negated() {
575        let mut tokens = Tokens::default();
576        tokens.push(Token::Literal(literal("a")));
577        tokens.push(Token::Class {
578            negated: true,
579            ranges: Ranges::Single(Range::single('b')),
580        });
581        tokens.push(Token::Literal(literal("c")));
582        assert!(!is_match("abc", &tokens, Default::default()));
583        assert!(is_match("aac", &tokens, Default::default()));
584        assert!(!is_match("abbc", &tokens, Default::default()));
585    }
586
587    #[test]
588    fn test_any_one() {
589        let mut tokens = Tokens::default();
590        tokens.push(Token::Literal(literal("a")));
591        tokens.push(Token::Any(NonZeroUsize::MIN));
592        tokens.push(Token::Literal(literal("c")));
593        assert!(is_match("abc", &tokens, Default::default()));
594        assert!(is_match("aඞc", &tokens, Default::default()));
595        assert!(!is_match("abbc", &tokens, Default::default()));
596    }
597
598    #[test]
599    fn test_any_many() {
600        let mut tokens = Tokens::default();
601        tokens.push(Token::Literal(literal("a")));
602        tokens.push(Token::Any(NonZeroUsize::new(2).unwrap()));
603        tokens.push(Token::Literal(literal("d")));
604        assert!(is_match("abcd", &tokens, Default::default()));
605        assert!(is_match("aඞ_d", &tokens, Default::default()));
606        assert!(!is_match("abbc", &tokens, Default::default()));
607        assert!(!is_match("abcde", &tokens, Default::default()));
608        assert!(!is_match("abc", &tokens, Default::default()));
609        assert!(!is_match("bcd", &tokens, Default::default()));
610    }
611
612    #[test]
613    fn test_any_unicode() {
614        let mut tokens = Tokens::default();
615        tokens.push(Token::Literal(literal("a")));
616        tokens.push(Token::Any(NonZeroUsize::new(3).unwrap()));
617        tokens.push(Token::Literal(literal("a")));
618        assert!(is_match("abbba", &tokens, Default::default()));
619        assert!(is_match("aඞbඞa", &tokens, Default::default()));
620        // `i̇` is `i\u{307}`
621        assert!(is_match("aඞi̇a", &tokens, Default::default()));
622        assert!(!is_match("aඞi̇ඞa", &tokens, Default::default()));
623    }
624
625    #[test]
626    fn test_wildcard_start() {
627        let mut tokens = Tokens::default();
628        tokens.push(Token::Wildcard);
629        tokens.push(Token::Literal(literal("b")));
630        assert!(is_match("b", &tokens, Default::default()));
631        assert!(is_match("aaaab", &tokens, Default::default()));
632        assert!(is_match("ඞb", &tokens, Default::default()));
633        assert!(is_match("bbbbbbbbb", &tokens, Default::default()));
634        assert!(!is_match("", &tokens, Default::default()));
635        assert!(!is_match("a", &tokens, Default::default()));
636        assert!(!is_match("aa", &tokens, Default::default()));
637        assert!(!is_match("aaa", &tokens, Default::default()));
638        assert!(!is_match("ba", &tokens, Default::default()));
639    }
640
641    #[test]
642    fn test_wildcard_end() {
643        let mut tokens = Tokens::default();
644        tokens.push(Token::Literal(literal("a")));
645        tokens.push(Token::Wildcard);
646        assert!(is_match("a", &tokens, Default::default()));
647        assert!(is_match("aaaab", &tokens, Default::default()));
648        assert!(is_match("aඞ", &tokens, Default::default()));
649        assert!(!is_match("", &tokens, Default::default()));
650        assert!(!is_match("b", &tokens, Default::default()));
651        assert!(!is_match("bb", &tokens, Default::default()));
652        assert!(!is_match("bbb", &tokens, Default::default()));
653        assert!(!is_match("ba", &tokens, Default::default()));
654    }
655
656    #[test]
657    fn test_wildcard_end_unicode_case_insensitive() {
658        let options = Options {
659            case_insensitive: true,
660        };
661        let mut tokens = Tokens::default();
662        tokens.push(Token::Literal(Literal::new("İ".to_owned(), options)));
663        tokens.push(Token::Wildcard);
664
665        assert!(is_match("İ___", &tokens, options));
666        assert!(is_match("İ", &tokens, options));
667        assert!(is_match("i̇", &tokens, options));
668        assert!(is_match("i\u{307}___", &tokens, options));
669        assert!(!is_match("i____", &tokens, options));
670    }
671
672    #[test]
673    fn test_alternate() {
674        let mut tokens = Tokens::default();
675        tokens.push(Token::Literal(literal("a")));
676        tokens.push(Token::Alternates(vec![
677            {
678                let mut tokens = Tokens::default();
679                tokens.push(Token::Literal(literal("b")));
680                tokens
681            },
682            {
683                let mut tokens = Tokens::default();
684                tokens.push(Token::Literal(literal("c")));
685                tokens
686            },
687        ]));
688        tokens.push(Token::Literal(literal("a")));
689        assert!(is_match("aba", &tokens, Default::default()));
690        assert!(is_match("aca", &tokens, Default::default()));
691        assert!(!is_match("ada", &tokens, Default::default()));
692    }
693
694    #[test]
695    fn test_optional() {
696        let mut tokens = Tokens::default();
697
698        tokens.push(Token::Optional(Tokens(vec![Token::Literal(literal(
699            "foo",
700        ))])));
701        assert!(is_match("foo", &tokens, Default::default()));
702        assert!(is_match("", &tokens, Default::default()));
703    }
704
705    #[test]
706    fn test_optional_alternate() {
707        let mut tokens = Tokens::default();
708        let alternates = Token::Alternates(vec![
709            {
710                let mut tokens = Tokens::default();
711                tokens.push(Token::Literal(literal("foo")));
712                tokens
713            },
714            {
715                let mut tokens = Tokens::default();
716                tokens.push(Token::Literal(literal("bar")));
717                tokens
718            },
719        ]);
720
721        tokens.push(Token::Optional(Tokens(vec![alternates])));
722        assert!(is_match("foo", &tokens, Default::default()));
723        assert!(is_match("bar", &tokens, Default::default()));
724        assert!(is_match("", &tokens, Default::default()));
725    }
726
727    #[test]
728    fn test_matcher_case_sensitive_prefix() {
729        macro_rules! test {
730            ($haystack:expr, $needle:expr, $result:expr) => {
731                assert_eq!(
732                    CaseSensitive::is_prefix($haystack, &literal($needle)),
733                    $result
734                );
735            };
736        }
737
738        test!("foobar", "f", Some(1));
739        test!("foobar", "foo", Some(3));
740        test!("foobar", "foobar", Some(6));
741        test!("foobar", "oobar", None);
742        test!("foobar", "foobar2", None);
743        test!("İ", "İ", Some(2));
744        test!("İ", "i", None);
745        test!("i", "İ", None);
746        test!("i", "i", Some(1));
747        test!("i̇", "i", Some(1));
748        test!("i̇", "i\u{307}", Some(3));
749        test!("i̇x", "i\u{307}", Some(3));
750        test!("i̇x", "i\u{307}x", Some(4));
751        test!("i̇x", "i\u{307}_", None);
752    }
753
754    #[test]
755    fn test_matcher_case_sensitive_find() {
756        macro_rules! test {
757            ($haystack:expr, $needle:expr, $result:expr) => {
758                assert_eq!(CaseSensitive::find($haystack, &literal($needle)), $result);
759            };
760        }
761
762        test!("foobar", "f", Some((0, 1)));
763        test!("foobar", "foo", Some((0, 3)));
764        test!("foobar", "foobar", Some((0, 6)));
765        test!("foobar", "bar", Some((3, 3)));
766        test!("foobar", "oobar", Some((1, 5)));
767        test!("foobar", "foobar2", None);
768        test!("İ", "İ", Some((0, 2)));
769        test!("i", "i", Some((0, 1)));
770        test!("i̇", "i\u{307}", Some((0, 3)));
771        test!("i̇x", "i\u{307}x", Some((0, 4)));
772        test!("i̇x", "i\u{307}_", None);
773        test!("xi̇x", "i\u{307}", Some((1, 3)));
774        test!("xi̇ඞi̇x", "ඞ", Some((4, 3)));
775        test!("xi̇ඞi̇x", "ඞi̇", Some((4, 6)));
776    }
777
778    #[test]
779    fn test_matcher_case_sensitive_ranges_match() {
780        macro_rules! test {
781            ($c:expr, $negated:expr, [$start:literal - $end:literal], $result:expr) => {
782                assert_eq!(
783                    CaseSensitive::ranges_match($c, $negated, &range($start, $end)),
784                    $result
785                );
786            };
787        }
788
789        test!('a', false, ['a' - 'a'], true);
790        test!('a', true, ['a' - 'a'], false);
791        test!('b', false, ['a' - 'a'], false);
792        test!('b', true, ['a' - 'a'], true);
793        test!('b', false, ['a' - 'c'], true);
794        test!('b', false, ['b' - 'c'], true);
795        test!('b', false, ['a' - 'b'], true);
796
797        test!('A', false, ['a' - 'a'], false);
798        test!('ඞ', false, ['ඞ' - 'ඞ'], true);
799    }
800
801    #[test]
802    fn test_matcher_case_sensitive_ranges_find() {
803        macro_rules! test {
804            ($haystack:expr, $negated:expr, [$start:literal - $end:literal], $result:expr) => {
805                assert_eq!(
806                    CaseSensitive::ranges_find($haystack, $negated, &range($start, $end)),
807                    $result
808                );
809            };
810        }
811
812        test!("ඞaඞ", false, ['a' - 'a'], Some((3, 'a')));
813        test!("a", true, ['a' - 'a'], None);
814        test!("ඞaඞ", true, ['a' - 'a'], Some((0, 'ඞ')));
815        test!("aඞaඞ", true, ['a' - 'a'], Some((1, 'ඞ')));
816        test!("ඞbඞ", false, ['a' - 'a'], None);
817        test!("ඞbඞ", true, ['ඞ' - 'ඞ'], Some((3, 'b')));
818        test!("ඞbඞ", false, ['a' - 'c'], Some((3, 'b')));
819        test!("ඞbඞ", false, ['b' - 'c'], Some((3, 'b')));
820        test!("ඞbඞ", false, ['a' - 'b'], Some((3, 'b')));
821        test!("AAAAA", false, ['a' - 'a'], None);
822        test!("aaaaaaabb", true, ['a' - 'a'], Some((7, 'b')));
823        test!("AaaaaaAbb", false, ['b' - 'b'], Some((7, 'b')));
824    }
825
826    #[test]
827    fn test_matcher_case_insensitive_prefix() {
828        macro_rules! test {
829            ($haystack:expr, $needle:expr, $result:expr) => {
830                assert_eq!(
831                    CaseInsensitive::is_prefix($haystack, &literal_ci($needle)),
832                    $result
833                );
834            };
835        }
836
837        test!("foobar", "f", Some(1));
838        test!("foobar", "F", Some(1));
839        test!("fOobar", "foo", Some(3));
840        test!("fooBAR", "foobar", Some(6));
841        test!("foobar", "oobar", None);
842        test!("FOOBAR", "oobar", None);
843        test!("foobar", "foobar2", None);
844        test!("İ", "İ", Some(2));
845        test!("İ", "i", Some(0));
846        test!("İ", "i̇", Some(2));
847        test!("i", "İ", None);
848        test!("i", "i", Some(1));
849        test!("i̇", "i", Some(1));
850        test!("i̇", "i\u{307}", Some(3));
851        test!("i̇x", "i\u{307}", Some(3));
852        test!("i̇x", "i\u{307}x", Some(4));
853        test!("i̇x", "i\u{307}_", None);
854    }
855
856    #[test]
857    fn test_matcher_case_insensitive_find() {
858        macro_rules! test {
859            ($haystack:expr, $needle:expr, $result:expr) => {
860                assert_eq!(
861                    CaseInsensitive::find($haystack, &literal_ci($needle)),
862                    $result
863                );
864            };
865        }
866
867        test!("Foobar", "f", Some((0, 1)));
868        test!("foObar", "FOO", Some((0, 3)));
869        test!("foObar", "Foobar", Some((0, 6)));
870        test!("foObar", "bar", Some((3, 3)));
871        test!("foObarx", "bar", Some((3, 3)));
872        test!("foObarbarbar", "bar", Some((3, 3)));
873        test!("foObar", "Oobar", Some((1, 5)));
874        test!("foObar", "Foobar2", None);
875        test!("İ", "İ", Some((0, 2)));
876        test!("i", "i", Some((0, 1)));
877        test!("i̇", "i\u{307}", Some((0, 3)));
878        test!("i̇x", "i\u{307}x", Some((0, 4)));
879        test!("i̇x", "i\u{307}_", None);
880        test!("xi̇x", "i\u{307}", Some((1, 3)));
881        test!("xi̇ඞi̇x", "ඞ", Some((4, 3)));
882        test!("xi̇ඞi̇x", "ඞi̇", Some((4, 6)));
883        test!("xi̇ඞİx", "ඞi̇", Some((4, 5)));
884    }
885
886    #[test]
887    fn test_matcher_case_insensitive_ranges_match() {
888        macro_rules! test {
889            ($c:expr, $negated:expr, [$start:literal - $end:literal], $result:expr) => {
890                assert_eq!(
891                    CaseInsensitive::ranges_match($c, $negated, &range($start, $end)),
892                    $result
893                );
894            };
895        }
896
897        test!('a', false, ['a' - 'a'], true);
898        test!('a', true, ['a' - 'a'], false);
899        test!('b', false, ['a' - 'a'], false);
900        test!('b', true, ['a' - 'a'], true);
901        test!('b', false, ['a' - 'c'], true);
902        test!('b', false, ['b' - 'c'], true);
903        test!('b', false, ['a' - 'b'], true);
904
905        test!('b', false, ['A' - 'A'], false);
906        test!('b', true, ['A' - 'A'], true);
907        test!('b', false, ['A' - 'C'], true);
908        test!('b', false, ['B' - 'C'], true);
909        test!('b', false, ['A' - 'B'], true);
910
911        test!('B', false, ['a' - 'a'], false);
912        test!('B', true, ['a' - 'a'], true);
913        test!('B', false, ['a' - 'c'], true);
914        test!('B', false, ['b' - 'c'], true);
915        test!('B', false, ['a' - 'b'], true);
916
917        test!('ǧ', false, ['Ǧ' - 'Ǧ'], true);
918        test!('Ǧ', false, ['ǧ' - 'ǧ'], true);
919        test!('ǧ', true, ['Ǧ' - 'Ǧ'], false);
920        test!('Ǧ', true, ['ǧ' - 'ǧ'], false);
921
922        test!('ඞ', false, ['ඞ' - 'ඞ'], true);
923    }
924
925    #[test]
926    fn test_matcher_case_insensitive_ranges_find() {
927        macro_rules! test {
928            ($haystack:expr, $negated:expr, [$start:literal - $end:literal], $result:expr) => {
929                assert_eq!(
930                    CaseInsensitive::ranges_find($haystack, $negated, &range($start, $end)),
931                    $result
932                );
933            };
934        }
935
936        test!("ඞaඞ", false, ['a' - 'a'], Some((3, 'a')));
937        test!("a", true, ['a' - 'a'], None);
938        test!("ඞaඞ", true, ['a' - 'a'], Some((0, 'ඞ')));
939        test!("aඞaඞ", true, ['a' - 'a'], Some((1, 'ඞ')));
940        test!("ඞbඞ", false, ['a' - 'a'], None);
941        test!("ඞbඞ", true, ['ඞ' - 'ඞ'], Some((3, 'b')));
942        test!("ඞbඞ", false, ['a' - 'c'], Some((3, 'b')));
943        test!("ඞbඞ", false, ['b' - 'c'], Some((3, 'b')));
944        test!("ඞbඞ", false, ['a' - 'b'], Some((3, 'b')));
945        test!("AAAAA", false, ['a' - 'a'], Some((0, 'A')));
946        test!("aaaaaaabb", true, ['a' - 'a'], Some((7, 'b')));
947        test!("AaaaaaAbb", false, ['b' - 'b'], Some((7, 'b')));
948
949        test!("ඞBඞ", false, ['a' - 'a'], None);
950        test!("ඞBඞ", true, ['ඞ' - 'ඞ'], Some((3, 'B')));
951        test!("ඞBඞ", false, ['a' - 'c'], Some((3, 'B')));
952        test!("ඞBඞ", false, ['b' - 'c'], Some((3, 'B')));
953        test!("ඞBඞ", false, ['a' - 'b'], Some((3, 'B')));
954
955        test!("ඞbඞ", false, ['A' - 'A'], None);
956        test!("ඞbඞ", true, ['ඞ' - 'ඞ'], Some((3, 'b')));
957        test!("ඞbඞ", false, ['A' - 'C'], Some((3, 'b')));
958        test!("ඞbඞ", false, ['B' - 'C'], Some((3, 'b')));
959        test!("ඞbඞ", false, ['A' - 'B'], Some((3, 'b')));
960
961        test!("fඞoǧbar", false, ['ǧ' - 'ǧ'], Some((5, 'ǧ')));
962        test!("fඞoǧbar", false, ['Ǧ' - 'Ǧ'], Some((5, 'ǧ')));
963        test!("fඞoǦbar", false, ['ǧ' - 'ǧ'], Some((5, 'Ǧ')));
964    }
965}