packages feed

wai-extra-3.1.15: Network/Wai/Parse.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE PatternGuards #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeFamilies #-}

-- | Some helpers for parsing data out of a raw WAI 'Request'.
module Network.Wai.Parse (
    parseHttpAccept,
    parseRequestBody,
    RequestBodyType (..),
    getRequestBodyType,
    sinkRequestBody,
    sinkRequestBodyEx,
    RequestParseException (..),
    BackEnd,
    lbsBackEnd,
    tempFileBackEnd,
    tempFileBackEndOpts,
    Param,
    File,
    FileInfo (..),
    parseContentType,
    ParseRequestBodyOptions,
    defaultParseRequestBodyOptions,
    noLimitParseRequestBodyOptions,
    parseRequestBodyEx,
    setMaxRequestKeyLength,
    clearMaxRequestKeyLength,
    setMaxRequestNumFiles,
    clearMaxRequestNumFiles,
    setMaxRequestFileSize,
    clearMaxRequestFileSize,
    setMaxRequestFilesSize,
    clearMaxRequestFilesSize,
    setMaxRequestParmsSize,
    clearMaxRequestParmsSize,
    setMaxHeaderLines,
    clearMaxHeaderLines,
    setMaxHeaderLineLength,
    clearMaxHeaderLineLength,
#if TEST
    Bound (..),
    findBound,
    sinkTillBound,
    killCR,
    killCRLF,
    takeLine,
#endif
) where

import Prelude hiding (lines)

#if __GLASGOW_HASKELL__ < 710
import Control.Applicative ((<$>))
#endif
import Control.Exception (catchJust)
import qualified Control.Exception as E
import Control.Monad (guard, unless, when)
import Control.Monad.Trans.Resource (
    InternalState,
    allocate,
    register,
    release,
    runInternalState,
 )
import Data.Bifunctor (bimap)
import qualified Data.ByteString as S
import qualified Data.ByteString.Char8 as S8
import qualified Data.ByteString.Lazy as L
import Data.CaseInsensitive (mk)
import Data.Function (fix, on)
import Data.IORef
import Data.Int (Int64)
import Data.List (sortBy)
import Data.Maybe (catMaybes, fromMaybe)
import Data.Typeable
import Data.Word8
import Network.HTTP.Types (hContentType)
import qualified Network.HTTP.Types as H
import Network.Wai
import Network.Wai.Handler.Warp (InvalidRequest (..))
import System.Directory (getTemporaryDirectory, removeFile)
import System.IO (hClose, openBinaryTempFile)
import System.IO.Error (isDoesNotExistError)

breakDiscard :: Word8 -> S.ByteString -> (S.ByteString, S.ByteString)
breakDiscard w s =
    let (x, y) = S.break (== w) s
     in (x, S.drop 1 y)

