on master help sign in
Allow FromJSON instance for Ratio a parse numbers (#745)

* Allow FromJSON instance for Ratio a parse numbers * Implement reviewer suggestion to avoid DoS * Remove redundant import Redundant despite all CPP. S.* is never used * Protect against small rationals as well

commitf2306d8df1a8f4519a164dc8f3f09972ab16224d authorAleksey Khudyakov <alexey.skladnoy@gmail.com> dateMon 3 Feb 2020, 11:40 (7 years ago) parent4d3767c
4 files changed+43−7
Data/Aeson/Types/FromJSON.hs+16−6 Data/Aeson/Types/ToJSON.hs−1 changelog.md+5 tests/UnitTests.hs+22
Data/Aeson/Types/FromJSON.hs+16−6
@@ -1541,12 +1541,22 @@ instance FromJSONKey Float where
15411541 _ -> Scientific.toRealFloat <$> parseScientificText t
15421542
15431543instance (FromJSON a, Integral a) => FromJSON (Ratio a) where
1544− parseJSON = withObject "Rational" $ \obj -> do
1545− numerator <- obj .: "numerator"
1546− denominator <- obj .: "denominator"
1547− if denominator == 0
1548− then fail "Ratio denominator was 0"
1549− else pure $ numerator % denominator
1544+ parseJSON (Number x)
1545+ | exp10 <= 1024
1546+ , exp10 >= -1024 = return $! realToFrac x
1547+ | otherwise = prependContext "Ratio" $ fail msg
1548+ where
1549+ exp10 = base10Exponent x
1550+ msg = "found a number with exponent " ++ show exp10
1551+ ++ ", but it must not be greater than 1024 or less than -1024"
1552+ parseJSON o = objParser o
1553+ where
1554+ objParser = withObject "Rational" $ \obj -> do
1555+ numerator <- obj .: "numerator"
1556+ denominator <- obj .: "denominator"
1557+ if denominator == 0
1558+ then fail "Ratio denominator was 0"
1559+ else pure $ numerator % denominator
15501560 {-# INLINE parseJSON #-}
15511561
15521562-- | This instance includes a bounds check to prevent maliciously
Data/Aeson/Types/ToJSON.hs−1
@@ -100,7 +100,6 @@ import GHC.Generics
100100import Numeric.Natural (Natural)
101101import qualified Data.Aeson.Encoding as E
102102import qualified Data.Aeson.Encoding.Internal as E (InArray, comma, econcat, retagEncoding)
103−import qualified Data.ByteString as S
104103import qualified Data.ByteString.Lazy as L
105104import qualified Data.DList as DList
106105import qualified Data.HashMap.Strict as H
changelog.md+5
@@ -1,5 +1,10 @@
11For the latest version of this document, please see [https://github.com/bos/aeson/blob/master/changelog.md](https://github.com/bos/aeson/blob/master/changelog.md).
22
3+### upcoming
4+
5+* `FromJSON` instance of `Ratio a` now parses numbers in addtion to
6+ standard `{numerator=..., denumerator=...}` encoding.
7+
38### 1.4.6.0
49
510* Provide a clearer error message when a required tagKey for a constructor is missing, thanks to Guru Devanla.
tests/UnitTests.hs+22
@@ -114,6 +114,9 @@ tests = testGroup "unit" [
114114 , testCase "SingleFieldCon" singleFieldCon
115115 , testGroup "Ordering of object keys" keyOrdering
116116 , testCase "Ratio with denominator 0" ratioDenominator0
117+ , testCase "Rational parses number" rationalNumber
118+ , testCase "Big rational" bigRationalDecoding
119+ , testCase "Small rational" smallRationalDecoding
117120 , testCase "Big scientific exponent" bigScientificExponent
118121 , testCase "Big integer decoding" bigIntegerDecoding
119122 , testCase "Big natural decading" bigNaturalDecoding
@@ -620,6 +623,25 @@ ratioDenominator0 =
620623 (Left "Error in $: Ratio denominator was 0")
621624 (eitherDecode "{ \"numerator\": 1, \"denominator\": 0 }" :: Either String Rational)
622625
626+rationalNumber :: Assertion
627+rationalNumber =
628+ assertEqual "Ratio with denominator 0"
629+ (Right 1.37)
630+ (eitherDecode "1.37" :: Either String Rational)
631+
632+bigRationalDecoding :: Assertion
633+bigRationalDecoding =
634+ assertEqual "Decoding an Integer with a large exponent should fail"
635+ (Left "Error in $: parsing Ratio failed, found a number with exponent 2000, but it must not be greater than 1024 or less than -1024")
636+ ((eitherDecode :: L.ByteString -> Either String Rational) "1e2000")
637+
638+smallRationalDecoding :: Assertion
639+smallRationalDecoding =
640+ assertEqual "Decoding an Integer with a large exponent should fail"
641+ (Left "Error in $: parsing Ratio failed, found a number with exponent -2000, but it must not be greater than 1024 or less than -1024")
642+ ((eitherDecode :: L.ByteString -> Either String Rational) "1e-2000")
643+
644+
623645bigScientificExponent :: Assertion
624646bigScientificExponent =
625647 assertEqual "Encoding an integral scientific with a large exponent should normalize it"