packages feed

irc-dcc (empty) → 1.0.0

raw patch · 9 files changed

+731/−0 lines, 9 filesdep +attoparsecdep +basedep +binarysetup-changed

Dependencies added: attoparsec, base, binary, bytestring, errors, hspec-attoparsec, io-streams, iproute, irc-ctcp, network, path, tasty, tasty-hspec, transformers, utf8-string

Files

+ LICENSE view
@@ -0,0 +1,21 @@+The MIT License (MIT)++Copyright (c) 2016 Jan Gerlinger <git@jangerlinger.de>++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:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++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.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ irc-dcc.cabal view
@@ -0,0 +1,66 @@+name:                 irc-dcc+version:              1.0.0+synopsis:             A DCC message parsing and helper library for IRC clients+description:          DCC (Direct Client-to-Client) is an IRC sub-protocol for+                      establishing and maintaining direct connections to+                      exchange messages and files.+                      .+                      See <http://www.irchelp.org/irchelp/rfc/ctcpspec.html> for+                      more details.+license:              MIT+license-file:         LICENSE+homepage:             https://github.com/JanGe/irc-dcc+bug-reports:          https://github.com/JanGe/irc-dcc/issues+author:               Jan Gerlinger+maintainer:           git@jangerlinger.de+category:             Network+-- copyright:+build-type:           Simple+cabal-version:        >=1.10++library+  hs-source-dirs:       src+  exposed-modules:      Network.IRC.DCC+                      , Network.IRC.DCC.FileTransfer+  other-modules:        Network.IRC.DCC.Internal+                      , Network.Socket.ByteString.Extended+  -- other-extensions:+  build-depends:        base >= 4.7 && < 5+                      , attoparsec+                      , binary+                      , bytestring+                      , errors+                      , io-streams+                      , iproute+                      , irc-ctcp+                      , network+                      , path+                      , transformers+                      , utf8-string+  default-language:     Haskell2010+  ghc-options:          -Wall -fno-warn-unused-do-bind++test-suite tests+  hs-source-dirs:       tests+                      , src+  main-is:              Main.hs+  other-modules:        Network.IRC.DCCTest+                      , Network.IRC.DCC.Internal+  type:                 exitcode-stdio-1.0+  build-depends:        base >= 4.7 && < 5+                      , tasty+                      , tasty-hspec+                      , hspec-attoparsec+                      , attoparsec+                      , binary+                      , bytestring+                      , iproute+                      , irc-ctcp+                      , network+                      , path+                      , utf8-string+  default-language:     Haskell2010++source-repository head+  type:     git+  location: https://github.com/JanGe/irc-dcc.git
+ src/Network/IRC/DCC.hs view
@@ -0,0 +1,30 @@+module Network.IRC.DCC (+  -- * Types+  -- ** DCC service+    CtcpCommand(..)+  , Service(..)+  -- ** Messaging commands (DCC CHAT)+  , OpenChat(..)+  , CloseChat(..)+  -- ** File Transfer commands (DCC SEND)+  , OfferFile(..)+  , TryResumeFile(..)+  , AcceptResumeFile(..)+  , OfferFileSink(..)+  -- *** Helper Types+  , TransferType(..)+  , FileMetadata(..)+  , Token(..)+  , FileOffset+  -- * DCC command parsing+  , runParser+  , parseService+  , parseOpenChat+  , parseCloseChat+  , parseOfferFile+  , parseTryResumeFile+  , parseAcceptResumeFile+  , parseOfferFileSink+  ) where++import Network.IRC.DCC.Internal
+ src/Network/IRC/DCC/FileTransfer.hs view
@@ -0,0 +1,79 @@+{-| Functions for receiving files.++    Each chunk is acknowledged by sending the total sum of bytes received for a+    file. See the+    <http://www.irchelp.org/irchelp/rfc/ctcpspec.html CTCP specification>.+-}+module Network.IRC.DCC.FileTransfer+  ( acceptFile+  , resumeFile+  ) where++import           Network.IRC.DCC.Internal++import           Control.Error+import           Control.Monad                      (unless)+import           Data.ByteString.Char8              (ByteString, length, null)+import           Network.Socket.ByteString.Extended+import           Path                               (File, Path, Rel,+                                                     fromRelFile)+import           Prelude                            hiding (length, null)+import           System.IO                          (BufferMode (NoBuffering), IOMode (WriteMode, AppendMode))+import           System.IO.Streams                  (OutputStream,+                                                     withFileAsOutputExt, write)++-- | Accept a DCC file offer+acceptFile :: Integral a+           => OfferFile+           -> (PortNumber -> ExceptT String IO ())+           -- ^ Callback when socket is ready+           -> (a -> IO ())+           -- ^ Callback when a chunk of data was transfered+           -> ExceptT String IO ()+acceptFile (OfferFile tt f) =+    download (fileName f) WriteMode 0 tt++-- | Accept a DCC file offer for a partially downloaded file+resumeFile :: Integral a+           => AcceptResumeFile+           -> (PortNumber -> ExceptT String IO ())+           -- ^ Callback when socket is ready+           -> (a -> IO ())+           -- ^ Callback when a chunk of data was transfered+           -> ExceptT String IO ()+resumeFile (AcceptResumeFile tt f pos) =+    download (fileName f) AppendMode (fromIntegral pos) tt++download :: Integral a+         => Path Rel File+         -> IOMode+         -> a+         -> TransferType+         -> (PortNumber -> ExceptT String IO ())+         -> (a -> IO ())+         -> ExceptT String IO ()+download fn mode pos tt onListen onChunk =+    withSocket tt onListen $+        toFile (fromRelFile fn) . stream pos onChunk+  where withSocket (Active i p) = withActiveSocket i p+        withSocket (Passive i _) = withPassiveSocket i+        toFile f = withFileAsOutputExt f mode NoBuffering++stream :: Integral a+       => a+       -> (a -> IO ())+       -> Socket+       -> OutputStream ByteString+       -> IO ()+stream pos onChunk sock h =+  do buf <- recv sock (4096 * 1024)+     unless (null buf) $+            do let l = fromIntegral (length buf)+               onChunk l+               let pos' = pos + l+               sendPosition sock pos'+               Just buf `write` h+               stream pos' onChunk sock h++sendPosition :: Integral a => Socket -> a -> IO ()+sendPosition sock = sendAll sock . toNetworkByteOrder
+ src/Network/IRC/DCC/Internal.hs view
@@ -0,0 +1,332 @@+{-# LANGUAGE OverloadedStrings #-}++module Network.IRC.DCC.Internal where++import           Control.Applicative+import           Control.Monad                    (when)+import           Data.Attoparsec.ByteString.Char8+import           Data.Binary                      (byteSwap32)+import           Data.ByteString.Char8            (ByteString, pack)+import qualified Data.ByteString.UTF8             as UTF8 (toString)+import           Data.IP                          (IPv4, fromHostAddress,+                                                   toHostAddress)+import           Data.Monoid                      ((<>))+import           Data.Word                        (Word64)+import           Network.IRC.CTCP                 (CTCPByteString, decodeCTCP,+                                                   encodeCTCP)+import           Network.Socket                   (PortNumber)+import           Path                             (File, Path, Rel, filename,+                                                   fromRelFile, parseAbsFile,+                                                   parseRelFile)++-- | Type of DCC service offered+data Service+  -- | Offer chat session+  = Messaging OpenChat+  -- | Offer file transfer+  | FileTransfer OfferFile+  deriving (Eq, Show)++-- | Type of DCC chat to open+data OpenChat+  -- | Text messages exchange+  = Chat IPv4 PortNumber+  -- | Drawing commands exchange+  | Whiteboard IPv4 PortNumber+  deriving (Eq, Show)++-- | Signal intent to close DCC chat connection+data CloseChat = CloseChat++-- | DCC file transfer instructions+data OfferFile = OfferFile TransferType FileMetadata+  deriving (Eq, Show)++-- | Signal intent to resume DCC file transfer at specific position+data TryResumeFile = TryResumeFile TransferType FileMetadata FileOffset+  deriving (Eq, Show)++-- | Signal acceptance to resume DCC file transfer at specific position+data AcceptResumeFile = AcceptResumeFile TransferType FileMetadata FileOffset+  deriving (Eq, Show)++-- | Signal readiness to accept a connection+data OfferFileSink = OfferFileSink Token FileMetadata IPv4 PortNumber+  deriving (Eq, Show)++-- | Type of a DCC file transfer connection+data TransferType+  -- | Connection where the owner of the file offers a socket to connect to+  = Active IPv4 PortNumber+  -- | Connection where the recipient of the file offers a socket to connect to+  | Passive IPv4 Token+  deriving (Eq, Show)++-- | Properties of a file+data FileMetadata = FileMetadata { fileName :: Path Rel File+                                 , fileSize :: Maybe FileOffset }+  deriving (Eq, Show)++-- | An identifier for knowing which negotiation a request belongs to+data Token = Token ByteString+  deriving (Eq, Show)++type FileOffset = Word64++-- | Class for types that can be sent as CTCP commands+class CtcpCommand a where+    encodeCtcp :: a -> CTCPByteString++instance CtcpCommand ByteString where+    encodeCtcp = encodeCTCP++instance CtcpCommand OpenChat where+    encodeCtcp = encodeCtcp . encodeOpenChat++instance CtcpCommand CloseChat where+    encodeCtcp = encodeCtcp . encodeChatClose++instance CtcpCommand OfferFile where+    encodeCtcp = encodeCtcp . encodeOfferFile++instance CtcpCommand TryResumeFile where+    encodeCtcp = encodeCtcp . encodeTryResumeFile++instance CtcpCommand AcceptResumeFile where+    encodeCtcp = encodeCtcp . encodeAcceptResume++instance CtcpCommand OfferFileSink where+    encodeCtcp = encodeCtcp . encodeOfferFileSink++runParser :: Parser a -> CTCPByteString -> Either String a+runParser p = parseOnly p . decodeCTCP++parseService :: Parser Service+parseService = Messaging <$> parseOpenChat+           <|> FileTransfer <$> parseOfferFile++encodeService :: Service -> ByteString+encodeService (Messaging m) = encodeOpenChat m+encodeService (FileTransfer o) = encodeOfferFile o++parseOpenChat :: Parser OpenChat+parseOpenChat = choice [ do "DCC CHAT chat "+                            (i, p) <- parseSocket+                            endOfInput+                            return $ Chat i p+                       , do "DCC CHAT wboard "+                            (i, p) <- parseSocket+                            endOfInput+                            return $ Whiteboard i p+                       ]++encodeOpenChat :: OpenChat -> ByteString+encodeOpenChat (Chat i p) = "DCC CHAT chat " <> encodeSocket (i, p)+encodeOpenChat (Whiteboard i p) = "DCC CHAT wboard " <> encodeSocket (i, p)++parseCloseChat :: Parser CloseChat+parseCloseChat = do "DCC CLOSE"+                    endOfInput+                    return CloseChat++encodeChatClose :: CloseChat -> ByteString+encodeChatClose _ = "DCC CLOSE"++parseOfferFile :: Parser OfferFile+parseOfferFile =+    do "DCC SEND "+       fn <- parseFileName+       space+       i <- parseIpBigEndian+       space+       choice [ do p <- parseTcpPort+                   fs <- Just <$> (space *> parseFileOffset)+                     <|> return Nothing+                   endOfInput+                   return (OfferFile+                              (Active i p)+                              (FileMetadata fn fs))+              , do "0"+                   space+                   fs <- parseFileOffset+                   space+                   t <- parseToken+                   endOfInput+                   return (OfferFile+                              (Passive i t)+                              (FileMetadata fn (Just fs)))+              ]++encodeOfferFile :: OfferFile -> ByteString+encodeOfferFile (OfferFile (Active i p) (FileMetadata fn fs)) =+    "DCC SEND "+ <> encodeFileName fn+ <> " " <> encodeIpBigEndian i+ <> " " <> encodeTcpPort p+ <> appendSpacedIfJust (encodeFileOffset <$> fs)+encodeOfferFile (OfferFile (Passive i t) (FileMetadata fn fs)) =+    "DCC SEND "+ <> encodeFileName fn+ <> " " <> encodeIpBigEndian i+ <> " 0"+ <> appendSpacedIfJust (encodeFileOffset <$> fs)+ <> " " <> encodeToken t++parseTryResumeFile :: OfferFile -> Parser TryResumeFile+parseTryResumeFile (OfferFile tt f) =+    do "DCC RESUME"+       space+       fn' <- parseFileName+       when (fn' /= fileName f) $+         fail "File name for resume didn't match file name in offer."+       space+       pos <- case tt of+                Active _ p -> do+                    string (encodeTcpPort p)+                    space+                    parseFileOffset+                Passive _ t -> do+                    "0"+                    space+                    pos <- parseFileOffset+                    space+                    string (encodeToken t)+                    return pos+       endOfInput+       return (TryResumeFile tt f pos)+++encodeTryResumeFile :: TryResumeFile -> ByteString+encodeTryResumeFile (TryResumeFile (Active _ p) (FileMetadata fn _) pos) =+    "DCC RESUME "+ <> encodeFileName fn+ <> " " <> encodeTcpPort p+ <> " " <> encodeFileOffset pos+encodeTryResumeFile (TryResumeFile (Passive _ t) (FileMetadata fn _) pos) =+    "DCC RESUME "+ <> encodeFileName fn+ <> " 0 "+ <> encodeFileOffset pos+ <> " " <> encodeToken t++parseAcceptResumeFile :: TryResumeFile -> Parser AcceptResumeFile+parseAcceptResumeFile (TryResumeFile tt f _) =+    do "DCC ACCEPT "+       fn' <- parseFileName+       when (fn' /= fileName f) $+         fail "File name for accepting resume didn't match file name in offer."+       space+       ackPos <- case tt of+                   Active _ p -> do+                       string (encodeTcpPort p)+                       space+                       parseFileOffset+                   Passive _ t -> do+                       "0"+                       space+                       ackPos <- parseFileOffset+                       space+                       string (encodeToken t)+                       return ackPos+       endOfInput+       return (AcceptResumeFile tt f ackPos)++encodeAcceptResume :: AcceptResumeFile -> ByteString+encodeAcceptResume (AcceptResumeFile (Active _ p) (FileMetadata fn _) pos) =+    "DCC ACCEPT "+ <> encodeFileName fn+ <> " " <> encodeTcpPort p+ <> " " <> encodeFileOffset pos+encodeAcceptResume (AcceptResumeFile (Passive _ t) (FileMetadata fn _) pos) =+    "DCC ACCEPT "+ <> encodeFileName fn+ <> " 0 "+ <> encodeFileOffset pos+ <> " " <> encodeToken t++parseOfferFileSink :: AcceptResumeFile -> Parser (Maybe OfferFileSink)+parseOfferFileSink (AcceptResumeFile (Passive _ t) f _) =+    do "DCC SEND "+       fn <- parseFileName+       when (fn /= fileName f) $+         fail "File name for accepting resume didn't match file name in offer."+       space+       (i, p) <- parseSocket+       string (appendSpacedIfJust (encodeFileOffset <$> fileSize f))+       space+       string (encodeToken t)+       endOfInput+       return (Just (OfferFileSink t f i p))+parseOfferFileSink _ = return Nothing++encodeOfferFileSink :: OfferFileSink -> ByteString+encodeOfferFileSink (OfferFileSink t (FileMetadata fn fs) i p) =+    "DCC SEND "+ <> encodeFileName fn+ <> " " <> encodeSocket (i, p)+ <> appendSpacedIfJust (encodeFileOffset <$> fs)+ <> " " <> encodeToken t++parseSocket :: Parser (IPv4, PortNumber)+parseSocket =+    do i <- parseIpBigEndian+       space+       p <- parseTcpPort+       return (i, p)++encodeSocket :: (IPv4, PortNumber) -> ByteString+encodeSocket (i, p) = encodeIpBigEndian i <> " " <> encodeTcpPort p++parseFileName :: Parser (Path Rel File)+parseFileName = do name <- "\"" *> takeWhile1 (/= '"') <* "\""+                       <|> takeWhile1 (/= ' ')+                   case parseRelOrAbs (UTF8.toString name) of+                     Nothing -> fail "Could not parse file name."+                     Just path -> return path+  where parseRelOrAbs n = parseRelFile n+                      <|> filename <$> parseAbsFile n++encodeFileName :: Path Rel File -> ByteString+encodeFileName = pack . fromRelFile++parseIpBigEndian :: Parser IPv4+parseIpBigEndian = fromBigEndianIp <$> parseBoundedInteger 0 4294967295++encodeIpBigEndian :: IPv4 -> ByteString+encodeIpBigEndian = pack . show . toBigEndianIp++parseTcpPort :: Parser PortNumber+parseTcpPort = fromInteger <$> parseBoundedInteger 1 65535++encodeTcpPort :: PortNumber -> ByteString+encodeTcpPort = pack . show++parseFileOffset :: Parser FileOffset+parseFileOffset = decimal++encodeFileOffset :: FileOffset -> ByteString+encodeFileOffset = pack . show++parseToken :: Parser Token+parseToken = Token <$> takeByteString++encodeToken :: Token -> ByteString+encodeToken (Token t) = t++parseBoundedInteger :: Integer -> Integer -> Parser Integer+parseBoundedInteger lower upper = do+    num <- decimal+    when (num < lower || num > upper) $+         fail ("Failed to parse " ++ show num ++ ", not in range [" +++               show lower ++ ", " ++ show upper ++ "].")+    return num++appendSpacedIfJust :: Maybe ByteString -> ByteString+appendSpacedIfJust (Just x) = " " <> x+appendSpacedIfJust Nothing = mempty++fromBigEndianIp :: Integer -> IPv4+fromBigEndianIp = fromHostAddress . byteSwap32 . fromIntegral++toBigEndianIp :: IPv4 -> Integer+toBigEndianIp = fromIntegral . byteSwap32 . toHostAddress
+ src/Network/Socket/ByteString/Extended.hs view
@@ -0,0 +1,70 @@+-- | Common functions simplyfing the use of "Network.Socket.ByteString"+module Network.Socket.ByteString.Extended+  ( module Network.Socket+  , module Network.Socket.ByteString+  , withActiveSocket+  , withPassiveSocket+  , toNetworkByteOrder+  ) where++import           Control.Error+import           Control.Monad.IO.Class     (liftIO)+import           Data.Binary.Put            (putWord32be, runPut)+import           Data.ByteString.Char8      (ByteString)+import qualified Data.ByteString.Lazy.Char8 as Lazy (toStrict)+import           Data.IP                    (IPv4, fromHostAddress,+                                             toHostAddress)+import           Network.Socket             hiding (recv, recvFrom, send,+                                             sendTo)+import           Network.Socket.ByteString+import           System.Timeout++-- | Run functions on socket when connected and close socket afterwards+withActiveSocket :: IPv4+                 -> PortNumber+                 -> (PortNumber -> ExceptT String IO ())+                 -- ^ Callback when socket is ready+                 -> (Socket -> IO ())+                 -- ^ Callback when socket is connected to server+                 -> ExceptT String IO ()+withActiveSocket i p onListen onConnected = do+    liftIO $ return withSocketsDo+    sock <- liftIO $ socket AF_INET Stream defaultProtocol+    onListen p+    liftIO $ connect sock (SockAddrInet p (toHostAddress i))+    liftIO $ onConnected sock+    liftIO $ sClose sock++{- | Run functions on passive socket when listening and when connected and close+     socket afterwards.+-}+withPassiveSocket :: IPv4+                  -> (PortNumber -> ExceptT String IO ())+                  -- ^ Callback when socket is open and listening+                  -> (Socket -> IO ())+                  -- ^ Callback when client connected to socket+                  -> ExceptT String IO ()+withPassiveSocket i onListen onConnected = do+    liftIO $ return withSocketsDo+    sock <- liftIO $ socket AF_INET Stream defaultProtocol+    liftIO $ bind sock (SockAddrInet aNY_PORT iNADDR_ANY)+    liftIO $ listen sock 1+    p <- liftIO $ socketPort sock+    onListen p+    accepted <- liftIO $ timeout 10000000 $ accept sock+    case accepted of+      Just (con, SockAddrInet _ client)+        | client == toHostAddress i -> liftIO $ do onConnected con+                                                   sClose con+        | otherwise -> do liftIO $ sClose con+                          throwE ( "Expected connection from host "+                                ++ show (fromHostAddress client)+                                ++ ", not from " ++ show i ++". Aborting…\n" )++      _ -> throwE ( "Timeout when waiting for other party to connect on port "+                 ++ show p ++ "…\n")+    liftIO $ sClose sock++-- | Converts numbers to a '32bit unsigned int' in network byte order.+toNetworkByteOrder :: Integral a => a -> ByteString+toNetworkByteOrder = Lazy.toStrict . runPut . putWord32be . fromIntegral
+ tests/Main.hs view
@@ -0,0 +1,10 @@+module Main where++import qualified Network.IRC.DCCTest as DCC+import           Test.Tasty+import           Test.Tasty.Hspec++main = tests++tests :: IO ()+tests = DCC.spec >>= defaultMain
+ tests/Network/IRC/DCCTest.hs view
@@ -0,0 +1,121 @@+{-# LANGUAGE TemplateHaskell #-}++module Network.IRC.DCCTest where++import           Network.IRC.DCC.Internal++import           Data.Attoparsec.ByteString.Char8 (IResult (Done, Fail), Parser,+                                                   endOfInput, maybeResult,+                                                   parse)+import           Data.ByteString.Char8            (pack)+import qualified Data.ByteString.UTF8             as UTF8 (fromString)+import           Data.IP                          (IPv4, toIPv4)+import           Path                             (mkRelFile)+import           Test.Hspec.Attoparsec+import           Test.Tasty                       (testGroup)+import           Test.Tasty.Hspec                 (SpecWith, describe, it,+                                                   testSpec)++spec = testSpec "DCC message serialization" $++    describe "Parsing single elements" $ do++        describe "Network byte order IP" $ do++            describe "[SUCCESS]" $ do++                it "Min IPv4" $+                   pack "0" ~> parseIpBigEndian+                       `shouldParse` toIPv4 [0, 0, 0, 0]++                it "Max IPv4" $+                   pack "4294967295" ~> parseIpBigEndian+                       `shouldParse` toIPv4 [255, 255, 255, 255]++                it "Local IPv4" $+                   pack "3232235521" ~> parseIpBigEndian+                       `shouldParse` toIPv4 [192, 168, 0, 1]++                it "Public IPv4" $+                   pack "134743044" ~> parseIpBigEndian+                       `shouldParse` toIPv4 [8, 8, 4, 4]++                it "IPv4 at beginning of stream" $+                   pack "0abcd" ~?> parseIpBigEndian+                       `leavesUnconsumed` pack "abcd"++            describe "[FAILURE]" $ do++                it "Negative IPv4" $+                   parseIpBigEndian `shouldFailOn` pack "-1"++                it "Bigger than max IPv4" $+                   parseIpBigEndian `shouldFailOn` pack "4294967296"++                it "Max IPv4 with additional digit" $+                   parseIpBigEndian `shouldFailOn` pack "42949672950"++                it "Non-digits" $+                   parseIpBigEndian `shouldFailOn` pack "abcd"++                it "When not at beginning of stream" $+                   parseIpBigEndian `shouldFailOn` pack " 0"++        describe "File name" $ do++            describe "[SUCCESS]" $ do++                it "Without extension" $+                    pack "filename" ~> parseFileName+                        `shouldParse` $(mkRelFile "filename")++                it "With extension" $+                    pack "filename.txt" ~> parseFileName+                        `shouldParse` $(mkRelFile "filename.txt")++                it "Quoted with extension" $+                    pack "\"filename.txt\"" ~> parseFileName+                        `shouldParse` $(mkRelFile "filename.txt")++                it "Quoted with space" $+                    pack "\"file name.txt\"" ~> parseFileName+                        `shouldParse` $(mkRelFile "file name.txt")++                it "UTF8 with em space" $+                    UTF8.fromString "file\8195name.txt" ~> parseFileName+                        `shouldParse` $(mkRelFile "file\8195name.txt")++                it "UTF8 with skin tone modifier" $+                    UTF8.fromString "\128110\127997" ~> parseFileName+                        `shouldParse` $(mkRelFile "\128110\127997")++                it "Quoted UTF8 with space" $+                    UTF8.fromString "\"file\8195 name.txt\"" ~> parseFileName+                        `shouldParse` $(mkRelFile "file\8195 name.txt")++                it "From absolute unix path" $+                    pack "/home/user/filename.txt" ~> parseFileName+                        `shouldParse` $(mkRelFile "filename.txt")++                it "From quoted absolute unix path" $+                    pack "\"/home/user/filename.txt\"" ~> parseFileName+                        `shouldParse` $(mkRelFile "filename.txt")++                it "At beginning of stream" $+                    pack "filename 122350" ~?> parseFileName+                        `leavesUnconsumed` pack " 122350"++-- TODO Not sure exactly what to do with this yet. On Unix the whole thing+--      could be a file name, while on windows it is obviously an absolute path+--                 it "File name from absoulte unix path" $+--                     ("c:\\Users\\user\\filename.txt" ~> parseFileName+--                         `shouldParse` $(mkRelFile "filename.txt")++            describe "[FAILURE]" $ do++                it "ASCII filename with space" $+                    pack "file name.txt" ~?> parseFileName+                        `leavesUnconsumed` pack " name.txt"++                it "Not at beginning of stream" $+                    parseFileName `shouldFailOn` pack " filename"