packages feed

dns-patterns 0.1.3 → 0.2.0

raw patch · 7 files changed

+550/−339 lines, 7 filesdep ~bytestringPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: bytestring

API changes (from Hackage documentation)

- Network.DNS.Pattern: DomGlob :: LabelPattern
- Network.DNS.Pattern: DomGlobStar :: LabelPattern
- Network.DNS.Pattern: DomLiteral :: ByteString -> LabelPattern
- Network.DNS.Pattern: Domain :: [DomainLabel] -> Domain
- Network.DNS.Pattern: DomainLabel :: ByteString -> DomainLabel
- Network.DNS.Pattern: DomainPattern :: [LabelPattern] -> DomainPattern
- Network.DNS.Pattern: [getDomainLabel] :: DomainLabel -> ByteString
- Network.DNS.Pattern: [getDomainPattern] :: DomainPattern -> [LabelPattern]
- Network.DNS.Pattern: [getDomain] :: Domain -> [DomainLabel]
- Network.DNS.Pattern: absDomainP :: Parser Domain
- Network.DNS.Pattern: absDomainRelaxP :: Parser Domain
- Network.DNS.Pattern: domainLabelP :: Parser DomainLabel
- Network.DNS.Pattern: encodedLength :: Domain -> Int
- Network.DNS.Pattern: foldCase :: Domain -> Domain
- Network.DNS.Pattern: foldCaseLabel :: DomainLabel -> DomainLabel
- Network.DNS.Pattern: instance GHC.Classes.Eq Network.DNS.Pattern.Domain
- Network.DNS.Pattern: instance GHC.Classes.Eq Network.DNS.Pattern.DomainLabel
- Network.DNS.Pattern: instance GHC.Classes.Eq Network.DNS.Pattern.DomainPattern
- Network.DNS.Pattern: instance GHC.Classes.Eq Network.DNS.Pattern.LabelPattern
- Network.DNS.Pattern: instance GHC.Classes.Ord Network.DNS.Pattern.Domain
- Network.DNS.Pattern: instance GHC.Classes.Ord Network.DNS.Pattern.DomainLabel
- Network.DNS.Pattern: instance GHC.Classes.Ord Network.DNS.Pattern.DomainPattern
- Network.DNS.Pattern: instance GHC.Classes.Ord Network.DNS.Pattern.LabelPattern
- Network.DNS.Pattern: instance GHC.Show.Show Network.DNS.Pattern.LabelPattern
- Network.DNS.Pattern: newtype Domain
- Network.DNS.Pattern: newtype DomainLabel
- Network.DNS.Pattern: newtype DomainPattern
- Network.DNS.Pattern: parseAbsDomain :: Text -> Either String Domain
- Network.DNS.Pattern: parseAbsDomainRelax :: Text -> Either String Domain
- Network.DNS.Pattern: parseDomainLabel :: Text -> Either String DomainLabel
- Network.DNS.Pattern: pprDomain :: Domain -> Text
- Network.DNS.Pattern: pprDomainLabel :: DomainLabel -> Text
+ Network.DNS: absDomainP :: Parser Domain
+ Network.DNS: absDomainRelaxP :: Parser Domain
+ Network.DNS: data Domain
+ Network.DNS: data DomainLabel
+ Network.DNS: domainLabelP :: Parser DomainLabel
+ Network.DNS: foldCase :: Domain -> Domain
+ Network.DNS: foldCaseLabel :: DomainLabel -> DomainLabel
+ Network.DNS: getDomain :: Domain -> [DomainLabel]
+ Network.DNS: getDomainLabel :: DomainLabel -> ShortByteString
+ Network.DNS: getDomainLabelCF :: DomainLabel -> ShortByteString
+ Network.DNS: mkDomain :: [DomainLabel] -> Domain
+ Network.DNS: mkDomainLabel :: ShortByteString -> DomainLabel
+ Network.DNS: parseAbsDomain :: Text -> Either String Domain
+ Network.DNS: parseAbsDomainRelax :: Text -> Either String Domain
+ Network.DNS: parseDomainLabel :: Text -> Either String DomainLabel
+ Network.DNS: pprDomain :: Domain -> Text
+ Network.DNS: pprDomainCF :: Domain -> Text
+ Network.DNS: pprDomainLabel :: DomainLabel -> Text
+ Network.DNS: pprDomainLabelCF :: DomainLabel -> Text
+ Network.DNS: unsafeMkDomainLabel :: ShortByteString -> DomainLabel
+ Network.DNS: unsafeSingletonDomainLabel :: Word8 -> DomainLabel
+ Network.DNS.Internal: DList :: ([a] -> [a]) -> DList a
+ Network.DNS.Internal: Domain :: [DomainLabel] -> Domain
+ Network.DNS.Internal: DomainLabel :: !ShortByteString -> !ShortByteString -> DomainLabel
+ Network.DNS.Internal: [getDomainLabelCF_] :: DomainLabel -> !ShortByteString
+ Network.DNS.Internal: [getDomainLabel_] :: DomainLabel -> !ShortByteString
+ Network.DNS.Internal: buildLabel :: DomainLabel -> DList Char
+ Network.DNS.Internal: buildLabelCF :: DomainLabel -> DList Char
+ Network.DNS.Internal: data DomainLabel
+ Network.DNS.Internal: fromDList :: DList a -> [a]
+ Network.DNS.Internal: instance GHC.Base.Monoid (Network.DNS.Internal.DList a)
+ Network.DNS.Internal: instance GHC.Base.Semigroup (Network.DNS.Internal.DList a)
+ Network.DNS.Internal: instance GHC.Classes.Eq Network.DNS.Internal.Domain
+ Network.DNS.Internal: instance GHC.Classes.Eq Network.DNS.Internal.DomainLabel
+ Network.DNS.Internal: instance GHC.Classes.Ord Network.DNS.Internal.Domain
+ Network.DNS.Internal: instance GHC.Classes.Ord Network.DNS.Internal.DomainLabel
+ Network.DNS.Internal: isLabelChar :: Char -> Bool
+ Network.DNS.Internal: newtype DList a
+ Network.DNS.Internal: newtype Domain
+ Network.DNS.Internal: singleton :: a -> DList a
+ Network.DNS.Internal: toDList :: [a] -> DList a
+ Network.DNS.Pattern: data DomainPattern
+ Network.DNS.Pattern: labelMatchesPattern :: DomainLabel -> LabelPattern -> Bool
+ Network.DNS.Pattern: pprLabelPattern :: LabelPattern -> Text
+ Network.DNS.Pattern: pprLabelPatternCF :: LabelPattern -> Text
+ Network.DNS.Pattern: pprPatternCF :: DomainPattern -> Text
+ Network.DNS.Pattern.Internal: DomGlob :: LabelPattern
+ Network.DNS.Pattern.Internal: DomGlobStar :: LabelPattern
+ Network.DNS.Pattern.Internal: DomLiteral :: DomainLabel -> LabelPattern
+ Network.DNS.Pattern.Internal: Domain :: [DomainLabel] -> Domain
+ Network.DNS.Pattern.Internal: DomainLabel :: !ShortByteString -> !ShortByteString -> DomainLabel
+ Network.DNS.Pattern.Internal: DomainPattern :: [LabelPattern] -> DomainPattern
+ Network.DNS.Pattern.Internal: [getDomainLabelCF_] :: DomainLabel -> !ShortByteString
+ Network.DNS.Pattern.Internal: [getDomainLabel_] :: DomainLabel -> !ShortByteString
+ Network.DNS.Pattern.Internal: [getDomainPattern] :: DomainPattern -> [LabelPattern]
+ Network.DNS.Pattern.Internal: data DomainLabel
+ Network.DNS.Pattern.Internal: data LabelPattern
+ Network.DNS.Pattern.Internal: instance GHC.Classes.Eq Network.DNS.Pattern.Internal.DomainPattern
+ Network.DNS.Pattern.Internal: instance GHC.Classes.Eq Network.DNS.Pattern.Internal.LabelPattern
+ Network.DNS.Pattern.Internal: instance GHC.Classes.Ord Network.DNS.Pattern.Internal.DomainPattern
+ Network.DNS.Pattern.Internal: instance GHC.Classes.Ord Network.DNS.Pattern.Internal.LabelPattern
+ Network.DNS.Pattern.Internal: newtype Domain
+ Network.DNS.Pattern.Internal: newtype DomainPattern

