on master help sign in
Beginning of parseRequestBody code
commite2e63ff8533969912d0880c44c9501baefdbafac authorMichael Snoyman <michael@snoyman.com> dateThu 20 May 2010, 01:44 (16 years ago) parent0a0f047
2 files changed+270−5
Network/Wai/Parse.hs+160−5 runtests.hs+110
Network/Wai/Parse.hs+160−5
@@ -4,15 +4,24 @@ module Network.Wai.Parse
44 ( parseQueryString
55 , parseCookies
66 , parseHttpAccept
7+ , parseRequestBody
8+ , Sink (..)
9+ , lbsSink
10+ , tempFileSink
11+ , FileInfo (..)
712 ) where
813
914import qualified Data.ByteString as S
15+import qualified Data.ByteString.Lazy as L
1016import qualified Data.ByteString.Char8 as S8
1117import Data.Word (Word8)
1218import Data.Bits
1319import Data.Maybe (fromMaybe)
1420import Data.List (sortBy)
1521import Data.Function (on)
22+import System.Directory (removeFile, getTemporaryDirectory)
23+import System.IO (hClose, openBinaryTempFile, Handle)
24+import Network.Wai
1625
1726uncons :: S.ByteString -> Maybe (Word8, S.ByteString)
1827uncons s
@@ -84,7 +93,7 @@ parseHttpAccept :: S.ByteString -> [S.ByteString]
8493parseHttpAccept = map fst
8594 . sortBy (rcompare `on` snd)
8695 . map grabQ
87− . split
96+ . S.split 44 -- comma
8897 where
8998 rcompare x y = compare y x
9099 grabQ s =
@@ -95,7 +104,153 @@ parseHttpAccept = map fst
95104 (x, _):_ -> x
96105 _ -> 1.0
97106 trimWhite = S.dropWhile (== 32) -- space
98− split s
99− | S.null s = []
100− | otherwise = let (x, y) = breakDiscard 44 s -- comma
101− in x : split y
107+
108+-- | A destination for data, the opposite of a 'Source'.
109+data Sink x y = Sink
110+ { sinkInit :: IO x
111+ , sinkAppend :: x -> S.ByteString -> IO x
112+ , sinkClose :: x -> IO y
113+ , sinkFinalize :: y -> IO ()
114+ }
115+
116+lbsSink :: Sink ([S.ByteString] -> [S.ByteString]) L.ByteString
117+lbsSink = Sink
118+ { sinkInit = return id
119+ , sinkAppend = \front bs -> return $ front . (:) bs
120+ , sinkClose = \front -> return $ L.fromChunks $ front []
121+ , sinkFinalize = \_ -> return ()
122+ }
123+
124+tempFileSink :: Sink (FilePath, Handle) FilePath
125+tempFileSink = Sink
126+ { sinkInit = do
127+ tempDir <- getTemporaryDirectory
128+ openBinaryTempFile tempDir "webenc.buf"
129+ , sinkAppend = \(fp, h) bs -> S.hPut h bs >> return (fp, h)
130+ , sinkClose = \(fp, h) -> do
131+ hClose h
132+ return fp
133+ , sinkFinalize = \fp -> removeFile fp
134+ }
135+
136+-- | Information on an uploaded file.
137+data FileInfo c = FileInfo
138+ { fileName :: S.ByteString
139+ , fileContentType :: S.ByteString
140+ , fileContent :: c
141+ }
142+ deriving (Eq, Show)
143+
144+type Param = (S.ByteString, S.ByteString)
145+type File y = (S.ByteString, FileInfo y)
146+
147+parseRequestBody :: Sink x y
148+ -> Request
149+ -> IO ([Param], [File y])
150+parseRequestBody sink req = do
151+ let ctype = do
152+ ctype' <- lookup ReqContentType $ requestHeaders req
153+ if urlenc `S.isPrefixOf` ctype'
154+ then Just Nothing
155+ else if formBound `S.isPrefixOf` ctype'
156+ then Just $ Just $ S.drop (S.length formBound) ctype'
157+ else Nothing
158+ case ctype of
159+ Nothing -> return ([], [])
160+ Just Nothing -> do -- url-encoded
161+ -- NOTE: in general, url-encoded data will be in a single chunk.
162+ -- Therefore, I'm optimizing for the usual case by sticking with
163+ -- strict byte strings here.
164+ bs <- sourceToBs $ requestBody req
165+ return (parseQueryString bs, [])
166+ Just (Just bound) -> -- multi-part
167+ parsePieces sink bound (S.empty, Just $ requestBody req) True
168+ where
169+ urlenc = S8.pack "application/x-www-form-urlencoded"
170+ formBound = S8.pack "multipart/form-data; boundary="
171+
172+sourceToBs :: Source -> IO S.ByteString
173+sourceToBs = fmap S.concat . go id
174+ where
175+ go front (Source src) = do
176+ res <- src
177+ case res of
178+ Nothing -> return $ front []
179+ Just (bs, src') -> go (front . (:) bs) src'
180+
181+type Source' = (S.ByteString, Maybe Source)
182+
183+takeLine :: Source' -> IO (Maybe (S.ByteString, Source'))
184+takeLine (s, msrc)
185+ | S.null s = case msrc of
186+ Nothing -> return Nothing
187+ Just (Source src) -> do
188+ res <- src
189+ case res of
190+ Nothing -> return Nothing
191+ Just (x, y) -> takeLine (x, Just y)
192+takeLine (s, msrc) =
193+ let (x, y) = S.break (== 10) s -- newline
194+ in if S.null y
195+ then do
196+ case msrc of
197+ Nothing -> return $ Just (x, (y, msrc))
198+ Just (Source src) -> do
199+ res <- src
200+ case res of
201+ Nothing -> return $ Just (x, (y, Nothing))
202+ Just (s', src') ->
203+ takeLine (s `S.append` s', Just src')
204+ else return $ Just (killCarriage x, (S.drop 1 y, msrc))
205+ where
206+ killCarriage bs
207+ | S.null bs = bs
208+ | S.last bs == 13 = S.init bs -- carriage return
209+ | otherwise = bs
210+
211+takeLines :: Source' -> IO (Maybe ([S.ByteString], Source'))
212+takeLines src = do
213+ res <- takeLine src
214+ case res of
215+ Nothing -> return Nothing
216+ Just (l, src') ->
217+ if S.null l
218+ then return $ Just ([], src')
219+ else do
220+ res' <- takeLines src'
221+ case res' of
222+ Nothing -> return $ Just ([l], src')
223+ Just (ls, src'') -> return $ Just (l : ls, src'')
224+
225+parsePieces :: Sink x y -> S.ByteString -> Source' -> Bool
226+ -> IO ([Param], [File y])
227+parsePieces sink bound src isFirst = do
228+ print ("bound", bound)
229+ src' <- if isFirst
230+ then do
231+ res <- takeLine src -- first line should be bound
232+ case res of
233+ Nothing -> return (S.empty, Nothing)
234+ Just (_, x) -> return x
235+ else return src
236+ res <- takeLines src'
237+ case res of
238+ Nothing -> return ([], [])
239+ Just (ls, src'') -> do
240+ let ls' = map parsePair ls
241+ case (lookup contDisp ls', lookup contType ls') of
242+ (Just cd, Just ct) -> do
243+ return ([], []) -- FIXME
244+ _ -> do
245+ (isEnd, src''') <- dropTillBound bound src''
246+ if isEnd
247+ then return ([], [])
248+ else parsePieces sink bound src''' False
249+ where
250+ contDisp = S8.pack "Content-Disposition"
251+ contType = S8.pack "Content-Type"
252+ parsePair s =
253+ let (x, y) = breakDiscard 58 s -- colon
254+ in (x, S.dropWhile (== 32) y) -- space
255+
256+dropTillBound bound (bs, msrc) = error "FIXME dropTillBound"
runtests.hs+110
@@ -2,8 +2,11 @@ import Test.Framework (defaultMain, testGroup, Test)
22import Test.Framework.Providers.HUnit
33import Test.HUnit hiding (Test)
44
5+import Network.Wai
56import Network.Wai.Parse
7+import Network.Wai.Source
68import qualified Data.ByteString.Char8 as B8
9+import qualified Data.ByteString.Lazy.Char8 as L8
710import Control.Arrow
811
912main :: IO ()
@@ -14,6 +17,7 @@ testSuite = testGroup "Network.Wai.Parse"
1417 [ testCase "parseQueryString" caseParseQueryString
1518 , testCase "parseCookies" caseParseCookies
1619 , testCase "parseHttpAccept" caseParseHttpAccept
20+ , testCase "parseRequestBody" caseParseRequestBody
1721 ]
1822
1923caseParseQueryString :: Assertion
@@ -44,3 +48,109 @@ caseParseHttpAccept = do
4448 B8.pack "text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"
4549 expected = ["text/html", "text/x-c", "text/x-dvi", "text/plain"]
4650 map B8.pack expected @=? parseHttpAccept input
51+
52+caseParseRequestBody :: Assertion
53+caseParseRequestBody = t where
54+ content2 = B8.pack $
55+ "--AaB03x\n" ++
56+ "Content-Disposition: form-data; name=\"document\"; filename=\"b.txt\"\n" ++
57+ "Content-Type: text/plain; charset=iso-8859-1\n\n" ++
58+ "This is a file.\n" ++
59+ "It has two lines.\n" ++
60+ "--AaB03x\n" ++
61+ "Content-Disposition: form-data; name=\"title\"\n" ++
62+ "Content-Type: text/plain; charset=iso-8859-1\n\n" ++
63+ "A File\n" ++
64+ "--AaB03x\n" ++
65+ "Content-Disposition: form-data; name=\"summary\"\n" ++
66+ "Content-Type: text/plain; charset=iso-8859-1\n\n" ++
67+ "This is my file\n" ++
68+ "file test\n" ++
69+ "--AaB03x--"
70+ content3 = B8.pack "------WebKitFormBoundaryB1pWXPZ6lNr8RiLh\r\nContent-Disposition: form-data; name=\"yaml\"; filename=\"README\"\r\nContent-Type: application/octet-stream\r\n\r\nPhoto blog using Hack.\n\r\n------WebKitFormBoundaryB1pWXPZ6lNr8RiLh--\r\n"
71+ t = do
72+ let content1 = B8.pack "foo=bar&baz=bin"
73+ let ctype1 = B8.pack "application/x-www-form-urlencoded"
74+ result1 <- parseRequestBody lbsSink $ toRequest ctype1 content1
75+ assertEqual "parsing post x-www-form-urlencoded"
76+ (map (B8.pack *** B8.pack) [("foo", "bar"), ("baz", "bin")], [])
77+ result1
78+ putStrLn "passed 1" -- FIXME remove
79+
80+ let ctype2 = B8.pack "multipart/form-data; boundary=AaB03x"
81+ result2 <- parseRequestBody lbsSink $ toRequest ctype2 content2
82+ let expectedsmap2 =
83+ [ ("title", "A File")
84+ , ("summary", "This is my file\nfile test")
85+ ]
86+ let expectedfile2 =
87+ [(B8.pack "document", FileInfo (B8.pack "b.txt") (B8.pack "text/plain") $ L8.pack
88+ "This is a file.\nIt has two lines.")]
89+ let expected2 = (map (B8.pack *** B8.pack) expectedsmap2, expectedfile2)
90+ assertEqual "parsing post multipart/form-data"
91+ expected2
92+ result2
93+ putStrLn "passed 2" -- FIXME remove
94+
95+ let ctype3 = B8.pack "multipart/form-data; boundary=----WebKitFormBoundaryB1pWXPZ6lNr8RiLh"
96+ result3 <- parseRequestBody lbsSink $ toRequest ctype3 content3
97+ let expectedsmap3 = []
98+ let expectedfile3 = [(B8.pack "yaml", FileInfo (B8.pack "README") (B8.pack "application/octet-stream") $
99+ L8.pack "Photo blog using Hack.\n")]
100+ let expected3 = (expectedsmap3, expectedfile3)
101+ assertEqual "parsing actual post multipart/form-data"
102+ expected3
103+ result3
104+ putStrLn "passed 3" -- FIXME remove
105+
106+ result2' <- parseRequestBody lbsSink $ toRequest' ctype2 content2
107+ assertEqual "parsing post multipart/form-data 2"
108+ expected2
109+ result2'
110+ putStrLn "passed 4" -- FIXME remove
111+
112+ putStrLn "\n\n\n\n\n"
113+ result3' <- parseRequestBody lbsSink $ toRequest' ctype3 content3
114+ assertEqual "parsing actual post multipart/form-data 2"
115+ expected3
116+ result3'
117+ putStrLn "passed 5" -- FIXME remove
118+
119+toRequest :: B8.ByteString -> B8.ByteString -> Request
120+toRequest ctype content = Request
121+ { requestHeaders = [(ReqContentType, ctype)]
122+ , requestBody = toSource content
123+ , requestMethod = undefined
124+ , httpVersion = undefined
125+ , pathInfo = undefined
126+ , queryString = undefined
127+ , serverName = undefined
128+ , serverPort = undefined
129+ , urlScheme = undefined
130+ , errorHandler = undefined
131+ , remoteHost = undefined
132+ }
133+
134+toRequest' :: B8.ByteString -> B8.ByteString -> Request
135+toRequest' ctype content = Request
136+ { requestHeaders = [(ReqContentType, ctype)]
137+ , requestBody = toSource' content
138+ , requestMethod = undefined
139+ , httpVersion = undefined
140+ , pathInfo = undefined
141+ , queryString = undefined
142+ , serverName = undefined
143+ , serverPort = undefined
144+ , urlScheme = undefined
145+ , errorHandler = undefined
146+ , remoteHost = undefined
147+ }
148+
149+toSource :: B8.ByteString -> Source
150+toSource bs = Source $
151+ case B8.uncons bs of
152+ Nothing -> return Nothing
153+ Just (x, xs) -> return $ Just (B8.singleton x, toSource xs)
154+
155+toSource' :: B8.ByteString -> Source
156+toSource' bs = Source $ return $ Just (bs, Source $ return Nothing)