Skip to content

Commit

Permalink
refactor simpler glob match with one wildcard only
Browse files Browse the repository at this point in the history
  • Loading branch information
fabiobozzo committed Sep 16, 2024
1 parent 2459f1a commit 282db65
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 124 deletions.
79 changes: 79 additions & 0 deletions capability/policy/glob.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package policy

// validateGlobPattern ensures the pattern conforms to the spec: only '*' and escaped '\*' are allowed.
func validateGlobPattern(pattern string) bool {
for i := 0; i < len(pattern); i++ {
if pattern[i] == '*' {
continue
}
if pattern[i] == '\\' && i+1 < len(pattern) && pattern[i+1] == '*' {
i++ // skip the escaped '*'
continue
}
if pattern[i] == '\\' && i+1 < len(pattern) {
i++ // skip the escaped character
continue
}
if pattern[i] == '\\' {
return false // invalid escape sequence
}
}

return true
}

// globMatch matches a string against a pattern with '*' wildcards, handling escaped '\*' literals.
func globMatch(pattern, str string) bool {
if !validateGlobPattern(pattern) {
return false
}

var i, j int // i is the index for the pattern, j is the index for the string
for i < len(pattern) && j < len(str) {
switch pattern[i] {
case '*':
// Skip consecutive '*' characters
for i < len(pattern) && pattern[i] == '*' {
i++
}
if i == len(pattern) {
return true
}
// Match the rest of the pattern
for j < len(str) {
if globMatch(pattern[i:], str[j:]) {
return true
}
j++
}
return false
case '\\':
// Handle escaped '*'
i++
if i < len(pattern) && pattern[i] == '*' {
if str[j] != '*' {
return false
}
i++
j++
} else {
if i >= len(pattern) || pattern[i] != str[j] {
return false
}
i++
j++
}
default:
if pattern[i] != str[j] {
return false
}
i++
j++
}
}
// Check for remaining characters in pattern
for i < len(pattern) && pattern[i] == '*' {
i++
}
return i == len(pattern) && j == len(str)
}
62 changes: 62 additions & 0 deletions capability/policy/glob_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package policy

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestSimpleGlobMatch(t *testing.T) {
tests := []struct {
pattern string
str string
matches bool
}{
// Basic matching
{"*", "anything", true},
{"a*", "abc", true},
{"*c", "abc", true},
{"a*c", "abc", true},
{"a*c", "abxc", true},
{"a*c", "ac", true},
{"a*c", "a", false},
{"a*c", "ab", false},

// Escaped characters
{"a\\*c", "a*c", true},
{"a\\*c", "abc", false},

// Mixed wildcards and literals
{"a*b*c", "abc", true},
{"a*b*c", "aXbYc", true},
{"a*b*c", "aXbY", false},
{"a*b*c", "abYc", true},
{"a*b*c", "aXbc", true},
{"a*b*c", "aXbYcZ", false},

// Edge cases
{"", "", true},
{"", "a", false},
{"*", "", true},
{"*", "a", true},
{"\\*", "*", true},
{"\\*", "a", false},

// Specified test cases
{"Alice\\*, Bob*, Carol.", "Alice*, Bob, Carol.", true},
{"Alice\\*, Bob*, Carol.", "Alice*, Bob, Dan, Erin, Carol.", true},
{"Alice\\*, Bob*, Carol.", "Alice*, Bob , Carol.", true},
{"Alice\\*, Bob*, Carol.", "Alice*, Bob*, Carol.", true},
{"Alice\\*, Bob*, Carol.", "Alice*, Bob, Carol", false},
{"Alice\\*, Bob*, Carol.", "Alice*, Bob*, Carol!", false},
{"Alice\\*, Bob*, Carol.", "Alice, Bob, Carol.", false},
{"Alice\\*, Bob*, Carol.", "Alice Cooper, Bob, Carol.", false},
{"Alice\\*, Bob*, Carol.", " Alice*, Bob, Carol. ", false},
}

for _, tt := range tests {
t.Run(tt.pattern+"_"+tt.str, func(t *testing.T) {
assert.Equal(t, tt.matches, globMatch(tt.pattern, tt.str))
})
}
}
66 changes: 0 additions & 66 deletions capability/policy/match.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,69 +181,3 @@ func gt(order int) bool { return order == 1 }
func gte(order int) bool { return order == 0 || order == 1 }
func lt(order int) bool { return order == -1 }
func lte(order int) bool { return order == 0 || order == -1 }

