{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
module Data.FileEmbed
(
embedFile
, embedOneFileOf
, embedDir
, embedDirListing
, getDir
, embedStringFile
, embedOneStringFileOf
#if MIN_VERSION_template_haskell(2,5,0)
, dummySpace
, dummySpaceWith
#endif
, inject
, injectFile
, injectWith
, injectFileWith
, makeRelativeToProject
, stringToBs
, bsToExp
, strToExp
) where
import Language.Haskell.TH.Syntax
( Exp (AppE, ListE, LitE, TupE, SigE, VarE)
, Lit (..)
, Q
, runIO
, qLocation, loc_filename
#if MIN_VERSION_template_haskell(2,7,0)
, Quasi(qAddDependentFile)
#endif
)
#if MIN_VERSION_template_haskell(2,16,0)
import Language.Haskell.TH ( mkBytes, bytesPrimL )
import qualified Data.ByteString.Internal as B
#endif
import System.Directory (doesDirectoryExist, doesFileExist,
getDirectoryContents, canonicalizePath)
import Control.Exception (throw, ErrorCall(..))
import Control.Monad (filterM)
import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as B8
import Control.Arrow ((&&&), second)
import Control.Applicative ((<$>))
import Data.ByteString.Unsafe (unsafePackAddressLen)
import System.IO.Unsafe (unsafePerformIO)
import System.FilePath ((</>), takeDirectory, takeExtension)
import Data.String (fromString)
import Prelude as P
import Data.List (sortBy)
import Data.Ord (comparing)
embedFile :: FilePath -> Q Exp
embedFile fp =
#if MIN_VERSION_template_haskell(2,7,0)
qAddDependentFile fp >>
#endif
(runIO $ B.readFile fp) >>= bsToExp
embedOneFileOf :: [FilePath] -> Q Exp
embedOneFileOf ps =
(runIO $ readExistingFile ps) >>= \ ( path, content ) -> do
#if MIN_VERSION_template_haskell(2,7,0)
qAddDependentFile path
#endif
bsToExp content
where
readExistingFile :: [FilePath] -> IO ( FilePath, B.ByteString )
readExistingFile xs = do
ys <- filterM doesFileExist xs
case ys of
(p:_) -> B.readFile p >>= \ c -> return ( p, c )
_ -> throw $ ErrorCall "Cannot find file to embed as resource"
embedDir :: FilePath -> Q Exp
embedDir fp = do
typ <- [t| [(FilePath, B.ByteString)] |]
e <- ListE <$> ((runIO $ fileList fp) >>= mapM (pairToExp fp))
return $ SigE e typ
embedDirListing :: FilePath -> Q Exp
embedDirListing fp = do
typ <- [t| [FilePath] |]
e <- ListE <$> ((runIO $ fmap fst <$> fileList fp) >>= mapM strToExp)
return $ SigE e typ
getDir :: FilePath -> IO [(FilePath, B.ByteString)]
getDir = fileList
pairToExp :: FilePath -> (FilePath, B.ByteString) -> Q Exp
pairToExp _root (path, bs) = do
#if MIN_VERSION_template_haskell(2,7,0)
qAddDependentFile $ _root ++ '/' : path
#endif
exp' <- bsToExp bs
return $! TupE
#if MIN_VERSION_template_haskell(2,16,0)
$ map Just
#endif
[LitE $ StringL path, exp']
bsToExp :: B.ByteString -> Q Exp
#if MIN_VERSION_template_haskell(2, 5, 0)
bsToExp bs =
return $ VarE 'unsafePerformIO
`AppE` (VarE 'unsafePackAddressLen
`AppE` LitE (IntegerL $ fromIntegral $ B8.length bs)
#if MIN_VERSION_template_haskell(2, 16, 0)
`AppE` LitE (bytesPrimL (
let B.PS ptr off sz = bs
in mkBytes ptr (fromIntegral off) (fromIntegral sz))))
#elif MIN_VERSION_template_haskell(2, 8, 0)
`AppE` LitE (StringPrimL $ B.unpack bs))
#else
`AppE` LitE (StringPrimL $ B8.unpack bs))
#endif
#else
bsToExp bs = do
helper <- [| stringToBs |]
let chars = B8.unpack bs
return $! AppE helper $! LitE $! StringL chars
#endif
stringToBs :: String -> B.ByteString
stringToBs = B8.pack
embedStringFile :: FilePath -> Q Exp
embedStringFile fp =
#if MIN_VERSION_template_haskell(2,7,0)
qAddDependentFile fp >>
#endif
(runIO $ P.readFile fp) >>= strToExp
embedOneStringFileOf :: [FilePath] -> Q Exp
embedOneStringFileOf ps =
(runIO $ readExistingFile ps) >>= \ ( path, content ) -> do
#if MIN_VERSION_template_haskell(2,7,0)
qAddDependentFile path
#endif
strToExp content
where
readExistingFile :: [FilePath] -> IO ( FilePath, String )
readExistingFile xs = do
ys <- filterM doesFileExist xs
case ys of
(p:_) -> P.readFile p >>= \ c -> return ( p, c )
_ -> throw $ ErrorCall "Cannot find file to embed as resource"
strToExp :: String -> Q Exp
#if MIN_VERSION_template_haskell(2, 5, 0)
strToExp s =
return $ VarE 'fromString
`AppE` LitE (StringL s)
#else
strToExp s = do
helper <- [| fromString |]
return $! AppE helper $! LitE $! StringL s
#endif
notHidden :: FilePath -> Bool
notHidden ('.':_) = False
notHidden _ = True
fileList :: FilePath -> IO [(FilePath, B.ByteString)]
fileList top = fileList' top ""
fileList' :: FilePath -> FilePath -> IO [(FilePath, B.ByteString)]
fileList' realTop top = do
allContents <- filter notHidden <$> getDirectoryContents (realTop </> top)
let all' = map ((top </>) &&& (\x -> realTop </> top </> x)) allContents
files <- filterM (doesFileExist . snd) all' >>=
mapM (liftPair2 . second B.readFile)
dirs <- filterM (doesDirectoryExist . snd) all' >>=
mapM (fileList' realTop . fst)
return $ sortBy (comparing fst) $ concat $ files : dirs
liftPair2 :: Monad m => (a, m b) -> m (a, b)
liftPair2 (a, b) = b >>= \b' -> return (a, b')
magic :: B.ByteString -> B.ByteString
magic x = B8.concat ["fe", x]
sizeLen :: Int
sizeLen = 20
getInner :: B.ByteString -> B.ByteString
getInner b =
let (sizeBS, rest) = B.splitAt sizeLen b
in case reads $ B8.unpack sizeBS of
(i, _):_ -> B.take i rest
[] -> error "Data.FileEmbed (getInner): Your dummy space has been corrupted."
padSize :: Int -> String
padSize i =
let s = show i
in replicate (sizeLen - length s) '0' ++ s
#if MIN_VERSION_template_haskell(2,5,0)
dummySpace :: Int -> Q Exp
dummySpace = dummySpaceWith "MS"
dummySpaceWith :: B.ByteString -> Int -> Q Exp
dummySpaceWith postfix space = do
let size = padSize space
magic' = magic postfix
start = B8.unpack magic' ++ size
magicLen = B8.length magic'
len = magicLen + sizeLen + space
chars = LitE $ StringPrimL $
#if MIN_VERSION_template_haskell(2,6,0)
map (toEnum . fromEnum) $
#endif
start ++ replicate space '0'
[| getInner (B.drop magicLen (unsafePerformIO (unsafePackAddressLen len $(return chars)))) |]
#endif
inject :: B.ByteString
-> B.ByteString
-> Maybe B.ByteString
inject = injectWith "MS"
injectWith :: B.ByteString
-> B.ByteString
-> B.ByteString
-> Maybe B.ByteString
injectWith postfix toInj orig =
if toInjL > size
then Nothing
else Just $ B.concat [before, magic', B8.pack $ padSize toInjL, toInj, B8.pack $ replicate (size - toInjL) '0', after]
where
magic' = magic postfix
toInjL = B.length toInj
(before, rest) = B.breakSubstring magic' orig
(sizeBS, rest') = B.splitAt sizeLen $ B.drop (B8.length magic') rest
size = case reads $ B8.unpack sizeBS of
(i, _):_ -> i
[] -> error $ "Data.FileEmbed (inject): Your dummy space has been corrupted. Size is: " ++ show sizeBS
after = B.drop size rest'
injectFile :: B.ByteString
-> FilePath
-> FilePath
-> IO ()
injectFile = injectFileWith "MS"
injectFileWith :: B.ByteString
-> B.ByteString
-> FilePath
-> FilePath
-> IO ()
injectFileWith postfix inj srcFP dstFP = do
src <- B.readFile srcFP
case injectWith postfix inj src of
Nothing -> error "Insufficient dummy space"
Just dst -> B.writeFile dstFP dst
makeRelativeToProject :: FilePath -> Q FilePath
makeRelativeToProject rel = do
loc <- qLocation
runIO $ do
srcFP <- canonicalizePath $ loc_filename loc
mdir <- findProjectDir srcFP
case mdir of
Nothing -> error $ "Could not find .cabal file for path: " ++ srcFP
Just dir -> return $ dir </> rel
where
findProjectDir x = do
let dir = takeDirectory x
if dir == x
then return Nothing
else do
contents <- getDirectoryContents dir
if any isCabalFile contents
then return (Just dir)
else findProjectDir dir
isCabalFile fp = takeExtension fp == ".cabal"