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..4909d32 100644 --- a/internal/jsonwire/wire_test.go +++ b/internal/jsonwire/wire_test.go @@ -73,3 +73,24 @@ 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"}, + {"/fizz/buzz/bazz", "/fizz/…/buzz"}, + {"/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)