1// Copyright 2018 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package json
6
7import (
8 "math"
9 "math/bits"
10 "strconv"
11 "strings"
12 "unicode/utf8"
13
14 "google.golang.org/protobuf/internal/detrand"
15 "google.golang.org/protobuf/internal/errors"
16)
17
18// kind represents an encoding type.
19type kind uint8
20
21const (
22 _ kind = (1 << iota) / 2
23 name
24 scalar
25 objectOpen
26 objectClose
27 arrayOpen
28 arrayClose
29)
30
31// Encoder provides methods to write out JSON constructs and values. The user is
32// responsible for producing valid sequences of JSON constructs and values.
33type Encoder struct {
34 indent string
35 lastKind kind
36 indents []byte
37 out []byte
38}
39
40// NewEncoder returns an Encoder.
41//
42// If indent is a non-empty string, it causes every entry for an Array or Object
43// to be preceded by the indent and trailed by a newline.
44func NewEncoder(buf []byte, indent string) (*Encoder, error) {
45 e := &Encoder{
46 out: buf,
47 }
48 if len(indent) > 0 {
49 if strings.Trim(indent, " \t") != "" {
50 return nil, errors.New("indent may only be composed of space or tab characters")
51 }
52 e.indent = indent
53 }
54 return e, nil
55}
56
57// Bytes returns the content of the written bytes.
58func (e *Encoder) Bytes() []byte {
59 return e.out
60}
61
62// WriteNull writes out the null value.
63func (e *Encoder) WriteNull() {
64 e.prepareNext(scalar)
65 e.out = append(e.out, "null"...)
66}
67
68// WriteBool writes out the given boolean value.
69func (e *Encoder) WriteBool(b bool) {
70 e.prepareNext(scalar)
71 if b {
72 e.out = append(e.out, "true"...)
73 } else {
74 e.out = append(e.out, "false"...)
75 }
76}
77
78// WriteString writes out the given string in JSON string value. Returns error
79// if input string contains invalid UTF-8.
80func (e *Encoder) WriteString(s string) error {
81 e.prepareNext(scalar)
82 var err error
83 if e.out, err = appendString(e.out, s); err != nil {
84 return err
85 }
86 return nil
87}
88
89// Sentinel error used for indicating invalid UTF-8.
90var errInvalidUTF8 = errors.New("invalid UTF-8")
91
92func appendString(out []byte, in string) ([]byte, error) {
93 out = append(out, '"')
94 i := indexNeedEscapeInString(in)
95 in, out = in[i:], append(out, in[:i]...)
96 for len(in) > 0 {
97 switch r, n := utf8.DecodeRuneInString(in); {
98 case r == utf8.RuneError && n == 1:
99 return out, errInvalidUTF8
100 case r < ' ' || r == '"' || r == '\\':
101 out = append(out, '\\')
102 switch r {
103 case '"', '\\':
104 out = append(out, byte(r))
105 case '\b':
106 out = append(out, 'b')
107 case '\f':
108 out = append(out, 'f')
109 case '\n':
110 out = append(out, 'n')
111 case '\r':
112 out = append(out, 'r')
113 case '\t':
114 out = append(out, 't')
115 default:
116 out = append(out, 'u')
117 out = append(out, "0000"[1+(bits.Len32(uint32(r))-1)/4:]...)
118 out = strconv.AppendUint(out, uint64(r), 16)
119 }
120 in = in[n:]
121 default:
122 i := indexNeedEscapeInString(in[n:])
123 in, out = in[n+i:], append(out, in[:n+i]...)
124 }
125 }
126 out = append(out, '"')
127 return out, nil
128}
129
130// indexNeedEscapeInString returns the index of the character that needs
131// escaping. If no characters need escaping, this returns the input length.
132func indexNeedEscapeInString(s string) int {
133 for i, r := range s {
134 if r < ' ' || r == '\\' || r == '"' || r == utf8.RuneError {
135 return i
136 }
137 }
138 return len(s)
139}
140
141// WriteFloat writes out the given float and bitSize in JSON number value.
142func (e *Encoder) WriteFloat(n float64, bitSize int) {
143 e.prepareNext(scalar)
144 e.out = appendFloat(e.out, n, bitSize)
145}
146
147// appendFloat formats given float in bitSize, and appends to the given []byte.
148func appendFloat(out []byte, n float64, bitSize int) []byte {
149 switch {
150 case math.IsNaN(n):
151 return append(out, `"NaN"`...)
152 case math.IsInf(n, +1):
153 return append(out, `"Infinity"`...)
154 case math.IsInf(n, -1):
155 return append(out, `"-Infinity"`...)
156 }
157
158 // JSON number formatting logic based on encoding/json.
159 // See floatEncoder.encode for reference.
160 fmt := byte('f')
161 if abs := math.Abs(n); abs != 0 {
162 if bitSize == 64 && (abs < 1e-6 || abs >= 1e21) ||
163 bitSize == 32 && (float32(abs) < 1e-6 || float32(abs) >= 1e21) {
164 fmt = 'e'
165 }
166 }
167 out = strconv.AppendFloat(out, n, fmt, -1, bitSize)
168 if fmt == 'e' {
169 n := len(out)
170 if n >= 4 && out[n-4] == 'e' && out[n-3] == '-' && out[n-2] == '0' {
171 out[n-2] = out[n-1]
172 out = out[:n-1]
173 }
174 }
175 return out
176}
177
178// WriteInt writes out the given signed integer in JSON number value.
179func (e *Encoder) WriteInt(n int64) {
180 e.prepareNext(scalar)
181 e.out = strconv.AppendInt(e.out, n, 10)
182}
183
184// WriteUint writes out the given unsigned integer in JSON number value.
185func (e *Encoder) WriteUint(n uint64) {
186 e.prepareNext(scalar)
187 e.out = strconv.AppendUint(e.out, n, 10)
188}
189
190// StartObject writes out the '{' symbol.
191func (e *Encoder) StartObject() {
192 e.prepareNext(objectOpen)
193 e.out = append(e.out, '{')
194}
195
196// EndObject writes out the '}' symbol.
197func (e *Encoder) EndObject() {
198 e.prepareNext(objectClose)
199 e.out = append(e.out, '}')
200}
201
202// WriteName writes out the given string in JSON string value and the name
203// separator ':'. Returns error if input string contains invalid UTF-8, which
204// should not be likely as protobuf field names should be valid.
205func (e *Encoder) WriteName(s string) error {
206 e.prepareNext(name)
207 var err error
208 // Append to output regardless of error.
209 e.out, err = appendString(e.out, s)
210 e.out = append(e.out, ':')
211 return err
212}
213
214// StartArray writes out the '[' symbol.
215func (e *Encoder) StartArray() {
216 e.prepareNext(arrayOpen)
217 e.out = append(e.out, '[')
218}
219
220// EndArray writes out the ']' symbol.
221func (e *Encoder) EndArray() {
222 e.prepareNext(arrayClose)
223 e.out = append(e.out, ']')
224}
225
226// prepareNext adds possible comma and indentation for the next value based
227// on last type and indent option. It also updates lastKind to next.
228func (e *Encoder) prepareNext(next kind) {
229 defer func() {
230 // Set lastKind to next.
231 e.lastKind = next
232 }()
233
234 if len(e.indent) == 0 {
235 // Need to add comma on the following condition.
236 if e.lastKind&(scalar|objectClose|arrayClose) != 0 &&
237 next&(name|scalar|objectOpen|arrayOpen) != 0 {
238 e.out = append(e.out, ',')
239 // For single-line output, add a random extra space after each
240 // comma to make output unstable.
241 if detrand.Bool() {
242 e.out = append(e.out, ' ')
243 }
244 }
245 return
246 }
247
248 switch {
249 case e.lastKind&(objectOpen|arrayOpen) != 0:
250 // If next type is NOT closing, add indent and newline.
251 if next&(objectClose|arrayClose) == 0 {
252 e.indents = append(e.indents, e.indent...)
253 e.out = append(e.out, '\n')
254 e.out = append(e.out, e.indents...)
255 }
256
257 case e.lastKind&(scalar|objectClose|arrayClose) != 0:
258 switch {
259 // If next type is either a value or name, add comma and newline.
260 case next&(name|scalar|objectOpen|arrayOpen) != 0:
261 e.out = append(e.out, ',', '\n')
262
263 // If next type is a closing object or array, adjust indentation.
264 case next&(objectClose|arrayClose) != 0:
265 e.indents = e.indents[:len(e.indents)-len(e.indent)]
266 e.out = append(e.out, '\n')
267 }
268 e.out = append(e.out, e.indents...)
269
270 case e.lastKind&name != 0:
271 e.out = append(e.out, ' ')
272 // For multi-line output, add a random extra space after key: to make
273 // output unstable.
274 if detrand.Bool() {
275 e.out = append(e.out, ' ')
276 }
277 }
278}