diff --git a/capability/policy/glob.go b/capability/policy/glob.go new file mode 100644 index 0000000..1c57b67 --- /dev/null +++ b/capability/policy/glob.go @@ -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) +} diff --git a/capability/policy/glob_test.go b/capability/policy/glob_test.go new file mode 100644 index 0000000..31451a0 --- /dev/null +++ b/capability/policy/glob_test.go @@ -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)) + }) + } +} diff --git a/capability/policy/match.go b/capability/policy/match.go index 66e6e18..0564113 100644 --- a/capability/policy/match.go +++ b/capability/policy/match.go @@ -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) -} diff --git a/capability/policy/match_test.go b/capability/policy/match_test.go index 6587265..4f7dc9b 100644 --- a/capability/policy/match_test.go +++ b/capability/policy/match_test.go @@ -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"}`))