From 7d5bcac49b87362fb431a0956eccebbe4e74d367 Mon Sep 17 00:00:00 2001 From: "piotr.laczykowski" Date: Wed, 25 Mar 2026 15:14:04 +0100 Subject: [PATCH 1/2] fix(engine): override toPrettyJson to disable HTML escaping Sprig's toPrettyJson uses json.MarshalIndent which HTML-escapes &, <, > to \u0026, \u003c, \u003e. This breaks URLs and other strings containing these characters when used in Helm chart templates. Override toPrettyJson and mustToPrettyJson in the engine FuncMap with implementations that use json.Encoder with SetEscapeHTML(false), matching Sprig's two-space indentation for backward compatibility. Signed-off-by: piotr.laczykowski --- pkg/engine/funcs.go | 68 +++++++++++++++++++++++++++++++++------- pkg/engine/funcs_test.go | 20 ++++++++++++ 2 files changed, 76 insertions(+), 12 deletions(-) diff --git a/pkg/engine/funcs.go b/pkg/engine/funcs.go index e03c13b38..b5fb62578 100644 --- a/pkg/engine/funcs.go +++ b/pkg/engine/funcs.go @@ -49,18 +49,20 @@ func funcMap() template.FuncMap { // Add some extra functionality extra := template.FuncMap{ - "toToml": toTOML, - "mustToToml": mustToTOML, - "fromToml": fromTOML, - "toYaml": toYAML, - "mustToYaml": mustToYAML, - "toYamlPretty": toYAMLPretty, - "fromYaml": fromYAML, - "fromYamlArray": fromYAMLArray, - "toJson": toJSON, - "mustToJson": mustToJSON, - "fromJson": fromJSON, - "fromJsonArray": fromJSONArray, + "toToml": toTOML, + "mustToToml": mustToTOML, + "fromToml": fromTOML, + "toYaml": toYAML, + "mustToYaml": mustToYAML, + "toYamlPretty": toYAMLPretty, + "fromYaml": fromYAML, + "fromYamlArray": fromYAMLArray, + "toJson": toJSON, + "mustToJson": mustToJSON, + "toPrettyJson": toPrettyJSON, + "mustToPrettyJson": mustToPrettyJSON, + "fromJson": fromJSON, + "fromJsonArray": fromJSONArray, // This is a placeholder for the "include" function, which is // late-bound to a template. By declaring it here, we preserve the @@ -220,6 +222,48 @@ func mustToJSON(v any) string { return string(data) } +// encodePrettyJSON encodes v as indented JSON without HTML-escaping special +// characters (&, <, >). It uses two-space indentation to match the behavior +// of Sprig's toPrettyJson. +func encodePrettyJSON(v any) (string, error) { + var buf bytes.Buffer + enc := json.NewEncoder(&buf) + enc.SetEscapeHTML(false) + enc.SetIndent("", " ") + if err := enc.Encode(v); err != nil { + return "", err + } + return strings.TrimSuffix(buf.String(), "\n"), nil +} + +// toPrettyJSON takes an interface, marshals it to indented JSON without +// HTML-escaping special characters (&, <, >), and returns a string. It will +// always return a string, even on marshal error (empty string). +// +// This is designed to be called from a template. +func toPrettyJSON(v any) string { + s, err := encodePrettyJSON(v) + if err != nil { + // Swallow errors inside of a template. + return "" + } + return s +} + +// mustToPrettyJSON takes an interface, marshals it to indented JSON without +// HTML-escaping special characters (&, <, >), and returns a string. +// It will panic if there is an error. +// +// This is designed to be called from a template when you need to ensure that +// the output JSON is valid. +func mustToPrettyJSON(v any) string { + s, err := encodePrettyJSON(v) + if err != nil { + panic(err) + } + return s +} + // fromJSON converts a JSON document into a map[string]interface{}. // // This is not a general-purpose JSON parser, and will not parse all valid diff --git a/pkg/engine/funcs_test.go b/pkg/engine/funcs_test.go index be9d0153f..824f1033c 100644 --- a/pkg/engine/funcs_test.go +++ b/pkg/engine/funcs_test.go @@ -69,6 +69,15 @@ keyInElement1 = "valueInElement1"`, tpl: `{{ toJson . }}`, expect: `{"foo":"bar"}`, vars: map[string]any{"foo": "bar"}, + }, { + // toPrettyJson must not HTML-escape &, <, > and must use 2-space indent + tpl: "{{ toPrettyJson . }}", + expect: "{\n \"url\": \"https://example.com?a=1&b=2<>\"\n}", + vars: map[string]any{"url": "https://example.com?a=1&b=2<>"}, + }, { + tpl: "{{ toPrettyJson . }}", + expect: "{\n \"foo\": \"bar\"\n}", + vars: map[string]any{"foo": "bar"}, }, { tpl: `{{ fromYaml . }}`, expect: "map[hello:world]", @@ -151,6 +160,17 @@ keyInElement1 = "valueInElement1"`, }, { tpl: `{{ mustToJson . }}`, vars: loopMap, + }, { + tpl: `{{ mustToPrettyJson . }}`, + vars: loopMap, // circular reference must panic + }, { + tpl: `{{ mustToPrettyJson . }}`, + expect: "{\n \"foo\": \"bar\"\n}", + vars: map[string]any{"foo": "bar"}, + }, { + tpl: `{{ toPrettyJson . }}`, + expect: "", // circular reference must swallow error and return "" + vars: loopMap, }, { tpl: `{{ toYaml . }}`, expect: "", // should return empty string and swallow error From e98802f10e613ddd5c361c473011c8199f653305 Mon Sep 17 00:00:00 2001 From: "piotr.laczykowski" Date: Sat, 30 May 2026 21:13:00 +0200 Subject: [PATCH 2/2] fix(engine): introduce toPrettyRawJson instead of overriding toPrettyJson MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per maintainer review, do not override Sprig's toPrettyJson/mustToPrettyJson (which HTML-escape &, <, >), since existing charts may rely on the escaped output. Instead add new toPrettyRawJson/mustToPrettyRawJson functions that produce indented JSON with HTML characters unescaped — the indented counterpart to Sprig's toRawJson — preserving backwards compatibility. Add a regression test asserting toPrettyJson still escapes HTML. Co-Authored-By: claude-flow Signed-off-by: piotr.laczykowski --- pkg/engine/funcs.go | 40 ++++++++++++++++++++++++---------------- pkg/engine/funcs_test.go | 18 ++++++++++++------ 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/pkg/engine/funcs.go b/pkg/engine/funcs.go index 38576a3ed..9e88ee950 100644 --- a/pkg/engine/funcs.go +++ b/pkg/engine/funcs.go @@ -63,12 +63,12 @@ func funcMap() template.FuncMap { "toYamlPretty": toYAMLPretty, "fromYaml": fromYAML, "fromYamlArray": fromYAMLArray, - "toJson": toJSON, - "mustToJson": mustToJSON, - "toPrettyJson": toPrettyJSON, - "mustToPrettyJson": mustToPrettyJSON, - "fromJson": fromJSON, - "fromJsonArray": fromJSONArray, + "toJson": toJSON, + "mustToJson": mustToJSON, + "toPrettyRawJson": toPrettyRawJSON, + "mustToPrettyRawJson": mustToPrettyRawJSON, + "fromJson": fromJSON, + "fromJsonArray": fromJSONArray, // Duration helpers "mustToDuration": mustToDuration, @@ -241,10 +241,11 @@ func mustToJSON(v any) string { return string(data) } -// encodePrettyJSON encodes v as indented JSON without HTML-escaping special -// characters (&, <, >). It uses two-space indentation to match the behavior -// of Sprig's toPrettyJson. -func encodePrettyJSON(v any) (string, error) { +// encodePrettyRawJSON encodes v as indented JSON without HTML-escaping special +// characters (&, <, >). It uses two-space indentation to match the indentation +// of Sprig's toPrettyJson, while leaving HTML characters unescaped like Sprig's +// toRawJson. +func encodePrettyRawJSON(v any) (string, error) { var buf bytes.Buffer enc := json.NewEncoder(&buf) enc.SetEscapeHTML(false) @@ -255,13 +256,17 @@ func encodePrettyJSON(v any) (string, error) { return strings.TrimSuffix(buf.String(), "\n"), nil } -// toPrettyJSON takes an interface, marshals it to indented JSON without +// toPrettyRawJSON takes an interface, marshals it to indented JSON without // HTML-escaping special characters (&, <, >), and returns a string. It will // always return a string, even on marshal error (empty string). // +// Unlike Sprig's toPrettyJson, HTML characters are not escaped. This is the +// indented counterpart to Sprig's toRawJson. The escaping behavior of +// toPrettyJson is intentionally left unchanged for backwards compatibility. +// // This is designed to be called from a template. -func toPrettyJSON(v any) string { - s, err := encodePrettyJSON(v) +func toPrettyRawJSON(v any) string { + s, err := encodePrettyRawJSON(v) if err != nil { // Swallow errors inside of a template. return "" @@ -269,14 +274,17 @@ func toPrettyJSON(v any) string { return s } -// mustToPrettyJSON takes an interface, marshals it to indented JSON without +// mustToPrettyRawJSON takes an interface, marshals it to indented JSON without // HTML-escaping special characters (&, <, >), and returns a string. // It will panic if there is an error. // +// Unlike Sprig's mustToPrettyJson, HTML characters are not escaped. This is the +// indented counterpart to Sprig's mustToRawJson. +// // This is designed to be called from a template when you need to ensure that // the output JSON is valid. -func mustToPrettyJSON(v any) string { - s, err := encodePrettyJSON(v) +func mustToPrettyRawJSON(v any) string { + s, err := encodePrettyRawJSON(v) if err != nil { panic(err) } diff --git a/pkg/engine/funcs_test.go b/pkg/engine/funcs_test.go index 38a1b7282..66effe841 100644 --- a/pkg/engine/funcs_test.go +++ b/pkg/engine/funcs_test.go @@ -73,14 +73,20 @@ keyInElement1 = "valueInElement1"`, expect: `{"foo":"bar"}`, vars: map[string]any{"foo": "bar"}, }, { - // toPrettyJson must not HTML-escape &, <, > and must use 2-space indent - tpl: "{{ toPrettyJson . }}", + // toPrettyRawJson must not HTML-escape &, <, > and must use 2-space indent + tpl: "{{ toPrettyRawJson . }}", expect: "{\n \"url\": \"https://example.com?a=1&b=2<>\"\n}", vars: map[string]any{"url": "https://example.com?a=1&b=2<>"}, }, { - tpl: "{{ toPrettyJson . }}", + tpl: "{{ toPrettyRawJson . }}", expect: "{\n \"foo\": \"bar\"\n}", vars: map[string]any{"foo": "bar"}, + }, { + // toPrettyJson (from Sprig) must keep HTML-escaping &, <, > for + // backwards compatibility. + tpl: "{{ toPrettyJson . }}", + expect: "{\n \"url\": \"https://example.com?a=1\\u0026b=2\\u003c\\u003e\"\n}", + vars: map[string]any{"url": "https://example.com?a=1&b=2<>"}, }, { tpl: `{{ fromYaml . }}`, expect: "map[hello:world]", @@ -164,14 +170,14 @@ keyInElement1 = "valueInElement1"`, tpl: `{{ mustToJson . }}`, vars: loopMap, }, { - tpl: `{{ mustToPrettyJson . }}`, + tpl: `{{ mustToPrettyRawJson . }}`, vars: loopMap, // circular reference must panic }, { - tpl: `{{ mustToPrettyJson . }}`, + tpl: `{{ mustToPrettyRawJson . }}`, expect: "{\n \"foo\": \"bar\"\n}", vars: map[string]any{"foo": "bar"}, }, { - tpl: `{{ toPrettyJson . }}`, + tpl: `{{ toPrettyRawJson . }}`, expect: "", // circular reference must swallow error and return "" vars: loopMap, }, {