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