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+90−76
@@ -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,62 +60,84 @@ 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
68+ mkTZ (TtInfo gmtoff isdst _ abbr) =
91− mkTZ (TtInfo gmtoff isdst _ abbr) = TimeZone (gmtoff `div` 60) isdst abbr
69+ TimeZone ((gmtoff + 30) `div` 60) isdst abbr
92 lookupTZ ttinfos ttime = fmap (((,) $ toUTC ttime) . mkTZ) . listToMaybe $
70 lookupTZ ttinfos ttime = fmap (((,) $ toUTC ttime) . mkTZ) . listToMaybe $
93 drop (transIndex ttime) ttinfos
71 drop (transIndex ttime) ttinfos
94 toUTC = posixSecondsToUTCTime . fromIntegral . transTime
72 toUTC = posixSecondsToUTCTime . fromIntegral . transTime
73+ uniqTimes = map last . groupBy ((==) `on` transTime)
74+ futureToPast = comparing $ negate . transTime
95olsonToTimeZoneSeries _ = Nothing
75olsonToTimeZoneSeries _ = Nothing
96
76
97-- | Read timezone data from a binary Olson timezone file and convert
77-- | Read timezone data from a binary Olson timezone file and convert
98−-- 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.
99getTimeZoneSeriesFromOlsonFile :: FilePath -> IO TimeZoneSeries
86getTimeZoneSeriesFromOlsonFile :: FilePath -> IO TimeZoneSeries
100getTimeZoneSeriesFromOlsonFile fp = getOlsonFromFile fp >>=
87getTimeZoneSeriesFromOlsonFile fp = getOlsonFromFile fp >>=
101 maybe (throwOlson fp "no timezone found in OlsonData") return .
88 maybe (throwOlson fp "no timezone found in OlsonData") return .
102 olsonToTimeZoneSeries
89 olsonToTimeZoneSeries
103
90
104-- | 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.
105getOlsonFromFile :: FilePath -> IO OlsonData
97getOlsonFromFile :: FilePath -> IO OlsonData
106getOlsonFromFile fp = do
98getOlsonFromFile fp = do
107− e <- try . fmap (runGet getOlson) $ L.readFile fp
99+ e <- try . fmap (runGet $ getOlson defaultLimits) $ L.readFile fp
108 either (formatError fp) return e
100 either (formatError fp) return e
109
101
110formatError :: FilePath -> ErrorCall -> IO a
102formatError :: FilePath -> ErrorCall -> IO a
111formatError fp e = throwOlson fp $ show e
103formatError fp e = throwOlson fp $ show e
112
104
113-- | A binary parser for binary Olson timezone files
105-- | A binary parser for binary Olson timezone files
114−getOlson :: Get OlsonData
106+getOlson :: SizeLimits -> Get OlsonData
115−getOlson = do
107+getOlson limits = do
116− getByteString 4
108+ (version, part1) <- getOlsonPart True limits get32bitInteger
117− >>= verify ((== "TZif") . toASCII . B.unpack) "missing magic number"
118− version <- getWord8
119− >>= verify (`elem` [0, 50]) "invalid version number"
120− replicateM_ 15 getWord8 -- padding nulls
121− getOlsonParts version
122−
123−-- 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
124−-- TZ string for Version 2 format data
110+ -- TZ string for Version 2 or 3 format data
125−getOlsonParts :: Word8 -> Get OlsonData
111+ case () of
126−getOlsonParts 0 = getOlsonPart get32bitInteger
112+ _ | version == 0 -> return part1
127−getOlsonParts 50 = do part1 <- getOlsonPart get32bitInteger
113+ | version == 50 || version == 51 -> do
128− part2 <- getOlsonPart get64bitInteger
114+ (_, part2) <- getOlsonPart False limits get64bitInteger
129− posixTZ <- getPosixTZ
115+ posixTZ <- getPosixTZ
130− return $ part1 `mappend` part2 `mappend` posixTZ
116+ return $ part1 `mappend` part2 `mappend` posixTZ
131−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
132
120
133-- Parse the part of an Olson file that contains timezone data
121-- Parse the part of an Olson file that contains timezone data
134−getOlsonPart :: Integral a => Get a -> Get OlsonData
122+getOlsonPart :: Integral a => Bool -> SizeLimits -> Get a ->
123+ Get (Word8, OlsonData)
135−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
136 tzh_ttisgmtcnt <- get32bitInt
129 tzh_ttisgmtcnt <- get32bitInt
137 tzh_ttisstdcnt <- get32bitInt
130 tzh_ttisstdcnt <- get32bitInt
138 tzh_leapcnt <- get32bitInt
131 tzh_leapcnt <- get32bitInt
132+ >>= verify (withinLimit maxLeaps) "too many leap second specifications"
139 tzh_timecnt <- get32bitInt
133 tzh_timecnt <- get32bitInt
134+ >>= verify (withinLimit maxTimes) "too many timezone transitions"
140 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
141 tzh_charcnt <- get32bitInt
139 tzh_charcnt <- get32bitInt
140+ >>= verify (withinLimit maxAbbrChars) "too many tilezone specifiers"
142 times <- fmap (map toInteger) $ replicateM tzh_timecnt getTime
141 times <- fmap (map toInteger) $ replicateM tzh_timecnt getTime
143 indexes <- replicateM tzh_timecnt get8bitInt
142 indexes <- replicateM tzh_timecnt get8bitInt
144 ttinfos <- replicateM tzh_typecnt getTtInfo
143 ttinfos <- replicateM tzh_typecnt getTtInfo
@@ -146,22 +145,32 @@ getOlsonPart getTime = do
146 leaps <- replicateM tzh_leapcnt $ getLeapInfo getTime
145 leaps <- replicateM tzh_leapcnt $ getLeapInfo getTime
147 isstds <- replicateM tzh_ttisstdcnt getBool
146 isstds <- replicateM tzh_ttisstdcnt getBool
148 isgmts <- replicateM tzh_ttisgmtcnt getBool
147 isgmts <- replicateM tzh_ttisgmtcnt getBool
148+ return
149+ (version,
149− return $ OlsonData
150+ OlsonData
150− (zipWith Transition times indexes)
151+ (zipWith Transition times indexes)
151− (map (flip lookupAbbr abbr_chars) . zipWith setTtype ttinfos .
152+ (map (flip lookupAbbr abbr_chars) . zipWith setTtype ttinfos $
152− (++ repeat UnknownType) $
153− zipWith boolsToTType (map Just isstds ++ repeat Nothing) isgmts)
153+ zipWithExtend boolsToTType False False isstds isgmts)
154− leaps
154+ leaps
155− Nothing
155+ Nothing
156+ )
156 where
157 where
158+ withinLimit limit value = maybe True (value <=) $ limit limits
157 lookupAbbr (TtInfo gmtoff isdst ttype abbrind) =
159 lookupAbbr (TtInfo gmtoff isdst ttype abbrind) =
158 TtInfo gmtoff isdst ttype . takeWhile (/= '\NUL') . drop abbrind
160 TtInfo gmtoff isdst ttype . takeWhile (/= '\NUL') . drop abbrind
159 setTtype ttinfo ttype = ttinfo {tt_ttype = ttype}
161 setTtype ttinfo ttype = ttinfo {tt_ttype = ttype}
160− boolsToTType _ isgmt | isgmt = UTC
162+ boolsToTType _ isgmt | isgmt = UTC
161− booslToTType isstd _ = maybe UnknownType stdOrWall isstd
163+ boolsToTType isstd _
162− stdOrWall isstd
163− | isstd = Std
164+ | isstd = Std
164− | 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
165
174
166-- Parse a POSIX-style TZ string.
175-- Parse a POSIX-style TZ string.
167-- 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.
@@ -182,7 +191,7 @@ getTtInfo = do
182 gmtoff <- get32bitInt
191 gmtoff <- get32bitInt
183 isdst <- getBool
192 isdst <- getBool
184 abbrind <- get8bitInt
193 abbrind <- get8bitInt
185− return $ TtInfo gmtoff isdst UnknownType abbrind
194+ return $ TtInfo gmtoff isdst Wall abbrind
186
195
187-- Parse leap second info. (usually not used)
196-- Parse leap second info. (usually not used)
188getLeapInfo :: Integral a => Get a -> Get LeapInfo
197getLeapInfo :: Integral a => Get a -> Get LeapInfo
@@ -225,6 +234,11 @@ verify pred msg val
225 | pred val = return val
234 | pred val = return val
226 | otherwise = error msg
235 | otherwise = error msg
227
236
237+verify_ :: Monad m => (a -> Bool) -> String -> a -> m ()
238+verify_ pred msg val
239+ | pred val = return ()
240+ | otherwise = error msg
241+
228throwOlson :: FilePath -> String -> IO a
242throwOlson :: FilePath -> String -> IO a
229throwOlson fp msg = throw . OlsonError $
243throwOlson fp msg = throw . OlsonError $
230 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.hsnew file+118
@@ -0,0 +1,118 @@
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
3+-- names. This is useful for testing the Haskell implementation
4+-- of the Olson timezone parser and renderer against the reference
5+-- implementation in C. But less useful than it might seem at first,
6+-- because Haskell rounds historical solar mean timezones to the
7+-- nearest minute, whereas the C implementation rounds to the nearest
8+-- second.
9+module Main where
10+
11+import Data.Time.LocalTime.TimeZone.Series
12+import Data.Time.LocalTime.TimeZone.Olson
13+import Data.Time
14+import Data.Maybe (listToMaybe)
15+import System.Environment (getArgs)
16+import System.Exit (exitWith, exitSuccess, ExitCode(ExitFailure))
17+import System.Locale (defaultTimeLocale)
18+import Control.Monad (guard)
19+
20+version :: IO a
21+version = do
22+ putStrLn "hzdump version 0.1, clone of zdump 1.7 -c option fixed,"
23+ putStrLn "takes zone file paths instead of zone specifications"
24+ exitSuccess
25+
26+usage :: IO a
27+usage = do
28+ putStrLn "usage: hzdump [--version] [-v] [-c cutoff] zone-file-path ..."
29+ putStrLn " where cutoff is lo-year,hi-year or hi-year"
30+ exitWith $ ExitFailure 1
31+
32+illegalOpt :: Char -> IO a -- sic
33+illegalOpt opt = do
34+ putStrLn $ "hzdump: illegal option -- " ++ [opt]
35+ usage
36+
37+data Option = Version | Illegal Char |
38+ Verbose (Maybe Integer, Maybe Integer) | Now
39+ deriving (Eq, Ord, Show)
40+
41+main = do
42+ (opts, zones) <- fmap parseArgs getArgs
43+ (getTimes, displayTime) <- case opts of
44+ Version -> version
45+ Illegal opt -> illegalOpt opt
46+ Verbose rng -> return (transitionTimes rng, displayVerbose)
47+ _ -> do now <- getCurrentTime
48+ return (const [now], displayConcise)
49+ tzss <- mapM getZone zones
50+ putStr . unlines . concat $
51+ zipWith (displayZone getTimes displayTime) zones tzss
52+ where
53+ getZone "-" = return utcTZ -- sic
54+ getZone z = getTimeZoneSeriesFromOlsonFile z
55+
56+ displayZone getTimes displayTime zone tzs =
57+ map ((zone ++) . (" " ++) . displayTime tzs) $ getTimes tzs
58+
59+parseArgs :: [String] -> (Option, [FilePath])
60+parseArgs args | "--version" `elem` args = (Version, [])
61+parseArgs args = getOpts False (Nothing, Nothing) args
62+ where
63+ getOpts _ cutoff ("-v":args) = getOpts True cutoff args
64+ getOpts v _ ("-c":c:args) = maybe (Illegal 'c', [])
65+ (\y -> getOpts v y args) $
66+ parseCutoff c
67+ getOpts v cutoff ("--":zones ) = (opts v cutoff, zones)
68+ getOpts _ _ (('-':x:_):_) = (Illegal x, [])
69+ getOpts v cutoff zones = (opts v cutoff, zones)
70+
71+ opts v cutoff = if v then Verbose cutoff else Now
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+
81+displayConcise :: TimeZoneSeries -> UTCTime -> String
82+displayConcise tzs t = formatTime defaultTimeLocale format $
83+ ZoneSeriesTime t tzs
84+ where
85+ format = "%a %b %e %T %Y %Z"
86+
87+displayVerbose :: TimeZoneSeries -> UTCTime -> String
88+displayVerbose tzs t = concat [displayConcise utcTZ t, " = ",
89+ displayConcise tzs t, " isdst=", if isdst then "1" else "0"]
90+ where
91+ isdst = timeZoneSummerOnly $ timeZoneFromSeries tzs t
92+
93+transitionTimes :: (Maybe Integer, Maybe Integer) -> TimeZoneSeries ->
94+ [UTCTime]
95+transitionTimes (lo, hi) =
96+ maybe id (takeWhile . (>)) (fmap happyNewYear hi) .
97+ maybe id (dropWhile . (>)) (fmap happyNewYear lo) .
98+ addBugs . addPrevSecond . actualTransitions
99+ where
100+ happyNewYear y = UTCTime (fromGregorian y 1 1) 0
101+ actualTransitions (TimeZoneSeries d cs) =
102+ let rs = reverse cs
103+ in map snd . filter fst $
104+ zipWith (\(t, tz) prevTz -> (tz /= prevTz, t))
105+ rs
106+ (d : map snd rs)
107+ addPrevSecond = concatMap (\t -> [(-1) `addUTCTime` t, t])
108+ addBugs = (bug0 ++) . (++ bug1) -- sic
109+ bug0 = [UTCTime (fromGregorian 1901 12 13) (20*3600+45*60+52),
110+ UTCTime (fromGregorian 1901 12 14) (20*3600+45*60+52)]
111+ bug1 = [UTCTime (fromGregorian 2038 1 18) ( 3*3600+14*60+ 7),
112+ UTCTime (fromGregorian 2038 1 19) ( 3*3600+14*60+ 7)]
113+
114+utcTZ :: TimeZoneSeries
115+utcTZ = TimeZoneSeries utc []
116+
117+maybeRead :: Read a => String -> Maybe a
118+maybeRead = fmap fst . listToMaybe . filter (null . snd) . reads
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