From 2459f1a5c3054fea4125f13136e4c04f9e2a1b35 Mon Sep 17 00:00:00 2001 From: Fabio Bozzo Date: Mon, 16 Sep 2024 18:23:14 +0200 Subject: [PATCH 1/9] wip: simple glob match and FuzzMatch --- capability/policy/match.go | 68 +++++++++++++++++++- capability/policy/match_test.go | 107 ++++++++++++++++++++++++++++++++ 2 files changed, 174 insertions(+), 1 deletion(-) diff --git a/capability/policy/match.go b/capability/policy/match.go index 14cdc94..66e6e18 100644 --- a/capability/policy/match.go +++ b/capability/policy/match.go @@ -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 { @@ -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) +} diff --git a/capability/policy/match_test.go b/capability/policy/match_test.go index b18aee9..6587265 100644 --- a/capability/policy/match_test.go +++ b/capability/policy/match_test.go @@ -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" @@ -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) + }) +} From 282db659009c0f863fc3ba141154a1c78e3f8e13 Mon Sep 17 00:00:00 2001 From: Fabio Bozzo Date: Mon, 16 Sep 2024 18:33:36 +0200 Subject: [PATCH 2/9] refactor simpler glob match with one wildcard only --- capability/policy/glob.go | 79 +++++++++++++++++++++++++++++++++ capability/policy/glob_test.go | 62 ++++++++++++++++++++++++++ capability/policy/match.go | 66 --------------------------- capability/policy/match_test.go | 58 ------------------------ 4 files changed, 141 insertions(+), 124 deletions(-) create mode 100644 capability/policy/glob.go create mode 100644 capability/policy/glob_test.go 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"}`)) From d4d45149714fb9a6eaf33ba29d73b10762c53d97 Mon Sep 17 00:00:00 2001 From: Fabio Bozzo Date: Mon, 16 Sep 2024 18:52:01 +0200 Subject: [PATCH 3/9] few comments --- capability/policy/glob.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/capability/policy/glob.go b/capability/policy/glob.go index 1c57b67..23fe8f2 100644 --- a/capability/policy/glob.go +++ b/capability/policy/glob.go @@ -28,27 +28,31 @@ func globMatch(pattern, str string) bool { return false } - var i, j int // i is the index for the pattern, j is the index for the string + // i is the index for the pattern + // j is the index for the string + var i, j int for i < len(pattern) && j < len(str) { switch pattern[i] { case '*': - // Skip consecutive '*' characters + // skip consecutive '*' characters for i < len(pattern) && pattern[i] == '*' { i++ } if i == len(pattern) { return true } - // Match the rest of the pattern + + // match the rest of the pattern for j < len(str) { if globMatch(pattern[i:], str[j:]) { return true } j++ } + return false case '\\': - // Handle escaped '*' + // handle escaped '*' i++ if i < len(pattern) && pattern[i] == '*' { if str[j] != '*' { @@ -71,9 +75,11 @@ func globMatch(pattern, str string) bool { j++ } } - // Check for remaining characters in pattern + + // check for remaining characters in pattern for i < len(pattern) && pattern[i] == '*' { i++ } + return i == len(pattern) && j == len(str) } From c960481a10103a1f40889c144fe980777dc78080 Mon Sep 17 00:00:00 2001 From: Fabio Bozzo Date: Mon, 16 Sep 2024 18:55:04 +0200 Subject: [PATCH 4/9] remove gobwas dep --- capability/policy/policy.go | 8 +------- go.mod | 1 - go.sum | 2 -- 3 files changed, 1 insertion(+), 10 deletions(-) diff --git a/capability/policy/policy.go b/capability/policy/policy.go index 73bdb12..55072b6 100644 --- a/capability/policy/policy.go +++ b/capability/policy/policy.go @@ -3,7 +3,6 @@ package policy // https://github.com/ucan-wg/delegation/blob/4094d5878b58f5d35055a3b93fccda0b8329ebae/README.md#policy import ( - "github.com/gobwas/glob" "github.com/ipld/go-ipld-prime" "github.com/ucan-wg/go-ucan/capability/policy/selector" @@ -91,7 +90,6 @@ func Or(stmts ...Statement) Statement { type wildcard struct { selector selector.Selector pattern string - glob glob.Glob // not serialized } func (n wildcard) Kind() string { @@ -99,11 +97,7 @@ func (n wildcard) Kind() string { } func Like(selector selector.Selector, pattern string) (Statement, error) { - g, err := glob.Compile(pattern) - if err != nil { - return nil, err - } - return wildcard{selector: selector, pattern: pattern, glob: g}, nil + return wildcard{selector: selector, pattern: pattern}, nil } type quantifier struct { diff --git a/go.mod b/go.mod index 21ee27e..e7db82f 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.21 toolchain go1.22.1 require ( - github.com/gobwas/glob v0.2.3 github.com/ipfs/go-cid v0.4.1 github.com/ipld/go-ipld-prime v0.21.0 github.com/libp2p/go-libp2p v0.36.2 diff --git a/go.sum b/go.sum index 2b9f555..1697b33 100644 --- a/go.sum +++ b/go.sum @@ -5,8 +5,6 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= -github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= -github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= From 53ef97231df3f32a6b7d908a5e13e1ad4e049cac Mon Sep 17 00:00:00 2001 From: Fabio Bozzo Date: Tue, 17 Sep 2024 13:18:12 +0200 Subject: [PATCH 5/9] iterative glob.go and limit FuzzMatch input size --- capability/policy/glob.go | 64 +++++++++++++-------------------- capability/policy/match_test.go | 6 ++++ 2 files changed, 31 insertions(+), 39 deletions(-) diff --git a/capability/policy/glob.go b/capability/policy/glob.go index 23fe8f2..5ab2f9b 100644 --- a/capability/policy/glob.go +++ b/capability/policy/glob.go @@ -22,7 +22,7 @@ func validateGlobPattern(pattern string) bool { return true } -// globMatch matches a string against a pattern with '*' wildcards, handling escaped '\*' literals. +// globMatch matches a string against a pattern with * wildcards, handling escaped '\*' literals. func globMatch(pattern, str string) bool { if !validateGlobPattern(pattern) { return false @@ -31,55 +31,41 @@ func globMatch(pattern, str string) bool { // i is the index for the pattern // j is the index for the string 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++ - } + // starIdx keeps track of the position of the last * in the pattern. + // matchIdx keeps track of the position in the string where the last * matched. + var starIdx, matchIdx int = -1, -1 - 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 - } + for j < len(str) { + if i < len(pattern) && (pattern[i] == str[j] || pattern[i] == '\\' && i+1 < len(pattern) && pattern[i+1] == str[j]) { + // characters match or if there's an escaped character that matches + if pattern[i] == '\\' { + // skip the escape character i++ - j++ - } - default: - if pattern[i] != str[j] { - return false } i++ j++ + } else if i < len(pattern) && pattern[i] == '*' { + // there's a * wildcard in the pattern + starIdx = i + matchIdx = j + i++ + } else if starIdx != -1 { + // there's a previous * wildcard, backtrack + i = starIdx + 1 + matchIdx++ + j = matchIdx + } else { + // no match found + return false } } - // check for remaining characters in pattern + // check for remaining characters in the pattern for i < len(pattern) && pattern[i] == '*' { i++ } - return i == len(pattern) && j == len(str) + // the entire pattern is processed, it's a match + return i == len(pattern) } diff --git a/capability/policy/match_test.go b/capability/policy/match_test.go index 4f7dc9b..d7d9360 100644 --- a/capability/policy/match_test.go +++ b/capability/policy/match_test.go @@ -491,6 +491,8 @@ func TestPolicyExamples(t *testing.T) { }) } +const maxFuzzInputLength = 128 + func FuzzMatch(f *testing.F) { // Policy + Data examples f.Add([]byte(`[["==", ".status", "draft"]]`), []byte(`{"status": "draft"}`)) @@ -520,6 +522,10 @@ func FuzzMatch(f *testing.F) { ) f.Fuzz(func(t *testing.T, policyBytes []byte, dataBytes []byte) { + if len(policyBytes) > maxFuzzInputLength || len(dataBytes) > maxFuzzInputLength { + t.Skip() + } + policyNode, err := ipld.Decode(policyBytes, dagjson.Decode) if err != nil { t.Skip() From 94a0d4d56ef810f4a8ba81f69e974d85c0dce640 Mon Sep 17 00:00:00 2001 From: Fabio Bozzo Date: Tue, 17 Sep 2024 13:54:42 +0200 Subject: [PATCH 6/9] remove max input size check from fuzz test --- capability/policy/match_test.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/capability/policy/match_test.go b/capability/policy/match_test.go index d7d9360..4f7dc9b 100644 --- a/capability/policy/match_test.go +++ b/capability/policy/match_test.go @@ -491,8 +491,6 @@ func TestPolicyExamples(t *testing.T) { }) } -const maxFuzzInputLength = 128 - func FuzzMatch(f *testing.F) { // Policy + Data examples f.Add([]byte(`[["==", ".status", "draft"]]`), []byte(`{"status": "draft"}`)) @@ -522,10 +520,6 @@ func FuzzMatch(f *testing.F) { ) f.Fuzz(func(t *testing.T, policyBytes []byte, dataBytes []byte) { - if len(policyBytes) > maxFuzzInputLength || len(dataBytes) > maxFuzzInputLength { - t.Skip() - } - policyNode, err := ipld.Decode(policyBytes, dagjson.Decode) if err != nil { t.Skip() From 16ba4b392d5ab1861e264ecb2eb185602a50dbae Mon Sep 17 00:00:00 2001 From: Fabio Bozzo Date: Tue, 17 Sep 2024 14:15:36 +0200 Subject: [PATCH 7/9] apply pr feedback --- capability/policy/ipld.go | 2 +- capability/policy/match.go | 2 +- capability/policy/policy.go | 25 +++++++++++++++++++++++-- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/capability/policy/ipld.go b/capability/policy/ipld.go index 36f1519..2aea8d8 100644 --- a/capability/policy/ipld.go +++ b/capability/policy/ipld.go @@ -232,7 +232,7 @@ func statementToIPLD(statement Statement) (datamodel.Node, error) { if err != nil { return nil, err } - err = listBuilder.AssembleValue().AssignString(statement.pattern) + err = listBuilder.AssembleValue().AssignString(string(statement.pattern)) if err != nil { return nil, err } diff --git a/capability/policy/match.go b/capability/policy/match.go index 0564113..ee65967 100644 --- a/capability/policy/match.go +++ b/capability/policy/match.go @@ -112,7 +112,7 @@ func matchStatement(statement Statement, node ipld.Node) bool { if err != nil { return false } - return globMatch(s.pattern, v) + return s.pattern.match(v) } case KindAll: if s, ok := statement.(quantifier); ok { diff --git a/capability/policy/policy.go b/capability/policy/policy.go index 55072b6..157be8f 100644 --- a/capability/policy/policy.go +++ b/capability/policy/policy.go @@ -3,6 +3,8 @@ package policy // https://github.com/ucan-wg/delegation/blob/4094d5878b58f5d35055a3b93fccda0b8329ebae/README.md#policy import ( + "errors" + "github.com/ipld/go-ipld-prime" "github.com/ucan-wg/go-ucan/capability/policy/selector" @@ -87,9 +89,23 @@ func Or(stmts ...Statement) Statement { return connective{kind: KindOr, statements: stmts} } +type wildcardPattern string + +func parseWildcardPattern(pattern string) (wildcardPattern, error) { + if !validateGlobPattern(pattern) { + return "", errors.New("invalid wildcard pattern") + } + + return wildcardPattern(pattern), nil +} + +func (wp wildcardPattern) match(str string) bool { + return globMatch(string(wp), str) +} + type wildcard struct { selector selector.Selector - pattern string + pattern wildcardPattern } func (n wildcard) Kind() string { @@ -97,7 +113,12 @@ func (n wildcard) Kind() string { } func Like(selector selector.Selector, pattern string) (Statement, error) { - return wildcard{selector: selector, pattern: pattern}, nil + parsedPattern, err := parseWildcardPattern(pattern) + if err != nil { + return nil, err + } + + return wildcard{selector: selector, pattern: parsedPattern}, nil } type quantifier struct { From a19d3505fed8b396404a89adbd97469e4e6fb2ef Mon Sep 17 00:00:00 2001 From: Fabio Bozzo Date: Wed, 18 Sep 2024 11:12:46 +0200 Subject: [PATCH 8/9] validateGlobPattern is now responsibility of the caller --- capability/policy/glob.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/capability/policy/glob.go b/capability/policy/glob.go index 5ab2f9b..34d42a0 100644 --- a/capability/policy/glob.go +++ b/capability/policy/glob.go @@ -24,10 +24,6 @@ func validateGlobPattern(pattern string) bool { // globMatch matches a string against a pattern with * wildcards, handling escaped '\*' literals. func globMatch(pattern, str string) bool { - if !validateGlobPattern(pattern) { - return false - } - // i is the index for the pattern // j is the index for the string var i, j int From ac73cae3ecb14218c5340f6f515bfe7a1a62251b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Wed, 18 Sep 2024 11:24:37 +0200 Subject: [PATCH 9/9] glob: a bit of reshaping, and a benchmark --- capability/policy/glob.go | 24 ++++++++++++++++++------ capability/policy/glob_test.go | 15 +++++++++++++-- capability/policy/match.go | 2 +- capability/policy/policy.go | 22 +++------------------- 4 files changed, 35 insertions(+), 28 deletions(-) diff --git a/capability/policy/glob.go b/capability/policy/glob.go index 34d42a0..56cb505 100644 --- a/capability/policy/glob.go +++ b/capability/policy/glob.go @@ -1,7 +1,11 @@ package policy -// validateGlobPattern ensures the pattern conforms to the spec: only '*' and escaped '\*' are allowed. -func validateGlobPattern(pattern string) bool { +import "fmt" + +type glob string + +// parseGlob ensures that the pattern conforms to the spec: only '*' and escaped '\*' are allowed. +func parseGlob(pattern string) (glob, error) { for i := 0; i < len(pattern); i++ { if pattern[i] == '*' { continue @@ -15,15 +19,23 @@ func validateGlobPattern(pattern string) bool { continue } if pattern[i] == '\\' { - return false // invalid escape sequence + return "", fmt.Errorf("invalid escape sequence") } } - return true + return glob(pattern), nil +} + +func mustParseGlob(pattern string) glob { + g, err := parseGlob(pattern) + if err != nil { + panic(err) + } + return g } -// globMatch matches a string against a pattern with * wildcards, handling escaped '\*' literals. -func globMatch(pattern, str string) bool { +// Match matches a string against the glob pattern with * wildcards, handling escaped '\*' literals. +func (pattern glob) Match(str string) bool { // i is the index for the pattern // j is the index for the string var i, j int diff --git a/capability/policy/glob_test.go b/capability/policy/glob_test.go index 31451a0..a89bce3 100644 --- a/capability/policy/glob_test.go +++ b/capability/policy/glob_test.go @@ -3,7 +3,7 @@ package policy import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestSimpleGlobMatch(t *testing.T) { @@ -56,7 +56,18 @@ func TestSimpleGlobMatch(t *testing.T) { for _, tt := range tests { t.Run(tt.pattern+"_"+tt.str, func(t *testing.T) { - assert.Equal(t, tt.matches, globMatch(tt.pattern, tt.str)) + g, err := parseGlob(tt.pattern) + require.NoError(t, err) + require.Equal(t, tt.matches, g.Match(tt.str)) }) } } + +func BenchmarkGlob(b *testing.B) { + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + g := mustParseGlob("Alice\\*, Bob*, Carol.") + g.Match("Alice*, Bob*, Carol!") + } +} diff --git a/capability/policy/match.go b/capability/policy/match.go index ee65967..5313af4 100644 --- a/capability/policy/match.go +++ b/capability/policy/match.go @@ -112,7 +112,7 @@ func matchStatement(statement Statement, node ipld.Node) bool { if err != nil { return false } - return s.pattern.match(v) + return s.pattern.Match(v) } case KindAll: if s, ok := statement.(quantifier); ok { diff --git a/capability/policy/policy.go b/capability/policy/policy.go index 157be8f..e6f385e 100644 --- a/capability/policy/policy.go +++ b/capability/policy/policy.go @@ -3,8 +3,6 @@ package policy // https://github.com/ucan-wg/delegation/blob/4094d5878b58f5d35055a3b93fccda0b8329ebae/README.md#policy import ( - "errors" - "github.com/ipld/go-ipld-prime" "github.com/ucan-wg/go-ucan/capability/policy/selector" @@ -89,23 +87,9 @@ func Or(stmts ...Statement) Statement { return connective{kind: KindOr, statements: stmts} } -type wildcardPattern string - -func parseWildcardPattern(pattern string) (wildcardPattern, error) { - if !validateGlobPattern(pattern) { - return "", errors.New("invalid wildcard pattern") - } - - return wildcardPattern(pattern), nil -} - -func (wp wildcardPattern) match(str string) bool { - return globMatch(string(wp), str) -} - type wildcard struct { selector selector.Selector - pattern wildcardPattern + pattern glob } func (n wildcard) Kind() string { @@ -113,12 +97,12 @@ func (n wildcard) Kind() string { } func Like(selector selector.Selector, pattern string) (Statement, error) { - parsedPattern, err := parseWildcardPattern(pattern) + g, err := parseGlob(pattern) if err != nil { return nil, err } - return wildcard{selector: selector, pattern: parsedPattern}, nil + return wildcard{selector: selector, pattern: g}, nil } type quantifier struct {