diff --git a/changelog.md b/changelog.md index b3ea3a7..b474222 100644 --- a/changelog.md +++ b/changelog.md @@ -5,6 +5,7 @@ - fix gregorianPaschalMoon and gregorianEaster - fix negative rollover differences in diffGregorianDurationRollOver and diffJulianDurationRollOver - fix case-insensitive parsing for AM/PM markers, UTC, and known timezone names +- fix timezone offset parsing of malformed minutes and ISO8601 offset ranges - fix Unix timezone fallback offset sign and DST abbreviation - fix JavaScript getTimeZone offsets and summer flag around DST transitions - fix numeric parsers accepting overflowing bounded values diff --git a/lib/Data/Time/Format/ISO8601.hs b/lib/Data/Time/Format/ISO8601.hs index 1459b68..093a999 100644 --- a/lib/Data/Time/Format/ISO8601.hs +++ b/lib/Data/Time/Format/ISO8601.hs @@ -282,22 +282,28 @@ withUTCDesignator f = f <** specialCaseReadFormat ((), "") (literalFormat "Z") timeOffsetFormat :: FormatExtension -> Format TimeZone timeOffsetFormat fe = let - toTimeZone (sign, ehm) = - minutesToTimeZone $ - sign * case ehm of - Left h -> h * 60 - Right (h, m) -> h * 60 + m - fromTimeZone tz = + toOffsetMinutes h m + | h < 0 || h > 24 = Nothing + | m < 0 || m > 59 = Nothing + | h == 24 && m /= 0 = Nothing + | otherwise = Just $ h * 60 + m + toTimeZone (sign, ehm) = do + offset <- + case ehm of + Left h -> toOffsetMinutes h 0 + Right (h, m) -> toOffsetMinutes h m + return $ minutesToTimeZone $ sign * offset + fromTimeZone tz = do let mm = timeZoneMinutes tz (h, m) = quotRem (abs mm) 60 - in - (signum mm, Right (h, m)) + _ <- toOffsetMinutes h m + return (signum mm, Right (h, m)) digits2 = integerFormat NoSign (Just 2) in specialCaseReadFormat (utc, "") $ specialCaseReadFormat (utc, "Z") $ - isoMap toTimeZone fromTimeZone $ + mapMFormat toTimeZone fromTimeZone $ mandatorySignFormat <**> (digits2 <++> extColonFormat fe digits2 digits2) -- | @hh:mm:ss±hh:mm@ (extended), @hhmmss±hhmm@ (basic) [ISO 8601:2004(E) sec. 4.2.5.2] diff --git a/lib/Data/Time/Format/Parse/Instances.hs b/lib/Data/Time/Format/Parse/Instances.hs index 53f2ba5..e7b20b5 100644 --- a/lib/Data/Time/Format/Parse/Instances.hs +++ b/lib/Data/Time/Format/Parse/Instances.hs @@ -577,7 +577,7 @@ readTzOffset str = calc s h1 h2 m1 m2 = do sign <- getSign s h <- readMaybe [h1, h2] - m <- readMaybe [m1, m2] + m <- clipValid 0 59 =<< readMaybe [m1, m2] return $ sign * (60 * h + m) in case str of diff --git a/test/main/Test/Format/ISO8601.hs b/test/main/Test/Format/ISO8601.hs index 71e4a4c..66316ef 100644 --- a/test/main/Test/Format/ISO8601.hs +++ b/test/main/Test/Format/ISO8601.hs @@ -163,6 +163,12 @@ testReadFormat name fmt str val = nameTest (name ++ ": " ++ str) $ assertEqual " testReadFormatFails :: (Show t, Eq t) => String -> Format t -> String -> TestTree testReadFormatFails name fmt str = nameTest (name ++ ": " ++ str) $ assertEqual "" Nothing $ formatParseM fmt str +testReadFormatFails :: (Show t, Eq t) => String -> Format t -> String -> TestTree +testReadFormatFails name fmt str = nameTest (name ++ ": " ++ str) $ assertEqual "" Nothing $ formatParseM fmt str + +testShowFormatFails :: Show t => String -> Format t -> t -> TestTree +testShowFormatFails name fmt val = nameTest (name ++ ": " ++ show val) $ assertEqual "" Nothing $ formatShowM fmt val + testShowFormats :: TestTree testShowFormats = nameTest @@ -260,6 +266,18 @@ testShowFormats = , testReadFormat "timeOffsetFormat" (timeOffsetFormat BasicFormat) "+00" (minutesToTimeZone 0) , testReadFormat "timeOffsetFormat" (timeOffsetFormat BasicFormat) "-0000" (minutesToTimeZone 0) , testReadFormat "timeOffsetFormat" (timeOffsetFormat BasicFormat) "-00" (minutesToTimeZone 0) + , testShowReadFormat "timeOffsetFormat" iso8601Format "+24:00" (minutesToTimeZone 1440) + , testReadFormat "timeOffsetFormat" iso8601Format "+24" (minutesToTimeZone 1440) + , testShowReadFormat "timeOffsetFormat" (timeOffsetFormat BasicFormat) "-2400" (minutesToTimeZone (-1440)) + , testReadFormat "timeOffsetFormat" (timeOffsetFormat BasicFormat) "-24" (minutesToTimeZone (-1440)) + , testReadFormatFails "timeOffsetFormat" (iso8601Format :: Format TimeZone) "+23:60" + , testReadFormatFails "timeOffsetFormat" (iso8601Format :: Format TimeZone) "+24:01" + , testReadFormatFails "timeOffsetFormat" (iso8601Format :: Format TimeZone) "+25:00" + , testReadFormatFails "timeOffsetFormat" (timeOffsetFormat BasicFormat) "-2360" + , testReadFormatFails "timeOffsetFormat" (timeOffsetFormat BasicFormat) "-2401" + , testReadFormatFails "timeOffsetFormat" (timeOffsetFormat BasicFormat) "-2500" + , testShowFormatFails "timeOffsetFormat" (iso8601Format :: Format TimeZone) (minutesToTimeZone 1441) + , testShowFormatFails "timeOffsetFormat" (timeOffsetFormat BasicFormat) (minutesToTimeZone (-1441)) , testShowReadFormat "timeOffsetFormat" iso8601Format "+00:10" (minutesToTimeZone 10) , testShowReadFormat "timeOffsetFormat" iso8601Format "-00:10" (minutesToTimeZone (-10)) , testShowReadFormat "timeOffsetFormat" iso8601Format "+01:35" (minutesToTimeZone 95) diff --git a/test/main/Test/Format/ParseTime.hs b/test/main/Test/Format/ParseTime.hs index 2289706..0bdfed8 100644 --- a/test/main/Test/Format/ParseTime.hs +++ b/test/main/Test/Format/ParseTime.hs @@ -286,6 +286,8 @@ particularParseTests = "" (Just lowerKnownZone) (parseTimeM False lowerKnownZoneLocale "%Z" "foo") + , parseTest False (Just $ TimeZone 1440 False "") "%z" "+2400" + , parseTest False (Just $ TimeZone 5940 False "") "%z" "+9900" ] where lowerAmPmLocale = defaultTimeLocale{amPm = ("am", "pm")} @@ -298,6 +300,10 @@ badParseTests = "bad" [ parseTest False (Nothing :: Maybe Day) "%Y" "" , parseTest False (Nothing :: Maybe TimeOfDay) "%-H" "18446744073709551616" + , parseTest False (Nothing :: Maybe TimeZone) "%z" "+2360" + , parseTest False (Nothing :: Maybe TimeZone) "%Ez" "+23:60" + , parseTest False (Nothing :: Maybe TimeZone) "%Z" "+2360" + , parseTest False (Nothing :: Maybe TimeZone) "%EZ" "+23:60" ] {-