From 012dbc9d81ca61a7c099c5069eefdb663622aae2 Mon Sep 17 00:00:00 2001 From: George Robinson Date: Tue, 7 Mar 2023 10:37:39 +0000 Subject: [PATCH] Fix corruption in templates that use title function (#3278) This commit fixes data corruption in templates that use the title function as it used a shared cases.Title when casers should not be shared between goroutines. When templates that used the title function were executed at the same time, data corruption would occur in the text that was being cased. This is fixed using a separate caser for each function call. Add tests to assertDefaultFuncs are thread-safe Signed-off-by: George Robinson --- template/template.go | 11 +++++--- template/template_test.go | 58 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/template/template.go b/template/template.go index 3295544a..73035647 100644 --- a/template/template.go +++ b/template/template.go @@ -168,11 +168,14 @@ func (t *Template) ExecuteHTMLString(html string, data interface{}) (string, err type FuncMap map[string]interface{} var DefaultFuncs = FuncMap{ - "toUpper": strings.ToUpper, - "toLower": strings.ToLower, + "toUpper": strings.ToUpper, + "toLower": strings.ToLower, + "title": func(text string) string { + // casers should not be shared between goroutines, instead + // create a new caser each time this function is called + return cases.Title(language.AmericanEnglish).String(text) + }, "trimSpace": strings.TrimSpace, - - "title": cases.Title(language.AmericanEnglish).String, // join is equal to strings.Join but inverts the argument order // for easier pipelining in templates. "join": func(sep string, s []string) string { diff --git a/template/template_test.go b/template/template_test.go index c28fa592..97d36fb1 100644 --- a/template/template_test.go +++ b/template/template_test.go @@ -16,6 +16,7 @@ package template import ( tmplhtml "html/template" "net/url" + "sync" "testing" tmpltext "text/template" "time" @@ -465,3 +466,60 @@ func TestTemplateExpansionWithOptions(t *testing.T) { }) } } + +// This test asserts that template functions are thread-safe. +func TestTemplateFuncs(t *testing.T) { + tmpl, err := FromGlobs([]string{}) + require.NoError(t, err) + + for _, tc := range []struct { + title string + in string + data interface{} + exp string + }{{ + title: "Template using toUpper", + in: `{{ "abc" | toUpper }}`, + exp: "ABC", + }, { + title: "Template using toLower", + in: `{{ "ABC" | toLower }}`, + exp: "abc", + }, { + title: "Template using title", + in: `{{ "abc" | title }}`, + exp: "Abc", + }, { + title: "Template using trimSpace", + in: `{{ " abc " | trimSpace }}`, + exp: "abc", + }, { + title: "Template using join", + in: `{{ . | join "," }}`, + data: []string{"abc", "def"}, + exp: "abc,def", + }, { + title: "Template using match", + in: `{{ match "[a-z]+" "abc" }}`, + exp: "true", + }, { + title: "Template using reReplaceAll", + in: `{{ reReplaceAll "ab" "AB" "abc" }}`, + exp: "ABc", + }} { + tc := tc + t.Run(tc.title, func(t *testing.T) { + wg := sync.WaitGroup{} + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + defer wg.Done() + got, err := tmpl.ExecuteTextString(tc.in, tc.data) + require.NoError(t, err) + require.Equal(t, tc.exp, got) + }() + } + wg.Wait() + }) + } +}