encoding/jsonpb: add support for unmarshaling Duration and Timestamp
Change-Id: Ia8319ed82d1d031e344ad7b095df2018286dcd43
Reviewed-on: https://go-review.googlesource.com/c/protobuf/+/169698
Reviewed-by: Joe Tsai <thebrokentoaster@gmail.com>
diff --git a/encoding/jsonpb/decode_test.go b/encoding/jsonpb/decode_test.go
index 725c5a8..648a3b1 100644
--- a/encoding/jsonpb/decode_test.go
+++ b/encoding/jsonpb/decode_test.go
@@ -1532,6 +1532,126 @@
},
wantErr: true,
}, {
+ desc: "Duration empty string",
+ inputMessage: &knownpb.Duration{},
+ inputText: `""`,
+ wantErr: true,
+ }, {
+ desc: "Duration with secs",
+ inputMessage: &knownpb.Duration{},
+ inputText: `"3s"`,
+ wantMessage: &knownpb.Duration{Seconds: 3},
+ }, {
+ desc: "Duration with escaped unicode",
+ inputMessage: &knownpb.Duration{},
+ inputText: `"\u0033s"`,
+ wantMessage: &knownpb.Duration{Seconds: 3},
+ }, {
+ desc: "Duration with -secs",
+ inputMessage: &knownpb.Duration{},
+ inputText: `"-3s"`,
+ wantMessage: &knownpb.Duration{Seconds: -3},
+ }, {
+ desc: "Duration with nanos",
+ inputMessage: &knownpb.Duration{},
+ inputText: `"0.001s"`,
+ wantMessage: &knownpb.Duration{Nanos: 1e6},
+ }, {
+ desc: "Duration with -nanos",
+ inputMessage: &knownpb.Duration{},
+ inputText: `"-0.001s"`,
+ wantMessage: &knownpb.Duration{Nanos: -1e6},
+ }, {
+ desc: "Duration with -secs -nanos",
+ inputMessage: &knownpb.Duration{},
+ inputText: `"-123.000000450s"`,
+ wantMessage: &knownpb.Duration{Seconds: -123, Nanos: -450},
+ }, {
+ desc: "Duration with large secs",
+ inputMessage: &knownpb.Duration{},
+ inputText: `"10000000000.000000001s"`,
+ wantMessage: &knownpb.Duration{Seconds: 1e10, Nanos: 1},
+ }, {
+ desc: "Duration with decimal without fractional",
+ inputMessage: &knownpb.Duration{},
+ inputText: `"3.s"`,
+ wantMessage: &knownpb.Duration{Seconds: 3},
+ }, {
+ desc: "Duration with decimal without integer",
+ inputMessage: &knownpb.Duration{},
+ inputText: `"0.5s"`,
+ wantMessage: &knownpb.Duration{Nanos: 5e8},
+ }, {
+ desc: "Duration with +secs out of range",
+ inputMessage: &knownpb.Duration{},
+ inputText: `"315576000001s"`,
+ wantErr: true,
+ }, {
+ desc: "Duration with -secs out of range",
+ inputMessage: &knownpb.Duration{},
+ inputText: `"-315576000001s"`,
+ wantErr: true,
+ }, {
+ desc: "Duration with nanos beyond 9 digits",
+ inputMessage: &knownpb.Duration{},
+ inputText: `"0.9999999990s"`,
+ wantErr: true,
+ }, {
+ desc: "Duration without suffix s",
+ inputMessage: &knownpb.Duration{},
+ inputText: `"123"`,
+ wantErr: true,
+ }, {
+ desc: "Timestamp zero",
+ inputMessage: &knownpb.Timestamp{},
+ inputText: `"1970-01-01T00:00:00Z"`,
+ wantMessage: &knownpb.Timestamp{},
+ }, {
+ desc: "Timestamp with tz adjustment",
+ inputMessage: &knownpb.Timestamp{},
+ inputText: `"1970-01-01T00:00:00+01:00"`,
+ wantMessage: &knownpb.Timestamp{Seconds: -3600},
+ }, {
+ desc: "Timestamp UTC",
+ inputMessage: &knownpb.Timestamp{},
+ inputText: `"2019-03-19T23:03:21Z"`,
+ wantMessage: &knownpb.Timestamp{Seconds: 1553036601},
+ }, {
+ desc: "Timestamp with escaped unicode",
+ inputMessage: &knownpb.Timestamp{},
+ inputText: `"2019-0\u0033-19T23:03:21Z"`,
+ wantMessage: &knownpb.Timestamp{Seconds: 1553036601},
+ }, {
+ desc: "Timestamp with nanos",
+ inputMessage: &knownpb.Timestamp{},
+ inputText: `"2019-03-19T23:03:21.000000001Z"`,
+ wantMessage: &knownpb.Timestamp{Seconds: 1553036601, Nanos: 1},
+ }, {
+ desc: "Timestamp upper limit",
+ inputMessage: &knownpb.Timestamp{},
+ inputText: `"9999-12-31T23:59:59.999999999Z"`,
+ wantMessage: &knownpb.Timestamp{Seconds: 253402300799, Nanos: 999999999},
+ }, {
+ desc: "Timestamp above upper limit",
+ inputMessage: &knownpb.Timestamp{},
+ inputText: `"9999-12-31T23:59:59-01:00"`,
+ wantErr: true,
+ }, {
+ desc: "Timestamp lower limit",
+ inputMessage: &knownpb.Timestamp{},
+ inputText: `"0001-01-01T00:00:00Z"`,
+ wantMessage: &knownpb.Timestamp{Seconds: -62135596800},
+ }, {
+ desc: "Timestamp below lower limit",
+ inputMessage: &knownpb.Timestamp{},
+ inputText: `"0001-01-01T00:00:00+01:00"`,
+ wantErr: true,
+ }, {
+ desc: "Timestamp with nanos beyond 9 digits",
+ inputMessage: &knownpb.Timestamp{},
+ inputText: `"1970-01-01T00:00:00.0000000001Z"`,
+ wantErr: true,
+ }, {
desc: "FieldMask empty",
inputMessage: &knownpb.FieldMask{},
inputText: `""`,
diff --git a/encoding/jsonpb/well_known_types.go b/encoding/jsonpb/well_known_types.go
index 04c7aa5..822520e 100644
--- a/encoding/jsonpb/well_known_types.go
+++ b/encoding/jsonpb/well_known_types.go
@@ -5,7 +5,10 @@
package jsonpb
import (
+ "bytes"
"fmt"
+ "regexp"
+ "strconv"
"strings"
"time"
@@ -90,10 +93,8 @@
func (d decoder) unmarshalCustomType(m pref.Message) error {
name := m.Type().FullName()
switch name {
- case "google.protobuf.Any",
- "google.protobuf.Duration",
- "google.protobuf.Timestamp":
- panic(fmt.Sprintf("unmarshaling of %v is not implemented yet", name))
+ case "google.protobuf.Any":
+ panic("unmarshaling of google.protobuf.Any is not implemented yet")
case "google.protobuf.BoolValue",
"google.protobuf.DoubleValue",
@@ -115,6 +116,12 @@
case "google.protobuf.Value":
return d.unmarshalKnownValue(m)
+ case "google.protobuf.Duration":
+ return d.unmarshalDuration(m)
+
+ case "google.protobuf.Timestamp":
+ return d.unmarshalTimestamp(m)
+
case "google.protobuf.FieldMask":
return d.unmarshalFieldMask(m)
}
@@ -417,6 +424,91 @@
return nil
}
+func (d decoder) unmarshalDuration(m pref.Message) error {
+ var nerr errors.NonFatal
+ jval, err := d.Read()
+ if !nerr.Merge(err) {
+ return err
+ }
+ if jval.Type() != json.String {
+ return unexpectedJSONError{jval}
+ }
+
+ msgType := m.Type()
+ input := jval.String()
+ secs, nanos, ok := parseDuration(input)
+ if !ok {
+ return errors.New("%s: invalid duration value %q", msgType.FullName(), input)
+ }
+ // Validate seconds. No need to validate nanos because parseDuration would
+ // have covered that already.
+ if secs < -maxSecondsInDuration || secs > maxSecondsInDuration {
+ return errors.New("%s: out of range %q", msgType.FullName(), input)
+ }
+
+ knownFields := m.KnownFields()
+ knownFields.Set(fieldnum.Duration_Seconds, pref.ValueOf(secs))
+ knownFields.Set(fieldnum.Duration_Nanos, pref.ValueOf(nanos))
+ 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
+// 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$`)
+
+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 {
+ return 0, 0, false
+ }
+
+ var neg bool
+ if b[0] == '-' {
+ neg = true
+ }
+ var secb []byte
+ if len(matched[1]) == 0 {
+ secb = []byte{'0'}
+ } else {
+ secb = matched[1]
+ }
+ 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')
+ }
+ // Remove unnecessary 0s in the left.
+ nanob = bytes.TrimLeft(nanob, "0")
+ }
+
+ secs, err := strconv.ParseInt(string(secb), 10, 64)
+ if err != nil {
+ return 0, 0, false
+ }
+
+ nanos, err := strconv.ParseInt(string(nanob), 10, 32)
+ if err != nil {
+ return 0, 0, false
+ }
+
+ if neg {
+ if secs > 0 {
+ secs = -secs
+ }
+ if nanos > 0 {
+ nanos = -nanos
+ }
+ }
+ return secs, int32(nanos), true
+}
+
// The JSON representation for a Timestamp is a JSON string in the RFC 3339
// format, i.e. "{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z" where
// {year} is always expressed using four digits while {month}, {day}, {hour},
@@ -460,6 +552,35 @@
return nil
}
+func (d decoder) unmarshalTimestamp(m pref.Message) error {
+ var nerr errors.NonFatal
+ jval, err := d.Read()
+ if !nerr.Merge(err) {
+ return err
+ }
+ if jval.Type() != json.String {
+ return unexpectedJSONError{jval}
+ }
+
+ msgType := m.Type()
+ input := jval.String()
+ t, err := time.Parse(time.RFC3339Nano, input)
+ if err != nil {
+ return errors.New("%s: invalid timestamp value %q", msgType.FullName(), input)
+ }
+ // Validate seconds. No need to validate nanos because time.Parse would have
+ // covered that already.
+ secs := t.Unix()
+ if secs < minTimestampSeconds || secs > maxTimestampSeconds {
+ return errors.New("%s: out of range %q", msgType.FullName(), input)
+ }
+
+ knownFields := m.KnownFields()
+ knownFields.Set(fieldnum.Timestamp_Seconds, pref.ValueOf(secs))
+ knownFields.Set(fieldnum.Timestamp_Nanos, pref.ValueOf(int32(t.Nanosecond())))
+ return nerr.E
+}
+
// The JSON representation for a FieldMask is a JSON string where paths are
// separated by a comma. Fields name in each path are converted to/from
// lower-camel naming conventions. Encoding should fail if the path name would