xref: /openbsd-src/lib/libc/gen/fnmatch.c (revision 50b7afb2c2c0993b0894d4e34bf857cb13ed9c80)
1 /*	$OpenBSD: fnmatch.c,v 1.17 2013/11/24 23:51:29 deraadt Exp $	*/
2 
3 /* Copyright (c) 2011, VMware, Inc.
4  * All rights reserved.
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions are met:
8  *     * Redistributions of source code must retain the above copyright
9  *       notice, this list of conditions and the following disclaimer.
10  *     * Redistributions in binary form must reproduce the above copyright
11  *       notice, this list of conditions and the following disclaimer in the
12  *       documentation and/or other materials provided with the distribution.
13  *     * Neither the name of the VMware, Inc. nor the names of its contributors
14  *       may be used to endorse or promote products derived from this software
15  *       without specific prior written permission.
16  *
17  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20  * ARE DISCLAIMED. IN NO EVENT SHALL VMWARE, INC. OR CONTRIBUTORS BE LIABLE FOR
21  * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27  */
28 
29 /*
30  * Copyright (c) 2008 Todd C. Miller <millert@openbsd.org>
31  *
32  * Permission to use, copy, modify, and distribute this software for any
33  * purpose with or without fee is hereby granted, provided that the above
34  * copyright notice and this permission notice appear in all copies.
35  *
36  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
37  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
38  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
39  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
40  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
41  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
42  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
43  */
44 
45 /* Authored by William A. Rowe Jr. <wrowe; apache.org, vmware.com>, April 2011
46  *
47  * Derived from The Open Group Base Specifications Issue 7, IEEE Std 1003.1-2008
48  * as described in;
49  *   http://pubs.opengroup.org/onlinepubs/9699919799/functions/fnmatch.html
50  *
51  * Filename pattern matches defined in section 2.13, "Pattern Matching Notation"
52  * from chapter 2. "Shell Command Language"
53  *   http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_13
54  * where; 1. A bracket expression starting with an unquoted <circumflex> '^'
55  * character CONTINUES to specify a non-matching list; 2. an explicit <period> '.'
56  * in a bracket expression matching list, e.g. "[.abc]" does NOT match a leading
57  * <period> in a filename; 3. a <left-square-bracket> '[' which does not introduce
58  * a valid bracket expression is treated as an ordinary character; 4. a differing
59  * number of consecutive slashes within pattern and string will NOT match;
60  * 5. a trailing '\' in FNM_ESCAPE mode is treated as an ordinary '\' character.
61  *
62  * Bracket expansion defined in section 9.3.5, "RE Bracket Expression",
63  * from chapter 9, "Regular Expressions"
64  *   http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap09.html#tag_09_03_05
65  * with no support for collating symbols, equivalence class expressions or
66  * character class expressions.  A partial range expression with a leading
67  * hyphen following a valid range expression will match only the ordinary
68  * <hyphen> and the ending character (e.g. "[a-m-z]" will match characters
69  * 'a' through 'm', a <hyphen> '-', or a 'z').
70  *
71  * Supports BSD extensions FNM_LEADING_DIR to match pattern to the end of one
72  * path segment of string, and FNM_CASEFOLD to ignore alpha case.
73  *
74  * NOTE: Only POSIX/C single byte locales are correctly supported at this time.
75  * Notably, non-POSIX locales with FNM_CASEFOLD produce undefined results,
76  * particularly in ranges of mixed case (e.g. "[A-z]") or spanning alpha and
77  * nonalpha characters within a range.
78  *
79  * XXX comments below indicate porting required for multi-byte character sets
80  * and non-POSIX locale collation orders; requires mbr* APIs to track shift
81  * state of pattern and string (rewinding pattern and string repeatedly).
82  *
83  * Certain parts of the code assume 0x00-0x3F are unique with any MBCS (e.g.
84  * UTF-8, SHIFT-JIS, etc).  Any implementation allowing '\' as an alternate
85  * path delimiter must be aware that 0x5C is NOT unique within SHIFT-JIS.
86  */
87 
88 #include <fnmatch.h>
89 #include <string.h>
90 #include <ctype.h>
91 #include <limits.h>
92 
93 #include "charclass.h"
94 
95 #define	RANGE_MATCH	1
96 #define	RANGE_NOMATCH	0
97 #define	RANGE_ERROR	(-1)
98 
99 static int
100 classmatch(const char *pattern, char test, int foldcase, const char **ep)
101 {
102 	struct cclass *cc;
103 	const char *colon;
104 	size_t len;
105 	int rval = RANGE_NOMATCH;
106 	const char * const mismatch = pattern;
107 
108 	if (*pattern != '[' || pattern[1] != ':') {
109 		*ep = mismatch;
110 		return(RANGE_ERROR);
111 	}
112 
113 	pattern += 2;
114 
115 	if ((colon = strchr(pattern, ':')) == NULL || colon[1] != ']') {
116 		*ep = mismatch;
117 		return(RANGE_ERROR);
118 	}
119 	*ep = colon + 2;
120 	len = (size_t)(colon - pattern);
121 
122 	if (foldcase && strncmp(pattern, "upper:]", 7) == 0)
123 		pattern = "lower:]";
124 	for (cc = cclasses; cc->name != NULL; cc++) {
125 		if (!strncmp(pattern, cc->name, len) && cc->name[len] == '\0') {
126 			if (cc->isctype((unsigned char)test))
127 				rval = RANGE_MATCH;
128 			break;
129 		}
130 	}
131 	if (cc->name == NULL) {
132 		/* invalid character class, treat as normal text */
133 		*ep = mismatch;
134 		rval = RANGE_ERROR;
135 	}
136 	return(rval);
137 }
138 
139 /* Most MBCS/collation/case issues handled here.  Wildcard '*' is not handled.
140  * EOS '\0' and the FNM_PATHNAME '/' delimiters are not advanced over,
141  * however the "\/" sequence is advanced to '/'.
142  *
143  * Both pattern and string are **char to support pointer increment of arbitrary
144  * multibyte characters for the given locale, in a later iteration of this code
145  */
146 static int fnmatch_ch(const char **pattern, const char **string, int flags)
147 {
148     const char * const mismatch = *pattern;
149     const int nocase = !!(flags & FNM_CASEFOLD);
150     const int escape = !(flags & FNM_NOESCAPE);
151     const int slash = !!(flags & FNM_PATHNAME);
152     int result = FNM_NOMATCH;
153     const char *startch;
154     int negate;
155 
156     if (**pattern == '[')
157     {
158         ++*pattern;
159 
160         /* Handle negation, either leading ! or ^ operators (never both) */
161         negate = ((**pattern == '!') || (**pattern == '^'));
162         if (negate)
163             ++*pattern;
164 
165         /* ']' is an ordinary character at the start of the range pattern */
166         if (**pattern == ']')
167             goto leadingclosebrace;
168 
169         while (**pattern)
170         {
171             if (**pattern == ']') {
172                 ++*pattern;
173                 /* XXX: Fix for MBCS character width */
174                 ++*string;
175                 return (result ^ negate);
176             }
177 
178             if (escape && (**pattern == '\\')) {
179                 ++*pattern;
180 
181                 /* Patterns must be terminated with ']', not EOS */
182                 if (!**pattern)
183                     break;
184             }
185 
186             /* Patterns must be terminated with ']' not '/' */
187             if (slash && (**pattern == '/'))
188                 break;
189 
190             /* Match character classes. */
191             if (classmatch(*pattern, **string, nocase, pattern)
192                 == RANGE_MATCH) {
193                 result = 0;
194                 continue;
195             }
196 
197 leadingclosebrace:
198             /* Look at only well-formed range patterns;
199              * "x-]" is not allowed unless escaped ("x-\]")
200              * XXX: Fix for locale/MBCS character width
201              */
202             if (((*pattern)[1] == '-') && ((*pattern)[2] != ']'))
203             {
204                 startch = *pattern;
205                 *pattern += (escape && ((*pattern)[2] == '\\')) ? 3 : 2;
206 
207                 /* NOT a properly balanced [expr] pattern, EOS terminated
208                  * or ranges containing a slash in FNM_PATHNAME mode pattern
209                  * fall out to to the rewind and test '[' literal code path
210                  */
211                 if (!**pattern || (slash && (**pattern == '/')))
212                     break;
213 
214                 /* XXX: handle locale/MBCS comparison, advance by MBCS char width */
215                 if ((**string >= *startch) && (**string <= **pattern))
216                     result = 0;
217                 else if (nocase && (isupper((unsigned char)**string) ||
218 			    isupper((unsigned char)*startch) ||
219                             isupper((unsigned char)**pattern))
220                             && (tolower((unsigned char)**string) >=
221 			        tolower((unsigned char)*startch))
222                             && (tolower((unsigned char)**string) <=
223 				tolower((unsigned char)**pattern)))
224                     result = 0;
225 
226                 ++*pattern;
227                 continue;
228             }
229 
230             /* XXX: handle locale/MBCS comparison, advance by MBCS char width */
231             if ((**string == **pattern))
232                 result = 0;
233             else if (nocase && (isupper((unsigned char)**string) ||
234 			    isupper((unsigned char)**pattern))
235                             && (tolower((unsigned char)**string) ==
236 				tolower((unsigned char)**pattern)))
237                 result = 0;
238 
239             ++*pattern;
240         }
241 
242         /* NOT a properly balanced [expr] pattern; Rewind
243          * and reset result to test '[' literal
244          */
245         *pattern = mismatch;
246         result = FNM_NOMATCH;
247     }
248     else if (**pattern == '?') {
249         /* Optimize '?' match before unescaping **pattern */
250         if (!**string || (slash && (**string == '/')))
251             return FNM_NOMATCH;
252         result = 0;
253         goto fnmatch_ch_success;
254     }
255     else if (escape && (**pattern == '\\') && (*pattern)[1]) {
256         ++*pattern;
257     }
258 
259     /* XXX: handle locale/MBCS comparison, advance by the MBCS char width */
260     if (**string == **pattern)
261         result = 0;
262     else if (nocase && (isupper((unsigned char)**string) ||
263 		    isupper((unsigned char)**pattern))
264                     && (tolower((unsigned char)**string) ==
265 			tolower((unsigned char)**pattern)))
266         result = 0;
267 
268     /* Refuse to advance over trailing slash or nulls
269      */
270     if (!**string || !**pattern || (slash && ((**string == '/') || (**pattern == '/'))))
271         return result;
272 
273 fnmatch_ch_success:
274     ++*pattern;
275     ++*string;
276     return result;
277 }
278 
279 
280 int fnmatch(const char *pattern, const char *string, int flags)
281 {
282     static const char dummystring[2] = {' ', 0};
283     const int escape = !(flags & FNM_NOESCAPE);
284     const int slash = !!(flags & FNM_PATHNAME);
285     const int leading_dir = !!(flags & FNM_LEADING_DIR);
286     const char *strendseg;
287     const char *dummyptr;
288     const char *matchptr;
289     int wild;
290     /* For '*' wild processing only; surpress 'used before initialization'
291      * warnings with dummy initialization values;
292      */
293     const char *strstartseg = NULL;
294     const char *mismatch = NULL;
295     int matchlen = 0;
296 
297     if (strnlen(pattern, PATH_MAX) == PATH_MAX ||
298         strnlen(string, PATH_MAX) == PATH_MAX)
299             return (FNM_NOMATCH);
300 
301     if (*pattern == '*')
302         goto firstsegment;
303 
304     while (*pattern && *string)
305     {
306         /* Pre-decode "\/" which has no special significance, and
307          * match balanced slashes, starting a new segment pattern
308          */
309         if (slash && escape && (*pattern == '\\') && (pattern[1] == '/'))
310             ++pattern;
311         if (slash && (*pattern == '/') && (*string == '/')) {
312             ++pattern;
313             ++string;
314         }
315 
316 firstsegment:
317         /* At the beginning of each segment, validate leading period behavior.
318          */
319         if ((flags & FNM_PERIOD) && (*string == '.'))
320         {
321             if (*pattern == '.')
322                 ++pattern;
323             else if (escape && (*pattern == '\\') && (pattern[1] == '.'))
324                 pattern += 2;
325             else
326                 return FNM_NOMATCH;
327             ++string;
328         }
329 
330         /* Determine the end of string segment
331          *
332          * Presumes '/' character is unique, not composite in any MBCS encoding
333          */
334         if (slash) {
335             strendseg = strchr(string, '/');
336             if (!strendseg)
337                 strendseg = strchr(string, '\0');
338         }
339         else {
340             strendseg = strchr(string, '\0');
341         }
342 
343         /* Allow pattern '*' to be consumed even with no remaining string to match
344          */
345         while (*pattern)
346         {
347             if ((string > strendseg)
348                 || ((string == strendseg) && (*pattern != '*')))
349                 break;
350 
351             if (slash && ((*pattern == '/')
352                            || (escape && (*pattern == '\\')
353                                       && (pattern[1] == '/'))))
354                 break;
355 
356             /* Reduce groups of '*' and '?' to n '?' matches
357              * followed by one '*' test for simplicity
358              */
359             for (wild = 0; ((*pattern == '*') || (*pattern == '?')); ++pattern)
360             {
361                 if (*pattern == '*') {
362                     wild = 1;
363                 }
364                 else if (string < strendseg) {  /* && (*pattern == '?') */
365                     /* XXX: Advance 1 char for MBCS locale */
366                     ++string;
367                 }
368                 else {  /* (string >= strendseg) && (*pattern == '?') */
369                     return FNM_NOMATCH;
370                 }
371             }
372 
373             if (wild)
374             {
375                 strstartseg = string;
376                 mismatch = pattern;
377 
378                 /* Count fixed (non '*') char matches remaining in pattern
379                  * excluding '/' (or "\/") and '*'
380                  */
381                 for (matchptr = pattern, matchlen = 0; 1; ++matchlen)
382                 {
383                     if ((*matchptr == '\0')
384                         || (slash && ((*matchptr == '/')
385                                       || (escape && (*matchptr == '\\')
386                                                  && (matchptr[1] == '/')))))
387                     {
388                         /* Compare precisely this many trailing string chars,
389                          * the resulting match needs no wildcard loop
390                          */
391                         /* XXX: Adjust for MBCS */
392                         if (string + matchlen > strendseg)
393                             return FNM_NOMATCH;
394 
395                         string = strendseg - matchlen;
396                         wild = 0;
397                         break;
398                     }
399 
400                     if (*matchptr == '*')
401                     {
402                         /* Ensure at least this many trailing string chars remain
403                          * for the first comparison
404                          */
405                         /* XXX: Adjust for MBCS */
406                         if (string + matchlen > strendseg)
407                             return FNM_NOMATCH;
408 
409                         /* Begin first wild comparison at the current position */
410                         break;
411                     }
412 
413                     /* Skip forward in pattern by a single character match
414                      * Use a dummy fnmatch_ch() test to count one "[range]" escape
415                      */
416                     /* XXX: Adjust for MBCS */
417                     if (escape && (*matchptr == '\\') && matchptr[1]) {
418                         matchptr += 2;
419                     }
420                     else if (*matchptr == '[') {
421                         dummyptr = dummystring;
422                         fnmatch_ch(&matchptr, &dummyptr, flags);
423                     }
424                     else {
425                         ++matchptr;
426                     }
427                 }
428             }
429 
430             /* Incrementally match string against the pattern
431              */
432             while (*pattern && (string < strendseg))
433             {
434                 /* Success; begin a new wild pattern search
435                  */
436                 if (*pattern == '*')
437                     break;
438 
439                 if (slash && ((*string == '/')
440                               || (*pattern == '/')
441                               || (escape && (*pattern == '\\')
442                                          && (pattern[1] == '/'))))
443                     break;
444 
445                 /* Compare ch's (the pattern is advanced over "\/" to the '/',
446                  * but slashes will mismatch, and are not consumed)
447                  */
448                 if (!fnmatch_ch(&pattern, &string, flags))
449                     continue;
450 
451                 /* Failed to match, loop against next char offset of string segment
452                  * until not enough string chars remain to match the fixed pattern
453                  */
454                 if (wild) {
455                     /* XXX: Advance 1 char for MBCS locale */
456                     string = ++strstartseg;
457                     if (string + matchlen > strendseg)
458                         return FNM_NOMATCH;
459 
460                     pattern = mismatch;
461                     continue;
462                 }
463                 else
464                     return FNM_NOMATCH;
465             }
466         }
467 
468         if (*string && !((slash || leading_dir) && (*string == '/')))
469             return FNM_NOMATCH;
470 
471         if (*pattern && !(slash && ((*pattern == '/')
472                                     || (escape && (*pattern == '\\')
473                                                && (pattern[1] == '/')))))
474             return FNM_NOMATCH;
475 
476         if (leading_dir && !*pattern && *string == '/')
477             return 0;
478     }
479 
480     /* Where both pattern and string are at EOS, declare success
481      */
482     if (!*string && !*pattern)
483         return 0;
484 
485     /* pattern didn't match to the end of string */
486     return FNM_NOMATCH;
487 }
488