Skip to content

Commit

Permalink
hcl2template: add text(encode|decode)base64 funcs
Browse files Browse the repository at this point in the history
Compared to Terraform, Packer was lacking a capability to encode/decode
strings to/from base64-encoded text encoded with another encoding.

This could be problematic in some cases, mainly when working with
Windows, as most of the OS uses UTF-16LE as its standard encoding for
many operations.

Therefore, we take a page from Terraform here, and add those functions
to what Packer supports in an HCL2 context.
  • Loading branch information
lbajolet-hashicorp committed May 27, 2024
1 parent 92aabc7 commit 3b10a3c
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 0 deletions.
97 changes: 97 additions & 0 deletions hcl2template/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
package hcl2template

import (
"bytes"
"encoding/base64"
"fmt"

"github.com/hashicorp/go-cty-funcs/cidr"
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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.
8 changes: 8 additions & 0 deletions website/data/docs-nav-data.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 3b10a3c

Please sign in to comment.