Skip to main content

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;