]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
Upgrade to to Go 1.26.1 (#14597) (note)
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Wed, 11 Mar 2026 11:36:13 +0000 (12:36 +0100)
committerGitHub <noreply@github.com>
Wed, 11 Mar 2026 11:36:13 +0000 (12:36 +0100)
This fixes a security issue: https://www.cve.org/CVERecord?id=CVE-2026-27142

Closes #14595

29 files changed:
.circleci/config.yml
common/hreflect/helpers.go
scripts/fork_go_templates/main.go
tpl/internal/go_templates/htmltemplate/attr_string.go
tpl/internal/go_templates/htmltemplate/context.go
tpl/internal/go_templates/htmltemplate/doc.go
tpl/internal/go_templates/htmltemplate/element_string.go
tpl/internal/go_templates/htmltemplate/error.go
tpl/internal/go_templates/htmltemplate/escape.go
tpl/internal/go_templates/htmltemplate/escape_test.go
tpl/internal/go_templates/htmltemplate/exec_test.go
tpl/internal/go_templates/htmltemplate/state_string.go
tpl/internal/go_templates/htmltemplate/template.go
tpl/internal/go_templates/htmltemplate/template_test.go
tpl/internal/go_templates/htmltemplate/transition.go
tpl/internal/go_templates/testenv/testenv.go
tpl/internal/go_templates/testenv/testenv_test.go
tpl/internal/go_templates/testenv/testenv_unix.go
tpl/internal/go_templates/texttemplate/doc.go
tpl/internal/go_templates/texttemplate/example_test.go
tpl/internal/go_templates/texttemplate/examplefunc_test.go
tpl/internal/go_templates/texttemplate/exec.go
tpl/internal/go_templates/texttemplate/exec_test.go
tpl/internal/go_templates/texttemplate/funcs.go
tpl/internal/go_templates/texttemplate/multi_test.go
tpl/internal/go_templates/texttemplate/parse/lex_test.go
tpl/internal/go_templates/texttemplate/parse/parse.go
tpl/internal/go_templates/texttemplate/parse/parse_test.go
tpl/internal/go_templates/texttemplate/template.go

index 18047d6e8755b6d27b01ec54fbcd5f3a5e56e299..cc15e71e0a8101648417c21e726d85660c6c3acb 100644 (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
index fe11d2b9fbfdb5bef90cbedbc002941e36208969..802f0e59ca95ab9709a8c071e4397d621c314619 100644 (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
index e4895c87ac54115fc94309cdeff7f9d7adf00852..3ea3d6c329070b591053ff65940f36df167bb560 100644 (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 ...")
index 51c3f262084c04592e18c6c563070c706eac9ba2..7159fa9cbaa15a0b78c6c98324ad0133d180b05b 100644 (file)
@@ -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) {
index 3acb9bdf0e23180e6b0d64075b238f3f27dfc4ea..5bb2253ebad19f93bc27184cc88b56e366143b0a 100644 (file)
@@ -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
 )
index 7442194f79ced058d57be0d9e6ae9f100c58229a..f2d896b294568ef8cce63e6e9c96a49ae6d84424 100644 (file)
@@ -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
index db286655aa30aaa908519eee37c82f04b484ef31..bdf9da7b9d53e665e4f4fc684db8117e59ef57dd 100644 (file)
@@ -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) {
index de4b4de1e5306c684fb4bc1df41d8a42db555988..f8ad0081f549ce78f2af164e843fe493b66dc279 100644 (file)
@@ -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
index f5d5674fae88fd02a0e97a47f41c49726b52137f..67c515273e687c9195091c855bcd3ca2af8abdbf 100644 (file)
@@ -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.
index a5a810ffccf807d8d281a01fde326c1480d3eed8..7ea1c72248df2c15a11ea6e9ff144eebd92053f6 100644 (file)
@@ -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)
-
                }
        }
 }
