Skip to content

Commit

Permalink
add urldecode function (opentofu#1234)
Browse files Browse the repository at this point in the history
Signed-off-by: pooriaghaedi <[email protected]>
  • Loading branch information
pooriaghaedi committed Feb 21, 2024
1 parent 851391f commit a3fc024
Show file tree
Hide file tree
Showing 6 changed files with 223 additions and 1 deletion.
30 changes: 30 additions & 0 deletions internal/lang/funcs/encoding.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
151 changes: 151 additions & 0 deletions internal/lang/funcs/encoding_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions internal/lang/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions internal/lang/functions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
{
Expand Down
11 changes: 10 additions & 1 deletion website/data/language-nav-data.json
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,10 @@
"title": "<code>urlencode</code>",
"path": "language/functions/urlencode"
},
{
"title": "<code>urldecode</code>",
"path": "language/functions/urldecode"
},
{
"title": "<code>yamldecode</code>",
"path": "language/functions/yamldecode"
Expand Down Expand Up @@ -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 },
{
Expand Down Expand Up @@ -1353,4 +1362,4 @@
"title": "v1.x Compatibility Promises",
"path": "language/v1-compatibility-promises"
}
]
]
25 changes: 25 additions & 0 deletions website/docs/language/functions/urldecode.mdx
Original file line number Diff line number Diff line change
@@ -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
```

0 comments on commit a3fc024

Please sign in to comment.