From 1676e31a70fd3169f4030d3e251a3b10a8e1c9ee Mon Sep 17 00:00:00 2001 From: 42Atomys Date: Thu, 9 May 2024 19:30:06 +0200 Subject: [PATCH] chore: do the final cleanup of the migration --- .github/codecov.yml | 1 + ... sprig_functions_not_included_in_sprout.go | 72 +-- sprout.go | 6 - sprout_test.go | 21 - strings_functions.go | 55 +- to_migrate.go | 74 --- to_migrate_test.go | 557 ------------------ 7 files changed, 80 insertions(+), 706 deletions(-) rename migrated_functions.go => sprig_functions_not_included_in_sprout.go (92%) delete mode 100644 to_migrate.go delete mode 100644 to_migrate_test.go diff --git a/.github/codecov.yml b/.github/codecov.yml index c279ff5..1893f1c 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -15,6 +15,7 @@ github_checks: ignore: - benchmark/** - docs/** + - sprig_functions_not_included_in_sprout.go comment: behavior: new require_changes: true diff --git a/migrated_functions.go b/sprig_functions_not_included_in_sprout.go similarity index 92% rename from migrated_functions.go rename to sprig_functions_not_included_in_sprout.go index ef7bbbd..237a429 100644 --- a/migrated_functions.go +++ b/sprig_functions_not_included_in_sprout.go @@ -1,3 +1,24 @@ +/** + * This file lists the functions originally part of the Sprig library that are + * intentionally excluded from the Sprout library. The exclusions are based on\ + * community decisions and technical evaluations aimed at enhancing security, + * relevance, and performance in the context of Go templates. + * Each exclusion is supported by rational and further community discussions + * can be found on our GitHub issues page. + * + * Exclusion Criteria: + * 1. Crypto functions: Deemed inappropriate for Go templates due to inherent security risks. + * 2. Irrelevant functions: Omitted because they do not provide utility in the context of Go templates. + * 3. Deprecated/Insecure: Functions using outdated or insecure standards are excluded. + * 4. Temporary exclusions: Certain functions are temporarily excluded to prevent breaking changes, + * pending the implementation of the new loader feature. + * 5. Community decision: Choices made by the community are documented and can be discussed at + * https://github.com/42atomys/sprout/issues/1. + * + * The Sprout library is an open-source project and welcomes contributions from the community. + * To discuss existing exclusions or propose new ones, please contribute to the discussions on + * our GitHub repository. + */ package sprout import ( @@ -34,31 +55,10 @@ import ( "time" sv2 "github.com/Masterminds/semver/v3" - "github.com/shopspring/decimal" bcrypt_lib "golang.org/x/crypto/bcrypt" "golang.org/x/crypto/scrypt" ) -func (fh *FunctionHandler) FillMapWithParts(parts []string) map[string]string { - res := make(map[string]string, len(parts)) - for i, v := range parts { - res[fmt.Sprintf("_%d", i)] = v - } - return res -} - -func (fh *FunctionHandler) DictGetOrEmpty(dict map[string]any, key string) string { - value, ok := dict[key] - if !ok { - return "" - } - tp := reflect.TypeOf(value).Kind() - if tp != reflect.String { - panic(fmt.Sprintf("unable to parse %s key, must be of type string, but %s found", key, tp.String())) - } - return reflect.ValueOf(value).String() -} - func (fh *FunctionHandler) UrlParse(v string) map[string]any { dict := map[string]any{} parsedURL, err := url.Parse(v) @@ -83,14 +83,14 @@ func (fh *FunctionHandler) UrlParse(v string) map[string]any { func (fh *FunctionHandler) UrlJoin(d map[string]any) string { resURL := url.URL{ - Scheme: fh.DictGetOrEmpty(d, "scheme"), - Host: fh.DictGetOrEmpty(d, "host"), - Path: fh.DictGetOrEmpty(d, "path"), - RawQuery: fh.DictGetOrEmpty(d, "query"), - Opaque: fh.DictGetOrEmpty(d, "opaque"), - Fragment: fh.DictGetOrEmpty(d, "fragment"), - } - userinfo := fh.DictGetOrEmpty(d, "userinfo") + Scheme: fh.Get(d, "scheme").(string), + Host: fh.Get(d, "host").(string), + Path: fh.Get(d, "path").(string), + RawQuery: fh.Get(d, "query").(string), + Opaque: fh.Get(d, "opaque").(string), + Fragment: fh.Get(d, "fragment").(string), + } + userinfo := fh.Get(d, "userinfo").(string) var user *url.Userinfo if userinfo != "" { tempURL, err := url.Parse(fmt.Sprintf("proto://%s@host", userinfo)) @@ -104,20 +104,6 @@ func (fh *FunctionHandler) UrlJoin(d map[string]any) string { return resURL.String() } -func (fh *FunctionHandler) IntArrayToString(slice []int, delimeter string) string { - return strings.Trim(strings.Join(strings.Fields(fmt.Sprint(slice)), delimeter), "[]") -} - -func (fh *FunctionHandler) ExecDecimalOp(a any, b []any, f func(d1, d2 decimal.Decimal) decimal.Decimal) float64 { - prt := decimal.NewFromFloat(fh.ToFloat64(a)) - for _, x := range b { - dx := decimal.NewFromFloat(fh.ToFloat64(x)) - prt = f(prt, dx) - } - rslt, _ := prt.Float64() - return rslt -} - func (fh *FunctionHandler) GetHostByName(name string) string { addrs, _ := net.LookupHost(name) //TODO: add error handing when release v3 comes out diff --git a/sprout.go b/sprout.go index 432e280..8cad5a2 100644 --- a/sprout.go +++ b/sprout.go @@ -88,12 +88,6 @@ func WithFunctionHandler(new *FunctionHandler) FunctionHandlerOption { func FuncMap(opts ...FunctionHandlerOption) template.FuncMap { fnHandler := NewFunctionHandler(opts...) - // BACKWARD COMPATIBILITY - // Fallback to FuncMap() to get the unmigrated functions - for k, v := range TxtFuncMap() { - fnHandler.funcMap[k] = v - } - // Added migrated functions fnHandler.funcMap["hello"] = fnHandler.Hello diff --git a/sprout_test.go b/sprout_test.go index 0e47bd1..43b8425 100644 --- a/sprout_test.go +++ b/sprout_test.go @@ -85,24 +85,3 @@ func TestFuncMap_IncludesHello(t *testing.T) { assert.Equal(t, "Hello!", helloFunc()) } - -// This test ensures backward compatibility by checking if FuncMap (the function mentioned in the comment) exists or needs to be implemented for the test. -func TestFuncMap_BackwardCompatibility(t *testing.T) { - // Assuming FuncMap() is implemented and returns a template.FuncMap - // Replace the implementation details as per actual FuncMap function. - genericMap["TestFuncMap_BackwardCompatibility"] = func() string { - return "example" - } - - funcMap := FuncMap() - exampleFunc, exists := funcMap["TestFuncMap_BackwardCompatibility"] - assert.True(t, exists) - - result, ok := exampleFunc.(func() string) - assert.True(t, ok) - assert.Equal(t, "example", result()) - - helloFunc, ok := funcMap["hello"].(func() string) - assert.True(t, ok) - assert.Equal(t, "Hello!", helloFunc()) -} diff --git a/strings_functions.go b/strings_functions.go index 382bb73..4f66ae0 100644 --- a/strings_functions.go +++ b/strings_functions.go @@ -955,7 +955,7 @@ func (fh *FunctionHandler) SwapCase(str string) string { // {{ "apple,banana,cherry" | split "," }} // Output: { "_0":"apple", "_1":"banana", "_2":"cherry" } func (fh *FunctionHandler) Split(sep, orig string) map[string]string { parts := strings.Split(orig, sep) - return fh.FillMapWithParts(parts) + return fh.populateMapWithParts(parts) } // Splitn divides 'orig' into a map of string parts using 'sep' as the separator @@ -976,7 +976,31 @@ func (fh *FunctionHandler) Split(sep, orig string) map[string]string { // {{ "apple,banana,cherry" | split "," 2 }} // Output: { "_0":"apple", "_1":"banana,cherry" } func (fh *FunctionHandler) Splitn(sep string, n int, orig string) map[string]string { parts := strings.SplitN(orig, sep, n) - return fh.FillMapWithParts(parts) + return fh.populateMapWithParts(parts) +} + +// populateMapWithParts converts an array of strings into a map with keys based +// on the index of each string. +// +// Parameters: +// +// parts []string - the array of strings to be converted into a map. +// +// Returns: +// +// map[string]string - a map where each key corresponds to an index (with an underscore prefix) of the string in the input array. +// +// Example: +// +// parts := []string{"apple", "banana", "cherry"} +// result := fh.populateMapWithParts(parts) +// fmt.Println(result) // Output: {"_0": "apple", "_1": "banana", "_2": "cherry"} +func (fh *FunctionHandler) populateMapWithParts(parts []string) map[string]string { + res := make(map[string]string, len(parts)) + for i, v := range parts { + res[fmt.Sprintf("_%d", i)] = v + } + return res } // Substring extracts a substring from 's' starting at 'start' and ending at 'end'. @@ -1088,7 +1112,7 @@ func (fh *FunctionHandler) Seq(params ...int) string { if end < start { increment = -1 } - return fh.IntArrayToString(fh.UntilStep(start, end+increment, increment), " ") + return fh.convertIntArrayToString(fh.UntilStep(start, end+increment, increment), " ") case 3: start := params[0] end := params[2] @@ -1099,7 +1123,7 @@ func (fh *FunctionHandler) Seq(params ...int) string { return "" } } - return fh.IntArrayToString(fh.UntilStep(start, end+increment, step), " ") + return fh.convertIntArrayToString(fh.UntilStep(start, end+increment, step), " ") case 2: start := params[0] end := params[1] @@ -1107,8 +1131,29 @@ func (fh *FunctionHandler) Seq(params ...int) string { if end < start { step = -1 } - return fh.IntArrayToString(fh.UntilStep(start, end+step, step), " ") + return fh.convertIntArrayToString(fh.UntilStep(start, end+step, step), " ") default: return "" } } + +// convertIntArrayToString converts an array of integers into a single string +// with elements separated by a given delimiter. +// +// Parameters: +// +// slice []int - the array of integers to convert. +// delimiter string - the string to use as a delimiter between the integers in the output string. +// +// Returns: +// +// string - the resulting string that concatenates all the integers in the array separated by the specified delimiter. +// +// Example: +// +// slice := []int{1, 2, 3, 4, 5} +// result := fh.convertIntArrayToString(slice, ", ") +// fmt.Println(result) // Output: "1, 2, 3, 4, 5" +func (fh *FunctionHandler) convertIntArrayToString(slice []int, delimeter string) string { + return strings.Trim(strings.Join(strings.Fields(fmt.Sprint(slice)), delimeter), "[]") +} diff --git a/to_migrate.go b/to_migrate.go deleted file mode 100644 index 73a6144..0000000 --- a/to_migrate.go +++ /dev/null @@ -1,74 +0,0 @@ -package sprout - -import ( - "html/template" - ttemplate "text/template" -) - -// These functions are not guaranteed to evaluate to the same result for given input, because they -// refer to the environment or global state. -var nonhermeticFunctions = []string{ - // Date functions - "date", - "date_in_zone", - "dateInZone", - "date_modify", - "dateModify", - "now", - "htmlDate", - "htmlDateInZone", - - // Strings - "randAlphaNum", - "randAlpha", - "randAscii", - "randNumeric", - "randBytes", - "uuidv4", - - // OS - "env", - "expandenv", - - // Network - "getHostByName", -} - -var genericMap = map[string]interface{}{} - -// HermeticTxtFuncMap returns a 'text/template'.FuncMap with only repeatable functions. -func HermeticTxtFuncMap() ttemplate.FuncMap { - r := TxtFuncMap() - for _, name := range nonhermeticFunctions { - delete(r, name) - } - return r -} - -// HermeticHtmlFuncMap returns an 'html/template'.Funcmap with only repeatable functions. -func HermeticHtmlFuncMap() template.FuncMap { - r := HtmlFuncMap() - for _, name := range nonhermeticFunctions { - delete(r, name) - } - return r -} - -// TxtFuncMap returns a 'text/template'.FuncMap -func TxtFuncMap() ttemplate.FuncMap { - return ttemplate.FuncMap(GenericFuncMap()) -} - -// HtmlFuncMap returns an 'html/template'.Funcmap -func HtmlFuncMap() template.FuncMap { - return template.FuncMap(GenericFuncMap()) -} - -// GenericFuncMap returns a copy of the basic function map as a map[string]interface{}. -func GenericFuncMap() map[string]interface{} { - gfm := make(map[string]interface{}, len(genericMap)) - for k, v := range genericMap { - gfm[k] = v - } - return gfm -} diff --git a/to_migrate_test.go b/to_migrate_test.go deleted file mode 100644 index e2259e3..0000000 --- a/to_migrate_test.go +++ /dev/null @@ -1,557 +0,0 @@ -package sprout - -import ( - "bytes" - "crypto/x509" - "encoding/base64" - "encoding/pem" - "fmt" - "net" - "strings" - "testing" - "text/template" - - "github.com/stretchr/testify/assert" - bcrypt_lib "golang.org/x/crypto/bcrypt" -) - -var urlTests = map[string]map[string]interface{}{ - "proto://auth@host:80/path?query#fragment": { - "fragment": "fragment", - "host": "host:80", - "hostname": "host", - "opaque": "", - "path": "/path", - "query": "query", - "scheme": "proto", - "userinfo": "auth", - }, - "proto://host:80/path": { - "fragment": "", - "host": "host:80", - "hostname": "host", - "opaque": "", - "path": "/path", - "query": "", - "scheme": "proto", - "userinfo": "", - }, - "something": { - "fragment": "", - "host": "", - "hostname": "", - "opaque": "", - "path": "something", - "query": "", - "scheme": "", - "userinfo": "", - }, - "proto://user:passwor%20d@host:80/path": { - "fragment": "", - "host": "host:80", - "hostname": "host", - "opaque": "", - "path": "/path", - "query": "", - "scheme": "proto", - "userinfo": "user:passwor%20d", - }, - "proto://host:80/pa%20th?key=val%20ue": { - "fragment": "", - "host": "host:80", - "hostname": "host", - "opaque": "", - "path": "/pa th", - "query": "key=val%20ue", - "scheme": "proto", - "userinfo": "", - }, -} - -func TestUrlParse(t *testing.T) { - // testing that function is exported and working properly - assert.NoError(t, runt( - `{{ index ( urlParse "proto://auth@host:80/path?query#fragment" ) "host" }}`, - "host:80")) - - // testing scenarios - fh := NewFunctionHandler() - for url, expected := range urlTests { - assert.EqualValues(t, expected, fh.UrlParse(url)) - } -} - -func TestUrlJoin(t *testing.T) { - tests := map[string]string{ - `{{ urlJoin (dict "fragment" "fragment" "host" "host:80" "path" "/path" "query" "query" "scheme" "proto") }}`: "proto://host:80/path?query#fragment", - `{{ urlJoin (dict "fragment" "fragment" "host" "host:80" "path" "/path" "scheme" "proto" "userinfo" "ASDJKJSD") }}`: "proto://ASDJKJSD@host:80/path#fragment", - } - for tpl, expected := range tests { - assert.NoError(t, runt(tpl, expected)) - } - - fh := NewFunctionHandler() - for expected, urlMap := range urlTests { - assert.EqualValues(t, expected, fh.UrlJoin(urlMap)) - } - -} - -func TestSemverCompare(t *testing.T) { - tests := map[string]string{ - `{{ semverCompare "1.2.3" "1.2.3" }}`: `true`, - `{{ semverCompare "^1.2.0" "1.2.3" }}`: `true`, - `{{ semverCompare "^1.2.0" "2.2.3" }}`: `false`, - } - for tpl, expect := range tests { - assert.NoError(t, runt(tpl, expect)) - } -} - -func TestSemver(t *testing.T) { - tests := map[string]string{ - `{{ $s := semver "1.2.3-beta.1+c0ff33" }}{{ $s.Prerelease }}`: "beta.1", - `{{ $s := semver "1.2.3-beta.1+c0ff33" }}{{ $s.Major}}`: "1", - `{{ semver "1.2.3" | (semver "1.2.3").Compare }}`: `0`, - `{{ semver "1.2.3" | (semver "1.3.3").Compare }}`: `1`, - `{{ semver "1.4.3" | (semver "1.2.3").Compare }}`: `-1`, - } - for tpl, expect := range tests { - assert.NoError(t, runt(tpl, expect)) - } -} - -func TestGetHostByName(t *testing.T) { - tpl := `{{"www.google.com" | getHostByName}}` - - resolvedIP, _ := runRaw(tpl, nil) - - ip := net.ParseIP(resolvedIP) - assert.NotNil(t, ip) - assert.NotEmpty(t, ip) -} - -func TestIssue188(t *testing.T) { - tests := map[string]string{ - - // This first test shows two merges and the merge is NOT A DEEP COPY MERGE. - // The first merge puts $one on to $target. When the second merge of $two - // on to $target the nested dict brought over from $one is changed on - // $one as well as $target. - `{{- $target := dict -}} - {{- $one := dict "foo" (dict "bar" "baz") "qux" true -}} - {{- $two := dict "foo" (dict "bar" "baz2") "qux" false -}} - {{- mergeOverwrite $target $one | toString | trunc 0 }}{{ $__ := mergeOverwrite $target $two }}{{ $one }}`: "map[foo:map[bar:baz2] qux:true]", - - // This test uses deepCopy on $one to create a deep copy and then merge - // that. In this case the merge of $two on to $target does not affect - // $one because a deep copy was used for that merge. - `{{- $target := dict -}} - {{- $one := dict "foo" (dict "bar" "baz") "qux" true -}} - {{- $two := dict "foo" (dict "bar" "baz2") "qux" false -}} - {{- deepCopy $one | mergeOverwrite $target | toString | trunc 0 }}{{ $__ := mergeOverwrite $target $two }}{{ $one }}`: "map[foo:map[bar:baz] qux:true]", - } - - for tpl, expect := range tests { - if err := runt(tpl, expect); err != nil { - t.Error(err) - } - } -} - -// runt runs a template and checks that the output exactly matches the expected string. -func runt(tpl, expect string) error { - return runtv(tpl, expect, map[string]string{}) -} - -// runtv takes a template, and expected return, and values for substitution. -// -// It runs the template and verifies that the output is an exact match. -func runtv(tpl, expect string, vars interface{}) error { - t := template.Must(template.New("test").Funcs(FuncMap()).Parse(tpl)) - var b bytes.Buffer - err := t.Execute(&b, vars) - if err != nil { - return err - } - if expect != b.String() { - return fmt.Errorf("Expected '%v', got '%v'", expect, b.String()) - } - return nil -} - -// runRaw runs a template with the given variables and returns the result. -func runRaw(tpl string, vars interface{}) (string, error) { - t := template.Must(template.New("test").Funcs(FuncMap()).Parse(tpl)) - var b bytes.Buffer - err := t.Execute(&b, vars) - if err != nil { - return "", err - } - return b.String(), nil -} - -const ( - beginCertificate = "-----BEGIN CERTIFICATE-----" - endCertificate = "-----END CERTIFICATE-----" -) - -var ( - // fastCertKeyAlgos is the list of private key algorithms that are supported for certificate use, and - // are fast to generate. - fastCertKeyAlgos = []string{ - "ecdsa", - "ed25519", - } -) - -func TestSha256Sum(t *testing.T) { - tpl := `{{"abc" | sha256sum}}` - if err := runt(tpl, "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"); err != nil { - t.Error(err) - } -} -func TestSha1Sum(t *testing.T) { - tpl := `{{"abc" | sha1sum}}` - if err := runt(tpl, "a9993e364706816aba3e25717850c26c9cd0d89d"); err != nil { - t.Error(err) - } -} - -func TestAdler32Sum(t *testing.T) { - tpl := `{{"abc" | adler32sum}}` - if err := runt(tpl, "38600999"); err != nil { - t.Error(err) - } -} - -func TestBcrypt(t *testing.T) { - out, err := runRaw(`{{"abc" | bcrypt}}`, nil) - if err != nil { - t.Error(err) - } - if bcrypt_lib.CompareHashAndPassword([]byte(out), []byte("abc")) != nil { - t.Error("Generated hash is not the equivalent for password:", "abc") - } -} - -type HtpasswdCred struct { - Username string - Password string - Valid bool -} - -func TestHtpasswd(t *testing.T) { - expectations := []HtpasswdCred{ - {Username: "myUser", Password: "myPassword", Valid: true}, - {Username: "special'o79Cv_*qFe,)