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 }