packages feed

uri-bytestring 0.2.1.0 → 0.2.1.1

raw patch · 7 files changed

+331/−27 lines, 7 filesdep +containersPVP: minor bump suggested

API additions: PVP suggests at least a minor version bump

Dependencies added: containers

API changes (from Hackage documentation)

+ URI.ByteString: URINormalizationOptions :: Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Map Scheme Port -> URINormalizationOptions
+ URI.ByteString: aggressiveNormalization :: URINormalizationOptions
+ URI.ByteString: data URINormalizationOptions
+ URI.ByteString: httpDefaultPorts :: Map Scheme Port
+ URI.ByteString: httpNormalization :: URINormalizationOptions
+ URI.ByteString: noNormalization :: URINormalizationOptions
+ URI.ByteString: normalizeURIRef :: URINormalizationOptions -> URIRef a -> Builder
+ URI.ByteString: normalizeURIRef' :: URINormalizationOptions -> URIRef a -> ByteString
+ URI.ByteString: rfc3986Normalization :: URINormalizationOptions
+ URI.ByteString: unoDefaultPorts :: URINormalizationOptions -> Map Scheme Port
+ URI.ByteString: unoDowncaseHost :: URINormalizationOptions -> Bool
+ URI.ByteString: unoDowncaseScheme :: URINormalizationOptions -> Bool
+ URI.ByteString: unoDropDefPort :: URINormalizationOptions -> Bool
+ URI.ByteString: unoDropExtraSlashes :: URINormalizationOptions -> Bool
+ URI.ByteString: unoRemoveDotSegments :: URINormalizationOptions -> Bool
+ URI.ByteString: unoSlashEmptyPath :: URINormalizationOptions -> Bool
+ URI.ByteString: unoSortParameters :: URINormalizationOptions -> Bool

Files

