github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/lang/funcs/encoding.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package funcs
     5  
     6  import (
     7  	"bytes"
     8  	"compress/gzip"
     9  	"encoding/base64"
    10  	"fmt"
    11  	"log"
    12  	"net/url"
    13  	"unicode/utf8"
    14  
    15  	"github.com/zclconf/go-cty/cty"
    16  	"github.com/zclconf/go-cty/cty/function"
    17  	"golang.org/x/text/encoding/ianaindex"
    18  )
    19  
    20  // Base64DecodeFunc constructs a function that decodes a string containing a base64 sequence.
    21  var Base64DecodeFunc = function.New(&function.Spec{
    22  	Params: []function.Parameter{
    23  		{
    24  			Name:        "str",
    25  			Type:        cty.String,
    26  			AllowMarked: true,
    27  		},
    28  	},
    29  	Type:         function.StaticReturnType(cty.String),
    30  	RefineResult: refineNotNull,
    31  	Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
    32  		str, strMarks := args[0].Unmark()
    33  		s := str.AsString()
    34  		sDec, err := base64.StdEncoding.DecodeString(s)
    35  		if err != nil {
    36  			return cty.UnknownVal(cty.String), fmt.Errorf("failed to decode base64 data %s", redactIfSensitive(s, strMarks))
    37  		}
    38  		if !utf8.Valid([]byte(sDec)) {
    39  			log.Printf("[DEBUG] the result of decoding the provided string is not valid UTF-8: %s", redactIfSensitive(sDec, strMarks))
    40  			return cty.UnknownVal(cty.String), fmt.Errorf("the result of decoding the provided string is not valid UTF-8")
    41  		}
    42  		return cty.StringVal(string(sDec)).WithMarks(strMarks), nil
    43  	},
    44  })
    45  
    46  // Base64EncodeFunc constructs a function that encodes a string to a base64 sequence.
    47  var Base64EncodeFunc = function.New(&function.Spec{
    48  	Params: []function.Parameter{
    49  		{
    50  			Name: "str",
    51  			Type: cty.String,
    52  		},
    53  	},
    54  	Type:         function.StaticReturnType(cty.String),
    55  	RefineResult: refineNotNull,
    56  	Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
    57  		return cty.StringVal(base64.StdEncoding.EncodeToString([]byte(args[0].AsString()))), nil
    58  	},
    59  })
    60  
    61  // TextEncodeBase64Func constructs a function that encodes a string to a target encoding and then to a base64 sequence.
    62  var TextEncodeBase64Func = function.New(&function.Spec{
    63  	Params: []function.Parameter{
    64  		{
    65  			Name: "string",
    66  			Type: cty.String,
    67  		},
    68  		{
    69  			Name: "encoding",
    70  			Type: cty.String,
    71  		},
    72  	},
    73  	Type:         function.StaticReturnType(cty.String),
    74  	RefineResult: refineNotNull,
    75  	Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
    76  		encoding, err := ianaindex.IANA.Encoding(args[1].AsString())
    77  		if err != nil || encoding == nil {
    78  			return cty.UnknownVal(cty.String), function.NewArgErrorf(1, "%q is not a supported IANA encoding name or alias in this Terraform version", args[1].AsString())
    79  		}
    80  
    81  		encName, err := ianaindex.IANA.Name(encoding)
    82  		if err != nil { // would be weird, since we just read this encoding out
    83  			encName = args[1].AsString()
    84  		}
    85  
    86  		encoder := encoding.NewEncoder()
    87  		encodedInput, err := encoder.Bytes([]byte(args[0].AsString()))
    88  		if err != nil {
    89  			// The string representations of "err" disclose implementation
    90  			// details of the underlying library, and the main error we might
    91  			// like to return a special message for is unexported as
    92  			// golang.org/x/text/encoding/internal.RepertoireError, so this
    93  			// is just a generic error message for now.
    94  			//
    95  			// We also don't include the string itself in the message because
    96  			// it can typically be very large, contain newline characters,
    97  			// etc.
    98  			return cty.UnknownVal(cty.String), function.NewArgErrorf(0, "the given string contains characters that cannot be represented in %s", encName)
    99  		}
   100  
   101  		return cty.StringVal(base64.StdEncoding.EncodeToString(encodedInput)), nil
   102  	},
   103  })
   104  
   105  // TextDecodeBase64Func constructs a function that decodes a base64 sequence to a target encoding.
   106  var TextDecodeBase64Func = function.New(&function.Spec{
   107  	Params: []function.Parameter{
   108  		{
   109  			Name: "source",
   110  			Type: cty.String,
   111  		},
   112  		{
   113  			Name: "encoding",
   114  			Type: cty.String,
   115  		},
   116  	},
   117  	Type:         function.StaticReturnType(cty.String),
   118  	RefineResult: refineNotNull,
   119  	Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
   120  		encoding, err := ianaindex.IANA.Encoding(args[1].AsString())
   121  		if err != nil || encoding == nil {
   122  			return cty.UnknownVal(cty.String), function.NewArgErrorf(1, "%q is not a supported IANA encoding name or alias in this Terraform version", args[1].AsString())
   123  		}
   124  
   125  		encName, err := ianaindex.IANA.Name(encoding)
   126  		if err != nil { // would be weird, since we just read this encoding out
   127  			encName = args[1].AsString()
   128  		}
   129  
   130  		s := args[0].AsString()
   131  		sDec, err := base64.StdEncoding.DecodeString(s)
   132  		if err != nil {
   133  			switch err := err.(type) {
   134  			case base64.CorruptInputError:
   135  				return cty.UnknownVal(cty.String), function.NewArgErrorf(0, "the given value is has an invalid base64 symbol at offset %d", int(err))
   136  			default:
   137  				return cty.UnknownVal(cty.String), function.NewArgErrorf(0, "invalid source string: %w", err)
   138  			}
   139  
   140  		}
   141  
   142  		decoder := encoding.NewDecoder()
   143  		decoded, err := decoder.Bytes(sDec)
   144  		if err != nil || bytes.ContainsRune(decoded, '�') {
   145  			return cty.UnknownVal(cty.String), function.NewArgErrorf(0, "the given string contains symbols that are not defined for %s", encName)
   146  		}
   147  
   148  		return cty.StringVal(string(decoded)), nil
   149  	},
   150  })
   151  
   152  // Base64GzipFunc constructs a function that compresses a string with gzip and then encodes the result in
   153  // Base64 encoding.
   154  var Base64GzipFunc = function.New(&function.Spec{
   155  	Params: []function.Parameter{
   156  		{
   157  			Name: "str",
   158  			Type: cty.String,
   159  		},
   160  	},
   161  	Type:         function.StaticReturnType(cty.String),
   162  	RefineResult: refineNotNull,
   163  	Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
   164  		s := args[0].AsString()
   165  
   166  		var b bytes.Buffer
   167  		gz := gzip.NewWriter(&b)
   168  		if _, err := gz.Write([]byte(s)); err != nil {
   169  			return cty.UnknownVal(cty.String), fmt.Errorf("failed to write gzip raw data: %w", err)
   170  		}
   171  		if err := gz.Flush(); err != nil {
   172  			return cty.UnknownVal(cty.String), fmt.Errorf("failed to flush gzip writer: %w", err)
   173  		}
   174  		if err := gz.Close(); err != nil {
   175  			return cty.UnknownVal(cty.String), fmt.Errorf("failed to close gzip writer: %w", err)
   176  		}
   177  		return cty.StringVal(base64.StdEncoding.EncodeToString(b.Bytes())), nil
   178  	},
   179  })
   180  
   181  // URLEncodeFunc constructs a function that applies URL encoding to a given string.
   182  var URLEncodeFunc = function.New(&function.Spec{
   183  	Params: []function.Parameter{
   184  		{
   185  			Name: "str",
   186  			Type: cty.String,
   187  		},
   188  	},
   189  	Type:         function.StaticReturnType(cty.String),
   190  	RefineResult: refineNotNull,
   191  	Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
   192  		return cty.StringVal(url.QueryEscape(args[0].AsString())), nil
   193  	},
   194  })
   195  
   196  // Base64Decode decodes a string containing a base64 sequence.
   197  //
   198  // Terraform uses the "standard" Base64 alphabet as defined in RFC 4648 section 4.
   199  //
   200  // Strings in the Terraform language are sequences of unicode characters rather
   201  // than bytes, so this function will also interpret the resulting bytes as
   202  // UTF-8. If the bytes after Base64 decoding are _not_ valid UTF-8, this function
   203  // produces an error.
   204  func Base64Decode(str cty.Value) (cty.Value, error) {
   205  	return Base64DecodeFunc.Call([]cty.Value{str})
   206  }
   207  
   208  // Base64Encode applies Base64 encoding to a string.
   209  //
   210  // Terraform uses the "standard" Base64 alphabet as defined in RFC 4648 section 4.
   211  //
   212  // Strings in the Terraform language are sequences of unicode characters rather
   213  // than bytes, so this function will first encode the characters from the string
   214  // as UTF-8, and then apply Base64 encoding to the result.
   215  func Base64Encode(str cty.Value) (cty.Value, error) {
   216  	return Base64EncodeFunc.Call([]cty.Value{str})
   217  }
   218  
   219  // Base64Gzip compresses a string with gzip and then encodes the result in
   220  // Base64 encoding.
   221  //
   222  // Terraform uses the "standard" Base64 alphabet as defined in RFC 4648 section 4.
   223  //
   224  // Strings in the Terraform language are sequences of unicode characters rather
   225  // than bytes, so this function will first encode the characters from the string
   226  // as UTF-8, then apply gzip compression, and then finally apply Base64 encoding.
   227  func Base64Gzip(str cty.Value) (cty.Value, error) {
   228  	return Base64GzipFunc.Call([]cty.Value{str})
   229  }
   230  
   231  // URLEncode applies URL encoding to a given string.
   232  //
   233  // This function identifies characters in the given string that would have a
   234  // special meaning when included as a query string argument in a URL and
   235  // escapes them using RFC 3986 "percent encoding".
   236  //
   237  // If the given string contains non-ASCII characters, these are first encoded as
   238  // UTF-8 and then percent encoding is applied separately to each UTF-8 byte.
   239  func URLEncode(str cty.Value) (cty.Value, error) {
   240  	return URLEncodeFunc.Call([]cty.Value{str})
   241  }
   242  
   243  // TextEncodeBase64 applies Base64 encoding to a string that was encoded before with a target encoding.
   244  //
   245  // Terraform uses the "standard" Base64 alphabet as defined in RFC 4648 section 4.
   246  //
   247  // First step is to apply the target IANA encoding (e.g. UTF-16LE).
   248  // Strings in the Terraform language are sequences of unicode characters rather
   249  // than bytes, so this function will first encode the characters from the string
   250  // as UTF-8, and then apply Base64 encoding to the result.
   251  func TextEncodeBase64(str, enc cty.Value) (cty.Value, error) {
   252  	return TextEncodeBase64Func.Call([]cty.Value{str, enc})
   253  }
   254  
   255  // TextDecodeBase64 decodes a string containing a base64 sequence whereas a specific encoding of the string is expected.
   256  //
   257  // Terraform uses the "standard" Base64 alphabet as defined in RFC 4648 section 4.
   258  //
   259  // Strings in the Terraform language are sequences of unicode characters rather
   260  // than bytes, so this function will also interpret the resulting bytes as
   261  // the target encoding.
   262  func TextDecodeBase64(str, enc cty.Value) (cty.Value, error) {
   263  	return TextDecodeBase64Func.Call([]cty.Value{str, enc})
   264  }