From c4450377bd4cbbfec173c6b5380bdcb2aeb02d9e Mon Sep 17 00:00:00 2001 From: Herbie Ong Date: Wed, 27 Mar 2019 09:59:51 -0700 Subject: [PATCH] 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 --- encoding/jsonpb/decode_test.go | 120 ++++++++++++++++++++++++++ encoding/jsonpb/well_known_types.go | 129 +++++++++++++++++++++++++++- 2 files changed, 245 insertions(+), 4 deletions(-) diff --git a/encoding/jsonpb/decode_test.go b/encoding/jsonpb/decode_test.go index 725c5a8a..648a3b19 100644 --- a/encoding/jsonpb/decode_test.go +++ b/encoding/jsonpb/decode_test.go @@ -1531,6 +1531,126 @@ func TestUnmarshal(t *testing.T) { }, }, 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{}, diff --git a/encoding/jsonpb/well_known_types.go b/encoding/jsonpb/well_known_types.go index 04c7aa53..822520e8 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 (e encoder) marshalCustomType(m pref.Message) error { 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 @@ func (d decoder) unmarshalCustomType(m pref.Message) error { 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 @@ func (e encoder) marshalDuration(m pref.Message) error { 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 @@ func (e encoder) marshalTimestamp(m pref.Message) error { 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