Skip to content

Commit

Permalink
wip: simple glob match and FuzzMatch
Browse files Browse the repository at this point in the history
  • Loading branch information
fabiobozzo committed Sep 16, 2024
1 parent 37f5286 commit 2459f1a
Show file tree
Hide file tree
Showing 2 changed files with 174 additions and 1 deletion.
68 changes: 67 additions & 1 deletion capability/policy/match.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ func matchStatement(statement Statement, node ipld.Node) bool {
if err != nil {
return false
}
return s.glob.Match(v)
return globMatch(s.pattern, v)
}
case KindAll:
if s, ok := statement.(quantifier); ok {
Expand Down Expand Up @@ -181,3 +181,69 @@ 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)
}
107 changes: 107 additions & 0 deletions capability/policy/match_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ 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 @@ -490,3 +491,109 @@ func TestPolicyExamples(t *testing.T) {
require.True(t, evaluate(`["any", ".a", ["==", ".b", 2]]`, data))
})
}

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"}`))
f.Add([]byte(`[["all", ".reviewer", ["like", ".email", "*@example.com"]]]`), []byte(`{"reviewer": [{"email": "alice@example.com"}, {"email": "bob@example.com"}]}`))
f.Add([]byte(`[["any", ".tags", ["or", [["==", ".", "news"], ["==", ".", "press"]]]]]`), []byte(`{"tags": ["news", "press"]}`))
f.Add([]byte(`[["==", ".name", "Alice"]]`), []byte(`{"name": "Alice"}`))
f.Add([]byte(`[[">", ".age", 30]]`), []byte(`{"age": 31}`))
f.Add([]byte(`[["<=", ".height", 180]]`), []byte(`{"height": 170}`))
f.Add([]byte(`[["not", ["==", ".status", "inactive"]]]`), []byte(`{"status": "active"}`))
f.Add([]byte(`[["and", [["==", ".role", "admin"], [">=", ".experience", 5]]]]`), []byte(`{"role": "admin", "experience": 6}`))
f.Add([]byte(`[["or", [["==", ".department", "HR"], ["==", ".department", "Finance"]]]]`), []byte(`{"department": "HR"}`))
f.Add([]byte(`[["like", ".email", "*@company.com"]]`), []byte(`{"email": "user@company.com"}`))
f.Add([]byte(`[["all", ".projects", [">", ".budget", 10000]]]`), []byte(`{"projects": [{"budget": 15000}, {"budget": 8000}]}`))
f.Add([]byte(`[["any", ".skills", ["==", ".", "Go"]]]`), []byte(`{"skills": ["Go", "Python", "JavaScript"]}`))
f.Add(
[]byte(`[["and", [
["==", ".name", "Bob"],
["or", [[">", ".age", 25],["==", ".status", "active"]]],
["all", ".tasks", ["==", ".completed", true]]
]]]`),
[]byte(`{
"name": "Bob",
"age": 26,
"status": "active",
"tasks": [{"completed": true}, {"completed": true}, {"completed": false}]
}`),
)

f.Fuzz(func(t *testing.T, policyBytes []byte, dataBytes []byte) {
policyNode, err := ipld.Decode(policyBytes, dagjson.Decode)
if err != nil {
t.Skip()
}

dataNode, err := ipld.Decode(dataBytes, dagjson.Decode)
if err != nil {
t.Skip()
}

// policy node -> policy object
policy, err := FromIPLD(policyNode)
if err != nil {
t.Skip()
}

Match(policy, dataNode)
})
}

0 comments on commit 2459f1a

Please sign in to comment.