sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/gitattributes/pattern.go (about)

     1  /*
     2  Copyright 2019 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package gitattributes
    18  
    19  import (
    20  	"fmt"
    21  	"path/filepath"
    22  	"strings"
    23  )
    24  
    25  const (
    26  	negativePrefix = "!"
    27  	patternDirSep  = "/"
    28  	zeroToManyDirs = "**"
    29  )
    30  
    31  type pattern struct {
    32  	pattern []string
    33  	isPath  bool
    34  }
    35  
    36  // parsePattern parses a gitattributes pattern string into the Pattern structure.
    37  // The rules by which the pattern matches paths are the same as in .gitignore files (see https://git-scm.com/docs/gitignore), with a few exceptions:
    38  //   - negative patterns are forbidden
    39  //   - patterns that match a directory do not recursively match paths inside that directory
    40  //
    41  // https://git-scm.com/docs/gitattributes
    42  func parsePattern(p string) (Pattern, error) {
    43  	res := pattern{}
    44  
    45  	// negative patterns are forbidden
    46  	if strings.HasPrefix(p, negativePrefix) {
    47  		return nil, fmt.Errorf("negative patterns are forbidden: <%s>", p)
    48  	}
    49  
    50  	// trailing spaces are ignored unless they are quoted with backslash
    51  	if !strings.HasSuffix(p, `\ `) {
    52  		p = strings.TrimRight(p, " ")
    53  	}
    54  
    55  	// patterns that match a directory do not recursively match paths inside that directory
    56  	if strings.HasSuffix(p, patternDirSep) {
    57  		return nil, fmt.Errorf("directory patterns are not matched recursively, use path/** instead: <%s>", p)
    58  	}
    59  
    60  	if strings.Contains(p, patternDirSep) {
    61  		res.isPath = true
    62  	}
    63  
    64  	res.pattern = strings.Split(p, patternDirSep)
    65  	return &res, nil
    66  }
    67  
    68  func (p *pattern) Match(path string) bool {
    69  	pathElements := strings.Split(path, "/")
    70  	if p.isPath && p.pathMatch(pathElements) {
    71  		return true
    72  	} else if !p.isPath && p.nameMatch(pathElements) {
    73  		return true
    74  	}
    75  	return false
    76  }
    77  
    78  func (p *pattern) nameMatch(path []string) bool {
    79  	for _, name := range path {
    80  		if match, err := filepath.Match(p.pattern[0], name); err != nil {
    81  			return false
    82  		} else if !match {
    83  			continue
    84  		}
    85  		return true
    86  	}
    87  	return false
    88  }
    89  
    90  func (p *pattern) pathMatch(path []string) bool {
    91  	matched := false
    92  	canTraverse := false
    93  	for i, pattern := range p.pattern {
    94  		if pattern == "" {
    95  			canTraverse = false
    96  			continue
    97  		}
    98  		if pattern == zeroToManyDirs {
    99  			if i == len(p.pattern)-1 {
   100  				break
   101  			}
   102  			canTraverse = true
   103  			continue
   104  		}
   105  		if strings.Contains(pattern, zeroToManyDirs) {
   106  			return false
   107  		}
   108  		if len(path) == 0 {
   109  			return false
   110  		}
   111  		if canTraverse {
   112  			canTraverse = false
   113  			for len(path) > 0 {
   114  				e := path[0]
   115  				path = path[1:]
   116  				if match, err := filepath.Match(pattern, e); err != nil {
   117  					return false
   118  				} else if match {
   119  					matched = true
   120  					break
   121  				} else if len(path) == 0 {
   122  					// if nothing left then fail
   123  					matched = false
   124  				}
   125  			}
   126  		} else {
   127  			if match, err := filepath.Match(pattern, path[0]); err != nil || !match {
   128  				return false
   129  			}
   130  			matched = true
   131  			path = path[1:]
   132  		}
   133  	}
   134  	return matched
   135  }