1 /* Licensed to the Apache Software Foundation (ASF) under one or more
2 * contributor license agreements. See the NOTICE file distributed with
3 * this work for additional information regarding copyright ownership.
4 * The ASF licenses this file to You under the Apache License, Version 2.0
5 * (the "License"); you may not use this file except in compliance with
6 * the License. You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
18 /* Derived from The Open Group Base Specifications Issue 7, IEEE Std 1003.1-2008
20 * http://pubs.opengroup.org/onlinepubs/9699919799/functions/fnmatch.html
22 * Filename pattern matches defined in section 2.13, "Pattern Matching Notation"
23 * from chapter 2. "Shell Command Language"
24 * http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_13
25 * where; 1. A bracket expression starting with an unquoted <circumflex> '^'
26 * character CONTINUES to specify a non-matching list; 2. an explicit <period> '.'
27 * in a bracket expression matching list, e.g. "[.abc]" does NOT match a leading
28 * <period> in a filename; 3. a <left-square-bracket> '[' which does not introduce
29 * a valid bracket expression is treated as an ordinary character; 4. a differing
30 * number of consecutive slashes within pattern and string will NOT match;
31 * 5. a trailing '\' in FNM_ESCAPE mode is treated as an ordinary '\' character.
33 * Bracket expansion defined in section 9.3.5, "RE Bracket Expression",
34 * from chapter 9, "Regular Expressions"
35 * http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap09.html#tag_09_03_05
36 * with no support for collating symbols, equivalence class expressions or
37 * character class expressions. A partial range expression with a leading
38 * hyphen following a valid range expression will match only the ordinary
39 * <hyphen> and the ending character (e.g. "[a-m-z]" will match characters
40 * 'a' through 'm', a <hyphen> '-', or a 'z').
42 * NOTE: Only POSIX/C single byte locales are correctly supported at this time.
43 * Notably, non-POSIX locales with FNM_CASEFOLD produce undefined results,
44 * particularly in ranges of mixed case (e.g. "[A-z]") or spanning alpha and
45 * nonalpha characters within a range.
47 * XXX comments below indicate porting required for multi-byte character sets
48 * and non-POSIX locale collation orders; requires mbr* APIs to track shift
49 * state of pattern and string (rewinding pattern and string repeatedly).
51 * Certain parts of the code assume 0x00-0x3F are unique with any MBCS (e.g.
52 * UTF-8, SHIFT-JIS, etc). Any implementation allowing '\' as an alternate
53 * path delimiter must be aware that 0x5C is NOT unique within SHIFT-JIS.
56 #include "apr_file_info.h"
57 #include "apr_fnmatch.h"
58 #include "apr_tables.h"
60 #include "apr_strings.h"
67 /* Most MBCS/collation/case issues handled here. Wildcard '*' is not handled.
68 * EOS '\0' and the FNM_PATHNAME '/' delimiters are not advanced over,
69 * however the "\/" sequence is advanced to '/'.
71 * Both pattern and string are **char to support pointer increment of arbitrary
72 * multibyte characters for the given locale, in a later iteration of this code
74 static APR_INLINE int fnmatch_ch(const char **pattern, const char **string, int flags)
76 const char * const mismatch = *pattern;
77 const int nocase = !!(flags & APR_FNM_CASE_BLIND);
78 const int escape = !(flags & APR_FNM_NOESCAPE);
79 const int slash = !!(flags & APR_FNM_PATHNAME);
80 int result = APR_FNM_NOMATCH;
88 /* Handle negation, either leading ! or ^ operators (never both) */
89 negate = ((**pattern == '!') || (**pattern == '^'));
93 /* ']' is an ordinary character at the start of the range pattern */
95 goto leadingclosebrace;
99 if (**pattern == ']') {
101 /* XXX: Fix for MBCS character width */
103 return (result ^ negate);
106 if (escape && (**pattern == '\\')) {
109 /* Patterns must be terminated with ']', not EOS */
114 /* Patterns must be terminated with ']' not '/' */
115 if (slash && (**pattern == '/'))
119 /* Look at only well-formed range patterns;
120 * "x-]" is not allowed unless escaped ("x-\]")
121 * XXX: Fix for locale/MBCS character width
123 if (((*pattern)[1] == '-') && ((*pattern)[2] != ']'))
126 *pattern += (escape && ((*pattern)[2] == '\\')) ? 3 : 2;
128 /* NOT a properly balanced [expr] pattern, EOS terminated
129 * or ranges containing a slash in FNM_PATHNAME mode pattern
130 * fall out to to the rewind and test '[' literal code path
132 if (!**pattern || (slash && (**pattern == '/')))
135 /* XXX: handle locale/MBCS comparison, advance by MBCS char width */
136 if ((**string >= *startch) && (**string <= **pattern))
138 else if (nocase && (isupper(**string) || isupper(*startch)
139 || isupper(**pattern))
140 && (tolower(**string) >= tolower(*startch))
141 && (tolower(**string) <= tolower(**pattern)))
148 /* XXX: handle locale/MBCS comparison, advance by MBCS char width */
149 if ((**string == **pattern))
151 else if (nocase && (isupper(**string) || isupper(**pattern))
152 && (tolower(**string) == tolower(**pattern)))
158 /* NOT a properly balanced [expr] pattern; Rewind
159 * and reset result to test '[' literal
162 result = APR_FNM_NOMATCH;
164 else if (**pattern == '?') {
165 /* Optimize '?' match before unescaping **pattern */
166 if (!**string || (slash && (**string == '/')))
167 return APR_FNM_NOMATCH;
169 goto fnmatch_ch_success;
171 else if (escape && (**pattern == '\\') && (*pattern)[1]) {
175 /* XXX: handle locale/MBCS comparison, advance by the MBCS char width */
176 if (**string == **pattern)
178 else if (nocase && (isupper(**string) || isupper(**pattern))
179 && (tolower(**string) == tolower(**pattern)))
182 /* Refuse to advance over trailing slash or nulls
184 if (!**string || !**pattern || (slash && ((**string == '/') || (**pattern == '/'))))
194 APR_DECLARE(int) apr_fnmatch(const char *pattern, const char *string, int flags)
196 static const char dummystring[2] = {' ', 0};
197 const int escape = !(flags & APR_FNM_NOESCAPE);
198 const int slash = !!(flags & APR_FNM_PATHNAME);
199 const char *strendseg;
200 const char *dummyptr;
201 const char *matchptr;
203 /* For '*' wild processing only; surpress 'used before initialization'
204 * warnings with dummy initialization values;
206 const char *strstartseg = NULL;
207 const char *mismatch = NULL;
213 while (*pattern && *string)
215 /* Pre-decode "\/" which has no special significance, and
216 * match balanced slashes, starting a new segment pattern
218 if (slash && escape && (*pattern == '\\') && (pattern[1] == '/'))
220 if (slash && (*pattern == '/') && (*string == '/')) {
226 /* At the beginning of each segment, validate leading period behavior.
228 if ((flags & APR_FNM_PERIOD) && (*string == '.'))
232 else if (escape && (*pattern == '\\') && (pattern[1] == '.'))
235 return APR_FNM_NOMATCH;
239 /* Determine the end of string segment
241 * Presumes '/' character is unique, not composite in any MBCS encoding
244 strendseg = strchr(string, '/');
246 strendseg = strchr(string, '\0');
249 strendseg = strchr(string, '\0');
252 /* Allow pattern '*' to be consumed even with no remaining string to match
256 if ((string > strendseg)
257 || ((string == strendseg) && (*pattern != '*')))
260 if (slash && ((*pattern == '/')
261 || (escape && (*pattern == '\\')
262 && (pattern[1] == '/'))))
265 /* Reduce groups of '*' and '?' to n '?' matches
266 * followed by one '*' test for simplicity
268 for (wild = 0; ((*pattern == '*') || (*pattern == '?')); ++pattern)
270 if (*pattern == '*') {
273 else if (string < strendseg) { /* && (*pattern == '?') */
274 /* XXX: Advance 1 char for MBCS locale */
277 else { /* (string >= strendseg) && (*pattern == '?') */
278 return APR_FNM_NOMATCH;
284 strstartseg = string;
287 /* Count fixed (non '*') char matches remaining in pattern
288 * excluding '/' (or "\/") and '*'
290 for (matchptr = pattern, matchlen = 0; 1; ++matchlen)
292 if ((*matchptr == '\0')
293 || (slash && ((*matchptr == '/')
294 || (escape && (*matchptr == '\\')
295 && (matchptr[1] == '/')))))
297 /* Compare precisely this many trailing string chars,
298 * the resulting match needs no wildcard loop
300 /* XXX: Adjust for MBCS */
301 if (string + matchlen > strendseg)
302 return APR_FNM_NOMATCH;
304 string = strendseg - matchlen;
309 if (*matchptr == '*')
311 /* Ensure at least this many trailing string chars remain
312 * for the first comparison
314 /* XXX: Adjust for MBCS */
315 if (string + matchlen > strendseg)
316 return APR_FNM_NOMATCH;
318 /* Begin first wild comparison at the current position */
322 /* Skip forward in pattern by a single character match
323 * Use a dummy fnmatch_ch() test to count one "[range]" escape
325 /* XXX: Adjust for MBCS */
326 if (escape && (*matchptr == '\\') && matchptr[1]) {
329 else if (*matchptr == '[') {
330 dummyptr = dummystring;
331 fnmatch_ch(&matchptr, &dummyptr, flags);
339 /* Incrementally match string against the pattern
341 while (*pattern && (string < strendseg))
343 /* Success; begin a new wild pattern search
348 if (slash && ((*string == '/')
350 || (escape && (*pattern == '\\')
351 && (pattern[1] == '/'))))
354 /* Compare ch's (the pattern is advanced over "\/" to the '/',
355 * but slashes will mismatch, and are not consumed)
357 if (!fnmatch_ch(&pattern, &string, flags))
360 /* Failed to match, loop against next char offset of string segment
361 * until not enough string chars remain to match the fixed pattern
364 /* XXX: Advance 1 char for MBCS locale */
365 string = ++strstartseg;
366 if (string + matchlen > strendseg)
367 return APR_FNM_NOMATCH;
373 return APR_FNM_NOMATCH;
377 if (*string && !(slash && (*string == '/')))
378 return APR_FNM_NOMATCH;
380 if (*pattern && !(slash && ((*pattern == '/')
381 || (escape && (*pattern == '\\')
382 && (pattern[1] == '/')))))
383 return APR_FNM_NOMATCH;
386 /* Where both pattern and string are at EOS, declare success
388 if (!*string && !*pattern)
391 /* pattern didn't match to the end of string */
392 return APR_FNM_NOMATCH;
396 /* This function is an Apache addition
397 * return non-zero if pattern has any glob chars in it
398 * @bug Function does not distinguish for FNM_PATHNAME mode, which renders
399 * a false positive for test[/]this (which is not a range, but
400 * seperate test[ and ]this segments and no glob.)
401 * @bug Function does not distinguish for non-FNM_ESCAPE mode.
402 * @bug Function does not parse []] correctly
403 * Solution may be to use fnmatch_ch() to walk the patterns?
405 APR_DECLARE(int) apr_fnmatch_test(const char *pattern)
417 if (*++pattern == '\0') {
422 case '[': /* '[' is only a glob if it has a matching ']' */
437 /* Find all files matching the specified pattern */
438 APR_DECLARE(apr_status_t) apr_match_glob(const char *pattern,
439 apr_array_header_t **result,
447 /* XXX So, this is kind of bogus. Basically, I need to strip any leading
448 * directories off the pattern, but there is no portable way to do that.
449 * So, for now we just find the last occurance of '/' and if that doesn't
450 * return anything, then we look for '\'. This means that we could
451 * screw up on unix if the pattern is something like "foo\.*" That '\'
452 * isn't a directory delimiter, it is a part of the filename. To fix this,
453 * we really need apr_filepath_basename, which will be coming as soon as
456 char *idx = strrchr(pattern, '/');
459 idx = strrchr(pattern, '\\');
465 path = apr_pstrndup(p, pattern, idx - pattern);
469 *result = apr_array_make(p, 0, sizeof(char *));
470 rv = apr_dir_open(&dir, path, p);
471 if (rv != APR_SUCCESS) {
475 while (apr_dir_read(&finfo, APR_FINFO_NAME, dir) == APR_SUCCESS) {
476 if (apr_fnmatch(pattern, finfo.name, 0) == APR_SUCCESS) {
477 *(const char **)apr_array_push(*result) = apr_pstrdup(p, finfo.name);