github.com/joomcode/cue@v0.4.4-0.20221111115225-539fe3512047/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  // Match reports whether name matches the shell file name pattern.
    31  // The pattern syntax is:
    32  //
    33  //	pattern:
    34  //		{ term }
    35  //	term:
    36  //		'*'         matches any sequence of non-Separator characters
    37  //		'?'         matches any single non-Separator character
    38  //		'[' [ '^' ] { character-range } ']'
    39  //		            character class (must be non-empty)
    40  //		c           matches character c (c != '*', '?', '\\', '[')
    41  //		'\\' c      matches character c
    42  //
    43  //	character-range:
    44  //		c           matches character c (c != '\\', '-', ']')
    45  //		'\\' c      matches character c
    46  //		lo '-' hi   matches character c for lo <= c <= hi
    47  //
    48  // Match requires pattern to match all of name, not just a substring.
    49  // The only possible returned error is ErrBadPattern, when pattern
    50  // is malformed.
    51  //
    52  // On Windows, escaping is disabled. Instead, '\\' is treated as
    53  // path separator.
    54  //
    55  func Match(pattern, name string, o OS) (matched bool, err error) {
    56  	os := getOS(o)
    57  Pattern:
    58  	for len(pattern) > 0 {
    59  		var star bool
    60  		var chunk string
    61  		star, chunk, pattern = scanChunk(pattern, os)
    62  		if star && chunk == "" {
    63  			// Trailing * matches rest of string unless it has a /.
    64  			return !strings.Contains(name, string(os.Separator)), nil
    65  		}
    66  		// Look for match at current position.
    67  		t, ok, err := matchChunk(chunk, name, os)
    68  		// if we're the last chunk, make sure we've exhausted the name
    69  		// otherwise we'll give a false result even if we could still match
    70  		// using the star
    71  		if ok && (len(t) == 0 || len(pattern) > 0) {
    72  			name = t
    73  			continue
    74  		}
    75  		if err != nil {
    76  			return false, err
    77  		}
    78  		if star {
    79  			// Look for match skipping i+1 bytes.
    80  			// Cannot skip /.
    81  			for i := 0; i < len(name) && name[i] != os.Separator; i++ {
    82  				t, ok, err := matchChunk(chunk, name[i+1:], os)
    83  				if ok {
    84  					// if we're the last chunk, make sure we exhausted the name
    85  					if len(pattern) == 0 && len(t) > 0 {
    86  						continue
    87  					}
    88  					name = t
    89  					continue Pattern
    90  				}
    91  				if err != nil {
    92  					return false, err
    93  				}
    94  			}
    95  		}
    96  		return false, nil
    97  	}
    98  	return len(name) == 0, nil
    99  }
   100  
   101  // scanChunk gets the next segment of pattern, which is a non-star string
   102  // possibly preceded by a star.
   103  func scanChunk(pattern string, os os) (star bool, chunk, rest string) {
   104  	for len(pattern) > 0 && pattern[0] == '*' {
   105  		pattern = pattern[1:]
   106  		star = true
   107  	}
   108  	inrange := false
   109  	var i int
   110  Scan:
   111  	for i = 0; i < len(pattern); i++ {
   112  		switch pattern[i] {
   113  		case '\\':
   114  			if !os.isWindows() {
   115  				// error check handled in matchChunk: bad pattern.
   116  				if i+1 < len(pattern) {
   117  					i++
   118  				}
   119  			}
   120  		case '[':
   121  			inrange = true
   122  		case ']':
   123  			inrange = false
   124  		case '*':
   125  			if !inrange {
   126  				break Scan
   127  			}
   128  		}
   129  	}
   130  	return star, pattern[0:i], pattern[i:]
   131  }
   132  
   133  // matchChunk checks whether chunk matches the beginning of s.
   134  // If so, it returns the remainder of s (after the match).
   135  // Chunk is all single-character operators: literals, char classes, and ?.
   136  func matchChunk(chunk, s string, os os) (rest string, ok bool, err error) {
   137  	// failed records whether the match has failed.
   138  	// After the match fails, the loop continues on processing chunk,
   139  	// checking that the pattern is well-formed but no longer reading s.
   140  	failed := false
   141  	for len(chunk) > 0 {
   142  		if !failed && len(s) == 0 {
   143  			failed = true
   144  		}
   145  		switch chunk[0] {
   146  		case '[':
   147  			// character class
   148  			var r rune
   149  			if !failed {
   150  				var n int
   151  				r, n = utf8.DecodeRuneInString(s)
   152  				s = s[n:]
   153  			}
   154  			chunk = chunk[1:]
   155  			// possibly negated
   156  			negated := false
   157  			if len(chunk) > 0 && chunk[0] == '^' {
   158  				negated = true
   159  				chunk = chunk[1:]
   160  			}
   161  			// parse all ranges
   162  			match := false
   163  			nrange := 0
   164  			for {
   165  				if len(chunk) > 0 && chunk[0] == ']' && nrange > 0 {
   166  					chunk = chunk[1:]
   167  					break
   168  				}
   169  				var lo, hi rune
   170  				if lo, chunk, err = getEsc(chunk, os); err != nil {
   171  					return "", false, err
   172  				}
   173  				hi = lo
   174  				if chunk[0] == '-' {
   175  					if hi, chunk, err = getEsc(chunk[1:], os); err != nil {
   176  						return "", false, err
   177  					}
   178  				}
   179  				if lo <= r && r <= hi {
   180  					match = true
   181  				}
   182  				nrange++
   183  			}
   184  			if match == negated {
   185  				failed = true
   186  			}
   187  
   188  		case '?':
   189  			if !failed {
   190  				if s[0] == os.Separator {
   191  					failed = true
   192  				}
   193  				_, n := utf8.DecodeRuneInString(s)
   194  				s = s[n:]
   195  			}
   196  			chunk = chunk[1:]
   197  
   198  		case '\\':
   199  			if !os.isWindows() {
   200  				chunk = chunk[1:]
   201  				if len(chunk) == 0 {
   202  					return "", false, ErrBadPattern
   203  				}
   204  			}
   205  			fallthrough
   206  
   207  		default:
   208  			if !failed {
   209  				if chunk[0] != s[0] {
   210  					failed = true
   211  				}
   212  				s = s[1:]
   213  			}
   214  			chunk = chunk[1:]
   215  		}
   216  	}
   217  	if failed {
   218  		return "", false, nil
   219  	}
   220  	return s, true, nil
   221  }
   222  
   223  // getEsc gets a possibly-escaped character from chunk, for a character class.
   224  func getEsc(chunk string, os os) (r rune, nchunk string, err error) {
   225  	if len(chunk) == 0 || chunk[0] == '-' || chunk[0] == ']' {
   226  		err = ErrBadPattern
   227  		return
   228  	}
   229  	if chunk[0] == '\\' && !os.isWindows() {
   230  		chunk = chunk[1:]
   231  		if len(chunk) == 0 {
   232  			err = ErrBadPattern
   233  			return
   234  		}
   235  	}
   236  	r, n := utf8.DecodeRuneInString(chunk)
   237  	if r == utf8.RuneError && n == 1 {
   238  		err = ErrBadPattern
   239  	}
   240  	nchunk = chunk[n:]
   241  	if len(nchunk) == 0 {
   242  		err = ErrBadPattern
   243  	}
   244  	return
   245  }