tz-0.0.0.2: Data/Time/Zones/Read.hs
{- |
Module : Data.Time.Zones.Read
Copyright : (C) 2014 Mihaly Barasz
License : Apache-2.0, see LICENSE
Maintainer : Mihaly Barasz <klao@nilcons.com>
Stability : experimental
-}
{-# LANGUAGE OverloadedStrings #-}
module Data.Time.Zones.Read (
-- * Various ways of loading `TZ`
loadTZFromFile,
loadSystemTZ,
loadLocalTZ,
loadTZFromDB,
-- * Reading only the description, no parsing
tzDescriptionFromFile,
tzDescriptionFromDB,
systemTZDescription,
-- * Parsing Olson data
olsonGet,
parseTZDescription,
) where
import Control.Applicative
import Control.Monad
import Data.Binary
import Data.Binary.Get
import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy.Char8 as BL
import Data.Maybe
import Data.Vector.Generic (stream, unstream)
import qualified Data.Vector.Unboxed as VU
import qualified Data.Vector as VB
import Data.Int
import Data.Time.Zones.Types
import System.Environment
import System.IO.Error
import Paths_tz hiding (version)
-- | Reads and parses a time zone information file (in @tzfile(5)@
-- aka. Olson file format) and returns the corresponding TZ data
-- structure.
loadTZFromFile :: FilePath -> IO TZ
loadTZFromFile fname = runGet olsonGet <$> BL.readFile fname
-- | Looks for the time zone file in the system timezone directory, which is
-- @\/usr\/share\/zoneinfo@, or if the @TZDIR@ environment variable is
-- set, then there.
loadSystemTZ :: String -> IO TZ
loadSystemTZ tzName = do
dir <- fromMaybe "/usr/share/zoneinfo" <$> getEnvMaybe "TZDIR"
loadTZFromFile $ dir ++ "/" ++ tzName
-- | Returns the local `TZ` based on the @TZ@ and @TZDIR@
-- environment variables.
--
-- See @tzset(3)@ for details, but basically:
--
-- * If @TZ@ environment variable is unset, we @loadTZFromFile \"\/etc\/localtime\"@.
--
-- * If @TZ@ is set, but empty, we @loadSystemTZ \"UTC\"@.
--
-- * Otherwise, we just @loadSystemTZ@ it.
--
-- Note, this means we don't support POSIX-style @TZ@ variables (like
-- @\"EST5EDT\"@), only those that are explicitly present in the time
-- zone database.
loadLocalTZ :: IO TZ
loadLocalTZ = do
tzEnv <- getEnvMaybe "TZ"
case tzEnv of
Nothing -> loadTZFromFile "/etc/localtime"
Just "" -> loadSystemTZ "UTC"
Just z -> loadSystemTZ z
getEnvMaybe :: String -> IO (Maybe String)
getEnvMaybe var =
fmap Just (getEnv var) `catchIOError`
(\e -> if isDoesNotExistError e then return Nothing else ioError e)
-- | Reads the corresponding file from the time zone database shipped
-- with this package.
loadTZFromDB :: String -> IO TZ
loadTZFromDB tzName = do
-- TODO(klao): this probably won't work on Windows.
fn <- getDataFileName $ tzName ++ ".zone"
loadTZFromFile fn
tzDescriptionFromFile :: FilePath -> IO BL.ByteString
tzDescriptionFromFile fname = olsonDescription <$> BL.readFile fname
tzDescriptionFromDB :: String -> IO BL.ByteString
tzDescriptionFromDB tzName = do
-- TODO(klao): this probably won't work on Windows.
fn <- getDataFileName $ tzName ++ ".zone"
tzDescriptionFromFile fn
systemTZDescription :: String -> IO BL.ByteString
systemTZDescription tzName = do
dir <- fromMaybe "/usr/share/zoneinfo" <$> getEnvMaybe "TZDIR"
tzDescriptionFromFile $ dir ++ "/" ++ tzName
--------------------------------------------------------------------------------
olsonGet :: Get TZ
olsonGet = olsonGet' False
olsonGet' :: Bool -> Get TZ
olsonGet' abridged = do
version <- olsonHeader
case () of
() | version == '\0' -> olsonGetWith 4 getTime32
() | version `elem` ['2', '3'] -> do
unless abridged $ skipOlson0 >> void olsonHeader
olsonGetWith 8 getTime64
-- TODO(klao): read the rule string
_ -> fail $ "olsonGet: invalid version character: " ++ show version
olsonDescription :: BL.ByteString -> BL.ByteString
olsonDescription input = flip runGet input $ do
version <- olsonHeader
if version `elem` ['2', '3']
then skipOlson0 >> getRemainingLazyByteString
else return input
parseTZDescription :: BL.ByteString -> TZ
parseTZDescription = runGet (olsonGet' True)
olsonHeader :: Get Char
olsonHeader = do
magic <- getByteString 4
unless (magic == "TZif") $ fail "olsonHeader: bad magic"
version <- toEnum <$> getInt8
skip 15
return version
skipOlson0 :: Get ()
skipOlson0 = do
tzh_ttisgmtcnt <- getInt32
tzh_ttisstdcnt <- getInt32
tzh_leapcnt <- getInt32
tzh_timecnt <- getInt32
tzh_typecnt <- getInt32
tzh_charcnt <- getInt32
skip $ (4 * tzh_timecnt) + tzh_timecnt + (6 * tzh_typecnt) + tzh_charcnt +
(8 * tzh_leapcnt) + tzh_ttisstdcnt + tzh_ttisgmtcnt
olsonGetWith :: Int -> Get Int64 -> Get TZ
olsonGetWith szTime getTime = do
tzh_ttisgmtcnt <- getInt32
tzh_ttisstdcnt <- getInt32
tzh_leapcnt <- getInt32
tzh_timecnt <- getInt32
tzh_typecnt <- getInt32
tzh_charcnt <- getInt32
transitions <- VU.replicateM tzh_timecnt getTime
indices <- VU.replicateM tzh_timecnt getInt8
infos <- VU.replicateM tzh_typecnt getTTInfo
abbrs <- getByteString tzh_charcnt
skip $ tzh_leapcnt * (szTime + 4)
skip tzh_ttisstdcnt
skip tzh_ttisgmtcnt
let isDst (_,x,_) = x
gmtOff (x,_,_) = x
isDstName (_,d,ni) = (d, abbrForInd ni abbrs)
lInfos = VU.toList infos
first = head $ filter (not . isDst) lInfos ++ lInfos
vtrans = VU.cons minBound transitions
eInfos = VU.cons first $ VU.map (infos VU.!) indices
vdiffs = VU.map gmtOff eInfos
vinfos = VB.map isDstName $ unstream $ stream eInfos
return $ TZ vtrans vdiffs vinfos
abbrForInd :: Int -> BS.ByteString -> String
abbrForInd i = BS.unpack . BS.takeWhile (/= '\0') . BS.drop i
getTTInfo :: Get (Int, Bool, Int) -- (gmtoff, isdst, abbrind)
getTTInfo = (,,) <$> getInt32 <*> get <*> getInt8
getInt8 :: Get Int
{-# INLINE getInt8 #-}
getInt8 = fromIntegral <$> getWord8
getInt32 :: Get Int
{-# INLINE getInt32 #-}
getInt32 = (fromIntegral :: Int32 -> Int) . fromIntegral <$> getWord32be
getTime32 :: Get Int64
{-# INLINE getTime32 #-}
getTime32 = fromIntegral <$> getInt32
getTime64 :: Get Int64
{-# INLINE getTime64 #-}
getTime64 = fromIntegral <$> getWord64be