├── sample ├── .hidden ├── binary ├── bar ├── baz └── bin ├── test ├── sample │ ├── bar │ │ └── baz │ └── foo └── main.hs ├── .gitignore ├── README ├── test-inject.sh ├── template.hs ├── mkbin.hs ├── inject.hs ├── Setup.lhs ├── test.hs ├── runtests.hs ├── file-embed.cabal ├── LICENSE └── Data └── FileEmbed.hs /sample/.hidden: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sample/binary: -------------------------------------------------------------------------------- 1 | 234123 -------------------------------------------------------------------------------- /sample/bar: -------------------------------------------------------------------------------- 1 | bar bar bar 2 | -------------------------------------------------------------------------------- /sample/baz: -------------------------------------------------------------------------------- 1 | baz baz baz 2 | -------------------------------------------------------------------------------- /sample/bin: -------------------------------------------------------------------------------- 1 | bin bin bin 2 | -------------------------------------------------------------------------------- /test/sample/bar/baz: -------------------------------------------------------------------------------- 1 | baz 2 | -------------------------------------------------------------------------------- /test/sample/foo: -------------------------------------------------------------------------------- 1 | foo 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | *.swp 3 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Use Template Haskell to embed file contents directly. 2 | -------------------------------------------------------------------------------- /test-inject.sh: -------------------------------------------------------------------------------- 1 | ghc --make -fforce-recomp template.hs && runghc inject.hs && chmod +x injected && ./injected 2 | -------------------------------------------------------------------------------- /template.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE TemplateHaskell #-} 2 | import Data.FileEmbed 3 | 4 | main = print $(dummySpace 100) 5 | -------------------------------------------------------------------------------- /mkbin.hs: -------------------------------------------------------------------------------- 1 | import qualified Data.ByteString as S 2 | 3 | main = S.writeFile "sample/binary" $ S.pack [50, 51, 52, 0, 49, 50, 51] 4 | -------------------------------------------------------------------------------- /inject.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | import Data.FileEmbed 3 | 4 | main = injectFile "Hello World" "template" "injected" 5 | -------------------------------------------------------------------------------- /Setup.lhs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env runhaskell 2 | 3 | > module Main where 4 | > import Distribution.Simple 5 | 6 | > main :: IO () 7 | > main = defaultMain 8 | -------------------------------------------------------------------------------- /test.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE TemplateHaskell #-} 2 | 3 | import Data.FileEmbed 4 | import qualified Data.ByteString as B 5 | 6 | plainfile :: B.ByteString 7 | plainfile = $(embedFile "sample/bar") 8 | 9 | plaindir :: [(FilePath, B.ByteString)] 10 | plaindir = $(embedDir "sample") 11 | 12 | main :: IO () 13 | main = do 14 | print plainfile 15 | print plaindir 16 | -------------------------------------------------------------------------------- /test/main.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE TemplateHaskell #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | 4 | import Data.FileEmbed 5 | import Test.HUnit ((@?=)) 6 | import System.FilePath (()) 7 | 8 | main :: IO () 9 | main = do 10 | let received = $(embedDir "test/sample") 11 | received @?= 12 | [ ("foo", "foo\r\n") 13 | , ("bar" "baz", "baz\r\n") 14 | ] 15 | -------------------------------------------------------------------------------- /runtests.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE TemplateHaskell #-} 2 | import Test.Hspec 3 | import Test.Hspec.HUnit 4 | import Test.HUnit 5 | 6 | import Data.FileEmbed 7 | import qualified Data.ByteString as S 8 | import Data.List (sort) 9 | 10 | myeq x y = do 11 | y' <- y 12 | x @?= y' 13 | 14 | main = hspec $ descriptions 15 | [ describe "embedFile" 16 | [ it "handles text files" $ $(embedFile "sample/bar") `myeq` S.readFile "sample/bar" 17 | , it "handles binary files" $ $(embedFile "sample/binary") `myeq` S.readFile "sample/binary" 18 | ] 19 | , describe "getDir" 20 | [ it "takes all non-hidden files" $ ["bar", "baz", "bin", "binary"] `myeq` (fmap (sort . map fst) $ getDir "sample") 21 | ] 22 | , describe "embedDir" 23 | [ it "handles text and binary files" $ $(embedDir "sample") `myeq` getDir "sample" 24 | ] 25 | ] 26 | -------------------------------------------------------------------------------- /file-embed.cabal: -------------------------------------------------------------------------------- 1 | name: file-embed 2 | version: 0.0.4.8 3 | license: BSD3 4 | license-file: LICENSE 5 | author: Michael Snoyman 6 | maintainer: Michael Snoyman 7 | synopsis: Use Template Haskell to embed file contents directly. 8 | description: Use Template Haskell to read a file or all the files in a 9 | directory, and turn them into (path, text) pairs embedded 10 | in your haskell code. 11 | category: Data 12 | stability: Stable 13 | cabal-version: >= 1.8 14 | build-type: Simple 15 | homepage: https://github.com/snoyberg/file-embed 16 | extra-source-files: test/main.hs, test/sample/foo, test/sample/bar/baz 17 | 18 | library 19 | build-depends: base >= 4 && < 5 20 | , bytestring >= 0.9.1.4 21 | , directory >= 1.0.0.3 22 | , template-haskell 23 | , filepath 24 | exposed-modules: Data.FileEmbed 25 | ghc-options: -Wall 26 | 27 | test-suite test 28 | type: exitcode-stdio-1.0 29 | main-is: main.hs 30 | hs-source-dirs: test 31 | build-depends: base 32 | , file-embed 33 | , HUnit 34 | , filepath 35 | 36 | source-repository head 37 | type: git 38 | location: https://github.com/snoyberg/file-embed 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The following license covers this documentation, and the source code, except 2 | where otherwise indicated. 3 | 4 | Copyright 2008, Michael Snoyman. All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY EXPRESS OR 17 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 18 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 19 | EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, 20 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 21 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, 22 | OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 23 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 24 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 25 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /Data/FileEmbed.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE TemplateHaskell #-} 2 | {-# LANGUAGE CPP #-} 3 | -- | This module uses template Haskell. Following is a simplified explanation of usage for those unfamiliar with calling Template Haskell functions. 4 | -- 5 | -- The function @embedFile@ in this modules embeds a file into the exceutable 6 | -- that you can use it at runtime. A file is represented as a @ByteString@. 7 | -- However, as you can see below, the type signature indicates a value of type 8 | -- @Q Exp@ will be returned. In order to convert this into a @ByteString@, you 9 | -- must use Template Haskell syntax, e.g.: 10 | -- 11 | -- > $(embedFile "myfile.txt") 12 | -- 13 | -- This expression will have type @ByteString@. Be certain to enable the 14 | -- TemplateHaskell language extension, usually by adding the following to the 15 | -- top of your module: 16 | -- 17 | -- > {-# LANGUAGE TemplateHaskell #-} 18 | module Data.FileEmbed 19 | ( -- * Embed at compile time 20 | embedFile 21 | , embedDir 22 | , getDir 23 | -- * Inject into an executable 24 | #if MIN_VERSION_template_haskell(2,5,0) 25 | , dummySpace 26 | #endif 27 | , inject 28 | , injectFile 29 | ) where 30 | 31 | import Language.Haskell.TH.Syntax 32 | ( Exp (AppE, ListE, LitE, TupE, SigE) 33 | #if MIN_VERSION_template_haskell(2,5,0) 34 | , Lit (StringL, StringPrimL, IntegerL) 35 | #else 36 | , Lit (StringL, IntegerL) 37 | #endif 38 | , Q 39 | , runIO 40 | #if MIN_VERSION_template_haskell(2,7,0) 41 | , Quasi(qAddDependentFile) 42 | #endif 43 | ) 44 | import System.Directory (doesDirectoryExist, doesFileExist, 45 | getDirectoryContents) 46 | import Control.Monad (filterM) 47 | import qualified Data.ByteString as B 48 | import qualified Data.ByteString.Char8 as B8 49 | import Control.Arrow ((&&&), second) 50 | import Control.Applicative ((<$>)) 51 | import Data.ByteString.Unsafe (unsafePackAddressLen) 52 | import System.IO.Unsafe (unsafePerformIO) 53 | import System.FilePath (()) 54 | 55 | -- | Embed a single file in your source code. 56 | -- 57 | -- > import qualified Data.ByteString 58 | -- > 59 | -- > myFile :: Data.ByteString.ByteString 60 | -- > myFile = $(embedFile "dirName/fileName") 61 | embedFile :: FilePath -> Q Exp 62 | embedFile fp = 63 | #if MIN_VERSION_template_haskell(2,7,0) 64 | qAddDependentFile fp >> 65 | #endif 66 | (runIO $ B.readFile fp) >>= bsToExp 67 | 68 | -- | Embed a directory recusrively in your source code. 69 | -- 70 | -- > import qualified Data.ByteString 71 | -- > 72 | -- > myDir :: [(FilePath, Data.ByteString.ByteString)] 73 | -- > myDir = $(embedDir "dirName") 74 | embedDir :: FilePath -> Q Exp 75 | embedDir fp = do 76 | typ <- [t| [(FilePath, B.ByteString)] |] 77 | e <- ListE <$> ((runIO $ fileList fp) >>= mapM (pairToExp fp)) 78 | return $ SigE e typ 79 | 80 | -- | Get a directory tree in the IO monad. 81 | -- 82 | -- This is the workhorse of 'embedDir' 83 | getDir :: FilePath -> IO [(FilePath, B.ByteString)] 84 | getDir = fileList 85 | 86 | pairToExp :: FilePath -> (FilePath, B.ByteString) -> Q Exp 87 | pairToExp _root (path, bs) = do 88 | #if MIN_VERSION_template_haskell(2,7,0) 89 | qAddDependentFile $ _root ++ '/' : path 90 | #endif 91 | exp' <- bsToExp bs 92 | return $! TupE [LitE $ StringL path, exp'] 93 | 94 | bsToExp :: B.ByteString -> Q Exp 95 | bsToExp bs = do 96 | helper <- [| stringToBs |] 97 | let chars = B8.unpack bs 98 | return $! AppE helper $! LitE $! StringL chars 99 | 100 | stringToBs :: String -> B.ByteString 101 | stringToBs = B8.pack 102 | 103 | notHidden :: FilePath -> Bool 104 | notHidden ('.':_) = False 105 | notHidden _ = True 106 | 107 | fileList :: FilePath -> IO [(FilePath, B.ByteString)] 108 | fileList top = fileList' top "" 109 | 110 | fileList' :: FilePath -> FilePath -> IO [(FilePath, B.ByteString)] 111 | fileList' realTop top = do 112 | allContents <- filter notHidden <$> getDirectoryContents (realTop top) 113 | let all' = map ((top ) &&& (\x -> realTop top x)) allContents 114 | files <- filterM (doesFileExist . snd) all' >>= 115 | mapM (liftPair2 . second B.readFile) 116 | dirs <- filterM (doesDirectoryExist . snd) all' >>= 117 | mapM (fileList' realTop . fst) 118 | return $ concat $ files : dirs 119 | 120 | liftPair2 :: Monad m => (a, m b) -> m (a, b) 121 | liftPair2 (a, b) = b >>= \b' -> return (a, b') 122 | 123 | magic :: String 124 | magic = concat ["fe", "MS"] 125 | 126 | sizeLen :: Int 127 | sizeLen = 20 128 | 129 | getInner :: B.ByteString -> B.ByteString 130 | getInner b = 131 | let (sizeBS, rest) = B.splitAt sizeLen $ B.drop (length magic) b 132 | in case reads $ B8.unpack sizeBS of 133 | (i, _):_ -> B.take i rest 134 | [] -> error "Data.FileEmbed (getInner): Your dummy space has been corrupted." 135 | 136 | padSize :: Int -> String 137 | padSize i = 138 | let s = show i 139 | in replicate (sizeLen - length s) '0' ++ s 140 | 141 | #if MIN_VERSION_template_haskell(2,5,0) 142 | dummySpace :: Int -> Q Exp 143 | dummySpace space = do 144 | let size = padSize space 145 | let start = magic ++ size 146 | let chars = LitE $ StringPrimL $ 147 | #if MIN_VERSION_template_haskell(2,6,0) 148 | map (toEnum . fromEnum) $ 149 | #endif 150 | start ++ replicate space '0' 151 | let len = LitE $ IntegerL $ fromIntegral $ length start + space 152 | upi <- [|unsafePerformIO|] 153 | pack <- [|unsafePackAddressLen|] 154 | getInner' <- [|getInner|] 155 | return $ getInner' `AppE` (upi `AppE` (pack `AppE` len `AppE` chars)) 156 | #endif 157 | 158 | inject :: B.ByteString -- ^ bs to inject 159 | -> B.ByteString -- ^ original BS containing dummy 160 | -> Maybe B.ByteString -- ^ new BS, or Nothing if there is insufficient dummy space 161 | inject toInj orig = 162 | if toInjL > size 163 | then Nothing 164 | else Just $ B.concat [before, B8.pack magic, B8.pack $ padSize toInjL, toInj, B8.pack $ replicate (size - toInjL) '0', after] 165 | where 166 | toInjL = B.length toInj 167 | (before, rest) = B.breakSubstring (B8.pack magic) orig 168 | (sizeBS, rest') = B.splitAt sizeLen $ B.drop (length magic) rest 169 | size = case reads $ B8.unpack sizeBS of 170 | (i, _):_ -> i 171 | [] -> error $ "Data.FileEmbed (inject): Your dummy space has been corrupted. Size is: " ++ show sizeBS 172 | after = B.drop size rest' 173 | 174 | injectFile :: B.ByteString 175 | -> FilePath -- ^ template file 176 | -> FilePath -- ^ output file 177 | -> IO () 178 | injectFile inj srcFP dstFP = do 179 | src <- B.readFile srcFP 180 | case inject inj src of 181 | Nothing -> error "Insufficient dummy space" 182 | Just dst -> B.writeFile dstFP dst 183 | --------------------------------------------------------------------------------