packages feed

ip 1.5.1 → 1.6.0

raw patch · 9 files changed

+722/−132 lines, 9 filesdep +byteslicedep +bytesmithdep +natural-arithmeticdep −test-frameworkdep −test-framework-hunitdep −test-framework-quickcheck2dep ~bytestringPVP ok

version bump matches the API change (PVP)

Dependencies added: byteslice, bytesmith, natural-arithmetic, small-bytearray-builder, tasty, tasty-hunit, tasty-quickcheck, text-short

Dependencies removed: test-framework, test-framework-hunit, test-framework-quickcheck2

Dependency ranges changed: bytestring

API changes (from Hackage documentation)

+ Net.IP: boundedBuilderUtf8 :: IP -> Builder 39
+ Net.IP: decodeShort :: ShortText -> Maybe IP
+ Net.IP: encodeShort :: IP -> ShortText
+ Net.IP: parserUtf8Bytes :: e -> Parser e s IP
+ Net.IPv4: boundedBuilderUtf8 :: IPv4 -> Builder 15
+ Net.IPv4: box :: IPv4# -> IPv4
+ Net.IPv4: byteArrayBuilderUtf8 :: IPv4 -> Builder
+ Net.IPv4: decodeShort :: ShortText -> Maybe IPv4
+ Net.IPv4: decodeUtf8Bytes :: Bytes -> Maybe IPv4
+ Net.IPv4: encodeShort :: IPv4 -> ShortText
+ Net.IPv4: parserUtf8Bytes :: e -> Parser e s IPv4
+ Net.IPv4: parserUtf8Bytes# :: e -> Parser e s IPv4#
+ Net.IPv4: type IPv4# = Word#
+ Net.IPv4: unbox :: IPv4 -> IPv4#
+ Net.IPv6: boundedBuilderUtf8 :: IPv6 -> Builder 39
+ Net.IPv6: decodeShort :: ShortText -> Maybe IPv6
+ Net.IPv6: decodeUtf8Bytes :: Bytes -> Maybe IPv6
+ Net.IPv6: encodeShort :: IPv6 -> ShortText
+ Net.IPv6: parserUtf8Bytes :: e -> Parser e s IPv6

Files

