mirror of https://github.com/docker/cli.git
277 lines
7.1 KiB
Go
277 lines
7.1 KiB
Go
|
// 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, ' ')
|
||
|
}
|
||
|
}
|
||
|
}
|