dormouse-uri-0.1.0.0: src/Dormouse/Uri/Parser.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DisambiguateRecordFields #-}
{-# LANGUAGE LambdaCase #-}
module Dormouse.Uri.Parser
( pUri
, pAbsoluteUri
, pRelativeUri
, pScheme
, pUsername
, pPassword
, pUserInfo
, pIPv4
, pRegName
, pHost
, pPort
, pAuthority
, pPathAbsAuth
, pPathAbsNoAuth
, pPathRel
, pQuery
, pFragment
) where
import Control.Applicative ((<|>))
import Data.Attoparsec.ByteString.Char8 as A
import Data.Char as C
import Data.Bits (Bits, shiftL, (.|.))
import Data.Maybe (isJust)
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import Dormouse.Uri.Types
import Dormouse.Uri.RFC3986
import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as B8
repack :: String -> T.Text
repack = TE.decodeUtf8 . B8.pack
pMaybe :: Parser a -> Parser (Maybe a)
pMaybe p = option Nothing (Just <$> p)
pAsciiAlpha :: Parser Char
pAsciiAlpha = satisfy isAsciiAlpha
pAsciiAlphaNumeric :: Parser Char
pAsciiAlphaNumeric = satisfy isAsciiAlphaNumeric
pSubDelim :: Parser Char
pSubDelim = satisfy isSubDelim
pUnreserved :: Parser Char
pUnreserved = satisfy isUnreserved
pSizedHexadecimal :: (Integral a, Bits a) => Int -> Parser a
pSizedHexadecimal n = do
bytes <- A.take n
if B.all isHexDigit' bytes then return $ B.foldl' step 0 $ bytes else fail "pSizedHexadecimal"
where
isHexDigit' w = (w >= 48 && w <= 57) || (w >= 97 && w <= 102) ||(w >= 65 && w <= 70)
step a w | w >= 48 && w <= 57 = (a `shiftL` 4) .|. fromIntegral (w - 48)
| w >= 97 = (a `shiftL` 4) .|. fromIntegral (w - 87)
| otherwise = (a `shiftL` 4) .|. fromIntegral (w - 55)
pPercentEnc :: Parser Char
pPercentEnc = do
_ <- char '%'
hexdig1 <- pSizedHexadecimal 1
hexdig2 <- pSizedHexadecimal 1
return . chr $ hexdig1 * 16 + hexdig2
pUsername :: Parser Username
pUsername = do
xs <- many1' (satisfy isUsernameChar <|> pPercentEnc)
return $ Username (repack xs)
pPassword :: Parser Password
pPassword = do
xs <- many1' (satisfy isPasswordChar <|> pPercentEnc)
return $ Password (repack xs)
pRegName :: Parser T.Text
pRegName = do
xs <- many1' (satisfy isRegNameChar <|> pPercentEnc)
return . repack $ xs
pIPv4 :: Parser T.Text
pIPv4 = do
oct1 <- pOctet
_ <- char '.'
oct2 <- pOctet
_ <- char '.'
oct3 <- pOctet
_ <- char '.'
oct4 <- pOctet
return . T.pack $ show oct1 <> "." <> show oct2 <> "." <> show oct3 <> "." <> show oct4
where
pOctet :: Parser Int
pOctet = decimal >>= \case
i | i > 255 -> fail "IPv4 Octects must be in range 0-255"
i -> return i
pHost :: Parser Host
pHost = do
hostText <- pIPv4 <|> pRegName
return . Host $ hostText
pUserInfo :: Parser UserInfo
pUserInfo = do
username <- pUsername
password <- pMaybe (char ':' *> pPassword)
_ <- char '@'
return $ UserInfo { userInfoUsername = username, userInfoPassword = password }
pPort :: Parser Int
pPort =
(char ':' *> decimal) >>= \case
i | i > 65535 -> fail "Port must be in the range 0-65535"
i -> return i
pAuthority :: Parser Authority
pAuthority = do
_ <- string "//"
authUserInfo <- pMaybe pUserInfo
authHost <- pHost
authPort <- pMaybe pPort
_ <- peekChar >>= \case
Nothing -> return ()
Just c | c == '/' || c == '?' || c == '#' -> return ()
_ -> fail "Invalid authority termination character, must be /, ?, # or end of input"
return Authority { authorityUserInfo = authUserInfo, authorityHost = authHost, authorityPort = authPort}
pPathChar :: Parser Char
pPathChar = satisfy isPathChar <|> pPercentEnc
pPathCharNc :: Parser Char
pPathCharNc = satisfy isPathCharNoColon <|> pPercentEnc
pSegmentNz :: Parser PathSegment
pSegmentNz = PathSegment . repack <$> many1' pPathChar
pSegmentNzNc :: Parser PathSegment
pSegmentNzNc = PathSegment . repack <$> many1' pPathCharNc
pSegment :: Parser PathSegment
pSegment = PathSegment . repack <$> many' pPathChar
pPathsAbEmpty :: Parser [PathSegment]
pPathsAbEmpty = many1' (char '/' *> pSegment)
pPathsAbsolute :: Parser [PathSegment]
pPathsAbsolute = do
_ <- char '/'
seg <- pSegmentNz
comps <- many' (char '/' *> pSegment)
return $ seg : comps
pPathsNoScheme :: Parser [PathSegment]
pPathsNoScheme = do
seg <- pSegmentNzNc
comps <- many' (char '/' *> pSegment)
return $ seg : comps
pPathsRootless :: Parser [PathSegment]
pPathsRootless = do
seg <- pSegmentNz
comps <- many' (char '/' *> pSegment)
return $ seg : comps
pPathsEmpty :: Parser [PathSegment]
pPathsEmpty = return []
pPathAbsAuth :: Parser (Path 'Absolute)
pPathAbsAuth = fmap Path (pPathsAbEmpty <|> pPathsAbsolute <|> pPathsEmpty)
pPathAbsNoAuth :: Parser (Path 'Absolute)
pPathAbsNoAuth = fmap Path (pPathsAbsolute <|> pPathsRootless <|> pPathsEmpty)
pPathRel :: Parser (Path 'Relative)
pPathRel = fmap Path (pPathsAbsolute <|> pPathsNoScheme <|> pPathsEmpty)
pQuery :: Parser Query
pQuery = do
queryText <- (char '?' *> (many1' (satisfy isQueryChar <|> pPercentEnc)))
_ <- peekChar >>= \case
Nothing -> return ()
Just c | c == '#' -> return ()
c -> fail $ "Invalid query termination character: " <> show c <> ", must be # or end of input"
return . Query . repack $ queryText
pFragment :: Parser Fragment
pFragment = do
fragmentText <- (char '#' *> (many1' (satisfy isFragmentChar <|> pPercentEnc)))
_ <- peekChar >>= \case
Nothing -> return ()
c -> fail $ "Invalid fragment termination character: " <> show c <> ", must be end of input"
return . Fragment . repack $ fragmentText
pScheme :: Parser Scheme
pScheme = do
x <- pAsciiAlpha
xs <- many' (pAsciiAlphaNumeric <|> char '+' <|> char '.' <|> char '-' )
_ <- char ':'
return $ Scheme (T.toLower . repack $ x:xs)
pAbsolutePart :: Parser (Scheme, Maybe Authority)
pAbsolutePart = do
scheme <- pScheme
authority <- pMaybe pAuthority
return (scheme, authority)
pRelativeUri :: Parser Uri
pRelativeUri = do
path <- pPathRel
query <- pMaybe pQuery
fragment <- pMaybe pFragment
_ <- endOfInput
return $ RelativeUri $ RelUri { uriPath = path, uriQuery = query, uriFragment = fragment }
pAbsoluteUri :: Parser Uri
pAbsoluteUri = do
(scheme, authority) <- pAbsolutePart
path <- if isJust authority then pPathAbsAuth else pPathAbsNoAuth
query <- pMaybe pQuery
fragment <- pMaybe pFragment
_ <- endOfInput
return $ AbsoluteUri $ AbsUri {uriScheme = scheme, uriAuthority = authority, uriPath = path, uriQuery = query, uriFragment = fragment }
pUri :: Parser Uri
pUri = pAbsoluteUri <|> pRelativeUri