mini-2.0.1.0: src/Mini/String/URI.hs
-- | An implementation of URI (RFC 3986): <https://doi.org/10.17487/RFC3986>
module Mini.String.URI (
-- * Types
URI (
URI
),
Authority (
Authority
),
Host (
IPv4,
IPv6,
IPvF,
RegName
),
Path (
Path
),
Address (
Address
),
Fragment (
Fragment
),
Port (
Port
),
Query (
Query
),
Scheme (
Scheme
),
Segment (
Segment
),
UserInfo (
UserInfo
),
Version (
Version
),
-- * Parsers
absoluteURI,
genericURI,
relativeURI,
-- * Combinators
relativeTo,
-- * Encoding
encode,
decode,
-- * Normalization
normalizeCase,
normalizeDots,
normalizeEncoding,
) where
import Control.Applicative (
empty,
many,
optional,
some,
(<|>),
)
import Control.Monad (
replicateM,
)
import Data.Bits (
shiftL,
shiftR,
(.&.),
(.|.),
)
import Data.Bool (
Bool,
bool,
)
import Data.Char (
digitToInt,
toLower,
toUpper,
)
import Data.Functor.Identity (
runIdentity,
)
import Data.List (
intercalate,
)
import Data.Maybe (
isJust,
)
import Data.Word (
Word8,
)
import qualified Mini.String.UTF8 as UTF8 (
decode,
encode,
)
import Mini.Transformers.Parser (
ParserT,
atMost,
match,
oneOf,
option,
range,
reject,
sat,
string,
symbol,
)
import Prelude (
Bool (
False,
True
),
Char,
Eq,
Int,
Maybe (
Just,
Nothing
),
Monad,
Ord,
Show,
String,
concat,
concatMap,
drop,
fmap,
fromEnum,
id,
maybe,
not,
null,
pure,
reverse,
show,
toEnum,
uncurry,
($),
(&&),
(*>),
(+),
(.),
(<$),
(<$>),
(<*),
(<*>),
(<=),
(<>),
(>=),
(>>=),
)
-- Types
-- | An absolute, generic, or relative URI
data URI
= URI
(Maybe Scheme)
(Maybe Authority)
Path
(Maybe Query)
(Maybe Fragment)
deriving (Eq, Ord)
instance Show URI where
show (URI s a p q f) =
maybe "" ((<> ":") . show) s
<> maybe "" (("//" <>) . show) a
<> show p
<> maybe "" (("?" <>) . show) q
<> maybe "" (("#" <>) . show) f
-- | The authority component of a URI
data Authority
= -- | without the leading @"\/\/"@
Authority (Maybe UserInfo) Host (Maybe Port)
deriving (Eq, Ord)
instance Show Authority where
show (Authority u h p) =
maybe "" ((<> "@") . show) u
<> show h
<> maybe "" ((":" <>) . show) p
-- | The host component of an authority
data Host
= IPv4 Address
| IPv6 Address
| IPvF Version Address
| RegName Address
deriving (Eq, Ord)
instance Show Host where
show (IPv4 a) = show a
show (IPv6 a) = show a
show (IPvF v a) = "v" <> show v <> "." <> show a
show (RegName a) = show a
-- | The path component of a URI
data Path
= Path
Bool
-- ^ Leading slash
[Segment]
Bool
-- ^ Trailing slash
deriving (Eq, Ord)
instance Show Path where
show (Path l segs t) =
bool "" "/" l
<> intercalate "/" (fmap show segs)
<> bool "" "/" t
-- | The address of a host
newtype Address = Address String
deriving (Eq, Ord)
instance Show Address where
show (Address a) = a
-- | The fragment component of a URI
newtype Fragment
= -- | without the leading @"#"@
Fragment String
deriving (Eq, Ord)
instance Show Fragment where
show (Fragment f) = f
-- | The port component of an authority
newtype Port
= -- | without the leading @":"@
Port String
deriving (Eq, Ord)
instance Show Port where
show (Port p) = p
-- | The query component of a URI
newtype Query
= -- | without the leading @"?"@
Query String
deriving (Eq, Ord)
instance Show Query where
show (Query q) = q
-- | The scheme component of a URI
newtype Scheme
= -- | without the trailing @":"@
Scheme String
deriving (Eq, Ord)
instance Show Scheme where
show (Scheme s) = s
-- | A segment of a path
newtype Segment
= -- | without any delimiting @"\/"@
Segment String
deriving (Eq, Ord)
instance Show Segment where
show (Segment s) = s
-- | The userinfo component of an authority
newtype UserInfo
= -- | without the trailing @"\@"@
UserInfo String
deriving (Eq, Ord)
instance Show UserInfo where
show (UserInfo u) = u
-- | The version of a future IP
newtype Version
= -- | without the leading @"v"@ and trailing @"."@
Version String
deriving (Eq, Ord)
instance Show Version where
show (Version v) = v
-- Parsers
-- | Parse an absolute URI (a generic URI without a fragment)
absoluteURI :: (Monad m) => ParserT Char m URI
absoluteURI =
( \(AbsoluteURI_ s h q) ->
let (a, p) = fromHierPart_ h
in URI (Just $ Scheme s) a p (Query <$> q) Nothing
)
<$> absoluteURI_
-- | Parse a generic URI (an absolute URI with an optional fragment)
genericURI :: (Monad m) => ParserT Char m URI
genericURI =
( \(URI_ s h q f) ->
let (a, p) = fromHierPart_ h
in URI (Just $ Scheme s) a p (Query <$> q) (Fragment <$> f)
)
<$> uri_
-- | Parse a relative URI (a generic URI without a scheme)
relativeURI :: (Monad m) => ParserT Char m URI
relativeURI =
( \(RelativeRef_ r q f) ->
let (a, p) = fromRelativePart_ r
in URI Nothing a p (Query <$> q) (Fragment <$> f)
)
<$> relativeRef_
-- Combinators
-- | Resolve a reference @r@ relative to a base @b@ via @r \`relativeTo\` b@
relativeTo :: URI -> URI -> URI
relativeTo (URI s' a' p'@(Path l' segs' t') q' f') (URI s a p q _) =
bool
( bool
( bool
(URI s a (removeDots $ bool (merge p p') p' l') q' f')
(URI s a p (bool q q' $ isJust q') f')
$ null segs' && not l' && not t'
)
(URI s a' (removeDots p') q' f')
$ isJust a'
)
(URI s' a' (removeDots p') q' f')
$ isJust s'
-- Encoding
-- | Turn a character into a percent-encoded octet sequence in UTF-8 format
encode :: Char -> String
encode = concatMap (('%' :) . twoDigitHex) . UTF8.encode
where
twoDigitHex w = [hex $ w `shiftR` 4, hex $ w .&. 0x0f]
hex n = toEnum . fromEnum . (+ n) . bool 0x37 0x30 $ n <= 9
-- | Parse a percent-encoded octet sequence in UTF-8 format into a character
decode :: (Monad m) => ParserT Char m Char
decode =
next >>= \w0 ->
maybe
( next >>= \w1 ->
maybe
( next >>= \w2 ->
maybe
( next >>= \w3 ->
maybe
empty
pure
. runIdentity
$ match UTF8.decode [w0, w1, w2, w3]
)
pure
. runIdentity
$ match UTF8.decode [w0, w1, w2]
)
pure
. runIdentity
$ match UTF8.decode [w0, w1]
)
pure
. runIdentity
$ match UTF8.decode [w0]
where
next :: (Monad m) => ParserT Char m Word8
next = do
hi <- symbol '%' *> hexdig
lo <- hexdig
pure . toEnum $ (digitToInt hi `shiftL` 4) .|. digitToInt lo
-- Normalization
-- | Lowercase scheme and host components, and uppercase percent-encoded octets
normalizeCase :: URI -> URI
normalizeCase (URI s a p q f) =
URI
(ncScheme <$> s)
(ncAuthority <$> a)
(ncPath p)
(ncQuery <$> q)
(ncFragment <$> f)
where
ncScheme (Scheme s') = Scheme $ fmap toLower s'
ncAuthority (Authority u h p') = Authority u (ncHost h) p'
ncHost (IPv6 (Address addr)) = IPv6 . Address $ fmap toLower addr
ncHost (IPvF (Version v) (Address addr)) =
IPvF (Version $ fmap toLower v) (Address $ fmap toLower addr)
ncHost (RegName (Address addr)) = RegName . Address $ go toLower addr
ncHost h@(IPv4 _) = h
ncPath (Path l segs t) =
Path l (fmap (\(Segment seg) -> Segment $ go id seg) segs) t
ncQuery (Query q') = Query $ go id q'
ncFragment (Fragment f') = Fragment $ go id f'
go z ('%' : h1 : h2 : rest) = '%' : toUpper h1 : toUpper h2 : go z rest
go z (c : rest) = z c : go z rest
go _ [] = []
-- | Remove dot segments
normalizeDots :: URI -> URI
normalizeDots (URI s a p q f) = URI s a (removeDots p) q f
-- | Decode percent-encoded octets that correspond to unreserved characters
normalizeEncoding :: URI -> URI
normalizeEncoding (URI s a p q f) =
URI
s
(neAuthority <$> a)
(nePath p)
(neQuery <$> q)
(neFragment <$> f)
where
neAuthority (Authority u h p') = Authority (neUserInfo <$> u) h p'
where
neUserInfo (UserInfo u') = UserInfo $ go u'
nePath (Path l segs t) =
Path
l
(fmap (\(Segment seg) -> Segment $ go seg) segs)
t
neQuery (Query q') = Query $ go q'
neFragment (Fragment f') = Fragment $ go f'
go ('%' : h1 : h2 : rest) =
maybe ('%' : h1 : h2 : go rest) (: go rest)
. maybe
Nothing
(runIdentity . match unreserved . pure)
. runIdentity
$ match decode ['%', h1, h2]
go (c : rest) = c : go rest
go [] = []
-- Syntax
-- URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
data URI_ = URI_ String HierPart_ (Maybe String) (Maybe String)
uri_ :: (Monad m) => ParserT Char m URI_
uri_ =
URI_
<$> scheme
<* symbol ':'
<*> hierPart_
<*> optional (symbol '?' *> query)
<*> optional (symbol '#' *> fragment)
-- hier-part
-- = "//" authority path-abempty
-- / path-absolute
-- / path-rootless
-- / path-empty
data HierPart_
= HierPartAbempty_ Authority_ [String]
| HierPartAbsolute_ [String]
| HierPartRootless_ [String]
| HierPartEmpty_
hierPart_ :: (Monad m) => ParserT Char m HierPart_
hierPart_ =
(HierPartAbempty_ <$> (string "//" *> authority_) <*> path_abempty)
<|> (HierPartAbsolute_ <$> path_absolute)
<|> (HierPartRootless_ <$> path_rootless)
<|> (HierPartEmpty_ <$ path_empty)
-- absolute-URI = scheme ":" hier-part [ "?" query ]
data AbsoluteURI_ = AbsoluteURI_ String HierPart_ (Maybe String)
absoluteURI_ :: (Monad m) => ParserT Char m AbsoluteURI_
absoluteURI_ =
AbsoluteURI_
<$> scheme
<* symbol ':'
<*> hierPart_
<*> optional (symbol '?' *> query)
-- relative-ref = relative-part [ "?" query ] [ "#" fragment ]
data RelativeRef_ = RelativeRef_ RelativePart_ (Maybe String) (Maybe String)
relativeRef_ :: (Monad m) => ParserT Char m RelativeRef_
relativeRef_ =
RelativeRef_
<$> relativePart_
<*> optional (symbol '?' *> query)
<*> optional (symbol '#' *> fragment)
-- relative-part
-- = "//" authority path-abempty
-- / path-absolute
-- / path-noscheme
-- / path-empty
data RelativePart_
= RelativePartAbempty_ Authority_ [String]
| RelativePartAbsolute_ [String]
| RelativePartNoScheme_ [String]
| RelativePartEmpty_
relativePart_ :: (Monad m) => ParserT Char m RelativePart_
relativePart_ =
(RelativePartAbempty_ <$> (string "//" *> authority_) <*> path_abempty)
<|> (RelativePartAbsolute_ <$> path_absolute)
<|> (RelativePartNoScheme_ <$> path_noscheme)
<|> (RelativePartEmpty_ <$ path_empty)
-- scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
scheme :: (Monad m) => ParserT Char m String
scheme = (:) <$> alpha <*> many (alpha <|> digit <|> oneOf "+-.")
-- authority = [ userinfo "@" ] host [ ":" port ]
data Authority_ = Authority_ (Maybe String) Host_ (Maybe String)
authority_ :: (Monad m) => ParserT Char m Authority_
authority_ =
Authority_
<$> optional (userinfo <* symbol '@')
<*> host_
<*> optional (symbol ':' *> port)
-- userinfo = *( unreserved / pct-encoded / sub-delims / ":" )
userinfo :: (Monad m) => ParserT Char m String
userinfo =
concat
<$> many
( fmap pure unreserved
<|> pct_encoded
<|> fmap pure sub_delims
<|> string ":"
)
-- host = IP-literal / IPv4address / reg-name
data Host_
= HostIPLiteral_ IPLiteral_
| HostIPv4Address_ String
| HostRegName_ String
host_ :: (Monad m) => ParserT Char m Host_
host_ =
(HostIPLiteral_ <$> ipLiteral_)
<|> (HostIPv4Address_ <$> ipv4address)
<|> (HostRegName_ <$> reg_name)
-- port = *DIGIT
port :: (Monad m) => ParserT Char m String
port = many digit
-- IP-literal = "[" ( IPv6address / IPvFuture ) "]"
data IPLiteral_
= IPLiteralIPv6Address_ String
| IPLiteralIPvFuture_ IPvFuture_
ipLiteral_ :: (Monad m) => ParserT Char m IPLiteral_
ipLiteral_ =
symbol '['
*> ( (IPLiteralIPv6Address_ <$> ipv6address)
<|> (IPLiteralIPvFuture_ <$> ipvFuture_)
)
<* symbol ']'
-- IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" )
-- NOTE: ABNF strings are case-insensitive
data IPvFuture_ = IPvFuture_ String String
ipvFuture_ :: (Monad m) => ParserT Char m IPvFuture_
ipvFuture_ =
IPvFuture_
<$> (oneOf "vV" *> some hexdig <* symbol '.')
<*> some (unreserved <|> sub_delims <|> symbol ':')
-- IPv6address
-- = 6( h16 ":" ) ls32
-- / "::" 5( h16 ":" ) ls32
-- / [ h16 ] "::" 4( h16 ":" ) ls32
-- / [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32
-- / [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32
-- / [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32
-- / [ *4( h16 ":" ) h16 ] "::" ls32
-- / [ *5( h16 ":" ) h16 ] "::" h16
-- / [ *6( h16 ":" ) h16 ] "::"
-- NOTE: parsing [ *n( h16 ":" ) h16 ]
-- as [ h16 *n( ":" h16 ) ]
-- to avoid consuming part of "::"
ipv6address :: (Monad m) => ParserT Char m String
ipv6address =
(fmap concat (replicateM 6 $ h16 <> string ":") <> ls32)
<|> ( string "::"
<> fmap concat (replicateM 5 $ h16 <> string ":")
<> ls32
)
<|> ( option [] h16
<> string "::"
<> fmap concat (replicateM 4 $ h16 <> string ":")
<> ls32
)
<|> ( option [] (h16 <> fmap concat (atMost 1 $ string ":" <> h16))
<> string "::"
<> fmap concat (replicateM 3 $ h16 <> string ":")
<> ls32
)
<|> ( option [] (h16 <> fmap concat (atMost 2 $ string ":" <> h16))
<> string "::"
<> fmap concat (replicateM 2 $ h16 <> string ":")
<> ls32
)
<|> ( option [] (h16 <> fmap concat (atMost 3 $ string ":" <> h16))
<> string "::"
<> h16
<> string ":"
<> ls32
)
<|> ( option [] (h16 <> fmap concat (atMost 4 $ string ":" <> h16))
<> string "::"
<> ls32
)
<|> ( option [] (h16 <> fmap concat (atMost 5 $ string ":" <> h16))
<> string "::"
<> h16
)
<|> ( option [] (h16 <> fmap concat (atMost 6 $ string ":" <> h16))
<> string "::"
)
-- h16 = 1*4HEXDIG
h16 :: (Monad m) => ParserT Char m String
h16 = range 1 4 hexdig
-- ls32 = ( h16 ":" h16 ) / IPv4address
ls32 :: (Monad m) => ParserT Char m String
ls32 = (h16 <> string ":" <> h16) <|> ipv4address
-- IPv4address = dec-octet "." dec-octet "." dec-octet "." dec-octet
ipv4address :: (Monad m) => ParserT Char m String
ipv4address =
dec_octet
<> string "."
<> dec_octet
<> string "."
<> dec_octet
<> string "."
<> dec_octet
-- dec-octet
-- = DIGIT
-- / %x31-39 DIGIT
-- / "1" 2DIGIT
-- / "2" %x30-34 DIGIT
-- / "25" %x30-35
-- NOTE: ordered by descending length below
-- to avoid stopping early
dec_octet :: (Monad m) => ParserT Char m String
dec_octet =
(string "1" <> replicateM 2 digit)
<|> (string "2" <> fmap pure (octetRange (0x30, 0x34)) <> fmap pure digit)
<|> (string "25" <> fmap pure (octetRange (0x30, 0x35)))
<|> (fmap pure (octetRange (0x31, 0x39)) <> fmap pure digit)
<|> (fmap pure digit)
-- reg-name = *( unreserved / pct-encoded / sub-delims )
reg_name :: (Monad m) => ParserT Char m String
reg_name =
concat
<$> many (fmap pure unreserved <|> pct_encoded <|> fmap pure sub_delims)
-- path-abempty = *( "/" segment )
path_abempty :: (Monad m) => ParserT Char m [String]
path_abempty = many (symbol '/' *> segment)
-- path-absolute = "/" [ segment-nz *( "/" segment ) ]
path_absolute :: (Monad m) => ParserT Char m [String]
path_absolute =
symbol '/' *> option [] ((:) <$> segment_nz <*> many (symbol '/' *> segment))
-- path-noscheme = segment-nz-nc *( "/" segment )
path_noscheme :: (Monad m) => ParserT Char m [String]
path_noscheme = (:) <$> segment_nz_nc <*> many (symbol '/' *> segment)
-- path-rootless = segment-nz *( "/" segment )
path_rootless :: (Monad m) => ParserT Char m [String]
path_rootless = (:) <$> segment_nz <*> many (symbol '/' *> segment)
-- path-empty = 0<pchar>
path_empty :: (Monad m) => ParserT Char m [String]
path_empty = [] <$ reject pchar
-- segment = *pchar
segment :: (Monad m) => ParserT Char m String
segment = concat <$> many pchar
-- segment-nz = 1*pchar
segment_nz :: (Monad m) => ParserT Char m String
segment_nz = concat <$> some pchar
-- segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" )
segment_nz_nc :: (Monad m) => ParserT Char m String
segment_nz_nc =
concat
<$> some
( fmap pure unreserved
<|> pct_encoded
<|> fmap pure sub_delims
<|> string "@"
)
-- pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
pchar :: (Monad m) => ParserT Char m String
pchar =
fmap pure unreserved
<|> pct_encoded
<|> fmap pure sub_delims
<|> string ":"
<|> string "@"
-- query = *( pchar / "/" / "?" )
query :: (Monad m) => ParserT Char m String
query = concat <$> many (pchar <|> string "/" <|> string "?")
-- fragment = *( pchar / "/" / "?" )
fragment :: (Monad m) => ParserT Char m String
fragment = concat <$> many (pchar <|> string "/" <|> string "?")
-- pct-encoded = "%" HEXDIG HEXDIG
pct_encoded :: (Monad m) => ParserT Char m String
pct_encoded = string "%" <> replicateM 2 hexdig
-- unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
unreserved :: (Monad m) => ParserT Char m Char
unreserved = alpha <|> digit <|> oneOf "-._~"
-- sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
sub_delims :: (Monad m) => ParserT Char m Char
sub_delims = oneOf "!$&'()*+,;="
-- ALPHA = %x41-5A / %x61-7A
alpha :: (Monad m) => ParserT Char m Char
alpha = octetRange (0x41, 0x5A) <|> octetRange (0x61, 0x7A)
-- DIGIT = %x30-39
digit :: (Monad m) => ParserT Char m Char
digit = octetRange (0x30, 0x39)
-- HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
-- NOTE: ABNF strings are case-insensitive
hexdig :: (Monad m) => ParserT Char m Char
hexdig = digit <|> oneOf (['A' .. 'F'] <> ['a' .. 'f'])
-- Helpers
octetRange :: (Monad m) => (Int, Int) -> ParserT Char m Char
octetRange (lo, hi) = sat $ (\n -> n >= lo && n <= hi) . fromEnum
fromHierPart_ :: HierPart_ -> (Maybe Authority, Path)
fromHierPart_ (HierPartAbempty_ a segs) =
(Just $ fromAuthority_ a, bool (mkAbsolute segs) mkEmpty $ null segs)
fromHierPart_ (HierPartAbsolute_ segs) = (Nothing, mkAbsolute segs)
fromHierPart_ (HierPartRootless_ segs) = (Nothing, mkRootless segs)
fromHierPart_ HierPartEmpty_ = (Nothing, mkEmpty)
fromAuthority_ :: Authority_ -> Authority
fromAuthority_ (Authority_ u h p) =
Authority
(UserInfo <$> u)
( case h of
HostIPLiteral_ (IPLiteralIPv6Address_ addr) -> IPv6 (Address addr)
HostIPLiteral_ (IPLiteralIPvFuture_ (IPvFuture_ v addr)) ->
IPvF (Version v) (Address addr)
HostIPv4Address_ addr -> IPv4 (Address addr)
HostRegName_ addr -> RegName (Address addr)
)
(Port <$> p)
fromRelativePart_ :: RelativePart_ -> (Maybe Authority, Path)
fromRelativePart_ (RelativePartAbempty_ a segs) =
(Just $ fromAuthority_ a, bool (mkAbsolute segs) mkEmpty $ null segs)
fromRelativePart_ (RelativePartAbsolute_ segs) =
(Nothing, mkAbsolute segs)
fromRelativePart_ (RelativePartNoScheme_ segs) =
(Nothing, mkRootless segs)
fromRelativePart_ RelativePartEmpty_ = (Nothing, mkEmpty)
mkEmpty :: Path
mkEmpty = Path False [] False
mkAbsolute :: [String] -> Path
mkAbsolute ("" : []) = Path True [] False -- edge case: path_abempty "/"
mkAbsolute segs = mkPath (Path True) segs
mkRootless :: [String] -> Path
mkRootless = mkPath (Path False)
mkPath :: ([Segment] -> Bool -> Path) -> [String] -> Path
mkPath c = uncurry c . go
where
go ("" : []) = ([], True)
go (x : xs) =
let (xs', t) = go xs
in (Segment x : xs', t)
go [] = ([], False)
merge :: Path -> Path -> Path
merge (Path l segs t) (Path _ segs' t') =
bool
(Path l (bool (go segs segs') (segs <> segs') t) t')
(Path False segs' t')
$ null segs && not l && not t
where
go (_ : []) xs' = xs'
go (x : xs) xs' = x : go xs xs'
go [] xs' = xs'
removeDots :: Path -> Path
removeDots (Path l segs t) = uncurry (Path l) $ go [] segs
where
go os (Segment "." : []) = (reverse os, not $ null os)
go os (Segment "." : is) = go os is
go os (Segment ".." : []) = (reverse $ drop 1 os, not . null $ drop 1 os)
go os (Segment ".." : is) = go (drop 1 os) is
go os (i : is) = go (i : os) is
go os [] = (reverse os, t)