Skip to content

Commit

Permalink
DB Access: redis like key pattern matcher (sonic-net#70)
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
sachinholla authored May 9, 2023
1 parent 6ce18c6 commit 160001d
Show file tree
Hide file tree
Showing 3 changed files with 296 additions and 22 deletions.
22 changes: 0 additions & 22 deletions translib/db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,16 +212,6 @@ type TableSpec struct {
NoDelete bool
}

// Key gives the key components.
// (Eg: { Comp : [] string { "acl1", "rule1" } } ).
type Key struct {
Comp []string
}

func (k Key) String() string {
return fmt.Sprintf("{ Comp: %v }", k.Comp)
}

func (v Value) String() string {
var str string
for k, v1 := range v.Field {
Expand Down Expand Up @@ -1080,18 +1070,6 @@ func (t *Table) GetEntry(key Key) (Value, error) {
return v, nil
}

//===== Functions for db.Key =====

// Len returns number of components in the Key
func (k *Key) Len() int {
return len(k.Comp)
}

// Get returns the key component at given index
func (k *Key) Get(index int) string {
return k.Comp[index]
}

//===== Functions for db.Value =====

func (v *Value) IsPopulated() bool {
Expand Down
161 changes: 161 additions & 0 deletions translib/db/db_key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
////////////////////////////////////////////////////////////////////////////////
// //
// Copyright 2021 Broadcom. The term Broadcom refers to Broadcom Inc. and/or //
// its subsidiaries. //
// //
// Licensed under the Apache License, Version 2.0 (the "License"); //
// you may not use this file except in compliance with the License. //
// You may obtain a copy of the License at //
// //
// http://www.apache.org/licenses/LICENSE-2.0 //
// //
// Unless required by applicable law or agreed to in writing, software //
// distributed under the License is distributed on an "AS IS" BASIS, //
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. //
// See the License for the specific language governing permissions and //
// limitations under the License. //
// //
////////////////////////////////////////////////////////////////////////////////

package db

import (
"fmt"
)

// Key is the db key components without table name prefix.
// (Eg: { Comp : [] string { "acl1", "rule1" } } ).
type Key struct {
Comp []string
}

// NewKey returns a Key object with given key components
func NewKey(comps ...string) *Key {
return &Key{Comp: comps}
}

// Copy returns a (deep) copy of the given Key
func (k Key) Copy() (rK Key) {
rK = Key{Comp: make([]string, len(k.Comp))}
copy(rK.Comp, k.Comp)
return
}

func (k Key) String() string {
return fmt.Sprintf("{Comp: %v}", k.Comp)
}

// Len returns number of components in the Key
func (k Key) Len() int {
return len(k.Comp)
}

// Get returns the key component at given index
func (k Key) Get(index int) string {
return k.Comp[index]
}

// IsPattern checks if the key has redis glob-style pattern.
// Supports only '*' and '?' wildcards.
func (k Key) IsPattern() bool {
for _, s := range k.Comp {
n := len(s)
for i := 0; i < n; i++ {
switch s[i] {
case '\\':
i++
case '*', '?':
return true
}
}
}
return false
}

// Equals checks if db key k equals to the other key.
func (k Key) Equals(other Key) bool {
if k.Len() != other.Len() {
return false
}
for i, c := range k.Comp {
if c != other.Comp[i] {
return false
}
}
return true
}

// Matches checks if db key k matches a key pattern.
func (k Key) Matches(pattern Key) bool {
if k.Len() != pattern.Len() {
return false
}
for i, c := range k.Comp {
if pattern.Comp[i] == "*" {
continue
}
if !patternMatch(c, 0, pattern.Comp[i], 0) {
return false
}
}
return true
}

// patternMatch checks if the value matches a key pattern.
// vIndex and pIndex are start positions of value and pattern strings to match.
// Mimics redis pattern matcher - i.e, glob like pattern matcher which
// matches '/' against wildcard.
// Supports '*' and '?' wildcards with '\' as the escape character.
// '*' matches any char sequence or none; '?' matches exactly one char.
// Character classes are not supported (redis supports it).
func patternMatch(value string, vIndex int, pattern string, pIndex int) bool {
for pIndex < len(pattern) {
switch pattern[pIndex] {
case '*':
// Skip successive *'s in the pattern
pIndex++
for pIndex < len(pattern) && pattern[pIndex] == '*' {
pIndex++
}
// Pattern ends with *. Its a match always
if pIndex == len(pattern) {
return true
}
// Try to match remaining pattern with every value substring
for ; vIndex < len(value); vIndex++ {
if patternMatch(value, vIndex, pattern, pIndex) {
return true
}
}
// No match for remaining pattern
return false

case '?':
// Accept any char.. there should be at least one
if vIndex >= len(value) {
return false
}
vIndex++
pIndex++

case '\\':
// Do not treat \ as escape char if it is the last pattern char.
// Redis commands behave this way.
if pIndex+1 < len(pattern) {
pIndex++
}
fallthrough

default:
if vIndex >= len(value) || pattern[pIndex] != value[vIndex] {
return false
}
vIndex++
pIndex++
}
}

// All pattern chars have been compared.
// It is a match if all value chars have been exhausted too.
return (vIndex == len(value))
}
135 changes: 135 additions & 0 deletions translib/db/db_key_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
////////////////////////////////////////////////////////////////////////////////
// //
// Copyright 2021 Broadcom. The term Broadcom refers to Broadcom Inc. and/or //
// its subsidiaries. //
// //
// Licensed under the Apache License, Version 2.0 (the "License"); //
// you may not use this file except in compliance with the License. //
// You may obtain a copy of the License at //
// //
// http://www.apache.org/licenses/LICENSE-2.0 //
// //
// Unless required by applicable law or agreed to in writing, software //
// distributed under the License is distributed on an "AS IS" BASIS, //
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. //
// See the License for the specific language governing permissions and //
// limitations under the License. //
// //
////////////////////////////////////////////////////////////////////////////////

package db

import "testing"

func TestIsPattern(t *testing.T) {
t.Run("none1", testNotPattern("aaa"))
t.Run("none5", testNotPattern("aaa", "bbb", "ccc", "ddd", "eee"))
t.Run("* frst", testPattern("*aa", "bbb"))
t.Run("* last", testPattern("aa*", "bbb"))
t.Run("* midl", testPattern("a*a", "bbb"))
t.Run("* frst", testPattern("aaa", "*bb"))
t.Run("* last", testPattern("aaa", "bb*"))
t.Run("* midl", testPattern("aaa", "b*b"))
t.Run("? frst", testPattern("aaa", "?bb"))
t.Run("? last", testPattern("aaa", "bb?"))
t.Run("? midl", testPattern("a?a", "bbb"))
t.Run("\\* frst", testNotPattern("\\*aa", "bbb"))
t.Run("\\* last", testNotPattern("aaa", "bb\\*"))
t.Run("\\* midl", testNotPattern("a\\*a", "bbb"))
t.Run("\\? frst", testNotPattern("aaa", "\\?bb"))
t.Run("\\? last", testNotPattern("aa\\?", "bbb"))
t.Run("\\? midl", testNotPattern("aaa", "b\\?b"))
t.Run("**", testPattern("aaa", "b**b"))
t.Run("??", testPattern("a**a", "bbb"))
t.Run("\\**", testPattern("aa\\**", "bbb"))
t.Run("\\??", testPattern("aaa", "b\\??b"))
t.Run("class", testNotPattern("a[bcd]e"))
t.Run("range", testNotPattern("a[b-d]e"))
// TODO have * and ? inside character class :)
}

func testPattern(comp ...string) func(*testing.T) {
return func(t *testing.T) {
t.Helper()
k := NewKey(comp...)
if !k.IsPattern() {
t.Fatalf("IsPattern() did not detect pattern in %v", k)
}
}
}

func testNotPattern(comp ...string) func(*testing.T) {
return func(t *testing.T) {
t.Helper()
k := NewKey(comp...)
if k.IsPattern() {
t.Fatalf("IsPattern() wrongly detected pattern in %v", k)
}
}
}

func TestKeyEquals(t *testing.T) {
t.Run("empty", keyEq(NewKey(), NewKey(), true))
t.Run("1comp", keyEq(NewKey("aa"), NewKey("aa"), true))
t.Run("2comps", keyEq(NewKey("aa", "bb"), NewKey("aa", "bb"), true))
t.Run("diff", keyEq(NewKey("aa", "bb"), NewKey("aa", "b"), false))
t.Run("bigger", keyEq(NewKey("AA", "BB"), NewKey("AA", "BB", "CC"), false))
t.Run("smallr", keyEq(NewKey("AA", "BB"), NewKey("AA"), false))
}

func keyEq(k1, k2 *Key, exp bool) func(*testing.T) {
return func(t *testing.T) {
t.Helper()
if k1.Equals(*k2) != exp {
t.Fatalf("Equals() failed for k1=%v, k2=%v", k1, k2)
}
}
}

func TestKeyMatches(t *testing.T) {
t.Run("empty", keyMatch(NewKey(), NewKey(), true))
t.Run("bigger", keyMatch(NewKey("AA"), NewKey("AA", "BB"), false))
t.Run("smallr", keyMatch(NewKey("AA", "BB"), NewKey("AA"), false))
t.Run("equals", keyMatch(NewKey("AA", "BB"), NewKey("AA", "BB"), true))
t.Run("nequal", keyMatch(NewKey("AA", "BB"), NewKey("AA", "BBc"), false))
t.Run("AA|*", keyMatch(NewKey("AA", "BB"), NewKey("AA", "*"), true))
t.Run("*|*", keyMatch(NewKey("AA", "BB"), NewKey("*", "*"), true))
t.Run("*A|B*", keyMatch(NewKey("xyzA", "Bcd"), NewKey("*A", "B*"), true))
t.Run("neg1:*A|B*", keyMatch(NewKey("xyzABC", "Bcd"), NewKey("*A", "B*"), false))
t.Run("neg2:*A|B*", keyMatch(NewKey("xyzA", "bcd"), NewKey("*A", "B*"), false))
t.Run("AA|B*C", keyMatch(NewKey("AA", "BxyzC"), NewKey("A*A", "B*C"), true))
t.Run("AA|B\\*C", keyMatch(NewKey("AA", "B*C"), NewKey("AA", "B\\*C"), true))
t.Run("neg1:AA|B\\*C", keyMatch(NewKey("AA", "BxyzC"), NewKey("AA", "B\\*C"), false))
t.Run("AA|B?", keyMatch(NewKey("AA", "BB"), NewKey("AA", "B?"), true))
t.Run("??|?B", keyMatch(NewKey("AA", "BB"), NewKey("??", "?B"), true))
t.Run("?\\?|?B", keyMatch(NewKey("A?", "bB"), NewKey("?\\?", "?B"), true))
t.Run("*:aa/bb", keyMatch(NewKey("aa/bb"), NewKey("*"), true))
t.Run("*/*:aa/bb", keyMatch(NewKey("aa/bb"), NewKey("*/*"), true))
t.Run("*ab*:aabb", keyMatch(NewKey("aabb"), NewKey("*ab*"), true))
t.Run("*ab*:aab", keyMatch(NewKey("aab"), NewKey("*ab*"), true))
t.Run("*ab*:abb", keyMatch(NewKey("abb"), NewKey("*ab*"), true))
t.Run("ab*:abb", keyMatch(NewKey("abb"), NewKey("ab*"), true))
t.Run("ab\\*:ab*", keyMatch(NewKey("ab*"), NewKey("ab\\*"), true))
t.Run("ab\\*:abb", keyMatch(NewKey("ab*"), NewKey("abb"), false))
t.Run("ab\\:abb", keyMatch(NewKey("ab\\"), NewKey("abb"), false))
t.Run("abb:ab", keyMatch(NewKey("ab"), NewKey("abb"), false))
t.Run("aa:bb", keyMatch(NewKey("bb"), NewKey("aa"), false))
t.Run("a*b:aa/bb", keyMatch(NewKey("aa/bb"), NewKey("a*b"), true))
t.Run("a**b:ab", keyMatch(NewKey("ab"), NewKey("a******b"), true))
t.Run("a**b:axyb", keyMatch(NewKey("axyb"), NewKey("a******b"), true))
t.Run("**b:axyb", keyMatch(NewKey("axyb"), NewKey("******b"), true))
t.Run("a**:axyb", keyMatch(NewKey("axyb"), NewKey("a******"), true))
t.Run("ipaddr", keyMatch(NewKey("10.1.2.3/24"), NewKey("10.*"), true))
}

func keyMatch(k, p *Key, exp bool) func(*testing.T) {
return func(t *testing.T) {
t.Helper()
if k.Matches(*p) == exp {
} else if exp {
t.Fatalf("Key %v did not match pattern %v", k, p)
} else {
t.Fatalf("Key %v should not have matched pattern %v", k, p)
}
}
}

0 comments on commit 160001d

Please sign in to comment.