diff --git a/hcl2template/functions.go b/hcl2template/functions.go index c74754bdf..5340f34dd 100644 --- a/hcl2template/functions.go +++ b/hcl2template/functions.go @@ -4,6 +4,8 @@ package hcl2template import ( + "bytes" + "encoding/base64" "fmt" "github.com/hashicorp/go-cty-funcs/cidr" @@ -19,6 +21,7 @@ import ( "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/function" "github.com/zclconf/go-cty/cty/function/stdlib" + "golang.org/x/text/encoding/ianaindex" ) // Functions returns the set of functions that should be used to when @@ -102,6 +105,8 @@ func Functions(basedir string) map[string]function.Function { "split": stdlib.SplitFunc, "strrev": stdlib.ReverseFunc, "substr": stdlib.SubstrFunc, + "textdecodebase64": TextDecodeBase64Func, + "textencodebase64": TextEncodeBase64Func, "timestamp": pkrfunction.TimestampFunc, "timeadd": stdlib.TimeAddFunc, "title": stdlib.TitleFunc, @@ -130,6 +135,98 @@ func Functions(basedir string) map[string]function.Function { return funcs } +// TextEncodeBase64Func constructs a function that encodes a string to a target encoding and then to a base64 sequence. +var TextEncodeBase64Func = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "string", + Type: cty.String, + }, + { + Name: "encoding", + Type: cty.String, + }, + }, + Description: "Encodes the input string (UTF-8) to the destination encoding. The output is base64 to account for cty limiting strings to NFC normalised UTF-8 strings.", + Type: function.StaticReturnType(cty.String), + RefineResult: func(rb *cty.RefinementBuilder) *cty.RefinementBuilder { return rb.NotNull() }, + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + encoding, err := ianaindex.IANA.Encoding(args[1].AsString()) + if err != nil || encoding == nil { + return cty.UnknownVal(cty.String), function.NewArgErrorf(1, "%q is not a supported IANA encoding name or alias", args[1].AsString()) + } + + encName, err := ianaindex.IANA.Name(encoding) + if err != nil { // would be weird, since we just read this encoding out + encName = args[1].AsString() + } + + encoder := encoding.NewEncoder() + encodedInput, err := encoder.Bytes([]byte(args[0].AsString())) + if err != nil { + // The string representations of "err" disclose implementation + // details of the underlying library, and the main error we might + // like to return a special message for is unexported as + // golang.org/x/text/encoding/internal.RepertoireError, so this + // is just a generic error message for now. + // + // We also don't include the string itself in the message because + // it can typically be very large, contain newline characters, + // etc. + return cty.UnknownVal(cty.String), function.NewArgErrorf(0, "the given string contains characters that cannot be represented in %s", encName) + } + + return cty.StringVal(base64.StdEncoding.EncodeToString(encodedInput)), nil + }, +}) + +// TextDecodeBase64Func constructs a function that decodes a base64 sequence from the source encoding to UTF-8. +var TextDecodeBase64Func = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "source", + Type: cty.String, + }, + { + Name: "encoding", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.String), + Description: "Encodes the input base64 blob from an encoding to utf-8. The input is base64 to account for cty limiting strings to NFC normalised UTF-8 strings.", + RefineResult: func(rb *cty.RefinementBuilder) *cty.RefinementBuilder { return rb.NotNull() }, + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + encoding, err := ianaindex.IANA.Encoding(args[1].AsString()) + if err != nil || encoding == nil { + return cty.UnknownVal(cty.String), function.NewArgErrorf(1, "%q is not a supported IANA encoding name or alias", args[1].AsString()) + } + + encName, err := ianaindex.IANA.Name(encoding) + if err != nil { // would be weird, since we just read this encoding out + encName = args[1].AsString() + } + + s := args[0].AsString() + sDec, err := base64.StdEncoding.DecodeString(s) + if err != nil { + switch err := err.(type) { + case base64.CorruptInputError: + return cty.UnknownVal(cty.String), function.NewArgErrorf(0, "the given value is has an invalid base64 symbol at offset %d", int(err)) + default: + return cty.UnknownVal(cty.String), function.NewArgErrorf(0, "invalid source string: %w", err) + } + } + + decoder := encoding.NewDecoder() + decoded, err := decoder.Bytes(sDec) + if err != nil || bytes.ContainsRune(decoded, '�') { + return cty.UnknownVal(cty.String), function.NewArgErrorf(0, "the given string contains symbols that are not defined for %s", encName) + } + + return cty.StringVal(string(decoded)), nil + }, +}) + var unimplFunc = function.New(&function.Spec{ Type: func([]cty.Value) (cty.Type, error) { return cty.DynamicPseudoType, fmt.Errorf("function not yet implemented") diff --git a/website/content/docs/templates/hcl_templates/functions/encoding/textdecodebase64.mdx b/website/content/docs/templates/hcl_templates/functions/encoding/textdecodebase64.mdx new file mode 100644 index 000000000..923d66bc8 --- /dev/null +++ b/website/content/docs/templates/hcl_templates/functions/encoding/textdecodebase64.mdx @@ -0,0 +1,28 @@ +--- +page_title: testdecodebase64 - Functions - Configuration Language +description: The testdecodebase64 function converts a base64 encoded string, whose underlying encoding is the one specified as argument, into a UTF-8 string. +--- + +# `textdecodebase64` Function + +Encodes the input string from a speicified encoding into UTF-8. +The input is base64-encoded to account for HCL's string encoding limitations: they must be UTF-8, NFC-normalised. + +Packer uses the "standard" Base64 alphabet as defined in +[RFC 4648 section 4](https://tools.ietf.org/html/rfc4648#section-4). + +The `encoding_name` argument must contain one of the encoding names or aliases recorded in +[the IANA character encoding registry](https://www.iana.org/assignments/character-sets/character-sets.xhtml). + +## Examples + +```shell-session +# Usage: textencodebase64(input_base64, encoding_name) +> textdecodebase64("SABlAGwAbABvACAAVwBvAHIAbABkAA==", "UTF-16LE") +Hello World +``` + +## Related Functions + +- [`base64encode`](/packer/docs/templates/hcl_templates/functions/encoding/base64encode) performs the opposite operation, + encoding the UTF-8 bytes for a string as Base64. diff --git a/website/content/docs/templates/hcl_templates/functions/encoding/textencodebase64.mdx b/website/content/docs/templates/hcl_templates/functions/encoding/textencodebase64.mdx new file mode 100644 index 000000000..6abff6371 --- /dev/null +++ b/website/content/docs/templates/hcl_templates/functions/encoding/textencodebase64.mdx @@ -0,0 +1,28 @@ +--- +page_title: testencodebase64 - Functions - Configuration Language +description: The testencodebase64 function converts a UTF-8 NFC input string to a base64 blob that encodes the target encoding's rendering of the input string. +--- + +# `textencodebase64` Function + +Encodes the input string to the destination encoding. +The output is base64-encoded to account for HCL's string encoding limitations: they must be UTF-8, NFC-normalised. + +Packer uses the "standard" Base64 alphabet as defined in +[RFC 4648 section 4](https://tools.ietf.org/html/rfc4648#section-4). + +The `encoding_name` argument must contain one of the encoding names or aliases recorded in +[the IANA character encoding registry](https://www.iana.org/assignments/character-sets/character-sets.xhtml). + +## Examples + +```shell-session +# Usage: textencodebase64(input_string, encoding_name) +> textencodebase64("Hello World", "UTF-16LE") +SABlAGwAbABvACAAVwBvAHIAbABkAA== +``` + +## Related Functions + +- [`base64encode`](/packer/docs/templates/hcl_templates/functions/encoding/base64encode) performs the opposite operation, + encoding the UTF-8 bytes for a string as Base64. diff --git a/website/data/docs-nav-data.json b/website/data/docs-nav-data.json index e5423f814..396ee9010 100644 --- a/website/data/docs-nav-data.json +++ b/website/data/docs-nav-data.json @@ -443,6 +443,14 @@ "title": "urlencode", "path": "templates/hcl_templates/functions/encoding/urlencode" }, + { + "title": "textencodebase64", + "path": "templates/hcl_templates/functions/encoding/textencodebase64" + }, + { + "title": "textdecodebase64", + "path": "templates/hcl_templates/functions/encoding/textdecodebase64" + }, { "title": "yamldecode", "path": "templates/hcl_templates/functions/encoding/yamldecode"