From 9d298c7af12d6123ec928e2221dae4329830bd0d Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Thu, 12 Dec 2024 17:04:51 -0800 Subject: [PATCH] Truncate JSON pointer when printing For deeply nested JSON values, the printed JSON pointer could become excessively long for logging. Truncate the pointer and preserve the head and tail of the pointer. --- errors.go | 2 +- internal/jsonwire/wire.go | 45 ++++++++++++++++++++++++++++++++++ internal/jsonwire/wire_test.go | 22 +++++++++++++++++ jsontext/errors.go | 3 +-- 4 files changed, 69 insertions(+), 3 deletions(-) diff --git a/errors.go b/errors.go index 35099d9..269e848 100644 --- a/errors.go +++ b/errors.go @@ -99,7 +99,7 @@ func (e *SemanticError) Error() string { switch { case e.JSONPointer != "": sb.WriteString(" within JSON value at ") - sb.WriteString(strconv.Quote(string(e.JSONPointer))) + sb.WriteString(strconv.Quote(jsonwire.TruncatePointer(string(e.JSONPointer), 100))) case e.ByteOffset > 0: sb.WriteString(" after byte offset ") sb.WriteString(strconv.FormatInt(e.ByteOffset, 10)) diff --git a/internal/jsonwire/wire.go b/internal/jsonwire/wire.go index a1add8e..6632382 100644 --- a/internal/jsonwire/wire.go +++ b/internal/jsonwire/wire.go @@ -162,3 +162,48 @@ func NewInvalidEscapeSequenceError[Bytes ~[]byte | ~string](what Bytes) error { return errors.New("invalid " + label + " `" + string(what) + "` within string") } } + +// TruncatePointer optionally truncates the JSON pointer, +// enforcing that the length roughly does not exceed n. +func TruncatePointer(s string, n int) string { + if len(s) <= n { + return s + } + i := n / 2 + j := len(s) - n/2 + + // Avoid truncating a name if there are multiple names present. + if k := strings.LastIndexByte(s[:i], '/'); k > 0 { + i = k + } + if k := strings.IndexByte(s[j:], '/'); k >= 0 { + j += k + len("/") + } + + // Avoid truncation in the middle of a UTF-8 rune. + isInvalidUTF8 := func(r rune, rn int) bool { return r == utf8.RuneError && rn == 1 } + for i > 0 && isInvalidUTF8(utf8.DecodeLastRuneInString(s[:i])) { + i-- + } + for j < len(s) && isInvalidUTF8(utf8.DecodeRuneInString(s[j:])) { + j++ + } + + // Determine the right middle fragment to use. + var middle string + switch strings.Count(s[i:j], "/") { + case 0: + middle = "…" + case 1: + middle = "…/…" + default: + middle = "…/…/…" + } + if strings.HasPrefix(s[i:j], "/") && middle != "…" { + middle = strings.TrimPrefix(middle, "…") + } + if strings.HasSuffix(s[i:j], "/") && middle != "…" { + middle = strings.TrimSuffix(middle, "…") + } + return s[:i] + middle + s[j:] +} diff --git a/internal/jsonwire/wire_test.go b/internal/jsonwire/wire_test.go index 369aa00..e27ff2d 100644 --- a/internal/jsonwire/wire_test.go +++ b/internal/jsonwire/wire_test.go @@ -73,3 +73,25 @@ func FuzzCompareUTF16(f *testing.F) { } }) } + +func TestTruncatePointer(t *testing.T) { + tests := []struct{ in, want string }{ + {"hello", "hello"}, + {"/a/b/c", "/a/b/c"}, + {"/a/b/c/d/e/f/g", "/a/b/…/f/g"}, + {"superlongname", "super…gname"}, + {"/superlongname/superlongname", "/supe…/…gname"}, + {"/superlongname/superlongname/superlongname", "/supe…/…/…gname"}, + {"/fizz/buzz/bazz", "/fizz/…/bazz"}, + {"/fizz/buzz/bazz/razz", "/fizz/…/razz"}, + {"/////////////////////////////", "/////…/////"}, + {"/🎄❤️✨/🎁✅😊/🎅🔥⭐", "/🎄…/…/…⭐"}, + } + for _, tt := range tests { + got := TruncatePointer(tt.in, 10) + if got != tt.want { + t.Errorf("TruncatePointer(%q) = %q, want %q", tt.in, got, tt.want) + } + } + +} diff --git a/jsontext/errors.go b/jsontext/errors.go index 6ad397f..050824a 100644 --- a/jsontext/errors.go +++ b/jsontext/errors.go @@ -91,8 +91,7 @@ func (e *SyntacticError) Error() string { b = append(b, "syntactic error"...) } if pointer != "" { - // TODO: Truncate excessively long pointers. - b = strconv.AppendQuote(append(b, " within "...), string(pointer)) + b = strconv.AppendQuote(append(b, " within "...), jsonwire.TruncatePointer(string(pointer), 100)) } if offset > 0 { b = strconv.AppendInt(append(b, " after offset "...), offset, 10)