Skip to main content

fmf_core\query/
matchers.rs

1use super::compile::{CTerm, Matcher};
2use super::memo::PathMemos;
3use crate::index::{EntryId, VolumeIndex};
4
5// ── Residual matcher evaluation ─────────────────────────────────────────
6
7/// Per-thread scratch: the entry's full path, built at most once per entry
8/// per variant, only when a path matcher is actually reached.
9#[derive(Default)]
10pub(super) struct EvalCtx {
11    lower_path: Vec<u8>,
12    orig_path: Vec<u8>,
13    lower_built: bool,
14    orig_built: bool,
15}
16
17impl EvalCtx {
18    #[inline]
19    const fn reset(&mut self) {
20        self.lower_built = false;
21        self.orig_built = false;
22    }
23
24    #[inline]
25    fn lower_path<'a>(&'a mut self, idx: &VolumeIndex, memo: &PathMemos, id: EntryId) -> &'a [u8] {
26        if !self.lower_built {
27            self.lower_path.clear();
28            if id != VolumeIndex::ROOT {
29                self.lower_path
30                    .extend_from_slice(memo.lower_prefix(idx.parent(id)));
31            }
32            self.lower_path.extend_from_slice(idx.lower_name(id));
33            self.lower_built = true;
34        }
35        &self.lower_path
36    }
37
38    #[inline]
39    fn orig_path<'a>(&'a mut self, idx: &VolumeIndex, memo: &PathMemos, id: EntryId) -> &'a [u8] {
40        if !self.orig_built {
41            self.orig_path.clear();
42            if id != VolumeIndex::ROOT {
43                self.orig_path
44                    .extend_from_slice(memo.orig_prefix(idx.parent(id)));
45            }
46            self.orig_path.extend_from_slice(idx.name(id));
47            self.orig_built = true;
48        }
49        &self.orig_path
50    }
51}
52
53/// The haystack for a case-exact name literal. Fold-identical entries
54/// resolve in O(1) (ADR-0004): a needle that is not its own fold can never
55/// occur in a name whose every character is fold-stable (UTF-8/WTF-8
56/// self-synchronization makes the byte-level argument sound), and for a
57/// fold-stable needle the folded bytes *are* the original bytes.
58#[inline]
59fn exact_hay<'a>(idx: &'a VolumeIndex, t: &CTerm, id: EntryId) -> Option<&'a [u8]> {
60    if idx.is_fold_identical(id) {
61        if t.exact_needle_unstable {
62            None
63        } else {
64            Some(idx.lower_name(id))
65        }
66    } else {
67        Some(idx.name(id))
68    }
69}
70
71#[inline]
72fn eval(idx: &VolumeIndex, memo: &PathMemos, ctx: &mut EvalCtx, t: &CTerm, id: EntryId) -> bool {
73    match &t.matcher {
74        Matcher::True => true,
75        Matcher::Size { min, max } => !idx.is_dir(id) && (*min..=*max).contains(&idx.size(id)),
76        Matcher::Mtime { min, max } => (*min..=*max).contains(&idx.mtime(id)),
77        Matcher::IsDir(d) => idx.is_dir(id) == *d,
78        Matcher::Ext { exts } => {
79            let lower = idx.lower_name(id);
80            match memchr::memrchr(b'.', lower) {
81                Some(p) if !idx.is_dir(id) => {
82                    let ext = &lower[p + 1..];
83                    exts.iter().any(|e| e.as_slice() == ext)
84                }
85                _ => false,
86            }
87        }
88        Matcher::NameSub { finder, folded } => {
89            let hay = if *folded {
90                idx.lower_name(id)
91            } else {
92                match exact_hay(idx, t, id) {
93                    Some(h) => h,
94                    None => return false,
95                }
96            };
97            finder.find(hay).is_some()
98        }
99        Matcher::NamePrefix { bytes, folded } => {
100            let hay = if *folded {
101                idx.lower_name(id)
102            } else {
103                match exact_hay(idx, t, id) {
104                    Some(h) => h,
105                    None => return false,
106                }
107            };
108            hay.starts_with(bytes)
109        }
110        Matcher::NameSuffix { bytes, folded } => {
111            let hay = if *folded {
112                idx.lower_name(id)
113            } else {
114                match exact_hay(idx, t, id) {
115                    Some(h) => h,
116                    None => return false,
117                }
118            };
119            hay.ends_with(bytes)
120        }
121        Matcher::NameRegex { re } => re.is_match(idx.name(id)),
122        Matcher::PathSub { finder, folded } => {
123            let hay = if *folded {
124                ctx.lower_path(idx, memo, id)
125            } else {
126                ctx.orig_path(idx, memo, id)
127            };
128            finder.find(hay).is_some()
129        }
130        Matcher::PathRegex { re } => re.is_match(ctx.orig_path(idx, memo, id)),
131    }
132}
133
134#[inline]
135pub(super) fn terms_match(
136    idx: &VolumeIndex,
137    memo: &PathMemos,
138    ctx: &mut EvalCtx,
139    terms: &[CTerm],
140    id: EntryId,
141) -> bool {
142    terms_match_iter(idx, memo, ctx, terms.iter(), id)
143}
144
145/// Iterator form so refine can chain the driver term with the residuals
146/// without cloning matchers (`CompiledGroup::all_terms`).
147#[inline]
148pub(super) fn terms_match_iter<'a>(
149    idx: &VolumeIndex,
150    memo: &PathMemos,
151    ctx: &mut EvalCtx,
152    terms: impl Iterator<Item = &'a CTerm>,
153    id: EntryId,
154) -> bool {
155    ctx.reset();
156    for t in terms {
157        if eval(idx, memo, ctx, t, id) == t.negated {
158            return false;
159        }
160    }
161    true
162}