packages feed

cookie 0.4.1.2 → 0.5.1

raw patch · 7 files changed

Files

+ 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++[![Build Status](https://github.com/snoyberg/cookie/actions/workflows/tests.yaml/badge.svg)](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