From 138d2ff1275bc2d1bcf610cecab6f6e134705b79 Mon Sep 17 00:00:00 2001 From: Paras Negi Date: Mon, 6 Jul 2026 11:47:12 +0530 Subject: [PATCH 1/2] CF-3932 : Stop HTML-escaping <, >, & in -o json output --- pkg/output/serialized_output.go | 16 +++++++++++- pkg/output/table.go | 3 +-- pkg/output/table_test.go | 35 +++++++++++++++++++++++++++ test/fixtures/output/api-key/7.golden | 6 ++--- 4 files changed, 54 insertions(+), 6 deletions(-) diff --git a/pkg/output/serialized_output.go b/pkg/output/serialized_output.go index dfbf8405d5..2fb28fcffe 100644 --- a/pkg/output/serialized_output.go +++ b/pkg/output/serialized_output.go @@ -1,6 +1,7 @@ package output import ( + "bytes" "encoding/json" "github.com/spf13/cobra" @@ -12,7 +13,7 @@ import ( func SerializedOutput(cmd *cobra.Command, v any) error { switch GetFormat(cmd) { default: - out, err := json.Marshal(v) + out, err := marshalJSON(v) if err != nil { return err } @@ -26,3 +27,16 @@ func SerializedOutput(cmd *cobra.Command, v any) error { } return nil } + +// marshalJSON marshals v to JSON without HTML-escaping <, >, and & (which +// encoding/json does by default), keeping text like SQL statements readable. +func marshalJSON(v any) ([]byte, error) { + buffer := new(bytes.Buffer) + encoder := json.NewEncoder(buffer) + encoder.SetEscapeHTML(false) + if err := encoder.Encode(v); err != nil { + return nil, err + } + // Encode appends a trailing newline; trim it to match json.Marshal. + return bytes.TrimRight(buffer.Bytes(), "\n"), nil +} diff --git a/pkg/output/table.go b/pkg/output/table.go index 07226087f1..8a43d3330c 100644 --- a/pkg/output/table.go +++ b/pkg/output/table.go @@ -1,7 +1,6 @@ package output import ( - "encoding/json" "fmt" "io" "reflect" @@ -126,7 +125,7 @@ func (t *Table) printCore(writer io.Writer, auto bool) error { switch t.format { default: - out, err := json.Marshal(v) + out, err := marshalJSON(v) if err != nil { return err } diff --git a/pkg/output/table_test.go b/pkg/output/table_test.go index 5ed13fcd7e..18fb134d33 100644 --- a/pkg/output/table_test.go +++ b/pkg/output/table_test.go @@ -128,6 +128,41 @@ func TestTable_NoAutoWrap(t *testing.T) { } } +func TestTable_NoHTMLEscape(t *testing.T) { + // <, >, and & must appear verbatim in -o json output, not HTML-escaped. + buf := new(bytes.Buffer) + cmd := &cobra.Command{} + cmd.Flags().String("output", JSON.String(), "") + cmd.SetOut(buf) + + table := NewTable(cmd) + table.Add(&out{ + Id: 1, + Name: "lkc-123456", + Description: "SELECT 1 WHERE 1 < 2 AND 3 > 2 & true", + }) + + err := table.Print() + require.NoError(t, err) + + expected := strings.Join([]string{ + "{", + ` "is_current": false,`, + ` "id": 1,`, + ` "name": "lkc-123456",`, + ` "description": "SELECT 1 WHERE 1 < 2 AND 3 > 2 & true"`, + "}", + }, "\n") + "\n" + require.Equal(t, expected, buf.String()) +} + +func TestMarshalJSON_NoHTMLEscape(t *testing.T) { + // marshalJSON backs both the Table and SerializedOutput JSON paths. + out, err := marshalJSON(map[string]string{"statement": "SELECT 1 WHERE 1 < 2 AND 3 > 2 & true"}) + require.NoError(t, err) + require.Equal(t, `{"statement":"SELECT 1 WHERE 1 < 2 AND 3 > 2 & true"}`, string(out)) +} + func TestTable_Filter(t *testing.T) { tests := map[string][]string{ Human.String(): { diff --git a/test/fixtures/output/api-key/7.golden b/test/fixtures/output/api-key/7.golden index e079df5e44..a891c83737 100644 --- a/test/fixtures/output/api-key/7.golden +++ b/test/fixtures/output/api-key/7.golden @@ -3,7 +3,7 @@ "key": "DEACTIVATEDUSERKEY", "description": "", "owner": "sa-6666", - "owner_email": "\u003cdeactivated user\u003e", + "owner_email": "", "resource_type": "kafka", "resource": "lkc-bob", "created": "1999-02-24T00:00:00Z" @@ -30,7 +30,7 @@ "key": "MULTICLUSTERKEY3", "description": "works for two clusters and owned by service account", "owner": "sa-12345", - "owner_email": "\u003cservice account\u003e", + "owner_email": "", "resource_type": "kafka", "resource": "lkc-abc", "created": "1999-02-24T00:00:00Z" @@ -66,7 +66,7 @@ "key": "SERVICEACCOUNTKEY1", "description": "", "owner": "sa-12345", - "owner_email": "\u003cservice account\u003e", + "owner_email": "", "resource_type": "kafka", "resource": "lkc-bob", "created": "1999-02-24T00:00:00Z" From 9eaeeba967b1f08b3a844801e79180ad353b6e98 Mon Sep 17 00:00:00 2001 From: Paras Negi Date: Tue, 7 Jul 2026 11:19:38 +0530 Subject: [PATCH 2/2] CF-3932 : Trim only the single trailing newline from marshalJSON --- pkg/output/serialized_output.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/output/serialized_output.go b/pkg/output/serialized_output.go index 2fb28fcffe..2f9eae96c7 100644 --- a/pkg/output/serialized_output.go +++ b/pkg/output/serialized_output.go @@ -37,6 +37,6 @@ func marshalJSON(v any) ([]byte, error) { if err := encoder.Encode(v); err != nil { return nil, err } - // Encode appends a trailing newline; trim it to match json.Marshal. - return bytes.TrimRight(buffer.Bytes(), "\n"), nil + // Encode appends a single trailing newline; drop just it to match json.Marshal. + return bytes.TrimSuffix(buffer.Bytes(), []byte("\n")), nil }