From a3fc0247ac4fba68a8a8b8c1cd716f510e6baf60 Mon Sep 17 00:00:00 2001 From: pooriaghaedi Date: Wed, 21 Feb 2024 19:02:01 +0300 Subject: [PATCH] add urldecode function (#1234) Signed-off-by: pooriaghaedi --- internal/lang/funcs/encoding.go | 30 ++++ internal/lang/funcs/encoding_test.go | 151 ++++++++++++++++++ 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 +++ 6 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 website/docs/language/functions/urldecode.mdx diff --git a/internal/lang/funcs/encoding.go b/internal/lang/funcs/encoding.go index 4899caebf64..b19cb8bd2a7 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 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 af235945de0..7c526440fa5 100644 --- a/internal/lang/funcs/encoding_test.go +++ b/internal/lang/funcs/encoding_test.go @@ -243,6 +243,157 @@ 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/bar"), + cty.StringVal("foo%2Fbar"), + 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, 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% 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, 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.Errorf("encode() error = %v, wantErr = false", err) + return + } + 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 71638b103af..92a147dbd7b 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 2608dca0679..492556537da 100644 --- a/internal/lang/functions_test.go +++ b/internal/lang/functions_test.go @@ -1125,6 +1125,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 afd7a83d606..e4189e37f73 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" @@ -1175,6 +1179,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 }, { @@ -1353,4 +1362,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 00000000000..e8d0e825d0a --- /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 +```