index 2790470fda00cf97b74cc62cc6dc6872a5e8203d..e01813e68523a44e7071ee593735bf22796155d0 100644 (file)
@@ -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)
index eed1e8bcc018c22bb99abb70f2265d956e9fdcdf..f5a70b2231c2da6065827496114523eb97c7612d 100644 (file)
@@ -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) {
index cd80e0183dde046c00dc66e75fb5e1b2cb451bff..4582ddd5fd16943d25880030a07fb6bc0771272a 100644 (file)
@@ -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{
index 0838308a146e8559aa023a580d96279899250f38..c5a5df4375a661d3ccfa593762527d49615cd6a5 100644 (file)
@@ -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 {
index c430389a345f69b256ede5488a4e2b2940ffe9a2..7fbab1df7b06ee02a40cc6b7c126864642830c71 100644 (file)
@@ -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:
@@ -162,12 +172,13 @@ func tAfterName(c context, s []byte) (context, int) {
 }
 
 var attrStartStates = [...]state{
-       attrNone:       stateAttr,
-       attrScript:     stateJS,
-       attrScriptType: stateAttr,
-       attrStyle:      stateCSS,
-       attrURL:        stateURL,
-       attrSrcset:     stateSrcset,
+       attrNone:        stateAttr,
+       attrScript:      stateJS,
+       attrScriptType:  stateAttr,
+       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.
index e87ed1c9b226e0877abe563113ce4182cbd6d209..d19d09cd47f697040ac1563c3050689e54a9260f 100644 (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
 }
index 87bcb1c7197d2b55d2e9395f3959f65dd37fe5ec..d1f0970d6224b60bfc344279e63ddf546a7690c3 100644 (file)
@@ -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)
                        }
index a629078842eadcbb904dd354fe5567ea8a28300f..af806fb1363707a3aa3a6fc45bc6b58f6b598925 100644 (file)
@@ -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
 }
index 7b63bb76ae4c5d0fa99a72490416d38fbdf730cd..01b99f0e2123a19b6b677fab36f863b5a8bdf4ee 100644 (file)
@@ -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.
index 975ceea930629202cb028e5c3d97ea5d996de7bb..295a810b853e5f12071a2f5567a230199e0708f4 100644 (file)
@@ -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},
index 4a13b1f9a73afa97f301c7ece6dfa8f3476aa693..0c745bdd4a492ebb8509814d89675b9d915c7959 100644 (file)
@@ -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.
+}
index a61fcde191b53e27671b812d36cbd7908b92ba81..d20c7bba779e883d46971a8908d368c06c99e739 100644 (file)
@@ -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
index b2cf3e7c6dc85680bb46bcea5042ed7b1813954f..5f3e0e90247ae16aa01edecb604d6a6bfa4abf4a 100644 (file)
@@ -18,6 +18,7 @@ import (
        "strings"
        "sync"
        "testing"
+       "unsafe"
 )
 
 var debug = flag.Bool("debug", false, "show the errors produced by the tests")
@@ -45,7 +46,8 @@ type T struct {
        SIEmpty []int
        SB      []bool
        // Arrays
-       AI [3]int
+       AI  [3]int
+       PAI *[3]int // pointer to array
        // Maps
        MSI      map[string]int
        MSIone   map[string]int // one element, for deterministic output
@@ -74,10 +76,12 @@ type T struct {
        Str fmt.Stringer
        Err error
        // Pointers
-       PI  *int
-       PS  *string
-       PSI *[]int
-       NIL *int
+       PI       *int
+       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)
index 5318798ad375f1a40995b134711a64a399fdbba8..ed9c7ea5d550158c8f6deb4aa277bb59bb49cf58 100644 (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 {
index 6e383c131f8367abb691eb202864615fbff4f92e..869a443fa224f77c4913c7e870b1ba4c12c6f33a 100644 (file)
@@ -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) {
index b1ea699673be33c405a851e00c3c2eea8b828a53..633ffb055951a83e95b2c920b8db41e290f84454 100644 (file)
@@ -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
-}
index cc8a9336042dc1f6707fd7954045d16bf45fcb5c..b74dfb7f4eaa5c6a7b5d4dc6164643121c1cf9aa 100644 (file)
@@ -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"),
@@ -534,7 +546,7 @@ func (t *Tree) parseControl(context string) (pos Pos, line int, pipe *PipeNode,
                t.rangeDepth--
        }
        switch next.Type() {
-       case nodeEnd: // done
+       case nodeEnd: //done
        case nodeElse:
                // Special case for "else if" and "else with".
                // If the "else" is followed immediately by an "if" or "with",
@@ -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,8 +839,10 @@ 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]) {
-               return v
+       for _, varName := range t.vars {
+               if varName == v.Ident[0] {
+                       return v
+               }
        }
        t.errorf("undefined variable %q", v.Ident[0])
        return nil
index 47951a9c9d788031763db642195533f53d1a7dce..90ec9852b325473332a924972a911946e60d5ba7 100644 (file)
@@ -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{
index 0be64eee2d902a0a7081e55ccc125307cf2621d0..e66c01fd444ff5f6dc73b8573d916c0e00546935 100644 (file)
@@ -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 {