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