first commit

This commit is contained in:
Josh Baker 2016-08-30 07:19:45 -07:00
commit 970f69b4db
5 changed files with 556 additions and 0 deletions

1
.travis.yml Normal file
View File

@ -0,0 +1 @@
language: go

20
LICENSE Normal file
View File

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2016 Josh Baker
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

31
README.md Normal file
View File

@ -0,0 +1,31 @@
Match
=====
<a href="https://travis-ci.org/tidwall/match"><img src="https://img.shields.io/travis/tidwall/match.svg?style=flat-square" alt="Build Status"></a>
<a href="https://godoc.org/github.com/tidwall/match"><img src="https://img.shields.io/badge/api-reference-blue.svg?style=flat-square" alt="GoDoc"></a>
Match is a very simple pattern matcher where '*' matches on any
number characters and '?' matches on any one character.
Installing
----------
```
go get -u github.com/tidwall/match
```
Example
-------
```go
match.Match("hello", "*llo")
match.Match("jello", "?ello")
match.Match("hello", "h*o")
```
Contact
-------
Josh Baker [@tidwall](http://twitter.com/tidwall)
License
-------
Redcon source code is available under the MIT [License](/LICENSE).

120
match.go Normal file
View File

@ -0,0 +1,120 @@
// Match provides a simple pattern matcher with unicode support.
package match
import "unicode/utf8"
// Match returns true if str matches pattern. This is a very
// simple wildcard match where '*' matches on any number characters
// and '?' matches on any one character.
// pattern:
// { term }
// term:
// '*' matches any sequence of non-Separator characters
// '?' matches any single non-Separator character
// c matches character c (c != '*', '?', '\\')
// '\\' c matches character c
//
func Match(str, pattern string) bool {
if pattern == "*" {
return true
}
return deepMatch(str, pattern)
}
func deepMatch(str, pattern string) bool {
for len(pattern) > 0 {
if pattern[0] > 0x7f {
return deepMatchRune(str, pattern)
}
switch pattern[0] {
default:
if len(str) == 0 {
return false
}
if str[0] > 0x7f {
return deepMatchRune(str, pattern)
}
if str[0] != pattern[0] {
return false
}
case '?':
if len(str) == 0 {
return false
}
case '*':
return deepMatch(str, pattern[1:]) ||
(len(str) > 0 && deepMatch(str[1:], pattern))
}
str = str[1:]
pattern = pattern[1:]
}
return len(str) == 0 && len(pattern) == 0
}
func deepMatchRune(str, pattern string) bool {
var sr, pr rune
var srsz, prsz int
// read the first rune ahead of time
if len(str) > 0 {
if str[0] > 0x7f {
sr, srsz = utf8.DecodeRuneInString(str)
} else {
sr, srsz = rune(str[0]), 1
}
} else {
sr, srsz = utf8.RuneError, 0
}
if len(pattern) > 0 {
if pattern[0] > 0x7f {
pr, prsz = utf8.DecodeRuneInString(pattern)
} else {
pr, prsz = rune(pattern[0]), 1
}
} else {
pr, prsz = utf8.RuneError, 0
}
// done reading
for pr != utf8.RuneError {
switch pr {
default:
if srsz == utf8.RuneError {
return false
}
if sr != pr {
return false
}
case '?':
if srsz == utf8.RuneError {
return false
}
case '*':
return deepMatchRune(str, pattern[prsz:]) ||
(srsz > 0 && deepMatchRune(str[srsz:], pattern))
}
str = str[srsz:]
pattern = pattern[prsz:]
// read the next runes
if len(str) > 0 {
if str[0] > 0x7f {
sr, srsz = utf8.DecodeRuneInString(str)
} else {
sr, srsz = rune(str[0]), 1
}
} else {
sr, srsz = utf8.RuneError, 0
}
if len(pattern) > 0 {
if pattern[0] > 0x7f {
pr, prsz = utf8.DecodeRuneInString(pattern)
} else {
pr, prsz = rune(pattern[0]), 1
}
} else {
pr, prsz = utf8.RuneError, 0
}
// done reading
}
return srsz == 0 && prsz == 0
}

384
match_test.go Normal file
View File

@ -0,0 +1,384 @@
package match
import (
"math/rand"
"testing"
"time"
)
func TestMatch(t *testing.T) {
if !Match("hello world", "hello world") {
t.Fatal("fail")
}
if Match("hello world", "jello world") {
t.Fatal("fail")
}
if !Match("hello world", "hello*") {
t.Fatal("fail")
}
if Match("hello world", "jello*") {
t.Fatal("fail")
}
if !Match("hello world", "hello?world") {
t.Fatal("fail")
}
if Match("hello world", "jello?world") {
t.Fatal("fail")
}
if !Match("hello world", "he*o?world") {
t.Fatal("fail")
}
if !Match("hello world", "he*o?wor*") {
t.Fatal("fail")
}
if !Match("hello world", "he*o?*r*") {
t.Fatal("fail")
}
if !Match("的情况下解析一个", "*") {
t.Fatal("fail")
}
if !Match("的情况下解析一个", "*况下*") {
t.Fatal("fail")
}
if !Match("的情况下解析一个", "*况?*") {
t.Fatal("fail")
}
if !Match("的情况下解析一个", "的情况?解析一个") {
t.Fatal("fail")
}
}
// TestWildcardMatch - Tests validate the logic of wild card matching.
// `WildcardMatch` supports '*' and '?' wildcards.
// Sample usage: In resource matching for folder policy validation.
func TestWildcardMatch(t *testing.T) {
testCases := []struct {
pattern string
text string
matched bool
}{
// Test case - 1.
// Test case with pattern containing key name with a prefix. Should accept the same text without a "*".
{
pattern: "my-folder/oo*",
text: "my-folder/oo",
matched: true,
},
// Test case - 2.
// Test case with "*" at the end of the pattern.
{
pattern: "my-folder/In*",
text: "my-folder/India/Karnataka/",
matched: true,
},
// Test case - 3.
// Test case with prefixes shuffled.
// This should fail.
{
pattern: "my-folder/In*",
text: "my-folder/Karnataka/India/",
matched: false,
},
// Test case - 4.
// Test case with text expanded to the wildcards in the pattern.
{
pattern: "my-folder/In*/Ka*/Ban",
text: "my-folder/India/Karnataka/Ban",
matched: true,
},
// Test case - 5.
// Test case with the keyname part is repeated as prefix several times.
// This is valid.
{
pattern: "my-folder/In*/Ka*/Ban",
text: "my-folder/India/Karnataka/Ban/Ban/Ban/Ban/Ban",
matched: true,
},
// Test case - 6.
// Test case to validate that `*` can be expanded into multiple prefixes.
{
pattern: "my-folder/In*/Ka*/Ban",
text: "my-folder/India/Karnataka/Area1/Area2/Area3/Ban",
matched: true,
},
// Test case - 7.
// Test case to validate that `*` can be expanded into multiple prefixes.
{
pattern: "my-folder/In*/Ka*/Ban",
text: "my-folder/India/State1/State2/Karnataka/Area1/Area2/Area3/Ban",
matched: true,
},
// Test case - 8.
// Test case where the keyname part of the pattern is expanded in the text.
{
pattern: "my-folder/In*/Ka*/Ban",
text: "my-folder/India/Karnataka/Bangalore",
matched: false,
},
// Test case - 9.
// Test case with prefixes and wildcard expanded for all "*".
{
pattern: "my-folder/In*/Ka*/Ban*",
text: "my-folder/India/Karnataka/Bangalore",
matched: true,
},
// Test case - 10.
// Test case with keyname part being a wildcard in the pattern.
{pattern: "my-folder/*",
text: "my-folder/India",
matched: true,
},
// Test case - 11.
{
pattern: "my-folder/oo*",
text: "my-folder/odo",
matched: false,
},
// Test case with pattern containing wildcard '?'.
// Test case - 12.
// "my-folder?/" matches "my-folder1/", "my-folder2/", "my-folder3" etc...
// doesn't match "myfolder/".
{
pattern: "my-folder?/abc*",
text: "myfolder/abc",
matched: false,
},
// Test case - 13.
{
pattern: "my-folder?/abc*",
text: "my-folder1/abc",
matched: true,
},
// Test case - 14.
{
pattern: "my-?-folder/abc*",
text: "my--folder/abc",
matched: false,
},
// Test case - 15.
{
pattern: "my-?-folder/abc*",
text: "my-1-folder/abc",
matched: true,
},
// Test case - 16.
{
pattern: "my-?-folder/abc*",
text: "my-k-folder/abc",
matched: true,
},
// Test case - 17.
{
pattern: "my??folder/abc*",
text: "myfolder/abc",
matched: false,
},
// Test case - 18.
{
pattern: "my??folder/abc*",
text: "my4afolder/abc",
matched: true,
},
// Test case - 19.
{
pattern: "my-folder?abc*",
text: "my-folder/abc",
matched: true,
},
// Test case 20-21.
// '?' matches '/' too. (works with s3).
// This is because the namespace is considered flat.
// "abc?efg" matches both "abcdefg" and "abc/efg".
{
pattern: "my-folder/abc?efg",
text: "my-folder/abcdefg",
matched: true,
},
{
pattern: "my-folder/abc?efg",
text: "my-folder/abc/efg",
matched: true,
},
// Test case - 22.
{
pattern: "my-folder/abc????",
text: "my-folder/abc",
matched: false,
},
// Test case - 23.
{
pattern: "my-folder/abc????",
text: "my-folder/abcde",
matched: false,
},
// Test case - 24.
{
pattern: "my-folder/abc????",
text: "my-folder/abcdefg",
matched: true,
},
// Test case 25-26.
// test case with no '*'.
{
pattern: "my-folder/abc?",
text: "my-folder/abc",
matched: false,
},
{
pattern: "my-folder/abc?",
text: "my-folder/abcd",
matched: true,
},
{
pattern: "my-folder/abc?",
text: "my-folder/abcde",
matched: false,
},
// Test case 27.
{
pattern: "my-folder/mnop*?",
text: "my-folder/mnop",
matched: false,
},
// Test case 28.
{
pattern: "my-folder/mnop*?",
text: "my-folder/mnopqrst/mnopqr",
matched: true,
},
// Test case 29.
{
pattern: "my-folder/mnop*?",
text: "my-folder/mnopqrst/mnopqrs",
matched: true,
},
// Test case 30.
{
pattern: "my-folder/mnop*?",
text: "my-folder/mnop",
matched: false,
},
// Test case 31.
{
pattern: "my-folder/mnop*?",
text: "my-folder/mnopq",
matched: true,
},
// Test case 32.
{
pattern: "my-folder/mnop*?",
text: "my-folder/mnopqr",
matched: true,
},
// Test case 33.
{
pattern: "my-folder/mnop*?and",
text: "my-folder/mnopqand",
matched: true,
},
// Test case 34.
{
pattern: "my-folder/mnop*?and",
text: "my-folder/mnopand",
matched: false,
},
// Test case 35.
{
pattern: "my-folder/mnop*?and",
text: "my-folder/mnopqand",
matched: true,
},
// Test case 36.
{
pattern: "my-folder/mnop*?",
text: "my-folder/mn",
matched: false,
},
// Test case 37.
{
pattern: "my-folder/mnop*?",
text: "my-folder/mnopqrst/mnopqrs",
matched: true,
},
// Test case 38.
{
pattern: "my-folder/mnop*??",
text: "my-folder/mnopqrst",
matched: true,
},
// Test case 39.
{
pattern: "my-folder/mnop*qrst",
text: "my-folder/mnopabcdegqrst",
matched: true,
},
// Test case 40.
{
pattern: "my-folder/mnop*?and",
text: "my-folder/mnopqand",
matched: true,
},
// Test case 41.
{
pattern: "my-folder/mnop*?and",
text: "my-folder/mnopand",
matched: false,
},
// Test case 42.
{
pattern: "my-folder/mnop*?and?",
text: "my-folder/mnopqanda",
matched: true,
},
// Test case 43.
{
pattern: "my-folder/mnop*?and",
text: "my-folder/mnopqanda",
matched: false,
},
// Test case 44.
{
pattern: "my-?-folder/abc*",
text: "my-folder/mnopqanda",
matched: false,
},
}
// Iterating over the test cases, call the function under test and asert the output.
for i, testCase := range testCases {
actualResult := Match(testCase.text, testCase.pattern)
if testCase.matched != actualResult {
t.Errorf("Test %d: Expected the result to be `%v`, but instead found it to be `%v`", i+1, testCase.matched, actualResult)
}
}
}
func TestRandomInput(t *testing.T) {
rand.Seed(time.Now().UnixNano())
b1 := make([]byte, 100)
b2 := make([]byte, 100)
for i := 0; i < 1000000; i++ {
if _, err := rand.Read(b1); err != nil {
t.Fatal(err)
}
if _, err := rand.Read(b2); err != nil {
t.Fatal(err)
}
Match(string(b1), string(b2))
}
}
func BenchmarkAscii(t *testing.B) {
for i := 0; i < t.N; i++ {
if !Match("hello", "hello") {
t.Fatal("fail")
}
}
}
func BenchmarkUnicode(t *testing.B) {
for i := 0; i < t.N; i++ {
if !Match("h情llo", "h情llo") {
t.Fatal("fail")
}
}
}