Skip to content

Commit

Permalink
Truncate JSON pointer when printing
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
dsnet committed Dec 13, 2024
1 parent cef7d84 commit 9d298c7
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 3 deletions.
2 changes: 1 addition & 1 deletion errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
45 changes: 45 additions & 0 deletions internal/jsonwire/wire.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:]
}
22 changes: 22 additions & 0 deletions internal/jsonwire/wire_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

}
3 changes: 1 addition & 2 deletions jsontext/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit 9d298c7

Please sign in to comment.