// Copyright 2023 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build goexperiment.jsonv2 // Package jsonwire implements stateless functionality for handling JSON text. package jsonwire import ( "cmp" "errors" "strconv" "strings" "unicode" "unicode/utf16" "unicode/utf8" ) // TrimSuffixWhitespace trims JSON whitespace from the end of b. func TrimSuffixWhitespace(b []byte) []byte { // NOTE: The arguments and logic are kept simple to keep this inlinable. n := len(b) - 1 for n >= 0 && (b[n] == ' ' || b[n] == '\t' || b[n] == '\r' || b[n] == '\n') { n-- } return b[:n+1] } // TrimSuffixString trims a valid JSON string at the end of b. // The behavior is undefined if there is not a valid JSON string present. func TrimSuffixString(b []byte) []byte { // NOTE: The arguments and logic are kept simple to keep this inlinable. if len(b) > 0 && b[len(b)-1] == '"' { b = b[:len(b)-1] } for len(b) >= 2 && !(b[len(b)-1] == '"' && b[len(b)-2] != '\\') { b = b[:len(b)-1] // trim all characters except an unescaped quote } if len(b) > 0 && b[len(b)-1] == '"' { b = b[:len(b)-1] } return b } // HasSuffixByte reports whether b ends with c. func HasSuffixByte(b []byte, c byte) bool { // NOTE: The arguments and logic are kept simple to keep this inlinable. return len(b) > 0 && b[len(b)-1] == c } // TrimSuffixByte removes c from the end of b if it is present. func TrimSuffixByte(b []byte, c byte) []byte { // NOTE: The arguments and logic are kept simple to keep this inlinable. if len(b) > 0 && b[len(b)-1] == c { return b[:len(b)-1] } return b } // QuoteRune quotes the first rune in the input. func QuoteRune(b []byte) string { r, n := utf8.DecodeRune(b) if r == utf8.RuneError && n == 1 { return `'\x` + strconv.FormatUint(uint64(b[0]), 16) + `'` } return strconv.QuoteRune(r) } // CompareUTF16 lexicographically compares x to y according // to the UTF-16 codepoints of the UTF-8 encoded input strings. // This implements the ordering specified in RFC 8785, section 3.2.3. func CompareUTF16(x, y []byte) int { // NOTE: This is an optimized, mostly allocation-free implementation // of CompareUTF16Simple in wire_test.go. FuzzCompareUTF16 verifies that the // two implementations agree on the result of comparing any two strings. isUTF16Self := func(r rune) bool { return ('\u0000' <= r && r <= '\uD7FF') || ('\uE000' <= r && r <= '\uFFFF') } for { if len(x) == 0 || len(y) == 0 { return cmp.Compare(len(x), len(y)) } // ASCII fast-path. if x[0] < utf8.RuneSelf || y[0] < utf8.RuneSelf { if x[0] != y[0] { return cmp.Compare(x[0], y[0]) } x, y = x[1:], y[1:] continue } // Decode next pair of runes as UTF-8. rx, nx := utf8.DecodeRune(x) ry, ny := utf8.DecodeRune(y) selfx := isUTF16Self(rx) selfy := isUTF16Self(ry) switch { // The x rune is a single UTF-16 codepoint, while // the y rune is a surrogate pair of UTF-16 codepoints. case selfx && !selfy: ry, _ = utf16.EncodeRune(ry) // The y rune is a single UTF-16 codepoint, while // the x rune is a surrogate pair of UTF-16 codepoints. case selfy && !selfx: rx, _ = utf16.EncodeRune(rx) } if rx != ry { return cmp.Compare(rx, ry) } // Check for invalid UTF-8, in which case, // we just perform a byte-for-byte comparison. if isInvalidUTF8(rx, nx) || isInvalidUTF8(ry, ny) { if x[0] != y[0] { return cmp.Compare(x[0], y[0]) } } x, y = x[nx:], y[ny:] } } // TODO(https://go.dev/issue/70547): Use utf8.ErrInvalid instead. var ErrInvalidUTF8 = errors.New("invalid UTF-8") func NewInvalidCharacterError[Bytes ~[]byte | ~string](prefix Bytes, where string) error { _, n := utf8.DecodeRune([]byte(prefix)) return &InvalidTextError{"character", string(prefix[:n]), where} } func NewInvalidEscapeSequenceError[Bytes ~[]byte | ~string](what Bytes) error { if len(what) > 6 { return &InvalidTextError{"surrogate pair", string(what), "in string"} } return &InvalidTextError{"escape sequence", string(what), "in string"} } type InvalidTextError struct { Label string // e.g., "character" | "escape sequence" | "surrogate pair" What string // raw invalid text Where string // e.g., "in string" | "at start of value" } func (e *InvalidTextError) Error() string { var what string needEscape := strings.ContainsFunc(e.What, func(r rune) bool { return r == '`' || r == utf8.RuneError || unicode.IsSpace(r) || !unicode.IsPrint(r) }) switch { case utf8.RuneCount([]byte(e.What)) == 1: what = QuoteRune([]byte(e.What)) case needEscape: what = strconv.Quote(e.What) default: what = "`" + e.What + "`" } return strings.TrimSuffix("invalid "+e.Label+" "+what+" "+e.Where, " ") } // 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. 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:] } func isInvalidUTF8(r rune, rn int) bool { return r == utf8.RuneError && rn == 1 }