{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeInType #-}
{-# LANGUAGE UnboxedTuples #-}
module Net.IPv6
(
ipv6
, fromOctets
, fromWord16s
, fromWord32s
, fromTupleWord16s
, fromTupleWord32s
, toWord16s
, toWord32s
, any
, loopback
, localhost
, encode
, encodeShort
, decode
, decodeShort
, parser
, parserUtf8Bytes
, decodeUtf8Bytes
, boundedBuilderUtf8
, print
, range
, fromBounds
, normalize
, contains
, member
, lowerInclusive
, upperInclusive
, encodeRange
, decodeRange
, parserRange
, printRange
, parserRangeUtf8Bytes
, parserRangeUtf8BytesLenient
, IPv6(..)
, IPv6Range(..)
) where
import Prelude hiding (any, print)
import Net.IPv4 (IPv4(..))
import Control.Applicative
import Control.DeepSeq (NFData)
import Control.Monad (mzero)
import Control.Monad.ST (ST)
import Data.Bits
import Data.Char (chr)
import Data.Data (Data)
import Data.Ix (Ix)
import Data.List (intercalate, group)
import Data.Primitive (MutablePrimArray)
import Data.Primitive.Types (Prim)
import Data.Text (Text)
import Data.Text.Short (ShortText)
import Data.WideWord.Word128 (Word128(..), zeroWord128)
import Data.Word
import Foreign.Storable (Storable)
import GHC.Exts (Int#,Word#,Int(I#))
import GHC.Generics (Generic)
import GHC.Word (Word16(W16#))
import Numeric (showHex)
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.Bytes.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
newtype IPv6 = IPv6 { getIPv6 :: Word128 }
deriving (Bounded,Enum,Eq,Ord,Storable,Bits,FiniteBits,NFData,Prim,Ix,Data)
instance Show IPv6 where
showsPrec p addr = showParen (p > 10)
$ showString "ipv6 "
. showHexWord16 a
. showChar ' '
. showHexWord16 b
. showChar ' '
. showHexWord16 c
. showChar ' '
. showHexWord16 d
. showChar ' '
. showHexWord16 e
. showChar ' '
. showHexWord16 f
. showChar ' '
. showHexWord16 g
. showChar ' '
. showHexWord16 h
where
(a,b,c,d,e,f,g,h) = toWord16s addr
print :: IPv6 -> IO ()
print = TIO.putStrLn . encode
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"
. showChar (nibbleToHex (unsafeShiftR (fromIntegral w) 12))
. showChar (nibbleToHex ((unsafeShiftR (fromIntegral w) 8) .&. 0xF))
. showChar (nibbleToHex ((unsafeShiftR (fromIntegral w) 4) .&. 0xF))
. showChar (nibbleToHex ((fromIntegral w) .&. 0xF))
nibbleToHex :: Word -> Char
nibbleToHex w
| w < 10 = chr (fromIntegral (w + 48))
| otherwise = chr (fromIntegral (w + 87))
instance Read IPv6 where
readPrec = parens $ prec 10 $ do
Ident "ipv6" <- lexP
a <- step readPrec
b <- step readPrec
c <- step readPrec
d <- step readPrec
e <- step readPrec
f <- step readPrec
g <- step readPrec
h <- step readPrec
return (fromWord16s a b c d e f g h)
instance Aeson.ToJSON IPv6 where
toJSON = Aeson.String . encode
instance Aeson.FromJSON IPv6 where
parseJSON = Aeson.withText "IPv6" $ \t -> case decode t of
Nothing -> fail "invalid IPv6 address"
Just i -> return i
rightToMaybe :: Either a b -> Maybe b
rightToMaybe = either (const Nothing) Just
fromOctets ::
Word8 -> Word8 -> Word8 -> Word8
-> Word8 -> Word8 -> Word8 -> Word8
-> Word8 -> Word8 -> Word8 -> Word8
-> Word8 -> Word8 -> Word8 -> Word8
-> IPv6
fromOctets a b c d e f g h i j k l m n o p =
IPv6 $ fromOctetsWord128
(fromIntegral a) (fromIntegral b) (fromIntegral c) (fromIntegral d)
(fromIntegral e) (fromIntegral f) (fromIntegral g) (fromIntegral h)
(fromIntegral i) (fromIntegral j) (fromIntegral k) (fromIntegral l)
(fromIntegral m) (fromIntegral n) (fromIntegral o) (fromIntegral p)
fromOctetsWord128 ::
Word128 -> Word128 -> Word128 -> Word128
-> Word128 -> Word128 -> Word128 -> Word128
-> Word128 -> Word128 -> Word128 -> Word128
-> Word128 -> Word128 -> Word128 -> Word128
-> Word128
fromOctetsWord128 a b c d e f g h i j k l m n o p = fromIntegral
( shiftL a 120
.|. shiftL b 112
.|. shiftL c 104
.|. shiftL d 96
.|. shiftL e 88
.|. shiftL f 80
.|. shiftL g 72
.|. shiftL h 64
.|. shiftL i 56
.|. shiftL j 48
.|. shiftL k 40
.|. shiftL l 32
.|. shiftL m 24
.|. shiftL n 16
.|. shiftL o 8
.|. p
)
ipv6 ::
Word16 -> Word16 -> Word16 -> Word16
-> Word16 -> Word16 -> Word16 -> Word16
-> IPv6
ipv6 = fromWord16s
fromWord16s ::
Word16 -> Word16 -> Word16 -> Word16
-> Word16 -> Word16 -> Word16 -> Word16
-> IPv6
fromWord16s a b c d e f g h =
IPv6 $ fromWord16sWord128
(fromIntegral a) (fromIntegral b) (fromIntegral c) (fromIntegral d)
(fromIntegral e) (fromIntegral f) (fromIntegral g) (fromIntegral h)
fromWord16sWord128 ::
Word128 -> Word128 -> Word128 -> Word128
-> Word128 -> Word128 -> Word128 -> Word128
-> Word128
fromWord16sWord128 a b c d e f g h = fromIntegral
( shiftL a 112
.|. shiftL b 96
.|. shiftL c 80
.|. shiftL d 64
.|. shiftL e 48
.|. shiftL f 32
.|. shiftL g 16
.|. h
)
toWord16s :: IPv6 -> (Word16,Word16,Word16,Word16,Word16,Word16,Word16,Word16)
toWord16s (IPv6 (Word128 a b)) =
( fromIntegral (unsafeShiftR a 48)
, fromIntegral (unsafeShiftR a 32)
, fromIntegral (unsafeShiftR a 16)
, fromIntegral a
, fromIntegral (unsafeShiftR b 48)
, fromIntegral (unsafeShiftR b 32)
, fromIntegral (unsafeShiftR b 16)
, fromIntegral b
)
fromTupleWord16s :: (Word16,Word16,Word16,Word16,Word16,Word16,Word16,Word16) -> IPv6
fromTupleWord16s (a,b,c,d,e,f,g,h) = fromWord16s a b c d e f g h
fromWord32s :: Word32 -> Word32 -> Word32 -> Word32 -> IPv6
fromWord32s a b c d =
IPv6 $ fromWord32sWord128
(fromIntegral a) (fromIntegral b) (fromIntegral c) (fromIntegral d)
fromWord32sWord128 ::
Word128 -> Word128 -> Word128 -> Word128
-> Word128
fromWord32sWord128 a b c d = fromIntegral
( shiftL a 96
.|. shiftL b 64
.|. shiftL c 32
.|. d
)
fromTupleWord32s :: (Word32,Word32,Word32,Word32) -> IPv6
fromTupleWord32s (a,b,c,d) = fromWord32s a b c d
toWord32s :: IPv6 -> (Word32,Word32,Word32,Word32)
toWord32s (IPv6 (Word128 a b)) =
( fromIntegral (unsafeShiftR a 32)
, fromIntegral a
, fromIntegral (unsafeShiftR b 32)
, fromIntegral b
)
loopback :: IPv6
loopback = IPv6 (Word128 0 1)
localhost :: IPv6
localhost = loopback
any :: IPv6
any = IPv6 zeroWord128
encode :: IPv6 -> Text
encode !ip =
if isIPv4Mapped ip
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
(w1, w2, w3, w4, w5, w6, w7, w8) = toWord16s ip
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)
| x == 0 && len == longest =
(if i == 0 || (i+len) == 8 then ":" else "")
: expand (i+len) 0 wsNext
| otherwise = replicate len (showHex x "") ++ expand (i+len) longest wsNext
longestZ = maximum . (0:) . map snd . filter ((==0) . fst) $ grouped
grouped = map (\x -> (head x, length x)) (group ws)
isIPv4Mapped :: IPv6 -> Bool
isIPv4Mapped (IPv6 (Word128 w1 w2)) =
w1 == 0 && (0xFFFFFFFF00000000 .&. w2 == 0x0000FFFF00000000)
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
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
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
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
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
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
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 :: Text -> Maybe IPv6
decode t = rightToMaybe (AT.parseOnly (parser <* AT.endOfInput) t)
parserUtf8Bytes :: e -> Parser.Parser e s IPv6
parserUtf8Bytes e = do
marr <- Parser.effect (PM.newPrimArray 8)
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
preZeroes ::
e
-> MutablePrimArray s Word16
-> 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)
postZeroesBegin ::
e
-> MutablePrimArray s Word16
-> Int
-> Int
-> Parser.Parser e s IPv6
postZeroesBegin e !marr !ix !compress = do
optionalPieceParser e >>= \case
Nothing -> do
Parser.effect (conclude marr ix compress)
Just w -> do
Parser.effect (PM.writePrimArray marr ix w)
postZeroes e marr (ix + 1) compress
postZeroes ::
e
-> MutablePrimArray s Word16
-> Int
-> Int
-> Parser.Parser e s IPv6
postZeroes e !marr !ix !compress = case ix of
8 -> Parser.fail e
_ -> do
Latin.trySatisfy (== ':') >>= \case
False ->
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
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
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
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)
parserRangeUtf8Bytes :: e -> Parser.Parser e s IPv6Range
parserRangeUtf8Bytes e = do
base <- parserUtf8Bytes e
Latin.char e '/'
theMask <- Latin.decWord8 e
if theMask > 128
then Parser.fail e
else pure $! normalize (IPv6Range base theMask)
parserRangeUtf8BytesLenient :: e -> Parser.Parser e s IPv6Range
parserRangeUtf8BytesLenient e = do
base <- parserUtf8Bytes e
Latin.trySatisfy (=='/') >>= \case
True -> do
theMask <- Latin.decWord8 e
if theMask > 128
then Parser.fail e
else pure $! normalize (IPv6Range base theMask)
False -> pure $! IPv6Range base 128
parser :: Atto.Parser IPv6
parser = makeIP <$> ip
where
makeIP [w1, w2, w3, w4, w5, w6, w7, w8] = fromWord16s w1 w2 w3 w4 w5 w6 w7 w8
makeIP _ = error "Net.IPv6.parser: Implementation error. Please open a bug report."
ip = (Atto.char ':' *> Atto.char ':' *> doubleColon 0) <|> part 0
part :: Int -> Atto.Parser [Word16]
part n =
case n of
7 -> pure <$> Atto.hexadecimal
6 -> ipv4 <|> hexPart
_ -> hexPart
where
hexPart = (:)
<$> Atto.hexadecimal
<*> (Atto.char ':' *>
(
(Atto.char ':' *> doubleColon (n+1))
<|>
part (n+1)
)
)
doubleColon :: Int -> Atto.Parser [Word16]
doubleColon count = do
rest <- afterDoubleColon <|> pure []
let fillerLength = (8 - count - length rest)
if fillerLength <= 0
then fail "too many parts in IPv6 address"
else pure (replicate fillerLength 0 ++ rest)
afterDoubleColon :: Atto.Parser [Word16]
afterDoubleColon =
ipv4 <|>
(:) <$> Atto.hexadecimal <*> ((Atto.char ':' *> afterDoubleColon) <|> pure [])
ipv4 :: Atto.Parser [Word16]
ipv4 = ipv4ToWord16s <$> IPv4.parser
ipv4ToWord16s :: IPv4 -> [Word16]
ipv4ToWord16s (IPv4 word) = [fromIntegral (word `unsafeShiftR` 16), fromIntegral (word .&. 0xFFFF)]
data IPv6Range = IPv6Range
{ ipv6RangeBase :: {-# UNPACK #-} !IPv6
, ipv6RangeLength :: {-# UNPACK #-} !Word8
} deriving (Eq,Ord,Show,Read,Generic,Data)
instance NFData IPv6Range
instance Aeson.ToJSON IPv6Range where
toJSON = Aeson.String . encodeRange
instance Aeson.FromJSON IPv6Range where
parseJSON (Aeson.String t) = case decodeRange t of
Nothing -> fail "Could not decodeRange IPv6 range"
Just res -> return res
parseJSON _ = mzero
mask128 :: IPv6
mask128 = maxBound
mask :: Word8 -> IPv6
mask = complement . shiftR mask128 . fromIntegral
normalize :: IPv6Range -> IPv6Range
normalize (IPv6Range ip len) =
let len' = min len 128
ip' = ip .&. mask len'
in IPv6Range ip' len'
encodeRange :: IPv6Range -> Text
encodeRange x = encode (ipv6RangeBase x) <> Text.pack "/" <> (Text.pack $ (show . fromEnum) $ ipv6RangeLength x)
decodeRange :: Text -> Maybe IPv6Range
decodeRange = rightToMaybe . AT.parseOnly (parserRange <* AT.endOfInput)
parserRange :: AT.Parser IPv6Range
parserRange = do
ip <- parser
_ <- AT.char '/'
theMask <- AT.decimal >>= limitSize
return (normalize (IPv6Range ip theMask))
where
limitSize i =
if i > 128
then fail "An IP range length must be between 0 and 128"
else return i
contains :: IPv6Range -> IPv6 -> Bool
contains (IPv6Range subnet len) =
let theMask = mask len
subnetNormalized = subnet .&. theMask
in \ip -> (ip .&. theMask) == subnetNormalized
member :: IPv6 -> IPv6Range -> Bool
member = flip contains
lowerInclusive :: IPv6Range -> IPv6
lowerInclusive = ipv6RangeBase . normalize
upperInclusive :: IPv6Range -> IPv6
upperInclusive (IPv6Range ip len) =
let len' = min 128 len
theInvertedMask :: IPv6
theInvertedMask = shiftR mask128 (fromIntegral len')
in ip .|. theInvertedMask
printRange :: IPv6Range -> IO ()
printRange = TIO.putStrLn . encodeRange
range :: IPv6 -> Word8 -> IPv6Range
range addr len = normalize (IPv6Range addr len)
fromBounds :: IPv6 -> IPv6 -> IPv6Range
fromBounds lo hi =
normalize (IPv6Range lo (maskFromBounds lo hi))
maskFromBounds :: IPv6 -> IPv6 -> Word8
maskFromBounds lo hi = fromIntegral (countLeadingZeros $ xor lo hi)