mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-24 15:28:54 +00:00
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:
committed by
GitHub
parent
842d8f1052
commit
1f578f160c
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: <nil>-", tVal, true},
|
||||
{".Method3(nil value)", "-{{.Method3 .MXI.unset}}-", "-Method3: <nil>-", 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>"}}`,
|
||||
"<script>alert("XSS");</script>", nil, true},
|
||||
{"html pipeline", `{{printf "<script>alert(\"XSS\");</script>" | html}}`,
|
||||
"<script>alert("XSS");</script>", nil, true},
|
||||
{
|
||||
"html", `{{html "<script>alert(\"XSS\");</script>"}}`,
|
||||
"<script>alert("XSS");</script>", nil, true,
|
||||
},
|
||||
{
|
||||
"html pipeline", `{{printf "<script>alert(\"XSS\");</script>" | html}}`,
|
||||
"<script>alert("XSS");</script>", nil, true,
|
||||
},
|
||||
{"html", `{{html .PS}}`, "a string", tVal, true},
|
||||
{"html typed nil", `{{html .NIL}}`, "<nil>", tVal, true},
|
||||
{"html untyped nil", `{{html .Empty0}}`, "<nil>", tVal, true}, // NOTE: "<no value>" 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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"),
|
||||
@@ -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,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 {
|
||||
|
||||
Reference in New Issue
Block a user