windows_file_enumeration_sys/predicate.rs
1// Copyright (c) 2026 Mike Grier
2//! The query-by-example predicate: what a request asks of each entry.
3//!
4//! A predicate is *data*, never a closure. That is what lets it cross the
5//! submission ring, be validated before the enumeration is accepted, and run
6//! inside a Windows thread-pool callback without this crate ever calling client
7//! code on its cadence path. A caller-supplied closure would bring panics,
8//! blocking, and reentrancy into a completion callback -- the one place none of
9//! them can be handled.
10//!
11//! # Shape
12//!
13//! [`QueryByExample`] is a flat conjunction: an entry matches when every clause
14//! matches. An empty query matches every entry the enumeration reaches.
15//!
16//! There is no explicit range clause because two comparison clauses over the
17//! same field already are one, and no `OR` because
18//! [`PatternToken::Alternation`](crate::PatternToken::Alternation) and
19//! [`NameInSet`](PredicateClause::NameInSet) cover the disjunction that name
20//! matching actually needs. Contradictory clauses are allowed and simply match
21//! nothing.
22//!
23//! # Why vacuous clauses are rejected
24//!
25//! A zero attribute mask and an empty name set are both *silent* match-alls:
26//! they look like a filter and behave like none. Both are rejected when the
27//! query is built, where the caller can still see which clause was wrong.
28
29use crate::entry::{DirectoryEntry, EntryType};
30use crate::error::{PredicateError, PredicateFailure};
31use crate::pattern::{CaseSensitivity, NamePattern};
32use crate::timestamp::WindowsFileTimestamp;
33
34/// How a numeric or timestamp clause compares.
35///
36/// The entry's value is always the left operand and the clause's value the
37/// right, so `LogicalSize { operator: Greater, value: 4096 }` reads as "the
38/// entry is larger than 4096 bytes".
39#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
40pub enum ComparisonOperator {
41 /// `<`
42 Less,
43 /// `<=`
44 LessOrEqual,
45 /// `==`
46 Equal,
47 /// `!=`
48 NotEqual,
49 /// `>=`
50 GreaterOrEqual,
51 /// `>`
52 Greater,
53}
54
55impl ComparisonOperator {
56 /// Apply this operator to an entry value and a clause value.
57 fn apply<T: Ord>(self, entry: T, value: T) -> bool {
58 match self {
59 ComparisonOperator::Less => entry < value,
60 ComparisonOperator::LessOrEqual => entry <= value,
61 ComparisonOperator::Equal => entry == value,
62 ComparisonOperator::NotEqual => entry != value,
63 ComparisonOperator::GreaterOrEqual => entry >= value,
64 ComparisonOperator::Greater => entry > value,
65 }
66 }
67}
68
69/// Which of an entry's four times a timestamp clause compares.
70#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
71pub enum TimestampField {
72 /// [`DirectoryEntry::creation_time`]
73 Creation,
74 /// [`DirectoryEntry::last_access_time`]
75 LastAccess,
76 /// [`DirectoryEntry::last_write_time`]
77 LastWrite,
78 /// [`DirectoryEntry::change_time`]
79 Change,
80}
81
82impl TimestampField {
83 /// Read this field from an entry.
84 fn read(self, entry: &DirectoryEntry) -> WindowsFileTimestamp {
85 match self {
86 TimestampField::Creation => entry.creation_time(),
87 TimestampField::LastAccess => entry.last_access_time(),
88 TimestampField::LastWrite => entry.last_write_time(),
89 TimestampField::Change => entry.change_time(),
90 }
91 }
92}
93
94/// One condition an entry must satisfy.
95///
96/// Clauses that can be sensibly inverted carry their own `negated` flag rather
97/// than relying on an enclosing `Not`, which keeps the query flat and keeps the
98/// negation adjacent to the thing it negates.
99#[derive(Clone, Debug, PartialEq, Eq)]
100#[non_exhaustive]
101pub enum PredicateClause {
102 /// The name matches a pattern.
103 Name {
104 /// The pattern to match the entry's leaf name against.
105 pattern: NamePattern,
106 /// How the comparison treats case.
107 case: CaseSensitivity,
108 /// Invert the result.
109 negated: bool,
110 },
111 /// The name matches any pattern in a non-empty set.
112 NameInSet {
113 /// The alternatives. Must not be empty.
114 patterns: Vec<NamePattern>,
115 /// How the comparisons treat case.
116 case: CaseSensitivity,
117 /// Invert the result, giving set non-membership.
118 negated: bool,
119 },
120 /// The entry is (or, negated, is not) the given kind.
121 IsType {
122 /// The kind to test for.
123 entry_type: EntryType,
124 /// Invert the result.
125 negated: bool,
126 },
127 /// The entry is (or, negated, is not) a reparse point.
128 IsReparsePoint {
129 /// Invert the result.
130 negated: bool,
131 },
132 /// The entry is a reparse point whose tag equals `tag`.
133 ///
134 /// A non-reparse entry has no tag, so it never satisfies this clause --
135 /// and, negated, always does.
136 ReparseTag {
137 /// The tag to compare against.
138 tag: u32,
139 /// Invert the result.
140 negated: bool,
141 },
142 /// Every bit in the mask is set in the entry's attributes.
143 ///
144 /// The mask must be non-zero.
145 AttributesAllSet(u32),
146 /// Every bit in the mask is clear in the entry's attributes.
147 ///
148 /// The mask must be non-zero.
149 AttributesAllClear(u32),
150 /// The entry's logical size compares to `value` bytes.
151 LogicalSize {
152 /// How to compare.
153 operator: ComparisonOperator,
154 /// The size in bytes to compare against.
155 value: u64,
156 },
157 /// The entry's allocation size compares to `value` bytes.
158 AllocationSize {
159 /// How to compare.
160 operator: ComparisonOperator,
161 /// The size in bytes to compare against.
162 value: u64,
163 },
164 /// One of the entry's four times compares to `value`.
165 Timestamp {
166 /// Which time to read.
167 field: TimestampField,
168 /// How to compare.
169 operator: ComparisonOperator,
170 /// The timestamp to compare against.
171 value: WindowsFileTimestamp,
172 },
173}
174
175impl PredicateClause {
176 /// Reject a clause that would silently match everything.
177 fn validate(&self) -> Result<(), PredicateError> {
178 match self {
179 PredicateClause::AttributesAllSet(0) | PredicateClause::AttributesAllClear(0) => {
180 Err(PredicateError::new(PredicateFailure::EmptyAttributeMask))
181 }
182 PredicateClause::NameInSet { patterns, .. } if patterns.is_empty() => {
183 Err(PredicateError::new(PredicateFailure::EmptyNameSet))
184 }
185 _ => Ok(()),
186 }
187 }
188
189 /// Whether `entry` satisfies this clause.
190 #[must_use]
191 pub fn matches(&self, entry: &DirectoryEntry) -> bool {
192 match self {
193 PredicateClause::Name {
194 pattern,
195 case,
196 negated,
197 } => pattern.matches(entry.name(), *case) != *negated,
198 PredicateClause::NameInSet {
199 patterns,
200 case,
201 negated,
202 } => {
203 let any = patterns
204 .iter()
205 .any(|pattern| pattern.matches(entry.name(), *case));
206 any != *negated
207 }
208 PredicateClause::IsType {
209 entry_type,
210 negated,
211 } => (entry.entry_type() == *entry_type) != *negated,
212 PredicateClause::IsReparsePoint { negated } => entry.is_reparse_point() != *negated,
213 PredicateClause::ReparseTag { tag, negated } => {
214 (entry.reparse_tag() == Some(*tag)) != *negated
215 }
216 PredicateClause::AttributesAllSet(mask) => entry.attributes() & mask == *mask,
217 PredicateClause::AttributesAllClear(mask) => entry.attributes() & mask == 0,
218 PredicateClause::LogicalSize { operator, value } => {
219 operator.apply(entry.logical_size(), *value)
220 }
221 PredicateClause::AllocationSize { operator, value } => {
222 operator.apply(entry.allocation_size(), *value)
223 }
224 PredicateClause::Timestamp {
225 field,
226 operator,
227 value,
228 } => operator.apply(field.read(entry), *value),
229 }
230 }
231}
232
233/// A validated conjunction of clauses.
234///
235/// Every clause is checked as it is added, so a built query can never carry a
236/// vacuous clause and evaluation has nothing left to validate.
237#[derive(Clone, Debug, Default, PartialEq, Eq)]
238pub struct QueryByExample {
239 clauses: Vec<PredicateClause>,
240}
241
242impl QueryByExample {
243 /// An empty query, which matches every entry.
244 #[must_use]
245 pub fn new() -> Self {
246 Self::default()
247 }
248
249 /// Add one clause.
250 ///
251 /// # Errors
252 ///
253 /// Returns [`PredicateError`] if the clause would silently match every
254 /// entry: a zero attribute mask, or an empty name set.
255 pub fn push(&mut self, clause: PredicateClause) -> Result<(), PredicateError> {
256 clause.validate()?;
257 self.clauses.push(clause);
258 Ok(())
259 }
260
261 /// Add one clause, taking and returning the query for chaining.
262 ///
263 /// # Errors
264 ///
265 /// As [`push`](Self::push).
266 pub fn with(mut self, clause: PredicateClause) -> Result<Self, PredicateError> {
267 self.push(clause)?;
268 Ok(self)
269 }
270
271 /// The clauses, in the order they were added.
272 #[must_use]
273 pub fn clauses(&self) -> &[PredicateClause] {
274 &self.clauses
275 }
276
277 /// Whether this query has no clauses, and so matches every entry.
278 #[must_use]
279 pub fn is_empty(&self) -> bool {
280 self.clauses.is_empty()
281 }
282
283 /// Whether `entry` satisfies every clause.
284 ///
285 /// Short-circuits on the first clause that fails, so an early cheap clause
286 /// spares the later expensive ones.
287 #[must_use]
288 pub fn matches(&self, entry: &DirectoryEntry) -> bool {
289 self.clauses.iter().all(|clause| clause.matches(entry))
290 }
291}
292
293/// What a request asks of each entry.
294///
295/// Deliberately an enum with a single variant today. The variant is the settled
296/// v1 predicate; the enum is the seam that lets a later predicate family --
297/// an expression tree, say -- be added without replacing the request API that
298/// carries it.
299#[derive(Clone, Debug, PartialEq, Eq)]
300#[non_exhaustive]
301pub enum EntryPredicate {
302 /// A flat conjunction of query-by-example clauses.
303 QueryByExample(QueryByExample),
304}
305
306impl EntryPredicate {
307 /// Whether `entry` satisfies this predicate.
308 #[must_use]
309 pub fn matches(&self, entry: &DirectoryEntry) -> bool {
310 match self {
311 EntryPredicate::QueryByExample(query) => query.matches(entry),
312 }
313 }
314
315 /// Whether this predicate accepts every entry, so evaluation can be skipped
316 /// entirely.
317 #[must_use]
318 pub fn matches_everything(&self) -> bool {
319 match self {
320 EntryPredicate::QueryByExample(query) => query.is_empty(),
321 }
322 }
323}
324
325impl Default for EntryPredicate {
326 /// An empty query, which accepts every entry.
327 fn default() -> Self {
328 EntryPredicate::QueryByExample(QueryByExample::new())
329 }
330}
331
332impl From<QueryByExample> for EntryPredicate {
333 fn from(query: QueryByExample) -> Self {
334 EntryPredicate::QueryByExample(query)
335 }
336}
337
338#[cfg(test)]
339mod tests;