]> gitweb @ CieloNegro.org - Lucu.git/blobdiff - Network/HTTP/Lucu/MultipartForm.hs
MIMEType and MultipartForm
[Lucu.git] / Network / HTTP / Lucu / MultipartForm.hs
index 21fca67b8519f2a13d29c5c11cfbda6a116c5c18..8d09d701fbe460a37059c6ed196e99b06d0f855d 100644 (file)
+{-# LANGUAGE
+    DoAndIfThenElse
+  , OverloadedStrings
+  , RecordWildCards
+  , ScopedTypeVariables
+  , UnicodeSyntax
+  #-}
 module Network.HTTP.Lucu.MultipartForm
-    ( multipartFormP
+    ( FormData(..)
+    , multipartFormP
     )
     where
-
-import           Data.ByteString.Base (LazyByteString(..))
-import qualified Data.ByteString.Char8 as C8
-import           Data.Char
-import           Data.List
-import           Network.HTTP.Lucu.Abortion
-import           Network.HTTP.Lucu.Headers
-import           Network.HTTP.Lucu.Parser
-import           Network.HTTP.Lucu.Parser.Http
-import           Network.HTTP.Lucu.Response
-import           Network.HTTP.Lucu.Utils
-
-
-data Part = Part Headers String
+import Control.Applicative hiding (many)
+import Data.Ascii (Ascii, CIAscii)
+import qualified Data.Ascii as A
+import Data.Attoparsec.Char8
+import qualified Data.ByteString.Char8 as BS
+import qualified Data.ByteString.Lazy.Char8 as LS
+import Data.Map (Map)
+import qualified Data.Map as M
+import Data.Maybe
+import Data.Monoid.Unicode
+import Data.Text (Text)
+import Network.HTTP.Lucu.Headers
+import Network.HTTP.Lucu.Parser.Http
+import Network.HTTP.Lucu.RFC2231
+import Prelude.Unicode
+
+-- |This data type represents a form value and possibly an uploaded
+-- file name.
+data FormData
+    = FormData {
+        fdFileName ∷ Maybe Text
+      , fdContent  ∷ LS.ByteString
+      }
+
+data Part
+    = Part {
+        ptHeaders   ∷ Headers
+      , ptContDispo ∷ ContDispo
+      , ptBody      ∷ LS.ByteString
+      }
 
 instance HasHeaders Part where
-    getHeaders (Part hs _)    = hs
-    setHeaders (Part _  b) hs = Part hs b
-
-
-data ContDispo = ContDispo String [(String, String)]
-
-instance Show ContDispo where
-    show (ContDispo dType dParams)
-        = dType ++
-          if null dParams then
-              ""
-          else
-              "; " ++ joinWith "; " (map showPair dParams)
-        where
-          showPair :: (String, String) -> String
-          showPair (name, value)
-              = name ++ "=" ++ if any (not . isToken) value then
-                                   quoteStr value
-                               else
-                                   value
-
-
-multipartFormP :: String -> Parser [(String, String)]
+    getHeaders = ptHeaders
+    setHeaders pt hs = pt { ptHeaders = hs }
+
+data ContDispo
+    = ContDispo {
+        dType   ∷ !CIAscii
+      , dParams ∷ !(Map CIAscii Text)
+      }
+
+printContDispo ∷ ContDispo → Ascii
+printContDispo d
+    = A.fromAsciiBuilder $
+      ( A.toAsciiBuilder (A.fromCIAscii $ dType d)
+        ⊕
+        printParams (dParams d) )
+
+multipartFormP ∷ Ascii → Parser [(Text, FormData)]
 multipartFormP boundary
-    = do parts <- many (partP boundary)
-         string "--"
-         string boundary
-         string "--"
+    = do parts ← many $ try $ partP boundary
+         _     ← string "--"
+         _     ← string $ A.toByteString boundary
+         _     ← string "--"
          crlf
-         eof
-         return $ map partToPair parts
+         catMaybes <$> mapM partToFormPair parts
 
-
-partP :: String -> Parser Part
+partP ∷ Ascii → Parser Part
 partP boundary
-    = do string "--"
-         string boundary
-         crlf -- バウンダリの末尾に -- が付いてゐたらここで fail する。
-         hs   <- headersP
-         body <- bodyP boundary
-         return $ Part hs body
-
+    = do _    ← string "--"
+         _    ← string $ A.toByteString boundary
+         crlf
+         hs    headersP
+         d    ← getContDispo hs
+         body ← bodyP boundary
+         return $ Part hs d body
 
-bodyP :: String -> Parser String
+bodyP ∷ Ascii → Parser LS.ByteString
 bodyP boundary
-    = do body <- many $
-                 do notFollowedBy $ do crlf
-                                       string "--"
-                                       string boundary
-                    anyChar
+    = do body ← manyCharsTill anyChar $
+                    try $
+                    do crlf
+                       _ ← string "--"
+                       _ ← string $ A.toByteString boundary
+                       return ()
          crlf
          return body
 
-
-partToPair :: Part -> (String, String)
-partToPair part@(Part _ body)
-    = case getHeader (C8.pack "Content-Disposition") part of
-        Nothing  
-            -> abortPurely BadRequest []
-               (Just "There is a part without Content-Disposition in the multipart/form-data.")
-        Just dispo
-            -> case parse contDispoP (LPS [dispo]) of
-                 (# Success dispo, _ #)
-                     -> (getName dispo, body)
-                 (# _, _ #)
-                     -> abortPurely BadRequest []
-                        (Just $ "Unparsable Content-Disposition: " ++ C8.unpack dispo)
-      where
-        getName :: ContDispo -> String
-        getName dispo@(ContDispo dType dParams)
-            | map toLower dType == "form-data"
-                = case find ((== "name") . map toLower . fst) dParams of
-                    Just (_, name) -> name
-                    Nothing   
-                        -> abortPurely BadRequest []
-                           (Just $ "form-data without name: " ++ show dispo)
-            | otherwise
-                = abortPurely BadRequest []
-                  (Just $ "Content-Disposition type is not form-data: " ++ dType)
-
-
-contDispoP :: Parser ContDispo
-contDispoP = do dispoType <- token
-                params    <- allowEOF $ many paramP
+partToFormPair ∷ Monad m ⇒ Part → m (Maybe (Text, FormData))
+{-# INLINEABLE partToFormPair #-}
+partToFormPair pt
+    | dType (ptContDispo pt) ≡ "form-data"
+        = do name  ← partName pt
+             let fname = partFileName pt
+             let fd    = FormData {
+                           fdFileName = fname
+                         , fdContent  = ptBody pt
+                         }
+             return $ Just (name, fd)
+    | otherwise
+        = return Nothing
+
+partName ∷ Monad m ⇒ Part → m Text
+{-# INLINEABLE partName #-}
+partName (Part {..})
+    = case M.lookup "name" $ dParams ptContDispo of
+        Just name
+            → return name
+        Nothing
+            → fail ("form-data without name: " ⧺
+                    A.toString (printContDispo ptContDispo))
+
+partFileName ∷ Part → Maybe Text
+{-# INLINEABLE partFileName #-}
+partFileName (Part {..})
+    = M.lookup "filename" $ dParams ptContDispo
+
+getContDispo ∷ Monad m ⇒ Headers → m ContDispo
+{-# INLINEABLE getContDispo #-}
+getContDispo hdr
+    = case getHeader "Content-Disposition" hdr of
+        Nothing
+            → fail ("There is a part without Content-Disposition in the multipart/form-data.")
+        Just str
+            → let p  = do d ← contDispoP
+                          endOfInput
+                          return d
+                  bs = A.toByteString str
+              in
+                case parseOnly p bs of
+                  Right  d → return d
+                  Left err → fail (concat [ "Unparsable Content-Disposition: "
+                                          , BS.unpack bs
+                                          , ": "
+                                          , err
+                                          ])
+
+contDispoP ∷ Parser ContDispo
+contDispoP = do dispoType ← A.toCIAscii <$> token
+                params    ← paramsP
                 return $ ContDispo dispoType params
-    where
-      paramP :: Parser (String, String)
-      paramP = do many lws
-                  char ';'
-                  many lws
-                  name <- token
-                  char '='
-                  value <- token <|> quotedStr
-                  return (name, value)