diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 8a1927a39ca..85d47d2496c 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -3,3 +3,5 @@ ### SDK Enhancements ### SDK Bugs +* `private/protocol`: Add support for UTC offset for ISO8601 datetime formats ([#3960](https://github.com/aws/aws-sdk-go/pull/3960)) + * Updates the SDK's parsing of ISO8601 date time formats to support UTC offsets. diff --git a/private/protocol/timestamp.go b/private/protocol/timestamp.go index 98f4caed91c..d486a4c2a0d 100644 --- a/private/protocol/timestamp.go +++ b/private/protocol/timestamp.go @@ -1,6 +1,8 @@ package protocol import ( + "bytes" + "fmt" "math" "strconv" "time" @@ -19,7 +21,9 @@ const ( // Output time is intended to not contain decimals const ( // RFC 7231#section-7.1.1.1 timetamp format. e.g Tue, 29 Apr 2014 18:30:38 GMT - RFC822TimeFormat = "Mon, 2 Jan 2006 15:04:05 GMT" + RFC822TimeFormat = "Mon, 2 Jan 2006 15:04:05 GMT" + rfc822TimeFormatSingleDigitDay = "Mon, _2 Jan 2006 15:04:05 GMT" + rfc822TimeFormatSingleDigitDayTwoDigitYear = "Mon, _2 Jan 06 15:04:05 GMT" // This format is used for output time without seconds precision RFC822OutputTimeFormat = "Mon, 02 Jan 2006 15:04:05 GMT" @@ -67,10 +71,20 @@ func FormatTime(name string, t time.Time) string { // the time if it was able to be parsed, and fails otherwise. func ParseTime(formatName, value string) (time.Time, error) { switch formatName { - case RFC822TimeFormatName: - return time.Parse(RFC822TimeFormat, value) - case ISO8601TimeFormatName: - return time.Parse(ISO8601TimeFormat, value) + case RFC822TimeFormatName: // Smithy HTTPDate format + return tryParse(value, + RFC822TimeFormat, + rfc822TimeFormatSingleDigitDay, + rfc822TimeFormatSingleDigitDayTwoDigitYear, + time.RFC850, + time.ANSIC, + ) + case ISO8601TimeFormatName: // Smithy DateTime format + return tryParse(value, + ISO8601TimeFormat, + time.RFC3339Nano, + time.RFC3339, + ) case UnixTimeFormatName: v, err := strconv.ParseFloat(value, 64) _, dec := math.Modf(v) @@ -83,3 +97,36 @@ func ParseTime(formatName, value string) (time.Time, error) { panic("unknown timestamp format name, " + formatName) } } + +func tryParse(v string, formats ...string) (time.Time, error) { + var errs parseErrors + for _, f := range formats { + t, err := time.Parse(f, v) + if err != nil { + errs = append(errs, parseError{ + Format: f, + Err: err, + }) + continue + } + return t, nil + } + + return time.Time{}, fmt.Errorf("unable to parse time string, %v", errs) +} + +type parseErrors []parseError + +func (es parseErrors) Error() string { + var s bytes.Buffer + for _, e := range es { + fmt.Fprintf(&s, "\n * %q: %v", e.Format, e.Err) + } + + return "parse errors:" + s.String() +} + +type parseError struct { + Format string + Err error +} diff --git a/private/protocol/timestamp_test.go b/private/protocol/timestamp_test.go index 9da63844c37..3bb561a0ced 100644 --- a/private/protocol/timestamp_test.go +++ b/private/protocol/timestamp_test.go @@ -80,21 +80,46 @@ func TestParseTime(t *testing.T) { input: "946845296.1229999", expectedOutput: time.Date(2000, time.January, 2, 20, 34, 56, .123e9, time.UTC), }, - "ISO8601Test1": { + "ISO8601Test milliseconds": { formatName: ISO8601TimeFormatName, input: "2000-01-02T20:34:56.123Z", expectedOutput: time.Date(2000, time.January, 2, 20, 34, 56, .123e9, time.UTC), }, - "ISO8601Test2": { + "ISO8601Test nanoseconds": { formatName: ISO8601TimeFormatName, input: "2000-01-02T20:34:56.123456789Z", expectedOutput: time.Date(2000, time.January, 2, 20, 34, 56, .123456789e9, time.UTC), }, - "RFC822Test1": { + "ISO8601Test millisecond utc offset": { + formatName: ISO8601TimeFormatName, + input: "2000-01-02T20:34:56.123-07:00", + expectedOutput: time.Date(2000, time.January, 3, 3, 34, 56, .123e9, time.UTC), + }, + "ISO8601Test millisecond positive utc offset": { + formatName: ISO8601TimeFormatName, + input: "2000-01-02T20:34:56.123+07:00", + expectedOutput: time.Date(2000, time.January, 2, 13, 34, 56, .123e9, time.UTC), + }, + "ISO8601Test nanosecond utc offset": { + formatName: ISO8601TimeFormatName, + input: "2000-01-02T20:34:56.123456789-07:00", + expectedOutput: time.Date(2000, time.January, 3, 3, 34, 56, .123456789e9, time.UTC), + }, + "RFC822Test single digit day": { formatName: RFC822TimeFormatName, input: "Sun, 2 Jan 2000 20:34:56 GMT", expectedOutput: time.Date(2000, time.January, 2, 20, 34, 56, 0, time.UTC), }, + "RFC822Test two digit day": { + formatName: RFC822TimeFormatName, + input: "Sun, 02 Jan 2000 20:34:56 GMT", + expectedOutput: time.Date(2000, time.January, 2, 20, 34, 56, 0, time.UTC), + }, + "RFC822Test two digit day year": { + formatName: RFC822TimeFormatName, + input: "Sun, 2 Jan 00 20:34:56 GMT", + expectedOutput: time.Date(2000, time.January, 2, 20, 34, 56, 0, time.UTC), + }, } for name, c := range cases {