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 +21/−0
- Setup.hs +2/−0
- irc-dcc.cabal +66/−0
- src/Network/IRC/DCC.hs +30/−0
- src/Network/IRC/DCC/FileTransfer.hs +79/−0
- src/Network/IRC/DCC/Internal.hs +332/−0
- src/Network/Socket/ByteString/Extended.hs +70/−0
- tests/Main.hs +10/−0
- tests/Network/IRC/DCCTest.hs +121/−0
+ 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"