From 7674e75755a61170eb94253b3a6e15a78c90ceba Mon Sep 17 00:00:00 2001 From: Quetzy Garcia Date: Mon, 5 Apr 2021 19:25:22 +0100 Subject: [PATCH 1/4] fix(request): ensure the precision isn't lost when decoding numeric values --- request.go | 119 +++++++++++++++++++++++++---------------------------- 1 file changed, 57 insertions(+), 62 deletions(-) diff --git a/request.go b/request.go index f665857..52808be 100644 --- a/request.go +++ b/request.go @@ -7,7 +7,6 @@ import ( "fmt" "io" "reflect" - "strconv" "strings" "time" ) @@ -95,7 +94,10 @@ func newErrUnsupportedPtrType(rf reflect.Value, t reflect.Type, structField refl func UnmarshalPayload(in io.Reader, model interface{}) error { payload := new(OnePayload) - if err := json.NewDecoder(in).Decode(payload); err != nil { + decoder := json.NewDecoder(in) + decoder.UseNumber() + + if err := decoder.Decode(payload); err != nil { return err } @@ -116,7 +118,10 @@ func UnmarshalPayload(in io.Reader, model interface{}) error { func UnmarshalManyPayload(in io.Reader, t reflect.Type) ([]interface{}, error) { payload := new(ManyPayload) - if err := json.NewDecoder(in).Decode(payload); err != nil { + decoder := json.NewDecoder(in) + decoder.UseNumber() + + if err := decoder.Decode(payload); err != nil { return nil, err } @@ -209,18 +214,9 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) continue } - // Value was not a string... only other supported type was a numeric, - // which would have been sent as a float value. - floatValue, err := strconv.ParseFloat(data.ID, 64) - if err != nil { - // Could not convert the value in the "id" attr to a float - er = ErrBadJSONAPIID - break - } - // Convert the numeric float to one of the supported ID numeric types // (int[8,16,32,64] or uint[8,16,32,64]) - idValue, err := handleNumeric(floatValue, fieldType.Type, fieldValue) + idValue, err := handleNumeric(json.Number(data.ID), fieldType.Type, fieldValue) if err != nil { // We had a JSON float (numeric), but our field was not one of the // allowed numeric types @@ -271,7 +267,10 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) buf := bytes.NewBuffer(nil) json.NewEncoder(buf).Encode(data.Relationships[args[1]]) - json.NewDecoder(buf).Decode(relationship) + + decoder := json.NewDecoder(buf) + decoder.UseNumber() + decoder.Decode(relationship) data := relationship.Data models := reflect.New(fieldValue.Type()).Elem() @@ -298,10 +297,11 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) buf := bytes.NewBuffer(nil) - json.NewEncoder(buf).Encode( - data.Relationships[args[1]], - ) - json.NewDecoder(buf).Decode(relationship) + json.NewEncoder(buf).Encode(data.Relationships[args[1]]) + + decoder := json.NewDecoder(buf) + decoder.UseNumber() + decoder.Decode(relationship) /* http://jsonapi.org/format/#document-resource-object-relationships @@ -416,9 +416,12 @@ func unmarshalAttribute( return } - // JSON value was a float (numeric) - if value.Kind() == reflect.Float64 { - value, err = handleNumeric(attribute, fieldType, fieldValue) + switch attribute.(type) { + case json.Number: + value, err = handleNumeric(attribute.(json.Number), fieldType, fieldValue) + return + case float64: + value, err = handleNumeric(json.Number(fmt.Sprint(attribute)), fieldType, fieldValue) return } @@ -495,27 +498,26 @@ func handleTime(attribute interface{}, args []string, fieldValue reflect.Value) return reflect.ValueOf(t), nil } - var at int64 + switch v.Interface().(type) { + case json.Number: + i, err := v.Interface().(json.Number).Int64() + if err == nil { + return reflect.ValueOf(time.Unix(i, 0)), nil + } - if v.Kind() == reflect.Float64 { - at = int64(v.Interface().(float64)) - } else if v.Kind() == reflect.Int { - at = v.Int() - } else { - return reflect.ValueOf(time.Now()), ErrInvalidTime + f, err := v.Interface().(json.Number).Float64() + if err == nil { + return reflect.ValueOf(time.Unix(int64(f), 0)), nil + } } - t := time.Unix(at, 0) - - return reflect.ValueOf(t), nil + return reflect.ValueOf(time.Now()), ErrInvalidTime } func handleNumeric( - attribute interface{}, + attribute json.Number, fieldType reflect.Type, fieldValue reflect.Value) (reflect.Value, error) { - v := reflect.ValueOf(attribute) - floatValue := v.Interface().(float64) var kind reflect.Kind if fieldValue.Kind() == reflect.Ptr { @@ -524,50 +526,41 @@ func handleNumeric( kind = fieldType.Kind() } - var numericValue reflect.Value + i, err := attribute.Int64() switch kind { case reflect.Int: - n := int(floatValue) - numericValue = reflect.ValueOf(&n) + return reflect.ValueOf(int(i)), err case reflect.Int8: - n := int8(floatValue) - numericValue = reflect.ValueOf(&n) + return reflect.ValueOf(int8(i)), err case reflect.Int16: - n := int16(floatValue) - numericValue = reflect.ValueOf(&n) + return reflect.ValueOf(int16(i)), err case reflect.Int32: - n := int32(floatValue) - numericValue = reflect.ValueOf(&n) + return reflect.ValueOf(int32(i)), err case reflect.Int64: - n := int64(floatValue) - numericValue = reflect.ValueOf(&n) + return reflect.ValueOf(i), err case reflect.Uint: - n := uint(floatValue) - numericValue = reflect.ValueOf(&n) + return reflect.ValueOf(uint(i)), err case reflect.Uint8: - n := uint8(floatValue) - numericValue = reflect.ValueOf(&n) + return reflect.ValueOf(uint8(i)), err case reflect.Uint16: - n := uint16(floatValue) - numericValue = reflect.ValueOf(&n) + return reflect.ValueOf(uint16(i)), err case reflect.Uint32: - n := uint32(floatValue) - numericValue = reflect.ValueOf(&n) + return reflect.ValueOf(uint32(i)), err case reflect.Uint64: - n := uint64(floatValue) - numericValue = reflect.ValueOf(&n) + return reflect.ValueOf(uint64(i)), err + } + + f, err := attribute.Float64() + + switch kind { case reflect.Float32: - n := float32(floatValue) - numericValue = reflect.ValueOf(&n) + return reflect.ValueOf(float32(f)), err case reflect.Float64: - n := floatValue - numericValue = reflect.ValueOf(&n) - default: - return reflect.Value{}, ErrUnknownFieldNumberType + return reflect.ValueOf(f), err } - return numericValue, nil + return reflect.Value{}, ErrUnknownFieldNumberType } func handlePointer( @@ -580,6 +573,8 @@ func handlePointer( var concreteVal reflect.Value switch cVal := attribute.(type) { + case json.Number: + return handleNumeric(attribute.(json.Number), fieldType, fieldValue) case string: concreteVal = reflect.ValueOf(&cVal) case bool: From 1774d8bacb8ecde6ef711cbee137e270439619a8 Mon Sep 17 00:00:00 2001 From: Quetzy Garcia Date: Mon, 5 Apr 2021 19:26:50 +0100 Subject: [PATCH 2/4] tests(request): update an expected ID value to a high value --- request_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/request_test.go b/request_test.go index 300c7de..fe239ee 100644 --- a/request_test.go +++ b/request_test.go @@ -303,7 +303,7 @@ func TestUnmarshalSetsID(t *testing.T) { t.Fatal(err) } - if out.ID != 2 { + if out.ID != 9223372036854775807 { t.Fatalf("Did not set ID on dst interface") } } @@ -1070,7 +1070,7 @@ func samplePayload() io.Reader { func samplePayloadWithID() io.Reader { payload := &OnePayload{ Data: &Node{ - ID: "2", + ID: "9223372036854775807", Type: "blogs", Attributes: map[string]interface{}{ "title": "New blog", From 7ea5e24053020109dfff6012e9241696b2682c33 Mon Sep 17 00:00:00 2001 From: Quetzy Garcia Date: Mon, 5 Apr 2021 22:12:18 +0100 Subject: [PATCH 3/4] tests(request): full coverage of the handleNumeric() function --- models_test.go | 19 +++++++++++ request_test.go | 88 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+) diff --git a/models_test.go b/models_test.go index f552d4f..2a1ea16 100644 --- a/models_test.go +++ b/models_test.go @@ -195,3 +195,22 @@ type CustomAttributeTypes struct { Float CustomFloatType `jsonapi:"attr,float"` String CustomStringType `jsonapi:"attr,string"` } + +type Numerics struct { + ID string `jsonapi:"primary,numerics"` + + Int int `jsonapi:"attr,int"` + Int8 int8 `jsonapi:"attr,int8"` + Int16 int16 `jsonapi:"attr,int16"` + Int32 int32 `jsonapi:"attr,int32"` + Int64 int64 `jsonapi:"attr,int64"` + + Uint uint `jsonapi:"attr,uint"` + Uint8 uint8 `jsonapi:"attr,uint8"` + Uint16 uint16 `jsonapi:"attr,uint16"` + Uint32 uint32 `jsonapi:"attr,uint32"` + Uint64 uint64 `jsonapi:"attr,uint64"` + + Float32 float32 `jsonapi:"attr,float32"` + Float64 float64 `jsonapi:"attr,float64"` +} diff --git a/request_test.go b/request_test.go index fe239ee..9c0c144 100644 --- a/request_test.go +++ b/request_test.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io" + "math" "reflect" "sort" "strings" @@ -1416,3 +1417,90 @@ func TestUnmarshalNestedStructSlice(t *testing.T) { out.Teams[0].Members[0].Firstname) } } + +func TestUnmarshalNumerics(t *testing.T) { + data, err := json.Marshal(map[string]interface{}{ + "data": map[string]interface{}{ + "id": "9223372036854775807", + "type": "numerics", + "attributes": map[string]interface{}{ + "int": math.MinInt32, + "int8": math.MinInt8, + "int16": math.MinInt16, + "int32": math.MinInt32, + "int64": math.MinInt64, + + "uint": math.MaxInt32, + "uint8": math.MaxInt8, + "uint16": math.MaxInt16, + "uint32": math.MaxInt32, + "uint64": math.MaxInt64, + + "float32": math.MaxFloat32, + "float64": math.MaxFloat64, + }, + }, + }) + + if err != nil { + t.Fatal(err) + } + in := bytes.NewReader(data) + n := new(Numerics) + + if err = UnmarshalPayload(in, n); err != nil { + t.Fatal(err) + } + + if n.ID != "9223372036854775807" { + t.Fatalf("Unexpected value for ID") + } + + if n.Int != math.MinInt32 { + t.Fatalf("Unexpected value for Int") + } + + if n.Int8 != math.MinInt8 { + t.Fatalf("Unexpected value for Int8") + } + + if n.Int16 != math.MinInt16 { + t.Fatalf("Unexpected value for Int16") + } + + if n.Int32 != math.MinInt32 { + t.Fatalf("Unexpected value for Int32") + } + + if n.Int64 != math.MinInt64 { + t.Fatalf("Unexpected value for Int64") + } + + if n.Uint != math.MaxInt32 { + t.Fatalf("Unexpected value for Uint") + } + + if n.Uint8 != math.MaxInt8 { + t.Fatalf("Unexpected value for Uint8") + } + + if n.Uint16 != math.MaxInt16 { + t.Fatalf("Unexpected value for Uint16") + } + + if n.Uint32 != math.MaxInt32 { + t.Fatalf("Unexpected value for Uint32") + } + + if n.Uint64 != math.MaxInt64 { + t.Fatalf("Unexpected value for Uint64") + } + + if n.Float32 != math.MaxFloat32 { + t.Fatalf("Unexpected value for Float32") + } + + if n.Float64 != math.MaxFloat64 { + t.Fatalf("Unexpected value for Float64") + } +} From cb512b4bbb88a70f0aef60c252c77bb161079131 Mon Sep 17 00:00:00 2001 From: Quetzy Garcia Date: Sun, 11 Apr 2021 23:17:13 +0100 Subject: [PATCH 4/4] fix(request): ensure numeric values are all handled as json.Number in the handleStruct() function --- request.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/request.go b/request.go index 52808be..a572ebe 100644 --- a/request.go +++ b/request.go @@ -420,9 +420,6 @@ func unmarshalAttribute( case json.Number: value, err = handleNumeric(attribute.(json.Number), fieldType, fieldValue) return - case float64: - value, err = handleNumeric(json.Number(fmt.Sprint(attribute)), fieldType, fieldValue) - return } // Field was a Pointer type @@ -611,8 +608,11 @@ func handleStruct( return reflect.Value{}, err } + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.UseNumber() + node := new(Node) - if err := json.Unmarshal(data, &node.Attributes); err != nil { + if err = decoder.Decode(&node.Attributes); err != nil { return reflect.Value{}, err }