Repository: d5/tengo (go)

Issue:
add hex and base64 encoding
I'm currently working on a [crypto module](https://github.com/Ma124/tengo/tree/stdlib-crypto) #135 and often you want your encrypted data base64 encoded or your hashes as hex.

Which is why I suggest adding these features to the text module or an encoding module with support for base64 encoding and hex because the only way I known to hex encode bytes at the moment is `sprintf("%x", bin)` which is neither immediately obvious nor efficient.

With some featback from @d5 I can create a pull request.

Pull request:
Stdlib encodings: hex, base64 (#216)
Fixes #216 

Patch:
diff --git a/docs/stdlib-base64.md b/docs/stdlib-base64.md
new file mode 100644
index 0000000..864cfa7
--- /dev/null
+++ b/docs/stdlib-base64.md
@@ -0,0 +1,16 @@
+# Module - "base64"
+
+```golang
+fmt := import("base64")
+```
+
+## Functions
+
+- `encode(src)`: returns the base64 encoding of src.
+- `decode(s)`: returns the bytes represented by the base64 string s.
+- `raw_encode(src)`: returns the base64 encoding of src but omits the padding.
+- `raw_decode(s)`: returns the bytes represented by the base64 string s which omits the padding.
+- `url_encode(src)`: returns the url-base64 encoding of src.
+- `url_decode(s)`: returns the bytes represented by the url-base64 string s.
+- `raw_url_encode(src)`: returns the url-base64 encoding of src but omits the padding.
+- `raw_url_decode(s)`: returns the bytes represented by the url-base64 string s which omits the padding.
\ No newline at end of file
diff --git a/docs/stdlib-hex.md b/docs/stdlib-hex.md
new file mode 100644
index 0000000..9d219ba
--- /dev/null
+++ b/docs/stdlib-hex.md
@@ -0,0 +1,10 @@
+# Module - "hex"
+
+```golang
+fmt := import("hex")
+```
+
+## Functions
+
+- `encode(src)`: returns the hexadecimal encoding of src.
+- `decode(s)`: returns the bytes represented by the hexadecimal string s.
\ No newline at end of file
diff --git a/docs/stdlib.md b/docs/stdlib.md
index ccb4f1c..6383060 100644
--- a/docs/stdlib.md
+++ b/docs/stdlib.md
@@ -7,4 +7,6 @@
 - [rand](https://github.com/d5/tengo/blob/master/docs/stdlib-rand.md): random functions
 - [fmt](https://github.com/d5/tengo/blob/master/docs/stdlib-fmt.md): formatting functions
 - [json](https://github.com/d5/tengo/blob/master/docs/stdlib-json.md): JSON functions
-- [enum](https://github.com/d5/tengo/blob/master/docs/stdlib-enum.md): Enumeration functions
\ No newline at end of file
+- [enum](https://github.com/d5/tengo/blob/master/docs/stdlib-enum.md): Enumeration functions
+- [hex](https://github.com/d5/tengo/blob/master/docs/stdlib-hex.md): hex encoding and decoding functions
+- [base64](https://github.com/d5/tengo/blob/master/docs/stdlib-base64.md): base64 encoding and decoding functions
\ No newline at end of file
diff --git a/stdlib/base64.go b/stdlib/base64.go
new file mode 100644
index 0000000..40a746c
--- /dev/null
+++ b/stdlib/base64.go
@@ -0,0 +1,20 @@
+package stdlib
+
+import (
+	"encoding/base64"
+	"github.com/d5/tengo/objects"
+)
+
+var base64Module = map[string]objects.Object{
+	"encode": &objects.UserFunction{Value: FuncAYRS(base64.StdEncoding.EncodeToString)},
+	"decode": &objects.UserFunction{Value: FuncASRYE(base64.StdEncoding.DecodeString)},
+
+	"raw_encode": &objects.UserFunction{Value: FuncAYRS(base64.RawStdEncoding.EncodeToString)},
+	"raw_decode": &objects.UserFunction{Value: FuncASRYE(base64.RawStdEncoding.DecodeString)},
+
+	"url_encode": &objects.UserFunction{Value: FuncAYRS(base64.URLEncoding.EncodeToString)},
+	"url_decode": &objects.UserFunction{Value: FuncASRYE(base64.URLEncoding.DecodeString)},
+
+	"raw_url_encode": &objects.UserFunction{Value: FuncAYRS(base64.RawURLEncoding.EncodeToString)},
+	"raw_url_decode": &objects.UserFunction{Value: FuncASRYE(base64.RawURLEncoding.DecodeString)},
+}
diff --git a/stdlib/builtin_modules.go b/stdlib/builtin_modules.go
index cc2796f..722461b 100644
--- a/stdlib/builtin_modules.go
+++ b/stdlib/builtin_modules.go
@@ -11,4 +11,6 @@ var BuiltinModules = map[string]map[string]objects.Object{
 	"rand":  randModule,
 	"fmt":   fmtModule,
 	"json":  jsonModule,
+	"base64": base64Module,
+	"hex":  hexModule,
 }
diff --git a/stdlib/func_typedefs.go b/stdlib/func_typedefs.go
index 26c7ddd..c7bd11f 100644
--- a/stdlib/func_typedefs.go
+++ b/stdlib/func_typedefs.go
@@ -1036,6 +1036,29 @@ func FuncAYRIE(fn func([]byte) (int, error)) objects.CallableFunc {
 	}
 }
 
+// FuncAYRS transform a function of 'func([]byte) string' signature
+// into CallableFunc type.
+func FuncAYRS(fn func([]byte) string) objects.CallableFunc {
+	return func(args ...objects.Object) (ret objects.Object, err error) {
+		if len(args) != 1 {
+			return nil, objects.ErrWrongNumArguments
+		}
+
+		y1, ok := objects.ToByteSlice(args[0])
+		if !ok {
+			return nil, objects.ErrInvalidArgumentType{
+				Name:     "first",
+				Expected: "bytes(compatible)",
+				Found:    args[0].TypeName(),
+			}
+		}
+
+		res := fn(y1)
+
+		return &objects.String{Value: res}, nil
+	}
+}
+
 // FuncASRIE transform a function of 'func(string) (int, error)' signature
 // into CallableFunc type.
 func FuncASRIE(fn func(string) (int, error)) objects.CallableFunc {
@@ -1062,6 +1085,36 @@ func FuncASRIE(fn func(string) (int, error)) objects.CallableFunc {
 	}
 }
 
+// FuncASRYE transform a function of 'func(string) ([]byte, error)' signature
+// into CallableFunc type.
+func FuncASRYE(fn func(string) ([]byte, error)) objects.CallableFunc {
+	return func(args ...objects.Object) (ret objects.Object, err error) {
+		if len(args) != 1 {
+			return nil, objects.ErrWrongNumArguments
+		}
+
+		s1, ok := objects.ToString(args[0])
+		if !ok {
+			return nil, objects.ErrInvalidArgumentType{
+				Name:     "first",
+				Expected: "string(compatible)",
+				Found:    args[0].TypeName(),
+			}
+		}
+
+		res, err := fn(s1)
+		if err != nil {
+			return wrapError(err), nil
+		}
+
+		if len(res) > tengo.MaxBytesLen {
+			return nil, objects.ErrBytesLimit
+		}
+
+		return &objects.Bytes{Value: res}, nil
+	}
+}
+
 // FuncAIRSsE transform a function of 'func(int) ([]string, error)' signature
 // into CallableFunc type.
 func FuncAIRSsE(fn func(int) ([]string, error)) objects.CallableFunc {
diff --git a/stdlib/hex.go b/stdlib/hex.go
new file mode 100644
index 0000000..acc29e6
--- /dev/null
+++ b/stdlib/hex.go
@@ -0,0 +1,11 @@
+package stdlib
+
+import (
+	"encoding/hex"
+	"github.com/d5/tengo/objects"
+)
+
+var hexModule = map[string]objects.Object{
+	"encode": &objects.UserFunction{Value: FuncAYRS(hex.EncodeToString)},
+	"decode": &objects.UserFunction{Value: FuncASRYE(hex.DecodeString)},
+}


Test patch:
diff --git a/stdlib/base64_test.go b/stdlib/base64_test.go
new file mode 100644
index 0000000..b1157d6
--- /dev/null
+++ b/stdlib/base64_test.go
@@ -0,0 +1,20 @@
+package stdlib_test
+
+import "testing"
+
+var base64Bytes1 = []byte{0x06, 0xAC, 0x76, 0x1B, 0x1D, 0x6A, 0xFA, 0x9D, 0xB1, 0xA0}
+const base64Std = "Bqx2Gx1q+p2xoA=="
+const base64URL = "Bqx2Gx1q-p2xoA=="
+const base64RawStd = "Bqx2Gx1q+p2xoA"
+const base64RawURL = "Bqx2Gx1q-p2xoA"
+
+func TestBase64(t *testing.T) {
+	module(t, `base64`).call("encode", base64Bytes1).expect(base64Std)
+	module(t, `base64`).call("decode", base64Std).expect(base64Bytes1)
+	module(t, `base64`).call("url_encode", base64Bytes1).expect(base64URL)
+	module(t, `base64`).call("url_decode", base64URL).expect(base64Bytes1)
+	module(t, `base64`).call("raw_encode", base64Bytes1).expect(base64RawStd)
+	module(t, `base64`).call("raw_decode", base64RawStd).expect(base64Bytes1)
+	module(t, `base64`).call("raw_url_encode", base64Bytes1).expect(base64RawURL)
+	module(t, `base64`).call("raw_url_decode", base64RawURL).expect(base64Bytes1)
+}
diff --git a/stdlib/hex_test.go b/stdlib/hex_test.go
new file mode 100644
index 0000000..f545a51
--- /dev/null
+++ b/stdlib/hex_test.go
@@ -0,0 +1,11 @@
+package stdlib_test
+
+import "testing"
+
+var hexBytes1 = []byte{0x06, 0xAC, 0x76, 0x1B, 0x1D, 0x6A, 0xFA, 0x9D, 0xB1, 0xA0}
+const hex1 = "06ac761b1d6afa9db1a0"
+
+func TestHex(t *testing.T)  {
+	module(t, `hex`).call("encode", hexBytes1).expect(hex1)
+	module(t, `hex`).call("decode", hex1).expect(hexBytes1)
+}