/* $OpenBSD: fnmatch.c,v 1.15 2011/02/10 21:31:59 stsp Exp $ */12/*3* SPDX-License-Identifier: BSD-3-Clause4*5* Copyright (c) 2011, VMware, Inc.6* All rights reserved.7*8* Redistribution and use in source and binary forms, with or without9* modification, are permitted provided that the following conditions are met:10* * Redistributions of source code must retain the above copyright11* notice, this list of conditions and the following disclaimer.12* * Redistributions in binary form must reproduce the above copyright13* notice, this list of conditions and the following disclaimer in the14* documentation and/or other materials provided with the distribution.15* * Neither the name of the VMware, Inc. nor the names of its contributors16* may be used to endorse or promote products derived from this software17* without specific prior written permission.18*19* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"20* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE21* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE22* ARE DISCLAIMED. IN NO EVENT SHALL VMWARE, INC. OR CONTRIBUTORS BE LIABLE FOR23* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES24* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;25* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND26* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT27* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF28* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.29*/3031/*32* SPDX-License-Identifier: ISC33*34* Copyright (c) 2008, 2016 Todd C. Miller <[email protected]>35*36* Permission to use, copy, modify, and distribute this software for any37* purpose with or without fee is hereby granted, provided that the above38* copyright notice and this permission notice appear in all copies.39*40* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES41* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF42* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR43* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES44* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN45* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF46* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.47*/4849/* Authored by William A. Rowe Jr. <wrowe; apache.org, vmware.com>, April 201150*51* Derived from The Open Group Base Specifications Issue 7, IEEE Std 1003.1-200852* as described in;53* http://pubs.opengroup.org/onlinepubs/9699919799/functions/fnmatch.html54*55* Filename pattern matches defined in section 2.13, "Pattern Matching Notation"56* from chapter 2. "Shell Command Language"57* http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_1358* where; 1. A bracket expression starting with an unquoted <circumflex> '^'59* character CONTINUES to specify a non-matching list; 2. an explicit <period> '.'60* in a bracket expression matching list, e.g. "[.abc]" does NOT match a leading61* <period> in a filename; 3. a <left-square-bracket> '[' which does not introduce62* a valid bracket expression is treated as an ordinary character; 4. a differing63* number of consecutive slashes within pattern and string will NOT match;64* 5. a trailing '\' in FNM_ESCAPE mode is treated as an ordinary '\' character.65*66* Bracket expansion defined in section 9.3.5, "RE Bracket Expression",67* from chapter 9, "Regular Expressions"68* http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap09.html#tag_09_03_0569* with no support for collating symbols, equivalence class expressions or70* character class expressions. A partial range expression with a leading71* hyphen following a valid range expression will match only the ordinary72* <hyphen> and the ending character (e.g. "[a-m-z]" will match characters73* 'a' through 'm', a <hyphen> '-', or a 'z').74*75* Supports BSD extensions FNM_LEADING_DIR to match pattern to the end of one76* path segment of string, and FNM_CASEFOLD to ignore alpha case.77*78* NOTE: Only POSIX/C single byte locales are correctly supported at this time.79* Notably, non-POSIX locales with FNM_CASEFOLD produce undefined results,80* particularly in ranges of mixed case (e.g. "[A-z]") or spanning alpha and81* nonalpha characters within a range.82*83* XXX comments below indicate porting required for multi-byte character sets84* and non-POSIX locale collation orders; requires mbr* APIs to track shift85* state of pattern and string (rewinding pattern and string repeatedly).86*87* Certain parts of the code assume 0x00-0x3F are unique with any MBCS (e.g.88* UTF-8, SHIFT-JIS, etc). Any implementation allowing '\' as an alternate89* path delimiter must be aware that 0x5C is NOT unique within SHIFT-JIS.90*/9192#include <config.h>9394#ifndef HAVE_FNMATCH9596#include <ctype.h>97#include <string.h>9899#include <sudo_compat.h>100#include <compat/charclass.h>101#include <compat/fnmatch.h>102103#define RANGE_MATCH 1104#define RANGE_NOMATCH 0105#define RANGE_ERROR (-1)106107static int108classmatch(const char *pattern, char test, int foldcase, const char **ep)109{110const char * const mismatch = pattern;111const char *colon;112struct cclass *cc;113int result = RANGE_NOMATCH;114size_t len;115116if (pattern[0] != '[' || pattern[1] != ':') {117*ep = mismatch;118return RANGE_ERROR;119}120pattern += 2;121122if ((colon = strchr(pattern, ':')) == NULL || colon[1] != ']') {123*ep = mismatch;124return RANGE_ERROR;125}126*ep = colon + 2;127len = (size_t)(colon - pattern);128129if (foldcase && strncmp(pattern, "upper:]", 7) == 0)130pattern = "lower:]";131for (cc = cclasses; cc->name != NULL; cc++) {132if (!strncmp(pattern, cc->name, len) && cc->name[len] == '\0') {133if (cc->isctype((unsigned char)test))134result = RANGE_MATCH;135break;136}137}138if (cc->name == NULL) {139/* invalid character class, treat as normal text */140*ep = mismatch;141result = RANGE_ERROR;142}143return result;144}145146/* Most MBCS/collation/case issues handled here. Wildcard '*' is not handled.147* EOS '\0' and the FNM_PATHNAME '/' delimiters are not advanced over,148* however the "\/" sequence is advanced to '/'.149*150* Both pattern and string are **char to support pointer increment of arbitrary151* multibyte characters for the given locale, in a later iteration of this code152*/153static int fnmatch_ch(const char **pattern, const char **string, int flags)154{155const char * const mismatch = *pattern;156const int nocase = !!(flags & FNM_CASEFOLD);157const int escape = !(flags & FNM_NOESCAPE);158const int slash = !!(flags & FNM_PATHNAME);159int result = FNM_NOMATCH;160const char *startch;161int negate;162163if (**pattern == '[')164{165++*pattern;166167/* Handle negation, either leading ! or ^ operators (never both) */168negate = ((**pattern == '!') || (**pattern == '^'));169if (negate)170++*pattern;171172/* ']' is an ordinary character at the start of the range pattern */173if (**pattern == ']')174goto leadingclosebrace;175176while (**pattern)177{178if (**pattern == ']') {179++*pattern;180/* XXX: Fix for MBCS character width */181++*string;182return (result ^ negate);183}184185if (escape && (**pattern == '\\')) {186++*pattern;187188/* Patterns must be terminated with ']', not EOS */189if (!**pattern)190break;191}192193/* Patterns must be terminated with ']' not '/' */194if (slash && (**pattern == '/'))195break;196197/* Match character classes. */198switch (classmatch(*pattern, **string, nocase, pattern)) {199case RANGE_MATCH:200result = 0;201continue;202case RANGE_NOMATCH:203/* Valid character class but no match. */204continue;205default:206/* Not a valid character class. */207break;208}209if (!**pattern)210break;211212leadingclosebrace:213/* Look at only well-formed range patterns;214* "x-]" is not allowed unless escaped ("x-\]")215* XXX: Fix for locale/MBCS character width216*/217if (((*pattern)[1] == '-') && ((*pattern)[2] != ']'))218{219startch = *pattern;220*pattern += (escape && ((*pattern)[2] == '\\')) ? 3 : 2;221222/* NOT a properly balanced [expr] pattern, EOS terminated223* or ranges containing a slash in FNM_PATHNAME mode pattern224* fall out to the rewind and test '[' literal code path225*/226if (!**pattern || (slash && (**pattern == '/')))227break;228229/* XXX: handle locale/MBCS comparison, advance by MBCS char width */230if ((**string >= *startch) && (**string <= **pattern))231result = 0;232else if (nocase && (isupper((unsigned char)**string) ||233isupper((unsigned char)*startch) ||234isupper((unsigned char)**pattern))235&& (tolower((unsigned char)**string) >= tolower((unsigned char)*startch))236&& (tolower((unsigned char)**string) <= tolower((unsigned char)**pattern)))237result = 0;238239++*pattern;240continue;241}242243/* XXX: handle locale/MBCS comparison, advance by MBCS char width */244if ((**string == **pattern))245result = 0;246else if (nocase && (isupper((unsigned char)**string) ||247isupper((unsigned char)**pattern))248&& (tolower((unsigned char)**string) == tolower((unsigned char)**pattern)))249result = 0;250251++*pattern;252}253254/* NOT a properly balanced [expr] pattern; Rewind255* and reset result to test '[' literal256*/257*pattern = mismatch;258result = FNM_NOMATCH;259}260else if (**pattern == '?') {261/* Optimize '?' match before unescaping **pattern */262if (!**string || (slash && (**string == '/')))263return FNM_NOMATCH;264result = 0;265goto fnmatch_ch_success;266}267else if (escape && (**pattern == '\\') && (*pattern)[1]) {268++*pattern;269}270271/* XXX: handle locale/MBCS comparison, advance by the MBCS char width */272if (**string == **pattern)273result = 0;274else if (nocase && (isupper((unsigned char)**string) || isupper((unsigned char)**pattern))275&& (tolower((unsigned char)**string) == tolower((unsigned char)**pattern)))276result = 0;277278/* Refuse to advance over trailing slash or nulls279*/280if (!**string || !**pattern || (slash && ((**string == '/') || (**pattern == '/'))))281return result;282283fnmatch_ch_success:284++*pattern;285++*string;286return result;287}288289int sudo_fnmatch(const char *pattern, const char *string, int flags)290{291static const char dummystring[2] = {' ', 0};292const int escape = !(flags & FNM_NOESCAPE);293const int slash = !!(flags & FNM_PATHNAME);294const int leading_dir = !!(flags & FNM_LEADING_DIR);295const char *strendseg;296const char *dummyptr;297const char *matchptr;298int wild;299/* For '*' wild processing only; suppress 'used before initialization'300* warnings with dummy initialization values;301*/302const char *strstartseg = NULL;303const char *mismatch = NULL;304int matchlen = 0;305306if (*pattern == '*')307goto firstsegment;308309while (*pattern && *string)310{311/* Pre-decode "\/" which has no special significance, and312* match balanced slashes, starting a new segment pattern313*/314if (slash && escape && (*pattern == '\\') && (pattern[1] == '/'))315++pattern;316if (slash && (*pattern == '/') && (*string == '/')) {317++pattern;318++string;319}320321firstsegment:322/* At the beginning of each segment, validate leading period behavior.323*/324if ((flags & FNM_PERIOD) && (*string == '.'))325{326if (*pattern == '.')327++pattern;328else if (escape && (*pattern == '\\') && (pattern[1] == '.'))329pattern += 2;330else331return FNM_NOMATCH;332++string;333}334335/* Determine the end of string segment336*337* Presumes '/' character is unique, not composite in any MBCS encoding338*/339if (slash) {340strendseg = strchr(string, '/');341if (!strendseg)342strendseg = strchr(string, '\0');343}344else {345strendseg = strchr(string, '\0');346}347348/* Allow pattern '*' to be consumed even with no remaining string to match349*/350while (*pattern)351{352if ((string > strendseg)353|| ((string == strendseg) && (*pattern != '*')))354break;355356if (slash && ((*pattern == '/')357|| (escape && (*pattern == '\\')358&& (pattern[1] == '/'))))359break;360361/* Reduce groups of '*' and '?' to n '?' matches362* followed by one '*' test for simplicity363*/364for (wild = 0; ((*pattern == '*') || (*pattern == '?')); ++pattern)365{366if (*pattern == '*') {367wild = 1;368}369else if (string < strendseg) { /* && (*pattern == '?') */370/* XXX: Advance 1 char for MBCS locale */371++string;372}373else { /* (string >= strendseg) && (*pattern == '?') */374return FNM_NOMATCH;375}376}377378if (wild)379{380strstartseg = string;381mismatch = pattern;382383/* Count fixed (non '*') char matches remaining in pattern384* excluding '/' (or "\/") and '*'385*/386for (matchptr = pattern, matchlen = 0; 1; ++matchlen)387{388if ((*matchptr == '\0')389|| (slash && ((*matchptr == '/')390|| (escape && (*matchptr == '\\')391&& (matchptr[1] == '/')))))392{393/* Compare precisely this many trailing string chars,394* the resulting match needs no wildcard loop395*/396/* XXX: Adjust for MBCS */397if (string + matchlen > strendseg)398return FNM_NOMATCH;399400string = strendseg - matchlen;401wild = 0;402break;403}404405if (*matchptr == '*')406{407/* Ensure at least this many trailing string chars remain408* for the first comparison409*/410/* XXX: Adjust for MBCS */411if (string + matchlen > strendseg)412return FNM_NOMATCH;413414/* Begin first wild comparison at the current position */415break;416}417418/* Skip forward in pattern by a single character match419* Use a dummy fnmatch_ch() test to count one "[range]" escape420*/421/* XXX: Adjust for MBCS */422if (escape && (*matchptr == '\\') && matchptr[1]) {423matchptr += 2;424}425else if (*matchptr == '[') {426dummyptr = dummystring;427fnmatch_ch(&matchptr, &dummyptr, flags);428}429else {430++matchptr;431}432}433}434435/* Incrementally match string against the pattern436*/437while (*pattern && (string < strendseg))438{439/* Success; begin a new wild pattern search440*/441if (*pattern == '*')442break;443444if (slash && ((*string == '/')445|| (*pattern == '/')446|| (escape && (*pattern == '\\')447&& (pattern[1] == '/'))))448break;449450/* Compare ch's (the pattern is advanced over "\/" to the '/',451* but slashes will mismatch, and are not consumed)452*/453if (!fnmatch_ch(&pattern, &string, flags))454continue;455456/* Failed to match, loop against next char offset of string segment457* until not enough string chars remain to match the fixed pattern458*/459if (wild) {460/* XXX: Advance 1 char for MBCS locale */461string = ++strstartseg;462if (string + matchlen > strendseg)463return FNM_NOMATCH;464465pattern = mismatch;466continue;467}468else469return FNM_NOMATCH;470}471}472473if (*string && !((slash || leading_dir) && (*string == '/')))474return FNM_NOMATCH;475476if (*pattern && !(slash && ((*pattern == '/')477|| (escape && (*pattern == '\\')478&& (pattern[1] == '/')))))479return FNM_NOMATCH;480481if (leading_dir && !*pattern && *string == '/')482return 0;483}484485/* Where both pattern and string are at EOS, declare success486*/487if (!*string && !*pattern)488return 0;489490/* pattern didn't match to the end of string */491return FNM_NOMATCH;492}493#endif /* HAVE_FNMATCH */494495496