From ef1869aaee841e31b48aa4bdfa77336106cb7a6e Mon Sep 17 00:00:00 2001 From: lazerg Date: Wed, 16 Sep 2026 11:00:45 +0500 Subject: [PATCH] encoding/json: fix Indent with a non-space prefix or indent When the prefix or indent contains characters that v2 rejects, the v1 emulation formats with placeholder spaces of the same length and rewrites them with the original strings afterwards. The rewrite ran in a defer, so it ran after the trailing whitespace of src had already been appended and overwrote that whitespace as if it were indentation; with an empty indent it looped forever. Move the logic before the trailing whitespace is appended and avoid a defer. Also, add a test to both v1 and v1in2 to codify this behavior. This only modifies code that is compiled in under goexperiment.jsonv2. Fixes #81547 --- src/encoding/json/scanner_test.go | 22 +++++++++++++++++++ src/encoding/json/v2_indent.go | 33 +++++++++++++++------------- src/encoding/json/v2_scanner_test.go | 22 +++++++++++++++++++ 3 files changed, 62 insertions(+), 15 deletions(-) diff --git a/src/encoding/json/scanner_test.go b/src/encoding/json/scanner_test.go index 0a26ba5c6b6491..0cf69c066bcd29 100644 --- a/src/encoding/json/scanner_test.go +++ b/src/encoding/json/scanner_test.go @@ -110,6 +110,28 @@ func TestCompactAndIndent(t *testing.T) { } } +func TestIndentPrefix(t *testing.T) { + tests := []struct { + CaseName + src, prefix, indent string + want string + }{ + {Name(""), "[1,[2]]", ">", "--", "[\n>--1,\n>--[\n>----2\n>--]\n>]"}, + {Name(""), "[1,2]\n ", ">", "--", "[\n>--1,\n>--2\n>]\n "}, + {Name(""), "1\n ", ">", "", "1\n "}, // See golang.org/issue/81547 + } + for _, tt := range tests { + t.Run(tt.Name, func(t *testing.T) { + var buf bytes.Buffer + if err := Indent(&buf, []byte(tt.src), tt.prefix, tt.indent); err != nil { + t.Errorf("%s: Indent error: %v", tt.Where, err) + } else if got := buf.String(); got != tt.want { + t.Errorf("%s: Indent:\n\tgot: %q\n\twant: %q", tt.Where, got, tt.want) + } + }) + } +} + func TestCompactSeparators(t *testing.T) { // U+2028 and U+2029 should be escaped inside strings. // They should not appear outside strings. diff --git a/src/encoding/json/v2_indent.go b/src/encoding/json/v2_indent.go index 541d5cee9ab864..e3ccbed0e0a1b5 100644 --- a/src/encoding/json/v2_indent.go +++ b/src/encoding/json/v2_indent.go @@ -91,24 +91,12 @@ func Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error { func appendIndent(dst, src []byte, prefix, indent string) ([]byte, error) { // In v2, only spaces and tabs are allowed, while v1 allowed any character. dstLen := len(dst) - if len(strings.Trim(prefix, " \t"))+len(strings.Trim(indent, " \t")) > 0 { + invalidPrefix, invalidIndent := prefix, indent + replaceIndent := len(strings.Trim(prefix, " \t"))+len(strings.Trim(indent, " \t")) > 0 + if replaceIndent { // Use placeholder spaces of correct length, and replace afterwards. - invalidPrefix, invalidIndent := prefix, indent prefix = strings.Repeat(" ", len(prefix)) indent = strings.Repeat(" ", len(indent)) - defer func() { - b := dst[dstLen:] - for i := bytes.IndexByte(b, '\n'); i >= 0; i = bytes.IndexByte(b, '\n') { - b = b[i+len("\n"):] - n := len(b) - len(bytes.TrimLeft(b, " ")) // len(prefix)+n*len(indent) - spaces := b[:n] - spaces = spaces[copy(spaces, invalidPrefix):] - for len(spaces) > 0 { - spaces = spaces[copy(spaces, invalidIndent):] - } - b = b[n:] - } - }() } dst, err := jsontext.AppendFormat(dst, src, @@ -123,6 +111,21 @@ func appendIndent(dst, src []byte, prefix, indent string) ([]byte, error) { return dst[:dstLen], transformSyntacticError(err) } + // Replace the placeholder spaces with the original prefix and indent. + if replaceIndent { + b := dst[dstLen:] + for i := bytes.IndexByte(b, '\n'); i >= 0; i = bytes.IndexByte(b, '\n') { + b = b[i+len("\n"):] + n := len(b) - len(bytes.TrimLeft(b, " ")) // len(prefix)+n*len(indent) + spaces := b[:n] + spaces = spaces[copy(spaces, invalidPrefix):] + for len(spaces) > 0 { + spaces = spaces[copy(spaces, invalidIndent):] + } + b = b[n:] + } + } + // In v2, trailing whitespace is discarded, while v1 preserved it. if n := len(src) - len(bytes.TrimRight(src, " \n\r\t")); n > 0 { dst = append(dst, src[len(src)-n:]...) diff --git a/src/encoding/json/v2_scanner_test.go b/src/encoding/json/v2_scanner_test.go index 7de7a6bedce513..f7ee5c06fa2cc6 100644 --- a/src/encoding/json/v2_scanner_test.go +++ b/src/encoding/json/v2_scanner_test.go @@ -110,6 +110,28 @@ func TestCompactAndIndent(t *testing.T) { } } +func TestIndentPrefix(t *testing.T) { + tests := []struct { + CaseName + src, prefix, indent string + want string + }{ + {Name(""), "[1,[2]]", ">", "--", "[\n>--1,\n>--[\n>----2\n>--]\n>]"}, + {Name(""), "[1,2]\n ", ">", "--", "[\n>--1,\n>--2\n>]\n "}, + {Name(""), "1\n ", ">", "", "1\n "}, // See golang.org/issue/81547 + } + for _, tt := range tests { + t.Run(tt.Name, func(t *testing.T) { + var buf bytes.Buffer + if err := Indent(&buf, []byte(tt.src), tt.prefix, tt.indent); err != nil { + t.Errorf("%s: Indent error: %v", tt.Where, err) + } else if got := buf.String(); got != tt.want { + t.Errorf("%s: Indent:\n\tgot: %q\n\twant: %q", tt.Where, got, tt.want) + } + }) + } +} + func TestCompactSeparators(t *testing.T) { // U+2028 and U+2029 should be escaped inside strings. // They should not appear outside strings.