encoding/jsonpb: improve and fix unmarshaling of Duration
Change use of regular expression to manually parsing the value.
Allow value with + symbol in front, e.g. "+3s". Previous regex missed
this.
Do not allow values without numbers, e.g. "-s". Previous regex missed
this as well.
name old time/op new time/op delta
Unmarshal_Duration-4 1.96µs ± 0% 1.24µs ± 0% ~ (p=1.000 n=1+1)
name old alloc/op new alloc/op delta
Unmarshal_Duration-4 703B ± 0% 512B ± 0% ~ (p=1.000 n=1+1)
name old allocs/op new allocs/op delta
Unmarshal_Duration-4 20.0 ± 0% 17.0 ± 0% ~ (p=1.000 n=1+1)
Change-Id: I4db58d70f55607213631c49d698ee6a048b5e094
Reviewed-on: https://go-review.googlesource.com/c/protobuf/+/170012
Reviewed-by: Joe Tsai <thebrokentoaster@gmail.com>
diff --git a/encoding/jsonpb/bench_test.go b/encoding/jsonpb/bench_test.go
new file mode 100644
index 0000000..eba4119
--- /dev/null
+++ b/encoding/jsonpb/bench_test.go
@@ -0,0 +1,23 @@
+// Copyright 2019 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 jsonpb_test
+
+import (
+ "testing"
+
+ "github.com/golang/protobuf/v2/encoding/jsonpb"
+ knownpb "github.com/golang/protobuf/v2/types/known"
+)
+
+func BenchmarkUnmarshal_Duration(b *testing.B) {
+ input := []byte(`"-123456789.123456789s"`)
+
+ for i := 0; i < b.N; i++ {
+ err := jsonpb.Unmarshal(&knownpb.Duration{}, input)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
diff --git a/encoding/jsonpb/decode_test.go b/encoding/jsonpb/decode_test.go
index e7f4239..587b2b4 100644
--- a/encoding/jsonpb/decode_test.go
+++ b/encoding/jsonpb/decode_test.go
@@ -1757,6 +1757,11 @@
inputText: `"-3s"`,
wantMessage: &knownpb.Duration{Seconds: -3},
}, {
+ desc: "Duration with plus sign",
+ inputMessage: &knownpb.Duration{},
+ inputText: `"+3s"`,
+ wantMessage: &knownpb.Duration{Seconds: 3},
+ }, {
desc: "Duration with nanos",
inputMessage: &knownpb.Duration{},
inputText: `"0.001s"`,
@@ -1767,6 +1772,16 @@
inputText: `"-0.001s"`,
wantMessage: &knownpb.Duration{Nanos: -1e6},
}, {
+ desc: "Duration with -nanos",
+ inputMessage: &knownpb.Duration{},
+ inputText: `"-.001s"`,
+ wantMessage: &knownpb.Duration{Nanos: -1e6},
+ }, {
+ desc: "Duration with +nanos",
+ inputMessage: &knownpb.Duration{},
+ inputText: `"+.001s"`,
+ wantMessage: &knownpb.Duration{Nanos: 1e6},
+ }, {
desc: "Duration with -secs -nanos",
inputMessage: &knownpb.Duration{},
inputText: `"-123.000000450s"`,
@@ -1799,7 +1814,7 @@
}, {
desc: "Duration with nanos beyond 9 digits",
inputMessage: &knownpb.Duration{},
- inputText: `"0.9999999990s"`,
+ inputText: `"0.1000000000s"`,
wantErr: true,
}, {
desc: "Duration without suffix s",
@@ -1807,6 +1822,21 @@
inputText: `"123"`,
wantErr: true,
}, {
+ desc: "Duration invalid signed fraction",
+ inputMessage: &knownpb.Duration{},
+ inputText: `"123.+123s"`,
+ wantErr: true,
+ }, {
+ desc: "Duration invalid multiple .",
+ inputMessage: &knownpb.Duration{},
+ inputText: `"123.123.s"`,
+ wantErr: true,
+ }, {
+ desc: "Duration invalid integer",
+ inputMessage: &knownpb.Duration{},
+ inputText: `"01s"`,
+ wantErr: true,
+ }, {
desc: "Timestamp zero",
inputMessage: &knownpb.Timestamp{},
inputText: `"1970-01-01T00:00:00Z"`,
diff --git a/encoding/jsonpb/well_known_types.go b/encoding/jsonpb/well_known_types.go
index 788e314..e7ec40f 100644
--- a/encoding/jsonpb/well_known_types.go
+++ b/encoding/jsonpb/well_known_types.go
@@ -7,7 +7,6 @@
import (
"bytes"
"fmt"
- "regexp"
"strconv"
"strings"
"time"
@@ -699,50 +698,103 @@
return nerr.E
}
-// Regular expression for Duration type in JSON format. This allows for values
-// like 1s, 0.1s, 1.s, .1s. It limits fractional part to 9 digits only for
+// parseDuration parses the given input string for seconds and nanoseconds value
+// for the Duration JSON format. The format is a decimal number with a suffix
+// 's'. It can have optional plus/minus sign. There needs to be at least an
+// integer or fractional part. Fractional part is limited to 9 digits only for
// nanoseconds precision, regardless of whether there are trailing zero digits.
-var durationRE = regexp.MustCompile(`^-?([0-9]|[1-9][0-9]+)?(\.[0-9]{0,9})?s$`)
-
+// Example values are 1s, 0.1s, 1.s, .1s, +1s, -1s, -.1s.
func parseDuration(input string) (int64, int32, bool) {
b := []byte(input)
- // TODO: Parse input directly instead of using a regular expression.
- matched := durationRE.FindSubmatch(b)
- if len(matched) != 3 {
+ size := len(b)
+ if size < 2 {
return 0, 0, false
}
+ if b[size-1] != 's' {
+ return 0, 0, false
+ }
+ b = b[:size-1]
+ // Read optional plus/minus symbol.
var neg bool
- if b[0] == '-' {
+ switch b[0] {
+ case '-':
neg = true
+ b = b[1:]
+ case '+':
+ b = b[1:]
}
- var secb []byte
- if len(matched[1]) == 0 {
- secb = []byte{'0'}
- } else {
- secb = matched[1]
+ if len(b) == 0 {
+ return 0, 0, false
}
- var nanob []byte
- if len(matched[2]) <= 1 {
- nanob = []byte{'0'}
- } else {
- nanob = matched[2][1:]
- // Right-pad with 0s for nanosecond-precision.
- for i := len(nanob); i < 9; i++ {
- nanob = append(nanob, '0')
+
+ // Read the integer part.
+ var intp []byte
+ switch {
+ case b[0] == '0':
+ b = b[1:]
+
+ case '1' <= b[0] && b[0] <= '9':
+ intp = b[0:]
+ b = b[1:]
+ n := 1
+ for len(b) > 0 && '0' <= b[0] && b[0] <= '9' {
+ n++
+ b = b[1:]
}
- // Remove unnecessary 0s in the left.
- nanob = bytes.TrimLeft(nanob, "0")
- }
+ intp = intp[:n]
- secs, err := strconv.ParseInt(string(secb), 10, 64)
- if err != nil {
+ case b[0] == '.':
+ // Continue below.
+
+ default:
return 0, 0, false
}
- nanos, err := strconv.ParseInt(string(nanob), 10, 32)
- if err != nil {
- return 0, 0, false
+ hasFrac := false
+ var frac [9]byte
+ if len(b) > 0 {
+ if b[0] != '.' {
+ return 0, 0, false
+ }
+ // Read the fractional part.
+ b = b[1:]
+ n := 0
+ for len(b) > 0 && n < 9 && '0' <= b[0] && b[0] <= '9' {
+ frac[n] = b[0]
+ n++
+ b = b[1:]
+ }
+ // It is not valid if there are more bytes left.
+ if len(b) > 0 {
+ return 0, 0, false
+ }
+ // Pad fractional part with 0s.
+ for i := n; i < 9; i++ {
+ frac[i] = '0'
+ }
+ hasFrac = true
+ }
+
+ var secs int64
+ if len(intp) > 0 {
+ var err error
+ secs, err = strconv.ParseInt(string(intp), 10, 64)
+ if err != nil {
+ return 0, 0, false
+ }
+ }
+
+ var nanos int64
+ if hasFrac {
+ nanob := bytes.TrimLeft(frac[:], "0")
+ if len(nanob) > 0 {
+ var err error
+ nanos, err = strconv.ParseInt(string(nanob), 10, 32)
+ if err != nil {
+ return 0, 0, false
+ }
+ }
}
if neg {