| // Copyright 2025 The Dawn & Tint Authors | |
| // | |
| // Redistribution and use in source and binary forms, with or without | |
| // modification, are permitted provided that the following conditions are met: | |
| // | |
| // 1. Redistributions of source code must retain the above copyright notice, this | |
| // list of conditions and the following disclaimer. | |
| // | |
| // 2. Redistributions in binary form must reproduce the above copyright notice, | |
| // this list of conditions and the following disclaimer in the documentation | |
| // and/or other materials provided with the distribution. | |
| // | |
| // 3. Neither the name of the copyright holder nor the names of its | |
| // contributors may be used to endorse or promote products derived from | |
| // this software without specific prior written permission. | |
| // | |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |
| // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
| // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |
| // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | |
| // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |
| // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | |
| // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | |
| // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | |
| // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| package reducedglob | |
| import ( | |
| "slices" | |
| "strings" | |
| ) | |
| // This package provides wildcard matching for "*" characters. However, unlike | |
| // third-party "glob" Go packages or Python's fnmatch module, this intentionally | |
| // does not match other types of wildcards such as "?". This is based on the | |
| // Python implementation in typ | |
| // https://chromium.googlesource.com/catapult/+/5b409767f0b2a05044d7139c3c000773da013775/third_party/typ/typ/reduced_glob.py | |
| const ( | |
| ESCAPED_WILDCARD = "\\*" | |
| UNESCAPED_WILDCARD = "*" | |
| ) | |
| // Struct representing a "compiled" glob that only matches "*" characters. | |
| // Should not be created directly. Use NewReducedGlob() instead. | |
| type ReducedGlob struct { | |
| pattern string | |
| substrings []string | |
| } | |
| // NewReducedGlob creates and returns a new ReducedGlob instance with the | |
| // provided pattern. | |
| func NewReducedGlob(pattern string) *ReducedGlob { | |
| rg := ReducedGlob{pattern: pattern} | |
| rg.computeSubstrings() | |
| return &rg | |
| } | |
| // computeSubstrings performs the one-time computations to get the list of | |
| // substrings a ReducedGlob looks for. | |
| func (rg *ReducedGlob) computeSubstrings() { | |
| // Find all indices of * characters, ignoring those escaped via \*. Then, use | |
| // that to create an ordered list of substrings that we need to have, with | |
| // any number of characters permitted between them. | |
| // Calculate indices of un-escaped wildcards. | |
| allWildcardIndices := findAllIndices(rg.pattern, UNESCAPED_WILDCARD) | |
| escapedWildcardIndices := findAllIndices(rg.pattern, ESCAPED_WILDCARD) | |
| // Offset by 1 so that the indices match those in allWildcardIndices. | |
| for i, value := range escapedWildcardIndices { | |
| escapedWildcardIndices[i] = value + 1 | |
| } | |
| unescapedWildcardIndices := []int{} | |
| for _, index := range allWildcardIndices { | |
| if !slices.Contains(escapedWildcardIndices, index) { | |
| unescapedWildcardIndices = append(unescapedWildcardIndices, index) | |
| } | |
| } | |
| // Split |pattern| into strings to match using the calculated indices. | |
| previousIndex := 0 | |
| for _, index := range unescapedWildcardIndices { | |
| rg.substrings = append(rg.substrings, rg.pattern[previousIndex:index]) | |
| previousIndex = index + 1 | |
| } | |
| rg.substrings = append(rg.substrings, rg.pattern[previousIndex:]) | |
| for i, substr := range rg.substrings { | |
| rg.substrings[i] = strings.ReplaceAll(substr, ESCAPED_WILDCARD, UNESCAPED_WILDCARD) | |
| } | |
| } | |
| // Matchcase checks whether |name| matches the stored pattern. Case-sensitive. | |
| func (rg *ReducedGlob) Matchcase(name string) bool { | |
| if len(rg.substrings) == 0 { | |
| panic("ReducedGlob used without calling computeSubstrings()") | |
| } | |
| // Look for each substring in order, shifting the starting point to avoid | |
| // anything we've matched already. | |
| startingIndex := 0 | |
| for i, substr := range rg.substrings { | |
| substrStartIndex := strings.Index(name[startingIndex:], substr) | |
| if substrStartIndex == -1 { | |
| return false | |
| } | |
| // The first substring is special since we need to ensure that |name| starts | |
| // with it. Otherwise, we could potentially match later in the string, which | |
| // would implicitly add a * to the front of the stored pattern. | |
| if i == 0 && substrStartIndex != 0 { | |
| return false | |
| } | |
| // Similarly, the last substring is special since we need to ensure that all | |
| // characters in |name| were matched. Otherwise, we would implicitly add a | |
| // * to the end of the stored pattern. | |
| if i+1 == len(rg.substrings) && !strings.HasSuffix(name, substr) { | |
| return false | |
| } | |
| // Consume everything we just matched. | |
| startingIndex += substrStartIndex + len(substr) | |
| } | |
| return true | |
| } | |
| // findAllIndices finds all indices where |substr| exists within |s|. | |
| func findAllIndices(s, substr string) []int { | |
| allIndices := []int{} | |
| offset := 0 | |
| index := strings.Index(s, substr) | |
| for index != -1 { | |
| allIndices = append(allIndices, index+offset) | |
| offset += index + 1 | |
| index = strings.Index(s[offset:], substr) | |
| } | |
| return allIndices | |
| } |