diff --git a/interfaces/prompting/patterns/export_test.go b/interfaces/prompting/patterns/export_test.go
new file mode 100644
index 00000000000..9b9ef4b61a4
--- /dev/null
+++ b/interfaces/prompting/patterns/export_test.go
@@ -0,0 +1,22 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2024 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package patterns
+
+var ParsePatternVariant = parsePatternVariant
diff --git a/interfaces/prompting/patterns/patterns.go b/interfaces/prompting/patterns/patterns.go
index e76a9971e5f..eca7f4345b3 100644
--- a/interfaces/prompting/patterns/patterns.go
+++ b/interfaces/prompting/patterns/patterns.go
@@ -21,13 +21,15 @@ package patterns
import (
"encoding/json"
+ "errors"
"fmt"
- "regexp"
"strings"
doublestar "github.com/bmatcuk/doublestar/v4"
)
+var ErrNoPatterns = errors.New("cannot establish precedence: no patterns given")
+
// Limit the number of expanded path patterns for a particular pattern.
// When fully expanded, the number of patterns for a given unexpanded pattern
// may not exceed this limit.
@@ -76,7 +78,7 @@ func (p *PathPattern) Match(path string) (bool, error) {
// MarshalJSON implements json.Marshaller for PathPattern.
func (p *PathPattern) MarshalJSON() ([]byte, error) {
- return []byte(p.original), nil
+ return json.Marshal(p.original)
}
// UnmarshalJSON implements json.Unmarshaller for PathPattern.
@@ -95,46 +97,13 @@ func (p *PathPattern) NumVariants() int {
}
// RenderAllVariants enumerates every alternative for each group in the path
-// pattern and renders each one into a string which is then passed into the
-// given observe closure, along with the index of the variant.
+// pattern and renders each one into a PatternVariant which is then passed into
+// the given observe closure, along with the index of the variant.
//
// The given observe closure should perform some action with the rendered
// variant, such as adding it to a data structure.
-func (p *PathPattern) RenderAllVariants(observe func(index int, variant string)) {
- cleanThenObserve := func(i int, variant string) {
- cleaned := cleanPattern(variant)
- observe(i, cleaned)
- }
- renderAllVariants(p.renderTree, cleanThenObserve)
-}
-
-var (
- duplicateSlashes = regexp.MustCompile(`(^|[^\\])/+`)
- charsDoublestar = regexp.MustCompile(`([^/\\])\*\*+`)
- doublestarChars = regexp.MustCompile(`([^\\])\*\*+([^/])`)
- duplicateDoublestar = regexp.MustCompile(`/\*\*(/\*\*)+`) // relies on charsDoublestar running first
- starsAnyMaybeStars = regexp.MustCompile(`([^\\])\*+(\?\**)+`)
-)
-
-func cleanPattern(pattern string) string {
- pattern = duplicateSlashes.ReplaceAllString(pattern, `${1}/`)
- pattern = charsDoublestar.ReplaceAllString(pattern, `${1}*`)
- pattern = doublestarChars.ReplaceAllString(pattern, `${1}*${2}`)
- pattern = duplicateDoublestar.ReplaceAllString(pattern, `/**`)
- pattern = starsAnyMaybeStars.ReplaceAllStringFunc(pattern, func(s string) string {
- deleteStars := func(r rune) rune {
- if r == '*' {
- return -1
- }
- return r
- }
- return strings.Map(deleteStars, s) + "*"
- })
- if strings.HasSuffix(pattern, "/**/*") {
- // Strip trailing "/*" from suffix
- return pattern[:len(pattern)-len("/*")]
- }
- return pattern
+func (p *PathPattern) RenderAllVariants(observe func(index int, variant PatternVariant)) {
+ renderAllVariants(p.renderTree, observe)
}
// PathPatternMatches returns true if the given pattern matches the given path.
@@ -173,3 +142,30 @@ func PathPatternMatches(pattern string, path string) (bool, error) {
// match paths like `/foo/`.
return doublestar.Match(pattern+"/", path)
}
+
+// HighestPrecedencePattern determines which of the given path patterns is the
+// most specific, and thus has the highest precedence.
+//
+// Precedence is only defined between patterns which match the same path.
+// Precedence is determined according to which pattern places the earliest and
+// greatest restriction on the path.
+func HighestPrecedencePattern(patterns []PatternVariant, matchingPath string) (PatternVariant, error) {
+ switch len(patterns) {
+ case 0:
+ return PatternVariant{}, ErrNoPatterns
+ case 1:
+ return patterns[0], nil
+ }
+
+ currHighest := patterns[0]
+ for _, contender := range patterns[1:] {
+ result, err := currHighest.Compare(contender, matchingPath)
+ if err != nil {
+ return PatternVariant{}, err
+ }
+ if result < 0 {
+ currHighest = contender
+ }
+ }
+ return currHighest, nil
+}
diff --git a/interfaces/prompting/patterns/patterns_test.go b/interfaces/prompting/patterns/patterns_test.go
index af6e9ce9c46..0bede7a9ccb 100644
--- a/interfaces/prompting/patterns/patterns_test.go
+++ b/interfaces/prompting/patterns/patterns_test.go
@@ -275,17 +275,20 @@ func (s *patternsSuite) TestPathPatternMatch(c *C) {
}
}
-func (s *patternsSuite) TestPathPatternMarshalJSON(c *C) {
+func (s *patternsSuite) TestPathPatternMarshalUnmarshalJSON(c *C) {
for _, pattern := range []string{
"/foo",
- "/foo/ba{r,s}/**",
+ "/f?o/ba{r,s}/**",
"/{a,b}{c,d}{e,f}{g,h}",
} {
pathPattern, err := patterns.ParsePathPattern(pattern)
c.Check(err, IsNil)
marshalled, err := pathPattern.MarshalJSON()
c.Check(err, IsNil)
- c.Check(marshalled, DeepEquals, []byte(pattern))
+ c.Check(marshalled, DeepEquals, []byte(`"`+pattern+`"`))
+ unmarshalled := patterns.PathPattern{}
+ err = unmarshalled.UnmarshalJSON(marshalled)
+ c.Check(err, IsNil)
}
}
@@ -300,8 +303,7 @@ func (s *patternsSuite) TestPathPatternUnmarshalJSONHappy(c *C) {
c.Check(err, IsNil)
marshalled, err := pathPattern.MarshalJSON()
c.Check(err, IsNil)
- // Marshalled pattern excludes surrounding '"' for some reason
- c.Check(marshalled, DeepEquals, pattern[1:len(pattern)-1])
+ c.Check(marshalled, DeepEquals, pattern)
}
}
@@ -473,8 +475,8 @@ func (s *patternsSuite) TestPathPatternRenderAllVariants(c *C) {
pathPattern, err := patterns.ParsePathPattern(testCase.pattern)
c.Check(err, IsNil, Commentf("testCase: %+v", testCase))
expanded := make([]string, 0, pathPattern.NumVariants())
- pathPattern.RenderAllVariants(func(i int, str string) {
- expanded = append(expanded, str)
+ pathPattern.RenderAllVariants(func(i int, variant patterns.PatternVariant) {
+ expanded = append(expanded, variant.String())
})
c.Check(expanded, DeepEquals, testCase.expanded, Commentf("test case: %+v", testCase))
}
@@ -864,3 +866,843 @@ func (s *patternsSuite) TestPathPatternMatchesErrors(c *C) {
c.Check(err, Equals, doublestar.ErrBadPattern)
c.Check(matches, Equals, false)
}
+
+func (s *patternsSuite) TestHighestPrecedencePattern(c *C) {
+ for i, testCase := range []struct {
+ matchingPath string
+ patterns []string
+ highestPrecedence string
+ }{
+ // A single pattern
+ {
+ "/foo",
+ []string{
+ "/foo",
+ },
+ "/foo",
+ },
+ // Test cases from componentType documentation
+ // Literal
+ {
+ "/foo/bar",
+ []string{
+ "/foo/bar",
+ "/foo/?ar",
+ "/foo/ba?",
+ "/foo/*",
+ },
+ "/foo/bar",
+ },
+ {
+ "/foo/bar/",
+ []string{
+ "/foo/b*r",
+ "/foo/b*",
+ "/foo/b*/",
+ },
+ "/foo/b*r",
+ },
+ {
+ "/foo/bar",
+ []string{
+ "/f*o/bar",
+ "/f*/bar",
+ },
+ "/f*o/bar",
+ },
+ // Wildcard '?'
+ {
+ "/foo/bar/",
+ []string{
+ "/foo/ba*?",
+ "/foo/ba*/",
+ },
+ "/foo/ba?*",
+ },
+ {
+ "/foo/bar",
+ []string{
+ "/foo/?ar",
+ "/foo/*bar",
+ },
+ "/foo/?ar",
+ },
+ {
+ "/foo/bar",
+ []string{
+ "/foo/*a?",
+ "/foo/*r/**",
+ },
+ "/foo/*a?",
+ },
+ // Separator '/'
+ {
+ "/foo/bar/",
+ []string{
+ "/foo/bar/",
+ "/foo/bar",
+ "/foo/bar*",
+ "/foo/bar/**",
+ "/foo/bar/**/",
+ },
+ "/foo/bar/",
+ },
+ {
+ "/foo/bar",
+ []string{
+ "/foo/bar",
+ "/foo/**/bar",
+ },
+ "/foo/bar",
+ },
+ // Terminal
+ {
+ "/foo/bar/",
+ []string{
+ "/foo/bar",
+ "/foo/bar/**",
+ "/foo/bar/**/",
+ "/foo/bar*",
+ },
+ "/foo/bar",
+ },
+ // Non-terminal "/**"
+ {
+ "/foo/bar/",
+ []string{
+ "/foo/**/bar",
+ "/foo/**/",
+ "/foo/**",
+ "/foo*/bar",
+ },
+ "/foo/**/bar",
+ },
+ // Terminal "/**/"
+ {
+ "/foo/bar/",
+ []string{
+ "/foo/**/",
+ "/foo/**",
+ },
+ "/foo/**/",
+ },
+ {
+ "/foo/bar/",
+ []string{
+ "/foo/bar/**/",
+ "/foo/bar*",
+ },
+ "/foo/bar/**/",
+ },
+ // Terminal "/**"
+ {
+ "/foo/bar",
+ []string{
+ "/foo/bar/**",
+ "/foo/bar*",
+ },
+ "/foo/bar/**",
+ },
+ // Test cases from Compare documentation
+ {
+ "/foo/bar",
+ []string{
+ "/foo/*b*",
+ "/foo/*a*",
+ "/foo/*r*",
+ },
+ "/foo/*b*",
+ },
+ {
+ "/foo/bar",
+ []string{
+ "/foo/bar*",
+ "/foo/ba*",
+ "/foo/b*",
+ },
+ "/foo/bar*",
+ },
+ {
+ "/foo/bar",
+ []string{
+ "/foo/*bar",
+ "/foo/*ar",
+ "/foo/*r",
+ },
+ "/foo/*bar",
+ },
+ {
+ "/foo/bar/bazz/quxxx",
+ []string{
+ "/foo/**/bar/bazz/quxxx",
+ "/foo/**/bazz/quxxx",
+ "/foo/**/quxxx",
+ },
+ "/foo/**/bar/bazz/quxxx",
+ },
+ // Related test cases
+ {
+ "/foo",
+ []string{
+ "/f?o",
+ "/fo?",
+ },
+ "/fo?",
+ },
+ {
+ "/foo/aaa",
+ []string{
+ "/foo/*a?",
+ "/foo/*a??",
+ },
+ "/foo/*a??",
+ },
+ {
+ "/foo/aaa",
+ []string{
+ "/foo/*?a",
+ "/foo/*??a",
+ },
+ "/foo/??*a",
+ },
+ {
+ "/foo/bar",
+ []string{
+ "/foo/bar",
+ "/foo/ba?",
+ "/foo/b??",
+ },
+ "/foo/bar",
+ },
+ // Other test cases
+ {
+ "/foo/bar/baz",
+ []string{
+ "/foo/bar/baz",
+ "/foo/bar/ba?",
+ },
+ "/foo/bar/baz",
+ },
+ {
+ "/foo/bar/baz",
+ []string{
+ "/foo/bar/b?z",
+ "/foo/bar/baz",
+ },
+ "/foo/bar/baz",
+ },
+ {
+ "/foo/bar/baz",
+ []string{
+ "/foo/bar/baz*",
+ "/foo/bar/baz",
+ },
+ "/foo/bar/baz",
+ },
+ {
+ "/foo/bar/baz",
+ []string{
+ "/foo/b?r/baz",
+ "/foo/bar/**",
+ },
+ "/foo/bar/**",
+ },
+ {
+ "/foo/bar/",
+ []string{
+ "/foo/bar/",
+ "/foo/bar",
+ },
+ "/foo/bar/",
+ },
+ {
+ "/foo/bar/",
+ []string{
+ "/foo/bar/",
+ "/foo/bar/*",
+ },
+ "/foo/bar/",
+ },
+ {
+ "/foo/bar/",
+ []string{
+ "/foo/bar/",
+ "/foo/bar/**",
+ },
+ "/foo/bar/",
+ },
+ {
+ "/foo/bar/",
+ []string{
+ "/foo/bar/",
+ "/foo/bar/**/",
+ },
+ "/foo/bar/",
+ },
+ {
+ "/foo/bar",
+ []string{
+ "/foo/bar",
+ "/foo/bar/**",
+ },
+ "/foo/bar",
+ },
+ {
+ "/foo/barxbaz",
+ []string{
+ "/foo/bar?baz",
+ "/foo/bar*baz",
+ },
+ "/foo/bar?baz",
+ },
+ {
+ "/foo/barxbaz",
+ []string{
+ "/foo/bar?baz",
+ "/foo/bar**baz",
+ },
+ "/foo/bar?baz",
+ },
+ {
+ "/foo/bar/baz/",
+ []string{
+ "/foo/bar/baz",
+ "/foo/b*r/baz/",
+ },
+ "/foo/bar/baz",
+ },
+ {
+ "/foo/bar/x/baz",
+ []string{
+ "/foo/bar/*/baz",
+ "/foo/bar/*/*baz",
+ },
+ "/foo/bar/*/baz",
+ },
+ {
+ "/foo/bar/x/baz",
+ []string{
+ "/foo/bar/*/baz",
+ "/foo/bar/*/*",
+ },
+ "/foo/bar/*/baz",
+ },
+ {
+ "/foo/bar/baz/",
+ []string{
+ "/foo/bar/*/",
+ "/foo/bar/*",
+ },
+ "/foo/bar/*/",
+ },
+ {
+ "/foo/bar/baz/",
+ []string{
+ "/foo/bar/*/",
+ "/foo/bar/*/**/",
+ },
+ "/foo/bar/*/",
+ },
+ {
+ "/foo/bar/baz/",
+ []string{
+ "/foo/bar/*/",
+ "/foo/bar/*/**",
+ },
+ "/foo/bar/*/",
+ },
+ {
+ "/foo/bar/x/baz",
+ []string{
+ "/foo/bar/*/*baz",
+ "/foo/bar/*/*",
+ },
+ "/foo/bar/*/*baz",
+ },
+ {
+ "/foo/bar/x/baz",
+ []string{
+ "/foo/bar/*/*baz",
+ "/foo/bar/*/**",
+ },
+ "/foo/bar/*/*baz",
+ },
+ {
+ "/foo/bar/x/baz",
+ []string{
+ "/foo/bar/*/*",
+ "/foo/bar/*/**",
+ },
+ "/foo/bar/*/*",
+ },
+ {
+ "/foo/bar/baz",
+ []string{
+ "/foo/bar/*",
+ "/foo/bar/*/**",
+ },
+ "/foo/bar/*",
+ },
+ {
+ "/foo/bar/baz",
+ []string{
+ "/foo/bar/*",
+ "/foo/bar/**/baz",
+ },
+ "/foo/bar/*",
+ },
+ {
+ "/foo/bar/baz",
+ []string{
+ "/foo/bar/*/**",
+ "/foo/bar/**/baz",
+ },
+ "/foo/bar/*/**",
+ },
+ {
+ "/foo/barxbaz",
+ []string{
+ "/foo/bar*baz",
+ "/foo/bar*",
+ },
+ "/foo/bar*baz",
+ },
+ {
+ "/foo/bar/baz",
+ []string{
+ "/foo/bar*/baz",
+ "/foo/bar*/*",
+ },
+ "/foo/bar*/baz",
+ },
+ {
+ "/foo/bar/baz",
+ []string{
+ "/foo/bar*/baz",
+ "/foo/bar*/baz/**",
+ },
+ "/foo/bar*/baz",
+ },
+ {
+ "/foo/bar/baz",
+ []string{
+ "/foo/bar*/baz",
+ "/foo/bar/**",
+ },
+ "/foo/bar/**",
+ },
+ {
+ "/foo/barxxx/xxxbaz",
+ []string{
+ "/foo/bar*/*",
+ "/foo/bar*/*baz",
+ },
+ "/foo/bar*/*baz",
+ },
+ {
+ "/foo/barxxx",
+ []string{
+ "/foo/bar*/**",
+ "/foo/bar*",
+ },
+ "/foo/bar*",
+ },
+ {
+ "/foo/bar/",
+ []string{
+ "/foo/bar*/",
+ "/foo/bar*/**",
+ },
+ "/foo/bar*/",
+ },
+ {
+ "/foo/bar/",
+ []string{
+ "/foo/bar*/",
+ "/foo/bar*/**/",
+ },
+ "/foo/bar*/",
+ },
+ {
+ "/foo/bar/",
+ []string{
+ "/foo/bar*/",
+ "/foo/bar/**/",
+ },
+ "/foo/bar/**/",
+ },
+ {
+ "/foo/bar/baz/",
+ []string{
+ "/foo/bar*/*baz",
+ "/foo/bar/**/baz",
+ },
+ "/foo/bar/**/baz",
+ },
+ {
+ "/foo/bar/baz/",
+ []string{
+ "/foo/bar*/*baz",
+ "/foo/bar*/**/baz",
+ },
+ "/foo/bar*/*baz",
+ },
+ {
+ "/foo/bar/x/baz/",
+ []string{
+ "/foo/bar*/*/baz",
+ "/foo/bar*/*/*",
+ },
+ "/foo/bar*/*/baz",
+ },
+ {
+ "/foo/bar/x/baz/",
+ []string{
+ "/foo/bar*/*/baz",
+ "/foo/bar/**/baz",
+ },
+ "/foo/bar/**/baz",
+ },
+ {
+ "/foo/bar/baz/",
+ []string{
+ "/foo/bar*/*/",
+ "/foo/bar*/*",
+ },
+ "/foo/bar*/*/",
+ },
+ {
+ "/foo/bar/baz/",
+ []string{
+ "/foo/bar/**/baz",
+ "/foo/bar/**/*baz",
+ },
+ "/foo/bar/**/baz",
+ },
+ {
+ "/foo/bar/baz/",
+ []string{
+ "/foo/bar/**/baz",
+ "/foo/bar/**",
+ },
+ "/foo/bar/**/baz",
+ },
+ // Prioritize earlier matches after /**/
+ {
+ "/foo/bar/fizz/buzz/file.txt",
+ []string{
+ "/foo/bar/**/fizz/**",
+ "/foo/bar/**/buzz/**",
+ },
+ "/foo/bar/**/fizz/**",
+ },
+ {
+ "/foo/bar/buzz/fizz/file.txt",
+ []string{
+ "/foo/bar/**/fizz/**",
+ "/foo/bar/**/buzz/**",
+ },
+ "/foo/bar/**/buzz/**",
+ },
+ {
+ "/foo/bar/baz/",
+ []string{
+ "/foo/bar/**/*baz/",
+ "/foo/bar/**/*baz",
+ },
+ "/foo/bar/**/*baz/",
+ },
+ {
+ "/foo/bar/baz/",
+ []string{
+ "/foo/bar/**/*baz/",
+ "/foo/bar/**/",
+ },
+ "/foo/bar/**/*baz/",
+ },
+ {
+ "/foo/bar/baz/",
+ []string{
+ "/foo/bar/**/*baz",
+ "/foo/bar/**/",
+ },
+ "/foo/bar/**/*baz",
+ },
+ {
+ "/foo/bar/x/baz",
+ []string{
+ "/foo/bar/**/*baz",
+ "/foo/bar/**",
+ },
+ "/foo/bar/**/*baz",
+ },
+ {
+ "/foo/bar/fizz/buzz/baz",
+ []string{
+ "/foo/bar/**/*baz",
+ "/foo/bar*/**/baz",
+ },
+ "/foo/bar/**/*baz",
+ },
+ {
+ "/foo/bar/fizz/buzz/baz/",
+ []string{
+ "/foo/bar/**/",
+ "/foo/bar/**",
+ },
+ "/foo/bar/**/",
+ },
+ {
+ "/foo/bar/fizz/buzz/baz/",
+ []string{
+ "/foo/bar/**/",
+ "/foo/bar*/**/baz/",
+ },
+ "/foo/bar/**/",
+ },
+ {
+ "/foo/bar/fizz/buzz/baz/",
+ []string{
+ "/foo/bar/**",
+ "/foo/bar*/**/baz/",
+ },
+ "/foo/bar/**",
+ },
+ {
+ "/foo/bar/fizz/buzz/baz/",
+ []string{
+ "/foo/bar*/**/baz",
+ "/foo/bar*/**/",
+ },
+ "/foo/bar*/**/baz",
+ },
+ {
+ "/foo/barfizz/buzz/baz/",
+ []string{
+ "/foo/bar*/**/",
+ "/foo/bar*/**",
+ },
+ "/foo/bar*/**/",
+ },
+ {
+ "/foo/bar/file.tar.gz",
+ []string{
+ "/foo/bar/*.gz",
+ "/foo/bar/*.tar.gz",
+ },
+ "/foo/bar/*.tar.gz",
+ },
+ {
+ "/foo/bar/file.tar.gz",
+ []string{
+ "/foo/bar/**/*.gz",
+ "/foo/**/*.tar.gz",
+ },
+ "/foo/bar/**/*.gz",
+ },
+ {
+ "/foo/bar/x/y/z/file.tar.gz",
+ []string{
+ "/foo/bar/x/**/*.gz",
+ "/foo/bar/**/*.tar.gz",
+ },
+ "/foo/bar/x/**/*.gz",
+ },
+ {
+ "/foo/bar/file.tar.gz",
+ []string{
+ "/foo/bar/**/*.tar.gz",
+ "/foo/bar/*",
+ },
+ "/foo/bar/*",
+ },
+ {
+ "/foo/bar/baz/x/y/z/file.txt",
+ []string{
+ "/foo/bar/**",
+ "/foo/bar/baz/**",
+ "/foo/bar/baz/**/*.txt",
+ },
+ "/foo/bar/baz/**/*.txt",
+ },
+ {
+ "/foo/bar",
+ []string{
+ "/foo/bar*",
+ "/foo/bar/**",
+ },
+ "/foo/bar/**",
+ },
+ {
+ `/foo/\`,
+ []string{
+ `/foo/\\`,
+ `/foo/*/**`,
+ },
+ `/foo/\\`,
+ },
+ {
+ `/foo/*fizz/bar/x*`,
+ []string{
+ `/foo/\**/b\ar/*\*`,
+ `/foo/*/bar/x\*`,
+ },
+ `/foo/\**/bar/*\*`,
+ },
+ {
+ "/foo/barxxxbaz",
+ []string{
+ "/foo/bar**",
+ "/foo/bar**baz",
+ },
+ "/foo/bar*baz",
+ },
+ {
+ "/foo/xxxbar",
+ []string{
+ "/foo/**",
+ "/foo/**bar",
+ },
+ "/foo/*bar",
+ },
+ {
+ "/foo/x/y/z/bar/baz/",
+ []string{
+ "/foo/**/bar/*/",
+ "/foo/**/b?r/baz/",
+ },
+ "/foo/**/bar/*/",
+ },
+ {
+ "/foo/x/y/z/bar/fizz",
+ []string{
+ "/foo/**/fizz",
+ "/foo/**/bar/fizz",
+ },
+ "/foo/**/bar/fizz",
+ },
+ {
+ "/foo/x/y/z/bar",
+ []string{
+ "/foo/**/*/bar",
+ "/foo/**/*",
+ },
+ "/foo/*/**/bar",
+ },
+ {
+ "/foo/bar",
+ []string{
+ "/foo/**/*",
+ "/**",
+ },
+ "/foo/**",
+ },
+ // Duplicate patterns should never be passed into HighestPrecedencePattern,
+ // but if they are, handle them correctly.
+ {
+ "/foo/bar/",
+ []string{
+ "/foo/bar/",
+ "/foo/bar/",
+ },
+ "/foo/bar/",
+ },
+ {
+ "/foo/bar/",
+ []string{
+ "/foo/bar/",
+ "/foo/bar/",
+ "/foo/bar",
+ },
+ "/foo/bar/",
+ },
+ {
+ "/foo/bar/baz/",
+ []string{
+ "/foo/bar/**",
+ "/foo/bar/**",
+ "/foo/bar/*",
+ },
+ "/foo/bar/*",
+ },
+ } {
+ variants := make([]patterns.PatternVariant, len(testCase.patterns))
+ variantsReversed := make([]patterns.PatternVariant, len(testCase.patterns))
+ for i, pattern := range testCase.patterns {
+ variant, err := patterns.ParsePatternVariant(pattern)
+ c.Assert(err, IsNil, Commentf("pattern: %s", pattern))
+ variants[i] = variant
+ variantsReversed[len(variantsReversed)-1-i] = variant
+ // Check that the rendered variant actually matches the path
+ matches, err := patterns.PathPatternMatches(variant.String(), testCase.matchingPath)
+ c.Check(err, IsNil, Commentf("testCase: %+v\npath: %s\nvariant: %s", testCase, testCase.matchingPath, variant.String()))
+ c.Check(matches, Equals, true, Commentf("testCase: %+v\npath: %s\nvariant: %s", testCase, testCase.matchingPath, variant.String()))
+ }
+ highestPrecedence, err := patterns.HighestPrecedencePattern(variants, testCase.matchingPath)
+ c.Check(err, IsNil, Commentf("Error occurred during test case %d:\n%+v\nerror: %v", i, testCase, err))
+ if err != nil {
+ continue
+ }
+ c.Check(highestPrecedence.String(), Equals, testCase.highestPrecedence, Commentf("Highest precedence pattern incorrect for test case %d:\n%+v", i, testCase))
+ highestPrecedence, err = patterns.HighestPrecedencePattern(variantsReversed, testCase.matchingPath)
+ c.Check(err, IsNil, Commentf("Error occurred during test case %d:\n%+v\nerror: %v", i, testCase, err))
+ if err != nil {
+ continue
+ }
+ c.Check(highestPrecedence.String(), Equals, testCase.highestPrecedence, Commentf("Highest precedence pattern incorrect for reversed test case %d:\n%+v", i, testCase))
+ }
+}
+
+func (s *patternsSuite) TestHighestPrecedencePatternOrdered(c *C) {
+ matchingPath := "/foo/bar/baz/myfile.txt"
+ orderedPatterns := []string{
+ "/foo/bar/baz/myfile.txt",
+ "/foo/bar/baz/m?file.*",
+ "/foo/bar/baz/m*file.txt",
+ "/foo/bar/baz/m*file*",
+ "/foo/bar/baz/*",
+ "/foo/bar/*/myfile.txt",
+ "/foo/bar/*/myfile*",
+ "/foo/bar/*/*.txt",
+ "/foo/bar/*/*",
+ "/foo/bar/**/baz/myfile.txt",
+ "/foo/bar/**/baz/*.txt",
+ "/foo/bar/**/baz/*",
+ "/foo/bar/**/myfile.txt",
+ "/foo/bar/**",
+ "/foo/ba*r/baz/myfile.txt",
+ "/foo/b?r/baz/myfile.txt",
+ "/foo/b*r/baz/myfile.txt",
+ "/foo/?*/baz/myfile.txt",
+ "/foo/?*/**",
+ "/foo/*/baz/myfile.txt",
+ "/foo/*/baz/*",
+ "/foo/*/*/*",
+ "/foo/*/**/baz/myfile.txt",
+ "/foo/**/bar/baz/myfile.txt",
+ "/foo/**/baz/myfile.txt",
+ "/foo/**/baz/**",
+ "/foo/**/myfile.txt",
+ "/**/foo/bar/baz/myfile.txt",
+ "/**/myfile.txt",
+ "/**",
+ }
+ for i := 0; i < len(orderedPatterns); i++ {
+ window := orderedPatterns[i:]
+ variants := make([]patterns.PatternVariant, 0, len(window))
+ for _, pattern := range window {
+ variant, err := patterns.ParsePatternVariant(pattern)
+ c.Assert(err, IsNil, Commentf("pattern: %s", pattern))
+ variants = append(variants, variant)
+ }
+ result, err := patterns.HighestPrecedencePattern(variants, matchingPath)
+ c.Assert(err, IsNil, Commentf("Error occurred while computing precedence between %v: %v", window, err))
+ c.Check(result.String(), Equals, window[0], Commentf("patterns: %+v", window))
+ }
+}
+
+func (s *patternsSuite) TestHighestPrecedencePatternUnhappy(c *C) {
+ result, err := patterns.HighestPrecedencePattern([]patterns.PatternVariant{}, "")
+ c.Check(err, Equals, patterns.ErrNoPatterns)
+ c.Check(result, DeepEquals, patterns.PatternVariant{})
+}
diff --git a/interfaces/prompting/patterns/render.go b/interfaces/prompting/patterns/render.go
index 838b76e34ae..90fba610597 100644
--- a/interfaces/prompting/patterns/render.go
+++ b/interfaces/prompting/patterns/render.go
@@ -23,6 +23,8 @@ import (
"bytes"
"errors"
"strings"
+
+ "github.com/snapcore/snapd/logger"
)
// variantState is the current variant of a render node.
@@ -63,7 +65,7 @@ type renderNode interface {
// for each group in the path pattern. The given observe closure is then called
// on each variant, along with its index, and it can perform some action with
// the variant, such as adding it to a data structure.
-func renderAllVariants(n renderNode, observe func(index int, variant string)) {
+func renderAllVariants(n renderNode, observe func(index int, variant PatternVariant)) {
var buf bytes.Buffer
v, length := n.InitialVariant()
@@ -74,7 +76,13 @@ func renderAllVariants(n renderNode, observe func(index int, variant string)) {
buf.Truncate(lengthUnchanged)
buf.Grow(length - lengthUnchanged)
v.Render(&buf, lengthUnchanged)
- observe(i, buf.String())
+ variant, err := parsePatternVariant(buf.String())
+ if err != nil {
+ // This should never occur
+ logger.Noticef("patterns: cannot parse pattern variant '%s': %v", variant, err)
+ continue
+ }
+ observe(i, variant)
length, lengthUnchanged, moreRemain = v.NextVariant()
}
}
diff --git a/interfaces/prompting/patterns/render_internal_test.go b/interfaces/prompting/patterns/render_internal_test.go
index 6ca3dac4c92..2252515f335 100644
--- a/interfaces/prompting/patterns/render_internal_test.go
+++ b/interfaces/prompting/patterns/render_internal_test.go
@@ -86,8 +86,8 @@ func (s *renderSuite) TestRenderAllVariants(c *C) {
}
variants := make([]string, 0, len(expectedVariants))
- renderAllVariants(parsed, func(index int, variant string) {
- variants = append(variants, variant)
+ renderAllVariants(parsed, func(index int, variant PatternVariant) {
+ variants = append(variants, variant.String())
})
c.Check(variants, DeepEquals, expectedVariants)
diff --git a/interfaces/prompting/patterns/variant.go b/interfaces/prompting/patterns/variant.go
new file mode 100644
index 00000000000..75bbf4f2b39
--- /dev/null
+++ b/interfaces/prompting/patterns/variant.go
@@ -0,0 +1,505 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2024 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package patterns
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "regexp"
+ "strings"
+)
+
+type componentType int
+
+// Component types in order from lowest to highest precedence.
+//
+// A literal exactly matches the next characters in the path, so it has the
+// highest precedence.
+// - `/foo/bar` has precedence over `/foo/ba?`, `/foo/?ar`, and `/foo/*`
+// - `/foo/b*r` has precedence over `/foo/b*` and `/foo/b*/` (though this is caught by match length of '*')
+// - `/f*o/bar` has precedence over `/f*/bar` (though this is caught by match length of '*')
+//
+// A wildcard '?' character matches exactly one non-separator character, so it
+// has precedence over variable-width components such as globstars and double-
+// stars. The parser ensures that all '?'s are moved before any adjacent '*',
+// so a '?' can never match at the same position as a separator or terminal,
+// without precedence having decided by a previous component. Thus, it doesn't
+// actually matter whether '?' has precedence over '/' or terminal.
+// - `/foo/ba*?` (parsed to `/foo/ba?*`) has precedence over `/foo/ba*/`
+// - `/foo/?ar` has precedence over `/foo/*bar`
+// - `/foo/*a?` has precedence over `/foo/*r/**` (though this is caught by match length of '*')
+//
+// A separator '/' character is like a literal, except that when following a
+// '*', it precludes any more information being given about the length or
+// content of the path prior to the separator. Thus, '/' has lower precedence
+// than literals, though in practice, any situation where '/' would be compared
+// against a literal must follow a variable-length component (e.g. '*' or "/**")
+// where the component prior to the literal would match fewer characters in the
+// path than the component prior to the '/', and thus precedence would already
+// be given to the component prior to the literal. Patterns with trailing '/'
+// match only directories, while patterns without match both files and
+// directories, so '/' has precedence over terminals.
+// - `/foo/bar/` has precedence over `/foo/bar` and `/foo/bar*`
+// - `/foo/bar/` has precedence over `/foo/bar/**` and `/foo/bar/**/`
+// - `/foo/bar` has precedence over `/foo/**/bar`
+//
+// Terminals match when there is no path left, so they have precedence over all
+// the variable-length component types, which may match zero or more characters.
+// - `/foo/bar` has precedence over `/foo/bar/**/`, `/foo/bar/**`
+// - `/foo/bar` has precedence over `/foo/bar*`
+//
+// The remaining component types are variable-width, as they may match zero or
+// more characters in the path. When two variable-width components of the same
+// type are compared, the one which matches fewer characters in the path has
+// precedence, since that means that the next component matches earlier in the
+// path, and thus provides greater specificity earlier.
+//
+// The next three component types relate to doublestars, which always follow a
+// '/' character, and may match zero or more characters; in order to know
+// whether a '/' is followed by a "**" or not without looking ahead in the list
+// of components, we group these components together, along with their suffix
+// when relevant. Since "/**" components match zero or more characters,
+// including the terminal in the component allows us to know whether there are
+// more non-terminal components to come without needing to look ahead.
+//
+// The non-terminal "/**" must be followed by a component which gives more
+// information about the matching path, so it has the highest precedence of the
+// three doublestar component types.
+// - `/foo/**/bar` has precedence over `/foo/**/` and `/foo/**`
+// - `/foo/**/bar` has precedence over `/foo*/bar
+//
+// The terminal "/**/" component means that the variant only matches
+// directories, while the terminal "/**" component can match files or
+// directories, so the former has precedence over the latter.
+// - `/foo/**/` has precedence over `/foo/**`
+// - `/foo/bar/**/` has precedence over `/foo/bar*`
+//
+// The terminal "/**" has lower precedence than "/**/", as discussed above.
+// However, it still has higher precedence than '*', since the leading separator
+// which is built into the "/**" puts a constraint on the length/content of the
+// path segment preceding it, while '*' does not.
+// - `/foo/bar/**` has precedence over `/foo/bar*`
+//
+// The globstar '*' has the lowest precedence, since all other component
+// types begin with more information about the length or content of the next
+// characters in the path: as discussed above, "/foo/**" has precedence over
+// "/foo*" since the former matches "/foo" exactly or a path in the "/foo"
+// directory, while "/foo*" matches any path which happens to begin with "/foo".
+const (
+ compUnset componentType = iota
+ compGlobstar
+ compSeparatorDoublestarTerminal // need to bundle separator and terminal marker with /**
+ compSeparatorDoublestarSeparatorTerminal // need to bundle separators and terminal marker with /**/
+ compSeparatorDoublestar
+ compTerminal // marker of end of pattern.
+ compSeparator
+ compAnySingle // ? has precedence over / so that /foo*?/ has precedence over /foo*/
+ compLiteral
+)
+
+type component struct {
+ compType componentType
+ compText string
+}
+
+// String returns the globstar-style pattern string associated with the given
+// component.
+func (c component) String() string {
+ switch c.compType {
+ case compGlobstar:
+ return "*"
+ case compSeparatorDoublestarTerminal:
+ return "/**"
+ case compSeparatorDoublestarSeparatorTerminal:
+ return "/**/"
+ case compSeparatorDoublestar:
+ return "/**"
+ case compTerminal: // end of pattern
+ return ""
+ case compSeparator:
+ return "/"
+ case compAnySingle:
+ return "?"
+ case compLiteral:
+ return c.compText
+ }
+ panic(fmt.Sprintf("unknown component type: %d", int(c.compType)))
+}
+
+// componentRegex returns a regular expression corresponding to the bash-style
+// globstar matching behavior of the receiving component.
+//
+// For example, "*" matches any non-separator characters, so we return the regex
+// `((?:[^/]|\\/)*)` for the globstar component type.
+//
+// The returned regexps should each be enclosed in a capturing group with no
+// capturing groups within. This allows a single regex to be constructed for a
+// given pattern variant by concatenating all the component regular expressions
+// together, and the resulting regex has exactly one capturing group for each
+// component, in order.
+func (c component) componentRegex() string {
+ switch c.compType {
+ case compGlobstar:
+ return `((?:[^/]|\\/)*)`
+ case compSeparatorDoublestarTerminal:
+ return `((?:/.+)?/?)`
+ case compSeparatorDoublestarSeparatorTerminal:
+ return `((?:/.+)?/)`
+ case compSeparatorDoublestar:
+ return `((?:/.+)?)`
+ case compTerminal:
+ return `(/?)`
+ case compSeparator:
+ return `(/)`
+ case compAnySingle:
+ return `([^/])` // does escaped '/' (e.g. `\\/`) count as one character?
+ case compLiteral:
+ return `(` + regexp.QuoteMeta(unescapeLiteral(c.compText)) + `)`
+ }
+ return `()`
+}
+
+var escapeFinder = regexp.MustCompile(`\\(.)`)
+
+// unescapeLiteral removes any `\` characters which are used to escape another
+// character. Note that escaped `\` characters are not removed, since they are
+// not acting as an escape character in those instances. That is, `\\` is
+// reduced to `\`.
+func unescapeLiteral(literal string) string {
+ return escapeFinder.ReplaceAllString(literal, "${1}")
+}
+
+type PatternVariant struct {
+ variant string
+ components []component
+ regex *regexp.Regexp
+}
+
+// String returns the rendered string associated with the pattern variant.
+func (v PatternVariant) String() string {
+ return v.variant
+}
+
+// parsePatternVariant parses a rendered variant string into a PatternVariant
+// whose precedence can be compared against others.
+func parsePatternVariant(variant string) (PatternVariant, error) {
+ var components []component
+ var runes []rune
+
+ // prevComponentsAre returns true if the most recent components have types
+ // matching the given target types from least recent to most recent.
+ prevComponentsAre := func(target []componentType) bool {
+ if len(components) < len(target) {
+ return false
+ }
+ for i, t := range target {
+ if components[len(components)-len(target)+i].compType != t {
+ return false
+ }
+ }
+ return true
+ }
+
+ // addGlobstar adds a globstar to the components if the previous component
+ // was not a globstar or a doublestar.
+ addGlobstar := func() {
+ if !prevComponentsAre([]componentType{compGlobstar}) && !prevComponentsAre([]componentType{compSeparatorDoublestar}) {
+ components = append(components, component{compType: compGlobstar})
+ }
+ }
+
+ // reducePrevDoublestar checks if the most recent component was a
+ // doublestar, and if it was, replaces it with a globstar '*'.
+ // This is necessary because a doublestar followed by anything except a
+ // separator is treated as a globstar '*' instead.
+ reducePrevDoublestar := func() {
+ if prevComponentsAre([]componentType{compSeparatorDoublestar}) {
+ // SeparatorDoublestar followed by anything except separator should
+ // replaced by a separator '/' and globstar '*'.
+ components[len(components)-1] = component{compType: compSeparator}
+ components = append(components, component{compType: compGlobstar})
+ }
+ }
+
+ // consumeText writes any accumulated runes as a literal component.
+ consumeText := func() {
+ if len(runes) > 0 {
+ reducePrevDoublestar()
+ components = append(components, component{compType: compLiteral, compText: string(runes)})
+ runes = nil
+ }
+ }
+
+ preparedVariant := prepareVariantForParsing(variant)
+
+ rr := strings.NewReader(preparedVariant)
+ for {
+ r, _, err := rr.ReadRune()
+ if err != nil {
+ if errors.Is(err, io.EOF) {
+ break
+ }
+ // Should not occur, err is only set if no rune available to read
+ return PatternVariant{}, fmt.Errorf("internal error: failed to read rune while scanning variant: %w", err)
+ }
+
+ switch r {
+ case '/':
+ consumeText()
+ if prevComponentsAre([]componentType{compSeparatorDoublestar, compSeparator, compGlobstar}) {
+ // Replace previous /**/* with /*/** before adding separator
+ components[len(components)-3] = component{compType: compSeparator}
+ components[len(components)-2] = component{compType: compGlobstar}
+ components[len(components)-1] = component{compType: compSeparatorDoublestar}
+ }
+ if !prevComponentsAre([]componentType{compSeparator}) {
+ // Collapse repeated separators
+ components = append(components, component{compType: compSeparator})
+ }
+ case '?':
+ reducePrevDoublestar()
+ consumeText()
+ if prevComponentsAre([]componentType{compGlobstar}) {
+ // Insert '?' before previous '*'
+ components[len(components)-1] = component{compType: compAnySingle}
+ components = append(components, component{compType: compGlobstar})
+ } else {
+ components = append(components, component{compType: compAnySingle})
+ }
+ case '⁑':
+ consumeText()
+ if prevComponentsAre([]componentType{compSeparatorDoublestar, compSeparator}) {
+ // Reduce /**/** to simply /** by removing most recent separator
+ components = components[:len(components)-1]
+ } else if prevComponentsAre([]componentType{compSeparator}) {
+ // Replace previous separator with separatorDoublestar
+ components[len(components)-1] = component{compType: compSeparatorDoublestar}
+ } else {
+ // Reduce to * since previous component is not a separator
+ addGlobstar()
+ }
+ case '*':
+ reducePrevDoublestar()
+ consumeText()
+ addGlobstar()
+ case '\\':
+ r2, _, err := rr.ReadRune()
+ if err != nil {
+ // Should be impossible, we just rendered this variant
+ return PatternVariant{}, errors.New(`internal error: trailing unescaped '\' character`)
+ }
+ switch r2 {
+ case '*', '?', '[', ']', '{', '}', '\\':
+ runes = append(runes, r, r2)
+ default:
+ // do not add r to runes if it's unnecessary
+ runes = append(runes, r2)
+ }
+ case '[', ']', '{', '}':
+ // Should be impossible, we just rendered this variant
+ return PatternVariant{}, fmt.Errorf(`internal error: unexpected unescaped '%v' character`, r)
+ default:
+ runes = append(runes, r)
+ }
+ }
+
+ consumeText()
+
+ if prevComponentsAre([]componentType{compSeparatorDoublestar, compSeparator, compGlobstar}) {
+ // If components end with /**/*, strip trailing /*
+ components = components[:len(components)-2]
+ }
+
+ // Add terminal marker or convert existing doublestar to terminal doublestar
+ if prevComponentsAre([]componentType{compSeparatorDoublestar, compSeparator}) {
+ components = components[:len(components)-2]
+ components = append(components, component{compType: compSeparatorDoublestarSeparatorTerminal})
+ } else if prevComponentsAre([]componentType{compSeparatorDoublestar}) {
+ components[len(components)-1] = component{compType: compSeparatorDoublestarTerminal}
+ } else {
+ components = append(components, component{compType: compTerminal})
+ }
+
+ var variantBuf strings.Builder
+ var regexBuf strings.Builder
+ regexBuf.WriteRune('^')
+ for _, c := range components {
+ variantBuf.WriteString(c.String())
+ regexBuf.WriteString(c.componentRegex())
+ }
+ regexBuf.WriteRune('$')
+ regex := regexpMustCompileLongest(regexBuf.String())
+
+ v := PatternVariant{
+ variant: variantBuf.String(),
+ components: components,
+ regex: regex,
+ }
+
+ return v, nil
+}
+
+// regexpMustCompileLongest compiles the given string into a Regexp and then
+// calls Longest() on it before returning it.
+func regexpMustCompileLongest(str string) *regexp.Regexp {
+ re := regexp.MustCompile(str)
+ re.Longest()
+ return re
+}
+
+// Need to escape any unescaped literal "⁑" runes before we use that symbol to
+// indicate the presence of a "**" doublestar.
+var doublestarEscaper = regexpMustCompileLongest(`((\\)*)⁑`)
+
+// Need to replace unescaped "**", but must be careful about an escaped '\\'
+// before the first '*', since that doesn't escape the first '*'.
+var doublestarReplacer = regexpMustCompileLongest(`((\\)*)\*\*`)
+
+// prepareVariantForParsing escapes any unescaped '⁑' characters and then
+// replaces any unescaped "**" with a single '⁑' so that doublestars can be
+// identified without needing to look ahead whenever a '*' is seen.
+func prepareVariantForParsing(variant string) string {
+ escaped := doublestarEscaper.ReplaceAllStringFunc(variant, func(s string) string {
+ if (len(s)-len("⁑"))%2 == 1 {
+ // Odd number of leading '\\'s, so already escaped
+ return s
+ }
+ // Escape any unescaped literal "⁑"
+ return s[:len(s)-len("⁑")] + `\` + "⁑"
+ })
+ prepared := doublestarReplacer.ReplaceAllStringFunc(escaped, func(s string) string {
+ if (len(s)-len("**"))%2 == 1 {
+ // Odd number of leading '\\'s, so escaped
+ return s
+ }
+ // Discard trailing "**", add "⁑" instead
+ return s[:len(s)-2] + "⁑"
+ })
+ return prepared
+}
+
+type componentReader struct {
+ components []component
+ submatches []string
+ index int
+}
+
+func (r *componentReader) next() (*component, string) {
+ if r.index >= len(r.components) {
+ return &component{compType: compTerminal}, ""
+ }
+ comp := &r.components[r.index]
+ submatch := r.submatches[r.index]
+ r.index++
+ return comp, submatch
+}
+
+// Compare returns the relative precence of the receiver and the given pattern
+// variant when considering the given matching path.
+//
+// Returns one of the following, if no error occurs:
+// -1 if v has lower precedence than other
+// 0 if v and other have equal precedence (only possible if v == other)
+// 1 if v has higher precedence than other.
+func (v PatternVariant) Compare(other PatternVariant, matchingPath string) (int, error) {
+ selfSubmatches := v.regex.FindStringSubmatch(matchingPath)
+ switch {
+ case selfSubmatches == nil:
+ return 0, fmt.Errorf("internal error: no matches for pattern variant against given path:\ncomponents: %+v\nregex: %s\npath: %s", v.components, v.regex.String(), matchingPath)
+ case len(selfSubmatches)-1 != len(v.components):
+ return 0, fmt.Errorf("internal error: submatch count not equal to component count:\ncomponents: %+v\nregex: %s\npath: %s", v.components, v.regex.String(), matchingPath)
+ }
+
+ otherSubmatches := other.regex.FindStringSubmatch(matchingPath)
+ if otherSubmatches == nil {
+ return 0, fmt.Errorf("internal error: no matches for pattern variant against given path\ncomponents: %+v\nregex: %s\npath: %s", other.components, other.regex.String(), matchingPath)
+ } else if len(otherSubmatches)-1 != len(other.components) {
+ return 0, fmt.Errorf("internal error: submatch count not equal to component count:\ncomponents: %+v\nregex: %s\npath: %s", other.components, other.regex.String(), matchingPath)
+ }
+
+ selfReader := componentReader{components: v.components, submatches: selfSubmatches[1:]}
+ otherReader := componentReader{components: other.components, submatches: otherSubmatches[1:]}
+
+loop:
+ for {
+ selfComp, selfSubmatch := selfReader.next()
+ otherComp, otherSubmatch := otherReader.next()
+ if selfComp.compType < otherComp.compType {
+ return -1, nil
+ } else if selfComp.compType > otherComp.compType {
+ return 1, nil
+ }
+ switch selfComp.compType {
+ case compGlobstar, compSeparatorDoublestar:
+ // Prioritize shorter matches for variable-width non-terminal
+ // components. We do this because the fewer characters are matched
+ // by a variable-width component (i.e. "*" or "/**", as terminal
+ // doublestar characters always match to the end of the path), the
+ // earlier in the path the next component matches, and thus provides
+ // provides greater specificity. Given equality up to the current
+ // position in the patterns, whichever pattern matches with greater
+ // specificity earlier in the path has precedence.
+ //
+ // For example, when computing precedence after matching `/foo/bar`:
+ // - `/foo/*b*` has precedence over
+ // - `/foo/*a*`, which has precedence over
+ // - `/foo/*r*`.
+ // All these patterns are {separator, literal, separator, globstar,
+ // literal, globstar, terminal}, but the distinction is that the
+ // globstar which matches fewer characters in the pattern implies
+ // that the next literal in the pattern matches earlier in the path,
+ // and so that pattern has precedence.
+ //
+ // Note: similar logic applies to:
+ // - `/foo/bar*` vs `/foo/ba*` vs `/foo/b*` and
+ // - `/foo/*bar` vs `/foo/*ar` vs `/foo/*r`.
+ // In these cases, however, the relative lengths of the literals
+ // would be sufficient to determine precedence.
+ //
+ // Likewise, all of the following match `/foo/bar/bazz/quxxx`:
+ // - `/foo/**/bar/bazz/quxxx` has precedence over
+ // - `/foo/**/bazz/quxxx`, which has precedence over
+ // - `/foo/**/quxxx`.
+ // If not for the precedence for fewest characters matched by the
+ // `/**`, the longest literal after the next separator, `quxxx`,
+ // would take precedence incorrectly. By prioritizing the doublestar
+ // which matches the fewest characters, the next component matches
+ // the earliest position in the path, and thus indicates precedence.
+ if len(selfSubmatch) > len(otherSubmatch) {
+ return -1, nil
+ } else if len(selfSubmatch) < len(otherSubmatch) {
+ return 1, nil
+ }
+ case compSeparatorDoublestarTerminal, compSeparatorDoublestarSeparatorTerminal, compTerminal:
+ break loop
+ case compLiteral:
+ // Prioritize longer literals (which must match exactly)
+ if selfSubmatch < otherSubmatch {
+ return -1, nil
+ } else if selfSubmatch > otherSubmatch {
+ return 1, nil
+ }
+ default:
+ continue
+ }
+ }
+ return 0, nil
+}
diff --git a/interfaces/prompting/patterns/variant_internal_test.go b/interfaces/prompting/patterns/variant_internal_test.go
new file mode 100644
index 00000000000..79684d3b1c8
--- /dev/null
+++ b/interfaces/prompting/patterns/variant_internal_test.go
@@ -0,0 +1,371 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*",
+ * Copyright (C) 2024 Canonical Ltd
+ *",
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *",
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *",
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, Text: see .
+ *",
+ */
+
+package patterns
+
+import (
+ . "gopkg.in/check.v1"
+)
+
+type variantSuite struct{}
+
+var _ = Suite(&variantSuite{})
+
+func (s *variantSuite) TestParsePatternVariant(c *C) {
+ for _, testCase := range []struct {
+ pattern string
+ preparedPattern string
+ components []component
+ variantStr string
+ }{
+ {
+ "/foo/bar/baz",
+ "/foo/bar/baz",
+ []component{
+ {compType: compSeparator},
+ {compType: compLiteral, compText: "foo"},
+ {compType: compSeparator},
+ {compType: compLiteral, compText: "bar"},
+ {compType: compSeparator},
+ {compType: compLiteral, compText: "baz"},
+ {compType: compTerminal},
+ },
+ "/foo/bar/baz",
+ },
+ {
+ "/foo/bar/baz/",
+ "/foo/bar/baz/",
+ []component{
+ {compType: compSeparator},
+ {compType: compLiteral, compText: "foo"},
+ {compType: compSeparator},
+ {compType: compLiteral, compText: "bar"},
+ {compType: compSeparator},
+ {compType: compLiteral, compText: "baz"},
+ {compType: compSeparator},
+ {compType: compTerminal},
+ },
+ "/foo/bar/baz/",
+ },
+ {
+ "/?o*/b?r/*a?/",
+ "/?o*/b?r/*a?/",
+ []component{
+ {compType: compSeparator},
+ {compType: compAnySingle},
+ {compType: compLiteral, compText: "o"},
+ {compType: compGlobstar},
+ {compType: compSeparator},
+ {compType: compLiteral, compText: "b"},
+ {compType: compAnySingle},
+ {compType: compLiteral, compText: "r"},
+ {compType: compSeparator},
+ {compType: compGlobstar},
+ {compType: compLiteral, compText: "a"},
+ {compType: compAnySingle},
+ {compType: compSeparator},
+ {compType: compTerminal},
+ },
+ "/?o*/b?r/*a?/",
+ },
+ {
+ "/foo////bar",
+ "/foo////bar",
+ []component{
+ {compType: compSeparator},
+ {compType: compLiteral, compText: "foo"},
+ {compType: compSeparator},
+ {compType: compLiteral, compText: "bar"},
+ {compType: compTerminal},
+ },
+ "/foo/bar",
+ },
+ {
+ "/foo**/bar",
+ "/foo⁑/bar",
+ []component{
+ {compType: compSeparator},
+ {compType: compLiteral, compText: "foo"},
+ {compType: compGlobstar},
+ {compType: compSeparator},
+ {compType: compLiteral, compText: "bar"},
+ {compType: compTerminal},
+ },
+ "/foo*/bar",
+ },
+ {
+ "/foo/**bar",
+ "/foo/⁑bar",
+ []component{
+ {compType: compSeparator},
+ {compType: compLiteral, compText: "foo"},
+ {compType: compSeparator},
+ {compType: compGlobstar},
+ {compType: compLiteral, compText: "bar"},
+ {compType: compTerminal},
+ },
+ "/foo/*bar",
+ },
+ {
+ "/foo/**/**/bar",
+ "/foo/⁑/⁑/bar",
+ []component{
+ {compType: compSeparator},
+ {compType: compLiteral, compText: "foo"},
+ {compType: compSeparatorDoublestar},
+ {compType: compSeparator},
+ {compType: compLiteral, compText: "bar"},
+ {compType: compTerminal},
+ },
+ "/foo/**/bar",
+ },
+ {
+ "/foo/**/*/bar",
+ "/foo/⁑/*/bar",
+ []component{
+ {compType: compSeparator},
+ {compType: compLiteral, compText: "foo"},
+ {compType: compSeparator},
+ {compType: compGlobstar},
+ {compType: compSeparatorDoublestar},
+ {compType: compSeparator},
+ {compType: compLiteral, compText: "bar"},
+ {compType: compTerminal},
+ },
+ "/foo/*/**/bar",
+ },
+ {
+ "/foo/**/**/",
+ "/foo/⁑/⁑/",
+ []component{
+ {compType: compSeparator},
+ {compType: compLiteral, compText: "foo"},
+ {compType: compSeparatorDoublestarSeparatorTerminal},
+ },
+ "/foo/**/",
+ },
+ {
+ "/foo/**/**",
+ "/foo/⁑/⁑",
+ []component{
+ {compType: compSeparator},
+ {compType: compLiteral, compText: "foo"},
+ {compType: compSeparatorDoublestarTerminal},
+ },
+ "/foo/**",
+ },
+ {
+ "/foo/**/*",
+ "/foo/⁑/*",
+ []component{
+ {compType: compSeparator},
+ {compType: compLiteral, compText: "foo"},
+ {compType: compSeparatorDoublestarTerminal},
+ },
+ "/foo/**",
+ },
+ {
+ "/foo/**?/*?*?*",
+ "/foo/⁑?/*?*?*",
+ []component{
+ {compType: compSeparator},
+ {compType: compLiteral, compText: "foo"},
+ {compType: compSeparator},
+ {compType: compAnySingle},
+ {compType: compGlobstar},
+ {compType: compSeparator},
+ {compType: compAnySingle},
+ {compType: compAnySingle},
+ {compType: compGlobstar},
+ {compType: compTerminal},
+ },
+ "/foo/?*/??*",
+ },
+ {
+ "/foo/**?/***?***?***",
+ "/foo/⁑?/⁑*?⁑*?⁑*",
+ []component{
+ {compType: compSeparator},
+ {compType: compLiteral, compText: "foo"},
+ {compType: compSeparator},
+ {compType: compAnySingle},
+ {compType: compGlobstar},
+ {compType: compSeparator},
+ {compType: compAnySingle},
+ {compType: compAnySingle},
+ {compType: compGlobstar},
+ {compType: compTerminal},
+ },
+ "/foo/?*/??*",
+ },
+ // Check that unicode in patterns treated as a single rune, and that escape
+ // characters are not counted, even when escaping unicode runes.
+ {
+ "/foo/🚵🚵",
+ "/foo/🚵🚵",
+ []component{
+ {compType: compSeparator},
+ {compType: compLiteral, compText: "foo"},
+ {compType: compSeparator},
+ {compType: compLiteral, compText: "🚵🚵"},
+ {compType: compTerminal},
+ },
+ "/foo/🚵🚵",
+ },
+ {
+ `/foo/\🚵\🚵\🚵\🚵\🚵`,
+ `/foo/\🚵\🚵\🚵\🚵\🚵`,
+ []component{
+ {compType: compSeparator},
+ {compType: compLiteral, compText: "foo"},
+ {compType: compSeparator},
+ {compType: compLiteral, compText: `🚵🚵🚵🚵🚵`},
+ {compType: compTerminal},
+ },
+ `/foo/🚵🚵🚵🚵🚵`,
+ },
+ {
+ `/foo/\\`,
+ `/foo/\\`,
+ []component{
+ {compType: compSeparator},
+ {compType: compLiteral, compText: "foo"},
+ {compType: compSeparator},
+ {compType: compLiteral, compText: `\\`},
+ {compType: compTerminal},
+ },
+ `/foo/\\`,
+ },
+ {
+ `/foo/⁑⁑⁑⁑⁑`,
+ `/foo/\⁑\⁑\⁑\⁑\⁑`,
+ []component{
+ {compType: compSeparator},
+ {compType: compLiteral, compText: "foo"},
+ {compType: compSeparator},
+ {compType: compLiteral, compText: `⁑⁑⁑⁑⁑`},
+ {compType: compTerminal},
+ },
+ `/foo/⁑⁑⁑⁑⁑`,
+ },
+ {
+ `/foo/⁑\\⁑\\⁑\\⁑\\⁑`,
+ `/foo/\⁑\\\⁑\\\⁑\\\⁑\\\⁑`,
+ []component{
+ {compType: compSeparator},
+ {compType: compLiteral, compText: "foo"},
+ {compType: compSeparator},
+ {compType: compLiteral, compText: `⁑\\⁑\\⁑\\⁑\\⁑`},
+ {compType: compTerminal},
+ },
+ `/foo/⁑\\⁑\\⁑\\⁑\\⁑`,
+ },
+ {
+ `/foo/⁑\⁑\\⁑\\\⁑\\\\⁑`,
+ `/foo/\⁑\⁑\\\⁑\\\⁑\\\\\⁑`,
+ []component{
+ {compType: compSeparator},
+ {compType: compLiteral, compText: "foo"},
+ {compType: compSeparator},
+ {compType: compLiteral, compText: `⁑⁑\\⁑\\⁑\\\\⁑`},
+ {compType: compTerminal},
+ },
+ `/foo/⁑⁑\\⁑\\⁑\\\\⁑`,
+ },
+ {
+ `/foo/**********`,
+ `/foo/⁑⁑⁑⁑⁑`,
+ []component{
+ {compType: compSeparator},
+ {compType: compLiteral, compText: "foo"},
+ {compType: compSeparatorDoublestarTerminal},
+ },
+ `/foo/**`,
+ },
+ {
+ `/foo/\**\**\**\**\**`,
+ `/foo/\**\**\**\**\**`,
+ []component{
+ {compType: compSeparator},
+ {compType: compLiteral, compText: "foo"},
+ {compType: compSeparator},
+ {compType: compLiteral, compText: `\*`},
+ {compType: compGlobstar},
+ {compType: compLiteral, compText: `\*`},
+ {compType: compGlobstar},
+ {compType: compLiteral, compText: `\*`},
+ {compType: compGlobstar},
+ {compType: compLiteral, compText: `\*`},
+ {compType: compGlobstar},
+ {compType: compLiteral, compText: `\*`},
+ {compType: compGlobstar},
+ {compType: compTerminal},
+ },
+ `/foo/\**\**\**\**\**`,
+ },
+ {
+ `/foo/**\\**\\**\\**\\**`,
+ `/foo/⁑\\⁑\\⁑\\⁑\\⁑`,
+ []component{
+ {compType: compSeparator},
+ {compType: compLiteral, compText: "foo"},
+ {compType: compSeparator},
+ {compType: compGlobstar},
+ {compType: compLiteral, compText: `\\`},
+ {compType: compGlobstar},
+ {compType: compLiteral, compText: `\\`},
+ {compType: compGlobstar},
+ {compType: compLiteral, compText: `\\`},
+ {compType: compGlobstar},
+ {compType: compLiteral, compText: `\\`},
+ {compType: compGlobstar},
+ {compType: compTerminal},
+ },
+ `/foo/*\\*\\*\\*\\*`,
+ },
+ {
+ `/foo/⁑**⁑\**\⁑*\\*⁑\\\**\\\⁑`,
+ `/foo/\⁑⁑\⁑\**\⁑*\\*\⁑\\\**\\\⁑`,
+ []component{
+ {compType: compSeparator},
+ {compType: compLiteral, compText: "foo"},
+ {compType: compSeparator},
+ {compType: compLiteral, compText: `⁑`},
+ {compType: compGlobstar},
+ {compType: compLiteral, compText: `⁑\*`},
+ {compType: compGlobstar},
+ {compType: compLiteral, compText: `⁑`},
+ {compType: compGlobstar},
+ {compType: compLiteral, compText: `\\`},
+ {compType: compGlobstar},
+ {compType: compLiteral, compText: `⁑\\\*`},
+ {compType: compGlobstar},
+ {compType: compLiteral, compText: `\\⁑`},
+ {compType: compTerminal},
+ },
+ `/foo/⁑*⁑\**⁑*\\*⁑\\\**\\⁑`,
+ },
+ } {
+ c.Check(prepareVariantForParsing(testCase.pattern), Equals, testCase.preparedPattern, Commentf("testCase: %+v", testCase))
+ variant, err := parsePatternVariant(testCase.pattern)
+ c.Assert(err, IsNil, Commentf("testCase: %+v", testCase))
+ c.Check(variant.components, DeepEquals, testCase.components, Commentf("testCase: %+v", testCase))
+ c.Check(variant.String(), DeepEquals, testCase.variantStr, Commentf("testCase: %+v", testCase))
+ }
+}