cookie 0.4.1.2 → 0.5.1
raw patch · 7 files changed
Files
- ChangeLog.md +37/−0
- LICENSE +17/−22
- README.md +5/−0
- Setup.lhs +0/−8
- Web/Cookie.hs +174/−65
- cookie.cabal +19/−18
- test/Spec.hs +40/−8
+ ChangeLog.md view
@@ -0,0 +1,37 @@+## 0.5.1++* Add the `Partitioned` cookie attribute.++## 0.5.0++* Remove surrounding double quotes from cookie values when parsing [#31](https://github.com/snoyberg/cookie/pull/31)++ This is a breaking change, as it changes the behavior of `parseCookies` and `parseSetCookie` to no+ longer include the surrounding double quotes in the cookie value. This is the correct behavior+ according to the RFC.++## 0.4.6++* Resolve redundant import of Data.Monoid [#26](https://github.com/snoyberg/cookie/pull/26)+* Added `renderSetCookieBS` and `renderCookiesBS`++## 0.4.5++* Added `SameSite=None`++## 0.4.4++* Dropped dependency on blaze-builder+* Made cookie text rendering slightly more efficient++## 0.4.3++* Added `defaultSetCookie` [#16](https://github.com/snoyberg/cookie/pull/16)++## 0.4.2.1++* Clarified MIT license++## 0.4.2++* Added SameSite [#13](https://github.com/snoyberg/cookie/pull/13)
LICENSE view
@@ -1,25 +1,20 @@-The following license covers this documentation, and the source code, except-where otherwise indicated.--Copyright 2010, Michael Snoyman. All rights reserved.--Redistribution and use in source and binary forms, with or without-modification, are permitted provided that the following conditions are met:+Copyright (c) 2010 Michael Snoyman, http://www.yesodweb.com/ -* Redistributions of source code must retain the above copyright notice, this- list of conditions and the following disclaimer.+Permission is hereby granted, free of charge, to any person obtaining+a copy of this software and associated documentation files (the+"Software"), to deal in the Software without restriction, including+without limitation the rights to use, copy, modify, merge, publish,+distribute, sublicense, and/or sell copies of the Software, and to+permit persons to whom the Software is furnished to do so, subject to+the following conditions: -* Redistributions in binary form must reproduce the above copyright notice,- this list of conditions and the following disclaimer in the documentation- and/or other materials provided with the distribution.+The above copyright notice and this permission notice shall be+included in all copies or substantial portions of the Software. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY EXPRESS OR-IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF-MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO-EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,-INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT-NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,-OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF-LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE-OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF-ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ README.md view
@@ -0,0 +1,5 @@+## cookie++[](https://github.com/snoyberg/cookie/actions)++HTTP cookie parsing and rendering
− Setup.lhs
@@ -1,8 +0,0 @@-#!/usr/bin/env runhaskell--> module Main where-> import Distribution.Simple-> import System.Cmd (system)--> main :: IO ()-> main = defaultMain
Web/Cookie.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} module Web.Cookie ( -- * Server to client@@ -11,14 +12,23 @@ , setCookieDomain , setCookieHttpOnly , setCookieSecure+ , setCookieSameSite+ , setCookiePartitioned+ , SameSiteOption+ , sameSiteLax+ , sameSiteStrict+ , sameSiteNone -- ** Functions , parseSetCookie , renderSetCookie+ , renderSetCookieBS+ , defaultSetCookie , def -- * Client to server , Cookies , parseCookies , renderCookies+ , renderCookiesBS -- ** UTF8 Version , CookiesText , parseCookiesText@@ -31,22 +41,23 @@ import qualified Data.ByteString as S import qualified Data.ByteString.Char8 as S8-import Data.Char (toLower)-import Blaze.ByteString.Builder (Builder, fromByteString, copyByteString)-import Blaze.ByteString.Builder.Char8 (fromChar)+import qualified Data.ByteString.Lazy as L+import Data.Char (toLower, isDigit)+import Data.ByteString.Builder (Builder, byteString, char8, toLazyByteString)+import Data.ByteString.Builder.Extra (byteStringCopy)+#if !(MIN_VERSION_base(4,8,0)) import Data.Monoid (mempty, mappend, mconcat)+#endif import Data.Word (Word8) import Data.Ratio (numerator, denominator)-import Data.Time (UTCTime (UTCTime), toGregorian, fromGregorian, formatTime, parseTime)+import Data.Time (UTCTime (UTCTime), toGregorian, fromGregorian, formatTime, parseTimeM, defaultTimeLocale) import Data.Time.Clock (DiffTime, secondsToDiffTime)-import System.Locale (defaultTimeLocale)-import Control.Arrow (first)+import Control.Arrow (first, (***)) import Data.Text (Text)-import Data.Text.Encoding (encodeUtf8, decodeUtf8With)+import Data.Text.Encoding (encodeUtf8Builder, decodeUtf8With) import Data.Text.Encoding.Error (lenientDecode)-import Control.Arrow ((***))-import Data.Maybe (isJust)-import Data.Default (Default (def))+import Data.Maybe (isJust, fromMaybe, listToMaybe)+import Data.Default.Class (Default (def)) import Control.DeepSeq (NFData (rnf)) -- | Textual cookies. Functions assume UTF8 encoding.@@ -58,56 +69,125 @@ where go = decodeUtf8With lenientDecode --- FIXME to speed things up, skip encodeUtf8 and use fromText instead renderCookiesText :: CookiesText -> Builder-renderCookiesText = renderCookies . map (encodeUtf8 *** encodeUtf8)+renderCookiesText = renderCookiesBuilder . map (encodeUtf8Builder *** encodeUtf8Builder) type Cookies = [(S.ByteString, S.ByteString)] +semicolon :: Word8+semicolon = 59++equalsSign :: Word8+equalsSign = 61++space :: Word8+space = 32++doubleQuote :: Word8+doubleQuote = 34+ -- | Decode the value of a \"Cookie\" request header into key/value pairs. parseCookies :: S.ByteString -> Cookies parseCookies s | S.null s = [] | otherwise =- let (x, y) = breakDiscard 59 s -- semicolon+ let (x, y) = breakDiscard semicolon s in parseCookie x : parseCookies y parseCookie :: S.ByteString -> (S.ByteString, S.ByteString) parseCookie s =- let (key, value) = breakDiscard 61 s -- equals sign- key' = S.dropWhile (== 32) key -- space- in (key', value)+ let (key, value) = breakDiscard equalsSign s+ key' = S.dropWhile (== space) key+ value' = dropEnds doubleQuote value+ in (key', value') breakDiscard :: Word8 -> S.ByteString -> (S.ByteString, S.ByteString) breakDiscard w s =- let (x, y) = S.breakByte w s+ let (x, y) = S.break (== w) s in (x, S.drop 1 y) -renderCookies :: Cookies -> Builder-renderCookies [] = mempty-renderCookies cs =+dropEnds :: Word8 -> S.ByteString -> S.ByteString+dropEnds w s =+ case S.unsnoc s of+ Just (s', w') | w' == w -> case S.uncons s' of+ Just (w'', s'') | w'' == w -> s''+ _ -> s+ _ -> s++type CookieBuilder = (Builder, Builder)++renderCookiesBuilder :: [CookieBuilder] -> Builder+renderCookiesBuilder [] = mempty+renderCookiesBuilder cs = foldr1 go $ map renderCookie cs where- go x y = x `mappend` fromChar ';' `mappend` y+ go x y = x `mappend` char8 ';' `mappend` y -renderCookie :: (S.ByteString, S.ByteString) -> Builder-renderCookie (k, v) = fromByteString k `mappend` fromChar '='- `mappend` fromByteString v+renderCookie :: CookieBuilder -> Builder+renderCookie (k, v) = k `mappend` char8 '=' `mappend` v +renderCookies :: Cookies -> Builder+renderCookies = renderCookiesBuilder . map (byteString *** byteString)++-- | @since 0.4.6+renderCookiesBS :: Cookies -> S.ByteString+renderCookiesBS = L.toStrict . toLazyByteString . renderCookies++-- | Data type representing the key-value pair to use for a cookie, as well as configuration options for it.+--+-- ==== Creating a SetCookie+--+-- 'SetCookie' does not export a constructor; instead, use 'defaultSetCookie' and override values (see <http://www.yesodweb.com/book/settings-types> for details):+--+-- @+-- import Web.Cookie+-- :set -XOverloadedStrings+-- let cookie = 'defaultSetCookie' { 'setCookieName' = "cookieName", 'setCookieValue' = "cookieValue" }+-- @+--+-- ==== Cookie Configuration+--+-- Cookies have several configuration options; a brief summary of each option is given below. For more information, see <http://tools.ietf.org/html/rfc6265#section-4.1.2 RFC 6265> or <https://en.wikipedia.org/wiki/HTTP_cookie#Cookie_attributes Wikipedia>. data SetCookie = SetCookie- { setCookieName :: S.ByteString- , setCookieValue :: S.ByteString- , setCookiePath :: Maybe S.ByteString- , setCookieExpires :: Maybe UTCTime- , setCookieMaxAge :: Maybe DiffTime- , setCookieDomain :: Maybe S.ByteString- , setCookieHttpOnly :: Bool- , setCookieSecure :: Bool+ { setCookieName :: S.ByteString -- ^ The name of the cookie. Default value: @"name"@+ , setCookieValue :: S.ByteString -- ^ The value of the cookie. Default value: @"value"@+ , setCookiePath :: Maybe S.ByteString -- ^ The URL path for which the cookie should be sent. Default value: @Nothing@ (The browser defaults to the path of the request that sets the cookie).+ , setCookieExpires :: Maybe UTCTime -- ^ The time at which to expire the cookie. Default value: @Nothing@ (The browser will default to expiring a cookie when the browser is closed).+ , setCookieMaxAge :: Maybe DiffTime -- ^ The maximum time to keep the cookie, in seconds. Default value: @Nothing@ (The browser defaults to expiring a cookie when the browser is closed).+ , setCookieDomain :: Maybe S.ByteString -- ^ The domain for which the cookie should be sent. Default value: @Nothing@ (The browser defaults to the current domain).+ , setCookieHttpOnly :: Bool -- ^ Marks the cookie as "HTTP only", i.e. not accessible from Javascript. Default value: @False@+ , setCookieSecure :: Bool -- ^ Instructs the browser to only send the cookie over HTTPS. Default value: @False@+ , setCookieSameSite :: Maybe SameSiteOption -- ^ The "same site" policy of the cookie, i.e. whether it should be sent with cross-site requests. Default value: @Nothing@+ , setCookiePartitioned :: Bool -- ^ Cookies marked Partitioned are double-keyed: by the origin that sets them and the origin of the top-level page. Default value: @False@ } deriving (Eq, Show) +-- | Data type representing the options for a <https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-4.1 SameSite cookie>+data SameSiteOption = Lax+ | Strict+ | None+ deriving (Show, Eq)++instance NFData SameSiteOption where+ rnf x = x `seq` ()++-- | Directs the browser to send the cookie for <https://tools.ietf.org/html/rfc7231#section-4.2.1 safe requests> (e.g. @GET@), but not for unsafe ones (e.g. @POST@)+sameSiteLax :: SameSiteOption+sameSiteLax = Lax++-- | Directs the browser to not send the cookie for /any/ cross-site request, including e.g. a user clicking a link in their email to open a page on your site.+sameSiteStrict :: SameSiteOption+sameSiteStrict = Strict++-- |+-- Directs the browser to send the cookie for cross-site requests.+--+-- @since 0.4.5+sameSiteNone :: SameSiteOption+sameSiteNone = None+ instance NFData SetCookie where- rnf (SetCookie a b c d e f g h) =+ rnf (SetCookie a b c d e f g h i j) = a `seq` b `seq` rnfMBS c `seq`@@ -115,57 +195,80 @@ rnf e `seq` rnfMBS f `seq` rnf g `seq`- rnf h+ rnf h `seq`+ rnf i `seq`+ rnf j where -- For backwards compatibility rnfMBS Nothing = () rnfMBS (Just bs) = bs `seq` () +-- | @'def' = 'defaultSetCookie'@ instance Default SetCookie where- def = SetCookie- { setCookieName = "name"- , setCookieValue = "value"- , setCookiePath = Nothing- , setCookieExpires = Nothing- , setCookieMaxAge = Nothing- , setCookieDomain = Nothing- , setCookieHttpOnly = False- , setCookieSecure = False- }+ def = defaultSetCookie +-- | A minimal 'SetCookie'. All fields are 'Nothing' or 'False' except @'setCookieName' = "name"@ and @'setCookieValue' = "value"@. You need this to construct a 'SetCookie', because it does not export a constructor. Equivalently, you may use 'def'.+--+-- @since 0.4.2.2+defaultSetCookie :: SetCookie+defaultSetCookie = SetCookie+ { setCookieName = "name"+ , setCookieValue = "value"+ , setCookiePath = Nothing+ , setCookieExpires = Nothing+ , setCookieMaxAge = Nothing+ , setCookieDomain = Nothing+ , setCookieHttpOnly = False+ , setCookieSecure = False+ , setCookieSameSite = Nothing+ , setCookiePartitioned = False+ }+ renderSetCookie :: SetCookie -> Builder renderSetCookie sc = mconcat- [ fromByteString (setCookieName sc)- , fromChar '='- , fromByteString (setCookieValue sc)+ [ byteString (setCookieName sc)+ , char8 '='+ , byteString (setCookieValue sc) , case setCookiePath sc of Nothing -> mempty- Just path -> copyByteString "; Path="- `mappend` fromByteString path+ Just path -> byteStringCopy "; Path="+ `mappend` byteString path , case setCookieExpires sc of Nothing -> mempty- Just e -> copyByteString "; Expires=" `mappend`- fromByteString (formatCookieExpires e)+ Just e -> byteStringCopy "; Expires=" `mappend`+ byteString (formatCookieExpires e) , case setCookieMaxAge sc of Nothing -> mempty- Just ma -> copyByteString"; Max-Age=" `mappend`- fromByteString (formatCookieMaxAge ma)+ Just ma -> byteStringCopy "; Max-Age=" `mappend`+ byteString (formatCookieMaxAge ma) , case setCookieDomain sc of Nothing -> mempty- Just d -> copyByteString "; Domain=" `mappend`- fromByteString d+ Just d -> byteStringCopy "; Domain=" `mappend`+ byteString d , if setCookieHttpOnly sc- then copyByteString "; HttpOnly"+ then byteStringCopy "; HttpOnly" else mempty , if setCookieSecure sc- then copyByteString "; Secure"+ then byteStringCopy "; Secure" else mempty+ , case setCookieSameSite sc of+ Nothing -> mempty+ Just Lax -> byteStringCopy "; SameSite=Lax"+ Just Strict -> byteStringCopy "; SameSite=Strict"+ Just None -> byteStringCopy "; SameSite=None"+ , if setCookiePartitioned sc+ then byteStringCopy "; Partitioned"+ else mempty ] +-- | @since 0.4.6+renderSetCookieBS :: SetCookie -> S.ByteString+renderSetCookieBS = L.toStrict . toLazyByteString . renderSetCookie+ parseSetCookie :: S.ByteString -> SetCookie parseSetCookie a = SetCookie { setCookieName = name- , setCookieValue = value+ , setCookieValue = dropEnds doubleQuote value , setCookiePath = lookup "path" flags , setCookieExpires = lookup "expires" flags >>= parseCookieExpires@@ -174,13 +277,19 @@ , setCookieDomain = lookup "domain" flags , setCookieHttpOnly = isJust $ lookup "httponly" flags , setCookieSecure = isJust $ lookup "secure" flags+ , setCookieSameSite = case lookup "samesite" flags of+ Just "Lax" -> Just Lax+ Just "Strict" -> Just Strict+ Just "None" -> Just None+ _ -> Nothing+ , setCookiePartitioned = isJust $ lookup "partitioned" flags } where- pairs = map (parsePair . dropSpace) $ S.split 59 a ++ [S8.empty] -- 59 = semicolon- (name, value) = head pairs- flags = map (first (S8.map toLower)) $ tail pairs- parsePair = breakDiscard 61 -- equals sign- dropSpace = S.dropWhile (== 32) -- space+ pairs = map (parsePair . dropSpace) $ S.split semicolon a+ (name, value) = fromMaybe mempty $ listToMaybe pairs+ flags = map (first (S8.map toLower)) $ drop 1 pairs+ parsePair = breakDiscard equalsSign+ dropSpace = S.dropWhile (== space) expiresFormat :: String expiresFormat = "%a, %d-%b-%Y %X GMT"@@ -192,7 +301,7 @@ parseCookieExpires :: S.ByteString -> Maybe UTCTime parseCookieExpires =- fmap fuzzYear . parseTime defaultTimeLocale expiresFormat . S8.unpack+ fmap fuzzYear . parseTimeM True defaultTimeLocale expiresFormat . S8.unpack where -- See: https://github.com/snoyberg/cookie/issues/5 fuzzYear orig@(UTCTime day diff)@@ -212,6 +321,6 @@ parseCookieMaxAge :: S.ByteString -> Maybe DiffTime parseCookieMaxAge bs- | all (\ c -> c >= '0' && c <= '9') $ unpacked = Just $ secondsToDiffTime $ read unpacked+ | all isDigit unpacked = Just $ secondsToDiffTime $ read unpacked | otherwise = Nothing where unpacked = S8.unpack bs
cookie.cabal view
@@ -1,44 +1,45 @@+cabal-version: >= 1.10 name: cookie-version: 0.4.1.2-license: BSD3+version: 0.5.1+license: MIT license-file: LICENSE author: Michael Snoyman <michael@snoyman.com> maintainer: Michael Snoyman <michael@snoyman.com> synopsis: HTTP cookie parsing and rendering+description: Hackage documentation generation is not reliable. For up to date documentation, please see: <https://www.stackage.org/package/cookie>. category: Web, Yesod stability: Stable-cabal-version: >= 1.8 build-type: Simple-homepage: http://github.com/snoyberg/cookie+homepage: https://github.com/snoyberg/cookie+extra-source-files: README.md ChangeLog.md library+ default-language: Haskell2010 build-depends: base >= 4 && < 5- , bytestring >= 0.9.1.4- , blaze-builder >= 0.2.1- , old-locale >= 1- , time >= 1.1.4- , text >= 0.7- , data-default+ , bytestring >= 0.10.2+ , time >= 1.5+ , text >= 1.1+ , data-default-class , deepseq exposed-modules: Web.Cookie ghc-options: -Wall test-suite test+ default-language: Haskell2010 hs-source-dirs: test main-is: Spec.hs type: exitcode-stdio-1.0 build-depends: base , HUnit , QuickCheck- , blaze-builder- , bytestring+ , bytestring >= 0.10.2 , cookie- , test-framework- , test-framework-hunit- , test-framework-quickcheck2- , text- , time+ , tasty+ , tasty-hunit+ , tasty-quickcheck+ , text >= 1.1+ , time >= 1.5 source-repository head type: git- location: git://github.com/snoyberg/cookie.git+ location: https://github.com/snoyberg/cookie.git
test/Spec.hs view
@@ -1,26 +1,30 @@-import Test.Framework (defaultMain)-import Test.Framework.Providers.QuickCheck2 (testProperty)-import Test.Framework.Providers.HUnit (testCase)+{-# OPTIONS_GHC -Wno-orphans #-}+module Main where++import Test.Tasty (defaultMain, testGroup, TestTree)+import Test.Tasty.QuickCheck (testProperty)+import Test.Tasty.HUnit (testCase) import Test.QuickCheck import Test.HUnit ((@=?), Assertion) import Web.Cookie-import Blaze.ByteString.Builder (Builder, toLazyByteString)+import Data.ByteString.Builder (Builder, toLazyByteString) import qualified Data.ByteString as S import qualified Data.ByteString.Char8 as S8 import qualified Data.ByteString.Lazy as L import Data.Word (Word8) import Control.Arrow ((***))-import Control.Applicative ((<$>), (<*>)) import Data.Time (UTCTime (UTCTime), toGregorian) import qualified Data.Text as T main :: IO ()-main = defaultMain+main = defaultMain $ testGroup "cookie" [ testProperty "parse/render cookies" propParseRenderCookies , testProperty "parse/render SetCookie" propParseRenderSetCookie , testProperty "parse/render cookies text" propParseRenderCookiesText , testCase "parseCookies" caseParseCookies+ , testCase "parseQuotedCookies" caseParseQuotedCookies+ , testCase "parseQuotedSetCookie" caseParseQuotedSetCookie , twoDigit 24 2024 , twoDigit 69 2069 , twoDigit 70 1970@@ -51,7 +55,10 @@ show (Char' w) = [toEnum $ fromEnum w] showList = (++) . show . concatMap show instance Arbitrary Char' where- arbitrary = fmap (Char' . toEnum) $ choose (62, 125)+ arbitrary = Char' . toEnum <$> choose (62, 125)+newtype SameSiteOption' = SameSiteOption' { unSameSiteOption' :: SameSiteOption }+instance Arbitrary SameSiteOption' where+ arbitrary = fmap SameSiteOption' (elements [sameSiteLax, sameSiteStrict, sameSiteNone]) propParseRenderSetCookie :: SetCookie -> Bool propParseRenderSetCookie sc =@@ -67,6 +74,8 @@ domain <- fmap (fmap fromUnChars) arbitrary httponly <- arbitrary secure <- arbitrary+ sameSite <- fmap (fmap unSameSiteOption') arbitrary+ partitioned <- arbitrary return def { setCookieName = name , setCookieValue = value@@ -75,6 +84,8 @@ , setCookieDomain = domain , setCookieHttpOnly = httponly , setCookieSecure = secure+ , setCookieSameSite = sameSite+ , setCookiePartitioned = partitioned } caseParseCookies :: Assertion@@ -83,17 +94,38 @@ expected = [("a", "a1"), ("b", "b2"), ("c", "c3")] map (S8.pack *** S8.pack) expected @=? parseCookies input +-- TODO: Use `Year` from Data.Time when we'll remove support for GHC <9.2+type Year = Integer+ -- Tests for two digit years, see: -- -- https://github.com/snoyberg/cookie/issues/5+twoDigit :: Year -> Year -> TestTree twoDigit x y = testCase ("year " ++ show x) (y @=? year) where (year, _, _) = toGregorian day- Just (UTCTime day _) = setCookieExpires sc+ day =+ case setCookieExpires sc of+ Just (UTCTime day' _) -> day'+ Nothing -> error $ "setCookieExpires == Nothing for: " ++ show str sc = parseSetCookie str str = S8.pack $ concat [ "foo=bar; Expires=Mon, 29-Jul-" , show x , " 04:52:08 GMT" ]++caseParseQuotedCookies :: Assertion+caseParseQuotedCookies = do+ let input = S8.pack "a=\"a1\";b=\"b2\"; c=\"c3\""+ expected = [("a", "a1"), ("b", "b2"), ("c", "c3")]+ map (S8.pack *** S8.pack) expected @=? parseCookies input++caseParseQuotedSetCookie :: Assertion+caseParseQuotedSetCookie = do+ let input = S8.pack "a=\"a1\""+ result = parseSetCookie input+ resultNameAndValue = (setCookieName result, setCookieValue result)+ expected = (S8.pack "a", S8.pack "a1")+ expected @=? resultNameAndValue