-
Notifications
You must be signed in to change notification settings - Fork 16
fix: replace Pattern.Includes with Contains and Overlaps #644
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
joe0BAB
wants to merge
2
commits into
main
Choose a base branch
from
fix/include-check
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+206
−100
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,15 +22,9 @@ import ( | |
|
|
||
| var ErrInvalidPattern = errors.New("invalid pattern") | ||
|
|
||
| // validPattern checks if a pattern is valid without using regexp or unicode. | ||
| // Rules: | ||
| // - Components separated by '/' | ||
| // - Each component is non-empty | ||
| // - Only characters A-Z, a-z, 0-9, '.', '-', '_' or '*' | ||
| // - No leading, trailing, or double slashes | ||
| // - Asterisks rules: | ||
| // - '*' cannot be mixed with other characters in the same component | ||
| // - there can be no more than two '*' per component | ||
| // validPattern reports whether s is a valid pattern: non-empty '/'-separated | ||
| // components of A-Z, a-z, 0-9, '.', '-', '_', ':', where a component may | ||
| // instead be '*' or '**'. | ||
| func validPattern(s string) bool { | ||
| if len(s) == 0 { | ||
| return false | ||
|
|
@@ -77,15 +71,36 @@ func isValidPatternRune(c rune) bool { | |
| return isValidRune(c) || c == '*' | ||
| } | ||
|
|
||
| // Pattern can be used to match secret identifiers. | ||
| // Valid patterns must follow the same validation rules as secret identifiers, with the exception | ||
| // that '*' can be used to match a single component, and '**' can be used to match zero or more components. | ||
| // Pattern matches secret IDs. It follows the ID validation rules, except | ||
| // that '*' matches one component and '**' matches zero or more. | ||
| // Below, matches(p) denotes the set of IDs a pattern p matches. | ||
| type Pattern interface { | ||
| // Match the [Pattern] against an [ID] | ||
| // Match reports whether the pattern matches id. | ||
| // Complexity: O(n*m^k), where id has n components and the pattern has | ||
| // m components and k occurrences of '**'. | ||
| Match(id ID) bool | ||
| // Includes returns true if all matches of Pattern [other] are also matches of the current pattern. | ||
| Includes(other Pattern) bool | ||
| // String formats the [Pattern] as a string | ||
| // Contains reports whether every ID that [other] matches can also be | ||
| // matched by the pattern: the set of IDs [other] matches is contained | ||
| // in the set the pattern matches, i.e., matches(other) ⊆ matches(p). | ||
| // | ||
| // Examples: | ||
| // - docker/** contains docker/*/mcp/*: '**' is more general than | ||
| // '*/mcp/*'. | ||
| // - docker/proj1/** does not contain docker/*/mcp/*: docker/*/mcp/* | ||
| // matches docker/proj2/mcp/x, but docker/proj1/** does not. | ||
| // | ||
| // Complexity: O(n*m) | ||
| Contains(other Pattern) bool | ||
| // Overlaps reports whether the pattern and [other] match at least one | ||
| // ID in common, i.e., matches(p) ∩ matches(other) ≠ ∅. | ||
| // | ||
| // Examples: | ||
| // - docker/*/mcp/* and docker/proj1/** overlap: both match e.g. docker/proj1/mcp/x. | ||
| // - bar/** and foo/** do not overlap: an ID cannot begin with both bar and foo. | ||
| // | ||
| // Complexity: O(n*m) | ||
| Overlaps(other Pattern) bool | ||
|
Comment on lines
+94
to
+102
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think Overlaps needs a bit more explaining or better godocs. Difficult to understand the concept of
|
||
| // String returns the pattern text. | ||
| String() string | ||
|
|
||
| ExpandID(other ID) (ID, error) | ||
|
|
@@ -101,35 +116,146 @@ func (p pattern) Match(id ID) bool { | |
| return match(patternParts, pathParts) | ||
| } | ||
|
|
||
| func (p pattern) Includes(other Pattern) bool { | ||
| otherParts := split(other.String()) | ||
| patternParts := split(string(p)) | ||
| func (p pattern) Contains(other Pattern) bool { | ||
| return covers(canonicalize(string(p)), canonicalize(other.String())) | ||
| } | ||
|
|
||
| func (p pattern) Overlaps(other Pattern) bool { | ||
| return compatible(canonicalize(string(p)), canonicalize(other.String())) | ||
| } | ||
|
|
||
| type tokenKind uint8 | ||
|
|
||
| const ( | ||
| tokenLit tokenKind = iota | ||
| tokenStar | ||
| tokenGap | ||
| ) | ||
|
|
||
| type token struct { | ||
| kind tokenKind | ||
| lit string | ||
| } | ||
|
|
||
| // canonicalize tokenizes a pattern so that two patterns match the same set | ||
| // of IDs exactly when their canonical tokens are equal. Two rewrites give | ||
| // that property: | ||
| // | ||
| // - In a run of consecutive wildcard components, the '*' move to the | ||
| // front and the '**' merge into one at the end: order within a run | ||
| // does not affect what it matches, and one '**' already matches any | ||
| // number of components. | ||
| // - A pattern consisting only of '**' becomes "*/**": an ID always has | ||
| // at least one component, so both match the same IDs, and the first | ||
| // rewrite already turns the equivalent spelling "**/*" into "*/**". | ||
| // | ||
| // Examples: | ||
| // | ||
| // a/a/** and a/a/**/** -> [a a **] | ||
| // a/**/*/**/b -> [a * ** b] | ||
| // ** and **/* -> [* **] | ||
| func canonicalize(s string) []token { | ||
| parts := split(s) | ||
| toks := make([]token, 0, len(parts)) | ||
| stars, gap := 0, false | ||
| flush := func() { | ||
| for range stars { | ||
| toks = append(toks, token{kind: tokenStar}) | ||
| } | ||
| if gap { | ||
| toks = append(toks, token{kind: tokenGap}) | ||
| } | ||
| stars, gap = 0, false | ||
| } | ||
| for _, part := range parts { | ||
| switch part { | ||
| case "*": | ||
| stars++ | ||
| case "**": | ||
| gap = true | ||
| default: | ||
| flush() | ||
| toks = append(toks, token{kind: tokenLit, lit: part}) | ||
| } | ||
| } | ||
| flush() | ||
| if len(toks) == 1 && toks[0].kind == tokenGap { | ||
| toks = []token{{kind: tokenStar}, {kind: tokenGap}} | ||
| } | ||
| return toks | ||
| } | ||
|
|
||
| return match(patternParts, otherParts) | ||
| // covers reports whether p matches every ID q matches, for canonical tokens. | ||
| // The component alphabet is unbounded, so a literal in p never covers an '*' | ||
| // or '**' in q. | ||
| func covers(p, q []token) bool { | ||
| np, nq := len(p), len(q) | ||
| prev := make([]bool, nq+1) | ||
| cur := make([]bool, nq+1) | ||
| prev[nq] = true | ||
| for i := np - 1; i >= 0; i-- { | ||
| cur[nq] = p[i].kind == tokenGap && prev[nq] | ||
| for j := nq - 1; j >= 0; j-- { | ||
| switch { | ||
| case p[i].kind == tokenGap: | ||
| cur[j] = prev[j] || cur[j+1] | ||
| case q[j].kind == tokenGap: | ||
| cur[j] = p[i].kind == tokenStar && cur[j+1] && prev[j] | ||
| case p[i].kind == tokenStar: | ||
| cur[j] = prev[j+1] | ||
| default: | ||
| cur[j] = q[j].kind == tokenLit && p[i].lit == q[j].lit && prev[j+1] | ||
| } | ||
| } | ||
| prev, cur = cur, prev | ||
| } | ||
| return prev[0] | ||
| } | ||
|
|
||
| // compatible reports whether p and q match at least one ID in common, for | ||
| // canonical tokens. A common ID aligns both token lists over its | ||
| // components: wildcards accept anything, so only differing literals or | ||
| // mismatched lengths (* vs */foo) rule one out. | ||
| func compatible(p, q []token) bool { | ||
| np, nq := len(p), len(q) | ||
| prev := make([]bool, nq+1) | ||
| cur := make([]bool, nq+1) | ||
| prev[nq] = true | ||
| for j := nq - 1; j >= 0; j-- { | ||
| prev[j] = q[j].kind == tokenGap && prev[j+1] | ||
| } | ||
| for i := np - 1; i >= 0; i-- { | ||
| cur[nq] = p[i].kind == tokenGap && prev[nq] | ||
| for j := nq - 1; j >= 0; j-- { | ||
| switch { | ||
| case p[i].kind == tokenGap || q[j].kind == tokenGap: | ||
| cur[j] = prev[j] || cur[j+1] | ||
| case p[i].kind == tokenLit && q[j].kind == tokenLit && p[i].lit != q[j].lit: | ||
| cur[j] = false | ||
| default: | ||
| cur[j] = prev[j+1] | ||
| } | ||
| } | ||
| prev, cur = cur, prev | ||
| } | ||
| return prev[0] | ||
| } | ||
|
|
||
| func (p pattern) String() string { | ||
| return string(p) | ||
| } | ||
|
|
||
| // ParsePattern parses a string into a [Pattern] | ||
| // Rules: | ||
| // - Components separated by '/' | ||
| // - Each component is non-empty | ||
| // - Only characters A-Z, a-z, 0-9, '.', '-', '_' or '*' | ||
| // - No leading, trailing, or double slashes | ||
| // - Asterisks rules: | ||
| // - '*' cannot be mixed with other characters in the same component | ||
| // - there can be no more than two '*' per component | ||
| // ParsePattern parses s into a [Pattern]: non-empty '/'-separated components | ||
| // of A-Z, a-z, 0-9, '.', '-', '_', ':', where a component may instead be '*' | ||
| // or '**'. It returns [ErrInvalidPattern] for anything else. | ||
| func ParsePattern(s string) (Pattern, error) { | ||
| if !validPattern(s) { | ||
| return nil, ErrInvalidPattern | ||
| } | ||
| return pattern(s), nil | ||
| } | ||
|
|
||
| // MustParsePattern parses a string into a [Pattern] like with [ParsePattern], | ||
| // however, it panics when a validation error occurs. | ||
| // MustParsePattern is like [ParsePattern] but panics on an invalid pattern. | ||
| func MustParsePattern(s string) Pattern { | ||
| if !validPattern(s) { | ||
| panic(ErrInvalidPattern) | ||
|
|
@@ -153,23 +279,6 @@ func (p pattern) ExpandPattern(other Pattern) (Pattern, error) { | |
| return pattern(val), err | ||
| } | ||
|
|
||
| // Filter returns a reduced [Pattern] that is subset equal to [filter]. | ||
| // Returns false if there's no overlap between [filter] and [other]. | ||
| // Examples: | ||
| // - Filter(MustParsePattern("bar/**"), MustParsePattern("**")) => returns "bar/**" | ||
| // - Filter(MustParsePattern("**"), MustParsePattern("**")) => returns "**" | ||
| // - Filter(MustParsePattern("bar/**"), MustParsePattern("bar")) => returns "bar" | ||
| // - Filter(MustParsePattern("bar/**"), MustParsePattern("foo/**")) => returns false | ||
| func Filter(filter, other Pattern) (Pattern, bool) { | ||
| if filter.Includes(other) { | ||
| return other, true | ||
| } | ||
| if other.Includes(filter) { | ||
| return filter, true | ||
| } | ||
| return nil, false | ||
| } | ||
|
|
||
| func replace1(original, other string) (string, error) { | ||
| components := split(original) | ||
| var candidates []int | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.