refactor simpler glob match with one wildcard only
This commit is contained in:
79
capability/policy/glob.go
Normal file
79
capability/policy/glob.go
Normal file
@@ -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
capability/policy/glob_test.go
Normal file
62
capability/policy/glob_test.go
Normal file
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -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"}`))
|
||||
|
||||
Reference in New Issue
Block a user