This commit is contained in:
bluepython508 committed 2026-10-22 12:27:19 +01:00
1 parent 3530afb854
commit 6903fcff27
2146 files changed
+112256 -190703

No files matched your search

+1 -1
View File
@@ -32,7 +32,7 @@ var (
// It is called after a user-defined Marshal method/function fails.
NewMarshalerError func(any, error, string) error
// TransformUnmarshalError converts a v2 error into a v1 error.
// It is called only at the top-level of a Unmarshal function.
// It is called only at the top-level of an Unmarshal function.
TransformUnmarshalError func(any, error) error
// NewRawNumber returns new(jsonv1.Number).
+21 -13
View File
@@ -43,9 +43,10 @@ const (
ByteLimit |
DepthLimit |
Marshalers |
Unmarshalers
Unmarshalers |
FormatTag
// DefaultV1Flags is the set of booleans flags that default to true under
// DefaultV1Flags is the set of boolean flags that default to true under
// v1 semantics. None of the non-boolean flags differ between v1 and v2.
DefaultV1Flags = 0 |
AllowDuplicateNames |
@@ -83,6 +84,12 @@ const (
// CanonicalizeNumbers is the set of flags related to raw number canonicalization.
CanonicalizeNumbers = CanonicalizeRawInts | CanonicalizeRawFloats
// TagFlags is the set of flags related to the presence of struct field tags.
// Tags have non-recursive effects, where the tag only applies to
// the top-level of the field value itself.
// Whenever descending into a JSON object or array, these flags are cleared.
TagFlags = StringTag | FormatTag
)
// Encoder and decoder flags.
@@ -120,10 +127,12 @@ const (
FormatNilSliceAsNull // marshal only
OmitZeroStructFields // marshal only
MatchCaseInsensitiveNames // marshal or unmarshal
DiscardUnknownMembers // marshal only
RejectUnknownMembers // unmarshal only
Marshalers // marshal only; non-boolean flag
Unmarshalers // unmarshal only; non-boolean flag
StringTag // marshal or unmarshal
FormatTag // marshal or unmarshal; non-boolean flag
FormatTagSupported // marshal or unmarshal
maxArshalV2Flag
)
@@ -143,7 +152,6 @@ const (
ParseTimeWithLooseRFC3339 // unmarshal
ReportErrorsWithLegacySemantics // marshal or unmarshal
StringifyWithLegacySemantics // marshal or unmarshal
StringifyBoolsAndStrings // marshal or unmarshal; for internal use by jsonv2.makeStructArshaler
UnmarshalAnyWithRawNumber // unmarshal; for internal use by jsonv1.Decoder.UseNumber
UnmarshalArrayFromAnyLength // unmarshal
@@ -151,7 +159,7 @@ const (
)
// bitsUsed is the number of bits used in the 64-bit boolean flags
const bitsUsed = 42
const bitsUsed = 43
// Static compile check that bitsUsed and maxArshalV1Flag are in sync.
const _ = uint64((1<<bitsUsed)-maxArshalV1Flag) + uint64(maxArshalV1Flag-(1<<bitsUsed))
@@ -169,11 +177,11 @@ func (dst *Flags) Join(src Flags) {
// Copy over all source presence bits over to the destination (using OR),
// then invert the source presence bits to clear out source value (using AND-NOT),
// then copy over source value bits over to the destination (using OR).
// e.g., dst := Flags{Presence: 0b_1100_0011, Value: 0b_1000_0011}
// e.g., src := Flags{Presence: 0b_0101_1010, Value: 0b_1001_0010}
dst.Presence |= src.Presence // e.g., 0b_1100_0011 | 0b_0101_1010 -> 0b_110_11011
dst.Values &= ^src.Presence // e.g., 0b_1000_0011 & 0b_1010_0101 -> 0b_100_00001
dst.Values |= src.Values // e.g., 0b_1000_0001 | 0b_1001_0010 -> 0b_100_10011
// e.g., dst := Flags{Presence: 0b_1100_0011, Values: 0b_1000_0011}
// e.g., src := Flags{Presence: 0b_0101_1010, Values: 0b_1001_0010}
dst.Presence |= src.Presence // e.g., 0b_1100_0011 | 0b_0101_1010 -> 0b_1101_1011
dst.Values &= ^src.Presence // e.g., 0b_1000_0011 & 0b_1010_0101 -> 0b_1000_0001
dst.Values |= src.Values // e.g., 0b_1000_0001 | 0b_1001_0010 -> 0b_1001_0011
}
// Set sets both the presence and value for the provided bool (or set of bools).
@@ -182,10 +190,10 @@ func (fs *Flags) Set(f Bools) {
// then set the presence for all the identifier bits (using OR),
// then invert the identifier bits to clear out the values (using AND-NOT),
// then copy over all the identifier bits to the value if LSB is 1.
// e.g., fs := Flags{Presence: 0b_0101_0010, Value: 0b_0001_0010}
// e.g., fs := Flags{Presence: 0b_0101_0010, Values: 0b_0001_0010}
// e.g., f := 0b_1001_0001
id := uint64(f) &^ uint64(1) // e.g., 0b_1001_0001 & 0b_1111_1110 -> 0b_1001_0000
fs.Presence |= id // e.g., 0b_0101_0010 | 0b_1001_0000 -> 0b_1101_0011
fs.Presence |= id // e.g., 0b_0101_0010 | 0b_1001_0000 -> 0b_1101_0010
fs.Values &= ^id // e.g., 0b_0001_0010 & 0b_0110_1111 -> 0b_0000_0010
fs.Values |= uint64(f&1) * id // e.g., 0b_0000_0010 | 0b_1001_0000 -> 0b_1001_0010
}
@@ -207,7 +215,7 @@ func (fs Flags) Has(f Bools) bool {
// The value bit of f (i.e., the LSB) is ignored.
func (fs *Flags) Clear(f Bools) {
// Invert f to produce a mask to clear all bits in f (using AND).
// e.g., fs := Flags{Presence: 0b_0101_0010, Value: 0b_0001_0010}
// e.g., fs := Flags{Presence: 0b_0101_0010, Values: 0b_0001_0010}
// e.g., f := 0b_0001_1000
mask := uint64(^f) // e.g., 0b_0001_1000 -> 0b_1110_0111
fs.Presence &= mask // e.g., 0b_0101_0010 & 0b_1110_0111 -> 0b_0100_0010
+56 -41
View File
@@ -17,6 +17,11 @@ type Options interface {
JSONOptions(internal.NotForPublicUse)
}
// experimentalFormatTagSupporter is a special option method that allows
// [github.com/go-json-experiment/json.ExperimentalSupportFormatTag]
// to enable experimental `format` tag functionality at runtime.
type experimentalFormatTagSupporter interface{ ExperimentalSupportFormatTag() bool }
// Struct is the combination of all options in struct form.
// This is efficient to pass down the call stack and to query.
type Struct struct {
@@ -41,23 +46,22 @@ type ArshalValues struct {
Marshalers any // jsonflags.Marshalers
Unmarshalers any // jsonflags.Unmarshalers
Format string
FormatDepth int
Format string // valid if jsonflags.FormatTag is set
}
// DefaultOptionsV2 is the set of all options that define default v2 behavior.
var DefaultOptionsV2 = Struct{
Flags: jsonflags.Flags{
Presence: uint64(jsonflags.AllFlags & ^jsonflags.WhitespaceFlags),
Values: uint64(0),
Presence: uint64(jsonflags.DefaultV1Flags),
Values: uint64(0), // all flags in DefaultV1Flags are false
},
}
// DefaultOptionsV1 is the set of all options that define default v1 behavior.
var DefaultOptionsV1 = Struct{
Flags: jsonflags.Flags{
Presence: uint64(jsonflags.AllFlags & ^jsonflags.WhitespaceFlags),
Values: uint64(jsonflags.DefaultV1Flags),
Presence: uint64(jsonflags.DefaultV1Flags),
Values: uint64(jsonflags.DefaultV1Flags), // all flags in DefaultV1Flags are true
},
}
@@ -82,6 +86,13 @@ func GetOption[T any](opts Options, setter func(T) Options) (T, bool) {
case jsonflags.Bools:
v := structOpts.Flags.Get(opt)
ok := structOpts.Flags.Has(opt)
if !ok && opt == jsonflags.StringifyNumbers && structOpts.Flags.Get(jsonflags.StringTag) {
return any(true).(T), true // check also whether the option is specified via a `string` tag
}
return any(v).(T), ok
case experimentalFormatTagSupporter:
v := structOpts.Flags.Get(jsonflags.FormatTagSupported)
ok := structOpts.Flags.Has(jsonflags.FormatTagSupported)
return any(v).(T), ok
case Indent:
if !structOpts.Flags.Has(jsonflags.Indent) {
@@ -114,72 +125,54 @@ func GetOption[T any](opts Options, setter func(T) Options) (T, bool) {
var JoinUnknownOption = func(Struct, Options) Struct { panic("unknown option") }
func (dst *Struct) Join(srcs ...Options) {
dst.join(false, srcs...)
}
func (dst *Struct) JoinWithoutCoderOptions(srcs ...Options) {
dst.join(true, srcs...)
}
func (dst *Struct) join(excludeCoderOptions bool, srcs ...Options) {
for _, src := range srcs {
switch src := src.(type) {
case nil:
continue
case jsonflags.Bools:
if excludeCoderOptions {
src &= ^jsonflags.AllCoderFlags
}
dst.Flags.Set(src)
case Indent:
if excludeCoderOptions {
continue
case experimentalFormatTagSupporter:
if src.ExperimentalSupportFormatTag() {
dst.Flags.Set(jsonflags.FormatTagSupported | 1)
} else {
dst.Flags.Set(jsonflags.FormatTagSupported | 0)
}
case Indent:
dst.Flags.Set(jsonflags.Multiline | jsonflags.Indent | 1)
dst.Indent = string(src)
case IndentPrefix:
if excludeCoderOptions {
continue
}
dst.Flags.Set(jsonflags.Multiline | jsonflags.IndentPrefix | 1)
dst.IndentPrefix = string(src)
case ByteLimit:
if excludeCoderOptions {
continue
}
dst.Flags.Set(jsonflags.ByteLimit | 1)
dst.ByteLimit = int64(src)
case DepthLimit:
if excludeCoderOptions {
continue
}
dst.Flags.Set(jsonflags.DepthLimit | 1)
dst.DepthLimit = int(src)
case *Struct:
srcFlags := src.Flags // shallow copy the flags
if excludeCoderOptions {
srcFlags.Clear(jsonflags.AllCoderFlags)
}
dst.Flags.Join(srcFlags)
if srcFlags.Has(jsonflags.NonBooleanFlags) {
if srcFlags.Has(jsonflags.Indent) {
dst.Flags.Join(src.Flags)
if src.Flags.Has(jsonflags.NonBooleanFlags) {
if src.Flags.Has(jsonflags.Indent) {
dst.Indent = src.Indent
}
if srcFlags.Has(jsonflags.IndentPrefix) {
if src.Flags.Has(jsonflags.IndentPrefix) {
dst.IndentPrefix = src.IndentPrefix
}
if srcFlags.Has(jsonflags.ByteLimit) {
if src.Flags.Has(jsonflags.ByteLimit) {
dst.ByteLimit = src.ByteLimit
}
if srcFlags.Has(jsonflags.DepthLimit) {
if src.Flags.Has(jsonflags.DepthLimit) {
dst.DepthLimit = src.DepthLimit
}
if srcFlags.Has(jsonflags.Marshalers) {
if src.Flags.Has(jsonflags.Marshalers) {
dst.Marshalers = src.Marshalers
}
if srcFlags.Has(jsonflags.Unmarshalers) {
if src.Flags.Has(jsonflags.Unmarshalers) {
dst.Unmarshalers = src.Unmarshalers
}
if src.Flags.Has(jsonflags.FormatTag) {
dst.Format = src.Format
}
}
default:
*dst = JoinUnknownOption(*dst, src)
@@ -187,6 +180,28 @@ func (dst *Struct) join(excludeCoderOptions bool, srcs ...Options) {
}
}
// InitializeMultiline sets default options implied by Multiline.
func (s *Struct) InitializeMultiline() {
if !s.Flags.Has(jsonflags.SpaceAfterColon) {
s.Flags.Set(jsonflags.SpaceAfterColon | 1)
}
if !s.Flags.Has(jsonflags.SpaceAfterComma) {
s.Flags.Set(jsonflags.SpaceAfterComma | 0)
}
if !s.Flags.Has(jsonflags.Indent) {
s.Flags.Set(jsonflags.Indent | 1)
s.Indent = "\t"
}
}
// ChangedWhitespace reports whether whitespace values have changed.
func ChangedWhitespace(s1, s2 Struct) bool {
return s1.Flags.Get(jsonflags.Multiline) != s2.Flags.Get(jsonflags.Multiline) ||
s1.Flags.Get(jsonflags.SpaceAfterColon) != s2.Flags.Get(jsonflags.SpaceAfterColon) ||
s1.Flags.Get(jsonflags.SpaceAfterComma) != s2.Flags.Get(jsonflags.SpaceAfterComma) ||
(s2.Flags.Get(jsonflags.Multiline) && (s1.Indent != s2.Indent || s1.IndentPrefix != s2.IndentPrefix))
}
type (
Indent string // jsontext.WithIndent
IndentPrefix string // jsontext.WithIndentPrefix
@@ -0,0 +1,124 @@
// Copyright 2026 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 || !go1.25
package jsonopts
// NOTE: While [ExperimentalSupportFormatTag] is exported,
// it is in an internal package and thus inaccessible for public use.
//
// The [github.com/go-json-experiment/json] module is kept in sync
// with the Go standard standard library and will expose this option
// in a way that public code can now directly reference.
type supportFormatTag struct {
Options
v bool
}
var (
enableFormatTag = supportFormatTag{v: true}
disableFormatTag = supportFormatTag{v: false}
)
// ExperimentalSupportFormatTag is a marker method that is specially
// recognized by the "github.com/go-json-experiment/json" runtime.
func (o *supportFormatTag) ExperimentalSupportFormatTag() bool {
return o.v
}
// ExperimentalSupportFormatTag enables support for the `format` tag.
//
// WARNING: This is an experimental feature and will be removed in the future
// as either a failed experiment or be formally included in "github.com/go-json-experiment/json"
// in some semantically similar form, in which case, users of this option
// must migrate to the officially supported feature.
//
// The `format` tag was originally part of the "github.com/go-json-experiment/json" experiment
// but support for it was removed (see https://go.dev/issue/79071) for the
// initial release of "github.com/go-json-experiment/json" in light of the anticipation that
// the Go language might support typed struct tags (https://go.dev/issue/74472).
//
// Typed struct tags are a more expressive and type-safe way to express format
// attributes than the bespoke `format` tag option that implements a miniature
// domain-specific language (DSL) within the "json" package itself.
//
// While "github.com/go-json-experiment/json" was in the experimental phase,
// some users were already depending on the `format` tag option.
// This experimental option exists to provide a temporary workaround
// until the (hopeful) inclusion of typed struct tags and support for
// formatting directives in "github.com/go-json-experiment/json" using that language mechanism.
//
// This option enables support for the `format` tag option, which specifies
// a format flag used to specialize the formatting of the field value.
// The option is a key-value pair specified as "format:value" where
// the value must be either a literal consisting of letters and numbers
// (e.g., `format:RFC3339`) or a single-quoted string literal
// (e.g., `format:'2006-01-02'`). The interpretation of the format flag
// is determined by the struct field type.
//
// Go types with alternative representations are as follows:
//
// - A Go []byte or [N]byte is usually represented as a JSON string
// containing the binary value encoded using RFC 4648.
// If the format is "base64" or unspecified, then this uses RFC 4648, section 4.
// If the format is "base64url", then this uses RFC 4648, section 5.
// If the format is "base32", then this uses RFC 4648, section 6.
// If the format is "base32hex", then this uses RFC 4648, section 7.
// If the format is "base16" or "hex", then this uses RFC 4648, section 8.
// If the format is "array", then the bytes value is represented as a JSON array
// where each element recursively uses the JSON representation of each byte.
//
// - A Go float is usually represented as a JSON number.
// If the format is "nonfinite", then NaN, +Inf, and -Inf are represented as
// the JSON strings "NaN", "Infinity", and "-Infinity", respectively.
// Without the use of this format, such string values result in a [SemanticError].
//
// - A nil Go map is usually encoded using an empty JSON object.
// If the format is "emitnull", then a nil map is encoded as a JSON null.
// If the format is "emitempty", then a nil map is encoded as an empty JSON object,
// regardless of whether [FormatNilMapAsNull] is specified.
//
// - A nil Go slice is usually encoded using an empty JSON array.
// If the format is "emitnull", then a nil slice is encoded as a JSON null.
// If the format is "emitempty", then a nil slice is encoded as an empty JSON array,
// regardless of whether [FormatNilSliceAsNull] is specified.
//
// - A Go pointer usually uses the JSON representation of the underlying value.
// The format is forwarded to the marshaling and unmarshaling of the underlying type.
//
// - A Go [time.Time] is usually represented as a JSON string containing
// the timestamp formatted in RFC 3339 with nanosecond precision.
// If the format matches one of the format constants declared
// in the time package (e.g., RFC1123), then that format is used.
// If the format is "unix", "unixmilli", "unixmicro", or "unixnano",
// then the timestamp is represented as a possibly fractional JSON number
// of the number of seconds (or milliseconds, microseconds, or nanoseconds)
// since the Unix epoch, which is January 1st, 1970 at 00:00:00 UTC.
// To avoid a fractional component when encoding,
// round the timestamp to the relevant unit.
// Otherwise if non-empty, the format is used as-is and
// encoded using [time.Time.Format] and
// decoded using [time.Time.Parse].
//
// - A Go [time.Duration] usually has no default representation.
// If the format is "sec", "milli", "micro", or "nano",
// then the duration is represented as a possibly fractional JSON number
// of the number of seconds (or milliseconds, microseconds, or nanoseconds).
// To avoid a fractional component when encoding,
// round the duration to the relevant unit.
// If the format is "units", it is represented as a JSON string
// encoded using [time.Duration.String] and decoded using [time.ParseDuration]
// (e.g., "1h30m" for 1 hour 30 minutes).
// If the format is "iso8601", it is represented as a JSON string using the
// ISO 8601 standard for durations (e.g., "PT1H30M" for 1 hour 30 minutes)
// using only accurate units of hours, minutes, and seconds.
func ExperimentalSupportFormatTag(v bool) Options {
if v {
return &enableFormatTag
} else {
return &disableFormatTag
}
}
+13 -36
View File
@@ -39,7 +39,7 @@ func ConsumeWhitespace(b []byte) (n int) {
}
// ConsumeNull consumes the next JSON null literal per RFC 7159, section 3.
// It returns 0 if it is invalid, in which case consumeLiteral should be used.
// It returns 0 if it is invalid, in which case ConsumeLiteral should be used.
func ConsumeNull(b []byte) int {
// NOTE: The arguments and logic are kept simple to keep this inlinable.
const literal = "null"
@@ -50,7 +50,7 @@ func ConsumeNull(b []byte) int {
}
// ConsumeFalse consumes the next JSON false literal per RFC 7159, section 3.
// It returns 0 if it is invalid, in which case consumeLiteral should be used.
// It returns 0 if it is invalid, in which case ConsumeLiteral should be used.
func ConsumeFalse(b []byte) int {
// NOTE: The arguments and logic are kept simple to keep this inlinable.
const literal = "false"
@@ -61,7 +61,7 @@ func ConsumeFalse(b []byte) int {
}
// ConsumeTrue consumes the next JSON true literal per RFC 7159, section 3.
// It returns 0 if it is invalid, in which case consumeLiteral should be used.
// It returns 0 if it is invalid, in which case ConsumeLiteral should be used.
func ConsumeTrue(b []byte) int {
// NOTE: The arguments and logic are kept simple to keep this inlinable.
const literal = "true"
@@ -88,7 +88,7 @@ func ConsumeLiteral(b []byte, lit string) (n int, err error) {
// ConsumeSimpleString consumes the next JSON string per RFC 7159, section 7
// but is limited to the grammar for an ASCII string without escape sequences.
// It returns 0 if it is invalid or more complicated than a simple string,
// in which case consumeString should be called.
// in which case [ConsumeString] should be called.
//
// It rejects '<', '>', and '&' for compatibility reasons since these were
// always escaped in the v1 implementation. Thus, if this function reports
@@ -118,7 +118,7 @@ func ConsumeString(flags *ValueFlags, b []byte, validateUTF8 bool) (n int, err e
return ConsumeStringResumable(flags, b, 0, validateUTF8)
}
// ConsumeStringResumable is identical to consumeString but supports resuming
// ConsumeStringResumable is identical to [ConsumeString] but supports resuming
// from a previous call that returned io.ErrUnexpectedEOF.
func ConsumeStringResumable(flags *ValueFlags, b []byte, resumeOffset int, validateUTF8 bool) (n int, err error) {
// Consume the leading double quote.
@@ -254,7 +254,7 @@ func ConsumeStringResumable(flags *ValueFlags, b []byte, resumeOffset int, valid
// Any invalid UTF-8 within the string will be replaced with utf8.RuneError,
// but the error will be specified as having encountered such an error.
// The input must be an entire JSON string with no surrounding whitespace.
func AppendUnquote[Bytes ~[]byte | ~string](dst []byte, src Bytes) (v []byte, err error) {
func AppendUnquote(dst, src []byte) (v []byte, err error) {
dst = slices.Grow(dst, len(src))
// Consume the leading double quote.
@@ -292,7 +292,7 @@ func AppendUnquote[Bytes ~[]byte | ~string](dst []byte, src Bytes) (v []byte, er
return dst, err
}
switch r, rn := utf8.DecodeRuneInString(string(truncateMaxUTF8(src[n:]))); {
switch r, rn := utf8.DecodeRune(src[n:]); {
// Handle UTF-8 encoded byte sequence.
// Due to specialized handling of ASCII above, we know that
// all normal sequences at this point must be 2 bytes or larger.
@@ -364,7 +364,7 @@ func AppendUnquote[Bytes ~[]byte | ~string](dst []byte, src Bytes) (v []byte, er
// Handle invalid UTF-8.
case r == utf8.RuneError:
dst = append(dst, src[i:n]...)
if !utf8.FullRuneInString(string(truncateMaxUTF8(src[n:]))) {
if !utf8.FullRune(src[n:]) {
return dst, io.ErrUnexpectedEOF
}
// NOTE: An unescaped string may be longer than the escaped string
@@ -387,7 +387,7 @@ func AppendUnquote[Bytes ~[]byte | ~string](dst []byte, src Bytes) (v []byte, er
// hasEscapedUTF16Prefix reports whether b is possibly
// the truncated prefix of a \uFFFF escape sequence.
func hasEscapedUTF16Prefix[Bytes ~[]byte | ~string](b Bytes, lowerSurrogateHalf bool) bool {
func hasEscapedUTF16Prefix(b []byte, lowerSurrogateHalf bool) bool {
for i := range len(b) {
switch c := b[i]; {
case i == 0 && c != '\\':
@@ -420,9 +420,9 @@ func UnquoteMayCopy(b []byte, isVerbatim bool) []byte {
}
// ConsumeSimpleNumber consumes the next JSON number per RFC 7159, section 6
// but is limited to the grammar for a positive integer.
// but is limited to the grammar for a non-negative integer.
// It returns 0 if it is invalid or more complicated than a simple integer,
// in which case consumeNumber should be called.
// in which case ConsumeNumber should be called.
func ConsumeSimpleNumber(b []byte) (n int) {
// NOTE: The arguments and logic are kept simple to keep this inlinable.
if len(b) > 0 {
@@ -467,7 +467,7 @@ func ConsumeNumber(b []byte) (n int, err error) {
return n, err
}
// ConsumeNumberResumable is identical to consumeNumber but supports resuming
// ConsumeNumberResumable is identical to ConsumeNumber but supports resuming
// from a previous call that returned io.ErrUnexpectedEOF.
func ConsumeNumberResumable(b []byte, resumeOffset int, state ConsumeNumberState) (n int, _ ConsumeNumberState, err error) {
// Jump to the right state when resuming from a partial consumption.
@@ -565,7 +565,7 @@ beforeExponent:
// parseHexUint16 is similar to strconv.ParseUint,
// but operates directly on []byte and is optimized for base-16.
// See https://go.dev/issue/42429.
func parseHexUint16[Bytes ~[]byte | ~string](b Bytes) (v uint16, ok bool) {
func parseHexUint16(b []byte) (v uint16, ok bool) {
if len(b) != 4 {
return 0, false
}
@@ -604,26 +604,3 @@ func ParseUint(b []byte) (v uint64, ok bool) {
}
return v, true
}
// ParseFloat parses a floating point number according to the Go float grammar.
// Note that the JSON number grammar is a strict subset.
//
// If the number overflows the finite representation of a float,
// then we return MaxFloat since any finite value will always be infinitely
// more accurate at representing another finite value than an infinite value.
func ParseFloat(b []byte, bits int) (v float64, ok bool) {
fv, err := strconv.ParseFloat(string(b), bits)
if math.IsInf(fv, 0) {
switch {
case bits == 32 && math.IsInf(fv, +1):
fv = +math.MaxFloat32
case bits == 64 && math.IsInf(fv, +1):
fv = +math.MaxFloat64
case bits == 32 && math.IsInf(fv, -1):
fv = -math.MaxFloat32
case bits == 64 && math.IsInf(fv, -1):
fv = -math.MaxFloat64
}
}
return fv, err == nil
}
+7 -7
View File
@@ -32,7 +32,7 @@ var escapeASCII = [...]uint8{
// NeedEscape reports whether src needs escaping of any characters.
// It conservatively assumes EscapeForHTML and EscapeForJS.
// It reports true for inputs with invalid UTF-8.
func NeedEscape[Bytes ~[]byte | ~string](src Bytes) bool {
func NeedEscape(src []byte) bool {
var i int
for uint(len(src)) > uint(i) {
if c := src[i]; c < utf8.RuneSelf {
@@ -41,7 +41,7 @@ func NeedEscape[Bytes ~[]byte | ~string](src Bytes) bool {
}
i++
} else {
r, rn := utf8.DecodeRuneInString(string(truncateMaxUTF8(src[i:])))
r, rn := utf8.DecodeRune(src[i:])
if r == utf8.RuneError || r == '\u2028' || r == '\u2029' {
return true
}
@@ -62,7 +62,7 @@ func NeedEscape[Bytes ~[]byte | ~string](src Bytes) bool {
// invalid bytes are replaced with the Unicode replacement character ('\ufffd').
// If no escape flags are set, then the shortest representable form is used,
// which is also the canonical form for strings (RFC 8785, section 3.2.2.2).
func AppendQuote[Bytes ~[]byte | ~string](dst []byte, src Bytes, flags *jsonflags.Flags) ([]byte, error) {
func AppendQuote(dst, src []byte, flags *jsonflags.Flags) ([]byte, error) {
var i, n int
var hasInvalidUTF8 bool
dst = slices.Grow(dst, len(`"`)+len(src)+len(`"`))
@@ -82,7 +82,7 @@ func AppendQuote[Bytes ~[]byte | ~string](dst []byte, src Bytes, flags *jsonflag
}
} else {
// Handle multi-byte Unicode.
r, rn := utf8.DecodeRuneInString(string(truncateMaxUTF8(src[n:])))
r, rn := utf8.DecodeRune(src[n:])
n += rn
if r != utf8.RuneError && r != '\u2028' && r != '\u2029' {
continue // no escaping possibly needed
@@ -178,7 +178,7 @@ func ReformatString(dst, src []byte, flags *jsonflags.Flags) ([]byte, int, error
}
i++
} else {
r, rn := utf8.DecodeRune(truncateMaxUTF8(src[i:]))
r, rn := utf8.DecodeRune(src[i:])
if (r == '\u2028' || r == '\u2029') && flags.Get(jsonflags.EscapeForJS) {
dst = append(dst, src[lastAppendIndex:i]...)
dst = appendEscapedUnicode(dst, r)
@@ -193,7 +193,7 @@ func ReformatString(dst, src []byte, flags *jsonflags.Flags) ([]byte, int, error
// The input contains characters that might need escaping,
// unnecessary escape sequences, or invalid UTF-8.
// Perform a round-trip unquote and quote to properly reformat
// these sequences according the current flags.
// these sequences according to the current flags.
b, _ := AppendUnquote(nil, src[:n])
dst, _ = AppendQuote(dst, b, flags)
return dst, n, nil
@@ -235,7 +235,7 @@ func AppendFloat(dst []byte, src float64, bits int) []byte {
return dst
}
// ReformatNumber consumes a JSON string from src and appends it to dst,
// ReformatNumber consumes a JSON number from src and appends it to dst,
// canonicalizing it if specified.
// It returns the appended output and the number of consumed input bytes.
func ReformatNumber(dst, src []byte, flags *jsonflags.Flags) ([]byte, int, error) {
+30 -38
View File
@@ -17,7 +17,7 @@ import (
"unicode/utf8"
)
// TrimSuffixWhitespace trims JSON from the end of b.
// 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
@@ -59,8 +59,8 @@ func TrimSuffixByte(b []byte, c byte) []byte {
}
// QuoteRune quotes the first rune in the input.
func QuoteRune[Bytes ~[]byte | ~string](b Bytes) string {
r, n := utf8.DecodeRuneInString(string(truncateMaxUTF8(b)))
func QuoteRune(b []byte) string {
r, n := utf8.DecodeRune(b)
if r == utf8.RuneError && n == 1 {
return `'\x` + strconv.FormatUint(uint64(b[0]), 16) + `'`
}
@@ -70,7 +70,7 @@ func QuoteRune[Bytes ~[]byte | ~string](b Bytes) string {
// 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[Bytes ~[]byte | ~string](x, y Bytes) int {
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.
@@ -93,8 +93,8 @@ func CompareUTF16[Bytes ~[]byte | ~string](x, y Bytes) int {
}
// Decode next pair of runes as UTF-8.
rx, nx := utf8.DecodeRuneInString(string(truncateMaxUTF8(x)))
ry, ny := utf8.DecodeRuneInString(string(truncateMaxUTF8(y)))
rx, nx := utf8.DecodeRune(x)
ry, ny := utf8.DecodeRune(y)
selfx := isUTF16Self(rx)
selfy := isUTF16Self(ry)
@@ -123,49 +123,41 @@ func CompareUTF16[Bytes ~[]byte | ~string](x, y Bytes) int {
}
}
// truncateMaxUTF8 truncates b such it contains at least one rune.
//
// The utf8 package currently lacks generic variants, which complicates
// generic functions that operates on either []byte or string.
// As a hack, we always call the utf8 function operating on strings,
// but always truncate the input such that the result is identical.
//
// Example usage:
//
// utf8.DecodeRuneInString(string(truncateMaxUTF8(b)))
//
// Converting a []byte to a string is stack allocated since
// truncateMaxUTF8 guarantees that the []byte is short.
func truncateMaxUTF8[Bytes ~[]byte | ~string](b Bytes) Bytes {
// TODO(https://go.dev/issue/56948): Remove this function and
// instead directly call generic utf8 functions wherever used.
if len(b) > utf8.UTFMax {
return b[:utf8.UTFMax]
}
return b
}
// 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 {
what := QuoteRune(prefix)
return errors.New("invalid character " + what + " " + where)
_, n := utf8.DecodeRune([]byte(prefix))
return &InvalidTextError{"character", string(prefix[:n]), where}
}
func NewInvalidEscapeSequenceError[Bytes ~[]byte | ~string](what Bytes) error {
label := "escape sequence"
if len(what) > 6 {
label = "surrogate pair"
return &InvalidTextError{"surrogate pair", string(what), "in string"}
}
needEscape := strings.IndexFunc(string(what), func(r rune) bool {
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)
}) >= 0
if needEscape {
return errors.New("invalid " + label + " " + strconv.Quote(string(what)) + " in string")
} else {
return errors.New("invalid " + label + " `" + string(what) + "` in string")
})
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,