]> gitweb @ CieloNegro.org - Lucu.git/blobdiff - ImplantFile.hs
Make use of mimeType quasi-quoter.
[Lucu.git] / ImplantFile.hs
index 7410b5f4cde3803c44a652a179763ee749298660..c253c2abd05395b3311dba1fd9d3ed999d37d89b 100644 (file)
@@ -1,26 +1,35 @@
-import           Codec.Binary.Base64
-import           Codec.Compression.GZip
-import           Control.Monad
-import           Data.Bits
-import qualified Data.ByteString.Lazy as Lazy (ByteString)
-import qualified Data.ByteString.Lazy as L hiding (ByteString)
-import           Data.Char
-import           Data.Digest.SHA2
-import           Data.Int
-import           Data.Maybe
-import           Data.Time
-import           Data.Time.Clock.POSIX
-import           Data.Word
-import           Language.Haskell.Pretty
-import           Language.Haskell.Syntax
-import           Network.HTTP.Lucu.MIMEType
-import           Network.HTTP.Lucu.MIMEType.DefaultExtensionMap
-import           Network.HTTP.Lucu.MIMEType.Guess
-import           System.Console.GetOpt
-import           System.Environment
-import           System.Exit
-import           System.Posix.Files
-import           System.IO
+{-# LANGUAGE
+    OverloadedStrings
+  , UnicodeSyntax
+  #-}
+module Main where
+import Codec.Compression.GZip
+import Control.Applicative
+import Control.Monad
+import qualified Data.Ascii as A
+import qualified Data.ByteString.Base64 as B64
+import qualified Data.ByteString.Char8 as Strict
+import qualified Data.ByteString.Lazy as Lazy
+import Data.Char
+import Data.Digest.Pure.SHA
+import Data.Int
+import Data.Maybe
+import Data.Time
+import Data.Time.Clock.POSIX
+import Language.Haskell.Exts.Build
+import Language.Haskell.Exts.Extension
+import Language.Haskell.Exts.Pretty
+import Language.Haskell.Exts.Syntax
+import Network.HTTP.Lucu.ETag
+import Network.HTTP.Lucu.MIMEType
+import Network.HTTP.Lucu.MIMEType.DefaultExtensionMap
+import Network.HTTP.Lucu.MIMEType.Guess
+import Prelude.Unicode
+import System.Console.GetOpt
+import System.Environment
+import System.Exit
+import System.Posix.Files
+import System.IO
 
 data CmdOpt
     = OptOutput FilePath
@@ -31,416 +40,412 @@ data CmdOpt
     | OptHelp
     deriving (Eq, Show)
 
-
-options :: [OptDescr CmdOpt]
-options = [ Option ['o'] ["output"]
+options ∷ [OptDescr CmdOpt]
+options = [ Option "o" ["output"]
                        (ReqArg OptOutput "FILE")
                        "Output to the FILE."
 
-          , Option ['m'] ["module"]
+          , Option "m" ["module"]
                        (ReqArg OptModName "MODULE")
                        "Specify the resulting module name. (required)"
 
-          , Option ['s'] ["symbol"]
+          , Option "s" ["symbol"]
                        (ReqArg OptSymName "SYMBOL")
                        "Specify the resulting symbol name."
 
-          , Option ['t'] ["mime-type"]
+          , Option "t" ["mime-type"]
                        (ReqArg OptMIMEType "TYPE")
                        "Specify the MIME Type of the file."
 
-          , Option ['e'] ["etag"]
+          , Option "e" ["etag"]
                        (ReqArg OptETag "TAG")
                        "Specify the ETag of the file."
 
-          , Option ['h'] ["help"]
+          , Option "h" ["help"]
                        (NoArg OptHelp)
                        "Print this message."
           ]
 
-
-printUsage :: IO ()
-printUsage = do putStrLn ""
-                putStrLn "Description:"
-                putStrLn ("  lucu-implant-file is an utility that generates " ++
-                          "Haskell code containing an arbitrary file to " ++
-                          "compile it directly into programs and serve it " ++
-                          "statically with the Lucu HTTP server.")
-                putStrLn ""
-                putStrLn "Usage:"
-                putStrLn "  lucu-implant-file [OPTIONS...] FILE"
-                putStrLn ""
+printUsage ∷ IO ()
+printUsage = do mapM_ putStrLn msg
                 putStr $ usageInfo "Options:" options
                 putStrLn ""
-
-
-main :: IO ()
-main = do (opts, sources, errors) <- return . getOpt Permute options =<< getArgs
-
-          when (not $ null errors)
-                   $ do mapM_ putStr errors
-                        exitWith $ ExitFailure 1
-
-          when (any (\ x -> x == OptHelp) opts)
-                   $ do printUsage
-                        exitWith ExitSuccess
+    where
+      msg = [ ""
+            , "Description:"
+            , concat [ "  lucu-implant-file is an utility that generates " 
+                     , "Haskell code containing an arbitrary file to "
+                     , "compile it directly into programs and serve it "
+                     , "statically with the Lucu HTTP server."
+                     ]
+            , ""
+            , "Usage:"
+            , "  lucu-implant-file [OPTIONS...] FILE"
+            , ""
+            ]
+
+main ∷ IO ()
+main = do (opts, sources, errors) ← getOpt Permute options <$> getArgs
+
+          unless (null errors)
+              $ do mapM_ putStr errors
+                   exitWith $ ExitFailure 1
+
+          when (any (≡ OptHelp) opts)
+              $ do printUsage
+                   exitWith ExitSuccess
 
           when (null sources)
-                   $ do printUsage
-                        exitWith $ ExitFailure 1
+               $ do printUsage
+                    exitWith $ ExitFailure 1
 
-          when (length sources >= 2)
-                   $ error "too many input files."
+          when (length sources  2)
+               $ fail "too many input files."
 
           generateHaskellSource opts (head sources)
 
-
-generateHaskellSource :: [CmdOpt] -> FilePath -> IO ()
+generateHaskellSource ∷ [CmdOpt] → FilePath → IO ()
 generateHaskellSource opts srcFile
-    = do modName  <- getModuleName opts
-         symName  <- getSymbolName opts modName
-         mimeType <- getMIMEType opts srcFile
-         lastMod  <- getLastModified srcFile
-         input    <- openInput srcFile
-         output   <- openOutput opts
-         eTag     <- getETag opts input
-
-         let gzippedData = compressWith BestCompression input
-             originalLen = L.length input
-             gzippedLen  = L.length gzippedData
+    = do modName ← getModuleName opts
+         symName ← getSymbolName opts modName
+         mType   ← getMIMEType opts srcFile
+         lastMod ← getLastModified srcFile
+         input   ← openInput srcFile
+         output  ← openOutput opts
+         tag     ← getETag opts input
+
+         let compParams  = defaultCompressParams { compressLevel = bestCompression }
+             gzippedData = compressWith compParams input
+             originalLen = Lazy.length input
+             gzippedLen  = Lazy.length gzippedData
              useGZip     = originalLen > gzippedLen
-             rawB64      = encode $ L.unpack input
-             gzippedB64  = encode $ L.unpack gzippedData
-
-         header <- mkHeader srcFile originalLen gzippedLen useGZip mimeType eTag lastMod
-             
-         let hsModule = HsModule undefined (Module modName) (Just exports) imports decls
-             exports  = [HsEVar (UnQual (HsIdent symName))]
-             imports  = [ HsImportDecl undefined (Module "Codec.Binary.Base64")
-                                       False Nothing Nothing
-                        , HsImportDecl undefined (Module "Data.ByteString.Lazy")
-                                       True (Just (Module "L")) Nothing
-                        , HsImportDecl undefined (Module "Data.Time")
-                                       False Nothing Nothing
-                        , HsImportDecl undefined (Module "Network.HTTP.Lucu")
-                                       False Nothing Nothing
-                        ]
-                        ++
-                        (if useGZip then
-                             [ HsImportDecl undefined (Module "Control.Monad")
-                                            False Nothing Nothing
-                             , HsImportDecl undefined (Module "Codec.Compression.GZip")
-                                            False Nothing Nothing
-                             ]
-                         else
-                             [])
-             decls    = declResourceDef
-                        ++
-                        declEntityTag
-                        ++
-                        declLastModified
-                        ++
-                        declContentType
-                        ++
-                        (if useGZip
-                         then declGZippedData
-                         else declRawData)
-
-             declResourceDef :: [HsDecl]
-             declResourceDef
-                 = [ HsTypeSig undefined [HsIdent symName]
-                               (HsQualType []
-                                (HsTyCon (UnQual (HsIdent "ResourceDef"))))
-                   , HsFunBind [HsMatch undefined (HsIdent symName)
-                                [] (HsUnGuardedRhs defResourceDef) []]
-                   ]
-
-             defResourceDef :: HsExp
-             defResourceDef 
-                 = let defResGet = if useGZip
-                                   then defResGetGZipped
-                                   else defResGetRaw
-                   in 
-                     (HsRecConstr (UnQual (HsIdent "ResourceDef"))
-                      [ HsFieldUpdate (UnQual (HsIdent "resUsesNativeThread"))
-                                      (HsCon (UnQual (HsIdent "False")))
-                      , HsFieldUpdate (UnQual (HsIdent "resIsGreedy"))
-                                      (HsCon (UnQual (HsIdent "False")))
-                      , HsFieldUpdate (UnQual (HsIdent "resGet")) defResGet
-                      , HsFieldUpdate (UnQual (HsIdent "resHead"))
-                                      (HsCon (UnQual (HsIdent "Nothing")))
-                      , HsFieldUpdate (UnQual (HsIdent "resPost"))
-                                      (HsCon (UnQual (HsIdent "Nothing")))
-                      , HsFieldUpdate (UnQual (HsIdent "resPut"))
-                                      (HsCon (UnQual (HsIdent "Nothing")))
-                      , HsFieldUpdate (UnQual (HsIdent "resDelete"))
-                                      (HsCon (UnQual (HsIdent "Nothing")))
-                      ]
-                     )
-
-             defResGetGZipped :: HsExp
-             defResGetGZipped
-                 = let doExp = HsDo [ doFoundEntity
-                                    , doSetContentType
-                                    , bindMustGunzip
-                                    , doConditionalOutput
-                                    ]
-                       doFoundEntity
-                           = HsQualifier (HsApp (HsApp (HsVar (UnQual (HsIdent "foundEntity")))
-                                                 (HsVar (UnQual (HsIdent "entityTag"))))
-                                          (HsVar (UnQual (HsIdent "lastModified"))))
-                       doSetContentType
-                           = HsQualifier (HsApp (HsVar (UnQual (HsIdent "setContentType")))
-                                          (HsVar (UnQual (HsIdent "contentType"))))
-                       bindMustGunzip
-                           = HsGenerator undefined
-                             (HsPVar (HsIdent "mustGunzip"))
-                             (HsApp (HsApp (HsVar (UnQual (HsIdent "liftM")))
-                                     (HsVar (UnQual (HsIdent "not"))))
-                              (HsParen
-                               (HsApp (HsVar (UnQual (HsIdent "isEncodingAcceptable")))
-                                      (HsLit (HsString "gzip")))))
-                       doConditionalOutput
-                           = HsQualifier
-                             (HsIf (HsVar (UnQual (HsIdent "mustGunzip")))
-                                   expOutputGunzipped
-                                   expOutputGZipped)
-                       expOutputGunzipped
-                           = (HsApp (HsVar (UnQual (HsIdent "outputLBS")))
-                              (HsParen
-                               (HsApp (HsVar (UnQual (HsIdent "decompress")))
-                                      (HsVar (UnQual (HsIdent "gzippedData"))))))
-                       expOutputGZipped
-                           = HsDo [ doSetContentEncodingGZip
-                                  , doOutputGZipped
-                                  ]
-                       doSetContentEncodingGZip
-                           = HsQualifier (HsApp (HsVar (UnQual (HsIdent "setContentEncoding")))
-                                          (HsList [HsLit (HsString "gzip")]))
-                       doOutputGZipped
-                           = HsQualifier (HsApp (HsVar (UnQual (HsIdent "outputLBS")))
-                                          (HsVar (UnQual (HsIdent "gzippedData"))))
-                   in 
-                     HsApp (HsCon (UnQual (HsIdent "Just")))
-                           (HsParen doExp)
-
-             defResGetRaw :: HsExp
-             defResGetRaw
-                 = let doExp = HsDo [ doFoundEntity
-                                    , doSetContentType
-                                    , doOutputRawData
-                                    ]
-                       doFoundEntity
-                           = HsQualifier (HsApp (HsApp (HsVar (UnQual (HsIdent "foundEntity")))
-                                                 (HsVar (UnQual (HsIdent "entityTag"))))
-                                          (HsVar (UnQual (HsIdent "lastModified"))))
-                       doSetContentType
-                           = HsQualifier (HsApp (HsVar (UnQual (HsIdent "setContentType")))
-                                          (HsVar (UnQual (HsIdent "contentType"))))
-                       doOutputRawData
-                           = HsQualifier (HsApp (HsVar (UnQual (HsIdent "outputLBS")))
-                                          (HsVar (UnQual (HsIdent "rawData"))))
-                   in
-                     HsApp (HsCon (UnQual (HsIdent "Just")))
-                           (HsParen doExp)
-
-             declEntityTag :: [HsDecl]
-             declEntityTag
-                 = [ HsTypeSig undefined [HsIdent "entityTag"]
-                               (HsQualType []
-                                (HsTyCon (UnQual (HsIdent "ETag"))))
-                   , HsFunBind [HsMatch undefined (HsIdent "entityTag")
-                                [] (HsUnGuardedRhs defEntityTag) []]
-                   ]
-
-             defEntityTag :: HsExp
-             defEntityTag
-                 = HsApp (HsVar (UnQual (HsIdent "strongETag")))
-                   (HsLit (HsString eTag))
-
-             declLastModified :: [HsDecl]
-             declLastModified
-                 = [ HsTypeSig undefined [HsIdent "lastModified"]
-                               (HsQualType []
-                                (HsTyCon (UnQual (HsIdent "UTCTime"))))
-                   , HsFunBind [HsMatch undefined (HsIdent "lastModified")
-                                [] (HsUnGuardedRhs defLastModified) []]
-                   ]
-
-             defLastModified :: HsExp
-             defLastModified 
-                 = HsApp (HsVar (UnQual (HsIdent "read")))
-                   (HsLit (HsString $ show lastMod))
-                            
-
-             declContentType :: [HsDecl]
-             declContentType 
-                 = [ HsTypeSig undefined [HsIdent "contentType"]
-                               (HsQualType []
-                                (HsTyCon (UnQual (HsIdent "MIMEType"))))
-                   , HsFunBind [HsMatch undefined (HsIdent "contentType")
-                                [] (HsUnGuardedRhs defContentType) []]
-                   ]
-
-             defContentType :: HsExp
-             defContentType
-                 = HsApp (HsVar (UnQual (HsIdent "read")))
-                   (HsLit (HsString $ show mimeType))
-
-             declGZippedData :: [HsDecl]
-             declGZippedData 
-                 = [ HsTypeSig undefined [HsIdent "gzippedData"]
-                               (HsQualType []
-                                (HsTyCon (Qual (Module "L") (HsIdent "ByteString"))))
-                   , HsFunBind [HsMatch undefined (HsIdent "gzippedData")
-                                [] (HsUnGuardedRhs defGZippedData) []]
-                   ]
-
-             defGZippedData :: HsExp
-             defGZippedData 
-                 = HsApp (HsVar (Qual (Module "L") (HsIdent "pack")))
-                   (HsParen
-                    (HsApp (HsVar (UnQual (HsIdent "decode")))
-                     (HsLit (HsString gzippedB64))))
-
-             declRawData :: [HsDecl]
-             declRawData 
-                 = [ HsTypeSig undefined [HsIdent "rawData"]
-                               (HsQualType []
-                                (HsTyCon (Qual (Module "L") (HsIdent "ByteString"))))
-                   , HsFunBind [HsMatch undefined (HsIdent "rawData")
-                                [] (HsUnGuardedRhs defRawData) []]
-                   ]
-
-             defRawData :: HsExp
-             defRawData
-                 = HsApp (HsVar (Qual (Module "L") (HsIdent "pack")))
-                   (HsParen
-                    (HsApp (HsVar (UnQual (HsIdent "decode")))
-                     (HsLit (HsString rawB64))))
+             rawB64      = B64.encode <$> Lazy.toChunks input
+             gzippedB64  = B64.encode <$> Lazy.toChunks gzippedData
+
+         header ← mkHeader srcFile originalLen gzippedLen useGZip mType tag lastMod
+
+         let hsModule = mkModule modName symName imports decls
+             imports  = mkImports useGZip
+             decls    = concat [ resourceDecl symName useGZip
+                               , entityTagDecl tag
+                               , lastModifiedDecl lastMod
+                               , contentTypeDecl mType
+                               , if useGZip then
+                                     dataDecl (name "gzippedData") gzippedB64
+                                 else
+                                     dataDecl (name "rawData") rawB64
+                               ]
 
          hPutStrLn output header
          hPutStrLn output (prettyPrint hsModule)
          hClose output
 
-
-mkHeader :: FilePath -> Int64 -> Int64 -> Bool -> MIMEType -> String -> UTCTime -> IO String
-mkHeader srcFile originalLen gzippedLen useGZip mimeType eTag lastMod
-    = do localLastMod <- utcToLocalZonedTime lastMod
-         return ("{- DO NOT EDIT THIS FILE.\n" ++
-                 "   This file is automatically generated by the lucu-implant-file program.\n" ++
-                 "\n" ++
-                 "              Source: " ++ (if srcFile == "-"
-                                              then "(stdin)"
-                                              else srcFile) ++ "\n" ++
-                 "     Original Length: " ++ show originalLen ++ " bytes\n" ++
-                 (if useGZip
-                  then "   Compressed Length: " ++ show gzippedLen ++ " bytes\n" ++
-                       "         Compression: gzip\n"
-                  else "         Compression: disabled\n") ++
-                 "           MIME Type: " ++ show mimeType ++ "\n" ++
-                 "                ETag: " ++ eTag ++ "\n" ++
-                 "       Last Modified: " ++ show localLastMod ++ "\n" ++
-                 " -}")
-
-
-getModuleName :: [CmdOpt] -> IO String
-getModuleName opts
-    = let modNameOpts = filter (\ x -> case x of
-                                         OptModName _ -> True
-                                         _            -> False) opts
-      in
-        case modNameOpts of
-          []                      -> error "a module name must be given."
-          (OptModName modName):[] -> return modName
-          _                       -> error "too many --module options."
-
-
-getSymbolName :: [CmdOpt] -> String -> IO String
-getSymbolName opts modName
-    = let symNameOpts    = filter (\ x -> case x of
-                                            OptSymName _ -> True
-                                            _            -> False) opts
-          -- モジュール名をピリオドで分割した時の最後の項目の先頭文字を
-          -- 小文字にしたものを使ふ。
-          defaultSymName = mkDefault modName
-          mkDefault      = headToLower . getLastComp
-          headToLower    = \ str -> case str of
-                                      []     -> error "module name must not be empty"
-                                      (x:xs) -> toLower x : xs
-          getLastComp    = reverse . fst . break (== '.') . reverse
-      in
-        case symNameOpts of
-          []                      -> return defaultSymName
-          (OptSymName symName):[] -> return symName
-          _                       -> error "too many --symbol options."
-
-
-getMIMEType :: [CmdOpt] -> FilePath -> IO MIMEType
-getMIMEType opts srcFile
-    = let mimeTypeOpts = filter (\ x -> case x of
-                                          OptMIMEType _ -> True
-                                          _             -> False) opts
-          defaultType  = fromMaybe (read "application/octet-stream")
-                         $ guessTypeByFileName defaultExtensionMap srcFile
+mkModule ∷ ModuleName → Name → [ImportDecl] → [Decl] → Module
+mkModule modName symName imports decls
+    = let modPragma = [ LanguagePragma (⊥) [ name (show OverloadedStrings) ] ]
+          exports   = [ EVar (UnQual symName) ]
       in
-        case mimeTypeOpts of
-          []                        -> return defaultType
-          (OptMIMEType mimeType):[] -> return $ read mimeType
-          _                         -> error "too many --mime-type options."
-
+        Module (⊥) modName modPragma Nothing (Just exports) imports decls
+
+mkImports ∷ Bool → [ImportDecl]
+mkImports useGZip
+    = [ ImportDecl (⊥) (ModuleName "Data.ByteString.Base64")
+                   True False Nothing (Just (ModuleName "B64")) Nothing
+      , ImportDecl (⊥) (ModuleName "Data.ByteString.Lazy")
+                   True False Nothing (Just (ModuleName "Lazy")) Nothing
+      , ImportDecl (⊥) (ModuleName "Data.Time")
+                   False False Nothing Nothing Nothing
+      , ImportDecl (⊥) (ModuleName "Network.HTTP.Lucu")
+                   False False Nothing Nothing Nothing
+      ]
+      ⧺
+      [ ImportDecl (⊥) (ModuleName "Codec.Compression.GZip")
+                   False False Nothing Nothing Nothing
+        | useGZip ]
+
+resourceDecl ∷ Name → Bool → [Decl]
+resourceDecl symName useGZip
+    = [ TypeSig (⊥) [symName] (TyCon (UnQual (name "ResourceDef")))
+      , nameBind (⊥) symName valExp
+      ]
+    where
+      valExp ∷ Exp
+      valExp = RecUpdate (function "emptyResource")
+               [ FieldUpdate (UnQual (name "resGet" )) resGet
+               , FieldUpdate (UnQual (name "resHead")) resHead
+               ]
+
+      resGet ∷ Exp
+      resGet | useGZip   = resGetGZipped
+             | otherwise = resGetRaw
+
+resHead ∷ Exp
+resHead
+    = function "Just" `app`
+      paren (doE [ foundEntityStmt
+                 , setContentTypeStmt
+                 ])
+
+resGetGZipped ∷ Exp
+resGetGZipped
+    = function "Just" `app`
+      paren (doE [ foundEntityStmt
+                 , setContentTypeStmt
+                 , bindGZipStmt
+                 , conditionalOutputStmt
+                 ])
+    where
+      condVarName ∷ Name
+      condVarName = name "gzipAllowed"
+
+      dataVarName ∷ Name
+      dataVarName = name "gzippedData"
+
+      bindGZipStmt ∷ Stmt
+      bindGZipStmt
+          = genStmt (⊥)
+            (pvar condVarName)
+            (function "isEncodingAcceptable" `app` strE "gzip")
+
+      conditionalOutputStmt ∷ Stmt
+      conditionalOutputStmt
+          = qualStmt $
+            If (var condVarName)
+               (doE [ setContentEncodingGZipStmt
+                    , putChunksStmt (var dataVarName)
+                    ])
+               (putChunksExp
+                (paren
+                 (function "decompress" `app` var dataVarName)))
+
+resGetRaw ∷ Exp
+resGetRaw
+    = function "Just" `app`
+      paren (doE [ foundEntityStmt
+                 , setContentTypeStmt
+                 , putChunksStmt (function "rawData")
+                 ])
+
+setContentEncodingGZipStmt ∷ Stmt
+setContentEncodingGZipStmt
+    = qualStmt
+      ( function "setContentEncoding"
+        `app`
+        listE [ strE "gzip" ]
+      )
+
+foundEntityStmt ∷ Stmt
+foundEntityStmt
+    = qualStmt $
+      metaFunction "foundEntity"
+                       [ var (name "entityTag")
+                       , var (name "lastModified")
+                       ]
+
+setContentTypeStmt ∷ Stmt
+setContentTypeStmt
+    = qualStmt
+      ( function "setContentType"
+        `app`
+        function "contentType"
+      )
+
+putChunksExp ∷ Exp → Exp
+putChunksExp = app (function "putChunks")
+
+putChunksStmt ∷ Exp → Stmt
+putChunksStmt = qualStmt ∘ putChunksExp
+
+entityTagDecl ∷ ETag → [Decl]
+entityTagDecl tag
+    = [ TypeSig (⊥) [varName] (TyCon (UnQual (name "ETag")))
+      , nameBind (⊥) varName valExp
+      ]
+    where
+      varName ∷ Name
+      varName = name "entityTag"
+
+      valExp ∷ Exp
+      valExp = function "parseETag" `app` strE (eTagToString tag)
+
+lastModifiedDecl ∷ UTCTime → [Decl]
+lastModifiedDecl lastMod
+    = [ TypeSig (⊥) [varName] (TyCon (UnQual (name "UTCTime")))
+      , nameBind (⊥) varName valExp
+      , InlineSig (⊥) False AlwaysActive (UnQual varName)
+      ]
+    where
+      varName ∷ Name
+      varName = name "lastModified"
+
+      valExp ∷ Exp
+      valExp = function "read" `app` strE (show lastMod)
+
+contentTypeDecl ∷ MIMEType → [Decl]
+contentTypeDecl mime
+    = [ TypeSig (⊥) [varName] (TyCon (UnQual (name "MIMEType")))
+      , nameBind (⊥) varName valExp
+      , InlineSig (⊥) False AlwaysActive (UnQual varName)
+      ]
+    where
+      varName ∷ Name
+      varName = name "contentType"
 
-getLastModified :: FilePath -> IO UTCTime
-getLastModified "-"   = getCurrentTime
-getLastModified fpath = getFileStatus fpath
-                        >>= return . posixSecondsToUTCTime . fromRational . toRational . modificationTime
+      valExp ∷ Exp
+      valExp = function "parseMIMEType" `app` strE (mimeToString mime)
 
+      mimeToString ∷ MIMEType → String
+      mimeToString = A.toString ∘ A.fromAsciiBuilder ∘ printMIMEType
 
-getETag :: [CmdOpt] -> Lazy.ByteString -> IO String
-getETag opts input
-    = let eTagOpts = filter (\ x -> case x of
-                                      OptETag _ -> True
-                                      _         -> False) opts
-      in
-        case eTagOpts of
-          []               -> return mkETagFromInput
-          (OptETag str):[] -> return str
-          _                -> error "too many --etag options."
+dataDecl ∷ Name → [Strict.ByteString] → [Decl]
+dataDecl varName chunks
+    = [ TypeSig (⊥) [varName] (TyCon (Qual (ModuleName "Lazy") (name "ByteString")))
+      , nameBind (⊥) varName valExp
+      , InlineSig (⊥) False AlwaysActive (UnQual varName)
+      ]
+    where
+      valExp ∷ Exp
+      valExp = qvar (ModuleName "Lazy") (name "fromChunks")
+               `app`
+               listE (chunkToExp <$> chunks)
+
+      chunkToExp ∷ Strict.ByteString → Exp
+      chunkToExp chunk
+          = qvar (ModuleName "B64") (name "decodeLenient")
+            `app`
+            strE (Strict.unpack chunk)
+
+mkHeader ∷ FilePath → Int64 → Int64 → Bool → MIMEType → ETag → UTCTime → IO String
+mkHeader srcFile originalLen gzippedLen useGZip mType tag lastMod
+    = do localLastMod ← utcToLocalZonedTime lastMod
+         return $ concat
+                    [ "{- DO NOT EDIT THIS FILE.\n"
+                    , "   This file is automatically generated by the lucu-implant-file program.\n"
+                    , "\n"
+                    , "              Source: ", if srcFile ≡ "-" then
+                                                    "(stdin)"
+                                                else
+                                                    srcFile
+                    , "\n"
+                    , "     Original Length: ", show originalLen, " bytes\n"
+                    , if useGZip then
+                          "   Compressed Length: " ⧺ show gzippedLen ⧺ " bytes\n" ⧺
+                          "         Compression: gzip\n"
+                      else
+                          "         Compression: disabled\n"
+                    , "           MIME Type: ", mimeTypeToString mType, "\n"
+                    , "                ETag: ", eTagToString tag, "\n"
+                    , "       Last Modified: ", show localLastMod, "\n"
+                    , " -}"
+                    ]
+
+eTagToString ∷ ETag → String
+eTagToString = A.toString ∘ A.fromAsciiBuilder ∘ printETag
+
+mimeTypeToString ∷ MIMEType → String
+mimeTypeToString = A.toString ∘ A.fromAsciiBuilder ∘ printMIMEType
+
+getModuleName ∷ [CmdOpt] → IO ModuleName
+getModuleName opts
+    = case modNameOpts of
+        []                    → fail "a module name must be given."
+        OptModName modName:[] → return $ ModuleName modName
+        _                     → fail "too many --module options."
     where
-      mkETagFromInput :: String
-      mkETagFromInput = "SHA-1:" ++ (toHex $ toOctets $ sha256 $ L.unpack input)
+      modNameOpts ∷ [CmdOpt]
+      modNameOpts = filter (\ x → case x of
+                                     OptModName _ → True
+                                     _            → False) opts
+
+getSymbolName ∷ [CmdOpt] → ModuleName → IO Name
+getSymbolName opts (ModuleName modName)
+    = case symNameOpts of
+        []                    → return defaultSymName
+        OptSymName symName:[] → return $ name symName
+        _                     → fail "too many --symbol options."
+    where
+      symNameOpts ∷ [CmdOpt]
+      symNameOpts = filter (\ x → case x of
+                                     OptSymName _ → True
+                                     _            → False) opts
 
-      toHex :: [Word8] -> String
-      toHex []     = ""
-      toHex (x:xs) = hexByte (fromIntegral x) ++ toHex xs
+      defaultSymName ∷ Name
+      defaultSymName
+          = name $ headToLower $ getLastComp modName
 
-      hexByte :: Int -> String
-      hexByte n
-          = hex4bit ((n `shiftR` 4) .&. 0x0F) : hex4bit (n .&. 0x0F) : []
+      headToLower ∷ String → String
+      headToLower []     = error "module name must not be empty"
+      headToLower (x:xs) = toLower x : xs
 
-      hex4bit :: Int -> Char
-      hex4bit n
-          | n < 10    = (chr $ ord '0' + n     )
-          | n < 16    = (chr $ ord 'a' + n - 10)
-          | otherwise = undefined
+      getLastComp ∷ String → String
+      getLastComp = reverse ∘ fst ∘ break (≡ '.') ∘ reverse
 
+getMIMEType ∷ [CmdOpt] → FilePath → IO MIMEType
+getMIMEType opts srcFile
+    = case mimeTypeOpts of
+        []  → return defaultType
+        OptMIMEType ty:[]
+            → case A.fromChars ty of
+                 Just a  → return $ parseMIMEType a
+                 Nothing → fail "MIME type must not contain any non-ASCII letters."
+        _   → fail "too many --mime-type options."
+    where
+      mimeTypeOpts ∷ [CmdOpt]
+      mimeTypeOpts
+          = filter (\ x → case x of
+                             OptMIMEType _ → True
+                             _             → False) opts
 
-openInput :: FilePath -> IO Lazy.ByteString
-openInput "-"   = L.getContents
-openInput fpath = L.readFile fpath
+      octetStream ∷ MIMEType
+      octetStream = parseMIMEType "application/octet-stream"
 
+      defaultType ∷ MIMEType
+      defaultType = fromMaybe octetStream
+                    $ guessTypeByFileName defaultExtensionMap srcFile
 
-openOutput :: [CmdOpt] -> IO Handle
-openOutput opts
-    = let outputOpts = filter (\ x -> case x of
-                                        OptOutput _ -> True
-                                        _           -> False) opts
-      in
-        case outputOpts of
-          []                   -> return stdout
-          (OptOutput fpath):[] -> openFile fpath WriteMode
-          _                    -> error "two many --output options."
+getLastModified ∷ FilePath → IO UTCTime
+getLastModified "-"   = getCurrentTime
+getLastModified fpath = (posixSecondsToUTCTime ∘ fromRational ∘ toRational ∘ modificationTime)
+                        <$>
+                        getFileStatus fpath
 
+getETag ∷ [CmdOpt] → Lazy.ByteString → IO ETag
+getETag opts input
+    = case eTagOpts of
+        []             → return mkETagFromInput
+        OptETag str:[] → return $ strToETag str
+        _              → fail "too many --etag options."
+    where
+      eTagOpts ∷ [CmdOpt]
+      eTagOpts = filter (\ x → case x of
+                                  OptETag _ → True
+                                  _         → False) opts
+
+      mkETagFromInput ∷ ETag
+      mkETagFromInput
+          = strToETag $ "SHA-1:" ⧺ showDigest (sha1 input)
+
+      strToETag ∷ String → ETag
+      strToETag str
+          = case A.fromChars str of
+              Just a  → strongETag a
+              Nothing → error "ETag must not contain any non-ASCII letters."
+
+openInput ∷ FilePath → IO Lazy.ByteString
+openInput "-"   = Lazy.getContents
+openInput fpath = Lazy.readFile fpath
+
+openOutput ∷ [CmdOpt] → IO Handle
+openOutput opts
+    = case outputOpts of
+        []                 → return stdout
+        OptOutput fpath:[] → openFile fpath WriteMode
+        _                  → fail "two many --output options."
+    where
+      outputOpts ∷ [CmdOpt]
+      outputOpts = filter (\ x → case x of
+                                    OptOutput _ → True
+                                    _           → False) opts
 
 {-
   作られるファイルの例 (壓縮されない場合):
@@ -453,71 +458,85 @@ openOutput opts
      Compressed Length: 453 bytes  -- これは Compression: disabled の時には無い
            Compression: disabled
              MIME Type: image/png
-                  ETag: d41d8cd98f00b204e9800998ecf8427e
+                  ETag: "d41d8cd98f00b204e9800998ecf8427e"
          Last Modified: 2007-11-05 13:53:42.231882 JST
    -}
+  {-# LANGUAGE OverloadedStrings #-}
   module Foo.Bar.Baz (baz) where
-  import Codec.Binary.Base64
-  import qualified Data.ByteString.Lazy as L
+  import qualified Data.ByteString.Base64 as B64
+  import qualified Data.ByteString.Lazy as Lazy
   import Data.Time
   import Network.HTTP.Lucu
 
-  baz :: ResourceDef
+  baz  ResourceDef
   baz = ResourceDef {
           resUsesNativeThread = False
         , resIsGreedy         = False
         , resGet
-            = Just (do foundEntity entityTag lastModified
-                       setContentType contentType
-                       outputLBS rawData)
-        , resHead   = Nothing
+            = Just $ do foundEntity entityTag lastModified
+                        setContentType contentType
+                        putChunk rawData
+        , resHead
+            = Just $ do foundEntity entityTag lastModified
+                        setContentType contentType
         , resPost   = Nothing
         , resPut    = Nothing
         , resDelete = Nothing
         }
 
-  entityTag :: ETag
+  entityTag  ETag
   entityTag = strongETag "d41d8cd98f00b204e9800998ecf8427e"
 
-  lastModified :: UTCTime
+  lastModified ∷ UTCTime
+  {-# NOINLINE lastModified #-}
   lastModified = read "2007-11-05 04:47:56.008366 UTC"
 
-  contentType :: MIMEType
-  contentType = read "image/png"
+  contentType ∷ MIMEType
+  {-# NOINLINE contentType #-}
+  contentType = parseMIMEType "image/png"
 
-  rawData :: L.ByteString
-  rawData = L.pack (decode "IyEvdXNyL2Jpbi9lbnYgcnVuZ2hjCgppbXBvcnQgRGlzdHJ...")
+  rawData ∷ Lazy.ByteString
+  {-# NOINLINE rawData #-}
+  rawData = Lazy.fromChunks
+            [ B64.decodeLenient "IyEvdXNyL2Jpbi9lbnYgcnVuZ2hjCgppbXBvcnQgRG..."
+            , B64.decodeLenient "Otb/+DniOlRgAAAAYAAAAGAAAAB/6QOmToAEIGAAAA..."
+            ]
   ------------------------------------------------------------------------------
 
   壓縮される場合は次のやうに變はる:
   ------------------------------------------------------------------------------
   -- import に追加
-  import Control.Monad
-  import Codec.Compression.GZip
+  import Codec.Compression.Zlib
 
   -- ResourceDef は次のやうに變化
-  baz :: ResourceDef
+  baz  ResourceDef
   baz = ResourceDef {
           resUsesNativeThread = False
         , resIsGreedy         = False
         , resGet
-            = Just (do foundEntity entityTag lastModified
-                       setContentType contentType
-
-                       mustGunzip <- liftM not (isEncodingAcceptable "gzip")
-                       if mustGunzip then
-                           outputLBS (decompress gzippedData)
-                         else
-                           do setContentEncoding ["gzip"]
-                              outputLBS gzippedData
-        , resHead   = Nothing
+            = Just $ do foundEntity entityTag lastModified
+                        setContentType contentType
+
+                        gzipAllowed ← isEncodingAcceptable "gzip"
+                        if gzipAllowed then
+                            do setContentEncoding ["gzip"]
+                               putChunks gzippedData
+                        else
+                            putChunks (decompress gzippedData)
+        , resHead
+            = Just $ do foundEntity entityTag lastModified
+                        setContentType contentType
         , resPost   = Nothing
         , resPut    = Nothing
         , resDelete = Nothing
         }
-  
+
   -- rawData の代はりに gzippedData
-  gzippedData :: L.ByteString
-  gzippedData = L.pack (decode "Otb/+DniOlRgAAAAYAAAAGAAAAB/6QOmToAEIGAAAAB...")
+  gzippedData ∷ Lazy.ByteString
+  {-# NOINLINE gzippedData #-}
+  gzippedData = Lazy.fromChunks
+                [ B64.decodeLenient "IyEvdXNyL2Jpbi9lbnYgcnVuZ2hjCgppbXBvcnQ..."
+                , B64.decodeLenient "Otb/+DniOlRgAAAAYAAAAGAAAAB/6QOmToAEIGA..."
+                ]
   ------------------------------------------------------------------------------
  -}