pkg/internal/expression/interpolation.go (108 lines of code) (raw):

package expression import ( "fmt" "regexp" "google.golang.org/protobuf/types/known/structpb" "gitlab.com/gitlab-org/step-runner/pkg/context" ) const InterpolateOpen = "${{" const InterpolateClose = "}}" var interpolateRegex = regexp.MustCompile(regexp.QuoteMeta(InterpolateOpen) + "|" + regexp.QuoteMeta(InterpolateClose)) func interpolateString(obj *InterpolationContext, value string) (*context.Value, error) { output := []*structpb.Value{} depth := 0 prev_idx := 0 open_idx := 0 sensitive := false sensitiveReasons := make([]string, 0) for _, loc := range interpolateRegex.FindAllStringIndex(value, -1) { if depth == 0 && prev_idx != loc[0] { // add prefix to output output = append(output, structpb.NewStringValue(value[prev_idx:loc[0]])) } prev_idx = loc[1] switch value[loc[0]:loc[1]] { case InterpolateOpen: depth += 1 if depth == 1 { open_idx = loc[1] } case InterpolateClose: depth -= 1 insideString := value[open_idx:loc[0]] if depth < 0 { return nil, fmt.Errorf("The %q has extra '}}'", insideString) } else if depth > 0 { break } insideValue, err := Evaluate(obj, insideString) if err != nil { return nil, err } if insideValue.Sensitive { sensitive = true sensitiveReasons = append(sensitiveReasons, insideValue.SensitiveReason) } output = append(output, insideValue.Value) default: } } if depth > 0 { return nil, fmt.Errorf("The %q is not closed: ${{ ... }}", value[open_idx:]) } // add suffix to output if prev_idx != len(value) { output = append(output, structpb.NewStringValue(value[prev_idx:])) } // retain type if this is single item, otherwise convert to string if len(output) == 1 { return context.NewValue(output[0], sensitive, sensitiveReasons...), nil } // concat all items res := "" for _, o := range output { str, err := ValueToString(o) if err != nil { return nil, err } res += str } return context.NewStringValue(res, sensitive, sensitiveReasons...), nil } func expandStruct(obj *InterpolationContext, value *structpb.Struct) (*context.Value, error) { res := &structpb.Struct{Fields: make(map[string]*structpb.Value, len(value.Fields))} for fieldKey, fieldValue := range value.Fields { fieldNewValue, err := Expand(obj, fieldValue) if err != nil { return nil, err } res.Fields[fieldKey] = fieldNewValue.Value } return context.NewNonSensitiveValue(structpb.NewStructValue(res)), nil } func expandList(obj *InterpolationContext, value *structpb.ListValue) (*context.Value, error) { res := &structpb.ListValue{Values: make([]*structpb.Value, len(value.Values))} for listIndex, listValue := range value.Values { listNewValue, err := Expand(obj, listValue) if err != nil { return nil, err } res.Values[listIndex] = listNewValue.Value } return context.NewNonSensitiveValue(structpb.NewListValue(res)), nil } // The Expand rewrites struct/list/string mutating data structure func Expand(obj *InterpolationContext, value *structpb.Value) (*context.Value, error) { switch value.Kind.(type) { case *structpb.Value_StringValue: return interpolateString(obj, value.GetStringValue()) case *structpb.Value_StructValue: return expandStruct(obj, value.GetStructValue()) case *structpb.Value_ListValue: return expandList(obj, value.GetListValue()) default: return context.NewNonSensitiveValue(value), nil } } // The ExpandString rewrites string and returns string func ExpandString(obj *InterpolationContext, value string) (string, error) { res, err := interpolateString(obj, value) if err != nil { return "", err } return ValueToString(res.Value) }