blob: ae2ee9ab6956af47a81e387e947aaa3bb7470f53 [file] [log] [blame]
Joe Tsai879b18d2018-08-03 17:22:24 -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 json
6
7import (
8 "bytes"
Herbie Ongd3f8f2d2019-03-06 00:28:23 -08009 "fmt"
Joe Tsai879b18d2018-08-03 17:22:24 -070010 "io"
11 "regexp"
Herbie Ongd3f8f2d2019-03-06 00:28:23 -080012 "strconv"
Joe Tsai879b18d2018-08-03 17:22:24 -070013 "unicode/utf8"
14
Damien Neile89e6242019-05-13 23:55:40 -070015 "google.golang.org/protobuf/internal/errors"
Joe Tsai879b18d2018-08-03 17:22:24 -070016)
17
Herbie Ongc96a79d2019-03-08 10:49:17 -080018// call specifies which Decoder method was invoked.
19type call uint8
20
21const (
22 readCall call = iota
23 peekCall
24)
25
Herbie Ongd3f8f2d2019-03-06 00:28:23 -080026// Decoder is a token-based JSON decoder.
27type Decoder struct {
Herbie Ong8ac9dd22019-03-27 12:20:50 -070028 // lastCall is last method called, either readCall or peekCall.
29 // Initial value is readCall.
Herbie Ongc96a79d2019-03-08 10:49:17 -080030 lastCall call
31
32 // value contains the last read value.
33 value Value
34
35 // err contains the last read error.
36 err error
Joe Tsai879b18d2018-08-03 17:22:24 -070037
Herbie Ongd3f8f2d2019-03-06 00:28:23 -080038 // startStack is a stack containing StartObject and StartArray types. The
39 // top of stack represents the object or the array the current value is
40 // directly located in.
41 startStack []Type
42
43 // orig is used in reporting line and column.
44 orig []byte
45 // in contains the unconsumed input.
46 in []byte
Joe Tsai879b18d2018-08-03 17:22:24 -070047}
48
Herbie Ongd3f8f2d2019-03-06 00:28:23 -080049// NewDecoder returns a Decoder to read the given []byte.
50func NewDecoder(b []byte) *Decoder {
51 return &Decoder{orig: b, in: b}
52}
53
Herbie Ongc96a79d2019-03-08 10:49:17 -080054// Peek looks ahead and returns the next JSON type without advancing a read.
55func (d *Decoder) Peek() Type {
56 defer func() { d.lastCall = peekCall }()
57 if d.lastCall == readCall {
58 d.value, d.err = d.Read()
59 }
60 return d.value.typ
61}
62
63// Read returns the next JSON value. It will return an error if there is no
Herbie Ongdecef412019-04-17 15:47:43 -070064// valid value. For String types containing invalid UTF8 characters, a non-fatal
65// error is returned and caller can call Read for the next value.
Herbie Ongc96a79d2019-03-08 10:49:17 -080066func (d *Decoder) Read() (Value, error) {
67 defer func() { d.lastCall = readCall }()
68 if d.lastCall == peekCall {
69 return d.value, d.err
70 }
71
Herbie Ongdecef412019-04-17 15:47:43 -070072 value, err := d.parseNext()
Damien Neil8c86fc52019-06-19 09:28:29 -070073 if err != nil {
Joe Tsai879b18d2018-08-03 17:22:24 -070074 return Value{}, err
75 }
Herbie Ongdecef412019-04-17 15:47:43 -070076 n := value.size
Herbie Ongd3f8f2d2019-03-06 00:28:23 -080077
78 switch value.typ {
79 case EOF:
80 if len(d.startStack) != 0 ||
Herbie Ongc96a79d2019-03-08 10:49:17 -080081 d.value.typ&Null|Bool|Number|String|EndObject|EndArray == 0 {
Herbie Ongd3f8f2d2019-03-06 00:28:23 -080082 return Value{}, io.ErrUnexpectedEOF
83 }
84
85 case Null:
86 if !d.isValueNext() {
87 return Value{}, d.newSyntaxError("unexpected value null")
88 }
89
90 case Bool, Number:
91 if !d.isValueNext() {
Herbie Ong8ac9dd22019-03-27 12:20:50 -070092 return Value{}, d.newSyntaxError("unexpected value %v", value.Raw())
Herbie Ongd3f8f2d2019-03-06 00:28:23 -080093 }
94
95 case String:
96 if d.isValueNext() {
97 break
98 }
99 // Check if this is for an object name.
Herbie Ongc96a79d2019-03-08 10:49:17 -0800100 if d.value.typ&(StartObject|comma) == 0 {
Herbie Ong8ac9dd22019-03-27 12:20:50 -0700101 return Value{}, d.newSyntaxError("unexpected value %v", value.Raw())
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800102 }
103 d.in = d.in[n:]
104 d.consume(0)
105 if c := d.in[0]; c != ':' {
106 return Value{}, d.newSyntaxError(`unexpected character %v, missing ":" after object name`, string(c))
107 }
108 n = 1
109 value.typ = Name
110
111 case StartObject, StartArray:
112 if !d.isValueNext() {
Herbie Ong8ac9dd22019-03-27 12:20:50 -0700113 return Value{}, d.newSyntaxError("unexpected character %v", value.Raw())
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800114 }
115 d.startStack = append(d.startStack, value.typ)
116
117 case EndObject:
118 if len(d.startStack) == 0 ||
Herbie Ongc96a79d2019-03-08 10:49:17 -0800119 d.value.typ == comma ||
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800120 d.startStack[len(d.startStack)-1] != StartObject {
121 return Value{}, d.newSyntaxError("unexpected character }")
122 }
123 d.startStack = d.startStack[:len(d.startStack)-1]
124
125 case EndArray:
126 if len(d.startStack) == 0 ||
Herbie Ongc96a79d2019-03-08 10:49:17 -0800127 d.value.typ == comma ||
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800128 d.startStack[len(d.startStack)-1] != StartArray {
129 return Value{}, d.newSyntaxError("unexpected character ]")
130 }
131 d.startStack = d.startStack[:len(d.startStack)-1]
132
133 case comma:
134 if len(d.startStack) == 0 ||
Herbie Ongc96a79d2019-03-08 10:49:17 -0800135 d.value.typ&(Null|Bool|Number|String|EndObject|EndArray) == 0 {
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800136 return Value{}, d.newSyntaxError("unexpected character ,")
137 }
Joe Tsai879b18d2018-08-03 17:22:24 -0700138 }
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800139
Herbie Ongdecef412019-04-17 15:47:43 -0700140 // Update d.value only after validating value to be in the right sequence.
141 d.value = value
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800142 d.in = d.in[n:]
143
Herbie Ongc96a79d2019-03-08 10:49:17 -0800144 if d.value.typ == comma {
145 return d.Read()
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800146 }
Damien Neil8c86fc52019-06-19 09:28:29 -0700147 return value, nil
Joe Tsai879b18d2018-08-03 17:22:24 -0700148}
149
Herbie Ongdecef412019-04-17 15:47:43 -0700150// Any sequence that looks like a non-delimiter (for error reporting).
151var errRegexp = regexp.MustCompile(`^([-+._a-zA-Z0-9]{1,32}|.)`)
Joe Tsai879b18d2018-08-03 17:22:24 -0700152
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800153// parseNext parses for the next JSON value. It returns a Value object for
Herbie Ongdecef412019-04-17 15:47:43 -0700154// different types, except for Name. It does not handle whether the next value
155// is in a valid sequence or not.
156func (d *Decoder) parseNext() (value Value, err error) {
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800157 // Trim leading spaces.
158 d.consume(0)
Joe Tsai879b18d2018-08-03 17:22:24 -0700159
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800160 in := d.in
161 if len(in) == 0 {
Herbie Ongdecef412019-04-17 15:47:43 -0700162 return d.newValue(EOF, nil, 0), nil
Joe Tsai879b18d2018-08-03 17:22:24 -0700163 }
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800164
165 switch in[0] {
Herbie Ongdecef412019-04-17 15:47:43 -0700166 case 'n':
167 n := matchWithDelim("null", in)
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800168 if n == 0 {
Herbie Ongdecef412019-04-17 15:47:43 -0700169 return Value{}, d.newSyntaxError("invalid value %s", errRegexp.Find(in))
Joe Tsai879b18d2018-08-03 17:22:24 -0700170 }
Herbie Ongdecef412019-04-17 15:47:43 -0700171 return d.newValue(Null, in, n), nil
172
173 case 't':
174 n := matchWithDelim("true", in)
175 if n == 0 {
176 return Value{}, d.newSyntaxError("invalid value %s", errRegexp.Find(in))
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800177 }
Herbie Ongdecef412019-04-17 15:47:43 -0700178 return d.newBoolValue(in, n, true), nil
179
180 case 'f':
181 n := matchWithDelim("false", in)
182 if n == 0 {
183 return Value{}, d.newSyntaxError("invalid value %s", errRegexp.Find(in))
184 }
185 return d.newBoolValue(in, n, false), nil
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800186
Joe Tsai879b18d2018-08-03 17:22:24 -0700187 case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
Herbie Onga3421952019-03-21 18:12:26 -0700188 n, ok := consumeNumber(in)
189 if !ok {
Herbie Ongdecef412019-04-17 15:47:43 -0700190 return Value{}, d.newSyntaxError("invalid number %s", errRegexp.Find(in))
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800191 }
Herbie Ongdecef412019-04-17 15:47:43 -0700192 return d.newValue(Number, in, n), nil
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800193
Joe Tsai879b18d2018-08-03 17:22:24 -0700194 case '"':
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800195 s, n, err := d.parseString(in)
Damien Neil8c86fc52019-06-19 09:28:29 -0700196 if err != nil {
Herbie Ongdecef412019-04-17 15:47:43 -0700197 return Value{}, err
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800198 }
Damien Neil8c86fc52019-06-19 09:28:29 -0700199 return d.newStringValue(in, n, s), nil
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800200
Joe Tsai879b18d2018-08-03 17:22:24 -0700201 case '{':
Herbie Ongdecef412019-04-17 15:47:43 -0700202 return d.newValue(StartObject, in, 1), nil
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800203
204 case '}':
Herbie Ongdecef412019-04-17 15:47:43 -0700205 return d.newValue(EndObject, in, 1), nil
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800206
207 case '[':
Herbie Ongdecef412019-04-17 15:47:43 -0700208 return d.newValue(StartArray, in, 1), nil
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800209
210 case ']':
Herbie Ongdecef412019-04-17 15:47:43 -0700211 return d.newValue(EndArray, in, 1), nil
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800212
213 case ',':
Herbie Ongdecef412019-04-17 15:47:43 -0700214 return d.newValue(comma, in, 1), nil
Joe Tsai879b18d2018-08-03 17:22:24 -0700215 }
Herbie Ongdecef412019-04-17 15:47:43 -0700216 return Value{}, d.newSyntaxError("invalid value %s", errRegexp.Find(in))
Joe Tsai879b18d2018-08-03 17:22:24 -0700217}
218
Herbie Ongdecef412019-04-17 15:47:43 -0700219// position returns line and column number of index in given orig slice.
220func position(orig []byte, idx int) (int, int) {
221 b := orig[:idx]
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800222 line := bytes.Count(b, []byte("\n")) + 1
223 if i := bytes.LastIndexByte(b, '\n'); i >= 0 {
224 b = b[i+1:]
Joe Tsai879b18d2018-08-03 17:22:24 -0700225 }
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800226 column := utf8.RuneCount(b) + 1 // ignore multi-rune characters
227 return line, column
Joe Tsai879b18d2018-08-03 17:22:24 -0700228}
229
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800230// newSyntaxError returns an error with line and column information useful for
231// syntax errors.
232func (d *Decoder) newSyntaxError(f string, x ...interface{}) error {
233 e := errors.New(f, x...)
Herbie Ongdecef412019-04-17 15:47:43 -0700234 line, column := position(d.orig, len(d.orig)-len(d.in))
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800235 return errors.New("syntax error (line %d:%d): %v", line, column, e)
Joe Tsai879b18d2018-08-03 17:22:24 -0700236}
237
Herbie Ongdecef412019-04-17 15:47:43 -0700238// matchWithDelim matches s with the input b and verifies that the match
Joe Tsai879b18d2018-08-03 17:22:24 -0700239// terminates with a delimiter of some form (e.g., r"[^-+_.a-zA-Z0-9]").
Herbie Ongdecef412019-04-17 15:47:43 -0700240// As a special case, EOF is considered a delimiter. It returns the length of s
241// if there is a match, else 0.
242func matchWithDelim(s string, b []byte) int {
243 if !bytes.HasPrefix(b, []byte(s)) {
244 return 0
245 }
246
247 n := len(s)
248 if n < len(b) && isNotDelim(b[n]) {
249 return 0
Joe Tsai879b18d2018-08-03 17:22:24 -0700250 }
251 return n
252}
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800253
254// isNotDelim returns true if given byte is a not delimiter character.
255func isNotDelim(c byte) bool {
256 return (c == '-' || c == '+' || c == '.' || c == '_' ||
257 ('a' <= c && c <= 'z') ||
258 ('A' <= c && c <= 'Z') ||
259 ('0' <= c && c <= '9'))
260}
261
262// consume consumes n bytes of input and any subsequent whitespace.
263func (d *Decoder) consume(n int) {
264 d.in = d.in[n:]
265 for len(d.in) > 0 {
266 switch d.in[0] {
267 case ' ', '\n', '\r', '\t':
268 d.in = d.in[1:]
269 default:
270 return
271 }
272 }
273}
274
275// isValueNext returns true if next type should be a JSON value: Null,
276// Number, String or Bool.
277func (d *Decoder) isValueNext() bool {
278 if len(d.startStack) == 0 {
Herbie Ongc96a79d2019-03-08 10:49:17 -0800279 return d.value.typ == 0
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800280 }
281
282 start := d.startStack[len(d.startStack)-1]
283 switch start {
284 case StartObject:
Herbie Ongc96a79d2019-03-08 10:49:17 -0800285 return d.value.typ&Name != 0
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800286 case StartArray:
Herbie Ongc96a79d2019-03-08 10:49:17 -0800287 return d.value.typ&(StartArray|comma) != 0
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800288 }
289 panic(fmt.Sprintf(
290 "unreachable logic in Decoder.isValueNext, lastType: %v, startStack: %v",
Herbie Ongc96a79d2019-03-08 10:49:17 -0800291 d.value.typ, start))
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800292}
293
Herbie Ong670d8082019-03-31 19:10:33 -0700294// newValue constructs a Value for given Type.
Herbie Ongdecef412019-04-17 15:47:43 -0700295func (d *Decoder) newValue(typ Type, input []byte, size int) Value {
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800296 return Value{
Herbie Ongdecef412019-04-17 15:47:43 -0700297 typ: typ,
298 input: d.orig,
299 start: len(d.orig) - len(input),
300 size: size,
Herbie Ong670d8082019-03-31 19:10:33 -0700301 }
302}
303
304// newBoolValue constructs a Value for a JSON boolean.
Herbie Ongdecef412019-04-17 15:47:43 -0700305func (d *Decoder) newBoolValue(input []byte, size int, b bool) Value {
Herbie Ong670d8082019-03-31 19:10:33 -0700306 return Value{
Herbie Ongdecef412019-04-17 15:47:43 -0700307 typ: Bool,
308 input: d.orig,
309 start: len(d.orig) - len(input),
310 size: size,
311 boo: b,
Herbie Ong670d8082019-03-31 19:10:33 -0700312 }
313}
314
315// newStringValue constructs a Value for a JSON string.
Herbie Ongdecef412019-04-17 15:47:43 -0700316func (d *Decoder) newStringValue(input []byte, size int, s string) Value {
Herbie Ong670d8082019-03-31 19:10:33 -0700317 return Value{
Herbie Ongdecef412019-04-17 15:47:43 -0700318 typ: String,
319 input: d.orig,
320 start: len(d.orig) - len(input),
321 size: size,
322 str: s,
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800323 }
324}
325
Herbie Ong8ac9dd22019-03-27 12:20:50 -0700326// Clone returns a copy of the Decoder for use in reading ahead the next JSON
327// object, array or other values without affecting current Decoder.
328func (d *Decoder) Clone() *Decoder {
329 ret := *d
330 ret.startStack = append([]Type(nil), ret.startStack...)
331 return &ret
332}
333
Herbie Ongdecef412019-04-17 15:47:43 -0700334// Value provides a parsed JSON type and value.
335//
336// The original input slice is stored in this struct in order to compute for
337// position as needed. The raw JSON value is derived from the original input
338// slice given start and size.
339//
Herbie Ong670d8082019-03-31 19:10:33 -0700340// For JSON boolean and string, it holds the converted value in boo and str
Herbie Ongdecef412019-04-17 15:47:43 -0700341// fields respectively. For JSON number, the raw JSON value holds a valid number
342// which is converted only in Int or Float. Other JSON types do not require any
Herbie Ong670d8082019-03-31 19:10:33 -0700343// additional data.
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800344type Value struct {
Herbie Ongdecef412019-04-17 15:47:43 -0700345 typ Type
346 input []byte
347 start int
348 size int
349 boo bool
350 str string
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800351}
352
353func (v Value) newError(f string, x ...interface{}) error {
354 e := errors.New(f, x...)
Herbie Ongdecef412019-04-17 15:47:43 -0700355 line, col := v.Position()
356 return errors.New("error (line %d:%d): %v", line, col, e)
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800357}
358
359// Type returns the JSON type.
360func (v Value) Type() Type {
361 return v.typ
362}
363
364// Position returns the line and column of the value.
365func (v Value) Position() (int, int) {
Herbie Ongdecef412019-04-17 15:47:43 -0700366 return position(v.input, v.start)
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800367}
368
369// Bool returns the bool value if token is Bool, else it will return an error.
370func (v Value) Bool() (bool, error) {
371 if v.typ != Bool {
Herbie Ongdecef412019-04-17 15:47:43 -0700372 return false, v.newError("%s is not a bool", v.Raw())
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800373 }
Herbie Ong670d8082019-03-31 19:10:33 -0700374 return v.boo, nil
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800375}
376
377// String returns the string value for a JSON string token or the read value in
378// string if token is not a string.
379func (v Value) String() string {
380 if v.typ != String {
Herbie Ongdecef412019-04-17 15:47:43 -0700381 return v.Raw()
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800382 }
Herbie Ong670d8082019-03-31 19:10:33 -0700383 return v.str
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800384}
385
386// Name returns the object name if token is Name, else it will return an error.
387func (v Value) Name() (string, error) {
388 if v.typ != Name {
Herbie Ongdecef412019-04-17 15:47:43 -0700389 return "", v.newError("%s is not an object name", v.Raw())
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800390 }
Herbie Ong670d8082019-03-31 19:10:33 -0700391 return v.str, nil
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800392}
393
Herbie Ong8ac9dd22019-03-27 12:20:50 -0700394// Raw returns the read value in string.
395func (v Value) Raw() string {
Herbie Ongdecef412019-04-17 15:47:43 -0700396 return string(v.input[v.start : v.start+v.size])
Herbie Ong8ac9dd22019-03-27 12:20:50 -0700397}
398
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800399// Float returns the floating-point number if token is Number, else it will
400// return an error.
401//
402// The floating-point precision is specified by the bitSize parameter: 32 for
403// float32 or 64 for float64. If bitSize=32, the result still has type float64,
404// but it will be convertible to float32 without changing its value. It will
405// return an error if the number exceeds the floating point limits for given
406// bitSize.
407func (v Value) Float(bitSize int) (float64, error) {
408 if v.typ != Number {
Herbie Ongdecef412019-04-17 15:47:43 -0700409 return 0, v.newError("%s is not a number", v.Raw())
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800410 }
Herbie Ongdecef412019-04-17 15:47:43 -0700411 f, err := strconv.ParseFloat(v.Raw(), bitSize)
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800412 if err != nil {
413 return 0, v.newError("%v", err)
414 }
415 return f, nil
416}
417
418// Int returns the signed integer number if token is Number, else it will
419// return an error.
420//
421// The given bitSize specifies the integer type that the result must fit into.
422// It returns an error if the number is not an integer value or if the result
423// exceeds the limits for given bitSize.
424func (v Value) Int(bitSize int) (int64, error) {
425 s, err := v.getIntStr()
426 if err != nil {
427 return 0, err
428 }
429 n, err := strconv.ParseInt(s, 10, bitSize)
430 if err != nil {
431 return 0, v.newError("%v", err)
432 }
433 return n, nil
434}
435
436// Uint returns the signed integer number if token is Number, else it will
437// return an error.
438//
439// The given bitSize specifies the unsigned integer type that the result must
440// fit into. It returns an error if the number is not an unsigned integer value
441// or if the result exceeds the limits for given bitSize.
442func (v Value) Uint(bitSize int) (uint64, error) {
443 s, err := v.getIntStr()
444 if err != nil {
445 return 0, err
446 }
447 n, err := strconv.ParseUint(s, 10, bitSize)
448 if err != nil {
449 return 0, v.newError("%v", err)
450 }
451 return n, nil
452}
453
454func (v Value) getIntStr() (string, error) {
455 if v.typ != Number {
456 return "", v.newError("%s is not a number", v.input)
457 }
Herbie Ongdecef412019-04-17 15:47:43 -0700458 parts, ok := parseNumber(v.input[v.start : v.start+v.size])
Herbie Onga3421952019-03-21 18:12:26 -0700459 if !ok {
460 return "", v.newError("%s is not a number", v.input)
461 }
462 num, ok := normalizeToIntString(parts)
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800463 if !ok {
464 return "", v.newError("cannot convert %s to integer", v.input)
465 }
466 return num, nil
467}