Pull in the latest code from Go's template packages (#11771)

Fixes #10707
Fixes #11507
This commit is contained in:
Bjørn Erik Pedersen
2023-12-04 12:07:54 +01:00
committed by GitHub
parent 14d85ec136
commit 9f978d387f
25 changed files with 417 additions and 190 deletions

View File

@@ -6,13 +6,14 @@ package fmtsort_test
import (
"fmt"
"github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"
"math"
"reflect"
"sort"
"strings"
"testing"
"unsafe"
"github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"
)
var compareTests = [][]reflect.Value{
@@ -38,7 +39,7 @@ var compareTests = [][]reflect.Value{
ct(reflect.TypeOf(chans[0]), chans[0], chans[1], chans[2]),
ct(reflect.TypeOf(toy{}), toy{0, 1}, toy{0, 2}, toy{1, -1}, toy{1, 1}),
ct(reflect.TypeOf([2]int{}), [2]int{1, 1}, [2]int{1, 2}, [2]int{2, 0}),
ct(reflect.TypeOf(any(any(0))), iFace, 1, 2, 3),
ct(reflect.TypeOf(any(0)), iFace, 1, 2, 3),
}
var iFace any
@@ -190,12 +191,15 @@ func sprintKey(key reflect.Value) string {
var (
ints [3]int
chans = makeChans()
// pin runtime.Pinner
)
func makeChans() []chan int {
cs := []chan int{make(chan int), make(chan int), make(chan int)}
// Order channels by address. See issue #49431.
// TODO: pin these pointers once pinning is available (#46787).
for i := range cs {
reflect.ValueOf(cs[i]).UnsafePointer()
}
sort.Slice(cs, func(i, j int) bool {
return uintptr(reflect.ValueOf(cs[i]).UnsafePointer()) < uintptr(reflect.ValueOf(cs[j]).UnsafePointer())
})

View File

@@ -22,10 +22,15 @@ type context struct {
delim delim
urlPart urlPart
jsCtx jsCtx
attr attr
element element
n parse.Node // for range break/continue
err *Error
// jsBraceDepth contains the current depth, for each JS template literal
// string interpolation expression, of braces we've seen. This is used to
// determine if the next } will close a JS template literal string
// interpolation expression or not.
jsBraceDepth []int
attr attr
element element
n parse.Node // for range break/continue
err *Error
}
func (c context) String() string {
@@ -121,8 +126,8 @@ const (
stateJSDqStr
// stateJSSqStr occurs inside a JavaScript single quoted string.
stateJSSqStr
// stateJSBqStr occurs inside a JavaScript back quoted string.
stateJSBqStr
// stateJSTmplLit occurs inside a JavaScript back quoted string.
stateJSTmplLit
// stateJSRegexp occurs inside a JavaScript regexp literal.
stateJSRegexp
// stateJSBlockCmt occurs inside a JavaScript /* block comment */.
@@ -176,14 +181,14 @@ func isInTag(s state) bool {
}
// isInScriptLiteral returns true if s is one of the literal states within a
// <script> tag, and as such occurances of "<!--", "<script", and "</script"
// <script> tag, and as such occurrences of "<!--", "<script", and "</script"
// need to be treated specially.
func isInScriptLiteral(s state) bool {
// Ignore the comment states (stateJSBlockCmt, stateJSLineCmt,
// stateJSHTMLOpenCmt, stateJSHTMLCloseCmt) because their content is already
// omitted from the output.
switch s {
case stateJSDqStr, stateJSSqStr, stateJSBqStr, stateJSRegexp:
case stateJSDqStr, stateJSSqStr, stateJSTmplLit, stateJSRegexp:
return true
}
return false

View File

@@ -222,6 +222,10 @@ const (
// Discussion:
// Package html/template does not support actions inside of JS template
// literals.
//
// Deprecated: ErrJSTemplate is no longer returned when an action is present
// in a JS template literal. Actions inside of JS template literals are now
// escaped as expected.
ErrJSTemplate
)

View File

@@ -8,8 +8,6 @@ import (
"bytes"
"fmt"
"html"
//"internal/godebug"
"io"
"regexp"
@@ -64,22 +62,23 @@ func evalArgs(args ...any) string {
// funcMap maps command names to functions that render their inputs safe.
var funcMap = template.FuncMap{
"_html_template_attrescaper": attrEscaper,
"_html_template_commentescaper": commentEscaper,
"_html_template_cssescaper": cssEscaper,
"_html_template_cssvaluefilter": cssValueFilter,
"_html_template_htmlnamefilter": htmlNameFilter,
"_html_template_htmlescaper": htmlEscaper,
"_html_template_jsregexpescaper": jsRegexpEscaper,
"_html_template_jsstrescaper": jsStrEscaper,
"_html_template_jsvalescaper": jsValEscaper,
"_html_template_nospaceescaper": htmlNospaceEscaper,
"_html_template_rcdataescaper": rcdataEscaper,
"_html_template_srcsetescaper": srcsetFilterAndEscaper,
"_html_template_urlescaper": urlEscaper,
"_html_template_urlfilter": urlFilter,
"_html_template_urlnormalizer": urlNormalizer,
"_eval_args_": evalArgs,
"_html_template_attrescaper": attrEscaper,
"_html_template_commentescaper": commentEscaper,
"_html_template_cssescaper": cssEscaper,
"_html_template_cssvaluefilter": cssValueFilter,
"_html_template_htmlnamefilter": htmlNameFilter,
"_html_template_htmlescaper": htmlEscaper,
"_html_template_jsregexpescaper": jsRegexpEscaper,
"_html_template_jsstrescaper": jsStrEscaper,
"_html_template_jstmpllitescaper": jsTmplLitEscaper,
"_html_template_jsvalescaper": jsValEscaper,
"_html_template_nospaceescaper": htmlNospaceEscaper,
"_html_template_rcdataescaper": rcdataEscaper,
"_html_template_srcsetescaper": srcsetFilterAndEscaper,
"_html_template_urlescaper": urlEscaper,
"_html_template_urlfilter": urlFilter,
"_html_template_urlnormalizer": urlNormalizer,
"_eval_args_": evalArgs,
}
// escaper collects type inferences about templates and changes needed to make
@@ -164,7 +163,6 @@ func (e *escaper) escape(c context, n parse.Node) context {
panic("escaping " + n.String() + " is unimplemented")
}
// Modified by Hugo.
// var debugAllowActionJSTmpl = godebug.New("jstmpllitinterp")
// escapeAction escapes an action template node.
@@ -230,16 +228,8 @@ func (e *escaper) escapeAction(c context, n *parse.ActionNode) context {
c.jsCtx = jsCtxDivOp
case stateJSDqStr, stateJSSqStr:
s = append(s, "_html_template_jsstrescaper")
case stateJSBqStr:
if SecurityAllowActionJSTmpl.Load() {
// debugAllowActionJSTmpl.IncNonDefault()
s = append(s, "_html_template_jsstrescaper")
} else {
return context{
state: stateError,
err: errorf(ErrJSTemplate, n, n.Line, "%s appears in a JS template literal", n),
}
}
case stateJSTmplLit:
s = append(s, "_html_template_jstmpllitescaper")
case stateJSRegexp:
s = append(s, "_html_template_jsregexpescaper")
case stateCSS:
@@ -398,6 +388,9 @@ var redundantFuncs = map[string]map[string]bool{
"_html_template_jsstrescaper": {
"_html_template_attrescaper": true,
},
"_html_template_jstmpllitescaper": {
"_html_template_attrescaper": true,
},
"_html_template_urlescaper": {
"_html_template_urlnormalizer": true,
},

View File

@@ -35,14 +35,14 @@ func (x *goodMarshaler) MarshalJSON() ([]byte, error) {
func TestEscape(t *testing.T) {
data := struct {
F, T bool
C, G, H string
A, E []string
B, M json.Marshaler
N int
U any // untyped nil
Z *int // typed nil
W htmltemplate.HTML
F, T bool
C, G, H, I string
A, E []string
B, M json.Marshaler
N int
U any // untyped nil
Z *int // typed nil
W htmltemplate.HTML
}{
F: false,
T: true,
@@ -57,6 +57,7 @@ func TestEscape(t *testing.T) {
U: nil,
Z: nil,
W: htmltemplate.HTML(`&iexcl;<b class="foo">Hello</b>, <textarea>O'World</textarea>!`),
I: "${ asd `` }",
}
pdata := &data
@@ -723,6 +724,21 @@ func TestEscape(t *testing.T) {
"<p name=\"{{.U}}\">",
"<p name=\"\">",
},
{
"JS template lit special characters",
"<script>var a = `{{.I}}`</script>",
"<script>var a = `\\u0024\\u007b asd \\u0060\\u0060 \\u007d`</script>",
},
{
"JS template lit special characters, nested lit",
"<script>var a = `${ `{{.I}}` }`</script>",
"<script>var a = `${ `\\u0024\\u007b asd \\u0060\\u0060 \\u007d` }`</script>",
},
{
"JS template lit, nested JS",
"<script>var a = `${ var a = \"{{\"a \\\" d\"}}\" }`</script>",
"<script>var a = `${ var a = \"a \\u0022 d\" }`</script>",
},
}
for _, test := range tests {
@@ -981,6 +997,31 @@ func TestErrors(t *testing.T) {
"<script>var a = `${a+b}`</script>`",
"",
},
{
"<script>var tmpl = `asd`;</script>",
``,
},
{
"<script>var tmpl = `${1}`;</script>",
``,
},
{
"<script>var tmpl = `${return ``}`;</script>",
``,
},
{
"<script>var tmpl = `${return {{.}} }`;</script>",
``,
},
{
"<script>var tmpl = `${ let a = {1:1} {{.}} }`;</script>",
``,
},
{
"<script>var tmpl = `asd ${return \"{\"}`;</script>",
``,
},
// Error cases.
{
"{{if .Cond}}<a{{end}}",
@@ -1127,10 +1168,6 @@ func TestErrors(t *testing.T) {
// html is allowed since it is the last command in the pipeline, but urlquery is not.
`predefined escaper "urlquery" disallowed in template`,
},
{
"<script>var tmpl = `asd {{.}}`;</script>",
`{{.}} appears in a JS template literal`,
},
}
for _, test := range tests {
buf := new(bytes.Buffer)
@@ -1354,7 +1391,7 @@ func TestEscapeText(t *testing.T) {
},
{
"<a onclick=\"`foo",
context{state: stateJSBqStr, delim: delimDoubleQuote, attr: attrScript},
context{state: stateJSTmplLit, delim: delimDoubleQuote, attr: attrScript},
},
{
`<A ONCLICK="'`,
@@ -1696,6 +1733,94 @@ func TestEscapeText(t *testing.T) {
`<svg:a svg:onclick="x()">`,
context{},
},
{
"<script>var a = `",
context{state: stateJSTmplLit, element: elementScript},
},
{
"<script>var a = `${",
context{state: stateJS, element: elementScript},
},
{
"<script>var a = `${}",
context{state: stateJSTmplLit, element: elementScript},
},
{
"<script>var a = `${`",
context{state: stateJSTmplLit, element: elementScript},
},
{
"<script>var a = `${var a = \"",
context{state: stateJSDqStr, element: elementScript},
},
{
"<script>var a = `${var a = \"`",
context{state: stateJSDqStr, element: elementScript},
},
{
"<script>var a = `${var a = \"}",
context{state: stateJSDqStr, element: elementScript},
},
{
"<script>var a = `${``",
context{state: stateJS, element: elementScript},
},
{
"<script>var a = `${`}",
context{state: stateJSTmplLit, element: elementScript},
},
{
"<script>`${ {} } asd`</script><script>`${ {} }",
context{state: stateJSTmplLit, element: elementScript},
},
{
"<script>var foo = `${ (_ => { return \"x\" })() + \"${",
context{state: stateJSDqStr, element: elementScript},
},
{
"<script>var a = `${ {</script><script>var b = `${ x }",
context{state: stateJSTmplLit, element: elementScript, jsCtx: jsCtxDivOp},
},
{
"<script>var foo = `x` + \"${",
context{state: stateJSDqStr, element: elementScript},
},
{
"<script>function f() { var a = `${}`; }",
context{state: stateJS, element: elementScript},
},
{
"<script>{`${}`}",
context{state: stateJS, element: elementScript},
},
{
"<script>`${ function f() { return `${1}` }() }`",
context{state: stateJS, element: elementScript, jsCtx: jsCtxDivOp},
},
{
"<script>function f() {`${ function f() { `${1}` } }`}",
context{state: stateJS, element: elementScript, jsCtx: jsCtxDivOp},
},
{
"<script>`${ { `` }",
context{state: stateJS, element: elementScript},
},
{
"<script>`${ { }`",
context{state: stateJSTmplLit, element: elementScript},
},
{
"<script>var foo = `${ foo({ a: { c: `${",
context{state: stateJS, element: elementScript},
},
{
"<script>var foo = `${ foo({ a: { c: `${ {{.}} }` }, b: ",
context{state: stateJS, element: elementScript},
},
{
"<script>`${ `}",
context{state: stateJSTmplLit, element: elementScript},
},
}
for _, test := range tests {

View 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
@@ -838,7 +852,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]
@@ -861,7 +875,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)
@@ -962,7 +976,7 @@ const treeTemplate = `
`
func TestTree(t *testing.T) {
var tree = &Tree{
tree := &Tree{
1,
&Tree{
2, &Tree{
@@ -1213,7 +1227,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

View File

@@ -14,15 +14,9 @@
package template
import (
"sync/atomic"
template "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
)
// See https://github.com/golang/go/issues/59234
// Moved here to avoid dependency on Go's internal/debug package.
var SecurityAllowActionJSTmpl atomic.Bool
/*
This files contains the Hugo related addons. All the other files in this

View File

@@ -239,6 +239,11 @@ func jsStrEscaper(args ...any) string {
return replace(s, jsStrReplacementTable)
}
func jsTmplLitEscaper(args ...any) string {
s, _ := stringify(args...)
return replace(s, jsBqStrReplacementTable)
}
// jsRegexpEscaper behaves like jsStrEscaper but escapes regular expression
// specials so the result is treated literally when included in a regular
// expression literal. /foo{{.X}}bar/ matches the string "foo" followed by
@@ -325,6 +330,31 @@ var jsStrReplacementTable = []string{
'\\': `\\`,
}
// jsBqStrReplacementTable is like jsStrReplacementTable except it also contains
// the special characters for JS template literals: $, {, and }.
var jsBqStrReplacementTable = []string{
0: `\u0000`,
'\t': `\t`,
'\n': `\n`,
'\v': `\u000b`, // "\v" == "v" on IE 6.
'\f': `\f`,
'\r': `\r`,
// Encode HTML specials as hex so the output can be embedded
// in HTML attributes without further encoding.
'"': `\u0022`,
'`': `\u0060`,
'&': `\u0026`,
'\'': `\u0027`,
'+': `\u002b`,
'/': `\/`,
'<': `\u003c`,
'>': `\u003e`,
'\\': `\\`,
'$': `\u0024`,
'{': `\u007b`,
'}': `\u007d`,
}
// jsStrNormReplacementTable is like jsStrReplacementTable but does not
// overencode existing escapes since this table has no entry for `\`.
var jsStrNormReplacementTable = []string{
@@ -345,6 +375,7 @@ var jsStrNormReplacementTable = []string{
'<': `\u003c`,
'>': `\u003e`,
}
var jsRegexpReplacementTable = []string{
0: `\u0000`,
'\t': `\t`,

View File

@@ -21,7 +21,7 @@ func _() {
_ = x[stateJS-10]
_ = x[stateJSDqStr-11]
_ = x[stateJSSqStr-12]
_ = x[stateJSBqStr-13]
_ = x[stateJSTmplLit-13]
_ = x[stateJSRegexp-14]
_ = x[stateJSBlockCmt-15]
_ = x[stateJSLineCmt-16]
@@ -39,9 +39,9 @@ func _() {
_ = x[stateDead-28]
}
const _state_name = "stateTextstateTagstateAttrNamestateAfterNamestateBeforeValuestateHTMLCmtstateRCDATAstateAttrstateURLstateSrcsetstateJSstateJSDqStrstateJSSqStrstateJSBqStrstateJSRegexpstateJSBlockCmtstateJSLineCmtstateJSHTMLOpenCmtstateJSHTMLCloseCmtstateCSSstateCSSDqStrstateCSSSqStrstateCSSDqURLstateCSSSqURLstateCSSURLstateCSSBlockCmtstateCSSLineCmtstateErrorstateDead"
const _state_name = "stateTextstateTagstateAttrNamestateAfterNamestateBeforeValuestateHTMLCmtstateRCDATAstateAttrstateURLstateSrcsetstateJSstateJSDqStrstateJSSqStrstateJSTmplLitstateJSRegexpstateJSBlockCmtstateJSLineCmtstateJSHTMLOpenCmtstateJSHTMLCloseCmtstateCSSstateCSSDqStrstateCSSSqStrstateCSSDqURLstateCSSSqURLstateCSSURLstateCSSBlockCmtstateCSSLineCmtstateErrorstateDead"
var _state_index = [...]uint16{0, 9, 17, 30, 44, 60, 72, 83, 92, 100, 111, 118, 130, 142, 154, 167, 182, 196, 214, 233, 241, 254, 267, 280, 293, 304, 320, 335, 345, 354}
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}
func (i state) String() string {
if i >= state(len(_state_index)-1) {

View File

@@ -27,8 +27,8 @@ var transitionFunc = [...]func(context, []byte) (context, int){
stateJS: tJS,
stateJSDqStr: tJSDelimited,
stateJSSqStr: tJSDelimited,
stateJSBqStr: tJSDelimited,
stateJSRegexp: tJSDelimited,
stateJSTmplLit: tJSTmpl,
stateJSBlockCmt: tBlockCmt,
stateJSLineCmt: tLineCmt,
stateJSHTMLOpenCmt: tLineCmt,
@@ -270,7 +270,7 @@ func tURL(c context, s []byte) (context, int) {
// tJS is the context transition function for the JS state.
func tJS(c context, s []byte) (context, int) {
i := bytes.IndexAny(s, "\"`'/<-#")
i := bytes.IndexAny(s, "\"`'/{}<-#")
if i == -1 {
// Entire input is non string, comment, regexp tokens.
c.jsCtx = nextJSCtx(s, c.jsCtx)
@@ -283,7 +283,7 @@ func tJS(c context, s []byte) (context, int) {
case '\'':
c.state, c.jsCtx = stateJSSqStr, jsCtxRegexp
case '`':
c.state, c.jsCtx = stateJSBqStr, jsCtxRegexp
c.state, c.jsCtx = stateJSTmplLit, jsCtxRegexp
case '/':
switch {
case i+1 < len(s) && s[i+1] == '/':
@@ -320,12 +320,66 @@ func tJS(c context, s []byte) (context, int) {
if i+1 < len(s) && s[i+1] == '!' {
c.state, i = stateJSLineCmt, i+1
}
case '{':
// We only care about tracking brace depth if we are inside of a
// template literal.
if len(c.jsBraceDepth) == 0 {
return c, i + 1
}
c.jsBraceDepth[len(c.jsBraceDepth)-1]++
case '}':
if len(c.jsBraceDepth) == 0 {
return c, i + 1
}
// There are no cases where a brace can be escaped in the JS context
// that are not syntax errors, it seems. Because of this we can just
// count "\}" as "}" and move on, the script is already broken as
// fully fledged parsers will just fail anyway.
c.jsBraceDepth[len(c.jsBraceDepth)-1]--
if c.jsBraceDepth[len(c.jsBraceDepth)-1] >= 0 {
return c, i + 1
}
c.jsBraceDepth = c.jsBraceDepth[:len(c.jsBraceDepth)-1]
c.state = stateJSTmplLit
default:
panic("unreachable")
}
return c, i + 1
}
func tJSTmpl(c context, s []byte) (context, int) {
var k int
for {
i := k + bytes.IndexAny(s[k:], "`\\$")
if i < k {
break
}
switch s[i] {
case '\\':
i++
if i == len(s) {
return context{
state: stateError,
err: errorf(ErrPartialEscape, nil, 0, "unfinished escape sequence in JS string: %q", s),
}, len(s)
}
case '$':
if len(s) >= i+2 && s[i+1] == '{' {
c.jsBraceDepth = append(c.jsBraceDepth, 0)
c.state = stateJS
return c, i + 2
}
case '`':
// end
c.state = stateJS
return c, i + 1
}
k = i + 1
}
return c, len(s)
}
// tJSDelimited is the context transition function for the JS string and regexp
// states.
func tJSDelimited(c context, s []byte) (context, int) {
@@ -333,8 +387,6 @@ func tJSDelimited(c context, s []byte) (context, int) {
switch c.state {
case stateJSSqStr:
specials = `\'`
case stateJSBqStr:
specials = "`\\"
case stateJSRegexp:
specials = `\/[]`
}

View File

@@ -60,13 +60,6 @@ func tryExec() error {
// may as well use the same path so that this branch can be tested without
// an ios environment.
/*if !testing.Testing() {
// This isn't a standard 'go test' binary, so we don't know how to
// self-exec in a way that should succeed without side effects.
// Just forget it.
return errors.New("can't probe for exec support with a non-test executable")
}*/
// We know that this is a test executable. We should be able to run it with a
// no-op flag to check for overall exec support.
exe, err := os.Executable()
@@ -99,11 +92,14 @@ func MustHaveExecPath(t testing.TB, path string) {
// CleanCmdEnv will fill cmd.Env with the environment, excluding certain
// variables that could modify the behavior of the Go tools such as
// GODEBUG and GOTRACEBACK.
//
// If the caller wants to set cmd.Dir, set it before calling this function,
// so PWD will be set correctly in the environment.
func CleanCmdEnv(cmd *exec.Cmd) *exec.Cmd {
if cmd.Env != nil {
panic("environment already set")
}
for _, env := range os.Environ() {
for _, env := range cmd.Environ() {
// Exclude GODEBUG from the environment to prevent its output
// from breaking tests that are trying to parse other command output.
if strings.HasPrefix(env, "GODEBUG=") {

View File

@@ -15,10 +15,6 @@ import (
"errors"
"flag"
"fmt"
"github.com/gohugoio/hugo/tpl/internal/go_templates/cfg"
//"internal/platform"
"os"
"os/exec"
"path/filepath"
@@ -27,6 +23,8 @@ 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
@@ -45,8 +43,8 @@ func Builder() string {
// HasGoBuild reports whether the current system can build programs with “go build”
// and then run them with os.StartProcess or exec.Command.
// Modified by Hugo (not needed)
func HasGoBuild() bool {
// Modified by Hugo (not needed)
return false
}
@@ -69,13 +67,13 @@ func MustHaveGoBuild(t testing.TB) {
}
}
// HasGoRun reports whether the current system can run programs with “go run.
// HasGoRun reports whether the current system can run programs with “go run”.
func HasGoRun() bool {
// For now, having go run and having go build are the same.
return HasGoBuild()
}
// MustHaveGoRun checks that the current system can run programs with “go run.
// MustHaveGoRun checks that the current system can run programs with “go run”.
// If not, MustHaveGoRun calls t.Skip with an explanation.
func MustHaveGoRun(t testing.TB) {
if !HasGoRun() {
@@ -300,8 +298,8 @@ func MustHaveCGO(t testing.TB) {
// CanInternalLink reports whether the current system can link programs with
// internal linking.
// Modified by Hugo (not needed)
func CanInternalLink(withCgo bool) bool {
// Modified by Hugo (not needed)
return false
}
@@ -320,8 +318,8 @@ func MustInternalLink(t testing.TB, withCgo bool) {
// MustHaveBuildMode reports whether the current system can build programs in
// the given build mode.
// If not, MustHaveBuildMode calls t.Skip with an explanation.
// Modified by Hugo (not needed)
func MustHaveBuildMode(t testing.TB, buildmode string) {
// Modified by Hugo (not needed)
}
// HasSymlink reports whether the current system can use os.Symlink.
@@ -438,7 +436,7 @@ func WriteImportcfg(t testing.TB, dstPath string, packageFiles map[string]string
}
}
if err := os.WriteFile(dstPath, icfg.Bytes(), 0666); err != nil {
if err := os.WriteFile(dstPath, icfg.Bytes(), 0o666); err != nil {
t.Fatal(err)
}
}

View File

@@ -7,6 +7,8 @@
package testenv
import (
"errors"
"io/fs"
"os"
)
@@ -15,6 +17,5 @@ import (
var Sigquit = os.Kill
func syscallIsNotSupported(err error) bool {
// Removed by Hugo (not supported in Go 1.20).
return false
return errors.Is(err, fs.ErrPermission)
}

View File

@@ -5,13 +5,13 @@
package testenv_test
import (
"github.com/gohugoio/hugo/tpl/internal/go_templates/testenv"
//"internal/platform"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/gohugoio/hugo/tpl/internal/go_templates/testenv"
)
func TestGoToolLocation(t *testing.T) {
@@ -83,3 +83,26 @@ func TestMustHaveExec(t *testing.T) {
}
}
}
func TestCleanCmdEnvPWD(t *testing.T) {
// Test that CleanCmdEnv sets PWD if cmd.Dir is set.
switch runtime.GOOS {
case "plan9", "windows":
t.Skipf("PWD is not used on %s", runtime.GOOS)
}
dir := t.TempDir()
cmd := testenv.Command(t, testenv.GoToolPath(t), "help")
cmd.Dir = dir
cmd = testenv.CleanCmdEnv(cmd)
for _, env := range cmd.Env {
if strings.HasPrefix(env, "PWD=") {
pwd := strings.TrimPrefix(env, "PWD=")
if pwd != dir {
t.Errorf("unexpected PWD: want %s, got %s", dir, pwd)
}
return
}
}
t.Error("PWD not set in cmd.Env")
}

View File

@@ -7,6 +7,8 @@
package testenv
import (
"errors"
"io/fs"
"syscall"
)
@@ -15,6 +17,27 @@ import (
var Sigquit = syscall.SIGQUIT
func syscallIsNotSupported(err error) bool {
// Removed by Hugo (not supported in Go 1.20)
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) {
return true
}
return false
}

View File

@@ -438,13 +438,13 @@ produce the text
By construction, a template may reside in only one association. If it's
necessary to have a template addressable from multiple associations, the
template definition must be parsed multiple times to create distinct *Template
values, or must be copied with the Clone or AddParseTree method.
values, or must be copied with [Template.Clone] or [Template.AddParseTree].
Parse may be called multiple times to assemble the various associated templates;
see the ParseFiles and ParseGlob functions and methods for simple ways to parse
related templates stored in files.
see [ParseFiles], [ParseGlob], [Template.ParseFiles] and [Template.ParseGlob]
for simple ways to parse related templates stored in files.
A template may be executed directly or through ExecuteTemplate, which executes
A template may be executed directly or through [Template.ExecuteTemplate], which executes
an associated template identified by name. To invoke our example above, we
might write,

View 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

View File

@@ -320,12 +320,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},
@@ -372,15 +376,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},
@@ -466,10 +476,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;no value&gt;", tVal, true},
@@ -844,7 +858,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]
@@ -867,7 +881,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)
@@ -990,7 +1004,7 @@ const treeTemplate = `
`
func TestTree(t *testing.T) {
var tree = &Tree{
tree := &Tree{
1,
&Tree{
2, &Tree{
@@ -1243,7 +1257,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

View File

@@ -478,7 +478,7 @@ func eq(arg1 reflect.Value, arg2 ...reflect.Value) (bool, error) {
case k1 == uintKind && k2 == intKind:
truth = arg.Int() >= 0 && arg1.Uint() == uint64(arg.Int())
default:
if arg1 != zero && arg != zero {
if arg1.IsValid() && arg.IsValid() {
return false, errBadComparison
}
}