github.com/nektos/act@v0.2.63/pkg/workflowpattern/workflow_pattern.go (about)

     1  package workflowpattern
     2  
     3  import (
     4  	"fmt"
     5  	"regexp"
     6  	"strings"
     7  )
     8  
     9  type WorkflowPattern struct {
    10  	Pattern  string
    11  	Negative bool
    12  	Regex    *regexp.Regexp
    13  }
    14  
    15  func CompilePattern(rawpattern string) (*WorkflowPattern, error) {
    16  	negative := false
    17  	pattern := rawpattern
    18  	if strings.HasPrefix(rawpattern, "!") {
    19  		negative = true
    20  		pattern = rawpattern[1:]
    21  	}
    22  	rpattern, err := PatternToRegex(pattern)
    23  	if err != nil {
    24  		return nil, err
    25  	}
    26  	regex, err := regexp.Compile(rpattern)
    27  	if err != nil {
    28  		return nil, err
    29  	}
    30  	return &WorkflowPattern{
    31  		Pattern:  pattern,
    32  		Negative: negative,
    33  		Regex:    regex,
    34  	}, nil
    35  }
    36  
    37  //nolint:gocyclo
    38  func PatternToRegex(pattern string) (string, error) {
    39  	var rpattern strings.Builder
    40  	rpattern.WriteString("^")
    41  	pos := 0
    42  	errors := map[int]string{}
    43  	for pos < len(pattern) {
    44  		switch pattern[pos] {
    45  		case '*':
    46  			if pos+1 < len(pattern) && pattern[pos+1] == '*' {
    47  				if pos+2 < len(pattern) && pattern[pos+2] == '/' {
    48  					rpattern.WriteString("(.+/)?")
    49  					pos += 3
    50  				} else {
    51  					rpattern.WriteString(".*")
    52  					pos += 2
    53  				}
    54  			} else {
    55  				rpattern.WriteString("[^/]*")
    56  				pos++
    57  			}
    58  		case '+', '?':
    59  			if pos > 0 {
    60  				rpattern.WriteByte(pattern[pos])
    61  			} else {
    62  				rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos]})))
    63  			}
    64  			pos++
    65  		case '[':
    66  			rpattern.WriteByte(pattern[pos])
    67  			pos++
    68  			if pos < len(pattern) && pattern[pos] == ']' {
    69  				errors[pos] = "Unexpected empty brackets '[]'"
    70  				pos++
    71  				break
    72  			}
    73  			validChar := func(a, b, test byte) bool {
    74  				return test >= a && test <= b
    75  			}
    76  			startPos := pos
    77  			for pos < len(pattern) && pattern[pos] != ']' {
    78  				switch pattern[pos] {
    79  				case '-':
    80  					if pos <= startPos || pos+1 >= len(pattern) {
    81  						errors[pos] = "Invalid range"
    82  						pos++
    83  						break
    84  					}
    85  					validRange := func(a, b byte) bool {
    86  						return validChar(a, b, pattern[pos-1]) && validChar(a, b, pattern[pos+1]) && pattern[pos-1] <= pattern[pos+1]
    87  					}
    88  					if !validRange('A', 'z') && !validRange('0', '9') {
    89  						errors[pos] = "Ranges can only include a-z, A-Z, A-z, and 0-9"
    90  						pos++
    91  						break
    92  					}
    93  					rpattern.WriteString(pattern[pos : pos+2])
    94  					pos += 2
    95  				default:
    96  					if !validChar('A', 'z', pattern[pos]) && !validChar('0', '9', pattern[pos]) {
    97  						errors[pos] = "Ranges can only include a-z, A-Z and 0-9"
    98  						pos++
    99  						break
   100  					}
   101  					rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos]})))
   102  					pos++
   103  				}
   104  			}
   105  			if pos >= len(pattern) || pattern[pos] != ']' {
   106  				errors[pos] = "Missing closing bracket ']' after '['"
   107  				pos++
   108  			}
   109  			rpattern.WriteString("]")
   110  			pos++
   111  		case '\\':
   112  			if pos+1 >= len(pattern) {
   113  				errors[pos] = "Missing symbol after \\"
   114  				pos++
   115  				break
   116  			}
   117  			rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos+1]})))
   118  			pos += 2
   119  		default:
   120  			rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos]})))
   121  			pos++
   122  		}
   123  	}
   124  	if len(errors) > 0 {
   125  		var errorMessage strings.Builder
   126  		for position, err := range errors {
   127  			if errorMessage.Len() > 0 {
   128  				errorMessage.WriteString(", ")
   129  			}
   130  			errorMessage.WriteString(fmt.Sprintf("Position: %d Error: %s", position, err))
   131  		}
   132  		return "", fmt.Errorf("invalid Pattern '%s': %s", pattern, errorMessage.String())
   133  	}
   134  	rpattern.WriteString("$")
   135  	return rpattern.String(), nil
   136  }
   137  
   138  func CompilePatterns(patterns ...string) ([]*WorkflowPattern, error) {
   139  	ret := []*WorkflowPattern{}
   140  	for _, pattern := range patterns {
   141  		cp, err := CompilePattern(pattern)
   142  		if err != nil {
   143  			return nil, err
   144  		}
   145  		ret = append(ret, cp)
   146  	}
   147  	return ret, nil
   148  }
   149  
   150  // returns true if the workflow should be skipped paths/branches
   151  func Skip(sequence []*WorkflowPattern, input []string, traceWriter TraceWriter) bool {
   152  	if len(sequence) == 0 {
   153  		return false
   154  	}
   155  	for _, file := range input {
   156  		matched := false
   157  		for _, item := range sequence {
   158  			if item.Regex.MatchString(file) {
   159  				pattern := item.Pattern
   160  				if item.Negative {
   161  					matched = false
   162  					traceWriter.Info("%s excluded by pattern %s", file, pattern)
   163  				} else {
   164  					matched = true
   165  					traceWriter.Info("%s included by pattern %s", file, pattern)
   166  				}
   167  			}
   168  		}
   169  		if matched {
   170  			return false
   171  		}
   172  	}
   173  	return true
   174  }
   175  
   176  // returns true if the workflow should be skipped paths-ignore/branches-ignore
   177  func Filter(sequence []*WorkflowPattern, input []string, traceWriter TraceWriter) bool {
   178  	if len(sequence) == 0 {
   179  		return false
   180  	}
   181  	for _, file := range input {
   182  		matched := false
   183  		for _, item := range sequence {
   184  			if item.Regex.MatchString(file) == !item.Negative {
   185  				pattern := item.Pattern
   186  				traceWriter.Info("%s ignored by pattern %s", file, pattern)
   187  				matched = true
   188  				break
   189  			}
   190  		}
   191  		if !matched {
   192  			return false
   193  		}
   194  	}
   195  	return true
   196  }