Files

CHANGELOG.md view
@@ -1,17 +1,46 @@ # Revision history for dns-patterns -## 0.1 -- 2022-02-09+## 0.2.0 -- 2022-08-24 -* Initial release+* Reversed CHANGELOG item order+* Added the following functions:+  * 'pprDomainCF'+  * 'pprDomainLabelCF'+  * 'unsafeMkDomainLabel'+  * 'unsafeSingletonDomainLabel'+  * 'mkDomainLabel' (smart constructor)+  * 'pprPatternCF'+  * 'pprLabelPattern'+  * 'pprLabelPatternCF'+  * 'mkDomain'+  * 'getDomainLabel'+  * 'getDomainLabelCF'+* Moved following data constructors and fields to an internal module:+  * 'DomainLabel'+  * 'Domain'+  * 'LabelPattern'+  * 'DomainPattern'+* Added case-insensitive Eq/Ord for LabelPattern and DomainPattern+* Drastic performance optimizations for pretty printing+* Domain labels and domain label patterns now may only contain '[a-zA-Z0-0_-]', '\.', '\\' and '\OCTAL'+* Split 'Network.DNS.Pattern' into the following modules:+  * 'Network.DNS'+  * 'Network.DNS.Internal'+  * 'Network.DNS.Pattern.Internal'+  * 'Network.DNS.Pattern.Internal' -## 0.1.1 -- 2022-07-06+## 0.1.3 -- 2022-08-19 -* Allow parseAbsDomainRelax for absolute domains+* Fix incorrect Ord instance for 'Domain' and 'DomainLabel'  ## 0.1.2 -- 2022-08-18  * Add 'foldCase' and 'foldCaseLabel' -## 0.1.3 -- 2022-08-19+## 0.1.1 -- 2022-07-06 -* Fix incorrect Ord instance for 'Domain' and 'DomainLabel'+* Allow parseAbsDomainRelax for absolute domains++## 0.1 -- 2022-02-09++* Initial release
dns-patterns.cabal view
@@ -1,6 +1,6 @@ cabal-version:      3.0 name:               dns-patterns-version:            0.1.3+version:            0.2.0 author:             Victor Nawothnig maintainer:         Victor Nawothnig (dminuoso@icloud.com) copyright:          (c) 2022 Wobcom GmbH@@ -9,7 +9,8 @@ license-file:       LICENSE category:           Network description:-        See the main module "Network.DNS.Pattern" for full documentation+        Domain name pattern matching, parsing and pretty printing utilities.+        See the modules "Network.DNS.Pattern" and "Network.DNS" for full documentation.  synopsis:           DNS name parsing and pattern matching utilities extra-source-files: CHANGELOG.md@@ -19,11 +20,14 @@     build-depends:    base ^>= { 4.12.0.0, 4.13.0.0, 4.14.1.0, 4.15.0.0 }                     , attoparsec ^>= { 0.13.2.5, 0.14.4 }                     , text ^>= { 1.2.4 }-                    , bytestring ^>= { 0.10.12 }+                    , bytestring ^>= { 0.11.3 }  library     import:           all-    exposed-modules:  Network.DNS.Pattern+    exposed-modules:  Network.DNS+                    , Network.DNS.Internal+                    , Network.DNS.Pattern+                    , Network.DNS.Pattern.Internal     build-depends:    parser-combinators ^>= { 1.3.0 }     hs-source-dirs:   lib     ghc-options:      -Wall -Wcompat
+ lib/Network/DNS.hs view
@@ -0,0 +1,249 @@+{-# LANGUAGE MagicHash #-}+-- |+-- Module      : Network.DNS+-- Description : Generic DNS utilities+--+-- There is no standardized presentation and parsing format for domain names.+-- In this library we assume a domain name and pattern to be specified as a text with an ASCII dot @.@ acting as a separator and terminator.+-- We do not admit arbitrary unicode codepoints, only the following subset of ASCII is acceptable per label:+-- [a-z], [A-Z], [0-9], '_', '-'+--+-- Punycoding, if desired, must be taken care of the user.+--+-- In addition, we allow a backslash to be used as an escaping character for the following possible sequences:+--+-- @+--    \\.      gives a dot inside a label, rather than a label separator+--    \\\\      gives a backslash inside a label+--    \\012    gives an arbitrary octet inside a label as specified by the three octets+-- @+--+-- For example: @foo\\.bar.quux.@ is a domain name comprised of two labels @foo.bar@ and @quux@+module Network.DNS+  ( Domain+  , getDomain+  , mkDomain+  , DomainLabel+  , getDomainLabel+  , getDomainLabelCF+  , mkDomainLabel+  , unsafeMkDomainLabel+  , unsafeSingletonDomainLabel+  , foldCase+  , foldCaseLabel++  -- * Parsing+  , parseAbsDomain+  , parseAbsDomainRelax+  , parseDomainLabel+  , absDomainP+  , absDomainRelaxP+  , domainLabelP++  -- * Pretty printing+  , pprDomain+  , pprDomainCF+  , pprDomainLabel+  , pprDomainLabelCF+  )+where++import           Data.Coerce (coerce)+import           GHC.Exts (Addr#, indexWord8OffAddr#)+import           GHC.Int (Int(..))+import           GHC.Word (Word8(..))++import           Control.Applicative.Combinators+import           Control.Monad (when)+import           Data.Char (ord)+import           Data.Foldable (asum)++import           Data.Attoparsec.Text ((<?>))+import qualified Data.Attoparsec.Text as A+import           Data.ByteString.Internal (c2w)+import qualified Data.ByteString.Short as BS+import qualified Data.Text as T++import           Network.DNS.Internal++-- | Parse an absolute domain. Convenience wrapper for 'absDomainP'.+parseAbsDomain :: T.Text -> Either String Domain+parseAbsDomain = A.parseOnly (absDomainP <* A.endOfInput)++-- | Parse a singular domain label. Convenience wrapper for 'domainLabelP'.+parseDomainLabel :: T.Text -> Either String DomainLabel+parseDomainLabel = A.parseOnly (domainLabelP <* A.endOfInput)++-- | Version of parseAbsDomain that also considers a domain name without a trailing dot+-- to be absolute.+parseAbsDomainRelax :: T.Text -> Either String Domain+parseAbsDomainRelax = A.parseOnly (absDomainRelaxP <* A.endOfInput)++-- | Turn a 'Domain' into a list of its labels.+--+-- prop> getDomain . mkDomain ~~~ id+-- prop> mkDomain . getDomain ~~~ id+getDomain :: Domain -> [DomainLabel]+getDomain = coerce++-- | Turn a list of labels into a 'Domain'.+--+-- prop> getDomain . mkDomain ~~~ id+-- prop> mkDomain . getDomain ~~~ id+mkDomain :: [DomainLabel] -> Domain+mkDomain = coerce++-- | Get the wire-representation of a domain label.+{-# INLINE getDomainLabel #-}+getDomainLabel :: DomainLabel -> BS.ShortByteString+getDomainLabel = getDomainLabel_++-- | Get the [RFC4343](https://datatracker.ietf.org/doc/html/rfc4343#section-3) case-folded wire-representation of a domain label.+{-# INLINE getDomainLabelCF #-}+getDomainLabelCF :: DomainLabel -> BS.ShortByteString+getDomainLabelCF = getDomainLabelCF_++-- | Smart constructor for 'DomainLabel'+mkDomainLabel :: BS.ShortByteString -> DomainLabel+mkDomainLabel l = DomainLabel l (BS.map foldCase_ l)++-- | Unsafely construct a 'DomainLabel'. The argument must already be case-folded according to [RFC4343](https://datatracker.ietf.org/doc/html/rfc4343#section-3).+unsafeMkDomainLabel :: BS.ShortByteString -> DomainLabel+unsafeMkDomainLabel l = DomainLabel l l+++-- | Unsafely construct a 'DomainLabel' from a single Word8. The argument must already be case-folded according to [RFC4343](https://datatracker.ietf.org/doc/html/rfc4343#section-3).+unsafeSingletonDomainLabel :: Word8 -> DomainLabel+unsafeSingletonDomainLabel l = DomainLabel (BS.singleton l) (BS.singleton l)++-- | Case-folding of a domain according to [RFC4343](https://datatracker.ietf.org/doc/html/rfc4343#section-3).+-- Note 'Domain' will memoize a case-folded variant for 'Eq', 'Ord' and pretty printing already. This function is not useful to most.+foldCase :: Domain -> Domain+foldCase (Domain ls) = Domain (foldCaseLabel <$> ls)++-- | Case-folding of a domain label according to [RFC4343](https://datatracker.ietf.org/doc/html/rfc4343#section-3).+-- Note 'DomainLabel' will memoize a case-folded variant for 'Eq', 'Ord' and pretty printing already. This function is not useful to most.+{-# INLINE foldCaseLabel #-}+foldCaseLabel :: DomainLabel -> DomainLabel+foldCaseLabel (DomainLabel _l cf) = DomainLabel cf cf++foldCase_ :: Word8 -> Word8+foldCase_ w = case fromIntegral w of+    I# n -> W8# (indexWord8OffAddr# table n)+  where+    table :: Addr#+    table = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\+            \\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\+            \\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\+            \\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\+            \\x40\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\+            \\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x5b\x5c\x5d\x5e\x5f\+            \\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\+            \\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\+            \\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\+            \\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\+            \\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\+            \\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\+            \\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\+            \\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\+            \\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\+            \\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"#++-- | Print an arbitrary domain into a presentation format.+--+-- This function nearly roundtrips with 'parseAbsDomain' up to escape sequence equivalence+--+-- prop> parseAbsDomain . pprDomain ~~~ id+pprDomain :: Domain -> T.Text+pprDomain (Domain l) = T.pack (fromDList build)+  where+    build :: DList Char+    build = foldr (\x buf -> buildLabel x <> singleton '.' <> buf) mempty l++-- | Print an arbitrary domain into a presentation format after case-folding according to [RFC4343](https://datatracker.ietf.org/doc/html/rfc4343#section-3).+--+-- This function nearly roundtrips with 'parseAbsDomain' up to escape sequence equivalence and case folding.+--+-- prop> parseAbsDomain . pprDomainCF ~~~ id+pprDomainCF :: Domain -> T.Text+pprDomainCF (Domain l) = T.pack (fromDList build)+  where+    build :: DList Char+    build = foldr (\x buf -> buildLabelCF x <> singleton '.' <> buf) mempty l++-- | Print a singular domain label into a presentation format.+pprDomainLabel :: DomainLabel -> T.Text+pprDomainLabel = T.pack . fromDList . buildLabel++-- | Print a singular domain label into a presentation format after case-folding according to [RFC4343](https://datatracker.ietf.org/doc/html/rfc4343#section-3).+pprDomainLabelCF :: DomainLabel -> T.Text+pprDomainLabelCF = T.pack . fromDList . buildLabelCF+++-- | Attoparsec 'A.Parser' for absolute domains. See 'parseAbsDomainRelax' for a convenience warpper.+-- This variant differs from 'absDomainP' in that it does not care whether the domain+-- name ends in a dot.+absDomainRelaxP :: A.Parser Domain+absDomainRelaxP = do+    d <- go+    let l = encodedLength d+    when (l >= 255) (fail "domain name too long")+    pure d++  where+    go = Domain <$> domainLabelP `sepBy1` A.char '.' <* optional (A.char '.')++-- | Calculate the wire-encoded length of a domain name.+encodedLength :: Domain -> Int+encodedLength (Domain labels) = sum (BS.length <$> l') + length l'+  where+    l' :: [BS.ShortByteString]+    l' = getDomainLabel <$> labels++-- | Attoparsec 'A.Parser' for absolute domains. See 'parseAbsDomain' for a convenience wrapper.+-- For a parser that also admits domain forms without a leading dot, see 'absDomainRelaxP'.+absDomainP :: A.Parser Domain+absDomainP = do+    d <- go+    let l = encodedLength d+    when (l >= 255) (fail "domain name too long")+    pure d++  where+    go = Domain <$> asum+        [ domainLabelP `endBy1` A.char '.'+        , [] <$ A.char '.' -- The root domain itself+        ]++octal :: A.Parser Word8+octal = do+  o1 <- v <$> A.satisfy isOctal+  o2 <- v <$> A.satisfy isOctal+  o3 <- v <$> A.satisfy isOctal+  pure (fromIntegral (o1 * 64 ++                      o2 * 8 ++                      o3))++  where+    v c = ord c - 48++    isOctal :: Char -> Bool+    isOctal c = c >= '0' && c <= '7'++-- | Attoparsec 'A.Parser' for a singular domain label. See 'parseDomainLabel' for a convenince wrapper. Also see 'absDomainP'.+domainLabelP :: A.Parser DomainLabel+domainLabelP = mkDomainLabel . BS.pack <$> (some labelChar)+  where+    labelChar :: A.Parser Word8+    labelChar = do+        c <- A.satisfy (\x -> isLabelChar x || x == '\\') <?> "domain label character"+        case c of+            '\\' -> escapable+            _    -> pure (c2w c)++    escapable :: A.Parser Word8+    escapable = asum+        [ c2w <$> A.char '.'+        , c2w <$> A.char '\\'+        , octal+        ] <?> "escapable character"
+ lib/Network/DNS/Internal.hs view
@@ -0,0 +1,105 @@+-- |+-- Module      : Network.DNS.Pattern.Internal+-- Description : Internal DNS types and definitions+--+-- This module is not part of public API and may change even between patch versions.++module Network.DNS.Internal+  ( DomainLabel(..)+  , Domain(..)+  , DList(..)+  , toDList+  , fromDList+  , buildLabel+  , buildLabelCF+  , singleton+  , isLabelChar+  )++where++import           Data.ByteString.Internal (w2c)+import qualified Data.ByteString.Short as BS+import           Data.Function (on)+import           GHC.Word (Word8(..))++import           Data.Char (isDigit, isLower, isUpper)++-- | Domain label with case-insensitive 'Eq' and 'Ord' as per [RFC4343](https://datatracker.ietf.org/doc/html/rfc4343#section-3).+data DomainLabel = DomainLabel { getDomainLabel_ :: !BS.ShortByteString+                               , getDomainLabelCF_ :: !BS.ShortByteString }++-- | A domain parsed into labels. Each label is a 'BS.ShortByteString' rather than 'T.Text' or 'String' because a label can contain arbitrary bytes.+-- However, the 'Ord' and 'Eq' instances do limited case-folding according to [RFC4343](https://datatracker.ietf.org/doc/html/rfc4343#section-3).+newtype Domain = Domain [DomainLabel] deriving (Eq, Ord)++instance Ord DomainLabel where+  (<=) = (<=) `on` getDomainLabelCF_+  compare = compare `on` getDomainLabelCF_++instance Eq DomainLabel where+  (==) = (==) `on` getDomainLabelCF_++-- | Difference list à la Huhges+newtype DList a = DList ([a] -> [a])++-- | Turn a list into 'DList'+{-# INLINE toDList #-}+toDList :: [a] -> DList a+toDList = DList . (++)++-- | Turn 'DList' back into a list.+{-# INLINE fromDList #-}+fromDList :: DList a -> [a]+fromDList (DList dl) = dl []++-- | Create a 'DList' containing just the specified element+{-# INLINE singleton #-}+singleton :: a -> DList a+singleton = DList . (:)++instance Semigroup (DList a) where+  {-# INLINE (<>) #-}+  DList l <> DList r = DList (l . r)++instance Monoid (DList a) where+  {-# INLINE mempty #-}+  mempty = DList id++-- | Make a case-folded string from a 'DomainLabel' suitable for pretty printing+{-# INLINE buildLabelCF #-}+buildLabelCF :: DomainLabel -> DList Char+buildLabelCF = buildLabel_ . getDomainLabelCF_++-- | Make a string from a 'DomainLabel' suitable for pretty printing+{-# INLINE buildLabel #-}+buildLabel :: DomainLabel -> DList Char+buildLabel = buildLabel_ . getDomainLabel_++{-# INLINE buildLabel_ #-}+buildLabel_ :: BS.ShortByteString -> DList Char+buildLabel_ bs = toDList (replace (BS.unpack bs))+  where+    {-# INLINE replace #-}+    replace :: [Word8] -> [Char]+    replace (x:xs) = case x of+      _ | isLabelChar (w2c x)+           -> (w2c x) : replace xs++      0x2e -> '\\' : '.' : replace xs+      0x5c -> '\\' : '\\' : replace xs+      _    -> '\\' : o1 : o2 : o3 : replace xs+        where+            (o1, o2, o3) = case quotRem x 8 of+                (v1, r3) -> case quotRem v1 8 of+                    (v2, r2) -> case quotRem v2 8 of+                        (_, r1)  -> (showD r1, showD r2, showD r3)+    replace [] = []+    {-# INLINE showD #-}+    showD :: Word8 -> Char+    showD x = w2c (x + 0x30)++-- | Predicate selecting characters allowed in a domain label without escaping.+{-# INLINABLE isLabelChar #-}+isLabelChar :: Char -> Bool+isLabelChar x = isLower x || isDigit x || isUpper x || x == '-' || x == '_'
lib/Network/DNS/Pattern.hs view
@@ -1,94 +1,15 @@ -- | -- Module      : Network.DNS.Pattern--- Description : Generic network related types.------ Provides utilities and parsers for a simple domain name pattern language.-{-# LANGUAGE OverloadedStrings #-}-module Network.DNS.Pattern-  (-  -- * Domain names-  -- $domainNames-    parseAbsDomain-  , parseAbsDomainRelax-  , parseDomainLabel-  , absDomainP-  , absDomainRelaxP-  , Domain(..)-  , DomainLabel(..)-  , pprDomain-  , pprDomainLabel-  , foldCase-  , foldCaseLabel--  -- * Pattern language-  -- $patterns-  , parsePattern-  , patternWorksInside-  , matchesPattern-  , domainLabelP-  , patternP-  , DomainPattern(..)-  , LabelPattern(..)-  , encodedLength-  , pprPattern-  )-where--import           Control.Monad (when)-import           Data.Char (isAscii, ord)-import           Data.Coerce (coerce)-import           Data.Foldable (asum, foldl')-import           Data.Function (on)-import           Data.Word (Word8)-import           Text.Printf (printf)--import           Control.Applicative.Combinators-import           Data.Attoparsec.Text ((<?>))-import qualified Data.Attoparsec.Text as A-import           Data.ByteString.Internal (c2w, w2c)-import qualified Data.Text as T-import qualified Data.Text.Lazy as TL-import qualified Data.Text.Lazy.Builder as TB--import qualified Data.ByteString as BS------ * Domain names------ $domainNames------ There is no standardized presentation and parsing format for domain names.--- In this library we assume a domain name and pattern to be specified as a text with an ASCII dot @.@ acting as a separator and terminator.--- We do not admit arbitrary unicode codepoints, only ASCII is acceptable. Punycoding, if desired, must be taken care of the user.------ Escape sequences--- The domain name and pattern language here allows for the following escape sequences------ @---    \\.      gives a dot inside a label, rather than a label separator---    \\\\      gives a backslash inside a label---    \\012    gives an arbitrary octet inside a label as specified by the three octets--- @------ For example: @foo\\.bar.quux.@ is a domain name comprised of two labels @foo.bar@ and @quux@----- * Patterns------ $patterns------ Patterns can be simple absolute domain names, where labels can be replaced with either a single glob @*@ or a globstar @**@.--- A single glob will match any label in its place, where globstar will greedily match as many labels as possible.------ Admits the escape sequences from domain names as well as the following+-- Description : DNS pattern matching ----- @---    \\*   gives an asterisk inside a label, rather than a glob/globstar.--- @+-- Patterns can be simple absolute domain names, where labels are replaceable with a single glob @*@ or a globstar @**@.+-- A single glob will match any label in its place, where globstar will greedily match as many labels as possible towards the left. --+-- Admits the escape sequences from domain names. See 'Network.DNS'. -- -- Note: Currently a globstar is only supported on the left-most label. ----- Examples or valid patterns are:+-- Examples of valid patterns are: -- -- @ --    *.foo.bar.@@ -97,129 +18,77 @@ --    foo.bar.*. -- @ -type Parser = A.Parser+{-# LANGUAGE OverloadedStrings #-}+module Network.DNS.Pattern+  ( -- * Pattern language+    DomainPattern+  , LabelPattern+  , matchesPattern+  , patternWorksInside+  , labelMatchesPattern --- | A domain pattern.-newtype DomainPattern = DomainPattern-  { getDomainPattern :: [LabelPattern]-  } deriving (Eq, Ord)+  -- * Parsing+  , parsePattern+  , patternP --- | A domain parsed into labels. Each label is a 'BS.ByteString' rather than 'T.Text' or 'String' because a label can contain arbitrary bytes.--- However, the 'Ord' and 'Eq' instances do limited case-folding according to [RFC4343](https://datatracker.ietf.org/doc/html/rfc4343#section-3).-newtype Domain = Domain-  { getDomain :: [DomainLabel]-  } deriving (Eq, Ord)+  -- * Pretty printing+  , pprPattern+  , pprPatternCF+  , pprLabelPattern+  , pprLabelPatternCF+  )+where --- | Newtype warpper for 'BS.ByteString' that implements case-insensitive 'Eq' and 'Ord' as per [RFC4343](https://datatracker.ietf.org/doc/html/rfc4343#section-3).-newtype DomainLabel = DomainLabel { getDomainLabel :: BS.ByteString }+import           Data.Foldable (asum) -instance Ord DomainLabel where-  compare = compare `on` (BS.map foldCase_ . getDomainLabel)+import           Control.Applicative.Combinators+import qualified Data.Attoparsec.Text as A+import qualified Data.Text as T -instance Eq DomainLabel where-  DomainLabel l == DomainLabel r-    = BS.map foldCase_ l == BS.map foldCase_ r+import           Network.DNS+import           Network.DNS.Internal+import           Network.DNS.Pattern.Internal  --- | Case-folding of a domain according to [RFC4343](https://datatracker.ietf.org/doc/html/rfc4343#section-3).-foldCase :: Domain -> Domain-foldCase (Domain ls) = Domain (foldCaseLabel <$> ls)---- | Case-folding of a domain label according to [RFC4343](https://datatracker.ietf.org/doc/html/rfc4343#section-3).-foldCaseLabel :: DomainLabel -> DomainLabel-foldCaseLabel (DomainLabel l) = DomainLabel (BS.map foldCase_ l)--{-# INLINE foldCase_ #-}-foldCase_ :: Word8 -> Word8-foldCase_ x = case x of-    0x41 -> c2w 'a'-    0x42 -> c2w 'b'-    0x43 -> c2w 'c'-    0x44 -> c2w 'd'-    0x45 -> c2w 'e'-    0x46 -> c2w 'f'-    0x47 -> c2w 'g'-    0x48 -> c2w 'h'-    0x49 -> c2w 'i'-    0x4a -> c2w 'j'-    0x4b -> c2w 'k'-    0x4c -> c2w 'l'-    0x4d -> c2w 'm'-    0x4e -> c2w 'n'-    0x4f -> c2w 'o'-    0x50 -> c2w 'p'-    0x51 -> c2w 'q'-    0x52 -> c2w 'r'-    0x53 -> c2w 's'-    0x54 -> c2w 't'-    0x55 -> c2w 'u'-    0x56 -> c2w 'v'-    0x57 -> c2w 'w'-    0x58 -> c2w 'x'-    0x59 -> c2w 'y'-    0x5a -> c2w 'z'-    _    -> x---- | Print an arbitrary domain into a presentation format.------ This function nearly roundtrips with 'parseAbsDomain' in the sense that octet escape sequences might change case or drop a leading zero.------ prop> parseAbsDomain . pretty ~~~ id-pprDomain :: Domain -> T.Text-pprDomain (Domain l) = TL.toStrict (TB.toLazyText build)-  where-    build :: TB.Builder-    build = foldl' (\buf x -> buf <> buildLabel x <> ".") mempty l---- | Print a singular domain label into a presentation format.-pprDomainLabel :: DomainLabel -> T.Text-pprDomainLabel = TL.toStrict . TB.toLazyText . buildLabel---- | Print domain into presentation format.+-- | Print domain pattern. ----- This function nearly roundtrips with 'parsePattern' in the sense that octet escape sequences might change case or drop a leading zero.+-- This function nearly roundtrips with 'parsePattern' up to escape sequence equivalence. -- -- prop> parsePattern . pprPattern ~~~ id pprPattern :: DomainPattern -> T.Text-pprPattern (DomainPattern l) = TL.toStrict (TB.toLazyText build)+pprPattern (DomainPattern l) = T.pack (fromDList build)   where-    build :: TB.Builder-    build = foldl' (\buf x -> buf <> buildLabelPattern x <> ".") mempty l+    build = foldr (\x buf -> buildLabelPattern x <> singleton '.' <> buf) mempty l -buildLabel :: DomainLabel -> TB.Builder-buildLabel (DomainLabel d) = BS.foldl' (\buf x -> buf <> go x) mempty d+-- | Print domain pattern after into presentation format after case-folding according to [RFC4343](https://datatracker.ietf.org/doc/html/rfc4343#section-3).+--+-- This function nearly roundtrips with 'parsePattern' up to escape sequence equivalence and case folding.+--+-- prop> parsePattern . pprPatternCF ~~~ id+pprPatternCF :: DomainPattern -> T.Text+pprPatternCF (DomainPattern l) = T.pack (fromDList build)   where-    go :: Word8 -> TB.Builder-    go 0x2e = "\\."-    go 0x5c = "\\\\"-    go c | c  > 0x20-         && c <= 0x7E-         = TB.singleton (w2c c)+    build = foldr (\x buf -> buildLabelPatternCF x <> singleton '.' <> buf) mempty l -         | otherwise-         = "\\" <> TB.fromString (printf "%03o" c)+-- | Print a singular domain label pattern into a presentation format.+pprLabelPattern :: LabelPattern -> T.Text+pprLabelPattern = T.pack . fromDList . buildLabelPattern -buildLabelPattern :: LabelPattern -> TB.Builder-buildLabelPattern (DomLiteral d) = BS.foldl' (\buf x -> buf <> go x) mempty d-  where-    go :: Word8 -> TB.Builder-    go 0x2e = "\\."-    go 0x5c = "\\\\"-    go 0x2a = "\\*"-    go c |  c >  0x20-         && c <= 0x7E = TB.singleton (w2c c)+-- | Print a singular domain label pattern into a presentation format after case-folding according to [RFC4343](https://datatracker.ietf.org/doc/html/rfc4343#section-3).+pprLabelPatternCF :: LabelPattern -> T.Text+pprLabelPatternCF = T.pack . fromDList . buildLabelPatternCF -         | otherwise-         = "\\" <> TB.fromString (printf "%03o" c)-buildLabelPattern DomGlob     = "*"-buildLabelPattern DomGlobStar = "**"+{-# INLINE buildLabelPattern #-}+buildLabelPattern :: LabelPattern -> DList Char+buildLabelPattern (DomLiteral d) = buildLabel d+buildLabelPattern DomGlob        = singleton '*'+buildLabelPattern DomGlobStar    = toDList "**" --- | A pattern for a singular label.-data LabelPattern-  = DomLiteral BS.ByteString -- ^ Represents an exact label that must be matched.-  | DomGlob -- ^ Represents a single asterisk glob matching any arbitrary domain at a given level.-  | DomGlobStar -- ^ Represents a double asterisk matching any arbitrary subdomain at a given level.-  deriving (Eq, Ord, Show)+{-# INLINE buildLabelPatternCF #-}+buildLabelPatternCF :: LabelPattern -> DList Char+buildLabelPatternCF (DomLiteral d) = buildLabelCF d+buildLabelPatternCF DomGlob        = singleton '*'+buildLabelPatternCF DomGlobStar    = toDList "**"  -- | Given a pattern and a DNS zone specified by a domain name, test whether or not the pattern -- is applicable beneath that zone.@@ -238,7 +107,7 @@     go [] []           = True     go [] _ls          = False     go _p []           = True-    go (p:ps) (l:ls)   = labelMatchesPattern (getDomainLabel l) p && go ps ls+    go (p:ps) (l:ls)   = labelMatchesPattern l p && go ps ls  -- | Test whether a given domain matches a 'DomainPattern' matchesPattern :: Domain -> DomainPattern -> Bool@@ -249,17 +118,17 @@     go []  _ps            = False     go _ls  []            = False     go _ls  [DomGlobStar] = True-    go (l:ls) (p:ps)      = labelMatchesPattern (getDomainLabel l) p && go ls ps+    go (l:ls) (p:ps)      = labelMatchesPattern l p && go ls ps  -- | Test whether a single label matches a label pattern-labelMatchesPattern :: BS.ByteString -> LabelPattern -> Bool+labelMatchesPattern :: DomainLabel -> LabelPattern -> Bool labelMatchesPattern _l DomGlob        = True labelMatchesPattern  l (DomLiteral p) = l == p labelMatchesPattern _l DomGlobStar    = True  --- | Parser for domain patterns. See 'parsePattern' for a convenince wrapper.-patternP :: Parser DomainPattern+-- | Attoparsec 'A.Parser' for domain patterns. See 'parsePattern' for a convenince wrapper.+patternP :: A.Parser DomainPattern patternP = asum [ do p  <- litGlobStar <* A.char '.'                      ps <- litGlob `endBy` A.char '.'                      pure (DomainPattern (p:ps))@@ -267,123 +136,17 @@                 , DomainPattern [] <$ A.char '.' -- Literal pattern of the root domain "."                 ]   where-    litGlobStar :: Parser LabelPattern-    litGlobStar = asum [ DomLiteral <$> patternLabelP+    litGlobStar :: A.Parser LabelPattern+    litGlobStar = asum [ DomLiteral <$> domainLabelP                        , DomGlobStar <$ A.string "**"                        , DomGlob <$ A.char '*'                        ] -    litGlob :: Parser LabelPattern-    litGlob = asum [ DomLiteral <$> patternLabelP+    litGlob :: A.Parser LabelPattern+    litGlob = asum [ DomLiteral <$> domainLabelP                    , DomGlob <$ A.char '*'                    ]  -- | Parse a domain pattern. Convenience wrapper for 'patternP. parsePattern :: T.Text -> Either String DomainPattern parsePattern = A.parseOnly (patternP <* A.endOfInput)---- | Parse an absolute domain. Convenience wrapper for 'absDomainP'.-parseAbsDomain :: T.Text -> Either String Domain-parseAbsDomain = A.parseOnly (absDomainP <* A.endOfInput)---- | Parse a singular domain label. Convenience wrapper for 'domainLabelP'.-parseDomainLabel :: T.Text -> Either String DomainLabel-parseDomainLabel = A.parseOnly (domainLabelP <* A.endOfInput)---- | Version of parseAbsDomain that also considers a domain name without a trailing dot--- to be absolute.-parseAbsDomainRelax :: T.Text -> Either String Domain-parseAbsDomainRelax = A.parseOnly (absDomainRelaxP <* A.endOfInput)---- | Parser for absolute domains. See 'parseAbsDomainRelax' for a convenience warpper.--- This variant differs from 'absDomainP' in that it does not care whether the domain--- name is terminated with a dot.-absDomainRelaxP :: Parser Domain-absDomainRelaxP = do-    d <- go-    let l = encodedLength d-    when (l >= 255) (fail "domain name too long")-    pure d--  where-    go = Domain <$> domainLabelP `sepBy1` A.char '.' <* optional (A.char '.')---- | Calculate the wire-encoded length of a domain name.-encodedLength :: Domain -> Int-encodedLength (Domain labels) = sum (BS.length <$> l') + length l'-  where-    l' = coerce labels :: [BS.ByteString]---- | Parser for absolute domains. See 'parseAbsDomain' for a convenience wrapper.--- For a parser that also admits domain forms without a leading dot, see 'absDomainRelaxP'.-absDomainP :: Parser Domain-absDomainP = do-    d <- go-    let l = encodedLength d-    when (l >= 255) (fail "domain name too long")-    pure d--  where-    go = Domain <$> asum-        [ domainLabelP `endBy1` A.char '.'-        , [] <$ A.char '.' -- The root domain itself-        ]--patternLabelP :: Parser BS.ByteString-patternLabelP = do-    r <- BS.pack <$> (some labelChar)-    when (BS.length r >= 64) (fail "label must not be longer than 63 octets")-    pure r-  where-    labelChar :: Parser Word8-    labelChar = do-        c <- A.satisfy (\x -> x /= '.'-                           && x /= '*'-                           && isAscii x-                        ) <?> "pattern label character"-        case c of-            '\\' -> escapable-            _    -> pure (c2w c)--    escapable :: Parser Word8-    escapable = asum-        [ c2w <$> A.char '.'-        , c2w <$> A.char '\\'-        , c2w <$> A.char '*'-        , octal-        ] <?> "escapable character"--octal :: Parser Word8-octal = do-  o1 <- v <$> A.satisfy isOctal-  o2 <- v <$> A.satisfy isOctal-  o3 <- v <$> A.satisfy isOctal-  pure (fromIntegral (o1 * 64 +-                      o2 * 8 +-                      o3))--  where-    v c = ord c - 48--    isOctal :: Char -> Bool-    isOctal c = c >= '0' && c <= '7'---- | Parser for a singular domain label. See 'parseDomainLabel' for a convenince wrapper. Also see 'absDomainP'.-domainLabelP :: Parser DomainLabel-domainLabelP = DomainLabel . BS.pack <$> (some labelChar)-  where-    labelChar :: Parser Word8-    labelChar = do-        c <- A.satisfy (\x -> x /= '.'-                           && isAscii x-                       ) <?> "domain label character"-        case c of-            '\\' -> escapable-            _    -> pure (c2w c)--    escapable :: Parser Word8-    escapable = asum-        [ c2w <$> A.char '.'-        , c2w <$> A.char '\\'-        , octal-        ] <?> "escapable character"
+ lib/Network/DNS/Pattern/Internal.hs view
@@ -0,0 +1,27 @@+-- |+-- Module      : Network.DNS.Pattern.Internal+-- Description : Internal pattern types and definitions+--+-- This module is not part of public API and may change even between patch versions.+module Network.DNS.Pattern.Internal+  ( DomainLabel(..)+  , Domain(..)+  , LabelPattern(..)+  , DomainPattern(..)+  )++where++import Network.DNS.Internal++-- | A domain pattern.+newtype DomainPattern = DomainPattern+  { getDomainPattern :: [LabelPattern]+  } deriving (Eq, Ord)++-- | A pattern for a singular label.+data LabelPattern+  = DomLiteral DomainLabel -- ^ Represents an exact label that must be matched.+  | DomGlob -- ^ Represents a single asterisk glob matching any arbitrary domain at a given level.+  | DomGlobStar -- ^ Represents a double asterisk matching any arbitrary subdomain at a given level.+  deriving (Eq, Ord)
test/Spec.hs view
@@ -1,4 +1,5 @@-{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE OverloadedStrings  #-}+{-# LANGUAGE StandaloneDeriving #-} module Main  where@@ -11,32 +12,41 @@ import           Test.HUnit.Base import           Test.HUnit.Text +import           GHC.IO.Handle (NewlineMode(inputNL))+import           Network.DNS import           Network.DNS.Pattern+import           Network.DNS.Pattern.Internal  instance IsString DomainPattern where-  fromString s = case parsePattern (T.pack s) of-    Right s  -> s-    Left err -> error ("DomPat: no parse, " <> err)+  fromString = unsafeParseDomainPattern . T.pack  instance IsString DomainLabel where   fromString s = case parseDomainLabel (T.pack s) of     Right s  -> s-    Left err -> error ("DomainLabel: no parse, " <> err)+    Left err -> error ("DomainLabel: failed to parse: \"" <> s <> "\"")  instance IsString Domain where-  fromString s = case parseAbsDomain (T.pack s) of-    Right s  -> s-    Left err -> error ("Domain: no parse, " <> err)+  fromString = unsafeParseDomain . T.pack  instance Show Domain where   show = T.unpack . pprDomain -instance Show DomainPattern where-  show = T.unpack . pprPattern-+deriving instance Show DomainPattern instance Show DomainLabel where   show = T.unpack . pprDomainLabel +deriving instance Show LabelPattern++unsafeParseDomainPattern :: T.Text -> DomainPattern+unsafeParseDomainPattern s = case parsePattern s of+    Right s  -> s+    Left err -> error ("DomPat: failed to parse: " <> T.unpack s)++unsafeParseDomain :: T.Text -> Domain+unsafeParseDomain s = case parseAbsDomain s of+    Right s  -> s+    Left err -> error ("Domain: failed to parse: " <> T.unpack s)+ main :: IO () main = do   runTestTTAndExit $ TestList@@ -46,6 +56,7 @@     , caseFoldSpecs     , patternContainedSpecs     , pprRoundTrips+    , pprSpecs     ]  @@ -140,10 +151,9 @@                  (parsePattern ".")      , TestCase $ assertEqual "allow escapable characters in patterns"-                 (parsePattern "\\..\\*.\\\\.")-                 (Right (DomainPattern [ DomLiteral "."-                                       , DomLiteral "*"-                                       , DomLiteral "\\" ]))+                 (parsePattern "\\..\\\\.")+                 (Right (DomainPattern [ DomLiteral (mkDomainLabel ".")+                                       , DomLiteral (mkDomainLabel "\\") ]))      , TestCase $ assertBool "Does not allow unescaped asterisk"                  (isLeft (parsePattern "a*."))@@ -164,19 +174,43 @@ isLeft _      = False  +pprSpecs :: Test+pprSpecs = TestList+  [ let lab = mkDomainLabel "." in+        TestCase $ assertEqual "p1: correctly escapes dots"+             "\\."+             (pprDomainLabel lab)+  , let lab = mkDomainLabel "." in+        TestCase $ assertEqual "p2: correctly escapes dots after case folding"+             "\\."+             (pprDomainLabelCF lab)++  , let lab = mkDomainLabel "\\" in+        TestCase $ assertEqual "p3: correctly escapes backslash"+             "\\\\"+             (pprDomainLabel lab)++  , let lab = mkDomainLabel "\\" in+        TestCase $ assertEqual "p4: correctly escapes backslash after case folding"+             "\\\\"+             (pprDomainLabelCF lab)++  , let lab = mkDomainLabel "-_" in+        TestCase $ assertEqual "p5: correctly does not escape hyphen or underscore"+             "-_"+             (pprDomainLabelCF lab)+  ] pprRoundTrips :: Test pprRoundTrips = TestList-  [ TestCase $ assertEqual "patterns roundtrip between parse and ppr"-               (Right True)-               (do let input = "**.*.\\.\\*\\000.foo."-                   r <- parsePattern input-                   pure (pprPattern r == input))+  [ let input = "**.*.\\.\\000.foo." in+        TestCase $ assertEqual "p1: patterns roundtrip between parse and ppr"+               input+               (pprPattern (unsafeParseDomainPattern input)) -  , TestCase $ assertEqual "domains roundtrip between parse and ppr"-               (Right True)-               (do let input = "**.*\\000.foo."-                   r <- parseAbsDomain input-                   pure (pprDomain r == input))+  , let input = "\\000.foo." in+          TestCase $ assertEqual "p2: domains roundtrip between parse and ppr"+               input+               (pprDomain (unsafeParseDomain input))    ]