github.com/nektos/act@v0.2.63/pkg/exprparser/functions.go (about)

     1  package exprparser
     2  
     3  import (
     4  	"crypto/sha256"
     5  	"encoding/hex"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"io/fs"
    10  	"os"
    11  	"path/filepath"
    12  	"reflect"
    13  	"strconv"
    14  	"strings"
    15  
    16  	"github.com/go-git/go-git/v5/plumbing/format/gitignore"
    17  
    18  	"github.com/nektos/act/pkg/model"
    19  	"github.com/rhysd/actionlint"
    20  )
    21  
    22  func (impl *interperterImpl) contains(search, item reflect.Value) (bool, error) {
    23  	switch search.Kind() {
    24  	case reflect.String, reflect.Int, reflect.Float64, reflect.Bool, reflect.Invalid:
    25  		return strings.Contains(
    26  			strings.ToLower(impl.coerceToString(search).String()),
    27  			strings.ToLower(impl.coerceToString(item).String()),
    28  		), nil
    29  
    30  	case reflect.Slice:
    31  		for i := 0; i < search.Len(); i++ {
    32  			arrayItem := search.Index(i).Elem()
    33  			result, err := impl.compareValues(arrayItem, item, actionlint.CompareOpNodeKindEq)
    34  			if err != nil {
    35  				return false, err
    36  			}
    37  
    38  			if isEqual, ok := result.(bool); ok && isEqual {
    39  				return true, nil
    40  			}
    41  		}
    42  	}
    43  
    44  	return false, nil
    45  }
    46  
    47  func (impl *interperterImpl) startsWith(searchString, searchValue reflect.Value) (bool, error) {
    48  	return strings.HasPrefix(
    49  		strings.ToLower(impl.coerceToString(searchString).String()),
    50  		strings.ToLower(impl.coerceToString(searchValue).String()),
    51  	), nil
    52  }
    53  
    54  func (impl *interperterImpl) endsWith(searchString, searchValue reflect.Value) (bool, error) {
    55  	return strings.HasSuffix(
    56  		strings.ToLower(impl.coerceToString(searchString).String()),
    57  		strings.ToLower(impl.coerceToString(searchValue).String()),
    58  	), nil
    59  }
    60  
    61  const (
    62  	passThrough = iota
    63  	bracketOpen
    64  	bracketClose
    65  )
    66  
    67  func (impl *interperterImpl) format(str reflect.Value, replaceValue ...reflect.Value) (string, error) {
    68  	input := impl.coerceToString(str).String()
    69  	output := ""
    70  	replacementIndex := ""
    71  
    72  	state := passThrough
    73  	for _, character := range input {
    74  		switch state {
    75  		case passThrough: // normal buffer output
    76  			switch character {
    77  			case '{':
    78  				state = bracketOpen
    79  
    80  			case '}':
    81  				state = bracketClose
    82  
    83  			default:
    84  				output += string(character)
    85  			}
    86  
    87  		case bracketOpen: // found {
    88  			switch character {
    89  			case '{':
    90  				output += "{"
    91  				replacementIndex = ""
    92  				state = passThrough
    93  
    94  			case '}':
    95  				index, err := strconv.ParseInt(replacementIndex, 10, 32)
    96  				if err != nil {
    97  					return "", fmt.Errorf("The following format string is invalid: '%s'", input)
    98  				}
    99  
   100  				replacementIndex = ""
   101  
   102  				if len(replaceValue) <= int(index) {
   103  					return "", fmt.Errorf("The following format string references more arguments than were supplied: '%s'", input)
   104  				}
   105  
   106  				output += impl.coerceToString(replaceValue[index]).String()
   107  
   108  				state = passThrough
   109  
   110  			default:
   111  				replacementIndex += string(character)
   112  			}
   113  
   114  		case bracketClose: // found }
   115  			switch character {
   116  			case '}':
   117  				output += "}"
   118  				replacementIndex = ""
   119  				state = passThrough
   120  
   121  			default:
   122  				panic("Invalid format parser state")
   123  			}
   124  		}
   125  	}
   126  
   127  	if state != passThrough {
   128  		switch state {
   129  		case bracketOpen:
   130  			return "", fmt.Errorf("Unclosed brackets. The following format string is invalid: '%s'", input)
   131  
   132  		case bracketClose:
   133  			return "", fmt.Errorf("Closing bracket without opening one. The following format string is invalid: '%s'", input)
   134  		}
   135  	}
   136  
   137  	return output, nil
   138  }
   139  
   140  func (impl *interperterImpl) join(array reflect.Value, sep reflect.Value) (string, error) {
   141  	separator := impl.coerceToString(sep).String()
   142  	switch array.Kind() {
   143  	case reflect.Slice:
   144  		var items []string
   145  		for i := 0; i < array.Len(); i++ {
   146  			items = append(items, impl.coerceToString(array.Index(i).Elem()).String())
   147  		}
   148  
   149  		return strings.Join(items, separator), nil
   150  	default:
   151  		return strings.Join([]string{impl.coerceToString(array).String()}, separator), nil
   152  	}
   153  }
   154  
   155  func (impl *interperterImpl) toJSON(value reflect.Value) (string, error) {
   156  	if value.Kind() == reflect.Invalid {
   157  		return "null", nil
   158  	}
   159  
   160  	json, err := json.MarshalIndent(value.Interface(), "", "  ")
   161  	if err != nil {
   162  		return "", fmt.Errorf("Cannot convert value to JSON. Cause: %v", err)
   163  	}
   164  
   165  	return string(json), nil
   166  }
   167  
   168  func (impl *interperterImpl) fromJSON(value reflect.Value) (interface{}, error) {
   169  	if value.Kind() != reflect.String {
   170  		return nil, fmt.Errorf("Cannot parse non-string type %v as JSON", value.Kind())
   171  	}
   172  
   173  	var data interface{}
   174  
   175  	err := json.Unmarshal([]byte(value.String()), &data)
   176  	if err != nil {
   177  		return nil, fmt.Errorf("Invalid JSON: %v", err)
   178  	}
   179  
   180  	return data, nil
   181  }
   182  
   183  func (impl *interperterImpl) hashFiles(paths ...reflect.Value) (string, error) {
   184  	var ps []gitignore.Pattern
   185  
   186  	const cwdPrefix = "." + string(filepath.Separator)
   187  	const excludeCwdPrefix = "!" + cwdPrefix
   188  	for _, path := range paths {
   189  		if path.Kind() == reflect.String {
   190  			cleanPath := path.String()
   191  			if strings.HasPrefix(cleanPath, cwdPrefix) {
   192  				cleanPath = cleanPath[len(cwdPrefix):]
   193  			} else if strings.HasPrefix(cleanPath, excludeCwdPrefix) {
   194  				cleanPath = "!" + cleanPath[len(excludeCwdPrefix):]
   195  			}
   196  			ps = append(ps, gitignore.ParsePattern(cleanPath, nil))
   197  		} else {
   198  			return "", fmt.Errorf("Non-string path passed to hashFiles")
   199  		}
   200  	}
   201  
   202  	matcher := gitignore.NewMatcher(ps)
   203  
   204  	var files []string
   205  	if err := filepath.Walk(impl.config.WorkingDir, func(path string, fi fs.FileInfo, err error) error {
   206  		if err != nil {
   207  			return err
   208  		}
   209  		sansPrefix := strings.TrimPrefix(path, impl.config.WorkingDir+string(filepath.Separator))
   210  		parts := strings.Split(sansPrefix, string(filepath.Separator))
   211  		if fi.IsDir() || !matcher.Match(parts, fi.IsDir()) {
   212  			return nil
   213  		}
   214  		files = append(files, path)
   215  		return nil
   216  	}); err != nil {
   217  		return "", fmt.Errorf("Unable to filepath.Walk: %v", err)
   218  	}
   219  
   220  	if len(files) == 0 {
   221  		return "", nil
   222  	}
   223  
   224  	hasher := sha256.New()
   225  
   226  	for _, file := range files {
   227  		f, err := os.Open(file)
   228  		if err != nil {
   229  			return "", fmt.Errorf("Unable to os.Open: %v", err)
   230  		}
   231  
   232  		if _, err := io.Copy(hasher, f); err != nil {
   233  			return "", fmt.Errorf("Unable to io.Copy: %v", err)
   234  		}
   235  
   236  		if err := f.Close(); err != nil {
   237  			return "", fmt.Errorf("Unable to Close file: %v", err)
   238  		}
   239  	}
   240  
   241  	return hex.EncodeToString(hasher.Sum(nil)), nil
   242  }
   243  
   244  func (impl *interperterImpl) getNeedsTransitive(job *model.Job) []string {
   245  	needs := job.Needs()
   246  
   247  	for _, need := range needs {
   248  		parentNeeds := impl.getNeedsTransitive(impl.config.Run.Workflow.GetJob(need))
   249  		needs = append(needs, parentNeeds...)
   250  	}
   251  
   252  	return needs
   253  }
   254  
   255  func (impl *interperterImpl) always() (bool, error) {
   256  	return true, nil
   257  }
   258  
   259  func (impl *interperterImpl) jobSuccess() (bool, error) {
   260  	jobs := impl.config.Run.Workflow.Jobs
   261  	jobNeeds := impl.getNeedsTransitive(impl.config.Run.Job())
   262  
   263  	for _, needs := range jobNeeds {
   264  		if jobs[needs].Result != "success" {
   265  			return false, nil
   266  		}
   267  	}
   268  
   269  	return true, nil
   270  }
   271  
   272  func (impl *interperterImpl) stepSuccess() (bool, error) {
   273  	return impl.env.Job.Status == "success", nil
   274  }
   275  
   276  func (impl *interperterImpl) jobFailure() (bool, error) {
   277  	jobs := impl.config.Run.Workflow.Jobs
   278  	jobNeeds := impl.getNeedsTransitive(impl.config.Run.Job())
   279  
   280  	for _, needs := range jobNeeds {
   281  		if jobs[needs].Result == "failure" {
   282  			return true, nil
   283  		}
   284  	}
   285  
   286  	return false, nil
   287  }
   288  
   289  func (impl *interperterImpl) stepFailure() (bool, error) {
   290  	return impl.env.Job.Status == "failure", nil
   291  }
   292  
   293  func (impl *interperterImpl) cancelled() (bool, error) {
   294  	return impl.env.Job.Status == "cancelled", nil
   295  }