Skip to main content

windows_file_enumeration_sys/
pattern.rs

1// Copyright (c) 2026 Mike Grier
2//! Single-segment name patterns and the matcher that evaluates them.
3//!
4//! Patterns are *compiled data*, not a wildcard string handed to a filesystem.
5//! That is a deliberate ownership choice: the crate specifies what a pattern
6//! means, so the answer does not change with the volume, the redirector, or the
7//! Windows version underneath. It also means a caller building a pattern
8//! programmatically never has to escape anything.
9//!
10//! # What a code point is here
11//!
12//! [`PatternToken::AnyOne`] matches exactly one Unicode scalar. In WTF-16 that
13//! is a valid surrogate pair (two code units) or a single non-surrogate unit --
14//! and, because names may be ill-formed, a lone unpaired surrogate counts as one
15//! as well. A pattern can therefore never split a valid pair, and can still
16//! match a name a filesystem should not have allowed.
17//!
18//! # Case
19//!
20//! [`CaseSensitivity::Sensitive`] is exact code-unit comparison, which needs no
21//! Win32 call. [`CaseSensitivity::Insensitive`] uses `CompareStringOrdinal`,
22//! Windows' own non-linguistic uppercase table -- the same notion of "equal
23//! ignoring case" the filesystem uses, rather than a locale-dependent collation
24//! that would make matching depend on the caller's user profile.
25//!
26//! Ordinal case folding is one-to-one per code unit, which is what lets a
27//! literal run consume exactly its own length even when matched insensitively.
28
29use windows_sys::Win32::Foundation::TRUE;
30use windows_sys::Win32::Globalization::{CSTR_EQUAL, CompareStringOrdinal};
31use wtf_string::{Wtf16Str, Wtf16String};
32
33/// How a name comparison treats case.
34#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
35pub enum CaseSensitivity {
36    /// Exact code-unit comparison.
37    ///
38    /// The default, because it is the only comparison that cannot surprise: it
39    /// depends on nothing but the two values.
40    #[default]
41    Sensitive,
42    /// Comparison through Windows' ordinal uppercase table.
43    Insensitive,
44}
45
46/// One element of a [`NamePattern`].
47#[derive(Clone, Debug, PartialEq, Eq)]
48#[non_exhaustive]
49pub enum PatternToken {
50    /// A run of literal code units, matched under the clause's case rule.
51    Literal(Wtf16String),
52    /// Exactly one code point.
53    AnyOne,
54    /// Zero or more code points.
55    AnyRun,
56    /// Any one of several alternative token sequences.
57    ///
58    /// An empty alternation matches nothing, which is a contradiction rather
59    /// than a vacuous match-all, so it needs no validation.
60    Alternation(Vec<NamePattern>),
61}
62
63/// A compiled pattern for one leaf name.
64///
65/// A pattern never spans path separators because the value it is matched
66/// against is a single directory entry's own name, which contains none. There
67/// is consequently no "crosses a segment" case to specify.
68#[derive(Clone, Debug, Default, PartialEq, Eq)]
69pub struct NamePattern {
70    tokens: Vec<PatternToken>,
71}
72
73impl NamePattern {
74    /// A pattern matching only the empty name.
75    ///
76    /// A directory entry's name is never empty, so this matches nothing. It is
77    /// the identity a builder starts from, not a useful clause on its own.
78    #[must_use]
79    pub fn empty() -> Self {
80        Self { tokens: Vec::new() }
81    }
82
83    /// A pattern that matches exactly `name`.
84    #[must_use]
85    pub fn literal(name: &Wtf16Str) -> Self {
86        Self {
87            tokens: vec![PatternToken::Literal(Wtf16String::from_units(
88                name.as_units(),
89            ))],
90        }
91    }
92
93    /// A pattern built from an explicit token sequence.
94    #[must_use]
95    pub fn from_tokens(tokens: Vec<PatternToken>) -> Self {
96        Self { tokens }
97    }
98
99    /// Append one token.
100    pub fn push(&mut self, token: PatternToken) {
101        self.tokens.push(token);
102    }
103
104    /// Append one token, taking and returning the pattern for chaining.
105    #[must_use]
106    pub fn with(mut self, token: PatternToken) -> Self {
107        self.tokens.push(token);
108        self
109    }
110
111    /// The pattern's tokens, in order.
112    #[must_use]
113    pub fn tokens(&self) -> &[PatternToken] {
114        &self.tokens
115    }
116
117    /// Whether this pattern matches `name` in its entirety.
118    #[must_use]
119    pub fn matches(&self, name: &Wtf16Str, case: CaseSensitivity) -> bool {
120        match_tokens(&self.tokens, name.as_units(), case)
121    }
122}
123
124/// Whether `tokens` match all of `units`.
125///
126/// Backtracking rather than table-driven: a leaf name is short, the token count
127/// is caller-chosen and small, and a matcher that is obviously correct is worth
128/// more here than one that is asymptotically better on inputs this API never
129/// sees.
130fn match_tokens(tokens: &[PatternToken], units: &[u16], case: CaseSensitivity) -> bool {
131    let Some((first, rest)) = tokens.split_first() else {
132        return units.is_empty();
133    };
134
135    match first {
136        PatternToken::Literal(literal) => {
137            let width = literal.len();
138            match_literal_prefix(units, literal, case) && match_tokens(rest, &units[width..], case)
139        }
140        PatternToken::AnyOne => match code_point_width(units) {
141            Some(width) => match_tokens(rest, &units[width..], case),
142            None => false,
143        },
144        // Try the shortest consumption first and grow. Advancing by whole code
145        // points rather than code units is what keeps a valid surrogate pair
146        // from being split across the boundary between this token and the next.
147        PatternToken::AnyRun => {
148            let mut remaining = units;
149            loop {
150                if match_tokens(rest, remaining, case) {
151                    return true;
152                }
153                match code_point_width(remaining) {
154                    Some(width) => remaining = &remaining[width..],
155                    None => return false,
156                }
157            }
158        }
159        PatternToken::Alternation(arms) => arms.iter().any(|arm| {
160            // Each arm must be matched jointly with what follows it, so an arm
161            // that could match several lengths still lets the rest of the
162            // pattern decide. Splicing is the simplest way to express that
163            // without a continuation-passing matcher.
164            let mut spliced = arm.tokens.clone();
165            spliced.extend_from_slice(rest);
166            match_tokens(&spliced, units, case)
167        }),
168    }
169}
170
171/// Whether `units` starts with `literal` under `case`.
172fn match_literal_prefix(units: &[u16], literal: &Wtf16Str, case: CaseSensitivity) -> bool {
173    let width = literal.len();
174    if units.len() < width {
175        return false;
176    }
177    units_equal(&units[..width], literal.as_units(), case)
178}
179
180/// Whether two equal-length code-unit runs are equal under `case`.
181fn units_equal(left: &[u16], right: &[u16], case: CaseSensitivity) -> bool {
182    match case {
183        CaseSensitivity::Sensitive => left == right,
184        CaseSensitivity::Insensitive => ordinal_equal_ignoring_case(left, right),
185    }
186}
187
188/// Whether two runs are equal through Windows' ordinal uppercase table.
189///
190/// `CompareStringOrdinal` accepts explicit lengths, so an interior NUL in a name
191/// is compared as content rather than terminating the comparison. An empty run
192/// is handled here rather than passed on, because a zero length would otherwise
193/// have to be distinguished from the API's own "NUL-terminated" convention.
194fn ordinal_equal_ignoring_case(left: &[u16], right: &[u16]) -> bool {
195    if left.len() != right.len() {
196        return false;
197    }
198    if left.is_empty() {
199        return true;
200    }
201    let Ok(left_len) = i32::try_from(left.len()) else {
202        // A run this long cannot be a filesystem name; fall back to the exact
203        // comparison rather than truncating the length and comparing a prefix.
204        return left == right;
205    };
206    let Ok(right_len) = i32::try_from(right.len()) else {
207        return left == right;
208    };
209    // SAFETY: both pointers address `left_len`/`right_len` initialised code
210    // units, which is exactly the counted form this API documents; the call
211    // reads only that many units and writes nothing.
212    let result =
213        unsafe { CompareStringOrdinal(left.as_ptr(), left_len, right.as_ptr(), right_len, TRUE) };
214    result == CSTR_EQUAL
215}
216
217/// How many code units the first code point of `units` occupies, or `None` when
218/// `units` is empty.
219///
220/// A high surrogate followed by a low surrogate is one code point of two units.
221/// Anything else -- including an unpaired surrogate, which a WTF-16 name may
222/// legitimately contain -- is one unit.
223fn code_point_width(units: &[u16]) -> Option<usize> {
224    let first = *units.first()?;
225    let is_high_surrogate = (0xD800..0xDC00).contains(&first);
226    let has_low_surrogate = units
227        .get(1)
228        .is_some_and(|second| (0xDC00..0xE000).contains(second));
229    Some(if is_high_surrogate && has_low_surrogate {
230        2
231    } else {
232        1
233    })
234}
235
236#[cfg(test)]
237mod tests;