// globMatch matches a string against a pattern with '*' and '?' wildcards, handling escape sequences.
func globMatch(pattern, str string) bool {
var i, j int
for i < len(pattern) && j < len(str) {
switch pattern[i] {
case '*':
// skip consecutive '*' characters
for i < len(pattern) && pattern[i] == '*' {
i++
}
if i == len(pattern) {
return true
}

// match the rest of the pattern
for j < len(str) {
if globMatch(pattern[i:], str[j:]) {
return true
}
j++
}

return false
case '?':
// match any single character
i++
j++
case '\\':
// Handle escape sequences
i++
if i < len(pattern) && pattern[i] == '*' {
if str[j] != '*' {
return false
}
i++
j++
} else if i < len(pattern) && pattern[i] == '?' {
if str[j] != '?' {
return false
}
i++
j++
} else {
if i >= len(pattern) || pattern[i] != str[j] {
return false
}
i++
j++
}
default:
if pattern[i] != str[j] {
return false
}
i++
j++
}
}

// check for remaining characters in pattern
for i < len(pattern) && pattern[i] == '*' {
i++
}

return i == len(pattern) && j == len(str)
}
58 changes: 0 additions & 58 deletions capability/policy/match_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"github.com/ipld/go-ipld-prime/codec/dagjson"
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
"github.com/ipld/go-ipld-prime/node/basicnode"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/ucan-wg/go-ucan/capability/policy/literal"
Expand Down Expand Up @@ -492,63 +491,6 @@ func TestPolicyExamples(t *testing.T) {
})
}

func Test_globMatch(t *testing.T) {

tests := []struct {
pattern string
str string
matches bool
}{
// Basic matching
{"*", "anything", true},
{"?", "a", true},
{"?", "ab", false},
{"a*", "abc", true},
{"*c", "abc", true},
{"a*c", "abc", true},
{"a*c", "abxc", true},
{"a*c", "ac", true},
{"a*c", "a", false},
{"a*c", "ab", false},
{"a?c", "abc", true},
{"a?c", "ac", false},
{"a?c", "abxc", false},

// Escaped characters
{"a\\*c", "a*c", true},
{"a\\*c", "abc", false},
{"a\\?c", "a?c", true},
{"a\\?c", "abc", false},

// Mixed wildcards and literals
{"a*b*c", "abc", true},
{"a*b*c", "aXbYc", true},
{"a*b*c", "aXbY", false},
{"a*b*c", "abYc", true},
{"a*b*c", "aXbc", true},
{"a*b*c", "aXbYcZ", false},

// Edge cases
{"", "", true},
{"", "a", false},
{"*", "", true},
{"*", "a", true},
{"?", "", false},
{"?", "a", true},
{"?", "ab", false},
{"\\*", "*", true},
{"\\*", "a", false},
{"\\?", "?", true},
{"\\?", "a", false},
}

for _, tt := range tests {
t.Run(tt.pattern+"_"+tt.str, func(t *testing.T) {
assert.Equal(t, tt.matches, globMatch(tt.pattern, tt.str))
})
}
}

func FuzzMatch(f *testing.F) {
// Policy + Data examples
f.Add([]byte(`[["==", ".status", "draft"]]`), []byte(`{"status": "draft"}`))
Expand Down

0 comments on commit 282db65

Please sign in to comment.