/
FileEmbed.hs
139 lines (122 loc) · 4.6 KB
/
FileEmbed.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
{-# LANGUAGE TemplateHaskell #-}
module Data.FileEmbed
( -- * Embed at compile time
embedFile
, embedDir
, getDir
-- * Inject into an executable
, dummySpace
, inject
, injectFile
) where
import Language.Haskell.TH (runQ,
Exp (AppE, ListE, LitE, TupE),
Lit (StringL, StringPrimL, IntegerL),
Q,
runIO)
import System.Directory (doesDirectoryExist, doesFileExist,
getDirectoryContents)
import Control.Monad (filterM)
import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as B8
import Control.Arrow ((&&&), second, first)
import Control.Applicative ((<$>))
import Data.Monoid (mappend)
import Data.ByteString.Unsafe (unsafePackAddressLen)
import System.IO.Unsafe (unsafePerformIO)
-- | Embed a single file in your source code.
--
-- > import qualified Data.ByteString
-- >
-- > myFile :: Data.ByteString.ByteString
-- > myFile = $(embedFile "dirName/fileName")
embedFile :: FilePath -> Q Exp
embedFile fp = (runIO $ B.readFile fp) >>= bsToExp
-- | Embed a directory recusrively in your source code.
--
-- > import qualified Data.ByteString
-- >
-- > myDir :: [(FilePath, Data.ByteString.ByteString)]
-- > myDir = $(embedDir "dirName")
embedDir :: FilePath -> Q Exp
embedDir fp = ListE <$> ((runIO $ fileList fp) >>= mapM pairToExp)
-- | Get a directory tree in the IO monad.
--
-- This is the workhorse of 'embedDir'
getDir :: FilePath -> IO [(FilePath, B.ByteString)]
getDir = fileList
pairToExp :: (FilePath, B.ByteString) -> Q Exp
pairToExp (path, bs) = do
exp' <- bsToExp bs
return $! TupE [LitE $ StringL path, exp']
bsToExp :: B.ByteString -> Q Exp
bsToExp bs = do
helper <- runQ [| stringToBs |]
let chars = B8.unpack bs
return $! AppE helper $! LitE $! StringL chars
stringToBs :: String -> B.ByteString
stringToBs = B8.pack
notHidden :: FilePath -> Bool
notHidden ('.':_) = False
notHidden _ = True
fileList :: FilePath -> IO [(FilePath, B.ByteString)]
fileList top = map (first tail) <$> fileList' top ""
fileList' :: FilePath -> FilePath -> IO [(FilePath, B.ByteString)]
fileList' realTop top = do
let prefix1 = top ++ "/"
prefix2 = realTop ++ prefix1
allContents <- filter notHidden <$> getDirectoryContents prefix2
let all' = map (mappend prefix1 &&& mappend prefix2) allContents
files <- filterM (doesFileExist . snd) all' >>=
mapM (liftPair2 . second B.readFile)
dirs <- filterM (doesDirectoryExist . snd) all' >>=
mapM (fileList' realTop . fst)
return $ concat $ files : dirs
liftPair2 :: Monad m => (a, m b) -> m (a, b)
liftPair2 (a, b) = b >>= \b' -> return (a, b')
magic = concat ["fe", "MS"]
sizeLen = 20
getInner :: B.ByteString -> B.ByteString
getInner b =
let (sizeBS, rest) = B.splitAt sizeLen $ B.drop (length magic) 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
dummySpace :: Int -> Q Exp
dummySpace space = do
let size = padSize space
let start = magic ++ size
let chars = LitE $ StringPrimL $ start ++ replicate space '0'
let len = LitE $ IntegerL $ fromIntegral $ length start + space
upi <- [|unsafePerformIO|]
pack <- [|unsafePackAddressLen|]
getInner' <- [|getInner|]
return $ getInner' `AppE` (upi `AppE` (pack `AppE` len `AppE` chars))
inject :: B.ByteString -- ^ bs to inject
-> B.ByteString -- ^ original BS containing dummy
-> Maybe B.ByteString -- ^ new BS, or Nothing if there is insufficient dummy space
inject toInj orig =
if toInjL > size
then Nothing
else Just $ B.concat [before, B8.pack magic, B8.pack $ padSize toInjL, toInj, B8.pack $ replicate (size - toInjL) '0', after]
where
toInjL = B.length toInj
(before, rest) = B.breakSubstring (B8.pack magic) orig
(sizeBS, rest') = B.splitAt sizeLen $ B.drop (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
(dummy, after) = B.splitAt size rest'
injectFile :: B.ByteString
-> FilePath -- ^ template file
-> FilePath -- ^ output file
-> IO ()
injectFile inj srcFP dstFP = do
src <- B.readFile srcFP
case inject inj src of
Nothing -> error "Insufficient dummy space"
Just dst -> B.writeFile dstFP dst