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;