github.com/treeverse/lakefs@v1.24.1-0.20240520134607-95648127bfb0/pkg/auth/wildcard/match.go (about)

     1  package wildcard
     2  
     3  import (
     4  	"unicode/utf8"
     5  )
     6  
     7  // Match reports whether name matches the shell pattern.
     8  // This is a strip down version of Go's `path.Match` https://pkg.go.dev/path#Match
     9  // Call a "fixword" a maximal portion of the pattern consisting only of regular characters and ?s.
    10  // So a fixword has to begin after * or at the beginning of the string, and it has to end before * or at the end of the string.
    11  // Each fixword matches a fixed length of string. Now a pattern is a list of fixwords separated by *s.
    12  // Consider a fixword that is not preceded by a *; that's an easy match to find because it can only be at one place.
    13  // Consider a fixword that is preceded by a *; if it matches at multiple places then it is always safe to match it at
    14  // the first possible location: either the pattern ends after that fixword in which case there's only one possible location,
    15  // or the pattern continues with *, in which case that * can "expand" to pick up all characters and the next match of the fixword.
    16  func Match(pattern, name string) bool {
    17  Pattern:
    18  	for len(pattern) > 0 {
    19  		var (
    20  			star  bool
    21  			chunk string
    22  		)
    23  		star, chunk, pattern = scanChunk(pattern)
    24  		if star && chunk == "" {
    25  			// Trailing * matches rest of string
    26  			return true
    27  		}
    28  		// Look for match at current position.
    29  		t, ok := matchChunk(chunk, name)
    30  		// if we're the last chunk, make sure we've exhausted the name
    31  		// otherwise we'll give a false result even if we could still match
    32  		// using the star
    33  		if ok && (len(t) == 0 || len(pattern) > 0) {
    34  			name = t
    35  			continue
    36  		}
    37  		if star {
    38  			// Look for match skipping i+1 bytes.
    39  			for i := 0; i < len(name); i++ {
    40  				t, ok := matchChunk(chunk, name[i+1:])
    41  				if ok {
    42  					// if we're the last chunk, make sure we exhausted the name
    43  					if len(pattern) == 0 && len(t) > 0 {
    44  						continue
    45  					}
    46  					name = t
    47  					continue Pattern
    48  				}
    49  			}
    50  		}
    51  		return false
    52  	}
    53  	return len(name) == 0
    54  }
    55  
    56  // scanChunk gets the next segment of pattern, which is a non-star string
    57  // possibly preceded by a star.
    58  func scanChunk(pattern string) (star bool, chunk, rest string) {
    59  	for len(pattern) > 0 && pattern[0] == '*' {
    60  		pattern = pattern[1:]
    61  		star = true
    62  	}
    63  	for i := 1; i < len(pattern); i++ {
    64  		if pattern[i] == '*' {
    65  			return star, pattern[0:i], pattern[i:]
    66  		}
    67  	}
    68  	return star, pattern, ""
    69  }
    70  
    71  // matchChunk checks whether chunk matches the beginning of s.
    72  // If so, it returns the remainder of s (after the match).
    73  // Chunk is all single-character operators: literals, char classes, and ?.
    74  func matchChunk(chunk, s string) (rest string, ok bool) {
    75  	for len(chunk) > 0 {
    76  		if len(s) == 0 {
    77  			return "", false
    78  		}
    79  		n := 1
    80  		if chunk[0] == '?' {
    81  			_, n = utf8.DecodeRuneInString(s)
    82  		} else if chunk[0] != s[0] {
    83  			return "", false
    84  		}
    85  		s = s[n:]
    86  		chunk = chunk[1:]
    87  	}
    88  	return s, true
    89  }