Skip to content

Commit e3b923b

Browse files
committed
DB Access: redis like key pattern matcher
Added following new methods to the db.Key struct: - IsPattern() : check if key contains any wildcard pattern - Matches(pattern Key) : check if key matches a given key pattern - Equals(other Key) : check if key is same another key Signed-off-by: Sachin Holla <[email protected]>
1 parent 22f82b4 commit e3b923b

3 files changed

Lines changed: 296 additions & 22 deletions

File tree

translib/db/db.go

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -212,16 +212,6 @@ type TableSpec struct {
212212
NoDelete bool
213213
}
214214

215-
// Key gives the key components.
216-
// (Eg: { Comp : [] string { "acl1", "rule1" } } ).
217-
type Key struct {
218-
Comp []string
219-
}
220-
221-
func (k Key) String() string {
222-
return fmt.Sprintf("{ Comp: %v }", k.Comp)
223-
}
224-
225215
func (v Value) String() string {
226216
var str string
227217
for k, v1 := range v.Field {
@@ -1080,18 +1070,6 @@ func (t *Table) GetEntry(key Key) (Value, error) {
10801070
return v, nil
10811071
}
10821072

1083-
//===== Functions for db.Key =====
1084-
1085-
// Len returns number of components in the Key
1086-
func (k *Key) Len() int {
1087-
return len(k.Comp)
1088-
}
1089-
1090-
// Get returns the key component at given index
1091-
func (k *Key) Get(index int) string {
1092-
return k.Comp[index]
1093-
}
1094-
10951073
//===== Functions for db.Value =====
10961074

10971075
func (v *Value) IsPopulated() bool {

translib/db/db_key.go

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
////////////////////////////////////////////////////////////////////////////////
2+
// //
3+
// Copyright 2021 Broadcom. The term Broadcom refers to Broadcom Inc. and/or //
4+
// its subsidiaries. //
5+
// //
6+
// Licensed under the Apache License, Version 2.0 (the "License"); //
7+
// you may not use this file except in compliance with the License. //
8+
// You may obtain a copy of the License at //
9+
// //
10+
// http://www.apache.org/licenses/LICENSE-2.0 //
11+
// //
12+
// Unless required by applicable law or agreed to in writing, software //
13+
// distributed under the License is distributed on an "AS IS" BASIS, //
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. //
15+
// See the License for the specific language governing permissions and //
16+
// limitations under the License. //
17+
// //
18+
////////////////////////////////////////////////////////////////////////////////
19+
20+
package db
21+
22+
import (
23+
"fmt"
24+
)
25+
26+
// Key is the db key components without table name prefix.
27+
// (Eg: { Comp : [] string { "acl1", "rule1" } } ).
28+
type Key struct {
29+
Comp []string
30+
}
31+
32+
// NewKey returns a Key object with given key components
33+
func NewKey(comps ...string) *Key {
34+
return &Key{Comp: comps}
35+
}
36+
37+
// Copy returns a (deep) copy of the given Key
38+
func (k Key) Copy() (rK Key) {
39+
rK = Key{Comp: make([]string, len(k.Comp))}
40+
copy(rK.Comp, k.Comp)
41+
return
42+
}
43+
44+
func (k Key) String() string {
45+
return fmt.Sprintf("{Comp: %v}", k.Comp)
46+
}
47+
48+
// Len returns number of components in the Key
49+
func (k Key) Len() int {
50+
return len(k.Comp)
51+
}
52+
53+
// Get returns the key component at given index
54+
func (k Key) Get(index int) string {
55+
return k.Comp[index]
56+
}
57+
58+
// IsPattern checks if the key has redis glob-style pattern.
59+
// Supports only '*' and '?' wildcards.
60+
func (k Key) IsPattern() bool {
61+
for _, s := range k.Comp {
62+
n := len(s)
63+
for i := 0; i < n; i++ {
64+
switch s[i] {
65+
case '\\':
66+
i++
67+
case '*', '?':
68+
return true
69+
}
70+
}
71+
}
72+
return false
73+
}
74+
75+
// Equals checks if db key k equals to the other key.
76+
func (k Key) Equals(other Key) bool {
77+
if k.Len() != other.Len() {
78+
return false
79+
}
80+
for i, c := range k.Comp {
81+
if c != other.Comp[i] {
82+
return false
83+
}
84+
}
85+
return true
86+
}
87+
88+
// Matches checks if db key k matches a key pattern.
89+
func (k Key) Matches(pattern Key) bool {
90+
if k.Len() != pattern.Len() {
91+
return false
92+
}
93+
for i, c := range k.Comp {
94+
if pattern.Comp[i] == "*" {
95+
continue
96+
}
97+
if !patternMatch(c, 0, pattern.Comp[i], 0) {
98+
return false
99+
}
100+
}
101+
return true
102+
}
103+
104+
// patternMatch checks if the value matches a key pattern.
105+
// vIndex and pIndex are start positions of value and pattern strings to match.
106+
// Mimics redis pattern matcher - i.e, glob like pattern matcher which
107+
// matches '/' against wildcard.
108+
// Supports '*' and '?' wildcards with '\' as the escape character.
109+
// '*' matches any char sequence or none; '?' matches exactly one char.
110+
// Character classes are not supported (redis supports it).
111+
func patternMatch(value string, vIndex int, pattern string, pIndex int) bool {
112+
for pIndex < len(pattern) {
113+
switch pattern[pIndex] {
114+
case '*':
115+
// Skip successive *'s in the pattern
116+
pIndex++
117+
for pIndex < len(pattern) && pattern[pIndex] == '*' {
118+
pIndex++
119+
}
120+
// Pattern ends with *. Its a match always
121+
if pIndex == len(pattern) {
122+
return true
123+
}
124+
// Try to match remaining pattern with every value substring
125+
for ; vIndex < len(value); vIndex++ {
126+
if patternMatch(value, vIndex, pattern, pIndex) {
127+
return true
128+
}
129+
}
130+
// No match for remaining pattern
131+
return false
132+
133+
case '?':
134+
// Accept any char.. there should be at least one
135+
if vIndex >= len(value) {
136+
return false
137+
}
138+
vIndex++
139+
pIndex++
140+
141+
case '\\':
142+
// Do not treat \ as escape char if it is the last pattern char.
143+
// Redis commands behave this way.
144+
if pIndex+1 < len(pattern) {
145+
pIndex++
146+
}
147+
fallthrough
148+
149+
default:
150+
if vIndex >= len(value) || pattern[pIndex] != value[vIndex] {
151+
return false
152+
}
153+
vIndex++
154+
pIndex++
155+
}
156+
}
157+
158+
// All pattern chars have been compared.
159+
// It is a match if all value chars have been exhausted too.
160+
return (vIndex == len(value))
161+
}

translib/db/db_key_test.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
////////////////////////////////////////////////////////////////////////////////
2+
// //
3+
// Copyright 2021 Broadcom. The term Broadcom refers to Broadcom Inc. and/or //
4+
// its subsidiaries. //
5+
// //
6+
// Licensed under the Apache License, Version 2.0 (the "License"); //
7+
// you may not use this file except in compliance with the License. //
8+
// You may obtain a copy of the License at //
9+
// //
10+
// http://www.apache.org/licenses/LICENSE-2.0 //
11+
// //
12+
// Unless required by applicable law or agreed to in writing, software //
13+
// distributed under the License is distributed on an "AS IS" BASIS, //
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. //
15+
// See the License for the specific language governing permissions and //
16+
// limitations under the License. //
17+
// //
18+
////////////////////////////////////////////////////////////////////////////////
19+
20+
package db
21+
22+
import "testing"
23+
24+
func TestIsPattern(t *testing.T) {
25+
t.Run("none1", testNotPattern("aaa"))
26+
t.Run("none5", testNotPattern("aaa", "bbb", "ccc", "ddd", "eee"))
27+
t.Run("* frst", testPattern("*aa", "bbb"))
28+
t.Run("* last", testPattern("aa*", "bbb"))
29+
t.Run("* midl", testPattern("a*a", "bbb"))
30+
t.Run("* frst", testPattern("aaa", "*bb"))
31+
t.Run("* last", testPattern("aaa", "bb*"))
32+
t.Run("* midl", testPattern("aaa", "b*b"))
33+
t.Run("? frst", testPattern("aaa", "?bb"))
34+
t.Run("? last", testPattern("aaa", "bb?"))
35+
t.Run("? midl", testPattern("a?a", "bbb"))
36+
t.Run("\\* frst", testNotPattern("\\*aa", "bbb"))
37+
t.Run("\\* last", testNotPattern("aaa", "bb\\*"))
38+
t.Run("\\* midl", testNotPattern("a\\*a", "bbb"))
39+
t.Run("\\? frst", testNotPattern("aaa", "\\?bb"))
40+
t.Run("\\? last", testNotPattern("aa\\?", "bbb"))
41+
t.Run("\\? midl", testNotPattern("aaa", "b\\?b"))
42+
t.Run("**", testPattern("aaa", "b**b"))
43+
t.Run("??", testPattern("a**a", "bbb"))
44+
t.Run("\\**", testPattern("aa\\**", "bbb"))
45+
t.Run("\\??", testPattern("aaa", "b\\??b"))
46+
t.Run("class", testNotPattern("a[bcd]e"))
47+
t.Run("range", testNotPattern("a[b-d]e"))
48+
// TODO have * and ? inside character class :)
49+
}
50+
51+
func testPattern(comp ...string) func(*testing.T) {
52+
return func(t *testing.T) {
53+
t.Helper()
54+
k := NewKey(comp...)
55+
if !k.IsPattern() {
56+
t.Fatalf("IsPattern() did not detect pattern in %v", k)
57+
}
58+
}
59+
}
60+
61+
func testNotPattern(comp ...string) func(*testing.T) {
62+
return func(t *testing.T) {
63+
t.Helper()
64+
k := NewKey(comp...)
65+
if k.IsPattern() {
66+
t.Fatalf("IsPattern() wrongly detected pattern in %v", k)
67+
}
68+
}
69+
}
70+
71+
func TestKeyEquals(t *testing.T) {
72+
t.Run("empty", keyEq(NewKey(), NewKey(), true))
73+
t.Run("1comp", keyEq(NewKey("aa"), NewKey("aa"), true))
74+
t.Run("2comps", keyEq(NewKey("aa", "bb"), NewKey("aa", "bb"), true))
75+
t.Run("diff", keyEq(NewKey("aa", "bb"), NewKey("aa", "b"), false))
76+
t.Run("bigger", keyEq(NewKey("AA", "BB"), NewKey("AA", "BB", "CC"), false))
77+
t.Run("smallr", keyEq(NewKey("AA", "BB"), NewKey("AA"), false))
78+
}
79+
80+
func keyEq(k1, k2 *Key, exp bool) func(*testing.T) {
81+
return func(t *testing.T) {
82+
t.Helper()
83+
if k1.Equals(*k2) != exp {
84+
t.Fatalf("Equals() failed for k1=%v, k2=%v", k1, k2)
85+
}
86+
}
87+
}
88+
89+
func TestKeyMatches(t *testing.T) {
90+
t.Run("empty", keyMatch(NewKey(), NewKey(), true))
91+
t.Run("bigger", keyMatch(NewKey("AA"), NewKey("AA", "BB"), false))
92+
t.Run("smallr", keyMatch(NewKey("AA", "BB"), NewKey("AA"), false))
93+
t.Run("equals", keyMatch(NewKey("AA", "BB"), NewKey("AA", "BB"), true))
94+
t.Run("nequal", keyMatch(NewKey("AA", "BB"), NewKey("AA", "BBc"), false))
95+
t.Run("AA|*", keyMatch(NewKey("AA", "BB"), NewKey("AA", "*"), true))
96+
t.Run("*|*", keyMatch(NewKey("AA", "BB"), NewKey("*", "*"), true))
97+
t.Run("*A|B*", keyMatch(NewKey("xyzA", "Bcd"), NewKey("*A", "B*"), true))
98+
t.Run("neg1:*A|B*", keyMatch(NewKey("xyzABC", "Bcd"), NewKey("*A", "B*"), false))
99+
t.Run("neg2:*A|B*", keyMatch(NewKey("xyzA", "bcd"), NewKey("*A", "B*"), false))
100+
t.Run("AA|B*C", keyMatch(NewKey("AA", "BxyzC"), NewKey("A*A", "B*C"), true))
101+
t.Run("AA|B\\*C", keyMatch(NewKey("AA", "B*C"), NewKey("AA", "B\\*C"), true))
102+
t.Run("neg1:AA|B\\*C", keyMatch(NewKey("AA", "BxyzC"), NewKey("AA", "B\\*C"), false))
103+
t.Run("AA|B?", keyMatch(NewKey("AA", "BB"), NewKey("AA", "B?"), true))
104+
t.Run("??|?B", keyMatch(NewKey("AA", "BB"), NewKey("??", "?B"), true))
105+
t.Run("?\\?|?B", keyMatch(NewKey("A?", "bB"), NewKey("?\\?", "?B"), true))
106+
t.Run("*:aa/bb", keyMatch(NewKey("aa/bb"), NewKey("*"), true))
107+
t.Run("*/*:aa/bb", keyMatch(NewKey("aa/bb"), NewKey("*/*"), true))
108+
t.Run("*ab*:aabb", keyMatch(NewKey("aabb"), NewKey("*ab*"), true))
109+
t.Run("*ab*:aab", keyMatch(NewKey("aab"), NewKey("*ab*"), true))
110+
t.Run("*ab*:abb", keyMatch(NewKey("abb"), NewKey("*ab*"), true))
111+
t.Run("ab*:abb", keyMatch(NewKey("abb"), NewKey("ab*"), true))
112+
t.Run("ab\\*:ab*", keyMatch(NewKey("ab*"), NewKey("ab\\*"), true))
113+
t.Run("ab\\*:abb", keyMatch(NewKey("ab*"), NewKey("abb"), false))
114+
t.Run("ab\\:abb", keyMatch(NewKey("ab\\"), NewKey("abb"), false))
115+
t.Run("abb:ab", keyMatch(NewKey("ab"), NewKey("abb"), false))
116+
t.Run("aa:bb", keyMatch(NewKey("bb"), NewKey("aa"), false))
117+
t.Run("a*b:aa/bb", keyMatch(NewKey("aa/bb"), NewKey("a*b"), true))
118+
t.Run("a**b:ab", keyMatch(NewKey("ab"), NewKey("a******b"), true))
119+
t.Run("a**b:axyb", keyMatch(NewKey("axyb"), NewKey("a******b"), true))
120+
t.Run("**b:axyb", keyMatch(NewKey("axyb"), NewKey("******b"), true))
121+
t.Run("a**:axyb", keyMatch(NewKey("axyb"), NewKey("a******"), true))
122+
t.Run("ipaddr", keyMatch(NewKey("10.1.2.3/24"), NewKey("10.*"), true))
123+
}
124+
125+
func keyMatch(k, p *Key, exp bool) func(*testing.T) {
126+
return func(t *testing.T) {
127+
t.Helper()
128+
if k.Matches(*p) == exp {
129+
} else if exp {
130+
t.Fatalf("Key %v did not match pattern %v", k, p)
131+
} else {
132+
t.Fatalf("Key %v should not have matched pattern %v", k, p)
133+
}
134+
}
135+
}

0 commit comments

Comments
 (0)