Skip to content

Commit

Permalink
Don't silently convert floats to integers in JsonProvider.
Browse files Browse the repository at this point in the history
Sufficiently large float values will end up converted as Int.MinValue and succeed
  • Loading branch information
Eduard Ostrovsky committed Jul 24, 2018
1 parent 9caffdb commit 8d05e4b
Show file tree
Hide file tree
Showing 3 changed files with 27 additions and 8 deletions.
12 changes: 8 additions & 4 deletions src/Json/JsonConversions.fs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ open FSharp.Data

[<AutoOpen>]
module private Helpers =
let inline inRange lo hi v = (v >= decimal lo) && (v <= decimal hi)
let inline isInteger v = Math.Round(v:decimal) = v
let inline inRangeDecimal (lo:'a) (hi:'b) (v:decimal) : bool = (v >= decimal lo) && (v <= decimal hi)
let inline inRangeFloat (lo:'a) (hi:'b) (v:float) : bool = (v >= float lo) && (v <= float hi)
let inline isIntegerDecimal (v:decimal) : bool = Math.Round v = v
let inline isIntegerFloat (v:float) : bool = Math.Round v = v

/// Conversions from JsonValue to string/int/int64/decimal/float/boolean/datetime/guid options
type JsonConversions =
Expand All @@ -24,12 +26,14 @@ type JsonConversions =
| _ -> None

static member AsInteger cultureInfo = function
| JsonValue.Number n when inRange Int32.MinValue Int32.MaxValue n && isInteger n -> Some (int n)
| JsonValue.Number n when inRangeDecimal Int32.MinValue Int32.MaxValue n && isIntegerDecimal n -> Some (int n)
| JsonValue.Float f when inRangeFloat Int32.MinValue Int32.MaxValue f && isIntegerFloat f -> Some (int f)
| JsonValue.String s -> TextConversions.AsInteger cultureInfo s
| _ -> None

static member AsInteger64 cultureInfo = function
| JsonValue.Number n when inRange Int64.MinValue Int64.MaxValue n && isInteger n -> Some (int64 n)
| JsonValue.Number n when inRangeDecimal Int64.MinValue Int64.MaxValue n && isIntegerDecimal n -> Some (int64 n)
| JsonValue.Float f when inRangeFloat Int64.MinValue Int64.MaxValue f && isIntegerFloat f -> Some (int64 f)
| JsonValue.String s -> TextConversions.AsInteger64 cultureInfo s
| _ -> None

Expand Down
12 changes: 8 additions & 4 deletions src/Json/JsonInference.fs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ open FSharp.Data.Runtime.StructuralTypes
/// `inferCollectionType` and various functions to find common subtype), so
/// here we just need to infer types of primitive JSON values.
let rec inferType inferTypesFromValues cultureInfo parentName json =
let inline inRange lo hi v = (v >= decimal lo) && (v <= decimal hi)
let inline isInteger v = Math.Round(v:decimal) = v
let inline inRangeDecimal (lo:'a) (hi:'b) (v:decimal) : bool = (v >= decimal lo) && (v <= decimal hi)
let inline inRangeFloat (lo:'a) (hi:'b) (v:float) : bool = (v >= float lo) && (v <= float hi)
let inline isIntegerDecimal (v:decimal) : bool = Math.Round v = v
let inline isIntegerFloat (v:float) : bool = Math.Round v = v

match json with
// Null and primitives without subtyping hiearchies
Expand All @@ -26,9 +28,11 @@ let rec inferType inferTypesFromValues cultureInfo parentName json =
// For numbers, we test if it is integer and if it fits in smaller range
| JsonValue.Number 0M when inferTypesFromValues -> InferedType.Primitive(typeof<Bit0>, None, false)
| JsonValue.Number 1M when inferTypesFromValues -> InferedType.Primitive(typeof<Bit1>, None, false)
| JsonValue.Number n when inferTypesFromValues && inRange Int32.MinValue Int32.MaxValue n && isInteger n -> InferedType.Primitive(typeof<int>, None, false)
| JsonValue.Number n when inferTypesFromValues && inRange Int64.MinValue Int64.MaxValue n && isInteger n -> InferedType.Primitive(typeof<int64>, None, false)
| JsonValue.Number n when inferTypesFromValues && inRangeDecimal Int32.MinValue Int32.MaxValue n && isIntegerDecimal n -> InferedType.Primitive(typeof<int>, None, false)
| JsonValue.Number n when inferTypesFromValues && inRangeDecimal Int64.MinValue Int64.MaxValue n && isIntegerDecimal n -> InferedType.Primitive(typeof<int64>, None, false)
| JsonValue.Number _ -> InferedType.Primitive(typeof<decimal>, None, false)
| JsonValue.Float f when inferTypesFromValues && inRangeFloat Int32.MinValue Int32.MaxValue f && isIntegerFloat f -> InferedType.Primitive(typeof<int>, None, false)
| JsonValue.Float f when inferTypesFromValues && inRangeFloat Int64.MinValue Int64.MaxValue f && isIntegerFloat f -> InferedType.Primitive(typeof<int64>, None, false)
| JsonValue.Float _ -> InferedType.Primitive(typeof<float>, None, false)
// More interesting types
| JsonValue.Array ar -> StructuralInference.inferCollectionType (*allowEmptyValues*)false (Seq.map (inferType inferTypesFromValues cultureInfo (NameUtils.singularize parentName)) ar)
Expand Down
11 changes: 11 additions & 0 deletions tests/FSharp.Data.Tests/JsonProvider.fs
Original file line number Diff line number Diff line change
Expand Up @@ -675,3 +675,14 @@ let ``Whitespace is preserved``() =
let ``Getting a decimal at runtime when an integer was inferred should throw``() =
let json = JsonProvider<"""{ "x" : 0.500, "y" : 0.000 }""">.Parse("""{ "x" : -0.250, "y" : 0.800 }""")
(fun () -> json.Y) |> shouldThrow "Expecting a Int32 at '/y', got 0.800"

[<Test>]
let ``Getting a large decimal at runtime when an integer was inferred should throw``() =
let json = JsonProvider<"""{ "x" : 0.500, "y" : 0.000 }""">.Parse("""{ "x" : -0.250, "y" : 12345678901234567890 }""")
(fun () -> json.Y) |> shouldThrow "Expecting a Int32 at '/y', got 12345678901234567890"

[<Test>]
let ``Getting a large float at runtime when an integer was inferred should throw``() =
let f = 1234567890123456789012345678901234567890.
let json = JsonProvider<"""{ "x" : 0.500, "y" : 0.000 }""">.Parse("""{ "x" : -0.250, "y" : 1234567890123456789012345678901234567890 }""")
(fun () -> json.Y) |> shouldThrow (sprintf "Expecting a Int32 at '/y', got %s" (f.ToString "E14"))

0 comments on commit 8d05e4b

Please sign in to comment.