-- | Parse the HTTP accept string to determine supported content types.
parseHttpAccept :: S.ByteString -> [S.ByteString]
parseHttpAccept =
    map fst
        . sortBy (rcompare `on` snd)
        . map (addSpecificity . grabQ)
        . S.split _comma
  where
    rcompare :: (Double, Int) -> (Double, Int) -> Ordering
    rcompare = flip compare
    addSpecificity (s, q) =
        -- Prefer higher-specificity types
        let semicolons = S.count _semicolon s
            stars = S.count _asterisk s
         in (s, (q, semicolons - stars))
    grabQ s =
        -- Stripping all spaces may be too harsh.
        -- Maybe just strip either side of semicolon?
        let (s', q) = S.breakSubstring ";q=" (S.filter (/= _space) s)
            q' = S.takeWhile (/= _semicolon) (S.drop 3 q)
         in (s', readQ q')
    readQ s = case reads $ S8.unpack s of
        (x, _) : _ -> x
        _ -> 1.0

-- | Store uploaded files in memory
lbsBackEnd
    :: Monad m => ignored1 -> ignored2 -> m S.ByteString -> m L.ByteString
lbsBackEnd _ _ popper =
    loop id
  where
    loop front = do
        bs <- popper
        if S.null bs
            then return $ L.fromChunks $ front []
            else loop $ front . (bs :)

-- | Save uploaded files on disk as temporary files
--
-- Note: starting with version 2.0, removal of temp files is registered with
-- the provided @InternalState@. It is the responsibility of the caller to
-- ensure that this @InternalState@ gets cleaned up.
tempFileBackEnd
    :: InternalState -> ignored1 -> ignored2 -> IO S.ByteString -> IO FilePath
tempFileBackEnd = tempFileBackEndOpts getTemporaryDirectory "webenc.buf"

-- | Same as 'tempFileBackEnd', but use configurable temp folders and patterns.
tempFileBackEndOpts
    :: IO FilePath
    -- ^ get temporary directory
    -> String
    -- ^ filename pattern
    -> InternalState
    -> ignored1
    -> ignored2
    -> IO S.ByteString
    -> IO FilePath
tempFileBackEndOpts getTmpDir pattrn internalState _ _ popper = do
    (key, (fp, h)) <-
        flip runInternalState internalState $ allocate it (hClose . snd)
    _ <- runInternalState (register $ removeFileQuiet fp) internalState
    fix $ \loop -> do
        bs <- popper
        unless (S.null bs) $ do
            S.hPut h bs
            loop
    release key
    return fp
  where
    it = do
        tempDir <- getTmpDir
        openBinaryTempFile tempDir pattrn
    removeFileQuiet fp =
        catchJust
            (guard . isDoesNotExistError)
            (removeFile fp)
            (const $ return ())

-- | A data structure that describes the behavior of
-- the parseRequestBodyEx function.
--
-- @since 3.0.16.0
data ParseRequestBodyOptions = ParseRequestBodyOptions
    { prboKeyLength :: Maybe Int
    -- ^ The maximum length of a filename
    , prboMaxNumFiles :: Maybe Int
    -- ^ The maximum number of files.
    , prboMaxFileSize :: Maybe Int64
    -- ^ The maximum filesize per file.
    , prboMaxFilesSize :: Maybe Int64
    -- ^ The maximum total filesize.
    , prboMaxParmsSize :: Maybe Int
    -- ^ The maximum size of the sum of all parameters
    , prboMaxHeaderLines :: Maybe Int
    -- ^ The maximum header lines per mime/multipart entry
    , prboMaxHeaderLineLength :: Maybe Int
    -- ^ The maximum header line length per mime/multipart entry
    }

-- | Set the maximum length of a filename.
--
-- @since 3.0.16.0
setMaxRequestKeyLength
    :: Int -> ParseRequestBodyOptions -> ParseRequestBodyOptions
setMaxRequestKeyLength l p = p{prboKeyLength = Just l}

-- | Do not limit the length of filenames.
--
-- @since 3.0.16.0
clearMaxRequestKeyLength :: ParseRequestBodyOptions -> ParseRequestBodyOptions
clearMaxRequestKeyLength p = p{prboKeyLength = Nothing}

-- | Set the maximum number of files per request.
--
-- @since 3.0.16.0
setMaxRequestNumFiles
    :: Int -> ParseRequestBodyOptions -> ParseRequestBodyOptions
setMaxRequestNumFiles l p = p{prboMaxNumFiles = Just l}

-- | Do not limit the maximum number of files per request.
--
-- @since 3.0.16.0
clearMaxRequestNumFiles :: ParseRequestBodyOptions -> ParseRequestBodyOptions
clearMaxRequestNumFiles p = p{prboMaxNumFiles = Nothing}

-- | Set the maximum filesize per file (in bytes).
--
-- @since 3.0.16.0
setMaxRequestFileSize
    :: Int64 -> ParseRequestBodyOptions -> ParseRequestBodyOptions
setMaxRequestFileSize l p = p{prboMaxFileSize = Just l}

-- | Do not limit the maximum filesize per file.
--
-- @since 3.0.16.0
clearMaxRequestFileSize :: ParseRequestBodyOptions -> ParseRequestBodyOptions
clearMaxRequestFileSize p = p{prboMaxFileSize = Nothing}

-- | Set the maximum size of all files per request.
--
-- @since 3.0.16.0
setMaxRequestFilesSize
    :: Int64 -> ParseRequestBodyOptions -> ParseRequestBodyOptions
setMaxRequestFilesSize l p = p{prboMaxFilesSize = Just l}

-- | Do not limit the maximum size of all files per request.
--
-- @since 3.0.16.0
clearMaxRequestFilesSize :: ParseRequestBodyOptions -> ParseRequestBodyOptions
clearMaxRequestFilesSize p = p{prboMaxFilesSize = Nothing}

-- | Set the maximum size of the sum of all parameters.
--
-- @since 3.0.16.0
setMaxRequestParmsSize
    :: Int -> ParseRequestBodyOptions -> ParseRequestBodyOptions
setMaxRequestParmsSize l p = p{prboMaxParmsSize = Just l}

-- | Do not limit the maximum size of the sum of all parameters.
--
-- @since 3.0.16.0
clearMaxRequestParmsSize :: ParseRequestBodyOptions -> ParseRequestBodyOptions
clearMaxRequestParmsSize p = p{prboMaxParmsSize = Nothing}

-- | Set the maximum header lines per mime/multipart entry.
--
-- @since 3.0.16.0
setMaxHeaderLines :: Int -> ParseRequestBodyOptions -> ParseRequestBodyOptions
setMaxHeaderLines l p = p{prboMaxHeaderLines = Just l}

-- | Do not limit the maximum header lines per mime/multipart entry.
--
-- @since 3.0.16.0
clearMaxHeaderLines :: ParseRequestBodyOptions -> ParseRequestBodyOptions
clearMaxHeaderLines p = p{prboMaxHeaderLines = Nothing}

-- | Set the maximum header line length per mime/multipart entry.
--
-- @since 3.0.16.0
setMaxHeaderLineLength
    :: Int -> ParseRequestBodyOptions -> ParseRequestBodyOptions
setMaxHeaderLineLength l p = p{prboMaxHeaderLineLength = Just l}

-- | Do not limit the maximum header lines per mime/multipart entry.
--
-- @since 3.0.16.0
clearMaxHeaderLineLength :: ParseRequestBodyOptions -> ParseRequestBodyOptions
clearMaxHeaderLineLength p = p{prboMaxHeaderLineLength = Nothing}

-- | A reasonable default set of parsing options.
-- Maximum key/filename length: 32 bytes;
-- maximum files: 10; filesize unlimited; maximum
-- size for parameters: 64kbytes; maximum number of header
-- lines: 32 bytes (applies only to headers of a mime/multipart message);
-- maximum header line length: Apache's default for that is 8190 bytes
-- (http://httpd.apache.org/docs/2.2/mod/core.html#limitrequestline)
-- so we're using that here as well.
--
-- @since 3.0.16.0
defaultParseRequestBodyOptions :: ParseRequestBodyOptions
defaultParseRequestBodyOptions =
    ParseRequestBodyOptions
        { prboKeyLength = Just 32
        , prboMaxNumFiles = Just 10
        , prboMaxFileSize = Nothing
        , prboMaxFilesSize = Nothing
        , prboMaxParmsSize = Just 65336
        , prboMaxHeaderLines = Just 32
        , prboMaxHeaderLineLength = Just 8190
        }

-- | Do not impose any memory limits.
--
-- @since 3.0.21.0
noLimitParseRequestBodyOptions :: ParseRequestBodyOptions
noLimitParseRequestBodyOptions =
    ParseRequestBodyOptions
        { prboKeyLength = Nothing
        , prboMaxNumFiles = Nothing
        , prboMaxFileSize = Nothing
        , prboMaxFilesSize = Nothing
        , prboMaxParmsSize = Nothing
        , prboMaxHeaderLines = Nothing
        , prboMaxHeaderLineLength = Nothing
        }

-- | Information on an uploaded file.
data FileInfo c = FileInfo
    { fileName :: S.ByteString
    , fileContentType :: S.ByteString
    , fileContent :: c
    }
    deriving (Eq, Show)

-- | Post parameter name and value.
type Param = (S.ByteString, S.ByteString)

-- | Post parameter name and associated file information.
type File y = (S.ByteString, FileInfo y)

-- | A file uploading backend. Takes the parameter name, file name, and a
-- stream of data.
type BackEnd a =
    S.ByteString
    -- ^ parameter name
    -> FileInfo ()
    -> IO S.ByteString
    -> IO a

-- | The mimetype of the http body.
-- Depending on whether just parameters or parameters and files
-- are passed, one or the other mimetype should be used.
data RequestBodyType
    = -- | application/x-www-form-urlencoded (parameters only)
      UrlEncoded
    | -- | multipart/form-data (parameters and files)
      Multipart S.ByteString

-- | Get the mimetype of the body of an http request.
getRequestBodyType :: Request -> Maybe RequestBodyType
getRequestBodyType req = do
    ctype' <- lookup hContentType $ requestHeaders req
    let (ctype, attrs) = parseContentType ctype'
    case ctype of
        "application/x-www-form-urlencoded" -> return UrlEncoded
        "multipart/form-data" | Just bound <- lookup "boundary" attrs -> return $ Multipart bound
        _ -> Nothing

-- | Parse a content type value, turning a single @ByteString@ into the actual
-- content type and a list of pairs of attributes.
--
-- @since 1.3.2
parseContentType
    :: S.ByteString -> (S.ByteString, [(S.ByteString, S.ByteString)])
parseContentType a = do
    let (ctype, b) = S.break (== _semicolon) a
        attrs = goAttrs id $ S.drop 1 b
     in (ctype, attrs)
  where
    dq s =
        if S.length s > 2 && S.head s == _quotedbl && S.last s == _quotedbl
            then S.tail $ S.init s
            else s
    goAttrs front bs
        | S.null bs = front []
        | otherwise =
            let (x, rest) = S.break (== _semicolon) bs
             in goAttrs (front . (goAttr x :)) $ S.drop 1 rest
    goAttr bs =
        let (k, v') = S.break (== _equal) bs
            v = S.drop 1 v'
         in (strip k, dq $ strip v)
    strip = S.dropWhile (== _space) . fst . S.breakEnd (/= _space)

-- | Parse the body of an HTTP request.
-- See parseRequestBodyEx for details.
-- Note: This function does not limit the memory it allocates.
-- When dealing with untrusted data (as is usually the case when
-- receiving input from the internet), it is recommended to
-- use the 'parseRequestBodyEx' function instead.
--
-- since 3.1.15 : throws 'RequestParseException' if something goes wrong
parseRequestBody
    :: BackEnd y
    -> Request
    -> IO ([Param], [File y])
parseRequestBody = parseRequestBodyEx noLimitParseRequestBodyOptions

-- | Parse the body of an HTTP request, limit resource usage.
-- The HTTP body can contain both parameters and files.
-- This function will return a list of key,value pairs
-- for all parameters, and a list of key,a pairs
-- for filenames. The a depends on the used backend that
-- is responsible for storing the received files.
--
-- since 3.1.15 : throws 'RequestParseException' if something goes wrong
parseRequestBodyEx
    :: ParseRequestBodyOptions
    -> BackEnd y
    -> Request
    -> IO ([Param], [File y])
parseRequestBodyEx o s r =
    case getRequestBodyType r of
        Nothing -> return ([], [])
        Just rbt -> sinkRequestBodyEx o s rbt (getRequestBodyChunk r)

-- | since 3.1.15 : throws 'RequestParseException' if something goes wrong
sinkRequestBody
    :: BackEnd y
    -> RequestBodyType
    -> IO S.ByteString
    -> IO ([Param], [File y])
sinkRequestBody = sinkRequestBodyEx noLimitParseRequestBodyOptions

-- | Throws 'RequestParseException' if something goes wrong
--
-- @since 3.0.16.0
--
-- since 3.1.15 : throws 'RequestParseException' if something goes wrong
sinkRequestBodyEx
    :: ParseRequestBodyOptions
    -> BackEnd y
    -> RequestBodyType
    -> IO S.ByteString
    -> IO ([Param], [File y])
sinkRequestBodyEx o s r body = do
    ref <- newIORef ([], [])
    let add x = atomicModifyIORef ref $ \(y, z) ->
            case x of
                Left y' -> ((y' : y, z), ())
                Right z' -> ((y, z' : z), ())
    conduitRequestBodyEx o s r body add
    bimap reverse reverse <$> readIORef ref

