From 160001de925b91e0de6679dab0359877f27c5054 Mon Sep 17 00:00:00 2001 From: Sachin Holla <51310506+sachinholla@users.noreply.github.com> Date: Wed, 10 May 2023 00:32:07 +0530 Subject: [PATCH] DB Access: redis like key pattern matcher (#70) 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 --- translib/db/db.go | 22 ----- translib/db/db_key.go | 161 +++++++++++++++++++++++++++++++++++++ translib/db/db_key_test.go | 135 +++++++++++++++++++++++++++++++ 3 files changed, 296 insertions(+), 22 deletions(-) create mode 100644 translib/db/db_key.go create mode 100644 translib/db/db_key_test.go diff --git a/translib/db/db.go b/translib/db/db.go index d553541db59e..07455793ab34 100644 --- a/translib/db/db.go +++ b/translib/db/db.go @@ -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 { @@ -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 { diff --git a/translib/db/db_key.go b/translib/db/db_key.go new file mode 100644 index 000000000000..8e5c39386cb3 --- /dev/null +++ b/translib/db/db_key.go @@ -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)) +} diff --git a/translib/db/db_key_test.go b/translib/db/db_key_test.go new file mode 100644 index 000000000000..7166938d133c --- /dev/null +++ b/translib/db/db_key_test.go @@ -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) + } + } +}