Upgrade to to Go 1.26.1 (#14597) (note)

This fixes a security issue: https://www.cve.org/CVERecord?id=CVE-2026-27142

Closes #14595
This commit is contained in:
Bjørn Erik Pedersen
2026-03-11 12:36:13 +01:00
committed by GitHub
parent 842d8f1052
commit 1f578f160c
29 changed files with 447 additions and 212 deletions
+2 -2
View File
@@ -4,7 +4,7 @@ parameters:
defaults: &defaults
resource_class: large
docker:
- image: bepsays/ci-hugoreleaser:1.22600.20000
- image: bepsays/ci-hugoreleaser:1.22600.20100
environment: &buildenv
GOMODCACHE: /root/project/gomodcache
version: 2
@@ -58,7 +58,7 @@ jobs:
environment:
<<: [*buildenv]
docker:
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22600.20000
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22600.20100
steps:
- *restore-cache
- &attach-workspace
+1 -1
View File
@@ -136,7 +136,7 @@ func IsTruthfulValue(val reflect.Value) (truth bool) {
truth = val.Bool()
case reflect.Complex64, reflect.Complex128:
truth = val.Complex() != 0
case reflect.Chan, reflect.Func, reflect.Pointer, reflect.Interface:
case reflect.Chan, reflect.Func, reflect.Pointer, reflect.UnsafePointer, reflect.Interface:
truth = !val.IsNil()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
truth = val.Int() != 0
+1 -1
View File
@@ -15,7 +15,7 @@ import (
)
func main() {
// The current is built with 3901409b5d [release-branch.go1.24] go1.24.0
// The current is built with e87b10ea2a2c6c65b80c4374af42b9c02ac9fb20 go1.26.1
// TODO(bep) preserve the staticcheck.conf file.
fmt.Println("Forking ...")
defer fmt.Println("Done ...")
@@ -14,11 +14,12 @@ func _() {
_ = x[attrStyle-3]
_ = x[attrURL-4]
_ = x[attrSrcset-5]
_ = x[attrMetaContent-6]
}
const _attr_name = "attrNoneattrScriptattrScriptTypeattrStyleattrURLattrSrcset"
const _attr_name = "attrNoneattrScriptattrScriptTypeattrStyleattrURLattrSrcsetattrMetaContent"
var _attr_index = [...]uint8{0, 8, 18, 32, 41, 48, 58}
var _attr_index = [...]uint8{0, 8, 18, 32, 41, 48, 58, 73}
func (i attr) String() string {
if i >= attr(len(_attr_index)-1) {
@@ -157,6 +157,10 @@ const (
// stateError is an infectious error state outside any valid
// HTML/CSS/JS construct.
stateError
// stateMetaContent occurs inside a HTML meta element content attribute.
stateMetaContent
// stateMetaContentURL occurs inside a "url=" tag in a HTML meta element content attribute.
stateMetaContentURL
// stateDead marks unreachable code after a {{break}} or {{continue}}.
stateDead
)
@@ -268,6 +272,8 @@ const (
elementTextarea
// elementTitle corresponds to the RCDATA <title> element.
elementTitle
// elementMeta corresponds to the HTML <meta> element.
elementMeta
)
//go:generate stringer -type attr
@@ -289,4 +295,6 @@ const (
attrURL
// attrSrcset corresponds to a srcset attribute.
attrSrcset
// attrMetaContent corresponds to the content attribute in meta HTML element.
attrMetaContent
)
@@ -28,6 +28,9 @@ HTML templates treat data values as plain text which should be encoded so they
can be safely embedded in an HTML document. The escaping is contextual, so
actions can appear within JavaScript, CSS, and URI contexts.
Comments are stripped from output, except for those passed in via the
[HTML], [CSS], and [JS] types for their respective contexts.
The security model used by this package assumes that template authors are
trusted, while Execute's data parameter is not. More details are
provided below.
@@ -210,7 +213,7 @@ that would have been produced if {{.}} was a regular string.
# Security Model
https://rawgit.com/mikesamuel/sanitized-jquery-templates/trunk/safetemplate.html#problem_definition defines "safe" as used by this package.
https://web.archive.org/web/20160501113828/http://js-quasis-libraries-and-repl.googlecode.com/svn/trunk/safetemplate.html#problem_definition defines "safe" as used by this package.
This package assumes that template authors are trusted, that Execute's data
parameter is not, and seeks to preserve the properties below in the face
@@ -13,11 +13,12 @@ func _() {
_ = x[elementStyle-2]
_ = x[elementTextarea-3]
_ = x[elementTitle-4]
_ = x[elementMeta-5]
}
const _element_name = "elementNoneelementScriptelementStyleelementTextareaelementTitle"
const _element_name = "elementNoneelementScriptelementStyleelementTextareaelementTitleelementMeta"
var _element_index = [...]uint8{0, 11, 24, 36, 51, 63}
var _element_index = [...]uint8{0, 11, 24, 36, 51, 63, 74}
func (i element) String() string {
if i >= element(len(_element_index)-1) {
@@ -80,15 +80,18 @@ const (
ErrBadHTML
// ErrBranchEnd: "{{if}} branches end in different contexts"
// Example:
// Examples:
// {{if .C}}<a href="{{end}}{{.X}}
// <script {{with .T}}type="{{.}}"{{end}}>
// Discussion:
// Package html/template statically examines each path through an
// {{if}}, {{range}}, or {{with}} to escape any following pipelines.
// The example is ambiguous since {{.X}} might be an HTML text node,
// The first example is ambiguous since {{.X}} might be an HTML text node,
// or a URL prefix in an HTML attribute. The context of {{.X}} is
// used to figure out how to escape it, but that context depends on
// the run-time value of {{.C}} which is not statically known.
// The second example is ambiguous as the script type attribute
// can change the type of escaping needed for the script contents.
//
// The problem is usually something like missing quotes or angle
// brackets, or can be avoided by refactoring to put the two contexts
@@ -164,7 +164,7 @@ func (e *escaper) escape(c context, n parse.Node) context {
panic("escaping " + n.String() + " is unimplemented")
}
// var debugAllowActionJSTmpl = godebug.New("jstmpllitinterp")
var htmlmetacontenturlescape = true // godebug.New("htmlmetacontenturlescape")
// escapeAction escapes an action template node.
func (e *escaper) escapeAction(c context, n *parse.ActionNode) context {
@@ -223,6 +223,18 @@ func (e *escaper) escapeAction(c context, n *parse.ActionNode) context {
default:
panic(c.urlPart.String())
}
case stateMetaContent:
// Handled below in delim check.
case stateMetaContentURL:
if htmlmetacontenturlescape {
s = append(s, "_html_template_urlfilter")
} else {
// We don't have a great place to increment this, since it's hard to
// know if we actually escape any urls in _html_template_urlfilter,
// since it has no information about what context it is being
// executed in etc. This is probably the best we can do.
// htmlmetacontenturlescape.IncNonDefault()
}
case stateJS:
s = append(s, "_html_template_jsvalescaper")
// A slash after a value starts a div operator.
@@ -739,6 +739,16 @@ func TestEscape(t *testing.T) {
"<script>var a = `${ var a = \"{{\"a \\\" d\"}}\" }`</script>",
"<script>var a = `${ var a = \"a \\u0022 d\" }`</script>",
},
{
"meta content attribute url",
`<meta http-equiv="refresh" content="asd; url={{"javascript:alert(1)"}}; asd; url={{"vbscript:alert(1)"}}; asd">`,
`<meta http-equiv="refresh" content="asd; url=#ZgotmplZ; asd; url=#ZgotmplZ; asd">`,
},
{
"meta content string",
`<meta http-equiv="refresh" content="{{"asd: 123"}}">`,
`<meta http-equiv="refresh" content="asd: 123">`,
},
}
for _, test := range tests {
@@ -944,7 +954,6 @@ func TestEscapeSet(t *testing.T) {
t.Errorf("want\n\t%q\ngot\n\t%q", test.want, got)
}
}
}
func TestErrors(t *testing.T) {
@@ -1021,6 +1030,14 @@ func TestErrors(t *testing.T) {
"<script>var tmpl = `asd ${return \"{\"}`;</script>",
``,
},
{
`{{if eq "" ""}}<meta>{{end}}`,
``,
},
{
`{{if eq "" ""}}<meta content="url={{"asd"}}">{{end}}`,
``,
},
// Error cases.
{
@@ -1198,7 +1215,6 @@ func TestErrors(t *testing.T) {
// Check that we get the same error if we call Execute again.
if err := tmpl.Execute(buf, nil); err == nil || err.Error() != got {
t.Errorf("input=%q: unexpected error on second call %q", test.input, err)
}
}
}
@@ -325,12 +325,16 @@ var execTests = []execTest{
{"$.U.V", "{{$.U.V}}", "v", tVal, true},
{"declare in action", "{{$x := $.U.V}}{{$x}}", "v", tVal, true},
{"simple assignment", "{{$x := 2}}{{$x = 3}}{{$x}}", "3", tVal, true},
{"nested assignment",
{
"nested assignment",
"{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{$x}}",
"3", tVal, true},
{"nested assignment changes the last declaration",
"3", tVal, true,
},
{
"nested assignment changes the last declaration",
"{{$x := 1}}{{if true}}{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{end}}{{$x}}",
"1", tVal, true},
"1", tVal, true,
},
// Type with String method.
{"V{6666}.String()", "-{{.V0}}-", "-{6666}-", tVal, true}, // NOTE: -<6666>- in text/template
@@ -377,15 +381,21 @@ var execTests = []execTest{
{".Method3(nil constant)", "-{{.Method3 nil}}-", "-Method3: &lt;nil&gt;-", tVal, true},
{".Method3(nil value)", "-{{.Method3 .MXI.unset}}-", "-Method3: &lt;nil&gt;-", tVal, true},
{"method on var", "{{if $x := .}}-{{$x.Method2 .U16 $x.X}}{{end}}-", "-Method2: 16 x-", tVal, true},
{"method on chained var",
{
"method on chained var",
"{{range .MSIone}}{{if $.U.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}",
"true", tVal, true},
{"chained method",
"true", tVal, true,
},
{
"chained method",
"{{range .MSIone}}{{if $.GetU.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}",
"true", tVal, true},
{"chained method on variable",
"true", tVal, true,
},
{
"chained method on variable",
"{{with $x := .}}{{with .SI}}{{$.GetU.TrueFalse $.True}}{{end}}{{end}}",
"true", tVal, true},
"true", tVal, true,
},
{".NilOKFunc not nil", "{{call .NilOKFunc .PI}}", "false", tVal, true},
{".NilOKFunc nil", "{{call .NilOKFunc nil}}", "true", tVal, true},
{"method on nil value from slice", "-{{range .}}{{.Method1 1234}}{{end}}-", "-1234-", tSliceOfNil, true},
@@ -471,10 +481,14 @@ var execTests = []execTest{
{"printf lots", `{{printf "%d %s %g %s" 127 "hello" 7-3i .Method0}}`, "127 hello (7-3i) M0", tVal, true},
// HTML.
{"html", `{{html "<script>alert(\"XSS\");</script>"}}`,
"&lt;script&gt;alert(&#34;XSS&#34;);&lt;/script&gt;", nil, true},
{"html pipeline", `{{printf "<script>alert(\"XSS\");</script>" | html}}`,
"&lt;script&gt;alert(&#34;XSS&#34;);&lt;/script&gt;", nil, true},
{
"html", `{{html "<script>alert(\"XSS\");</script>"}}`,
"&lt;script&gt;alert(&#34;XSS&#34;);&lt;/script&gt;", nil, true,
},
{
"html pipeline", `{{printf "<script>alert(\"XSS\");</script>" | html}}`,
"&lt;script&gt;alert(&#34;XSS&#34;);&lt;/script&gt;", nil, true,
},
{"html", `{{html .PS}}`, "a string", tVal, true},
{"html typed nil", `{{html .NIL}}`, "&lt;nil&gt;", tVal, true},
{"html untyped nil", `{{html .Empty0}}`, "&lt;nil&gt;", tVal, true}, // NOTE: "&lt;no value&gt;" in text/template
@@ -716,7 +730,7 @@ func count(n int) chan string {
}
c := make(chan string)
go func() {
for i := range n {
for i := 0; i < n; i++ {
c <- "abcdefghijklmnop"[i : i+1]
}
close(c)
@@ -840,7 +854,7 @@ var delimPairs = []string{
func TestDelims(t *testing.T) {
const hello = "Hello, world"
var value = struct{ Str string }{hello}
value := struct{ Str string }{hello}
for i := 0; i < len(delimPairs); i += 2 {
text := ".Str"
left := delimPairs[i+0]
@@ -863,7 +877,7 @@ func TestDelims(t *testing.T) {
if err != nil {
t.Fatalf("delim %q text %q parse err %s", left, text, err)
}
var b = new(strings.Builder)
b := new(strings.Builder)
err = tmpl.Execute(b, value)
if err != nil {
t.Fatalf("delim %q exec err %s", left, err)
@@ -964,7 +978,7 @@ const treeTemplate = `
`
func TestTree(t *testing.T) {
var tree = &Tree{
tree := &Tree{
1,
&Tree{
2, &Tree{
@@ -1215,7 +1229,7 @@ var cmpTests = []cmpTest{
func TestComparison(t *testing.T) {
b := new(strings.Builder)
var cmpStruct = struct {
cmpStruct := struct {
Uthree, Ufour uint
NegOne, Three int
Ptr, NilPtr *int
@@ -1604,8 +1618,8 @@ func TestInterfaceValues(t *testing.T) {
"Nil": nil,
"Zero": 0,
})
if after, ok := strings.CutPrefix(tt.out, "ERROR:"); ok {
e := strings.TrimSpace(after)
if strings.HasPrefix(tt.out, "ERROR:") {
e := strings.TrimSpace(strings.TrimPrefix(tt.out, "ERROR:"))
if err == nil || !strings.Contains(err.Error(), e) {
t.Errorf("%s: Execute: %v, want error %q", tt.text, err, e)
}
@@ -1741,7 +1755,7 @@ func TestEscapeRace(t *testing.T) {
t.Fatal(err)
}
const count = 20
for i := range count {
for i := 0; i < count; i++ {
_, err := tmpl.New(fmt.Sprintf("x%d.html", i)).Parse(`{{ template "templ.html" .}}`)
if err != nil {
t.Fatal(err)
@@ -1749,11 +1763,11 @@ func TestEscapeRace(t *testing.T) {
}
var wg sync.WaitGroup
for range 10 {
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := range count {
for j := 0; j < count; j++ {
sub := tmpl.Lookup(fmt.Sprintf("x%d.html", j))
if err := sub.Execute(io.Discard, nil); err != nil {
t.Error(err)
@@ -36,12 +36,14 @@ func _() {
_ = x[stateCSSBlockCmt-25]
_ = x[stateCSSLineCmt-26]
_ = x[stateError-27]
_ = x[stateDead-28]
_ = x[stateMetaContent-28]
_ = x[stateMetaContentURL-29]
_ = x[stateDead-30]
}
const _state_name = "stateTextstateTagstateAttrNamestateAfterNamestateBeforeValuestateHTMLCmtstateRCDATAstateAttrstateURLstateSrcsetstateJSstateJSDqStrstateJSSqStrstateJSTmplLitstateJSRegexpstateJSBlockCmtstateJSLineCmtstateJSHTMLOpenCmtstateJSHTMLCloseCmtstateCSSstateCSSDqStrstateCSSSqStrstateCSSDqURLstateCSSSqURLstateCSSURLstateCSSBlockCmtstateCSSLineCmtstateErrorstateDead"
const _state_name = "stateTextstateTagstateAttrNamestateAfterNamestateBeforeValuestateHTMLCmtstateRCDATAstateAttrstateURLstateSrcsetstateJSstateJSDqStrstateJSSqStrstateJSTmplLitstateJSRegexpstateJSBlockCmtstateJSLineCmtstateJSHTMLOpenCmtstateJSHTMLCloseCmtstateCSSstateCSSDqStrstateCSSSqStrstateCSSDqURLstateCSSSqURLstateCSSURLstateCSSBlockCmtstateCSSLineCmtstateErrorstateMetaContentstateMetaContentURLstateDead"
var _state_index = [...]uint16{0, 9, 17, 30, 44, 60, 72, 83, 92, 100, 111, 118, 130, 142, 156, 169, 184, 198, 216, 235, 243, 256, 269, 282, 295, 306, 322, 337, 347, 356}
var _state_index = [...]uint16{0, 9, 17, 30, 44, 60, 72, 83, 92, 100, 111, 118, 130, 142, 156, 169, 184, 198, 216, 235, 243, 256, 269, 282, 295, 306, 322, 337, 347, 363, 382, 391}
func (i state) String() string {
if i >= state(len(_state_index)-1) {
@@ -267,7 +267,7 @@ func (t *Template) Clone() (*Template, error) {
name := x.Name()
src := t.set[name]
if src == nil || src.escapeErr != nil {
return nil, fmt.Errorf("html/template: cannot Clone %q after it has executed, %q not found", t.Name(), name)
return nil, fmt.Errorf("html/template: cannot Clone %q after it has executed", t.Name())
}
x.Tree = x.Tree.Copy()
ret.set[name] = &Template{
@@ -14,11 +14,11 @@ import (
"testing"
. "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse" // https://golang.org/issue/12996
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
)
func TestTemplateClone(t *testing.T) {
// https://golang.org/issue/12996
orig := New("name")
clone, err := orig.Clone()
if err != nil {
@@ -23,6 +23,8 @@ var transitionFunc = [...]func(context, []byte) (context, int){
stateRCDATA: tSpecialTagEnd,
stateAttr: tAttr,
stateURL: tURL,
stateMetaContent: tMetaContent,
stateMetaContentURL: tMetaContentURL,
stateSrcset: tURL,
stateJS: tJS,
stateJSDqStr: tJSDelimited,
@@ -83,6 +85,7 @@ var elementContentType = [...]state{
elementStyle: stateCSS,
elementTextarea: stateRCDATA,
elementTitle: stateRCDATA,
elementMeta: stateText,
}
// tTag is the context transition function for the tag state.
@@ -93,6 +96,11 @@ func tTag(c context, s []byte) (context, int) {
return c, len(s)
}
if s[i] == '>' {
// Treat <meta> specially, because it doesn't have an end tag, and we
// want to transition into the correct state/element for it.
if c.element == elementMeta {
return context{state: stateText, element: elementNone}, i + 1
}
return context{
state: elementContentType[c.element],
element: c.element,
@@ -113,6 +121,8 @@ func tTag(c context, s []byte) (context, int) {
attrName := strings.ToLower(string(s[i:j]))
if c.element == elementScript && attrName == "type" {
attr = attrScriptType
} else if c.element == elementMeta && attrName == "content" {
attr = attrMetaContent
} else {
switch attrType(attrName) {
case contentTypeURL:
@@ -168,6 +178,7 @@ var attrStartStates = [...]state{
attrStyle: stateCSS,
attrURL: stateURL,
attrSrcset: stateSrcset,
attrMetaContent: stateMetaContent,
}
// tBeforeValue is the context transition function for stateBeforeValue.
@@ -203,6 +214,7 @@ var specialTagEndMarkers = [...][]byte{
elementStyle: []byte("style"),
elementTextarea: []byte("textarea"),
elementTitle: []byte("title"),
elementMeta: []byte(""),
}
var (
@@ -612,6 +624,28 @@ func tError(c context, s []byte) (context, int) {
return c, len(s)
}
// tMetaContent is the context transition function for the meta content attribute state.
func tMetaContent(c context, s []byte) (context, int) {
for i := 0; i < len(s); i++ {
if i+3 <= len(s)-1 && bytes.Equal(bytes.ToLower(s[i:i+4]), []byte("url=")) {
c.state = stateMetaContentURL
return c, i + 4
}
}
return c, len(s)
}
// tMetaContentURL is the context transition function for the "url=" part of a meta content attribute state.
func tMetaContentURL(c context, s []byte) (context, int) {
for i := 0; i < len(s); i++ {
if s[i] == ';' {
c.state = stateMetaContent
return c, i + 1
}
}
return c, len(s)
}
// eatAttrName returns the largest j such that s[i:j] is an attribute name.
// It returns an error if s[i:] does not look like it begins with an
// attribute name, such as encountering a quote mark without a preceding
@@ -638,6 +672,7 @@ var elementNameMap = map[string]element{
"style": elementStyle,
"textarea": elementTextarea,
"title": elementTitle,
"meta": elementMeta,
}
// asciiAlpha reports whether c is an ASCII letter.
+82 -26
View File
@@ -12,6 +12,7 @@ package testenv
import (
"bytes"
"errors"
"flag"
"fmt"
"os"
@@ -22,8 +23,6 @@ import (
"strings"
"sync"
"testing"
"github.com/gohugoio/hugo/tpl/internal/go_templates/cfg"
)
// Save the original environment during init for use in checks. A test
@@ -32,8 +31,13 @@ import (
// environment might cause environment checks to behave erratically.
var origEnv = os.Environ()
// Builder reports the name of the builder running this test
// (for example, "linux-amd64" or "windows-386-gce").
// Builder reports the name of the builder running this test. For example,
// "gotip-linux-amd64_avx512-test_only" or "go1.24-windows-arm64" on LUCI,
// or "linux-amd64" on our old infrastructure. Prefer using runtime.GOOS,
// runtime.GOARCH, race.Enabled, reading the OS version, checking CPU
// feature flags with internal/cpu, etc. over parsing builder names when
// possible. When matching builder names, prefer a fuzzy match instead
// of a strict comparison.
// If the test is not running on the build infrastructure,
// Builder returns the empty string.
func Builder() string {
@@ -55,7 +59,31 @@ func HasGoBuild() bool {
}
var tryGoBuild = sync.OnceValue(func() error {
// Removed by Hugo, not used.
// To run 'go build', we need to be able to exec a 'go' command.
// We somewhat arbitrarily choose to exec 'go tool -n compile' because that
// also confirms that cmd/go can find the compiler. (Before CL 472096,
// we sometimes ended up with cmd/go installed in the test environment
// without a cmd/compile it could use to actually build things.)
goTool, err := goTool()
if err != nil {
return err
}
cmd := exec.Command(goTool, "tool", "-n", "compile")
cmd.Env = origEnv
out, err := cmd.Output()
if err != nil {
return fmt.Errorf("%v: %w", cmd, err)
}
out = bytes.TrimSpace(out)
if len(out) == 0 {
return fmt.Errorf("%v: no tool reported", cmd)
}
if _, err := exec.LookPath(string(out)); err != nil {
return err
}
// Removed by Hugo (not used)
return nil
})
@@ -113,18 +141,9 @@ func MustHaveParallelism(t testing.TB) {
// If the tool is unavailable GoToolPath calls t.Skip.
// If the tool should be available and isn't, GoToolPath calls t.Fatal.
func GoToolPath(t testing.TB) string {
MustHaveGoBuild(t)
path, err := GoTool()
if err != nil {
t.Fatal(err)
}
// Add all environment variables that affect the Go command to test metadata.
// Cached test results will be invalidate when these variables change.
// See golang.org/issue/32285.
for envVar := range strings.FieldsSeq(cfg.KnownEnv) {
os.Getenv(envVar)
}
return path
t.Skip()
// Removed by Hugo (not used)
return ""
}
var findGOROOT = sync.OnceValues(func() (path string, err error) {
@@ -210,8 +229,8 @@ func GOROOT(t testing.TB) string {
// GoTool reports the path to the Go tool.
func GoTool() (string, error) {
// Removed by Hugo, not used.
return "", nil
// Removed by Hugo (not used)
return "", errors.New("GoTool is not implemented")
}
var goTool = sync.OnceValues(func() (string, error) {
@@ -282,17 +301,31 @@ func MustHaveCGO(t testing.TB) {
// CanInternalLink reports whether the current system can link programs with
// internal linking.
func CanInternalLink(withCgo bool) bool {
// Removed by Hugo, not used.
// Removed by Hugo (not used)
return false
}
// SpecialBuildTypes are interesting build types that may affect linking.
type SpecialBuildTypes struct {
Cgo bool
Asan bool
Msan bool
Race bool
}
// NoSpecialBuildTypes indicates a standard, no cgo go build.
var NoSpecialBuildTypes SpecialBuildTypes
// MustInternalLink checks that the current system can link programs with internal
// linking.
// If not, MustInternalLink calls t.Skip with an explanation.
func MustInternalLink(t testing.TB, withCgo bool) {
if !CanInternalLink(withCgo) {
func MustInternalLink(t testing.TB, with SpecialBuildTypes) {
if with.Asan || with.Msan || with.Race {
t.Skipf("skipping test: internal linking with sanitizers is not supported")
}
if !CanInternalLink(with.Cgo) {
t.Helper()
if withCgo && CanInternalLink(false) {
if with.Cgo && CanInternalLink(false) {
t.Skipf("skipping test: internal linking on %s/%s is not supported with cgo", runtime.GOOS, runtime.GOARCH)
}
t.Skipf("skipping test: internal linking on %s/%s is not supported", runtime.GOOS, runtime.GOARCH)
@@ -303,14 +336,14 @@ func MustInternalLink(t testing.TB, withCgo bool) {
// internal linking.
// If not, MustInternalLinkPIE calls t.Skip with an explanation.
func MustInternalLinkPIE(t testing.TB) {
// Removed by Hugo, not used.
// Removed by Hugo (not used)
}
// MustHaveBuildMode reports whether the current system can build programs in
// the given build mode.
// If not, MustHaveBuildMode calls t.Skip with an explanation.
func MustHaveBuildMode(t testing.TB, buildmode string) {
// Removed by Hugo, not used.
// Removed by Hugo (not used)
}
// HasSymlink reports whether the current system can use os.Symlink.
@@ -444,5 +477,28 @@ func SyscallIsNotSupported(err error) bool {
// This function should be used when it is necessary to avoid t.Parallel on
// 32-bit machines, typically because the test uses lots of memory.
func ParallelOn64Bit(t *testing.T) {
// Removed by Hugo, not used.
// Removed by Hugo (not used)
}
// CPUProfilingBroken returns true if CPU profiling has known issues on this
// platform.
func CPUProfilingBroken() bool {
switch runtime.GOOS {
case "plan9":
// Profiling unimplemented.
return true
case "aix":
// See https://golang.org/issue/45170.
return true
case "ios", "dragonfly", "netbsd", "illumos", "solaris":
// See https://golang.org/issue/13841.
return true
case "openbsd":
if runtime.GOARCH == "arm" || runtime.GOARCH == "arm64" {
// See https://golang.org/issue/13841.
return true
}
}
return false
}
@@ -15,44 +15,7 @@ import (
)
func TestGoToolLocation(t *testing.T) {
t.Skip("This test is not relevant for Hugo")
testenv.MustHaveGoBuild(t)
var exeSuffix string
if runtime.GOOS == "windows" {
exeSuffix = ".exe"
}
// Tests are defined to run within their package source directory,
// and this package's source directory is $GOROOT/src/internal/testenv.
// The 'go' command is installed at $GOROOT/bin/go, so if the environment
// is correct then testenv.GoTool() should be identical to ../../../bin/go.
relWant := "../../../bin/go" + exeSuffix
absWant, err := filepath.Abs(relWant)
if err != nil {
t.Fatal(err)
}
wantInfo, err := os.Stat(absWant)
if err != nil {
t.Fatal(err)
}
t.Logf("found go tool at %q (%q)", relWant, absWant)
goTool, err := testenv.GoTool()
if err != nil {
t.Fatalf("testenv.GoTool(): %v", err)
}
t.Logf("testenv.GoTool() = %q", goTool)
gotInfo, err := os.Stat(goTool)
if err != nil {
t.Fatal(err)
}
if !os.SameFile(wantInfo, gotInfo) {
t.Fatalf("%q is not the same file as %q", absWant, goTool)
}
// Removed by Hugo.
}
func TestHasGoBuild(t *testing.T) {
@@ -90,7 +53,12 @@ func TestHasGoBuild(t *testing.T) {
return
}
case "android":
panic("Removed by Hugo, should not be used")
if isEmulatedBuilder(b) {
// As of 2023-05-02, the test environment on the emulated builders is
// missing a C linker.
t.Logf("HasGoBuild is false on %s", b)
return
}
}
if strings.Contains(b, "-noopt") {
@@ -172,8 +140,8 @@ func TestCleanCmdEnvPWD(t *testing.T) {
cmd = testenv.CleanCmdEnv(cmd)
for _, env := range cmd.Env {
if after, ok := strings.CutPrefix(env, "PWD="); ok {
pwd := after
if strings.HasPrefix(env, "PWD=") {
pwd := strings.TrimPrefix(env, "PWD=")
if pwd != dir {
t.Errorf("unexpected PWD: want %s, got %s", dir, pwd)
}
@@ -7,8 +7,6 @@
package testenv
import (
"errors"
"io/fs"
"syscall"
)
@@ -17,27 +15,7 @@ import (
var Sigquit = syscall.SIGQUIT
func syscallIsNotSupported(err error) bool {
if err == nil {
return false
}
var errno syscall.Errno
if errors.As(err, &errno) {
switch errno {
case syscall.EPERM, syscall.EROFS:
// User lacks permission: either the call requires root permission and the
// user is not root, or the call is denied by a container security policy.
return true
case syscall.EINVAL:
// Some containers return EINVAL instead of EPERM if a system call is
// denied by security policy.
return true
}
}
if errors.Is(err, fs.ErrPermission) || errors.Is(err, errors.ErrUnsupported) {
return true
}
// Removed by Hugo.
return false
}
@@ -15,6 +15,11 @@ Execution of the template walks the structure and sets the cursor, represented
by a period '.' and called "dot", to the value at the current location in the
structure as execution proceeds.
The security model used by this package assumes that template authors are
trusted. The package does not auto-escape output, so injecting code into
a template can lead to arbitrary code execution if the template is executed
by an untrusted source.
The input text for a template is UTF-8-encoded text in any format.
"Actions"--data evaluations or control structures--are delimited by
"{{" and "}}"; all text outside actions is copied to the output unchanged.
@@ -35,7 +35,7 @@ Josie
Name, Gift string
Attended bool
}
recipients := []Recipient{
var recipients = []Recipient{
{"Aunt Mildred", "bone china tea set", true},
{"Uncle John", "moleskin pants", false},
{"Cousin Rodney", "", false},
@@ -55,3 +55,72 @@ Output 2: {{printf "%q" . | title}}
// Output 1: "The Go Programming Language"
// Output 2: "The Go Programming Language"
}
// This example demonstrates registering two custom template functions
// and how to overwite one of the functions after the template has been
// parsed. Overwriting can be used, for example, to alter the operation
// of cloned templates.
func ExampleTemplate_funcs() {
// Define a simple template to test the functions.
const tmpl = `{{ . | lower | repeat }}`
// Define the template funcMap with two functions.
var funcMap = template.FuncMap{
"lower": strings.ToLower,
"repeat": func(s string) string { return strings.Repeat(s, 2) },
}
// Define a New template, add the funcMap using Funcs and then Parse
// the template.
parsedTmpl, err := template.New("t").Funcs(funcMap).Parse(tmpl)
if err != nil {
log.Fatal(err)
}
if err := parsedTmpl.Execute(os.Stdout, "ABC\n"); err != nil {
log.Fatal(err)
}
// [Funcs] must be called before a template is parsed to add
// functions to the template. [Funcs] can also be used after a
// template is parsed to overwrite template functions.
//
// Here the function identified by "repeat" is overwritten.
parsedTmpl.Funcs(template.FuncMap{
"repeat": func(s string) string { return strings.Repeat(s, 3) },
})
if err := parsedTmpl.Execute(os.Stdout, "DEF\n"); err != nil {
log.Fatal(err)
}
// Output:
// abc
// abc
// def
// def
// def
}
// This example demonstrates how to use "if".
func ExampleTemplate_if() {
type book struct {
Stars float32
Name string
}
tpl, err := template.New("book").Parse(`{{ if (gt .Stars 4.0) }}"{{.Name }}" is a great book.{{ else }}"{{.Name}}" is not a great book.{{ end }}`)
if err != nil {
log.Fatalf("failed to parse template: %s", err)
}
b := &book{
Stars: 4.9,
Name: "Good Night, Gopher",
}
err = tpl.Execute(os.Stdout, b)
if err != nil {
log.Fatalf("failed to execute template: %s", err)
}
// Output:
// "Good Night, Gopher" is a great book.
}
@@ -7,12 +7,13 @@ package template
import (
"errors"
"fmt"
"github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
"io"
"reflect"
"runtime"
"strings"
"github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
)
// maxExecDepth specifies the maximum stack depth of templates within
@@ -326,6 +327,7 @@ func isTrueOld(val reflect.Value) (truth, ok bool) {
// Something like var x interface{}, never set. It's a form of nil.
return false, true
}
switch val.Kind() {
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
truth = val.Len() > 0
@@ -333,7 +335,7 @@ func isTrueOld(val reflect.Value) (truth, ok bool) {
truth = val.Bool()
case reflect.Complex64, reflect.Complex128:
truth = val.Complex() != 0
case reflect.Chan, reflect.Func, reflect.Pointer, reflect.Interface:
case reflect.Chan, reflect.Func, reflect.Pointer, reflect.UnsafePointer, reflect.Interface:
truth = !val.IsNil()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
truth = val.Int() != 0
@@ -18,6 +18,7 @@ import (
"strings"
"sync"
"testing"
"unsafe"
)
var debug = flag.Bool("debug", false, "show the errors produced by the tests")
@@ -46,6 +47,7 @@ type T struct {
SB []bool
// Arrays
AI [3]int
PAI *[3]int // pointer to array
// Maps
MSI map[string]int
MSIone map[string]int // one element, for deterministic output
@@ -78,6 +80,8 @@ type T struct {
PS *string
PSI *[]int
NIL *int
UPI unsafe.Pointer
EmptyUPI unsafe.Pointer
// Function (not method)
BinaryFunc func(string, string) string
VariadicFunc func(...string) string
@@ -142,6 +146,7 @@ var tVal = &T{
SI: []int{3, 4, 5},
SICap: make([]int, 5, 10),
AI: [3]int{3, 4, 5},
PAI: &[3]int{3, 4, 5},
SB: []bool{true, false},
MSI: map[string]int{"one": 1, "two": 2, "three": 3},
MSIone: map[string]int{"one": 1},
@@ -169,6 +174,7 @@ var tVal = &T{
PI: newInt(23),
PS: newString("a string"),
PSI: newIntSlice(21, 22, 23),
UPI: newUnsafePointer(23),
BinaryFunc: func(a, b string) string { return fmt.Sprintf("[%s=%s]", a, b) },
VariadicFunc: func(s ...string) string { return fmt.Sprint("<", strings.Join(s, "+"), ">") },
VariadicFuncInt: func(a int, s ...string) string { return fmt.Sprint(a, "=<", strings.Join(s, "+"), ">") },
@@ -195,6 +201,10 @@ func newInt(n int) *int {
return &n
}
func newUnsafePointer(n int) unsafe.Pointer {
return unsafe.Pointer(&n)
}
func newString(s string) *string {
return &s
}
@@ -456,6 +466,10 @@ var execTests = []execTest{
{"if 0.0", "{{if .FloatZero}}NON-ZERO{{else}}ZERO{{end}}", "ZERO", tVal, true},
{"if 1.5i", "{{if 1.5i}}NON-ZERO{{else}}ZERO{{end}}", "NON-ZERO", tVal, true},
{"if 0.0i", "{{if .ComplexZero}}NON-ZERO{{else}}ZERO{{end}}", "ZERO", tVal, true},
{"if nonNilPointer", "{{if .PI}}NON-ZERO{{else}}ZERO{{end}}", "NON-ZERO", tVal, true},
{"if nilPointer", "{{if .NIL}}NON-ZERO{{else}}ZERO{{end}}", "ZERO", tVal, true},
{"if UPI", "{{if .UPI}}NON-ZERO{{else}}ZERO{{end}}", "NON-ZERO", tVal, true},
{"if EmptyUPI", "{{if .EmptyUPI}}NON-ZERO{{else}}ZERO{{end}}", "ZERO", tVal, true},
{"if emptystring", "{{if ``}}NON-EMPTY{{else}}EMPTY{{end}}", "EMPTY", tVal, true},
{"if string", "{{if `notempty`}}NON-EMPTY{{else}}EMPTY{{end}}", "NON-EMPTY", tVal, true},
{"if emptyslice", "{{if .SIEmpty}}NON-EMPTY{{else}}EMPTY{{end}}", "EMPTY", tVal, true},
@@ -561,6 +575,9 @@ var execTests = []execTest{
{"array[:]", "{{slice .AI}}", "[3 4 5]", tVal, true},
{"array[1:]", "{{slice .AI 1}}", "[4 5]", tVal, true},
{"array[1:2]", "{{slice .AI 1 2}}", "[4]", tVal, true},
{"pointer to array[:]", "{{slice .PAI}}", "[3 4 5]", tVal, true},
{"pointer to array[1:]", "{{slice .PAI 1}}", "[4 5]", tVal, true},
{"pointer to array[1:2]", "{{slice .PAI 1 2}}", "[4]", tVal, true},
{"string[:]", "{{slice .S}}", "xyz", tVal, true},
{"string[0:1]", "{{slice .S 0 1}}", "x", tVal, true},
{"string[1:]", "{{slice .S 1}}", "yz", tVal, true},
@@ -804,7 +821,7 @@ func count(n int) chan string {
}
c := make(chan string)
go func() {
for i := range n {
for i := 0; i < n; i++ {
c <- "abcdefghijklmnop"[i : i+1]
}
close(c)
@@ -1008,24 +1025,6 @@ type CustomError struct{}
func (*CustomError) Error() string { return "heyo !" }
// Check that a custom error can be returned.
func TestExecError_CustomError(t *testing.T) {
failingFunc := func() (string, error) {
return "", &CustomError{}
}
tmpl := Must(New("top").Funcs(FuncMap{
"err": failingFunc,
}).Parse("{{ err }}"))
var b bytes.Buffer
err := tmpl.Execute(&b, nil)
var e *CustomError
if !errors.As(err, &e) {
t.Fatalf("expected custom error; got %s", err)
}
}
func TestJSEscaping(t *testing.T) {
testCases := []struct {
in, exp string
@@ -1510,6 +1509,45 @@ func TestBadFuncNames(t *testing.T) {
}
}
func TestIsTrue(t *testing.T) {
var nil_ptr *int
var nil_chan chan int
tests := []struct {
v any
want bool
}{
{1, true},
{0, false},
{uint8(1), true},
{uint8(0), false},
{float64(1.0), true},
{float64(0.0), false},
{complex64(1.0), true},
{complex64(0.0), false},
{true, true},
{false, false},
{[2]int{1, 2}, true},
{[0]int{}, false},
{[]byte("abc"), true},
{[]byte(""), false},
{map[string]int{"a": 1, "b": 2}, true},
{map[string]int{}, false},
{make(chan int), true},
{nil_chan, false},
//{new(int), true}, // Commented out for Hugo. We have a slightly different view on ... the truth.
{nil_ptr, false},
{unsafe.Pointer(new(int)), true},
{unsafe.Pointer(nil_ptr), false},
}
for i, test_case := range tests {
got, _ := IsTrue(test_case.v)
if got != test_case.want {
t.Fatalf("[%d] expect result %v, got %v", i, test_case.want, got)
}
}
}
func testBadFuncName(name string, t *testing.T) {
t.Helper()
defer func() {
@@ -1714,8 +1752,8 @@ func TestInterfaceValues(t *testing.T) {
"Nil": nil,
"Zero": 0,
})
if after, ok := strings.CutPrefix(tt.out, "ERROR:"); ok {
e := strings.TrimSpace(after)
if strings.HasPrefix(tt.out, "ERROR:") {
e := strings.TrimSpace(strings.TrimPrefix(tt.out, "ERROR:"))
if err == nil || !strings.Contains(err.Error(), e) {
t.Errorf("%s: Execute: %v, want error %q", tt.text, err, e)
}
@@ -1940,7 +1978,7 @@ func TestIssue39807(t *testing.T) {
wg.Add(1)
go func() {
defer wg.Done()
for range numTemplates {
for j := 0; j < numTemplates; j++ {
_, err := tplFoo.AddParseTree(tplBar.Name(), tplBar.Tree)
if err != nil {
t.Error(err)
+15 -24
View File
@@ -8,7 +8,6 @@ import (
"errors"
"fmt"
"io"
"maps"
"net/url"
"reflect"
"strings"
@@ -23,7 +22,7 @@ import (
// return value evaluates to non-nil during execution, execution terminates and
// Execute returns that error.
//
// Errors returned by Execute wrap the underlying error; call [errors.As] to
// Errors returned by Execute wrap the underlying error; call [errors.AsType] to
// unwrap them.
//
// When template execution invokes a function with an argument list, that list
@@ -63,26 +62,13 @@ func builtins() FuncMap {
}
}
var builtinFuncsOnce struct {
sync.Once
v map[string]reflect.Value
}
// builtinFuncsOnce lazily computes & caches the builtinFuncs map.
// TODO: revert this back to a global map once golang.org/issue/2559 is fixed.
func builtinFuncs() map[string]reflect.Value {
builtinFuncsOnce.Do(func() {
builtinFuncsOnce.v = createValueFuncs(builtins())
})
return builtinFuncsOnce.v
}
// createValueFuncs turns a FuncMap into a map[string]reflect.Value
func createValueFuncs(funcMap FuncMap) map[string]reflect.Value {
m := make(map[string]reflect.Value)
// builtinFuncs lazily computes & caches the builtinFuncs map.
var builtinFuncs = sync.OnceValue(func() map[string]reflect.Value {
funcMap := builtins()
m := make(map[string]reflect.Value, len(funcMap))
addValueFuncs(m, funcMap)
return m
}
})
// addValueFuncs adds to values the functions in funcs, converting them to reflect.Values.
func addValueFuncs(out map[string]reflect.Value, in FuncMap) {
@@ -104,7 +90,9 @@ func addValueFuncs(out map[string]reflect.Value, in FuncMap) {
// addFuncs adds to values the functions in funcs. It does no checking of the input -
// call addValueFuncs first.
func addFuncs(out, in FuncMap) {
maps.Copy(out, in)
for name, fn := range in {
out[name] = fn
}
}
// goodFunc reports whether the function or method has the right result signature.
@@ -256,6 +244,10 @@ func slice(item reflect.Value, indexes ...reflect.Value) (reflect.Value, error)
if !item.IsValid() {
return reflect.Value{}, fmt.Errorf("slice of untyped nil")
}
var isNil bool
if item, isNil = indirect(item); isNil {
return reflect.Value{}, fmt.Errorf("slice of nil pointer")
}
if len(indexes) > 3 {
return reflect.Value{}, fmt.Errorf("too many slice indexes: %d", len(indexes))
}
@@ -408,7 +400,6 @@ func not(arg reflect.Value) bool {
var (
errBadComparisonType = errors.New("invalid type for comparison")
errBadComparison = errors.New("incompatible types for comparison")
errNoComparison = errors.New("missing argument for comparison")
)
@@ -486,7 +477,7 @@ func eq(arg1 reflect.Value, arg2 ...reflect.Value) (bool, error) {
truth = arg.Int() >= 0 && arg1.Uint() == uint64(arg.Int())
default:
if arg1.IsValid() && arg.IsValid() {
return false, errBadComparison
return false, fmt.Errorf("incompatible types for comparison: %v and %v", arg1.Type(), arg.Type())
}
}
} else {
@@ -552,7 +543,7 @@ func lt(arg1, arg2 reflect.Value) (bool, error) {
case k1 == uintKind && k2 == intKind:
truth = arg2.Int() >= 0 && arg1.Uint() < uint64(arg2.Int())
default:
return false, errBadComparison
return false, fmt.Errorf("incompatible types for comparison: %v and %v", arg1.Type(), arg2.Type())
}
} else {
switch k1 {
@@ -213,6 +213,7 @@ const (
cloneText2 = `{{define "b"}}b{{end}}`
cloneText3 = `{{define "c"}}root{{end}}`
cloneText4 = `{{define "c"}}clone{{end}}`
cloneText5 = `{{define "e"}}{{.Foo}}{{end}}`
)
func TestClone(t *testing.T) {
@@ -225,6 +226,8 @@ func TestClone(t *testing.T) {
if err != nil {
t.Fatal(err)
}
root.Parse(cloneText5)
root.Option("missingkey=error")
clone := Must(root.Clone())
// Add variants to both.
_, err = root.Parse(cloneText3)
@@ -262,6 +265,14 @@ func TestClone(t *testing.T) {
if b.String() != "bclone" {
t.Errorf("expected %q got %q", "bclone", b.String())
}
b.Reset()
rootErr := root.ExecuteTemplate(&b, "e", map[string]any{})
cloneErr := clone.ExecuteTemplate(&b, "e", map[string]any{})
if cloneErr == nil {
t.Errorf("expected error from missing key in cloned template")
} else if got, want := cloneErr.Error(), rootErr.Error(); got != want {
t.Errorf("got %q, wan t %q", got, want)
}
}
func TestAddParseTree(t *testing.T) {
@@ -581,15 +581,3 @@ func TestPos(t *testing.T) {
}
}
}
// parseLexer is a local version of parse that lets us pass in the lexer instead of building it.
// We expect an error, so the tree set and funcs list are explicitly nil.
func (t *Tree) parseLexer(lex *lexer) (tree *Tree, err error) {
defer t.recover(&err)
t.ParseName = t.Name
t.startParse(nil, lex, map[string]*Tree{})
t.parse()
t.add()
t.stopParse()
return t, nil
}
@@ -12,7 +12,6 @@ import (
"bytes"
"fmt"
"runtime"
"slices"
"strconv"
"strings"
)
@@ -33,9 +32,10 @@ type Tree struct {
treeSet map[string]*Tree
actionLine int // line of left delim starting action
rangeDepth int
stackDepth int // depth of nested parenthesized expressions
}
// A mode value is a set of flags (or 0). Modes control parser behavior.
// A Mode value is a set of flags (or 0). Modes control parser behavior.
type Mode uint
const (
@@ -43,6 +43,17 @@ const (
SkipFuncCheck // do not check that functions are defined
)
// maxStackDepth is the maximum depth permitted for nested
// parenthesized expressions.
var maxStackDepth = 10000
// init reduces maxStackDepth for WebAssembly due to its smaller stack size.
func init() {
if runtime.GOARCH == "wasm" {
maxStackDepth = 1000
}
}
// Copy returns a copy of the [Tree]. Any parsing state is discarded.
func (t *Tree) Copy() *Tree {
if t == nil {
@@ -224,6 +235,7 @@ func (t *Tree) startParse(funcs []map[string]any, lex *lexer, treeSet map[string
t.vars = []string{"$"}
t.funcs = funcs
t.treeSet = treeSet
t.stackDepth = 0
lex.options = lexOptions{
emitComment: t.Mode&ParseComments != 0,
breakOK: !t.hasFunction("break"),
@@ -788,6 +800,11 @@ func (t *Tree) term() Node {
}
return number
case itemLeftParen:
if t.stackDepth >= maxStackDepth {
t.errorf("max expression depth exceeded")
}
t.stackDepth++
defer func() { t.stackDepth-- }()
return t.pipeline("parenthesized pipeline", itemRightParen)
case itemString, itemRawString:
s, err := strconv.Unquote(token.val)
@@ -822,9 +839,11 @@ func (t *Tree) popVars(n int) {
// variable is not defined.
func (t *Tree) useVar(pos Pos, name string) Node {
v := t.newVariable(pos, name)
if slices.Contains(t.vars, v.Ident[0]) {
for _, varName := range t.vars {
if varName == v.Ident[0] {
return v
}
}
t.errorf("undefined variable %q", v.Ident[0])
return nil
}
@@ -89,6 +89,11 @@ var numberTests = []numberTest{
{"0xef", true, true, true, false, 0xef, 0xef, 0xef, 0},
}
func init() {
// Use a small stack limit for testing to avoid creating huge expressions.
maxStackDepth = 3
}
func TestNumberParse(t *testing.T) {
for _, test := range numberTests {
// If fmt.Sscan thinks it's complex, it's complex. We can't trust the output
@@ -330,6 +335,15 @@ var parseTests = []parseTest{
{"empty pipeline", `{{printf "%d" ( ) }}`, hasError, ""},
// Missing pipeline in block
{"block definition", `{{block "foo"}}hello{{end}}`, hasError, ""},
// Expression nested depth tests.
{"paren nesting normal", "{{ (( 1 )) }}", noError, "{{((1))}}"},
{"paren nesting at limit", "{{ ((( 1 ))) }}", noError, "{{(((1)))}}"},
{"paren nesting exceeds limit", "{{ (((( 1 )))) }}", hasError, "template: test:1: max expression depth exceeded"},
{"paren nesting in pipeline", "{{ ((( 1 ))) | printf }}", noError, "{{(((1))) | printf}}"},
{"paren nesting in pipeline exceeds limit", "{{ (((( 1 )))) | printf }}", hasError, "template: test:1: max expression depth exceeded"},
{"paren nesting with other constructs", "{{ if ((( true ))) }}YES{{ end }}", noError, "{{if (((true)))}}\"YES\"{{end}}"},
{"paren nesting with other constructs exceeds limit", "{{ if (((( true )))) }}YES{{ end }}", hasError, "template: test:1: max expression depth exceeded"},
}
var builtins = map[string]any{
@@ -90,6 +90,7 @@ func (t *Template) Clone() (*Template, error) {
if t.common == nil {
return nt, nil
}
nt.option = t.option
t.muTmpl.RLock()
defer t.muTmpl.RUnlock()
for k, v := range t.tmpl {