From b5889b10ebdba23061e8da3b14c61f2a12fdb61d Mon Sep 17 00:00:00 2001 From: Pooria Ghaedi <36617391+pooriaghaedi@users.noreply.github.com> Date: Fri, 1 Mar 2024 18:39:45 +0330 Subject: [PATCH] Feat: urldecode function #1234 (#1283) Signed-off-by: pooriaghaedi --- internal/lang/funcs/descriptions.go | 4 + internal/lang/funcs/encoding.go | 30 ++++ internal/lang/funcs/encoding_test.go | 135 ++++++++++++++++++ internal/lang/functions.go | 1 + internal/lang/functions_test.go | 6 + website/data/language-nav-data.json | 11 +- website/docs/language/functions/urldecode.mdx | 25 ++++ 7 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 website/docs/language/functions/urldecode.mdx diff --git a/internal/lang/funcs/descriptions.go b/internal/lang/funcs/descriptions.go index 4a0ce927f2..0ee1e6e819 100644 --- a/internal/lang/funcs/descriptions.go +++ b/internal/lang/funcs/descriptions.go @@ -522,6 +522,10 @@ var DescriptionList = map[string]descriptionEntry{ Description: "`urlencode` applies URL encoding to a given string.", ParamDescription: []string{""}, }, + "urldecode": { + Description: "`urldecode` applies URL decoding to a given encoded string.", + ParamDescription: []string{""}, + }, "uuid": { Description: "`uuid` generates a unique identifier string.", ParamDescription: []string{}, diff --git a/internal/lang/funcs/encoding.go b/internal/lang/funcs/encoding.go index 4899caebf6..084d3a6ace 100644 --- a/internal/lang/funcs/encoding.go +++ b/internal/lang/funcs/encoding.go @@ -227,6 +227,26 @@ var URLEncodeFunc = function.New(&function.Spec{ }, }) +// URLDecodeFunc constructs a function that applies URL decoding to a given encoded string. +var URLDecodeFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "str", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + query, err := url.QueryUnescape(args[0].AsString()) + if err != nil { + return cty.UnknownVal(cty.String), fmt.Errorf("failed to decode URL '%s': %v", query, err) + } + + return cty.StringVal(query), nil + }, +}) + // Base64Decode decodes a string containing a base64 sequence. // // OpenTofu uses the "standard" Base64 alphabet as defined in RFC 4648 section 4. @@ -281,6 +301,16 @@ func URLEncode(str cty.Value) (cty.Value, error) { return URLEncodeFunc.Call([]cty.Value{str}) } +// URLDecode decodes a URL encoded string. +// +// This function decodes the given string that has been encoded. +// +// If the given string contains non-ASCII characters, these are first encoded as +// UTF-8 and then percent decoding is applied separately to each UTF-8 byte. +func URLDecode(str cty.Value) (cty.Value, error) { + return URLDecodeFunc.Call([]cty.Value{str}) +} + // TextEncodeBase64 applies Base64 encoding to a string that was encoded before with a target encoding. // // OpenTofu uses the "standard" Base64 alphabet as defined in RFC 4648 section 4. diff --git a/internal/lang/funcs/encoding_test.go b/internal/lang/funcs/encoding_test.go index af235945de..d7d92108bc 100644 --- a/internal/lang/funcs/encoding_test.go +++ b/internal/lang/funcs/encoding_test.go @@ -243,6 +243,141 @@ func TestURLEncode(t *testing.T) { } } +func TestURLDecode(t *testing.T) { + tests := []struct { + String cty.Value + Want cty.Value + Err bool + }{ + { + cty.StringVal("abc123-_"), + cty.StringVal("abc123-_"), + false, + }, + { + cty.StringVal("foo%3Abar%40localhost%3Ffoo%3Dbar%26bar%3Dbaz"), + cty.StringVal("foo:bar@localhost?foo=bar&bar=baz"), + false, + }, + { + cty.StringVal("mailto%3Aemail%3Fsubject%3Dthis%2Bis%2Bmy%2Bsubject"), + cty.StringVal("mailto:email?subject=this+is+my+subject"), + false, + }, + { + cty.StringVal("foo%2Fbar"), + cty.StringVal("foo/bar"), + false, + }, + { + cty.StringVal("foo% bar"), + cty.UnknownVal(cty.String), + true, + }, + { + cty.StringVal("foo%2 bar"), + cty.UnknownVal(cty.String), + true, + }, + { + cty.StringVal("%GGfoo%2bar"), + cty.UnknownVal(cty.String), + true, + }, + { + cty.StringVal("foo%00, bar!"), + cty.StringVal("foo\x00, bar!"), + false, + }, + { + cty.StringVal("hello%20%E4%B8%96%E7%95%8C"), //Unicode character support + cty.StringVal("hello 世界"), + false, + }, + { + cty.StringVal("hello%20%D8%AF%D9%86%DB%8C%D8%A7"), //Unicode character support + cty.StringVal("hello دنیا"), + false, + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("urldecode(%#v)", test.String), func(t *testing.T) { + got, err := URLDecode(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 TestURLEncodeDecode(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:bar@localhost?foo=bar&bar=baz"), + false, + }, + { + cty.StringVal("mailto:email?subject=this+is+my+subject"), + cty.StringVal("mailto:email?subject=this+is+my+subject"), + false, + }, + { + cty.StringVal("foo/bar"), + cty.StringVal("foo/bar"), + false, + }, + { + cty.StringVal("foo%00, bar!"), + cty.StringVal("foo%00, bar!"), + false, + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("url encode decode(%#v)", test.String), func(t *testing.T) { + encoded, err := URLEncode(test.String) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + got, err := URLDecode(encoded) + + 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 TestBase64TextEncode(t *testing.T) { tests := []struct { String cty.Value diff --git a/internal/lang/functions.go b/internal/lang/functions.go index 3898e40f85..06494030f5 100644 --- a/internal/lang/functions.go +++ b/internal/lang/functions.go @@ -148,6 +148,7 @@ func (s *Scope) Functions() map[string]function.Function { "try": tryfunc.TryFunc, "upper": stdlib.UpperFunc, "urlencode": funcs.URLEncodeFunc, + "urldecode": funcs.URLDecodeFunc, "uuid": funcs.UUIDFunc, "uuidv5": funcs.UUIDV5Func, "values": stdlib.ValuesFunc, diff --git a/internal/lang/functions_test.go b/internal/lang/functions_test.go index 76ca8954f9..d471f07a3e 100644 --- a/internal/lang/functions_test.go +++ b/internal/lang/functions_test.go @@ -1132,6 +1132,12 @@ func TestFunctions(t *testing.T) { cty.StringVal("foo%3Abar%40localhost%3Ffoo%3Dbar%26bar%3Dbaz"), }, }, + "urldecode": { + { + `urldecode("foo%3Abar%40localhost%3Ffoo%3Dbar%26bar%3Dbaz")`, + cty.StringVal("foo:bar@localhost?foo=bar&bar=baz"), + }, + }, "uuidv5": { { diff --git a/website/data/language-nav-data.json b/website/data/language-nav-data.json index ed5dd4e3e3..0a0c0f0085 100644 --- a/website/data/language-nav-data.json +++ b/website/data/language-nav-data.json @@ -541,6 +541,10 @@ "title": "urlencode", "path": "language/functions/urlencode" }, + { + "title": "urldecode", + "path": "language/functions/urldecode" + }, { "title": "yamldecode", "path": "language/functions/yamldecode" @@ -1180,6 +1184,11 @@ "title": "urlencode", "path": "language/functions/urlencode", "hidden": true + }, + { + "title": "urldecode", + "path": "language/functions/urldecode", + "hidden": true }, { "title": "uuid", "path": "language/functions/uuid", "hidden": true }, { @@ -1358,4 +1367,4 @@ "title": "v1.x Compatibility Promises", "path": "language/v1-compatibility-promises" } -] +] \ No newline at end of file diff --git a/website/docs/language/functions/urldecode.mdx b/website/docs/language/functions/urldecode.mdx new file mode 100644 index 0000000000..e8d0e825d0 --- /dev/null +++ b/website/docs/language/functions/urldecode.mdx @@ -0,0 +1,25 @@ +--- +sidebar_label: urldecode +description: The urldecode function applies URL decoding to a given string. +--- + +# `urldecode` Function + +`urldecode` targets encoded characters within a string. + +The function is capable of decoding a comprehensive range of characters, +including those outside the ASCII range. Non-ASCII characters are first treated as UTF-8 bytes, +followed by the application of percent decoding to each byte, +facilitating the accurate decoding of multibyte characters. + + +## Examples + +``` +> urldecode("Hello+World%21") +Hello World! +> urldecode("%E2%98%83") +☃ +> urldecode("foo%3Abar%40localhost%3Ffoo%3Dbar%26bar%3Dbaz") +foo:bar@localhost?foo=bar&bar=baz +```