Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions internal/digest/digest.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,14 +343,13 @@ func RemoteDockerImageSha256(client *requests.Client, imageName, imageTag, regis
return strings.TrimPrefix(digestHeader, "sha256:"), nil
}

const validSha256regex = "^([a-f0-9]{64})$"

var validSha256CompiledRegex = regexp.MustCompile(validSha256regex)

// ValidateDigest checks if a digest matches the sha256 regex
func ValidateDigest(sha256ToCheck string) error {
validSha256regex := "^([a-f0-9]{64})$"
r, err := regexp.Compile(validSha256regex)
if err != nil {
return fmt.Errorf("failed to validate the provided SHA256 fingerprint")
}
if !r.MatchString(sha256ToCheck) {
if !validSha256CompiledRegex.MatchString(sha256ToCheck) {
return fmt.Errorf("%s is not a valid SHA256 fingerprint. It should match the pattern %v", sha256ToCheck, validSha256regex)
}
return nil
Expand Down
8 changes: 8 additions & 0 deletions internal/digest/digest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -973,3 +973,11 @@ func (suite *DigestTestSuite) TestGetExcludePathsFromIgnoreFile() {
func TestDigestTestSuite(t *testing.T) {
suite.Run(t, new(DigestTestSuite))
}

func BenchmarkValidateDigest(b *testing.B) {
sha := "db40d79b3a15b17ee9fcc2f49aa73736e0073de6b5a35c459268bb9a31e55139"
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = ValidateDigest(sha)
}
}
72 changes: 62 additions & 10 deletions internal/filters/resourceFilter.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,84 @@ import (
"fmt"
"regexp"
"slices"
"sync"
)

type ResourceFilterOptions struct {
IncludeNames []string
IncludeNamesRegex []string
ExcludeNames []string
ExcludeNamesRegex []string

excludeOnce sync.Once
excludeErr error
compiledExclude []*regexp.Regexp

includeOnce sync.Once
includeErr error
compiledInclude []*regexp.Regexp
}

// IsSet checks if the filter options are set
func (filter *ResourceFilterOptions) IsSet() bool {
return len(filter.IncludeNames) > 0 || len(filter.IncludeNamesRegex) > 0 || len(filter.ExcludeNames) > 0 || len(filter.ExcludeNamesRegex) > 0
}

// CompilePatterns pre-compiles all include and exclude regex patterns
func (filter *ResourceFilterOptions) CompilePatterns() error {
if _, err := filter.compileExcludeRegexes(); err != nil {
return err
}
if _, err := filter.compileIncludeRegexes(); err != nil {
return err
}
return nil
}

func (filter *ResourceFilterOptions) compileExcludeRegexes() ([]*regexp.Regexp, error) {
filter.excludeOnce.Do(func() {
compiled := make([]*regexp.Regexp, 0, len(filter.ExcludeNamesRegex))
for _, pattern := range filter.ExcludeNamesRegex {
re, err := regexp.Compile(pattern)
if err != nil {
filter.excludeErr = fmt.Errorf("invalid exclude name regex pattern %s: %v", pattern, err)
return
}
compiled = append(compiled, re)
}
filter.compiledExclude = compiled
})
return filter.compiledExclude, filter.excludeErr
}

func (filter *ResourceFilterOptions) compileIncludeRegexes() ([]*regexp.Regexp, error) {
filter.includeOnce.Do(func() {
compiled := make([]*regexp.Regexp, 0, len(filter.IncludeNamesRegex))
for _, pattern := range filter.IncludeNamesRegex {
re, err := regexp.Compile(pattern)
if err != nil {
filter.includeErr = fmt.Errorf("invalid include name regex pattern %s: %v", pattern, err)
return
}
compiled = append(compiled, re)
}
filter.compiledInclude = compiled
})
return filter.compiledInclude, filter.includeErr
}

// ShouldInclude checks if a name should be included or not according to the filter options
// the filter should only be used for one operation (include, exclude)
func (filter *ResourceFilterOptions) ShouldInclude(name string) (bool, error) {
if len(filter.ExcludeNames) > 0 || len(filter.ExcludeNamesRegex) > 0 {
if slices.Contains(filter.ExcludeNames, name) {
return false, nil
}
for _, pattern := range filter.ExcludeNamesRegex {
re, err := regexp.Compile(pattern)
if err != nil {
return false, fmt.Errorf("invalid exclude name regex pattern %s: %v", pattern, err)
}
excludeRegexes, err := filter.compileExcludeRegexes()
if err != nil {
return false, err
}
for _, re := range excludeRegexes {
if re.MatchString(name) {
return false, nil
}
Expand All @@ -41,11 +93,11 @@ func (filter *ResourceFilterOptions) ShouldInclude(name string) (bool, error) {
return true, nil
}

for _, pattern := range filter.IncludeNamesRegex {
re, err := regexp.Compile(pattern)
if err != nil {
return false, fmt.Errorf("invalid include name regex pattern %s: %v", pattern, err)
}
includeRegexes, err := filter.compileIncludeRegexes()
if err != nil {
return false, err
}
for _, re := range includeRegexes {
if re.MatchString(name) {
return true, nil
}
Expand Down
115 changes: 115 additions & 0 deletions internal/filters/resourceFilter_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package filters

import (
"fmt"
"sync"
"testing"

"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -112,8 +114,121 @@ func (suite *FiltersSuite) TestShouldInclude() {
}
}

func (suite *FiltersSuite) TestCompilePatterns() {
validFilter := &ResourceFilterOptions{
ExcludeNamesRegex: []string{"^baz.*$", "^qux.*$"},
}
err := validFilter.CompilePatterns()
require.NoError(suite.T(), err)
require.Len(suite.T(), validFilter.compiledExclude, 2)

// Subsequent ShouldInclude calls reuse compiled regexes
inc, err := validFilter.ShouldInclude("baz123")
require.NoError(suite.T(), err)
require.False(suite.T(), inc)

inc, err = validFilter.ShouldInclude("qux456")
require.NoError(suite.T(), err)
require.False(suite.T(), inc)

inc, err = validFilter.ShouldInclude("other")
require.NoError(suite.T(), err)
require.True(suite.T(), inc)

includeFilter := &ResourceFilterOptions{
IncludeNamesRegex: []string{"^foo.*$", "^bar.*$"},
}
err = includeFilter.CompilePatterns()
require.NoError(suite.T(), err)
require.Len(suite.T(), includeFilter.compiledInclude, 2)

inc, err = includeFilter.ShouldInclude("foo123")
require.NoError(suite.T(), err)
require.True(suite.T(), inc)

inc, err = includeFilter.ShouldInclude("bar456")
require.NoError(suite.T(), err)
require.True(suite.T(), inc)

inc, err = includeFilter.ShouldInclude("other")
require.NoError(suite.T(), err)
require.False(suite.T(), inc)

invalidInclude := &ResourceFilterOptions{
IncludeNamesRegex: []string{"[invalid"},
}
require.Error(suite.T(), invalidInclude.CompilePatterns())

invalidExclude := &ResourceFilterOptions{
ExcludeNamesRegex: []string{"[invalid"},
}
require.Error(suite.T(), invalidExclude.CompilePatterns())
}

func (suite *FiltersSuite) TestConcurrentShouldInclude() {
filter := &ResourceFilterOptions{
IncludeNamesRegex: []string{"^include-.*$"},
}

type result struct {
included bool
err error
}

const workerCount = 20
results := make([]result, workerCount)
var wg sync.WaitGroup

for i := 0; i < workerCount; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
inc, err := filter.ShouldInclude(fmt.Sprintf("include-%d", idx))
results[idx] = result{included: inc, err: err}
}(i)
}
wg.Wait()

for _, res := range results {
require.NoError(suite.T(), res.err)
require.True(suite.T(), res.included)
}

// Concurrent exclude test
excludeFilter := &ResourceFilterOptions{
ExcludeNamesRegex: []string{"^exclude-.*$"},
}
excludeResults := make([]result, workerCount)
for i := 0; i < workerCount; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
inc, err := excludeFilter.ShouldInclude(fmt.Sprintf("exclude-%d", idx))
excludeResults[idx] = result{included: inc, err: err}
}(i)
}
wg.Wait()

for _, res := range excludeResults {
require.NoError(suite.T(), res.err)
require.False(suite.T(), res.included)
}
}

// In order for 'go test' to run this suite, we need to create
// a normal test function and pass our suite to suite.Run
func TestFiltersSuite(t *testing.T) {
suite.Run(t, new(FiltersSuite))
}

func BenchmarkShouldInclude_Regex(b *testing.B) {
filter := &ResourceFilterOptions{
IncludeNamesRegex: []string{"^prod-.*$", "^staging-.*$"},
}
_ = filter.CompilePatterns()

b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = filter.ShouldInclude("prod-namespace-app")
}
}
10 changes: 9 additions & 1 deletion internal/gitview/gitView.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,12 +282,20 @@ func getCommitURL(repoURL, commitHash string) string {
// matches lookup happens in the commit message first, and if none is found, matching against the branch name is done
// if no matches are found in both the commit message and the branch name, an empty slice is returned
func (gv *GitView) MatchPatternInCommitMessageORBranchName(pattern, commitSHA, secondarySource string, ignoreBranchMatch bool) ([]string, *CommitInfo, error) {
re, err := regexp.Compile(pattern)
if err != nil {
return []string{}, nil, fmt.Errorf("invalid pattern regex %s: %w", pattern, err)
}
return gv.MatchRegexpInCommitMessageORBranchName(re, commitSHA, secondarySource, ignoreBranchMatch)
}

// MatchRegexpInCommitMessageORBranchName returns a slice of strings matching a pre-compiled regular expression in a commit message or branch name
func (gv *GitView) MatchRegexpInCommitMessageORBranchName(re *regexp.Regexp, commitSHA, secondarySource string, ignoreBranchMatch bool) ([]string, *CommitInfo, error) {
commitInfo, err := gv.GetCommitInfoFromCommitSHA(commitSHA, true, []string{})
if err != nil {
return []string{}, nil, err
}

re := regexp.MustCompile(pattern)
commitMatches := re.FindAllString(commitInfo.Message, -1)
branchMatches := re.FindAllString(commitInfo.Branch, -1)
secondaryMatches := re.FindAllString(secondarySource, -1)
Expand Down
25 changes: 25 additions & 0 deletions internal/gitview/gitView_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os"
"os/exec"
"path/filepath"
"regexp"
"testing"

"github.com/kosli-dev/cli/internal/jira"
Expand Down Expand Up @@ -513,6 +514,13 @@ func (suite *GitViewTestSuite) TestMatchPatternInCommitMessageORBranchName() {
want: []string{"#324"},
wantError: false,
},
{
name: "Invalid regex pattern returns an error",
pattern: "[invalid",
commitMessage: "test commit",
want: []string{},
wantError: true,
},
} {
suite.Run(t.name, func() {

Expand All @@ -538,6 +546,23 @@ func (suite *GitViewTestSuite) TestMatchPatternInCommitMessageORBranchName() {
}
}

func (suite *GitViewTestSuite) TestMatchRegexpInCommitMessageORBranchName() {
_, workTree, fs, err := testHelpers.InitializeGitRepo(suite.tmpDir)
require.NoError(suite.T(), err)

commitSha, err := testHelpers.CommitToRepo(workTree, fs, "Resolves JIRA-100 and JIRA-200")
require.NoError(suite.T(), err)

gitView, err := New(suite.tmpDir)
require.NoError(suite.T(), err)

re := regexp.MustCompile(`JIRA-[0-9]+`)
matches, commitInfo, err := gitView.MatchRegexpInCommitMessageORBranchName(re, commitSha, "", false)
require.NoError(suite.T(), err)
require.NotNil(suite.T(), commitInfo)
require.ElementsMatch(suite.T(), []string{"JIRA-100", "JIRA-200"}, matches)
}

func (suite *GitViewTestSuite) TestResolveRevision() {
_, workTree, fs, err := testHelpers.InitializeGitRepo(suite.tmpDir)
require.NoError(suite.T(), err)
Expand Down
Loading