web-encodings 0.0.1 → 0.2.0
raw patch · 9 files changed
+686/−359 lines, 9 filesdep +HUnitdep +QuickCheckdep +convertibledep −bytestring-classdep ~bytestringdep ~old-localedep ~timenew-component:exe:runtestsPVP ok
version bump matches the API change (PVP)
Dependencies added: HUnit, QuickCheck, convertible, failure, safe, test-framework, test-framework-hunit, test-framework-quickcheck, text
Dependencies removed: bytestring-class
Dependency ranges changed: bytestring, old-locale, time
API changes (from Hackage documentation)
- Web.Encodings: instance Show FileInfo
+ Web.Encodings: decodeHtml :: (StringLike s) => s -> s
+ Web.Encodings: decodeJson :: (StringLike s) => s -> s
+ Web.Encodings: instance (Show s) => Show (FileInfo s a)
+ Web.Encodings.ListHelper: cons :: a -> [a] -> [a]
+ Web.Encodings.MimeHeader: AttributeNotFound :: s -> s -> AttributeNotFound s
+ Web.Encodings.MimeHeader: HeaderNotFound :: s -> HeaderNotFound s
+ Web.Encodings.MimeHeader: data AttributeNotFound s
+ Web.Encodings.MimeHeader: data HeaderNotFound s
+ Web.Encodings.MimeHeader: instance (Show s) => Show (AttributeNotFound s)
+ Web.Encodings.MimeHeader: instance (Show s) => Show (HeaderNotFound s)
+ Web.Encodings.MimeHeader: instance (Show s, Typeable s) => Exception (HeaderNotFound s)
+ Web.Encodings.MimeHeader: instance (Typeable s, Show s) => Exception (AttributeNotFound s)
+ Web.Encodings.MimeHeader: instance Typeable1 AttributeNotFound
+ Web.Encodings.MimeHeader: instance Typeable1 HeaderNotFound
+ Web.Encodings.MimeHeader: lookupHeader :: (StringLike s, MonadFailure (HeaderNotFound s) m, Eq s) => s -> [Header s] -> m s
+ Web.Encodings.MimeHeader: lookupHeaderAttr :: (MonadFailure (AttributeNotFound s) m, StringLike s, Eq s) => s -> s -> [Header s] -> m s
+ Web.Encodings.MimeHeader: parseHeader :: (StringLike a) => a -> Header a
+ Web.Encodings.MimeHeader: type Header a = (a, a, SomeMap a)
+ Web.Encodings.StringLike: append :: (StringLike a) => a -> a -> a
+ Web.Encodings.StringLike: break :: (StringLike a) => (Char -> Bool) -> a -> (a, a)
+ Web.Encodings.StringLike: breakChar :: (StringLike a) => Char -> a -> (a, a)
+ Web.Encodings.StringLike: breakCharMaybe :: (StringLike a) => Char -> a -> Maybe (a, a)
+ Web.Encodings.StringLike: breakString :: (StringLike a) => a -> a -> (a, a)
+ Web.Encodings.StringLike: chomp :: (StringLike a) => a -> a
+ Web.Encodings.StringLike: class (Eq a, Show a) => StringLike a
+ Web.Encodings.StringLike: concatMap :: (StringLike a) => (Char -> String) -> a -> a
+ Web.Encodings.StringLike: cons :: (StringLike a) => Char -> a -> a
+ Web.Encodings.StringLike: dropPrefix :: (StringLike a) => a -> a -> Maybe a
+ Web.Encodings.StringLike: dropPrefix' :: (StringLike a) => a -> a -> a
+ Web.Encodings.StringLike: dropQuotes :: (StringLike a) => a -> a
+ Web.Encodings.StringLike: dropWhile :: (StringLike a) => (Char -> Bool) -> a -> a
+ Web.Encodings.StringLike: empty :: (StringLike a) => a
+ Web.Encodings.StringLike: head :: (StringLike a) => a -> Char
+ Web.Encodings.StringLike: init :: (StringLike a) => a -> a
+ Web.Encodings.StringLike: instance StringLike ByteString
+ Web.Encodings.StringLike: instance StringLike Text
+ Web.Encodings.StringLike: instance StringLike [Char]
+ Web.Encodings.StringLike: intercalate :: (StringLike a) => a -> [a] -> a
+ Web.Encodings.StringLike: isPrefixOf :: (StringLike a) => a -> a -> Bool
+ Web.Encodings.StringLike: last :: (StringLike a) => a -> Char
+ Web.Encodings.StringLike: lengthGE :: (StringLike a) => Int -> a -> Bool
+ Web.Encodings.StringLike: lengthLT :: (StringLike a) => Int -> a -> Bool
+ Web.Encodings.StringLike: null :: (StringLike a) => a -> Bool
+ Web.Encodings.StringLike: pack :: (StringLike a) => String -> a
+ Web.Encodings.StringLike: span :: (StringLike a) => (Char -> Bool) -> a -> (a, a)
+ Web.Encodings.StringLike: split :: (StringLike a) => Char -> a -> [a]
+ Web.Encodings.StringLike: tail :: (StringLike a) => a -> a
+ Web.Encodings.StringLike: take :: (StringLike a) => Int -> a -> a
+ Web.Encodings.StringLike: takeLine :: (StringLike a) => a -> (a, a)
+ Web.Encodings.StringLike: takeUntilBlank :: (StringLike a) => a -> ([a], a)
+ Web.Encodings.StringLike: uncons :: (StringLike a) => a -> Maybe (Char, a)
+ Web.Encodings.StringLike: unpack :: (StringLike a) => a -> String
- Web.Encodings: FileInfo :: String -> String -> ByteString -> FileInfo
+ Web.Encodings: FileInfo :: s -> s -> c -> FileInfo s c
- Web.Encodings: data FileInfo
+ Web.Encodings: data FileInfo s c
- Web.Encodings: decodeUrl :: (LazyByteString x, LazyByteString y) => x -> y
+ Web.Encodings: decodeUrl :: (StringLike s) => s -> s
- Web.Encodings: decodeUrlPairs :: (LazyByteString x, LazyByteString y, LazyByteString z) => x -> [(y, z)]
+ Web.Encodings: decodeUrlPairs :: (StringLike s) => s -> [(s, s)]
- Web.Encodings: encodeHtml :: String -> String
+ Web.Encodings: encodeHtml :: (StringLike s) => s -> s
- Web.Encodings: encodeJson :: (LazyByteString x) => String -> x
+ Web.Encodings: encodeJson :: (StringLike s) => s -> s
- Web.Encodings: encodeUrl :: (LazyByteString x, LazyByteString y) => x -> y
+ Web.Encodings: encodeUrl :: (StringLike s) => s -> s
- Web.Encodings: encodeUrlPairs :: (LazyByteString x, LazyByteString y, LazyByteString z) => [(x, y)] -> z
+ Web.Encodings: encodeUrlPairs :: (StringLike s) => [(s, s)] -> s
- Web.Encodings: fileContent :: FileInfo -> ByteString
+ Web.Encodings: fileContent :: FileInfo s c -> c
- Web.Encodings: fileContentType :: FileInfo -> String
+ Web.Encodings: fileContentType :: FileInfo s c -> s
- Web.Encodings: fileName :: FileInfo -> String
+ Web.Encodings: fileName :: FileInfo s c -> s
- Web.Encodings: parseMultipart :: (LazyByteString lbs) => lbs -> ByteString -> ([(String, String)], [(String, FileInfo)])
+ Web.Encodings: parseMultipart :: (StringLike s) => String -> s -> ([(s, s)], [(s, FileInfo s s)])
- Web.Encodings: parsePost :: String -> String -> ByteString -> ([(String, String)], [(String, FileInfo)])
+ Web.Encodings: parsePost :: (StringLike s) => String -> String -> s -> ([(s, s)], [(s, FileInfo s s)])
Files
- Data/ByteString/Lazy/Util.hs +0/−89
- Data/Mime/Header.hs +0/−69
- Data/String/Util.hs +0/−55
- Test.hs +153/−0
- Web/Encodings.hs +210/−138
- Web/Encodings/ListHelper.hs +4/−0
- Web/Encodings/MimeHeader.hs +82/−0
- Web/Encodings/StringLike.hs +205/−0
- web-encodings.cabal +32/−8
− Data/ByteString/Lazy/Util.hs
@@ -1,89 +0,0 @@------------------------------------------------------------- |--- Module : Data.ByteString.Lazy.Util--- Copyright : Michael Snoyman--- License : BSD3------ Maintainer : Michael Snoyman <michael@snoyman.com>--- Stability : Unstable--- Portability : portable------ Various utilities to assist in dealing with lazy bytestrings.---------------------------------------------------------------module Data.ByteString.Lazy.Util- ( ord- , stripPrefix- , breakAt- , breakAtString- , takeLine- , chompBS- , takeUntilBlank- ) where--import qualified Data.ByteString.Lazy as BS-import qualified Data.Char as C-import Data.Word (Word8)---- | Get the ASCII value of character. Differers from regular ord in that--- it returns an Integral, so it is automatically cast to eg a Word8.-ord :: Integral a => Char -> a-ord = fromInteger . toInteger . C.ord---- | Strip a prefix from a bytestring if it's there.-stripPrefix :: Word8 -> BS.ByteString -> BS.ByteString-stripPrefix p bs- | BS.null bs = bs- | BS.head bs == p = BS.tail bs- | otherwise = bs---- | Break a bytestring into two at the first occurence of the given 'Word8'.--- That 'Word8' should not appear in either piece.-breakAt :: Word8 -> BS.ByteString -> (BS.ByteString, BS.ByteString)-breakAt p bs =- let (x, y) = BS.span (/= p) bs- y' = stripPrefix p y- in (x, y')---- | Same as 'breakAt', but use a bytestring instead of a 'Word8'.-breakAtString :: BS.ByteString- -> BS.ByteString- -> (BS.ByteString, BS.ByteString)-breakAtString p c- | BS.null c = (BS.empty, BS.empty)- | p `BS.isPrefixOf` c = (BS.empty, BS.drop (BS.length p) c)- | otherwise =- let x = BS.head c- xs = BS.tail c- (next, rest) = breakAtString p xs- in (BS.cons' x next, rest)---- | Take a single line from a bytestring.-takeLine :: BS.ByteString -> (BS.ByteString, BS.ByteString)-takeLine bs =- let (x, y) = BS.span (/= ord '\n') bs- x' = if not (BS.null x) && BS.last x == ord '\r' then BS.init x else x- y' = if not (BS.null y) && BS.head y == ord '\n' then BS.tail y else y- in (x', y')---- | Removes newline characters from the end of a string.-chompBS :: BS.ByteString -> BS.ByteString-chompBS s- | BS.null s = s- | BS.last s == ord '\n' =- if BS.length s == 1 || BS.last (BS.init s) /= ord '\r'- then BS.init s- else BS.init (BS.init s)- | BS.last s == ord '\r' = BS.init s- | otherwise = s---- | Take each line until the first blank line and return as first.--- The rest of the content is returned as second.-takeUntilBlank :: BS.ByteString -> ([BS.ByteString], BS.ByteString)-takeUntilBlank bs =- let (next, rest) = takeLine bs- in if BS.null next- then ([], rest)- else let (nexts, rest') = takeUntilBlank rest- in (next : nexts, rest')
− Data/Mime/Header.hs
@@ -1,69 +0,0 @@------------------------------------------------------------- |--- Module : Data.Mime.Header--- Copyright : Michael Snoyman--- License : BSD3------ Maintainer : Michael Snoyman <michael@snoyman.com>--- Stability : Unstable--- Portability : portable------ Functions for parsing MIME headers (Key: value; k1=v1; k2=v2)---------------------------------------------------------------module Data.Mime.Header- ( Header- , parseHeader- , lookupHeader- , lookupHeaderAttr- ) where--import Data.String.Util-import Data.ByteString.Class-import qualified Data.ByteString.Lazy as BS--type SMap = [(String, String)]---- | A single MIME header.-type Header = (String, String, SMap)---- | Parse a header line in the format--- Name: value; attkey=attval; attkey2=attval2.-parseHeader :: BS.ByteString -> Header-parseHeader bs =- let s = fromLazyByteString bs- (k, rest) = span (/= ':') s- (v, attrs) = parseRest rest- in (k, v, attrs) where- parseRest :: String -> (String, SMap)- parseRest s =- let (v, rest) = span (/= ';') s- attrs = parseAttrs rest- in (dropPrefix ": " v, attrs)- parseAttrs :: String -> SMap- parseAttrs [] = []- parseAttrs s =- let s' = dropPrefix "; " s- (next, rest) = span (/= ';') s'- (k, v) = span (/= '=') next- v' = dropPrefix "=" v- v'' = dropQuotes v'- in (k, v'') : parseAttrs rest--lookupHeaderAttr :: Monad m => String -> String -> [Header] -> m String-lookupHeaderAttr k1 k2 [] =- fail $ "Could not find header when looking for attr: " ++ - k1 ++ ":" ++ k2-lookupHeaderAttr k1 k2 ((key, _, vals):rest)- | k1 == key = case lookup k2 vals of- Nothing -> fail $ "Could not find header attr "- ++ k1 ++ ":" ++ k2- Just v -> return v- | otherwise = lookupHeaderAttr k1 k2 rest--lookupHeader :: Monad m => String -> [Header] -> m String-lookupHeader k [] = fail $ "Header " ++ k ++ " not found"-lookupHeader k ((key, val, _):rest)- | k == key = return val- | otherwise = lookupHeader k rest
− Data/String/Util.hs
@@ -1,55 +0,0 @@------------------------------------------------------------- |--- Module : Data.String.Util--- Copyright : Michael Snoyman--- License : BSD3------ Maintainer : Michael Snoyman <michael@snoyman.com>--- Stability : Unstable--- Portability : portable------ Various utilities to assist in dealing with Strings.---------------------------------------------------------------module Data.String.Util- ( chomp- , dropPrefix- , dropQuotes- , splitList- ) where--import Data.List (isPrefixOf)---- | Removes newline characters from the end of a string.-chomp :: String -> String-chomp s- | null s = s- | last s == '\n' =- if length s == 1 || last (init s) /= '\r'- then init s- else init (init s)- | last s == '\r' = init s- | otherwise = s---- | Drop a string from the beginning of another, if present.-dropPrefix :: Eq a => [a] -> [a] -> [a]-dropPrefix x y- | x `isPrefixOf` y = drop (length x) y- | otherwise = y---- | Drop surrounding quotes, if present.-dropQuotes :: String -> String-dropQuotes s- | length s > 2 && head s == '"' && last s == '"' = tail $ init s- | otherwise = s---- | Split up a list into sublists at every occurence of the split--- element. That element is thrown away.-splitList :: Eq a => a -> [a] -> [[a]]-splitList c s = helper s [[]] where- helper [] res = filter (not . null) $ reverse $ map reverse res- helper (x:xs) (y:ys)- | x == c = helper xs ([]:y:ys)- | otherwise = helper xs ((x:y):ys)- helper _ [] = error "This case should never be"
+ Test.hs view
@@ -0,0 +1,153 @@+{-# OPTIONS_GHC -fno-warn-orphans #-}+{-# LANGUAGE ScopedTypeVariables #-}+import Test.Framework+import Test.Framework.Providers.HUnit+import Test.Framework.Providers.QuickCheck++import Test.HUnit+import Test.QuickCheck++import Web.Encodings+import Data.Char (chr, ord)++import qualified Data.ByteString.Char8 as BS+import qualified Data.ByteString.Lazy.Char8 as BL+import qualified Data.Text as TS+import qualified Data.Text.Lazy as TL+--import Debug.Trace++import qualified Web.Encodings.StringLike as SL+import Web.Encodings.StringLike (StringLike)+import Control.Arrow ((***))++main :: IO ()+main = defaultMain [tests]++allTests :: (Arbitrary a, StringLike a) => String -> a -> Test.Framework.Test+allTests n s = testGroup n+ [ testProperty "encode/decode URL" $ qcEncodeDecodeUrl s+ , testProperty "encode/decode HTML" $ qcEncodeDecodeHtml s+ , testProperty "encode/decode JSON" $ qcEncodeDecodeJson s+ , testProperty "encode/decode URL pairs" $ qcEncodeDecodeUrlPairs s+ , testCase "hunit query string" $ huQueryString s+ , testCase "hunit encode json" $ huEncodeJson s+ -- FIXME , testCase "parse post" huParsePost+ ]++tests :: Test.Framework.Test+tests = testGroup "Web.Encodings"+ [ allTests "String" (undefined :: String)+ , allTests "Strict ByteString" (undefined :: BS.ByteString)+ , allTests "Lazy ByteString" (undefined :: BL.ByteString)+ , allTests "Strict Text" (undefined :: TS.Text)+ , allTests "Lazy Text" (undefined :: TL.Text)+ ]++qcEncodeDecodeUrl :: StringLike a => a -> a -> Bool+qcEncodeDecodeUrl _ s = decodeUrl (encodeUrl s) == s++qcEncodeDecodeHtml :: StringLike a => a -> a -> Bool+qcEncodeDecodeHtml _ s = decodeHtml (encodeHtml s) == s+{-+qcEncodeDecodeHtml _ s =+ let encoded = encodeHtml s+ decoded = decodeHtml encoded+ res = decoded == s+ in trace ("en/de html: " ++ show (s, encoded, decoded)) res+-}++qcEncodeDecodeJson :: StringLike a => a -> a -> Bool+qcEncodeDecodeJson _ s = decodeJson (encodeJson s) == s+{-+qcEncodeDecodeJson _ s =+ let encoded = encodeJson s+ decoded = decodeJson encoded+ res = decoded == s+ in trace ("en/de json: " ++ show (s, encoded, decoded)) res+-}++qcEncodeDecodeUrlPairs :: StringLike a => a -> [(a, a)] -> Bool+qcEncodeDecodeUrlPairs _ s =+ let --encoded :: String+ encoded = encodeUrlPairs s+ --decoded :: [(String, String)]+ decoded = decodeUrlPairs encoded+ --in trace ("url pairs: " ++ show (s, encoded, decoded)) $ s == decoded+ in s == decoded++huQueryString :: StringLike a => a -> IO ()+huQueryString dummy = mapM_ t' [(k `asTypeOf` dummy, v)] where+ --t' :: StringLike a => (a, [(a, a)]) -> IO ()+ t' (s, p) = do+ assertEqual (SL.unpack s) s $ encodeUrlPairs p+ assertEqual (SL.unpack s) p $ decodeUrlPairs s+ k = SL.pack "foo=bar&baz=bin"+ v = map (SL.pack *** SL.pack) [("foo", "bar"), ("baz", "bin")]++huEncodeJson :: StringLike a => a -> IO ()+huEncodeJson dummy = do+ let s = SL.pack "this is just a plain string" `asTypeOf` dummy+ assertEqual "encodeJson on a plain string" s $ encodeJson s++instance Arbitrary Char where+ arbitrary = choose (32,255) >>= \n -> return (chr n)+ coarbitrary n = variant (ord n)++instance Arbitrary BS.ByteString where+ arbitrary = fmap SL.pack arbitrary+ coarbitrary = undefined++instance Arbitrary BL.ByteString where+ arbitrary = fmap SL.pack arbitrary+ coarbitrary = undefined++instance Arbitrary TS.Text where+ arbitrary = fmap SL.pack arbitrary+ coarbitrary = undefined++instance Arbitrary TL.Text where+ arbitrary = fmap SL.pack arbitrary+ coarbitrary = undefined++{- FIXME+huParsePost = t where+ content2 = BSLU.fromString $+ "--AaB03x\n" +++ "Content-Disposition: form-data; name=\"document\"; filename=\"b.txt\"\n" +++ "Content-Type: text/plain; charset=iso-8859-1\n" +++ "This is a file.\n" +++ "It has two lines.\n" +++ "--AaB03x\n" +++ "Content-Disposition: form-data; name=\"title\"\n" +++ "Content-Type: text/plain; charset=iso-8859-1\n" +++ "A File\n" +++ "--AaB03x\n" +++ "Content-Disposition: form-data; name=\"summary\"\n" +++ "Content-Type: text/plain; charset=iso-8859-1\n" +++ "This is my file\n" +++ "file test\n" +++ "--AaB03x--\n"+ t = do+ let content1 = BSLU.fromString "foo=bar&baz=bin"+ let len1 = BSLU.fromString $ show $ BS.length content1+ let ctype1 = BSLU.fromString "application/x-www-form-urlencoded"+ let result1 = parsePost ctype1 len1 content1+ assertEqual "parsing post x-www-form-urlencoded"+ ([("foo", "bar"), ("baz", "bin")], [])+ result1++ let ctype2 = BSLU.fromString "multipart/form-data; boundary=AaB03x"+ let len2 = BSLU.fromString $ show $ BS.length content2+ let result2 = parsePost ctype2 len2 content2+ let expectedsmap2 =+ [ ("title", "A File")+ , ("summary", "This is my file\nfile test")+ ]+ let expectedfile2 =+ [ ("document", "b.txt", "text/plain", BSLU.fromString $+ "This is a file.\nIt has two lines.\n") ]+ let expected2 = (expectedsmap2, expectedfile2)+ assertEqual "parsing post multipart/form-data"+ expected2+ result2+-}
Web/Encodings.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE TypeSynonymInstances #-}+{-# LANGUAGE FlexibleContexts #-} --------------------------------------------------------- -- | -- Module : Web.Encodings@@ -6,7 +6,7 @@ -- License : BSD3 -- -- Maintainer : Michael Snoyman <michael@snoyman.com>--- Stability : Unstable+-- Stability : Stable -- Portability : portable -- -- Various web encodings.@@ -20,9 +20,10 @@ , decodeUrl -- ** HTML (entity encoding) , encodeHtml--- FIXME , decodeHtml+ , decodeHtml -- ** JSON , encodeJson+ , decodeJson -- * HTTP level encoding. -- ** Query string- pairs of percentage encoding , encodeUrlPairs@@ -37,67 +38,67 @@ , formatW3 ) where -import Data.ByteString.Class-import qualified Data.ByteString.Lazy as BS-import Text.Printf (printf)-import Data.Word (Word8) import Numeric (showHex) import Data.List (isPrefixOf)-import Data.ByteString.Lazy.Util hiding (ord)-import Data.Mime.Header+import Web.Encodings.MimeHeader import Data.Maybe (fromMaybe) import Data.Time.Clock import System.Locale import Data.Time.Format +import Web.Encodings.StringLike (StringLike)+import qualified Web.Encodings.StringLike as SL+import Control.Failure+import Safe+import Data.Char (ord, isControl)+ -- | Encode all but unreserved characters with percentage encoding. -- -- Assumes use of UTF-8 character encoding.-encodeUrl :: (LazyByteString x, LazyByteString y) => x -> y-encodeUrl = fromLazyByteString- . BS.concatMap encodeUrlByte- . toLazyByteString--ord :: Integral i => Char -> i-ord = fromIntegral . fromEnum+encodeUrl :: StringLike s => s -> s+encodeUrl = SL.concatMap encodeUrlChar -encodeUrlByte :: Word8 -> BS.ByteString-encodeUrlByte w+encodeUrlChar :: Char -> String+encodeUrlChar c -- List of unreserved characters per RFC 3986 -- Gleaned from http://en.wikipedia.org/wiki/Percent-encoding- | ord 'A' <= w && w <= ord 'Z' = BS.singleton w- | ord 'a' <= w && w <= ord 'z' = BS.singleton w- | ord '0' <= w && w <= ord '9' = BS.singleton w- | ord '-' == w = BS.singleton w- | ord '_' == w = BS.singleton w- | ord '.' == w = BS.singleton w- | ord '~' == w = BS.singleton w- | otherwise = toLazyByteString $ (printf "%%%02x" w :: String)+ | 'A' <= c && c <= 'Z' = [c]+ | 'a' <= c && c <= 'z' = [c]+ | '0' <= c && c <= '9' = [c]+encodeUrlChar c@'-' = [c]+encodeUrlChar c@'_' = [c]+encodeUrlChar c@'.' = [c]+encodeUrlChar c@'~' = [c]+encodeUrlChar y =+ let (a, c) = fromEnum y `divMod` 16+ b = a `mod` 16+ showHex' x -- FIXME just use Numeric version?+ | x < 10 = toEnum $ x + (fromEnum '0')+ | x < 16 = toEnum $ x - 10 + (fromEnum 'A')+ | otherwise = error $ "Invalid argument to showHex: " ++ show x+ in ['%', showHex' b, showHex' c] -- | Decode percentage encoding. Assumes use of UTF-8 character encoding.-decodeUrl :: (LazyByteString x, LazyByteString y) => x -> y-decodeUrl = fromLazyByteString . BS.pack . decodeUrlList . BS.unpack . toLazyByteString+decodeUrl :: StringLike s => s -> s+decodeUrl s = fromMaybe s $ do+ (a, s') <- SL.uncons s+ case a of+ '%' -> do+ (b, s'') <- SL.uncons s'+ (c, s''') <- SL.uncons s''+ return $ getHex b c `SL.cons` decodeUrl s'''+ _ -> return $ a `SL.cons` decodeUrl s' -decodeUrlList :: [Word8] -> [Word8]--- note: percent sign is 37-decodeUrlList (37:x:y:rest) = (fromHex x) * 16 + (fromHex y)- : decodeUrlList rest-decodeUrlList (x:rest)- | x == 43 = 32 : decodeUrlList rest -- convert plus to space- | otherwise = x : decodeUrlList rest-decodeUrlList [] = []+getHex :: Char -> Char -> Char+getHex x y = toEnum $ (fromHex x) * 16 + fromHex y -fromHex :: Word8 -> Word8-fromHex x- | 48 <= x && x <= 57 = x - 48 -- 0 - 9- | 65 <= x && x <= 70 = x - 65 + 10 -- A - F- | 97 <= x && x <= 102 = x - 97 + 10 -- a - f- | otherwise = 0 -- FIXME+fromHex :: Char -> Int+fromHex = fromMaybe 0 . hexVal -- FIXME this fromMaybe is rather bad... -- | Escape special HTML characters.-encodeHtml :: String -> String-encodeHtml = concatMap encodeHtmlChar+encodeHtml :: StringLike s => s -> s+encodeHtml = SL.concatMap encodeHtmlChar encodeHtmlChar :: Char -> String encodeHtmlChar '<' = "<"@@ -107,87 +108,152 @@ encodeHtmlChar '\'' = "'" encodeHtmlChar c = [c] +-- | Decode HTML-encoded content into plain content.+--+-- Note: this does not support all HTML entities available. It also swallows+-- all failures.+decodeHtml :: StringLike s => s -> s+decodeHtml s = case SL.uncons s of+ Nothing -> SL.empty+ Just ('&', xs) -> fromMaybe ('&' `SL.cons` decodeHtml xs) $ do+ (before, after) <- SL.breakCharMaybe ';' xs+ c <- case SL.unpack before of -- this are small enough that unpack is ok+ "lt" -> return '<'+ "gt" -> return '>'+ "amp" -> return '&'+ "quot" -> return '"'+ '#' : 'x' : hex -> readHexChar hex+ '#' : 'X' : hex -> readHexChar hex+ '#' : dec -> readDecChar dec+ _ -> Nothing -- just to shut up a warning+ return $ c `SL.cons` decodeHtml after+ Just (x, xs) -> x `SL.cons` decodeHtml xs++readHexChar :: String -> Maybe Char+readHexChar s = helper 0 s where+ helper i "" = return $ toEnum i+ helper i (c:cs) = do+ c' <- hexVal c+ helper (i * 16 + c') cs++hexVal :: Char -> Maybe Int+hexVal c+ | '0' <= c && c <= '9' = Just $ ord c - ord '0'+ | 'A' <= c && c <= 'F' = Just $ ord c - ord 'A' + 10+ | 'a' <= c && c <= 'f' = Just $ ord c - ord 'a' + 10+ | otherwise = Nothing++readDecChar :: String -> Maybe Char+readDecChar s = do+ i <- readMay s :: Maybe Int+ return $ toEnum i+ -- | Convert into key-value pairs. Strips the leading ? if necesary.-decodeUrlPairs :: (LazyByteString x, LazyByteString y, LazyByteString z)- => x- -> [(y, z)]+decodeUrlPairs :: StringLike s+ => s+ -> [(s, s)] decodeUrlPairs = map decodeUrlPair- . BS.split (ord '&')- . BS.dropWhile (== ord '?')- . toLazyByteString+ . SL.split '&'+ . SL.dropWhile (== '?')+{-+decodeUrlPairs s = unsafePerformIO $ do+ putStrLn $ "Received: " ++ show s+ let dropped = SL.dropWhile (== '?') s+ putStrLn $ "Dropped: " ++ show dropped+ let sp = SL.split '&' dropped+ putStrLn $ "Split: " ++ show sp+ let f = filter (not . SL.null) sp+ putStrLn $ "Filtered: " ++ show f+ let res = map decodeUrlPair f+ putStrLn $ "Result: " ++ show res+ return res+-} -decodeUrlPair :: (LazyByteString a, LazyByteString b)- => BS.ByteString- -> (a, b)+decodeUrlPair :: StringLike s+ => s+ -> (s, s) decodeUrlPair b =- let (x, y) = BS.break (== ord '=') b- y' = BS.dropWhile (== ord '=') y- in (decodeUrl x, decodeUrl y')+ let (x, y) = SL.breakChar '=' b+ in (decodeUrl x, decodeUrl y) -- | Convert a list of key-values pairs into a query string. -- Does not include the question mark at the beginning.-encodeUrlPairs :: (LazyByteString x, LazyByteString y, LazyByteString z)- => [(x, y)]- -> z-encodeUrlPairs = fromLazyByteString- . BS.intercalate (BS.singleton $ ord '&')+encodeUrlPairs :: StringLike s+ => [(s, s)]+ -> s+encodeUrlPairs = SL.intercalate (SL.pack "&") . map encodeUrlPair -encodeUrlPair :: (LazyByteString x, LazyByteString y)- => (x, y)- -> BS.ByteString-encodeUrlPair (x, y) = BS.concat- [ encodeUrl x- , BS.singleton $ ord '='- , encodeUrl y- ]+encodeUrlPair :: StringLike s+ => (s, s)+ -> s+encodeUrlPair (x, y) = encodeUrl x `SL.append` ('=' `SL.cons` encodeUrl y) -- | Perform JSON-encoding on a string. Does not wrap in quotation marks.-encodeJson :: LazyByteString x => String -> x-encodeJson = fromLazyByteString . toLazyByteString . encJSString+-- Taken from json package by Sigbjorn Finne.+encodeJson :: StringLike s => s -> s+encodeJson = SL.concatMap encodeJsonChar --- | Taken from json package by Sigbjorn Finne.-encJSString :: String -> String-encJSString jss = go jss- where- go s1 =- case s1 of- (x :xs) | x < '\x20' -> '\\' : encControl x (go xs)- ('"' :xs) -> '\\' : '"' : go xs- ('\\':xs) -> '\\' : '\\' : go xs- (x :xs) -> x : go xs- "" -> ""+encodeJsonChar :: Char -> String+encodeJsonChar '\b' = "\\b"+encodeJsonChar '\f' = "\\f"+encodeJsonChar '\n' = "\\n"+encodeJsonChar '\r' = "\\r"+encodeJsonChar '\t' = "\\t"+encodeJsonChar '"' = "\\\""+encodeJsonChar '\\' = "\\\\"+encodeJsonChar c+ | not $ isControl c = [c]+ | c < '\x10' = '\\' : 'u' : '0' : '0' : '0' : hexxs+ | c < '\x100' = '\\' : 'u' : '0' : '0' : hexxs+ | c < '\x1000' = '\\' : 'u' : '0' : hexxs+ where hexxs = showHex (fromEnum c) "" -- FIXME+encodeJsonChar c = [c] - encControl x xs = case x of- '\b' -> 'b' : xs- '\f' -> 'f' : xs- '\n' -> 'n' : xs- '\r' -> 'r' : xs- '\t' -> 't' : xs- _ | x < '\x10' -> 'u' : '0' : '0' : '0' : hexxs- | x < '\x100' -> 'u' : '0' : '0' : hexxs- | x < '\x1000' -> 'u' : '0' : hexxs- | otherwise -> 'u' : hexxs- where hexxs = showHex (fromEnum x) xs+decodeJson :: StringLike s => s -> s+decodeJson s = case SL.uncons s of+ Nothing -> SL.empty+ Just ('\\', xs) -> fromMaybe ('\\' `SL.cons` decodeJson xs) $ do+ (x, xs') <- SL.uncons xs+ if x == 'u'+ then do+ (a, e) <- SL.uncons xs'+ (b, f) <- SL.uncons e+ (c, g) <- SL.uncons f+ (d, h) <- SL.uncons g+ res <- readHexChar [a, b, c, d]+ return $ res `SL.cons` decodeJson h+ else do+ c <- case x of+ 'b' -> return '\b'+ 'f' -> return '\f'+ 'n' -> return '\n'+ 'r' -> return '\r'+ 't' -> return '\t'+ '"' -> return '"'+ '\'' -> return '\''+ '\\' -> return '\\'+ _ -> Nothing+ return $ c `SL.cons` decodeJson xs'+ Just (x, xs) -> x `SL.cons` decodeJson xs -- | Information on an uploaded file.-data FileInfo = FileInfo- { fileName :: String- , fileContentType :: String- , fileContent :: BS.ByteString+data FileInfo s c = FileInfo+ { fileName :: s+ , fileContentType :: s+ , fileContent :: c }-instance Show FileInfo where- show (FileInfo fn ct _) = "FileInfo: " ++ fn ++ " (" ++ ct ++ ")"+instance Show s => Show (FileInfo s a) where+ show (FileInfo fn ct _) =+ "FileInfo: " ++ show fn ++ " (" ++ show ct ++ ")" -- | Parse a multipart form into parameters and files.-parseMultipart :: LazyByteString lbs- => lbs -- ^ boundary- -> BS.ByteString -- ^ content- -> ([(String, String)], [(String, FileInfo)])-parseMultipart boundary' content =- let boundary :: String- boundary = fromLazyByteString $ toLazyByteString boundary'- pieces = getPieces boundary content+parseMultipart :: StringLike s+ => String -- ^ boundary+ -> s -- ^ content+ -> ([(s, s)], [(s, FileInfo s s)])+parseMultipart boundary content =+ let pieces = getPieces boundary content getJusts [] = [] getJusts (Nothing:rest) = getJusts rest getJusts ((Just x):rest) = x : getJusts rest@@ -201,24 +267,30 @@ in (getLefts pieces', getRights pieces') -- | Parse a single segment of a multipart/form-data POST.-parsePiece :: Monad m- => BS.ByteString- -> m (Either (String, String) (String, FileInfo))+parsePiece :: (StringLike s, MonadFailure (AttributeNotFound s) m)+ => s+ -> m (Either (s, s) (s, FileInfo s s)) parsePiece b = do- let (headers', content) = takeUntilBlank b+ let (headers', content) = SL.takeUntilBlank b headers = map parseHeader headers'- name <- lookupHeaderAttr "Content-Disposition" "name" headers- let filename = lookupHeaderAttr "Content-Disposition" "filename" headers- let ctype = fromMaybe "" $ lookupHeader "Content-Type" headers+ name <- lookupHeaderAttr (SL.pack "Content-Disposition")+ (SL.pack "name")+ headers+ let filename = lookupHeaderAttr (SL.pack "Content-Disposition")+ (SL.pack "filename")+ headers+ let ctype = fromMaybe SL.empty $ lookupHeader (SL.pack "Content-Type")+ headers -- charset = lookupHeaderAttr "Content-Type" "charset" headers return $ case filename of- Nothing -> Left (name, (fromLazyByteString $ chompBS content))+ Nothing -> Left (name, SL.chomp content) Just f -> Right (name, FileInfo f ctype content) -- | Split up a bytestring along the given boundary.-getPieces :: String -- ^ boundary- -> BS.ByteString -- ^ content- -> [BS.ByteString]+getPieces :: StringLike s+ => String -- ^ boundary+ -> s -- ^ content+ -> [s] {- FIXME this would be nice... getPieces b c = let fullBound = ord '-' `BS.cons'` (ord '-' `BS.cons'` b)@@ -227,41 +299,41 @@ filter (not . BS.null) $ map chompBS pieces -}-getPieces b c- | BS.null c = []- | otherwise =- let fullBound = toLazyByteString ('-':'-':b)- (next, rest) = breakAtString fullBound c+getPieces _ c | SL.null c = []+getPieces b c =+ let fullBound = SL.pack $ '-' `SL.cons` ('-' `SL.cons` b)+ (next, rest) = SL.breakString fullBound c rest' = checkRest rest rest'' = getPieces b rest'- in if BS.null next then rest'' else chompBS next : rest''+ in if SL.null next then rest'' else SL.chomp next : rest'' where- br = ord '\r'- bn = ord '\n'- dash = ord '-'+ br = '\r'+ bn = '\n'+ dash = '-' checkRest bs- | BS.length bs < 2 = BS.empty- | BS.head bs == bn = BS.tail bs- | BS.head bs == br && BS.head (BS.tail bs) == bn =- BS.tail $ BS.tail bs- | BS.head bs == dash && BS.head (BS.tail bs) == dash = BS.empty- | otherwise = BS.empty -- FIXME+ | SL.lengthLT 2 bs = SL.empty+ | SL.head bs == bn = SL.tail bs+ | SL.head bs == br && SL.head (SL.tail bs) == bn =+ SL.tail $ SL.tail bs+ | SL.head bs == dash && SL.head (SL.tail bs) == dash = SL.empty+ | otherwise = SL.empty -- FIXME -- | Parse a post request. This function determines the correct decoding -- function to use.-parsePost :: String -- ^ content type+parsePost :: StringLike s+ => String -- ^ content type -> String -- ^ content length- -> BS.ByteString -- ^ body of the post- -> ([(String, String)], [(String, FileInfo)])+ -> s -- ^ body of the post+ -> ([(s, s)], [(s, FileInfo s s)]) parsePost ctype clength body- | urlenc `isPrefixOf` ctype = (decodeUrlPairs content, [])+ | urlenc `SL.isPrefixOf` ctype = (decodeUrlPairs content, []) | formBound `isPrefixOf` ctype = parseMultipart boundProcessed content | otherwise = ([], []) where len = case reads clength of ((x, _):_) -> x [] -> 0- content = BS.take len body+ content = SL.take len body urlenc = "application/x-www-form-urlencoded" formBound = "multipart/form-data; boundary=" boundProcessed = drop (length formBound) ctype
+ Web/Encodings/ListHelper.hs view
@@ -0,0 +1,4 @@+module Web.Encodings.ListHelper (cons) where++cons :: a -> [a] -> [a]+cons = (:)
+ Web/Encodings/MimeHeader.hs view
@@ -0,0 +1,82 @@+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE FlexibleContexts #-}+---------------------------------------------------------+-- Copyright : Michael Snoyman+-- License : BSD3+-- Maintainer : Michael Snoyman <michael@snoyman.com>+---------------------------------------------------------++-- | Functions for parsing MIME headers (Key: value; k1=v1; k2=v2)+module Web.Encodings.MimeHeader+ ( Header+ , parseHeader+ , lookupHeader+ , lookupHeaderAttr+ , AttributeNotFound (..)+ , HeaderNotFound (..)+ ) where++--import Data.String.Util+import qualified Web.Encodings.StringLike as SL+import Web.Encodings.StringLike (StringLike)+import Control.Failure+import Control.Exception (Exception)+import Data.Typeable (Typeable)++type SomeMap a = [(a, a)]++-- | A single MIME header.+--type Header = (B8.ByteString, B8.ByteString, BSMap)+type Header a = (a, a, SomeMap a)++-- | Parse a header line in the format:+--+-- Name: value; attkey=attval; attkey2=attval2.+parseHeader :: StringLike a => a -> Header a+parseHeader s =+ let (k, rest) = SL.span (/= ':') s+ (v, attrs) = parseRest rest+ in (k, v, attrs) where+ parseRest :: StringLike a => a -> (a, SomeMap a)+ parseRest s' =+ let (v, rest) = SL.span (/= ';') s'+ attrs = parseAttrs rest+ in (SL.dropPrefix' (SL.pack ": ") v, attrs)+ parseAttrs :: StringLike a => a -> SomeMap a+ parseAttrs a | SL.null a = []+ parseAttrs a =+ let s' = SL.dropPrefix' (SL.pack "; ") a+ (next, rest) = SL.span (/= ';') s'+ (k, v) = SL.span (/= '=') next+ v' = SL.dropPrefix' (SL.pack "=") v+ v'' = SL.dropQuotes v'+ in (k, v'') : parseAttrs rest++data AttributeNotFound s = AttributeNotFound s s+ deriving (Show, Typeable)+instance (Typeable s, Show s) => Exception (AttributeNotFound s)+lookupHeaderAttr :: ( MonadFailure (AttributeNotFound s) m, StringLike s+ , Eq s)+ => s+ -> s+ -> [Header s]+ -> m s+lookupHeaderAttr k1 k2 [] = failure $ AttributeNotFound k1 k2+lookupHeaderAttr k1 k2 ((key, _, vals):rest)+ | k1 == key = case lookup k2 vals of+ Nothing -> failure $ AttributeNotFound k1 k2+ Just v -> return v+ | otherwise = lookupHeaderAttr k1 k2 rest++data HeaderNotFound s = HeaderNotFound s+ deriving (Typeable, Show)+instance (Show s, Typeable s) => Exception (HeaderNotFound s)++lookupHeader :: (StringLike s, MonadFailure (HeaderNotFound s) m, Eq s)+ => s+ -> [Header s]+ -> m s+lookupHeader k [] = failure $ HeaderNotFound k+lookupHeader k ((key, val, _):rest)+ | k == key = return val+ | otherwise = lookupHeader k rest
+ Web/Encodings/StringLike.hs view
@@ -0,0 +1,205 @@+{-# LANGUAGE FlexibleInstances #-}+module Web.Encodings.StringLike+ ( StringLike (..)+ ) where++import Prelude (Char, Bool (..), String, Int, Eq (..), Show, ($), (.),+ (<=), (-), otherwise, Maybe (..), (&&), not)+import qualified Prelude as P+import qualified Data.List as L+import qualified Web.Encodings.ListHelper as LH+import qualified Data.ByteString.Char8 as BS+import qualified Data.ByteString.Lazy.Char8 as BL+import qualified Data.Monoid as M+import qualified Data.Text as TS+import qualified Data.Text.Lazy as TL+import Data.Maybe (fromMaybe)++class (Eq a, Show a) => StringLike a where+ span :: (Char -> Bool) -> a -> (a, a)+ null :: a -> Bool+ concatMap :: (Char -> String) -> a -> a+ dropWhile :: (Char -> Bool) -> a -> a+ break :: (Char -> Bool) -> a -> (a, a)+ cons :: Char -> a -> a+ uncons :: a -> Maybe (Char, a)+ append :: a -> a -> a+ intercalate :: a -> [a] -> a+ isPrefixOf :: a -> a -> Bool+ take :: Int -> a -> a+ head :: a -> Char+ tail :: a -> a+ init :: a -> a+ last :: a -> Char+ empty :: a++ pack :: String -> a+ unpack :: a -> String++ dropPrefix :: a -> a -> Maybe a+ dropPrefix porig sorig = helper porig sorig where+ helper p s+ | null p && null s = Just empty+ | null p = Just s+ | null s = Nothing+ | head p == head s = helper (tail p) (tail s)+ | otherwise = Nothing+ dropPrefix' :: a -> a -> a+ dropPrefix' p c = case dropPrefix p c of+ Just x -> x+ Nothing -> c+ dropQuotes :: a -> a+ dropQuotes s+ | lengthGE 2 s && head s == '"' && last s == '"' = tail $ init s+ | otherwise = s+ chomp :: a -> a+ chomp s | null s = s+ chomp s = case last s of+ '\n' -> chomp $ init s+ '\r' -> chomp $ init s+ _ -> s+ split :: Char -> a -> [a]+ split c s =+ let (next, rest) = breakChar c s+ in if null next+ then (if null rest then [] else [rest])+ else next : split c rest+ breakCharMaybe :: Char -> a -> Maybe (a, a)+ breakCharMaybe c s+ | null s = Nothing+ | c == head s = Just (empty, tail s)+ | otherwise = do+ (next, rest) <- breakCharMaybe c (tail s)+ Just (cons (head s) next, rest)+ breakChar :: Char -> a -> (a, a)+ breakChar c s = fromMaybe (s, empty) $ breakCharMaybe c s+ breakString :: a -> a -> (a, a)+ breakString _ c | null c = (empty, empty)+ breakString p c = case dropPrefix p c of+ Just x -> (empty, x)+ Nothing ->+ let x = head c+ xs = tail c+ (next, rest) = breakString p xs+ in (cons x next, rest)+ takeLine :: a -> (a, a)+ takeLine a =+ let (x, y) = breakChar '\n' a+ x' = chomp x+ in (x', y)+ takeUntilBlank :: a -> ([a], a)+ takeUntilBlank a =+ let (next, rest) = takeLine a+ in if null next+ then ([], rest)+ else let (nexts, rest') = takeUntilBlank rest+ in (next : nexts, rest')++ lengthLT :: Int -> a -> Bool+ lengthLT i _ | i <= 0 = False+ lengthLT i a+ | null a = True+ | otherwise = lengthLT (i - 1) $ tail a+ lengthGE :: Int -> a -> Bool+ lengthGE i = not . lengthLT i++instance StringLike [Char] where+ intercalate = L.intercalate+ null = P.null+ concatMap = P.concatMap+ tail = P.tail+ head = P.head+ cons = LH.cons+ uncons [] = Nothing+ uncons (x:xs) = Just (x, xs)+ span = P.span+ dropWhile = P.dropWhile+ break = P.break+ append = M.mappend+ isPrefixOf = L.isPrefixOf+ take = P.take+ empty = M.mempty+ pack = P.id+ unpack = P.id+ init = P.init+ last = P.last++instance StringLike BS.ByteString where+ span = BS.span+ null = BS.null+ concatMap f = BS.concatMap $ pack . f+ dropWhile = BS.dropWhile+ break = BS.break+ cons = BS.cons+ uncons = BS.uncons+ append = BS.append+ intercalate = BS.intercalate+ isPrefixOf = BS.isPrefixOf+ take = BS.take+ head = BS.head+ tail = BS.tail+ empty = BS.empty+ pack = BS.pack+ unpack = BS.unpack+ init = BS.init+ last = BS.last++instance StringLike BL.ByteString where+ span = BL.span+ null = BL.null+ concatMap f = BL.concatMap $ pack . f+ dropWhile = BL.dropWhile+ break = BL.break+ cons = BL.cons+ uncons = BL.uncons+ append = BL.append+ intercalate = BL.intercalate+ isPrefixOf = BL.isPrefixOf+ take i = BL.take $ P.fromIntegral i+ head = BL.head+ tail = BL.tail+ empty = BL.empty+ pack = BL.pack+ unpack = BL.unpack+ init = BL.init+ last = BL.last++instance StringLike TS.Text where+ span = TS.spanBy+ null = TS.null+ concatMap f = TS.concatMap $ pack . f+ dropWhile = TS.dropWhile+ break = TS.breakBy+ cons = TS.cons+ uncons = TS.uncons+ append = TS.append+ intercalate = TS.intercalate+ isPrefixOf = TS.isPrefixOf+ take i = TS.take $ P.fromIntegral i+ head = TS.head+ tail = TS.tail+ empty = TS.empty+ pack = TS.pack+ unpack = TS.unpack+ init = TS.init+ last = TS.last++instance StringLike TL.Text where+ span = TL.spanBy+ null = TL.null+ concatMap f = TL.concatMap $ pack . f+ dropWhile = TL.dropWhile+ break = TL.breakBy+ cons = TL.cons+ uncons = TL.uncons+ append = TL.append+ intercalate = TL.intercalate+ isPrefixOf = TL.isPrefixOf+ take i = TL.take $ P.fromIntegral i+ head = TL.head+ tail = TL.tail+ empty = TL.empty+ pack = TL.pack+ unpack = TL.unpack+ init = TL.init+ last = TL.last
web-encodings.cabal view
@@ -1,5 +1,5 @@ name: web-encodings-version: 0.0.1+version: 0.2.0 license: BSD3 license-file: LICENSE author: Michael Snoyman <michael@snoyman.com>@@ -12,14 +12,38 @@ build-type: Simple homepage: http://github.com/snoyberg/web-encodings/tree/master +flag buildtests+ description: Build the executable to run unit tests+ default: False+ library+ if flag(buildtests)+ Buildable: False+ else+ Buildable: True build-depends: base >=4 && <5,- bytestring,- bytestring-class,- time >= 1.1,- old-locale+ time >= 1.1.2.4 && < 1.2,+ old-locale >= 1.0.0.1 && < 1.1,+ bytestring >= 0.9.1.4 && < 0.10,+ text >= 0.5 && < 0.6,+ failure >= 0.0.0 && < 0.1,+ safe >= 0.2 && < 0.3 exposed-modules: Web.Encodings- other-modules: Data.Mime.Header,- Data.ByteString.Lazy.Util,- Data.String.Util+ Web.Encodings.MimeHeader,+ Web.Encodings.StringLike,+ Web.Encodings.ListHelper ghc-options: -Wall++executable runtests+ if flag(buildtests)+ Buildable: True+ build-depends: test-framework,+ test-framework-quickcheck,+ test-framework-hunit,+ HUnit,+ QuickCheck >= 1 && < 2,+ convertible >= 1.2.0 && < 1.3+ else+ Buildable: False+ ghc-options: -Wall+ main-is: Test.hs