@@ -2,17 +2,17 @@
2-----------------------------------------------------------------------------
2-----------------------------------------------------------------------------
4-- Module : Data.Time.LocalTime.TimeZone.Olson.Render
4-- Module : Data.Time.LocalTime.TimeZone.Olson.Render
5−-- Copyright : Yitzchak Gale 2010
5+-- Copyright : Yitzchak Gale 2018
7-- Maintainer : Yitzchak Gale <gale@sefer.org>
7-- Maintainer : Yitzchak Gale <gale@sefer.org>
8-- Portability : portable
8-- Portability : portable
10−-- Render Olson timezone data in the standard binary format as
10+-- Render Olson timezone data in the standard binary format as used in
11−-- used in Olson timezone files, and as specified by the
11+-- Olson timezone files, and as specified by the tzfile(5) man page on
12−-- tzfile(5) man page on Unix-like systems. For more information
12+-- Unix-like systems. For more information about this format, see
13−-- about this format, see http://www.twinsun.com/tz/tz-link.htm.
13+-- <http://www.twinsun.com/tz/tz-link.htm>.
15−{- Copyright (c) 2010 Yitzchak Gale. All rights reserved.
15+{- Copyright (c) 2018ZZ Yitzchak Gale. All rights reserved.
16For licensing information, see the BSD3-style license in the file
16For licensing information, see the BSD3-style license in the file
17LICENSE that was originally distributed by the author together with
17LICENSE that was originally distributed by the author together with
18this file. -}
18this file. -}
@@ -25,9 +25,12 @@ module Data.Time.LocalTime.TimeZone.Olson.Render
25 -- timestamp, or if a POSIX-style TZ string is specified, timezone
25 -- timestamp, or if a POSIX-style TZ string is specified, timezone
26 -- data is rendered using Version 2 format. Otherwise, the timezone data
26 -- data is rendered using Version 2 format. Otherwise, the timezone data
27 -- is rendered using Version 1 format.
27 -- is rendered using Version 1 format.
28+ renderTimeZoneSeriesToOlsonFile,
29+ timeZoneSeriesToOlson,
28 renderOlsonToFile,
30 renderOlsonToFile,
30− splitOlson
33+ extractOlsonV1
@@ -44,9 +47,15 @@ import Data.Ord (comparing)
44import Data.Word (Word8)
47import Data.Word (Word8)
45import Data.Maybe (listToMaybe, maybeToList, isNothing, fromMaybe, catMaybes)
48import Data.Maybe (listToMaybe, maybeToList, isNothing, fromMaybe, catMaybes)
46import Data.Monoid (mempty)
49import Data.Monoid (mempty)
47−import Control.Monad (guard, replicateM_)
50+import Control.Monad (guard, replicateM_, unless)
49-- | Render a @TimeZoneSeries@ as a binary Olson timezone file.
52-- | Render a @TimeZoneSeries@ as a binary Olson timezone file.
54+-- If the values in the Olson timezone data exceed the standard size
55+-- limits (see 'defaultLimits'), this function throws an
56+-- exception. For other behavior, use 'timeZoneSeriesToOlson',
57+-- 'verifyOlsonLimits', 'putOlson' and 'Data.Binary.Put.runPut'
50renderTimeZoneSeriesToOlsonFile :: FilePath -> TimeZoneSeries -> IO ()
59renderTimeZoneSeriesToOlsonFile :: FilePath -> TimeZoneSeries -> IO ()
51renderTimeZoneSeriesToOlsonFile fp = renderOlsonToFile fp .
60renderTimeZoneSeriesToOlsonFile fp = renderOlsonToFile fp .
52 fromMaybe (error "Cannot render TimeZoneSeries: default is summer time") .
61 fromMaybe (error "Cannot render TimeZoneSeries: default is summer time") .
@@ -60,86 +69,93 @@ timeZoneSeriesToOlson (TimeZoneSeries dflt pairs)
60 | otherwise = Just $
69 | otherwise = Just $
62 [Transition secs ttinfo |
71 [Transition secs ttinfo |
63− (t, tzs) <- pairs,
72+ (t, tz) <- reverse pairs,
64 let secs = round $ utcTimeToPOSIXSeconds t,
73 let secs = round $ utcTimeToPOSIXSeconds t,
65− ttinfo <- maybeToList $ lookup (mkTT tzs) ttAssocs]
74+ ttinfo <- maybeToList $ lookup (mkTT tz) ttAssocs]
70− mkTT (TimeZone offset issdst abbr) =
79+ mkTT (TimeZone offset isdst abbr) =
71− TtInfo (offset*60) issdst Wall abbr
80+ TtInfo (offset*60) isdst Wall abbr
72 dfltTT = mkTT dflt
81 dfltTT = mkTT dflt
73 ttAssocs = (dfltTT, 0) :
82 ttAssocs = (dfltTT, 0) :
74− zip (filter (/= dfltTT) $ map (mkTT . snd) pairs) [1..]
83+ zip (uniq . sort . filter (/= dfltTT) $ map (mkTT . snd) pairs) [1..]
75 ttinfos = map fst ttAssocs
84 ttinfos = map fst ttAssocs
86+-- | Check whether @OlsonData@ is within size limits.
87+verifyOlsonLimits :: SizeLimits -> OlsonData -> Bool
88+verifyOlsonLimits limits (OlsonData transs ttinfos leaps _) =
89+ withinLimit maxTimes transs &&
90+ withinLimit maxTypes ttinfos &&
91+ withinLimit maxLeaps leaps &&
92+ withinLimit maxAbbrChars abbrChars
94+ withinLimit limit items = maybe True (null . flip drop items) $
96+ abbrChars = concat abbrStrs ++ map (const '\NUL') abbrStrs
97+ abbrStrs = map tt_abbr ttinfos
77-- | Render Olson timezone data as a binary Olson timezone file
99-- | Render Olson timezone data as a binary Olson timezone file
101+-- If the values in the Olson timezone data exceed the standard size
102+-- limits (see 'defaultLimits'), this function throws an
103+-- exception. For other behavior, use 'verifyOlsonLimits', 'putOlson'
104+-- and 'Data.Binary.Put.runPut' directly.
78renderOlsonToFile :: FilePath -> OlsonData -> IO ()
105renderOlsonToFile :: FilePath -> OlsonData -> IO ()
106+renderOlsonToFile fp olson = do
107+ unless (verifyOlsonLimits defaultLimits olson) $
108+ error "Olson timezone data exceeds size limits"
79−renderOlsonToFile fp = L.writeFile fp . runPut . putOlson
109+ L.writeFile fp . runPut . putOlson $ olson
81-- | Render Olson timezone data in binary Olson timezone file format
111-- | Render Olson timezone data in binary Olson timezone file format
82-- as a lazy @ByteString@
112-- as a lazy @ByteString@
83−putOlson :: OlsonData -> Put
113+putOlson olson@(OlsonData _ _ _ posix)
84−putOlson olson = putOlsonParts version olson1 olson2 posix >> flush
114+ | fitsInVersion1 = putOlsonPart 0 put32bitIntegral olson
115+ | otherwise = do putOlsonPart 50 put32bitIntegral olson1
116+ putOlsonPart 50 put64bitIntegral olson
86− (olson1, olson2, posix) = splitOlson olson
119+ olson1 = extractOlsonV1 olson
87− version
120+ fitsInVersion1 = fmap null posix /= Just False &&
88− | olson2 /= mempty = 50
121+ olson1 == olson {olsonPosixTZ = Nothing}
89− | otherwise = maybe 0 (const 50) $ posix >>= guard . not . null
122+
91−-- | Split Olson timezone data into three parts: timezone data that can
92−-- be rendered using Version 1 format, timezone data that can only be
123+-- | Extract Olson timezone data that can be rendered using Version 1 format
93−-- rendered using Version 2 format, and the POSIX-style TZ string
95−splitOlson :: OlsonData -> (OlsonData, OlsonData, Maybe String)
124+extractOlsonV1 :: OlsonData -> OlsonData
125+extractOlsonV1 (OlsonData transs ttinfos leaps _)
96−splitOlson (OlsonData transs ttinfos leaps posix) =
126+ | allV1 = OlsonData transs ttinfos leaps Nothing
97− (OlsonData transs1 ttinfos1 leaps1 Nothing,
127+ | otherwise = OlsonData transs1 ttinfos1 leaps1 Nothing
98− OlsonData transs2 ttinfos2 leaps2 Nothing,
101 cutoff = 0x80000000 -- 2^31
129 cutoff = 0x80000000 -- 2^31
102 fitsIn32bits x = x < cutoff && x >= negate cutoff
130 fitsIn32bits x = x < cutoff && x >= negate cutoff
103− ( leaps1 , leaps2 ) = partition (fitsIn32bits . leapTime) leaps
131+ leaps1 = filter (fitsIn32bits . leapTime) leaps
104− (transs1', transs2') = partition (fitsIn32bits . transTime) transs
132+ transs1' = filter (fitsIn32bits . transTime) transs
105− assoc1 = mkAssoc [0] transs1'
133+ allV1 = length leaps1 == length leaps && length transs1' == length transs
106− assoc2 = mkAssoc [] transs2'
134+ assoc1 = zip
107− transs1 = mkTranss transs1' assoc1
108− transs2 = mkTranss transs2' assoc2
109− ttinfos1 = mkTtinfos assoc1
110− ttinfos2 = mkTtinfos assoc2
111− mkAssoc prepend transs' = zip
112 (sortBy (comparing $ fmap tt_ttype . listToMaybe . flip drop ttinfos) .
135 (sortBy (comparing $ fmap tt_ttype . listToMaybe . flip drop ttinfos) .
113− uniq . sort . (prepend ++) $ map transIndex transs')
136+ uniq . sort . (0 :) $ map transIndex transs1')
115− mkTranss transs' assoc = [t {transIndex = i} |
138+ transs1 = [t {transIndex = i} |
116− t <- transs', i <- maybeToList $ lookup (transIndex t) assoc]
139+ t <- transs1', i <- maybeToList $ lookup (transIndex t) assoc1]
117− mkTtinfos assoc = map snd . dropWhile (isNothing . fst) .
140+ ttinfos1 = map snd . dropWhile (isNothing . fst) .
118− sortBy (comparing fst) $ zip (map (flip lookup assoc) [0..]) ttinfos
141+ sortBy (comparing fst) $ zip (map (flip lookup assoc1) [0..]) ttinfos
120−putOlsonParts :: Word8 -> OlsonData -> OlsonData -> Maybe String -> Put
121−putOlsonParts 0 olson1 _ _ = putOlsonPart 0 put32bitIntegral olson1
122−putOlsonParts v2 olson1 olson2 posix = do
123− putOlsonPart v2 put32bitIntegral olson1
124− putOlsonPart v2 put64bitIntegral olson2
127putOlsonPart :: Word8 -> (Integer -> Put) -> OlsonData -> Put
143putOlsonPart :: Word8 -> (Integer -> Put) -> OlsonData -> Put
128putOlsonPart version putTime (OlsonData transs ttinfos leaps _) = do
144putOlsonPart version putTime (OlsonData transs ttinfos leaps _) = do
129− putASCII "TZif"
145+ putASCII "magic number" "TZif"
130 putWord8 version
146 putWord8 version
131 putByteString . B.pack $ replicate 15 0 -- padding nulls
147 putByteString . B.pack $ replicate 15 0 -- padding nulls
132 replicateM_ 2 $ putCount ttinfosWithTtype
148 replicateM_ 2 $ putCount ttinfosWithTtype
133− -- tzh_ttisgmtcnt
149+ -- tzh_ttisgmtcnt
134− -- tzh_ttisstdcnt
150+ -- tzh_ttisstdcnt
135− putCount leaps -- tzh_leapcnt
151+ putCount leaps -- tzh_leapcnt
136− putCount transs -- tzh_timecnt
152+ putCount transs -- tzh_timecnt
137− putCount ttinfos -- tzh_typecnt
153+ putCount ttinfos -- tzh_typecnt
138− putCount abbrChars -- tzh_charcnt
154+ put32bitIntegral abbrChars -- tzh_charcnt
139 mapM_ (putTime . transTime ) transs
155 mapM_ (putTime . transTime ) transs
140 mapM_ (put8bitIntegral . transIndex) transs
156 mapM_ (put8bitIntegral . transIndex) transs
141 mapM_ putTtInfo ttinfosIndexed
157 mapM_ putTtInfo ttinfosIndexed
142− putASCII abbrChars
158+ mapM_ putAbbr abbrStrings
143 mapM_ (putLeapInfo putTime) leaps
159 mapM_ (putLeapInfo putTime) leaps
144 mapM_ (putBool . (== Std) . tt_ttype) ttinfosWithTtype -- isstd
160 mapM_ (putBool . (== Std) . tt_ttype) ttinfosWithTtype -- isstd
145 mapM_ (putBool . (== UTC) . tt_ttype) ttinfosWithTtype -- isgmt
161 mapM_ (putBool . (== UTC) . tt_ttype) ttinfosWithTtype -- isgmt
@@ -147,7 +163,8 @@ putOlsonPart version putTime (OlsonData transs ttinfos leaps _) = do
147 putCount = put32bitIntegral . length
163 putCount = put32bitIntegral . length
148 ttinfosWithTtype = takeWhile ((<= UTC) . tt_ttype) ttinfosIndexed
164 ttinfosWithTtype = takeWhile ((<= UTC) . tt_ttype) ttinfosIndexed
149 abbrStrings = uniq . sort $ map tt_abbr ttinfos
165 abbrStrings = uniq . sort $ map tt_abbr ttinfos
150− abbrChars = concatMap (++ "\NUL") abbrStrings
166+ abbrChars = sum (map length abbrStrings) + length abbrStrings
167+ putAbbr abbr = putASCII "time zone abbreviation" abbr >> putWord8 0
151 abbrAssocs = zip abbrStrings . scanl (+) 0 $
168 abbrAssocs = zip abbrStrings . scanl (+) 0 $
152 map ((+ 1) . length) abbrStrings
169 map ((+ 1) . length) abbrStrings
153 ttinfosIndexed = [TtInfo gmtoff isdst ttype i |
170 ttinfosIndexed = [TtInfo gmtoff isdst ttype i |
@@ -157,7 +174,7 @@ putOlsonPart version putTime (OlsonData transs ttinfos leaps _) = do
157putPosixTZ :: Maybe String -> Put
174putPosixTZ :: Maybe String -> Put
158putPosixTZ posix = do
175putPosixTZ posix = do
159 putWord8 10
176 putWord8 10
160− putASCII $ fromMaybe "" posix
177+ putASCII "POSIX TZ string"$ fromMaybe "" posix
161 putWord8 10
178 putWord8 10
163putTtInfo :: TtInfo Int -> Put
180putTtInfo :: TtInfo Int -> Put
@@ -190,5 +207,11 @@ putBool True = putWord8 1
190uniq :: Eq a => [a] -> [a]
207uniq :: Eq a => [a] -> [a]
191uniq = map head . group
208uniq = map head . group
193−putASCII :: String -> Put
210+putASCII :: String -> String -> Put
194−putASCII = putByteString . B.pack . map (fromIntegral . fromEnum)
212+ putByteString . B.pack . map (fromIntegral . verify . fromEnum)
215+ | c >= 32 && c <= 126 = c
216+ | otherwise = error $ "Cannot render TimeZoneSeries: " ++
217+ what ++ " contains non-ASCII characters"