packages feed

dormouse-uri-0.2.0.0: src/Dormouse/Uri/Parser.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DisambiguateRecordFields #-}
{-# LANGUAGE LambdaCase #-}

module Dormouse.Uri.Parser
  ( pUri
  , pUriRef
  , pRelativeUri
  , pScheme
  , pUserInfo
  , pIPv4
  , pRegName
  , pHost
  , pPort
  , pAuthority
  , pPathAbsAuth
  , pPathAbsNoAuth
  , pPathRel
  , pQuery
  , pFragment
  , percentDecode
  ) where

import Data.Word ( Word8 ) 
import Control.Applicative ((<|>))
import Data.Attoparsec.ByteString.Char8 as A
import qualified Data.Attoparsec.ByteString as AB
import qualified Data.ByteString.Internal as BS (c2w, w2c)
import Data.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

pMaybe :: Parser a -> Parser (Maybe a)
pMaybe p = option Nothing (Just <$> p)

pAsciiAlpha :: Parser Char
pAsciiAlpha = satisfy isAsciiAlpha

data PDState = Percent | Hex1 Word8 | Other | PDError

percentDecode :: B.ByteString -> Maybe B.ByteString
percentDecode xs =
  if B.elem 37 xs then
    case B.foldl' f (B.empty, Other) xs of
      (_, PDError)  -> Nothing 
      (bs, _)       -> Just bs
  else 
    Just xs
  where
    f (es, Percent) e                                     = (es, Hex1 e)
    f (es, Hex1 e1) e2 | isHexDigit' e1 && isHexDigit' e2 = (B.snoc es (hexToWord8 e1 `shiftL` 4 .|. hexToWord8 e2), Other)
    f (es, Hex1 _)  _                                     = (es, PDError)
    f (es, Other)   37                                    = (es, Percent)
    f (es, Other)   e                                     = (B.snoc es e, Other)
    f (es, PDError) _                                     = (es, PDError)
    hexToWord8 w | w >= 48 && w <= 57 = fromIntegral (w - 48)
                 | w >= 97            = fromIntegral (w - 87)
                 | otherwise          = fromIntegral (w - 55)
    isHexDigit' w = (w >= 48 && w <= 57) ||  (w >= 97 && w <= 102) ||(w >= 65 && w <= 70)

takeWhileW8 :: (Char -> Bool) -> Parser B.ByteString 
takeWhileW8 f = AB.takeWhile (f . BS.w2c)

takeWhile1W8 :: (Char -> Bool) -> Parser B.ByteString 
takeWhile1W8 f = AB.takeWhile1 (f . BS.w2c)

pUserInfo :: Parser UserInfo
pUserInfo = do
  xs <- takeWhileW8 (\x -> isUserInfoChar x || x == '%')
  xs' <- maybe (fail "Failed to percent-decode") pure $ percentDecode xs
  _ <- char '@'
  return $ UserInfo (TE.decodeUtf8 xs')

pRegName :: Parser T.Text
pRegName = do
  xs <- takeWhileW8 (\x -> isRegNameChar x || x == '%')
  xs' <- maybe (fail "Failed to percent-decode") pure $ percentDecode xs
  return . TE.decodeUtf8 $ 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 <- pRegName <|> pIPv4
  return . Host  $ hostText

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}

pPathAbsAuth :: Parser (Path rt)
pPathAbsAuth = do
  p <- takeWhileW8 (\x -> isPathChar x || x == '%' || x == '/')
  p' <- maybe (fail "Failed to percent-decode") pure $ percentDecode p
  let ps = PathSegment <$> T.split (== '/') (TE.decodeUtf8 p')
  case ps of -- begins with "/" is empty
    (PathSegment x):xs | T.null x -> return $ Path xs
    (PathSegment _):_             -> fail "must begin with /"
    xs                            -> return $ Path xs

pPathAbsNoAuth :: Parser (Path 'Absolute)
pPathAbsNoAuth = do
  p <- takeWhileW8 (\x -> isPathChar x || x == '%' || x == '/')
  p' <- maybe (fail "Failed to percent-decode") pure $ percentDecode p
  let ps = PathSegment <$> T.split (== '/') (TE.decodeUtf8 p')
  case ps of -- begins with "/" but not "//" OR begins with segment OR empty
    (PathSegment x1):(PathSegment x2):_ | T.null x1 && T.null x2 -> fail "cannot begin with //"
    (PathSegment x):xs                  | T.null x               -> return $ Path xs
    xs                                                           -> return $ Path xs

pPathRel :: Parser (Path 'Relative)
pPathRel = do
  p <- takeWhileW8 (\x -> isPathChar x || x == '%' || x == '/')
  p' <- maybe (fail "Failed to percent-decode") pure $ percentDecode p
  let ps = PathSegment <$> T.split (== '/') (TE.decodeUtf8 p')
  case ps of
    (PathSegment x1):(PathSegment x2):_ | T.null x1 && T.null x2 -> fail "cannot begin with //"
    (PathSegment x):_                   | T.isPrefixOf ":" x     -> fail "first character of a relative path cannot be :"
    (PathSegment x):xs                  | T.null x               -> return $ Path xs
    xs                                                           -> return $ Path xs

pQuery :: Parser Query
pQuery = do
  qt <- char '?' *> takeWhile1W8 (\x -> isQueryChar x || x == '%')
  queryText <- maybe (fail "Failed to percent-decode") pure $ percentDecode qt
  _ <- peekChar >>= \case
    Nothing           -> return ()
    Just c | c == '#' -> return ()
    c                 -> fail $ "Invalid query termination character: " <> show c <> ", must be # or end of input"
  return . Query . TE.decodeUtf8 $ queryText

pFragment :: Parser Fragment
pFragment = do
  ft <- char '#' *> takeWhile1W8 (\x -> isFragmentChar x || x == '%')
  fragmentText <- maybe (fail "Failed to percent-decode") pure $ percentDecode ft
  _ <- peekChar >>= \case
    Nothing           -> return ()
    c                 -> fail $ "Invalid fragment termination character: " <> show c <> ", must be end of input"
  return . Fragment . TE.decodeUtf8 $ fragmentText

pScheme :: Parser Scheme
pScheme = do
  x <- pAsciiAlpha
  xs <- A.takeWhile isSchemeChar
  _ <- char ':'
  return $ Scheme (T.toLower . TE.decodeUtf8 $ B.cons (BS.c2w x) xs)

pAbsolutePart :: Parser (Scheme, Maybe Authority)
pAbsolutePart = do
  scheme <- pScheme
  authority <- pMaybe pAuthority
  return (scheme, authority)

pRelativeUri :: Parser RelRef
pRelativeUri = do
  authority <- pMaybe pAuthority
  path <- if isJust authority then pPathAbsAuth else pPathRel
  query <- pMaybe pQuery
  fragment <- pMaybe pFragment
  _ <- endOfInput
  return  $ RelRef { relRefAuthority = authority, relRefPath = path, relRefQuery = query, relRefFragment = fragment }

pUri :: Parser Uri
pUri = do
  (scheme, authority) <- pAbsolutePart
  path <- if isJust authority then pPathAbsAuth else pPathAbsNoAuth
  query <- pMaybe pQuery
  fragment <- pMaybe pFragment
  _ <- endOfInput
  return $ Uri {uriScheme = scheme, uriAuthority = authority, uriPath = path, uriQuery = query, uriFragment = fragment }

pUriRef :: Parser UriReference
pUriRef = (AbsoluteUri <$> pUri) <|> (RelativeRef <$> pRelativeUri)