ip 1.0.0 → 1.1.0
raw patch · 12 files changed
+249/−108 lines, 12 filesdep +quickcheck-classesdep ~attoparsecdep ~primitivePVP ok
version bump matches the API change (PVP)
Dependencies added: quickcheck-classes
Dependency ranges changed: attoparsec, primitive
API changes (from Hackage documentation)
- Net.IPv4: print :: IPv4 -> IO ()
- Net.Mac: [getMac] :: Mac -> Word64
- Net.Mac: decodeEither :: Text -> Either String Mac
- Net.Mac: decodeEitherWith :: MacCodec -> Text -> Either String Mac
- Net.Mac: parserLenientUtf8 :: Parser Mac
- Net.Types: [getMac] :: Mac -> Word64
+ Net.IPv4: ipv4 :: Word8 -> Word8 -> Word8 -> Word8 -> IPv4
+ Net.IPv6: ipv6 :: Word16 -> Word16 -> Word16 -> Word16 -> Word16 -> Word16 -> Word16 -> Word16 -> IPv6
+ Net.Mac: decodeBytes :: ByteString -> Maybe Mac
+ Net.Mac: mac :: Word64 -> Mac
- Net.IP: ipv4 :: IP -> Maybe IPv4
+ Net.IP: ipv4 :: Word8 -> Word8 -> Word8 -> Word8 -> IP
- Net.IP: ipv6 :: IP -> Maybe IPv6
+ Net.IP: ipv6 :: Word16 -> Word16 -> Word16 -> Word16 -> Word16 -> Word16 -> Word16 -> Word16 -> IP
Files
- ip.cabal +9/−8
- src/Data/ByteString/Builder/Fixed.hs +1/−10
- src/Data/Text/Builder/Fixed.hs +1/−12
- src/Data/Word/Synthetic/Word12.hs +0/−1
- src/Net/IP.hs +42/−22
- src/Net/IPv4.hs +24/−17
- src/Net/IPv4/Range.hs +4/−3
- src/Net/IPv6.hs +14/−7
- src/Net/Mac.hs +107/−20
- test/Doctests.hs +6/−0
- test/Naive.hs +3/−6
- test/Test.hs +38/−2
ip.cabal view
@@ -1,5 +1,5 @@ name: ip-version: 1.0.0+version: 1.1.0 synopsis: Library for IP and MAC addresses homepage: https://github.com/andrewthad/haskell-ip#readme license: BSD3@@ -48,13 +48,13 @@ Data.ByteString.Builder.Fixed build-depends: base >= 4.8 && < 5- , attoparsec+ , attoparsec >= 0.13 && < 0.14 , aeson >= 0.9 && < 1.3 , hashable >= 1.2 && < 1.3 , text >= 1.2 && < 1.3 , bytestring >= 0.10 && < 0.11 , vector >= 0.11 && < 0.13- , primitive+ , primitive >= 0.6 && < 0.7 -- if impl(ghcjs) -- build-depends: ghcjs-base >= 0.2 && < 0.3 ghc-options: -Wall -O2@@ -70,6 +70,7 @@ , test-framework , test-framework-quickcheck2 , QuickCheck+ , quickcheck-classes , text , bytestring , HUnit@@ -81,13 +82,13 @@ IPv4Text2 IPv4ByteString1 -- IPv4TextVariableBuilder- ghc-options: -Wall -O2- default-language: Haskell2010+ ghc-options: -Wall -O2+ default-language: Haskell2010 test-suite doctest- type: exitcode-stdio-1.0- hs-source-dirs: test- main-is: Doctests.hs+ type: exitcode-stdio-1.0+ hs-source-dirs: test+ main-is: Doctests.hs build-depends: base , ip
src/Data/ByteString/Builder/Fixed.hs view
@@ -3,7 +3,7 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE BangPatterns #-} -{-# OPTIONS_GHC -O2 -Wall -funbox-strict-fields #-}+{-# OPTIONS_GHC -Wall -funbox-strict-fields #-} {-| For concatenating fixed-width strings that are only a few characters each, this can be six times faster than the builder@@ -22,22 +22,17 @@ , word12HexFixedUpper ) where -import Control.Monad.ST import Data.Monoid import Data.Word import Data.Word.Synthetic.Word12 (Word12) import Data.Bits import Data.Char (ord) import Text.Printf-import Debug.Trace import Data.ByteString.Internal (ByteString(..)) import Foreign import Data.ByteString.Short (ShortByteString)-import System.IO.Unsafe import qualified Data.ByteString.Internal as BI-import qualified Data.ByteString.Builder as BBuilder import qualified Data.ByteString as ByteString-import qualified Data.ByteString.Lazy as LByteString import qualified Data.ByteString.Char8 as BC8 import qualified Data.ByteString.Short.Internal as SBS @@ -138,10 +133,6 @@ word8 :: Builder Word8 word8 = BuilderFunction (BC8.pack "-") $ \i marr w -> pokeByteOff marr i w {-# INLINE word8 #-}--word8At :: Int -> Word64 -> Word8-word8At i w = fromIntegral (unsafeShiftR w i)-{-# INLINE word8At #-} -- | Taken from @Data.ByteString.Internal@. The same warnings -- apply here.
src/Data/Text/Builder/Fixed.hs view
@@ -3,7 +3,7 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE BangPatterns #-} -{-# OPTIONS_GHC -O2 -Wall -funbox-strict-fields #-}+{-# OPTIONS_GHC -Wall -funbox-strict-fields #-} {-| For concatenating fixed-width strings that are only a few characters each, this can be ten times faster than the builder@@ -25,23 +25,12 @@ import Data.Monoid import Data.Word import Data.Bits-import Text.Printf-import Debug.Trace import Data.Char (ord) import Data.Word.Synthetic.Word12 (Word12)-import Data.Vector (Vector)-import Data.Foldable (fold) import Data.Text (Text)-import qualified Data.Vector as Vector import qualified Data.Text as Text-import qualified Data.Text.Lazy as LText-import qualified Data.Text.Lazy.IO as LText-import qualified Data.Text.Lazy.Builder as TBuilder-import qualified Data.Text.Lazy.Builder.Int as TBuilder-import qualified Data.Text.IO as Text import qualified Data.Text.Array as A import qualified Data.Text.Internal as TI-import qualified Data.Text.Internal.Unsafe.Char as TC import qualified Data.Text.Builder.Common.Internal as I data Builder a where
src/Data/Word/Synthetic/Word12.hs view
@@ -35,7 +35,6 @@ import GHC.Base import GHC.Enum import GHC.Num-import GHC.Ptr import GHC.Read import GHC.Real import GHC.Show
src/Net/IP.hs view
@@ -1,9 +1,35 @@+{-# OPTIONS_GHC -Wall #-}++{-| An IP data type representing either an IPv4 address or+ an IPv6 address. The user can think of this+ as though it were a sum type. However, to minimize indirections,+ it is actually implemented as an 'IPv6' address, with 'IPv4'+ addresses being represented as an IPv4-mapped IPv6 addresses:++ > +---------+---------+--------------++ > | 80 bits | 16 bits | 32 bits |+ > +---------+---------+--------------++ > | 00...00 | FFFF | IPv4 address |+ > +---------+---------+--------------+++ All functions and instance methods that deal with textual conversion+ will encode an 'IP' using either dot-decimal notation (for IPv4) or+ RFC 5952 (for IPv6). They will decode an 'IP' from either format+ as well. The 'Show' instance presents an address in as valid haskell code + that resembles the formatted address:+ + >>> decode "192.168.3.100"+ Just (ipv4 192 168 3 100)+ >>> decode "A3F5:12:F26::1466:8B91"+ Just (ipv6 0xa3f5 0x0012 0x0f26 0x0000 0x0000 0x0000 0x1466 0x8b91)+-}+ module Net.IP ( -- * Pattern Matching case_+ -- * Construction , ipv4 , ipv6- -- * Construction , fromIPv4 , fromIPv6 -- * Textual Conversion@@ -17,10 +43,11 @@ import Data.Bits import Net.IPv6 (IPv6(..)) import Net.IPv4 (IPv4(..))-import Text.Read (Read(..),Lexeme(Ident),lexP,parens)-import Text.ParserCombinators.ReadPrec (prec,step,(+++))+import Text.Read (Read(..))+import Text.ParserCombinators.ReadPrec ((+++)) import Data.Aeson (FromJSON(..),ToJSON(..)) import Data.Text (Text)+import Data.Word (Word8,Word16) import qualified Net.IPv4 as IPv4 import qualified Net.IPv6 as IPv6 import qualified Data.Aeson as Aeson@@ -30,14 +57,17 @@ then f (IPv4 (fromIntegral w2)) else g addr --- | If the address is an 'IPv4' address, return the address.-ipv4 :: IP -> Maybe IPv4-ipv4 = case_ Just (const Nothing)+-- | Construct an 'IP' address from the four octets of+-- an IPv4 address.+ipv4 :: Word8 -> Word8 -> Word8 -> Word8 -> IP+ipv4 a b c d = fromIPv4 (IPv4.fromOctets a b c d) --- | If the address is an 'IPv6' address, and if it is not--- an IPv4-mapped IPv6 address, return the address.-ipv6 :: IP -> Maybe IPv6-ipv6 = case_ (const Nothing) Just+-- | Construct an 'IP' address from the eight 16-bit+-- chunks of an IPv6 address.+ipv6 :: Word16 -> Word16 -> Word16 -> Word16+ -> Word16 -> Word16 -> Word16 -> Word16+ -> IP+ipv6 a b c d e f g h = fromIPv6 (IPv6.fromWord16s a b c d e f g h) fromIPv4 :: IPv4 -> IP fromIPv4 (IPv4 w) = IP (IPv6 0 (0x0000FFFF00000000 .|. fromIntegral w))@@ -64,20 +94,10 @@ deriving (Eq,Ord) instance Show IP where- showsPrec p = case_- (\x -> showParen (p > 10) $ showString "fromIPv4 " . showsPrec 11 x)- (\x -> showParen (p > 10) $ showString "fromIPv6 " . showsPrec 11 x)+ showsPrec p = case_ (showsPrec p) (showsPrec p) instance Read IP where- readPrec = parens $- ( prec 10 $ do- Ident "fromIPv4 " <- lexP- fmap fromIPv4 (step readPrec)- ) +++- ( prec 10 $ do- Ident "fromIPv6 " <- lexP- fmap fromIPv6 (step readPrec)- )+ readPrec = fmap fromIPv4 readPrec +++ fmap fromIPv6 readPrec instance ToJSON IP where toJSON = Aeson.String . encode
src/Net/IPv4.hs view
@@ -23,7 +23,8 @@ module Net.IPv4 ( -- * Conversion Functions- fromOctets+ ipv4+ , fromOctets , toOctets -- * Special IP Addresses , any@@ -40,7 +41,6 @@ , builder , reader , parser- , print -- ** UTF-8 ByteString , encodeUtf8 , decodeUtf8@@ -54,7 +54,7 @@ , IPv4(..) ) where -import Prelude hiding (any,print)+import Prelude hiding (any) import Data.Bits ((.&.),(.|.),shiftR,shiftL,unsafeShiftR) import Data.Word import Data.Hashable@@ -77,7 +77,6 @@ import Text.ParserCombinators.ReadPrec (prec,step) import qualified Data.Text.Read as TextRead import qualified Data.ByteString.Builder as Builder-import qualified Data.Text.IO as Text import qualified Data.ByteString.Unsafe as ByteString import qualified Data.Text.Read as Text (Reader) import qualified Data.Text.Lazy.Builder as TBuilder@@ -107,6 +106,7 @@ -- -- >>> import Test.QuickCheck (Arbitrary(..)) -- >>> import qualified Prelude as P+-- >>> import qualified Data.Text.IO as T -- >>> instance Arbitrary IPv4 where { arbitrary = fmap IPv4 arbitrary } -- @@ -117,12 +117,16 @@ -- Additionally, it is used for the 'Show' and 'Read' instances -- of 'IPv4' to help keep things readable in GHCi. ----- >>> let ip = fromOctets 192 168 1 1--- >>> ip--- fromOctets 192 168 1 1--- >>> getIPv4 ip+-- >>> let addr = ipv4 192 168 1 1+-- >>> addr+-- ipv4 192 168 1 1+-- >>> getIPv4 addr -- 3232235777 --+ipv4 :: Word8 -> Word8 -> Word8 -> Word8 -> IPv4+ipv4 = fromOctets++-- | An alias for the 'ipv4' smart constructor. fromOctets :: Word8 -> Word8 -> Word8 -> Word8 -> IPv4 fromOctets a b c d = fromOctets' (fromIntegral a) (fromIntegral b) (fromIntegral c) (fromIntegral d)@@ -211,12 +215,18 @@ public :: IPv4 -> Bool public = not . reserved +-- | Encode an 'IPv4' address to 'Text' using dot-decimal notation:+--+-- >>> T.putStrLn (encode (ipv4 192 168 2 47))+-- 192.168.2.47 encode :: IPv4 -> Text encode = toDotDecimalText +-- | Decode an 'IPv4' address. decode :: Text -> Maybe IPv4 decode = decodeIPv4TextMaybe +-- | Encode an 'IPv4' address to a text 'TBuilder.Builder'. builder :: IPv4 -> TBuilder.Builder builder = toDotDecimalBuilder @@ -226,10 +236,7 @@ parser :: AT.Parser IPv4 parser = dotDecimalParser --- | This exists mostly for testing purposes.-print :: IPv4 -> IO ()-print = Text.putStrLn . encode-+-- | Encode an 'IPv4' address to a UTF-8 encoded 'ByteString'. encodeUtf8 :: IPv4 -> ByteString encodeUtf8 = toBSPreAllocated @@ -292,7 +299,7 @@ where limitSize i = if i > 255- then fail "All octets in an ip address must be between 0 and 255"+ then fail "All octets in an ipv4 address must be between 0 and 255" else return i {- $string@@ -318,7 +325,7 @@ instance Show IPv4 where showsPrec p addr = showParen (p > 10)- $ showString "fromOctets "+ $ showString "ipv4 " . showsPrec 11 a . showChar ' ' . showsPrec 11 b@@ -331,7 +338,7 @@ instance Read IPv4 where readPrec = parens $ prec 10 $ do- Ident "fromOctets" <- lexP+ Ident "ipv4" <- lexP a <- step readPrec b <- step readPrec c <- step readPrec@@ -395,7 +402,7 @@ instance ToJSONKey IPv4 where toJSONKey = ToJSONKeyText encode- (\ip -> Aeson.unsafeToEncoding $ BB.char7 '"' <> builderUtf8 ip <> BB.char7 '"')+ (\addr -> Aeson.unsafeToEncoding $ BB.char7 '"' <> builderUtf8 addr <> BB.char7 '"') #if MIN_VERSION_aeson(1,0,0) instance FromJSONKey IPv4 where@@ -405,7 +412,7 @@ aesonParser :: Text -> Aeson.Parser IPv4 aesonParser t = case decode t of Nothing -> fail "Could not parse IPv4 address"- Just ip -> return ip+ Just addr -> return addr ------------------------------------
src/Net/IPv4/Range.hs view
@@ -59,7 +59,8 @@ -- -- >>> import qualified Prelude as P -- >>> import qualified Net.IPv4 as I--- >>> import Net.IPv4 (fromOctets)+-- >>> import qualified Data.Text.IO as T+-- >>> import Net.IPv4 (fromOctets,ipv4) -- >>> import Test.QuickCheck (Arbitrary(..)) -- >>> instance Arbitrary IPv4 where { arbitrary = fmap IPv4 arbitrary } -- >>> instance Arbitrary IPv4Range where { arbitrary = IPv4Range <$> arbitrary <*> arbitrary }@@ -108,7 +109,7 @@ -- | The inclusive lower bound of an 'IPv4Range'. This is conventionally -- understood to be the broadcast address of a subnet. For example: ----- >>> I.print $ lowerInclusive $ IPv4Range (fromOctets 10 10 1 160) 25+-- >>> T.putStrLn $ I.encode $ lowerInclusive $ IPv4Range (ipv4 10 10 1 160) 25 -- 10.10.1.128 -- -- Note that the lower bound of a normalized 'IPv4Range' is simply the@@ -149,7 +150,7 @@ -- | Convert an 'IPv4Range' into a list of the 'IPv4' addresses that -- are in it. -- >>> let r = IPv4Range (fromOctets 192 168 1 8) 30--- >>> mapM_ I.print (toList r)+-- >>> mapM_ (T.putStrLn . I.encode) (toList r) -- 192.168.1.8 -- 192.168.1.9 -- 192.168.1.10
src/Net/IPv6.hs view
@@ -6,6 +6,7 @@ ( -- * Types IPv6(..) -- * Convert+ , ipv6 , fromOctets , fromWord16s , toWord16s@@ -50,7 +51,7 @@ instance Show IPv6 where showsPrec p addr = showParen (p > 10)- $ showString "fromWord16s "+ $ showString "ipv6 " . showHexWord16 a . showChar ' ' . showHexWord16 b@@ -66,7 +67,6 @@ . showHexWord16 g . showChar ' ' . showHexWord16 h- . showChar ' ' where (a,b,c,d,e,f,g,h) = toWord16s addr @@ -86,7 +86,7 @@ instance Read IPv6 where readPrec = parens $ prec 10 $ do- Ident "fromWord16s" <- lexP+ Ident "ipv6" <- lexP a <- step readPrec b <- step readPrec c <- step readPrec@@ -128,11 +128,18 @@ -- feature for suppress zeroes in an 'IPv6' address, but it should be -- readable enough for hacking in GHCi. ----- >>> let ip = fromWord16s 0x3124 0x0 0x0 0xDEAD 0xCAFE 0xFF 0xFE00 0x1--- >>> ip--- fromWord16s 0x3124 0x0000 0x0000 0xdead 0xcafe 0x00ff 0xfe00 0x0001--- >>> T.putStrLn (encode ip)+-- >>> let addr = ipv6 0x3124 0x0 0x0 0xDEAD 0xCAFE 0xFF 0xFE00 0x1+-- >>> addr+-- ipv6 0x3124 0x0000 0x0000 0xdead 0xcafe 0x00ff 0xfe00 0x0001+-- >>> T.putStrLn (encode addr) -- 3124::dead:cafe:ff:fe00:1+ipv6 :: + Word16 -> Word16 -> Word16 -> Word16+ -> Word16 -> Word16 -> Word16 -> Word16+ -> IPv6+ipv6 = fromWord16s++-- | An alias for the 'ipv6' smart constructor. fromWord16s :: Word16 -> Word16 -> Word16 -> Word16 -> Word16 -> Word16 -> Word16 -> Word16
src/Net/Mac.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE CPP #-}+{-# LANGUAGE BangPatterns #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE InstanceSigs #-}@@ -11,7 +12,8 @@ {-# OPTIONS_GHC -Wall #-} module Net.Mac ( -- * Convert- fromOctets+ mac+ , fromOctets , toOctets -- * Textual Conversion -- ** Text@@ -19,8 +21,6 @@ , encodeWith , decode , decodeWith- , decodeEither- , decodeEitherWith , builder , parser , parserWith@@ -32,7 +32,8 @@ , builderUtf8 , parserUtf8 , parserWithUtf8- , parserLenientUtf8+ -- ** ByteString+ , decodeBytes -- * Types , Mac(..) , MacCodec(..)@@ -40,7 +41,7 @@ ) where import Data.Word-import Data.Bits ((.|.),unsafeShiftL,unsafeShiftR)+import Data.Bits ((.|.),unsafeShiftL,unsafeShiftR,(.&.)) import Data.Text (Text) import Data.Word (Word8) import Data.Word.Synthetic.Word12 (Word12)@@ -49,7 +50,11 @@ import Data.Aeson (FromJSON(..),ToJSON(..)) import Data.Hashable (Hashable) import GHC.Generics (Generic)-import Data.Char (ord)+import Data.Char (ord,chr)+import Text.Read (Read(..),Lexeme(Ident),lexP,parens)+import Text.ParserCombinators.ReadPrec (prec,step)+import qualified Data.ByteString as B+import qualified Data.ByteString.Unsafe as BU import qualified Data.ByteString.Builder as BB import qualified Data.Attoparsec.ByteString as ABW import qualified Data.Attoparsec.Text as AT@@ -65,12 +70,31 @@ ToJSONKeyFunction(..),FromJSONKeyFunction(..)) #endif +-- $setup+--+-- These are here to get doctest's property checking to work+--+-- >>> :set -XOverloadedStrings+-- >>> import Test.QuickCheck (Arbitrary(..))+-- >>> import qualified Data.Text.IO as T+-- >>> import qualified Data.ByteString.Char8 as BC+-- >>> instance Arbitrary Mac where { arbitrary = fmap (Mac . (0xFFFFFFFFFFFF .&.)) arbitrary }++-- | Construct a 'Mac' address from a 'Word64'. Only the lower+-- 48 bits are used.+mac :: Word64 -> Mac+mac w = Mac (w .&. 0xFFFFFFFFFFFF)++-- | Create a 'Mac' address from six octets. fromOctets :: Word8 -> Word8 -> Word8 -> Word8 -> Word8 -> Word8 -> Mac fromOctets a b c d e f = Mac $ unsafeWord48FromOctets (fromIntegral a) (fromIntegral b) (fromIntegral c) (fromIntegral d) (fromIntegral e) (fromIntegral f)-{-# INLINE fromOctets #-} +-- | Convert a 'Mac' address to the six octets that make it up.+-- This function and 'fromOctets' are inverses:+--+-- prop> m == (let (a,b,c,d,e,f) = toOctets m in fromOctets a b c d e f) toOctets :: Mac -> (Word8,Word8,Word8,Word8,Word8,Word8) toOctets (Mac w) = ( fromIntegral $ unsafeShiftR w 40@@ -81,14 +105,41 @@ , fromIntegral w ) +-- | Decode a 'Mac' address from a 'ByteString'. Each byte is interpreted+-- as an octet of the 'Mac' address. Consequently, 'ByteString's+-- of length 6 successfully decode, and all other 'ByteString's fail+-- to decode.+--+-- >>> decodeBytes (B.pack [0x6B,0x47,0x18,0x90,0x55,0xC3])+-- Just (mac 0x6b47189055c3)+-- >>> decodeBytes (B.replicate 6 0x3A)+-- Just (mac 0x3a3a3a3a3a3a)+-- >>> decodeBytes (B.replicate 7 0x3A)+-- Nothing+decodeBytes :: ByteString -> Maybe Mac+decodeBytes bs = if B.length bs == 6+ then Just $ fromOctets+ (BU.unsafeIndex bs 0)+ (BU.unsafeIndex bs 1)+ (BU.unsafeIndex bs 2)+ (BU.unsafeIndex bs 3)+ (BU.unsafeIndex bs 4)+ (BU.unsafeIndex bs 5)+ else Nothing+ rightToMaybe :: Either a b -> Maybe b rightToMaybe = either (const Nothing) Just c2w :: Char -> Word8 c2w = fromIntegral . ord +-- | Encode a 'Mac' address lowercase hex, separating every two characters+-- with a colon:+--+-- >>> T.putStrLn (encode (Mac 0xA47F247AB423))+-- a4:7f:24:7a:b4:23 encode :: Mac -> Text-encode = encodeWith defCodec -- Internal.macToTextDefault w+encode = encodeWith defCodec encodeWith :: MacCodec -> Mac -> Text encodeWith (MacCodec g u) m = case g of@@ -106,12 +157,6 @@ True -> TFB.run (fixedBuilderQuadruples TFB.word8HexFixedUpper) (Pair c m) False -> TFB.run (fixedBuilderQuadruples TFB.word8HexFixedLower) (Pair c m) -decodeEitherWith :: MacCodec -> Text -> Either String Mac-decodeEitherWith codec t = AT.parseOnly (parserWith codec <* AT.endOfInput) t--decodeEither :: Text -> Either String Mac-decodeEither = decodeEitherWith defCodec- decode :: Text -> Maybe Mac decode = decodeWith defCodec @@ -264,11 +309,23 @@ word12At i (Mac w) = fromIntegral (unsafeShiftR w i) {-# INLINE word12At #-} +-- | Encode a 'Mac' address, as lowercase hexadecimal digits+-- separated by a colon:+--+-- >>> BC.putStrLn (encodeUtf8 (mac 0x64255A0F2C47))+-- 64:25:5a:0f:2c:47 encodeUtf8 :: Mac -> ByteString encodeUtf8 = encodeWithUtf8 defCodec -- | Lenient decoding of MAC address that accepts lowercase, uppercase, -- and any kind separator.+--+-- >>> decodeUtf8 "A2:DE:AD:BE:EF:67"+-- Just (mac 0xa2deadbeef67)+-- >>> decodeUtf8 "13-a2-fe-a4-17-96"+-- Just (mac 0x13a2fea41796)+-- >>> decodeUtf8 "0A42.47BA.67C2"+-- Just (mac 0x0a4247ba67c2) decodeUtf8 :: ByteString -> Maybe Mac decodeUtf8 = decodeLenientUtf8 @@ -283,10 +340,11 @@ builderUtf8 :: Mac -> BB.Builder builderUtf8 = BB.byteString . encodeUtf8 --- | Parser for a 'Mac' address using with a colon as the--- separator (i.e. @FA:43:B2:C0:0F:99@).+-- | Lenient parser for a 'Mac' address using any character+-- as the separator and accepting any digit grouping+-- (i.e. @FA:43:B2:C0:0F:99@ or @A065.647B.87FA@). parserUtf8 :: AB.Parser Mac-parserUtf8 = parserWithUtf8 defCodec+parserUtf8 = parserLenientUtf8 -- | Parser for a 'Mac' address using the provided settings. parserWithUtf8 :: MacCodec -> AB.Parser Mac@@ -479,9 +537,38 @@ -- | A 48-bit MAC address. Do not use the data constructor for this -- type. It is not considered part of the stable API, and it -- allows you to construct invalid MAC addresses.-newtype Mac = Mac { getMac :: Word64 }- deriving (Eq,Ord,Show,Read,Generic)+newtype Mac = Mac Word64+ deriving (Eq,Ord,Generic) +-- What this instance does is to display the+-- inner contents in hexadecimal and pad them out to+-- 6 bytes in case it begins with several zeroes.+-- It also uses the smart constructor instead+-- of the actual constructor+instance Show Mac where+ showsPrec p (Mac addr) = showParen (p > 10)+ $ showString "mac "+ . showHexWord48 addr++instance Read Mac where+ readPrec = parens $ prec 10 $ do+ Ident "mac" <- lexP+ w <- step readPrec+ return (mac w)++showHexWord48 :: Word64 -> ShowS+showHexWord48 w = showString "0x" . go 11+ where+ go :: Int -> ShowS+ go !ix = if ix >= 0+ then showChar (nibbleToHex ((unsafeShiftR (fromIntegral w) (ix * 4)) .&. 0xF)) . go (ix - 1)+ else id++nibbleToHex :: Word -> Char+nibbleToHex w+ | w < 10 = chr (fromIntegral (w + 48))+ | otherwise = chr (fromIntegral (w + 87))+ data MacCodec = MacCodec { macCodecGrouping :: !MacGrouping , macCodecUpperCase :: !Bool@@ -512,7 +599,7 @@ instance FromJSONKey Mac where fromJSONKey = FromJSONKeyTextParser $ \t -> case decode t of Nothing -> fail "invalid mac address"- Just mac -> return mac+ Just addr -> return addr #endif instance FromJSON Mac where
test/Doctests.hs view
@@ -4,5 +4,11 @@ main = doctest [ "src/Net/IPv4.hs" , "src/Net/IPv6.hs"+ , "src/Net/IP.hs" , "src/Net/IPv4/Range.hs"+ , "src/Data/Word/Synthetic/Word12.hs"+ , "src/Data/Text/Builder/Common/Internal.hs"+ , "src/Data/Text/Builder/Fixed.hs"+ , "src/Data/ByteString/Builder/Fixed.hs"+ , "src/Net/Mac.hs" ]
test/Naive.hs view
@@ -13,12 +13,9 @@ import Data.ByteString (ByteString) import Control.Monad.ST import Data.Text.Encoding (encodeUtf8)-import qualified Data.ByteString.Char8 as BC8-import qualified Data.ByteString as ByteString-import qualified Data.ByteString.Unsafe as ByteString-import qualified Data.Text.Lazy as LText-import qualified Data.Text.Lazy.Builder as TBuilder-import qualified Data.Text.Array as TArray+import qualified Data.ByteString.Char8 as BC8+import qualified Data.ByteString as ByteString+import qualified Data.Text.Lazy as LText encodeByteString :: IPv4 -> ByteString encodeByteString = encodeUtf8 . encodeText
test/Test.hs view
@@ -6,22 +6,25 @@ module Main (main) where import Naive+import Data.Proxy (Proxy(..)) import Test.Framework (defaultMain, testGroup, Test) import Test.Framework.Providers.QuickCheck2 (testProperty)-import Test.QuickCheck (Arbitrary(..),oneof,Gen,elements)+import Test.QuickCheck (Arbitrary(..),Property,oneof,Gen,elements) import Test.HUnit (Assertion,(@?=)) import Numeric (showHex) import Test.QuickCheck.Property (failed,succeeded,Result(..)) import Data.Bifunctor+import Test.QuickCheck.Classes (jsonProps,showReadProps) import qualified Test.Framework.Providers.HUnit as PH -import Net.Types (IPv4(..),IPv4Range(..),Mac(..),IPv6(..),MacGrouping(..),MacCodec(..))+import Net.Types (IP,IPv4(..),IPv4Range(..),Mac(..),IPv6(..),MacGrouping(..),MacCodec(..)) import qualified Data.Text as Text import qualified Data.ByteString.Char8 as BC8 import qualified Net.IPv4 as IPv4 import qualified Net.IPv6 as IPv6 import qualified Net.IPv4.Range as IPv4Range import qualified Net.Mac as Mac+import qualified Net.IP as IP import qualified Data.Attoparsec.Text as AT import qualified Data.Attoparsec.ByteString as AB@@ -92,8 +95,29 @@ , testProperty "Membership agrees with bounds" propMemberUpperLower , testProperty "Range contains self" propRangeSelf ]+ , testGroup "Instances"+ [ testGroup "IPv4"+ [ namedPropertiesToTest "JSON" (jsonProps (Proxy :: Proxy IPv4))+ , namedPropertiesToTest "Show Read" (showReadProps (Proxy :: Proxy IPv4))+ ]+ , testGroup "IPv6"+ [ namedPropertiesToTest "JSON" (jsonProps (Proxy :: Proxy IPv6))+ , namedPropertiesToTest "Show Read" (showReadProps (Proxy :: Proxy IPv6))+ ]+ , testGroup "IP"+ [ namedPropertiesToTest "JSON" (jsonProps (Proxy :: Proxy IP))+ , namedPropertiesToTest "Show Read" (showReadProps (Proxy :: Proxy IP))+ ]+ , testGroup "Mac"+ [ namedPropertiesToTest "JSON" (jsonProps (Proxy :: Proxy Mac))+ , namedPropertiesToTest "Show Read" (showReadProps (Proxy :: Proxy Mac))+ ]+ ] ] +namedPropertiesToTest :: String -> [(String,Property)] -> Test+namedPropertiesToTest 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 @@ -291,6 +315,18 @@ deriving instance Arbitrary IPv4++instance Arbitrary IPv6 where+ arbitrary = IPv6 <$> arbitrary <*> arbitrary++-- Half of the test cases generated are IPv6 mapped+-- IPv4 addresses.+instance Arbitrary IP where+ arbitrary = oneof+ [ IP.fromIPv4 <$> arbitrary+ , IP.fromIPv6 <$> arbitrary+ ]+ instance Arbitrary Mac where arbitrary = Mac.fromOctets <$> arbitrary