Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

private/protocol/json/jsonutil: Fixes a bug that truncated millisecond precision time in API response to seconds #3474

Merged
merged 6 commits into from
Aug 11, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG_PENDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@
* Fixes [#3441](https://github.com/aws/aws-sdk-go/issues/3441) by adding a new XXX_Values function for each API enum type that returns a slice of enum values, e.g `DomainStatus_Values`.

### SDK Bugs
* `private/protocol/json/jsonutil`: Fixes a bug that truncated millisecond precision time in API response to seconds. ([#3474](https://github.com/aws/aws-sdk-go/pull/3474))
* Fixes [#3464](https://github.com/aws/aws-sdk-go/issues/3464)
* Fixes [#3410](https://github.com/aws/aws-sdk-go/issues/3410)
* `codegen`: Export event stream constructor for easier mocking ([#3473](https://github.com/aws/aws-sdk-go/pull/3473))
* Fixes [#3412](https://github.com/aws/aws-sdk-go/issues/3412) by exporting the operation's EventStream type's constructor function so it can be used to fully initialize fully when mocking out behavior for API operations with event streams.
36 changes: 29 additions & 7 deletions private/protocol/json/jsonutil/unmarshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"io"
"math/big"
"reflect"
"strings"
"time"
Expand All @@ -15,6 +16,8 @@ import (
"github.com/aws/aws-sdk-go/private/protocol"
)

var millisecondsFloat = new(big.Float).SetInt64(1e3)

// UnmarshalJSONError unmarshal's the reader's JSON document into the passed in
// type. The value to unmarshal the json document into must be a pointer to the
// type.
Expand All @@ -39,7 +42,9 @@ func UnmarshalJSONError(v interface{}, stream io.Reader) error {
func UnmarshalJSON(v interface{}, stream io.Reader) error {
var out interface{}

err := json.NewDecoder(stream).Decode(&out)
decoder := json.NewDecoder(stream)
decoder.UseNumber()
err := decoder.Decode(&out)
if err == io.EOF {
return nil
} else if err != nil {
Expand All @@ -54,7 +59,9 @@ func UnmarshalJSON(v interface{}, stream io.Reader) error {
func UnmarshalJSONCaseInsensitive(v interface{}, stream io.Reader) error {
var out interface{}

err := json.NewDecoder(stream).Decode(&out)
decoder := json.NewDecoder(stream)
decoder.UseNumber()
err := decoder.Decode(&out)
if err == io.EOF {
return nil
} else if err != nil {
Expand Down Expand Up @@ -254,16 +261,31 @@ func (u unmarshaler) unmarshalScalar(value reflect.Value, data interface{}, tag
default:
return fmt.Errorf("unsupported value: %v (%s)", value.Interface(), value.Type())
}
case float64:
case json.Number:
switch value.Interface().(type) {
case *int64:
di := int64(d)
// Retain the old behavior where we would just truncate the float64
// calling d.Int64() here could cause an invalid syntax error due to the usage of strconv.ParseInt
f, err := d.Float64()
if err != nil {
return err
}
di := int64(f)
value.Set(reflect.ValueOf(&di))
case *float64:
value.Set(reflect.ValueOf(&d))
f, err := d.Float64()
if err != nil {
return err
}
value.Set(reflect.ValueOf(&f))
case *time.Time:
// Time unmarshaled from a float64 can only be epoch seconds
t := time.Unix(int64(d), 0).UTC()
float, ok := new(big.Float).SetString(d.String())
if !ok {
return fmt.Errorf("unsupported float time representation: %v", d.String())
}
float = float.Mul(float, millisecondsFloat)
ms, _ := float.Int64()
t := time.Unix(0, ms*1e6).UTC()
value.Set(reflect.ValueOf(&t))
default:
return fmt.Errorf("unsupported value: %v (%s)", value.Interface(), value.Type())
Expand Down
121 changes: 121 additions & 0 deletions private/protocol/json/jsonutil/unmarshal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// +build go1.7

package jsonutil_test

import (
"bytes"
"reflect"
"testing"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/private/protocol/json/jsonutil"
)

func TestUnmarshalJSON_JSONNumber(t *testing.T) {
type input struct {
TimeField *time.Time `locationName:"timeField"`
IntField *int64 `locationName:"intField"`
FloatField *float64 `locationName:"floatField"`
}

cases := map[string]struct {
JSON string
Value input
Expected input
}{
"seconds precision": {
JSON: `{"timeField":1597094942}`,
Expected: input{
TimeField: func() *time.Time {
dt := time.Date(2020, 8, 10, 21, 29, 02, 00, time.UTC)
return &dt
}(),
},
},
"exact milliseconds precision": {
JSON: `{"timeField":1597094942.123}`,
Expected: input{
TimeField: func() *time.Time {
dt := time.Date(2020, 8, 10, 21, 29, 02, int(123*time.Millisecond), time.UTC)
return &dt
}(),
},
},
"microsecond precision truncated": {
JSON: `{"timeField":1597094942.1235}`,
Expected: input{
TimeField: func() *time.Time {
dt := time.Date(2020, 8, 10, 21, 29, 02, int(123*time.Millisecond), time.UTC)
return &dt
}(),
},
},
"nanosecond precision truncated": {
JSON: `{"timeField":1597094942.123456789}`,
Expected: input{
TimeField: func() *time.Time {
dt := time.Date(2020, 8, 10, 21, 29, 02, int(123*time.Millisecond), time.UTC)
return &dt
}(),
},
},
"milliseconds precision as small exponent": {
JSON: `{"timeField":1.597094942123e9}`,
Expected: input{
TimeField: func() *time.Time {
dt := time.Date(2020, 8, 10, 21, 29, 02, int(123*time.Millisecond), time.UTC)
return &dt
}(),
},
},
"milliseconds precision as large exponent": {
JSON: `{"timeField":1.597094942123E9}`,
Expected: input{
TimeField: func() *time.Time {
dt := time.Date(2020, 8, 10, 21, 29, 02, int(123*time.Millisecond), time.UTC)
return &dt
}(),
},
},
"milliseconds precision as exponent with sign": {
JSON: `{"timeField":1.597094942123e+9}`,
Expected: input{
TimeField: func() *time.Time {
dt := time.Date(2020, 8, 10, 21, 29, 02, int(123*time.Millisecond), time.UTC)
return &dt
}(),
},
},
"integer field": {
JSON: `{"intField":123456789}`,
Expected: input{
IntField: aws.Int64(123456789),
},
},
"integer field truncated": {
JSON: `{"intField":123456789.123}`,
Expected: input{
IntField: aws.Int64(123456789),
},
},
"float64 field": {
JSON: `{"floatField":123456789.123}`,
Expected: input{
FloatField: aws.Float64(123456789.123),
},
},
}

for name, tt := range cases {
t.Run(name, func(t *testing.T) {
err := jsonutil.UnmarshalJSON(&tt.Value, bytes.NewReader([]byte(tt.JSON)))
if err != nil {
t.Errorf("expect no error, got %v", err)
}
if e, a := tt.Expected, tt.Value; !reflect.DeepEqual(e, a) {
t.Errorf("expect %v, got %v", e, a)
}
})
}
}