git.iko.soy/typeable/timezone-olson
on master help sign in
.gitignorenew file+16
since splitsnapshots
unifiedsplit
@@ -0,0 +1,16 @@
1+dist
2+cabal-dev
3+*.o
4+*.hi
5+*.chi
6+*.chs.h
7+*.dyn_o
8+*.dyn_hi
9+.virtualenv
10+.hpc
11+.hsenv
12+.cabal-sandbox/
13+cabal.sandbox.config
14+*.prof
15+*.aux
16+*.hp
.travis.ymlnew file+107
@@ -0,0 +1,107 @@
1+# This Travis job script has been generated by a script via
2+#
3+# runghc make_travis_yml_2.hs 'timezone-olson.cabal'
4+#
5+# For more information, see https://github.com/hvr/multi-ghc-travis
6+#
7+language: c
8+sudo: false
9+
10+git:
11+ submodules: false # whether to recursively clone submodules
12+
13+cache:
14+ directories:
15+ - $HOME/.cabal/packages
16+ - $HOME/.cabal/store
17+
18+before_cache:
19+ - rm -fv $HOME/.cabal/packages/hackage.haskell.org/build-reports.log
20+ # remove files that are regenerated by 'cabal update'
21+ - rm -fv $HOME/.cabal/packages/hackage.haskell.org/00-index.*
22+ - rm -fv $HOME/.cabal/packages/hackage.haskell.org/*.json
23+ - rm -fv $HOME/.cabal/packages/hackage.haskell.org/01-index.cache
24+ - rm -fv $HOME/.cabal/packages/hackage.haskell.org/01-index.tar
25+ - rm -fv $HOME/.cabal/packages/hackage.haskell.org/01-index.tar.idx
26+
27+ - rm -rfv $HOME/.cabal/packages/head.hackage
28+
29+matrix:
30+ include:
31+ - compiler: "ghc-8.0.1"
32+ # env: TEST=--disable-tests BENCH=--disable-benchmarks
33+ addons: {apt: {packages: [ghc-ppa-tools,cabal-install-head,ghc-8.0.1], sources: [hvr-ghc]}}
34+ - compiler: "ghc-8.0.2"
35+ # env: TEST=--disable-tests BENCH=--disable-benchmarks
36+ addons: {apt: {packages: [ghc-ppa-tools,cabal-install-head,ghc-8.0.2], sources: [hvr-ghc]}}
37+ - compiler: "ghc-8.2.1"
38+ # env: TEST=--disable-tests BENCH=--disable-benchmarks
39+ addons: {apt: {packages: [ghc-ppa-tools,cabal-install-head,ghc-8.2.1], sources: [hvr-ghc]}}
40+ - compiler: "ghc-8.2.2"
41+ # env: TEST=--disable-tests BENCH=--disable-benchmarks
42+ addons: {apt: {packages: [ghc-ppa-tools,cabal-install-head,ghc-8.2.2], sources: [hvr-ghc]}}
43+ - compiler: "ghc-8.4.1"
44+ # env: TEST=--disable-tests BENCH=--disable-benchmarks
45+ addons: {apt: {packages: [ghc-ppa-tools,cabal-install-head,ghc-8.4.1], sources: [hvr-ghc]}}
46+
47+before_install:
48+ - HC=${CC}
49+ - HCPKG=${HC/ghc/ghc-pkg}
50+ - unset CC
51+ - ROOTDIR=$(pwd)
52+ - mkdir -p $HOME/.local/bin
53+ - "PATH=/opt/ghc/bin:/opt/ghc-ppa-tools/bin:$HOME/local/bin:$PATH"
54+ - HCNUMVER=$(( $(${HC} --numeric-version|sed -E 's/([0-9]+)\.([0-9]+)\.([0-9]+).*/\1 * 10000 + \2 * 100 + \3/') ))
55+ - echo $HCNUMVER
56+
57+install:
58+ - cabal --version
59+ - echo "$(${HC} --version) [$(${HC} --print-project-git-commit-id 2> /dev/null || echo '?')]"
60+ - BENCH=${BENCH---enable-benchmarks}
61+ - TEST=${TEST---enable-tests}
62+ - HADDOCK=${HADDOCK-true}
63+ - INSTALLED=${INSTALLED-true}
64+ - GHCHEAD=${GHCHEAD-false}
65+ - travis_retry cabal update -v
66+ - "sed -i.bak 's/^jobs:/-- jobs:/' ${HOME}/.cabal/config"
67+ - rm -fv cabal.project cabal.project.local
68+ - grep -Ev -- '^\s*--' ${HOME}/.cabal/config | grep -Ev '^\s*$'
69+ - "printf 'packages: \".\"\\n' > cabal.project"
70+ - cat cabal.project
71+ - if [ -f "./configure.ac" ]; then
72+ (cd "." && autoreconf -i);
73+ fi
74+ - rm -f cabal.project.freeze
75+ - cabal new-build -w ${HC} ${TEST} ${BENCH} --project-file="cabal.project" --dep -j2 all
76+ - cabal new-build -w ${HC} --disable-tests --disable-benchmarks --project-file="cabal.project" --dep -j2 all
77+ - rm -rf .ghc.environment.* "."/dist
78+ - DISTDIR=$(mktemp -d /tmp/dist-test.XXXX)
79+
80+# Here starts the actual work to be performed for the package under test;
81+# any command which exits with a non-zero exit code causes the build to fail.
82+script:
83+ # test that source-distributions can be generated
84+ - (cd "." && cabal sdist)
85+ - mv "."/dist/timezone-olson-*.tar.gz ${DISTDIR}/
86+ - cd ${DISTDIR} || false
87+ - find . -maxdepth 1 -name '*.tar.gz' -exec tar -xvf '{}' \;
88+ - "printf 'packages: timezone-olson-*/*.cabal\\n' > cabal.project"
89+ - cat cabal.project
90+ # this builds all libraries and executables (without tests/benchmarks)
91+ - cabal new-build -w ${HC} --disable-tests --disable-benchmarks all
92+
93+ # Build with installed constraints for packages in global-db
94+ - if $INSTALLED; then echo cabal new-build -w ${HC} --disable-tests --disable-benchmarks $(${HCPKG} list --global --simple-output --names-only | sed 's/\([a-zA-Z0-9-]\{1,\}\) */--constraint="\1 installed" /g') all | sh; else echo "Not building with installed constraints"; fi
95+
96+ # build & run tests, build benchmarks
97+ - cabal new-build -w ${HC} ${TEST} ${BENCH} all
98+
99+ # cabal check
100+ - (cd timezone-olson-* && cabal check)
101+
102+ # haddock
103+ - rm -rf ./dist-newstyle
104+ - if $HADDOCK; then cabal new-haddock -w ${HC} ${TEST} ${BENCH} all; else echo "Skipping haddock generation";fi
105+
106+# REGENDATA ["timezone-olson.cabal"]
107+# EOF
Data/Time/LocalTime/TimeZone/Olson.hsnew file+31
@@ -0,0 +1,31 @@
1+-----------------------------------------------------------------------------
2+-- |
3+-- Module : Data.Time.LocalTime.TimeZone.Olson
4+-- Copyright : Yitzchak Gale 2018
5+--
6+-- Maintainer : Yitzchak Gale <gale@sefer.org>
7+-- Portability : portable
8+--
9+-- A parser and renderer for binary Olson timezone files whose format
10+-- are specified by the tzfile(5) man page on Unix-like systems. For
11+-- more information about this format, see
12+-- <http://www.twinsun.com/tz/tz-link.htm>. Functions are provided for
13+-- converting the parsed data into @TimeZoneSeries@ and @TimeZone@
14+-- objects.
15+
16+{- Copyright (c) 2018 Yitzchak Gale. All rights reserved.
17+For licensing information, see the BSD3-style license in the file
18+LICENSE that was originally distributed by the author together with
19+this file. -}
20+
21+module Data.Time.LocalTime.TimeZone.Olson
22+(
23+ module Data.Time.LocalTime.TimeZone.Olson.Parse,
24+ module Data.Time.LocalTime.TimeZone.Olson.Render,
25+ module Data.Time.LocalTime.TimeZone.Olson.Types
26+)
27+where
28+
29+import Data.Time.LocalTime.TimeZone.Olson.Types
30+import Data.Time.LocalTime.TimeZone.Olson.Parse
31+import Data.Time.LocalTime.TimeZone.Olson.Render
Data/Time/LocalTime/TimeZone/Olson/Parse.hs+88−75
@@ -1,53 +1,27 @@
1{-# LANGUAGE DeriveDataTypeable #-}
1{-# LANGUAGE DeriveDataTypeable #-}
2+{-# OPTIONS_HADDOCK hide #-}
2-----------------------------------------------------------------------------
3-----------------------------------------------------------------------------
3-- |
4-- |
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
6--
7--
7-- Maintainer : Yitzchak Gale <gale@sefer.org>
8-- Maintainer : Yitzchak Gale <gale@sefer.org>
8-- Portability : portable
9-- Portability : portable
9--
10--
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.
15−-- objects.
16+
16−
17−{-
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. -}
22−met:
23−
24− * Redistributions of source code must retain the above copyright
25− notice, this list of conditions and the following disclaimer.
26−
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.
31−
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.
35−
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−----------------------------------------------------------------------------}
48
21
49module Data.Time.LocalTime.TimeZone.Olson.Parse
22module Data.Time.LocalTime.TimeZone.Olson.Parse
50(
23(
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)
74
51
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
88 where
65 where
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
97
76
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.
81+--
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
104
90
105-- | Parse a binary Olson timezone file.
91-- | Parse a binary Olson timezone file.
92+--
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
110
101
111formatError :: FilePath -> ErrorCall -> IO a
102formatError :: FilePath -> ErrorCall -> IO a
112formatError fp e = throwOlson fp $ show e
103formatError fp e = throwOlson fp $ show e
113
104
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"
119− version <- getWord8
120− >>= verify (`elem` [0, 50]) "invalid version number"
121− replicateM_ 15 getWord8 -- padding nulls
122− getOlsonParts version
123−
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
133
120
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
127+ version <- getWord8
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
148+ return
149+ (version,
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)
155− leaps
154+ leaps
156− Nothing
155+ Nothing
156+ )
157 where
157 where
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 _
163− stdOrWall isstd
164− | isstd = Std
164+ | isstd = Std
165− | otherwise = Wall
165+ | otherwise = Wall
166+
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
169+-- a default value
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
166
174
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
187
195
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
228
236
237+verify_ :: Monad m => (a -> Bool) -> String -> a -> m ()
238+verify_ pred msg val
239+ | pred val = return ()
240+ | otherwise = error msg
241+
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
Data/Time/LocalTime/TimeZone/Olson/Render.hs+127−100
@@ -1,63 +1,43 @@
1+{-# OPTIONS_HADDOCK hide #-}
1-----------------------------------------------------------------------------
2-----------------------------------------------------------------------------
2-- |
3-- |
3-- Module : Data.Time.LocalTime.TimeZone.Olson.Render
4-- Module : Data.Time.LocalTime.TimeZone.Olson.Render
4−-- Copyright : Yitzchak Gale 2010
5+-- Copyright : Yitzchak Gale 2018
5--
6--
6-- Maintainer : Yitzchak Gale <gale@sefer.org>
7-- Maintainer : Yitzchak Gale <gale@sefer.org>
7-- Portability : portable
8-- Portability : portable
8--
9--
9−-- Render Olson timezone data in the standard binary format as
10+-- Render Olson timezone data in the standard binary format as used in
10−-- used in Olson timezone files, and as specified by the
11+-- Olson timezone files, and as specified by the tzfile(5) man page on
11−-- tzfile(5) man page on Unix-like systems. For more information
12+-- Unix-like systems. For more information about this format, see
12−-- about this format, see http://www.twinsun.com/tz/tz-link.htm.
13+-- <http://www.twinsun.com/tz/tz-link.htm>.
13−
14+
14−{-
15−Copyright (c) 2010 Yitzchak Gale. All rights reserved.
15+{- Copyright (c) 2018ZZ Yitzchak Gale. All rights reserved.
16−
16+For licensing information, see the BSD3-style license in the file
17−Redistribution and use in source and binary forms, with or without
17+LICENSE that was originally distributed by the author together with
18−modification, are permitted provided that the following conditions are
18+this file. -}
19−met:
20−
21− * Redistributions of source code must retain the above copyright
22− notice, this list of conditions and the following disclaimer.
23−
24− * Redistributions in binary form must reproduce the above
25− copyright notice, this list of conditions and the following
26− disclaimer in the documentation and/or other materials provided
27− with the distribution.
28−
29− * Neither the name of Yitzchak Gale nor the names of other
30− contributors may be used to endorse or promote products derived
31− from this software without specific prior written permission.
32−
33−THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
34−"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
35−LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
36−A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
37−OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
38−SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
39−LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
40−DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
41−THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
42−(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
43−OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
44−----------------------------------------------------------------------------}
45
19
46module Data.Time.LocalTime.TimeZone.Olson.Render
20module Data.Time.LocalTime.TimeZone.Olson.Render
47(
21(
48− -- * Rendering Olson timezone data
22+ -- * Rendering Olson timezone files
49 -- | If any of the transition times or leap second times specified
23 -- | If any of the transition times or leap second times specified
50 -- require more than a 32-bit integer to represent as a Unix
24 -- require more than a 32-bit integer to represent as a Unix
51 -- timestamp, or if a POSIX-style TZ string is specified, timezone
25 -- timestamp, or if a POSIX-style TZ string is specified, timezone
52 -- data is rendered using Version 2 format. Otherwise, the timezone data
26 -- data is rendered using Version 2 format. Otherwise, the timezone data
53 -- is rendered using Version 1 format.
27 -- is rendered using Version 1 format.
28+ renderTimeZoneSeriesToOlsonFile,
29+ timeZoneSeriesToOlson,
54 renderOlsonToFile,
30 renderOlsonToFile,
31+ verifyOlsonLimits,
55 putOlson,
32 putOlson,
56− splitOlson
33+ extractOlsonV1
57)
34)
58where
35where
59
36
60import Data.Time.LocalTime.TimeZone.Olson.Types
37import Data.Time.LocalTime.TimeZone.Olson.Types
38+import Data.Time.LocalTime.TimeZone.Series (TimeZoneSeries(TimeZoneSeries))
39+import Data.Time (TimeZone(TimeZone, timeZoneSummerOnly, timeZoneName))
40+import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)
61import Data.Binary.Put (Put, runPut, putByteString, putWord8, flush,
41import Data.Binary.Put (Put, runPut, putByteString, putWord8, flush,
62 putWord32be, putWord64be)
42 putWord32be, putWord64be)
63import qualified Data.ByteString as B
43import qualified Data.ByteString as B
@@ -65,78 +45,117 @@ import qualified Data.ByteString.Lazy as L
65import Data.List (partition, sortBy, sort, group)
45import Data.List (partition, sortBy, sort, group)
66import Data.Ord (comparing)
46import Data.Ord (comparing)
67import Data.Word (Word8)
47import Data.Word (Word8)
68−import Data.Maybe (listToMaybe, maybeToList, isNothing, fromMaybe)
48+import Data.Maybe (listToMaybe, maybeToList, isNothing, fromMaybe, catMaybes)
69import Data.Monoid (mempty)
49import Data.Monoid (mempty)
70−import Control.Monad (guard, replicateM_)
50+import Control.Monad (guard, replicateM_, unless)
71
51
52+-- | Render a @TimeZoneSeries@ as a binary Olson timezone file.
53+--
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'
58+-- directly.
59+renderTimeZoneSeriesToOlsonFile :: FilePath -> TimeZoneSeries -> IO ()
60+renderTimeZoneSeriesToOlsonFile fp = renderOlsonToFile fp .
61+ fromMaybe (error "Cannot render TimeZoneSeries: default is summer time") .
62+ timeZoneSeriesToOlson
63+
64+-- | Convert a @TimeZoneSeries@ to @OlsonData@ for rendering.
65+timeZoneSeriesToOlson :: TimeZoneSeries -> Maybe OlsonData
66+timeZoneSeriesToOlson (TimeZoneSeries dflt pairs)
67+ | timeZoneSummerOnly dflt && not (all timeZoneSummerOnly $ map snd pairs)
68+ = Nothing
69+ | otherwise = Just $
70+ OlsonData
71+ [Transition secs ttinfo |
72+ (t, tz) <- reverse pairs,
73+ let secs = round $ utcTimeToPOSIXSeconds t,
74+ ttinfo <- maybeToList $ lookup (mkTT tz) ttAssocs]
75+ ttinfos
76+ []
77+ Nothing
78+ where
79+ mkTT (TimeZone offset isdst abbr) =
80+ TtInfo (offset*60) isdst Wall abbr
81+ dfltTT = mkTT dflt
82+ ttAssocs = (dfltTT, 0) :
83+ zip (uniq . sort . filter (/= dfltTT) $ map (mkTT . snd) pairs) [1..]
84+ ttinfos = map fst ttAssocs
85+
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
93+ where
94+ withinLimit limit items = maybe True (null . flip drop items) $
95+ limit limits
96+ abbrChars = concat abbrStrs ++ map (const '\NUL') abbrStrs
97+ abbrStrs = map tt_abbr ttinfos
72
98
73-- | Render Olson timezone data as a binary Olson timezone file
99-- | Render Olson timezone data as a binary Olson timezone file
100+--
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.
74renderOlsonToFile :: 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"
75−renderOlsonToFile fp = L.writeFile fp . runPut . putOlson
109+ L.writeFile fp . runPut . putOlson $ olson
76
110
77-- | Render Olson timezone data in binary Olson timezone file format
111-- | Render Olson timezone data in binary Olson timezone file format
78-- as a lazy @ByteString@
112-- as a lazy @ByteString@
79−putOlson :: OlsonData -> Put
113+putOlson olson@(OlsonData _ _ _ posix)
80−putOlson olson = do
114+ | fitsInVersion1 = putOlsonPart 0 put32bitIntegral olson
81− putASCII "TZif"
115+ | otherwise = do putOlsonPart 50 put32bitIntegral olson1
82− putWord8 version
116+ putOlsonPart 50 put64bitIntegral olson
83− putByteString . B.pack $ replicate 15 0 -- padding nulls
117+ putPosixTZ posix
84− putOlsonParts version olson1 olson2 posix
85− flush
86 where
118 where
87− (olson1, olson2, posix) = splitOlson olson
119+ olson1 = extractOlsonV1 olson
88− version
120+ fitsInVersion1 = fmap null posix /= Just False &&
89− | olson2 /= mempty = 50
121+ olson1 == olson {olsonPosixTZ = Nothing}
90− | otherwise = maybe 0 (const 50) $ posix >>= guard . not . null
122+
91−
92−-- | Split Olson timezone data into three parts: timezone data that can
93−-- 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
94−-- rendered using Version 2 format, and the POSIX-style TZ string
95−-- if specified
96−splitOlson :: OlsonData -> (OlsonData, OlsonData, Maybe String)
124+extractOlsonV1 :: OlsonData -> OlsonData
125+extractOlsonV1 (OlsonData transs ttinfos leaps _)
97−splitOlson (OlsonData transs ttinfos leaps posix) =
126+ | allV1 = OlsonData transs ttinfos leaps Nothing
98− (OlsonData transs1 ttinfos1 leaps1 Nothing,
127+ | otherwise = OlsonData transs1 ttinfos1 leaps1 Nothing
99− OlsonData transs2 ttinfos2 leaps2 Nothing,
100− posix)
101 where
128 where
102 cutoff = 0x80000000 -- 2^31
129 cutoff = 0x80000000 -- 2^31
103− ( leaps1 , leaps2 ) = partition ((>= cutoff) . leapTime) leaps
130+ fitsIn32bits x = x < cutoff && x >= negate cutoff
131+ leaps1 = filter (fitsIn32bits . leapTime) leaps
104− (transs1', transs2') = partition ((>= cutoff) . 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')
114 [0..]
137 [0..]
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
119−
142+
120−putOlsonParts :: Word8 -> OlsonData -> OlsonData -> Maybe String -> Put
121−putOlsonParts 0 olson1 _ _ = putOlsonPart put32bitIntegral olson1
122−putOlsonParts _ olson1 olson2 posix = do
123− putOlsonPart put32bitIntegral olson1
124− putOlsonPart put64bitIntegral olson2
125− putPosixTZ posix
126−
127−putOlsonPart :: (Integer -> Put) -> OlsonData -> Put
143+putOlsonPart :: Word8 -> (Integer -> Put) -> OlsonData -> Put
128−putOlsonPart putTime (OlsonData transs ttinfos leaps _) = do
144+putOlsonPart version putTime (OlsonData transs ttinfos leaps _) = do
145+ putASCII "magic number" "TZif"
146+ putWord8 version
147+ putByteString . B.pack $ replicate 15 0 -- padding nulls
129 replicateM_ 2 $ putCount ttinfosWithTtype
148 replicateM_ 2 $ putCount ttinfosWithTtype
130− -- tzh_ttisgmtcnt
149+ -- tzh_ttisgmtcnt
131− -- tzh_ttisstdcnt
150+ -- tzh_ttisstdcnt
132− putCount leaps -- tzh_leapcnt
151+ putCount leaps -- tzh_leapcnt
133− putCount transs -- tzh_timecnt
152+ putCount transs -- tzh_timecnt
134− putCount ttinfos -- tzh_typecnt
153+ putCount ttinfos -- tzh_typecnt
135− putCount abbrChars -- tzh_charcnt
154+ put32bitIntegral abbrChars -- tzh_charcnt
136 mapM_ (putTime . transTime ) transs
155 mapM_ (putTime . transTime ) transs
137 mapM_ (put8bitIntegral . transIndex) transs
156 mapM_ (put8bitIntegral . transIndex) transs
138 mapM_ putTtInfo ttinfosIndexed
157 mapM_ putTtInfo ttinfosIndexed
139− putASCII abbrChars
158+ mapM_ putAbbr abbrStrings
140 mapM_ (putLeapInfo putTime) leaps
159 mapM_ (putLeapInfo putTime) leaps
141 mapM_ (putBool . (== Std) . tt_ttype) ttinfosWithTtype -- isstd
160 mapM_ (putBool . (== Std) . tt_ttype) ttinfosWithTtype -- isstd
142 mapM_ (putBool . (== UTC) . tt_ttype) ttinfosWithTtype -- isgmt
161 mapM_ (putBool . (== UTC) . tt_ttype) ttinfosWithTtype -- isgmt
@@ -144,16 +163,18 @@ putOlsonPart putTime (OlsonData transs ttinfos leaps _) = do
144 putCount = put32bitIntegral . length
163 putCount = put32bitIntegral . length
145 ttinfosWithTtype = takeWhile ((<= UTC) . tt_ttype) ttinfosIndexed
164 ttinfosWithTtype = takeWhile ((<= UTC) . tt_ttype) ttinfosIndexed
146 abbrStrings = uniq . sort $ map tt_abbr ttinfos
165 abbrStrings = uniq . sort $ map tt_abbr ttinfos
166+ abbrChars = sum (map length abbrStrings) + length abbrStrings
167+ putAbbr abbr = putASCII "time zone abbreviation" abbr >> putWord8 0
168+ abbrAssocs = zip abbrStrings . scanl (+) 0 $
147− abbrChars = concatMap (++ "\NUL") abbrStrings
169+ map ((+ 1) . length) abbrStrings
148 ttinfosIndexed = [TtInfo gmtoff isdst ttype i |
170 ttinfosIndexed = [TtInfo gmtoff isdst ttype i |
149 TtInfo gmtoff isdst ttype abbr <- ttinfos,
171 TtInfo gmtoff isdst ttype abbr <- ttinfos,
150− i <- maybeToList . lookup abbr . zip abbrStrings .
172+ i <- maybeToList $ lookup abbr abbrAssocs]
151− scanl (+) 0 $ map ((+ 1) . length) abbrStrings]
152
173
153putPosixTZ :: Maybe String -> Put
174putPosixTZ :: Maybe String -> Put
154putPosixTZ posix = do
175putPosixTZ posix = do
155 putWord8 10
176 putWord8 10
156− putASCII $ fromMaybe "" posix
177+ putASCII "POSIX TZ string"$ fromMaybe "" posix
157 putWord8 10
178 putWord8 10
158
179
159putTtInfo :: TtInfo Int -> Put
180putTtInfo :: TtInfo Int -> Put
@@ -186,5 +207,11 @@ putBool True = putWord8 1
186uniq :: Eq a => [a] -> [a]
207uniq :: Eq a => [a] -> [a]
187uniq = map head . group
208uniq = map head . group
188
209
189−putASCII :: String -> Put
210+putASCII :: String -> String -> Put
211+putASCII what =
190−putASCII = putByteString . B.pack . map (fromIntegral . fromEnum)
212+ putByteString . B.pack . map (fromIntegral . verify . fromEnum)
213+ where
214+ verify c
215+ | c >= 32 && c <= 126 = c
216+ | otherwise = error $ "Cannot render TimeZoneSeries: " ++
217+ what ++ " contains non-ASCII characters"
Data/Time/LocalTime/TimeZone/Olson/Types.hs+102−57
@@ -1,7 +1,9 @@
1+{-# OPTIONS_HADDOCK hide #-}
2+{-# LANGUAGE CPP #-}
1-----------------------------------------------------------------------------
3-----------------------------------------------------------------------------
2-- |
4-- |
3-- Module : Data.Time.LocalTime.TimeZone.Olson.Types
5-- Module : Data.Time.LocalTime.TimeZone.Olson.Types
4−-- Copyright : Yitzchak Gale 2010
6+-- Copyright : Yitzchak Gale 2018
5--
7--
6-- Maintainer : Yitzchak Gale <gale@sefer.org>
8-- Maintainer : Yitzchak Gale <gale@sefer.org>
7-- Portability : portable
9-- Portability : portable
@@ -9,80 +11,89 @@
9-- Data types to represent timezone data as used in Olson timezone
11-- Data types to represent timezone data as used in Olson timezone
10-- files, and as specified by the tzfile(5) man page on Unix-like
12-- files, and as specified by the tzfile(5) man page on Unix-like
11-- systems. For more information about this format, see
13-- systems. For more information about this format, see
12−-- http://www.twinsun.com/tz/tz-link.htm.
14+-- <http://www.twinsun.com/tz/tz-link.htm>.
13--
15--
14-- Both Version 1 and Version 2 timezone data can be represented.
16-- Both Version 1 and Version 2 timezone data can be represented.
15
17
16−{-
17−Copyright (c) 2010 Yitzchak Gale. All rights reserved.
18+{- Copyright (c) 2018 Yitzchak Gale. All rights reserved.
18−
19+For licensing information, see the BSD3-style license in the file
19−Redistribution and use in source and binary forms, with or without
20+LICENSE that was originally distributed by the author together with
20−modification, are permitted provided that the following conditions are
21+this file. -}
21−met:
22−
23− * Redistributions of source code must retain the above copyright
24− notice, this list of conditions and the following disclaimer.
25−
26− * Redistributions in binary form must reproduce the above
27− copyright notice, this list of conditions and the following
28− disclaimer in the documentation and/or other materials provided
29− with the distribution.
30−
31− * Neither the name of Yitzchak Gale nor the names of other
32− contributors may be used to endorse or promote products derived
33− from this software without specific prior written permission.
34−
35−THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
36−"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
37−LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
38−A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
39−OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
40−SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
41−LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
42−DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
43−THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
44−(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
45−OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
46−-}
47
22
48module Data.Time.LocalTime.TimeZone.Olson.Types
23module Data.Time.LocalTime.TimeZone.Olson.Types
49(
24(
25+ -- * Olson timezone datatypes
50 OlsonData(..),
26 OlsonData(..),
51 Transition(..),
27 Transition(..),
52 TransitionType(..),
28 TransitionType(..),
53 TtInfo(..),
29 TtInfo(..),
54− LeapInfo(..)
30+ LeapInfo(..),
31+
32+ -- ** Size limits for Olson timezone data
33+ SizeLimits(..),
34+ defaultLimits,
35+ limitsNoSolar,
36+ noLimits
55)
37)
56where
38where
57
39
40+#if MIN_VERSION_base(4,11,0)
41+import Data.Semigroup (Semigroup(..))
42+#endif
58import Data.Monoid (Monoid(..))
43import Data.Monoid (Monoid(..))
59import Control.Monad (mplus)
44import Control.Monad (mplus)
60
45
61-- | @OlsonData@ represents a full set of timezone data for a location.
46-- | @OlsonData@ represents a full set of timezone data for a location.
62--
47--
63−-- @OlsonData@ can represent timezone data from files in Version 1 format
48+-- @OlsonData@ can represent timezone data from files in Version 1, 2,
49+-- or 3 format.
50+--
64−-- or Version 2 format. Version 1 format files can only contain timestamp
51+-- Version 1 format files can only contain timestamp values that can
65−-- values that can be represented in less than 32 bits, and cannot contain
52+-- be represented in less than 32 bits, and cannot contain a POSIX TZ
66−-- a POSIX TZ string.
53+-- string.
67--
54--
68−-- In a Version 2 format file, the timezone data is split into two parts.
55+-- In a Version 2 format file, the timezone data is split into two
69−-- The first part contains timezone data for which all timestamp values
56+-- parts. The first part contains timezone data for which all
70−-- can be represented in less 32 bits, and the second part contains
57+-- timestamp values can be represented in less than 32 bits, and the
71−-- timezone data for which 32 bits or more are required to represent
58+-- second part contains timezone data for which 32 bits or more are
72−-- timestamp values. The POSIX TZ string, if present, can only be rendered
59+-- required to represent timestamp values. The POSIX TZ string, if
60+-- present, can only be rendered in a Version 2 file, and appears
73−-- in a Version 2 file, and appears after both parts of the timezone data.
61+-- after both parts of the timezone data.
62+--
63+-- Version 3 format files relax certain syntax requirements for the
64+-- POSIX TZ string. Since we represent the POSIX TZ string as an
65+-- unparsed 'String', Version 3 is identical to Version 2 for our
66+-- purposes.
74data OlsonData =
67data OlsonData =
75− OlsonData
68+ OlsonData {
76− [Transition]
69+ olsonTransitions :: [Transition],
77− [TtInfo String]
70+ olsonTypes :: [TtInfo String],
78− [LeapInfo]
71+ olsonLeaps :: [LeapInfo],
72+ olsonPosixTZ :: (Maybe String)
79− (Maybe String) -- ^ Optional POSIX TZ string for the last @Transition@
73+ -- ^ Optional POSIX TZ string for
74+ -- times after the last @Transition@
75+ }
80 deriving (Eq, Show)
76 deriving (Eq, Show)
81
77
78+#if MIN_VERSION_base(4,11,0)
79+instance Semigroup OlsonData where
80+ OlsonData a b c d <> OlsonData a' b' c' d' =
81+ OlsonData (a ++ map (shiftBy $ length b) a')
82+ (b ++ b') (c ++ c') (d `mplus` d')
83+ where
84+ shiftBy n trans = trans {transIndex = n + transIndex trans}
85+
82instance Monoid OlsonData where
86instance Monoid OlsonData where
83 mempty = OlsonData [] [] [] Nothing
87 mempty = OlsonData [] [] [] Nothing
88+#else
89+instance Monoid OlsonData where
90+ mempty = OlsonData [] [] [] Nothing
84− mappend (OlsonData a b c d ) (OlsonData a' b' c' d')
91+ mappend (OlsonData a b c d ) (OlsonData a' b' c' d') =
92+ OlsonData (a ++ map (shiftBy $ length b) a')
85− = OlsonData (a++a') (b++b') (c++c') (d `mplus` d')
93+ (b ++ b') (c ++ c') (d `mplus` d')
94+ where
95+ shiftBy n trans = trans {transIndex = n + transIndex trans}
96+#endif
86
97
87-- | A @Transition@ represents a moment when the clocks change in a
98-- | A @Transition@ represents a moment when the clocks change in a
88-- timezone. It consists of a Unix timestamp value that indicates the
99-- timezone. It consists of a Unix timestamp value that indicates the
@@ -93,8 +104,9 @@ data Transition =
93 Transition
104 Transition
94 {transTime :: Integer, -- ^ Unix timestamp indicating the time
105 {transTime :: Integer, -- ^ Unix timestamp indicating the time
95 -- when the clocks change
106 -- when the clocks change
96− transIndex :: Int} -- ^ 0-based index in the list of @TtInfo@
107+ transIndex :: Int -- ^ 0-based index in the list of @TtInfo@
97 -- that describes the new time
108 -- that describes the new time
109+ }
98 deriving (Eq, Show)
110 deriving (Eq, Show)
99
111
100-- | A @TransitionType@ is historical information about whether the
112-- | A @TransitionType@ is historical information about whether the
@@ -104,7 +116,7 @@ data Transition =
104-- zone. This historical trivia may seem rather boring, but
116-- zone. This historical trivia may seem rather boring, but
105-- unfortunately it is needed to interpret a POSIX-style TZ string
117-- unfortunately it is needed to interpret a POSIX-style TZ string
106-- timezone specification correctly.
118-- timezone specification correctly.
107−data TransitionType = Std | Wall | UTC | UnknownType
119+data TransitionType = Std | Wall | UTC
108 deriving (Eq, Ord, Show)
120 deriving (Eq, Ord, Show)
109
121
110-- | A @TtInfo@ is a specification of local time in a timezone for
122-- | A @TtInfo@ is a specification of local time in a timezone for
@@ -119,15 +131,48 @@ data TtInfo abbr =
119 -- in seconds
131 -- in seconds
120 tt_isdst :: Bool, -- ^ True if local clocks are summer time
132 tt_isdst :: Bool, -- ^ True if local clocks are summer time
121 tt_ttype :: TransitionType,
133 tt_ttype :: TransitionType,
122− tt_abbr :: abbr} -- ^ The timezone abbreviation string.
134+ tt_abbr :: abbr -- ^ The timezone abbreviation string.
135+ }
123− deriving (Eq, Show)
136+ deriving (Eq, Ord, Show)
124
137
125-- | Olson timezone files can contain leap second specifications, though
138-- | Olson timezone files can contain leap second specifications, though
126−-- the official files have not done so since the late 1980s.
139+-- most do not.
127data LeapInfo =
140data LeapInfo =
128 LeapInfo
141 LeapInfo
129 {leapTime :: Integer, -- ^ A Unix timestamp indicating the time
142 {leapTime :: Integer, -- ^ A Unix timestamp indicating the time
130 -- that the leap second occurred
143 -- that the leap second occurred
131− leapOffset :: Int} -- ^ The new total offset of UTC from UT1
144+ leapOffset :: Int -- ^ The new total offset of UTC from UT1
132 -- after this leap second
145 -- after this leap second
146+ }
133 deriving (Eq, Show)
147 deriving (Eq, Show)
148+
149+-- | The reference C implentation imposes size limits on the data
150+-- structures in a timezone file.
151+data SizeLimits = SizeLimits
152+ {maxTimes :: Maybe Int, -- ^ The maximum number of transition times
153+ maxTypes :: Maybe Int, -- ^ The maximum number of TtInfo
154+ -- clock settings
155+ maxAbbrChars :: Maybe Int, -- ^ The maximum total number of bytes in
156+ -- all timezone abbreviations.
157+ maxLeaps :: Maybe Int -- ^ The maximum number of leap second
158+ -- specifications.
159+ }
160+
161+-- | The size limits in @defaultLimits@ are taken from the file
162+-- tzfile.h from tzcode version 2010f. These are the limits for the C
163+-- implementation on many platforms.
164+defaultLimits :: SizeLimits
165+defaultLimits = SizeLimits { maxTimes = Just 1200, maxTypes = Just 256,
166+ maxAbbrChars = Just 50, maxLeaps = Just 50 }
167+
168+-- | @limitsNoSolar@ contains the tighter size limits imposed on some
169+-- platforms that do not allow timezones that are based on solar time.
170+limitsNoSolar :: SizeLimits
171+limitsNoSolar = defaultLimits { maxTypes = Just 20 }
172+
173+-- | @noLimits@ imposes no size limits. If you use @noLimits@ when
174+-- parsing, you may exhaust all available memory when reading a faulty
175+-- or malicious timezone file. If you use @noLimits@ when rendering,
176+-- the rendered timezone file might not be readable on some systems.
177+noLimits :: SizeLimits
178+noLimits = SizeLimits Nothing Nothing Nothing Nothing
LICENSEnew file+29
@@ -0,0 +1,29 @@
1+Copyright (c) 2010-2018 Yitzchak Gale. All rights reserved.
2+
3+Redistribution and use in source and binary forms, with or without
4+modification, are permitted provided that the following conditions are
5+met:
6+
7+ * Redistributions of source code must retain the above copyright
8+ notice, this list of conditions and the following disclaimer.
9+
10+ * Redistributions in binary form must reproduce the above
11+ copyright notice, this list of conditions and the following
12+ disclaimer in the documentation and/or other materials provided
13+ with the distribution.
14+
15+ * Neither the name of Yitzchak Gale nor the names of other
16+ contributors may be used to endorse or promote products derived
17+ from this software without specific prior written permission.
18+
19+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
README.mdnew file+43
@@ -0,0 +1,43 @@
1+# timezone-olson
2+
3+On Hackage: [timezone-olson](http://hackage.haskell.org/package/timezone-olson)
4+
5+This package provides a parser and renderer for binary Olson timezone
6+files whose format is specified by the tzfile(5) man page on Unix-like
7+systems. For more information about this format, see
8+[the IANA timezone database site](https://www.iana.org/time-zones).
9+
10+Functions are provided for
11+converting the parsed data into `TimeZoneSeries` objects from the
12+[timezone-series](http://hackage.haskell.org/package/timezone-series)
13+package. On many platforms, binary Olson timezone
14+files suitable for use with this package are available in the
15+directory `/usr/share/zoneinfo` and its subdirectories on your computer.
16+
17+See also the
18+[timezone-olson-th](http://hackage.haskell.org/package/timezone-olson-th)
19+package for a way to include timezone informaton from a binary Olson
20+timezone file at compile time.
21+
22+Copyright (c) 2010-2018 Yitzchak Gale. All rights reserved.
23+
24+For licensing information, see the BSD3-style license in the file
25+LICENSE that was originally distributed by the author together with
26+this file.
27+
28+This package is part of the [time-ng project](http://projects.haskell.org/time-ng/).
29+
30+## Testing utilities
31+
32+This package also provides two Haskell files, each of which can be
33+compiled into a command-line utility that might be helpful for testing
34+purposes.
35+
36+zhdump.hs: A clone of zdump(8), including most of its bugs, that
37+ is usually present on systems that have an Olson timezone
38+ database, except hzdump takes paths to timezone files
39+ instead of timezone identifiers on the command line.
40+
41+catTZ: Read and parse a timezone file, then render it. With the
42+ -i flag, interprets the timezone data as a TimeZoneSeries
43+ object before rendering.
Setup.hsnew file+2
@@ -0,0 +1,2 @@
1+import Distribution.Simple
2+main = defaultMain
catTZ.hsnew file+14
@@ -0,0 +1,14 @@
1+-- | Parse an Olson timezone file, then render it.
2+-- If -i is specified, interpret it as a TimeZoneSeries before rendering.
3+module Main where
4+
5+import Data.Time.LocalTime.TimeZone.Olson
6+import Data.Time.LocalTime.TimeZone.Series
7+import System.Environment
8+
9+main = do
10+ args <- getArgs
11+ if head args == "-i"
12+ then getTimeZoneSeriesFromOlsonFile (args !! 1) >>=
13+ renderTimeZoneSeriesToOlsonFile (args !! 2)
14+ else getOlsonFromFile (args !! 0) >>= renderOlsonToFile (args !! 1)
hzdump.hs+30−15
@@ -1,6 +1,7 @@
1-- | A clone in Haskell of the zdump(8) command, including most of
1-- | A clone in Haskell of the zdump(8) command, including most of
2+-- the bugs, that takes paths to Olson files instead of timezone
2−-- the bugs. This is useful for testing the Haskell implementation
3+-- names. This is useful for testing the Haskell implementation
3−-- of an Olson timezone parser and renderer against the reference
4+-- of the Olson timezone parser and renderer against the reference
4-- implementation in C. But less useful than it might seem at first,
5-- implementation in C. But less useful than it might seem at first,
5-- because Haskell rounds historical solar mean timezones to the
6-- because Haskell rounds historical solar mean timezones to the
6-- nearest minute, whereas the C implementation rounds to the nearest
7-- nearest minute, whereas the C implementation rounds to the nearest
@@ -8,22 +9,24 @@
8module Main where
9module Main where
9
10
10import Data.Time.LocalTime.TimeZone.Series
11import Data.Time.LocalTime.TimeZone.Series
11−import Data.Time.LocalTime.TimeZone.Olson.Types
12+import Data.Time.LocalTime.TimeZone.Olson
12−import Data.Time.LocalTime.TimeZone.Olson.Parse
13import Data.Time
13import Data.Time
14import Data.Maybe (listToMaybe)
14import Data.Maybe (listToMaybe)
15import System.Environment (getArgs)
15import System.Environment (getArgs)
16import System.Exit (exitWith, exitSuccess, ExitCode(ExitFailure))
16import System.Exit (exitWith, exitSuccess, ExitCode(ExitFailure))
17import System.Locale (defaultTimeLocale)
17import System.Locale (defaultTimeLocale)
18+import Control.Monad (guard)
18
19
19version :: IO a
20version :: IO a
20version = do
21version = do
21− putStrLn "hzdump version 0.1, clone of zdump 1.7 with -c option fixed"
22+ putStrLn "hzdump version 0.1, clone of zdump 1.7 -c option fixed,"
23+ putStrLn "takes zone file paths instead of zone specifications"
22 exitSuccess
24 exitSuccess
23
25
24usage :: IO a
26usage :: IO a
25usage = do
27usage = do
26− putStrLn "usage: hzdump [--version] [-v] [-c cutoff] zonename ..."
28+ putStrLn "usage: hzdump [--version] [-v] [-c cutoff] zone-file-path ..."
29+ putStrLn " where cutoff is lo-year,hi-year or hi-year"
27 exitWith $ ExitFailure 1
30 exitWith $ ExitFailure 1
28
31
29illegalOpt :: Char -> IO a -- sic
32illegalOpt :: Char -> IO a -- sic
@@ -31,7 +34,8 @@ illegalOpt opt = do
31 putStrLn $ "hzdump: illegal option -- " ++ [opt]
34 putStrLn $ "hzdump: illegal option -- " ++ [opt]
32 usage
35 usage
33
36
34−data Option = Version | Illegal Char | Verbose (Maybe Integer) | Now
37+data Option = Version | Illegal Char |
38+ Verbose (Maybe Integer, Maybe Integer) | Now
35 deriving (Eq, Ord, Show)
39 deriving (Eq, Ord, Show)
36
40
37main = do
41main = do
@@ -39,7 +43,7 @@ main = do
39 (getTimes, displayTime) <- case opts of
43 (getTimes, displayTime) <- case opts of
40 Version -> version
44 Version -> version
41 Illegal opt -> illegalOpt opt
45 Illegal opt -> illegalOpt opt
42− Verbose yr -> return (transitionTimesUntil yr, displayVerbose)
46+ Verbose rng -> return (transitionTimes rng, displayVerbose)
43 _ -> do now <- getCurrentTime
47 _ -> do now <- getCurrentTime
44 return (const [now], displayConcise)
48 return (const [now], displayConcise)
45 tzss <- mapM getZone zones
49 tzss <- mapM getZone zones
@@ -54,17 +58,26 @@ main = do
54
58
55parseArgs :: [String] -> (Option, [FilePath])
59parseArgs :: [String] -> (Option, [FilePath])
56parseArgs args | "--version" `elem` args = (Version, [])
60parseArgs args | "--version" `elem` args = (Version, [])
57−parseArgs args = getOpts False Nothing args
61+parseArgs args = getOpts False (Nothing, Nothing) args
58 where
62 where
59 getOpts _ cutoff ("-v":args) = getOpts True cutoff args
63 getOpts _ cutoff ("-v":args) = getOpts True cutoff args
60− getOpts v Nothing ("-c":c:args) = maybe (Illegal 'c', [])
64+ getOpts v _ ("-c":c:args) = maybe (Illegal 'c', [])
61− (\y -> getOpts v (Just y) args) $
65+ (\y -> getOpts v y args) $
62− maybeRead c
66+ parseCutoff c
63 getOpts v cutoff ("--":zones ) = (opts v cutoff, zones)
67 getOpts v cutoff ("--":zones ) = (opts v cutoff, zones)
64 getOpts _ _ (('-':x:_):_) = (Illegal x, [])
68 getOpts _ _ (('-':x:_):_) = (Illegal x, [])
65 getOpts v cutoff zones = (opts v cutoff, zones)
69 getOpts v cutoff zones = (opts v cutoff, zones)
70+
66 opts v cutoff = if v then Verbose cutoff else Now
71 opts v cutoff = if v then Verbose cutoff else Now
67
72
73+ parseCutoff c = let p0 = listToMaybe $ reads c
74+ los = fmap fst p0
75+ his = do str <- fmap snd p0
76+ let (comma, str1) = splitAt 1 str
77+ guard $ comma == ","
78+ maybeRead str1
79+ in los >> Just (maybe (Nothing, los) ((,) los . Just) his)
80+
68displayConcise :: TimeZoneSeries -> UTCTime -> String
81displayConcise :: TimeZoneSeries -> UTCTime -> String
69displayConcise tzs t = formatTime defaultTimeLocale format $
82displayConcise tzs t = formatTime defaultTimeLocale format $
70 ZoneSeriesTime t tzs
83 ZoneSeriesTime t tzs
@@ -77,9 +90,11 @@ displayVerbose tzs t = concat [displayConcise utcTZ t, " = ",
77 where
90 where
78 isdst = timeZoneSummerOnly $ timeZoneFromSeries tzs t
91 isdst = timeZoneSummerOnly $ timeZoneFromSeries tzs t
79
92
80−transitionTimesUntil :: Maybe Integer -> TimeZoneSeries -> [UTCTime]
93+transitionTimes :: (Maybe Integer, Maybe Integer) -> TimeZoneSeries ->
81−transitionTimesUntil yr =
94+ [UTCTime]
95+transitionTimes (lo, hi) =
96+ maybe id (takeWhile . (>)) (fmap happyNewYear hi) .
82− maybe id (dropWhile . (>=)) (fmap happyNewYear yr) .
97+ maybe id (dropWhile . (>)) (fmap happyNewYear lo) .
83 addBugs . addPrevSecond . actualTransitions
98 addBugs . addPrevSecond . actualTransitions
84 where
99 where
85 happyNewYear y = UTCTime (fromGregorian y 1 1) 0
100 happyNewYear y = UTCTime (fromGregorian y 1 1) 0
timezone-olson.cabalnew file+45
@@ -0,0 +1,45 @@
1+Name: timezone-olson
2+Version: 0.1.9
3+Synopsis: A pure Haskell parser and renderer for binary Olson timezone files
4+Description: A parser and renderer for binary Olson timezone
5+ files whose format is specified by the tzfile(5)
6+ man page on Unix-like systems. For more
7+ information about this format, see
8+ <http://www.iana.org/time-zones/repository/tz-link.html>.
9+ Functions are provided for converting the parsed data into
10+ 'TimeZoneSeries' objects from the timezone-series
11+ package. On many platforms, binary Olson timezone
12+ files suitable for use with this package are
13+ available in the directory /usr/share/zoneinfo
14+ and its subdirectories on your computer.
15+ For a way to read binary Olson timezone files at compile
16+ time, see the timezone-olson-th package
17+ (<http://hackage.haskell.org/package/timezone-olson-th>).
18+Homepage: http://projects.haskell.org/time-ng/
19+License: BSD3
20+License-file: LICENSE
21+Author: Yitzchak Gale
22+Maintainer: yitz@sefer.org
23+Copyright: Copyright (c) 2010-2018 Yitzchak Gale. All rights reserved.
24+Category: Data
25+Build-type: Simple
26+Extra-source-files: README.md, catTZ.hs, hzdump.hs
27+Cabal-version: >=1.10
28+Tested-with: GHC > 8.0 && < 8.5
29+
30+Source-repository HEAD
31+ type: git
32+ location: https://github.com/ygale/timezone-olson.git
33+
34+Library
35+ Default-language: Haskell2010
36+ Exposed-modules: Data.Time.LocalTime.TimeZone.Olson
37+ , Data.Time.LocalTime.TimeZone.Olson.Parse
38+ , Data.Time.LocalTime.TimeZone.Olson.Render
39+ , Data.Time.LocalTime.TimeZone.Olson.Types
40+ Build-depends: base >= 3.0 && < 5.0,
41+ timezone-series >= 0.1.0 && < 0.2,
42+ time >= 1.6 && < 1.10,
43+ binary >= 0.4.1 && < 0.9,
44+ bytestring >= 0.9 && < 1.0,
45+ extensible-exceptions >= 0.1.0 && < 0.2