conduitRequestBodyEx
    :: ParseRequestBodyOptions
    -> BackEnd y
    -> RequestBodyType
    -> IO S.ByteString
    -> (Either Param (File y) -> IO ())
    -> IO ()
conduitRequestBodyEx o _ UrlEncoded rbody add = do
    -- NOTE: in general, url-encoded data will be in a single chunk.
    -- Therefore, I'm optimizing for the usual case by sticking with
    -- strict byte strings here.
    let loop size front = do
            bs <- rbody
            if S.null bs
                then return $ S.concat $ front []
                else do
                    let newsize = size + S.length bs
                    case prboMaxParmsSize o of
                        Just maxSize ->
                            when (newsize > maxSize) $
                                E.throwIO $
                                    MaxParamSizeExceeded newsize
                        Nothing -> return ()
                    loop newsize $ front . (bs :)
    bs <- loop 0 id
    mapM_ (add . Left) $ H.parseSimpleQuery bs
conduitRequestBodyEx o backend (Multipart bound) rbody add =
    parsePiecesEx o backend (S8.pack "--" `S.append` bound) rbody add

-- | Take one header or subheader line.
-- Since:  3.0.26
--  Throw 431 if headers too large.
takeLine :: Maybe Int -> Source -> IO (Maybe S.ByteString)
takeLine maxlen src =
    go ""
  where
    go front = do
        bs <- readSource src
        case maxlen of
            Just maxlen' ->
                when (S.length front > maxlen') $
                    E.throwIO RequestHeaderFieldsTooLarge
            Nothing -> return ()
        if S.null bs
            then close front
            else push front bs

    close front = leftover src front >> return Nothing
    push front bs = do
        let (x, y) = S.break (== _lf) bs
         in if S.null y
                then go $ front `S.append` x
                else do
                    when (S.length y > 1) $ leftover src $ S.drop 1 y
                    let res = front `S.append` x
                    case maxlen of
                        Just maxlen' ->
                            when (S.length res > maxlen') $
                                E.throwIO RequestHeaderFieldsTooLarge
                        Nothing -> return ()
                    return . Just $ killCR res

-- | @since 3.1.15 : throws 'RequestParseException' if something goes wrong
takeLines' :: Maybe Int -> Maybe Int -> Source -> IO [S.ByteString]
takeLines' lineLength maxLines source =
    reverse <$> takeLines'' [] lineLength maxLines source

-- | @since 3.1.15 : throws 'RequestParseException' if something goes wrong
takeLines''
    :: [S.ByteString]
    -> Maybe Int
    -> Maybe Int
    -> Source
    -> IO [S.ByteString]
takeLines'' lines lineLength maxLines src = do
    case maxLines of
        Just maxLines' ->
            when (length lines > maxLines') $
                E.throwIO $
                    TooManyHeaderLines (length lines)
        Nothing -> return ()
    res <- takeLine lineLength src
    case res of
        Nothing -> return lines
        Just l
            | S.null l -> return lines
            | otherwise -> takeLines'' (l : lines) lineLength maxLines src

data Source = Source (IO S.ByteString) (IORef S.ByteString)

mkSource :: IO S.ByteString -> IO Source
mkSource f = do
    ref <- newIORef S.empty
    return $ Source f ref

readSource :: Source -> IO S.ByteString
readSource (Source f ref) = do
    bs <- atomicModifyIORef ref $ \bs -> (S.empty, bs)
    if S.null bs
        then f
        else return bs

{- HLint ignore readSource "Use tuple-section" -}

leftover :: Source -> S.ByteString -> IO ()
leftover (Source _ ref) = writeIORef ref

-- | @since 3.1.15 : throws 'RequestParseException' if something goes wrong
parsePiecesEx
    :: ParseRequestBodyOptions
    -> BackEnd y
    -> S.ByteString
    -> IO S.ByteString
    -> (Either Param (File y) -> IO ())
    -> IO ()
parsePiecesEx o sink bound rbody add =
    mkSource rbody >>= loop 0 0 0 0
  where
    loop :: Int -> Int -> Int -> Int64 -> Source -> IO ()
    loop numParms numFiles parmSize filesSize src = do
        _boundLine <- takeLine (prboMaxHeaderLineLength o) src
        res' <-
            takeLines'
                (prboMaxHeaderLineLength o)
                (prboMaxHeaderLines o)
                src
        unless (null res') $ do
            let ls' = map parsePair res'
            let x = do
                    cd <- lookup contDisp ls'
                    let ct = lookup contType ls'
                    let attrs = parseAttrs cd
                    name <- lookup "name" attrs
                    return (ct, name, lookup "filename" attrs)
            case x of
                Just (mct, name, Just filename) -> do
                    case prboKeyLength o of
                        Just maxKeyLength ->
                            when (S.length name > maxKeyLength) $
                                E.throwIO $
                                    FilenameTooLong name maxKeyLength
                        Nothing -> return ()
                    case prboMaxNumFiles o of
                        Just maxFiles ->
                            when (numFiles >= maxFiles) $
                                E.throwIO $
                                    MaxFileNumberExceeded numFiles
                        Nothing -> return ()
                    let ct = fromMaybe "application/octet-stream" mct
                        fi0 = FileInfo filename ct ()
                        fs =
                            catMaybes
                                [ prboMaxFileSize o
                                , subtract filesSize <$> prboMaxFilesSize o
                                ]
                        mfs = if null fs then Nothing else Just $ minimum fs
                    ((wasFound, fileSize), y) <- sinkTillBound' bound name fi0 sink src mfs
                    let newFilesSize = filesSize + fileSize
                    add $ Right (name, fi0{fileContent = y})
                    when wasFound $ loop numParms (numFiles + 1) parmSize newFilesSize src
                Just (_ct, name, Nothing) -> do
                    case prboKeyLength o of
                        Just maxKeyLength ->
                            when (S.length name > maxKeyLength) $
                                E.throwIO $
                                    ParamNameTooLong name maxKeyLength
                        Nothing -> return ()
                    let seed = id
                    let iter front bs = return $ front . (:) bs
                    ((wasFound, _fileSize), front) <-
                        sinkTillBound
                            bound
                            iter
                            seed
                            src
                            (fromIntegral <$> prboMaxParmsSize o)
                    let bs = S.concat $ front []
                    let x' = (name, bs)
                    let newParmSize = parmSize + S.length name + S.length bs
                    case prboMaxParmsSize o of
                        Just maxParmSize ->
                            when (newParmSize > maxParmSize) $
                                E.throwIO $
                                    MaxParamSizeExceeded newParmSize
                        Nothing -> return ()
                    add $ Left x'
                    when wasFound $
                        loop
                            (numParms + 1)
                            numFiles
                            newParmSize
                            filesSize
                            src
                _ -> do
                    -- ignore this part
                    let seed = ()
                        iter () _ = return ()
                    ((wasFound, _fileSize), ()) <- sinkTillBound bound iter seed src Nothing
                    when wasFound $ loop numParms numFiles parmSize filesSize src
      where
        contDisp = mk $ S8.pack "Content-Disposition"
        contType = mk $ S8.pack "Content-Type"
        parsePair s =
            let (x, y) = breakDiscard _colon s
             in (mk x, S.dropWhile (== _space) y)

-- | Things that could go wrong while parsing a 'Request'
--
-- @since 3.1.15
data RequestParseException
    = MaxParamSizeExceeded Int
    | ParamNameTooLong S.ByteString Int
    | MaxFileNumberExceeded Int
    | FilenameTooLong S.ByteString Int
    | TooManyHeaderLines Int
    deriving (Eq, Typeable)

instance E.Exception RequestParseException
instance Show RequestParseException where
    show = \case
        MaxParamSizeExceeded lmax -> unwords ["maximum parameter size exceeded:", show lmax]
        ParamNameTooLong s lmax -> unwords ["parameter name", S8.unpack s, "is too long:", show lmax]
        MaxFileNumberExceeded lmax -> unwords ["maximum number of files exceeded:", show lmax]
        FilenameTooLong fn lmax ->
            unwords ["file name", S8.unpack fn, "too long:", show lmax]
        TooManyHeaderLines nmax -> unwords ["Too many lines in mime/multipart header:", show nmax]

data Bound
    = FoundBound S.ByteString S.ByteString
    | NoBound
    | PartialBound
    deriving (Eq, Show)

findBound :: S.ByteString -> S.ByteString -> Bound
findBound b bs = handleBreak $ S.breakSubstring b bs
  where
    handleBreak (h, t)
        | S.null t = go [lowBound .. S.length bs - 1]
        | otherwise = FoundBound h $ S.drop (S.length b) t

    lowBound = max 0 $ S.length bs - S.length b

    go [] = NoBound
    go (i : is)
        | mismatch [0 .. S.length b - 1] [i .. S.length bs - 1] = go is
        | otherwise =
            let endI = i + S.length b
             in if endI > S.length bs
                    then PartialBound
                    else FoundBound (S.take i bs) (S.drop endI bs)
    mismatch [] _ = False
    mismatch _ [] = False
    mismatch (x : xs) (y : ys)
        | S.index b x == S.index bs y = mismatch xs ys
        | otherwise = True

sinkTillBound'
    :: S.ByteString
    -> S.ByteString
    -> FileInfo ()
    -> BackEnd y
    -> Source
    -> Maybe Int64
    -> IO ((Bool, Int64), y)
sinkTillBound' bound name fi sink src max' = do
    (next, final) <- wrapTillBound bound src max'
    y <- sink name fi next
    b <- final
    return (b, y)

data WTB
    = WTBWorking (S.ByteString -> S.ByteString)
    | WTBDone Bool
wrapTillBound
    :: S.ByteString
    -- ^ bound
    -> Source
    -> Maybe Int64
    -> IO (IO S.ByteString, IO (Bool, Int64))
    -- ^ Bool indicates if the bound was found
wrapTillBound bound src max' = do
    ref <- newIORef $ WTBWorking id
    sref <- newIORef (0 :: Int64)
    return (go ref sref, final ref sref)
  where
    final ref sref = do
        x <- readIORef ref
        case x of
            WTBWorking _ -> error "wrapTillBound did not finish"
            WTBDone y -> do
                siz <- readIORef sref
                return (y, siz)

    go ref sref = do
        state <- readIORef ref
        case state of
            WTBDone _ -> return S.empty
            WTBWorking front -> do
                bs <- readSource src
                cur <- atomicModifyIORef' sref $ \cur ->
                    let new = cur + fromIntegral (S.length bs) in (new, new)
                case max' of
                    Just max'' | cur > max'' -> E.throwIO PayloadTooLarge
                    _ -> return ()
                if S.null bs
                    then do
                        writeIORef ref $ WTBDone False
                        return $ front bs
                    else push $ front bs
      where
        push bs = do
            case findBound bound bs of
                FoundBound before after -> do
                    let before' = killCRLF before
                    leftover src after
                    writeIORef ref $ WTBDone True
                    return before'
                NoBound -> do
                    -- don't emit newlines, in case it's part of a bound
                    let (toEmit, front') =
                            if not (S8.null bs) && S8.last bs `elem` ['\r', '\n']
                                then
                                    let (x, y) = S.splitAt (S.length bs - 2) bs
                                     in (x, S.append y)
                                else (bs, id)
                    writeIORef ref $ WTBWorking front'
                    if S.null toEmit
                        then go ref sref
                        else return toEmit
                PartialBound -> do
                    writeIORef ref $ WTBWorking $ S.append bs
                    go ref sref

sinkTillBound
    :: S.ByteString
    -> (x -> S.ByteString -> IO x)
    -> x
    -> Source
    -> Maybe Int64
    -> IO ((Bool, Int64), x)
sinkTillBound bound iter seed0 src max' = do
    (next, final) <- wrapTillBound bound src max'
    let loop seed = do
            bs <- next
            if S.null bs
                then return seed
                else iter seed bs >>= loop
    seed <- loop seed0
    b <- final
    return (b, seed)

parseAttrs :: S.ByteString -> [(S.ByteString, S.ByteString)]
parseAttrs = map go . S.split _semicolon
  where
    tw = S.dropWhile (== _space)
    dq s =
        if S.length s > 2 && S.head s == _quotedbl && S.last s == _quotedbl
            then S.tail $ S.init s
            else s
    go s =
        let (x, y) = breakDiscard _equal s
         in (tw x, dq $ tw y)

killCRLF :: S.ByteString -> S.ByteString
killCRLF bs
    | S.null bs || S.last bs /= _lf = bs
    | otherwise = killCR $ S.init bs

killCR :: S.ByteString -> S.ByteString
killCR bs
    | S.null bs || S.last bs /= _cr = bs
    | otherwise = S.init bs