packages feed

url-bytes-0.1.1.0: src/Url/Rebind.hs

{-# LANGUAGE
    BangPatterns
  , UnboxedTuples
  , UnboxedSums
  , MagicHash
  , RebindableSyntax
  , ScopedTypeVariables
  , RecordWildCards
  , NamedFieldPuns
#-}

-- The parser lives in its own module because it uses RebindableSyntax,
-- which adversely affects inference and error messages.
module Url.Rebind
  ( decodeUrl
  ) where

import Prelude hiding ((>>=),(>>),pure)
import Data.Bytes.Parser.Rebindable ((>>),(>>=),pure)

import Data.Bytes.Types (Bytes(..))
import Data.Char (ord)
import Data.Word (Word8)
import GHC.Exts (Int(I#),Int#,(+#),(<#),(-#),orI#,(>=#),(==#),(>#))
import GHC.Word (Word16(W16#))
import Url.Unsafe (Url(..),ParseError(..))
import qualified Data.Bytes.Parser as P
import qualified Data.Bytes.Parser.Latin as P (skipUntil, char, char2, decWord16, skipDigits1)
import qualified Data.Bytes.Parser.Unsafe as PU
import qualified GHC.Exts as Exts

-- | Decode a hierarchical URL
decodeUrl :: Bytes -> Either ParseError Url
decodeUrl urlSerialization = P.parseBytesEither parserUrl urlSerialization

parserAuthority :: Int# -> P.Parser ParseError s (# Int#, Int#, Int#, Int#, Int# #)
{-# inline parserAuthority #-}
parserAuthority urlSchemeEnd = do
  userStart <- PU.cursor#
  i3 <- P.measure_# (P.skipUntil ':')
  PU.unconsume (I# i3)
  i4 <- P.measure_# (P.skipUntil '@')
  PU.unconsume (I# i4)
  i5 <- P.measure_# (P.skipUntil '/')
  PU.unconsume (I# i5)
  urlUsernameEnd <- ( case (i4 >=# i5) `orI#` (i3 ==# i4) of
          1# -> PU.jump (I# userStart) >> pure userStart
          _ -> let jumpi = i4 in
            case i3 <# i4 of
              1# -> PU.jump (I# (userStart +# jumpi +# 1# )) >> pure (userStart +# i3)
              _ -> PU.jump (I# (userStart +# jumpi +# 1# )) >> pure (userStart +# i4)
      )
  urlHostStart <- PU.cursor#
  colonSlashNeither <- P.skipTrailedBy2# EndOfInput (c2w ':') (c2w '/') `orElse#` 2#
  (# !urlHostEnd, !urlPort #) <- case colonSlashNeither of
    0# -> do
      urlHostEnd <- PU.cursor# -- ':' encountered first
      (W16# urlPort) <- P.decWord16 InvalidPort
      pure (# urlHostEnd -# 1#, Exts.word2Int# urlPort #)
    1# -> do -- '/' encountered first
      urlHostEnd' <- PU.cursor#
      -- Backing up by one since we want to put the slash back
      -- to let it be part of the path.
      let urlHostEnd = urlHostEnd' -# 1#
      PU.jump (I# urlHostEnd)
      pure (# urlHostEnd, 0x10000# #)
    _ -> do -- neither encountered
      urlHostEnd <- PU.cursor#
      PU.jump (I# urlHostEnd)
      pure (# urlHostEnd, 0x10000# #)
  pure
    (# urlSchemeEnd 
    , urlUsernameEnd 
    , urlHostStart  
    , urlHostEnd    
    , urlPort       
    #)

-- | Parser type from @bytesmith@
-- Note: non-hierarchical Urls (such as relative paths) will not currently parse.
parserUrl :: P.Parser ParseError s Url
parserUrl = do
  urlSerialization@(Bytes _ _ (I# len)) <- P.peekRemaining 
  start <- PU.cursor#
  (I# i1, !slashFirst) <- P.measure $
               P.skipTrailedBy2 EndOfInput (c2w ':') (c2w '/')
    `P.orElse` pure True
  (# urlSchemeEnd, urlUsernameEnd, urlHostStart, urlHostEnd, urlPort #) <- case slashFirst of
    False ->
      -- If we see something like "abc://" with a colon followed by two
      -- slashes, we assume that the authority is present ("abc" in
      -- this case).
      succeeded (P.char2 InvalidAuthority '/' '/') >>= \hasAuthorityA -> do
        let urlSchemeEnd = (i1 -# 1# )
        case hasAuthorityA of
          0# -> succeeded (P.skipDigits1 () >> P.char () '/') >>= \hasAuthorityB ->
            -- Here, we are looking for things like "example.com:8888/" that
            -- are missing the scheme but include a port.
            case hasAuthorityB of
              0# -> pure (# urlSchemeEnd, urlSchemeEnd, urlSchemeEnd, urlSchemeEnd, 0x10000# #)
              _ -> PU.jump (I# start) >> parserAuthority start
          _ -> parserAuthority urlSchemeEnd
    True ->
      PU.jump (I# start) >> parserAuthority start 
  urlPathStart <- PU.cursor#
  i8 <- P.measure_# (P.skipUntil '?')
  PU.unconsume (I# i8)
  i9 <- P.measure_# (P.skipUntil '#')
  PU.unconsume (I# i9)
  (# !urlQueryStart, !urlFragmentStart #) <- case intCompare# i8 i9 of
    EQ -> pure (# len, len #)
    LT -> P.skipUntil '#' >> 
      let !urlFragmentStart = i9 +# urlPathStart
       in pure (# (i8 +# urlPathStart), urlFragmentStart #)
    GT -> P.skipUntil '#' >> 
      let !urlFragmentStart = i9 +# urlPathStart
       in pure (# urlFragmentStart, urlFragmentStart #)
  pure (Url {..})

intCompare# :: Int# -> Int# -> Ordering
intCompare# a b = case a ==# b of
  0# -> case a ># b of
    0# -> LT
    _ -> GT
  _ -> EQ

-- | Unsafe conversion between 'Char' and 'Word8'. This is a no-op and
-- silently truncates to 8 bits Chars > '\255'.
c2w :: Char -> Word8
c2w = fromIntegral . ord
{-# INLINE c2w #-}

orElse# :: forall e x s. PU.Parser x s Int# -> Int# -> PU.Parser e s Int#
{-# inline orElse# #-}
orElse# (PU.Parser f) i = PU.Parser
  (\x@(# _, b, c #) s0 -> case f x s0 of
    (# s1, r0 #) -> case r0 of
      (# _ | #) -> (# s1, (# | (# i, b, c #) #) #)
      (# | r #) -> (# s1, (# | r #) #)
  )

-- Runs the parser, returning 1 if it succeeds and 0 if it fails.
-- Rolls back on failure, but consumes on success.
succeeded :: PU.Parser x s a -> PU.Parser e s Int#
{-# inline succeeded #-}
succeeded (PU.Parser f) = PU.Parser
  (\x@(# _, b, c #) s0 -> case f x s0 of
    (# s1, r0 #) -> case r0 of
      (# _ | #) -> (# s1, (# | (# 0#, b, c #) #) #)
      (# | (# _, b1, c1 #) #) -> (# s1, (# | (# 1#, b1, c1 #) #) #)
  )