file-uri (empty) → 0.1.0.0
raw patch · 8 files changed
+664/−0 lines, 8 filesdep +attoparsecdep +basedep +bytestring
Dependencies added: attoparsec, base, bytestring, file-uri, tasty, tasty-bench, tasty-hunit
Files
- CHANGELOG.md +6/−0
- LICENSE +30/−0
- README.md +9/−0
- bench/Bench.hs +29/−0
- file-uri.cabal +66/−0
- lib/System/URI/File.hs +38/−0
- lib/System/URI/File/Internal.hs +355/−0
- test/Main.hs +131/−0
+ CHANGELOG.md view
@@ -0,0 +1,6 @@+# Revision history for file-uri++## 0.1.0.0 -- 2024-01-19++* Initial release+
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2024, Julian Ospald++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * 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.++ * Neither the name of Julian Ospald nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"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+OWNER OR CONTRIBUTORS 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.
+ README.md view
@@ -0,0 +1,9 @@+# file-uri++This Haskell library parses `file:///foo/bar` URIs based on RFC [8089](https://www.rfc-editor.org/rfc/rfc8089.html),+including windows filepaths. It's a subset of [RFC3986](https://www.rfc-editor.org/rfc/rfc3986),+but is better at interpreting the filepaths (especially on windows).++Part of the code is based on [uri-bytestring](https://hackage.haskell.org/package/uri-bytestring)+from Soostone.+
+ bench/Bench.hs view
@@ -0,0 +1,29 @@+{-# LANGUAGE OverloadedStrings #-}++module Main (main) where++import System.URI.File+import Test.Tasty.Bench++main :: IO ()+main = defaultMain benchMark+++benchMark :: [Benchmark]+benchMark =+ [ bgroup "parseFileURI StrictPosix"+ [ bench "file://hostname/path/to/file/foo/bar/baz"+ $ whnf (parseFileURI StrictPosix) "file://user@hostname/path/to/file/foo/bar/baz"+ ]+ , bgroup "parseFileURI ExtendedPosix"+ [ bench "file://user@hostname/path/to/file/foo/bar/baz"+ $ whnf (parseFileURI ExtendedPosix) "file://user@hostname/path/to/file/foo/bar/baz"+ ]+ , bgroup "parseFileURI ExtendedWindows"+ [ bench "file://user@hostname/c|/path/to/file"+ $ whnf (parseFileURI ExtendedWindows) "file://user@hostname/c|/path/to/file"+ , bench "file://///host.example.com/path/to/file"+ $ whnf (parseFileURI ExtendedWindows) "file://///host.example.com/path/to/file"+ ]+ ]+
+ file-uri.cabal view
@@ -0,0 +1,66 @@+cabal-version: 2.2+name: file-uri+version: 0.1.0.0+synopsis: File URI parsing+description: Parses file:///foo/bar URIs based on RFC 8089, including windows filepaths.+license: BSD-3-Clause+license-file: LICENSE+author: Julian Ospald+maintainer: hasufell@posteo.de+copyright: Julian Ospald 2024+category: System+build-type: Simple+extra-doc-files: CHANGELOG.md+ README.md+-- extra-source-files:+--+source-repository head+ type: git+ location: https://github.com/haskell/ghcup-hs.git++common warnings+ ghc-options: -Wall++library+ import: warnings+ exposed-modules: System.URI.File+ System.URI.File.Internal+ -- other-modules:+ -- other-extensions:+ build-depends: base >=4.12.0.0 && <4.20+ , bytestring >=0.9.1+ , attoparsec >=0.13.1.0+ hs-source-dirs: lib+ default-language: Haskell2010++test-suite file-uri-test+ import: warnings+ default-language: Haskell2010+ -- other-modules:+ -- other-extensions:+ type: exitcode-stdio-1.0+ hs-source-dirs: test+ main-is: Main.hs+ build-depends:+ base+ , bytestring+ , file-uri+ , tasty+ , tasty-hunit++benchmark file-uri-benchmark+ import: warnings+ default-language: Haskell2010+ -- other-modules:+ -- other-extensions:+ type: exitcode-stdio-1.0+ hs-source-dirs: bench+ ghc-options: -O2 "-with-rtsopts=-A32m"+ if impl(ghc >= 8.6)+ ghc-options: -fproc-alignment=64+ main-is: Bench.hs+ build-depends:+ base+ , file-uri+ , tasty-bench+
+ lib/System/URI/File.hs view
@@ -0,0 +1,38 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TupleSections #-}++-- |+--+-- Module : System.URI.File+-- Description : ByteString file URI Parser+-- Copyright : (c) Soostone Inc., 2014-2015+-- Michael Xavier, 2014-2015+-- Julian Ospald, 2024+-- License : BSD3+-- Maintainer : hasufell@posteo.de+-- Stability : experimental+--+-- @System.URI.File@ aims to be an <https://www.rfc-editor.org/rfc/rfc8089.html RFC8089> compliant URI file parser that uses+-- efficient ByteStrings for parsing and representing the data.+--+-- As such it only parses a subset of <https://www.rfc-editor.org/rfc/rfc3986 RFC3986>, but is better at interpreting+-- the file paths. __Filepaths are always absolute according to the spec__.+--+-- Part of this module was ripped off of the <https://hackage.haskell.org/package/uri-bytestring uri-bytestring> package from+-- Soostone (specifically the host part parsing).+module System.URI.File (+ -- * Data types+ FileURI(..)+, ParseSyntax(..)++ -- * Parsing+, parseFileURI++ -- * Attoparsec parsers+, fileURIStrictP+, fileURIExtendedPosixP+, fileURIExtendedWindowsP+) where++import System.URI.File.Internal+
+ lib/System/URI/File/Internal.hs view
@@ -0,0 +1,355 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TupleSections #-}++module System.URI.File.Internal where++import Control.Applicative+import Control.Monad+import Data.Attoparsec.ByteString (Parser)+import Data.Bits+import Data.ByteString (ByteString)+import Data.Maybe+import Data.Ix (range)+import Data.Word+import qualified Data.Attoparsec.ByteString as A+import qualified Data.Attoparsec.ByteString.Char8 as A (decimal)++import qualified Data.ByteString as BS+++-- $setup+-- >>> :set -XOverloadedStrings++++ -----------------------+ --[ Main data types ]--+ -----------------------+++-- | A parsed file URI. It can have an auth/host part.+data FileURI = FileURI {+ fileAuth :: Maybe ByteString -- ^ optional host part ("localhost" is parsed as 'Nothing'); <https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats#unc-paths UNC> paths on windows go into 'filePath' and are not split+ , filePath :: ByteString -- ^ the proper absolute filepath+ } deriving (Show, Eq)+++-- | RFC syntax configuration.+data ParseSyntax = StrictPosix -- ^ Only parses the strict syntax according to <https://www.rfc-editor.org/rfc/rfc8089.html#section-2 section 2 of RFC 8089>, which is technically posix paths.+ | ExtendedPosix -- ^ Also parses extended user information described in <https://www.rfc-editor.org/rfc/rfc8089.html#appendix-E.1 E.1>+ | ExtendedWindows -- ^ Parses windows paths according to <https://www.rfc-editor.org/rfc/rfc8089.html#appendix-E.1 E.1>, <https://www.rfc-editor.org/rfc/rfc8089.html#appendix-E.2 E.2> and <https://www.rfc-editor.org/rfc/rfc8089.html#appendix-E.3 E.3>. Unlike the spec, posix paths are rejected.+ deriving (Show, Eq)+++++ ---------------+ --[ Parsing ]--+ ---------------+++-- | Parse a file URI such as @file:\/\/\/foo\/bar@ into 'FileURI'.+--+-- >>> parseFileURI StrictPosix "file:/path/to/file"+-- Right (FileURI {fileAuth = Nothing, filePath = "/path/to/file"})+-- >>> parseFileURI StrictPosix "file:///path/to/file"+-- Right (FileURI {fileAuth = Nothing, filePath = "/path/to/file"})+-- >>> parseFileURI StrictPosix "file://hostname/path/to/file"+-- Right (FileURI {fileAuth = Just "hostname", filePath = "/path/to/file"})+-- >>> parseFileURI StrictPosix "file://localhost/path/to/file"+-- Right (FileURI {fileAuth = Nothing, filePath = "/path/to/file"})+-- >>> parseFileURI StrictPosix "http://localhost/path/to/file"+-- Left "string"+-- >>> parseFileURI StrictPosix "/path/to/file"+-- Left "string"+-- >>> parseFileURI ExtendedWindows "file://///host.example.com/path/to/file"+-- Right (FileURI {fileAuth = Nothing, filePath = "//host.example.com/path/to/file"})+-- >>> parseFileURI ExtendedWindows "file:///c:/path/to/file"+-- Right (FileURI {fileAuth = Nothing, filePath = "c:/path/to/file"})+-- >>> parseFileURI ExtendedWindows "file:/c:/path/to/file"+-- Right (FileURI {fileAuth = Nothing, filePath = "c:/path/to/file"})+-- >>> parseFileURI ExtendedWindows "file:c:/path/to/file"+-- Right (FileURI {fileAuth = Nothing, filePath = "c:/path/to/file"})+parseFileURI :: ParseSyntax -- ^ RFC syntax configuration+ -> ByteString -- ^ input file URI+ -> Either String FileURI+parseFileURI StrictPosix = A.parseOnly (fileURIStrictP <* A.endOfInput)+parseFileURI ExtendedPosix = A.parseOnly (fileURIExtendedPosixP <* A.endOfInput)+parseFileURI ExtendedWindows = A.parseOnly (fileURIExtendedWindowsP <* A.endOfInput)++++ --------------------------+ --[ Attoparsec parsers ]--+ --------------------------+++-- | Parse a file URI according to the <https://www.rfc-editor.org/rfc/rfc8089.html#section-2 main ABNF in RFC 8089>, without+-- any extended rules, which is as follows:+--+-- @+-- file-URI = file-scheme ":" file-hier-part+--+-- file-scheme = "file"+--+-- file-hier-part = ( "//" auth-path )+-- / local-path+--+-- auth-path = [ file-auth ] path-absolute+--+-- local-path = path-absolute+--+-- file-auth = "localhost"+-- / host+-- @+fileURIStrictP :: Parser FileURI+fileURIStrictP = A.string "file:" *> fileHierPart+ where+ fileHierPart = (A.string "//" *> authPath) <|> localPath+ authPath = (\mfA -> FileURI (if mfA == Just "localhost" then Nothing else mfA))+ <$> A.option Nothing (Just <$> fileAuth')+ <*> pathAbsoluteP+ fileAuth' = A.string "localhost" <|> hostP+ localPath = FileURI Nothing <$> pathAbsoluteP++-- | Parse a file URI according to the <https://www.rfc-editor.org/rfc/rfc8089.html#section-2 main ABNF in RFC 8089>, with+-- extended rule <https://www.rfc-editor.org/rfc/rfc8089.html#appendix-E.1 E.1>.+--+-- @+-- file-URI = file-scheme ":" file-hier-part+--+-- file-scheme = "file"+--+-- file-hier-part = ( "//" auth-path )+-- / local-path+--+-- auth-path = [ file-auth ] path-absolute+--+-- local-path = path-absolute+--+-- file-auth = "localhost"+-- / [ userinfo "@" ] host+-- @+fileURIExtendedPosixP :: Parser FileURI+fileURIExtendedPosixP = A.string "file:" *> fileHierPart+ where+ fileHierPart = (A.string "//" *> authPath) <|> localPath+ authPath = (\mfA -> FileURI (if mfA == Just "localhost" then Nothing else mfA))+ <$> A.option Nothing (Just <$> fileAuth')+ <*> pathAbsoluteP+ fileAuth' = A.string "localhost" <|> sequenceM [A.option BS.empty (sequenceM [userInfoP, "@"]), hostP]+ localPath = FileURI Nothing <$> pathAbsoluteP+++-- | Parse a file URI according for windows according to <https://www.rfc-editor.org/rfc/rfc8089.html#appendix-E.1 E.1>,+-- <https://www.rfc-editor.org/rfc/rfc8089.html#appendix-E.2 E.2> and+-- <https://www.rfc-editor.org/rfc/rfc8089.html#appendix-E.3 E.3>. Unlike the spec, posix paths are rejected. The ABNF+-- is a slight modification of <https://www.rfc-editor.org/rfc/rfc8089.html#appendix-F Appendix F>.+--+-- @+-- file-URI = file-scheme ":" file-hier-part+--+-- file-scheme = "file"+--+-- file-hier-part = ( "//" auth-path )+-- / local-path+--+-- auth-path = [ file-auth ] file-absolute+-- / unc-authority path-absolute+--+-- local-path = drive-letter path-absolute+-- / file-absolute+--+-- file-auth = "localhost"+-- / [ userinfo "@" ] host+--+-- unc-authority = 2*3"/" file-host+--+-- file-host = inline-IP / IPv4address / reg-name+--+-- inline-IP = "%5B" ( IPv6address / IPvFuture ) "%5D"+--+-- file-absolute = "/" drive-letter path-absolute+--+-- drive-letter = ALPHA ":"+-- / ALPHA "|"+-- @+fileURIExtendedWindowsP :: Parser FileURI+fileURIExtendedWindowsP = A.string "file:" *> fileHierPart+ where+ fileHierPart = (A.string "//" *> authPath) <|> localPath+ authPath = (\(mfA, p) -> FileURI (if mfA == Just "localhost" then Nothing else mfA) p) <$> (+ ((,) <$> A.option Nothing (Just <$> fileAuth') <*> fileAbsoluteP <* A.endOfInput)+ <|> ((,) <$> pure Nothing <*> sequenceM [uncAuthorityP, pathAbsoluteP] <* A.endOfInput)+ )+ fileAuth' = A.string "localhost" <|> sequenceM [A.option BS.empty (sequenceM [userInfoP, "@"]), hostP]+ localPath = fmap (FileURI Nothing) $ (fileAbsoluteP <* A.endOfInput) <|>+ (sequenceM [driveLetterP', pathAbsoluteP] <* A.endOfInput)++pathAbsoluteP :: Parser ByteString+pathAbsoluteP = sequenceM [A.string "/", A.option BS.empty $ sequenceM [segmentNZP, pathAbEmpty]]++uncAuthorityP :: Parser ByteString+uncAuthorityP = (\a b -> BS.concat [a, b]) <$> (A.string "//" <* (A.option BS.empty (A.string "/"))) <*> fileHostP++fileHostP :: Parser ByteString+fileHostP = hostP++fileAbsoluteP :: Parser ByteString+fileAbsoluteP = A.string "/" *> sequenceM [driveLetterP', pathAbsoluteP]++driveLetterP :: Parser Word8+driveLetterP = A.satisfy (A.inClass alpha) <* (A.string ":" <|> A.string "|")++-- | Like 'driveLetterP', but appends ':'.+driveLetterP' :: Parser ByteString+driveLetterP' = ((<> ":") . BS.singleton) <$> driveLetterP++userInfoP :: Parser ByteString+userInfoP = BS.pack <$> many (pctEncodedP <|> satisfyClass (":" ++ subDelims ++ unreserved))++hostP :: Parser ByteString+hostP = ipLiteralP <|> ipV4P <|> regNameP++regNameP :: Parser ByteString+regNameP = BS.pack <$> A.many1 (pctEncodedP <|> satisfyClass (subDelims ++ unreserved))++ipLiteralP :: Parser ByteString+ipLiteralP = A.word8 oBracket *> (ipVFutureP <|> ipV6P) <* A.word8 cBracket++-- | Parses IPVFuture addresses. See relevant section in RFC.+ipVFutureP :: Parser ByteString+ipVFutureP = do+ _ <- A.word8 lowercaseV+ ds <- A.takeWhile1 hexDigit+ _ <- A.word8 period+ rest <- A.takeWhile1 $ A.inClass $ subDelims ++ ":" ++ unreserved+ return $ "v" <> ds <> "." <> rest+ where+ lowercaseV = 118++-- | Parses IPV6 addresses. See relevant section in RFC.+ipV6P :: Parser ByteString+ipV6P = do+ leading <- h16s+ elided <- maybe [] (const [""]) <$> optional (A.string "::")+ trailing <- many (A.takeWhile (/= colon) <* A.word8 colon)+ (finalChunkLen, final) <- finalChunk+ let len = length (leading ++ trailing) + finalChunkLen+ when (len > 8) $ fail "Too many digits in IPv6 address"+ return $ rejoin $ [rejoin leading] ++ elided ++ trailing ++ maybeToList final+ where+ finalChunk = fromMaybe (0, Nothing) <$> optional (finalIpV4 <|> finalH16)+ finalH16 = (1,) . Just <$> h16+ finalIpV4 = (2,) . Just <$> ipV4P+ rejoin = BS.intercalate ":"+ h16s = h16 `A.sepBy` A.word8 colon+ h16 = mconcat <$> parseBetween 1 4 (A.takeWhile1 hexDigit)++-- | Parses a valid IPV4 address+ipV4P :: Parser ByteString+ipV4P =+ sequenceM+ [ decOctet,+ dot,+ decOctet,+ dot,+ decOctet,+ dot,+ decOctet+ ]+ where+ decOctet :: Parser ByteString+ decOctet = do+ (s, num) <- A.match A.decimal+ let len = BS.length s+ guard $ len <= 3+ guard $ num >= (1 :: Int) && num <= 255+ return s+ dot = A.string "."+++++pathAbEmpty :: Parser ByteString+pathAbEmpty = fmap BS.concat . many . sequenceM $ [A.string "/", segmentP]++segmentP :: Parser ByteString+segmentP = BS.pack <$> A.many' pcharP++segmentNZP :: Parser ByteString+segmentNZP = BS.pack <$> A.many1 pcharP++segmentNZNCP :: Parser ByteString+segmentNZNCP = BS.pack <$> A.many1 (pctEncodedP <|> satisfyClass (subDelims ++ "@" ++ unreserved))++pcharP :: Parser Word8+pcharP = pctEncodedP <|> satisfyClass (subDelims ++ ":" ++ "@" ++ unreserved)++pctEncodedP :: Parser Word8+pctEncodedP = A.string "%" *> (decode <$> A.satisfy hexDigit <*> A.satisfy hexDigit)+ where+ decode w1 w2 = combine (hexVal w1) (hexVal w2)+ hexVal w+ | 48 <= w && w <= 57 = w - 48 -- 0 - 9+ | 65 <= w && w <= 70 = w - 55 -- A - F+ | 97 <= w && w <= 102 = w - 87 -- a - f+ | otherwise = error $ "Not a hex value: " <> show w+ combine a b = shiftL a 4 .|. b+++++ ---------------------------+ --[ Word/String classes ]--+ ---------------------------+++hexDigit :: Word8 -> Bool+hexDigit = A.inClass "0-9a-fA-F"++unreserved :: String+unreserved = alphaNum ++ "~._-"++subDelims :: String+subDelims = "!$&'()*+,;="++alphaNum :: String+alphaNum = alpha ++ digit++alpha :: String+alpha = "a-zA-Z"++digit :: String+digit = "0-9"++oBracket :: Word8+oBracket = 91++cBracket :: Word8+cBracket = 93++colon :: Word8+colon = 58++period :: Word8+period = 46++++ -------------------------------------+ --[ Custom attoparsec combinators ]--+ -------------------------------------+++satisfyClass :: String -> Parser Word8+satisfyClass = A.satisfy . A.inClass++sequenceM :: Monad m => [(m ByteString)] -> m ByteString+sequenceM = fmap BS.concat . sequence++parseBetween :: (Alternative m, Monad m) => Int -> Int -> m a -> m [a]+parseBetween a b f = A.choice parsers+ where+ parsers = map (`A.count` f) $ reverse $ range (a, b)+
+ test/Main.hs view
@@ -0,0 +1,131 @@+{-# LANGUAGE OverloadedStrings #-}++module Main (main) where++import System.URI.File++import Data.ByteString+import Test.Tasty+import Test.Tasty.HUnit++import qualified Data.ByteString.Char8 as B8++main :: IO ()+main = defaultMain tests+++tests :: TestTree+tests =+ testGroup+ "System.URI.File"+ [ parseFileURIStrictPosix+ , parseFileURIExtendedPosix+ , parseFileURIExtendedWindows+ ]++parseFileURIStrictPosix :: TestTree+parseFileURIStrictPosix =+ testGroup+ "parseFileURI (StrictPosix)"+ [ parseTestURI StrictPosix "file:/path/to/file"+ (Right FileURI { fileAuth = Nothing, filePath = "/path/to/file" })+ , parseTestURI StrictPosix "file:///path/to/file"+ (Right FileURI { fileAuth = Nothing, filePath = "/path/to/file" })+ , parseTestURI StrictPosix "file://hostname/path/to/file"+ (Right FileURI { fileAuth = Just "hostname", filePath = "/path/to/file" })+ , parseTestURI StrictPosix "file://localhost/path/to/file"+ (Right FileURI { fileAuth = Nothing, filePath = "/path/to/file" })+ , parseTestURI StrictPosix "http://localhost/path/to/file"+ (Left "string")+ , parseTestURI StrictPosix "/path/to/file"+ (Left "string")++ , parseTestURI StrictPosix "file:///c:/path/to/file"+ (Right FileURI { fileAuth = Nothing, filePath = "/c:/path/to/file" })+ , parseTestURI StrictPosix "file:/c:/path/to/file"+ (Right FileURI { fileAuth = Nothing, filePath = "/c:/path/to/file" })+ , parseTestURI StrictPosix "file:c:/path/to/file"+ (Left "string")++ , parseTestURI StrictPosix "file://user@hostname/path/to/file"+ (Left "endOfInput")+ ]++parseFileURIExtendedPosix :: TestTree+parseFileURIExtendedPosix =+ testGroup+ "parseFileURI (ExtendedPosix)"+ [ parseTestURI ExtendedPosix "file:/path/to/file"+ (Right FileURI { fileAuth = Nothing, filePath = "/path/to/file" })+ , parseTestURI ExtendedPosix "file:///path/to/file"+ (Right FileURI { fileAuth = Nothing, filePath = "/path/to/file" })+ , parseTestURI ExtendedPosix "file://hostname/path/to/file"+ (Right FileURI { fileAuth = Just "hostname", filePath = "/path/to/file" })+ , parseTestURI ExtendedPosix "file://localhost/path/to/file"+ (Right FileURI { fileAuth = Nothing, filePath = "/path/to/file" })+ , parseTestURI ExtendedPosix "http://localhost/path/to/file"+ (Left "string")+ , parseTestURI ExtendedPosix "/path/to/file"+ (Left "string")++ , parseTestURI ExtendedPosix "file:///c:/path/to/file"+ (Right FileURI { fileAuth = Nothing, filePath = "/c:/path/to/file" })+ , parseTestURI ExtendedPosix "file:/c:/path/to/file"+ (Right FileURI { fileAuth = Nothing, filePath = "/c:/path/to/file" })+ , parseTestURI ExtendedPosix "file:c:/path/to/file"+ (Left "string")++ , parseTestURI ExtendedPosix "file://user@hostname/path/to/file"+ (Right (FileURI {fileAuth = Just "user@hostname", filePath = "/path/to/file"}))+ ]++parseFileURIExtendedWindows :: TestTree+parseFileURIExtendedWindows =+ testGroup+ "parseFileURI (ExtendedWindows)"+ [ parseTestURI ExtendedWindows "file:///c|/path/to/file"+ (Right FileURI { fileAuth = Nothing, filePath = "c:/path/to/file" })+ , parseTestURI ExtendedWindows "file:/c|/path/to/file"+ (Right FileURI { fileAuth = Nothing, filePath = "c:/path/to/file" })+ , parseTestURI ExtendedWindows "file:c|/path/to/file"+ (Right FileURI { fileAuth = Nothing, filePath = "c:/path/to/file" })+ , parseTestURI ExtendedWindows "file://localhost/c|/path/to/file"+ (Right FileURI { fileAuth = Nothing, filePath = "c:/path/to/file" })+ , parseTestURI ExtendedWindows "file://hostname/c|/path/to/file"+ (Right FileURI { fileAuth = Just "hostname", filePath = "c:/path/to/file" })++ , parseTestURI ExtendedWindows "file://user@hostname/c|/path/to/file"+ (Right FileURI { fileAuth = Just "user@hostname", filePath = "c:/path/to/file" })++ , parseTestURI ExtendedWindows "file:///c:/path/to/file"+ (Right FileURI { fileAuth = Nothing, filePath = "c:/path/to/file" })+ , parseTestURI ExtendedWindows "file:/c:/path/to/file"+ (Right FileURI { fileAuth = Nothing, filePath = "c:/path/to/file" })+ , parseTestURI ExtendedWindows "file:c:/path/to/file"+ (Right FileURI { fileAuth = Nothing, filePath = "c:/path/to/file" })++ , parseTestURI ExtendedWindows "file:/path/to/file"+ (Left "Failed reading: satisfy")+ , parseTestURI ExtendedWindows "file:///path/to/file"+ (Left "Failed reading: satisfy")+ , parseTestURI ExtendedWindows "file://hostname/path/to/file"+ (Left "Failed reading: satisfy")+ , parseTestURI ExtendedWindows "file://localhost/path/to/file"+ (Left "Failed reading: satisfy")+ , parseTestURI ExtendedWindows "http://localhost/path/to/file"+ (Left "string")+ , parseTestURI ExtendedWindows "/path/to/file"+ (Left "string")+ , parseTestURI ExtendedWindows "file:///c:\\path\\to\\file"+ (Left "Failed reading: satisfy")++ , parseTestURI ExtendedWindows "file://///host.example.com/path/to/file"+ (Right (FileURI {fileAuth = Nothing, filePath = "//host.example.com/path/to/file"}))+ , parseTestURI ExtendedWindows "file:////system07/C$/"+ (Right (FileURI {fileAuth = Nothing, filePath = "//system07/C$/"}))+ ]+++parseTestURI :: ParseSyntax -> ByteString -> Either String FileURI -> TestTree+parseTestURI cfg s r = testCase (B8.unpack s) $ parseFileURI cfg s @?= r+