on master help sign in
Merge pull request #442 from phadej/issue-317

Fix #317: Do not truncate or under/overflow non-integral numbers

commit9fcb4a023a240d7baf5df11058d04cc13aa7b6a4 authorAdam Bergmark <adam@bergmark.nl> dateThu 7 Jul 2016, 15:40 (10 years ago) parentfb484e4a14fb9f
3 files changed+70−38
Data/Aeson/Types/FromJSON.hs+51−25 changelog.md+3 tests/UnitTests.hs+16−13
Data/Aeson/Types/FromJSON.hs+51−25
@@ -178,18 +178,44 @@ parseRealFloat _ Null = pure (0/0)
178178parseRealFloat expected v = typeMismatch expected v
179179{-# INLINE parseRealFloat #-}
180180
181+parseIntegralFromScientific :: forall a. Integral a => String -> Scientific -> Parser a
182+parseIntegralFromScientific expected s =
183+ case Scientific.floatingOrInteger s :: Either Double a of
184+ Right x -> pure x
185+ Left _ -> fail $ "Floating number specified for " ++ expected ++ ": " ++ show s
186+{-# INLINE parseIntegralFromScientific #-}
187+
181188parseIntegral :: Integral a => String -> Value -> Parser a
182−parseIntegral expected = withScientific expected $ pure . truncate
189+parseIntegral expected =
190+ withScientific expected $ parseIntegralFromScientific expected
183191{-# INLINE parseIntegral #-}
184192
193+parseBoundedIntegralFromScientific :: (Bounded a, Integral a) => String -> Scientific -> Parser a
194+parseBoundedIntegralFromScientific expected s = maybe
195+ (fail $ expected ++ " is either floating or will cause over or underflow: " ++ show s)
196+ pure
197+ (Scientific.toBoundedInteger s)
198+{-# INLINE parseBoundedIntegralFromScientific #-}
199+
200+parseBoundedIntegral :: (Bounded a, Integral a) => String -> Value -> Parser a
201+parseBoundedIntegral expected =
202+ withScientific expected $ parseBoundedIntegralFromScientific expected
203+{-# INLINE parseBoundedIntegral #-}
204+
185205parseScientificText :: Text -> Parser Scientific
186206parseScientificText
187207 = either fail pure
188208 . A.parseOnly (A.scientific <* A.endOfInput)
189209 . T.encodeUtf8
190210
191−parseIntegralText :: Integral a => Text -> Parser a
192−parseIntegralText t = truncate <$> parseScientificText t
211+parseIntegralText :: Integral a => String -> Text -> Parser a
212+parseIntegralText expected t =
213+ parseScientificText t >>= parseIntegralFromScientific expected
214+{-# INLINE parseIntegralText #-}
215+
216+parseBoundedIntegralText :: (Bounded a, Integral a) => String -> Text -> Parser a
217+parseBoundedIntegralText expected t =
218+ parseScientificText t >>= parseBoundedIntegralFromScientific expected
193219
194220parseOptionalFieldWith :: (Value -> Parser (Maybe a))
195221 -> Object -> Text -> Parser (Maybe a)
@@ -1115,22 +1141,22 @@ instance HasResolution a => FromJSON (Fixed a) where
11151141 {-# INLINE parseJSON #-}
11161142
11171143instance FromJSON Int where
1118− parseJSON = parseIntegral "Int"
1144+ parseJSON = parseBoundedIntegral "Int"
11191145 {-# INLINE parseJSON #-}
11201146
11211147instance FromJSONKey Int where
1122− fromJSONKey = FromJSONKeyTextParser parseIntegralText
1148+ fromJSONKey = FromJSONKeyTextParser $ parseBoundedIntegralText "Int"
11231149
11241150-- | /WARNING:/ Only parse Integers from trusted input since an
11251151-- attacker could easily fill up the memory of the target system by
11261152-- specifying a scientific number with a big exponent like
11271153-- @1e1000000000@.
11281154instance FromJSON Integer where
1129− parseJSON = withScientific "Integral" $ pure . truncate
1155+ parseJSON = parseIntegral "Integer"
11301156 {-# INLINE parseJSON #-}
11311157
11321158instance FromJSONKey Integer where
1133− fromJSONKey = FromJSONKeyTextParser parseIntegralText
1159+ fromJSONKey = FromJSONKeyTextParser $ parseIntegralText "Integer"
11341160
11351161instance FromJSON Natural where
11361162 parseJSON = withScientific "Natural" $ \s ->
@@ -1145,67 +1171,67 @@ instance FromJSONKey Natural where
11451171 else pure $ truncate s
11461172
11471173instance FromJSON Int8 where
1148− parseJSON = parseIntegral "Int8"
1174+ parseJSON = parseBoundedIntegral "Int8"
11491175 {-# INLINE parseJSON #-}
11501176
11511177instance FromJSONKey Int8 where
1152− fromJSONKey = FromJSONKeyTextParser parseIntegralText
1178+ fromJSONKey = FromJSONKeyTextParser $ parseBoundedIntegralText "Int8"
11531179
11541180instance FromJSON Int16 where
1155− parseJSON = parseIntegral "Int16"
1181+ parseJSON = parseBoundedIntegral "Int16"
11561182 {-# INLINE parseJSON #-}
11571183
11581184instance FromJSONKey Int16 where
1159− fromJSONKey = FromJSONKeyTextParser parseIntegralText
1185+ fromJSONKey = FromJSONKeyTextParser $ parseBoundedIntegralText "Int16"
11601186
11611187instance FromJSON Int32 where
1162− parseJSON = parseIntegral "Int32"
1188+ parseJSON = parseBoundedIntegral "Int32"
11631189 {-# INLINE parseJSON #-}
11641190
11651191instance FromJSONKey Int32 where
1166− fromJSONKey = FromJSONKeyTextParser parseIntegralText
1192+ fromJSONKey = FromJSONKeyTextParser $ parseBoundedIntegralText "Int32"
11671193
11681194instance FromJSON Int64 where
1169− parseJSON = parseIntegral "Int64"
1195+ parseJSON = parseBoundedIntegral "Int64"
11701196 {-# INLINE parseJSON #-}
11711197
11721198instance FromJSONKey Int64 where
1173− fromJSONKey = FromJSONKeyTextParser parseIntegralText
1199+ fromJSONKey = FromJSONKeyTextParser $ parseBoundedIntegralText "Int64"
11741200
11751201instance FromJSON Word where
1176− parseJSON = parseIntegral "Word"
1202+ parseJSON = parseBoundedIntegral "Word"
11771203 {-# INLINE parseJSON #-}
11781204
11791205instance FromJSONKey Word where
1180− fromJSONKey = FromJSONKeyTextParser parseIntegralText
1206+ fromJSONKey = FromJSONKeyTextParser $ parseBoundedIntegralText "Word"
11811207
11821208instance FromJSON Word8 where
1183− parseJSON = parseIntegral "Word8"
1209+ parseJSON = parseBoundedIntegral "Word8"
11841210 {-# INLINE parseJSON #-}
11851211
11861212instance FromJSONKey Word8 where
1187− fromJSONKey = FromJSONKeyTextParser parseIntegralText
1213+ fromJSONKey = FromJSONKeyTextParser $ parseBoundedIntegralText "Word8"
11881214
11891215instance FromJSON Word16 where
1190− parseJSON = parseIntegral "Word16"
1216+ parseJSON = parseBoundedIntegral "Word16"
11911217 {-# INLINE parseJSON #-}
11921218
11931219instance FromJSONKey Word16 where
1194− fromJSONKey = FromJSONKeyTextParser parseIntegralText
1220+ fromJSONKey = FromJSONKeyTextParser $ parseBoundedIntegralText "Word16"
11951221
11961222instance FromJSON Word32 where
1197− parseJSON = parseIntegral "Word32"
1223+ parseJSON = parseBoundedIntegral "Word32"
11981224 {-# INLINE parseJSON #-}
11991225
12001226instance FromJSONKey Word32 where
1201− fromJSONKey = FromJSONKeyTextParser parseIntegralText
1227+ fromJSONKey = FromJSONKeyTextParser $ parseBoundedIntegralText "Word32"
12021228
12031229instance FromJSON Word64 where
1204− parseJSON = parseIntegral "Word64"
1230+ parseJSON = parseBoundedIntegral "Word64"
12051231 {-# INLINE parseJSON #-}
12061232
12071233instance FromJSONKey Word64 where
1208− fromJSONKey = FromJSONKeyTextParser parseIntegralText
1234+ fromJSONKey = FromJSONKeyTextParser $ parseBoundedIntegralText "Word64"
12091235
12101236
12111237instance FromJSON Text where
changelog.md+3
@@ -11,6 +11,9 @@ For the latest version of this document, please see [https://github.com/bos/aeso
1111* Modified instances for `Map` and `HashMap`. Introduced new `FromJSONKey` and
1212 `ToJSONKey` type classes.
1313
14+* Integral `FromJSON` instances do not accept non integral JSON numbers (e.g. `3.14`).
15+ Also over/underflows are catched for bounded types.
16+
1417* Added `Data.Aeson.Encoding` with functions to work with `Encoding` in an abstract way.
1518
1619* Export `decodeWith`, `decodeStrictWith`, `eitherDecodeWith`, and
tests/UnitTests.hs+16−13
@@ -86,7 +86,7 @@ tests = testGroup "unit" [
8686 ]
8787 , testGroup ".:, .:?, .:!" $ fmap (testCase "-") dotColonMark
8888 , testGroup "To JSON representation" $ fmap assertJsonEncodingExample jsonEncodingExamples
89− , testGroup "From JSON representation" $ fmap assertJsonDecodingExample jsonDecodingExamples
89+ , testGroup "From JSON representation" $ fmap assertJsonExample jsonDecodingExamples
9090 , testGroup "To/From JSON representation" $ fmap assertJsonExample jsonExamples
9191 , testGroup "JSONPath" $ fmap (testCase "-") jsonPath
9292 , testGroup "Hashable laws" $ fmap (testCase "-") hashableLaws
@@ -255,21 +255,24 @@ data Example where
255255 Example
256256 :: (Eq a, Show a, ToJSON a, FromJSON a)
257257 => String -> L.ByteString -> a -> Example
258+ MaybeExample
259+ :: (Eq a, Show a, FromJSON a)
260+ => String -> L.ByteString -> Maybe a -> Example
258261
259262assertJsonExample :: Example -> Test
260263assertJsonExample (Example name bs val) = testCase name $ do
261264 assertEqual "encode" bs (encode val)
262265 assertEqual "encode/via value" bs (encode $ toJSON val)
263266 assertEqual "decode" (Just val) (decode bs)
267+assertJsonExample (MaybeExample name bs mval) = testCase name $
268+ assertEqual "decode" mval (decode bs)
264269
265270assertJsonEncodingExample :: Example -> Test
266271assertJsonEncodingExample (Example name bs val) = testCase name $ do
267272 assertEqual "encode" bs (encode val)
268273 assertEqual "encode/via value" bs (encode $ toJSON val)
269−
270−assertJsonDecodingExample :: Example -> Test
271−assertJsonDecodingExample (Example name bs val) = testCase name $
272− assertEqual "decode" (Just val) (decode bs)
274+assertJsonEncodingExample (MaybeExample name _ _) = testCase name $
275+ assertFailure "cannot encode MaybeExample"
273276
274277jsonEncodingExamples :: [Example]
275278jsonEncodingExamples =
@@ -285,16 +288,16 @@ jsonDecodingExamples :: [Example]
285288jsonDecodingExamples = [
286289 -- Maybe serialising is lossy
287290 -- https://github.com/bos/aeson/issues/376
288− Example "Nothing" "null" (Nothing :: Maybe Int)
289− , Example "Just" "1" (Just 1 :: Maybe Int)
290− , Example "Just Nothing" "null" (Nothing :: Maybe (Maybe Int))
291+ MaybeExample "Nothing" "null" (Just $ Nothing :: Maybe (Maybe Int))
292+ , MaybeExample "Just" "1" (Just $ Just 1 :: Maybe (Maybe Int))
293+ , MaybeExample "Just Nothing" "null" (Just $ Nothing :: Maybe (Maybe (Maybe Int)))
291294 -- Integral values are truncated, and overflowed
292295 -- https://github.com/bos/aeson/issues/317
293− , Example "Word8 3" "3" (Just 3 :: Maybe Word8)
294− , Example "Word8 3.00" "3.00" (Just 3 :: Maybe Word8)
295− , Example "Word8 3.14" "3.14" (Just 3 :: Maybe Word8) -- TODO: should be Nothing
296− , Example "Word8 -1" "-1" (Just 255 :: Maybe Word8) -- TODO: should be Nothing
297− , Example "Word8 300" "300" (Just 44 :: Maybe Word8) -- TODO: should be Nothing
296+ , MaybeExample "Word8 3" "3" (Just 3 :: Maybe Word8)
297+ , MaybeExample "Word8 3.00" "3.00" (Just 3 :: Maybe Word8)
298+ , MaybeExample "Word8 3.14" "3.14" (Nothing :: Maybe Word8)
299+ , MaybeExample "Word8 -1" "-1" (Nothing :: Maybe Word8)
300+ , MaybeExample "Word8 300" "300" (Nothing :: Maybe Word8)
298301 ]
299302
300303jsonExamples :: [Example]