blob: 8f146ae9374a0d1d5442e238e7028cd1f9402938 [file] [log] [blame]
Joe Tsai27c2a762018-08-01 16:48:18 -07001// 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 text
6
7import (
8 "bytes"
Herbie Onga3369c52019-04-23 00:24:46 -07009 "regexp"
Joe Tsai27c2a762018-08-01 16:48:18 -070010 "strings"
11
Damien Neile89e6242019-05-13 23:55:40 -070012 "google.golang.org/protobuf/internal/detrand"
13 "google.golang.org/protobuf/internal/errors"
Joe Tsai27c2a762018-08-01 16:48:18 -070014)
15
16// Marshal serializes v as the proto text format, where v must be a Message.
17// In the proto text format, the top-level value is always a message where the
18// delimiters are elided.
19//
20// If indent is a non-empty string, it causes every entry in a List or Message
21// to be preceded by the indent and trailed by a newline.
22//
23// If delims is not the zero value, it controls the delimiter characters used
24// for messages (e.g., "{}" vs "<>").
25//
26// If outputASCII is true, strings will be serialized in such a way that
27// multi-byte UTF-8 sequences are escaped. This property ensures that the
28// overall output is ASCII (as opposed to UTF-8).
29func Marshal(v Value, indent string, delims [2]byte, outputASCII bool) ([]byte, error) {
30 p := encoder{}
31 if len(indent) > 0 {
32 if strings.Trim(indent, " \t") != "" {
33 return nil, errors.New("indent may only be composed of space and tab characters")
34 }
35 p.indent = indent
36 p.newline = "\n"
37 }
38 switch delims {
39 case [2]byte{0, 0}:
40 p.delims = [2]byte{'{', '}'}
41 case [2]byte{'{', '}'}, [2]byte{'<', '>'}:
42 p.delims = delims
43 default:
44 return nil, errors.New("delimiters may only be \"{}\" or \"<>\"")
45 }
46 p.outputASCII = outputASCII
47
48 err := p.marshalMessage(v, false)
Damien Neil8c86fc52019-06-19 09:28:29 -070049 if err != nil {
Joe Tsai27c2a762018-08-01 16:48:18 -070050 return nil, err
51 }
52 if len(indent) > 0 {
Damien Neil8c86fc52019-06-19 09:28:29 -070053 return append(bytes.TrimRight(p.out, "\n"), '\n'), nil
Joe Tsai27c2a762018-08-01 16:48:18 -070054 }
Damien Neil8c86fc52019-06-19 09:28:29 -070055 return p.out, nil
Joe Tsai27c2a762018-08-01 16:48:18 -070056}
57
58type encoder struct {
Damien Neil8c86fc52019-06-19 09:28:29 -070059 out []byte
Joe Tsai27c2a762018-08-01 16:48:18 -070060
61 indent string
62 indents []byte
63 newline string // set to "\n" if len(indent) > 0
64 delims [2]byte
65 outputASCII bool
66}
67
68func (p *encoder) marshalList(v Value) error {
69 if v.Type() != List {
70 return errors.New("invalid type %v, expected list", v.Type())
71 }
72 elems := v.List()
73 p.out = append(p.out, '[')
74 p.indents = append(p.indents, p.indent...)
75 if len(elems) > 0 {
76 p.out = append(p.out, p.newline...)
77 }
78 for i, elem := range elems {
79 p.out = append(p.out, p.indents...)
Damien Neil8c86fc52019-06-19 09:28:29 -070080 if err := p.marshalValue(elem); err != nil {
Joe Tsai27c2a762018-08-01 16:48:18 -070081 return err
82 }
83 if i < len(elems)-1 {
84 p.out = append(p.out, ',')
85 }
86 p.out = append(p.out, p.newline...)
87 }
88 p.indents = p.indents[:len(p.indents)-len(p.indent)]
89 if len(elems) > 0 {
90 p.out = append(p.out, p.indents...)
91 }
92 p.out = append(p.out, ']')
93 return nil
94}
95
96func (p *encoder) marshalMessage(v Value, emitDelims bool) error {
97 if v.Type() != Message {
98 return errors.New("invalid type %v, expected message", v.Type())
99 }
100 items := v.Message()
101 if emitDelims {
102 p.out = append(p.out, p.delims[0])
103 p.indents = append(p.indents, p.indent...)
104 if len(items) > 0 {
105 p.out = append(p.out, p.newline...)
106 }
107 }
108 for i, item := range items {
109 p.out = append(p.out, p.indents...)
Damien Neil8c86fc52019-06-19 09:28:29 -0700110 if err := p.marshalKey(item[0]); err != nil {
Joe Tsai27c2a762018-08-01 16:48:18 -0700111 return err
112 }
113 p.out = append(p.out, ':')
114 if len(p.indent) > 0 {
115 p.out = append(p.out, ' ')
Herbie Ong4eb4d612019-09-06 17:15:54 -0700116 // For multi-line output, add a random extra space after key:
117 // to make output unstable.
118 if detrand.Bool() {
119 p.out = append(p.out, ' ')
120 }
Herbie Ongc3f4d482018-11-22 14:29:07 -0800121 }
122
Damien Neil8c86fc52019-06-19 09:28:29 -0700123 if err := p.marshalValue(item[1]); err != nil {
Joe Tsai27c2a762018-08-01 16:48:18 -0700124 return err
125 }
126 if i < len(items)-1 && len(p.indent) == 0 {
127 p.out = append(p.out, ' ')
Herbie Ong4eb4d612019-09-06 17:15:54 -0700128 // For single-line output, add a random extra space after a field
129 // to make output unstable.
130 if detrand.Bool() {
131 p.out = append(p.out, ' ')
132 }
Herbie Ongc3f4d482018-11-22 14:29:07 -0800133 }
Joe Tsai27c2a762018-08-01 16:48:18 -0700134 p.out = append(p.out, p.newline...)
135 }
136 if emitDelims {
137 p.indents = p.indents[:len(p.indents)-len(p.indent)]
138 if len(items) > 0 {
139 p.out = append(p.out, p.indents...)
140 }
141 p.out = append(p.out, p.delims[1])
142 }
143 return nil
144}
145
Herbie Onga3369c52019-04-23 00:24:46 -0700146// This expression is more liberal than ConsumeAnyTypeUrl in C++.
147// However, the C++ parser does not handle many legal URL strings.
148// The Go implementation is more liberal to be backwards compatible with
149// the historical Go implementation which was overly liberal (and buggy).
150var urlRegexp = regexp.MustCompile(`^[-_a-zA-Z0-9]+([./][-_a-zA-Z0-9]+)*`)
151
Joe Tsai27c2a762018-08-01 16:48:18 -0700152func (p *encoder) marshalKey(v Value) error {
153 switch v.Type() {
154 case String:
155 var err error
156 p.out = append(p.out, '[')
157 if len(urlRegexp.FindString(v.str)) == len(v.str) {
158 p.out = append(p.out, v.str...)
159 } else {
160 err = p.marshalString(v)
161 }
162 p.out = append(p.out, ']')
163 return err
164 case Uint:
165 return p.marshalNumber(v)
166 case Name:
167 s, _ := v.Name()
168 p.out = append(p.out, s...)
169 return nil
170 default:
171 return errors.New("invalid type %v to encode key", v.Type())
172 }
173}
174
175func (p *encoder) marshalValue(v Value) error {
176 switch v.Type() {
Herbie Ong84f09602019-01-17 19:31:47 -0800177 case Bool, Int, Uint, Float32, Float64:
Joe Tsai27c2a762018-08-01 16:48:18 -0700178 return p.marshalNumber(v)
179 case String:
180 return p.marshalString(v)
181 case List:
182 return p.marshalList(v)
183 case Message:
184 return p.marshalMessage(v, true)
185 case Name:
186 s, _ := v.Name()
187 p.out = append(p.out, s...)
188 return nil
189 default:
190 return errors.New("invalid type %v to encode value", v.Type())
191 }
192}