diff --git a/Makefile b/Makefile index d4af7aff..71bebebe 100644 --- a/Makefile +++ b/Makefile @@ -339,11 +339,14 @@ update-template: exit 1; \ fi src="$$(go env GOROOT)/src"; \ - cp "$$src/text/template/exec.go" "$$src/text/template/funcs.go" "$$src/text/template/template.go" "$$src/text/template/option.go" template/gotext/; \ - cp "$$src/internal/fmtsort/sort.go" template/gotext/fmtsort/; \ + rm -f template/gotext/*.go template/gotext/fmtsort/*.go; \ + for f in $$(go list -f '{{range .GoFiles}}{{.}} {{end}}' text/template); do cp "$$src/text/template/$$f" template/gotext/; done; \ + for f in $$(go list -f '{{range .GoFiles}}{{.}} {{end}}' internal/fmtsort); do cp "$$src/internal/fmtsort/$$f" template/gotext/fmtsort/; done; \ + sed -i 's/^package template$$/package gotext/' template/gotext/*.go; \ + sed -i 's#"internal/fmtsort"#"heckel.io/ntfy/v2/template/gotext/fmtsort"#' template/gotext/*.go; \ ( cd template/gotext && for p in patches/*.patch; do echo "Applying $$p"; git apply "$$p" || exit 1; done ) go env GOVERSION > template/gotext/GENERATED_FROM - @echo "Regenerated template/gotext/ from $(TEMPLATE_GO_VERSION); review with 'git diff'." + @echo "Regenerated template/gotext/ from $(TEMPLATE_GO_VERSION) (files enumerated via 'go list'); review with 'git diff'." template-check: FORCE @if [ "$$(cat template/gotext/GENERATED_FROM)" != "$(TEMPLATE_GO_VERSION)" ]; then \ @@ -355,17 +358,20 @@ template-check: FORCE exit 0; \ fi @tmp=$$(mktemp -d); src="$$(go env GOROOT)/src"; \ - mkdir -p "$$tmp/gotext/fmtsort" "$$tmp/patches"; \ - cp "$$src/text/template/exec.go" "$$src/text/template/funcs.go" "$$src/text/template/template.go" "$$src/text/template/option.go" "$$tmp/gotext/"; \ - cp "$$src/internal/fmtsort/sort.go" "$$tmp/gotext/fmtsort/"; \ - cp template/gotext/patches/*.patch "$$tmp/patches/"; \ - ( cd "$$tmp/gotext" && for p in "$$tmp"/patches/*.patch; do git apply "$$p" || exit 1; done ); \ - ok=1; \ - for f in exec.go funcs.go template.go option.go fmtsort/sort.go; do \ - diff -q "$$tmp/gotext/$$f" "template/gotext/$$f" >/dev/null 2>&1 || { echo "DRIFT: template/gotext/$$f differs from GOROOT+patches"; ok=0; }; \ - done; \ - rm -rf "$$tmp"; \ - if [ "$$ok" != 1 ]; then echo "ERROR: template/gotext/ drifted from GOROOT+patches. Run 'make update-template' on Go $(TEMPLATE_GO_VERSION)."; exit 1; fi + mkdir -p "$$tmp/gotext/fmtsort"; \ + for f in $$(go list -f '{{range .GoFiles}}{{.}} {{end}}' text/template); do cp "$$src/text/template/$$f" "$$tmp/gotext/"; done; \ + for f in $$(go list -f '{{range .GoFiles}}{{.}} {{end}}' internal/fmtsort); do cp "$$src/internal/fmtsort/$$f" "$$tmp/gotext/fmtsort/"; done; \ + sed -i 's/^package template$$/package gotext/' "$$tmp/gotext/"*.go; \ + sed -i 's#"internal/fmtsort"#"heckel.io/ntfy/v2/template/gotext/fmtsort"#' "$$tmp/gotext/"*.go; \ + cp template/gotext/patches/*.patch "$$tmp/"; \ + ( cd "$$tmp/gotext" && for p in "$$tmp"/*.patch; do git apply "$$p" || exit 1; done ); \ + if diff -rq -x 'README.md' -x 'GENERATED_FROM' -x 'patches' "$$tmp/gotext" template/gotext >/dev/null 2>&1; then \ + rm -rf "$$tmp"; \ + else \ + echo "ERROR: template/gotext/ drifted from GOROOT+patches (or its file set changed). Run 'make update-template' on Go $(TEMPLATE_GO_VERSION):"; \ + diff -rq -x 'README.md' -x 'GENERATED_FROM' -x 'patches' "$$tmp/gotext" template/gotext; \ + rm -rf "$$tmp"; exit 1; \ + fi # go-check is advisory only (never fails): it warns when the pinned Go (.go-version) is behind the # latest upstream release, so template/gotext doesn't silently fall behind on text/template fixes. diff --git a/docs/publish.md b/docs/publish.md index f8328f84..e66356d6 100644 --- a/docs/publish.md +++ b/docs/publish.md @@ -3224,6 +3224,11 @@ You can use the following features in your templates: A good way to experiment with Go templates is the **[Go Template Playground](https://repeatit.io)**. It is _highly recommended_ to test your templates there first ([example for Grafana alert](https://repeatit.io/#/share/eyJ0ZW1wbGF0ZSI6InRpdGxlPUdyYWZhbmErYWxlcnQ6K3t7LnRpdGxlfX0mbWVzc2FnZT17ey5tZXNzYWdlfX0iLCJpbnB1dCI6IntcbiAgXCJyZWNlaXZlclwiOiBcIm50ZnlcXFxcLmV4YW1wbGVcXFxcLmNvbS9hbGVydHNcIixcbiAgXCJzdGF0dXNcIjogXCJyZXNvbHZlZFwiLFxuICBcImFsZXJ0c1wiOiBbXG4gICAge1xuICAgICAgXCJzdGF0dXNcIjogXCJyZXNvbHZlZFwiLFxuICAgICAgXCJsYWJlbHNcIjoge1xuICAgICAgICBcImFsZXJ0bmFtZVwiOiBcIkxvYWQgYXZnIDE1bSB0b28gaGlnaFwiLFxuICAgICAgICBcImdyYWZhbmFfZm9sZGVyXCI6IFwiTm9kZSBhbGVydHNcIixcbiAgICAgICAgXCJpbnN0YW5jZVwiOiBcIjEwLjEwOC4wLjI6OTEwMFwiLFxuICAgICAgICBcImpvYlwiOiBcIm5vZGUtZXhwb3J0ZXJcIlxuICAgICAgfSxcbiAgICAgIFwiYW5ub3RhdGlvbnNcIjoge1xuICAgICAgICBcInN1bW1hcnlcIjogXCIxNW0gbG9hZCBhdmVyYWdlIHRvbyBoaWdoXCJcbiAgICAgIH0sXG4gICAgICBcInN0YXJ0c0F0XCI6IFwiMjAyNC0wMy0xNVQwMjoyODowMFpcIixcbiAgICAgIFwiZW5kc0F0XCI6IFwiMjAyNC0wMy0xNVQwMjo0MjowMFpcIixcbiAgICAgIFwiZ2VuZXJhdG9yVVJMXCI6IFwibG9jYWxob3N0OjMwMDAvYWxlcnRpbmcvZ3JhZmFuYS9OVzlvRHctNHovdmlld1wiLFxuICAgICAgXCJmaW5nZXJwcmludFwiOiBcImJlY2JmYjk0YmQ4MWVmNDhcIixcbiAgICAgIFwic2lsZW5jZVVSTFwiOiBcImxvY2FsaG9zdDozMDAwL2FsZXJ0aW5nL3NpbGVuY2UvbmV3P2FsZXJ0bWFuYWdlcj1ncmFmYW5hJm1hdGNoZXI9YWxlcnRuYW1lJTNETG9hZCthdmcrMTVtK3RvbytoaWdoJm1hdGNoZXI9Z3JhZmFuYV9mb2xkZXIlM0ROb2RlK2FsZXJ0cyZtYXRjaGVyPWluc3RhbmNlJTNEMTAuMTA4LjAuMiUzQTkxMDAmbWF0Y2hlcj1qb2IlM0Rub2RlLWV4cG9ydGVyXCIsXG4gICAgICBcImRhc2hib2FyZFVSTFwiOiBcIlwiLFxuICAgICAgXCJwYW5lbFVSTFwiOiBcIlwiLFxuICAgICAgXCJ2YWx1ZXNcIjoge1xuICAgICAgICBcIkJcIjogMTguOTgyMTEzMTQ0NzU4NzYsXG4gICAgICAgIFwiQ1wiOiAwXG4gICAgICB9LFxuICAgICAgXCJ2YWx1ZVN0cmluZ1wiOiBcIlsgdmFyPSdCJyBsYWJlbHM9e19fbmFtZV9fPW5vZGVfbG9hZDE1LCBpbnN0YW5jZT0xMC4xMDguMC4yOjkxMDAsIGpvYj1ub2RlLWV4cG9ydGVyfSB2YWx1ZT0xOC45ODIxMTMxNDQ3NTg3NiBdLCBbIHZhcj0nQycgbGFiZWxzPXtfX25hbWVfXz1ub2RlX2xvYWQxNSwgaW5zdGFuY2U9MTAuMTA4LjAuMjo5MTAwLCBqb2I9bm9kZS1leHBvcnRlcn0gdmFsdWU9MCBdXCJcbiAgICB9XG4gIF0sXG4gIFwiZ3JvdXBMYWJlbHNcIjoge1xuICAgIFwiYWxlcnRuYW1lXCI6IFwiTG9hZCBhdmcgMTVtIHRvbyBoaWdoXCIsXG4gICAgXCJncmFmYW5hX2ZvbGRlclwiOiBcIk5vZGUgYWxlcnRzXCJcbiAgfSxcbiAgXCJjb21tb25MYWJlbHNcIjoge1xuICAgIFwiYWxlcnRuYW1lXCI6IFwiTG9hZCBhdmcgMTVtIHRvbyBoaWdoXCIsXG4gICAgXCJncmFmYW5hX2ZvbGRlclwiOiBcIk5vZGUgYWxlcnRzXCIsXG4gICAgXCJpbnN0YW5jZVwiOiBcIjEwLjEwOC4wLjI6OTEwMFwiLFxuICAgIFwiam9iXCI6IFwibm9kZS1leHBvcnRlclwiXG4gIH0sXG4gIFwiY29tbW9uQW5ub3RhdGlvbnNcIjoge1xuICAgIFwic3VtbWFyeVwiOiBcIjE1bSBsb2FkIGF2ZXJhZ2UgdG9vIGhpZ2hcIlxuICB9LFxuICBcImV4dGVybmFsVVJMXCI6IFwibG9jYWxob3N0OjMwMDAvXCIsXG4gIFwidmVyc2lvblwiOiBcIjFcIixcbiAgXCJncm91cEtleVwiOiBcInt9OnthbGVydG5hbWU9XFxcIkxvYWQgYXZnIDE1bSB0b28gaGlnaFxcXCIsIGdyYWZhbmFfZm9sZGVyPVxcXCJOb2RlIGFsZXJ0c1xcXCJ9XCIsXG4gIFwidHJ1bmNhdGVkQWxlcnRzXCI6IDAsXG4gIFwib3JnSWRcIjogMSxcbiAgXCJ0aXRsZVwiOiBcIltSRVNPTFZFRF0gTG9hZCBhdmcgMTVtIHRvbyBoaWdoIE5vZGUgYWxlcnRzICgxMC4xMDguMC4yOjkxMDAgbm9kZS1leHBvcnRlcilcIixcbiAgXCJzdGF0ZVwiOiBcIm9rXCIsXG4gIFwibWVzc2FnZVwiOiBcIioqUmVzb2x2ZWQqKlxcblxcblZhbHVlOiBCPTE4Ljk4MjExMzE0NDc1ODc2LCBDPTBcXG5MYWJlbHM6XFxuIC0gYWxlcnRuYW1lID0gTG9hZCBhdmcgMTVtIHRvbyBoaWdoXFxuIC0gZ3JhZmFuYV9mb2xkZXIgPSBOb2RlIGFsZXJ0c1xcbiAtIGluc3RhbmNlID0gMTAuMTA4LjAuMjo5MTAwXFxuIC0gam9iID0gbm9kZS1leHBvcnRlclxcbkFubm90YXRpb25zOlxcbiAtIHN1bW1hcnkgPSAxNW0gbG9hZCBhdmVyYWdlIHRvbyBoaWdoXFxuU291cmNlOiBsb2NhbGhvc3Q6MzAwMC9hbGVydGluZy9ncmFmYW5hL05XOW9Edy00ei92aWV3XFxuU2lsZW5jZTogbG9jYWxob3N0OjMwMDAvYWxlcnRpbmcvc2lsZW5jZS9uZXc/YWxlcnRtYW5hZ2VyPWdyYWZhbmEmbWF0Y2hlcj1hbGVydG5hbWUlM0RMb2FkK2F2ZysxNW0rdG9vK2hpZ2gmbWF0Y2hlcj1ncmFmYW5hX2ZvbGRlciUzRE5vZGUrYWxlcnRzJm1hdGNoZXI9aW5zdGFuY2UlM0QxMC4xMDguMC4yJTNBOTEwMCZtYXRjaGVyPWpvYiUzRG5vZGUtZXhwb3J0ZXJcXG5cIlxufVxuIiwiY29uZmlnIjp7InRlbXBsYXRlIjoidGV4dCIsImZ1bGxTY3JlZW5IVE1MIjpmYWxzZSwiZnVuY3Rpb25zIjpbInNwcmlnIl0sIm9wdGlvbnMiOlsibGl2ZSJdLCJpbnB1dFR5cGUiOiJ5YW1sIn19)). +!!! info + A few Go template features are disabled for user-supplied templates: `{{define}}`, `{{template}}`, + `{{block}}`, and `{{call}}` are not allowed. Templates also run with a short execution time limit -- + a template that loops too long is stopped and rejected with an HTTP 400 error. + ### Template functions ntfy supports a subset of the **[Sprig template functions](publish/template-functions.md)** (originally copied from [Sprig](https://github.com/Masterminds/sprig), thank you to the Sprig developers 🙏). This is useful for advanced message templating and for transforming the data provided through the JSON payload. diff --git a/server/errors.go b/server/errors.go index aae55b2e..a97df25a 100644 --- a/server/errors.go +++ b/server/errors.go @@ -136,7 +136,7 @@ var ( errHTTPBadRequestTemplateMessageTooLarge = &errHTTP{40041, http.StatusBadRequest, "invalid request: message or title is too large after replacing template", "https://ntfy.sh/docs/publish/#message-templating", nil} errHTTPBadRequestTemplateMessageNotJSON = &errHTTP{40042, http.StatusBadRequest, "invalid request: message body must be JSON if templating is enabled", "https://ntfy.sh/docs/publish/#message-templating", nil} errHTTPBadRequestTemplateInvalid = &errHTTP{40043, http.StatusBadRequest, "invalid request: could not parse template", "https://ntfy.sh/docs/publish/#message-templating", nil} - errHTTPBadRequestTemplateDisallowedFunctionCalls = &errHTTP{40044, http.StatusBadRequest, "invalid request: template contains disallowed function calls, e.g. template, call, or define", "https://ntfy.sh/docs/publish/#message-templating", nil} + errHTTPBadRequestTemplateDisallowedFunctionCalls = &errHTTP{40044, http.StatusBadRequest, "invalid request: template contains disallowed function calls, e.g. template, call, define, or block", "https://ntfy.sh/docs/publish/#message-templating", nil} errHTTPBadRequestTemplateExecuteFailed = &errHTTP{40045, http.StatusBadRequest, "invalid request: template execution failed", "https://ntfy.sh/docs/publish/#message-templating", nil} errHTTPBadRequestTemplateExecutionTimeout = &errHTTP{40055, http.StatusBadRequest, "invalid request: template execution timed out", "https://ntfy.sh/docs/publish/#message-templating", nil} errHTTPBadRequestInvalidUsername = &errHTTP{40046, http.StatusBadRequest, "invalid request: invalid username", "", nil} diff --git a/server/server.go b/server/server.go index 1d8591ea..284d6d48 100644 --- a/server/server.go +++ b/server/server.go @@ -150,10 +150,7 @@ var ( templatesFs embed.FS // Contains template config files (e.g. grafana.yml, github.yml, ...) templatesDir = "templates" - // templateDisallowedRegex tests a template for disallowed expressions. While not really dangerous, they - // are not useful, and seem potentially troublesome. - templateDisallowedRegex = regexp.MustCompile(`(?m)\{\{-?\s*(call|template|define)\b`) - templateNameRegex = regexp.MustCompile(`^[-_A-Za-z0-9]+$`) + templateNameRegex = regexp.MustCompile(`^[-_A-Za-z0-9]+$`) ) const ( diff --git a/server/server_template.go b/server/server_template.go index b7d0be29..b9f73b7d 100644 --- a/server/server_template.go +++ b/server/server_template.go @@ -7,6 +7,7 @@ import ( "os" "path/filepath" "strings" + "text/template/parse" "time" "gopkg.in/yaml.v2" @@ -105,9 +106,6 @@ func (s *Server) renderTemplateFromParams(m *model.Message, peekedBody string, p // renderTemplate renders a template with the given JSON source data. func (s *Server) renderTemplate(name, tpl, source string) (string, error) { - if templateDisallowedRegex.MatchString(tpl) { - return "", errHTTPBadRequestTemplateDisallowedFunctionCalls - } var data any if err := json.Unmarshal([]byte(source), &data); err != nil { return "", errHTTPBadRequestTemplateMessageNotJSON @@ -116,6 +114,9 @@ func (s *Server) renderTemplate(name, tpl, source string) (string, error) { if err != nil { return "", errHTTPBadRequestTemplateInvalid.Wrap("%s", err.Error()) } + if templateUsesDisallowedFeatures(t) { + return "", errHTTPBadRequestTemplateDisallowedFunctionCalls + } t.SetExecutionDeadline(time.Now().Add(templateMaxExecutionTime)) // Bail out of runaway templates (GHSA-rhwf-xgc9-m9fp) var buf bytes.Buffer limitWriter := util.NewLimitWriter(&buf, util.NewFixedLimiter(templateMaxOutputBytes)) @@ -127,3 +128,59 @@ func (s *Server) renderTemplate(name, tpl, source string) (string, error) { } return strings.TrimSpace(strings.ReplaceAll(buf.String(), "\\n", "\n")), nil // replace any remaining "\n" (those outside of template curly braces) with newlines } + +// templateUsesDisallowedFeatures reports whether the parsed template defines or invokes a +// sub-template ({{define}}/{{block}}/{{template}}) or uses the {{call}} builtin. None are useful for +// ntfy's JSON-data templates. Checking the parse tree (rather than the raw string) catches every +// syntactic form -- e.g. {{if call .x}} or {{$y := call .x}} -- that a regex would miss. +func templateUsesDisallowedFeatures(t *gotext.Template) bool { + if len(t.Templates()) > 1 { // {{define}}/{{block}} create additional associated templates + return true + } + return treeContainsDisallowedNode(t.Root) +} + +// treeContainsDisallowedNode reports whether the parse tree contains a {{template}}/{{block}} +// invocation or a {{call}} builtin, descending into pipes and command arguments (where {{call}} can +// appear anywhere a function is allowed). +func treeContainsDisallowedNode(node parse.Node) bool { + switch n := node.(type) { + case *parse.ListNode: + if n == nil { + return false + } + for _, child := range n.Nodes { + if treeContainsDisallowedNode(child) { + return true + } + } + case *parse.ActionNode: + return treeContainsDisallowedNode(n.Pipe) + case *parse.RangeNode: + return treeContainsDisallowedNode(n.Pipe) || treeContainsDisallowedNode(n.List) || treeContainsDisallowedNode(n.ElseList) + case *parse.IfNode: + return treeContainsDisallowedNode(n.Pipe) || treeContainsDisallowedNode(n.List) || treeContainsDisallowedNode(n.ElseList) + case *parse.WithNode: + return treeContainsDisallowedNode(n.Pipe) || treeContainsDisallowedNode(n.List) || treeContainsDisallowedNode(n.ElseList) + case *parse.TemplateNode: // {{template}} or {{block}} invocation + return true + case *parse.PipeNode: + if n == nil { + return false + } + for _, cmd := range n.Cmds { + if treeContainsDisallowedNode(cmd) { + return true + } + } + case *parse.CommandNode: + for _, arg := range n.Args { + if treeContainsDisallowedNode(arg) { + return true + } + } + case *parse.IdentifierNode: // a function name; {{call}} is the disallowed builtin + return n.Ident == "call" + } + return false +} diff --git a/server/server_test.go b/server/server_test.go index c2c6c763..6697ad4d 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -3660,6 +3660,70 @@ func TestServer_MessageTemplate_ExecutionTimeout(t *testing.T) { }) } +// TestServer_MessageTemplate_DataDrivenNestedRange_TimesOut is the regression for the exact hole the +// old write-triggered TimeoutWriter missed: a nested {{range}} over a JSON array field with a +// no-output body calls no function, so only the executor's wall-clock deadline can stop it +// (GHSA-rhwf-xgc9-m9fp). +func TestServer_MessageTemplate_DataDrivenNestedRange_TimesOut(t *testing.T) { + forEachBackend(t, func(t *testing.T, databaseURL string) { + t.Parallel() + s := newTestServer(t, newTestConfig(t, databaseURL)) + elems := make([]string, 1000) + for i := range elems { + elems[i] = "0" + } + jsonBody := `{"a":[` + strings.Join(elems, ",") + `]}` + msg := `{{range .a}}{{range $.a}}{{range $.a}}{{$x := .}}{{end}}{{end}}{{end}}done` + start := time.Now() + response := request(t, s, "POST", "/mytopic", jsonBody, map[string]string{ + "X-Message": msg, + "X-Template": "1", + }) + elapsed := time.Since(start) + require.Equal(t, 400, response.Code) + require.Equal(t, 40055, toHTTPError(t, response.Body.String()).Code) + require.Less(t, elapsed, 500*time.Millisecond, "data-driven nested range should be cut off by the deadline (took %s)", elapsed) + }) +} + +// TestServer_MessageTemplate_ExpensiveFunctionLoop_TimesOut ensures the deadline also bounds loops +// whose body calls an expensive function (hashing a large string), where a single call between +// deadline checks could otherwise overshoot (GHSA-rhwf-xgc9-m9fp). +func TestServer_MessageTemplate_ExpensiveFunctionLoop_TimesOut(t *testing.T) { + forEachBackend(t, func(t *testing.T, databaseURL string) { + t.Parallel() + s := newTestServer(t, newTestConfig(t, databaseURL)) + msg := `{{$big := repeat 990 "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"}}{{range until 1000}}{{range until 1000}}{{$h := sha512sum $big}}{{end}}{{end}}` + start := time.Now() + response := request(t, s, "POST", "/mytopic", `{}`, map[string]string{ + "X-Message": msg, + "X-Template": "1", + }) + elapsed := time.Since(start) + require.Equal(t, 400, response.Code) + require.Equal(t, 40055, toHTTPError(t, response.Body.String()).Code) + require.Less(t, elapsed, 1500*time.Millisecond, "expensive-function loop should be cut off by the deadline (took %s)", elapsed) + }) +} + +// TestServer_MessageTemplate_NestedLoopPoC_TimesOut is the exact proof-of-concept from the advisory: +// a range over a runtime-computed slice, nested, must be bounded by the deadline (GHSA-rhwf-xgc9-m9fp). +func TestServer_MessageTemplate_NestedLoopPoC_TimesOut(t *testing.T) { + forEachBackend(t, func(t *testing.T, databaseURL string) { + t.Parallel() + s := newTestServer(t, newTestConfig(t, databaseURL)) + start := time.Now() + response := request(t, s, "POST", "/mytopic", `{}`, map[string]string{ + "X-Message": `{{$x := until 10000}}{{range $x}}{{range $x}}{{end}}{{end}}done`, + "X-Template": "1", + }) + elapsed := time.Since(start) + require.Equal(t, 400, response.Code) + require.Equal(t, 40055, toHTTPError(t, response.Body.String()).Code) + require.Less(t, elapsed, 500*time.Millisecond, "advisory PoC should be cut off by the deadline (took %s)", elapsed) + }) +} + func TestServer_MessageTemplate_GenuineError_NotTimeout(t *testing.T) { forEachBackend(t, func(t *testing.T, databaseURL string) { t.Parallel() @@ -3810,11 +3874,18 @@ func TestServer_MessageTemplate_DisallowedCalls(t *testing.T) { `{{- template ""}}`, `{{- template ""}}`, - `{{ call abc}}`, - `{{ define "aa"}}`, - `We cannot {{define "aa"}}`, + `{{ call "aa"}}`, + `{{define "aa"}}hi{{end}}`, + `We cannot {{define "aa"}}hi{{end}}`, `We cannot {{ call "aa"}}`, `We cannot {{- template "aa"}}`, + `{{block "aa" .}}hi{{end}}`, + `We cannot {{- block "aa" .}}hi{{end}}`, + // call is a function, not a keyword, so it can hide in non-leading positions that a + // raw-string regex misses -- the parse-tree walk catches all of them. + `{{if call .x}}x{{end}}`, + `{{$y := call .x}}`, + `{{index (call .x) 0}}`, } for _, disallowedTemplate := range disallowedTemplates { messageTemplate := disallowedTemplate diff --git a/template/gotext/README.md b/template/gotext/README.md index 97c4ac38..4d8f3dc6 100644 --- a/template/gotext/README.md +++ b/template/gotext/README.md @@ -34,9 +34,9 @@ config: Twilio, `cmd/serve.go`) keep using the standard library -- they are not | File | Origin | |------|--------| -| `exec.go`, `funcs.go`, `template.go`, `option.go` | verbatim from `$(go env GOROOT)/src/text/template/` | +| `*.go` (`exec.go`, `funcs.go`, `template.go`, `option.go`, `helper.go`, `doc.go`) | verbatim from `$(go env GOROOT)/src/text/template/`, enumerated with `go list` so files added/removed upstream are picked up automatically | | `fmtsort/sort.go` | verbatim from `$(go env GOROOT)/src/internal/fmtsort/` -- `exec.go` needs it, and `internal/...` packages can't be imported from outside GOROOT, so it comes along | -| `patches/0001-exec-deadline.patch` | our only change (see below) | +| `patches/0001-exec-deadline.patch` | our only real change (see below) | | `GENERATED_FROM` | the exact Go version `make update-template` last regenerated this copy from; provenance, written by that target | The Go toolchain version this copy is pinned to lives in the repo-root [`.go-version`](../../.go-version) @@ -49,15 +49,20 @@ plain import. ## The patch `patches/` is a quilt-style ordered series (apply `0001-*`, then `0002-*`, ...). Today there is just -`0001-exec-deadline.patch`, which is small and purely additive: +`0001-exec-deadline.patch` -- small, purely additive, and touching only `exec.go`/`template.go`: -- renames the package to `gotext` and rewrites the `internal/fmtsort` import to `heckel.io/ntfy/v2/template/gotext/fmtsort` - adds `deadline`/`steps` fields to the executor `state` and a `deadline` field + a `SetExecutionDeadline(time.Time)` method on `Template` - adds the amortized deadline check at the top of `state.walk` - adds the exported sentinel `ErrExecutionInterrupted` (detect with `errors.Is`) -Keeping the patch tiny and additive is deliberate: it makes re-basing onto a new Go release cheap. +Two *mechanical* transforms are applied by `make update-template` with `sed`, **not** the patch -- +renaming the package to `gotext`, and rewriting the `internal/fmtsort` import to +`heckel.io/ntfy/v2/template/gotext/fmtsort`. Keeping them out of the patch means they apply to +whatever files `go list` returns, so they survive upstream files being added or removed. + +Keeping the patch tiny (deadline logic only, on two stable files) is deliberate: it makes re-basing +onto a new Go release cheap. ## Updating (when bumping the Go toolchain) diff --git a/template/gotext/doc.go b/template/gotext/doc.go new file mode 100644 index 00000000..fe63fa6e --- /dev/null +++ b/template/gotext/doc.go @@ -0,0 +1,502 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +Package template implements data-driven templates for generating textual output. + +To generate HTML output, see [html/template], which has the same interface +as this package but automatically secures HTML output against certain attacks. + +Templates are executed by applying them to a data structure. Annotations in the +template refer to elements of the data structure (typically a field of a struct +or a key in a map) to control execution and derive values to be displayed. +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. + +Once parsed, a template may be executed safely in parallel, although if parallel +executions share a Writer the output may be interleaved. + +Here is a trivial example that prints "17 items are made of wool". + + type Inventory struct { + Material string + Count uint + } + sweaters := Inventory{"wool", 17} + tmpl, err := template.New("test").Parse("{{.Count}} items are made of {{.Material}}") + if err != nil { panic(err) } + err = tmpl.Execute(os.Stdout, sweaters) + if err != nil { panic(err) } + +More intricate examples appear below. + +Text and spaces + +By default, all text between actions is copied verbatim when the template is +executed. For example, the string " items are made of " in the example above +appears on standard output when the program is run. + +However, to aid in formatting template source code, if an action's left +delimiter (by default "{{") is followed immediately by a minus sign and white +space, all trailing white space is trimmed from the immediately preceding text. +Similarly, if the right delimiter ("}}") is preceded by white space and a minus +sign, all leading white space is trimmed from the immediately following text. +In these trim markers, the white space must be present: +"{{- 3}}" is like "{{3}}" but trims the immediately preceding text, while +"{{-3}}" parses as an action containing the number -3. + +For instance, when executing the template whose source is + + "{{23 -}} < {{- 45}}" + +the generated output would be + + "23<45" + +For this trimming, the definition of white space characters is the same as in Go: +space, horizontal tab, carriage return, and newline. + +Actions + +Here is the list of actions. "Arguments" and "pipelines" are evaluations of +data, defined in detail in the corresponding sections that follow. + +*/ +// {{/* a comment */}} +// {{- /* a comment with white space trimmed from preceding and following text */ -}} +// A comment; discarded. May contain newlines. +// Comments do not nest and must start and end at the +// delimiters, as shown here. +/* + + {{pipeline}} + The default textual representation (the same as would be + printed by fmt.Print) of the value of the pipeline is copied + to the output. + + {{if pipeline}} T1 {{end}} + If the value of the pipeline is empty, no output is generated; + otherwise, T1 is executed. The empty values are false, 0, any + nil pointer or interface value, and any array, slice, map, or + string of length zero. + Dot is unaffected. + + {{if pipeline}} T1 {{else}} T0 {{end}} + If the value of the pipeline is empty, T0 is executed; + otherwise, T1 is executed. Dot is unaffected. + + {{if pipeline}} T1 {{else if pipeline}} T0 {{end}} + To simplify the appearance of if-else chains, the else action + of an if may include another if directly; the effect is exactly + the same as writing + {{if pipeline}} T1 {{else}}{{if pipeline}} T0 {{end}}{{end}} + + {{range pipeline}} T1 {{end}} + The value of the pipeline must be an array, slice, map, iter.Seq, + iter.Seq2, integer or channel. + If the value of the pipeline has length zero, nothing is output; + otherwise, dot is set to the successive elements of the array, + slice, or map and T1 is executed. If the value is a map and the + keys are of basic type with a defined order, the elements will be + visited in sorted key order. + + {{range pipeline}} T1 {{else}} T0 {{end}} + The value of the pipeline must be an array, slice, map, iter.Seq, + iter.Seq2, integer or channel. + If the value of the pipeline has length zero, dot is unaffected and + T0 is executed; otherwise, dot is set to the successive elements + of the array, slice, or map and T1 is executed. + + {{break}} + The innermost {{range pipeline}} loop is ended early, stopping the + current iteration and bypassing all remaining iterations. + + {{continue}} + The current iteration of the innermost {{range pipeline}} loop is + stopped, and the loop starts the next iteration. + + {{template "name"}} + The template with the specified name is executed with nil data. + + {{template "name" pipeline}} + The template with the specified name is executed with dot set + to the value of the pipeline. + + {{block "name" pipeline}} T1 {{end}} + A block is shorthand for defining a template + {{define "name"}} T1 {{end}} + and then executing it in place + {{template "name" pipeline}} + The typical use is to define a set of root templates that are + then customized by redefining the block templates within. + + {{with pipeline}} T1 {{end}} + If the value of the pipeline is empty, no output is generated; + otherwise, dot is set to the value of the pipeline and T1 is + executed. + + {{with pipeline}} T1 {{else}} T0 {{end}} + If the value of the pipeline is empty, dot is unaffected and T0 + is executed; otherwise, dot is set to the value of the pipeline + and T1 is executed. + + {{with pipeline}} T1 {{else with pipeline}} T0 {{end}} + To simplify the appearance of with-else chains, the else action + of a with may include another with directly; the effect is exactly + the same as writing + {{with pipeline}} T1 {{else}}{{with pipeline}} T0 {{end}}{{end}} + + +Arguments + +An argument is a simple value, denoted by one of the following. + + - A boolean, string, character, integer, floating-point, imaginary + or complex constant in Go syntax. These behave like Go's untyped + constants. Note that, as in Go, whether a large integer constant + overflows when assigned or passed to a function can depend on whether + the host machine's ints are 32 or 64 bits. + - The keyword nil, representing an untyped Go nil. + - The character '.' (period): + + . + + The result is the value of dot. + - A variable name, which is a (possibly empty) alphanumeric string + preceded by a dollar sign, such as + + $piOver2 + + or + + $ + + The result is the value of the variable. + Variables are described below. + - The name of a field of the data, which must be a struct, preceded + by a period, such as + + .Field + + The result is the value of the field. Field invocations may be + chained: + + .Field1.Field2 + + Fields can also be evaluated on variables, including chaining: + + $x.Field1.Field2 + - The name of a key of the data, which must be a map, preceded + by a period, such as + + .Key + + The result is the map element value indexed by the key. + Key invocations may be chained and combined with fields to any + depth: + + .Field1.Key1.Field2.Key2 + + Although the key must be an alphanumeric identifier, unlike with + field names they do not need to start with an upper case letter. + Keys can also be evaluated on variables, including chaining: + + $x.key1.key2 + - The name of a niladic method of the data, preceded by a period, + such as + + .Method + + The result is the value of invoking the method with dot as the + receiver, dot.Method(). Such a method must have one return value (of + any type) or two return values, the second of which is an error. + If it has two and the returned error is non-nil, execution terminates + and an error is returned to the caller as the value of Execute. + Method invocations may be chained and combined with fields and keys + to any depth: + + .Field1.Key1.Method1.Field2.Key2.Method2 + + Methods can also be evaluated on variables, including chaining: + + $x.Method1.Field + - The name of a niladic function, such as + + fun + + The result is the value of invoking the function, fun(). The return + types and values behave as in methods. Functions and function + names are described below. + - A parenthesized instance of one the above, for grouping. The result + may be accessed by a field or map key invocation. + + print (.F1 arg1) (.F2 arg2) + (.StructValuedMethod "arg").Field + +Arguments may evaluate to any type; if they are pointers the implementation +automatically indirects to the base type when required. +If an evaluation yields a function value, such as a function-valued +field of a struct, the function is not invoked automatically, but it +can be used as a truth value for an if action and the like. To invoke +it, use the call function, defined below. + +Pipelines + +A pipeline is a possibly chained sequence of "commands". A command is a simple +value (argument) or a function or method call, possibly with multiple arguments: + + Argument + The result is the value of evaluating the argument. + .Method [Argument...] + The method can be alone or the last element of a chain but, + unlike methods in the middle of a chain, it can take arguments. + The result is the value of calling the method with the + arguments: + dot.Method(Argument1, etc.) + functionName [Argument...] + The result is the value of calling the function associated + with the name: + function(Argument1, etc.) + Functions and function names are described below. + +A pipeline may be "chained" by separating a sequence of commands with pipeline +characters '|'. In a chained pipeline, the result of each command is +passed as the last argument of the following command. The output of the final +command in the pipeline is the value of the pipeline. + +The output of a command will be either one value or two values, the second of +which has type error. If that second value is present and evaluates to +non-nil, execution terminates and the error is returned to the caller of +Execute. + +Variables + +A pipeline inside an action may initialize a variable to capture the result. +The initialization has syntax + + $variable := pipeline + +where $variable is the name of the variable. An action that declares a +variable produces no output. + +Variables previously declared can also be assigned, using the syntax + + $variable = pipeline + +If a "range" action initializes a variable, the variable is set to the +successive elements of the iteration. Also, a "range" may declare two +variables, separated by a comma: + + range $index, $element := pipeline + +in which case $index and $element are set to the successive values of the +array/slice index or map key and element, respectively. Note that if there is +only one variable, it is assigned the element; this is opposite to the +convention in Go range clauses. + +A variable's scope extends to the "end" action of the control structure ("if", +"with", or "range") in which it is declared, or to the end of the template if +there is no such control structure. A template invocation does not inherit +variables from the point of its invocation. + +When execution begins, $ is set to the data argument passed to Execute, that is, +to the starting value of dot. + +Examples + +Here are some example one-line templates demonstrating pipelines and variables. +All produce the quoted word "output": + + {{"\"output\""}} + A string constant. + {{`"output"`}} + A raw string constant. + {{printf "%q" "output"}} + A function call. + {{"output" | printf "%q"}} + A function call whose final argument comes from the previous + command. + {{printf "%q" (print "out" "put")}} + A parenthesized argument. + {{"put" | printf "%s%s" "out" | printf "%q"}} + A more elaborate call. + {{"output" | printf "%s" | printf "%q"}} + A longer chain. + {{with "output"}}{{printf "%q" .}}{{end}} + A with action using dot. + {{with $x := "output" | printf "%q"}}{{$x}}{{end}} + A with action that creates and uses a variable. + {{with $x := "output"}}{{printf "%q" $x}}{{end}} + A with action that uses the variable in another action. + {{with $x := "output"}}{{$x | printf "%q"}}{{end}} + The same, but pipelined. + +Functions + +During execution functions are found in two function maps: first in the +template, then in the global function map. By default, no functions are defined +in the template but the Funcs method can be used to add them. + +Predefined global functions are named as follows. + + and + Returns the boolean AND of its arguments by returning the + first empty argument or the last argument. That is, + "and x y" behaves as "if x then y else x." + Evaluation proceeds through the arguments left to right + and returns when the result is determined. + call + Returns the result of calling the first argument, which + must be a function, with the remaining arguments as parameters. + Thus "call .X.Y 1 2" is, in Go notation, dot.X.Y(1, 2) where + Y is a func-valued field, map entry, or the like. + The first argument must be the result of an evaluation + that yields a value of function type (as distinct from + a predefined function such as print). The function must + return either one or two result values, the second of which + is of type error. If the arguments don't match the function + or the returned error value is non-nil, execution stops. + html + Returns the escaped HTML equivalent of the textual + representation of its arguments. This function is unavailable + in html/template, with a few exceptions. + index + Returns the result of indexing its first argument by the + following arguments. Thus "index x 1 2 3" is, in Go syntax, + x[1][2][3]. Each indexed item must be a map, slice, or array. + slice + slice returns the result of slicing its first argument by the + remaining arguments. Thus "slice x 1 2" is, in Go syntax, x[1:2], + while "slice x" is x[:], "slice x 1" is x[1:], and "slice x 1 2 3" + is x[1:2:3]. The first argument must be a string, slice, or array. + js + Returns the escaped JavaScript equivalent of the textual + representation of its arguments. + len + Returns the integer length of its argument. + not + Returns the boolean negation of its single argument. + or + Returns the boolean OR of its arguments by returning the + first non-empty argument or the last argument, that is, + "or x y" behaves as "if x then x else y". + Evaluation proceeds through the arguments left to right + and returns when the result is determined. + print + An alias for fmt.Sprint + printf + An alias for fmt.Sprintf + println + An alias for fmt.Sprintln + urlquery + Returns the escaped value of the textual representation of + its arguments in a form suitable for embedding in a URL query. + This function is unavailable in html/template, with a few + exceptions. + +The boolean functions take any zero value to be false and a non-zero +value to be true. + +There is also a set of binary comparison operators defined as +functions: + + eq + Returns the boolean truth of arg1 == arg2 + ne + Returns the boolean truth of arg1 != arg2 + lt + Returns the boolean truth of arg1 < arg2 + le + Returns the boolean truth of arg1 <= arg2 + gt + Returns the boolean truth of arg1 > arg2 + ge + Returns the boolean truth of arg1 >= arg2 + +For simpler multi-way equality tests, eq (only) accepts two or more +arguments and compares the second and subsequent to the first, +returning in effect + + arg1==arg2 || arg1==arg3 || arg1==arg4 ... + +(Unlike with || in Go, however, eq is a function call and all the +arguments will be evaluated.) + +The comparison functions work on any values whose type Go defines as +comparable. For basic types such as integers, the rules are relaxed: +size and exact type are ignored, so any integer value, signed or unsigned, +may be compared with any other integer value. (The arithmetic value is compared, +not the bit pattern, so all negative integers are less than all unsigned integers.) +However, as usual, one may not compare an int with a float32 and so on. + +Associated templates + +Each template is named by a string specified when it is created. Also, each +template is associated with zero or more other templates that it may invoke by +name; such associations are transitive and form a name space of templates. + +A template may use a template invocation to instantiate another associated +template; see the explanation of the "template" action above. The name must be +that of a template associated with the template that contains the invocation. + +Nested template definitions + +When parsing a template, another template may be defined and associated with the +template being parsed. Template definitions must appear at the top level of the +template, much like global variables in a Go program. + +The syntax of such definitions is to surround each template declaration with a +"define" and "end" action. + +The define action names the template being created by providing a string +constant. Here is a simple example: + + {{define "T1"}}ONE{{end}} + {{define "T2"}}TWO{{end}} + {{define "T3"}}{{template "T1"}} {{template "T2"}}{{end}} + {{template "T3"}} + +This defines two templates, T1 and T2, and a third T3 that invokes the other two +when it is executed. Finally it invokes T3. If executed this template will +produce the text + + ONE TWO + +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 [Template.Clone] or [Template.AddParseTree]. + +Parse may be called multiple times to assemble the various associated templates; +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 [Template.ExecuteTemplate], which executes +an associated template identified by name. To invoke our example above, we +might write, + + err := tmpl.Execute(os.Stdout, "no data needed") + if err != nil { + log.Fatalf("execution failed: %s", err) + } + +or to invoke a particular template explicitly by name, + + err := tmpl.ExecuteTemplate(os.Stdout, "T2", "no data needed") + if err != nil { + log.Fatalf("execution failed: %s", err) + } + +*/ +package gotext diff --git a/template/gotext/helper.go b/template/gotext/helper.go new file mode 100644 index 00000000..f5dd86cf --- /dev/null +++ b/template/gotext/helper.go @@ -0,0 +1,178 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Helper functions to make constructing templates easier. + +package gotext + +import ( + "fmt" + "io/fs" + "os" + "path" + "path/filepath" +) + +// Functions and methods to parse templates. + +// Must is a helper that wraps a call to a function returning ([*Template], error) +// and panics if the error is non-nil. It is intended for use in variable +// initializations such as +// +// var t = template.Must(template.New("name").Parse("text")) +func Must(t *Template, err error) *Template { + if err != nil { + panic(err) + } + return t +} + +// ParseFiles creates a new [Template] and parses the template definitions from +// the named files. The returned template's name will have the base name and +// parsed contents of the first file. There must be at least one file. +// If an error occurs, parsing stops and the returned *Template is nil. +// +// When parsing multiple files with the same name in different directories, +// the last one mentioned will be the one that results. +// For instance, ParseFiles("a/foo", "b/foo") stores "b/foo" as the template +// named "foo", while "a/foo" is unavailable. +func ParseFiles(filenames ...string) (*Template, error) { + return parseFiles(nil, readFileOS, filenames...) +} + +// ParseFiles parses the named files and associates the resulting templates with +// t. If an error occurs, parsing stops and the returned template is nil; +// otherwise it is t. There must be at least one file. +// Since the templates created by ParseFiles are named by the base +// (see [filepath.Base]) names of the argument files, t should usually have the +// name of one of the (base) names of the files. If it does not, depending on +// t's contents before calling ParseFiles, t.Execute may fail. In that +// case use t.ExecuteTemplate to execute a valid template. +// +// When parsing multiple files with the same name in different directories, +// the last one mentioned will be the one that results. +func (t *Template) ParseFiles(filenames ...string) (*Template, error) { + t.init() + return parseFiles(t, readFileOS, filenames...) +} + +// parseFiles is the helper for the method and function. If the argument +// template is nil, it is created from the first file. +func parseFiles(t *Template, readFile func(string) (string, []byte, error), filenames ...string) (*Template, error) { + if len(filenames) == 0 { + // Not really a problem, but be consistent. + return nil, fmt.Errorf("template: no files named in call to ParseFiles") + } + for _, filename := range filenames { + name, b, err := readFile(filename) + if err != nil { + return nil, err + } + s := string(b) + // First template becomes return value if not already defined, + // and we use that one for subsequent New calls to associate + // all the templates together. Also, if this file has the same name + // as t, this file becomes the contents of t, so + // t, err := New(name).Funcs(xxx).ParseFiles(name) + // works. Otherwise we create a new template associated with t. + var tmpl *Template + if t == nil { + t = New(name) + } + if name == t.Name() { + tmpl = t + } else { + tmpl = t.New(name) + } + _, err = tmpl.Parse(s) + if err != nil { + return nil, err + } + } + return t, nil +} + +// ParseGlob creates a new [Template] and parses the template definitions from +// the files identified by the pattern. The files are matched according to the +// semantics of [filepath.Match], and the pattern must match at least one file. +// The returned template will have the [filepath.Base] name and (parsed) +// contents of the first file matched by the pattern. ParseGlob is equivalent to +// calling [ParseFiles] with the list of files matched by the pattern. +// +// When parsing multiple files with the same name in different directories, +// the last one mentioned will be the one that results. +func ParseGlob(pattern string) (*Template, error) { + return parseGlob(nil, pattern) +} + +// ParseGlob parses the template definitions in the files identified by the +// pattern and associates the resulting templates with t. The files are matched +// according to the semantics of [filepath.Match], and the pattern must match at +// least one file. ParseGlob is equivalent to calling [Template.ParseFiles] with +// the list of files matched by the pattern. +// +// When parsing multiple files with the same name in different directories, +// the last one mentioned will be the one that results. +func (t *Template) ParseGlob(pattern string) (*Template, error) { + t.init() + return parseGlob(t, pattern) +} + +// parseGlob is the implementation of the function and method ParseGlob. +func parseGlob(t *Template, pattern string) (*Template, error) { + filenames, err := filepath.Glob(pattern) + if err != nil { + return nil, err + } + if len(filenames) == 0 { + return nil, fmt.Errorf("template: pattern matches no files: %#q", pattern) + } + return parseFiles(t, readFileOS, filenames...) +} + +// ParseFS is like [Template.ParseFiles] or [Template.ParseGlob] but reads from the file system fsys +// instead of the host operating system's file system. +// It accepts a list of glob patterns (see [path.Match]). +// (Note that most file names serve as glob patterns matching only themselves.) +func ParseFS(fsys fs.FS, patterns ...string) (*Template, error) { + return parseFS(nil, fsys, patterns) +} + +// ParseFS is like [Template.ParseFiles] or [Template.ParseGlob] but reads from the file system fsys +// instead of the host operating system's file system. +// It accepts a list of glob patterns (see [path.Match]). +// (Note that most file names serve as glob patterns matching only themselves.) +func (t *Template) ParseFS(fsys fs.FS, patterns ...string) (*Template, error) { + t.init() + return parseFS(t, fsys, patterns) +} + +func parseFS(t *Template, fsys fs.FS, patterns []string) (*Template, error) { + var filenames []string + for _, pattern := range patterns { + list, err := fs.Glob(fsys, pattern) + if err != nil { + return nil, err + } + if len(list) == 0 { + return nil, fmt.Errorf("template: pattern matches no files: %#q", pattern) + } + filenames = append(filenames, list...) + } + return parseFiles(t, readFileFS(fsys), filenames...) +} + +func readFileOS(file string) (name string, b []byte, err error) { + name = filepath.Base(file) + b, err = os.ReadFile(file) + return +} + +func readFileFS(fsys fs.FS) func(string) (string, []byte, error) { + return func(file string) (name string, b []byte, err error) { + name = path.Base(file) + b, err = fs.ReadFile(fsys, file) + return + } +} diff --git a/template/gotext/patches/0001-exec-deadline.patch b/template/gotext/patches/0001-exec-deadline.patch index 333bbdf6..07000d1e 100644 --- a/template/gotext/patches/0001-exec-deadline.patch +++ b/template/gotext/patches/0001-exec-deadline.patch @@ -1,17 +1,11 @@ diff -ruN a/exec.go b/exec.go ---- a/exec.go 2026-07-08 15:38:43.551040811 +0200 -+++ b/exec.go 2026-07-08 15:38:43.553556913 +0200 -@@ -2,17 +2,19 @@ - // Use of this source code is governed by a BSD-style - // license that can be found in the LICENSE file. - --package template -+package gotext - +--- a/exec.go 2026-07-08 21:46:30.952555712 +0200 ++++ b/exec.go 2026-07-08 21:46:30.953912265 +0200 +@@ -7,12 +7,14 @@ import ( "errors" "fmt" -- "internal/fmtsort" +- "heckel.io/ntfy/v2/template/gotext/fmtsort" "io" "reflect" "runtime" @@ -79,42 +73,10 @@ diff -ruN a/exec.go b/exec.go switch node := node.(type) { case *parse.ActionNode: // Do not pop variables so they persist until next end. -diff -ruN a/funcs.go b/funcs.go ---- a/funcs.go 2026-07-08 15:38:43.551127782 +0200 -+++ b/funcs.go 2026-07-08 15:38:43.553627333 +0200 -@@ -2,7 +2,7 @@ - // Use of this source code is governed by a BSD-style - // license that can be found in the LICENSE file. - --package template -+package gotext - - import ( - "errors" -diff -ruN a/option.go b/option.go ---- a/option.go 2026-07-08 15:38:43.551232856 +0200 -+++ b/option.go 2026-07-08 15:38:43.553688366 +0200 -@@ -4,7 +4,7 @@ - - // This file contains the code to handle template options. - --package template -+package gotext - - import "strings" - diff -ruN a/template.go b/template.go ---- a/template.go 2026-07-08 15:38:43.551182684 +0200 -+++ b/template.go 2026-07-08 15:38:43.553660525 +0200 -@@ -2,20 +2,22 @@ - // Use of this source code is governed by a BSD-style - // license that can be found in the LICENSE file. - --package template -+package gotext - - import ( - "maps" +--- a/template.go 2026-07-08 21:46:30.952848382 +0200 ++++ b/template.go 2026-07-08 21:46:30.953952891 +0200 +@@ -9,13 +9,15 @@ "reflect" "sync" "text/template/parse"