├── go.mod ├── README.md ├── .github └── workflows │ └── go.yml ├── LICENSE ├── match.go └── match_test.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tidwall/match 2 | 3 | go 1.15 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Match 2 | 3 | [![GoDoc](https://godoc.org/github.com/tidwall/match?status.svg)](https://godoc.org/github.com/tidwall/match) 4 | 5 | Match is a very simple pattern matcher where '*' matches on any 6 | number characters and '?' matches on any one character. 7 | 8 | ## Installing 9 | 10 | ``` 11 | go get -u github.com/tidwall/match 12 | ``` 13 | 14 | ## Example 15 | 16 | ```go 17 | match.Match("hello", "*llo") 18 | match.Match("jello", "?ello") 19 | match.Match("hello", "h*o") 20 | ``` 21 | 22 | 23 | ## Contact 24 | 25 | Josh Baker [@tidwall](http://twitter.com/tidwall) 26 | 27 | ## License 28 | 29 | Match source code is available under the MIT [License](/LICENSE). 30 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | name: Build 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Set up Go 1.x 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: ^1.13 20 | 21 | - name: Check out code into the Go module directory 22 | uses: actions/checkout@v2 23 | 24 | - name: Get dependencies 25 | run: | 26 | go get -v -t -d ./... 27 | if [ -f Gopkg.toml ]; then 28 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 29 | dep ensure 30 | fi 31 | 32 | - name: Build 33 | run: go build -v . 34 | 35 | - name: Test 36 | run: go test -v . 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Josh Baker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /match.go: -------------------------------------------------------------------------------- 1 | // Package match provides a simple pattern matcher with unicode support. 2 | package match 3 | 4 | import ( 5 | "unicode/utf8" 6 | ) 7 | 8 | // Match returns true if str matches pattern. This is a very 9 | // simple wildcard match where '*' matches on any number characters 10 | // and '?' matches on any one character. 11 | // 12 | // pattern: 13 | // 14 | // { term } 15 | // 16 | // term: 17 | // 18 | // '*' matches any sequence of non-Separator characters 19 | // '?' matches any single non-Separator character 20 | // c matches character c (c != '*', '?', '\\') 21 | // '\\' c matches character c 22 | func Match(str, pattern string) bool { 23 | return match0(str, pattern, false) 24 | } 25 | 26 | // MatchNoCase is the same as Match but performs a case-insensitive match. 27 | // Such that string "Hello World" with match with lower case pattern "hello*" 28 | func MatchNoCase(str, pattern string) bool { 29 | return match0(str, pattern, true) 30 | } 31 | 32 | func match0(str, pattern string, nocase bool) bool { 33 | if pattern == "*" { 34 | return true 35 | } 36 | return match(str, pattern, 0, nil, -1, nocase) == rMatch 37 | } 38 | 39 | // MatchLimit is the same as Match but will limit the complexity of the match 40 | // operation. This is to avoid long running matches, specifically to avoid ReDos 41 | // attacks from arbritary inputs. 42 | // 43 | // How it works: 44 | // The underlying match routine is recursive and may call itself when it 45 | // encounters a sandwiched wildcard pattern, such as: `user:*:name`. 46 | // Everytime it calls itself a counter is incremented. 47 | // The operation is stopped when counter > maxcomp*len(str). 48 | func MatchLimit(str, pattern string, maxcomp int) (matched, stopped bool) { 49 | return matchLimit0(str, pattern, maxcomp, false) 50 | } 51 | 52 | func MatchLimitNoCase(str, pattern string, maxcomp int, 53 | ) (matched, stopped bool) { 54 | return matchLimit0(str, pattern, maxcomp, true) 55 | } 56 | 57 | func matchLimit0(str, pattern string, maxcomp int, nocase bool, 58 | ) (matched, stopped bool) { 59 | if pattern == "*" { 60 | return true, false 61 | } 62 | counter := 0 63 | r := match(str, pattern, len(str), &counter, maxcomp, nocase) 64 | if r == rStop { 65 | return false, true 66 | } 67 | return r == rMatch, false 68 | } 69 | 70 | type result int 71 | 72 | const ( 73 | rNoMatch result = iota 74 | rMatch 75 | rStop 76 | ) 77 | 78 | func tolower(r rune) rune { 79 | if r >= 'A' && r <= 'Z' { 80 | return r + 32 81 | } 82 | return r 83 | } 84 | 85 | func match(str, pat string, slen int, counter *int, maxcomp int, nocase bool, 86 | ) result { 87 | // check complexity limit 88 | if maxcomp > -1 { 89 | if *counter > slen*maxcomp { 90 | return rStop 91 | } 92 | *counter++ 93 | } 94 | 95 | for len(pat) > 0 { 96 | var wild bool 97 | pc, ps := rune(pat[0]), 1 98 | if pc > 0x7f { 99 | pc, ps = utf8.DecodeRuneInString(pat) 100 | } 101 | var sc rune 102 | var ss int 103 | if len(str) > 0 { 104 | sc, ss = rune(str[0]), 1 105 | if sc > 0x7f { 106 | sc, ss = utf8.DecodeRuneInString(str) 107 | } 108 | } 109 | switch pc { 110 | case '?': 111 | if ss == 0 { 112 | return rNoMatch 113 | } 114 | case '*': 115 | // Ignore repeating stars. 116 | for len(pat) > 1 && pat[1] == '*' { 117 | pat = pat[1:] 118 | } 119 | 120 | // If this star is the last character then it must be a match. 121 | if len(pat) == 1 { 122 | return rMatch 123 | } 124 | 125 | // Match and trim any non-wildcard suffix characters. 126 | var ok bool 127 | str, pat, ok = matchTrimSuffix(str, pat, nocase) 128 | if !ok { 129 | return rNoMatch 130 | } 131 | 132 | // Check for single star again. 133 | if len(pat) == 1 { 134 | return rMatch 135 | } 136 | 137 | // Perform recursive wildcard search. 138 | r := match(str, pat[1:], slen, counter, maxcomp, nocase) 139 | if r != rNoMatch { 140 | return r 141 | } 142 | if len(str) == 0 { 143 | return rNoMatch 144 | } 145 | wild = true 146 | default: 147 | if ss == 0 { 148 | return rNoMatch 149 | } 150 | if pc == '\\' { 151 | pat = pat[ps:] 152 | pc, ps = utf8.DecodeRuneInString(pat) 153 | if ps == 0 { 154 | return rNoMatch 155 | } 156 | } 157 | if nocase { 158 | sc, pc = tolower(sc), tolower(pc) 159 | } 160 | if sc != pc { 161 | return rNoMatch 162 | } 163 | } 164 | str = str[ss:] 165 | if !wild { 166 | pat = pat[ps:] 167 | } 168 | } 169 | if len(str) == 0 { 170 | return rMatch 171 | } 172 | return rNoMatch 173 | } 174 | 175 | // matchTrimSuffix matches and trims any non-wildcard suffix characters. 176 | // Returns the trimed string and pattern. 177 | // 178 | // This is called because the pattern contains extra data after the wildcard 179 | // star. Here we compare any suffix characters in the pattern to the suffix of 180 | // the target string. Basically a reverse match that stops when a wildcard 181 | // character is reached. This is a little trickier than a forward match because 182 | // we need to evaluate an escaped character in reverse. 183 | // 184 | // Any matched characters will be trimmed from both the target 185 | // string and the pattern. 186 | func matchTrimSuffix(str, pat string, nocase bool) (string, string, bool) { 187 | // It's expected that the pattern has at least two bytes and the first byte 188 | // is a wildcard star '*' 189 | match := true 190 | for len(str) > 0 && len(pat) > 1 { 191 | pc, ps := utf8.DecodeLastRuneInString(pat) 192 | var esc bool 193 | for i := 0; ; i++ { 194 | if pat[len(pat)-ps-i-1] != '\\' { 195 | if i&1 == 1 { 196 | esc = true 197 | ps++ 198 | } 199 | break 200 | } 201 | } 202 | if pc == '*' && !esc { 203 | match = true 204 | break 205 | } 206 | sc, ss := utf8.DecodeLastRuneInString(str) 207 | if nocase { 208 | pc, sc = tolower(pc), tolower(sc) 209 | } 210 | if !((pc == '?' && !esc) || pc == sc) { 211 | match = false 212 | break 213 | } 214 | str = str[:len(str)-ss] 215 | pat = pat[:len(pat)-ps] 216 | } 217 | return str, pat, match 218 | } 219 | 220 | var maxRuneBytes = [...]byte{244, 143, 191, 191} 221 | 222 | // Allowable parses the pattern and determines the minimum and maximum allowable 223 | // values that the pattern can represent. 224 | // When the max cannot be determined, 'true' will be returned 225 | // for infinite. 226 | func Allowable(pattern string) (min, max string) { 227 | if pattern == "" || pattern[0] == '*' { 228 | return "", "" 229 | } 230 | 231 | minb := make([]byte, 0, len(pattern)) 232 | maxb := make([]byte, 0, len(pattern)) 233 | var wild bool 234 | for i := 0; i < len(pattern); i++ { 235 | if pattern[i] == '*' { 236 | wild = true 237 | break 238 | } 239 | if pattern[i] == '?' { 240 | minb = append(minb, 0) 241 | maxb = append(maxb, maxRuneBytes[:]...) 242 | } else { 243 | minb = append(minb, pattern[i]) 244 | maxb = append(maxb, pattern[i]) 245 | } 246 | } 247 | if wild { 248 | r, n := utf8.DecodeLastRune(maxb) 249 | if r != utf8.RuneError { 250 | if r < utf8.MaxRune { 251 | r++ 252 | if r > 0x7f { 253 | b := make([]byte, 4) 254 | nn := utf8.EncodeRune(b, r) 255 | maxb = append(maxb[:len(maxb)-n], b[:nn]...) 256 | } else { 257 | maxb = append(maxb[:len(maxb)-n], byte(r)) 258 | } 259 | } 260 | } 261 | } 262 | return string(minb), string(maxb) 263 | } 264 | 265 | // IsPattern returns true if the string is a pattern. 266 | func IsPattern(str string) bool { 267 | for i := 0; i < len(str); i++ { 268 | if str[i] == '*' || str[i] == '?' { 269 | return true 270 | } 271 | } 272 | return false 273 | } 274 | -------------------------------------------------------------------------------- /match_test.go: -------------------------------------------------------------------------------- 1 | package match 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "strings" 7 | "testing" 8 | "time" 9 | "unicode/utf8" 10 | ) 11 | 12 | func TestMatch(t *testing.T) { 13 | if !Match("hello world", "hello world") { 14 | t.Fatal("fail") 15 | } 16 | if Match("hello world", "jello world") { 17 | t.Fatal("fail") 18 | } 19 | if !Match("hello world", "hello*") { 20 | t.Fatal("fail") 21 | } 22 | if Match("hello world", "jello*") { 23 | t.Fatal("fail") 24 | } 25 | if !Match("hello world", "hello?world") { 26 | t.Fatal("fail") 27 | } 28 | if Match("hello world", "jello?world") { 29 | t.Fatal("fail") 30 | } 31 | if !Match("hello world", "he*o?world") { 32 | t.Fatal("fail") 33 | } 34 | if !Match("hello world", "he*o?wor*") { 35 | t.Fatal("fail") 36 | } 37 | if !Match("hello world", "he*o?*r*") { 38 | t.Fatal("fail") 39 | } 40 | if !Match("hello*world", `hello\*world`) { 41 | t.Fatal("fail") 42 | } 43 | if !Match("he解lo*world", `he解lo\*world`) { 44 | t.Fatal("fail") 45 | } 46 | if !Match("的情况下解析一个", "*") { 47 | t.Fatal("fail") 48 | } 49 | if !Match("的情况下解析一个", "*况下*") { 50 | t.Fatal("fail") 51 | } 52 | if !Match("的情况下解析一个", "*况?*") { 53 | t.Fatal("fail") 54 | } 55 | if !Match("的情况下解析一个", "的情况?解析一个") { 56 | t.Fatal("fail") 57 | } 58 | if Match("hello world\\", "hello world\\") { 59 | t.Fatal("fail") 60 | } 61 | } 62 | 63 | // TestWildcardMatch - Tests validate the logic of wild card matching. 64 | // `WildcardMatch` supports '*' and '?' wildcards. 65 | // Sample usage: In resource matching for folder policy validation. 66 | func TestWildcardMatch(t *testing.T) { 67 | testCases := []struct { 68 | pattern string 69 | text string 70 | matched bool 71 | }{ 72 | // Test case - 1. 73 | // Test case with pattern containing key name with a prefix. Should accept the same text without a "*". 74 | { 75 | pattern: "my-folder/oo*", 76 | text: "my-folder/oo", 77 | matched: true, 78 | }, 79 | // Test case - 2. 80 | // Test case with "*" at the end of the pattern. 81 | { 82 | pattern: "my-folder/In*", 83 | text: "my-folder/India/Karnataka/", 84 | matched: true, 85 | }, 86 | // Test case - 3. 87 | // Test case with prefixes shuffled. 88 | // This should fail. 89 | { 90 | pattern: "my-folder/In*", 91 | text: "my-folder/Karnataka/India/", 92 | matched: false, 93 | }, 94 | // Test case - 4. 95 | // Test case with text expanded to the wildcards in the pattern. 96 | { 97 | pattern: "my-folder/In*/Ka*/Ban", 98 | text: "my-folder/India/Karnataka/Ban", 99 | matched: true, 100 | }, 101 | // Test case - 5. 102 | // Test case with the keyname part is repeated as prefix several times. 103 | // This is valid. 104 | { 105 | pattern: "my-folder/In*/Ka*/Ban", 106 | text: "my-folder/India/Karnataka/Ban/Ban/Ban/Ban/Ban", 107 | matched: true, 108 | }, 109 | // Test case - 6. 110 | // Test case to validate that `*` can be expanded into multiple prefixes. 111 | { 112 | pattern: "my-folder/In*/Ka*/Ban", 113 | text: "my-folder/India/Karnataka/Area1/Area2/Area3/Ban", 114 | matched: true, 115 | }, 116 | // Test case - 7. 117 | // Test case to validate that `*` can be expanded into multiple prefixes. 118 | { 119 | pattern: "my-folder/In*/Ka*/Ban", 120 | text: "my-folder/India/State1/State2/Karnataka/Area1/Area2/Area3/Ban", 121 | matched: true, 122 | }, 123 | // Test case - 8. 124 | // Test case where the keyname part of the pattern is expanded in the text. 125 | { 126 | pattern: "my-folder/In*/Ka*/Ban", 127 | text: "my-folder/India/Karnataka/Bangalore", 128 | matched: false, 129 | }, 130 | // Test case - 9. 131 | // Test case with prefixes and wildcard expanded for all "*". 132 | { 133 | pattern: "my-folder/In*/Ka*/Ban*", 134 | text: "my-folder/India/Karnataka/Bangalore", 135 | matched: true, 136 | }, 137 | // Test case - 10. 138 | // Test case with keyname part being a wildcard in the pattern. 139 | {pattern: "my-folder/*", 140 | text: "my-folder/India", 141 | matched: true, 142 | }, 143 | // Test case - 11. 144 | { 145 | pattern: "my-folder/oo*", 146 | text: "my-folder/odo", 147 | matched: false, 148 | }, 149 | 150 | // Test case with pattern containing wildcard '?'. 151 | // Test case - 12. 152 | // "my-folder?/" matches "my-folder1/", "my-folder2/", "my-folder3" etc... 153 | // doesn't match "myfolder/". 154 | { 155 | pattern: "my-folder?/abc*", 156 | text: "myfolder/abc", 157 | matched: false, 158 | }, 159 | // Test case - 13. 160 | { 161 | pattern: "my-folder?/abc*", 162 | text: "my-folder1/abc", 163 | matched: true, 164 | }, 165 | // Test case - 14. 166 | { 167 | pattern: "my-?-folder/abc*", 168 | text: "my--folder/abc", 169 | matched: false, 170 | }, 171 | // Test case - 15. 172 | { 173 | pattern: "my-?-folder/abc*", 174 | text: "my-1-folder/abc", 175 | matched: true, 176 | }, 177 | // Test case - 16. 178 | { 179 | pattern: "my-?-folder/abc*", 180 | text: "my-k-folder/abc", 181 | matched: true, 182 | }, 183 | // Test case - 17. 184 | { 185 | pattern: "my??folder/abc*", 186 | text: "myfolder/abc", 187 | matched: false, 188 | }, 189 | // Test case - 18. 190 | { 191 | pattern: "my??folder/abc*", 192 | text: "my4afolder/abc", 193 | matched: true, 194 | }, 195 | // Test case - 19. 196 | { 197 | pattern: "my-folder?abc*", 198 | text: "my-folder/abc", 199 | matched: true, 200 | }, 201 | // Test case 20-21. 202 | // '?' matches '/' too. (works with s3). 203 | // This is because the namespace is considered flat. 204 | // "abc?efg" matches both "abcdefg" and "abc/efg". 205 | { 206 | pattern: "my-folder/abc?efg", 207 | text: "my-folder/abcdefg", 208 | matched: true, 209 | }, 210 | { 211 | pattern: "my-folder/abc?efg", 212 | text: "my-folder/abc/efg", 213 | matched: true, 214 | }, 215 | // Test case - 22. 216 | { 217 | pattern: "my-folder/abc????", 218 | text: "my-folder/abc", 219 | matched: false, 220 | }, 221 | // Test case - 23. 222 | { 223 | pattern: "my-folder/abc????", 224 | text: "my-folder/abcde", 225 | matched: false, 226 | }, 227 | // Test case - 24. 228 | { 229 | pattern: "my-folder/abc????", 230 | text: "my-folder/abcdefg", 231 | matched: true, 232 | }, 233 | // Test case 25-26. 234 | // test case with no '*'. 235 | { 236 | pattern: "my-folder/abc?", 237 | text: "my-folder/abc", 238 | matched: false, 239 | }, 240 | { 241 | pattern: "my-folder/abc?", 242 | text: "my-folder/abcd", 243 | matched: true, 244 | }, 245 | { 246 | pattern: "my-folder/abc?", 247 | text: "my-folder/abcde", 248 | matched: false, 249 | }, 250 | // Test case 27. 251 | { 252 | pattern: "my-folder/mnop*?", 253 | text: "my-folder/mnop", 254 | matched: false, 255 | }, 256 | // Test case 28. 257 | { 258 | pattern: "my-folder/mnop*?", 259 | text: "my-folder/mnopqrst/mnopqr", 260 | matched: true, 261 | }, 262 | // Test case 29. 263 | { 264 | pattern: "my-folder/mnop*?", 265 | text: "my-folder/mnopqrst/mnopqrs", 266 | matched: true, 267 | }, 268 | // Test case 30. 269 | { 270 | pattern: "my-folder/mnop*?", 271 | text: "my-folder/mnop", 272 | matched: false, 273 | }, 274 | // Test case 31. 275 | { 276 | pattern: "my-folder/mnop*?", 277 | text: "my-folder/mnopq", 278 | matched: true, 279 | }, 280 | // Test case 32. 281 | { 282 | pattern: "my-folder/mnop*?", 283 | text: "my-folder/mnopqr", 284 | matched: true, 285 | }, 286 | // Test case 33. 287 | { 288 | pattern: "my-folder/mnop*?and", 289 | text: "my-folder/mnopqand", 290 | matched: true, 291 | }, 292 | // Test case 34. 293 | { 294 | pattern: "my-folder/mnop*?and", 295 | text: "my-folder/mnopand", 296 | matched: false, 297 | }, 298 | // Test case 35. 299 | { 300 | pattern: "my-folder/mnop*?and", 301 | text: "my-folder/mnopqand", 302 | matched: true, 303 | }, 304 | // Test case 36. 305 | { 306 | pattern: "my-folder/mnop*?", 307 | text: "my-folder/mn", 308 | matched: false, 309 | }, 310 | // Test case 37. 311 | { 312 | pattern: "my-folder/mnop*?", 313 | text: "my-folder/mnopqrst/mnopqrs", 314 | matched: true, 315 | }, 316 | // Test case 38. 317 | { 318 | pattern: "my-folder/mnop*??", 319 | text: "my-folder/mnopqrst", 320 | matched: true, 321 | }, 322 | // Test case 39. 323 | { 324 | pattern: "my-folder/mnop*qrst", 325 | text: "my-folder/mnopabcdegqrst", 326 | matched: true, 327 | }, 328 | // Test case 40. 329 | { 330 | pattern: "my-folder/mnop*?and", 331 | text: "my-folder/mnopqand", 332 | matched: true, 333 | }, 334 | // Test case 41. 335 | { 336 | pattern: "my-folder/mnop*?and", 337 | text: "my-folder/mnopand", 338 | matched: false, 339 | }, 340 | // Test case 42. 341 | { 342 | pattern: "my-folder/mnop*?and?", 343 | text: "my-folder/mnopqanda", 344 | matched: true, 345 | }, 346 | // Test case 43. 347 | { 348 | pattern: "my-folder/mnop*?and", 349 | text: "my-folder/mnopqanda", 350 | matched: false, 351 | }, 352 | // Test case 44. 353 | 354 | { 355 | pattern: "my-?-folder/abc*", 356 | text: "my-folder/mnopqanda", 357 | matched: false, 358 | }, 359 | } 360 | // Iterating over the test cases, call the function under test and asert the output. 361 | for i, testCase := range testCases { 362 | // println("=====", i+1, "=====") 363 | actualResult := Match(testCase.text, testCase.pattern) 364 | if testCase.matched != actualResult { 365 | t.Errorf("Test %d: Expected the result to be `%v`, but instead found it to be `%v`", i+1, testCase.matched, actualResult) 366 | } 367 | } 368 | } 369 | func TestRandomInput(t *testing.T) { 370 | rand.Seed(time.Now().UnixNano()) 371 | b1 := make([]byte, 100) 372 | b2 := make([]byte, 100) 373 | for i := 0; i < 1000000; i++ { 374 | if _, err := rand.Read(b1); err != nil { 375 | t.Fatal(err) 376 | } 377 | if _, err := rand.Read(b2); err != nil { 378 | t.Fatal(err) 379 | } 380 | Match(string(b1), string(b2)) 381 | } 382 | } 383 | func testAllowable(pattern, exmin, exmax string) error { 384 | min, max := Allowable(pattern) 385 | if min != exmin || max != exmax { 386 | return fmt.Errorf("expected '%v'/'%v', got '%v'/'%v'", 387 | exmin, exmax, min, max) 388 | } 389 | return nil 390 | } 391 | func TestAllowable(t *testing.T) { 392 | if err := testAllowable("*", "", ""); err != nil { 393 | t.Fatal(err) 394 | } 395 | if err := testAllowable("hell*", "hell", "helm"); err != nil { 396 | t.Fatal(err) 397 | } 398 | if err := testAllowable("hell?", "hell"+string(rune(0)), "hell"+string(utf8.MaxRune)); err != nil { 399 | t.Fatal(err) 400 | } 401 | if err := testAllowable("h解析ell*", "h解析ell", "h解析elm"); err != nil { 402 | t.Fatal(err) 403 | } 404 | if err := testAllowable("h解*ell*", "h解", "h觤"); err != nil { 405 | t.Fatal(err) 406 | } 407 | } 408 | 409 | func TestIsPattern(t *testing.T) { 410 | patterns := []string{ 411 | "*", "hello*", "hello*world", "*world", 412 | "?", "hello?", "hello?world", "?world", 413 | } 414 | nonPatterns := []string{ 415 | "", "hello", 416 | } 417 | for _, pattern := range patterns { 418 | if !IsPattern(pattern) { 419 | t.Fatalf("expected true") 420 | } 421 | } 422 | 423 | for _, s := range nonPatterns { 424 | if IsPattern(s) { 425 | t.Fatalf("expected false") 426 | } 427 | } 428 | } 429 | func BenchmarkAscii(t *testing.B) { 430 | for i := 0; i < t.N; i++ { 431 | if !Match("hello", "hello") { 432 | t.Fatal("fail") 433 | } 434 | } 435 | } 436 | 437 | func BenchmarkUnicode(t *testing.B) { 438 | for i := 0; i < t.N; i++ { 439 | if !Match("h情llo", "h情llo") { 440 | t.Fatal("fail") 441 | } 442 | } 443 | } 444 | 445 | func TestLotsaStars(t *testing.T) { 446 | // This tests that a pattern with lots of stars will complete quickly. 447 | var str, pat string 448 | 449 | str = `,**,,**,**,**,**,**,**,` 450 | pat = `,**********************************************{**",**,,**,**,` + 451 | `**,**,"",**,**,**,**,**,**,**,**,**,**]` 452 | Match(pat, str) 453 | 454 | str = strings.Replace(str, ",", "情", -1) 455 | pat = strings.Replace(pat, ",", "情", -1) 456 | Match(pat, str) 457 | 458 | str = strings.Repeat("hello", 100) 459 | pat = `*?*?*?*?*?*?*""` 460 | Match(str, pat) 461 | 462 | str = `*?**?**?**?**?**?***?**?**?**?**?*""` 463 | pat = `*?*?*?*?*?*?**?**?**?**?**?**?**?*""` 464 | Match(str, pat) 465 | } 466 | 467 | func TestLimit(t *testing.T) { 468 | var str, pat string 469 | str = `,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,` 470 | pat = `*,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*"*,*` 471 | _, stopped := MatchLimit(str, pat, 100) 472 | if !stopped { 473 | t.Fatal("expected true") 474 | } 475 | 476 | match, _ := MatchLimit(str, "*", 100) 477 | if !match { 478 | t.Fatal("expected true") 479 | } 480 | match, _ = MatchLimit(str, "*,*", 100) 481 | if !match { 482 | t.Fatal("expected true") 483 | } 484 | } 485 | 486 | func TestSuffix(t *testing.T) { 487 | sufmatch := func(t *testing.T, str, pat string, exstr, expat string, exok bool) { 488 | t.Helper() 489 | rstr, rpat, rok := matchTrimSuffix(str, pat, false) 490 | if rstr != exstr || rpat != expat || rok != exok { 491 | t.Fatalf( 492 | "for '%s' '%s', expected '%s' '%s' '%t', got '%s' '%s' '%t'", 493 | str, pat, exstr, expat, exok, rstr, rpat, rok) 494 | } 495 | } 496 | sufmatch(t, "hello", "*hello", "", "*", true) 497 | sufmatch(t, "jello", "*hello", "j", "*h", false) 498 | sufmatch(t, "jello", "*?ello", "", "*", true) 499 | sufmatch(t, "jello", "*\\?ello", "j", "*\\?", false) 500 | sufmatch(t, "?ello", "*\\?ello", "", "*", true) 501 | sufmatch(t, "?ello", "*\\?ello", "", "*", true) 502 | sufmatch(t, "f?ello", "*\\?ello", "f", "*", true) 503 | sufmatch(t, "f?ello", "**\\?ello", "f", "**", true) 504 | sufmatch(t, "f?el*o", "**\\?el\\*o", "f", "**", true) 505 | } 506 | 507 | func TestNoCase(t *testing.T) { 508 | if Match("Hello", "*hello") { 509 | t.Fatal() 510 | } 511 | if !MatchNoCase("Hello", "*hello") { 512 | t.Fatal() 513 | } 514 | if !MatchNoCase("hello", "*hello") { 515 | t.Fatal() 516 | } 517 | if !MatchNoCase("HElLO", "*LL*") { 518 | t.Fatal() 519 | } 520 | if !MatchNoCase("HelLO", "*eLL*") { 521 | t.Fatal() 522 | } 523 | if !MatchNoCase("HelLO", "****eLL****") { 524 | t.Fatal() 525 | } 526 | } 527 | --------------------------------------------------------------------------------