refactor simpler glob match with one wildcard only

This commit is contained in:
Fabio Bozzo
2024-09-16 18:33:36 +02:00
parent 2459f1a5c3
commit 282db65900
4 changed files with 141 additions and 124 deletions

79
capability/policy/glob.go Normal file
View 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)
}

View 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))
})
}
}

View File

@@ -181,69 +181,3 @@ func gt(order int) bool { return order == 1 }
func gte(order int) bool { return order == 0 || order == 1 } func gte(order int) bool { return order == 0 || order == 1 }
func lt(order int) bool { return order == -1 } func lt(order int) bool { return order == -1 }
func lte(order int) bool { return order == 0 || 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)
}

View File

@@ -9,7 +9,6 @@ import (
"github.com/ipld/go-ipld-prime/codec/dagjson" "github.com/ipld/go-ipld-prime/codec/dagjson"
cidlink "github.com/ipld/go-ipld-prime/linking/cid" cidlink "github.com/ipld/go-ipld-prime/linking/cid"
"github.com/ipld/go-ipld-prime/node/basicnode" "github.com/ipld/go-ipld-prime/node/basicnode"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/ucan-wg/go-ucan/capability/policy/literal" "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) { func FuzzMatch(f *testing.F) {
// Policy + Data examples // Policy + Data examples
f.Add([]byte(`[["==", ".status", "draft"]]`), []byte(`{"status": "draft"}`)) f.Add([]byte(`[["==", ".status", "draft"]]`), []byte(`{"status": "draft"}`))