| // Copyright 2018 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. |
| |
| package json |
| |
| import ( |
| "math" |
| "math/bits" |
| "strconv" |
| "strings" |
| "unicode/utf8" |
| |
| "google.golang.org/protobuf/internal/detrand" |
| "google.golang.org/protobuf/internal/errors" |
| ) |
| |
| // kind represents an encoding type. |
| type kind uint8 |
| |
| const ( |
| _ kind = (1 << iota) / 2 |
| name |
| scalar |
| objectOpen |
| objectClose |
| arrayOpen |
| arrayClose |
| ) |
| |
| // Encoder provides methods to write out JSON constructs and values. The user is |
| // responsible for producing valid sequences of JSON constructs and values. |
| type Encoder struct { |
| indent string |
| lastKind kind |
| indents []byte |
| out []byte |
| } |
| |
| // NewEncoder returns an Encoder. |
| // |
| // If indent is a non-empty string, it causes every entry for an Array or Object |
| // to be preceded by the indent and trailed by a newline. |
| func NewEncoder(indent string) (*Encoder, error) { |
| e := &Encoder{} |
| if len(indent) > 0 { |
| if strings.Trim(indent, " \t") != "" { |
| return nil, errors.New("indent may only be composed of space or tab characters") |
| } |
| e.indent = indent |
| } |
| return e, nil |
| } |
| |
| // Bytes returns the content of the written bytes. |
| func (e *Encoder) Bytes() []byte { |
| return e.out |
| } |
| |
| // WriteNull writes out the null value. |
| func (e *Encoder) WriteNull() { |
| e.prepareNext(scalar) |
| e.out = append(e.out, "null"...) |
| } |
| |
| // WriteBool writes out the given boolean value. |
| func (e *Encoder) WriteBool(b bool) { |
| e.prepareNext(scalar) |
| if b { |
| e.out = append(e.out, "true"...) |
| } else { |
| e.out = append(e.out, "false"...) |
| } |
| } |
| |
| // WriteString writes out the given string in JSON string value. Returns error |
| // if input string contains invalid UTF-8. |
| func (e *Encoder) WriteString(s string) error { |
| e.prepareNext(scalar) |
| var err error |
| if e.out, err = appendString(e.out, s); err != nil { |
| return err |
| } |
| return nil |
| } |
| |
| // Sentinel error used for indicating invalid UTF-8. |
| var errInvalidUTF8 = errors.New("invalid UTF-8") |
| |
| func appendString(out []byte, in string) ([]byte, error) { |
| out = append(out, '"') |
| i := indexNeedEscapeInString(in) |
| in, out = in[i:], append(out, in[:i]...) |
| for len(in) > 0 { |
| switch r, n := utf8.DecodeRuneInString(in); { |
| case r == utf8.RuneError && n == 1: |
| return out, errInvalidUTF8 |
| case r < ' ' || r == '"' || r == '\\': |
| out = append(out, '\\') |
| switch r { |
| case '"', '\\': |
| out = append(out, byte(r)) |
| case '\b': |
| out = append(out, 'b') |
| case '\f': |
| out = append(out, 'f') |
| case '\n': |
| out = append(out, 'n') |
| case '\r': |
| out = append(out, 'r') |
| case '\t': |
| out = append(out, 't') |
| default: |
| out = append(out, 'u') |
| out = append(out, "0000"[1+(bits.Len32(uint32(r))-1)/4:]...) |
| out = strconv.AppendUint(out, uint64(r), 16) |
| } |
| in = in[n:] |
| default: |
| i := indexNeedEscapeInString(in[n:]) |
| in, out = in[n+i:], append(out, in[:n+i]...) |
| } |
| } |
| out = append(out, '"') |
| return out, nil |
| } |
| |
| // indexNeedEscapeInString returns the index of the character that needs |
| // escaping. If no characters need escaping, this returns the input length. |
| func indexNeedEscapeInString(s string) int { |
| for i, r := range s { |
| if r < ' ' || r == '\\' || r == '"' || r == utf8.RuneError { |
| return i |
| } |
| } |
| return len(s) |
| } |
| |
| // WriteFloat writes out the given float and bitSize in JSON number value. |
| func (e *Encoder) WriteFloat(n float64, bitSize int) { |
| e.prepareNext(scalar) |
| e.out = appendFloat(e.out, n, bitSize) |
| } |
| |
| // appendFloat formats given float in bitSize, and appends to the given []byte. |
| func appendFloat(out []byte, n float64, bitSize int) []byte { |
| switch { |
| case math.IsNaN(n): |
| return append(out, `"NaN"`...) |
| case math.IsInf(n, +1): |
| return append(out, `"Infinity"`...) |
| case math.IsInf(n, -1): |
| return append(out, `"-Infinity"`...) |
| } |
| |
| // JSON number formatting logic based on encoding/json. |
| // See floatEncoder.encode for reference. |
| fmt := byte('f') |
| if abs := math.Abs(n); abs != 0 { |
| if bitSize == 64 && (abs < 1e-6 || abs >= 1e21) || |
| bitSize == 32 && (float32(abs) < 1e-6 || float32(abs) >= 1e21) { |
| fmt = 'e' |
| } |
| } |
| out = strconv.AppendFloat(out, n, fmt, -1, bitSize) |
| if fmt == 'e' { |
| n := len(out) |
| if n >= 4 && out[n-4] == 'e' && out[n-3] == '-' && out[n-2] == '0' { |
| out[n-2] = out[n-1] |
| out = out[:n-1] |
| } |
| } |
| return out |
| } |
| |
| // WriteInt writes out the given signed integer in JSON number value. |
| func (e *Encoder) WriteInt(n int64) { |
| e.prepareNext(scalar) |
| e.out = append(e.out, strconv.FormatInt(n, 10)...) |
| } |
| |
| // WriteUint writes out the given unsigned integer in JSON number value. |
| func (e *Encoder) WriteUint(n uint64) { |
| e.prepareNext(scalar) |
| e.out = append(e.out, strconv.FormatUint(n, 10)...) |
| } |
| |
| // StartObject writes out the '{' symbol. |
| func (e *Encoder) StartObject() { |
| e.prepareNext(objectOpen) |
| e.out = append(e.out, '{') |
| } |
| |
| // EndObject writes out the '}' symbol. |
| func (e *Encoder) EndObject() { |
| e.prepareNext(objectClose) |
| e.out = append(e.out, '}') |
| } |
| |
| // WriteName writes out the given string in JSON string value and the name |
| // separator ':'. Returns error if input string contains invalid UTF-8, which |
| // should not be likely as protobuf field names should be valid. |
| func (e *Encoder) WriteName(s string) error { |
| e.prepareNext(name) |
| var err error |
| // Append to output regardless of error. |
| e.out, err = appendString(e.out, s) |
| e.out = append(e.out, ':') |
| return err |
| } |
| |
| // StartArray writes out the '[' symbol. |
| func (e *Encoder) StartArray() { |
| e.prepareNext(arrayOpen) |
| e.out = append(e.out, '[') |
| } |
| |
| // EndArray writes out the ']' symbol. |
| func (e *Encoder) EndArray() { |
| e.prepareNext(arrayClose) |
| e.out = append(e.out, ']') |
| } |
| |
| // prepareNext adds possible comma and indentation for the next value based |
| // on last type and indent option. It also updates lastKind to next. |
| func (e *Encoder) prepareNext(next kind) { |
| defer func() { |
| // Set lastKind to next. |
| e.lastKind = next |
| }() |
| |
| if len(e.indent) == 0 { |
| // Need to add comma on the following condition. |
| if e.lastKind&(scalar|objectClose|arrayClose) != 0 && |
| next&(name|scalar|objectOpen|arrayOpen) != 0 { |
| e.out = append(e.out, ',') |
| // For single-line output, add a random extra space after each |
| // comma to make output unstable. |
| if detrand.Bool() { |
| e.out = append(e.out, ' ') |
| } |
| } |
| return |
| } |
| |
| switch { |
| case e.lastKind&(objectOpen|arrayOpen) != 0: |
| // If next type is NOT closing, add indent and newline. |
| if next&(objectClose|arrayClose) == 0 { |
| e.indents = append(e.indents, e.indent...) |
| e.out = append(e.out, '\n') |
| e.out = append(e.out, e.indents...) |
| } |
| |
| case e.lastKind&(scalar|objectClose|arrayClose) != 0: |
| switch { |
| // If next type is either a value or name, add comma and newline. |
| case next&(name|scalar|objectOpen|arrayOpen) != 0: |
| e.out = append(e.out, ',', '\n') |
| |
| // If next type is a closing object or array, adjust indentation. |
| case next&(objectClose|arrayClose) != 0: |
| e.indents = e.indents[:len(e.indents)-len(e.indent)] |
| e.out = append(e.out, '\n') |
| } |
| e.out = append(e.out, e.indents...) |
| |
| case e.lastKind&name != 0: |
| e.out = append(e.out, ' ') |
| // For multi-line output, add a random extra space after key: to make |
| // output unstable. |
| if detrand.Bool() { |
| e.out = append(e.out, ' ') |
| } |
| } |
| } |