blob: d3930ee13a4b946e418b0c4d665b68df3b0c416f [file] [log] [blame]
// 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
}