cuelang.org/go@v0.10.1/pkg/path/match.go (about)

     1  // Copyright 2020 CUE Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Copyright 2010 The Go Authors. All rights reserved.
    16  // Use of this source code is governed by a BSD-style
    17  // license that can be found in the LICENSE file.
    18  
    19  package path
    20  
    21  import (
    22  	"errors"
    23  	"strings"
    24  	"unicode/utf8"
    25  )
    26  
    27  // ErrBadPattern indicates a pattern was malformed.
    28  var ErrBadPattern = errors.New("syntax error in pattern")
    29  
    30  var errStarStarDisallowed = errors.New("'**' is not supported in patterns as of yet")
    31  
    32  // Match reports whether name matches the shell file name pattern.
    33  // The pattern syntax is:
    34  //
    35  //	pattern:
    36  //		{ term }
    37  //	term:
    38  //		'*'         matches any sequence of non-Separator characters
    39  //		'?'         matches any single non-Separator character
    40  //		'[' [ '^' ] { character-range } ']'
    41  //		            character class (must be non-empty)
    42  //		c           matches character c (c != '*', '?', '\\', '[')
    43  //		'\\' c      matches character c
    44  //
    45  //	character-range:
    46  //		c           matches character c (c != '\\', '-', ']')
    47  //		'\\' c      matches character c
    48  //		lo '-' hi   matches character c for lo <= c <= hi
    49  //
    50  // Match requires pattern to match all of name, not just a substring.
    51  // The only possible returned error is ErrBadPattern, when pattern
    52  // is malformed.
    53  //
    54  // On Windows, escaping is disabled. Instead, '\\' is treated as
    55  // path separator.
    56  //
    57  // A pattern may not contain '**', as a wildcard matching separator characters
    58  // is not supported at this time.
    59  func Match(pattern, name string, o OS) (matched bool, err error) {
    60  	os := getOS(o)
    61  Pattern:
    62  	for len(pattern) > 0 {
    63  		var star bool
    64  		var chunk string
    65  		star, chunk, pattern, err = scanChunk(pattern, os)
    66  		if err != nil {
    67  			return false, err
    68  		}
    69  		if star && chunk == "" {
    70  			// Trailing * matches rest of string unless it has a /.
    71  			return !strings.Contains(name, string(os.Separator)), nil
    72  		}
    73  		// Look for match at current position.
    74  		t, ok, err := matchChunk(chunk, name, os)
    75  		// if we're the last chunk, make sure we've exhausted the name
    76  		// otherwise we'll give a false result even if we could still match
    77  		// using the star
    78  		if ok && (len(t) == 0 || len(pattern) > 0) {
    79  			name = t
    80  			continue
    81  		}
    82  		if err != nil {
    83  			return false, err
    84  		}
    85  		if star {
    86  			// Look for match skipping i+1 bytes.
    87  			// Cannot skip /.
    88  			for i := 0; i < len(name) && name[i] != os.Separator; i++ {
    89  				t, ok, err := matchChunk(chunk, name[i+1:], os)
    90  				if ok {
    91  					// if we're the last chunk, make sure we exhausted the name
    92  					if len(pattern) == 0 && len(t) > 0 {
    93  						continue
    94  					}
    95  					name = t
    96  					continue Pattern
    97  				}
    98  				if err != nil {
    99  					return false, err
   100  				}
   101  			}
   102  		}
   103  		// Before returning false with no error,
   104  		// check that the remainder of the pattern is syntactically valid.
   105  		for len(pattern) > 0 {
   106  			_, _, pattern, err = scanChunk(pattern, os)
   107  			if err != nil {
   108  				return false, err
   109  			}
   110  		}
   111  		return false, nil
   112  	}
   113  	return len(name) == 0, nil
   114  }
   115  
   116  // scanChunk gets the next segment of pattern, which is a non-star string
   117  // possibly preceded by a star.
   118  func scanChunk(pattern string, os os) (star bool, chunk, rest string, _ error) {
   119  	if len(pattern) > 0 && pattern[0] == '*' {
   120  		pattern = pattern[1:]
   121  		star = true
   122  		if len(pattern) > 0 && pattern[0] == '*' {
   123  			// ** is disallowed to allow for future functionality.
   124  			return false, "", "", errStarStarDisallowed
   125  		}
   126  	}
   127  	inrange := false
   128  	var i int
   129  Scan:
   130  	for i = 0; i < len(pattern); i++ {
   131  		switch pattern[i] {
   132  		case '\\':
   133  			if !os.isWindows() {
   134  				// error check handled in matchChunk: bad pattern.
   135  				if i+1 < len(pattern) {
   136  					i++
   137  				}
   138  			}
   139  		case '[':
   140  			inrange = true
   141  		case ']':
   142  			inrange = false
   143  		case '*':
   144  			if !inrange {
   145  				break Scan
   146  			}
   147  		}
   148  	}
   149  	return star, pattern[0:i], pattern[i:], nil
   150  }
   151  
   152  // matchChunk checks whether chunk matches the beginning of s.
   153  // If so, it returns the remainder of s (after the match).
   154  // Chunk is all single-character operators: literals, char classes, and ?.
   155  func matchChunk(chunk, s string, os os) (rest string, ok bool, err error) {
   156  	// failed records whether the match has failed.
   157  	// After the match fails, the loop continues on processing chunk,
   158  	// checking that the pattern is well-formed but no longer reading s.
   159  	failed := false
   160  	for len(chunk) > 0 {
   161  		if !failed && len(s) == 0 {
   162  			failed = true
   163  		}
   164  		switch chunk[0] {
   165  		case '[':
   166  			// character class
   167  			var r rune
   168  			if !failed {
   169  				var n int
   170  				r, n = utf8.DecodeRuneInString(s)
   171  				s = s[n:]
   172  			}
   173  			chunk = chunk[1:]
   174  			// possibly negated
   175  			negated := false
   176  			if len(chunk) > 0 && chunk[0] == '^' {
   177  				negated = true
   178  				chunk = chunk[1:]
   179  			}
   180  			// parse all ranges
   181  			match := false
   182  			nrange := 0
   183  			for {
   184  				if len(chunk) > 0 && chunk[0] == ']' && nrange > 0 {
   185  					chunk = chunk[1:]
   186  					break
   187  				}
   188  				var lo, hi rune
   189  				if lo, chunk, err = getEsc(chunk, os); err != nil {
   190  					return "", false, err
   191  				}
   192  				hi = lo
   193  				if chunk[0] == '-' {
   194  					if hi, chunk, err = getEsc(chunk[1:], os); err != nil {
   195  						return "", false, err
   196  					}
   197  				}
   198  				if lo <= r && r <= hi {
   199  					match = true
   200  				}
   201  				nrange++
   202  			}
   203  			if match == negated {
   204  				failed = true
   205  			}
   206  
   207  		case '?':
   208  			if !failed {
   209  				if s[0] == os.Separator {
   210  					failed = true
   211  				}
   212  				_, n := utf8.DecodeRuneInString(s)
   213  				s = s[n:]
   214  			}
   215  			chunk = chunk[1:]
   216  
   217  		case '\\':
   218  			if !os.isWindows() {
   219  				chunk = chunk[1:]
   220  				if len(chunk) == 0 {
   221  					return "", false, ErrBadPattern
   222  				}
   223  			}
   224  			fallthrough
   225  
   226  		default:
   227  			if !failed {
   228  				if chunk[0] != s[0] {
   229  					failed = true
   230  				}
   231  				s = s[1:]
   232  			}
   233  			chunk = chunk[1:]
   234  		}
   235  	}
   236  	if failed {
   237  		return "", false, nil
   238  	}
   239  	return s, true, nil
   240  }
   241  
   242  // getEsc gets a possibly-escaped character from chunk, for a character class.
   243  func getEsc(chunk string, os os) (r rune, nchunk string, err error) {
   244  	if len(chunk) == 0 || chunk[0] == '-' || chunk[0] == ']' {
   245  		err = ErrBadPattern
   246  		return
   247  	}
   248  	if chunk[0] == '\\' && !os.isWindows() {
   249  		chunk = chunk[1:]
   250  		if len(chunk) == 0 {
   251  			err = ErrBadPattern
   252  			return
   253  		}
   254  	}
   255  	r, n := utf8.DecodeRuneInString(chunk)
   256  	if r == utf8.RuneError && n == 1 {
   257  		err = ErrBadPattern
   258  	}
   259  	nchunk = chunk[n:]
   260  	if len(nchunk) == 0 {
   261  		err = ErrBadPattern
   262  	}
   263  	return
   264  }