@@ -1,53 +1,27 @@
1{-# LANGUAGE DeriveDataTypeable #-}
1{-# LANGUAGE DeriveDataTypeable #-}
2+{-# OPTIONS_HADDOCK hide #-}
2-----------------------------------------------------------------------------
3-----------------------------------------------------------------------------
4-- Module : Data.Time.LocalTime.TimeZone.Olson.Parse
5-- Module : Data.Time.LocalTime.TimeZone.Olson.Parse
5−-- Copyright : Yitzchak Gale 2010
6+-- Copyright : Yitzchak Gale 2018
7-- Maintainer : Yitzchak Gale <gale@sefer.org>
8-- Maintainer : Yitzchak Gale <gale@sefer.org>
8-- Portability : portable
9-- Portability : portable
10−-- A parser for binary Olson timezone files whose format are specified
11+-- A parser for binary Olson timezone files whose format is specified
11-- by the tzfile(5) man page on Unix-like systems. For more
12-- by the tzfile(5) man page on Unix-like systems. For more
12-- information about this format, see
13-- information about this format, see
13−-- http://www.twinsun.com/tz/tz-link.htm. Functions are provided for
14+-- <http://www.twinsun.com/tz/tz-link.htm>. Functions are provided for
14−-- converting the parsed data into @TimeZoneSeries@ and @TimeZone@
15+-- converting the parsed data into 'TimeZoneSeries' objects.
18−Copyright (c) 2010 Yitzchak Gale. All rights reserved.
17+{- Copyright (c) 2018 Yitzchak Gale. All rights reserved.
19−
18+For licensing information, see the BSD3-style license in the file
20−Redistribution and use in source and binary forms, with or without
19+LICENSE that was originally distributed by the author together with
21−modification, are permitted provided that the following conditions are
20+this file. -}
24− * Redistributions of source code must retain the above copyright
25− notice, this list of conditions and the following disclaimer.
27− * Redistributions in binary form must reproduce the above
28− copyright notice, this list of conditions and the following
29− disclaimer in the documentation and/or other materials provided
30− with the distribution.
32− * Neither the name of Yitzchak Gale nor the names of other
33− contributors may be used to endorse or promote products derived
34− from this software without specific prior written permission.
36−THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
37−"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
38−LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
39−A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
40−OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
41−SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
42−LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
43−DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
44−THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
45−(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
46−OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
47−----------------------------------------------------------------------------}
49module Data.Time.LocalTime.TimeZone.Olson.Parse
22module Data.Time.LocalTime.TimeZone.Olson.Parse
24+ -- * Parsing Olson timezone files
51 getTimeZoneSeriesFromOlsonFile,
25 getTimeZoneSeriesFromOlsonFile,
52 getOlsonFromFile,
26 getOlsonFromFile,
53 olsonToTimeZoneSeries,
27 olsonToTimeZoneSeries,
@@ -65,11 +39,14 @@ import Data.Binary.Get (Get, runGet, getWord8, getWord32be, getWord64be,
65import qualified Data.ByteString as B
39import qualified Data.ByteString as B
66import qualified Data.ByteString.Lazy as L
40import qualified Data.ByteString.Lazy as L
67import Data.Monoid (mappend)
41import Data.Monoid (mappend)
42+import Data.List (sortBy, groupBy)
68import Data.Maybe (listToMaybe, fromMaybe)
43import Data.Maybe (listToMaybe, fromMaybe)
69import Data.Word (Word8)
44import Data.Word (Word8)
70import Data.Int (Int32, Int64)
45import Data.Int (Int32, Int64)
46+import Data.Ord (comparing)
47+import Data.Function (on)
71import Data.Typeable (Typeable)
48import Data.Typeable (Typeable)
72−import Control.Monad (guard, replicateM, replicateM_)
49+import Control.Monad (guard, replicateM, replicateM_, when)
73import Control.Exception.Extensible (try, throw, Exception, ErrorCall)
50import Control.Exception.Extensible (try, throw, Exception, ErrorCall)
75-- | An exception indicating that the binary data being parsed was not
52-- | An exception indicating that the binary data being parsed was not
@@ -83,8 +60,8 @@ instance Exception OlsonError
83-- | Convert parsed Olson timezone data into a @TimeZoneSeries@.
60-- | Convert parsed Olson timezone data into a @TimeZoneSeries@.
84olsonToTimeZoneSeries :: OlsonData -> Maybe TimeZoneSeries
61olsonToTimeZoneSeries :: OlsonData -> Maybe TimeZoneSeries
85olsonToTimeZoneSeries (OlsonData ttimes ttinfos@(dflt0:_) _ _) =
62olsonToTimeZoneSeries (OlsonData ttimes ttinfos@(dflt0:_) _ _) =
86− fmap (TimeZoneSeries $ mkTZ dflt) . mapM (lookupTZ ttinfos) $
63+ fmap (TimeZoneSeries $ mkTZ dflt) . mapM (lookupTZ ttinfos) .
87− reverse ttimes
64+ uniqTimes . sortBy futureToPast $ ttimes
89 dflt = fromMaybe dflt0 . listToMaybe $ filter isStd ttinfos
66 dflt = fromMaybe dflt0 . listToMaybe $ filter isStd ttinfos
90 isStd (TtInfo _ isdst _ _) = not isdst
67 isStd (TtInfo _ isdst _ _) = not isdst
@@ -93,53 +70,74 @@ olsonToTimeZoneSeries (OlsonData ttimes ttinfos@(dflt0:_) _ _) =
93 lookupTZ ttinfos ttime = fmap (((,) $ toUTC ttime) . mkTZ) . listToMaybe $
70 lookupTZ ttinfos ttime = fmap (((,) $ toUTC ttime) . mkTZ) . listToMaybe $
94 drop (transIndex ttime) ttinfos
71 drop (transIndex ttime) ttinfos
95 toUTC = posixSecondsToUTCTime . fromIntegral . transTime
72 toUTC = posixSecondsToUTCTime . fromIntegral . transTime
73+ uniqTimes = map last . groupBy ((==) `on` transTime)
74+ futureToPast = comparing $ negate . transTime
96olsonToTimeZoneSeries _ = Nothing
75olsonToTimeZoneSeries _ = Nothing
98-- | Read timezone data from a binary Olson timezone file and convert
77-- | Read timezone data from a binary Olson timezone file and convert
99−-- it into a @TimeZoneSeries@.
78+-- it into a @TimeZoneSeries@ for use together with the types and
79+-- fucntions of "Data.Time". This is the function from this module
80+-- for which you are most likely to have use.
82+-- If the values in the Olson timezone file exceed the standard size
83+-- limits (see 'defaultLimits'), this function throws an
84+-- exception. For other behavior, use 'getOlson' and
85+-- 'Data.Binary.Get.runGet' directly.
100getTimeZoneSeriesFromOlsonFile :: FilePath -> IO TimeZoneSeries
86getTimeZoneSeriesFromOlsonFile :: FilePath -> IO TimeZoneSeries
101getTimeZoneSeriesFromOlsonFile fp = getOlsonFromFile fp >>=
87getTimeZoneSeriesFromOlsonFile fp = getOlsonFromFile fp >>=
102 maybe (throwOlson fp "no timezone found in OlsonData") return .
88 maybe (throwOlson fp "no timezone found in OlsonData") return .
103 olsonToTimeZoneSeries
89 olsonToTimeZoneSeries
105-- | Parse a binary Olson timezone file.
91-- | Parse a binary Olson timezone file.
93+-- If the values in the Olson timezone file exceed the standard size
94+-- limits (see 'defaultLimits'), this function throws an
95+-- exception. For other behavior, use 'getOlson' and
96+-- 'Data.Binary.Get.runGet' directly.
106getOlsonFromFile :: FilePath -> IO OlsonData
97getOlsonFromFile :: FilePath -> IO OlsonData
107getOlsonFromFile fp = do
98getOlsonFromFile fp = do
108− e <- try . fmap (runGet getOlson) $ L.readFile fp
99+ e <- try . fmap (runGet $ getOlson defaultLimits) $ L.readFile fp
109 either (formatError fp) return e
100 either (formatError fp) return e
111formatError :: FilePath -> ErrorCall -> IO a
102formatError :: FilePath -> ErrorCall -> IO a
112formatError fp e = throwOlson fp $ show e
103formatError fp e = throwOlson fp $ show e
114-- | A binary parser for binary Olson timezone files
105-- | A binary parser for binary Olson timezone files
115−getOlson :: Get OlsonData
106+getOlson :: SizeLimits -> Get OlsonData
116−getOlson = do
107+getOlson limits = do
117− getByteString 4
108+ (version, part1) <- getOlsonPart True limits get32bitInteger
118− >>= verify ((== "TZif") . toASCII . B.unpack) "missing magic number"
120− >>= verify (`elem` [0, 50]) "invalid version number"
121− replicateM_ 15 getWord8 -- padding nulls
122− getOlsonParts version
124−-- There is one part for Version 1 format data, and two parts and a POSIX
109+ -- There is one part for Version 1 format data, and two parts and a POSIX
125−-- TZ string for Version 2 format data
110+ -- TZ string for Version 2 or 3 format data
126−getOlsonParts :: Word8 -> Get OlsonData
111+ case () of
127−getOlsonParts 0 = getOlsonPart get32bitInteger
112+ _ | version == 0 -> return part1
128−getOlsonParts 50 = do part1 <- getOlsonPart get32bitInteger
113+ | version == 50 || version == 51 -> do
129− part2 <- getOlsonPart get64bitInteger
114+ (_, part2) <- getOlsonPart False limits get64bitInteger
130− posixTZ <- getPosixTZ
115+ posixTZ <- getPosixTZ
131− return $ part1 `mappend` part2 `mappend` posixTZ
116+ return $ part1 `mappend` part2 `mappend` posixTZ
132−getOlsonParts _ = error "Invalid version number" -- never reached
117+ | otherwise -> do
118+ let msg = "getOlson: invalid tzfile version " ++ toASCII [version]
119+ verify (const False) msg undefined
134-- Parse the part of an Olson file that contains timezone data
121-- Parse the part of an Olson file that contains timezone data
135−getOlsonPart :: Integral a => Get a -> Get OlsonData
122+getOlsonPart :: Integral a => Bool -> SizeLimits -> Get a ->
123+ Get (Word8, OlsonData)
136−getOlsonPart getTime = do
124+getOlsonPart verifyMagic limits getTime = do
125+ magic <- fmap (toASCII . B.unpack) $ getByteString 4
126+ when verifyMagic $ verify_ (== "TZif") "missing magic number" magic
128+ replicateM_ 15 getWord8 -- padding nulls
137 tzh_ttisgmtcnt <- get32bitInt
129 tzh_ttisgmtcnt <- get32bitInt
138 tzh_ttisstdcnt <- get32bitInt
130 tzh_ttisstdcnt <- get32bitInt
139 tzh_leapcnt <- get32bitInt
131 tzh_leapcnt <- get32bitInt
132+ >>= verify (withinLimit maxLeaps) "too many leap second specifications"
140 tzh_timecnt <- get32bitInt
133 tzh_timecnt <- get32bitInt
134+ >>= verify (withinLimit maxTimes) "too many timezone transitions"
141 tzh_typecnt <- get32bitInt
135 tzh_typecnt <- get32bitInt
136+ >>= verify (withinLimit maxTypes) "too many timezone type specifications"
137+ verify (withinLimit maxTypes) "too many isgmt specifiers" tzh_ttisgmtcnt
138+ verify (withinLimit maxTypes) "too many isstd specifiers" tzh_ttisstdcnt
142 tzh_charcnt <- get32bitInt
139 tzh_charcnt <- get32bitInt
140+ >>= verify (withinLimit maxAbbrChars) "too many tilezone specifiers"
143 times <- fmap (map toInteger) $ replicateM tzh_timecnt getTime
141 times <- fmap (map toInteger) $ replicateM tzh_timecnt getTime
144 indexes <- replicateM tzh_timecnt get8bitInt
142 indexes <- replicateM tzh_timecnt get8bitInt
145 ttinfos <- replicateM tzh_typecnt getTtInfo
143 ttinfos <- replicateM tzh_typecnt getTtInfo
@@ -147,22 +145,32 @@ getOlsonPart getTime = do
147 leaps <- replicateM tzh_leapcnt $ getLeapInfo getTime
145 leaps <- replicateM tzh_leapcnt $ getLeapInfo getTime
148 isstds <- replicateM tzh_ttisstdcnt getBool
146 isstds <- replicateM tzh_ttisstdcnt getBool
149 isgmts <- replicateM tzh_ttisgmtcnt getBool
147 isgmts <- replicateM tzh_ttisgmtcnt getBool
150− return $ OlsonData
150+ OlsonData
151− (zipWith Transition times indexes)
151+ (zipWith Transition times indexes)
152− (map (flip lookupAbbr abbr_chars) . zipWith setTtype ttinfos .
152+ (map (flip lookupAbbr abbr_chars) . zipWith setTtype ttinfos $
153− (++ repeat UnknownType) $
154− zipWith boolsToTType (map Just isstds ++ repeat Nothing) isgmts)
153+ zipWithExtend boolsToTType False False isstds isgmts)
158+ withinLimit limit value = maybe True (value <=) $ limit limits
158 lookupAbbr (TtInfo gmtoff isdst ttype abbrind) =
159 lookupAbbr (TtInfo gmtoff isdst ttype abbrind) =
159 TtInfo gmtoff isdst ttype . takeWhile (/= '\NUL') . drop abbrind
160 TtInfo gmtoff isdst ttype . takeWhile (/= '\NUL') . drop abbrind
160 setTtype ttinfo ttype = ttinfo {tt_ttype = ttype}
161 setTtype ttinfo ttype = ttinfo {tt_ttype = ttype}
161− boolsToTType _ isgmt | isgmt = UTC
162+ boolsToTType _ isgmt | isgmt = UTC
162− boolsToTType isstd _ = maybe UnknownType stdOrWall isstd
163+ boolsToTType isstd _
164− | isstd = Std
164+ | isstd = Std
165− | otherwise = Wall
165+ | otherwise = Wall
167+-- A variant of zipWith whose result is the length of the longer
168+-- rather than the shorter list, by extending the shorter list with
170+zipWithExtend :: (a -> b -> c) -> a -> b -> [a] -> [b] -> [c]
171+zipWithExtend f x0 y0 (x:xs) (y:ys) = f x y : zipWithExtend f x0 y0 xs ys
172+zipWithExtend f x0 _ [] ys = map (f x0) ys
173+zipWithExtend f _ y0 xs _ = map (flip f y0) xs
167-- Parse a POSIX-style TZ string.
175-- Parse a POSIX-style TZ string.
168-- We don't try to understand the TZ string, we just pass it along whole.
176-- We don't try to understand the TZ string, we just pass it along whole.
@@ -183,7 +191,7 @@ getTtInfo = do
183 gmtoff <- get32bitInt
191 gmtoff <- get32bitInt
184 isdst <- getBool
192 isdst <- getBool
185 abbrind <- get8bitInt
193 abbrind <- get8bitInt
186− return $ TtInfo gmtoff isdst UnknownType abbrind
194+ return $ TtInfo gmtoff isdst Wall abbrind
188-- Parse leap second info. (usually not used)
196-- Parse leap second info. (usually not used)
189getLeapInfo :: Integral a => Get a -> Get LeapInfo
197getLeapInfo :: Integral a => Get a -> Get LeapInfo
@@ -226,6 +234,11 @@ verify pred msg val
226 | pred val = return val
234 | pred val = return val
227 | otherwise = error msg
235 | otherwise = error msg
237+verify_ :: Monad m => (a -> Bool) -> String -> a -> m ()
239+ | pred val = return ()
240+ | otherwise = error msg
229throwOlson :: FilePath -> String -> IO a
242throwOlson :: FilePath -> String -> IO a
230throwOlson fp msg = throw . OlsonError $
243throwOlson fp msg = throw . OlsonError $
231 fp ++ ": invalid timezone file: " ++ msg
244 fp ++ ": invalid timezone file: " ++ msg