From 7393a405860e85fef69ed257ec08de97fb6a5aa4 Mon Sep 17 00:00:00 2001 From: Kristin Laemmert Date: Wed, 23 May 2018 07:27:28 -0700 Subject: [PATCH] porting encoding functions --- lang/funcs/encoding.go | 96 ++++++++++++++ lang/funcs/encoding_test.go | 120 ++++++++++++++++++ lang/functions.go | 8 +- .../configuration/functions/csvdecode.html.md | 2 +- 4 files changed, 221 insertions(+), 5 deletions(-) diff --git a/lang/funcs/encoding.go b/lang/funcs/encoding.go index f3385030e5..bc594ce5c2 100644 --- a/lang/funcs/encoding.go +++ b/lang/funcs/encoding.go @@ -1,8 +1,11 @@ package funcs import ( + "bytes" + "compress/gzip" "encoding/base64" "fmt" + "net/url" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/function" @@ -28,6 +31,62 @@ var Base64DecodeFunc = function.New(&function.Spec{ }, }) +// Base64EncodeFunc constructs a function that encodes a string to a base64 sequence. +var Base64EncodeFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "str", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.String), + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + return cty.StringVal(base64.StdEncoding.EncodeToString([]byte(args[0].AsString()))), nil + }, +}) + +// Base64GzipFunc constructs a function that compresses a string with gzip and then encodes the result in +// Base64 encoding. +var Base64GzipFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "str", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.String), + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + s := args[0].AsString() + + var b bytes.Buffer + gz := gzip.NewWriter(&b) + if _, err := gz.Write([]byte(s)); err != nil { + return cty.UnknownVal(cty.String), fmt.Errorf("failed to write gzip raw data: '%s'", s) + } + if err := gz.Flush(); err != nil { + return cty.UnknownVal(cty.String), fmt.Errorf("failed to flush gzip writer: '%s'", s) + } + if err := gz.Close(); err != nil { + return cty.UnknownVal(cty.String), fmt.Errorf("failed to close gzip writer: '%s'", s) + } + return cty.StringVal(base64.StdEncoding.EncodeToString(b.Bytes())), nil + }, +}) + +// URLEncodeFunc constructs a function that applies URL encoding to a given string. +var URLEncodeFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "str", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.String), + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + return cty.StringVal(url.QueryEscape(args[0].AsString())), nil + }, +}) + // Base64Decode decodes a string containing a base64 sequence. // // Terraform uses the "standard" Base64 alphabet as defined in @@ -40,3 +99,40 @@ var Base64DecodeFunc = function.New(&function.Spec{ func Base64Decode(str cty.Value) (cty.Value, error) { return Base64DecodeFunc.Call([]cty.Value{str}) } + +// Base64Encode applies Base64 encoding to a string. +// +// Terraform uses the "standard" Base64 alphabet as defined in +// [RFC 4648 section 4](https://tools.ietf.org/html/rfc4648#section-4). +// +// Strings in the Terraform language are sequences of unicode characters rather +// than bytes, so this function will first encode the characters from the string +// as UTF-8, and then apply Base64 encoding to the result. +func Base64Encode(str cty.Value) (cty.Value, error) { + return Base64EncodeFunc.Call([]cty.Value{str}) +} + +// Base64gzip compresses a string with gzip and then encodes the result in +// Base64 encoding. +// +// Terraform uses the "standard" Base64 alphabet as defined in +// [RFC 4648 section 4](https://tools.ietf.org/html/rfc4648#section-4) +// Strings in the Terraform language are sequences of unicode characters rather +// than bytes, so this function will first encode the characters from the string +// as UTF-8, then apply gzip compression, and then finally apply Base64 encoding. +func Base64Gzip(str cty.Value) (cty.Value, error) { + return Base64GzipFunc.Call([]cty.Value{str}) +} + +// UrlEncode applies URL encoding to a given string. +// +// This function identifies characters in the given string that would have a +// special meaning when included as a query string argument in a URL and +// escapes them using +// [RFC 3986 "percent encoding"](https://tools.ietf.org/html/rfc3986#section-2.1). +// +// If the given string contains non-ASCII characters, these are first encoded as +// UTF-8 and then percent encoding is applied separately to each UTF-8 byte. +func URLEncode(str cty.Value) (cty.Value, error) { + return URLEncodeFunc.Call([]cty.Value{str}) +} diff --git a/lang/funcs/encoding_test.go b/lang/funcs/encoding_test.go index dd4ea9acca..711ab2a418 100644 --- a/lang/funcs/encoding_test.go +++ b/lang/funcs/encoding_test.go @@ -46,3 +46,123 @@ func TestBase64Decode(t *testing.T) { }) } } + +func TestBase64Encode(t *testing.T) { + tests := []struct { + String cty.Value + Want cty.Value + Err bool + }{ + { + cty.StringVal("abc123!?$*&()'-=@~"), + cty.StringVal("YWJjMTIzIT8kKiYoKSctPUB+"), + false, + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("base64encode(%#v)", test.String), func(t *testing.T) { + got, err := Base64Encode(test.String) + + if test.Err { + if err == nil { + t.Fatal("succeeded; want error") + } + return + } else { + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + } + + if !got.RawEquals(test.Want) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) + } + }) + } +} + +func TestBase64Gzip(t *testing.T) { + tests := []struct { + String cty.Value + Want cty.Value + Err bool + }{ + { + cty.StringVal("test"), + cty.StringVal("H4sIAAAAAAAA/ypJLS4BAAAA//8BAAD//wx+f9gEAAAA"), + false, + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("base64gzip(%#v)", test.String), func(t *testing.T) { + got, err := Base64Gzip(test.String) + + if test.Err { + if err == nil { + t.Fatal("succeeded; want error") + } + return + } else { + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + } + + if !got.RawEquals(test.Want) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) + } + }) + } +} + +func TestURLEncode(t *testing.T) { + tests := []struct { + String cty.Value + Want cty.Value + Err bool + }{ + { + cty.StringVal("abc123-_"), + cty.StringVal("abc123-_"), + false, + }, + { + cty.StringVal("foo:bar@localhost?foo=bar&bar=baz"), + cty.StringVal("foo%3Abar%40localhost%3Ffoo%3Dbar%26bar%3Dbaz"), + false, + }, + { + cty.StringVal("mailto:email?subject=this+is+my+subject"), + cty.StringVal("mailto%3Aemail%3Fsubject%3Dthis%2Bis%2Bmy%2Bsubject"), + false, + }, + { + cty.StringVal("foo/bar"), + cty.StringVal("foo%2Fbar"), + false, + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("urlencode(%#v)", test.String), func(t *testing.T) { + got, err := URLEncode(test.String) + + if test.Err { + if err == nil { + t.Fatal("succeeded; want error") + } + return + } else { + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + } + + if !got.RawEquals(test.Want) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) + } + }) + } +} diff --git a/lang/functions.go b/lang/functions.go index 5af8f1b0c3..7b4c8afacf 100644 --- a/lang/functions.go +++ b/lang/functions.go @@ -31,8 +31,8 @@ func (s *Scope) Functions() map[string]function.Function { "abs": stdlib.AbsoluteFunc, "basename": funcs.BasenameFunc, "base64decode": funcs.Base64DecodeFunc, - "base64encode": unimplFunc, // TODO - "base64gzip": unimplFunc, // TODO + "base64encode": funcs.Base64EncodeFunc, + "base64gzip": funcs.Base64GzipFunc, "base64sha256": unimplFunc, // TODO "base64sha512": unimplFunc, // TODO "bcrypt": unimplFunc, // TODO @@ -86,12 +86,12 @@ func (s *Scope) Functions() map[string]function.Function { "split": funcs.SplitFunc, "substr": stdlib.SubstrFunc, "timestamp": funcs.TimestampFunc, - "timeadd": funs.TimeaddFunc, + "timeadd": funcs.TimeAddFunc, "title": unimplFunc, // TODO "transpose": unimplFunc, // TODO "trimspace": unimplFunc, // TODO "upper": stdlib.UpperFunc, - "urlencode": unimplFunc, // TODO + "urlencode": funcs.UrlEncodeFunc, "uuid": funcs.UUIDFunc, "zipmap": unimplFunc, // TODO } diff --git a/website/docs/configuration/functions/csvdecode.html.md b/website/docs/configuration/functions/csvdecode.html.md index 743dca198e..9862a7a1b0 100644 --- a/website/docs/configuration/functions/csvdecode.html.md +++ b/website/docs/configuration/functions/csvdecode.html.md @@ -6,7 +6,7 @@ description: |- The csvdecode function decodes CSV data into a list of maps. --- -# `base64decode` Function +# `csvdecode` Function `csvdecode` decodes a string containing CSV-formatted data and produces a list of maps representing that data.