ip.cabal view
@@ -1,9 +1,9 @@-cabal-version: 2.0+cabal-version: 2.2 name: ip-version: 1.5.1+version: 1.6.0 synopsis: Library for IP and MAC addresses homepage: https://github.com/andrewthad/haskell-ip#readme-license: BSD3+license: BSD-3-Clause license-file: LICENSE author: Andrew Martin maintainer: andrew.thaddeus@gmail.com@@ -34,26 +34,31 @@ library   hs-source-dirs: src   exposed-modules:-    Net.Mac+    Net.IP     Net.IPv4     Net.IPv6-    Net.IP+    Net.Mac     Net.Types   other-modules:-    Data.Word.Synthetic.Word12+    Data.ByteString.Builder.Fixed+    Data.Text.Builder.Common.Internal     Data.Text.Builder.Fixed     Data.Text.Builder.Variable-    Data.Text.Builder.Common.Internal-    Data.ByteString.Builder.Fixed+    Data.Word.Synthetic.Word12   build-depends:-      base >= 4.9.1.0 && < 5     , aeson >= 1.0 && < 1.5     , attoparsec >= 0.13 && < 0.14-    , bytestring >= 0.10 && < 0.11+    , base >= 4.9.1.0 && < 5+    , byteslice >= 0.1.2 && < 0.2+    , bytesmith >= 0.3 && < 0.4+    , bytestring >= 0.10.8 && < 0.11     , deepseq >= 1.4 && < 1.5     , hashable >= 1.2 && < 1.4+    , natural-arithmetic >= 0.1 && <0.2     , primitive >= 0.6.4 && < 0.8-    , text >= 1.2  && < 1.3+    , small-bytearray-builder >= 0.2.1 && <0.3+    , text >= 1.2 && < 1.3+    , text-short >= 0.1.3 && < 0.2     , vector >= 0.11 && < 0.13     , wide-word >= 0.1.0.8 && < 0.2   ghc-options: -Wall -O2@@ -64,38 +69,40 @@   hs-source-dirs:      test   main-is:             Test.hs   build-depends:-      base-    , ip-    , wide-word-    , test-framework-    , test-framework-quickcheck2+    , HUnit     , QuickCheck+    , attoparsec+    , base+    , byteslice >= 0.1.2 && < 0.2+    , bytestring+    , ip     , quickcheck-classes >= 0.4.13 && < 0.7.0.0+    , tasty+    , tasty-hunit+    , tasty-quickcheck     , text-    , bytestring-    , HUnit-    , test-framework-hunit-    , attoparsec+    , text-short+    , wide-word   other-modules:-    Naive+    IPv4ByteString1     IPv4Text1     IPv4Text2-    IPv4ByteString1+    Naive   ghc-options: -Wall -O2   default-language: Haskell2010  test-suite spec-  type:                exitcode-stdio-1.0-  hs-source-dirs:      test-  main-is:             Spec.hs+  type: exitcode-stdio-1.0+  hs-source-dirs: test+  main-is: Spec.hs   build-depends:-      base+    , base+    , hspec >= 2.5.5     , ip     , wide-word-    , hspec >= 2.5.5   other-modules:-    Net.IPv4Spec     Net.IPv4.RangeSpec+    Net.IPv4Spec   ghc-options: -Wall -O2   default-language: Haskell2010   build-tool-depends: hspec-discover:hspec-discover >= 2.5.5@@ -105,29 +112,30 @@   hs-source-dirs: test   main-is: Doctests.hs   build-depends:-      base+    , QuickCheck+    , base+    , doctest >= 0.10     , ip     , wide-word-    , doctest >= 0.10-    , QuickCheck   default-language:    Haskell2010  benchmark criterion   type: exitcode-stdio-1.0   build-depends:-      base-    , ip+    , attoparsec+    , base+    , byteslice+    , bytestring     , criterion+    , ip     , text-    , bytestring-    , attoparsec   other-modules:-    Naive-    IPv4Text1-    IPv4Text2+    IPv4ByteString1     IPv4DecodeText1     IPv4DecodeText2-    IPv4ByteString1+    IPv4Text1+    IPv4Text2+    Naive   ghc-options: -Wall -O2   default-language: Haskell2010   hs-source-dirs: test
src/Net/IP.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveGeneric #-}  {-# OPTIONS_GHC -Wall #-}@@ -39,7 +40,12 @@     -- * Textual Conversion     -- ** Text   , encode+  , encodeShort   , decode+  , decodeShort+  , boundedBuilderUtf8+    -- ** Bytes+  , parserUtf8Bytes     -- ** Printing   , print     -- * Types@@ -49,6 +55,7 @@ import Control.DeepSeq (NFData) import Data.Aeson (FromJSON(..),ToJSON(..)) import Data.Bits+import Data.Coerce (coerce) import Data.Text (Text) import Data.WideWord (Word128(..)) import Data.Word (Word8,Word16)@@ -58,13 +65,19 @@ import Prelude hiding (print) import Text.ParserCombinators.ReadPrec ((+++)) import Text.Read (Read(..))+import Data.Text.Short (ShortText)++import qualified Arithmetic.Lte as Lte import qualified Data.Aeson as Aeson+import qualified Data.ByteArray.Builder.Bounded as BB import qualified Data.Text.IO as TIO+import qualified Data.Bytes.Parser as Parser import qualified Net.IPv4 as IPv4 import qualified Net.IPv6 as IPv6  -- $setup -- >>> :set -XOverloadedStrings+-- >>> import qualified Arithmetic.Nat as Nat  -- | Run a function over an 'IP' depending on its status --   as an 'IPv4' or 'IPv6'.@@ -113,6 +126,25 @@ encode :: IP -> Text encode = case_ IPv4.encode IPv6.encode +-- | Encode an 'IP' as 'ShortText'.+--+--   >>> encodeShort (ipv4 10 0 1 26)+--   "10.0.1.26"+--+--   >>> encodeShort (ipv6 0x3124 0x0 0x0 0xDEAD 0xCAFE 0xFF 0xFE01 0x0000)+--   "3124::dead:cafe:ff:fe01:0"+encodeShort :: IP -> ShortText+encodeShort = case_ IPv4.encodeShort IPv6.encodeShort++-- | Encode an 'IP' as a bounded bytearray builder.+--+-- >>> BB.run Nat.constant (boundedBuilderUtf8 (ipv4 192 168 2 14))+-- [0x31, 0x39, 0x32, 0x2e, 0x31, 0x36, 0x38, 0x2e, 0x32, 0x2e, 0x31, 0x34]+boundedBuilderUtf8 :: IP -> BB.Builder 39+boundedBuilderUtf8 = case_+  (\y -> BB.weaken Lte.constant (IPv4.boundedBuilderUtf8 y))+  IPv6.boundedBuilderUtf8+ -- | Decode an 'IP' from 'Text'. -- --   >>> decode "10.0.0.25"@@ -132,6 +164,24 @@     Nothing -> Nothing     Just v6 -> Just (fromIPv6 v6)   Just v4 -> Just (fromIPv4 v4)++-- | Decode an 'IP' from 'ShortText'.+--+--   >>> decodeShort "10.0.0.25"+--   Just (ipv4 10 0 0 25)+--   >>> decodeShort "::dead:cafe"+--   Just (ipv6 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0xdead 0xcafe)+decodeShort :: ShortText -> Maybe IP+decodeShort t+  | Just x <- IPv4.decodeShort t = Just (fromIPv4 x)+  | otherwise = coerce (IPv6.decodeShort t)++-- | Parse UTF-8-encoded 'Bytes' as an 'IP' address.+parserUtf8Bytes :: e -> Parser.Parser e s IP+parserUtf8Bytes e =+  fmap fromIPv4 (IPv4.parserUtf8Bytes ())+  `Parser.orElse`+  coerce (IPv6.parserUtf8Bytes e)  -- | Is the 'IP' an IPv4 address? --
src/Net/IPv4.hs view
@@ -2,6 +2,7 @@ {-# LANGUAGE CPP #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE MagicHash #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeInType #-}@@ -33,11 +34,18 @@   , builder   , reader   , parser+  , decodeShort+  , encodeShort     -- ** UTF-8 ByteString   , encodeUtf8   , decodeUtf8   , builderUtf8   , parserUtf8+    -- ** UTF-8 Bytes+  , decodeUtf8Bytes+  , parserUtf8Bytes+  , byteArrayBuilderUtf8+  , boundedBuilderUtf8     -- ** String     -- $string   , encodeString@@ -69,7 +77,14 @@   , printRange     -- * Types   , IPv4(..)+  , IPv4#   , IPv4Range(..)+    -- * Unboxing+    -- | These functions are useful for micro-optimizing+    --   when GHC does a poor job with worker-wrapper.+  , box+  , unbox+  , parserUtf8Bytes#     -- * Interoperability     -- $interoperability   ) where@@ -81,30 +96,41 @@ import Data.Aeson (ToJSONKey(..),FromJSONKey(..),ToJSONKeyFunction(..),FromJSONKeyFunction(..)) import Data.Bits ((.&.),(.|.),shiftR,shiftL,unsafeShiftR,complement,shift) import Data.ByteString (ByteString)+import Data.Coerce (coerce) import Data.Hashable-import Data.Monoid ((<>)) import Data.Primitive.Types (Prim) import Data.Text (Text) import Data.Text.Encoding (decodeUtf8') import Data.Text.Internal (Text(..))+import Data.Text.Short (ShortText) import Data.Vector.Generic.Mutable (MVector(..)) import Data.Word import Foreign.Ptr (Ptr,plusPtr) import Foreign.Storable (Storable, poke)+import GHC.Exts (Word#) import GHC.Generics (Generic)+import GHC.Word (Word32(W32#)) import Prelude hiding (any, print, print) import Text.ParserCombinators.ReadPrec (prec,step) import Text.Printf (printf) import Text.Read (Read(..),Lexeme(Ident),lexP,parens)+import qualified Arithmetic.Nat as Nat import qualified Data.Aeson as Aeson import qualified Data.Aeson.Types as Aeson import qualified Data.Attoparsec.ByteString.Char8 as AB import qualified Data.Attoparsec.Text as AT import qualified Data.Bits as Bits+import qualified Data.ByteArray.Builder.Bounded as BB+import qualified Data.ByteArray.Builder as UB import qualified Data.ByteString.Builder as Builder import qualified Data.ByteString.Char8 as BC8 import qualified Data.ByteString.Internal as I import qualified Data.ByteString.Unsafe as ByteString+import qualified Data.ByteString.Short.Internal as BSS+import qualified Data.Bytes as Bytes+import qualified Data.Bytes.Parser as Parser+import qualified Data.Bytes.Parser.Latin as Latin+import qualified Data.Primitive as PM import qualified Data.Text as Text import qualified Data.Text.Array as TArray import qualified Data.Text.IO as TIO@@ -112,6 +138,8 @@ import qualified Data.Text.Lazy.Builder as TBuilder import qualified Data.Text.Lazy.Builder.Int as TBI import qualified Data.Text.Read as TextRead+import qualified Data.Text.Short as TS+import qualified Data.Text.Short.Unsafe as TS import qualified Data.Vector.Generic as GVector import qualified Data.Vector.Generic.Mutable as MGVector import qualified Data.Vector.Primitive as PVector@@ -348,15 +376,116 @@ -- --   >>> decodeUtf8 "192.168.2.47" --   Just (ipv4 192 168 2 47)+--+--   Currently not terribly efficient since the implementation+--   re-encodes the argument as UTF-16 text before decoding that+--   IPv4 address from that. PRs to fix this are welcome. decodeUtf8 :: ByteString -> Maybe IPv4 decodeUtf8 = decode <=< rightToMaybe . decodeUtf8' -- This (decodeUtf8) should be rewritten to not go through text -- as an intermediary. --- | Encode an 'IPv4' as a 'Builder.Builder'+-- | Decode 'ShortText' as an 'IPv4' address.+--+--   >>> decodeShort "192.168.3.48"+--   Just (ipv4 192 168 3 48)+decodeShort :: ShortText -> Maybe IPv4+decodeShort t = decodeUtf8Bytes (Bytes.fromByteArray b)+  where b = shortByteStringToByteArray (TS.toShortByteString t)++-- | Encode an 'IPv4' address as 'ShortText'.+--+--   >>> encodeShort (ipv4 192 168 5 99)+--   "192.168.5.99"+encodeShort :: IPv4 -> ShortText+encodeShort !w = id+  $ TS.fromShortByteStringUnsafe+  $ byteArrayToShortByteString+  $ BB.run Nat.constant+  $ boundedBuilderUtf8+  $ w++shortByteStringToByteArray :: BSS.ShortByteString -> PM.ByteArray+shortByteStringToByteArray (BSS.SBS x) = PM.ByteArray x++byteArrayToShortByteString :: PM.ByteArray -> BSS.ShortByteString+byteArrayToShortByteString (PM.ByteArray x) = BSS.SBS x++-- | Decode UTF-8-encoded 'Bytes' into an 'IPv4' address.+--+--   >>> decodeUtf8Bytes (Bytes.fromAsciiString "127.0.0.1")+--   Just (ipv4 127 0 0 1)+decodeUtf8Bytes :: Bytes.Bytes -> Maybe IPv4+decodeUtf8Bytes !b = case Parser.parseBytes (parserUtf8Bytes ()) b of+  Parser.Success (Parser.Slice _ len addr) -> case len of+    0 -> Just addr+    _ -> Nothing+  Parser.Failure _ -> Nothing++-- | Parse UTF-8-encoded 'Bytes' as an 'IPv4' address.+--+--   >>> Parser.parseBytes (parserUtf8Bytes ()) (Bytes.fromAsciiString "10.0.1.254")+--   Success (Slice {offset = 10, length = 0, value = ipv4 10 0 1 254})+parserUtf8Bytes :: e -> Parser.Parser e s IPv4+{-# inline parserUtf8Bytes #-}+parserUtf8Bytes e = coerce (Parser.boxWord32 (parserUtf8Bytes# e))++-- | Variant of 'parserUtf8Bytes' with unboxed result type.+parserUtf8Bytes# :: e -> Parser.Parser e s IPv4#+{-# noinline parserUtf8Bytes# #-}+parserUtf8Bytes# e = Parser.unboxWord32 $ do+  !a <- Latin.decWord8 e+  Latin.char e '.'+  !b <- Latin.decWord8 e+  Latin.char e '.'+  !c <- Latin.decWord8 e+  Latin.char e '.'+  !d <- Latin.decWord8 e+  pure (getIPv4 (fromOctets a b c d))++-- | Encode an 'IPv4' as a bytestring 'Builder.Builder'+--+-- >>> Builder.toLazyByteString (builderUtf8 (fromOctets 192 168 2 12))+-- "192.168.2.12" builderUtf8 :: IPv4 -> Builder.Builder builderUtf8 = Builder.byteString . encodeUtf8 +-- | Encode an 'IPv4' address as a unbounded byte array builder.+--+-- >>> UB.run 1 (byteArrayBuilderUtf8 (fromOctets 192 168 2 13))+-- [0x31, 0x39, 0x32, 0x2e, 0x31, 0x36, 0x38, 0x2e, 0x32, 0x2e, 0x31, 0x33]+--+-- Note that period is encoded by UTF-8 as @0x2e@.+byteArrayBuilderUtf8 :: IPv4 -> UB.Builder+byteArrayBuilderUtf8 = UB.fromBounded Nat.constant . boundedBuilderUtf8++-- | Encode an 'IPv4' address as a bounded byte array builder.+--+-- >>> BB.run Nat.constant (boundedBuilderUtf8 (fromOctets 192 168 2 14))+-- [0x31, 0x39, 0x32, 0x2e, 0x31, 0x36, 0x38, 0x2e, 0x32, 0x2e, 0x31, 0x34]+--+-- Note that period is encoded by UTF-8 as @0x2e@.+boundedBuilderUtf8 :: IPv4 -> BB.Builder 15+boundedBuilderUtf8 (IPv4 !w) =+  BB.word8Dec w1+  `BB.append`+  BB.ascii '.'+  `BB.append`+  BB.word8Dec w2+  `BB.append`+  BB.ascii '.'+  `BB.append`+  BB.word8Dec w3+  `BB.append`+  BB.ascii '.'+  `BB.append`+  BB.word8Dec w4+  where+  w1 = fromIntegral (shiftR w 24) :: Word8+  w2 = fromIntegral (shiftR w 16) :: Word8+  w3 = fromIntegral (shiftR w 8) :: Word8+  w4 = fromIntegral w :: Word8+ -- | Parse an 'IPv4' using a 'AB.Parser'. -- --   >>> AB.parseOnly parserUtf8 "192.168.2.47"@@ -398,6 +527,20 @@ decodeString = decode . Text.pack  +-- | Unboxed variant of 'IPv4'. Before GHC 8.10, this is+-- implemented as a type synonym. Portable use of this type requires+-- treating it as though it were opaque. Use 'box' and 'unbox' to+-- convert between this and the lifted 'IPv4'.+type IPv4# = Word#++-- | Convert an unboxed IPv4 address to a boxed one. +box :: IPv4# -> IPv4+box w = IPv4 (W32# w)++-- | Convert a boxed IPv4 address to an unboxed one. +unbox :: IPv4 -> IPv4#+unbox (IPv4 (W32# w)) = w+ -- | A 32-bit Internet Protocol version 4 address. To use this with the --   @network@ library, it is necessary to use @Network.Socket.htonl@ to --   convert the underlying 'Word32' from host byte order to network byte@@ -486,7 +629,6 @@ instance FromJSON IPv4 where   parseJSON = Aeson.withText "IPv4" aesonParser -#if MIN_VERSION_aeson(1,0,0) instance ToJSONKey IPv4 where   toJSONKey = ToJSONKeyText     encode@@ -494,7 +636,6 @@  instance FromJSONKey IPv4 where   fromJSONKey = FromJSONKeyTextParser aesonParser-#endif  aesonParser :: Text -> Aeson.Parser IPv4 aesonParser t = case decode t of
src/Net/IPv6.hs view
@@ -1,11 +1,14 @@-{-# LANGUAGE CPP                        #-}-{-# LANGUAGE DataKinds                  #-}-{-# LANGUAGE DeriveGeneric              #-}+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE InstanceSigs               #-}-{-# LANGUAGE ScopedTypeVariables        #-}-{-# LANGUAGE TypeInType                 #-}-{-# LANGUAGE UnboxedTuples              #-}+{-# LANGUAGE InstanceSigs #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE MagicHash #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeInType #-}+{-# LANGUAGE UnboxedTuples #-}  {-| This module provides the IPv6 data type and functions for working     with it.@@ -27,8 +30,14 @@     -- * Textual Conversion     -- ** Text   , encode+  , encodeShort   , decode+  , decodeShort   , parser+    -- * UTF-8 Bytes+  , parserUtf8Bytes+  , decodeUtf8Bytes+  , boundedBuilderUtf8     -- ** Printing   , print     -- * IPv6 Ranges@@ -51,34 +60,47 @@   , IPv6Range(..)   ) where +import Prelude hiding (any, print)+ import Net.IPv4 (IPv4(..))-import qualified Net.IPv4 as IPv4  import Control.Applicative import Control.DeepSeq (NFData)+import Control.Monad.ST (ST) import Data.Bits import Data.Char (chr) import Data.List (intercalate, group)+import Data.Primitive (MutablePrimArray) import Data.Primitive.Types (Prim)-#if !MIN_VERSION_base(4,11,0)-import Data.Semigroup ((<>))-#endif-import qualified Data.Aeson as Aeson-import qualified Data.Attoparsec.Text as AT-import qualified Data.Attoparsec.Text as Atto import Data.Text (Text)-import qualified Data.Text as Text-import qualified Data.Text.IO as TIO+import Data.Text.Short (ShortText) import Data.WideWord.Word128 (Word128(..), zeroWord128) import Data.Word import Foreign.Storable (Storable)-import GHC.Exts+import GHC.Exts (Int#,Word#,Int(I#)) import GHC.Generics (Generic)+import GHC.Word (Word16(W16#)) import Numeric (showHex)-import Prelude hiding (any, print) import Text.ParserCombinators.ReadPrec (prec,step) import Text.Read (Read(..),Lexeme(Ident),lexP,parens) +import qualified Arithmetic.Lte as Lte+import qualified Arithmetic.Nat as Nat+import qualified Data.Aeson as Aeson+import qualified Data.Attoparsec.Text as AT+import qualified Data.Attoparsec.Text as Atto+import qualified Data.ByteArray.Builder.Bounded as BB+import qualified Data.Bytes as Bytes+import qualified Data.Bytes.Parser as Parser+import qualified Data.Bytes.Parser.Latin as Latin+import qualified Data.ByteString.Short.Internal as BSS+import qualified Data.Primitive as PM+import qualified Data.Text as Text+import qualified Data.Text.IO as TIO+import qualified Data.Text.Short.Unsafe as TS+import qualified Data.Text.Short as TS+import qualified Net.IPv4 as IPv4+ -- $setup -- -- These are here to get doctest work.@@ -120,6 +142,17 @@ print :: IPv6 -> IO () print = TIO.putStrLn . encode +-- | Decode 'ShortText' as an 'IPv6' address.+--+--   >>> decodeShort "ffff::2:b"+--   Just (ipv6 0xffff 0x0000 0x0000 0x0000 0x0000 0x0000 0x0002 0x000b)+decodeShort :: ShortText -> Maybe IPv6+decodeShort t = decodeUtf8Bytes (Bytes.fromByteArray b)+  where b = shortByteStringToByteArray (TS.toShortByteString t)++shortByteStringToByteArray :: BSS.ShortByteString -> PM.ByteArray+shortByteStringToByteArray (BSS.SBS x) = PM.ByteArray x+ showHexWord16 :: Word16 -> ShowS showHexWord16 w =     showString "0x"@@ -318,8 +351,8 @@ any :: IPv6 any = IPv6 zeroWord128 --- | Encodes the IP, using zero-compression on the leftmost-longest string of--- zeroes in the address.+-- | Encodes the 'IPv6' address using zero-compression on the leftmost longest+-- string of zeroes in the address. -- Per <https://tools.ietf.org/html/rfc5952#section-5 RFC 5952 Section 5>, -- this uses mixed notation when encoding an IPv4-mapped IPv6 address: --@@ -329,20 +362,29 @@ -- ::ffff:100.55.165.180 -- >>> T.putStrLn $ encode $ fromWord16s 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 -- ::+--+-- Per <https://tools.ietf.org/html/rfc5952#section-4.2.2 Section 4.2.2> of the+-- same RFC, this does not use @::@ to shorten a single 16-bit 0 field. Only+-- runs of multiple 0 fields are considered. encode :: IPv6 -> Text-encode ip =-  if isIPv4MappedAddress-  -- This representation is RECOMMENDED by https://tools.ietf.org/html/rfc5952#section-5-  then Text.pack "::ffff:" `mappend` IPv4.encode (IPv4.IPv4 (fromIntegral w7 `unsafeShiftL` 16 .|. fromIntegral w8))-  else toText [w1, w2, w3, w4, w5, w6, w7, w8]+encode !ip =+  -- TODO: This implementation, while correct, is not particularly efficient.+  -- It uses string all over the place.+  if isIPv4Mapped ip+    -- This representation is RECOMMENDED by https://tools.ietf.org/html/rfc5952#section-5+    then+      Text.pack "::ffff:"+      `mappend`+      IPv4.encode (IPv4.IPv4 (fromIntegral w7 `unsafeShiftL` 16 .|. fromIntegral w8))+    else toText [w1, w2, w3, w4, w5, w6, w7, w8]   where-  isIPv4MappedAddress = w1 == 0 && w2 == 0 && w3 == 0 && w4 == 0 && w5 == 0 && w6 == 0xFFFF   (w1, w2, w3, w4, w5, w6, w7, w8) = toWord16s ip-  toText ws = Text.pack $ intercalate ":" $ expand 0 longestZ grouped+  toText ws = Text.pack $ intercalate ":"+      $ expand 0 (if longestZ > 1 then longestZ else 0) grouped     where-    expand _ 8 _ = ["::"]-    expand _ _ [] = []-    expand i longest ((x, len):wsNext)+    expand !_ 8 !_ = ["::"]+    expand !_ !_ [] = []+    expand !i !longest ((x, len):wsNext)         -- zero-compressed group:         | x == 0 && len == longest =             -- first and last need an extra colon since there's nothing@@ -354,15 +396,305 @@     longestZ = maximum . (0:) . map snd . filter ((==0) . fst) $ grouped     grouped = map (\x -> (head x, length x)) (group ws) --- | Decode an IPv6 address. This accepts both standard IPv6+isIPv4Mapped :: IPv6 -> Bool+isIPv4Mapped (IPv6 (Word128 w1 w2)) =+  w1 == 0 && (0xFFFFFFFF00000000 .&. w2 == 0x0000FFFF00000000)++-- | Decode UTF-8-encoded 'Bytes' into an 'IPv6' address.+--+--   >>> decodeUtf8Bytes (Bytes.fromAsciiString "::cab:1")+--   Just (ipv6 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0cab 0x0001)+decodeUtf8Bytes :: Bytes.Bytes -> Maybe IPv6+decodeUtf8Bytes !b = case Parser.parseBytes (parserUtf8Bytes ()) b of+  Parser.Success (Parser.Slice _ len addr) -> case len of+    0 -> Just addr+    _ -> Nothing+  Parser.Failure _ -> Nothing++-- | Encodes the 'IPv6' address using zero-compression on the+-- leftmost longest string of zeroes in the address.+--+-- >>> BB.run Nat.constant $ boundedBuilderUtf8 $ fromWord16s 0xDEAD 0xBEEF 0x0 0x0 0x0 0x0 0x0 0x1234+-- [0x64, 0x65, 0x61, 0x64, 0x3a, 0x62, 0x65, 0x65, 0x66, 0x3a, 0x3a, 0x31, 0x32, 0x33, 0x34]+boundedBuilderUtf8 :: IPv6 -> BB.Builder 39+boundedBuilderUtf8 !ip@(IPv6 (Word128 hi lo))+  | hi == 0 && lo == 0 = BB.weaken Lte.constant+      (BB.ascii ':' `BB.append` BB.ascii ':')+  | isIPv4Mapped ip = BB.weaken Lte.constant $+      BB.ascii ':'+      `BB.append`+      BB.ascii ':'+      `BB.append`+      BB.ascii 'f'+      `BB.append`+      BB.ascii 'f'+      `BB.append`+      BB.ascii 'f'+      `BB.append`+      BB.ascii 'f'+      `BB.append`+      BB.ascii ':'+      `BB.append`+      IPv4.boundedBuilderUtf8 (IPv4.IPv4 (fromIntegral lo))+  | otherwise =+      let (w0,w1,w2,w3,w4,w5,w6,w7) = toWord16s ip+          IntTriple startLongest longest _ = longestRun w0 w1 w2 w3 w4 w5 w6 w7+          start = startLongest+          end = start + longest+          -- start is inclusive. end is exclusive+       in firstPiece w0 start+          `BB.append`+          piece 1 w1 start end+          `BB.append`+          piece 2 w2 start end+          `BB.append`+          piece 3 w3 start end+          `BB.append`+          piece 4 w4 start end+          `BB.append`+          piece 5 w5 start end+          `BB.append`+          piece 6 w6 start end+          `BB.append`+          lastPiece w7 end++firstPiece :: Word16 -> Int -> BB.Builder 4+firstPiece !w !start = case start of+  0 -> BB.weaken Lte.constant (BB.ascii ':')+  _ -> BB.word16LowerHex w++-- Note about the implementation of piece:+-- It is important to manually perform worker-wrapper so that+-- we can stop piece from inlining. If we do not do this, GHC+-- inlines piece, leading to enormous blowup in the generated+-- Core. The implementation of boundedBuilderUtf8 becomes+-- thousands of lines of Core. Even in the microbenchmark that+-- comes with this library, it can be observed that preventing+-- this inlining improves performance of encodeShort by 50%.+piece :: Int -> Word16 -> Int -> Int -> BB.Builder 5+{-# inline piece #-}+piece (I# ix) (W16# w) (I# start) (I# end) =+  piece# ix w start end++piece# :: Int# -> Word# -> Int# -> Int# -> BB.Builder 5+{-# noinline piece# #-}+piece# !ix# !w# !start# !end# = case compare ix start of+  LT -> BB.ascii ':' `BB.append` BB.word16LowerHex w+  EQ -> BB.weaken Lte.constant (BB.ascii ':')+  GT -> if ix < end+    then BB.weaken Lte.constant BB.empty+    else BB.ascii ':' `BB.append` BB.word16LowerHex w+  where+  ix = I# ix#+  start = I# start#+  end = I# end#+  w = W16# w#++lastPiece :: Word16 -> Int -> BB.Builder 5+lastPiece !w !end = case end of+  8 -> BB.weaken Lte.constant (BB.ascii ':')+  _ -> BB.ascii ':' `BB.append` BB.word16LowerHex w++data IntTriple = IntTriple !Int !Int !Int++-- Choose the longest run. Prefer the leftmost run in the+-- event of a tie.+stepZeroRunLength :: Int -> Word16 -> IntTriple -> IntTriple+stepZeroRunLength !ix !w (IntTriple startLongest longest current) = case w of+  0 -> let !x = current + 1 in+    if x > longest+      then IntTriple (ix - current) x x+      else IntTriple startLongest longest x+  _ -> IntTriple startLongest longest 0++-- We start out by setting the longest run to size 1. This+-- means that we will only detect runs of length two or greater.+longestRun ::+     Word16+  -> Word16+  -> Word16+  -> Word16+  -> Word16+  -> Word16+  -> Word16+  -> Word16+  -> IntTriple+longestRun !w0 !w1 !w2 !w3 !w4 !w5 !w6 !w7 = id+  $ stepZeroRunLength 7 w7+  $ stepZeroRunLength 6 w6+  $ stepZeroRunLength 5 w5+  $ stepZeroRunLength 4 w4+  $ stepZeroRunLength 3 w3+  $ stepZeroRunLength 2 w2+  $ stepZeroRunLength 1 w1+  $ stepZeroRunLength 0 w0+  $ IntTriple (-1) 1 0++-- | Encodes the 'IPv6' address as 'ShortText' using zero-compression on+-- the leftmost longest string of zeroes in the address.+-- Per <https://tools.ietf.org/html/rfc5952#section-5 RFC 5952 Section 5>,+-- this uses mixed notation when encoding an IPv4-mapped IPv6 address.+-- +-- >>> encodeShort $ fromWord16s 0xDEAD 0xBEEF 0x0 0x0 0x0 0x0ABC 0x0 0x1234+-- "dead:beef::abc:0:1234"+encodeShort :: IPv6 -> ShortText+encodeShort w = id+  $ TS.fromShortByteStringUnsafe+  $ byteArrayToShortByteString+  $ BB.run Nat.constant+  $ boundedBuilderUtf8+  $ w++byteArrayToShortByteString :: PM.ByteArray -> BSS.ShortByteString+byteArrayToShortByteString (PM.ByteArray x) = BSS.SBS x++-- | Decode an 'IPv6' address. This accepts both standard IPv6 -- notation (with zero compression) and mixed notation for--- IPv4-mapped IPv6 addresses.+-- IPv4-mapped IPv6 addresses. For a decoding function that+-- additionally accepts dot-decimal-encoded IPv4 addresses,+-- see @Net.IP.decode@. decode :: Text -> Maybe IPv6 decode t = rightToMaybe (AT.parseOnly (parser <* AT.endOfInput) t) +-- | Parse UTF-8-encoded 'Bytes' as an 'IPv6' address. This accepts+-- both uppercase and lowercase characters in the hexadecimal components.+--+-- >>> let str = "dead:beef:3240:a426:ba68:1cd0:4263:109b -> alive"+-- >>> Parser.parseBytes (parserUtf8Bytes ()) (Bytes.fromAsciiString str)+-- Success (Slice {offset = 39, length = 9, value = ipv6 0xdead 0xbeef 0x3240 0xa426 0xba68 0x1cd0 0x4263 0x109b})+--+-- This does not currently support parsing embedded IPv4 address+-- (e.g. @ff00:8000:abc::224.1.2.3@).+parserUtf8Bytes :: e -> Parser.Parser e s IPv6+parserUtf8Bytes e = do+  marr <- Parser.effect (PM.newPrimArray 8)+  -- We cannot immidiately call preZeroes since it wants a+  -- leading colon present.+  Latin.trySatisfy (== ':') >>= \case+    True -> do+      Latin.char e ':'+      postZeroesBegin e marr 0 0+    False -> do+      w <- pieceParser e+      Parser.effect (PM.writePrimArray marr 0 w)+      preZeroes e marr 1++-- This is called when we are positioned before a colon.+-- We may encounter another colon immidiately after+-- the one that we consume here. This indicates zero+-- compression. Or we may encounter another hex-encoded+-- number.+preZeroes ::+     e+  -> MutablePrimArray s Word16 -- length must be 8+  -> Int+  -> Parser.Parser e s IPv6+preZeroes e !marr !ix = case ix of+  8 -> Parser.effect (combinePieces marr)+  _ -> do+    Latin.char e ':'+    Latin.trySatisfy (== ':') >>= \case+      True -> postZeroesBegin e marr ix ix+      False -> do+        w <- pieceParser e+        Parser.effect (PM.writePrimArray marr ix w)+        preZeroes e marr (ix + 1)++-- The same as postZeroes except that there is no+-- leading that gets consumed. This is called right+-- after a double colon is consumed.+-- Precondition: the index is less than 8. This parser+-- is only called by preZeroes, which ensures that+-- this holds.+postZeroesBegin ::+     e+  -> MutablePrimArray s Word16 -- length must be 8+  -> Int -- current index in array+  -> Int -- index where compression happened+  -> Parser.Parser e s IPv6+postZeroesBegin e !marr !ix !compress = do+  optionalPieceParser e >>= \case+    Nothing -> do -- the end has come+      Parser.effect (conclude marr ix compress)+    Just w -> do+      Parser.effect (PM.writePrimArray marr ix w)+      postZeroes e marr (ix + 1) compress++-- Should be run right before a colon.+postZeroes :: +     e+  -> MutablePrimArray s Word16 -- length must be 8+  -> Int -- current index in array+  -> Int -- index where compression happened+  -> Parser.Parser e s IPv6+postZeroes e !marr !ix !compress = case ix of+  8 -> Parser.fail e+  _ -> do+    Latin.trySatisfy (== ':') >>= \case+      False -> -- The end has come+        Parser.effect (conclude marr ix compress)+      True -> do+        w <- pieceParser e+        Parser.effect (PM.writePrimArray marr ix w)+        postZeroes e marr (ix + 1) compress++conclude :: MutablePrimArray s Word16 -> Int -> Int -> ST s IPv6+conclude !marr !ix !compress = do+  -- This will overlap, but GHC's copy primop is fine with that.+  let postCompressionLen = ix - compress+  PM.copyMutablePrimArray marr (8 - postCompressionLen) marr compress postCompressionLen+  let compressedArea = 8 - ix+  PM.setPrimArray marr compress compressedArea (0 :: Word16)+  combinePieces marr++-- Example memmove that may need to happen:+-- A B C H  ==> A B C 0 0 0 0 H+--       *+-- ix = 4, compress = 3, postCompressionLen = 1, compressedArea = 4+-- copyPrimArray marr 7 marr 3 1+-- setPrimArray marr 3 4 (0 :: Word16)++combinePieces ::+     MutablePrimArray s Word16+  -> ST s IPv6+combinePieces !marr = fromWord16s+  <$> PM.readPrimArray marr 0+  <*> PM.readPrimArray marr 1+  <*> PM.readPrimArray marr 2+  <*> PM.readPrimArray marr 3+  <*> PM.readPrimArray marr 4+  <*> PM.readPrimArray marr 5+  <*> PM.readPrimArray marr 6+  <*> PM.readPrimArray marr 7++optionalPieceParser :: e -> Parser.Parser e s (Maybe Word16)+optionalPieceParser e = Latin.tryHexNibble >>= \case+  Nothing -> pure Nothing+  Just w0 -> do+    r <- pieceParserStep e w0+    pure (Just r)++pieceParser :: e -> Parser.Parser e s Word16+pieceParser e = Latin.hexNibble e >>= pieceParserStep e++-- Parses the remainder of a lowercase hexadecimal number.+-- Leaves trailing colons alone. This fails if there are+-- more than four hex digits unless there are leading zeroes.+-- I cannot find a spec that is clear about what to do+-- if someone puts 00000 in a piece of an encoded IPv6+-- address, so I veer on the side of leniency.+pieceParserStep ::+     e+  -> Word+  -> Parser.Parser e s Word16+pieceParserStep e !acc = if acc > 0xFFFF+  then Parser.fail e+  else Latin.tryHexNibble >>= \case+    Nothing -> pure (fromIntegral acc)+    Just w -> pieceParserStep e (16 * acc + w)+ -- | Parse an 'IPv6' using 'Atto.Parser'. -----   >>> ip = ipv6 0xDEAD 0xBEEF 0x3240 0xA426 0xBA68 0x1CD0 0x4263 0x109B --   >>> Atto.parseOnly parser (Text.pack "dead:beef:3240:a426:ba68:1cd0:4263:109b") --   Right (ipv6 0xdead 0xbeef 0x3240 0xa426 0xba68 0x1cd0 0x4263 0x109b) parser :: Atto.Parser IPv6
src/Net/Mac.hs view
@@ -60,7 +60,6 @@ #endif import Data.Text (Text) import Data.Word-import Data.Word (Word8) import Data.Word.Synthetic.Word12 (Word12) import GHC.Enum (predError, succError) import GHC.Exts@@ -756,7 +755,6 @@ instance ToJSON Mac where   toJSON = Aeson.String . encode -#if MIN_VERSION_aeson(1,0,0)  instance ToJSONKey Mac where   toJSONKey = ToJSONKeyText     encode@@ -766,7 +764,6 @@   fromJSONKey = FromJSONKeyTextParser $ \t -> case decode t of     Nothing -> fail "invalid mac address"     Just addr -> return addr-#endif  instance FromJSON Mac where   parseJSON = attoparsecParseJSON parser
test/Bench.hs view
@@ -3,6 +3,7 @@ import Criterion.Main import Net.Types (IPv4(..),MacGrouping(..),MacCodec(..)) import Data.Maybe (fromJust)+import qualified Data.Bytes as Bytes import qualified Data.Text as Text import qualified Net.Mac as Mac import qualified Net.IPv4 as IPv4@@ -20,11 +21,17 @@ main = do   let ipAddr = IPv4 1000000009       ipText = Text.pack "192.168.5.99"+      ipBytes = Bytes.fromAsciiString "192.168.5.99"       mac = Mac.fromOctets 0xFA 0xBB 0x43 0xA1 0x22 0x09       ip6Text = Text.pack "::"       ip6 = fromJust $ IPv6.decode ip6Text-      ip6TextBigger = Text.pack "1:2:3:4:5:6:7:8"+      ip6StrBigger = "1:2:3:4:5:6:7:8"+      ip6TextBigger = Text.pack ip6StrBigger+      ip6BytesBigger = Bytes.fromAsciiString "1:2:3:4:5:6:7:8"       ip6Bigger = fromJust $ IPv6.decode ip6TextBigger+      ip6ComplicatedStr = "2001:db8:ba1:0:aaaa:542c:bb:cc00"+      ip6ComplicatedBytes = Bytes.fromAsciiString ip6ComplicatedStr+      ip6Complicated = fromJust $ IPv6.decode (Text.pack ip6ComplicatedStr)       ip6TextSkip = Text.pack "1:2::7:8"       ip6Skip = fromJust $ IPv6.decode ip6TextSkip       ip6TextHex = Text.pack "a:b::c:d"@@ -44,6 +51,9 @@       , bench "Current Implementation, no separator"           $ whnf (Mac.encodeWithUtf8 (MacCodec MacGroupingNoSeparator True)) mac       ]+    , bgroup "IPv4 to ShortText"+      [ bench "Implementation" $ whnf IPv4.encodeShort ipAddr+      ]     , bgroup "IPv4 to Text"       [ bench "Naive" $ whnf Naive.encodeText ipAddr       , bench "Text Builder" $ whnf IPv4Text2.encode ipAddr@@ -55,21 +65,34 @@       , bench "Attoparsec" $ whnf IPv4DecodeText2.decodeText ipText       , bench "Text Reader" $ whnf IPv4DecodeText1.decodeText ipText       ]+    , bgroup "IPv4 from Bytes"+      [ bench "Current" $ whnf IPv4.decodeUtf8Bytes ipBytes+      ]     , bgroup "IPv4 to ByteString"       [ bench "Naive" $ whnf Naive.encodeByteString ipAddr       , bench "Preallocated: No Lookup Tables" $ whnf IPv4ByteString1.encode ipAddr       , bench "Preallocated" $ whnf IPv4.encodeUtf8 ipAddr       ]+    , bgroup "IPv6 from Text"+      [ bench "::" $ whnf IPv6.decode ip6Text+      , bench "1:2:3:4:5:6:7:8" $ whnf IPv6.decode ip6TextBigger+      , bench "1:2::7:8" $ whnf IPv6.decode ip6TextSkip+      , bench "a:b::c:d" $ whnf IPv6.decode ip6TextHex+      ]+    , bgroup "IPv6 bytesmith"+      [ bench "1:2:3:4:5:6:7:8" $ whnf IPv6.decodeUtf8Bytes ip6BytesBigger+      , bench "2001:db8:ba1:0:aaaa:542c:bb:cc00" $ whnf IPv6.decodeUtf8Bytes ip6ComplicatedBytes+      ]     , bgroup "IPv6 to Text"-      [ bench "New '::'" $ whnf IPv6.decode ip6Text-      , bench "New '1:2:3:4:5:6:7:8'" $ whnf IPv6.decode ip6TextBigger-      , bench "New '1:2::7:8'" $ whnf IPv6.decode ip6TextSkip-      , bench "New 'a:b::c:d'" $ whnf IPv6.decode ip6TextHex+      [ bench "::" $ whnf IPv6.encode ip6+      , bench "1:2:3:4:5:6:7:8" $ whnf IPv6.encode ip6Bigger+      , bench "1:2::7:8" $ whnf IPv6.encode ip6Skip+      , bench "a:b::c:d" $ whnf IPv6.encode ip6Hex       ]-    , bgroup "IPv6 from Text"-      [ bench "New '::'" $ whnf IPv6.encode ip6-      , bench "New '1:2:3:4:5:6:7:8'" $ whnf IPv6.encode ip6Bigger-      , bench "New '1:2::7:8'" $ whnf IPv6.encode ip6Skip-      , bench "New 'a:b::c:d'" $ whnf IPv6.encode ip6Hex+    , bgroup "IPv6 to ShortText"+      [ bench "1:2:3:4:5:6:7:8" $ whnf IPv6.encodeShort ip6Bigger+      , bench "1:2::7:8" $ whnf IPv6.encodeShort ip6Skip+      , bench "a:b::c:d" $ whnf IPv6.encodeShort ip6Hex+      , bench "2001:db8:ba1:0:aaaa:542c:bb:cc00" $ whnf IPv6.encodeShort ip6Complicated       ]     ]
test/IPv4Text1.hs view
@@ -2,7 +2,6 @@  import Net.Types (IPv4(..)) import Data.Text (Text)-import Data.Monoid ((<>)) import Data.Text.Internal (Text(..)) import Data.Word import Data.ByteString (ByteString)
test/IPv4Text2.hs view
@@ -3,7 +3,6 @@ import Net.Types (IPv4(..)) import Data.Text (Text) import Data.Bits ((.&.),shiftR)-import Data.Monoid ((<>)) import Data.Text.Lazy.Builder.Int (decimal) import qualified Data.Text.Lazy         as LText import qualified Data.Text.Lazy.Builder as TBuilder
test/Test.hs view
@@ -7,20 +7,23 @@  import Naive import Control.Applicative (liftA2)+import Data.Bytes (Bytes) import Data.Proxy (Proxy(..))-import Test.Framework (defaultMain, testGroup, Test)-import Test.Framework.Providers.QuickCheck2 (testProperty)-import Test.QuickCheck (Arbitrary(..),Property,oneof,Gen,elements,choose,(===))+import Test.Tasty (defaultMain, testGroup, TestTree)+import Test.Tasty.QuickCheck (testProperty)+import Test.QuickCheck (Arbitrary(..),oneof,Gen,elements,choose,(===)) import Test.HUnit (Assertion,(@?=),(@=?)) import Numeric (showHex) import Test.QuickCheck.Property (failed,succeeded,Result(..)) import Data.Bifunctor-import Test.QuickCheck.Classes (Laws(..),jsonLaws,showReadLaws,bitsLaws,primLaws,boundedEnumLaws)-import qualified Test.Framework.Providers.HUnit as PH+import Test.QuickCheck.Classes (Laws(..),jsonLaws,showReadLaws,primLaws,boundedEnumLaws)+import qualified Test.Tasty.HUnit as PH  import Net.Types (IP,IPv4(..),IPv4Range(..),Mac(..),IPv6(..),MacGrouping(..),MacCodec(..),IPv6Range(..)) import Data.WideWord (Word128(..))+import qualified Data.Bytes as Bytes import qualified Data.Text as Text+import qualified Data.Text.Short as TS import qualified Data.ByteString.Char8 as BC8 import qualified Net.IPv4 as IPv4 import qualified Net.IPv6 as IPv6@@ -38,14 +41,23 @@ main :: IO () main = defaultMain tests -tests :: [Test]-tests =+tests :: TestTree+tests = testGroup "tests"   [ testGroup "Encoding and Decoding"     [ testGroup "Currently used IPv4 encode/decode" $       [ testProperty "Isomorphism"           $ propEncodeDecodeIso IPv4.encode IPv4.decode       , PH.testCase "Decode an IP" testIPv4Decode       ] ++ testDecodeFailures+    , testGroup "Currently used IPv4 encodeShort/decodeShort" $+      [ testProperty "Isomorphism"+          $ propEncodeDecodeIso IPv4.encodeShort IPv4.decodeShort+      ] ++ testDecodeFailures+    , testGroup "Currently used IPv4 UTF-8 Bytes decode"+      [ testProperty "Isomorphism"+          $ propEncodeDecodeIso (byteStringToBytes . IPv4.encodeUtf8) IPv4.decodeUtf8Bytes+      , PH.testCase "Encode a MAC Address" testMacEncode+      ]     , testGroup "Currently used MAC Text encode/decode"       [ testProperty "Isomorphism"           $ propEncodeDecodeIsoSettings Mac.encodeWith Mac.decodeWith@@ -84,9 +96,20 @@       [ PH.testCase "Parser Test Cases" testIPv4Parser       ]     , testGroup "IPv6 encode/decode"-      [ PH.testCase "Parser Test Cases" testIPv6Parser-      , PH.testCase "Encode test cases" testIPv6Encode-      , PH.testCase "Parser Failure Test Cases" testIPv6ParserFailure+      [ PH.testCase "Parser Test Cases" $ testIPv6Parser $ \str -> +          either (\_ -> Nothing) (Just . HexIPv6)+            (AT.parseOnly+              (IPv6.parser <* AT.endOfInput)+              (Text.pack str)+            )+      , PH.testCase "Bytes Parser Test Cases" $ testIPv6Parser $ \str ->+          fmap HexIPv6 (IPv6.decodeUtf8Bytes (Bytes.fromAsciiString str))+      , PH.testCase "Encode test cases" (testIPv6Encode IPv6.encode)+      , PH.testCase "Encode ShortText" (testIPv6Encode (TS.toText . IPv6.encodeShort))+      , PH.testCase "Parser Failure Test Cases"+          (testIPv6ParserFailure expectIPv6ParserFailure)+      , PH.testCase "Bytes Parser Failure Test Cases"+          (testIPv6ParserFailure expectIPv6BytesParserFailure)       ]     ]   , testGroup "IPv4 Range Operations"@@ -148,11 +171,21 @@     ]   ] -lawsToTest :: Laws -> Test+lawsToTest :: Laws -> TestTree lawsToTest (Laws name pairs) = testGroup name (map (uncurry testProperty) pairs) -propEncodeDecodeIso :: Eq a => (a -> b) -> (b -> Maybe a) -> a -> Bool-propEncodeDecodeIso f g a = g (f a) == Just a+propEncodeDecodeIso :: (Eq a, Show a, Show b)+  => (a -> b) -> (b -> Maybe a) -> a -> Result+propEncodeDecodeIso f g a =+  let fa = f a+      gfa = g fa+   in if gfa == Just a+        then succeeded+        else failure $ concat+          [ "x:       ", show a, "\n"+          , "f(x):    ", show fa, "\n"+          , "g(f(x)): ", show gfa, "\n"+          ]  propEncodeDecodeIsoSettings :: (Eq a,Show a,Show b,Show e)   => (e -> a -> b) -> (e -> b -> Maybe a) -> e -> a -> Result@@ -211,8 +244,8 @@           (BC8.pack str)         ) -testIPv6Parser :: Assertion-testIPv6Parser = do+testIPv6Parser :: (String -> Maybe HexIPv6) -> Assertion+testIPv6Parser decode = do   -- Basic test   go 0xABCD 0x1234 0xABCD 0x1234 0xDCBA 0x4321 0xFFFF 0xE0E0      "ABCD:1234:ABCD:1234:DCBA:4321:FFFF:E0E0"@@ -234,22 +267,19 @@      "AAAA:0000:0000:0000:BBBB::ABCD:1234"   where   go a b c d e f g h str =-    Right (HexIPv6 (IPv6.fromWord16s a b c d e f g h))-    @=? fmap HexIPv6-      (AT.parseOnly-        (IPv6.parser <* AT.endOfInput)-        (Text.pack str)-      )+    Just (HexIPv6 (IPv6.fromWord16s a b c d e f g h))+    @=?+    decode str -testIPv6ParserFailure :: Assertion-testIPv6ParserFailure = do+testIPv6ParserFailure :: (String -> Assertion) -> Assertion+testIPv6ParserFailure go = do   -- must not start or end in colon:   go ":::"   go "1::2:"-  go ":1::2"-  go "1:::"-  go ":1::"-  go "::1:"+  go ":1::3"+  go "a:::"+  go ":b::"+  go "::c:"   go "1:2:3:4:5:6:777:8:"   go ":1:2:3:4:5:6:7777:8" @@ -284,17 +314,26 @@   -- IPv4 decimal embedded, with too many parts:   go "1:2:3:4:5:6:7:127.0.0.1"   go "1:2:3:4:5:6:7:8:127.0.0.1"-  where-  go str =-    Left ()-    @=? bimap (\_ -> ()) HexIPv6-      (AT.parseOnly-        (IPv6.parser <* AT.endOfInput)-        (Text.pack str)) -testIPv6Encode :: Assertion-testIPv6Encode = do+expectIPv6ParserFailure :: String -> Assertion+expectIPv6ParserFailure str =+  Left ()+  @=?+  bimap (\_ -> ()) HexIPv6+    (AT.parseOnly+      (IPv6.parser <* AT.endOfInput)+      (Text.pack str)+    ) +expectIPv6BytesParserFailure :: String -> Assertion+expectIPv6BytesParserFailure s =+  Nothing+  @=?+  IPv6.decodeUtf8Bytes (Bytes.fromAsciiString s)++testIPv6Encode :: (IPv6 -> Text.Text) -> Assertion+testIPv6Encode enc = do+   -- degenerate cases:   "::" `roundTripsTo` "::"   "1234::" `roundTripsTo` "1234::"@@ -304,7 +343,7 @@   "1234:1234:0000:0000:0000:0000:3456:3434" `roundTripsTo` "1234:1234::3456:3434"    -- picks first case:-  "1234:0000:1234:0000:1234:0000:0123:1234" `roundTripsTo` "1234::1234:0:1234:0:123:1234"+  "1234:0000:1234:0000:1234:0000:0123:1234" `roundTripsTo` "1234:0:1234:0:1234:0:123:1234"    -- picks longest case:   "1234:0000:1234:0000:0:0000:0123:1234" `roundTripsTo` "1234:0:1234::123:1234"@@ -319,8 +358,8 @@   "1:2:3:4:5:6:7:8" `roundTripsTo` "1:2:3:4:5:6:7:8"    -- works with only first or last:-  "::2:3:4:5:6:7:8" `roundTripsTo` "::2:3:4:5:6:7:8"-  "1:2:3:4:5:6:7::" `roundTripsTo` "1:2:3:4:5:6:7::"+  "::2:3:4:5:6:7:8" `roundTripsTo` "0:2:3:4:5:6:7:8"+  "1:2:3:4:5:6:7::" `roundTripsTo` "1:2:3:4:5:6:7:0"    -- decimal notation in IPv6 addresses:   "1:2:3:4:5:6:0.7.0.8" `roundTripsTo` "1:2:3:4:5:6:7:8"@@ -335,7 +374,7 @@  where  roundTripsTo s sExpected =    case AT.parseOnly (IPv6.parser <* AT.endOfInput) (Text.pack s) of-      Right result -> IPv6.encode result @?= Text.pack sExpected+      Right result -> enc result @?= Text.pack sExpected       Left failMsg -> fail ("failed to parse '" ++ s ++ "': " ++ failMsg)  textBadIPv4 :: [String]@@ -350,7 +389,7 @@   , "1.9"   ] -testDecodeFailures :: [Test]+testDecodeFailures :: [TestTree] testDecodeFailures = flip map textBadIPv4 $ \str ->   PH.testCase ("Should fail to decode [" ++ str ++ "]") $ IPv4.decode (Text.pack str) @?= Nothing @@ -449,3 +488,5 @@ arbitraryMacSeparator :: Gen Char arbitraryMacSeparator = elements [':','-','.','_'] +byteStringToBytes :: BC8.ByteString -> Bytes+byteStringToBytes = Bytes.fromAsciiString . BC8.unpack