changelog.md view
@@ -1,3 +1,6 @@+0.2.1.1+* Add URI normalization features.+ 0.2.1.0 * Widen dependency on base. 
src/URI/ByteString.hs view
@@ -39,6 +39,12 @@     , URIParserOptions(..)     , strictURIParserOptions     , laxURIParserOptions+    , URINormalizationOptions(..)+    , noNormalization+    , rfc3986Normalization+    , httpNormalization+    , aggressiveNormalization+    , httpDefaultPorts     -- * Operations     , toAbsolute     -- * Parsing@@ -49,6 +55,9 @@     -- * Serializing     , serializeURIRef     , serializeURIRef'+    -- ** Normalized Serialization+    , normalizeURIRef+    , normalizeURIRef'     -- * Low level utility functions     , urlDecode     , urlDecodeQuery
src/URI/ByteString/Internal.hs view
@@ -16,12 +16,15 @@ import           Data.Bits import           Data.ByteString                    (ByteString) import qualified Data.ByteString                    as BS-import           Data.Char                          (ord)+import qualified Data.ByteString.Char8              as BS8+import           Data.Char                          (ord, toLower) import           Data.Ix import           Data.List                          (delete, intersperse,-                                                     stripPrefix, (\\))+                                                     sortBy, stripPrefix, (\\))+import qualified Data.Map.Strict                    as M import           Data.Maybe import           Data.Monoid+import           Data.Ord                           (comparing) import           Data.Word import           Text.Read                          (readMaybe) -------------------------------------------------------------------------------@@ -51,12 +54,60 @@   -------------------------------------------------------------------------------+-- | All normalization options disabled+noNormalization :: URINormalizationOptions+noNormalization = URINormalizationOptions False False False False False False False httpDefaultPorts+++-------------------------------------------------------------------------------+-- | The set of known default ports to schemes. Currently only+-- contains http\/80 and https\/443. Feel free to extend it if needed+-- with 'unoDefaultPorts'.+httpDefaultPorts :: M.Map Scheme Port+httpDefaultPorts = M.fromList [ (Scheme "http", Port 80)+                              , (Scheme "https", Port 443)+                              ]+++-------------------------------------------------------------------------------+-- | Only normalizations deemed appropriate for all protocols by+-- RFC3986 enabled, namely:+--+-- * Downcase Scheme+-- * Downcase Host+-- * Remove Dot Segments+rfc3986Normalization :: URINormalizationOptions+rfc3986Normalization = noNormalization { unoDowncaseScheme = True+                                       , unoDowncaseHost = True+                                       , unoRemoveDotSegments = True+                                       }+++-------------------------------------------------------------------------------+-- | The same as 'rfc3986Normalization' but with additional enabled+-- features if you're working with HTTP URIs:+--+-- * Drop Default Port (with 'httpDefaultPorts')+-- * Drop Extra Slashes+httpNormalization :: URINormalizationOptions+httpNormalization = rfc3986Normalization { unoDropDefPort = True+                                         , unoSlashEmptyPath = True+                                         }++-------------------------------------------------------------------------------+-- | All options enabled+aggressiveNormalization :: URINormalizationOptions+aggressiveNormalization = URINormalizationOptions True True True True True True True httpDefaultPorts+++------------------------------------------------------------------------------- -- | @toAbsolute scheme ref@ converts @ref@ to an absolute URI. -- If @ref@ is already absolute, then it is unchanged. toAbsolute :: Scheme -> URIRef a -> URIRef Absolute toAbsolute scheme (RelativeRef {..}) = URI scheme rrAuthority rrPath rrQuery rrFragment toAbsolute _ uri@(URI {..}) = uri + ------------------------------------------------------------------------------- -- | URI Serializer -------------------------------------------------------------------------------@@ -68,59 +119,168 @@ -- >>> BB.toLazyByteString $ serializeURIRef $ URI {uriScheme = Scheme {schemeBS = "http"}, uriAuthority = Just (Authority {authorityUserInfo = Nothing, authorityHost = Host {hostBS = "www.example.org"}, authorityPort = Nothing}), uriPath = "/foo", uriQuery = Query {queryPairs = [("bar","baz")]}, uriFragment = Just "quux"} -- "http://www.example.org/foo?bar=baz#quux" serializeURIRef :: URIRef a -> Builder-serializeURIRef uri@(URI {..}) = serializeURI uri-serializeURIRef uri@(RelativeRef {..}) = serializeRelativeRef uri+serializeURIRef = normalizeURIRef noNormalization ++------------------------------------------------------------------------------- -- | Like 'serializeURIRef', with conversion into a strict 'ByteString'. serializeURIRef' :: URIRef a -> ByteString serializeURIRef' = BB.toByteString . serializeURIRef ++------------------------------------------------------------------------------- -- | Serialize a URI into a Builder. serializeURI :: URIRef Absolute -> Builder-serializeURI URI {..} = scheme <> BB.fromString ":" <> serializeRelativeRef rr+serializeURI = normalizeURIRef noNormalization+{-# DEPRECATED serializeURI "Use 'serializeURIRef' instead" #-}+++-------------------------------------------------------------------------------+-- | Similar to 'serializeURIRef' but performs configurable degrees of+-- URI normalization. If your goal is the fastest serialization speed+-- possible, 'serializeURIRef' will be fine. If you intend on+-- comparing URIs (say for caching purposes), you'll want to use this.+normalizeURIRef :: URINormalizationOptions -> URIRef a -> Builder+normalizeURIRef o uri@(URI {..}) = normalizeURI o uri+normalizeURIRef o uri@(RelativeRef {}) = normalizeRelativeRef o Nothing uri+++-------------------------------------------------------------------------------+normalizeURIRef' :: URINormalizationOptions -> URIRef a -> ByteString+normalizeURIRef' o = BB.toByteString . normalizeURIRef o+++-------------------------------------------------------------------------------+normalizeURI :: URINormalizationOptions -> URIRef Absolute -> Builder+normalizeURI o@URINormalizationOptions {..} URI {..} =+  scheme <> BB.fromString ":" <> normalizeRelativeRef o (Just uriScheme) rr   where-    scheme = bs $ schemeBS uriScheme+    scheme = bs (sCase (schemeBS uriScheme))+    sCase+      | unoDowncaseScheme = downcaseBS+      | otherwise = id     rr = RelativeRef uriAuthority uriPath uriQuery uriFragment-{-# DEPRECATED serializeURI "Use 'serializeURIRef' instead" #-} ++-------------------------------------------------------------------------------+normalizeRelativeRef :: URINormalizationOptions -> Maybe Scheme -> URIRef Relative -> Builder+normalizeRelativeRef o@URINormalizationOptions {..} mScheme RelativeRef {..} =+  authority <> path <> query <> fragment+  where+    path+      | unoSlashEmptyPath && BS.null rrPath = "/"+      | otherwise  = mconcat (intersperse (c8 '/') (map urlEncodePath segs))+    segs = dropSegs (BS.split slash (pathRewrite rrPath))+    pathRewrite+      | unoRemoveDotSegments = removeDotSegments+      | otherwise = id+    dropSegs [] = []+    dropSegs (h:t)+      | unoDropExtraSlashes = h:(filter (not . BS.null) t)+      | otherwise = h:t+    authority = maybe mempty (serializeAuthority o mScheme) rrAuthority+    query = serializeQuery o rrQuery+    fragment = maybe mempty (\s -> c8 '#' <> bs s) rrFragment+++-------------------------------------------------------------------------------+--TODO: this is probably ripe for benchmarking+-- | Algorithm described in+-- <https://tools.ietf.org/html/rfc3986#section-5.2.4>, reproduced+-- artlessly.+removeDotSegments :: ByteString -> ByteString+removeDotSegments path = mconcat (rl2L (go path (RL [])))+  where+    go inBuf outBuf+      -- A. If the input buffer begins with prefix of ../ or ./ then+      -- remove the prefix from the input buffer+      | BS8.isPrefixOf "../" inBuf = go (BS8.drop 3 inBuf) outBuf+      | BS8.isPrefixOf "./" inBuf  = go (BS8.drop 2 inBuf) outBuf+      -- B. If the input buffer begins with a prefix of "/./" or "/.",+      -- where "." is a complete path segment, then replace that+      -- prefix with "/" in the input buffer. TODO: I think "a+      -- complete path segment" means its the whole thing?+      | BS.isPrefixOf "/./" inBuf = go (BS8.drop 2 inBuf) outBuf+      | inBuf == "/." = go "/" outBuf+      -- C. If the input buffer begins with a prefix of "/../" or+      -- "/..", where ".." is a complete path segment, then replace+      -- that prefix with "/" in the input buffer and remove the last+      -- segment and its preceding "/" (if any) from the output buffer+      | BS.isPrefixOf "/../" inBuf = go (BS8.drop 3 inBuf) (unsnoc (unsnoc outBuf))+      | inBuf == "/.." = go "/" (unsnoc (unsnoc outBuf))+      -- D. If the input buffer consists only of "." or "..", then+      -- remove that from the input buffer+      | inBuf == "." = go mempty outBuf+      | inBuf == ".." = go mempty outBuf+      -- E. Move the first path segment in the input buffer to the end+      -- of the output buffer, including the initial "/" character (if+      -- any) and any subsequent characters up to, but not including,+      -- the next "/" character or the end of the input buffer.+      | otherwise = case BS8.uncons inBuf of+                      Just ('/', rest) ->+                        let (thisSeg, inBuf') = BS8.span (/= '/') rest+                        in go inBuf' (outBuf |> "/" |> thisSeg)+                      Just (_, _) ->+                        let (thisSeg, inBuf') = BS8.span (/= '/') inBuf+                        in go inBuf' (outBuf |> thisSeg)+                      Nothing -> outBuf++++------------------------------------------------------------------------------- -- | Like 'serializeURI', with conversion into a strict 'ByteString'. serializeURI' :: URIRef Absolute -> ByteString serializeURI' = BB.toByteString . serializeURI {-# DEPRECATED serializeURI' "Use 'serializeURIRef'' instead" #-} ++------------------------------------------------------------------------------- -- | Like 'serializeURI', but do not render scheme. serializeRelativeRef :: URIRef Relative -> Builder-serializeRelativeRef RelativeRef {..} = authority <> path <> query <> fragment-  where-    path = mconcat $ intersperse (c8 '/') $ map urlEncodePath segs-    segs = BS.split slash rrPath-    authority = maybe mempty serializeAuthority rrAuthority-    query = serializeQuery rrQuery-    fragment = maybe mempty (\s -> c8 '#' <> bs s) rrFragment+serializeRelativeRef = normalizeRelativeRef noNormalization Nothing {-# DEPRECATED serializeRelativeRef "Use 'serializeURIRef' instead" #-} ++------------------------------------------------------------------------------- -- | Like 'serializeRelativeRef', with conversion into a strict 'ByteString'. serializeRelativeRef' :: URIRef Relative -> ByteString serializeRelativeRef' = BB.toByteString . serializeRelativeRef {-# DEPRECATED serializeRelativeRef' "Use 'serializeURIRef'' instead" #-} + --------------------------------------------------------------------------------serializeQuery :: Query -> Builder-serializeQuery (Query []) = mempty-serializeQuery (Query ps) =-    c8 '?' <> mconcat (intersperse (c8 '&') (map serializePair ps))+serializeQuery :: URINormalizationOptions -> Query -> Builder+serializeQuery _ (Query []) = mempty+serializeQuery URINormalizationOptions {..} (Query ps) =+    c8 '?' <> mconcat (intersperse (c8 '&') (map serializePair ps'))   where     serializePair (k, v) = urlEncodeQuery k <> c8 '=' <> urlEncodeQuery v+    ps'+      | unoSortParameters = sortBy (comparing fst) ps+      | otherwise = ps   --------------------------------------------------------------------------------serializeAuthority :: Authority -> Builder-serializeAuthority Authority {..} = BB.fromString "//" <> userinfo <> bs host <> port+serializeAuthority :: URINormalizationOptions -> Maybe Scheme -> Authority -> Builder+serializeAuthority URINormalizationOptions {..} mScheme Authority {..} = BB.fromString "//" <> userinfo <> bs host <> port   where     userinfo = maybe mempty serializeUserInfo authorityUserInfo-    host = hostBS authorityHost-    port = maybe mempty packPort authorityPort+    host = hCase (hostBS authorityHost)+    hCase+      | unoDowncaseHost = downcaseBS+      | otherwise = id+    port = maybe mempty packPort effectivePort+    effectivePort = do+      p <- authorityPort+      scheme <- mScheme+      dropPort scheme p     packPort (Port p) = c8 ':' <> BB.fromString (show p)+    dropPort scheme+      | unoDropDefPort = dropPort' scheme+      | otherwise = Just+    dropPort' s p+      | M.lookup s unoDefaultPorts == Just p = Nothing+      | otherwise = Just p   -------------------------------------------------------------------------------@@ -704,10 +864,10 @@ ------------------------------------------------------------------------------- -- | Stronger-typed variation of parseOnly'. Consumes all input. parseOnly' :: (Read e)-              => (String -> e) -- ^ Fallback if we can't parse a failure message for the sake of totality.-              -> Parser' e a-              -> ByteString-              -> Either e a+           => (String -> e) -- ^ Fallback if we can't parse a failure message for the sake of totality.+           -> Parser' e a+           -> ByteString+           -> Either e a parseOnly' noParse (Parser' p) = fmapL readWithFallback . parseOnly p   where     readWithFallback s = fromMaybe (noParse s) (readMaybe . stripAttoparsecGarbage $ s)@@ -793,3 +953,26 @@ -- | Encode a ByteString for use in the path section of a URL urlEncodePath :: ByteString -> Builder urlEncodePath = urlEncode unreservedPath8+++-------------------------------------------------------------------------------+downcaseBS :: ByteString -> ByteString+downcaseBS = BS8.map toLower+++-------------------------------------------------------------------------------+-- | Simple data structure to get O(1) prepends on a list and defers the O(n)+newtype RL a = RL [a] deriving (Show)+++(|>) :: RL a -> a -> RL a+RL as |> a = RL (a:as)+++rl2L :: RL a -> [a]+rl2L (RL as) = reverse as+++unsnoc :: RL a -> RL a+unsnoc (RL [])     = RL []+unsnoc (RL (_:xs)) = RL xs
src/URI/ByteString/Types.hs view
@@ -8,6 +8,7 @@  ------------------------------------------------------------------------------- import           Data.ByteString (ByteString)+import qualified Data.Map.Strict as M import           Data.Monoid import           Data.Typeable import           Data.Word@@ -101,6 +102,27 @@ data URIParserOptions = URIParserOptions {       upoValidQueryChar :: Word8 -> Bool     }+++-------------------------------------------------------------------------------+data URINormalizationOptions = URINormalizationOptions {+      unoDowncaseScheme    :: Bool+    -- ^ hTtP -> http+    , unoDowncaseHost      :: Bool+    -- ^ eXaMpLe.org -> example.org+    , unoDropDefPort       :: Bool+    -- ^ If the scheme is known and the port is the default (e.g. 80 for http) it is removed.+    , unoSlashEmptyPath    :: Bool+    -- ^ If the path is empty, set it to \/+    , unoDropExtraSlashes  :: Bool+    -- ^ Rewrite path from \/foo\/\/bar\/\/\/baz to \/foo\/bar\/baz+    , unoSortParameters    :: Bool+    -- ^ Sorts parameters by parameter name+    , unoRemoveDotSegments :: Bool+    -- ^ Remove dot segments as per <https://tools.ietf.org/html/rfc3986#section-5.2.4 RFC3986 Section 5.2.4>+    , unoDefaultPorts      :: M.Map Scheme Port+    -- ^ Map of known schemes to their default ports. Used when 'unoDropDefPort' is enabled.+    } deriving (Show, Eq)   -------------------------------------------------------------------------------
test/URI/ByteString/Arbitrary.hs view
@@ -63,5 +63,15 @@   arbitrary = URIParserOptions <$> arbitrary  +instance Arbitrary URINormalizationOptions where+  arbitrary = URINormalizationOptions <$> arbitrary+                                      <*> arbitrary+                                      <*> arbitrary+                                      <*> arbitrary+                                      <*> arbitrary+                                      <*> arbitrary+                                      <*> arbitrary+                                      <*> arbitrary+ $(derive makeArbitrary ''SchemaError) $(derive makeArbitrary ''URIParseError)
test/URI/ByteStringTests.hs view
@@ -7,6 +7,8 @@ import qualified Blaze.ByteString.Builder as BB import           Data.ByteString          (ByteString) import qualified Data.ByteString.Char8    as B8+import           Data.Either+import qualified Data.Map.Strict          as M import           Data.Monoid import           Lens.Simple import           Test.Tasty@@ -26,6 +28,7 @@   , uriParseErrorInstancesTests   , lensTests   , serializeURITests+  , normalizeURITests   ]  @@ -332,3 +335,75 @@        let res = BB.toLazyByteString (serializeURIRef uri)        res @?= "http://www.example.org/weird%20path"   ]+++-------------------------------------------------------------------------------+normalizeURITests :: TestTree+normalizeURITests = testGroup "normalization"+  [+    testCase "downcase schema" $ do+      normalizeURIBS o { unoDowncaseScheme = True } "hTtP://example.org" @?=+       "http://example.org"++  , testCase "downcase host" $ do+      normalizeURIBS o { unoDowncaseHost = True } "http://ExAmPlE.org" @?=+       "http://example.org"++  , testCase "drop default port http" $ do+      normalizeURIBS o { unoDropDefPort = True } "http://example.org:80" @?=+       "http://example.org"+  , testCase "drop default port https" $ do+      normalizeURIBS o { unoDropDefPort = True } "https://example.org:443" @?=+       "https://example.org"+  , testCase "drop default port no port" $ do+      normalizeURIBS o { unoDropDefPort = True } "http://example.org" @?=+       "http://example.org"+  , testCase "drop default port nondefault" $ do+      normalizeURIBS o { unoDropDefPort = True } "http://example.org:8000" @?=+       "http://example.org:8000"+  , testCase "drop default unknown schema" $ do+      normalizeURIBS o { unoDropDefPort = True } "bogus://example.org:9999" @?=+       "bogus://example.org:9999"+  , testCase "user-extensable port defaulting hit" $ do+      normalizeURIBS o { unoDropDefPort = True+                       , unoDefaultPorts = M.singleton (Scheme "ftp") (Port 21) }+                       "ftp://example.org:21" @?=+       "ftp://example.org"+  , testCase "user-extensable port defaulting off" $ do+      normalizeURIBS o { unoDropDefPort = False+                       , unoDefaultPorts = M.singleton (Scheme "ftp") (Port 21) }+                       "ftp://example.org:21" @?=+       "ftp://example.org:21"+  , testCase "user-extensable port defaulting miss" $ do+      normalizeURIBS o { unoDropDefPort = True+                       , unoDefaultPorts = M.singleton (Scheme "ftp") (Port 21) }+                       "http://example.org:80" @?=+       "http://example.org:80"++  , testCase "slash empty path" $ do+      normalizeURIBS o { unoSlashEmptyPath = True } "http://example.org" @?=+        "http://example.org/"+  , testCase "slash empty path with nonempty path" $ do+      normalizeURIBS o { unoSlashEmptyPath = True } "http://example.org/foo/bar" @?=+        "http://example.org/foo/bar"++  , testCase "drop redundant slashes" $ do+      normalizeURIBS o { unoDropExtraSlashes = True } "http://example.org/foo//bar///baz" @?=+        "http://example.org/foo/bar/baz"++  , testCase "sort params" $ do+      normalizeURIBS o { unoSortParameters = True } "http://example.org/foo?zulu=1&charlie=&alpha=1" @?=+        "http://example.org/foo?alpha=1&charlie=&zulu=1"++  , testCase "remove dot segments" $ do+      normalizeURIBS o { unoRemoveDotSegments = True } "http://example.org/a/b/c/./../../g" @?=+        "http://example.org/a/g"++  , testCase "percent encoding is upcased automatically" $ do+      normalizeURIBS o "http://example.org/a?foo%3abar=baz" @?=+        "http://example.org/a?foo%3Abar=baz"+  ]+  where+    o = noNormalization+    normalizeURIBS opts bs = let Right x = parseURI laxURIParserOptions bs+                             in normalizeURIRef' opts x
uri-bytestring.cabal view
@@ -1,5 +1,5 @@ name:                uri-bytestring-version:             0.2.1.0+version:             0.2.1.1 synopsis:            Haskell URI parsing as ByteStrings description: uri-bytestring aims to be an RFC3986 compliant URI parser that uses efficient ByteStrings for parsing and representing the URI data. license:             BSD3@@ -40,6 +40,7 @@     , base             >= 4.6     && < 4.10     , bytestring       >= 0.9.1   && < 0.11     , blaze-builder    >= 0.3.0.0 && < 0.5+    , containers    hs-source-dirs:      src   default-language:    Haskell2010@@ -74,6 +75,7 @@     , lens-simple     , quickcheck-instances     , semigroups+    , containers   default-language:    Haskell2010    if flag(lib-Werror)